fix merge conflicts

This commit is contained in:
Nicolas Constant 2019-12-21 20:43:12 +01:00
commit 3fc0b5f34f
4086 changed files with 10660 additions and 4814 deletions

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
.git
.gitignore
.travis.yml
appveyor.yml
.vscode
node_modules
dist

5
CREDITS.md Normal file
View 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
View 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"]

View File

@ -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)

View File

@ -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": [

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View 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>

View 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;
}

View File

@ -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; }

View File

@ -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);

View File

@ -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;

View File

@ -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;
}
}
}

View 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%;
}
}
}

View File

@ -5,4 +5,5 @@
@import "./layout/grid";
@import "./layout/header";
@import "./layout/section";
@import "./layout/footer";

BIN
docs/images/labels.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

BIN
docs/images/timelines.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View File

@ -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>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

14
lighttpd.conf Normal file
View 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"

View File

@ -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
View File

@ -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",

View File

@ -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"
]
}
}
}

View File

@ -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;

View File

@ -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 { }

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
});
});

View File

@ -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
}

View File

@ -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>

View File

@ -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;
// }

View File

@ -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/');
});
});

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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();
});
});

View File

@ -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) {
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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();
});
});

View File

@ -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;
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
});
});

View File

@ -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;
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
});
});

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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> {

View File

@ -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>

View File

@ -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;
}

View File

@ -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() {

View File

@ -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>

View File

@ -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);
// }
}

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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>

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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();
});
});

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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();
});
});

View File

@ -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;
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
});
});

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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;
// }
// }

View File

@ -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';
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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();
});
});

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
});
});

View File

@ -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) {
}
}

View File

@ -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>

Some files were not shown because too many files have changed in this diff Show More