fix merge conflicts
7
.dockerignore
Normal file
@ -0,0 +1,7 @@
|
||||
.git
|
||||
.gitignore
|
||||
.travis.yml
|
||||
appveyor.yml
|
||||
.vscode
|
||||
node_modules
|
||||
dist
|
5
CREDITS.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Credits
|
||||
|
||||
## Sounds
|
||||
* All eyes on me, Exquisite and Appointed are from [Notification Sounds](https://notificationsounds.com/)
|
||||
* Mastodon Boop is from the [Mastodon Project](https://github.com/tootsuite/mastodon) and made by [@jk@mastodon.social](https://mastodon.social/@jk)
|
21
Dockerfile
Normal file
@ -0,0 +1,21 @@
|
||||
FROM node:10-buster-slim AS build
|
||||
|
||||
WORKDIR /build
|
||||
ADD . /build
|
||||
|
||||
RUN apt update && apt install --yes git binutils
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --update --no-cache lighttpd
|
||||
|
||||
ADD lighttpd.conf /etc/lighttpd/lighttpd.conf
|
||||
COPY --from=build /build/dist /app/sengi
|
||||
COPY --from=build /build/assets/docker_init /app
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
ENTRYPOINT ["lighttpd", "-D"]
|
||||
CMD ["-f", "/etc/lighttpd/lighttpd.conf"]
|
18
README.md
@ -4,21 +4,25 @@
|
||||
|
||||
Sengi is a **Mastodon** and **Pleroma** desktop focused client. It takes inspiration from the old Tweetdeck [client](https://static.makeuseof.com/wp-content/uploads/2012/02/muo-tweetdeck2b.png), the new Tweetdeck webapp and Mastodon UI.
|
||||
|
||||
Focus will be made on the following points:
|
||||
It is strongly focused on the following points:
|
||||
|
||||
* Heavily oriented on multi-accounts usage
|
||||
* Desktop based interactions (right clic, left clic, etc)
|
||||
* One column at a time display (leaves it on the side of your screen, and keep an eye on it while doing your other stuff)
|
||||
|
||||
It will be released as a **browser webapp** and also packaged as an **cross-platform desktop application** (Mac, Windows, and Linux).
|
||||
It is released as a **browser webapp** and also packaged as an **cross-platform desktop application** (Mac, Windows, and Linux).
|
||||
|
||||
## Official project page
|
||||
|
||||
[Discover Sengi](https://nicolasconstant.github.io/sengi/)
|
||||
|
||||
## State of development
|
||||
|
||||
Sengi is at a very early development stage, a lot has to be done before a first pre-release.
|
||||
Sengi already supporting all the basics functionalities, but many minors enhancements are still needed before a 1.0.0 release.
|
||||
|
||||
## Screens
|
||||
|
||||
soon™
|
||||
![/docs/images/presentation_small.gif](/docs/images/presentation_small.gif)
|
||||
|
||||
## Contact
|
||||
|
||||
@ -32,12 +36,16 @@ It's a little [elephant shrew](https://en.wikipedia.org/wiki/Elephant_shrew) fro
|
||||
|
||||
## Contribute
|
||||
|
||||
Please see the [contributing guidelines](CONTRIBUTING.md).
|
||||
Please see the [contributing guidelines](CONTRIBUTING.md)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the AGPLv3 License - see [LICENSE](LICENSE) for details
|
||||
|
||||
## Credits
|
||||
|
||||
See [credits](CREDITS.md)
|
||||
|
||||
## Dependencies
|
||||
|
||||
* [Angular 7](https://github.com/angular/angular)
|
||||
|
@ -21,7 +21,8 @@
|
||||
"src/favicon.ico"
|
||||
],
|
||||
"styles": [
|
||||
"src/sass/styles.scss"
|
||||
"src/sass/styles.scss",
|
||||
"node_modules/@ctrl/ngx-emoji-mart/picker.css"
|
||||
],
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": [
|
||||
|
BIN
assets/docker_init/favicon.png
Normal file
After Width: | Height: | Size: 13 KiB |
29
assets/docker_init/index.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="stylesheet" href="main.css">
|
||||
<link rel="shortcut icon" type="image/png" href="favicon.png">
|
||||
|
||||
<title>Sengi Launcher</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="launcher-wrapper">
|
||||
<div class="launcher">
|
||||
<a href="#" class="button" title="launch sengi in popup"
|
||||
onClick="window.open('/sengi/'+'?qt='+ (new Date()).getTime(),'Sengi','toolbar=no,location=no,status=no,menubar=no,scrollbars=no, resizable=yes,width=377,height=800'); return false;">
|
||||
<span class="download-button__web--label">Launch Sengi Popup</span>
|
||||
</a><br />
|
||||
|
||||
<a href="/sengi/" class="button" title="launch sengi">
|
||||
<span class="download-button__web--label">Open Sengi</span>
|
||||
</a><br />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
45
assets/docker_init/main.css
Normal file
@ -0,0 +1,45 @@
|
||||
*, *::after, *::before {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
background-color: #141824;
|
||||
font-family: Verdana, Geneva, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.launcher-wrapper{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.launcher {
|
||||
height: 15rem;
|
||||
width: 30rem;
|
||||
margin: 35vh auto;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: #090b10;
|
||||
display: block;
|
||||
width: 30rem;
|
||||
padding: 1.5rem 2rem 1.75rem 2rem;
|
||||
color: white;
|
||||
border-radius: 3px;
|
||||
font-size: 1.8rem;
|
||||
font-weight: lighter;
|
||||
text-decoration: none;
|
||||
transition: all .2s;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #1e2433;
|
||||
}
|
@ -94,6 +94,17 @@ body {
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
margin-bottom: .5rem; }
|
||||
.header__download-box--buttons {
|
||||
margin-bottom: 5px; }
|
||||
.header__old-releases {
|
||||
transition: all .2s;
|
||||
color: #7a7a7a;
|
||||
float: left;
|
||||
font-size: 1.4rem;
|
||||
text-decoration: none; }
|
||||
.header__old-releases:hover {
|
||||
color: #faa424;
|
||||
text-decoration: underline; }
|
||||
|
||||
.download-button {
|
||||
transition: all .2s;
|
||||
@ -112,16 +123,10 @@ body {
|
||||
color: #faa424; }
|
||||
.download-button__web {
|
||||
font-size: 25px;
|
||||
background-color: #04d431;
|
||||
background-color: #faa424;
|
||||
background-color: #fd9d0d;
|
||||
background-color: #3f3f3f;
|
||||
color: white;
|
||||
color: #202020;
|
||||
color: white;
|
||||
padding: 10px 20px 10px 15px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #ffffff;
|
||||
border: 1px solid #e7e7e7; }
|
||||
.download-button__web--label {
|
||||
font-size: 25px;
|
||||
@ -129,7 +134,6 @@ body {
|
||||
padding-left: 10px; }
|
||||
.download-button__web:hover {
|
||||
color: #3d3d3d;
|
||||
background-color: #ffe5be;
|
||||
background-color: #faa424; }
|
||||
|
||||
.fa-apple {
|
||||
@ -143,9 +147,102 @@ body {
|
||||
width: 6rem;
|
||||
height: 6rem; }
|
||||
|
||||
.section {
|
||||
font-family: 'Open Sans', sans-serif; }
|
||||
.section-about {
|
||||
min-height: 10rem;
|
||||
background-color: #141414;
|
||||
color: whitesmoke;
|
||||
padding: 7rem 0; }
|
||||
.section-about__about {
|
||||
font-weight: 300;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
margin: auto;
|
||||
padding: 0 3rem;
|
||||
text-align: center;
|
||||
font-size: 2.5rem; }
|
||||
.section-clear {
|
||||
font-weight: 300;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
min-height: 20rem;
|
||||
background-color: whitesmoke;
|
||||
padding: 1rem; }
|
||||
.section-clear__big-title {
|
||||
font-weight: 400;
|
||||
font-size: 3rem;
|
||||
text-align: center; }
|
||||
.section-clear__title {
|
||||
font-weight: 400;
|
||||
font-size: 2.5rem;
|
||||
color: #141414;
|
||||
margin-top: 5rem;
|
||||
margin-left: 15vw; }
|
||||
@media (max-width: 56.25em) {
|
||||
.section-clear__title {
|
||||
margin: 3rem auto 0 auto;
|
||||
text-align: center; } }
|
||||
.section-clear__subtitle {
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
color: #141414;
|
||||
margin-left: 1rem;
|
||||
font-style: italic; }
|
||||
.section-separator {
|
||||
height: .5rem;
|
||||
background-color: #141414;
|
||||
background-color: white; }
|
||||
|
||||
.quick-overview__video {
|
||||
display: block;
|
||||
margin: 2rem auto;
|
||||
width: 800;
|
||||
height: 492; }
|
||||
@media (max-width: 56.25em) {
|
||||
.quick-overview__video {
|
||||
width: 100%;
|
||||
height: 492; } }
|
||||
|
||||
.functionalities__row {
|
||||
max-width: 100rem; }
|
||||
|
||||
.functionalities__text {
|
||||
display: block;
|
||||
margin: auto;
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
padding: 7rem 5rem 0 5rem;
|
||||
max-width: 50rem; }
|
||||
@media (max-width: 56.25em) {
|
||||
.functionalities__text {
|
||||
padding: 3rem 5rem 0 5rem; } }
|
||||
|
||||
.functionalities__conclusion {
|
||||
max-width: 60rem;
|
||||
padding: 2rem 5rem 5rem 5rem; }
|
||||
|
||||
.functionalities__strong {
|
||||
font-weight: 400;
|
||||
font-weight: bold; }
|
||||
|
||||
.functionalities__video {
|
||||
display: block;
|
||||
margin: 2rem auto;
|
||||
width: 326px;
|
||||
height: 260px; }
|
||||
.functionalities__video:focus {
|
||||
border: none;
|
||||
outline: none; }
|
||||
@media (max-width: 56.25em) {
|
||||
.functionalities__video {
|
||||
width: 100%;
|
||||
max-width: 326px;
|
||||
height: 60%; } }
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
background-color: #141414;
|
||||
background-color: #141414;
|
||||
color: white;
|
||||
height: 18rem;
|
||||
padding-top: 4em; }
|
||||
|
@ -25,4 +25,8 @@ $default-font-size: 1.6rem;
|
||||
$grid-width: 114rem;
|
||||
$gutter-vertical: 1rem;
|
||||
$gutter-vertical-small: 1rem;
|
||||
$gutter-horizontal: 1rem;
|
||||
$gutter-horizontal: 1rem;
|
||||
|
||||
|
||||
//COLOR
|
||||
$dark-background: rgb(20, 20, 20);
|
@ -1,6 +1,7 @@
|
||||
.footer {
|
||||
text-align: center;
|
||||
background-color: rgb(20, 20, 20);
|
||||
background-color: $dark-background;
|
||||
color: white;
|
||||
height: 18rem;
|
||||
padding-top: 4em;
|
||||
|
@ -1,3 +1,5 @@
|
||||
$link-hover-color: rgb(250, 164, 36);;
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
background-color: rgb(247, 247, 247);
|
||||
@ -52,6 +54,22 @@
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
&--buttons {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__old-releases {
|
||||
transition: all .2s;
|
||||
color: rgb(122, 122, 122);
|
||||
float: left;
|
||||
font-size: 1.4rem;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: $link-hover-color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,26 +95,17 @@
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&:hover {
|
||||
color: rgb(250, 164, 36);
|
||||
color: $link-hover-color;
|
||||
|
||||
}
|
||||
|
||||
&__web {
|
||||
font-size: 25px;
|
||||
background-color: rgb(4, 212, 49);
|
||||
background-color: rgb(250, 164, 36);
|
||||
background-color: rgb(253, 157, 13);
|
||||
background-color: rgb(63, 63, 63);;
|
||||
color: white;
|
||||
color: rgb(32, 32, 32);
|
||||
color: white;
|
||||
|
||||
|
||||
padding: 10px 20px 10px 15px;
|
||||
border-radius: 3px;
|
||||
|
||||
// border: 1px solid #ececec;
|
||||
border: 1px solid #ffffff;
|
||||
border: 1px solid #e7e7e7;
|
||||
|
||||
&--label {
|
||||
@ -107,8 +116,7 @@
|
||||
|
||||
&:hover {
|
||||
color: rgb(61, 61, 61);
|
||||
background-color: rgb(255, 229, 190);
|
||||
background-color: rgb(250, 164, 36);
|
||||
background-color: $link-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
127
docs/assets/sass/layout/_section.scss
Normal file
@ -0,0 +1,127 @@
|
||||
.section {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
|
||||
&-about {
|
||||
min-height: 10rem;
|
||||
background-color: $dark-background;
|
||||
color: whitesmoke;
|
||||
padding: 7rem 0;
|
||||
|
||||
&__about {
|
||||
font-weight: 300;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
margin: auto;
|
||||
padding: 0 3rem;
|
||||
text-align: center;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-clear {
|
||||
font-weight: 300;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
min-height: 20rem;
|
||||
background-color: whitesmoke;
|
||||
padding: 1rem;
|
||||
|
||||
&__big-title {
|
||||
font-weight: 400;
|
||||
font-size: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: 400;
|
||||
font-size: 2.5rem;
|
||||
color: $dark-background;
|
||||
margin-top: 5rem;
|
||||
margin-left: 15vw;
|
||||
|
||||
@include respond(tab-port) {
|
||||
margin: 3rem auto 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
color: $dark-background;
|
||||
margin-left: 1rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&-separator {
|
||||
height: .5rem;
|
||||
background-color: $dark-background;
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
|
||||
.quick-overview {
|
||||
&__video {
|
||||
display: block;
|
||||
margin: 2rem auto;
|
||||
width: 800;
|
||||
height: 492;
|
||||
|
||||
@include respond(tab-port) {
|
||||
width: 100%;
|
||||
height: 492;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.functionalities {
|
||||
|
||||
&__row {
|
||||
max-width: 100rem;
|
||||
}
|
||||
|
||||
&__text {
|
||||
display: block;
|
||||
margin: auto;
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
|
||||
text-align: center;
|
||||
padding: 7rem 5rem 0 5rem;
|
||||
max-width: 50rem;
|
||||
|
||||
@include respond(tab-port) {
|
||||
padding: 3rem 5rem 0 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__conclusion {
|
||||
max-width: 60rem;
|
||||
padding: 2rem 5rem 5rem 5rem;
|
||||
}
|
||||
|
||||
&__strong {
|
||||
font-weight: 400;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__video {
|
||||
display: block;
|
||||
margin: 2rem auto;
|
||||
|
||||
&:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
width: 326px;
|
||||
height: 260px;
|
||||
|
||||
@include respond(tab-port) {
|
||||
width: 100%;
|
||||
max-width: 326px;
|
||||
height: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -5,4 +5,5 @@
|
||||
|
||||
@import "./layout/grid";
|
||||
@import "./layout/header";
|
||||
@import "./layout/section";
|
||||
@import "./layout/footer";
|
BIN
docs/images/labels.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
docs/images/presentation_small.gif
Normal file
After Width: | Height: | Size: 1.6 MiB |
BIN
docs/images/sengi_image.old.png
Normal file
After Width: | Height: | Size: 313 KiB |
Before Width: | Height: | Size: 313 KiB After Width: | Height: | Size: 286 KiB |
BIN
docs/images/sengi_image_ubuntu.png
Normal file
After Width: | Height: | Size: 286 KiB |
BIN
docs/images/timelines.png
Normal file
After Width: | Height: | Size: 134 KiB |
227
docs/index.html
@ -20,7 +20,7 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-1-of-2">
|
||||
<img class="header__image" src="images/sengi_image.png" />
|
||||
<img id="main-illustration" class="header__image" src="images/sengi_image.png" />
|
||||
</div>
|
||||
<div class="col-1-of-2">
|
||||
<div class="header__download-box">
|
||||
@ -28,14 +28,15 @@
|
||||
|
||||
<div class="header__download-box--description">
|
||||
A FLOSS multi-account Mastodon and Pleroma desktop client<br />
|
||||
Now available in Beta (v0.7.0)<br />
|
||||
Now available in Beta <span id="sengi-version"></span> <br />
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<div class="header__download-box--buttons">
|
||||
<p>
|
||||
<h4 class="header__download-box--subtitle">Try it in your browser!</h4>
|
||||
<a href="#" class="download-button download-button__web" title="what are you waiting for? click!"
|
||||
<a href="#" class="download-button download-button__web"
|
||||
title="what are you waiting for? click!"
|
||||
onClick="window.open('http://sengi.nicolas-constant.com'+'?qt='+ (new Date()).getTime(),'Sengi','toolbar=no,location=no,status=no,menubar=no,scrollbars=no, resizable=yes,width=377,height=800'); return false;"
|
||||
class="button"><i class="fas fa-globe"></i><span
|
||||
class="download-button__web--label">launch!</span></a><br />
|
||||
@ -43,41 +44,225 @@
|
||||
<br />
|
||||
|
||||
<h4 class="header__download-box--subtitle">Or download the desktop client:</h4>
|
||||
<a href="https://github.com/NicolasConstant/sengi/releases/download/0.7.0/Sengi-0.7.0-win.exe" class="download-button" title="download client for windows"><i class="fab fa-windows"></i></a>
|
||||
<a href="https://github.com/NicolasConstant/sengi/releases/download/0.7.0/Sengi-0.7.0-mac.dmg" class="download-button" title="download client for mac"><i class="fab fa-apple"></i></a>
|
||||
<a href="https://github.com/NicolasConstant/sengi/releases/download/0.7.0/Sengi-0.7.0-linux.deb" class="download-button" title="download client for debian-based distrib"><i class="fab fa-ubuntu"></i></a>
|
||||
<a href="https://snapcraft.io/sengi" title="use Snap Store for linux"><img src="images/snap-store-white.png" /></a>
|
||||
<div id="download-buttons" style="display: none;">
|
||||
<a id="windows" href class="download-button" title="download client for windows">
|
||||
<i class="fab fa-windows"></i>
|
||||
</a>
|
||||
<a id="mac" href class="download-button" title="download client for mac">
|
||||
<i class="fab fa-apple"></i>
|
||||
</a>
|
||||
<a id="deb" href class="download-button"
|
||||
title="download client for debian-based distrib">
|
||||
<i class="fab fa-ubuntu"></i>
|
||||
</a>
|
||||
<a id="appimage" href class="download-button"
|
||||
title="download client for linux (AppImage)">
|
||||
<i class="fab fa-linux"></i>
|
||||
</a>
|
||||
<a href="https://snapcraft.io/sengi" title="use Snap Store for linux">
|
||||
<img src="images/snap-store-white.png" />
|
||||
</a>
|
||||
</div>
|
||||
<div id="download-buttons-nojs">
|
||||
<a href="https://github.com/NicolasConstant/sengi/releases/" class="download-button"
|
||||
title="latest releases">
|
||||
<i class="fab fa-github"></i></a>
|
||||
<a href="https://snapcraft.io/sengi" title="use Snap Store for linux">
|
||||
<img src="images/snap-store-white.png" />
|
||||
</a>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<a class="header__old-releases" href="https://github.com/NicolasConstant/sengi/releases/"
|
||||
title="browse previous releases">browse previous releases</a>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header__app-image-box">
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="header__app-image-box"></div>
|
||||
</header>
|
||||
<main>
|
||||
<section class="section-about">
|
||||
|
||||
</section>
|
||||
|
||||
<section class="section-about">
|
||||
<div class="section-about__about">
|
||||
<p>
|
||||
Sengi will let you use all your accounts<br /> easily and seamlessly<br />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="section-separator"></section>
|
||||
|
||||
<section class="section-clear">
|
||||
<h2 class="section-clear__big-title">Quick Overview</h2>
|
||||
|
||||
<video class="quick-overview__video" controls>
|
||||
<source src="videos/Quick_overview.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</section>
|
||||
|
||||
<section class="section-separator"></section>
|
||||
|
||||
<section class="section-clear">
|
||||
<h2 class="section-clear__big-title">Main Functionalities</h2>
|
||||
|
||||
<h4 class="section-clear__title">Seamless account switch</h4>
|
||||
<div class="row functionalities__row">
|
||||
<div class="col-1-of-2">
|
||||
<p class="functionalities__text">
|
||||
Just click on the account's avatar, <br />
|
||||
and all your next actions will be performed by it.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-1-of-2">
|
||||
<video width="326" height="260" controls class="functionalities__video">
|
||||
<source src="videos/Clip_account_switch.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="section-clear__title">All instances timelines in one place</h4>
|
||||
<div class="row functionalities__row">
|
||||
<div class="col-1-of-2">
|
||||
<p class="functionalities__text">
|
||||
Add timelines and lists from all your accounts in the same
|
||||
interface.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-1-of-2">
|
||||
<img src="images/timelines.png" class="functionalities__video" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="section-clear__title">Don't lose your focus</h4>
|
||||
<div class="row functionalities__row">
|
||||
<div class="col-1-of-2">
|
||||
<p class="functionalities__text">
|
||||
Opening a profile, thread, hashtag or even just replying to someone will always take place in the
|
||||
current Timeline.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-1-of-2">
|
||||
<video width="326" height="260" controls class="functionalities__video">
|
||||
<source src="videos/Clip_timelines.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="section-clear__title">Labels</h4>
|
||||
<div class="row functionalities__row">
|
||||
<div class="col-1-of-2">
|
||||
<p class="functionalities__text">
|
||||
Get a quick insight if a status is part of a thread, has replies, is from a bot, is old or was
|
||||
cross-posted (limited to local TL).
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-1-of-2">
|
||||
<img src="images/labels.png" class="functionalities__video" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="section-clear__title">Auto-remove Thread's Content-Warnings</h4>
|
||||
<div class="row functionalities__row">
|
||||
<div class="col-1-of-2">
|
||||
<p class="functionalities__text">
|
||||
Easily remove all CW from a thread<br />
|
||||
with one single click!
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-1-of-2">
|
||||
<video width="326" height="260" controls class="functionalities__video">
|
||||
<source src="videos/Clip_cw_button.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="section-clear__title">And many more!</h4>
|
||||
|
||||
<div class="row functionalities__row">
|
||||
<p class="functionalities__text functionalities__conclusion">
|
||||
There is a lot more things to discover<br/> and more to come too!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
<section class="section-separator"></section>
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<h3 class="footer__title">Let's keep in touch!</h3>
|
||||
<h3 class="footer__title">Let's keep in touch!</h3>
|
||||
|
||||
<div class="footer__buttons">
|
||||
<a href="https://mastodon.social/@sengi_app" rel="me" class="footer__buttons--button" title="open pleroma-compatible account"><i class="fab fa-mastodon"></i></a>
|
||||
<a href="https://github.com/NicolasConstant/sengi" class="footer__buttons--button" title="open microsoft github repository"><i class="fab fa-github"></i></a>
|
||||
<a href="https://mastodon.social/@sengi_app" rel="me" class="footer__buttons--button"
|
||||
title="open pleroma-compatible account"><i class="fab fa-mastodon"></i></a>
|
||||
<a href="https://github.com/NicolasConstant/sengi" class="footer__buttons--button"
|
||||
title="open microsoft github repository"><i class="fab fa-github"></i></a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
<script type="text/javascript" language="javascript">
|
||||
const getLastRelease = async () => {
|
||||
const response = await fetch('https://api.github.com/repos/NicolasConstant/sengi/releases/latest');
|
||||
const myJson = await response.json();
|
||||
return myJson;
|
||||
}
|
||||
|
||||
function getOS() {
|
||||
var userAgent = window.navigator.userAgent,
|
||||
platform = window.navigator.platform,
|
||||
macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'],
|
||||
windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'],
|
||||
iosPlatforms = ['iPhone', 'iPad', 'iPod'],
|
||||
os = null;
|
||||
|
||||
if (macosPlatforms.indexOf(platform) !== -1) {
|
||||
os = 'Mac OS';
|
||||
} else if (iosPlatforms.indexOf(platform) !== -1) {
|
||||
os = 'iOS';
|
||||
} else if (windowsPlatforms.indexOf(platform) !== -1) {
|
||||
os = 'Windows';
|
||||
} else if (/Android/.test(userAgent)) {
|
||||
os = 'Android';
|
||||
} else if (!os && /Linux/.test(platform)) {
|
||||
os = 'Linux';
|
||||
}
|
||||
|
||||
return os;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
let lastRelease = await getLastRelease();
|
||||
let version = lastRelease.tag_name;
|
||||
|
||||
var downloadButtons = document.getElementById('download-buttons');
|
||||
downloadButtons.style.display = 'block';
|
||||
|
||||
var downloadButtonsNojs = document.getElementById('download-buttons-nojs');
|
||||
downloadButtonsNojs.style.display = 'none';
|
||||
|
||||
var sengiVersion = document.getElementById('sengi-version');
|
||||
sengiVersion.textContent = `(${version})`;
|
||||
|
||||
document.getElementById('windows').href = `https://github.com/NicolasConstant/sengi/releases/download/${version}/Sengi-${version}-win.exe`;
|
||||
document.getElementById('mac').href = `https://github.com/NicolasConstant/sengi/releases/download/${version}/Sengi-${version}-mac.dmg`;
|
||||
document.getElementById('deb').href = `https://github.com/NicolasConstant/sengi/releases/download/${version}/Sengi-${version}-linux.deb`;
|
||||
document.getElementById('appimage').href = `https://github.com/NicolasConstant/sengi/releases/download/${version}/Sengi-${version}-linux.AppImage`;
|
||||
|
||||
|
||||
let userOs = getOS();
|
||||
if(userOs === 'Linux'){
|
||||
var illustration = document.getElementById('main-illustration');
|
||||
illustration.src = 'images/sengi_image_ubuntu.png';
|
||||
}
|
||||
}, false);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
BIN
docs/videos/Clip_account_switch.mp4
Normal file
BIN
docs/videos/Clip_cw_button.mp4
Normal file
BIN
docs/videos/Clip_timelines.mp4
Normal file
BIN
docs/videos/Quick_overview.mp4
Normal file
14
lighttpd.conf
Normal file
@ -0,0 +1,14 @@
|
||||
server.port = 80
|
||||
server.document-root = "/app"
|
||||
server.errorlog = "/dev/stdout"
|
||||
accesslog.filename = "/dev/stdout"
|
||||
dir-listing.activate = "disable"
|
||||
server.modules = (
|
||||
"mod_access",
|
||||
"mod_accesslog",
|
||||
)
|
||||
include "mime-types.conf"
|
||||
server.pid-file = "/run/lighttpd.pid"
|
||||
index-file.names = ( "index.html", "index.htm" )
|
||||
#url.rewrite-once = ( "^sengi/(.*)" => "/sengi/index.html" )
|
||||
server.error-handler-404 = "/sengi/index.html"
|
209
main-electron.js
@ -1,78 +1,174 @@
|
||||
const { app, Menu, server, BrowserWindow, shell } = require('electron');
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const { app, Menu, server, BrowserWindow, shell } = require("electron");
|
||||
const path = require("path");
|
||||
const url = require("url");
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let win
|
||||
let win;
|
||||
|
||||
function createWindow() {
|
||||
// Create the browser window.
|
||||
win = new BrowserWindow({ width: 377, height: 800, title: "Sengi", backgroundColor: '#FFF', 'useContentSize': true });
|
||||
win = new BrowserWindow({
|
||||
width: 377,
|
||||
height: 800,
|
||||
title: "Sengi",
|
||||
backgroundColor: "#FFF",
|
||||
useContentSize: true
|
||||
});
|
||||
|
||||
win.setAutoHideMenuBar(true);
|
||||
win.setMenuBarVisibility(false);
|
||||
|
||||
var server = http.createServer(requestHandler).listen(9527);
|
||||
win.loadURL('http://localhost:9527');
|
||||
|
||||
const sengiUrl = "http://localhost:9527";
|
||||
win.loadURL(sengiUrl);
|
||||
|
||||
const template = [
|
||||
{
|
||||
label: 'View',
|
||||
label: "View",
|
||||
submenu: [
|
||||
{ role: 'reload' },
|
||||
{ role: 'forcereload' },
|
||||
{
|
||||
label: "Return on Sengi",
|
||||
click() {
|
||||
win.loadURL(sengiUrl);
|
||||
}
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ role: "reload" },
|
||||
{ role: "forcereload" },
|
||||
{ type: 'separator' },
|
||||
{ role: 'close' }
|
||||
|
||||
{ role: 'togglefullscreen' },
|
||||
{ type: "separator" },
|
||||
{ role: "close" },
|
||||
{ role: 'quit' }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
role: "help",
|
||||
submenu: [
|
||||
{ role: 'toggledevtools' },
|
||||
{ role: "toggledevtools" },
|
||||
{
|
||||
label: 'Open GitHub project',
|
||||
click() { require('electron').shell.openExternal('https://github.com/NicolasConstant/sengi') }
|
||||
label: "Open GitHub project",
|
||||
click() {
|
||||
require("electron").shell.openExternal(
|
||||
"https://github.com/NicolasConstant/sengi"
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
win.setMenu(menu);
|
||||
|
||||
// Check if we are on a MAC
|
||||
if (process.platform === "darwin") {
|
||||
// Create our menu entries so that we can use MAC shortcuts
|
||||
Menu.setApplicationMenu(
|
||||
Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Sengi",
|
||||
submenu: [
|
||||
{ role: "close" },
|
||||
{ role: 'quit' }
|
||||
]
|
||||
},
|
||||
// {
|
||||
// label: "File",
|
||||
// submenu: [
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
label: "Edit",
|
||||
submenu: [
|
||||
{ role: "undo" },
|
||||
{ role: "redo" },
|
||||
{ type: "separator" },
|
||||
{ role: "cut" },
|
||||
{ role: "copy" },
|
||||
{ role: "paste" },
|
||||
{ role: "pasteandmatchstyle" },
|
||||
{ role: "delete" },
|
||||
{ role: "selectall" }
|
||||
]
|
||||
},
|
||||
// {
|
||||
// label: "Format",
|
||||
// submenu: [
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
label: "View",
|
||||
submenu: [
|
||||
{
|
||||
label: "Return on Sengi",
|
||||
click() {
|
||||
win.loadURL(sengiUrl);
|
||||
}
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ role: "reload" },
|
||||
{ role: "forcereload" },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' }
|
||||
]
|
||||
},
|
||||
// {
|
||||
// label: "Window",
|
||||
// submenu: [
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
role: "Help",
|
||||
submenu: [
|
||||
{ role: "toggledevtools" },
|
||||
{
|
||||
label: "Open GitHub project",
|
||||
click() {
|
||||
require("electron").shell.openExternal(
|
||||
"https://github.com/NicolasConstant/sengi"
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
// Open the DevTools.
|
||||
// win.webContents.openDevTools()
|
||||
|
||||
//open external links to browser
|
||||
win.webContents.on('new-window', function (event, url) {
|
||||
//open external links to browser
|
||||
win.webContents.on("new-window", function (event, url) {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
});
|
||||
|
||||
// Emitted when the window is closed.
|
||||
win.on('closed', () => {
|
||||
win.on("closed", () => {
|
||||
// Dereference the window object, usually you would store windows
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
win = null
|
||||
})
|
||||
};
|
||||
|
||||
win = null;
|
||||
});
|
||||
}
|
||||
|
||||
function requestHandler(req, res) {
|
||||
var file = req.url == '/' ? '/index.html' : req.url,
|
||||
root = __dirname + '/dist',
|
||||
page404 = root + '/404.html';
|
||||
var file = req.url == "/" ? "/index.html" : req.url,
|
||||
root = __dirname + "/dist",
|
||||
page404 = root + "/404.html";
|
||||
|
||||
if (file.includes('register') || file.includes('home')) file = '/index.html';
|
||||
if (file.includes("register") || file.includes("home")) file = "/index.html";
|
||||
|
||||
getFile((root + file), res, page404);
|
||||
};
|
||||
getFile(root + file, res, page404);
|
||||
}
|
||||
|
||||
function getFile(filePath, res, page404) {
|
||||
console.warn(`filePath: ${filePath}`)
|
||||
console.warn(`filePath: ${filePath}`);
|
||||
fs.exists(filePath, function (exists) {
|
||||
if (exists) {
|
||||
fs.readFile(filePath, function (err, contents) {
|
||||
@ -85,7 +181,7 @@ function getFile(filePath, res, page404) {
|
||||
} else {
|
||||
fs.readFile(page404, function (err, contents) {
|
||||
if (!err) {
|
||||
res.writeHead(404, { 'Content-Type': 'text/html' });
|
||||
res.writeHead(404, { "Content-Type": "text/html" });
|
||||
res.end(contents);
|
||||
} else {
|
||||
console.dir(err);
|
||||
@ -93,31 +189,46 @@ function getFile(filePath, res, page404) {
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
app.commandLine.appendSwitch('force-color-profile', 'srgb');
|
||||
app.commandLine.appendSwitch("force-color-profile", "srgb");
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on('ready', createWindow)
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
if (win) {
|
||||
if (win.isMinimized()) win.restore()
|
||||
win.focus()
|
||||
}
|
||||
});
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on("ready", createWindow);
|
||||
}
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
app.on("window-all-closed", () => {
|
||||
// On macOS it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
app.on("activate", () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (win === null) {
|
||||
createWindow()
|
||||
createWindow();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
||||
|
88
package-lock.json
generated
@ -214,6 +214,23 @@
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"@angular/cdk": {
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-7.3.7.tgz",
|
||||
"integrity": "sha512-xbXxhHHKGkVuW6K7pzPmvpJXIwpl0ykBnvA2g+/7Sgy5Pd35wCC+UtHD9RYczDM/mkygNxMQtagyCErwFnDtQA==",
|
||||
"requires": {
|
||||
"parse5": "^5.0.0",
|
||||
"tslib": "^1.7.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"parse5": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz",
|
||||
"integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@angular/cli": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-7.3.4.tgz",
|
||||
@ -694,6 +711,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@ctrl/ngx-emoji-mart": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@ctrl/ngx-emoji-mart/-/ngx-emoji-mart-0.17.0.tgz",
|
||||
"integrity": "sha512-gdHM/OPTbqWMIlFPAbjgAPo5BGsjkehILCInw5OttuT25HMZXJFjWVpi6vGixNVrAs8kz6sTYM/wbldS5GP9yQ==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"@fortawesome/angular-fontawesome": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.3.0.tgz",
|
||||
@ -832,6 +857,11 @@
|
||||
"@types/jasmine": "*"
|
||||
}
|
||||
},
|
||||
"@types/mousetrap": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.3.tgz",
|
||||
"integrity": "sha512-13gmo3M2qVvjQrWNseqM3+cR6S2Ss3grbR2NZltgMq94wOwqJYQdgn8qzwDshzgXqMlSUtyPZjysImmktu22ew=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "8.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.5.tgz",
|
||||
@ -1164,6 +1194,15 @@
|
||||
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
|
||||
"dev": true
|
||||
},
|
||||
"angular2-hotkeys": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/angular2-hotkeys/-/angular2-hotkeys-2.1.5.tgz",
|
||||
"integrity": "sha512-HiAnK1pW7lns5LpxtRsdkRRb5iVa7fv8Cf69Jye6l9gI6/IyvaVDptRtsWmdIG7VAr2Ngz6Yeehkym39O/LdgA==",
|
||||
"requires": {
|
||||
"@types/mousetrap": "^1.6.0",
|
||||
"mousetrap": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"ansi-align": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz",
|
||||
@ -2703,6 +2742,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"compute-scroll-into-view": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.11.tgz",
|
||||
"integrity": "sha512-uUnglJowSe0IPmWOdDtrlHXof5CTIJitfJEyITHBW6zDVOGu9Pjk5puaLM73SLcwak0L4hEjO7Td88/a6P5i7A=="
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@ -5870,6 +5914,11 @@
|
||||
"integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
|
||||
"dev": true
|
||||
},
|
||||
"howler": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/howler/-/howler-2.1.2.tgz",
|
||||
"integrity": "sha512-oKrTFaVXsDRoB/jik7cEpWKTj7VieoiuzMYJ7E/EU5ayvmpRhumCv3YQ3823zi9VTJkSWAhbryHnlZAionGAJg=="
|
||||
},
|
||||
"hpack.js": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
|
||||
@ -6327,11 +6376,6 @@
|
||||
"integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
|
||||
"dev": true
|
||||
},
|
||||
"ionicons": {
|
||||
"version": "4.5.5",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-4.5.5.tgz",
|
||||
"integrity": "sha512-dIGI73XG6Fg2Ps77ry5Ywe36Pq7wUGkDkl0pBhC4uhsiyoW+oXe+pplmarXEnKEcB5fmlkRrBOxYYzZaoRiUGw=="
|
||||
},
|
||||
"ip": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
|
||||
@ -8085,6 +8129,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mousetrap": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.3.tgz",
|
||||
"integrity": "sha512-bd+nzwhhs9ifsUrC2tWaSgm24/oo2c83zaRyZQF06hYA6sANfsXHtnZ19AbbbDXCDzeH5nZBSQ4NvCjgD62tJA=="
|
||||
},
|
||||
"move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
@ -8165,6 +8214,19 @@
|
||||
"integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==",
|
||||
"dev": true
|
||||
},
|
||||
"ng-pick-datetime": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ng-pick-datetime/-/ng-pick-datetime-7.0.0.tgz",
|
||||
"integrity": "sha512-SbS+zKX6gOlYpgH8zDSx2EL32ak0Z0y1Ksu1ECP/FiwVBM2mHgbzdfyDYhMmKFB0GKn5yCwXTandR1FCQXe62w=="
|
||||
},
|
||||
"ngx-contextmenu": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-contextmenu/-/ngx-contextmenu-5.2.0.tgz",
|
||||
"integrity": "sha512-S8W7YUJJ+McWCfHv04gWURK7doJBl+1YZUynyP8QQMMwGd02OiggBp38HrEHAdMr4TdvKyo4Td5Ud63x28tpLg==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"nice-try": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
@ -10269,6 +10331,14 @@
|
||||
"ajv-keywords": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"scroll-into-view-if-needed": {
|
||||
"version": "2.2.20",
|
||||
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.20.tgz",
|
||||
"integrity": "sha512-P9kYMrhi9f6dvWwTGpO5I3HgjSU/8Mts7xL3lkoH5xlewK7O9Obdc5WmMCzppln7bCVGNmf3qfoZXrpCeyNJXw==",
|
||||
"requires": {
|
||||
"compute-scroll-into-view": "1.0.11"
|
||||
}
|
||||
},
|
||||
"scss-tokenizer": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz",
|
||||
@ -10586,6 +10656,14 @@
|
||||
"integrity": "sha512-JDhEpTKzXusOqXZ0BUIdH+CjFdO/CR3tLlf5CN34IypI+xMmXW1uB16OOY8z3cICbJlDAVJzNbwBhNO0wt9OAw==",
|
||||
"dev": true
|
||||
},
|
||||
"smooth-scroll-into-view-if-needed": {
|
||||
"version": "1.1.23",
|
||||
"resolved": "https://registry.npmjs.org/smooth-scroll-into-view-if-needed/-/smooth-scroll-into-view-if-needed-1.1.23.tgz",
|
||||
"integrity": "sha512-52177sj5yR2novVCB+vJRCYEUkHFz2mq5UKmm5wwIWs0ZtC1sotVaTjKBsuNzBPF4nOV1NxMctyD4V/VMmivCQ==",
|
||||
"requires": {
|
||||
"scroll-into-view-if-needed": "2.2.20"
|
||||
}
|
||||
},
|
||||
"snapdragon": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
|
||||
|
18
package.json
@ -15,6 +15,7 @@
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"start-mem": "node --max_old_space_size=5048 ./node_modules/@angular/cli/bin/ng serve",
|
||||
"build": "ng build --prod",
|
||||
"test": "ng test",
|
||||
"test-nowatch": "ng test --watch=false",
|
||||
@ -22,11 +23,12 @@
|
||||
"e2e": "ng e2e",
|
||||
"electron": "ng build --prod && electron .",
|
||||
"electron-debug": "ng build && electron .",
|
||||
"dist": "npm run build && build --publish onTagOrDraft"
|
||||
"dist": "npm run build && electron-builder --publish onTagOrDraft"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^7.2.7",
|
||||
"@angular/cdk": "^7.2.7",
|
||||
"@angular/common": "^7.2.7",
|
||||
"@angular/compiler": "^7.2.7",
|
||||
"@angular/core": "^7.2.7",
|
||||
@ -35,6 +37,7 @@
|
||||
"@angular/platform-browser": "^7.2.7",
|
||||
"@angular/platform-browser-dynamic": "^7.2.7",
|
||||
"@angular/router": "^7.2.7",
|
||||
"@ctrl/ngx-emoji-mart": "^0.17.0",
|
||||
"@fortawesome/angular-fontawesome": "^0.3.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.13",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.7.0",
|
||||
@ -42,11 +45,15 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^5.7.0",
|
||||
"@ngxs/storage-plugin": "^3.2.0",
|
||||
"@ngxs/store": "^3.2.0",
|
||||
"angular2-hotkeys": "^2.1.5",
|
||||
"bootstrap": "^4.1.3",
|
||||
"core-js": "^2.5.4",
|
||||
"emojione": "^4.5.0",
|
||||
"ionicons": "^4.4.3",
|
||||
"emojione": "~4.5.0",
|
||||
"howler": "^2.1.2",
|
||||
"ng-pick-datetime": "^7.0.0",
|
||||
"ngx-contextmenu": "^5.2.0",
|
||||
"rxjs": "^6.4.0",
|
||||
"smooth-scroll-into-view-if-needed": "^1.1.23",
|
||||
"tslib": "^1.9.0",
|
||||
"zone.js": "^0.8.29"
|
||||
},
|
||||
@ -124,6 +131,11 @@
|
||||
"snap"
|
||||
],
|
||||
"category": "Network"
|
||||
},
|
||||
"snap": {
|
||||
"publish": [
|
||||
"github"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { debounceTime, map } from 'rxjs/operators';
|
||||
import { Select } from '@ngxs/store';
|
||||
// import { ElectronService } from 'ngx-electron';
|
||||
|
||||
import { NavigationService, LeftPanelType } from './services/navigation.service';
|
||||
import { NavigationService, LeftPanelType, OpenLeftPanelEvent } from './services/navigation.service';
|
||||
import { StreamElement } from './states/streams.state';
|
||||
import { OpenMediaEvent } from './models/common.model';
|
||||
import { ToolsService } from './services/tools.service';
|
||||
@ -44,8 +44,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
|
||||
this.columnEditorSub = this.navigationService.activatedPanelSubject.subscribe((type: LeftPanelType) => {
|
||||
if (type === LeftPanelType.Closed) {
|
||||
this.columnEditorSub = this.navigationService.activatedPanelSubject.subscribe((event: OpenLeftPanelEvent) => {
|
||||
if (event.type === LeftPanelType.Closed) {
|
||||
this.floatingColumnActive = false;
|
||||
} else {
|
||||
this.floatingColumnActive = true;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { BrowserModule } from "@angular/platform-browser";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { HttpModule } from "@angular/http";
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { NgModule, APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
||||
@ -9,8 +10,13 @@ import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { NgxsModule } from '@ngxs/store';
|
||||
import { NgxsStoragePluginModule } from '@ngxs/storage-plugin';
|
||||
import { OverlayModule } from '@angular/cdk/overlay';
|
||||
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { ContextMenuModule } from 'ngx-contextmenu';
|
||||
import { PickerModule } from '@ctrl/ngx-emoji-mart';
|
||||
import { OwlDateTimeModule, OwlNativeDateTimeModule } from 'ng-pick-datetime';
|
||||
import { HotkeyModule } from 'angular2-hotkeys';
|
||||
|
||||
import { AppComponent } from "./app.component";
|
||||
import { LeftSideBarComponent } from "./components/left-side-bar/left-side-bar.component";
|
||||
@ -28,6 +34,7 @@ import { FloatingColumnComponent } from './components/floating-column/floating-c
|
||||
import { StreamsState } from "./states/streams.state";
|
||||
import { StatusComponent } from "./components/stream/status/status.component";
|
||||
import { MastodonService } from "./services/mastodon.service";
|
||||
import { MastodonWrapperService } from "./services/mastodon-wrapper.service";
|
||||
import { AttachementsComponent } from './components/stream/status/attachements/attachements.component';
|
||||
import { SettingsComponent } from './components/floating-column/settings/settings.component';
|
||||
import { AddNewAccountComponent } from './components/floating-column/add-new-account/add-new-account.component';
|
||||
@ -57,71 +64,113 @@ import { MentionsComponent } from './components/floating-column/manage-account/m
|
||||
import { NotificationsComponent } from './components/floating-column/manage-account/notifications/notifications.component';
|
||||
import { SettingsState } from './states/settings.state';
|
||||
import { AccountEmojiPipe } from './pipes/account-emoji.pipe';
|
||||
import { CardComponent } from './components/stream/status/card/card.component';
|
||||
import { ListEditorComponent } from './components/floating-column/manage-account/my-account/list-editor/list-editor.component';
|
||||
import { ListAccountComponent } from './components/floating-column/manage-account/my-account/list-editor/list-account/list-account.component';
|
||||
import { PollComponent } from './components/stream/status/poll/poll.component';
|
||||
import { TimeLeftPipe } from './pipes/time-left.pipe';
|
||||
import { AutosuggestComponent } from './components/create-status/autosuggest/autosuggest.component';
|
||||
import { EmojiPickerComponent } from './components/create-status/emoji-picker/emoji-picker.component';
|
||||
import { StatusUserContextMenuComponent } from './components/stream/status/action-bar/status-user-context-menu/status-user-context-menu.component';
|
||||
import { StatusSchedulerComponent } from './components/create-status/status-scheduler/status-scheduler.component';
|
||||
import { PollEditorComponent } from './components/create-status/poll-editor/poll-editor.component';
|
||||
import { PollEntryComponent } from './components/create-status/poll-editor/poll-entry/poll-entry.component';
|
||||
import { ScheduledStatusesComponent } from './components/floating-column/scheduled-statuses/scheduled-statuses.component';
|
||||
import { ScheduledStatusComponent } from './components/floating-column/scheduled-statuses/scheduled-status/scheduled-status.component';
|
||||
import { StreamNotificationsComponent } from './components/stream/stream-notifications/stream-notifications.component';
|
||||
import { NotificationComponent } from './components/floating-column/manage-account/notifications/notification/notification.component';
|
||||
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: "", redirectTo: "home", pathMatch: "full" },
|
||||
{ path: "home", component: StreamsMainDisplayComponent },
|
||||
{ path: "register", component: RegisterNewAccountComponent},
|
||||
{ path: "**", redirectTo: "home" }
|
||||
{ path: "", redirectTo: "home", pathMatch: "full" },
|
||||
{ path: "home", component: StreamsMainDisplayComponent },
|
||||
{ path: "register", component: RegisterNewAccountComponent },
|
||||
{ path: "**", redirectTo: "home" }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
LeftSideBarComponent,
|
||||
StreamsMainDisplayComponent,
|
||||
StreamComponent,
|
||||
StreamsSelectionFooterComponent,
|
||||
StatusComponent,
|
||||
RegisterNewAccountComponent,
|
||||
AccountIconComponent,
|
||||
FloatingColumnComponent,
|
||||
ManageAccountComponent,
|
||||
AddNewStatusComponent,
|
||||
AttachementsComponent,
|
||||
SettingsComponent,
|
||||
AddNewAccountComponent,
|
||||
SearchComponent,
|
||||
ActionBarComponent,
|
||||
WaitingAnimationComponent,
|
||||
UserProfileComponent,
|
||||
ThreadComponent,
|
||||
HashtagComponent,
|
||||
StreamOverlayComponent,
|
||||
DatabindedTextComponent,
|
||||
TimeAgoPipe,
|
||||
StreamStatusesComponent,
|
||||
StreamEditionComponent,
|
||||
TutorialComponent,
|
||||
NotificationHubComponent,
|
||||
MediaViewerComponent,
|
||||
CreateStatusComponent,
|
||||
MediaComponent,
|
||||
MyAccountComponent,
|
||||
FavoritesComponent,
|
||||
DirectMessagesComponent,
|
||||
MentionsComponent,
|
||||
NotificationsComponent,
|
||||
AccountEmojiPipe
|
||||
],
|
||||
imports: [
|
||||
FontAwesomeModule,
|
||||
BrowserModule,
|
||||
HttpModule,
|
||||
HttpClientModule,
|
||||
FormsModule,
|
||||
RouterModule.forRoot(routes),
|
||||
declarations: [
|
||||
AppComponent,
|
||||
LeftSideBarComponent,
|
||||
StreamsMainDisplayComponent,
|
||||
StreamComponent,
|
||||
StreamsSelectionFooterComponent,
|
||||
StatusComponent,
|
||||
RegisterNewAccountComponent,
|
||||
AccountIconComponent,
|
||||
FloatingColumnComponent,
|
||||
ManageAccountComponent,
|
||||
AddNewStatusComponent,
|
||||
AttachementsComponent,
|
||||
SettingsComponent,
|
||||
AddNewAccountComponent,
|
||||
SearchComponent,
|
||||
ActionBarComponent,
|
||||
WaitingAnimationComponent,
|
||||
UserProfileComponent,
|
||||
ThreadComponent,
|
||||
HashtagComponent,
|
||||
StreamOverlayComponent,
|
||||
DatabindedTextComponent,
|
||||
TimeAgoPipe,
|
||||
StreamStatusesComponent,
|
||||
StreamEditionComponent,
|
||||
TutorialComponent,
|
||||
NotificationHubComponent,
|
||||
MediaViewerComponent,
|
||||
CreateStatusComponent,
|
||||
MediaComponent,
|
||||
MyAccountComponent,
|
||||
FavoritesComponent,
|
||||
DirectMessagesComponent,
|
||||
MentionsComponent,
|
||||
NotificationsComponent,
|
||||
AccountEmojiPipe,
|
||||
CardComponent,
|
||||
ListEditorComponent,
|
||||
ListAccountComponent,
|
||||
PollComponent,
|
||||
TimeLeftPipe,
|
||||
AutosuggestComponent,
|
||||
EmojiPickerComponent,
|
||||
StatusUserContextMenuComponent,
|
||||
StatusSchedulerComponent,
|
||||
PollEditorComponent,
|
||||
PollEntryComponent,
|
||||
ScheduledStatusesComponent,
|
||||
ScheduledStatusComponent,
|
||||
StreamNotificationsComponent,
|
||||
NotificationComponent
|
||||
],
|
||||
entryComponents: [
|
||||
EmojiPickerComponent
|
||||
],
|
||||
imports: [
|
||||
FontAwesomeModule,
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
HttpModule,
|
||||
HttpClientModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
PickerModule,
|
||||
OwlDateTimeModule,
|
||||
OwlNativeDateTimeModule,
|
||||
OverlayModule,
|
||||
RouterModule.forRoot(routes),
|
||||
|
||||
NgxsModule.forRoot([
|
||||
RegisteredAppsState,
|
||||
AccountsState,
|
||||
StreamsState,
|
||||
SettingsState
|
||||
]),
|
||||
NgxsStoragePluginModule.forRoot()
|
||||
],
|
||||
providers: [AuthService, NavigationService, NotificationService, MastodonService, StreamingService],
|
||||
bootstrap: [AppComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
NgxsModule.forRoot([
|
||||
RegisteredAppsState,
|
||||
AccountsState,
|
||||
StreamsState,
|
||||
SettingsState
|
||||
]),
|
||||
NgxsStoragePluginModule.forRoot(),
|
||||
ContextMenuModule.forRoot(),
|
||||
HotkeyModule.forRoot()
|
||||
],
|
||||
providers: [AuthService, NavigationService, NotificationService, MastodonWrapperService, MastodonService, StreamingService],
|
||||
bootstrap: [AppComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class AppModule { }
|
||||
|
@ -0,0 +1,17 @@
|
||||
<div class="autosuggest" *ngIf="accounts.length > 0 || hashtags.length > 0">
|
||||
<a href *ngFor="let a of accounts"
|
||||
title="@{{a.account.acct}}"
|
||||
(click)="accountSelected(a)"
|
||||
class="autosuggest__entry autosuggest__account"
|
||||
[class.autosuggest__entry--selected]="a.selected">
|
||||
<img class="autosuggest__account--avatar" src="{{ a.account.avatar }}" /> <span class="autosuggest__account--text"><span class="autosuggest__account--handle">{{ a.account.username }}</span> @{{ a.account.acct }}</span>
|
||||
</a>
|
||||
|
||||
<a href *ngFor="let h of hashtags"
|
||||
title="#{{h.hashtag}}"
|
||||
(click)="hashtagSelected(h)"
|
||||
class="autosuggest__entry"
|
||||
[class.autosuggest__entry--selected]="h.selected">
|
||||
<span class="autosuggest__account--handle">#{{ h.hashtag }}</span>
|
||||
</a>
|
||||
</div>
|
@ -0,0 +1,54 @@
|
||||
@import "variables";
|
||||
|
||||
|
||||
.autosuggest {
|
||||
background-color: $autosuggest-background;
|
||||
// border: solid $autosuggest-background;
|
||||
// border-width: 0 1px 1px 1px;
|
||||
|
||||
&__entry {
|
||||
display: block;
|
||||
padding: 1px 5px;
|
||||
color: $autosuggest-entry-color;
|
||||
background-color: $autosuggest-entry-background;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: calc(100%);
|
||||
|
||||
&:hover, &--selected {
|
||||
color: $autosuggest-entry-color-hover;
|
||||
text-decoration: none;
|
||||
background-color: $autosuggest-entry-background-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&__account {
|
||||
padding: 5px 5px 5px 5px;
|
||||
|
||||
&:last-child{
|
||||
padding: 5px 5px 5px 5px;
|
||||
}
|
||||
|
||||
&--avatar {
|
||||
width: 25px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
&--text {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
&--handle {
|
||||
color: $autosuggest-entry-handle-color;
|
||||
}
|
||||
// &--acct {
|
||||
// color: $autosuggest-entry-color;
|
||||
// }
|
||||
}
|
||||
|
||||
&__entry:hover &__account--handle, &__entry--selected &__account--handle {
|
||||
color: $autosuggest-entry-handle-color-hover;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AutosuggestComponent } from './autosuggest.component';
|
||||
|
||||
xdescribe('AutosuggestComponent', () => {
|
||||
let component: AutosuggestComponent;
|
||||
let fixture: ComponentFixture<AutosuggestComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AutosuggestComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AutosuggestComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,190 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter, OnDestroy } from '@angular/core';
|
||||
|
||||
import { ToolsService } from '../../../services/tools.service';
|
||||
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { Results, Account } from '../../../services/models/mastodon.interfaces';
|
||||
import { Actions } from '@ngxs/store';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-autosuggest',
|
||||
templateUrl: './autosuggest.component.html',
|
||||
styleUrls: ['./autosuggest.component.scss']
|
||||
})
|
||||
export class AutosuggestComponent implements OnInit, OnDestroy {
|
||||
|
||||
private lastPatternUsed: string;
|
||||
private lastPatternUsedWtType: string;
|
||||
accounts: SelectableAccount[] = [];
|
||||
hashtags: SelectableHashtag[] = [];
|
||||
|
||||
@Output() suggestionSelectedEvent = new EventEmitter<AutosuggestSelection>();
|
||||
@Output() hasSuggestionsEvent = new EventEmitter<boolean>();
|
||||
|
||||
private _pattern: string;
|
||||
@Input('pattern')
|
||||
set pattern(value: string) {
|
||||
if (value) {
|
||||
this._pattern = value;
|
||||
this.analysePattern(value);
|
||||
} else {
|
||||
this._pattern = null;
|
||||
this.accounts.length = 0;
|
||||
this.hashtags.length = 0;
|
||||
}
|
||||
}
|
||||
get pattern(): string {
|
||||
return this._pattern;
|
||||
}
|
||||
|
||||
@Input() autoSuggestUserActionsStream: EventEmitter<AutosuggestUserActionEnum>;
|
||||
private autoSuggestUserActionsSub: Subscription;
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mastodonService: MastodonWrapperService) { }
|
||||
|
||||
ngOnInit() {
|
||||
if (this.autoSuggestUserActionsStream) {
|
||||
this.autoSuggestUserActionsSub = this.autoSuggestUserActionsStream.subscribe((action: AutosuggestUserActionEnum) => {
|
||||
this.processUserInput(action);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.autoSuggestUserActionsSub) this.autoSuggestUserActionsSub.unsubscribe();
|
||||
}
|
||||
|
||||
private analysePattern(value: string) {
|
||||
const selectedAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
const isAccount = value[0] === '@';
|
||||
const pattern = value.substring(1);
|
||||
this.lastPatternUsed = pattern;
|
||||
this.lastPatternUsedWtType = value;
|
||||
|
||||
this.toolsService.getInstanceInfo(selectedAccount)
|
||||
.then(instance => {
|
||||
let version: 'v1' | 'v2' = 'v1';
|
||||
if(instance.major >= 3) version = 'v2';
|
||||
return this.mastodonService.search(selectedAccount, pattern, version, false);
|
||||
})
|
||||
.then((results: Results) => {
|
||||
if (this.lastPatternUsed !== pattern) return;
|
||||
|
||||
this.accounts.length = 0;
|
||||
this.hashtags.length = 0;
|
||||
|
||||
if (isAccount) {
|
||||
for (let account of results.accounts) {
|
||||
//if (account.acct != this.lastPatternUsed) {
|
||||
this.accounts.push(new SelectableAccount(account));
|
||||
this.accounts[0].selected = true;
|
||||
if (this.accounts.length > 7) return;
|
||||
//}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (let hashtag of results.hashtags) {
|
||||
//if (hashtag !== this.lastPatternUsed) {
|
||||
//if (hashtag.includes(this.lastPatternUsed.toLocaleLowerCase()) && hashtag !== this.lastPatternUsed) {
|
||||
//if (hashtag.includes(this.lastPatternUsed) && hashtag !== this.lastPatternUsed) {
|
||||
this.hashtags.push(new SelectableHashtag(hashtag));
|
||||
this.hashtags[0].selected = true;
|
||||
if (this.hashtags.length > 7) return;
|
||||
//}
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (this.hashtags.length > 0 || this.accounts.length > 0) {
|
||||
this.hasSuggestionsEvent.next(true);
|
||||
} else {
|
||||
this.hasSuggestionsEvent.next(false);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, selectedAccount);
|
||||
});
|
||||
}
|
||||
|
||||
private processUserInput(action: AutosuggestUserActionEnum) {
|
||||
const isAutosuggestingHashtag = this.hashtags.length > 0;
|
||||
|
||||
switch (action) {
|
||||
case AutosuggestUserActionEnum.Validate:
|
||||
if (isAutosuggestingHashtag) {
|
||||
let selection = this.hashtags.find(x => x.selected);
|
||||
this.hashtagSelected(selection);
|
||||
} else {
|
||||
let selection = this.accounts.find(x => x.selected);
|
||||
this.accountSelected(selection);
|
||||
}
|
||||
break;
|
||||
case AutosuggestUserActionEnum.MoveDown:
|
||||
if (isAutosuggestingHashtag) {
|
||||
let selectionIndex = this.hashtags.findIndex(x => x.selected);
|
||||
if (selectionIndex < (this.hashtags.length - 1)) {
|
||||
this.hashtags[selectionIndex].selected = false;
|
||||
this.hashtags[selectionIndex + 1].selected = true;
|
||||
}
|
||||
} else {
|
||||
let selectionIndex = this.accounts.findIndex(x => x.selected);
|
||||
if (selectionIndex < (this.accounts.length - 1)) {
|
||||
this.accounts[selectionIndex].selected = false;
|
||||
this.accounts[selectionIndex + 1].selected = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case AutosuggestUserActionEnum.MoveUp:
|
||||
if (isAutosuggestingHashtag) {
|
||||
let selectionIndex = this.hashtags.findIndex(x => x.selected);
|
||||
if (selectionIndex > 0) {
|
||||
this.hashtags[selectionIndex].selected = false;
|
||||
this.hashtags[selectionIndex - 1].selected = true;
|
||||
}
|
||||
} else {
|
||||
let selectionIndex = this.accounts.findIndex(x => x.selected);
|
||||
if (selectionIndex > 0) {
|
||||
this.accounts[selectionIndex].selected = false;
|
||||
this.accounts[selectionIndex - 1].selected = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
accountSelected(selAccount: SelectableAccount): boolean {
|
||||
const fullHandle = this.toolsService.getAccountFullHandle(selAccount.account);
|
||||
this.suggestionSelectedEvent.next(new AutosuggestSelection(this.lastPatternUsedWtType, fullHandle));
|
||||
return false;
|
||||
}
|
||||
|
||||
hashtagSelected(selHashtag: SelectableHashtag): boolean {
|
||||
this.suggestionSelectedEvent.next(new AutosuggestSelection(this.lastPatternUsedWtType, `#${selHashtag.hashtag}`));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class SelectableAccount {
|
||||
constructor(public account: Account, public selected: boolean = false) {
|
||||
}
|
||||
}
|
||||
|
||||
class SelectableHashtag {
|
||||
constructor(public hashtag: string, public selected: boolean = false) {
|
||||
}
|
||||
}
|
||||
|
||||
export class AutosuggestSelection {
|
||||
constructor(public pattern: string, public autosuggest: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export enum AutosuggestUserActionEnum {
|
||||
MoveDown,
|
||||
MoveUp,
|
||||
Validate
|
||||
}
|
@ -1,28 +1,83 @@
|
||||
<form class="status-form" (ngSubmit)="onSubmit()">
|
||||
<div class="status-form__sending" *ngIf="isSending">
|
||||
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
||||
</div>
|
||||
<form class="status-editor" (ngSubmit)="onSubmit()">
|
||||
<input [(ngModel)]="title" type="text" class="form-control form-control-sm status-editor__title" name="title"
|
||||
autocomplete="off" placeholder="Title, Content Warning (optional)" title="title, content warning (optional)" dir="auto" />
|
||||
|
||||
<input [(ngModel)]="title" type="text" class="form-control form-control-sm" name="title" autocomplete="off"
|
||||
placeholder="Title, Content Warning (optional)" title="title, content warning (optional)" />
|
||||
<a class="status-editor__emoji" title="Insert Emoji"
|
||||
#emojiButton href (click)="openEmojiPicker($event)">
|
||||
<img class="status-editor__emoji--image" src="/assets/emoji/72x72/1f636.png">
|
||||
</a>
|
||||
|
||||
<textarea #reply [(ngModel)]="status" name="status"
|
||||
class="form-control form-control-sm status-form__status flexcroll" rows="5" required title="content"
|
||||
placeholder="What's in your mind?" (keydown.control.enter)="onCtrlEnter()"></textarea>
|
||||
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm status-editor__content"
|
||||
rows="5" required title="content" placeholder="What's in your mind?" (keydown.control.enter)="onCtrlEnter()"
|
||||
(keydown)="handleKeyDown($event)" (blur)="statusTextEditorLostFocus()" dir="auto">
|
||||
</textarea>
|
||||
|
||||
<div class="status-form__mention-error" *ngIf="mentionTooFarAwayError">Error: mentions must be placed closer to the
|
||||
<div class="status-editor__mention-error" *ngIf="mentionTooFarAwayError">Error: mentions must be placed closer to
|
||||
the
|
||||
start in order to use multiposting.</div>
|
||||
|
||||
<select class="form-control form-control-sm form-control--privacy" id="privacy" name="privacy"
|
||||
[(ngModel)]="selectedPrivacy">
|
||||
<option *ngFor="let p of privacyList" [ngValue]="p">{{p}}</option>
|
||||
</select>
|
||||
<div class="status-form__counter">
|
||||
<span class="status-form__counter--count">{{charCountLeft}}</span> <span
|
||||
class="status-form__counter--posts">{{postCounts - 1}}/{{postCounts}}</span>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-sm btn-custom-primary" *ngIf="statusReplyingToWrapper">REPLY!</button>
|
||||
<button type="submit" class="btn btn-sm btn-custom-primary" *ngIf="!statusReplyingToWrapper">POST!</button>
|
||||
<app-autosuggest class="status-editor__autosuggest" *ngIf="autosuggestData" [pattern]="autosuggestData"
|
||||
[autoSuggestUserActionsStream]="autoSuggestUserActionsStream"
|
||||
(suggestionSelectedEvent)="suggestionSelected($event)" (hasSuggestionsEvent)="suggestionsChanged($event)">
|
||||
</app-autosuggest>
|
||||
|
||||
<app-poll-editor *ngIf="instanceSupportsPoll && pollIsActive"></app-poll-editor>
|
||||
|
||||
<app-status-scheduler class="scheduler" *ngIf="instanceSupportsScheduling && scheduleIsActive"></app-status-scheduler>
|
||||
|
||||
<div class="status-editor__footer" #footer>
|
||||
<button type="submit" title="reply" class="status-editor__footer--send-button" *ngIf="statusReplyingToWrapper">
|
||||
<span *ngIf="!isSending && !scheduleIsActive">REPLY!</span>
|
||||
<span *ngIf="!isSending && scheduleIsActive">PLAN!</span>
|
||||
<app-waiting-animation class="waiting-icon" *ngIf="isSending"></app-waiting-animation>
|
||||
</button>
|
||||
<button type="submit" title="post" class="status-editor__footer--send-button" *ngIf="!statusReplyingToWrapper">
|
||||
<span *ngIf="!isSending && !scheduleIsActive">POST!</span>
|
||||
<span *ngIf="!isSending && scheduleIsActive">PLAN!</span>
|
||||
<app-waiting-animation class="waiting-icon" *ngIf="isSending"></app-waiting-animation>
|
||||
</button>
|
||||
<div class="status-editor__footer__counter">
|
||||
<div class="status-editor__footer__counter--posts" title="number of statuses">
|
||||
{{postCounts - 1}}/{{postCounts}}</div>
|
||||
<div class="status-editor__footer__counter--count" title="chars left">{{charCountLeft}}</div>
|
||||
</div>
|
||||
|
||||
<a href class="status-editor__footer--link" title="add media" (click)="addMedia()">
|
||||
<fa-icon [icon]="faPaperclip"></fa-icon>
|
||||
</a>
|
||||
<input #fileInput type="file" id="file" style="display: none;" (change)="handleFileInput($event.target.files)">
|
||||
|
||||
<a href class="status-editor__footer--link" title="{{ selectedPrivacy }}" (click)="onContextMenu($event)">
|
||||
<fa-icon [icon]="faGlobeAmericas" *ngIf="selectedPrivacy === 'Public'"></fa-icon>
|
||||
<fa-icon [icon]="faLockOpen" *ngIf="selectedPrivacy === 'Unlisted'"></fa-icon>
|
||||
<fa-icon [icon]="faLock" *ngIf="selectedPrivacy === 'Follows-only'"></fa-icon>
|
||||
<fa-icon [icon]="faEnvelope" *ngIf="selectedPrivacy === 'DM'"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a href *ngIf="instanceSupportsPoll"
|
||||
class="status-editor__footer--link status-editor__footer--add-poll" title="add poll" (click)="addPoll()">
|
||||
<fa-icon [icon]="faPollH"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a href *ngIf="instanceSupportsScheduling"
|
||||
class="status-editor__footer--link" title="schedule" (click)="schedule()">
|
||||
<fa-icon [icon]="faClock"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<context-menu #contextMenu>
|
||||
<ng-template contextMenuItem (execute)="changePrivacy('Public')">
|
||||
<fa-icon [icon]="faGlobeAmericas" class="context-menu-icon"></fa-icon> Public
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="changePrivacy('Unlisted')">
|
||||
<fa-icon [icon]="faLockOpen" class="context-menu-icon"></fa-icon> Unlisted
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="changePrivacy('Follows-only')">
|
||||
<fa-icon [icon]="faLock" class="context-menu-icon"></fa-icon> Followers-only
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="changePrivacy('DM')">
|
||||
<fa-icon [icon]="faEnvelope" class="context-menu-icon"></fa-icon> Direct
|
||||
</ng-template>
|
||||
</context-menu>
|
||||
<app-media></app-media>
|
||||
</form>
|
||||
</form>
|
||||
|
@ -2,96 +2,213 @@
|
||||
@import "commons";
|
||||
@import "panel";
|
||||
@import "buttons";
|
||||
@import "mixins";
|
||||
$btn-send-status-width: 60px;
|
||||
$counter-width: 90px;
|
||||
|
||||
// @import "~@ctrl/ngx-emoji-mart/picker";
|
||||
|
||||
.form-control {
|
||||
margin: 0 0 5px 5px;
|
||||
width: calc(100% - 10px);
|
||||
background-color: $column-color;
|
||||
background-color: $status-editor-background;
|
||||
border-color: $status-secondary-color;
|
||||
color: #fff;
|
||||
color: $status-editor-color;
|
||||
font-size: $default-font-size;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&--privacy {
|
||||
display: inline-block;
|
||||
width: calc(100% - 15px - #{$btn-send-status-width} - #{$counter-width});
|
||||
}
|
||||
}
|
||||
|
||||
.status-editor {
|
||||
position: relative;
|
||||
font-size: $default-font-size;
|
||||
margin-bottom: 5px;
|
||||
|
||||
&__title {
|
||||
background-color: $status-editor-title-background;
|
||||
color: $status-editor-color;
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
border-width: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__emoji {
|
||||
position: absolute;
|
||||
top: 37px;
|
||||
right: 10px;
|
||||
|
||||
&--image {
|
||||
transition: all .2s;
|
||||
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
-webkit-filter: grayscale(100%);
|
||||
-moz-filter: grayscale(100%);
|
||||
-ms-filter: grayscale(100%);
|
||||
-o-filter: grayscale(100%);
|
||||
filter: gray;
|
||||
opacity: .7;
|
||||
|
||||
&:hover {
|
||||
filter: none;
|
||||
-webkit-filter: grayscale(0%);
|
||||
-moz-filter: grayscale(0%);
|
||||
-ms-filter: grayscale(0%);
|
||||
-o-filter: grayscale(0%);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
border-width: 0;
|
||||
background-color: $status-editor-background;
|
||||
color: $status-editor-color;
|
||||
margin-bottom: 0;
|
||||
|
||||
resize: none;
|
||||
border: none;
|
||||
overflow: auto;
|
||||
outline: none;
|
||||
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
|
||||
min-height: 110px;
|
||||
height: 110px;
|
||||
|
||||
padding-bottom: 10px;
|
||||
padding-right: 30px;
|
||||
//border-bottom: 1px solid black;
|
||||
|
||||
&::-webkit-resizer {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&__mention-error {
|
||||
background-color: $status-editor-background;
|
||||
color: rgb(255, 34, 34);
|
||||
padding: 5px 10px;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
&__autosuggest {
|
||||
display: block;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
height: 34px;
|
||||
margin: 0 5px;
|
||||
border-width: 0;
|
||||
|
||||
background-color: $status-editor-footer-background;
|
||||
|
||||
&--link {
|
||||
color: $status-editor-footer-link-color;
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
margin: 2px 0 0 5px;
|
||||
}
|
||||
|
||||
&--add-poll {
|
||||
font-size: 16px;
|
||||
margin: 0 0 0 5px;
|
||||
position: relative;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
&--send-button {
|
||||
@include clearButton;
|
||||
transition: all .2s;
|
||||
float: right;
|
||||
padding: 0 15px 0 15px;
|
||||
height: 34px;
|
||||
background-color: $status-editor-footer-background;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($status-editor-footer-background, 20%);
|
||||
background-color: darken($status-editor-footer-background, 20%);
|
||||
}
|
||||
|
||||
outline: inherit;
|
||||
&:focus {
|
||||
background-color: darken($status-editor-footer-background, 20%);
|
||||
}
|
||||
|
||||
& span {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__counter {
|
||||
float: right;
|
||||
height: 34px;
|
||||
padding: 6px 7px 0 7px;
|
||||
vertical-align: center;
|
||||
|
||||
&--count {
|
||||
display: block;
|
||||
margin-right: 40px
|
||||
}
|
||||
|
||||
&--posts {
|
||||
display: block;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-custom-primary {
|
||||
display: inline-block;
|
||||
width: $btn-send-status-width;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
left: 5px; // background-color: orange;
|
||||
// border-color: orange;
|
||||
// color: black;
|
||||
font-weight: 500; // &:hover {
|
||||
// }
|
||||
// &:focus {
|
||||
// border-color: darkblue;
|
||||
// }
|
||||
left: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-form {
|
||||
.context-menu-icon {
|
||||
position: relative;
|
||||
font-size: $default-font-size;
|
||||
&__sending {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 4px;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
background-color: rgba($column-color, .75);
|
||||
z-index: 2;
|
||||
&--waiting {
|
||||
margin-top: calc(25%);
|
||||
}
|
||||
}
|
||||
&__counter {
|
||||
display: inline-block;
|
||||
border: 1px solid $status-secondary-color;
|
||||
margin-left: 5px;
|
||||
width: calc(#{$counter-width} - 5px);
|
||||
height: 32px;
|
||||
position: relative;
|
||||
top: 0px;
|
||||
padding: 4px 7px 0 7px; // color: lighten($font-link-primary-hover, 10);
|
||||
// position: relative;
|
||||
// overflow: hidden;
|
||||
&--count {
|
||||
// position: absolute;
|
||||
// left: 0;
|
||||
// overflow: hidden;
|
||||
// outline: 1px solid greenyellow;
|
||||
}
|
||||
&--posts {
|
||||
// position: absolute;
|
||||
// right: 0;
|
||||
margin-left: 10px;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
&__status {
|
||||
&::-webkit-resizer {
|
||||
// border: 2px solid black;
|
||||
background: $font-link-primary-hover;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
// box-shadow: 0 0 5px 5px blue;
|
||||
// outline: 2px solid yellow;
|
||||
}
|
||||
left: -3px;
|
||||
font-size: 12px;
|
||||
color: #1f1f1f;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
}
|
||||
.emojipicker {
|
||||
font-size: $default-font-size !important;
|
||||
}
|
||||
|
||||
&__mention-error {
|
||||
border: 2px dashed red;
|
||||
padding: 5px 10px;
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
.scheduler {
|
||||
display: block;
|
||||
margin: 0 5px;
|
||||
border-bottom: 1px solid whitesmoke;
|
||||
}
|
||||
|
||||
@import '~@angular/cdk/overlay-prebuilt.css';
|
||||
// ::ng-deep .cdk-overlay-backdrop {
|
||||
// // width: 100%;
|
||||
// // height: 100%;
|
||||
// border: 3px solid greenyellow;
|
||||
// background-color: black;
|
||||
// min-height: 20px;
|
||||
// }
|
@ -2,6 +2,8 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgxsModule } from '@ngxs/store';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { ContextMenuModule } from 'ngx-contextmenu';
|
||||
|
||||
import { CreateStatusComponent } from './create-status.component';
|
||||
import { WaitingAnimationComponent } from '../waiting-animation/waiting-animation.component';
|
||||
@ -12,7 +14,8 @@ import { StreamsState } from '../../states/streams.state';
|
||||
import { NavigationService } from '../../services/navigation.service';
|
||||
import { NotificationService } from '../../services/notification.service';
|
||||
import { MastodonService } from '../../services/mastodon.service';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
|
||||
|
||||
describe('CreateStatusComponent', () => {
|
||||
let component: CreateStatusComponent;
|
||||
@ -26,13 +29,14 @@ describe('CreateStatusComponent', () => {
|
||||
imports: [
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
ContextMenuModule.forRoot(),
|
||||
NgxsModule.forRoot([
|
||||
RegisteredAppsState,
|
||||
AccountsState,
|
||||
StreamsState
|
||||
]),
|
||||
],
|
||||
providers: [NavigationService, NotificationService, MastodonService],
|
||||
providers: [NavigationService, NotificationService, MastodonService, AuthService],
|
||||
schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
|
||||
}).compileComponents();
|
||||
}));
|
||||
@ -47,6 +51,57 @@ describe('CreateStatusComponent', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not count emoji as multiple chars', () => {
|
||||
const status = '😃 😍 👌 👇 😱 😶 status with 😱 😶 emojis 😏 👍 ';
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(461);
|
||||
});
|
||||
|
||||
it('should not count emoji in CW as multiple chars', () => {
|
||||
const status = 'test';
|
||||
(<any>component).title = '🙂 test';
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(490);
|
||||
});
|
||||
|
||||
it('should not count domain chars in username', () => {
|
||||
const status = 'dsqdqs @NicolasConstant@mastodon.partipirate.org dsqdqsdqsd';
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(466);
|
||||
});
|
||||
|
||||
it('should not count https link more than the minimum', () => {
|
||||
const status = "https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/";
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(477);
|
||||
});
|
||||
|
||||
it('should not count http link more than the minimum', () => {
|
||||
const status = "http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/";
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(477);
|
||||
});
|
||||
|
||||
it('should not count links more than the minimum', () => {
|
||||
const status = "http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/ http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/ http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/";
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(429);
|
||||
});
|
||||
|
||||
it('should count correctly complex status', () => {
|
||||
const status = 'dsqdqs @NicolasConstant@mastodon.partipirate.org dsqdqs👇😱 😶 status https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/ #Pleroma with 😱 😶 emojis 😏 👍 #Mastodon @ddqsdqs @dsqdsq@dqsdsqqdsq';
|
||||
(<any>component).title = '🙂 test';
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(373);
|
||||
});
|
||||
|
||||
it('should not parse small status', () => {
|
||||
const status = 'this is a cool status';
|
||||
(<any>component).maxCharLength = 500;
|
||||
@ -131,4 +186,13 @@ describe('CreateStatusComponent', () => {
|
||||
expect(result[1]).toContain('@Lorem@ipsum.com ');
|
||||
});
|
||||
|
||||
it('should parse long link properly for multiposting', () => {
|
||||
const status = 'dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd dsq http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/';
|
||||
(<any>component).maxCharLength = 500;
|
||||
const result = <string[]>(<any>component).parseStatus(status);
|
||||
expect(result.length).toBe(2);
|
||||
expect(result[0].length).toBeLessThanOrEqual(527);
|
||||
expect(result[1].length).toBeLessThanOrEqual(527);
|
||||
expect(result[1]).toBe('http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/');
|
||||
});
|
||||
});
|
@ -1,18 +1,28 @@
|
||||
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ElementRef, ViewChild } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ElementRef, ViewChild, ViewContainerRef, ComponentRef, HostListener } from '@angular/core';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Store } from '@ngxs/store';
|
||||
import { Subscription, Observable } from 'rxjs';
|
||||
import { UP_ARROW, DOWN_ARROW, ENTER, ESCAPE } from '@angular/cdk/keycodes';
|
||||
import { faPaperclip, faGlobe, faGlobeAmericas, faLock, faLockOpen, faEnvelope, faPollH } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faClock, faWindowClose as faWindowCloseRegular } from "@fortawesome/free-regular-svg-icons";
|
||||
import { ContextMenuService, ContextMenuComponent } from 'ngx-contextmenu';
|
||||
|
||||
import { MastodonService, VisibilityEnum } from '../../services/mastodon.service';
|
||||
import { VisibilityEnum, PollParameters } from '../../services/mastodon.service';
|
||||
import { MastodonWrapperService } from '../../services/mastodon-wrapper.service';
|
||||
import { Status, Attachment } from '../../services/models/mastodon.interfaces';
|
||||
import { ToolsService } from '../../services/tools.service';
|
||||
import { ToolsService, InstanceInfo, InstanceType } from '../../services/tools.service';
|
||||
import { NotificationService } from '../../services/notification.service';
|
||||
import { StatusWrapper } from '../../models/common.model';
|
||||
import { AccountInfo } from '../../states/accounts.state';
|
||||
import { InstancesInfoService } from '../../services/instances-info.service';
|
||||
import { MediaService } from '../../services/media.service';
|
||||
import { identifierModuleUrl } from '@angular/compiler';
|
||||
|
||||
import { AutosuggestSelection, AutosuggestUserActionEnum } from './autosuggest/autosuggest.component';
|
||||
import { Overlay, OverlayConfig, FullscreenOverlayContainer, OverlayRef } from '@angular/cdk/overlay';
|
||||
import { ComponentPortal, TemplatePortal } from '@angular/cdk/portal';
|
||||
import { EmojiPickerComponent } from './emoji-picker/emoji-picker.component';
|
||||
import { PollEditorComponent } from './poll-editor/poll-editor.component';
|
||||
import { StatusSchedulerComponent } from './status-scheduler/status-scheduler.component';
|
||||
import { ScheduledStatusService } from '../../services/scheduled-status.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-status',
|
||||
@ -20,42 +30,144 @@ import { identifierModuleUrl } from '@angular/compiler';
|
||||
styleUrls: ['./create-status.component.scss']
|
||||
})
|
||||
export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||
title: string;
|
||||
faPaperclip = faPaperclip;
|
||||
faGlobe = faGlobe;
|
||||
faGlobeAmericas = faGlobeAmericas;
|
||||
faLock = faLock;
|
||||
faLockOpen = faLockOpen;
|
||||
faEnvelope = faEnvelope;
|
||||
faPollH = faPollH;
|
||||
faClock = faClock;
|
||||
|
||||
autoSuggestUserActionsStream = new EventEmitter<AutosuggestUserActionEnum>();
|
||||
|
||||
private _title: string;
|
||||
set title(value: string) {
|
||||
this._title = value;
|
||||
this.countStatusChar(this.status);
|
||||
}
|
||||
get title(): string {
|
||||
return this._title;
|
||||
}
|
||||
|
||||
private _status: string = '';
|
||||
@Input('status')
|
||||
set status(value: string) {
|
||||
this.countStatusChar(value);
|
||||
this.detectAutosuggestion(value);
|
||||
this._status = value;
|
||||
|
||||
setTimeout(() => {
|
||||
this.autoGrow();
|
||||
}, 0);
|
||||
}
|
||||
get status(): string {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
@Input('redraftedStatus')
|
||||
set redraftedStatus(value: StatusWrapper) {
|
||||
if (value) {
|
||||
this.statusLoaded = false;
|
||||
let parser = new DOMParser();
|
||||
var dom = parser.parseFromString(value.status.content, 'text/html')
|
||||
this.status = dom.body.textContent;
|
||||
|
||||
this.setVisibilityFromStatus(value.status);
|
||||
this.title = value.status.spoiler_text;
|
||||
this.statusLoaded = true;
|
||||
|
||||
if (value.status.in_reply_to_id) {
|
||||
this.isSending = true;
|
||||
this.mastodonService.getStatus(value.provider, value.status.in_reply_to_id)
|
||||
.then((status: Status) => {
|
||||
this.statusReplyingToWrapper = new StatusWrapper(status, value.provider);
|
||||
|
||||
const mentions = this.getMentions(this.statusReplyingToWrapper.status, this.statusReplyingToWrapper.provider);
|
||||
for (const mention of mentions) {
|
||||
const name = `@${mention.split('@')[0]}`;
|
||||
if (this.status.includes(name)) {
|
||||
this.status = this.status.replace(name, `@${mention}`);
|
||||
} else {
|
||||
this.status = `@${mention} ` + this.status;
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, value.provider);
|
||||
})
|
||||
.then(() => {
|
||||
this.isSending = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private maxCharLength: number;
|
||||
charCountLeft: number;
|
||||
postCounts: number = 1;
|
||||
isSending: boolean;
|
||||
mentionTooFarAwayError: boolean;
|
||||
autosuggestData: string = null;
|
||||
instanceSupportsPoll = true;
|
||||
instanceSupportsScheduling = true;
|
||||
private statusLoaded: boolean;
|
||||
private hasSuggestions: boolean;
|
||||
|
||||
@Input() statusReplyingToWrapper: StatusWrapper;
|
||||
@Output() onClose = new EventEmitter();
|
||||
@ViewChild('reply') replyElement: ElementRef;
|
||||
@ViewChild('fileInput') fileInputElement: ElementRef;
|
||||
@ViewChild('footer') footerElement: ElementRef;
|
||||
@ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent;
|
||||
@ViewChild(PollEditorComponent) pollEditor: PollEditorComponent;
|
||||
@ViewChild(StatusSchedulerComponent) statusScheduler: StatusSchedulerComponent;
|
||||
|
||||
private _isDirectMention: boolean;
|
||||
@Input('isDirectMention')
|
||||
set isDirectMention(value: boolean) {
|
||||
if (value) {
|
||||
this._isDirectMention = value;
|
||||
this.initMention();
|
||||
}
|
||||
}
|
||||
get isDirectMention(): boolean {
|
||||
return this._isDirectMention;
|
||||
}
|
||||
|
||||
private _replyingUserHandle: string;
|
||||
@Input('replyingUserHandle')
|
||||
set replyingUserHandle(value: string) {
|
||||
if (value) {
|
||||
this._replyingUserHandle = value;
|
||||
this.initMention();
|
||||
}
|
||||
}
|
||||
get replyingUserHandle(): string {
|
||||
return this._replyingUserHandle;
|
||||
}
|
||||
|
||||
private statusReplyingTo: Status;
|
||||
|
||||
selectedPrivacy = 'Public';
|
||||
privacyList: string[] = ['Public', 'Unlisted', 'Follows-only', 'DM'];
|
||||
// privacyList: string[] = ['Public', 'Unlisted', 'Follows-only', 'DM'];
|
||||
|
||||
private accounts$: Observable<AccountInfo[]>;
|
||||
private accountSub: Subscription;
|
||||
private selectedAccount: AccountInfo;
|
||||
|
||||
constructor(
|
||||
private readonly scheduledStatusService: ScheduledStatusService,
|
||||
private readonly contextMenuService: ContextMenuService,
|
||||
private readonly store: Store,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mastodonService: MastodonService,
|
||||
private readonly mastodonService: MastodonWrapperService,
|
||||
private readonly instancesInfoService: InstancesInfoService,
|
||||
private readonly mediaService: MediaService) {
|
||||
private readonly mediaService: MediaService,
|
||||
private readonly overlay: Overlay,
|
||||
public viewContainerRef: ViewContainerRef) {
|
||||
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
|
||||
}
|
||||
|
||||
@ -63,6 +175,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
|
||||
this.accountChanged(accounts);
|
||||
});
|
||||
this.selectedAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
if (this.statusReplyingToWrapper) {
|
||||
if (this.statusReplyingToWrapper.status.reblog) {
|
||||
@ -76,54 +189,189 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||
this.status += `@${mention} `;
|
||||
}
|
||||
|
||||
switch (this.statusReplyingTo.visibility) {
|
||||
case 'unlisted':
|
||||
this.setVisibility(VisibilityEnum.Unlisted);
|
||||
break;
|
||||
case 'public':
|
||||
this.setVisibility(VisibilityEnum.Public);
|
||||
break;
|
||||
case 'private':
|
||||
this.setVisibility(VisibilityEnum.Private);
|
||||
break;
|
||||
case 'direct':
|
||||
this.setVisibility(VisibilityEnum.Direct);
|
||||
break;
|
||||
}
|
||||
this.setVisibilityFromStatus(this.statusReplyingTo);
|
||||
|
||||
this.title = this.statusReplyingTo.spoiler_text;
|
||||
} else if (this.replyingUserHandle) {
|
||||
this.initMention();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.replyElement.nativeElement.focus();
|
||||
}, 0);
|
||||
this.statusLoaded = true;
|
||||
this.focus();
|
||||
|
||||
this.innerHeight = window.innerHeight;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.accountSub.unsubscribe();
|
||||
}
|
||||
|
||||
changePrivacy(value: string): boolean {
|
||||
this.selectedPrivacy = value;
|
||||
return false;
|
||||
}
|
||||
|
||||
addMedia(): boolean {
|
||||
this.fileInputElement.nativeElement.click();
|
||||
return false;
|
||||
}
|
||||
|
||||
handleFileInput(files: File[]): boolean {
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
this.mediaService.uploadMedia(acc, files);
|
||||
return false;
|
||||
}
|
||||
|
||||
private detectAutosuggestion(status: string) {
|
||||
if (!this.statusLoaded) return;
|
||||
|
||||
if(!status.includes('@') && !status.includes('#')){
|
||||
this.autosuggestData = null;
|
||||
this.hasSuggestions = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const caretPosition = this.replyElement.nativeElement.selectionStart;
|
||||
|
||||
const lastChar = status.substr(caretPosition - 1, 1);
|
||||
const lastCharIsSpace = lastChar === ' ';
|
||||
|
||||
const splitedStatus = status.split(/(\r\n|\n|\r)/);
|
||||
let offset = 0;
|
||||
let currentSection = '';
|
||||
for (let x of splitedStatus) {
|
||||
const sectionLength = x.length;
|
||||
if (offset + sectionLength >= caretPosition) {
|
||||
currentSection = x;
|
||||
break;
|
||||
} else {
|
||||
offset += sectionLength;
|
||||
}
|
||||
};
|
||||
|
||||
const word = this.getWordByPos(currentSection, caretPosition - offset);
|
||||
if (!lastCharIsSpace && word && word.length > 0 && (word.startsWith('@') || word.startsWith('#'))) {
|
||||
this.autosuggestData = word;
|
||||
return;
|
||||
}
|
||||
|
||||
this.autosuggestData = null;
|
||||
this.hasSuggestions = false;
|
||||
}
|
||||
|
||||
private getWordByPos(str, pos) {
|
||||
var preText = str.substring(0, pos);
|
||||
if (preText.indexOf(" ") > 0) {
|
||||
var words = preText.split(" ");
|
||||
return words[words.length - 1]; //return last word
|
||||
}
|
||||
else {
|
||||
return preText;
|
||||
}
|
||||
|
||||
// // str = str.replace(/(\r\n|\n|\r)/gm, "");
|
||||
// var left = str.substr(0, pos);
|
||||
// var right = str.substr(pos);
|
||||
|
||||
// left = left.replace(/^.+ /g, "");
|
||||
// right = right.replace(/ .+$/g, "");
|
||||
|
||||
// return left + right;
|
||||
}
|
||||
|
||||
private focus(caretPos = null) {
|
||||
setTimeout(() => {
|
||||
this.replyElement.nativeElement.focus();
|
||||
|
||||
if (caretPos) {
|
||||
this.replyElement.nativeElement.setSelectionRange(caretPos, caretPos);
|
||||
} else {
|
||||
this.replyElement.nativeElement.setSelectionRange(this.status.length, this.status.length);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private initMention() {
|
||||
this.statusLoaded = false;
|
||||
|
||||
if (!this.selectedAccount) {
|
||||
this.selectedAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
}
|
||||
|
||||
if (this.isDirectMention) {
|
||||
this.setVisibility(VisibilityEnum.Direct);
|
||||
} else {
|
||||
this.getDefaultPrivacy();
|
||||
}
|
||||
this.status = `${this.replyingUserHandle} `;
|
||||
this.countStatusChar(this.status);
|
||||
|
||||
this.statusLoaded = true;
|
||||
this.focus();
|
||||
}
|
||||
|
||||
private accountChanged(accounts: AccountInfo[]): void {
|
||||
if (accounts && accounts.length > 0) {
|
||||
const selectedAccount = accounts.filter(x => x.isSelected)[0];
|
||||
this.instancesInfoService.getMaxStatusChars(selectedAccount.instance)
|
||||
.then((maxChars: number) => {
|
||||
this.maxCharLength = maxChars;
|
||||
this.countStatusChar(this.status);
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
});
|
||||
this.selectedAccount = accounts.filter(x => x.isSelected)[0];
|
||||
|
||||
if (!this.statusReplyingToWrapper) {
|
||||
this.instancesInfoService.getDefaultPrivacy(selectedAccount)
|
||||
.then((defaultPrivacy: VisibilityEnum) => {
|
||||
this.setVisibility(defaultPrivacy);
|
||||
const settings = this.toolsService.getAccountSettings(this.selectedAccount);
|
||||
if (settings.customStatusCharLengthEnabled) {
|
||||
this.maxCharLength = settings.customStatusCharLength;
|
||||
this.countStatusChar(this.status);
|
||||
} else {
|
||||
this.instancesInfoService.getMaxStatusChars(this.selectedAccount.instance)
|
||||
.then((maxChars: number) => {
|
||||
this.maxCharLength = maxChars;
|
||||
this.countStatusChar(this.status);
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
this.notificationService.notifyHttpError(err, this.selectedAccount);
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.statusReplyingToWrapper && !this.replyingUserHandle) {
|
||||
this.getDefaultPrivacy();
|
||||
}
|
||||
|
||||
this.toolsService.getInstanceInfo(this.selectedAccount)
|
||||
.then((instance: InstanceInfo) => {
|
||||
if (instance.type === InstanceType.Pixelfed) {
|
||||
this.instanceSupportsPoll = false;
|
||||
this.instanceSupportsScheduling = false;
|
||||
this.pollIsActive = false;
|
||||
this.scheduleIsActive = false;
|
||||
} else {
|
||||
this.instanceSupportsPoll = true;
|
||||
this.instanceSupportsScheduling = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getDefaultPrivacy() {
|
||||
this.instancesInfoService.getDefaultPrivacy(this.selectedAccount)
|
||||
.then((defaultPrivacy: VisibilityEnum) => {
|
||||
this.setVisibility(defaultPrivacy);
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.notificationService.notifyHttpError(err, this.selectedAccount);
|
||||
});
|
||||
}
|
||||
|
||||
private setVisibilityFromStatus(status: Status) {
|
||||
switch (status.visibility) {
|
||||
case 'unlisted':
|
||||
this.setVisibility(VisibilityEnum.Unlisted);
|
||||
break;
|
||||
case 'public':
|
||||
this.setVisibility(VisibilityEnum.Public);
|
||||
break;
|
||||
case 'private':
|
||||
this.setVisibility(VisibilityEnum.Private);
|
||||
break;
|
||||
case 'direct':
|
||||
this.setVisibility(VisibilityEnum.Direct);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,14 +415,23 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||
|
||||
const currentStatus = parseStatus[parseStatus.length - 1];
|
||||
const statusExtraChars = this.getMentionExtraChars(status);
|
||||
const linksExtraChars = this.getLinksExtraChars(status);
|
||||
|
||||
const statusLength = currentStatus.length - statusExtraChars;
|
||||
this.charCountLeft = this.maxCharLength - statusLength;
|
||||
const statusLength = [...currentStatus].length - statusExtraChars - linksExtraChars;
|
||||
this.charCountLeft = this.maxCharLength - statusLength - this.getCwLength();
|
||||
this.postCounts = parseStatus.length;
|
||||
}
|
||||
|
||||
private getCwLength(): number {
|
||||
let cwLength = 0;
|
||||
if (this.title) {
|
||||
cwLength = [...this.title].length;
|
||||
}
|
||||
return cwLength;
|
||||
}
|
||||
|
||||
private getMentions(status: Status, providerInfo: AccountInfo): string[] {
|
||||
const mentions = [...status.mentions.map(x => x.acct), status.account.acct];
|
||||
const mentions = [status.account.acct, ...status.mentions.map(x => x.acct)];
|
||||
|
||||
let uniqueMentions = [];
|
||||
for (let mention of mentions) {
|
||||
@ -233,20 +490,35 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||
usableStatus = Promise.resolve(null);
|
||||
}
|
||||
|
||||
let poll: PollParameters = null;
|
||||
if (this.pollIsActive) {
|
||||
poll = this.pollEditor.getPollParameters();
|
||||
}
|
||||
|
||||
let scheduledTime = null;
|
||||
if (this.scheduleIsActive) {
|
||||
scheduledTime = this.statusScheduler.getScheduledDate();
|
||||
if (!scheduledTime || scheduledTime === '') {
|
||||
this.isSending = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
usableStatus
|
||||
.then((status: Status) => {
|
||||
return this.sendStatus(acc, this.status, visibility, this.title, status, mediaAttachments);
|
||||
return this.sendStatus(acc, this.status, visibility, this.title, status, mediaAttachments, poll, scheduledTime);
|
||||
})
|
||||
.then((res: Status) => {
|
||||
if (this.statusReplyingToWrapper) {
|
||||
this.notificationService.newStatusPosted(this.statusReplyingToWrapper.status.id, new StatusWrapper(res, acc));
|
||||
}
|
||||
this.title = '';
|
||||
this.status = '';
|
||||
this.onClose.emit();
|
||||
|
||||
if (this.scheduleIsActive) {
|
||||
this.scheduledStatusService.statusAdded(acc);
|
||||
}
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
})
|
||||
.then(() => {
|
||||
this.isSending = false;
|
||||
@ -255,28 +527,36 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||
return false;
|
||||
}
|
||||
|
||||
private sendStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, title: string, previousStatus: Status, attachments: Attachment[]): Promise<Status> {
|
||||
private sendStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, title: string, previousStatus: Status, attachments: Attachment[], poll: PollParameters, scheduledAt: string): Promise<Status> {
|
||||
let parsedStatus = this.parseStatus(status);
|
||||
let resultPromise = Promise.resolve(previousStatus);
|
||||
|
||||
for (let i = 0; i < parsedStatus.length; i++) {
|
||||
let s = parsedStatus[i];
|
||||
resultPromise = resultPromise.then((pStatus: Status) => {
|
||||
let inReplyToId = null;
|
||||
if (pStatus) {
|
||||
inReplyToId = pStatus.id;
|
||||
}
|
||||
resultPromise = resultPromise
|
||||
.then((pStatus: Status) => {
|
||||
let inReplyToId = null;
|
||||
if (pStatus) {
|
||||
inReplyToId = pStatus.id;
|
||||
}
|
||||
|
||||
if (i === 0) {
|
||||
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id))
|
||||
.then((status: Status) => {
|
||||
this.mediaService.clearMedia();
|
||||
return status;
|
||||
});
|
||||
} else {
|
||||
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, []);
|
||||
}
|
||||
});
|
||||
if (i === 0) {
|
||||
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id), poll, scheduledAt)
|
||||
.then((status: Status) => {
|
||||
this.mediaService.clearMedia();
|
||||
return status;
|
||||
});
|
||||
} else {
|
||||
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, [], null, scheduledAt);
|
||||
}
|
||||
})
|
||||
.then((status: Status) => {
|
||||
if (this.statusReplyingToWrapper) {
|
||||
this.notificationService.newStatusPosted(this.statusReplyingToWrapper.status.id, new StatusWrapper(status, account));
|
||||
}
|
||||
|
||||
return status;
|
||||
});
|
||||
}
|
||||
|
||||
return resultPromise;
|
||||
@ -293,7 +573,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||
aggregateMention += `${x} `;
|
||||
});
|
||||
|
||||
const currentMaxCharLength = this.maxCharLength + mentionExtraChars;
|
||||
const currentMaxCharLength = this.maxCharLength + mentionExtraChars - this.getCwLength();
|
||||
const maxChars = currentMaxCharLength - 6;
|
||||
|
||||
while (trucatedStatus.length > currentMaxCharLength) {
|
||||
@ -305,6 +585,18 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||
return results;
|
||||
}
|
||||
|
||||
private getLinksExtraChars(status: string): number {
|
||||
let mentionExtraChars = 0;
|
||||
let links = status.split(' ').filter(x => x.startsWith('http://') || x.startsWith('https://'));
|
||||
for (let link of links) {
|
||||
if (link.length > 23) {
|
||||
mentionExtraChars += link.length - 23;
|
||||
}
|
||||
}
|
||||
|
||||
return mentionExtraChars;
|
||||
}
|
||||
|
||||
private getMentionExtraChars(status: string): number {
|
||||
let mentionExtraChars = 0;
|
||||
let mentions = this.getMentionsFromStatus(status);
|
||||
@ -323,4 +615,181 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||
private getMentionsFromStatus(status: string): string[] {
|
||||
return status.split(' ').filter(x => x.indexOf('@') === 0 && x.length > 1);
|
||||
}
|
||||
|
||||
suggestionSelected(selection: AutosuggestSelection) {
|
||||
if (this.status.includes(selection.pattern)) {
|
||||
|
||||
let transformedStatus = this.status;
|
||||
transformedStatus = transformedStatus.replace(new RegExp(` ${selection.pattern} `), ` ${selection.autosuggest} `).replace(' ', ' ');
|
||||
transformedStatus = transformedStatus.replace(new RegExp(`${selection.pattern} `), `${selection.autosuggest} `).replace(' ', ' ');
|
||||
transformedStatus = transformedStatus.replace(new RegExp(`${selection.pattern}$`), `${selection.autosuggest} `).replace(' ', ' ');
|
||||
this.status = transformedStatus;
|
||||
|
||||
let newCaretPosition = this.status.indexOf(`${selection.autosuggest} `) + selection.autosuggest.length + 1;
|
||||
if (newCaretPosition > this.status.length) newCaretPosition = this.status.length;
|
||||
|
||||
this.autosuggestData = null;
|
||||
this.hasSuggestions = false;
|
||||
|
||||
if (document.activeElement === this.replyElement.nativeElement) {
|
||||
setTimeout(() => {
|
||||
this.replyElement.nativeElement.setSelectionRange(newCaretPosition, newCaretPosition);
|
||||
}, 0);
|
||||
} else {
|
||||
this.focus(newCaretPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suggestionsChanged(hasSuggestions: boolean) {
|
||||
this.hasSuggestions = hasSuggestions;
|
||||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent): boolean {
|
||||
if (this.hasSuggestions) {
|
||||
let keycode = event.keyCode;
|
||||
if (keycode === DOWN_ARROW || keycode === UP_ARROW || keycode === ENTER || keycode === ESCAPE) {
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
switch (keycode) {
|
||||
case DOWN_ARROW:
|
||||
this.autoSuggestUserActionsStream.next(AutosuggestUserActionEnum.MoveDown);
|
||||
break;
|
||||
case UP_ARROW:
|
||||
this.autoSuggestUserActionsStream.next(AutosuggestUserActionEnum.MoveUp);
|
||||
break;
|
||||
case ENTER:
|
||||
this.autoSuggestUserActionsStream.next(AutosuggestUserActionEnum.Validate);
|
||||
break;
|
||||
case ESCAPE:
|
||||
this.autosuggestData = null;
|
||||
this.hasSuggestions = false;
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statusTextEditorLostFocus(): boolean {
|
||||
setTimeout(() => {
|
||||
this.autosuggestData = null;
|
||||
this.hasSuggestions = false;
|
||||
}, 250);
|
||||
return false;
|
||||
}
|
||||
|
||||
private autoGrow() {
|
||||
let scrolling = (this.replyElement.nativeElement.scrollHeight);
|
||||
|
||||
if (scrolling > 110) {
|
||||
const isVisible = this.checkVisible(this.footerElement.nativeElement);
|
||||
//this.replyElement.nativeElement.style.height = `0px`;
|
||||
this.replyElement.nativeElement.style.height = `${this.replyElement.nativeElement.scrollHeight}px`;
|
||||
|
||||
if (isVisible) {
|
||||
setTimeout(() => {
|
||||
this.footerElement.nativeElement.scrollIntoViewIfNeeded({ behavior: 'instant', block: 'end', inline: 'start' });
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkVisible(elm) {
|
||||
var rect = elm.getBoundingClientRect();
|
||||
var viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
|
||||
return !(rect.bottom < 0 || rect.top - viewHeight >= 0);
|
||||
}
|
||||
|
||||
public onContextMenu($event: MouseEvent): void {
|
||||
this.contextMenuService.show.next({
|
||||
// Optional - if unspecified, all context menu components will open
|
||||
contextMenu: this.contextMenu,
|
||||
event: $event,
|
||||
item: null
|
||||
});
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
}
|
||||
|
||||
//https://stackblitz.com/edit/overlay-demo
|
||||
@ViewChild('emojiButton') emojiButtonElement: ElementRef;
|
||||
overlayRef: OverlayRef;
|
||||
|
||||
public innerHeight: number;
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(event) {
|
||||
this.innerHeight = window.innerHeight;
|
||||
}
|
||||
|
||||
private emojiCloseSub: Subscription;
|
||||
private emojiSelectedSub: Subscription;
|
||||
private beforeEmojiCaretPosition: number;
|
||||
openEmojiPicker(e: MouseEvent): boolean {
|
||||
if (this.overlayRef) return false;
|
||||
|
||||
this.beforeEmojiCaretPosition = this.replyElement.nativeElement.selectionStart;
|
||||
|
||||
let topPosition = e.pageY;
|
||||
if (this.innerHeight - e.pageY < 360) {
|
||||
topPosition -= 360;
|
||||
}
|
||||
|
||||
let config = new OverlayConfig();
|
||||
config.positionStrategy = this.overlay.position()
|
||||
.global()
|
||||
.left(`${e.pageX - 283}px`)
|
||||
.top(`${topPosition}px`);
|
||||
config.hasBackdrop = true;
|
||||
|
||||
this.overlayRef = this.overlay.create(config);
|
||||
// this.overlayRef.backdropClick().subscribe(() => {
|
||||
// console.warn('wut?');
|
||||
// this.overlayRef.dispose();
|
||||
// });
|
||||
|
||||
let comp = new ComponentPortal(EmojiPickerComponent);
|
||||
const compRef: ComponentRef<EmojiPickerComponent> = this.overlayRef.attach(comp);
|
||||
this.emojiCloseSub = compRef.instance.closedEvent.subscribe(() => {
|
||||
this.closeEmojiPanel();
|
||||
});
|
||||
this.emojiSelectedSub = compRef.instance.emojiSelectedEvent.subscribe((emoji) => {
|
||||
if (emoji) {
|
||||
this.status = [this.status.slice(0, this.beforeEmojiCaretPosition), emoji, ' ', this.status.slice(this.beforeEmojiCaretPosition)].join('').replace(' ', ' ');
|
||||
this.beforeEmojiCaretPosition += emoji.length + 1;
|
||||
|
||||
this.closeEmojiPanel();
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private closeEmojiPanel() {
|
||||
if (this.emojiCloseSub) this.emojiCloseSub.unsubscribe();
|
||||
if (this.emojiSelectedSub) this.emojiSelectedSub.unsubscribe();
|
||||
if (this.overlayRef) this.overlayRef.dispose();
|
||||
this.overlayRef = null;
|
||||
this.focus(this.beforeEmojiCaretPosition);
|
||||
}
|
||||
|
||||
closeEmoji(): boolean {
|
||||
this.overlayRef.dispose();
|
||||
return false;
|
||||
}
|
||||
|
||||
pollIsActive: boolean;
|
||||
addPoll(): boolean {
|
||||
this.pollIsActive = !this.pollIsActive;
|
||||
return false;
|
||||
}
|
||||
|
||||
scheduleIsActive: boolean;
|
||||
schedule(): boolean {
|
||||
this.scheduleIsActive = !this.scheduleIsActive;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
<emoji-mart
|
||||
*ngIf="loaded"
|
||||
[showPreview]="false" [perLine]="7" [isNative]="true" [sheetSize]="16" [emojiTooltip]="true"
|
||||
[custom]="customEmojis" (emojiSelect)="emojiSelected($event)" class="emojipicker" title="Pick your emoji…"
|
||||
emoji="point_up"></emoji-mart>
|
@ -0,0 +1,22 @@
|
||||
::ng-deep .emoji-mart {
|
||||
border-radius: 0 !important;
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
::ng-deep .emoji-mart-emoji-native {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
::ng-deep .emoji-mart-emoji-native span {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
left: -1px;
|
||||
font-size: 19px !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
::ng-deep .emoji-mart-emoji-custom span {
|
||||
position: relative;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EmojiPickerComponent } from './emoji-picker.component';
|
||||
|
||||
xdescribe('EmojiPickerComponent', () => {
|
||||
let component: EmojiPickerComponent;
|
||||
let fixture: ComponentFixture<EmojiPickerComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ EmojiPickerComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EmojiPickerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,75 @@
|
||||
import { Component, OnInit, HostListener, ElementRef, Output, EventEmitter } from '@angular/core';
|
||||
|
||||
import { ToolsService } from '../../../services/tools.service';
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { Emoji } from '../../../services/models/mastodon.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-emoji-picker',
|
||||
templateUrl: './emoji-picker.component.html',
|
||||
styleUrls: ['./emoji-picker.component.scss']
|
||||
})
|
||||
export class EmojiPickerComponent implements OnInit {
|
||||
private init = false;
|
||||
|
||||
@Output('closed') public closedEvent = new EventEmitter();
|
||||
@Output('emojiSelected') public emojiSelectedEvent = new EventEmitter<string>();
|
||||
|
||||
customEmojis: PickerCustomEmoji[] = [];
|
||||
loaded: boolean;
|
||||
|
||||
constructor(
|
||||
private notificationService: NotificationService,
|
||||
private toolsService: ToolsService,
|
||||
private eRef: ElementRef) { }
|
||||
|
||||
@HostListener('document:click', ['$event'])
|
||||
clickout(event) {
|
||||
if (!this.init) return;
|
||||
|
||||
if (!this.eRef.nativeElement.contains(event.target)) {
|
||||
this.closedEvent.emit(null);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
let currentAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
this.toolsService.getCustomEmojis(currentAccount)
|
||||
.then(emojis => {
|
||||
this.customEmojis = emojis.map(x => this.convertEmoji(x));
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, currentAccount);
|
||||
})
|
||||
.then(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.init = true;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private convertEmoji(emoji: Emoji): PickerCustomEmoji {
|
||||
return new PickerCustomEmoji(emoji.shortcode, [emoji.shortcode], emoji.shortcode, [emoji.shortcode], emoji.url);
|
||||
}
|
||||
|
||||
emojiSelected(select: any): boolean {
|
||||
if (select.emoji.custom) {
|
||||
this.emojiSelectedEvent.next(select.emoji.colons);
|
||||
} else {
|
||||
this.emojiSelectedEvent.next(select.emoji.native);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class PickerCustomEmoji {
|
||||
constructor(
|
||||
public name: string,
|
||||
public shortNames: string[],
|
||||
public text: string,
|
||||
public keywords: string[],
|
||||
public imageUrl: string) {
|
||||
}
|
||||
}
|
@ -2,18 +2,33 @@
|
||||
<div *ngIf="m.attachment === null" class="media__loading" title="{{m.file.name}}">
|
||||
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
||||
</div>
|
||||
<div *ngIf="m.attachment !== null" class="media__loaded" title="{{m.file.name}}"
|
||||
(mouseleave) ="updateMedia(m)">
|
||||
<div *ngIf="m.attachment !== null && m.attachment.type !== 'audio'" class="media__loaded" title="{{m.file.name}}"
|
||||
(mouseleave)="updateMedia(m)">
|
||||
<div class="media__loaded--migrating" *ngIf="m.isMigrating">
|
||||
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
||||
</div>
|
||||
<div class="media__loaded--hover">
|
||||
<button class="media__loaded--button" title="remove" (click)="removeMedia(m)">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
</button>
|
||||
<input class="media__loaded--description" [(ngModel)]="m.description"
|
||||
autocomplete="off" placeholder="Describe for the visually impaired"/>
|
||||
<a href class="media__loaded--button" title="remove" (click)="removeMedia(m)">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
<input class="media__loaded--description" [(ngModel)]="m.description" autocomplete="off"
|
||||
placeholder="Describe for the visually impaired" />
|
||||
</div>
|
||||
<img class="media__loaded--preview" src="{{m.attachment.preview_url}}" />
|
||||
</div>
|
||||
<div *ngIf="m.attachment !== null && m.attachment.type === 'audio'" class="audio">
|
||||
<a href class="audio__button" title="remove" (click)="removeMedia(m)">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
|
||||
<div *ngIf="m.isMigrating">
|
||||
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
||||
</div>
|
||||
|
||||
<audio *ngIf="m.audioType && !m.isMigrating" controls class="audio__player">
|
||||
<source src="{{ m.attachment.url }}" type="{{ m.audioType }}">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
|
||||
</div>
|
||||
</div>
|
@ -4,11 +4,11 @@
|
||||
|
||||
.media {
|
||||
width: calc(100%);
|
||||
padding: 0 5px 5px 5px;
|
||||
padding: 5px 5px 0px 5px;
|
||||
|
||||
&__loading{
|
||||
width: calc(100%);
|
||||
border: 1px solid $status-secondary-color;
|
||||
//border: 1px solid $status-secondary-color;
|
||||
// background: rgb(0, 96, 134);
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
@ -16,7 +16,7 @@
|
||||
&__loaded{
|
||||
width: calc(100%);
|
||||
height: 75px;
|
||||
border: 1px solid $status-secondary-color;
|
||||
//border: 1px solid $status-secondary-color;
|
||||
position: relative;
|
||||
transition: all .2s;
|
||||
|
||||
@ -46,12 +46,11 @@
|
||||
opacity: 100;
|
||||
}
|
||||
|
||||
&--button {
|
||||
@include clearButton;
|
||||
&--button {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
position: absolute;
|
||||
top:5px;
|
||||
top:0px;
|
||||
right:8px;
|
||||
color: white;
|
||||
}
|
||||
@ -61,20 +60,34 @@
|
||||
bottom:5px;
|
||||
left: 5px;
|
||||
width: calc(100% - 10px);
|
||||
// background: black;
|
||||
// color: white;
|
||||
}
|
||||
|
||||
&--preview {
|
||||
// display: block;
|
||||
|
||||
width: calc(100%);
|
||||
height: calc(100%);
|
||||
|
||||
object-fit: cover;
|
||||
object-position: 50% 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.audio {
|
||||
width: calc(100%);
|
||||
height: 30px;
|
||||
|
||||
&__player {
|
||||
width: calc(100% - 20px);
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
color: white;
|
||||
float: right;
|
||||
margin-top: 0px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<div class="poll-editor">
|
||||
<div class="poll-editor__entries">
|
||||
<div *ngFor="let e of entries">
|
||||
<app-poll-entry class="poll-editor__entry" [entry]="e" (removeEvent)="removeElement(e)"
|
||||
(toogleMultiEvent)="toogleMulti()"></app-poll-entry>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="poll-editor__footer">
|
||||
<select [(ngModel)]="selectedId" class="poll-editor__footer--select-duration">
|
||||
<option *ngFor="let d of delayChoice" [ngValue]="d.id">{{d.label}}</option>
|
||||
</select>
|
||||
|
||||
<a href (click)="addEntry()" class="poll-editor__footer--add-choice">
|
||||
<fa-icon [icon]="faPlus"></fa-icon> Add a choice
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,37 @@
|
||||
@import "variables";
|
||||
|
||||
.poll-editor {
|
||||
background-color: $poll-editor-background;
|
||||
border-top: 1px solid $poll-editor-separator;
|
||||
min-height: 30px;
|
||||
margin: 0 5px;
|
||||
|
||||
&__entries {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
&__entry {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
transition: all .2s;
|
||||
border-top: 1px solid $poll-editor-separator;
|
||||
min-height: 30px;
|
||||
padding: 5px;
|
||||
|
||||
&--add-choice {
|
||||
color: rgb(49, 49, 49);
|
||||
padding: 0 5px 0 5px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: rgb(122, 122, 122);
|
||||
}
|
||||
}
|
||||
|
||||
&--select-duration {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PollEditorComponent } from './poll-editor.component';
|
||||
|
||||
xdescribe('PollEditorComponent', () => {
|
||||
let component: PollEditorComponent;
|
||||
let fixture: ComponentFixture<PollEditorComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ PollEditorComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PollEditorComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,80 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { PollEntry } from './poll-entry/poll-entry.component';
|
||||
import { PollParameters } from '../../../services/mastodon.service';
|
||||
import { retry } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-poll-editor',
|
||||
templateUrl: './poll-editor.component.html',
|
||||
styleUrls: ['./poll-editor.component.scss']
|
||||
})
|
||||
export class PollEditorComponent implements OnInit {
|
||||
faPlus = faPlus;
|
||||
|
||||
private entryUuid: number = 0;
|
||||
entries: PollEntry[] = [];
|
||||
delayChoice: Delay[] = [];
|
||||
selectedId: string;
|
||||
private multiSelected: boolean;
|
||||
|
||||
constructor() {
|
||||
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
|
||||
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
|
||||
|
||||
this.delayChoice.push(new Delay(60 * 5, "5 minutes"));
|
||||
this.delayChoice.push(new Delay(60 * 30, "30 minutes"));
|
||||
this.delayChoice.push(new Delay(60 * 60, "1 hour"));
|
||||
this.delayChoice.push(new Delay(60 * 60 * 6, "6 hours"));
|
||||
this.delayChoice.push(new Delay(60 * 60 * 24, "1 day"));
|
||||
this.delayChoice.push(new Delay(60 * 60 * 24 * 3, "3 days"));
|
||||
this.delayChoice.push(new Delay(60 * 60 * 24 * 7, "7 days"));
|
||||
this.delayChoice.push(new Delay(60 * 60 * 24 * 15, "15 days"));
|
||||
this.delayChoice.push(new Delay(60 * 60 * 24 * 30, "30 days"));
|
||||
|
||||
this.selectedId = this.delayChoice[4].id;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
}
|
||||
|
||||
private getEntryUuid(): number {
|
||||
this.entryUuid++;
|
||||
return this.entryUuid;
|
||||
}
|
||||
|
||||
addEntry(): boolean {
|
||||
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
|
||||
return false;
|
||||
}
|
||||
|
||||
removeElement(entry: PollEntry){
|
||||
this.entries = this.entries.filter(x => x.id != entry.id);
|
||||
}
|
||||
|
||||
toogleMulti() {
|
||||
this.multiSelected = !this.multiSelected;
|
||||
this.entries.forEach((e: PollEntry) => {
|
||||
e.isMulti = this.multiSelected;
|
||||
});
|
||||
}
|
||||
|
||||
getPollParameters(): PollParameters {
|
||||
let params = new PollParameters();
|
||||
params.expires_in = this.delayChoice.find(x => x.id === this.selectedId).delayInSeconds;
|
||||
params.multiple = this.multiSelected;
|
||||
params.options = this.entries.map(x => x.label);
|
||||
params.hide_totals = false;
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
class Delay {
|
||||
constructor(public delayInSeconds: number, public label: string) {
|
||||
this.id = delayInSeconds.toString();
|
||||
}
|
||||
|
||||
id: string;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
<div class="poll-entry">
|
||||
<div class="poll-entry__remove">
|
||||
<a href (click)="remove()" title="remove" class="poll-entry__remove--link">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
<div class="poll-entry__multi">
|
||||
<a href (click)="toogleMulti()" class="poll-entry__multi--link">
|
||||
<span class="check-mark" [class.check-mark__round]="!entry.isMulti" [class.check-mark__box]="entry.isMulti">
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="poll-entry__label">
|
||||
<input type="text" [(ngModel)]="entry.label" class="poll-entry__label--input" [(ngModel)]="entry.label"/>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,66 @@
|
||||
@import "variables";
|
||||
|
||||
$selector-size: 20px;
|
||||
$selector-padding: 5px;
|
||||
|
||||
.poll-entry {
|
||||
position: relative;
|
||||
|
||||
&__multi {
|
||||
&--link {
|
||||
display: block;
|
||||
padding: $selector-padding;
|
||||
width: calc(#{$selector-size} + 2 * #{$selector-padding});
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
height: calc(#{$selector-size} + 2 * #{$selector-padding});
|
||||
padding-top: 3px;
|
||||
|
||||
&--input {
|
||||
width: calc(100% - #{$selector-size} - 2 * #{$selector-padding} - 30px);
|
||||
border:1px solid $poll-editor-input-border;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border:1px solid $poll-editor-input-border-focus;
|
||||
box-shadow: 0 0 0 #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__remove {
|
||||
float: right;
|
||||
width: 25px;
|
||||
height: 30px;
|
||||
|
||||
&--link {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: 5px;
|
||||
color: rgb(139, 139, 139);
|
||||
padding: 5px;
|
||||
|
||||
&:hover {
|
||||
color: rgb(0, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.check-mark {
|
||||
display: block;
|
||||
border: 1px solid rgb(100, 100, 100);
|
||||
width: $selector-size;
|
||||
height: $selector-size;
|
||||
|
||||
&__round {
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
&__box {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PollEntryComponent } from './poll-entry.component';
|
||||
|
||||
xdescribe('PollEntryComponent', () => {
|
||||
let component: PollEntryComponent;
|
||||
let fixture: ComponentFixture<PollEntryComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ PollEntryComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PollEntryComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,39 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
@Component({
|
||||
selector: 'app-poll-entry',
|
||||
templateUrl: './poll-entry.component.html',
|
||||
styleUrls: ['./poll-entry.component.scss']
|
||||
})
|
||||
export class PollEntryComponent implements OnInit {
|
||||
faTimes = faTimes;
|
||||
|
||||
@Input() entry: PollEntry;
|
||||
|
||||
@Output() removeEvent = new EventEmitter();
|
||||
@Output() toogleMultiEvent = new EventEmitter();
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
remove(): boolean {
|
||||
this.removeEvent.next();
|
||||
return false;
|
||||
}
|
||||
|
||||
toogleMulti(): boolean {
|
||||
this.toogleMultiEvent.next();
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class PollEntry {
|
||||
constructor(public id: number, public isMulti: boolean) {
|
||||
}
|
||||
|
||||
public label: string;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
<div class="scheduler">
|
||||
<input class="scheduler__input" [owlDateTime]="dt2" [owlDateTimeTrigger]="dt2" placeholder="" [min]="min" [(ngModel)]="scheduledDate">
|
||||
<a class="scheduler__icon" href (click)="openScheduler()" [owlDateTimeTrigger]="dt2" title="open datetime picker"><fa-icon [icon]="faCalendarAlt"></fa-icon></a>
|
||||
<owl-date-time #dt2></owl-date-time>
|
||||
</div>
|
@ -0,0 +1,26 @@
|
||||
@import "variables";
|
||||
|
||||
.scheduler {
|
||||
background-color: $scheduler-background;
|
||||
|
||||
&__input {
|
||||
color: whitesmoke;
|
||||
padding: 3px;
|
||||
width: calc(100% - 25px);
|
||||
border: 1px solid $scheduler-background;
|
||||
outline: 0;
|
||||
background-color: $scheduler-background;
|
||||
&:focus{
|
||||
border: 1px solid $scheduler-background;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
color: whitesmoke;
|
||||
&:hover {
|
||||
color:rgb(204, 204, 204);
|
||||
}
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { StatusSchedulerComponent } from './status-scheduler.component';
|
||||
|
||||
xdescribe('StatusSchedulerComponent', () => {
|
||||
let component: StatusSchedulerComponent;
|
||||
let fixture: ComponentFixture<StatusSchedulerComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ StatusSchedulerComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(StatusSchedulerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,32 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { faCalendarAlt } from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
@Component({
|
||||
selector: 'app-status-scheduler',
|
||||
templateUrl: './status-scheduler.component.html',
|
||||
styleUrls: ['./status-scheduler.component.scss']
|
||||
})
|
||||
export class StatusSchedulerComponent implements OnInit {
|
||||
faCalendarAlt = faCalendarAlt;
|
||||
min = new Date();
|
||||
// scheduledDate: string;
|
||||
|
||||
@Input() scheduledDate: string;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
openScheduler(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getScheduledDate(): string {
|
||||
try {
|
||||
return new Date(this.scheduledDate).toISOString();
|
||||
} catch(err){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,18 @@
|
||||
<div class="panel">
|
||||
<h3 class="panel__title">Add new account</h3>
|
||||
<div class="panel" [class.comrade__background]="isComrade">
|
||||
<h3 class="panel__title" [class.comrade__text]="isComrade">Add new account</h3>
|
||||
|
||||
<h2 class="comrade__title" *ngIf="isComrade">Welcome Comrade!</h2>
|
||||
<form (ngSubmit)="onSubmit()">
|
||||
<label>Please provide your account:</label>
|
||||
<input type="text" class="form-control form-control-sm" [(ngModel)]="mastodonFullHandle" name="mastodonFullHandle"
|
||||
<label [class.comrade__text]="isComrade">Please provide your <span *ngIf="isComrade">comrade</span> account:</label>
|
||||
<input type="text" class="form-control form-control-sm form-color" [(ngModel)]="mastodonFullHandle" name="mastodonFullHandle" [class.comrade__input]="isComrade"
|
||||
placeholder="@nickname@mastodon.social" />
|
||||
<br />
|
||||
<button type="submit" class="btn btn-success btn-sm">Submit</button>
|
||||
<button type="submit" class="btn btn-success btn-sm" [class.comrade__button]="isComrade">Submit</button>
|
||||
</form>
|
||||
|
||||
<div *ngIf="isComrade" class="comrade__video">
|
||||
<iframe width="300" height="170" src="https://www.youtube.com/embed/NzBjnoRG7Mo?feature=oembed&autoplay=1&auto_play=1" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
@ -1,2 +1,59 @@
|
||||
@import "variables";
|
||||
@import "panel";
|
||||
@import "panel";
|
||||
|
||||
.panel {
|
||||
|
||||
background-position: 0 100%;
|
||||
}
|
||||
|
||||
.form-color {
|
||||
background-color: $column-color;
|
||||
border-color: $button-border-color;
|
||||
color: #fff;
|
||||
font-size: $default-font-size;
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
height: 29px;
|
||||
padding: 0 5px 0 5px;
|
||||
}
|
||||
|
||||
$comrade_yellow: #ffcc00;
|
||||
$comrade_red: #a50000;
|
||||
.comrade {
|
||||
&__title {
|
||||
font-size: 18px;
|
||||
margin-left: 5px;
|
||||
color: $comrade_yellow;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__text {
|
||||
color: $comrade_yellow;
|
||||
}
|
||||
|
||||
&__button {
|
||||
background: $comrade_yellow;
|
||||
border-color: $comrade_yellow;
|
||||
color: $comrade_red;
|
||||
}
|
||||
|
||||
&__video {
|
||||
width: 300px;
|
||||
padding-top: 20px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
&__input {
|
||||
color: $comrade_red;
|
||||
border-color: $comrade_yellow;
|
||||
background: $comrade_yellow;
|
||||
}
|
||||
|
||||
&__background {
|
||||
transition: all 3s;
|
||||
background-image: url("assets/img/juche-background.jpg");
|
||||
background-color: $comrade_red;
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
@ -13,7 +13,22 @@ import { NotificationService } from '../../../services/notification.service';
|
||||
styleUrls: ['./add-new-account.component.scss']
|
||||
})
|
||||
export class AddNewAccountComponent implements OnInit {
|
||||
@Input() mastodonFullHandle: string;
|
||||
private blockList = ['gab.com', 'gab.ai', 'cyzed.com'];
|
||||
private comradeList = ['juche.town'];
|
||||
|
||||
private username: string;
|
||||
private instance: string;
|
||||
isComrade: boolean;
|
||||
|
||||
private _mastodonFullHandle: string;
|
||||
@Input()
|
||||
set mastodonFullHandle(value: string) {
|
||||
this._mastodonFullHandle = value;
|
||||
this.checkComrad();
|
||||
}
|
||||
get mastodonFullHandle(): string {
|
||||
return this._mastodonFullHandle;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
@ -23,29 +38,62 @@ export class AddNewAccountComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
checkComrad(): any {
|
||||
let fullHandle = this.mastodonFullHandle.split('@').filter(x => x != null && x !== '');
|
||||
this.username = fullHandle[0];
|
||||
this.instance = fullHandle[1];
|
||||
|
||||
const username = fullHandle[0];
|
||||
const instance = fullHandle[1];
|
||||
if (this.username && this.instance) {
|
||||
let cleanInstance = this.instance.replace('http://', '').replace('https://', '').toLowerCase();
|
||||
for (let b of this.comradeList) {
|
||||
if (cleanInstance == b || cleanInstance.includes(`.${b}`)) {
|
||||
this.isComrade = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.checkAndCreateApplication(instance)
|
||||
this.isComrade = false;
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
// let fullHandle = this.mastodonFullHandle.split('@').filter(x => x != null && x !== '');
|
||||
// const username = fullHandle[0];
|
||||
// const instance = fullHandle[1];
|
||||
|
||||
this.checkBlockList(this.instance);
|
||||
|
||||
this.checkAndCreateApplication(this.instance)
|
||||
.then((appData: AppData) => {
|
||||
this.redirectToInstanceAuthPage(username, instance, appData);
|
||||
this.redirectToInstanceAuthPage(this.username, this.instance, appData);
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
if (err instanceof HttpErrorResponse) {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
} else if ((<Error>err).message === 'CORS'){
|
||||
this.notificationService.notify('Connection Error. It\'s usually a CORS issue with the server you\'re connecting to. Please check in the console and if so, contact your administrator with those informations.', true);
|
||||
this.notificationService.notifyHttpError(err, null);
|
||||
} else if ((<Error>err).message === 'CORS') {
|
||||
this.notificationService.notify(null, null, 'Connection Error. It\'s usually a CORS issue with the server you\'re connecting to. Please check in the console and if so, contact your administrator with those informations.', true);
|
||||
} else {
|
||||
this.notificationService.notify('Unkown error', true);
|
||||
this.notificationService.notify(null, null, 'Unkown error', true);
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private checkBlockList(instance: string) {
|
||||
let cleanInstance = instance.replace('http://', '').replace('https://', '').toLowerCase();
|
||||
for (let b of this.blockList) {
|
||||
if (cleanInstance == b || cleanInstance.includes(`.${b}`)) {
|
||||
let content = '<div style="width:100%; height:100%; background-color: black;"><iframe style="pointer-events: none;" width="100%" height="100%" src="https://www.youtube.com/embed/dQw4w9WgXcQ?rel=0&autoplay=1&showinfo=0&controls=0" allow="autoplay; fullscreen"></div>';
|
||||
|
||||
document.open();
|
||||
document.write(content);
|
||||
document.close();
|
||||
throw Error('Oh Noz!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkAndCreateApplication(instance: string): Promise<AppData> {
|
||||
const alreadyRegisteredApps = this.getAllSavedApps();
|
||||
const instanceApps = alreadyRegisteredApps.filter(x => x.instance === instance);
|
||||
@ -53,8 +101,12 @@ export class AddNewAccountComponent implements OnInit {
|
||||
if (instanceApps.length !== 0) {
|
||||
return Promise.resolve(instanceApps[0].app);
|
||||
} else {
|
||||
const redirect_uri = this.getLocalHostname() + '/register';
|
||||
return this.authService.createNewApplication(instance, 'Sengi', redirect_uri, 'read write follow', 'https://github.com/NicolasConstant/sengi')
|
||||
let redirect_uri = this.getLocalHostname();
|
||||
if (process && process.versions && typeof((<any>process.versions).electron) === 'string') {
|
||||
redirect_uri += '/register';
|
||||
}
|
||||
|
||||
return this.authService.createNewApplication(instance, 'Sengi', redirect_uri, 'read write follow', 'https://nicolasconstant.github.io/sengi/')
|
||||
.then((appData: AppData) => {
|
||||
return this.saveNewApp(instance, appData)
|
||||
.then(() => { return appData; });
|
||||
@ -84,8 +136,12 @@ export class AddNewAccountComponent implements OnInit {
|
||||
}
|
||||
|
||||
private getLocalHostname(): string {
|
||||
let localHostname = location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '');
|
||||
return localHostname;
|
||||
let href = window.location.href;
|
||||
if(href.includes('/home')){
|
||||
return href.split('/home')[0];
|
||||
} else {
|
||||
return location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '');
|
||||
}
|
||||
}
|
||||
|
||||
private saveNewApp(instance: string, app: AppData): Promise<any> {
|
||||
|
@ -1,5 +1,9 @@
|
||||
<div class="panel">
|
||||
<h3 class="panel__title">new message</h3>
|
||||
|
||||
<app-create-status (onClose)="closeColumn()"></app-create-status>
|
||||
<div class=" new-message-body flexcroll">
|
||||
<app-create-status (onClose)="closeColumn()" [isDirectMention]="isDirectMention"
|
||||
[replyingUserHandle]="userHandle" [redraftedStatus]="redraftedStatus"></app-create-status>
|
||||
|
||||
</div>
|
||||
</div>
|
@ -1,6 +1,7 @@
|
||||
@import "variables";
|
||||
@import "panel";
|
||||
@import "buttons";
|
||||
@import "commons";
|
||||
|
||||
$btn-send-status-width: 60px;
|
||||
|
||||
@ -19,17 +20,18 @@ $btn-send-status-width: 60px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
left: 5px;
|
||||
// background-color: orange;
|
||||
// border-color: orange;
|
||||
// color: black;
|
||||
font-weight: 500;
|
||||
|
||||
// &:hover {
|
||||
|
||||
// }
|
||||
|
||||
// &:focus {
|
||||
// border-color: darkblue;
|
||||
// }
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.new-message-body {
|
||||
overflow: auto;
|
||||
height: calc(100% - 30px);
|
||||
padding-right: 5px;
|
||||
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
|
||||
import { NavigationService } from '../../../services/navigation.service';
|
||||
import { StatusWrapper } from '../../../models/common.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-new-status',
|
||||
@ -8,10 +9,15 @@ import { NavigationService } from '../../../services/navigation.service';
|
||||
styleUrls: ['./add-new-status.component.scss']
|
||||
})
|
||||
export class AddNewStatusComponent implements OnInit {
|
||||
constructor(
|
||||
private readonly navigationService: NavigationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
@Input() isDirectMention: boolean;
|
||||
@Input() userHandle: string;
|
||||
@Input() redraftedStatus: StatusWrapper;
|
||||
|
||||
constructor(private readonly navigationService: NavigationService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
closeColumn() {
|
||||
|
@ -1,25 +1,31 @@
|
||||
<div class="floating-column">
|
||||
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive" (closeOverlay)="closeOverlay()"
|
||||
[browseAccountData]="overlayAccountToBrowse"
|
||||
[browseHashtagData]="overlayHashtagToBrowse"
|
||||
[browseThreadData]="overlayThreadToBrowse"></app-stream-overlay>
|
||||
<div class="floating-column__inner">
|
||||
<div class="sliding-column" [class.sliding-column__right-display]="overlayActive">
|
||||
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive"
|
||||
(closeOverlay)="closeOverlay()"
|
||||
[browseAccountData]="overlayAccountToBrowse"
|
||||
[browseHashtagData]="overlayHashtagToBrowse"
|
||||
[browseThreadData]="overlayThreadToBrowse"></app-stream-overlay>
|
||||
|
||||
<div class="floating-column__header">
|
||||
<a class="close-button" href (click)="closePanel()" title="close">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
<div class="floating-column__inner--left">
|
||||
<div class="floating-column__header">
|
||||
<a class="close-button" href (click)="closePanel()" title="close">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<app-manage-account *ngIf="openPanel === 'manageAccount'" [account]="userAccountUsed"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-manage-account>
|
||||
<app-add-new-status *ngIf="openPanel === 'createNewStatus'" [isDirectMention]="isDirectMention"
|
||||
[userHandle]="userHandle" [redraftedStatus]="redraftedStatus"></app-add-new-status>
|
||||
<app-add-new-account *ngIf="openPanel === 'addNewAccount'"></app-add-new-account>
|
||||
<app-search *ngIf="openPanel === 'search'" (browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)" (browseThreadEvent)="browseThread($event)">
|
||||
</app-search>
|
||||
<app-settings *ngIf="openPanel === 'settings'"></app-settings>
|
||||
<app-scheduled-statuses *ngIf="openPanel === 'scheduledStatuses'"></app-scheduled-statuses>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-manage-account *ngIf="openPanel === 'manageAccount'"
|
||||
[account]="userAccountUsed"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-manage-account>
|
||||
<app-add-new-status *ngIf="openPanel === 'createNewStatus'"></app-add-new-status>
|
||||
<app-add-new-account *ngIf="openPanel === 'addNewAccount'"></app-add-new-account>
|
||||
<app-search *ngIf="openPanel === 'search'"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-search>
|
||||
<app-settings *ngIf="openPanel === 'settings'"></app-settings>
|
||||
</div>
|
@ -1,11 +1,8 @@
|
||||
@import "variables";
|
||||
@import "mixins";
|
||||
@import "panel";
|
||||
|
||||
|
||||
.floating-column {
|
||||
width: calc(100%);
|
||||
width: $floating-column-size;
|
||||
|
||||
.floating-column {
|
||||
background-color: $color-secondary;
|
||||
overflow: hidden;
|
||||
z-index: 200;
|
||||
@ -16,16 +13,19 @@
|
||||
|
||||
white-space: normal;
|
||||
|
||||
// &__header {
|
||||
|
||||
// }
|
||||
}
|
||||
&__inner {
|
||||
position: relative;
|
||||
width: $stream-column-width;
|
||||
height: calc(100%);
|
||||
|
||||
.stream-overlay {
|
||||
position: absolute;
|
||||
z-index: 50;
|
||||
width: $floating-column-size;
|
||||
height: calc(100%);
|
||||
margin: 0 0 0 $stream-column-separator;
|
||||
overflow: hidden;
|
||||
|
||||
&--left {
|
||||
width: $stream-column-width;
|
||||
height: calc(100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-button {
|
||||
@ -34,27 +34,4 @@
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
margin: 10px 16px 0 0;
|
||||
|
||||
// display: inline-block;
|
||||
// background-color: $color-primary;
|
||||
// color: darken(white, 30);
|
||||
// border-radius: 999px;
|
||||
// width: 26px;
|
||||
// height: 26px;
|
||||
// text-align: center;
|
||||
// text-decoration: none;
|
||||
// padding: 1px;
|
||||
|
||||
// z-index: 9999;
|
||||
// float: right;
|
||||
// margin: 10px;
|
||||
|
||||
// transition: all .2s;
|
||||
|
||||
// &:hover {
|
||||
// background-color: lighten($color-primary, 20);
|
||||
// color: white;
|
||||
// // transform: scale(1.2);
|
||||
// }
|
||||
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { NavigationService, LeftPanelType } from '../../services/navigation.service';
|
||||
import { NavigationService, LeftPanelType, OpenLeftPanelEvent, LeftPanelAction } from '../../services/navigation.service';
|
||||
import { AccountWrapper } from '../../models/account.models';
|
||||
import { OpenThreadEvent } from '../../services/tools.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { StatusWrapper } from '../../models/common.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-floating-column',
|
||||
@ -12,7 +13,7 @@ import { Subscription } from 'rxjs';
|
||||
styleUrls: ['./floating-column.component.scss']
|
||||
})
|
||||
export class FloatingColumnComponent implements OnInit, OnDestroy {
|
||||
|
||||
|
||||
faTimes = faTimes;
|
||||
overlayActive: boolean;
|
||||
overlayAccountToBrowse: string;
|
||||
@ -21,6 +22,10 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
|
||||
|
||||
userAccountUsed: AccountWrapper;
|
||||
|
||||
isDirectMention: boolean;
|
||||
userHandle: string;
|
||||
redraftedStatus: StatusWrapper;
|
||||
|
||||
openPanel: string = '';
|
||||
|
||||
private activatedPanelSub: Subscription;
|
||||
@ -28,9 +33,11 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
|
||||
constructor(private readonly navigationService: NavigationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.activatedPanelSub = this.navigationService.activatedPanelSubject.subscribe((type: LeftPanelType) => {
|
||||
this.activatedPanelSub = this.navigationService.activatedPanelSubject.subscribe((event: OpenLeftPanelEvent) => {
|
||||
this.isDirectMention = false;
|
||||
this.userHandle = null;
|
||||
this.overlayActive = false;
|
||||
switch (type) {
|
||||
switch (event.type) {
|
||||
case LeftPanelType.Closed:
|
||||
this.openPanel = '';
|
||||
break;
|
||||
@ -42,9 +49,12 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
break;
|
||||
case LeftPanelType.CreateNewStatus:
|
||||
if (this.openPanel === 'createNewStatus') {
|
||||
if (this.openPanel === 'createNewStatus' && !event.userHandle) {
|
||||
this.closePanel();
|
||||
} else {
|
||||
this.isDirectMention = event.action === LeftPanelAction.DM;
|
||||
this.userHandle = event.userHandle;
|
||||
this.redraftedStatus = event.status;
|
||||
this.openPanel = 'createNewStatus';
|
||||
}
|
||||
break;
|
||||
@ -75,6 +85,13 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
|
||||
this.openPanel = 'settings';
|
||||
}
|
||||
break;
|
||||
case LeftPanelType.ScheduledStatuses:
|
||||
if (this.openPanel === 'scheduledStatuses') {
|
||||
this.closePanel();
|
||||
} else {
|
||||
this.openPanel = 'scheduledStatuses';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
this.openPanel = '';
|
||||
}
|
||||
@ -82,7 +99,7 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if(this.activatedPanelSub) {
|
||||
if (this.activatedPanelSub) {
|
||||
this.activatedPanelSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,23 @@
|
||||
<p>
|
||||
direct-messages works!
|
||||
</p>
|
||||
<div class="stream-toots flexcroll" #statusstream (scroll)="onScroll()">
|
||||
<div class="stream-toots__remove-cw" *ngIf="isThread && hasContentWarnings">
|
||||
<button class="stream-toots__remove-cw--button" (click)="removeCw()" title="remove content warnings">Remove
|
||||
CWs</button>
|
||||
</div>
|
||||
<div *ngIf="displayError" class="stream-toots__error">{{displayError}}</div>
|
||||
|
||||
<!-- data-simplebar -->
|
||||
<div class="stream-toots__status" *ngFor="let conversationWrapper of conversations">
|
||||
<div class="conversation__participants" title="participants">
|
||||
<fa-icon [icon]="faUserFriends" class="conversation__icon"></fa-icon>
|
||||
<img class="conversation__avatar" src="{{ conversationWrapper.userAvatar }}" title="me" />
|
||||
<div *ngFor="let acc of conversationWrapper.conversation.accounts">
|
||||
<img class="conversation__avatar" src="{{ acc.avatar }}" title="{{ acc.acct }}" />
|
||||
</div>
|
||||
</div>
|
||||
<app-status [statusWrapper]="conversationWrapper.lastStatus" [isThreadDisplay]="isThread"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-status>
|
||||
</div>
|
||||
|
||||
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
|
||||
</div>
|
@ -0,0 +1,31 @@
|
||||
.conversation {
|
||||
&__participants {
|
||||
padding: 10px 0 0 10px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: block;
|
||||
margin-right: 10px;
|
||||
margin-left: 31px;
|
||||
padding-top: 3px;
|
||||
float: left;
|
||||
color: #5098eb;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 2px;
|
||||
margin-right: 5px;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.stream-toots__status {
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid #4e5572;
|
||||
border-bottom: 1px solid #2f3444;
|
||||
border-bottom: 1px solid #232733;
|
||||
}
|
||||
}
|
@ -1,20 +1,23 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
|
||||
import { faUserFriends } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { AccountWrapper } from '../../../../models/account.models';
|
||||
import { OpenThreadEvent } from '../../../../services/tools.service';
|
||||
import { StatusWrapper } from '../../../../models/common.model';
|
||||
import { NotificationService } from '../../../../services/notification.service';
|
||||
import { MastodonService } from '../../../../services/mastodon.service';
|
||||
import { StreamTypeEnum } from '../../../../states/streams.state';
|
||||
import { Status } from '../../../../services/models/mastodon.interfaces';
|
||||
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
|
||||
import { Conversation } from '../../../../services/models/mastodon.interfaces';
|
||||
import { AccountInfo } from '../../../../states/accounts.state';
|
||||
|
||||
@Component({
|
||||
selector: 'app-direct-messages',
|
||||
templateUrl: '../../../stream/stream-statuses/stream-statuses.component.html',
|
||||
templateUrl: './direct-messages.component.html',
|
||||
styleUrls: ['../../../stream/stream-statuses/stream-statuses.component.scss', './direct-messages.component.scss']
|
||||
})
|
||||
export class DirectMessagesComponent implements OnInit {
|
||||
statuses: StatusWrapper[] = [];
|
||||
faUserFriends = faUserFriends;
|
||||
|
||||
conversations: ConversationWrapper[] = [];
|
||||
displayError: string;
|
||||
isLoading = true;
|
||||
isThread = false;
|
||||
@ -29,7 +32,6 @@ export class DirectMessagesComponent implements OnInit {
|
||||
|
||||
@Input('account')
|
||||
set account(acc: AccountWrapper) {
|
||||
console.warn('account');
|
||||
this._account = acc;
|
||||
this.getDirectMessages();
|
||||
}
|
||||
@ -41,30 +43,29 @@ export class DirectMessagesComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly mastodonService: MastodonService) { }
|
||||
private readonly mastodonService: MastodonWrapperService) { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.isLoading = true;
|
||||
this.statuses.length = 0;
|
||||
this.conversations.length = 0;
|
||||
this.maxReached = false;
|
||||
}
|
||||
|
||||
private getDirectMessages() {
|
||||
this.reset();
|
||||
|
||||
this.mastodonService.getTimeline(this.account.info, StreamTypeEnum.directmessages)
|
||||
.then((statuses: Status[]) => {
|
||||
//this.maxId = statuses[statuses.length - 1].id;
|
||||
for (const s of statuses) {
|
||||
const wrapper = new StatusWrapper(s, this.account.info);
|
||||
this.statuses.push(wrapper);
|
||||
this.mastodonService.getConversations(this.account.info)
|
||||
.then((conversations: Conversation[]) => {
|
||||
for (const c of conversations) {
|
||||
const wrapper = new ConversationWrapper(c, this.account.info, this.account.avatar);
|
||||
this.conversations.push(wrapper);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
@ -83,22 +84,23 @@ export class DirectMessagesComponent implements OnInit {
|
||||
private scrolledToBottom() {
|
||||
if (this.isLoading || this.maxReached) return;
|
||||
|
||||
const maxId = this.statuses[this.statuses.length - 1].status.id;
|
||||
const maxId = this.conversations[this.conversations.length - 1].conversation.last_status.id;
|
||||
|
||||
this.isLoading = true;
|
||||
this.mastodonService.getTimeline(this.account.info, StreamTypeEnum.directmessages, maxId)
|
||||
.then((statuses: Status[]) => {
|
||||
if (statuses.length === 0) {
|
||||
this.mastodonService.getConversations(this.account.info, maxId)
|
||||
.then((conversations: Conversation[]) => {
|
||||
if (conversations.length === 0) {
|
||||
this.maxReached = true;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const s of statuses) {
|
||||
const wrapper = new StatusWrapper(s, this.account.info);
|
||||
this.statuses.push(wrapper);
|
||||
for (const c of conversations) {
|
||||
const wrapper = new ConversationWrapper(c, this.account.info, this.account.avatar);
|
||||
this.conversations.push(wrapper);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
@ -117,3 +119,16 @@ export class DirectMessagesComponent implements OnInit {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
}
|
||||
}
|
||||
|
||||
class ConversationWrapper {
|
||||
|
||||
constructor(
|
||||
public conversation: Conversation,
|
||||
public provider: AccountInfo,
|
||||
public userAvatar: string
|
||||
) {
|
||||
this.lastStatus = new StatusWrapper(conversation.last_status, provider);
|
||||
}
|
||||
|
||||
lastStatus: StatusWrapper;
|
||||
}
|
@ -3,7 +3,8 @@ import { Component, OnInit, Output, EventEmitter, Input, ViewChild, ElementRef }
|
||||
import { StatusWrapper } from '../../../../models/common.model';
|
||||
import { OpenThreadEvent } from '../../../../services/tools.service';
|
||||
import { AccountWrapper } from '../../../../models/account.models';
|
||||
import { MastodonService, FavoriteResult } from '../../../../services/mastodon.service';
|
||||
import { FavoriteResult } from '../../../../services/mastodon.service';
|
||||
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
|
||||
import { Status } from '../../../../services/models/mastodon.interfaces';
|
||||
import { NotificationService } from '../../../../services/notification.service';
|
||||
import { resetCompiledComponents } from '@angular/core/src/render3/jit/module';
|
||||
@ -41,7 +42,7 @@ export class FavoritesComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly mastodonService: MastodonService) { }
|
||||
private readonly mastodonService: MastodonWrapperService) { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
@ -65,7 +66,7 @@ export class FavoritesComponent implements OnInit {
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
@ -102,7 +103,7 @@ export class FavoritesComponent implements OnInit {
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
|
@ -2,7 +2,9 @@
|
||||
<h3 class="panel__title">Manage Account</h3>
|
||||
|
||||
<div class="account__header">
|
||||
<img class="account__avatar" src="{{account.avatar}}" title="{{ account.info.id }} " />
|
||||
<a href (click)="browseLocalAccount()" (auxclick)="openLocalAccount()" title="open {{ account.info.id }}">
|
||||
<img class="account__avatar" src="{{account.avatar}}"/>
|
||||
</a>
|
||||
|
||||
<!-- <a href class="account__header--button"><fa-icon [icon]="faUserPlus"></fa-icon></a> -->
|
||||
<a href class="account__header--button" title="favorites" (click)="loadSubPanel('favorites')"
|
||||
@ -17,8 +19,7 @@
|
||||
[ngClass]="{ 'account__header--button--selected': subPanel === 'mentions', 'account__header--button--notification': hasMentions }">
|
||||
<fa-icon [icon]="faAt"></fa-icon>
|
||||
</a>
|
||||
<a href class="account__header--button" title="notifications" (click)="loadSubPanel('notifications')"
|
||||
[ngClass]="{ 'account__header--button--selected': subPanel === 'notifications',
|
||||
<a href class="account__header--button" title="notifications" (click)="loadSubPanel('notifications')" [ngClass]="{ 'account__header--button--selected': subPanel === 'notifications',
|
||||
'account__header--button--notification': hasNotifications }">
|
||||
<fa-icon [icon]="faBell"></fa-icon>
|
||||
</a>
|
||||
@ -27,27 +28,18 @@
|
||||
<fa-icon [icon]="faUser"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<app-direct-messages class="account__body" *ngIf="subPanel === 'dm'"
|
||||
[account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
|
||||
<app-direct-messages class="account__body" *ngIf="subPanel === 'dm'" [account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-direct-messages>
|
||||
<app-favorites class="account__body" *ngIf="subPanel === 'favorites'"
|
||||
[account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
<app-favorites class="account__body" *ngIf="subPanel === 'favorites'" [account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-favorites>
|
||||
<app-mentions class="account__body" *ngIf="subPanel === 'mentions'"
|
||||
[account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
<app-mentions class="account__body" *ngIf="subPanel === 'mentions'" [account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-mentions>
|
||||
<app-my-account class="account__body" *ngIf="subPanel === 'account'"
|
||||
[account]="account"></app-my-account>
|
||||
<app-notifications class="account__body" *ngIf="subPanel === 'notifications'"
|
||||
[account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
<app-my-account class="account__body" *ngIf="subPanel === 'account'" [account]="account"></app-my-account>
|
||||
<app-notifications class="account__body" *ngIf="subPanel === 'notifications'" [account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-notifications>
|
||||
</div>
|
@ -5,7 +5,11 @@ import { Subscription } from 'rxjs';
|
||||
|
||||
import { AccountWrapper } from '../../../models/account.models';
|
||||
import { UserNotificationService, UserNotification } from '../../../services/user-notification.service';
|
||||
import { OpenThreadEvent } from '../../../services/tools.service';
|
||||
import { OpenThreadEvent, ToolsService } from '../../../services/tools.service';
|
||||
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
|
||||
import { Account } from "../../../services/models/mastodon.interfaces";
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { AccountInfo } from '../../../states/accounts.state';
|
||||
|
||||
|
||||
@Component({
|
||||
@ -13,7 +17,7 @@ import { OpenThreadEvent } from '../../../services/tools.service';
|
||||
templateUrl: './manage-account.component.html',
|
||||
styleUrls: ['./manage-account.component.scss']
|
||||
})
|
||||
export class ManageAccountComponent implements OnInit, OnDestroy {
|
||||
export class ManageAccountComponent implements OnInit, OnDestroy {
|
||||
faAt = faAt;
|
||||
faBell = faBell;
|
||||
faEnvelope = faEnvelope;
|
||||
@ -21,10 +25,12 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
|
||||
faStar = faStar;
|
||||
faUserPlus = faUserPlus;
|
||||
|
||||
subPanel = 'account';
|
||||
subPanel: 'account' | 'notifications' | 'mentions' | 'dm' | 'favorites' = 'account';
|
||||
hasNotifications = false;
|
||||
hasMentions = false;
|
||||
|
||||
userAccount: Account;
|
||||
|
||||
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
@ -33,6 +39,7 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
|
||||
set account(acc: AccountWrapper) {
|
||||
this._account = acc;
|
||||
this.checkNotifications();
|
||||
this.getUserUrl(acc.info);
|
||||
}
|
||||
get account(): AccountWrapper {
|
||||
return this._account;
|
||||
@ -42,31 +49,63 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
|
||||
private _account: AccountWrapper;
|
||||
|
||||
constructor(
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mastodonService: MastodonWrapperService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly userNotificationService: UserNotificationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.userNotificationServiceSub.unsubscribe();
|
||||
}
|
||||
|
||||
private checkNotifications(){
|
||||
if(this.userNotificationServiceSub){
|
||||
private getUserUrl(account: AccountInfo) {
|
||||
this.mastodonService.retrieveAccountDetails(this.account.info)
|
||||
.then((acc: Account) => {
|
||||
this.userAccount = acc;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
});
|
||||
}
|
||||
|
||||
private checkNotifications() {
|
||||
if (this.userNotificationServiceSub) {
|
||||
this.userNotificationServiceSub.unsubscribe();
|
||||
}
|
||||
|
||||
this.userNotificationServiceSub = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
|
||||
const userNotification = userNotifications.find(x => x.account.id === this.account.info.id);
|
||||
if(userNotification){
|
||||
this.hasNotifications = userNotification.hasNewNotifications;
|
||||
this.hasMentions = userNotification.hasNewMentions;
|
||||
if (userNotification) {
|
||||
let settings = this.toolsService.getSettings();
|
||||
let accSettings = this.toolsService.getAccountSettings(this.account.info);
|
||||
|
||||
if (!settings.disableAvatarNotifications && !accSettings.disableAvatarNotifications) {
|
||||
this.hasNotifications = userNotification.hasNewNotifications;
|
||||
this.hasMentions = userNotification.hasNewMentions;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let current = this.userNotificationService.userNotifications.value;
|
||||
const userNotification = current.find(x => x.account.id === this.account.info.id);
|
||||
if (userNotification) {
|
||||
let settings = this.toolsService.getSettings();
|
||||
let accSettings = this.toolsService.getAccountSettings(this.account.info);
|
||||
|
||||
if (!settings.disableAutofocus && !settings.disableAvatarNotifications && !accSettings.disableAvatarNotifications) {
|
||||
if (userNotification.hasNewNotifications) {
|
||||
this.loadSubPanel('notifications');
|
||||
} else if (userNotification.hasNewMentions) {
|
||||
this.loadSubPanel('mentions');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadSubPanel(subpanel: string): boolean {
|
||||
loadSubPanel(subpanel: 'account' | 'notifications' | 'mentions' | 'dm' | 'favorites'): boolean {
|
||||
this.subPanel = subpanel;
|
||||
return false;
|
||||
}
|
||||
@ -75,6 +114,17 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
|
||||
this.browseAccountEvent.next(accountName);
|
||||
}
|
||||
|
||||
browseLocalAccount(): boolean {
|
||||
var accountName = `@${this.account.info.username}@${this.account.info.instance}`;
|
||||
this.browseAccountEvent.next(accountName);
|
||||
return false;
|
||||
}
|
||||
|
||||
openLocalAccount(): boolean {
|
||||
window.open(this.userAccount.url, '_blank');
|
||||
return false;
|
||||
}
|
||||
|
||||
browseHashtag(hashtag: string): void {
|
||||
this.browseHashtagEvent.next(hashtag);
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { AccountWrapper } from '../../../../models/account.models';
|
||||
import { UserNotificationService, UserNotification } from '../../../../services/user-notification.service';
|
||||
import { StatusWrapper } from '../../../../models/common.model';
|
||||
import { Status, Notification } from '../../../../services/models/mastodon.interfaces';
|
||||
import { MastodonService } from '../../../../services/mastodon.service';
|
||||
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
|
||||
import { NotificationService } from '../../../../services/notification.service';
|
||||
import { OpenThreadEvent } from '../../../../services/tools.service';
|
||||
|
||||
@ -21,7 +21,7 @@ export class MentionsComponent implements OnInit, OnDestroy {
|
||||
isLoading = false;
|
||||
isThread = false;
|
||||
hasContentWarnings = false;
|
||||
|
||||
|
||||
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
@ -32,9 +32,9 @@ export class MentionsComponent implements OnInit, OnDestroy {
|
||||
this.loadMentions();
|
||||
}
|
||||
get account(): AccountWrapper {
|
||||
return this._account;
|
||||
return this._account;
|
||||
}
|
||||
|
||||
|
||||
@ViewChild('statusstream') public statustream: ElementRef;
|
||||
|
||||
private maxReached = false;
|
||||
@ -45,41 +45,48 @@ export class MentionsComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly userNotificationService: UserNotificationService,
|
||||
private readonly mastodonService: MastodonService) {
|
||||
|
||||
}
|
||||
private readonly mastodonService: MastodonWrapperService) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if(this.userNotificationServiceSub){
|
||||
if (this.userNotificationServiceSub) {
|
||||
this.userNotificationServiceSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
private loadMentions(){
|
||||
if(this.userNotificationServiceSub){
|
||||
private loadMentions() {
|
||||
if (this.userNotificationServiceSub) {
|
||||
this.userNotificationServiceSub.unsubscribe();
|
||||
}
|
||||
|
||||
this.statuses.length = 0;
|
||||
this.userNotificationService.markMentionsAsRead(this.account.info);
|
||||
this.userNotificationService.markMentionsAsRead(this.account.info);
|
||||
|
||||
this.userNotificationServiceSub = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
|
||||
this.statuses.length = 0; //TODO: don't reset, only add the new ones
|
||||
const userNotification = userNotifications.find(x => x.account.id === this.account.info.id);
|
||||
if(userNotification && userNotification.mentions){
|
||||
userNotification.mentions.forEach((mention: Status) => {
|
||||
const statusWrapper = new StatusWrapper(mention, this.account.info);
|
||||
this.statuses.push(statusWrapper);
|
||||
});
|
||||
}
|
||||
this.lastId = userNotification.lastId;
|
||||
this.userNotificationService.markMentionsAsRead(this.account.info);
|
||||
this.processNewMentions(userNotifications);
|
||||
if(this.statuses.length < 20) this.scrolledToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
private processNewMentions(userNotifications: UserNotification[]) {
|
||||
const userNotification = userNotifications.find(x => x.account.id === this.account.info.id);
|
||||
if (userNotification && userNotification.mentions) {
|
||||
let orderedMentions = [...userNotification.mentions.map(x => x.status)].reverse();
|
||||
for (let m of orderedMentions) {
|
||||
if (!this.statuses.find(x => x.status.id === m.id)) {
|
||||
const statusWrapper = new StatusWrapper(m, this.account.info);
|
||||
this.statuses.unshift(statusWrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.lastId = userNotification.lastMentionsId;
|
||||
this.userNotificationService.markMentionsAsRead(this.account.info);
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
var element = this.statustream.nativeElement as HTMLElement;
|
||||
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
|
||||
@ -94,7 +101,7 @@ export class MentionsComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
this.mastodonService.getNotifications(this.account.info, ['follow', 'favourite', 'reblog'], this.lastId)
|
||||
this.mastodonService.getNotifications(this.account.info, ['follow', 'favourite', 'reblog', 'poll'], this.lastId)
|
||||
.then((result: Notification[]) => {
|
||||
|
||||
const statuses = result.map(x => x.status);
|
||||
@ -102,7 +109,7 @@ export class MentionsComponent implements OnInit, OnDestroy {
|
||||
this.maxReached = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (const s of statuses) {
|
||||
const wrapper = new StatusWrapper(s, this.account.info);
|
||||
this.statuses.push(wrapper);
|
||||
@ -111,13 +118,13 @@ export class MentionsComponent implements OnInit, OnDestroy {
|
||||
this.lastId = result[result.length - 1].id;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
browseAccount(accountName: string): void {
|
||||
this.browseAccountEvent.next(accountName);
|
||||
}
|
||||
|
@ -0,0 +1,21 @@
|
||||
<div class="list-account">
|
||||
<div class="list-account__action">
|
||||
<a href class="list-account__action--button list-account__action--button--add" title="add account to list"
|
||||
*ngIf="!accountWrapper.isInList" (click)="add()">
|
||||
<fa-icon [icon]="faPlus"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a href class="list-account__action--button list-account__action--button--remove" title="remove account from list"
|
||||
*ngIf="accountWrapper.isInList" (click)="remove()">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<div class="list-account__account">
|
||||
<img src="{{ accountWrapper.account.avatar }}" alt="" class="list-account__account--avatar">
|
||||
|
||||
<span class="list-account__account--display-name" title="{{ accountWrapper.account.display_name }}" innerHTML="{{ accountWrapper.account | accountEmoji }}"></span>
|
||||
<span class="list-account__account--acct" title="{{ accountWrapper.account.acct }}">{{ accountWrapper.account.acct }}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
@ -0,0 +1,82 @@
|
||||
@import "variables";
|
||||
|
||||
.list-account {
|
||||
transition: all .2s;
|
||||
$actin-width: 50px;
|
||||
|
||||
&__action {
|
||||
float: right;
|
||||
width: $actin-width;
|
||||
position: relative;
|
||||
|
||||
&--button {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 12px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
// outline: 1px solid greenyellow;
|
||||
|
||||
&:hover{
|
||||
color: darken(#fff, 20);
|
||||
}
|
||||
|
||||
// $add-color: rgb(167, 220, 255);
|
||||
// $remove-color: rgb(255, 154, 196);
|
||||
// &--add {
|
||||
// color: $add-color;
|
||||
// &:hover{
|
||||
// color: darken($add-color, 20);
|
||||
// }
|
||||
// }
|
||||
|
||||
&--remove {
|
||||
color: $font-link-primary;
|
||||
&:hover{
|
||||
color: lighten($font-link-primary, 40);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__account {
|
||||
width: calc(100% - #{ $actin-width });
|
||||
position: relative;
|
||||
height: 50px;
|
||||
|
||||
|
||||
&--avatar {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
$max-name-width: 190px;
|
||||
&--display-name {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 60px;
|
||||
color: white;
|
||||
|
||||
max-width: $max-name-width;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&--acct {
|
||||
position: absolute;
|
||||
top: 26px;
|
||||
left: 60px;
|
||||
color: $status-secondary-color;
|
||||
|
||||
max-width: $max-name-width;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ListAccountComponent } from './list-account.component';
|
||||
|
||||
xdescribe('ListAccountComponent', () => {
|
||||
let component: ListAccountComponent;
|
||||
let fixture: ComponentFixture<ListAccountComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ListAccountComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ListAccountComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,37 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { faTimes, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { Account } from "../../../../../../services/models/mastodon.interfaces";
|
||||
import { AccountListWrapper } from '../list-editor.component';
|
||||
import { isUndefined } from 'util';
|
||||
|
||||
@Component({
|
||||
selector: 'app-list-account',
|
||||
templateUrl: './list-account.component.html',
|
||||
styleUrls: ['./list-account.component.scss']
|
||||
})
|
||||
export class ListAccountComponent implements OnInit {
|
||||
faTimes = faTimes;
|
||||
faPlus = faPlus;
|
||||
|
||||
@Input() accountWrapper: AccountListWrapper;
|
||||
@Output() addEvent = new EventEmitter<AccountListWrapper>();
|
||||
@Output() removeEvent = new EventEmitter<AccountListWrapper>();
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
add(): boolean {
|
||||
if(this.accountWrapper && this.accountWrapper.isLoading) return;
|
||||
this.addEvent.emit(this.accountWrapper);
|
||||
return false;
|
||||
}
|
||||
|
||||
remove(): boolean {
|
||||
if(this.accountWrapper && this.accountWrapper.isLoading) return;
|
||||
this.removeEvent.emit(this.accountWrapper);
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<div class="list-editor">
|
||||
<a href class="list-editor__close-search" title="close search" *ngIf="searchOpen" (click)="closeSearch()">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
<input class="list-editor__search" placeholder="search account" [(ngModel)]="searchPattern"
|
||||
(keyup.enter)="search()" />
|
||||
<div class="list-editor__list flexcroll" *ngIf="!searchOpen">
|
||||
<app-list-account class="list-editor__account" *ngFor="let account of accountsInList"
|
||||
[accountWrapper]="account" (addEvent)="addEvent($event)" (removeEvent)="removeEvent($event)">
|
||||
</app-list-account>
|
||||
</div>
|
||||
<div class="list-editor__list flexcroll" *ngIf="searchOpen">
|
||||
<app-list-account class="list-editor__account" *ngFor="let account of accountsSearch"
|
||||
[accountWrapper]="account" (addEvent)="addEvent($event)" (removeEvent)="removeEvent($event)">
|
||||
</app-list-account>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,46 @@
|
||||
@import "variables";
|
||||
@import "commons";
|
||||
|
||||
.list-editor {
|
||||
background-color: $color-primary;
|
||||
min-height: 20px;
|
||||
margin-top: 1px;
|
||||
position: relative;
|
||||
|
||||
&__search {
|
||||
color: #fff;
|
||||
background-color: darken($color-primary, 4);
|
||||
border: 2px solid $color-primary;
|
||||
width: calc(100%);
|
||||
padding: 3px 5px;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__close-search {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 5px;
|
||||
color: white;
|
||||
padding: 5px;
|
||||
&:hover {
|
||||
color: rgb(160, 160, 160);
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid #000;
|
||||
}
|
||||
|
||||
&__account {
|
||||
display: block;
|
||||
&:not(:last-child){
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ListEditorComponent } from './list-editor.component';
|
||||
|
||||
xdescribe('ListEditorComponent', () => {
|
||||
let component: ListEditorComponent;
|
||||
let fixture: ComponentFixture<ListEditorComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ListEditorComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ListEditorComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,148 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { StreamWrapper } from '../my-account.component';
|
||||
import { MastodonWrapperService } from '../../../../../services/mastodon-wrapper.service';
|
||||
import { AccountWrapper } from '../../../../../models/account.models';
|
||||
import { NotificationService } from '../../../../../services/notification.service';
|
||||
import { Account, Relationship, Instance } from "../../../../../services/models/mastodon.interfaces";
|
||||
import { of } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-list-editor',
|
||||
templateUrl: './list-editor.component.html',
|
||||
styleUrls: ['./list-editor.component.scss']
|
||||
})
|
||||
export class ListEditorComponent implements OnInit {
|
||||
faTimes = faTimes;
|
||||
|
||||
@Input() list: StreamWrapper;
|
||||
@Input() account: AccountWrapper;
|
||||
|
||||
accountsInList: AccountListWrapper[] = [];
|
||||
accountsSearch: AccountListWrapper[] = [];
|
||||
searchPattern: string;
|
||||
searchOpen: boolean;
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly mastodonService: MastodonWrapperService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.accountsInList.length = 0;
|
||||
this.mastodonService.getListAccounts(this.account.info, this.list.listId)
|
||||
.then((accounts: Account[]) => {
|
||||
this.accountsInList.length = 0;
|
||||
for (const account of accounts) {
|
||||
this.accountsInList.push(new AccountListWrapper(account, true));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
});
|
||||
}
|
||||
|
||||
search() {
|
||||
if (this.searchPattern === '')
|
||||
return this.closeSearch();
|
||||
|
||||
this.searchOpen = true;
|
||||
this.accountsSearch.length = 0;
|
||||
this.mastodonService.searchAccount(this.account.info, this.searchPattern, 15)
|
||||
.then((accounts: Account[]) => {
|
||||
this.accountsSearch.length = 0;
|
||||
for (const account of accounts) {
|
||||
const isInList = this.accountsInList.filter(x => x.account.id === account.id).length > 0;
|
||||
this.accountsSearch.push(new AccountListWrapper(account, isInList));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
});
|
||||
}
|
||||
|
||||
closeSearch(): boolean {
|
||||
this.searchPattern = null;
|
||||
this.searchOpen = false;
|
||||
this.accountsSearch.length = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
addEvent(accountWrapper: AccountListWrapper) {
|
||||
console.log(accountWrapper);
|
||||
|
||||
accountWrapper.isLoading = true;
|
||||
this.mastodonService.getInstance(this.account.info.instance)
|
||||
.then((instance: Instance) => {
|
||||
console.log(instance);
|
||||
if (instance.version.toLowerCase().includes('pleroma')) {
|
||||
return Promise.resolve(true);
|
||||
} else {
|
||||
return this.followAccount(accountWrapper);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
return this.mastodonService.addAccountToList(this.account.info, this.list.listId, accountWrapper.account.id);
|
||||
})
|
||||
.then(() => {
|
||||
accountWrapper.isInList = true;
|
||||
this.accountsInList.push(accountWrapper);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
})
|
||||
.then(() => {
|
||||
accountWrapper.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
private followAccount(accountWrapper: AccountListWrapper): Promise<boolean> {
|
||||
return this.mastodonService.getRelationships(this.account.info, [accountWrapper.account])
|
||||
.then((relationships: Relationship[]) => {
|
||||
var relationship = relationships.filter(x => x.id === accountWrapper.account.id)[0];
|
||||
return relationship;
|
||||
})
|
||||
.then((relationship: Relationship) => {
|
||||
if (relationship.following) {
|
||||
return Promise.resolve(true);
|
||||
} else {
|
||||
return this.mastodonService.follow(this.account.info, accountWrapper.account)
|
||||
.then((relationship: Relationship) => {
|
||||
return new Promise<boolean>((resolve) => setTimeout(resolve, 1500));
|
||||
// return Promise.resolve(relationship.following);
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// private delay(t, v) {
|
||||
// return new Promise(function(resolve) {
|
||||
// setTimeout(resolve.bind(null, v), t)
|
||||
// });
|
||||
// }
|
||||
|
||||
removeEvent(accountWrapper: AccountListWrapper) {
|
||||
console.log(accountWrapper);
|
||||
|
||||
accountWrapper.isLoading = true;
|
||||
this.mastodonService.removeAccountFromList(this.account.info, this.list.listId, accountWrapper.account.id)
|
||||
.then(() => {
|
||||
accountWrapper.isInList = false;
|
||||
this.accountsInList = this.accountsInList.filter(x => x.account.id !== accountWrapper.account.id);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
})
|
||||
.then(() => {
|
||||
accountWrapper.isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountListWrapper {
|
||||
constructor(public account: Account, public isInList: boolean) {
|
||||
}
|
||||
|
||||
isProcessing: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
@ -1,9 +1,60 @@
|
||||
<div class="my-account__body flexcroll">
|
||||
<h4 class="my-account__label">add column:</h4>
|
||||
<a class="my-account__link my-account__blue" href *ngFor="let stream of availableStreams"
|
||||
(click)="addStream(stream)" title="{{ stream.isAdded ? '' : 'add timeline'}}" [class.my-account__link--disabled]="stream.isAdded">
|
||||
{{ stream.name }} <fa-icon class="my-account__link--icon" *ngIf="stream.isAdded" [icon]="faCheckSquare"></fa-icon>
|
||||
<h4 class="my-account__label">add timeline:</h4>
|
||||
<a class="my-account__link my-account__link--margin-bottom my-account__blue" href
|
||||
*ngFor="let stream of availableStreams" (click)="addStream(stream)"
|
||||
title="{{ stream.isAdded ? '' : 'add timeline'}}" [class.my-account__link--disabled]="stream.isAdded">
|
||||
{{ stream.name }} <fa-icon class="my-account__link--icon" *ngIf="stream.isAdded" [icon]="faCheckSquare">
|
||||
</fa-icon>
|
||||
</a>
|
||||
|
||||
<h4 class="my-account__label my-account__margin-top">manage list:</h4>
|
||||
<div class="my-account__link--margin-bottom" *ngFor="let list of availableLists">
|
||||
<a href class="my-account__list--button" title="delete list" (click)="openCloseDeleteConfirmation(list, true)"
|
||||
*ngIf="!list.confirmDeletion">
|
||||
<fa-icon class="my-account__link--icon" [icon]="faTrash"></fa-icon>
|
||||
</a>
|
||||
<a href class="my-account__list--button" title="edit list" (click)="editList(list)"
|
||||
*ngIf="!list.confirmDeletion">
|
||||
<fa-icon class="my-account__link--icon" [icon]="faPenAlt"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a href class="my-account__list--button" title="cancel" (click)="openCloseDeleteConfirmation(list, false)"
|
||||
*ngIf="list.confirmDeletion">
|
||||
<fa-icon class="my-account__link--icon" [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
<a href class="my-account__list--button my-account__red" title="delete list" (click)="deleteList(list)"
|
||||
*ngIf="list.confirmDeletion">
|
||||
<fa-icon class="my-account__link--icon" [icon]="faCheck"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a class="my-account__link my-account__list my-account__blue" href (click)="addStream(list)"
|
||||
title="{{ list.isAdded ? '' : 'add list'}}" [class.my-account__link--disabled]="list.isAdded">
|
||||
{{ list.name }} <fa-icon class="my-account__link--icon" *ngIf="list.isAdded" [icon]="faCheckSquare">
|
||||
</fa-icon>
|
||||
</a>
|
||||
|
||||
<app-list-editor *ngIf="list.editList" [list]="list" [account]="account"></app-list-editor>
|
||||
</div>
|
||||
<a href class="my-account__list--button" title="create list" (click)="createList()">
|
||||
<fa-icon class="my-account__link--icon" [icon]="faPlus"></fa-icon>
|
||||
</a>
|
||||
<input class="my-account__list--new-list-title" placeholder="new list title" [(ngModel)]="listTitle"
|
||||
(keyup.enter)="createList()" [disabled]="creationLoading" />
|
||||
|
||||
<h4 class="my-account__label my-account__margin-top">advanced settings:</h4>
|
||||
<div class="advanced-settings">
|
||||
<input class="advanced-settings__checkbox" [(ngModel)]="avatarNotificationDisabled"
|
||||
(change)="onDisableAvatarNotificationChanged()" type="checkbox" name="avatarNotification" value="avatarNotification" id="avatarNotification"> <label class="noselect advanced-settings__label" for="avatarNotification">disable avatar notifications</label><br>
|
||||
<input class="advanced-settings__checkbox" [(ngModel)]="customStatusLengthEnabled"
|
||||
(change)="onCustomLengthEnabledChanged()" type="checkbox" name="customCharLength" value="customCharLength"
|
||||
id="customCharLength"> <label class="noselect advanced-settings__label" for="customCharLength">custom char
|
||||
limit</label><br>
|
||||
<p *ngIf="customStatusLengthEnabled" class="advanced-settings__text">use this only if your instance doesn't
|
||||
support status custom length detection (i.e. not a Pleroma or glitch-soc instance)</p>
|
||||
<input *ngIf="customStatusLengthEnabled" [(ngModel)]="customStatusLength"
|
||||
class="themed-form advanced-settings__input" type="number" (keyup)="customStatusLengthChanged($event)" />
|
||||
</div>
|
||||
|
||||
<h4 class="my-account__label my-account__margin-top">remove account from sengi:</h4>
|
||||
<a class="my-account__link my-account__red" href (click)="removeAccount()">
|
||||
Delete
|
||||
|
@ -1,58 +1,131 @@
|
||||
@import "variables";
|
||||
@import "commons";
|
||||
|
||||
.my-account {
|
||||
.my-account {
|
||||
transition: all .2s;
|
||||
|
||||
&__body {
|
||||
overflow: auto;
|
||||
height: calc(100%);
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-right: 10px;
|
||||
font-size: $small-font-size;
|
||||
padding-bottom: 20px;
|
||||
outline: 1px dotted greenyellow;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: $small-font-size;
|
||||
margin-top: 10px;
|
||||
margin-left: 5px;
|
||||
color: $font-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
&__blue {
|
||||
background-color: $color-primary;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($color-primary, 15);
|
||||
}
|
||||
}
|
||||
|
||||
&__red {
|
||||
$red-button-color: rgb(65, 3, 3);
|
||||
background-color: $red-button-color;
|
||||
background-color: $red-button-color !important;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($red-button-color, 15);
|
||||
background-color: lighten($red-button-color, 15) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__link {
|
||||
text-decoration: none;
|
||||
display: block; // width: calc(100% - 20px);
|
||||
width: 100%; // height: 30px;
|
||||
padding: 5px 10px; // border: solid 1px black;
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
&--icon {
|
||||
float: right;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
cursor: default;
|
||||
background-color: darken($color-primary, 4);
|
||||
|
||||
&:hover {
|
||||
background-color: darken($color-primary, 4);
|
||||
}
|
||||
}
|
||||
|
||||
&--margin-bottom {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
$list-width: 60px;
|
||||
width: calc(100% - #{$list-width} - 2px);
|
||||
|
||||
&--button {
|
||||
margin-left: 1px;
|
||||
width: calc(#{$list-width}/2);
|
||||
float: right;
|
||||
padding: 5px 10px;
|
||||
background-color: $color-primary;
|
||||
color: #fff;
|
||||
color: $font-color-secondary;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: lighten($color-primary, 15);
|
||||
}
|
||||
}
|
||||
|
||||
&--new-list-title {
|
||||
color: #fff;
|
||||
background-color: darken($color-primary, 4);
|
||||
border: 2px solid $color-primary;
|
||||
width: calc(100% - #{$list-width}/2 - 1px);
|
||||
padding: 3px 5px;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__margin-top {
|
||||
margin-top: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.advanced-settings {
|
||||
position: relative;
|
||||
|
||||
&__checkbox{
|
||||
position: relative;
|
||||
top:3px;
|
||||
left: 5px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
|
||||
|
||||
}
|
||||
|
||||
&__text {
|
||||
display: block;
|
||||
margin: 0 6px 9px 6px;
|
||||
color: rgb(140, 152, 173);
|
||||
}
|
||||
|
||||
&__input {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
@ -2,12 +2,16 @@ import { Component, OnInit, OnDestroy, Input } from '@angular/core';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { Store, Select } from '@ngxs/store';
|
||||
import { faCheckSquare } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faPenAlt, faTrash, faPlus, faCheck, faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { NotificationService } from '../../../../services/notification.service';
|
||||
import { StreamElement, StreamTypeEnum, AddStream, RemoveAllStreams } from '../../../../states/streams.state';
|
||||
import { StreamElement, StreamTypeEnum, AddStream, RemoveAllStreams, RemoveStream } from '../../../../states/streams.state';
|
||||
import { AccountWrapper } from '../../../../models/account.models';
|
||||
import { RemoveAccount } from '../../../../states/accounts.state';
|
||||
import { NavigationService } from '../../../../services/navigation.service';
|
||||
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
|
||||
import { ToolsService } from '../../../../services/tools.service';
|
||||
import { AccountSettings } from '../../../../states/settings.state';
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-account',
|
||||
@ -15,16 +19,27 @@ import { NavigationService } from '../../../../services/navigation.service';
|
||||
styleUrls: ['./my-account.component.scss']
|
||||
})
|
||||
export class MyAccountComponent implements OnInit, OnDestroy {
|
||||
|
||||
faPlus = faPlus;
|
||||
faTrash = faTrash;
|
||||
faPenAlt = faPenAlt;
|
||||
faCheckSquare = faCheckSquare;
|
||||
faCheck = faCheck;
|
||||
faTimes = faTimes;
|
||||
|
||||
avatarNotificationDisabled: boolean;
|
||||
customStatusLengthEnabled: boolean;
|
||||
customStatusLength: number;
|
||||
private accountSettings: AccountSettings;
|
||||
|
||||
availableStreams: StreamWrapper[] = [];
|
||||
availableLists: StreamWrapper[] = [];
|
||||
|
||||
private _account: AccountWrapper;
|
||||
@Input('account')
|
||||
set account(acc: AccountWrapper) {
|
||||
this._account = acc;
|
||||
this.loadStreams(acc);
|
||||
this.loadAccountSettings();
|
||||
}
|
||||
get account(): AccountWrapper {
|
||||
return this._account;
|
||||
@ -35,13 +50,15 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(
|
||||
private readonly store: Store,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly navigationService: NavigationService,
|
||||
private notificationService: NotificationService) { }
|
||||
private readonly mastodonService: MastodonWrapperService,
|
||||
private readonly notificationService: NotificationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.streamChangedSub = this.streamElements$.subscribe((streams: StreamElement[]) => {
|
||||
this.loadStreams(this.account);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@ -50,12 +67,33 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private loadAccountSettings(){
|
||||
this.accountSettings = this.toolsService.getAccountSettings(this.account.info);
|
||||
|
||||
this.customStatusLengthEnabled = this.accountSettings.customStatusCharLengthEnabled;
|
||||
this.customStatusLength = this.accountSettings.customStatusCharLength;
|
||||
this.avatarNotificationDisabled = this.accountSettings.disableAvatarNotifications;
|
||||
}
|
||||
|
||||
onCustomLengthEnabledChanged(): boolean {
|
||||
this.accountSettings.customStatusCharLengthEnabled = this.customStatusLengthEnabled;
|
||||
this.toolsService.saveAccountSettings(this.accountSettings);
|
||||
return false;
|
||||
}
|
||||
|
||||
customStatusLengthChanged(event): boolean{
|
||||
this.accountSettings.customStatusCharLength = this.customStatusLength;
|
||||
this.toolsService.saveAccountSettings(this.accountSettings);
|
||||
return false;
|
||||
}
|
||||
|
||||
private loadStreams(account: AccountWrapper){
|
||||
const instance = account.info.instance;
|
||||
this.availableStreams.length = 0;
|
||||
this.availableStreams.push(new StreamWrapper(new StreamElement(StreamTypeEnum.global, 'Federated Timeline', account.info.id, null, null, instance)));
|
||||
this.availableStreams.push(new StreamWrapper(new StreamElement(StreamTypeEnum.local, 'Local Timeline', account.info.id, null, null, instance)));
|
||||
this.availableStreams.push(new StreamWrapper(new StreamElement(StreamTypeEnum.personnal, 'Home', account.info.id, null, null, instance)));
|
||||
this.availableStreams.push(new StreamWrapper(new StreamElement(StreamTypeEnum.global, 'Federated Timeline', account.info.id, null, null, null, instance)));
|
||||
this.availableStreams.push(new StreamWrapper(new StreamElement(StreamTypeEnum.local, 'Local Timeline', account.info.id, null, null, null, instance)));
|
||||
this.availableStreams.push(new StreamWrapper(new StreamElement(StreamTypeEnum.personnal, 'Home', account.info.id, null, null, null, instance)));
|
||||
this.availableStreams.push(new StreamWrapper(new StreamElement(StreamTypeEnum.activity, 'Notifications', account.info.id, null, null, null, instance)));
|
||||
|
||||
const loadedStreams = <StreamElement[]>this.store.snapshot().streamsstatemodel.streams;
|
||||
this.availableStreams.forEach(s => {
|
||||
@ -65,6 +103,24 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
||||
s.isAdded = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.availableLists.length = 0;
|
||||
this.mastodonService.getLists(account.info)
|
||||
.then((streams: StreamElement[]) => {
|
||||
this.availableLists.length = 0;
|
||||
for (let stream of streams) {
|
||||
let wrappedStream = new StreamWrapper(stream);
|
||||
if(loadedStreams.find(x => x.id == stream.id)){
|
||||
wrappedStream.isAdded = true;
|
||||
} else {
|
||||
wrappedStream.isAdded = false;
|
||||
}
|
||||
this.availableLists.push(wrappedStream);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
});
|
||||
}
|
||||
|
||||
addStream(stream: StreamWrapper): boolean {
|
||||
@ -72,7 +128,6 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
||||
this.store.dispatch([new AddStream(stream)]).toPromise()
|
||||
.then(() => {
|
||||
stream.isAdded = true;
|
||||
//this.notificationService.notify(`stream added`, false);
|
||||
});
|
||||
}
|
||||
return false;
|
||||
@ -84,12 +139,68 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
||||
this.navigationService.closePanel();
|
||||
return false;
|
||||
}
|
||||
|
||||
listTitle: string;
|
||||
creationLoading: boolean;
|
||||
createList(): boolean {
|
||||
if(this.creationLoading || !this.listTitle || this.listTitle == '') return false;
|
||||
|
||||
this.creationLoading = true;
|
||||
this.mastodonService.createList(this.account.info, this.listTitle)
|
||||
.then((stream: StreamElement) => {
|
||||
this.listTitle = null;
|
||||
let wrappedStream = new StreamWrapper(stream);
|
||||
this.availableLists.push(wrappedStream);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
})
|
||||
.then(() => {
|
||||
this.creationLoading = false;
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
editList(list: StreamWrapper): boolean {
|
||||
list.editList = !list.editList;
|
||||
return false;
|
||||
}
|
||||
|
||||
openCloseDeleteConfirmation(list: StreamWrapper, state: boolean): boolean {
|
||||
list.confirmDeletion = state;
|
||||
return false;
|
||||
}
|
||||
|
||||
deleteList(list: StreamWrapper): boolean {
|
||||
this.mastodonService.deleteList(this.account.info, list.listId)
|
||||
.then(() => {
|
||||
const isAdded = this.availableLists.find(x => x.id === list.id).isAdded;
|
||||
if(isAdded){
|
||||
this.store.dispatch([new RemoveStream(list.id)]);
|
||||
}
|
||||
this.availableLists = this.availableLists.filter(x => x.id !== list.id);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onDisableAvatarNotificationChanged() {
|
||||
let settings = this.toolsService.getAccountSettings(this.account.info);
|
||||
settings.disableAvatarNotifications = this.avatarNotificationDisabled;
|
||||
this.toolsService.saveAccountSettings(settings);
|
||||
}
|
||||
}
|
||||
|
||||
class StreamWrapper extends StreamElement {
|
||||
export class StreamWrapper extends StreamElement {
|
||||
constructor(stream: StreamElement) {
|
||||
super(stream.type, stream.name, stream.accountId, stream.tag, stream.list, stream.instance);
|
||||
super(stream.type, stream.name, stream.accountId, stream.tag, stream.list, stream.listId, stream.instance);
|
||||
}
|
||||
|
||||
isAdded: boolean;
|
||||
confirmDeletion: boolean;
|
||||
editList: boolean;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<div class="notification">
|
||||
<div *ngIf="notification.type === 'follow'">
|
||||
<div class="stream__notification--icon" title="{{notification.account.acct}}">
|
||||
<fa-icon class="followed" [icon]="faUserPlus"></fa-icon>
|
||||
</div>
|
||||
<div class="stream__notification--label">
|
||||
<a href class="stream__link" title="{{notification.account.acct}}"
|
||||
(click)="openAccount(notification.account)" (auxclick)="openUrl(notification.account.url)"
|
||||
innerHTML="{{ notification.account | accountEmoji }}"></a>
|
||||
followed
|
||||
you!
|
||||
</div>
|
||||
|
||||
<a href (click)="openAccount(notification.account)" (auxclick)="openUrl(notification.account.url)"
|
||||
class="follow-account" title="{{notification.account.acct}}">
|
||||
<img class="follow-account__avatar" src="{{ notification.account.avatar }}" />
|
||||
<span class="follow-account__display-name" innerHTML="{{ notification.account | accountEmoji }}"></span>
|
||||
<span class="follow-account__acct">@{{ notification.account.acct }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<app-status *ngIf="notification.status && notification.type !== 'mention'" class="stream__status" [statusWrapper]="notification.status"
|
||||
[notificationAccount]="notification.account" [notificationType]="notification.type"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-status>
|
||||
|
||||
<app-status *ngIf="notification.status && notification.type === 'mention'" class="stream__status" [statusWrapper]="notification.status"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-status>
|
||||
</div>
|
@ -0,0 +1,85 @@
|
||||
@import "variables";
|
||||
@import "commons";
|
||||
@import "mixins";
|
||||
|
||||
.notification {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stream {
|
||||
position: relative;
|
||||
&__notification {
|
||||
position: relative;
|
||||
|
||||
&--icon {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 43px;
|
||||
text-align: center;
|
||||
width: 20px;
|
||||
// outline: 1px dotted greenyellow;
|
||||
}
|
||||
|
||||
&--label {
|
||||
margin: 0 10px 0 $avatar-column-space;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
|
||||
&:not(:last-child) {
|
||||
border: solid #06070b;
|
||||
border-width: 0 0 1px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__link {
|
||||
color: $status-links-color;
|
||||
}
|
||||
|
||||
&__status {
|
||||
display: block;
|
||||
// opacity: 0.65;
|
||||
}
|
||||
}
|
||||
|
||||
.followed {
|
||||
color: $boost-color;
|
||||
}
|
||||
|
||||
.follow-account {
|
||||
padding: 5px;
|
||||
height: 60px;
|
||||
width: calc(100%);
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
|
||||
&__avatar {
|
||||
float: left;
|
||||
margin: 0 0 0 10px;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
$acccount-info-left: 70px;
|
||||
&__display-name {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: $acccount-info-left;
|
||||
color: whitesmoke;
|
||||
}
|
||||
|
||||
&:hover &__display-name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&__acct {
|
||||
position: absolute;
|
||||
top: 27px;
|
||||
left: $acccount-info-left;
|
||||
font-size: 13px;
|
||||
color: $status-links-color;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NotificationComponent } from './notification.component';
|
||||
|
||||
xdescribe('NotificationComponent', () => {
|
||||
let component: NotificationComponent;
|
||||
let fixture: ComponentFixture<NotificationComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ NotificationComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(NotificationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,49 @@
|
||||
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
|
||||
import { faUserPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { NotificationWrapper } from '../notifications.component';
|
||||
import { OpenThreadEvent, ToolsService } from '../../../../../services/tools.service';
|
||||
import { Account } from '../../../../../services/models/mastodon.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-notification',
|
||||
templateUrl: './notification.component.html',
|
||||
styleUrls: ['./notification.component.scss']
|
||||
})
|
||||
export class NotificationComponent implements OnInit {
|
||||
faUserPlus = faUserPlus;
|
||||
|
||||
@Input() notification: NotificationWrapper;
|
||||
|
||||
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
|
||||
constructor(private readonly toolsService: ToolsService) { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
browseAccount(accountName: string): void {
|
||||
this.browseAccountEvent.next(accountName);
|
||||
}
|
||||
|
||||
browseHashtag(hashtag: string): void {
|
||||
this.browseHashtagEvent.next(hashtag);
|
||||
}
|
||||
|
||||
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
}
|
||||
|
||||
openAccount(account: Account): boolean {
|
||||
let accountName = this.toolsService.getAccountFullHandle(account);
|
||||
this.browseAccountEvent.next(accountName);
|
||||
return false;
|
||||
}
|
||||
|
||||
openUrl(url: string): boolean {
|
||||
window.open(url, '_blank');
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,43 +1,6 @@
|
||||
<div class="stream flexcroll" #statusstream (scroll)="onScroll()">
|
||||
<div class="stream__notification" *ngFor="let notification of notifications">
|
||||
<!-- <div *ngIf="notification.type === 'favourite'">
|
||||
<div class="stream__notification--icon">
|
||||
<fa-icon class="favorite" [icon]="faStar"></fa-icon>
|
||||
</div>
|
||||
<div class="stream__notification--label">
|
||||
<a href class="stream__link">{{ notification.account.username }}</a> favorited your status
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="notification.type === 'reblog'">
|
||||
<div class="stream__notification--icon">
|
||||
<fa-icon class="boost" [icon]="faRetweet"></fa-icon>
|
||||
</div>
|
||||
<div class="stream__notification--label">
|
||||
<a href class="stream__link">{{ notification.account.username }}</a> boosted your status
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div *ngIf="notification.type === 'follow'">
|
||||
<div class="stream__notification--icon">
|
||||
<fa-icon class="followed" [icon]="faUserPlus"></fa-icon>
|
||||
</div>
|
||||
<div class="stream__notification--label">
|
||||
<a href class="stream__link" (click)="openAccount(notification.account)" innerHTML="{{ notification.account | accountEmoji }}"></a> followed
|
||||
you!
|
||||
</div>
|
||||
|
||||
<a href (click)="openAccount(notification.account)" class="follow-account" title="{{notification.account.acct}}">
|
||||
<img class="follow-account__avatar" src="{{ notification.account.avatar }}" />
|
||||
<span class="follow-account__display-name" innerHTML="{{ notification.account | accountEmoji }}"></span>
|
||||
<span class="follow-account__acct">@{{ notification.account.acct }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<app-status *ngIf="notification.status" class="stream__status" [statusWrapper]="notification.status"
|
||||
[notificationAccount]="notification.account" [notificationType]="notification.type"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-status>
|
||||
<app-notification [notification]="notification" (browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)" (browseThreadEvent)="browseThread($event)"></app-notification>
|
||||
</div>
|
||||
|
||||
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
|
||||
|
@ -15,76 +15,77 @@
|
||||
|
||||
&__notification {
|
||||
position: relative;
|
||||
|
||||
&--icon {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 43px;
|
||||
text-align: center;
|
||||
width: 20px;
|
||||
// outline: 1px dotted greenyellow;
|
||||
}
|
||||
|
||||
&--label {
|
||||
margin: 0 10px 0 $avatar-column-space;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
|
||||
&:not(:last-child) {
|
||||
border: solid #06070b;
|
||||
border-width: 0 0 1px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__link {
|
||||
color: $status-links-color;
|
||||
}
|
||||
// &--icon {
|
||||
// position: absolute;
|
||||
// top: 5px;
|
||||
// left: 43px;
|
||||
// text-align: center;
|
||||
// width: 20px;
|
||||
// // outline: 1px dotted greenyellow;
|
||||
// }
|
||||
|
||||
&__status {
|
||||
display: block;
|
||||
// opacity: 0.65;
|
||||
}
|
||||
// &--label {
|
||||
// margin: 0 10px 0 $avatar-column-space;
|
||||
// padding-top: 5px;
|
||||
// }
|
||||
|
||||
|
||||
// &:not(:last-child) {
|
||||
// border: solid #06070b;
|
||||
// border-width: 0 0 1px 0;
|
||||
// }
|
||||
// }
|
||||
|
||||
// &__link {
|
||||
// color: $status-links-color;
|
||||
// }
|
||||
|
||||
// &__status {
|
||||
// display: block;
|
||||
// // opacity: 0.65;
|
||||
// }
|
||||
}
|
||||
|
||||
.followed {
|
||||
color: $boost-color;
|
||||
}
|
||||
// .followed {
|
||||
// color: $boost-color;
|
||||
// }
|
||||
|
||||
.follow-account {
|
||||
padding: 5px;
|
||||
height: 60px;
|
||||
width: calc(100%);
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
// .follow-account {
|
||||
// padding: 5px;
|
||||
// height: 60px;
|
||||
// width: calc(100%);
|
||||
// overflow: hidden;
|
||||
// display: block;
|
||||
// position: relative;
|
||||
// text-decoration: none;
|
||||
|
||||
&__avatar {
|
||||
float: left;
|
||||
margin: 0 0 0 10px;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
// &__avatar {
|
||||
// float: left;
|
||||
// margin: 0 0 0 10px;
|
||||
// width: 45px;
|
||||
// height: 45px;
|
||||
// border-radius: 2px;
|
||||
// }
|
||||
|
||||
$acccount-info-left: 70px;
|
||||
&__display-name {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: $acccount-info-left;
|
||||
color: whitesmoke;
|
||||
}
|
||||
// $acccount-info-left: 70px;
|
||||
// &__display-name {
|
||||
// position: absolute;
|
||||
// top: 7px;
|
||||
// left: $acccount-info-left;
|
||||
// color: whitesmoke;
|
||||
// }
|
||||
|
||||
&:hover &__display-name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
// &:hover &__display-name {
|
||||
// text-decoration: underline;
|
||||
// }
|
||||
|
||||
&__acct {
|
||||
position: absolute;
|
||||
top: 27px;
|
||||
left: $acccount-info-left;
|
||||
font-size: 13px;
|
||||
color: $status-links-color;
|
||||
}
|
||||
}
|
||||
// &__acct {
|
||||
// position: absolute;
|
||||
// top: 27px;
|
||||
// left: $acccount-info-left;
|
||||
// font-size: 13px;
|
||||
// color: $status-links-color;
|
||||
// }
|
||||
// }
|
@ -1,16 +1,14 @@
|
||||
import { Component, OnInit, Input, ViewChild, ElementRef, OnDestroy, Output, EventEmitter } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { faStar, faUserPlus, faRetweet } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faStar as faStar2 } from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
import { AccountWrapper } from '../../../../models/account.models';
|
||||
import { UserNotificationService, UserNotification } from '../../../../services/user-notification.service';
|
||||
import { StatusWrapper } from '../../../../models/common.model';
|
||||
import { Notification, Account } from '../../../../services/models/mastodon.interfaces';
|
||||
import { MastodonService } from '../../../../services/mastodon.service';
|
||||
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
|
||||
import { NotificationService } from '../../../../services/notification.service';
|
||||
import { AccountInfo } from '../../../../states/accounts.state';
|
||||
import { OpenThreadEvent } from '../../../../services/tools.service';
|
||||
import { OpenThreadEvent, ToolsService } from '../../../../services/tools.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-notifications',
|
||||
@ -18,10 +16,6 @@ import { OpenThreadEvent } from '../../../../services/tools.service';
|
||||
styleUrls: ['./notifications.component.scss']
|
||||
})
|
||||
export class NotificationsComponent implements OnInit, OnDestroy {
|
||||
faUserPlus = faUserPlus;
|
||||
// faStar = faStar;
|
||||
// faRetweet = faRetweet;
|
||||
|
||||
notifications: NotificationWrapper[] = [];
|
||||
isLoading = false;
|
||||
|
||||
@ -45,10 +39,10 @@ export class NotificationsComponent implements OnInit, OnDestroy {
|
||||
private userNotificationServiceSub: Subscription;
|
||||
private lastId: string;
|
||||
|
||||
constructor(
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly userNotificationService: UserNotificationService,
|
||||
private readonly mastodonService: MastodonService) { }
|
||||
private readonly mastodonService: MastodonWrapperService) { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
@ -68,19 +62,26 @@ export class NotificationsComponent implements OnInit, OnDestroy {
|
||||
this.userNotificationService.markNotificationAsRead(this.account.info);
|
||||
|
||||
this.userNotificationServiceSub = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
|
||||
this.notifications.length = 0; //TODO: don't reset, only add the new ones
|
||||
const userNotification = userNotifications.find(x => x.account.id === this.account.info.id);
|
||||
if(userNotification && userNotification.notifications){
|
||||
userNotification.notifications.forEach((notification: Notification) => {
|
||||
const notificationWrapper = new NotificationWrapper(notification, this.account.info);
|
||||
this.notifications.push(notificationWrapper);
|
||||
});
|
||||
}
|
||||
this.lastId = userNotification.lastId;
|
||||
this.userNotificationService.markNotificationAsRead(this.account.info);
|
||||
this.processNewNotifications(userNotifications);
|
||||
if(this.notifications.length < 20) this.scrolledToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
private processNewNotifications(userNotifications: UserNotification[]) {
|
||||
const userNotification = userNotifications.find(x => x.account.id === this.account.info.id);
|
||||
if (userNotification && userNotification.notifications) {
|
||||
let orderedNotifications = [...userNotification.notifications].reverse();
|
||||
for (let n of orderedNotifications) {
|
||||
const notificationWrapper = new NotificationWrapper(n, this.account.info);
|
||||
if (!this.notifications.find(x => x.wrapperId === notificationWrapper.wrapperId)) {
|
||||
this.notifications.unshift(notificationWrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.lastId = userNotification.lastNotificationsId;
|
||||
this.userNotificationService.markNotificationAsRead(this.account.info);
|
||||
}
|
||||
|
||||
|
||||
onScroll() {
|
||||
var element = this.statustream.nativeElement as HTMLElement;
|
||||
@ -111,22 +112,13 @@ export class NotificationsComponent implements OnInit, OnDestroy {
|
||||
this.lastId = notifications[notifications.length - 1].id;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
openAccount(account: Account): boolean {
|
||||
let accountName = account.acct;
|
||||
if (!accountName.includes('@'))
|
||||
accountName += `@${account.url.replace('https://', '').split('/')[0]}`;
|
||||
|
||||
this.browseAccountEvent.next(accountName);
|
||||
return false;
|
||||
}
|
||||
|
||||
browseAccount(accountName: string): void {
|
||||
this.browseAccountEvent.next(accountName);
|
||||
}
|
||||
@ -140,20 +132,25 @@ export class NotificationsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationWrapper {
|
||||
export class NotificationWrapper {
|
||||
constructor(notification: Notification, provider: AccountInfo) {
|
||||
this.type = notification.type;
|
||||
switch(this.type){
|
||||
case 'mention':
|
||||
case 'reblog':
|
||||
case 'favourite':
|
||||
case 'poll':
|
||||
this.status= new StatusWrapper(notification.status, provider);
|
||||
break;
|
||||
}
|
||||
this.account = notification.account;
|
||||
this.account = notification.account;
|
||||
this.wrapperId = `${this.type}-${notification.id}`;
|
||||
this.notification = notification;
|
||||
}
|
||||
|
||||
notification: Notification;
|
||||
wrapperId: string;
|
||||
account: Account;
|
||||
status: StatusWrapper;
|
||||
type: 'mention' | 'reblog' | 'favourite' | 'follow';
|
||||
type: 'mention' | 'reblog' | 'favourite' | 'follow' | 'poll';
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
<div class="scheduled-status">
|
||||
<div class="scheduled-status__date">
|
||||
{{ status.scheduled_at | date: 'MMM d, y, h:mm a' }}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="scheduled-status__avatar">
|
||||
<img class="scheduled-status__avatar--image" src="{{avatar}}" />
|
||||
</div>
|
||||
|
||||
<div class="scheduled-status__content">
|
||||
<div class="scheduled-status__content--text scheduled-status__content--spoiler"
|
||||
*ngIf="status.params.spoiler_text" title="spoiler">
|
||||
{{ status.params.spoiler_text }}
|
||||
</div>
|
||||
<div class="scheduled-status__content--text" title="status text">
|
||||
{{ status.params.text }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scheduled-status__edition">
|
||||
<div *ngIf="!deleting && !rescheduling">
|
||||
<button class="scheduled-status__edition--button" (click)="delete()" title="delete status">Delete</button>
|
||||
<button class="scheduled-status__edition--button" (click)="reschedule()"
|
||||
title="reschedule status">Reschedule</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="deleting">
|
||||
<button class="scheduled-status__edition--button" (click)="cancelDeletion()" title="cancel">CANCEL</button>
|
||||
<button class="scheduled-status__edition--button scheduled-status__edition--delete"
|
||||
(click)="confirmDeletion()" title="confirm status deletion">DO IT</button>
|
||||
|
||||
<div class="scheduled-status__edition--label">
|
||||
Delete the status?
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="rescheduling">
|
||||
<app-status-scheduler [scheduledDate]="status.scheduled_at" class="scheduled-status__edition--scheduler" #statusScheduler></app-status-scheduler>
|
||||
|
||||
<button class="scheduled-status__edition--button" (click)="cancelReschedule()" title="cancel">CANCEL</button>
|
||||
<button class="scheduled-status__edition--button"
|
||||
(click)="confirmReschedule()" title="confirm rescheduling">REPLAN</button>
|
||||
|
||||
</div>
|
||||
|
||||
<app-waiting-animation class="waiting-icon" *ngIf="isLoading"></app-waiting-animation>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,74 @@
|
||||
@import "commons";
|
||||
@import "variables";
|
||||
@import "mixins";
|
||||
|
||||
$avatar-size: 40px;
|
||||
|
||||
.scheduled-status {
|
||||
margin: 0 5px;
|
||||
padding: 5px 5px 5px 5px;
|
||||
|
||||
&__date {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
float: left;
|
||||
&--image {
|
||||
width: $avatar-size;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
&--text {
|
||||
width: calc(100% - #{$avatar-size});
|
||||
margin-left: $avatar-size;
|
||||
padding: 0 5px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
&--spoiler {
|
||||
color: gray;
|
||||
min-height: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&__edition {
|
||||
@include clearfix;
|
||||
|
||||
&--button {
|
||||
@include clearButton;
|
||||
transition: all .2s;
|
||||
float: right;
|
||||
margin: 5px 0 5px 5px;
|
||||
padding: 5px 10px;
|
||||
|
||||
background-color: #273047;
|
||||
background-color: $scheduler-background;
|
||||
|
||||
&:hover {
|
||||
background-color: #3b4769;
|
||||
background-color: #3d4b7c;
|
||||
}
|
||||
}
|
||||
|
||||
&--delete {
|
||||
background-color: rgb(95, 5, 5);
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(163, 4, 4);
|
||||
}
|
||||
}
|
||||
|
||||
&--label{
|
||||
height: 30px;
|
||||
float: right;
|
||||
padding: 9px 5px 0 5px;
|
||||
}
|
||||
|
||||
&--scheduler {
|
||||
display: block;
|
||||
margin: 5px 0 0 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ScheduledStatusComponent } from './scheduled-status.component';
|
||||
|
||||
xdescribe('ScheduledStatusComponent', () => {
|
||||
let component: ScheduledStatusComponent;
|
||||
let fixture: ComponentFixture<ScheduledStatusComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ScheduledStatusComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ScheduledStatusComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,97 @@
|
||||
import { Component, OnInit, Input, ViewChild, Output, EventEmitter } from '@angular/core';
|
||||
|
||||
import { AccountInfo } from '../../../../states/accounts.state';
|
||||
import { ScheduledStatus } from '../../../../services/models/mastodon.interfaces';
|
||||
import { ToolsService } from '../../../../services/tools.service';
|
||||
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
|
||||
import { NotificationService } from '../../../../services/notification.service';
|
||||
import { ScheduledStatusService } from '../../../../services/scheduled-status.service';
|
||||
import { StatusSchedulerComponent } from '../../../../components/create-status/status-scheduler/status-scheduler.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-scheduled-status',
|
||||
templateUrl: './scheduled-status.component.html',
|
||||
styleUrls: ['./scheduled-status.component.scss']
|
||||
})
|
||||
export class ScheduledStatusComponent implements OnInit {
|
||||
deleting: boolean = false;
|
||||
rescheduling: boolean = false;
|
||||
isLoading: boolean = false;
|
||||
|
||||
@ViewChild(StatusSchedulerComponent) statusScheduler: StatusSchedulerComponent;
|
||||
|
||||
avatar: string;
|
||||
@Input() account: AccountInfo;
|
||||
@Input() status: ScheduledStatus;
|
||||
@Output() rescheduledEvent = new EventEmitter();
|
||||
|
||||
constructor(
|
||||
private readonly scheduledStatusService: ScheduledStatusService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly mastodonService: MastodonWrapperService,
|
||||
private readonly toolsService: ToolsService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.toolsService.getAvatar(this.account)
|
||||
.then((avatar: string) => {
|
||||
this.avatar = avatar;
|
||||
});
|
||||
}
|
||||
|
||||
delete(): boolean {
|
||||
this.deleting = !this.deleting;
|
||||
return false;
|
||||
}
|
||||
|
||||
cancelDeletion(): boolean {
|
||||
this.deleting = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
confirmDeletion(): boolean {
|
||||
if(this.isLoading) return false;
|
||||
this.isLoading = true;
|
||||
|
||||
this.mastodonService.deleteScheduledStatus(this.account, this.status.id)
|
||||
.then(() => {
|
||||
this.scheduledStatusService.removeStatus(this.account, this.status.id);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, this.account);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
reschedule(): boolean {
|
||||
this.rescheduling = !this.rescheduling;
|
||||
return false;
|
||||
}
|
||||
|
||||
cancelReschedule(): boolean {
|
||||
this.rescheduling = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
confirmReschedule(): boolean {
|
||||
if(this.isLoading) return false;
|
||||
this.isLoading = true;
|
||||
|
||||
let scheduledTime = this.statusScheduler.getScheduledDate();
|
||||
this.mastodonService.changeScheduledStatus(this.account, this.status.id, scheduledTime)
|
||||
.then(() => {
|
||||
this.status.scheduled_at = scheduledTime;
|
||||
this.rescheduling = false;
|
||||
this.rescheduledEvent.next();
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, this.account);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<div class="panel">
|
||||
<h3 class="panel__title">Scheduled Statuses</h3>
|
||||
|
||||
<div class="scheduled-statuses-display flexcroll">
|
||||
<div *ngFor="let n of scheduledStatuses" class="scheduled-status">
|
||||
<app-scheduled-status
|
||||
(rescheduledEvent)="statusRescheduled()"
|
||||
[account]="n.account"
|
||||
[status]="n.status"></app-scheduled-status>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,22 @@
|
||||
@import "variables";
|
||||
@import "panel";
|
||||
@import "commons";
|
||||
|
||||
.panel {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.scheduled-statuses-display {
|
||||
overflow: auto;
|
||||
height: calc(100% - #{$stream-header-height});
|
||||
}
|
||||
|
||||
.scheduled-status {
|
||||
display: block;
|
||||
// outline: 1px dotted salmon;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid #141824;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ScheduledStatusesComponent } from './scheduled-statuses.component';
|
||||
|
||||
xdescribe('ScheduledStatusesComponent', () => {
|
||||
let component: ScheduledStatusesComponent;
|
||||
let fixture: ComponentFixture<ScheduledStatusesComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ScheduledStatusesComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ScheduledStatusesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,54 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { ScheduledStatusService, ScheduledStatusNotification } from '../../../services/scheduled-status.service';
|
||||
import { ScheduledStatus } from '../../../services/models/mastodon.interfaces';
|
||||
import { AccountInfo } from '../../../states/accounts.state';
|
||||
|
||||
@Component({
|
||||
selector: 'app-scheduled-statuses',
|
||||
templateUrl: './scheduled-statuses.component.html',
|
||||
styleUrls: ['./scheduled-statuses.component.scss']
|
||||
})
|
||||
export class ScheduledStatusesComponent implements OnInit, OnDestroy {
|
||||
private statusSub: Subscription;
|
||||
scheduledStatuses: ScheduledStatusWrapper[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly scheduledStatusService: ScheduledStatusService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.statusSub = this.scheduledStatusService.scheduledStatuses.subscribe((value: ScheduledStatusNotification[]) => {
|
||||
this.scheduledStatuses.length = 0;
|
||||
|
||||
value.forEach(notification => {
|
||||
notification.statuses.forEach(status => {
|
||||
let wrapper = new ScheduledStatusWrapper(notification.account, status);
|
||||
this.scheduledStatuses.push(wrapper);
|
||||
});
|
||||
});
|
||||
|
||||
this.sortStatuses();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.statusSub) this.statusSub.unsubscribe();
|
||||
}
|
||||
|
||||
private sortStatuses() {
|
||||
this.scheduledStatuses.sort((x, y) => new Date(x.status.scheduled_at).getTime() - new Date(y.status.scheduled_at).getTime());
|
||||
}
|
||||
|
||||
statusRescheduled() {
|
||||
this.sortStatuses();
|
||||
}
|
||||
}
|
||||
|
||||
class ScheduledStatusWrapper {
|
||||
constructor(
|
||||
public readonly account: AccountInfo,
|
||||
public status: ScheduledStatus) {
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
<div *ngIf="accounts.length > 0" class="search-results">
|
||||
<h3 class="search-results__title">Accounts</h3>
|
||||
<a href *ngFor="let account of accounts" class="account" title="open account"
|
||||
(click)="browseAccount(account.acct)">
|
||||
(click)="browseAccount(account)">
|
||||
<img src="{{account.avatar}}" class="account__avatar" />
|
||||
<div class="account__name">{{ account.username }}</div>
|
||||
<div class="account__fullhandle">@{{ account.acct }}</div>
|
||||
|