Compare commits
291 Commits
Author | SHA1 | Date |
---|---|---|
Nicolas Constant | 822ef21985 | |
Nicolas Constant | a154028a53 | |
Nicolas Constant | 6a8d85f40c | |
Nicolas Constant | 04153543a9 | |
Nicolas Constant | 92ec089eab | |
Nicolas Constant | 12ce0a3a4a | |
Nicolas Constant | 7a6eb9c3d2 | |
Nicolas Constant | 63b7c6fdf1 | |
Nicolas Constant | bd75317417 | |
Nicolas Constant | 74eed7e8ba | |
Nicolas Constant | ebce6282c5 | |
Nicolas Constant | 702e4daa44 | |
Nicolas Constant | d2221d539c | |
Nicolas Constant | c4de387f86 | |
Nicolas Constant | c0f84ddc11 | |
Nicolas Constant | 1830212a91 | |
Nicolas Constant | 46adf207bb | |
Nicolas Constant | 909b190b33 | |
Nicolas Constant | cfc4d5f915 | |
Nicolas Constant | 0f58252c61 | |
Nicolas Constant | 0d2ac6b569 | |
Nicolas Constant | e62987b11a | |
Nicolas Constant | 8cee7289eb | |
Nicolas Constant | 0305cc6ac7 | |
Nicolas Constant | f215d027f9 | |
Nicolas Constant | 335cbf4956 | |
Nicolas Constant | b41c31b4ac | |
Nicolas Constant | 41faa36087 | |
Nicolas Constant | 024042959e | |
Nicolas Constant | f4c87df078 | |
Nicolas Constant | d24441343a | |
Nicolas Constant | 8c9685045e | |
Nicolas Constant | a0cb240446 | |
Nicolas Constant | 2def5725f5 | |
Nicolas Constant | 450a0088d5 | |
Nicolas Constant | d7f988ecb9 | |
Nicolas Constant | 8703df27d5 | |
Nicolas Constant | 10fa412173 | |
Nicolas Constant | 0b93ed7307 | |
Nicolas Constant | c3cd6fe79e | |
Nicolas Constant | 14287b476c | |
Nicolas Constant | 2b106ba546 | |
Nicolas Constant | 4a2b408c1b | |
Nicolas Constant | 92a3ac6ae3 | |
Nicolas Constant | ec0bed4606 | |
Nicolas Constant | 62d4140d63 | |
Nicolas Constant | 4a34063dc8 | |
Nicolas Constant | 9cd709f44c | |
Nicolas Constant | 64ceb3e095 | |
Nicolas Constant | cb58be5bd8 | |
Nicolas Constant | 7a8dfd0c6b | |
Nicolas Constant | 89c5c33de2 | |
Nicolas Constant | 590627bc58 | |
Nicolas Constant | 7013d9174c | |
Nicolas Constant | ba08c0d0b2 | |
Nicolas Constant | 26a01b5c30 | |
Nicolas Constant | 73ac37a8f4 | |
Nicolas Constant | 38b052f06b | |
Nicolas Constant | 4511363408 | |
Nicolas Constant | c0f03570a0 | |
Nicolas Constant | 3d5c91a12b | |
Nicolas Constant | 27b22338c9 | |
Nicolas Constant | 191bd936aa | |
Nicolas Constant | 1c42f54db0 | |
Nicolas Constant | e8dbe214f4 | |
Nicolas Constant | 8cd4d30ac8 | |
Nicolas Constant | 30f678af04 | |
Nicolas Constant | 16bbf9aa2f | |
Nicolas Constant | 74af61ad78 | |
Nicolas Constant | 449506092a | |
Nicolas Constant | b37a2a2f0c | |
Nicolas Constant | 32efac5aa4 | |
Nicolas Constant | 91b2f4a0f0 | |
Nicolas Constant | 0d7821cd01 | |
Nicolas Constant | 18d6b8d96c | |
Nicolas Constant | 503cb6c9d4 | |
Nicolas Constant | 98e7d54c33 | |
Nicolas Constant | dbb5d8e71b | |
Nicolas Constant | a77b46755f | |
Nicolas Constant | a5f9feb10b | |
Nicolas Constant | 95c4d8b249 | |
Nicolas Constant | 128dfd7fe5 | |
Nicolas Constant | 2dc77dd39a | |
Nicolas Constant | f71e175375 | |
HamzaFarooqArif | a1a56e49f5 | |
HamzaFarooqArif | 5dc98c677e | |
Nicolas Constant | b00c52ff83 | |
Nicolas Constant | f46d7d433a | |
Nicolas Constant | 06dbdef1dc | |
Nicolas Constant | 5e865ed9a4 | |
Nicolas Constant | 253ea52590 | |
Nicolas Constant | 84a4b8c00a | |
Nicolas Constant | 982a670352 | |
Nicolas Constant | 314c736cf4 | |
Nicolas Constant | 9999944d1f | |
Nicolas Constant | 2bcac4622a | |
Nicolas Constant | 5d6672f379 | |
Nicolas Constant | eac8c6120a | |
Nicolas Constant | 22cad9e22d | |
Nicolas Constant | 232a86566c | |
Nicolas Constant | 2cb443dd4d | |
Nicolas Constant | cb342ce9b5 | |
Nicolas Constant | 8c9fe07109 | |
Nicolas Constant | 00134a7407 | |
Nicolas Constant | db6b37eef3 | |
Nicolas Constant | e14852e087 | |
Nicolas Constant | 6001a26f02 | |
Nicolas Constant | 48677e8e6c | |
Nicolas Constant | 1ca603f211 | |
Nicolas Constant | d60bf804b8 | |
Nicolas Constant | 8bd71afc55 | |
Nicolas Constant | ed8c935285 | |
Nicolas Constant | b1cd975422 | |
Nicolas Constant | c5e3f4abac | |
Nicolas Constant | 4599d64c60 | |
Nicolas Constant | 522c1c0133 | |
Nicolas Constant | b6ea1d8d43 | |
Nicolas Constant | 55a855d046 | |
Nicolas Constant | 410007dc25 | |
Nicolas Constant | 54d4b300f4 | |
Nicolas Constant | f4ba3a168f | |
Nicolas Constant | f2e1478cfa | |
Nicolas Constant | ce71965b5c | |
Nicolas Constant | 65c147bc6f | |
Nicolas Constant | 57f863e2a1 | |
Nicolas Constant | 0ce8be99bd | |
Nicolas Constant | f5de97993b | |
Nicolas Constant | 0777c23124 | |
Nicolas Constant | 70c9e2564b | |
Nicolas Constant | 54772d8487 | |
Nicolas Constant | 30c81ae143 | |
Nicolas Constant | 9cc2324fd2 | |
Nicolas Constant | c912f12db5 | |
Rob Petti | 513bb1e684 | |
Rob Petti | ec233754dd | |
Rob Petti | 39187c82fb | |
Nicolas Constant | 78f0f3ab5f | |
Nicolas Constant | 39abd6a175 | |
Nicolas Constant | 644b0d0b86 | |
Rob Petti | 83f52391ae | |
Nicolas Constant | 33a61f7347 | |
Nicolas Constant | 0409431105 | |
Nicolas Constant | 42fb269c24 | |
Nicolas Constant | c3a5306e56 | |
Nicolas Constant | 76b911351c | |
Nicolas Constant | 7cb0887749 | |
Nicolas Constant | 5c52c9c4f2 | |
Nicolas Constant | 59c3b19271 | |
Nicolas Constant | 2f84471a3e | |
Nicolas Constant | 640028ca08 | |
Nicolas Constant | 3f01c70bc9 | |
Nicolas Constant | 70bef7b98e | |
Nicolas Constant | 0956b623ce | |
Nicolas Constant | 6554a359b5 | |
Nicolas Constant | 1ebbece7ab | |
Nicolas Constant | a85e24b77f | |
Nicolas Constant | c2812fae43 | |
Nicolas Constant | 9426bc9e38 | |
Nicolas Constant | 06d142c4a5 | |
Nicolas Constant | eb74e34cb0 | |
Nicolas Constant | 50dc938295 | |
Nicolas Constant | f1596bf04f | |
Nicolas Constant | 67e69c64a4 | |
Nicolas Constant | ba5fead320 | |
Nicolas Constant | 9bba8a3352 | |
Nicolas Constant | 14a9aade0b | |
Nicolas Constant | 93847df4d8 | |
Nicolas Constant | aa705d7c5b | |
Nicolas Constant | 21ad2cffb6 | |
Nicolas Constant | f8cea22693 | |
Nicolas Constant | 03bcc95d65 | |
Nicolas Constant | 8bbc58d9c8 | |
Nicolas Constant | 28065912b2 | |
Nicolas Constant | cd96324442 | |
Nicolas Constant | 30cb395bda | |
Nicolas Constant | 8d13822000 | |
Nicolas Constant | d82da3d180 | |
Nicolas Constant | 7653398642 | |
Nicolas Constant | fb4c99870e | |
Nicolas Constant | 47a8cdc096 | |
Nicolas Constant | 3724b0b4c2 | |
Nicolas Constant | 030ce2e568 | |
Nicolas Constant | 1d9e3c5130 | |
Nicolas Constant | 5eef9506fe | |
Nicolas Constant | f152a3dc6f | |
Nicolas Constant | d30f5a8261 | |
Nicolas Constant | 4babd219b4 | |
Nicolas Constant | 9ae1711093 | |
Nicolas Constant | 1b7853ec4d | |
Nicolas Constant | 6144d12740 | |
Nicolas Constant | 6696ca4274 | |
Nicolas Constant | 70032f55f1 | |
Nicolas Constant | 63175d1e60 | |
Nicolas Constant | 269b8b87cd | |
Nicolas Constant | b2a198c6d9 | |
Nicolas Constant | af026a444d | |
Nicolas Constant | 31527d3914 | |
Nicolas Constant | d74b030688 | |
Nicolas Constant | 438867e49a | |
Nicolas Constant | 82e86039b4 | |
Nicolas Constant | 8b849a6650 | |
Nicolas Constant | 8c76056747 | |
Nicolas Constant | 4083b1017a | |
Nicolas Constant | 5b7c2de8ba | |
Nicolas Constant | 3c4fc074ef | |
Nicolas Constant | e772613193 | |
Nicolas Constant | 8566966463 | |
Nicolas Constant | 5f4e822b64 | |
Nicolas Constant | 7d42737c27 | |
Nicolas Constant | e5ce5fb14e | |
Nicolas Constant | 50758f1170 | |
Nicolas Constant | d720dc06a9 | |
Nicolas Constant | 7841f72890 | |
Nicolas Constant | a303f16afe | |
Nicolas Constant | 3be94a842d | |
Nicolas Constant | d67ef4aaf2 | |
Nicolas Constant | 0c361d57fc | |
Nicolas Constant | e46c878e36 | |
Nicolas Constant | 49c776a67c | |
Nicolas Constant | 24c188aa80 | |
Nicolas Constant | 180f218eb0 | |
Nicolas Constant | c2f9c17189 | |
Nicolas Constant | 45d735835b | |
Nicolas Constant | 0d1a2e59d4 | |
Nicolas Constant | c950744a48 | |
Nicolas Constant | 23abf0e0b7 | |
Nicolas Constant | 3dbc5c57e1 | |
Nicolas Constant | f13e30ebaf | |
Nicolas Constant | 45620de391 | |
Nicolas Constant | 711e351543 | |
Miosame | 91f75f2b0f | |
Miosame | 0afa7a0998 | |
Nicolas Constant | 912d9e31b5 | |
Nicolas Constant | a0952de788 | |
Nicolas Constant | 628b9c6733 | |
Nicolas Constant | bcc4549b9a | |
Nicolas Constant | 54bac5e0ee | |
Nicolas Constant | e223c1d032 | |
Nicolas Constant | 0d851560b6 | |
Nicolas Constant | b09c2a0b81 | |
Nicolas Constant | 386058eceb | |
Nicolas Constant | 7b94b950d2 | |
Nicolas Constant | 496b2b7dd2 | |
Nicolas Constant | e600f096cd | |
Nicolas Constant | b4eb092181 | |
Nicolas Constant | f5f1c2e8f8 | |
Nicolas Constant | d46fa0ffca | |
Nicolas Constant | 63c2385644 | |
Nicolas Constant | b02979430c | |
Nicolas Constant | 39af84785f | |
Nicolas Constant | 5992ac7001 | |
Nicolas Constant | 5ae8d668df | |
Nicolas Constant | a37b814c16 | |
Nicolas Constant | 60e99a1c30 | |
Nicolas Constant | a8f940eea7 | |
Nicolas Constant | 9adb9c5c44 | |
Nicolas Constant | 8f03d7f19e | |
Nicolas Constant | 4e453903f2 | |
Nicolas Constant | d8398a4af6 | |
Nicolas Constant | efbc3d3cdc | |
Nicolas Constant | ac3acfb4fa | |
Nicolas Constant | 4e9730a4ae | |
Nicolas Constant | 16a92bcc56 | |
Nicolas Constant | 2b0da954a1 | |
Nicolas Constant | 0e23b64b63 | |
Nicolas Constant | 8f719c515b | |
Nicolas Constant | 4df59f0edb | |
Nicolas Constant | 7a0de67f5d | |
Nicolas Constant | 65068f5dc0 | |
Nicolas Constant | 54ed9a8c4a | |
Nicolas Constant | 73f6030a87 | |
Nicolas Constant | 713fa918be | |
Nicolas Constant | a82afc6bd1 | |
Nicolas Constant | fbe5a53f60 | |
Nicolas Constant | 505f0b025a | |
Nicolas Constant | 534a4b11e3 | |
Nicolas Constant | 69c6fbc145 | |
Nicolas Constant | 1f93817a6f | |
Nicolas Constant | 58c1f04609 | |
Nicolas Constant | f3f63f569a | |
Nicolas Constant | a809274756 | |
Nicolas Constant | 8710b0267e | |
Nicolas Constant | 95454e29a0 | |
Nicolas Constant | 1ae9cc282f | |
Nicolas Constant | 031b1d5631 | |
Nicolas Constant | 5ddf555172 | |
Nicolas Constant | 0635397087 | |
Nicolas Constant | 94fe3eff31 | |
Nicolas Constant | 3c93dcb709 | |
Nicolas Constant | 8139f1a601 | |
Nicolas Constant | f7187353bb |
|
@ -0,0 +1 @@
|
|||
patreon: nicolasconstant
|
47
.travis.yml
47
.travis.yml
|
@ -1,47 +0,0 @@
|
|||
sudo: required
|
||||
dist: trusty
|
||||
|
||||
language: c
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
- os: linux
|
||||
env: CC=clang CXX=clang++ npm_config_clang=1
|
||||
compiler: clang
|
||||
|
||||
node_js:
|
||||
- 10.9.0
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
- icnsutils
|
||||
- graphicsmagick
|
||||
- libgnome-keyring-dev
|
||||
- xz-utils
|
||||
- xorriso
|
||||
- xvfb
|
||||
|
||||
install:
|
||||
- nvm install 10.9.0
|
||||
- npm install electron-builder@next
|
||||
- npm install
|
||||
- npm rebuild node-sass
|
||||
- export DISPLAY=':99.0'
|
||||
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||
|
||||
before_script:
|
||||
- export DISPLAY=:99.0
|
||||
- sh -e /etc/init.d/xvfb start &
|
||||
- sleep 3
|
||||
|
||||
script:
|
||||
- npm run travis
|
|
@ -20,5 +20,5 @@ For example:
|
|||
|
||||
|
||||
## Pull Requests
|
||||
Pull Requests are maybe a bit early right now, since the project and code can change a lot, so it's not really adviced to open PR today.
|
||||
I will notify explicitely when I'll be more opened to external contributions.
|
||||
|
||||
Please open first an [issue](https://github.com/NicolasConstant/sengi/issues/new) before working on a new functionality you would like to submit to this repository.
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
# Sengi's Docker documentation
|
||||
|
||||
Here is some more detailed informations for Sengi's Docker users.
|
||||
|
||||
## Deploy Sengi's
|
||||
|
||||
Execute:
|
||||
|
||||
```
|
||||
docker run -d -p 80:80 nicolasconstant/sengi
|
||||
```
|
||||
|
||||
Sengi will then be available on port 80
|
||||
|
||||
## Landing page
|
||||
|
||||
Sengi's docker contains a landing page so that you can open a pop-up easily.<br />
|
||||
It's available in ```https://your-host/start/index.html```
|
||||
|
||||
## Personalize the Privacy Statement
|
||||
|
||||
You can personalize the privacy statement by linking it as follow:
|
||||
|
||||
```
|
||||
docker run -d -p 80:80 -v /Path/privacy.html:/app/assets/docs/privacy.html nicolasconstant/sengi
|
||||
```
|
||||
|
||||
|
|
@ -12,8 +12,8 @@ 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
|
||||
COPY --from=build /build/dist /app
|
||||
COPY --from=build /build/assets/docker_init /app/start
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
|
|
60
README.md
60
README.md
|
@ -7,50 +7,66 @@ Sengi is a **Mastodon** and **Pleroma** desktop focused client. It takes inspira
|
|||
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)
|
||||
* Desktop based interactions (right click, left click, etc)
|
||||
* One column at a time display (leave it on the side of your screen, and keep an eye on it while doing your other stuff)
|
||||
|
||||
It is released as a **browser webapp** and also packaged as an **cross-platform desktop application** (Mac, Windows, and Linux).
|
||||
|
||||
The Electron code isn't hosted here anymore, and you'll find it [here](https://github.com/NicolasConstant/sengi-electron).
|
||||
|
||||
## Official project page
|
||||
|
||||
[Discover Sengi](https://nicolasconstant.github.io/sengi/)
|
||||
|
||||
## State of development
|
||||
|
||||
Sengi already supporting all the basics functionalities, but many minors enhancements are still needed before a 1.0.0 release.
|
||||
The first major stable release has been published (1.0.0), the project is open to external contributions.
|
||||
|
||||
## Screens
|
||||
|
||||
![/docs/images/presentation_small.gif](/docs/images/presentation_small.gif)
|
||||
![https://raw.githubusercontent.com/NicolasConstant/sengi/master/docs/images/presentation_small.gif](https://raw.githubusercontent.com/NicolasConstant/sengi/master/docs/images/presentation_small.gif)
|
||||
|
||||
## Docker
|
||||
|
||||
A docker image is available for auto-hosting your own Sengi webapp!
|
||||
|
||||
```
|
||||
docker run -d -p 80:80 nicolasconstant/sengi
|
||||
```
|
||||
|
||||
Find more informations [here](https://github.com/NicolasConstant/sengi/blob/master/DOCKER.md).
|
||||
|
||||
The docker image also provide a landing page so that you can open a pop-up really easily. <br />
|
||||
It's available in ```https://your-host/start/index.html```
|
||||
|
||||
## Contact
|
||||
|
||||
* [Official Sengi Account](https://mastodon.social/@sengi_app)
|
||||
|
||||
## Contribute
|
||||
|
||||
Please see the [contributing guidelines](https://github.com/NicolasConstant/sengi/blob/master/CONTRIBUTING.md)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the AGPLv3 License - see [LICENSE](https://github.com/NicolasConstant/sengi/blob/master/LICENSE) for details
|
||||
|
||||
## Credits
|
||||
|
||||
See [credits](https://github.com/NicolasConstant/sengi/blob/master/CREDITS.md)
|
||||
|
||||
## Dependencies
|
||||
|
||||
* [Angular 7](https://github.com/angular/angular)
|
||||
* [NGXS](https://github.com/ngxs/store)
|
||||
* [SASS](https://github.com/sass/dart-sass)
|
||||
* [Electron 10](https://github.com/electron/electron)
|
||||
|
||||
## What's a sengi?!
|
||||
|
||||
It's a little [elephant shrew](https://en.wikipedia.org/wiki/Elephant_shrew) from Africa:
|
||||
|
||||
![Rhynchocyon petersi](https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/Rhynchocyon_petersi_from_side.jpg/400px-Rhynchocyon_petersi_from_side.jpg)
|
||||
|
||||
## Contribute
|
||||
|
||||
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)
|
||||
* [NGXS](https://github.com/ngxs/store)
|
||||
* [SASS](https://github.com/sass/dart-sass)
|
||||
* [Electron 4](https://github.com/electron/electron)
|
||||
|
||||
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
os: unstable
|
||||
cache:
|
||||
- node_modules
|
||||
#- node_modules
|
||||
environment:
|
||||
GH_TOKEN:
|
||||
secure: wRRBU0GXTmTBgZBs2PGSaEJWOflynAyvp3Nc/7e9xmciPfkUCQAXcpOn0jIYmzpb
|
||||
secure: eXSiJiDFgLi4vixO5GS93lgrqZ+BzQNy7PKPCQCErHjCQD9mWiEtVQQnhvmUq1FPLUc3fNLmOFQu2nIWA9bnkHg5Yw9WiG2m7QSCPRB+xCnvSY6JbLqpzURZp5x5OLj6
|
||||
matrix:
|
||||
- nodejs_version: 10.9.0
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
- set CI=true
|
||||
- npm install -g npm@latest
|
||||
- npm install -g npm@6.9.0
|
||||
- set PATH=%APPDATA%\npm;%PATH%
|
||||
- npm install
|
||||
matrix:
|
||||
|
@ -45,3 +45,4 @@ deploy:
|
|||
application: dist.zip
|
||||
on:
|
||||
branch: master
|
||||
# APPVEYOR_REPO_TAG: true
|
||||
|
|
|
@ -15,11 +15,11 @@
|
|||
<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;">
|
||||
onClick="window.open('/../'+'?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">
|
||||
<a href="/../" class="button" title="launch sengi">
|
||||
<span class="download-button__web--label">Open Sengi</span>
|
||||
</a><br />
|
||||
</div>
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
|
@ -34,7 +34,7 @@
|
|||
|
||||
<div class="header__download-box--buttons">
|
||||
<p>
|
||||
<h4 class="header__download-box--subtitle">Try it in your browser!</h4>
|
||||
<h4 class="header__download-box--subtitle">Use it in your browser!</h4>
|
||||
<a href="#" class="download-button download-button__web"
|
||||
title="what are you waiting for? click!"
|
||||
onClick="window.open('https://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;"
|
||||
|
@ -43,7 +43,7 @@
|
|||
<br />
|
||||
<br />
|
||||
|
||||
<h4 class="header__download-box--subtitle">Or download the desktop client:</h4>
|
||||
<h4 class="header__download-box--subtitle">Or download the desktop client <span id="electron-version"></span>:</h4>
|
||||
<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>
|
||||
|
@ -75,7 +75,7 @@
|
|||
|
||||
</div>
|
||||
<div>
|
||||
<a class="header__old-releases" href="https://github.com/NicolasConstant/sengi/releases/"
|
||||
<a class="header__old-releases" href="https://github.com/NicolasConstant/sengi-electron/releases/"
|
||||
title="browse previous releases">browse previous releases</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -215,6 +215,12 @@
|
|||
return myJson;
|
||||
}
|
||||
|
||||
const getLastElectronRelease = async () => {
|
||||
const response = await fetch('https://api.github.com/repos/NicolasConstant/sengi-electron/releases/latest');
|
||||
const myJson = await response.json();
|
||||
return myJson;
|
||||
}
|
||||
|
||||
function getOS() {
|
||||
var userAgent = window.navigator.userAgent,
|
||||
platform = window.navigator.platform,
|
||||
|
@ -242,6 +248,9 @@
|
|||
let lastRelease = await getLastRelease();
|
||||
let version = lastRelease.tag_name;
|
||||
|
||||
let lastElectronRelease = await getLastElectronRelease();
|
||||
let electronVersion = lastElectronRelease.tag_name;
|
||||
|
||||
var downloadButtons = document.getElementById('download-buttons');
|
||||
downloadButtons.style.display = 'block';
|
||||
|
||||
|
@ -249,12 +258,15 @@
|
|||
downloadButtonsNojs.style.display = 'none';
|
||||
|
||||
var sengiVersion = document.getElementById('sengi-version');
|
||||
sengiVersion.textContent = `Current version: ${version}`;
|
||||
sengiVersion.textContent = `Current version: v${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`;
|
||||
var htmlElectronVersion = document.getElementById('electron-version');
|
||||
htmlElectronVersion.textContent = `(${electronVersion})`;
|
||||
|
||||
document.getElementById('windows').href = `https://github.com/NicolasConstant/sengi-electron/releases/download/${electronVersion}/Sengi-${electronVersion.replace('v', '')}-win.exe`;
|
||||
document.getElementById('mac').href = `https://github.com/NicolasConstant/sengi-electron/releases/download/${electronVersion}/Sengi-${electronVersion.replace('v', '')}-mac.dmg`;
|
||||
document.getElementById('deb').href = `https://github.com/NicolasConstant/sengi-electron/releases/download/${electronVersion}/Sengi-${electronVersion.replace('v', '')}-linux.deb`;
|
||||
document.getElementById('appimage').href = `https://github.com/NicolasConstant/sengi-electron/releases/download/${electronVersion}/Sengi-${electronVersion.replace('v', '')}-linux.AppImage`;
|
||||
|
||||
|
||||
let userOs = getOS();
|
||||
|
|
|
@ -11,4 +11,4 @@ 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"
|
||||
server.error-handler-404 = "/index.html"
|
198
main-electron.js
198
main-electron.js
|
@ -1,198 +0,0 @@
|
|||
const { app, Menu, BrowserWindow, shell } = require("electron");
|
||||
|
||||
// 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;
|
||||
|
||||
function createWindow() {
|
||||
// Create the browser window.
|
||||
win = new BrowserWindow({
|
||||
width: 377,
|
||||
height: 800,
|
||||
title: "Sengi",
|
||||
backgroundColor: "#131925",
|
||||
useContentSize: true,
|
||||
// webPreferences: {
|
||||
// contextIsolation: true,
|
||||
// nodeIntegration: false,
|
||||
// nodeIntegrationInWorker: false
|
||||
// }
|
||||
});
|
||||
|
||||
win.setAutoHideMenuBar(true);
|
||||
win.setMenuBarVisibility(false);
|
||||
|
||||
const sengiUrl = "https://sengi.nicolas-constant.com";
|
||||
win.loadURL(sengiUrl);
|
||||
|
||||
const template = [
|
||||
{
|
||||
label: "View",
|
||||
submenu: [
|
||||
{
|
||||
label: "Return on Sengi",
|
||||
click() {
|
||||
win.loadURL(sengiUrl);
|
||||
}
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ role: "reload" },
|
||||
{ role: "forcereload" },
|
||||
{ type: "separator" },
|
||||
{ role: "resetzoom" },
|
||||
{ role: "zoomin", accelerator: "CommandOrControl+numadd" },
|
||||
{ role: "zoomout", accelerator: "CommandOrControl+numsub" },
|
||||
{ type: "separator" },
|
||||
{ role: "togglefullscreen" },
|
||||
{ type: "separator" },
|
||||
{ role: "close" },
|
||||
{ role: "quit" }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: "help",
|
||||
submenu: [
|
||||
{ role: "toggledevtools" },
|
||||
{
|
||||
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 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", () => {
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
|
||||
app.commandLine.appendSwitch("force-color-profile", "srgb");
|
||||
|
||||
|
||||
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", () => {
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "sengi",
|
||||
"version": "0.30.0",
|
||||
"version": "1.7.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "main-electron.js",
|
||||
"description": "A multi-account desktop client for Mastodon and Pleroma",
|
||||
"author": {
|
||||
"name": "Nicolas Constant",
|
||||
"name": "Nicolas Constant",
|
||||
"email": "github@nicolas-constant.com"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -21,21 +21,18 @@
|
|||
"test-nowatch": "ng test --watch=false",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e",
|
||||
"electron": "electron .",
|
||||
"electron-prod": "ng build --prod && electron .",
|
||||
"electron-debug": "ng build && electron .",
|
||||
"dist": "npm run build && electron-builder --publish onTagOrDraft",
|
||||
"travis": "electron-builder --publish onTagOrDraft"
|
||||
"dist": "npm run build"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^7.2.7",
|
||||
"@angular/cdk": "^7.2.7",
|
||||
"@angular/animations": "^7.2.16",
|
||||
"@angular/cdk": "^7.3.7",
|
||||
"@angular/common": "^7.2.7",
|
||||
"@angular/compiler": "^7.2.7",
|
||||
"@angular/core": "^7.2.7",
|
||||
"@angular/forms": "^7.2.7",
|
||||
"@angular/http": "^7.2.7",
|
||||
"@angular/material": "^16.2.1",
|
||||
"@angular/platform-browser": "^7.2.7",
|
||||
"@angular/platform-browser-dynamic": "^7.2.7",
|
||||
"@angular/pwa": "^0.12.4",
|
||||
|
@ -47,9 +44,9 @@
|
|||
"@fortawesome/free-brands-svg-icons": "^5.7.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.7.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.7.0",
|
||||
"@ngxs/storage-plugin": "^3.2.0",
|
||||
"@ngxs/store": "^3.2.0",
|
||||
"angular2-hotkeys": "^2.1.5",
|
||||
"@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",
|
||||
|
@ -70,8 +67,6 @@
|
|||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "~8.9.4",
|
||||
"codelyzer": "~4.2.1",
|
||||
"electron": "^8.0.2",
|
||||
"electron-builder": "^20.39.0",
|
||||
"jasmine-core": "~2.99.1",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "~1.7.1",
|
||||
|
|
|
@ -16,6 +16,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="enhancedTutorialActive" class="enhanced-tutorial"
|
||||
[class.enhanced-tutorial__visible]="enhancedTutorialVisible">
|
||||
<app-tutorial-enhanced class="enhanced-tutorial__content" (closeEvent)="closeTutorial()"></app-tutorial-enhanced>
|
||||
</div>
|
||||
|
||||
<app-media-viewer id="media-viewer" *ngIf="openedMediaEvent" [openedMediaEvent]="openedMediaEvent"
|
||||
(closeSubject)="closeMedia()" (dragenter)="dragenter($event)"></app-media-viewer>
|
||||
|
||||
|
|
|
@ -171,4 +171,27 @@ app-streams-selection-footer {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.enhanced-tutorial {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0,0,0,0.9);
|
||||
z-index: 9999999;
|
||||
opacity: 0;
|
||||
transition: all .4s;
|
||||
|
||||
&__visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: block;
|
||||
padding: 25px;
|
||||
width: calc(100%);
|
||||
height: calc(100%);
|
||||
}
|
||||
}
|
|
@ -62,9 +62,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// disable tutorial for future update
|
||||
localStorage.setItem('tutorial', JSON.stringify(true));
|
||||
|
||||
this.paramsSub = this.activatedRoute.queryParams.subscribe(params => {
|
||||
const code = params['code'];
|
||||
if (!code) {
|
||||
|
@ -130,6 +127,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
this.columnEditorSub = this.navigationService.activatedPanelSubject.subscribe((event: OpenLeftPanelEvent) => {
|
||||
if (event.type === LeftPanelType.Closed) {
|
||||
this.floatingColumnActive = false;
|
||||
|
||||
this.checkEnhancedTutorial();
|
||||
} else {
|
||||
this.floatingColumnActive = true;
|
||||
}
|
||||
|
@ -158,6 +157,29 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
enhancedTutorialActive: boolean;
|
||||
enhancedTutorialVisible: boolean;
|
||||
private checkEnhancedTutorial() {
|
||||
let enhancedTutorialDesactivated = JSON.parse(localStorage.getItem('tutorial'));
|
||||
if (!this.floatingColumnActive && !this.tutorialActive && !enhancedTutorialDesactivated) {
|
||||
setTimeout(() => {
|
||||
this.enhancedTutorialActive = true;
|
||||
setTimeout(() => {
|
||||
this.enhancedTutorialVisible = true;
|
||||
}, 100);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
closeTutorial(){
|
||||
localStorage.setItem('tutorial', JSON.stringify(true));
|
||||
|
||||
this.enhancedTutorialVisible = false;
|
||||
setTimeout(() => {
|
||||
this.enhancedTutorialActive = false;
|
||||
}, 400);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.streamSub.unsubscribe();
|
||||
this.columnEditorSub.unsubscribe();
|
||||
|
|
|
@ -5,8 +5,8 @@ import { HttpModule } from "@angular/http";
|
|||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { NgModule, APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
// import { NgxElectronModule } from "ngx-electron";
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
// import { NgxElectronModule } from 'ngx-electron';
|
||||
|
||||
import { NgxsModule } from '@ngxs/store';
|
||||
import { NgxsStoragePluginModule } from '@ngxs/storage-plugin';
|
||||
|
@ -86,7 +86,11 @@ import { AttachementImageComponent } from './components/stream/status/attachemen
|
|||
import { EnsureHttpsPipe } from './pipes/ensure-https.pipe';
|
||||
import { UserFollowsComponent } from './components/stream/user-follows/user-follows.component';
|
||||
import { AccountComponent } from './components/common/account/account.component';
|
||||
|
||||
import { TutorialEnhancedComponent } from './components/tutorial-enhanced/tutorial-enhanced.component';
|
||||
import { NotificationsTutorialComponent } from './components/tutorial-enhanced/notifications-tutorial/notifications-tutorial.component';
|
||||
import { LabelsTutorialComponent } from './components/tutorial-enhanced/labels-tutorial/labels-tutorial.component';
|
||||
import { ThankyouTutorialComponent } from './components/tutorial-enhanced/thankyou-tutorial/thankyou-tutorial.component';
|
||||
import { StatusTranslateComponent } from './components/stream/status/status-translate/status-translate.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: "", component: StreamsMainDisplayComponent },
|
||||
|
@ -152,7 +156,12 @@ const routes: Routes = [
|
|||
AttachementImageComponent,
|
||||
EnsureHttpsPipe,
|
||||
UserFollowsComponent,
|
||||
AccountComponent
|
||||
AccountComponent,
|
||||
TutorialEnhancedComponent,
|
||||
NotificationsTutorialComponent,
|
||||
LabelsTutorialComponent,
|
||||
ThankyouTutorialComponent,
|
||||
StatusTranslateComponent
|
||||
],
|
||||
entryComponents: [
|
||||
EmojiPickerComponent
|
||||
|
@ -166,9 +175,11 @@ const routes: Routes = [
|
|||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
PickerModule,
|
||||
OwlDateTimeModule,
|
||||
OwlDateTimeModule,
|
||||
OwlNativeDateTimeModule,
|
||||
OverlayModule,
|
||||
DragDropModule,
|
||||
// NgxElectronModule,
|
||||
RouterModule.forRoot(routes),
|
||||
|
||||
NgxsModule.forRoot([
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<a href class="account" title="open account" (click)="selected()">
|
||||
<a href class="account" title="open account" (click)="selected()" (auxclick)="openAccount()">
|
||||
<img src="{{account.avatar}}" class="account__avatar" />
|
||||
<div class="account__name" innerHTML="{{ account | accountEmoji }}"></div>
|
||||
<div class="account__fullhandle">@{{ account.acct }}</div>
|
||||
|
|
|
@ -21,4 +21,9 @@ export class AccountComponent implements OnInit {
|
|||
this.accountSelected.next(this.account);
|
||||
return false;
|
||||
}
|
||||
|
||||
openAccount(): boolean {
|
||||
window.open(this.account.url, '_blank');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ export abstract class TimelineBase extends BrowseBase {
|
|||
statuses: StatusWrapper[] = [];
|
||||
bufferStream: Status[] = [];
|
||||
protected bufferWasCleared: boolean;
|
||||
numNewItems: number;
|
||||
streamPositionnedAtTop: boolean = true;
|
||||
protected isProcessingInfiniteScroll: boolean;
|
||||
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
<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 #mytitle [(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"
|
||||
(keydown.escape)="mytitle.blur()" />
|
||||
|
||||
<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-editor__content" (paste)="onPaste($event)"
|
||||
rows="5" required title="content" placeholder="What's on your mind?" (keydown.control.enter)="onCtrlEnter()"
|
||||
<a class="status-editor__lang" title="Change language" href *ngIf="configuredLanguages && configuredLanguages.length > 1" (click)="onLangContextMenu($event)">
|
||||
{{ selectedLanguage.iso639 }}
|
||||
</a>
|
||||
|
||||
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm status-editor__content" (paste)="onPaste($event)"
|
||||
rows="5" required title="content" placeholder="What's on your mind?"
|
||||
(keydown.control.enter)="onCtrlEnter()"
|
||||
(keydown.meta.enter)="onCtrlEnter()"
|
||||
(keydown.escape)="reply.blur()"
|
||||
(keydown)="handleKeyDown($event)" (blur)="statusTextEditorLostFocus()" dir="auto">
|
||||
</textarea>
|
||||
|
||||
|
@ -21,19 +30,21 @@
|
|||
(suggestionSelectedEvent)="suggestionSelected($event)" (hasSuggestionsEvent)="suggestionsChanged($event)">
|
||||
</app-autosuggest>
|
||||
|
||||
<app-poll-editor *ngIf="instanceSupportsPoll && pollIsActive"></app-poll-editor>
|
||||
<app-poll-editor *ngIf="instanceSupportsPoll && pollIsActive" [oldPoll]="oldPoll"></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>
|
||||
<span *ngIf="!isSending && !scheduleIsActive && !isEditing">REPLY!</span>
|
||||
<span *ngIf="!isSending && scheduleIsActive && !isEditing">PLAN!</span>
|
||||
<span *ngIf="!isSending && isEditing">EDIT!</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>
|
||||
<span *ngIf="!isSending && !scheduleIsActive && !isEditing">POST!</span>
|
||||
<span *ngIf="!isSending && scheduleIsActive && !isEditing">PLAN!</span>
|
||||
<span *ngIf="!isSending && isEditing">EDIT!</span>
|
||||
<app-waiting-animation class="waiting-icon" *ngIf="isSending"></app-waiting-animation>
|
||||
</button>
|
||||
<div class="status-editor__footer__counter">
|
||||
|
@ -64,6 +75,10 @@
|
|||
<fa-icon [icon]="faClock"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="language-warning" *ngIf="!configuredLanguages || configuredLanguages.length === 0">
|
||||
You haven't set your language(s) yet, please <a href class="language-warning__link" (click)="onNavigateToSettings()">go in the settings</a> to provide it.
|
||||
</div>
|
||||
|
||||
<context-menu #contextMenu>
|
||||
<ng-template contextMenuItem (execute)="changePrivacy('Public')">
|
||||
|
@ -79,5 +94,12 @@
|
|||
<fa-icon [icon]="faEnvelope" class="context-menu-icon"></fa-icon> Direct
|
||||
</ng-template>
|
||||
</context-menu>
|
||||
|
||||
<context-menu #langContextMenu>
|
||||
<ng-template contextMenuItem (execute)="setLanguage(l)" *ngFor="let l of configuredLanguages">
|
||||
{{ l.name }}
|
||||
</ng-template>
|
||||
</context-menu>
|
||||
|
||||
<app-media></app-media>
|
||||
</form>
|
||||
|
|
|
@ -70,6 +70,32 @@ $counter-width: 90px;
|
|||
}
|
||||
}
|
||||
|
||||
&__lang {
|
||||
position: absolute;
|
||||
top: 64px;
|
||||
right: 12px;
|
||||
|
||||
font-weight: bolder;
|
||||
font-size: 12px;
|
||||
color: #a5a5a5;
|
||||
text-decoration: none;
|
||||
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 19px;
|
||||
border-radius: 2px;
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
|
||||
padding: 1px 0 0 2px;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color:black;
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
border-width: 0;
|
||||
background-color: $status-editor-background;
|
||||
|
@ -154,6 +180,9 @@ $counter-width: 90px;
|
|||
}
|
||||
|
||||
& span {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -204,6 +233,20 @@ $counter-width: 90px;
|
|||
border-bottom: 1px solid whitesmoke;
|
||||
}
|
||||
|
||||
.language-warning {
|
||||
padding: 5px 10px;
|
||||
color: orange;
|
||||
|
||||
&__link {
|
||||
text-decoration: underline;
|
||||
color: #f0d124;
|
||||
|
||||
&:hover {
|
||||
color: #d18800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@import '~@angular/cdk/overlay-prebuilt.css';
|
||||
// ::ng-deep .cdk-overlay-backdrop {
|
||||
// // width: 100%;
|
||||
|
|
|
@ -15,7 +15,7 @@ import { NavigationService } from '../../services/navigation.service';
|
|||
import { NotificationService } from '../../services/notification.service';
|
||||
import { MastodonService } from '../../services/mastodon.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
|
||||
import { SettingsState } from '../../states/settings.state';
|
||||
|
||||
describe('CreateStatusComponent', () => {
|
||||
let component: CreateStatusComponent;
|
||||
|
@ -33,7 +33,8 @@ describe('CreateStatusComponent', () => {
|
|||
NgxsModule.forRoot([
|
||||
RegisteredAppsState,
|
||||
AccountsState,
|
||||
StreamsState
|
||||
StreamsState,
|
||||
SettingsState
|
||||
]),
|
||||
],
|
||||
providers: [NavigationService, NotificationService, MastodonService, AuthService],
|
||||
|
@ -164,6 +165,41 @@ describe('CreateStatusComponent', () => {
|
|||
expect(result.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should cound URL correctly', () => {
|
||||
const newLine = String.fromCharCode(13, 10);
|
||||
const status = `qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd https://google.com/testqsdqsdqsdqsdqsdqsdqsdqdqsdqsdqsdqsdqs dsqd qsd qsd dsqdqs dqs dqsd qsd qsd qsd qsd qsd qs dqsdsq qsd qsd qs dsqds qqs d dqs dqs dqs dqqsd qsd qsd qsd sqd qsd qsd sqd qds dsqd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s`;
|
||||
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(0);
|
||||
});
|
||||
|
||||
it('should cound URL correctly - new lines', () => {
|
||||
const status = `qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd\nhttps://google.com/testqsdqsdqsdqsdqsdqsdqsdqdqsdqsdqsdqsdqs\ndsqd qsd qsd dsqdqs dqs dqsd qsd qsd qsd qsd qsd qs dqsdsq qsd qsd qs dsqds qqs d dqs dqs dqs dqqsd qsd qsd qsd sqd qsd qsd sqd qds dsqd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s`;
|
||||
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(0);
|
||||
});
|
||||
|
||||
it('should cound URL correctly - dual post', () => {
|
||||
const status = `qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd https://google.com/testqsdqsdqsdqsdqsdqsdqsdqdqsdqsdqsdqsdqs dsqd qsd qsd dsqdqs dqs dqsd qsd qsd qsd qsd qsd qs dqsdsq qsd qsd qs dsqds qqs d dqs dqs dqs dqqsd qsd qsd qsd sqd qsd qsd sqd qds dsqd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsdd dqsd qs s`;
|
||||
|
||||
(<any>component).maxCharLength = 512;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(0);
|
||||
expect((<any>component).postCounts).toBe(2);
|
||||
});
|
||||
|
||||
it('should cound URL correctly - triple post', () => {
|
||||
const status = `qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd https://google.com/testqsdqsdqsdqsdqsdqsdqsdqdqsdqsdqsdqsdqs dsqd qsd qsd dsqdqs dqs dqsd qsd qsd qsd qsd qsd qs dqsdsq qsd qsd qs dsqds qqs d dqs dqs dqs dqqsd qsd qsd qsd sqd qsd qsd sqd qds dsqd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsdd dqsd qs s dsqs sd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsdd dqsd qs s dsqs sd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsdd dqsd qs s dsqs sd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsdd dqsd qs s dsqs sd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qs qsd qsd qsd qsd sqd qsd qsd sqd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd sd`;
|
||||
|
||||
(<any>component).maxCharLength = 512;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(0);
|
||||
expect((<any>component).postCounts).toBe(3);
|
||||
});
|
||||
|
||||
it('should add alias in multiposting replies', () => {
|
||||
const status = '@Lorem@ipsum.com ipsum dolor sit amet, consectetur adipiscing elit. Mauris sed ante id dolor vulputate pulvinar sit amet a nisl. Duis sagittis nisl sit amet est rhoncus rutrum. Duis aliquet eget erat nec molestie. Fusce bibendum consectetur rhoncus. Aenean vel neque ac diam hendrerit interdum id a nisl. Aenean leo ante, luctus eget erat at, interdum tincidunt turpis. Donec non efficitur magna. Nam placerat convallis tincidunt. Etiam ac scelerisque velit, at vestibulum turpis. In hac habitasse platea dictu0';
|
||||
(<any>component).maxCharLength = 500;
|
||||
|
|
|
@ -11,7 +11,7 @@ import { ContextMenuService, ContextMenuComponent } from 'ngx-contextmenu';
|
|||
|
||||
import { VisibilityEnum, PollParameters } from '../../services/mastodon.service';
|
||||
import { MastodonWrapperService } from '../../services/mastodon-wrapper.service';
|
||||
import { Status, Attachment } from '../../services/models/mastodon.interfaces';
|
||||
import { Status, Attachment, Poll } from '../../services/models/mastodon.interfaces';
|
||||
import { ToolsService, InstanceInfo, InstanceType } from '../../services/tools.service';
|
||||
import { NotificationService } from '../../services/notification.service';
|
||||
import { StatusWrapper } from '../../models/common.model';
|
||||
|
@ -24,6 +24,10 @@ import { PollEditorComponent } from './poll-editor/poll-editor.component';
|
|||
import { StatusSchedulerComponent } from './status-scheduler/status-scheduler.component';
|
||||
import { ScheduledStatusService } from '../../services/scheduled-status.service';
|
||||
import { StatusesStateService } from '../../services/statuses-state.service';
|
||||
import { SettingsService } from '../../services/settings.service';
|
||||
import { LanguageService } from '../../services/language.service';
|
||||
import { ILanguage } from '../../states/settings.state';
|
||||
import { LeftPanelType, NavigationService } from '../../services/navigation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-status',
|
||||
|
@ -64,6 +68,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
this.detectAutosuggestion(value);
|
||||
this._status = value;
|
||||
|
||||
this.languageService.autoDetectLang(value);
|
||||
|
||||
setTimeout(() => {
|
||||
this.autoGrow();
|
||||
}, 0);
|
||||
|
@ -82,12 +88,22 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
return s;
|
||||
}
|
||||
|
||||
@Input('statusToEdit')
|
||||
set statusToEdit(value: StatusWrapper) {
|
||||
if (value) {
|
||||
this.isEditing = true;
|
||||
this.editingStatusId = value.status.id;
|
||||
this.redraftedStatus = value;
|
||||
this.mediaService.loadMedia(value.status.media_attachments);
|
||||
}
|
||||
}
|
||||
|
||||
@Input('redraftedStatus')
|
||||
set redraftedStatus(value: StatusWrapper) {
|
||||
if (value) {
|
||||
this.isRedrafting = true;
|
||||
this.statusLoaded = false;
|
||||
|
||||
|
||||
if (value.status && value.status.media_attachments) {
|
||||
for (const m of value.status.media_attachments) {
|
||||
this.mediaService.addExistingMedia(new MediaWrapper(m.id, null, m));
|
||||
|
@ -111,6 +127,13 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
|
||||
// this.statusStateService.setStatusContent(this.status, this.statusReplyingToWrapper);
|
||||
|
||||
// Retrieve mentions
|
||||
for(let mention of value.status.mentions){
|
||||
if(this.status){
|
||||
this.status = this.status.replace(`@${mention.username}`, `@${mention.acct}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.setVisibilityFromStatus(value.status);
|
||||
this.title = value.status.spoiler_text;
|
||||
this.statusLoaded = true;
|
||||
|
@ -129,9 +152,19 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
this.isSending = false;
|
||||
});
|
||||
}
|
||||
|
||||
if(value.status.poll){
|
||||
this.pollIsActive = true;
|
||||
this.oldPoll = value.status.poll;
|
||||
// setTimeout(() => {
|
||||
// if(this.pollEditor) this.pollEditor.loadPollParameters(value.status.poll);
|
||||
// }, 250);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
oldPoll: Poll;
|
||||
|
||||
private maxCharLength: number;
|
||||
charCountLeft: number;
|
||||
postCounts: number = 1;
|
||||
|
@ -140,6 +173,10 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
autosuggestData: string = null;
|
||||
instanceSupportsPoll = true;
|
||||
instanceSupportsScheduling = true;
|
||||
isEditing: boolean;
|
||||
editingStatusId: string;
|
||||
configuredLanguages: ILanguage[] = [];
|
||||
selectedLanguage: ILanguage;
|
||||
private statusLoaded: boolean;
|
||||
private hasSuggestions: boolean;
|
||||
|
||||
|
@ -149,6 +186,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
@ViewChild('fileInput') fileInputElement: ElementRef;
|
||||
@ViewChild('footer') footerElement: ElementRef;
|
||||
@ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent;
|
||||
@ViewChild('langContextMenu') public langContextMenu: ContextMenuComponent;
|
||||
@ViewChild(PollEditorComponent) pollEditor: PollEditorComponent;
|
||||
@ViewChild(StatusSchedulerComponent) statusScheduler: StatusSchedulerComponent;
|
||||
|
||||
|
@ -183,10 +221,15 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
|
||||
private accounts$: Observable<AccountInfo[]>;
|
||||
private accountSub: Subscription;
|
||||
private langSub: Subscription;
|
||||
private selectLangSub: Subscription;
|
||||
private selectedAccount: AccountInfo;
|
||||
|
||||
constructor(
|
||||
private statusStateService: StatusesStateService,
|
||||
private readonly navigationService: NavigationService,
|
||||
private readonly languageService: LanguageService,
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly statusStateService: StatusesStateService,
|
||||
private readonly scheduledStatusService: ScheduledStatusService,
|
||||
private readonly contextMenuService: ContextMenuService,
|
||||
private readonly store: Store,
|
||||
|
@ -196,12 +239,41 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
private readonly instancesInfoService: InstancesInfoService,
|
||||
private readonly mediaService: MediaService,
|
||||
private readonly overlay: Overlay,
|
||||
public viewContainerRef: ViewContainerRef) {
|
||||
public viewContainerRef: ViewContainerRef,
|
||||
private readonly statusesStateService: StatusesStateService) {
|
||||
|
||||
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
|
||||
}
|
||||
|
||||
private initLanguages(){
|
||||
this.configuredLanguages = this.languageService.getConfiguredLanguages();
|
||||
this.selectedLanguage = this.languageService.getSelectedLanguage();
|
||||
this.langSub = this.languageService.configuredLanguagesChanged.subscribe(l => {
|
||||
this.configuredLanguages = l;
|
||||
// if(this.configuredLanguages.length > 0
|
||||
// && this.selectedLanguage
|
||||
// && this.configuredLanguages.findIndex(x => x.iso639 === this.selectedLanguage.iso639)){
|
||||
// this.languageService.setSelectedLanguage(this.configuredLanguages[0]);
|
||||
// }
|
||||
});
|
||||
this.selectLangSub = this.languageService.selectedLanguageChanged.subscribe(l => {
|
||||
this.selectedLanguage = l;
|
||||
});
|
||||
if(!this.selectedLanguage && this.configuredLanguages.length > 0){
|
||||
this.languageService.setSelectedLanguage(this.configuredLanguages[0]);
|
||||
}
|
||||
}
|
||||
|
||||
setLanguage(lang: ILanguage): boolean {
|
||||
if(lang){
|
||||
this.languageService.setSelectedLanguage(lang);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.initLanguages();
|
||||
|
||||
if (!this.isRedrafting) {
|
||||
this.status = this.statusStateService.getStatusContent(this.statusReplyingToWrapper);
|
||||
}
|
||||
|
@ -223,7 +295,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
// this.status = state;
|
||||
// } else {
|
||||
if (!this.status || this.status === '') {
|
||||
const uniqueMentions = this.getMentions(this.statusReplyingTo, this.statusReplyingToWrapper.provider);
|
||||
const uniqueMentions = this.getMentions(this.statusReplyingTo);
|
||||
for (const mention of uniqueMentions) {
|
||||
this.status += `@${mention} `;
|
||||
}
|
||||
|
@ -248,6 +320,13 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
this.accountSub.unsubscribe();
|
||||
this.langSub.unsubscribe();
|
||||
this.selectLangSub.unsubscribe();
|
||||
}
|
||||
|
||||
onNavigateToSettings(): boolean {
|
||||
this.navigationService.openPanel(LeftPanelType.Settings);
|
||||
return false;
|
||||
}
|
||||
|
||||
onPaste(e: any) {
|
||||
|
@ -306,7 +385,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
};
|
||||
|
||||
const word = this.getWordByPos(currentSection, caretPosition - offset);
|
||||
if (!lastCharIsSpace && word && word.length > 0 && (word.startsWith('@') || word.startsWith('#'))) {
|
||||
if (!lastCharIsSpace && word && word.length > 1 && (word.startsWith('@') || word.startsWith('#'))) {
|
||||
this.autosuggestData = word;
|
||||
return;
|
||||
}
|
||||
|
@ -370,7 +449,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
if (accounts && accounts.length > 0) {
|
||||
this.selectedAccount = accounts.filter(x => x.isSelected)[0];
|
||||
|
||||
const settings = this.toolsService.getAccountSettings(this.selectedAccount);
|
||||
const settings = this.settingsService.getAccountSettings(this.selectedAccount);
|
||||
if (settings.customStatusCharLengthEnabled) {
|
||||
this.maxCharLength = settings.customStatusCharLength;
|
||||
this.countStatusChar(this.status);
|
||||
|
@ -434,7 +513,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
private setVisibility(defaultPrivacy: VisibilityEnum) {
|
||||
if(this.selectedPrivacySetByRedraft) return;
|
||||
if (this.selectedPrivacySetByRedraft) return;
|
||||
|
||||
switch (defaultPrivacy) {
|
||||
case VisibilityEnum.Public:
|
||||
|
@ -474,8 +553,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
const currentStatus = parseStatus[parseStatus.length - 1];
|
||||
const statusExtraChars = this.getMentionExtraChars(status);
|
||||
const linksExtraChars = this.getLinksExtraChars(status);
|
||||
const statusExtraChars = this.getMentionExtraChars(currentStatus);
|
||||
const linksExtraChars = this.getLinksExtraChars(currentStatus);
|
||||
|
||||
const statusLength = [...currentStatus].length - statusExtraChars - linksExtraChars;
|
||||
this.charCountLeft = this.maxCharLength - statusLength - this.getCwLength();
|
||||
|
@ -490,8 +569,20 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
return cwLength;
|
||||
}
|
||||
|
||||
private getMentions(status: Status, providerInfo: AccountInfo): string[] {
|
||||
const mentions = [status.account.acct, ...status.mentions.map(x => x.acct)];
|
||||
private getMentions(status: Status): string[] {
|
||||
let acct = status.account.acct;
|
||||
if (!acct.includes('@')) {
|
||||
acct += `@${status.account.url.replace('https://', '').split('/')[0]}`
|
||||
}
|
||||
|
||||
const mentions = [acct];
|
||||
status.mentions.forEach(m => {
|
||||
let mentionAcct = m.acct;
|
||||
if (!mentionAcct.includes('@')) {
|
||||
mentionAcct += `@${m.url.replace('https://', '').split('/')[0]}`;
|
||||
}
|
||||
mentions.push(mentionAcct);
|
||||
});
|
||||
|
||||
let uniqueMentions = [];
|
||||
for (let mention of mentions) {
|
||||
|
@ -500,22 +591,10 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
let globalUniqueMentions = [];
|
||||
for (let mention of uniqueMentions) {
|
||||
if (!mention.includes('@')) {
|
||||
if (providerInfo) {
|
||||
mention += `@${providerInfo.instance}`;
|
||||
} else {
|
||||
mention += `@${status.url.replace('https://', '').split('/')[0]}`;
|
||||
}
|
||||
}
|
||||
globalUniqueMentions.push(mention);
|
||||
}
|
||||
|
||||
const selectedUser = this.toolsService.getSelectedAccounts()[0];
|
||||
globalUniqueMentions = globalUniqueMentions.filter(x => x.toLowerCase() !== `${selectedUser.username}@${selectedUser.instance}`.toLowerCase());
|
||||
uniqueMentions = uniqueMentions.filter(x => x.toLowerCase() !== `${selectedUser.username}@${selectedUser.instance}`.toLowerCase());
|
||||
|
||||
return globalUniqueMentions;
|
||||
return uniqueMentions;
|
||||
}
|
||||
|
||||
onCtrlEnter(): boolean {
|
||||
|
@ -523,7 +602,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
return false;
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
async onSubmit(): Promise<boolean> {
|
||||
if (this.isSending || this.mentionTooFarAwayError) return false;
|
||||
|
||||
this.isSending = true;
|
||||
|
@ -544,9 +623,10 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
break;
|
||||
}
|
||||
|
||||
const mediaAttachments = this.mediaService.mediaSubject.value.map(x => x.attachment);
|
||||
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
const mediaAttachments = (await this.mediaService.retrieveUpToDateMedia(acc)).map(x => x.attachment);
|
||||
|
||||
let usableStatus: Promise<Status>;
|
||||
if (this.statusReplyingToWrapper) {
|
||||
usableStatus = this.toolsService.getStatusUsableByAccount(acc, this.statusReplyingToWrapper);
|
||||
|
@ -570,7 +650,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
|
||||
usableStatus
|
||||
.then((status: Status) => {
|
||||
return this.sendStatus(acc, this.status, visibility, this.title, status, mediaAttachments, poll, scheduledTime);
|
||||
return this.sendStatus(acc, this.status, visibility, this.title, status, mediaAttachments, poll, scheduledTime, this.editingStatusId);
|
||||
})
|
||||
.then((res: Status) => {
|
||||
this.title = '';
|
||||
|
@ -597,7 +677,15 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
return false;
|
||||
}
|
||||
|
||||
private sendStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, title: string, previousStatus: Status, attachments: Attachment[], poll: PollParameters, scheduledAt: string): Promise<Status> {
|
||||
private currentLang(): string {
|
||||
if(this.selectedLanguage){
|
||||
return this.selectedLanguage.iso639;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private sendStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, title: string, previousStatus: Status, attachments: Attachment[], poll: PollParameters, scheduledAt: string, editingStatusId: string): Promise<Status> {
|
||||
let parsedStatus = this.parseStatus(status);
|
||||
let resultPromise = Promise.resolve(previousStatus);
|
||||
|
||||
|
@ -611,13 +699,25 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
if (i === 0) {
|
||||
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id), poll, scheduledAt)
|
||||
let postPromise: Promise<Status>;
|
||||
|
||||
if (this.isEditing) {
|
||||
postPromise = this.mastodonService.editStatus(account, editingStatusId, s, visibility, title, inReplyToId, attachments, poll, scheduledAt, this.currentLang());
|
||||
} else {
|
||||
postPromise = this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id), poll, scheduledAt, this.currentLang());
|
||||
}
|
||||
|
||||
return postPromise
|
||||
.then((status: Status) => {
|
||||
this.mediaService.clearMedia();
|
||||
return status;
|
||||
});
|
||||
} else {
|
||||
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, [], null, scheduledAt);
|
||||
if (this.isEditing) {
|
||||
return this.mastodonService.editStatus(account, editingStatusId, s, visibility, title, inReplyToId, [], null, scheduledAt, this.currentLang());
|
||||
} else {
|
||||
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, [], null, scheduledAt, this.currentLang());
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((status: Status) => {
|
||||
|
@ -626,6 +726,16 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
this.notificationService.newStatusPosted(this.statusReplyingToWrapper.status.id, new StatusWrapper(cwPolicy.status, account, cwPolicy.applyCw, cwPolicy.hide));
|
||||
}
|
||||
|
||||
return status;
|
||||
})
|
||||
.then((status: Status) => {
|
||||
if (this.isEditing) {
|
||||
let cwPolicy = this.toolsService.checkContentWarning(status);
|
||||
let statusWrapper = new StatusWrapper(status, account, cwPolicy.applyCw, cwPolicy.hide);
|
||||
|
||||
this.statusesStateService.statusEditedStatusChanged(status.url, account.id, statusWrapper);
|
||||
}
|
||||
|
||||
return status;
|
||||
});
|
||||
}
|
||||
|
@ -635,6 +745,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
|
||||
private parseStatus(status: string): string[] {
|
||||
let mentionExtraChars = this.getMentionExtraChars(status);
|
||||
let urlExtraChar = this.getLinksExtraChars(status);
|
||||
let trucatedStatus = `${status}`;
|
||||
let results = [];
|
||||
|
||||
|
@ -644,13 +755,24 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
aggregateMention += `${x} `;
|
||||
});
|
||||
|
||||
const currentMaxCharLength = this.maxCharLength + mentionExtraChars - this.getCwLength();
|
||||
const maxChars = currentMaxCharLength - 6;
|
||||
let currentMaxCharLength = this.maxCharLength + mentionExtraChars + urlExtraChar - this.getCwLength();
|
||||
let maxChars = currentMaxCharLength - 6;
|
||||
|
||||
while (trucatedStatus.length > currentMaxCharLength) {
|
||||
const nextIndex = trucatedStatus.lastIndexOf(' ', maxChars);
|
||||
|
||||
if (nextIndex === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
results.push(trucatedStatus.substr(0, nextIndex) + ' (...)');
|
||||
trucatedStatus = aggregateMention + trucatedStatus.substr(nextIndex + 1);
|
||||
|
||||
// Refresh max
|
||||
let mentionExtraChars = this.getMentionExtraChars(trucatedStatus);
|
||||
let urlExtraChar = this.getLinksExtraChars(trucatedStatus);
|
||||
currentMaxCharLength = this.maxCharLength + mentionExtraChars + urlExtraChar - this.getCwLength();
|
||||
maxChars = currentMaxCharLength - 6;
|
||||
}
|
||||
results.push(trucatedStatus);
|
||||
return results;
|
||||
|
@ -658,7 +780,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
|
||||
private getLinksExtraChars(status: string): number {
|
||||
let mentionExtraChars = 0;
|
||||
let links = status.split(' ').filter(x => x.startsWith('http://') || x.startsWith('https://'));
|
||||
let links = status.split(/\s+/).filter(x => x.startsWith('http://') || x.startsWith('https://'));
|
||||
for (let link of links) {
|
||||
if (link.length > 23) {
|
||||
mentionExtraChars += link.length - 23;
|
||||
|
@ -690,8 +812,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
suggestionSelected(selection: AutosuggestSelection) {
|
||||
if (this.status.includes(selection.pattern)) {
|
||||
this.status = this.replacePatternWithAutosuggest(this.status, selection.pattern, selection.autosuggest);
|
||||
|
||||
let cleanStatus = this.status.replace(/\r?\n/g, ' ');
|
||||
|
||||
let cleanStatus = this.status.replace(/\r?\n/g, ' ');
|
||||
let newCaretPosition = cleanStatus.indexOf(`${selection.autosuggest}`) + selection.autosuggest.length;
|
||||
if (newCaretPosition > cleanStatus.length) newCaretPosition = cleanStatus.length;
|
||||
|
||||
|
@ -740,7 +862,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
w++;
|
||||
result += `${word}`;
|
||||
|
||||
if(w < wordCount || i === nberLines){
|
||||
if (w < wordCount || i === nberLines) {
|
||||
result += ' ';
|
||||
}
|
||||
});
|
||||
|
@ -752,7 +874,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
result = result.replace(' ', ' ');
|
||||
|
||||
let endRegex = new RegExp(`${autosuggest} $`, 'i');
|
||||
if(!result.match(endRegex)){
|
||||
if (!result.match(endRegex)) {
|
||||
result = result.substring(0, result.length - 1);
|
||||
}
|
||||
|
||||
|
@ -837,6 +959,17 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
$event.stopPropagation();
|
||||
}
|
||||
|
||||
public onLangContextMenu($event: MouseEvent): void {
|
||||
this.contextMenuService.show.next({
|
||||
// Optional - if unspecified, all context menu components will open
|
||||
contextMenu: this.langContextMenu,
|
||||
event: $event,
|
||||
item: null
|
||||
});
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
}
|
||||
|
||||
//https://stackblitz.com/edit/overlay-demo
|
||||
@ViewChild('emojiButton') emojiButtonElement: ElementRef;
|
||||
overlayRef: OverlayRef;
|
||||
|
@ -869,7 +1002,6 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
|
||||
this.overlayRef = this.overlay.create(config);
|
||||
// this.overlayRef.backdropClick().subscribe(() => {
|
||||
// console.warn('wut?');
|
||||
// this.overlayRef.dispose();
|
||||
// });
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<div *ngFor="let m of media" class="media">
|
||||
<div *ngIf="m.attachment === null" class="media__loading" title="{{m.file.name}}">
|
||||
<div *ngIf="m.attachment === null" class="media__loading" title="{{getName(m)}}">
|
||||
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
||||
</div>
|
||||
<div *ngIf="m.attachment !== null && m.attachment.type !== 'audio'" class="media__loaded" title="{{m.file.name}}"
|
||||
<div *ngIf="m.attachment !== null && m.attachment.type !== 'audio'" class="media__loaded" title="{{getName(m)}}"
|
||||
(mouseleave)="updateMedia(m)">
|
||||
<div class="media__loaded--migrating" *ngIf="m.isMigrating">
|
||||
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
||||
|
|
|
@ -56,4 +56,13 @@ export class MediaComponent implements OnInit, OnDestroy {
|
|||
this.mediaService.update(account, media);
|
||||
return false;
|
||||
}
|
||||
|
||||
getName(media: MediaWrapper): string {
|
||||
if(media && media.file && media.file.name){
|
||||
return media.file.name;
|
||||
}
|
||||
if(media.attachment && media.attachment.description){
|
||||
return media.attachment.description;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, Input, OnInit, SimpleChanges } 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';
|
||||
import { Poll } from '../../../services/models/mastodon.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-poll-editor',
|
||||
|
@ -19,6 +19,8 @@ export class PollEditorComponent implements OnInit {
|
|||
selectedId: string;
|
||||
private multiSelected: boolean;
|
||||
|
||||
@Input() oldPoll: Poll;
|
||||
|
||||
constructor() {
|
||||
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
|
||||
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
|
||||
|
@ -40,6 +42,12 @@ export class PollEditorComponent implements OnInit {
|
|||
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['oldPoll']) {
|
||||
this.loadPollParameters(this.oldPoll);
|
||||
}
|
||||
}
|
||||
|
||||
private getEntryUuid(): number {
|
||||
this.entryUuid++;
|
||||
return this.entryUuid;
|
||||
|
@ -50,7 +58,7 @@ export class PollEditorComponent implements OnInit {
|
|||
return false;
|
||||
}
|
||||
|
||||
removeElement(entry: PollEntry){
|
||||
removeElement(entry: PollEntry) {
|
||||
this.entries = this.entries.filter(x => x.id != entry.id);
|
||||
}
|
||||
|
||||
|
@ -69,6 +77,19 @@ export class PollEditorComponent implements OnInit {
|
|||
params.hide_totals = false;
|
||||
return params;
|
||||
}
|
||||
|
||||
private loadPollParameters(poll: Poll) {
|
||||
if(!this.oldPoll) return;
|
||||
|
||||
const isMulti = poll.multiple;
|
||||
|
||||
this.entries.length = 0;
|
||||
for (let o of poll.options) {
|
||||
const entry = new PollEntry(this.getEntryUuid(), isMulti);
|
||||
entry.label = o.title;
|
||||
this.entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Delay {
|
||||
|
|
|
@ -15,7 +15,9 @@
|
|||
<button type="submit" class="form-button"
|
||||
title="add account"
|
||||
[class.comrade__button]="isComrade">
|
||||
<span *ngIf="!isLoading">Submit</span>
|
||||
|
||||
<span *ngIf="!isLoading && !this.isInstanceMultiAccountLoading">Submit</span>
|
||||
<span *ngIf="!isLoading && this.isInstanceMultiAccountLoading" class="faq__warning">See FAQ</span>
|
||||
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
|
||||
</button>
|
||||
|
||||
|
@ -29,5 +31,12 @@
|
|||
allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
<div class="faq" *ngIf="isInstanceMultiAccount">
|
||||
<p>
|
||||
FAQ<br/>
|
||||
<a href="https://github.com/NicolasConstant/sengi/wiki/How-to-add-multiple-accounts-from-the-same-instance" target="_blank">How to add multiple accounts from the same instance?</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -109,4 +109,21 @@ $comrade_red: #a50000;
|
|||
background-color: $comrade_red;
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.faq {
|
||||
margin: 20px 0 0 0;
|
||||
|
||||
& a {
|
||||
color: #ffcc00;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: #ffe88a;
|
||||
}
|
||||
}
|
||||
|
||||
&__warning {
|
||||
color: #ffdc52;
|
||||
}
|
||||
}
|
|
@ -6,13 +6,14 @@ import { RegisteredAppsStateModel, AppInfo, AddRegisteredApp } from '../../../st
|
|||
import { AuthService, CurrentAuthProcess } from '../../../services/auth.service';
|
||||
import { AppData } from '../../../services/models/mastodon.interfaces';
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { ToolsService } from '../../../services/tools.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-new-account',
|
||||
templateUrl: './add-new-account.component.html',
|
||||
styleUrls: ['./add-new-account.component.scss']
|
||||
})
|
||||
export class AddNewAccountComponent implements OnInit {
|
||||
export class AddNewAccountComponent implements OnInit {
|
||||
private blockList = ['gab.com', 'gab.ai', 'cyzed.com'];
|
||||
private comradeList = ['juche.town'];
|
||||
|
||||
|
@ -24,12 +25,14 @@ export class AddNewAccountComponent implements OnInit {
|
|||
set setInstance(value: string) {
|
||||
this.instance = value.replace('http://', '').replace('https://', '').replace('/', '').toLowerCase().trim();
|
||||
this.checkComrad();
|
||||
this.checkInstanceMultiAccount(value);
|
||||
}
|
||||
get setInstance(): string {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly store: Store) { }
|
||||
|
@ -51,8 +54,27 @@ export class AddNewAccountComponent implements OnInit {
|
|||
this.isComrade = false;
|
||||
}
|
||||
|
||||
isInstanceMultiAccount: boolean;
|
||||
isInstanceMultiAccountLoading: boolean;
|
||||
checkInstanceMultiAccount(value: string) {
|
||||
if(value) {
|
||||
const instances: string[] = this.toolsService.getAllAccounts().map(x => x.instance);
|
||||
if(instances && instances.indexOf(value) > -1){
|
||||
this.isInstanceMultiAccount = true;
|
||||
this.isInstanceMultiAccountLoading = true;
|
||||
|
||||
setTimeout(() => {
|
||||
this.isInstanceMultiAccountLoading = false;
|
||||
}, 2000);
|
||||
} else {
|
||||
this.isInstanceMultiAccount = false;
|
||||
this.isInstanceMultiAccountLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
if(this.isLoading || !this.instance) return false;
|
||||
if(this.isLoading || !this.instance || this.isInstanceMultiAccountLoading) return false;
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
<div class=" new-message-body flexcroll">
|
||||
<app-create-status (onClose)="closeColumn()" [isDirectMention]="isDirectMention"
|
||||
[replyingUserHandle]="userHandle" [redraftedStatus]="redraftedStatus"></app-create-status>
|
||||
|
||||
[replyingUserHandle]="userHandle" [statusToEdit]="statusToEdit" [redraftedStatus]="redraftedStatus"></app-create-status>
|
||||
</div>
|
||||
</div>
|
|
@ -13,6 +13,7 @@ export class AddNewStatusComponent implements OnInit {
|
|||
@Input() isDirectMention: boolean;
|
||||
@Input() userHandle: string;
|
||||
@Input() redraftedStatus: StatusWrapper;
|
||||
@Input() statusToEdit: StatusWrapper;
|
||||
|
||||
constructor(private readonly navigationService: NavigationService) {
|
||||
}
|
||||
|
|
|
@ -1,29 +1,31 @@
|
|||
<div class="floating-column">
|
||||
<div class="floating-column__inner">
|
||||
<div class="sliding-column" [class.sliding-column__right-display]="overlayActive">
|
||||
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive"
|
||||
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive"
|
||||
(closeOverlay)="closeOverlay()"
|
||||
[browseAccountData]="overlayAccountToBrowse"
|
||||
[browseAccountData]="overlayAccountToBrowse"
|
||||
[browseHashtagData]="overlayHashtagToBrowse"
|
||||
[browseThreadData]="overlayThreadToBrowse"></app-stream-overlay>
|
||||
|
||||
<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>
|
||||
<fa-icon class="close-button__icon" [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<app-manage-account *ngIf="openPanel === 'manageAccount'" [account]="userAccountUsed"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(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>
|
||||
[userHandle]="userHandle"
|
||||
[redraftedStatus]="redraftedStatus"
|
||||
[statusToEdit]="statusToEdit"></app-add-new-status>
|
||||
<app-add-new-account *ngIf="openPanel === 'addNewAccount'"></app-add-new-account>
|
||||
<app-search *ngIf="openPanel === 'search'"
|
||||
<app-search *ngIf="openPanel === 'search'"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)">
|
||||
</app-search>
|
||||
<app-settings *ngIf="openPanel === 'settings'"></app-settings>
|
||||
|
|
|
@ -29,9 +29,20 @@
|
|||
}
|
||||
|
||||
.close-button {
|
||||
// outline: 1px dotted orange;
|
||||
|
||||
display: block;
|
||||
float: right;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
margin: 10px 16px 0 0;
|
||||
margin: 5px 5px 0 0;
|
||||
|
||||
width: 40px;
|
||||
height: 34px;
|
||||
|
||||
&__icon {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
left: 17px;
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
|
|||
isDirectMention: boolean;
|
||||
userHandle: string;
|
||||
redraftedStatus: StatusWrapper;
|
||||
statusToEdit: StatusWrapper;
|
||||
|
||||
openPanel: string = '';
|
||||
|
||||
|
@ -49,12 +50,21 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
break;
|
||||
case LeftPanelType.CreateNewStatus:
|
||||
case LeftPanelType.EditStatus:
|
||||
if (this.openPanel === 'createNewStatus' && !event.userHandle) {
|
||||
this.closePanel();
|
||||
} else {
|
||||
this.isDirectMention = event.action === LeftPanelAction.DM;
|
||||
this.userHandle = event.userHandle;
|
||||
this.redraftedStatus = event.status;
|
||||
|
||||
if(event.type === LeftPanelType.CreateNewStatus){
|
||||
this.redraftedStatus = event.status;
|
||||
this.statusToEdit = null;
|
||||
} else {
|
||||
this.redraftedStatus = null;
|
||||
this.statusToEdit = event.status;
|
||||
}
|
||||
|
||||
this.openPanel = 'createNewStatus';
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -58,6 +58,11 @@ export class BookmarksComponent extends TimelineBase {
|
|||
this.mastodonService.getBookmarks(this.account)
|
||||
.then((result: BookmarkResult) => {
|
||||
this.maxId = result.max_id;
|
||||
|
||||
if(!this.maxId){
|
||||
this.lastCallReachedMax = true;
|
||||
}
|
||||
|
||||
for (const s of result.bookmarked) {
|
||||
let cwPolicy = this.toolsService.checkContentWarning(s);
|
||||
const wrapper = new StatusWrapper(cwPolicy.status, this.account, cwPolicy.applyCw, cwPolicy.hide);
|
||||
|
@ -73,6 +78,8 @@ export class BookmarksComponent extends TimelineBase {
|
|||
}
|
||||
|
||||
protected getNextStatuses(): Promise<Status[]> {
|
||||
if(this.lastCallReachedMax) return Promise.resolve([]);
|
||||
|
||||
return this.mastodonService.getBookmarks(this.account, this.maxId)
|
||||
.then((result: BookmarkResult) => {
|
||||
const statuses = result.bookmarked;
|
||||
|
|
|
@ -56,6 +56,11 @@ export class FavoritesComponent extends TimelineBase {
|
|||
this.mastodonService.getFavorites(this.account)
|
||||
.then((result: FavoriteResult) => {
|
||||
this.maxId = result.max_id;
|
||||
|
||||
if (!this.maxId) {
|
||||
this.lastCallReachedMax = true;
|
||||
}
|
||||
|
||||
for (const s of result.favorites) {
|
||||
let cwPolicy = this.toolsService.checkContentWarning(s);
|
||||
const wrapper = new StatusWrapper(cwPolicy.status, this.account, cwPolicy.applyCw, cwPolicy.hide);
|
||||
|
@ -72,12 +77,14 @@ export class FavoritesComponent extends TimelineBase {
|
|||
}
|
||||
|
||||
protected getNextStatuses(): Promise<Status[]> {
|
||||
return this.mastodonService.getFavorites(this.account, this.maxId)
|
||||
if (this.lastCallReachedMax) return Promise.resolve([]);
|
||||
|
||||
return this.mastodonService.getFavorites(this.account, this.maxId)
|
||||
.then((result: FavoriteResult) => {
|
||||
const statuses = result.favorites;
|
||||
this.maxId = result.max_id;
|
||||
|
||||
if(!this.maxId){
|
||||
if (!this.maxId) {
|
||||
this.lastCallReachedMax = true;
|
||||
}
|
||||
|
||||
|
@ -85,7 +92,7 @@ export class FavoritesComponent extends TimelineBase {
|
|||
});
|
||||
}
|
||||
|
||||
protected scrolledToTop() {}
|
||||
protected scrolledToTop() { }
|
||||
|
||||
protected statusProcessOnGoToTop(){}
|
||||
protected statusProcessOnGoToTop() { }
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import { MentionsComponent } from './mentions/mentions.component';
|
|||
import { DirectMessagesComponent } from './direct-messages/direct-messages.component';
|
||||
import { FavoritesComponent } from './favorites/favorites.component';
|
||||
import { BrowseBase } from '../../common/browse-base';
|
||||
import { SettingsService } from '../../../services/settings.service';
|
||||
|
||||
|
||||
@Component({
|
||||
|
@ -54,14 +55,15 @@ export class ManageAccountComponent extends BrowseBase {
|
|||
private _account: AccountWrapper;
|
||||
|
||||
constructor(
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mastodonService: MastodonWrapperService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly userNotificationService: UserNotificationService) {
|
||||
super();
|
||||
}
|
||||
private readonly userNotificationService: UserNotificationService) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
@ -69,13 +71,9 @@ export class ManageAccountComponent extends BrowseBase {
|
|||
}
|
||||
|
||||
private checkIfBookmarksAreAvailable() {
|
||||
this.toolsService.getInstanceInfo(this.account.info)
|
||||
.then((instance: InstanceInfo) => {
|
||||
if (instance.major >= 3 && instance.minor >= 1) {
|
||||
this.isBookmarksAvailable = true;
|
||||
} else {
|
||||
this.isBookmarksAvailable = false;
|
||||
}
|
||||
this.toolsService.isBookmarksAreAvailable(this.account.info)
|
||||
.then((isAvailable: boolean) => {
|
||||
this.isBookmarksAvailable = isAvailable;
|
||||
})
|
||||
.catch(err => {
|
||||
this.isBookmarksAvailable = false;
|
||||
|
@ -100,8 +98,8 @@ export class ManageAccountComponent extends BrowseBase {
|
|||
this.userNotificationServiceSub = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
|
||||
const userNotification = userNotifications.find(x => x.account.id === this.account.info.id);
|
||||
if (userNotification) {
|
||||
let settings = this.toolsService.getSettings();
|
||||
let accSettings = this.toolsService.getAccountSettings(this.account.info);
|
||||
let settings = this.settingsService.getSettings();
|
||||
let accSettings = this.settingsService.getAccountSettings(this.account.info);
|
||||
|
||||
if (!settings.disableAvatarNotifications && !accSettings.disableAvatarNotifications) {
|
||||
this.hasNotifications = userNotification.hasNewNotifications;
|
||||
|
@ -113,8 +111,8 @@ export class ManageAccountComponent extends BrowseBase {
|
|||
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);
|
||||
let settings = this.settingsService.getSettings();
|
||||
let accSettings = this.settingsService.getAccountSettings(this.account.info);
|
||||
|
||||
if (!settings.disableAutofocus && !settings.disableAvatarNotifications && !accSettings.disableAvatarNotifications) {
|
||||
if (userNotification.hasNewNotifications) {
|
||||
|
@ -126,16 +124,16 @@ export class ManageAccountComponent extends BrowseBase {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewChild('bookmarks') bookmarksComp:BookmarksComponent;
|
||||
@ViewChild('notifications') notificationsComp:NotificationsComponent;
|
||||
@ViewChild('mentions') mentionsComp:MentionsComponent;
|
||||
@ViewChild('dm') dmComp:DirectMessagesComponent;
|
||||
@ViewChild('favorites') favoritesComp:FavoritesComponent;
|
||||
@ViewChild('bookmarks') bookmarksComp: BookmarksComponent;
|
||||
@ViewChild('notifications') notificationsComp: NotificationsComponent;
|
||||
@ViewChild('mentions') mentionsComp: MentionsComponent;
|
||||
@ViewChild('dm') dmComp: DirectMessagesComponent;
|
||||
@ViewChild('favorites') favoritesComp: FavoritesComponent;
|
||||
|
||||
loadSubPanel(subpanel: 'account' | 'notifications' | 'mentions' | 'dm' | 'favorites' | 'bookmarks'): boolean {
|
||||
if(this.subPanel === subpanel){
|
||||
switch(subpanel){
|
||||
case 'bookmarks':
|
||||
if (this.subPanel === subpanel) {
|
||||
switch (subpanel) {
|
||||
case 'bookmarks':
|
||||
this.bookmarksComp.applyGoToTop();
|
||||
break;
|
||||
case 'notifications':
|
||||
|
@ -149,12 +147,12 @@ export class ManageAccountComponent extends BrowseBase {
|
|||
break;
|
||||
case 'favorites':
|
||||
this.favoritesComp.applyGoToTop();
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.subPanel = subpanel;
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ export class MentionsComponent extends TimelineBase {
|
|||
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)) {
|
||||
if (m && !this.statuses.find(x => x.status.id === m.id)) {
|
||||
let cwPolicy = this.toolsService.checkContentWarning(m);
|
||||
const statusWrapper = new StatusWrapper(cwPolicy.status, this.account, cwPolicy.applyCw, cwPolicy.hide);
|
||||
this.statuses.unshift(statusWrapper);
|
||||
|
@ -82,8 +82,7 @@ export class MentionsComponent extends TimelineBase {
|
|||
}
|
||||
|
||||
protected getNextStatuses(): Promise<Status[]> {
|
||||
console.warn('MENTIONS get next status');
|
||||
return this.mastodonService.getNotifications(this.account, ['follow', 'favourite', 'reblog', 'poll'], this.lastId)
|
||||
return this.mastodonService.getNotifications(this.account, ['follow', 'favourite', 'reblog', 'poll', 'move', 'update'], this.lastId)
|
||||
.then((result: Notification[]) => {
|
||||
const statuses = result.map(x => x.status);
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { MastodonWrapperService } from '../../../../../services/mastodon-wrapper
|
|||
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';
|
||||
import { SettingsService } from '../../../../../services/settings.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-list-editor',
|
||||
|
@ -25,6 +25,7 @@ export class ListEditorComponent implements OnInit {
|
|||
searchOpen: boolean;
|
||||
|
||||
constructor(
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly mastodonService: MastodonWrapperService) { }
|
||||
|
||||
|
@ -69,13 +70,12 @@ export class ListEditorComponent implements OnInit {
|
|||
}
|
||||
|
||||
addEvent(accountWrapper: AccountListWrapper) {
|
||||
console.log(accountWrapper);
|
||||
const settings = this.settingsService.getSettings();
|
||||
|
||||
accountWrapper.isLoading = true;
|
||||
this.mastodonService.getInstance(this.account.info.instance)
|
||||
.then((instance: Instance) => {
|
||||
console.log(instance);
|
||||
if (instance.version.toLowerCase().includes('pleroma')) {
|
||||
if (instance.version.toLowerCase().includes('pleroma') && !settings.autoFollowOnListEnabled) {
|
||||
return Promise.resolve(true);
|
||||
} else {
|
||||
return this.followAccount(accountWrapper);
|
||||
|
|
|
@ -65,6 +65,6 @@
|
|||
|
||||
<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
|
||||
Remove
|
||||
</a>
|
||||
</div>
|
|
@ -10,8 +10,8 @@ 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';
|
||||
import { SettingsService } from '../../../../services/settings.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-account',
|
||||
|
@ -49,8 +49,8 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
|||
private streamChangedSub: Subscription;
|
||||
|
||||
constructor(
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly store: Store,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly navigationService: NavigationService,
|
||||
private readonly mastodonService: MastodonWrapperService,
|
||||
private readonly notificationService: NotificationService) { }
|
||||
|
@ -68,7 +68,7 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
private loadAccountSettings(){
|
||||
this.accountSettings = this.toolsService.getAccountSettings(this.account.info);
|
||||
this.accountSettings = this.settingsService.getAccountSettings(this.account.info);
|
||||
|
||||
this.customStatusLengthEnabled = this.accountSettings.customStatusCharLengthEnabled;
|
||||
this.customStatusLength = this.accountSettings.customStatusCharLength;
|
||||
|
@ -77,13 +77,13 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
|||
|
||||
onCustomLengthEnabledChanged(): boolean {
|
||||
this.accountSettings.customStatusCharLengthEnabled = this.customStatusLengthEnabled;
|
||||
this.toolsService.saveAccountSettings(this.accountSettings);
|
||||
this.settingsService.saveAccountSettings(this.accountSettings);
|
||||
return false;
|
||||
}
|
||||
|
||||
customStatusLengthChanged(event): boolean{
|
||||
this.accountSettings.customStatusCharLength = this.customStatusLength;
|
||||
this.toolsService.saveAccountSettings(this.accountSettings);
|
||||
this.settingsService.saveAccountSettings(this.accountSettings);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -122,6 +122,17 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
})
|
||||
.then(_ => {
|
||||
this.availableLists.sort((a,b) => {
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
});
|
||||
|
@ -203,9 +214,9 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
onDisableAvatarNotificationChanged() {
|
||||
let settings = this.toolsService.getAccountSettings(this.account.info);
|
||||
let settings = this.settingsService.getAccountSettings(this.account.info);
|
||||
settings.disableAvatarNotifications = this.avatarNotificationDisabled;
|
||||
this.toolsService.saveAccountSettings(settings);
|
||||
this.settingsService.saveAccountSettings(settings);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,35 @@
|
|||
<div class="notification">
|
||||
<div *ngIf="notification.type === 'follow_request' && !followRequestProcessed">
|
||||
<div class="stream__notification--icon" title="{{notification.account.acct}}">
|
||||
<fa-icon class="followed" [icon]="faUserClock"></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>
|
||||
submitted a follow request
|
||||
</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 class="follow_request">
|
||||
<a href title="Authorize" class="follow_request__link follow_request__link--check"
|
||||
(click)="acceptFollowRequest()">
|
||||
<fa-icon class="follow_request__icon" [icon]="faCheck"></fa-icon>
|
||||
</a>
|
||||
<a href title="Reject" class="follow_request__link follow_request__link--cross"
|
||||
(click)="refuseFollowRequest()">
|
||||
<fa-icon class="follow_request__icon" [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div *ngIf="notification.type === 'follow'">
|
||||
<div class="stream__notification--icon" title="{{notification.account.acct}}">
|
||||
<fa-icon class="followed" [icon]="faUserPlus"></fa-icon>
|
||||
|
@ -19,12 +50,49 @@
|
|||
</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)"
|
||||
<div *ngIf="notification.type === 'move'">
|
||||
<div class="stream__notification--icon" title="{{notification.account.acct}}">
|
||||
<fa-icon class="followed" [icon]="faTruckMoving"></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>
|
||||
migrated to
|
||||
</div>
|
||||
|
||||
<a href (click)="openAccount(notification.target)" (auxclick)="openUrl(notification.target.url)"
|
||||
class="follow-account" title="{{notification.target.acct}}">
|
||||
<img class="follow-account__avatar" src="{{ notification.target.avatar }}" />
|
||||
<span class="follow-account__display-name" innerHTML="{{ notification.target | accountEmoji }}"></span>
|
||||
<span class="follow-account__acct">@{{ notification.target.acct }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<app-status *ngIf="notification.status && notification.type === 'update'" class="stream__status"
|
||||
[statusWrapper]="notification.status"
|
||||
[notificationAccount]="notification.account"
|
||||
[notificationType]="notification.type"
|
||||
[context]="'notifications'"
|
||||
(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)"
|
||||
<app-status *ngIf="notification.status && notification.type === 'mention'" class="stream__status"
|
||||
[statusWrapper]="notification.status"
|
||||
[context]="'notifications'"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-status>
|
||||
|
||||
<app-status *ngIf="notification.status && notification.type !== 'mention' && notification.type !== 'update'"
|
||||
class="stream__status"
|
||||
[statusWrapper]="notification.status"
|
||||
[notificationAccount]="notification.account"
|
||||
[notificationType]="notification.type"
|
||||
[context]="'notifications'"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-status>
|
||||
|
||||
</div>
|
|
@ -46,6 +46,7 @@
|
|||
color: $boost-color;
|
||||
}
|
||||
|
||||
$acccount-info-left: 70px;
|
||||
.follow-account {
|
||||
padding: 5px;
|
||||
height: 60px;
|
||||
|
@ -62,8 +63,7 @@
|
|||
height: 45px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
$acccount-info-left: 70px;
|
||||
|
||||
&__display-name {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
|
@ -81,5 +81,44 @@
|
|||
left: $acccount-info-left;
|
||||
font-size: 13px;
|
||||
color: $status-links-color;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
width: calc(100% - #{$acccount-info-left});
|
||||
}
|
||||
}
|
||||
|
||||
.follow_request {
|
||||
width: calc(100% - #{$acccount-info-left});
|
||||
margin-left: $acccount-info-left;
|
||||
|
||||
&__link {
|
||||
display: inline-block;
|
||||
width: calc(50%);
|
||||
padding: 2px;
|
||||
|
||||
text-align: center;
|
||||
color: rgb(182, 182, 182);
|
||||
transition: all .2s;
|
||||
|
||||
// outline: 1px dotted greenyellow;
|
||||
|
||||
&--check {
|
||||
&:hover {
|
||||
color: greenyellow;
|
||||
}
|
||||
}
|
||||
|
||||
&--cross {
|
||||
&:hover {
|
||||
color: orangered;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
|
@ -1,22 +1,31 @@
|
|||
import { Component, Input } from '@angular/core';
|
||||
import { faUserPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faUserPlus, faUserClock, faCheck, faTimes, faTruckMoving } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { NotificationWrapper } from '../notifications.component';
|
||||
import { ToolsService } from '../../../../../services/tools.service';
|
||||
import { Account } from '../../../../../services/models/mastodon.interfaces';
|
||||
import { BrowseBase } from '../../../../../components/common/browse-base';
|
||||
import { MastodonWrapperService } from '../../../../../services/mastodon-wrapper.service';
|
||||
import { NotificationService } from '../../../../../services/notification.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-notification',
|
||||
templateUrl: './notification.component.html',
|
||||
styleUrls: ['./notification.component.scss']
|
||||
})
|
||||
export class NotificationComponent extends BrowseBase {
|
||||
export class NotificationComponent extends BrowseBase {
|
||||
faUserPlus = faUserPlus;
|
||||
faUserClock = faUserClock;
|
||||
faCheck = faCheck;
|
||||
faTimes = faTimes;
|
||||
faTruckMoving = faTruckMoving;
|
||||
|
||||
@Input() notification: NotificationWrapper;
|
||||
|
||||
constructor(private readonly toolsService: ToolsService) {
|
||||
constructor(
|
||||
private readonly notificationsService: NotificationService,
|
||||
private readonly mastodonService: MastodonWrapperService,
|
||||
private readonly toolsService: ToolsService) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
@ -31,9 +40,47 @@ export class NotificationComponent extends BrowseBase {
|
|||
this.browseAccountEvent.next(accountName);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
openUrl(url: string): boolean {
|
||||
window.open(url, '_blank');
|
||||
return false;
|
||||
}
|
||||
|
||||
followRequestWorking: boolean;
|
||||
followRequestProcessed: boolean;
|
||||
acceptFollowRequest(): boolean {
|
||||
if(this.followRequestWorking) return false;
|
||||
this.followRequestWorking = true;
|
||||
|
||||
this.mastodonService.authorizeFollowRequest(this.notification.provider, this.notification.notification.account.id)
|
||||
.then(res => {
|
||||
this.followRequestProcessed = true;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationsService.notifyHttpError(err, this.notification.provider);
|
||||
})
|
||||
.then(res => {
|
||||
this.followRequestWorking = false;
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
refuseFollowRequest(): boolean {
|
||||
if(this.followRequestWorking) return false;
|
||||
this.followRequestWorking = true;
|
||||
|
||||
this.mastodonService.rejectFollowRequest(this.notification.provider, this.notification.notification.account.id)
|
||||
.then(res => {
|
||||
this.followRequestProcessed = true;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationsService.notifyHttpError(err, this.notification.provider);
|
||||
})
|
||||
.then(res => {
|
||||
this.followRequestWorking = false;
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -152,17 +152,22 @@ export class NotificationWrapper {
|
|||
case 'reblog':
|
||||
case 'favourite':
|
||||
case 'poll':
|
||||
case 'update':
|
||||
this.status = new StatusWrapper(notification.status, provider, applyCw, hideStatus);
|
||||
break;
|
||||
}
|
||||
this.account = notification.account;
|
||||
this.target = notification.target;
|
||||
this.wrapperId = `${this.type}-${notification.id}`;
|
||||
this.notification = notification;
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
provider: AccountInfo;
|
||||
notification: Notification;
|
||||
wrapperId: string;
|
||||
account: Account;
|
||||
target: Account;
|
||||
status: StatusWrapper;
|
||||
type: 'mention' | 'reblog' | 'favourite' | 'follow' | 'poll';
|
||||
type: 'mention' | 'reblog' | 'favourite' | 'follow' | 'poll' | 'follow_request' | 'move' | 'update';
|
||||
}
|
|
@ -4,8 +4,8 @@
|
|||
<h3 class="panel__title">search</h3>
|
||||
|
||||
<form class="form-section" (ngSubmit)="onSubmit()">
|
||||
<input type="text" class="form-control form-control-sm form-with-button" [(ngModel)]="searchHandle"
|
||||
name="searchHandle" placeholder="Search" autocomplete="off" />
|
||||
<input #search type="text" class="form-control form-control-sm form-with-button" [(ngModel)]="searchHandle"
|
||||
name="searchHandle" placeholder="Search" autocomplete="off" (keydown.escape)="search.blur()"/>
|
||||
<button class="form-button" type="submit" title="search">GO</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
|
||||
|
@ -26,12 +26,15 @@ export class SearchComponent implements OnInit {
|
|||
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
|
||||
@ViewChild('search') searchElement: ElementRef;
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mastodonService: MastodonWrapperService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.searchElement.nativeElement.focus();
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
</form>
|
||||
<a href class="form-button sound__play" type="submit" (click)="playNotificationSound()">play</a>
|
||||
</div>
|
||||
|
||||
<h4 class="panel__subtitle">Shortcuts</h4>
|
||||
<div class="sub-section">
|
||||
<span class="sub-section__title">switch column:</span><br />
|
||||
|
@ -50,6 +51,53 @@
|
|||
<br>
|
||||
</div>
|
||||
|
||||
<h4 class="panel__subtitle">Languages</h4>
|
||||
<div class="sub-section">
|
||||
<div class="sub-section__content">
|
||||
<div *ngIf="!configuredLangs || configuredLangs.length === 0" class="language__warning">
|
||||
No language set.
|
||||
</div>
|
||||
<div *ngFor="let l of configuredLangs" class="language__entry">
|
||||
<span class="language__entry__name">{{ l.name }} ({{l.iso639}})</span>
|
||||
<a href (click)="onRemoveLang(l)" class="form-button language__entry__action sound__play">remove</a>
|
||||
</div>
|
||||
<input type="text" (input)="onSearchLang($event.target.value)" [(ngModel)]="searchLang"
|
||||
placeholder="Find Language" autocomplete="off"
|
||||
class="form-control form-control-sm language__search" />
|
||||
<div *ngFor="let l of searchedLangs" class="language__entry">
|
||||
<span class="language__entry__name">{{ l.name }} ({{l.iso639}})</span>
|
||||
<a href (click)="onAddLang(l)" class="form-button language__entry__action sound__play">add</a>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<input class="sub-section__checkbox" [(ngModel)]="disableLangAutodetectEnabled"
|
||||
(change)="onDisableLangAutodetectChanged()" type="checkbox" name="disableLangAutodetec"
|
||||
value="disableLangAutodetec" id="disableLangAutodetec">
|
||||
<label class="noselect sub-section__label" for="disableLangAutodetec">disable language autodetection</label>
|
||||
</div>
|
||||
<h4 class="panel__subtitle">Twitter Bridge</h4>
|
||||
<div class="sub-section">
|
||||
<input class="sub-section__checkbox" [(ngModel)]="twitterBridgeEnabled"
|
||||
(change)="onTwitterBridgeEnabledChanged()" type="checkbox" name="onTwitterBridgeEnabled"
|
||||
value="onTwitterBridgeEnabled" id="onTwitterBridgeEnabled">
|
||||
<label class="noselect sub-section__label" for="onTwitterBridgeEnabled">enable bridge</label>
|
||||
<br>
|
||||
<div *ngIf="twitterBridgeEnabled">
|
||||
<p>Please provide your bridge instance:
|
||||
<input type="text" class="form-control form-control-sm sub_section__text-input"
|
||||
[(ngModel)]="setTwitterBridgeInstance" placeholder="bridge.tld" />
|
||||
If you don't know any, consider using <a href="https://github.com/NicolasConstant/BirdsiteLive"
|
||||
target="_blank" class="version__link">BirdsiteLIVE</a>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://github.com/NicolasConstant/sengi/wiki/BirdsiteLIVE-integration" target="_blank"
|
||||
class="version__link">What is this?</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="panel__subtitle">Content-Warning Policies</h4>
|
||||
<div class="sub-section">
|
||||
<span class="sub-section__title">global behavior:</span><br />
|
||||
|
@ -60,7 +108,7 @@
|
|||
|
||||
<input class="sub-section__checkbox" [checked]="contentWarningPolicy === 2" (change)="onCwPolicyChange(2)"
|
||||
type="radio" name="cw-hide-all" value="cw-hide-all" id="cw-hide-all">
|
||||
<label class="noselect sub-section__label" for="cw-hide-all">Hide all CWs</label>
|
||||
<label class="noselect sub-section__label" for="cw-hide-all">Expand all CWs</label>
|
||||
<br>
|
||||
<div class="sub-section__cw-settings" *ngIf="contentWarningPolicy === 2">
|
||||
<span class="sub-section__title">but add CW on content containing:</span><br />
|
||||
|
@ -117,6 +165,12 @@
|
|||
<label class="noselect sub-section__label" for="timelineheader-5">Title</label>
|
||||
<br>
|
||||
|
||||
<input class="sub-section__checkbox" [checked]="timeLineHeader === 6" (change)="onTimeLineHeaderChange(6)"
|
||||
type="radio" name="timelineheader-6" value="timelineheader-6" id="timelineheader-6">
|
||||
<label class="noselect sub-section__label" for="timelineheader-6">Title - Account Icon - Username - Domain
|
||||
Name</label>
|
||||
<br>
|
||||
|
||||
<span class="sub-section__title">loading behavior:</span><br />
|
||||
|
||||
<input class="sub-section__checkbox" [checked]="timeLineMode === 1" (change)="onTimeLineModeChange(1)"
|
||||
|
@ -135,6 +189,18 @@
|
|||
<br>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hasPleromaAccount">
|
||||
<h4 class="panel__subtitle">Pleroma</h4>
|
||||
<div class="sub-section">
|
||||
<input class="sub-section__checkbox" [(ngModel)]="autoFollowOnListEnabled"
|
||||
(change)="onAutoFollowOnListChanged()" type="checkbox" name="onAutoFollowOnListChanged"
|
||||
value="onAutoFollowOnListChanged" id="onAutoFollowOnListChanged">
|
||||
<label class="noselect sub-section__label" for="onAutoFollowOnListChanged">autofollow accounts when
|
||||
adding to list</label>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="panel__subtitle">Other</h4>
|
||||
<div class="sub-section">
|
||||
<input class="sub-section__checkbox" [(ngModel)]="disableRemoteStatusFetchingEnabled"
|
||||
|
@ -143,11 +209,27 @@
|
|||
<label class="noselect sub-section__label" for="disableRemoteFetching">disable remote status
|
||||
fetching</label>
|
||||
<br>
|
||||
|
||||
<input class="sub-section__checkbox" [(ngModel)]="enableAltLabelEnabled"
|
||||
(change)="onEnableAltLabelChanged()" type="checkbox" name="enableAltLabel"
|
||||
value="enableAltLabel" id="enableAltLabel">
|
||||
<label class="noselect sub-section__label" for="enableAltLabel">enable alt label</label>
|
||||
<br>
|
||||
|
||||
<input class="sub-section__checkbox" [(ngModel)]="enableFreezeAvatarEnabled"
|
||||
(change)="onEnableFreezeAvatarChanged()" type="checkbox" name="enableFreezeAvatar"
|
||||
value="enableFreezeAvatar" id="enableFreezeAvatar">
|
||||
<label class="noselect sub-section__label" for="enableFreezeAvatar">freeze animated avatar</label>
|
||||
<br>
|
||||
|
||||
reorder account's icons: <a href class="toogle-lock-icon-menu" (click)="toogleLockIconMenu()"><span *ngIf="iconMenuLocked">Unlock Icons</span><span *ngIf="!iconMenuLocked">Lock Icons</span></a>
|
||||
</div>
|
||||
|
||||
<h4 class="panel__subtitle">About</h4>
|
||||
<p class="version">
|
||||
Sengi version: {{version}}<br />
|
||||
<a href class="version__link" (click)="openTutorial()">open tutorial</a><br />
|
||||
<a href="assets/docs/privacy.html" class="version__link" target="_blank">imprint & privacy</a><br />
|
||||
<a href class="version__link" (click)="checkForUpdates()">check for updates</a>
|
||||
<app-waiting-animation *ngIf="isCheckingUpdates" class="waiting-icon"></app-waiting-animation>
|
||||
</p>
|
||||
|
|
|
@ -31,6 +31,13 @@
|
|||
padding: 0 5px 15px 5px;
|
||||
position: relative;
|
||||
|
||||
&__content {
|
||||
display: block;
|
||||
padding: 0 0 0 5px;
|
||||
|
||||
// outline: 1px dotted greenyellow;
|
||||
}
|
||||
|
||||
&__checkbox {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
|
@ -68,6 +75,41 @@
|
|||
}
|
||||
}
|
||||
|
||||
.language {
|
||||
&__warning {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
&__entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
|
||||
&:not(:last-child){
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
display: block;
|
||||
align-items: stretch;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
&__action {
|
||||
align-items: stretch;
|
||||
min-width: 70px;
|
||||
text-align: center;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__search {
|
||||
display: block;
|
||||
margin: 5px 0 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: 1px solid $settings-text-input-border;
|
||||
color: $settings-text-input-foreground;
|
||||
|
@ -111,4 +153,22 @@
|
|||
background-color: #32384d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toogle-lock-icon-menu {
|
||||
display: block;
|
||||
padding: 3px 40px;
|
||||
width: 170px;
|
||||
|
||||
float: right;
|
||||
|
||||
text-align: center;
|
||||
|
||||
color: white;
|
||||
background-color: #1f2330;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: #32384d;
|
||||
}
|
||||
}
|
|
@ -1,13 +1,17 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { Howl } from 'howler';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { ToolsService } from '../../../services/tools.service';
|
||||
import { ToolsService, InstanceType } from '../../../services/tools.service';
|
||||
import { UserNotificationService, NotificationSoundDefinition } from '../../../services/user-notification.service';
|
||||
import { ServiceWorkerService } from '../../../services/service-worker.service';
|
||||
import { ContentWarningPolicy, ContentWarningPolicyEnum, TimeLineModeEnum, TimeLineHeaderEnum } from '../../../states/settings.state';
|
||||
import { ContentWarningPolicy, ContentWarningPolicyEnum, TimeLineModeEnum, TimeLineHeaderEnum, ILanguage } from '../../../states/settings.state';
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { NavigationService } from '../../../services/navigation.service';
|
||||
import { SettingsService } from '../../../services/settings.service';
|
||||
import { LanguageService } from '../../../services/language.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
|
@ -15,7 +19,7 @@ import { NotificationService } from '../../../services/notification.service';
|
|||
styleUrls: ['./settings.component.scss']
|
||||
})
|
||||
|
||||
export class SettingsComponent implements OnInit {
|
||||
export class SettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
notificationSounds: NotificationSoundDefinition[];
|
||||
notificationSoundId: string;
|
||||
|
@ -25,13 +29,25 @@ export class SettingsComponent implements OnInit {
|
|||
disableRemoteStatusFetchingEnabled: boolean;
|
||||
disableAvatarNotificationsEnabled: boolean;
|
||||
disableSoundsEnabled: boolean;
|
||||
disableLangAutodetectEnabled: boolean;
|
||||
enableAltLabelEnabled: boolean;
|
||||
enableFreezeAvatarEnabled: boolean;
|
||||
version: string;
|
||||
|
||||
hasPleromaAccount: boolean;
|
||||
autoFollowOnListEnabled: boolean;
|
||||
|
||||
twitterBridgeEnabled: boolean;
|
||||
|
||||
columnShortcutEnabled: ColumnShortcut = ColumnShortcut.Ctrl;
|
||||
timeLineHeader: TimeLineHeaderEnum = TimeLineHeaderEnum.Title_DomainName;
|
||||
timeLineMode: TimeLineModeEnum = TimeLineModeEnum.OnTop;
|
||||
contentWarningPolicy: ContentWarningPolicyEnum = ContentWarningPolicyEnum.None;
|
||||
|
||||
configuredLangs: ILanguage[] = [];
|
||||
searchedLangs: ILanguage[] = [];
|
||||
searchLang: string;
|
||||
|
||||
private addCwOnContent: string;
|
||||
set setAddCwOnContent(value: string) {
|
||||
this.setCwPolicy(null, value, null, null);
|
||||
|
@ -59,17 +75,38 @@ export class SettingsComponent implements OnInit {
|
|||
return this.contentHidedCompletely;
|
||||
}
|
||||
|
||||
private twitterBridgeInstance: string;
|
||||
set setTwitterBridgeInstance(value: string) {
|
||||
let instance = value.replace('https://', '').replace('http://', '').replace('/', '').trim();
|
||||
this.setBridgeInstance(instance);
|
||||
this.twitterBridgeInstance = instance;
|
||||
}
|
||||
get setTwitterBridgeInstance(): string {
|
||||
return this.twitterBridgeInstance;
|
||||
}
|
||||
|
||||
private languageSub: Subscription;
|
||||
|
||||
constructor(
|
||||
private readonly languageService: LanguageService,
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly navigationService: NavigationService,
|
||||
private formBuilder: FormBuilder,
|
||||
private serviceWorkersService: ServiceWorkerService,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly userNotificationsService: UserNotificationService) { }
|
||||
private readonly userNotificationsService: UserNotificationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.languageSub = this.languageService.configuredLanguagesChanged.subscribe(l => {
|
||||
if(l){
|
||||
this.configuredLangs = l;
|
||||
}
|
||||
});
|
||||
|
||||
this.version = environment.VERSION;
|
||||
|
||||
const settings = this.toolsService.getSettings();
|
||||
const settings = this.settingsService.getSettings();
|
||||
|
||||
this.notificationSounds = this.userNotificationsService.getAllNotificationSounds();
|
||||
this.notificationSoundId = settings.notificationSoundFileId;
|
||||
|
@ -95,33 +132,86 @@ export class SettingsComponent implements OnInit {
|
|||
|
||||
this.timeLineHeader = settings.timelineHeader;
|
||||
this.timeLineMode = settings.timelineMode;
|
||||
|
||||
this.autoFollowOnListEnabled = settings.autoFollowOnListEnabled;
|
||||
const accs = this.toolsService.getAllAccounts();
|
||||
accs.forEach(a => {
|
||||
this.toolsService.getInstanceInfo(a)
|
||||
.then(ins => {
|
||||
if(ins.type === InstanceType.Pleroma){
|
||||
this.hasPleromaAccount = true;
|
||||
}
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
});
|
||||
|
||||
this.twitterBridgeEnabled = settings.twitterBridgeEnabled;
|
||||
this.twitterBridgeInstance = settings.twitterBridgeInstance;
|
||||
|
||||
this.configuredLangs = this.languageService.getConfiguredLanguages();
|
||||
this.disableLangAutodetectEnabled = settings.disableLangAutodetec;
|
||||
this.enableAltLabelEnabled = settings.enableAltLabel;
|
||||
this.enableFreezeAvatarEnabled = settings.enableFreezeAvatar;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if(this.languageSub) this.languageSub.unsubscribe();
|
||||
}
|
||||
|
||||
iconMenuLocked = true;
|
||||
toogleLockIconMenu(): boolean {
|
||||
this.navigationService.changeIconMenuState(this.iconMenuLocked);
|
||||
this.iconMenuLocked = ! this.iconMenuLocked;
|
||||
return false;
|
||||
}
|
||||
|
||||
onSearchLang(input: string) {
|
||||
this.searchedLangs = this.languageService.searchLanguage(input);
|
||||
}
|
||||
|
||||
onAddLang(lang: ILanguage): boolean {
|
||||
if(this.configuredLangs.findIndex(x => x.iso639 === lang.iso639) >= 0) return false;
|
||||
|
||||
// this.configuredLangs.push(lang);
|
||||
this.languageService.addLanguage(lang);
|
||||
|
||||
this.searchLang = '';
|
||||
this.searchedLangs.length = 0;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onRemoveLang(lang: ILanguage): boolean {
|
||||
// this.configuredLangs = this.configuredLangs.filter(x => x.iso639 !== lang.iso639);
|
||||
this.languageService.removeLanguage(lang);
|
||||
return false;
|
||||
}
|
||||
|
||||
onShortcutChange(id: ColumnShortcut) {
|
||||
this.columnShortcutEnabled = id;
|
||||
this.notifyRestartNeeded();
|
||||
|
||||
let settings = this.toolsService.getSettings();
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.columnSwitchingWinAlt = id === ColumnShortcut.Win;
|
||||
this.toolsService.saveSettings(settings);
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onTimeLineHeaderChange(id: TimeLineHeaderEnum){
|
||||
this.timeLineHeader = id;
|
||||
this.notifyRestartNeeded();
|
||||
|
||||
let settings = this.toolsService.getSettings();
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.timelineHeader = id;
|
||||
this.toolsService.saveSettings(settings);
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onTimeLineModeChange(id: TimeLineModeEnum){
|
||||
this.timeLineMode = id;
|
||||
this.notifyRestartNeeded();
|
||||
|
||||
let settings = this.toolsService.getSettings();
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.timelineMode = id;
|
||||
this.toolsService.saveSettings(settings);
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onCwPolicyChange(id: ContentWarningPolicyEnum) {
|
||||
|
@ -133,7 +223,7 @@ export class SettingsComponent implements OnInit {
|
|||
|
||||
private setCwPolicy(id: ContentWarningPolicyEnum = null, addCw: string = null, removeCw: string = null, hide: string = null){
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.toolsService.getSettings();
|
||||
let settings = this.settingsService.getSettings();
|
||||
let cwPolicySettings = new ContentWarningPolicy();
|
||||
|
||||
if(id !== null){
|
||||
|
@ -160,13 +250,19 @@ export class SettingsComponent implements OnInit {
|
|||
cwPolicySettings.hideCompletlyContent = settings.contentWarningPolicy.hideCompletlyContent;
|
||||
}
|
||||
|
||||
this.toolsService.saveContentWarningPolicy(cwPolicySettings);
|
||||
}
|
||||
this.settingsService.saveContentWarningPolicy(cwPolicySettings);
|
||||
}
|
||||
|
||||
private splitCwValues(data: string): string[]{
|
||||
return data.split(';').map(x => x.trim().toLowerCase()).filter((value, index, self) => self.indexOf(value) === index).filter(y => y !== '');
|
||||
}
|
||||
|
||||
private setBridgeInstance(instance: string){
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.twitterBridgeInstance = instance;
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
// reload(): boolean {
|
||||
// window.location.reload();
|
||||
// return false;
|
||||
|
@ -174,9 +270,9 @@ export class SettingsComponent implements OnInit {
|
|||
|
||||
onChange(soundId: string) {
|
||||
this.notificationSoundId = soundId;
|
||||
let settings = this.toolsService.getSettings()
|
||||
let settings = this.settingsService.getSettings()
|
||||
settings.notificationSoundFileId = soundId;
|
||||
this.toolsService.saveSettings(settings);
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
playNotificationSound(): boolean {
|
||||
|
@ -190,31 +286,64 @@ export class SettingsComponent implements OnInit {
|
|||
return false;
|
||||
}
|
||||
|
||||
onEnableFreezeAvatarChanged(){
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.enableFreezeAvatar = this.enableFreezeAvatarEnabled;
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onEnableAltLabelChanged(){
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.enableAltLabel = this.enableAltLabelEnabled;
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onDisableLangAutodetectChanged() {
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.disableLangAutodetec = this.disableLangAutodetectEnabled;
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onDisableAutofocusChanged() {
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.toolsService.getSettings();
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.disableAutofocus = this.disableAutofocusEnabled;
|
||||
this.toolsService.saveSettings(settings);
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onDisableRemoteStatusFetchingChanged() {
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.toolsService.getSettings();
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.disableRemoteStatusFetching = this.disableRemoteStatusFetchingEnabled;
|
||||
this.toolsService.saveSettings(settings);
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onDisableAvatarNotificationsChanged() {
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.toolsService.getSettings();
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.disableAvatarNotifications = this.disableAvatarNotificationsEnabled;
|
||||
this.toolsService.saveSettings(settings);
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onDisableSoundsEnabledChanged() {
|
||||
let settings = this.toolsService.getSettings();
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.disableSounds = this.disableSoundsEnabled;
|
||||
this.toolsService.saveSettings(settings);
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onAutoFollowOnListChanged(){
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.autoFollowOnListEnabled = this.autoFollowOnListEnabled;
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onTwitterBridgeEnabledChanged(){
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.twitterBridgeEnabled = this.twitterBridgeEnabled;
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
isCleanningAll: boolean = false;
|
||||
|
@ -250,6 +379,12 @@ export class SettingsComponent implements OnInit {
|
|||
notifyRestartNeeded(){
|
||||
this.notificationService.notifyRestartNotification('Reload to apply changes');
|
||||
}
|
||||
|
||||
openTutorial(): boolean {
|
||||
localStorage.setItem('tutorial', JSON.stringify(false));
|
||||
this.navigationService.closePanel();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
enum ColumnShortcut {
|
||||
|
|
|
@ -8,27 +8,36 @@
|
|||
<fa-icon [icon]="faSearch"></fa-icon>
|
||||
</a>
|
||||
|
||||
<div *ngFor="let account of accounts">
|
||||
<app-account-icon [account]="account" (toogleAccountNotify)="onToogleAccountNotify($event)"
|
||||
(openMenuNotify)="onOpenMenuNotify($event)">
|
||||
</app-account-icon>
|
||||
<div *ngIf="!iconMenuIsDraggable">
|
||||
<div *ngFor="let account of accounts">
|
||||
<app-account-icon [account]="account" (toogleAccountNotify)="onToogleAccountNotify($event)"
|
||||
(openMenuNotify)="onOpenMenuNotify($event)">
|
||||
</app-account-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="iconMenuIsDraggable" cdkDropList [cdkDropListData]="accounts" (cdkDropListDropped)="onDrop($event)">
|
||||
<div *ngFor="let account of accounts" cdkDrag class="draggable">
|
||||
<fa-icon class="draggable__icon" [icon]="faArrowsAltV"></fa-icon>
|
||||
<img class="draggable__avatar" src="{{ account.avatar }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<a class="left-bar-button left-bar-button--add left-bar-link" [ngClass]="{'no-accounts': hasAccounts === false }"
|
||||
href title="add new account" (click)="addNewAccount()" (contextmenu)="addNewAccount()">
|
||||
<fa-icon [icon]="faPlus"></fa-icon>
|
||||
</a>
|
||||
|
||||
|
||||
<a class="left-bar-button left-bar-button--scheduled left-bar-button--bottom left-bar-link" href title="scheduled statuses"
|
||||
*ngIf="hasAccounts && hasScheduledStatuses"
|
||||
(click)="openScheduledStatuses()"
|
||||
<a class="left-bar-button left-bar-button--scheduled left-bar-button--bottom left-bar-link" href
|
||||
title="scheduled statuses" *ngIf="hasAccounts && hasScheduledStatuses" (click)="openScheduledStatuses()"
|
||||
(contextmenu)="openScheduledStatuses()">
|
||||
<fa-icon [icon]="faCalendarAlt"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a class="left-bar-button left-bar-button--cog left-bar-button--bottom left-bar-link" href title="settings" (click)="openSettings()"
|
||||
(contextmenu)="openSettings()">
|
||||
<a class="left-bar-button left-bar-button--cog left-bar-button--bottom left-bar-link" href title="settings"
|
||||
(click)="openSettings()" (contextmenu)="openSettings()">
|
||||
<fa-icon [icon]="faCog"></fa-icon>
|
||||
</a>
|
||||
</div>
|
|
@ -82,4 +82,38 @@ $height-button: 40px;
|
|||
.no-accounts {
|
||||
padding-top: 10px;
|
||||
// color: cornflowerblue;
|
||||
}
|
||||
|
||||
|
||||
$draggable-accent-color: #47e927;
|
||||
// $draggable-accent-color: #a8ff97;
|
||||
.draggable {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: auto;
|
||||
margin-bottom: 5px;
|
||||
|
||||
border: 2px solid #df0adf;
|
||||
border: 2px solid $draggable-accent-color;
|
||||
border-radius: 2px;
|
||||
|
||||
position: relative;
|
||||
|
||||
&__avatar {
|
||||
width: calc(100%);
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
position: absolute;
|
||||
|
||||
float: left;
|
||||
z-index: 5;
|
||||
color:$draggable-accent-color;
|
||||
|
||||
top: 6px;
|
||||
left: 12px;
|
||||
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
|
@ -1,16 +1,18 @@
|
|||
import { Component, OnInit, OnDestroy } from "@angular/core";
|
||||
import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop";
|
||||
import { Subscription, Observable } from "rxjs";
|
||||
import { Store } from "@ngxs/store";
|
||||
import { faPlus, faCog, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faPlus, faCog, faSearch, faArrowsAltV } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCommentAlt, faCalendarAlt } from "@fortawesome/free-regular-svg-icons";
|
||||
import { HotkeysService, Hotkey } from 'angular2-hotkeys';
|
||||
|
||||
import { AccountWrapper } from "../../models/account.models";
|
||||
import { AccountInfo, SelectAccount } from "../../states/accounts.state";
|
||||
import { AccountInfo, ReorderAccounts, SelectAccount } from "../../states/accounts.state";
|
||||
import { NavigationService, LeftPanelType } from "../../services/navigation.service";
|
||||
import { UserNotificationService, UserNotification } from '../../services/user-notification.service';
|
||||
import { ToolsService } from '../../services/tools.service';
|
||||
import { ScheduledStatusService, ScheduledStatusNotification } from '../../services/scheduled-status.service';
|
||||
import { SettingsService } from '../../services/settings.service';
|
||||
|
||||
@Component({
|
||||
selector: "app-left-side-bar",
|
||||
|
@ -23,6 +25,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
|||
faPlus = faPlus;
|
||||
faCog = faCog;
|
||||
faCalendarAlt = faCalendarAlt;
|
||||
faArrowsAltV = faArrowsAltV;
|
||||
|
||||
accounts: AccountWithNotificationWrapper[] = [];
|
||||
hasAccounts: boolean;
|
||||
|
@ -32,8 +35,10 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
|||
private accountSub: Subscription;
|
||||
private scheduledSub: Subscription;
|
||||
private notificationSub: Subscription;
|
||||
private draggableIconMenuSub: Subscription;
|
||||
|
||||
constructor(
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly hotkeysService: HotkeysService,
|
||||
private readonly scheduledStatusService: ScheduledStatusService,
|
||||
private readonly toolsService: ToolsService,
|
||||
|
@ -101,7 +106,13 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
iconMenuIsDraggable = false;
|
||||
|
||||
ngOnInit() {
|
||||
this.draggableIconMenuSub = this.navigationService.enableDraggableIconMenu.subscribe(x => {
|
||||
this.iconMenuIsDraggable = x;
|
||||
});
|
||||
|
||||
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
|
||||
if (accounts) {
|
||||
//Update and Add
|
||||
|
@ -133,7 +144,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
|
||||
this.notificationSub = this.userNotificationServiceService.userNotifications.subscribe((notifications: UserNotification[]) => {
|
||||
const settings = this.toolsService.getSettings();
|
||||
const settings = this.settingsService.getSettings();
|
||||
notifications.forEach((notification: UserNotification) => {
|
||||
const acc = this.accounts.find(x => x.info.id === notification.account.id);
|
||||
|
||||
|
@ -162,6 +173,17 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
|||
this.accountSub.unsubscribe();
|
||||
this.notificationSub.unsubscribe();
|
||||
this.scheduledSub.unsubscribe();
|
||||
this.draggableIconMenuSub.unsubscribe();
|
||||
}
|
||||
|
||||
onDrop(event: CdkDragDrop<AccountWithNotificationWrapper[]>) {
|
||||
if (event.previousContainer === event.container) {
|
||||
moveItemInArray(event.container.data,
|
||||
event.previousIndex,
|
||||
event.currentIndex);
|
||||
|
||||
this.store.dispatch([new ReorderAccounts(this.accounts.map(x => x.info))])
|
||||
}
|
||||
}
|
||||
|
||||
onToogleAccountNotify(acc: AccountWrapper) {
|
||||
|
|
|
@ -66,7 +66,6 @@ export class MediaViewerComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
private escapeHotkey = new Hotkey('escape', (event: KeyboardEvent): boolean => {
|
||||
console.warn('CLOSE');
|
||||
this.close();
|
||||
return false;
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { NotificationService, NotificatioData } from '../../services/notification.service';
|
||||
import { NotificationService, NotificationData } from '../../services/notification.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-notification-hub',
|
||||
|
@ -12,7 +12,7 @@ export class NotificationHubComponent implements OnInit {
|
|||
constructor(private notificationService: NotificationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.notificationService.notifactionStream.subscribe((notification: NotificatioData) => {
|
||||
this.notificationService.notifactionStream.subscribe((notification: NotificationData) => {
|
||||
let alreadyExistingNotification = this.notifications.find(x => x.avatar === notification.avatar && x.message === notification.message);
|
||||
|
||||
if(alreadyExistingNotification){
|
||||
|
@ -40,13 +40,13 @@ export class NotificationHubComponent implements OnInit {
|
|||
// }, 1500);
|
||||
// }
|
||||
|
||||
onClick(notification: NotificatioData): void{
|
||||
onClick(notification: NotificationData): void{
|
||||
this.notifications = this.notifications.filter(x => x.id !== notification.id);
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationWrapper extends NotificatioData {
|
||||
constructor(data: NotificatioData) {
|
||||
class NotificationWrapper extends NotificationData {
|
||||
constructor(data: NotificationData) {
|
||||
super(data.avatar, data.errorCode, data.message, data.isError);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,15 +2,16 @@
|
|||
<div class="hashtag-header">
|
||||
<a href (click)="goToTop()" class="hashtag-header__gototop" title="go to top">
|
||||
<h3 class="hashtag-header__title">#{{hashtagElement.tag}}</h3>
|
||||
|
||||
<button class="btn-custom-secondary hashtag-header__add-column" (click)="addColumn($event)" title="add column to board">add column</button>
|
||||
<button *ngIf="isHashtagFollowingAvailable && !isFollowingHashtag" class="btn-custom-secondary hashtag-header__follow-button" (click)="followThisHashtag($event)" title="follow hashtag" [disabled]="followingLoading">follow</button>
|
||||
<button *ngIf="isHashtagFollowingAvailable && isFollowingHashtag" class="btn-custom-secondary hashtag-header__follow-button" (click)="unfollowThisHashtag($event)" title="unfollow hashtag" [disabled]="unfollowingLoading">unfollow</button>
|
||||
<button class="btn-custom-secondary hashtag-header__add-column" (click)="addColumn($event)" title="add column to board" [hidden]="columnAdded">add column</button>
|
||||
</a>
|
||||
</div>
|
||||
<app-stream-statuses #appStreamStatuses class="hashtag-stream" *ngIf="hashtagElement"
|
||||
[streamElement]="hashtagElement"
|
||||
<app-stream-statuses #appStreamStatuses class="hashtag-stream" *ngIf="hashtagElement"
|
||||
[streamElement]="hashtagElement"
|
||||
[goToTop]="goToTopSubject.asObservable()"
|
||||
[userLocked]="false"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-stream-statuses>
|
||||
</div>
|
|
@ -40,6 +40,14 @@ $inner-column-size: 320px;
|
|||
border: 1px solid black;
|
||||
color: white;
|
||||
}
|
||||
&__follow-button {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 114px;
|
||||
padding: 0 10px 0 10px;
|
||||
border: 1px solid black;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.hashtag-stream {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { Component, OnInit, Output, EventEmitter, Input, ViewChild, OnDestroy } from '@angular/core';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { Subject, Subscription, Observable } from 'rxjs';
|
||||
import { Store } from '@ngxs/store';
|
||||
|
||||
import { StreamElement, StreamTypeEnum, AddStream } from '../../../states/streams.state';
|
||||
import { OpenThreadEvent, ToolsService } from '../../../services/tools.service';
|
||||
import { StreamStatusesComponent } from '../stream-statuses/stream-statuses.component';
|
||||
import { AccountInfo } from '../../../states/accounts.state';
|
||||
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hashtag',
|
||||
|
@ -21,7 +22,7 @@ export class HashtagComponent implements OnInit, OnDestroy {
|
|||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
|
||||
private _hashtagElement: StreamElement;
|
||||
@Input()
|
||||
@Input()
|
||||
set hashtagElement(hashtagElement: StreamElement){
|
||||
this._hashtagElement = hashtagElement;
|
||||
this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
|
@ -29,7 +30,7 @@ export class HashtagComponent implements OnInit, OnDestroy {
|
|||
get hashtagElement(): StreamElement{
|
||||
return this._hashtagElement;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ViewChild('appStreamStatuses') appStreamStatuses: StreamStatusesComponent;
|
||||
|
||||
|
@ -38,10 +39,25 @@ export class HashtagComponent implements OnInit, OnDestroy {
|
|||
private lastUsedAccount: AccountInfo;
|
||||
private refreshSubscription: Subscription;
|
||||
private goToTopSubscription: Subscription;
|
||||
isHashtagFollowingAvailable: boolean;
|
||||
isFollowingHashtag: boolean;
|
||||
|
||||
private accounts$: Observable<AccountInfo[]>;
|
||||
|
||||
private accountSub: Subscription;
|
||||
|
||||
followingLoading: boolean;
|
||||
unfollowingLoading: boolean;
|
||||
|
||||
columnAdded: boolean;
|
||||
|
||||
constructor(
|
||||
private readonly store: Store,
|
||||
private readonly toolsService: ToolsService) { }
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mastodonService: MastodonWrapperService) {
|
||||
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
|
||||
}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
if(this.refreshEventEmitter) {
|
||||
|
@ -55,11 +71,22 @@ export class HashtagComponent implements OnInit, OnDestroy {
|
|||
this.goToTop();
|
||||
})
|
||||
}
|
||||
this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
this.updateHashtagFollowStatus(this.lastUsedAccount);
|
||||
|
||||
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
|
||||
const selectedAccounts = accounts.filter(x => x.isSelected);
|
||||
if (selectedAccounts.length > 0) {
|
||||
this.lastUsedAccount = selectedAccounts[0];
|
||||
this.updateHashtagFollowStatus(this.lastUsedAccount);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if(this.refreshSubscription) this.refreshSubscription.unsubscribe();
|
||||
if (this.goToTopSubscription) this.goToTopSubscription.unsubscribe();
|
||||
if (this.accountSub) this.accountSub.unsubscribe();
|
||||
}
|
||||
|
||||
goToTop(): boolean {
|
||||
|
@ -74,11 +101,17 @@ export class HashtagComponent implements OnInit, OnDestroy {
|
|||
const newStream = new StreamElement(StreamTypeEnum.tag, `${hashtag}`, this.lastUsedAccount.id, hashtag, null, null, this.lastUsedAccount.instance);
|
||||
this.store.dispatch([new AddStream(newStream)]);
|
||||
|
||||
this.columnAdded = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
refresh(): any {
|
||||
this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
this.updateHashtagFollowStatus(this.lastUsedAccount);
|
||||
if (this.isHashtagFollowingAvailable) {
|
||||
this.checkIfFollowingHashtag(this.lastUsedAccount);
|
||||
}
|
||||
this.appStreamStatuses.refresh();
|
||||
}
|
||||
|
||||
|
@ -95,4 +128,41 @@ export class HashtagComponent implements OnInit, OnDestroy {
|
|||
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
}
|
||||
|
||||
private updateHashtagFollowStatus(account: AccountInfo): void {
|
||||
this.toolsService.getInstanceInfo(account).then(instanceInfo => {
|
||||
if (instanceInfo.major >= 4) {
|
||||
this.isHashtagFollowingAvailable = true;
|
||||
this.checkIfFollowingHashtag(account);
|
||||
} else {
|
||||
this.isHashtagFollowingAvailable = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private checkIfFollowingHashtag(account: AccountInfo): void {
|
||||
this.mastodonService.getHashtag(account, this.hashtagElement.tag).then(tag => {
|
||||
this.isFollowingHashtag = tag.following;
|
||||
});
|
||||
}
|
||||
|
||||
followThisHashtag(event): boolean {
|
||||
this.followingLoading = true;
|
||||
event.stopPropagation();
|
||||
this.mastodonService.followHashtag(this.lastUsedAccount, this.hashtagElement.tag).then(tag => {
|
||||
this.isFollowingHashtag = tag.following;
|
||||
this.followingLoading = false;
|
||||
});
|
||||
return false
|
||||
}
|
||||
|
||||
unfollowThisHashtag(event): boolean {
|
||||
this.unfollowingLoading = true;
|
||||
event.stopPropagation();
|
||||
this.mastodonService.unfollowHashtag(this.lastUsedAccount, this.hashtagElement.tag).then(tag => {
|
||||
this.isFollowingHashtag = tag.following;
|
||||
this.unfollowingLoading = false;
|
||||
});
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,13 +102,13 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
|||
this.statusStateSub = this.statusStateService.stateNotification.subscribe((state: StatusState) => {
|
||||
if (state && state.statusId === this.displayedStatus.url) {
|
||||
|
||||
if (state.isFavorited) {
|
||||
if (state.isFavorited !== null) {
|
||||
this.favoriteStatePerAccountId[state.accountId] = state.isFavorited;
|
||||
}
|
||||
if (state.isRebloged) {
|
||||
if (state.isRebloged !== null) {
|
||||
this.bootedStatePerAccountId[state.accountId] = state.isRebloged;
|
||||
}
|
||||
if (state.isBookmarked) {
|
||||
if (state.isBookmarked !== null) {
|
||||
this.bookmarkStatePerAccountId[state.accountId] = state.isBookmarked;
|
||||
}
|
||||
|
||||
|
@ -138,7 +138,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
|||
private checkStatus(accounts: AccountInfo[]): void {
|
||||
const status = this.statusWrapper.status;
|
||||
const provider = this.statusWrapper.provider;
|
||||
this.selectedAccounts = accounts.filter(x => x.isSelected);
|
||||
this.selectedAccounts = accounts.filter(x => x.isSelected);
|
||||
|
||||
if (!this.statusWrapper.isRemote) {
|
||||
this.isProviderSelected = this.selectedAccounts.filter(x => x.id === provider.id).length > 0;
|
||||
|
@ -184,13 +184,18 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
|||
return false;
|
||||
}
|
||||
|
||||
private boostPromise: Promise<any>;
|
||||
boost(): boolean {
|
||||
if (this.boostIsLoading) return;
|
||||
if (!this.boostPromise) {
|
||||
this.boostPromise = Promise.resolve(true);
|
||||
}
|
||||
|
||||
this.boostIsLoading = true;
|
||||
const account = this.toolsService.getSelectedAccounts()[0];
|
||||
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
|
||||
usableStatus
|
||||
this.boostPromise = this.boostPromise
|
||||
.then(() => {
|
||||
this.boostIsLoading = true;
|
||||
return this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
|
||||
})
|
||||
.then((status: Status) => {
|
||||
if (this.isBoosted && status.reblogged) {
|
||||
return this.mastodonService.unreblog(account, status);
|
||||
|
@ -219,18 +224,24 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
|||
.then(() => {
|
||||
this.statusStateService.statusReblogStatusChanged(this.displayedStatus.url, account.id, this.bootedStatePerAccountId[account.id]);
|
||||
this.boostIsLoading = false;
|
||||
this.boostPromise = null;
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private favoritePromise: Promise<any>;
|
||||
favorite(): boolean {
|
||||
if (this.favoriteIsLoading) return;
|
||||
if (!this.favoritePromise) {
|
||||
this.favoritePromise = Promise.resolve(true);
|
||||
}
|
||||
|
||||
this.favoriteIsLoading = true;
|
||||
const account = this.toolsService.getSelectedAccounts()[0];
|
||||
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
|
||||
usableStatus
|
||||
this.favoritePromise = this.favoritePromise
|
||||
.then(() => {
|
||||
this.favoriteIsLoading = true;
|
||||
return this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
|
||||
})
|
||||
.then((status: Status) => {
|
||||
if (this.isFavorited && status.favourited) {
|
||||
return this.mastodonService.unfavorite(account, status);
|
||||
|
@ -254,19 +265,24 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
|||
.then(() => {
|
||||
this.statusStateService.statusFavoriteStatusChanged(this.displayedStatus.url, account.id, this.favoriteStatePerAccountId[account.id]);
|
||||
this.favoriteIsLoading = false;
|
||||
this.favoritePromise = null;
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bookmarkPromise: Promise<any>;
|
||||
bookmark(): boolean {
|
||||
if (this.bookmarkingIsLoading) return;
|
||||
|
||||
this.bookmarkingIsLoading = true;
|
||||
if (!this.bookmarkPromise) {
|
||||
this.bookmarkPromise = Promise.resolve(true);
|
||||
}
|
||||
|
||||
const account = this.toolsService.getSelectedAccounts()[0];
|
||||
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
|
||||
usableStatus
|
||||
this.bookmarkPromise = this.bookmarkPromise
|
||||
.then(() => {
|
||||
this.bookmarkingIsLoading = true;
|
||||
return this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
|
||||
})
|
||||
.then((status: Status) => {
|
||||
if (this.isBookmarked && status.bookmarked) {
|
||||
return this.mastodonService.unbookmark(account, status);
|
||||
|
@ -290,15 +306,9 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
|||
.then(() => {
|
||||
this.statusStateService.statusBookmarkStatusChanged(this.displayedStatus.url, account.id, this.bookmarkStatePerAccountId[account.id]);
|
||||
this.bookmarkingIsLoading = false;
|
||||
this.bookmarkPromise = null;
|
||||
});
|
||||
|
||||
|
||||
|
||||
// setTimeout(() => {
|
||||
// this.isBookmarked = !this.isBookmarked;
|
||||
// this.bookmarkingIsLoading = false;
|
||||
// }, 2000);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -332,13 +342,9 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
private checkIfBookmarksAreAvailable(account: AccountInfo) {
|
||||
this.toolsService.getInstanceInfo(account)
|
||||
.then((instance: InstanceInfo) => {
|
||||
if (instance.major >= 3 && instance.minor >= 1) {
|
||||
this.isBookmarksAvailable = true;
|
||||
} else {
|
||||
this.isBookmarksAvailable = false;
|
||||
}
|
||||
this.toolsService.isBookmarksAreAvailable(account)
|
||||
.then((isAvailable: boolean) => {
|
||||
this.isBookmarksAvailable = isAvailable;
|
||||
})
|
||||
.catch(err => {
|
||||
this.isBookmarksAvailable = false;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<a href class="context-menu-link" (click)="onContextMenu($event)"
|
||||
<a href class="context-menu-link" (click)="onContextMenu($event)"
|
||||
[class.context-menu-link__status]="statusWrapper"
|
||||
[class.context-menu-link__profile]="displayedAccount"
|
||||
title="More">
|
||||
|
@ -27,19 +27,42 @@
|
|||
<ng-template contextMenuItem (execute)="unmuteConversation()" *ngIf="statusWrapper && isOwnerSelected && displayedStatus.muted">
|
||||
Unmute conversation
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem divider="true"></ng-template>
|
||||
<ng-template contextMenuItem (execute)="muteAccount()" *ngIf="!isOwnerSelected">
|
||||
<ng-template contextMenuItem (execute)="hideBoosts()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.following && this.relationship.showing_reblogs">
|
||||
Hide boosts from @{{ this.username }}
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="unhideBoosts()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.following && !this.relationship.showing_reblogs">
|
||||
Unhide boosts from @{{ this.username }}
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem divider="true" *ngIf="!isOwnerSelected"></ng-template>
|
||||
<ng-template contextMenuItem (execute)="muteAccount()" *ngIf="!isOwnerSelected && this.relationship && !this.relationship.muting">
|
||||
Mute @{{ this.username }}
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="blockAccount()" *ngIf="!isOwnerSelected">
|
||||
Block @{{ this.username }}
|
||||
<ng-template contextMenuItem (execute)="unmuteAccount()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.muting">
|
||||
Unmute @{{ this.username }}
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="blockAccount()" *ngIf="!isOwnerSelected && this.relationship && !this.relationship.blocking">
|
||||
Block @{{ this.username }}
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="unblockAccount()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.blocking">
|
||||
Unblock @{{ this.username }}
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem divider="true" *ngIf="!isOwnerSelected"></ng-template>
|
||||
<ng-template contextMenuItem (execute)="blockDomain()" *ngIf="!isOwnerSelected && this.relationship && !this.relationship.domain_blocking">
|
||||
Block domain {{ this.domain }}
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="unblockDomain()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.domain_blocking">
|
||||
Unblock domain {{ this.domain }}
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem divider="true" *ngIf="isOwnerSelected"></ng-template>
|
||||
<ng-template contextMenuItem (execute)="pinOnProfile()" *ngIf="statusWrapper && isOwnerSelected && !displayedStatus.pinned && displayedStatus.visibility === 'public'">
|
||||
Pin on profile
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="unpinFromProfile()" *ngIf="statusWrapper && isOwnerSelected && displayedStatus.pinned && displayedStatus.visibility === 'public'">
|
||||
Unpin from profile
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="edit()" *ngIf="statusWrapper && isOwnerSelected && isEditingAvailable">
|
||||
Edit
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="delete(false)" *ngIf="statusWrapper && isOwnerSelected">
|
||||
Delete
|
||||
</ng-template>
|
||||
|
|
|
@ -4,8 +4,8 @@ import { ContextMenuComponent, ContextMenuService } from 'ngx-contextmenu';
|
|||
import { Observable, Subscription } from 'rxjs';
|
||||
import { Store } from '@ngxs/store';
|
||||
|
||||
import { Status, Account, Results } from '../../../../../services/models/mastodon.interfaces';
|
||||
import { ToolsService, OpenThreadEvent } from '../../../../../services/tools.service';
|
||||
import { Status, Account, Results, Relationship } from '../../../../../services/models/mastodon.interfaces';
|
||||
import { ToolsService, OpenThreadEvent, InstanceInfo } from '../../../../../services/tools.service';
|
||||
import { StatusWrapper } from '../../../../../models/common.model';
|
||||
import { NavigationService } from '../../../../../services/navigation.service';
|
||||
import { AccountInfo } from '../../../../../states/accounts.state';
|
||||
|
@ -25,12 +25,17 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
|
|||
private loadedAccounts: AccountInfo[];
|
||||
displayedStatus: Status;
|
||||
username: string;
|
||||
domain: string;
|
||||
isOwnerSelected: boolean;
|
||||
|
||||
isEditingAvailable: boolean;
|
||||
|
||||
@Input() statusWrapper: StatusWrapper;
|
||||
@Input() displayedAccount: Account;
|
||||
@Input() relationship: Relationship;
|
||||
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
@Output() relationshipChanged = new EventEmitter<Relationship>();
|
||||
|
||||
@ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent;
|
||||
|
||||
|
@ -70,6 +75,7 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
this.username = account.acct.split('@')[0];
|
||||
this.domain = account.acct.split('@')[1];
|
||||
this.fullHandle = this.toolsService.getAccountFullHandle(account);
|
||||
}
|
||||
|
||||
|
@ -78,6 +84,14 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
|
|||
|
||||
this.isOwnerSelected = selectedAccount.username.toLowerCase() === this.displayedStatus.account.username.toLowerCase()
|
||||
&& selectedAccount.instance.toLowerCase() === this.displayedStatus.account.url.replace('https://', '').split('/')[0].toLowerCase();
|
||||
|
||||
this.toolsService.getInstanceInfo(selectedAccount).then((instanceInfo: InstanceInfo) => {
|
||||
if (instanceInfo.major >= 4) {
|
||||
this.isEditingAvailable = true;
|
||||
} else {
|
||||
this.isEditingAvailable = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -155,38 +169,139 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
|
|||
return false;
|
||||
}
|
||||
|
||||
hideBoosts(): boolean {
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
this.toolsService.findAccount(acc, this.fullHandle)
|
||||
.then(async (target: Account) => {
|
||||
const relationship = await this.mastodonService.hideBoosts(acc, target);
|
||||
this.relationship = relationship;
|
||||
this.relationshipChanged.next(relationship);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
unhideBoosts(): boolean {
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
this.toolsService.findAccount(acc, this.fullHandle)
|
||||
.then(async (target: Account) => {
|
||||
const relationship = await this.mastodonService.unhideBoosts(acc, target);
|
||||
this.relationship = relationship;
|
||||
this.relationshipChanged.next(relationship);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
muteAccount(): boolean {
|
||||
this.loadedAccounts.forEach(acc => {
|
||||
this.toolsService.findAccount(acc, this.fullHandle)
|
||||
.then((target: Account) => {
|
||||
this.mastodonService.mute(acc, target.id);
|
||||
return target;
|
||||
})
|
||||
.then((target: Account) => {
|
||||
this.notificationService.hideAccount(target);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
});
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
this.toolsService.findAccount(acc, this.fullHandle)
|
||||
.then(async (target: Account) => {
|
||||
const relationship = await this.mastodonService.mute(acc, target.id);
|
||||
this.relationship = relationship;
|
||||
this.relationshipChanged.next(relationship);
|
||||
return target;
|
||||
})
|
||||
.then((target: Account) => {
|
||||
this.notificationService.hideAccount(target);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
unmuteAccount(): boolean {
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
this.toolsService.findAccount(acc, this.fullHandle)
|
||||
.then(async (target: Account) => {
|
||||
const relationship = await this.mastodonService.unmute(acc, target.id);
|
||||
this.relationship = relationship;
|
||||
this.relationshipChanged.next(relationship);
|
||||
return target;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
blockAccount(): boolean {
|
||||
this.loadedAccounts.forEach(acc => {
|
||||
this.toolsService.findAccount(acc, this.fullHandle)
|
||||
.then((target: Account) => {
|
||||
this.mastodonService.block(acc, target.id);
|
||||
return target;
|
||||
})
|
||||
.then((target: Account) => {
|
||||
this.notificationService.hideAccount(target);
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
this.toolsService.findAccount(acc, this.fullHandle)
|
||||
.then(async (target: Account) => {
|
||||
const relationship = await this.mastodonService.block(acc, target.id);
|
||||
this.relationship = relationship;
|
||||
this.relationshipChanged.next(relationship);
|
||||
return target;
|
||||
})
|
||||
.then((target: Account) => {
|
||||
this.notificationService.hideAccount(target);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
unblockAccount(): boolean {
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
this.toolsService.findAccount(acc, this.fullHandle)
|
||||
.then(async (target: Account) => {
|
||||
const relationship = await this.mastodonService.unblock(acc, target.id);
|
||||
this.relationship = relationship;
|
||||
this.relationshipChanged.next(relationship);
|
||||
return target;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
blockDomain(): boolean {
|
||||
const response = confirm(`Are you really sure you want to block the entire ${this.domain} domain? You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.`);
|
||||
|
||||
if (response) {
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
this.mastodonService.blockDomain(acc, this.domain)
|
||||
.then(_ => {
|
||||
this.relationship.domain_blocking = true;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
unblockDomain(): boolean {
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
this.mastodonService.blockDomain(acc, this.domain)
|
||||
.then(_ => {
|
||||
this.relationship.domain_blocking = false;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -282,6 +397,18 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
|
|||
return false;
|
||||
}
|
||||
|
||||
edit(): boolean {
|
||||
const selectedAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
this.getStatus(selectedAccount)
|
||||
.then(() => {
|
||||
this.navigationService.edit(this.statusWrapper);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, selectedAccount);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
private getStatus(account: AccountInfo): Promise<Status> {
|
||||
let statusPromise: Promise<Status> = Promise.resolve(this.statusWrapper.status);
|
||||
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
<div class="image">
|
||||
<div class="image">
|
||||
<div class="image__alt" *ngIf="displayAltLabel && attachment.description" title="{{ attachment.description }}">ALT</div>
|
||||
<a *ngIf="status" href class="image__status" (click)="openStatus()" (auxclick)="openStatus()" title="open status">
|
||||
<fa-icon class="image__status--icon" [icon]="faExternalLinkAlt"></fa-icon>
|
||||
</a>
|
||||
<a href class="image__link" (click)="openExternal()" (auxclick)="openExternal()" title="open image">
|
||||
<fa-icon class="image__link--icon" [icon]="faLink"></fa-icon>
|
||||
</a>
|
||||
|
|
|
@ -25,10 +25,48 @@
|
|||
// }
|
||||
}
|
||||
|
||||
&__status {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 25px;
|
||||
padding: 5px 5px 8px 8px;
|
||||
transition: all .2s;
|
||||
opacity: 0;
|
||||
color: white;
|
||||
|
||||
&--icon {
|
||||
filter: drop-shadow(0 0 3px rgb(78, 78, 78));
|
||||
}
|
||||
}
|
||||
|
||||
&:hover &__link {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover &__status {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__alt {
|
||||
display: inline;
|
||||
color: white;
|
||||
|
||||
z-index: 10;
|
||||
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 5px;
|
||||
|
||||
font-size: 10px;
|
||||
font-weight: bolder;
|
||||
|
||||
background-color: rgba($color: #000000, $alpha: 0.5);
|
||||
border-radius: 3px;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
img,
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { faLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faLink, faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { SettingsService } from '../../../../../services/settings.service';
|
||||
import { Attachment } from '../../../../../services/models/mastodon.interfaces';
|
||||
import { StatusWrapper } from '../../../../../models/common.model';
|
||||
import { OpenThreadEvent } from '../../../../../services/tools.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-attachement-image',
|
||||
|
@ -10,11 +13,19 @@ import { Attachment } from '../../../../../services/models/mastodon.interfaces';
|
|||
})
|
||||
export class AttachementImageComponent implements OnInit {
|
||||
faLink = faLink;
|
||||
faExternalLinkAlt = faExternalLinkAlt;
|
||||
displayAltLabel: boolean;
|
||||
|
||||
@Input() attachment: Attachment;
|
||||
@Input() status: StatusWrapper;
|
||||
@Output() openEvent = new EventEmitter();
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
|
||||
constructor() { }
|
||||
constructor(
|
||||
private readonly settingsService: SettingsService
|
||||
) {
|
||||
this.displayAltLabel = this.settingsService.getSettings().enableAltLabel;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
@ -28,4 +39,13 @@ export class AttachementImageComponent implements OnInit {
|
|||
window.open(this.attachment.url, '_blank');
|
||||
return false;
|
||||
}
|
||||
|
||||
openStatus(): boolean {
|
||||
if(!this.status) return false;
|
||||
|
||||
const openThreadEvent = new OpenThreadEvent(this.status.status, this.status.provider);
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,10 @@ export class AttachementsComponent implements OnInit {
|
|||
|
||||
@Input('attachments')
|
||||
set attachments(value: Attachment[]) {
|
||||
this.imageAttachments = [];
|
||||
this.videoAttachments = [];
|
||||
this.audioAttachments = [];
|
||||
|
||||
this._attachments = value;
|
||||
this.setAttachments(value);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="card-data" *ngIf="card.type === 'link' || card.type === 'video'">
|
||||
<a *ngIf="card.type === 'link'" class="card-data__link" href="{{ card.url }}" target="_blank" title="{{ card.title }} {{ host }}">
|
||||
<a *ngIf="card.type === 'link'" class="card-data__link" href="{{ card.url }}" target="_blank" rel="noopener noreferrer" title="{{ card.title }} {{ host }}">
|
||||
<img *ngIf="card.image" class="card-data__link--image" src="{{ card.image | ensureHttps }}" alt="" />
|
||||
<div *ngIf="!card.image" class="card-data__link--image">
|
||||
<fa-icon class="card-data__link--image--logo" [icon]="faFileAlt"></fa-icon>
|
||||
|
|
|
@ -22,9 +22,9 @@ $expand-color: $column-color;
|
|||
right: 0;
|
||||
padding-top: 60px;
|
||||
padding-left: 15px;
|
||||
background-image: linear-gradient(to bottom, rgba(0,0,0,0), rgba($expand-color ,0.25), rgba($expand-color,0.5), $expand-color, $expand-color);
|
||||
|
||||
&--link{
|
||||
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba($expand-color, 0.25), rgba($expand-color, 0.5), $expand-color, $expand-color);
|
||||
|
||||
&--link {
|
||||
transition: all .2s;
|
||||
color: #a9b5d8;
|
||||
color: #c0c8e0;
|
||||
|
@ -41,9 +41,9 @@ $expand-color: $column-color;
|
|||
}
|
||||
|
||||
&--selected {
|
||||
background-image: linear-gradient(to bottom, rgba(0,0,0,0), rgba($selected-status ,0.25), rgba($selected-status,0.5), $selected-status, $selected-status);
|
||||
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba($selected-status, 0.25), rgba($selected-status, 0.5), $selected-status, $selected-status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -51,20 +51,53 @@ $expand-color: $column-color;
|
|||
:host ::ng-deep .content {
|
||||
// font-size: 14px;
|
||||
color: $status-primary-color;
|
||||
|
||||
& a,
|
||||
.mention,
|
||||
.ellipsis {
|
||||
color: $status-links-color;
|
||||
}
|
||||
|
||||
& .invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& p {
|
||||
margin: 0px;
|
||||
white-space: pre-wrap;
|
||||
//font-size: .9em;
|
||||
// font-size: 14px;
|
||||
}
|
||||
|
||||
& p:not(:last-child) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
& code {
|
||||
color: #a0d1ff;
|
||||
}
|
||||
|
||||
& pre {
|
||||
padding: 0 5px 5px 5px;
|
||||
background-color: #000000;
|
||||
|
||||
scrollbar-width: thin;
|
||||
border-radius: 5px 5px 0 0;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: $scroll-bar-width;
|
||||
height: $scroll-bar-width;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
-webkit-border-radius: 0px;
|
||||
border-radius: 0px;
|
||||
background: $scrollbar-color-thumb;
|
||||
background: #384958;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
|
@ -42,7 +42,17 @@ describe('DatabindedTextComponent', () => {
|
|||
const url = 'https://test.social/tags/programmers';
|
||||
const sample = `<p>bla1 <a href="${url}" class="mention hashtag" rel="nofollow noopener" target="_blank">#<span>${hashtag}</span></a> bla2</p>`;
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href class="hashtag-programmers" title="#programmers">#programmers</a>');
|
||||
expect(component.processedText).toContain(`<a href="${url}" class="hashtag-programmers" title="#programmers" target="_blank" rel="noopener noreferrer">#programmers</a>`);
|
||||
expect(component.processedText).toContain('bla1');
|
||||
expect(component.processedText).toContain('bla2');
|
||||
});
|
||||
|
||||
it('should parse hashtag - Hometown', () => {
|
||||
const hashtag = 'MicroFiction';
|
||||
const url = 'https://mastodon.social/tags/MicroFiction';
|
||||
const sample = `<p>"bla1"<br><a href="${url}" class="mention hashtag" rel="nofollow noopener noreferrer" target="_blank">#<span class="article-type">${hashtag}</span></a> bla2</p>`;
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain(`<a href="${url}" class="hashtag-${hashtag}" title="#${hashtag}" target="_blank" rel="noopener noreferrer">#${hashtag}</a>`);
|
||||
expect(component.processedText).toContain('bla1');
|
||||
expect(component.processedText).toContain('bla2');
|
||||
});
|
||||
|
@ -50,7 +60,7 @@ describe('DatabindedTextComponent', () => {
|
|||
it('should parse hashtag - Pleroma 2.0.2', () => {
|
||||
const sample = `Blabla <a class="hashtag" data-tag="covid19" href="https://url.com/tag/covid19">#covid19</a> Blibli`;
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href class="hashtag-covid19" title="#covid19">#covid19</a>');
|
||||
expect(component.processedText).toContain(`<a href="https://url.com/tag/covid19" class="hashtag-covid19" title="#covid19" target="_blank" rel="noopener noreferrer">#covid19</a>`);
|
||||
expect(component.processedText).toContain('Blabla');
|
||||
expect(component.processedText).toContain('Blibli');
|
||||
});
|
||||
|
@ -60,7 +70,7 @@ describe('DatabindedTextComponent', () => {
|
|||
const url = 'https://mastodon.social/@sengi_app';
|
||||
const sample = `<p>bla1 <span class="h-card"><a href="${url}" class="u-url mention" rel="nofollow noopener" target="_blank">@<span>${mention}</span></a></span> bla2</p>`;
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href class="account--sengi_app-mastodon-social" title="@sengi_app@mastodon.social">@sengi_app</a>');
|
||||
expect(component.processedText).toContain(`<a href="${url}" class="account--sengi_app-mastodon-social" title="@sengi_app@mastodon.social" target="_blank" rel="noopener noreferrer">@sengi_app</a>`);
|
||||
expect(component.processedText).toContain('bla1');
|
||||
expect(component.processedText).toContain('bla2');
|
||||
});
|
||||
|
@ -69,14 +79,23 @@ describe('DatabindedTextComponent', () => {
|
|||
const sample = `<p><span class="article-type"><a href="https://domain.name/@username" class="u-url mention" rel="nofollow noopener noreferrer" target="_blank">@<span class="article-type">username</span></a></span> <br>Yes, indeed.</p>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toBe('<p><span class="article-type"><a href class="account--username-domain-name" title="@username@domain.name">@username</a> <br>Yes, indeed.</p>');
|
||||
expect(component.processedText).toBe('<p><span class="article-type"><a href="https://domain.name/@username" class="account--username-domain-name" title="@username@domain.name" target="_blank" rel="noopener noreferrer">@username</a> <br>Yes, indeed.</p>');
|
||||
});
|
||||
|
||||
it('should parse link', () => {
|
||||
const url = 'mydomain.co/test';
|
||||
const sample = `<p>bla1 <a href="https://${url}" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="">${url}</span><span class="invisible"></span></a> bla2</p>`;
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href class="link-httpsmydomaincotest" title="open link">mydomain.co/test</a>');
|
||||
expect(component.processedText).toContain(`<a href="https://${url}" class="link-httpsmydomaincotest" title="open link" target="_blank" rel="noopener noreferrer">mydomain.co/test</a>`);
|
||||
expect(component.processedText).toContain('bla1');
|
||||
expect(component.processedText).toContain('bla2');
|
||||
});
|
||||
|
||||
it('should parse link - Hometown', () => {
|
||||
const url = 'bbs.archlinux.org/test';
|
||||
const sample = `<p>bla1 <a href="https://${url}" rel="nofollow noopener noreferrer" target="_blank"><span class="article-type">https://</span><span class="article-type">${url}</span><span class="article-type">p?id=264086&action=new</span></a> bla2`;
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain(`<a href="https://${url}" class="link-httpsbbsarchlinuxorgtest" title="open link" target="_blank" rel="noopener noreferrer">${url}</a>`);
|
||||
expect(component.processedText).toContain('bla1');
|
||||
expect(component.processedText).toContain('bla2');
|
||||
});
|
||||
|
@ -85,21 +104,21 @@ describe('DatabindedTextComponent', () => {
|
|||
const url = 'bbc.com/news/magazine-34901704';
|
||||
const sample = `<p>The rise of"<br><a href="https:www//${url}" rel="nofollow noopener" target="_blank"><span class="invisible">https://www.</span><span class="">${url}</span><span class="invisible"></span></a></p>`;
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href class="link-httpswwwbbccomnewsmagazine34901704" title="open link">bbc.com/news/magazine-34901704</a></p>');
|
||||
expect(component.processedText).toContain(`<a href="https:www//${url}" class="link-httpswwwbbccomnewsmagazine34901704" title="open link" target="_blank" rel="noopener noreferrer">bbc.com/news/magazine-34901704</a></p>`);
|
||||
});
|
||||
|
||||
it('should parse link - dual section', () => {
|
||||
const sample = `<p>Test.<br><a href="https://peertube.fr/videos/watch/69bb6e90-ec0f-49a3-9e28-41792f4a7c5f" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="ellipsis">peertube.fr/videos/watch/69bb6</span><span class="invisible">e90-ec0f-49a3-9e28-41792f4a7c5f</span></a></p>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<p>Test.<br><a href class="link-httpspeertubefrvideoswatch69bb6e90ec0f49a39e2841792f4a7c5f" title="open link">peertube.fr/videos/watch/69bb6</a></p>');
|
||||
expect(component.processedText).toContain('<p>Test.<br><a href="https://peertube.fr/videos/watch/69bb6e90-ec0f-49a3-9e28-41792f4a7c5f" class="link-httpspeertubefrvideoswatch69bb6e90ec0f49a39e2841792f4a7c5f" title="open link" target="_blank" rel="noopener noreferrer">peertube.fr/videos/watch/69bb6</a></p>');
|
||||
});
|
||||
|
||||
it('should parse link with special character', () => {
|
||||
const sample = `<p>Magnitude: 2.5 Depth: 3.4 km<br>Details: 2018/09/27 06:50:17 34.968N 120.685W<br>Location: 10 km (6 mi) W of Guadalupe, CA<br>Map: <a href="https://www.google.com/maps/place/34°58'4%20N+120°41'6%20W/@34.968,-120.685,10z" rel="noopener" target="_blank" class="status-link" title="https://www.google.com/maps/place/34%C2%B058'4%20N+120%C2%B041'6%20W/@34.968,-120.685,10z"><span class="invisible">https://www.</span><span class="ellipsis">google.com/maps/place/34°58'4%</span><span class="invisible">20N+120°41'6%20W/@34.968,-120.685,10z</span></a><br><a href="https://mastodon.cloud/tags/earthquake" class="mention hashtag status-link" rel="noopener" target="_blank">#<span>EarthQuake</span></a> <a href="https://mastodon.cloud/tags/quake" class="mention hashtag status-link" rel="noopener" target="_blank">#<span>Quake</span></a> <a href="https://mastodon.cloud/tags/california" class="mention hashtag status-link" rel="noopener" target="_blank">#<span>California</span></a></p>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href class="link-httpswwwgooglecommapsplace3458420N12041620W3496812068510z" title="open link">google.com/maps/place/34°58\'4%</a>');
|
||||
expect(component.processedText).toContain(`<a href="https://www.google.com/maps/place/34°58'4%20N+120°41'6%20W/@34.968,-120.685,10z" class="link-httpswwwgooglecommapsplace3458420N12041620W3496812068510z" title="open link" target="_blank" rel="noopener noreferrer">google.com/maps/place/34°58\'4%</a>`);
|
||||
});
|
||||
|
||||
it('should parse combined hashtag, mention and link', () => {
|
||||
|
@ -110,9 +129,9 @@ describe('DatabindedTextComponent', () => {
|
|||
const linkUrl = 'mydomain.co/test';
|
||||
const sample = `<p>bla1 <a href="${hashtagUrl}" class="mention hashtag" rel="nofollow noopener" target="_blank">#<span>${hashtag}</span></a> bla2 <span class="h-card"><a href="${mentionUrl}" class="u-url mention" rel="nofollow noopener" target="_blank">@<span>${mention}</span></a></span> bla3 <a href="https://${linkUrl}" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="">${linkUrl}</span><span class="invisible"></span></a> bla4</p>`;
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href class="hashtag-programmers" title="#programmers">#programmers</a>');
|
||||
expect(component.processedText).toContain('<a href class="account--sengi_app-mastodon-social" title="@sengi_app@mastodon.social">@sengi_app</a>');
|
||||
expect(component.processedText).toContain('<a href class="link-httpsmydomaincotest" title="open link">mydomain.co/test</a>');
|
||||
expect(component.processedText).toContain(`<a href="${hashtagUrl}" class="hashtag-programmers" title="#programmers" target="_blank" rel="noopener noreferrer">#programmers</a>`);
|
||||
expect(component.processedText).toContain(`<a href="${mentionUrl}" class="account--sengi_app-mastodon-social" title="@sengi_app@mastodon.social" target="_blank" rel="noopener noreferrer">@sengi_app</a>`);
|
||||
expect(component.processedText).toContain(`<a href="https://${linkUrl}" class="link-httpsmydomaincotest" title="open link" target="_blank" rel="noopener noreferrer">mydomain.co/test</a>`);
|
||||
expect(component.processedText).toContain('bla1');
|
||||
expect(component.processedText).toContain('bla2');
|
||||
expect(component.processedText).toContain('bla3');
|
||||
|
@ -123,7 +142,7 @@ describe('DatabindedTextComponent', () => {
|
|||
const sample = `bla1 <a href="https://www.lemonde.fr/planete.html?xtor=RSS-3208" rel="nofollow noopener" class="" target="_blank">https://social.bitcast.info/url/819438</a>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href class="link-httpswwwlemondefrplanetehtmlxtorRSS3208" title="open link">https://social.bitcast.info/url/819438</a>');
|
||||
expect(component.processedText).toContain('<a href="https://www.lemonde.fr/planete.html?xtor=RSS-3208" class="link-httpswwwlemondefrplanetehtmlxtorRSS3208" title="open link" target="_blank" rel="noopener noreferrer">https://social.bitcast.info/url/819438</a>');
|
||||
expect(component.processedText).toContain('bla1');
|
||||
});
|
||||
|
||||
|
@ -131,7 +150,7 @@ describe('DatabindedTextComponent', () => {
|
|||
const sample = `<div>bla1 <br> @<a href="https://instance.club/user/1" class="h-card mention status-link" rel="noopener" target="_blank" title="https://instance.club/user/1">user</a> </div>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href class="account--user-instance-club" title="@user@instance.club">@user</a>');
|
||||
expect(component.processedText).toContain('<a href="https://instance.club/user/1" class="account--user-instance-club" title="@user@instance.club" target="_blank" rel="noopener noreferrer">@user</a>');
|
||||
expect(component.processedText).toContain('bla1');
|
||||
});
|
||||
|
||||
|
@ -139,42 +158,65 @@ describe('DatabindedTextComponent', () => {
|
|||
const sample = `<div><span><a class="mention status-link" href="https://pleroma.site/users/kaniini" rel="noopener" target="_blank" title="kaniini@pleroma.site">@<span>kaniini</span></a></span> <span><a class="mention status-link" href="https://mastodon.social/@Gargron" rel="noopener" target="_blank" title="Gargron@mastodon.social">@<span>Gargron</span></a></span> bla1?</div>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<div><span><a href class="account--kaniini-pleroma-site" title="@kaniini@pleroma.site">@kaniini</a> <span><a href class="account--Gargron-mastodon-social" title="@Gargron@mastodon.social">@Gargron</a> bla1?</div>');
|
||||
expect(component.processedText).toContain('<div><span><a href="https://pleroma.site/users/kaniini" class="account--kaniini-pleroma-site" title="@kaniini@pleroma.site" target="_blank" rel="noopener noreferrer">@kaniini</a> <span><a href="https://mastodon.social/@Gargron" class="account--Gargron-mastodon-social" title="@Gargron@mastodon.social" target="_blank" rel="noopener noreferrer">@Gargron</a> bla1?</div>');
|
||||
});
|
||||
|
||||
it('should parse mention - Friendica in Mastodon', () => {
|
||||
const sample = `@<span class=""><a href="https://m.s/me" class="u-url mention" rel="nofollow noopener" target="_blank"><span class="mention">me</span></a></span> Blablabla.`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<span class=""><a href class="account--me-m-s" title="@me@m.s">@me</a></span> Blablabla.');
|
||||
expect(component.processedText).toContain('<span class=""><a href="https://m.s/me" class="account--me-m-s" title="@me@m.s" target="_blank" rel="noopener noreferrer">@me</a></span> Blablabla.');
|
||||
});
|
||||
|
||||
it('should parse mention - Misskey in Mastodon', () => {
|
||||
const sample = `<p><a href="https://mastodon.social/users/sengi_app" class="mention" rel="nofollow noopener" target="_blank">@sengi_app@mastodon.social</a><span> Blabla</span></p>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<p><a href class="account--sengi_app-mastodon-social-mastodon-social" title="@sengi_app@mastodon.social@mastodon.social">@sengi_app@mastodon.social</a><span> Blabla</span></p>'); //FIXME: dont let domain appear in name
|
||||
expect(component.processedText).toContain('<p><a href="https://mastodon.social/users/sengi_app" class="account--sengi_app-mastodon-social-mastodon-social" title="@sengi_app@mastodon.social@mastodon.social" target="_blank" rel="noopener noreferrer">@sengi_app@mastodon.social</a><span> Blabla</span></p>'); //FIXME: dont let domain appear in name
|
||||
});
|
||||
|
||||
it('should parse mention - Misskey in Pleroma', () => {
|
||||
const sample = `<p><a href="https://domain.xyz/@sengi" class="u-url mention">@sengi@domain.xyz</a><span> </span><a href="https://domain.eu/@sengi" class="u-url mention">@sengi@domain.eu</a><span> bla bla<br/>bla bla bla</span></p>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href="https://domain.xyz/@sengi" class="account--sengi-domain-xyz" title="@sengi@domain.xyz" target="_blank" rel="noopener noreferrer">@sengi</a><span>');
|
||||
expect(component.processedText).toContain('<a href="https://domain.eu/@sengi" class="account--sengi-domain-eu" title="@sengi@domain.eu" target="_blank" rel="noopener noreferrer">@sengi</a>');
|
||||
expect(component.processedText).toContain('<span> bla bla<br/>bla bla bla</span>');
|
||||
});
|
||||
|
||||
it('should parse mention - Misskey in Mastodon - 2', () => {
|
||||
const sample = `<p><span>Since </span><a href="https://mastodon.technology/@test" class="u-url mention" rel="nofollow noopener noreferrer" target="_blank">@test@mastodon.technology</a><span> mentioned </span></p>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href="https://mastodon.technology/@test" class="account--test-mastodon-technology" title="@test@mastodon.technology" target="_blank" rel="noopener noreferrer">@test</a>');
|
||||
});
|
||||
|
||||
it('should parse mention - Zap in Mastodon', () => {
|
||||
const sample = `test @<span class="h-card"><a class="u-url mention" href="https://mastodon.social/@test" rel="nofollow noopener noreferrer" target="_blank">test</a></span> bla"`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('test <span class="h-card"><a href="https://mastodon.social/@test" class="account--test-mastodon-social" title="@test@mastodon.social" target="_blank" rel="noopener noreferrer">@test</a></span>');
|
||||
});
|
||||
|
||||
it('should parse hastag - Pleroma', () => {
|
||||
const sample = `<p>Bla <a href="https://ubuntu.social/tags/kubecon" rel="tag">#<span>KubeCon</span></a> Bla</p>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<p>Bla <a href class="hashtag-KubeCon" title="#KubeCon">#KubeCon</a> Bla</p>');
|
||||
expect(component.processedText).toContain('<p>Bla <a href="https://ubuntu.social/tags/kubecon" class="hashtag-KubeCon" title="#KubeCon" target="_blank" rel="noopener noreferrer">#KubeCon</a> Bla</p>');
|
||||
});
|
||||
|
||||
it('should parse link - Pleroma', () => {
|
||||
const sample = `<p>Bla <a href="https://cloudblogs.microsoft.com/opensource/2019/05/21/service-mesh-interface-smi-release/"><span>https://</span><span>cloudblogs.microsoft.com/opens</span><span>ource/2019/05/21/service-mesh-interface-smi-release/</span></a></p>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<p>Bla <a href class="link-httpscloudblogsmicrosoftcomopensource20190521servicemeshinterfacesmirelease" title="open link">cloudblogs.microsoft.com/opens</a></p>');
|
||||
expect(component.processedText).toContain('<p>Bla <a href="https://cloudblogs.microsoft.com/opensource/2019/05/21/service-mesh-interface-smi-release/" class="link-httpscloudblogsmicrosoftcomopensource20190521servicemeshinterfacesmirelease" title="open link" target="_blank" rel="noopener noreferrer">cloudblogs.microsoft.com/opens</a></p>');
|
||||
});
|
||||
|
||||
it('should parse link 2 - Pleroma', () => {
|
||||
const sample = `Bla<br /><br /><a href="https://link/">https://link/</a>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('Bla<br /><br /><a href class="link-httpslink" title="open link">https://link/</a>');
|
||||
expect(component.processedText).toContain('Bla<br /><br /><a href="https://link/" class="link-httpslink" title="open link" target="_blank" rel="noopener noreferrer">https://link/</a>');
|
||||
});
|
||||
|
||||
it('should sanitize link', () => {
|
||||
|
|
|
@ -28,7 +28,7 @@ export class DatabindedTextComponent implements OnInit {
|
|||
|
||||
@Input('text')
|
||||
set text(value: string) {
|
||||
//console.warn(value);
|
||||
// console.log(value);
|
||||
|
||||
let parser = new DOMParser();
|
||||
var dom = parser.parseFromString(value, 'text/html')
|
||||
|
@ -44,6 +44,10 @@ export class DatabindedTextComponent implements OnInit {
|
|||
value = value.replace('class="mention" rel="nofollow noopener" target="_blank">@', 'class="mention" rel="nofollow noopener" target="_blank">'); //Misskey sanitarization
|
||||
} while (value.includes('class="mention" rel="nofollow noopener" target="_blank">@'));
|
||||
|
||||
do {
|
||||
value = value.replace('@<span class="h-card">', '<span class="h-card">'); //Zap sanitarization
|
||||
} while (value.includes('@<span class="h-card">'));
|
||||
|
||||
let linksSections = value.split('<a ');
|
||||
|
||||
for (let section of linksSections) {
|
||||
|
@ -89,10 +93,11 @@ export class DatabindedTextComponent implements OnInit {
|
|||
|
||||
private processHashtag(section: string) {
|
||||
let extractedLinkAndNext = section.split('</a>');
|
||||
let extractedHashtag = extractedLinkAndNext[0].split('#')[1].replace('<span>', '').replace('</span>', '');
|
||||
let extractedHashtag = extractedLinkAndNext[0].split('#')[1].replace('<span class="article-type">', '').replace('<span>', '').replace('</span>', '');
|
||||
let extractedUrl = extractedLinkAndNext[0].split('href="')[1].split('"')[0];
|
||||
|
||||
let classname = this.getClassNameForHastag(extractedHashtag);
|
||||
this.processedText += ` <a href class="${classname}" title="#${extractedHashtag}">#${extractedHashtag}</a>`;
|
||||
this.processedText += `<a href="${extractedUrl}" class="${classname}" title="#${extractedHashtag}" target="_blank" rel="noopener noreferrer">#${extractedHashtag}</a>`;
|
||||
if (extractedLinkAndNext[1]) this.processedText += extractedLinkAndNext[1];
|
||||
this.hashtags.push(extractedHashtag);
|
||||
}
|
||||
|
@ -104,9 +109,22 @@ export class DatabindedTextComponent implements OnInit {
|
|||
if (section.includes('<span class="mention">')) { //Friendica
|
||||
extractedAccountAndNext = section.split('</a>');
|
||||
extractedAccountName = extractedAccountAndNext[0].split('<span class="mention">')[1].split('</span>')[0];
|
||||
} else if(section.includes('>@<span class="article-type">')){ //Remote status
|
||||
} else if (section.includes('>@<span class="article-type">')) { //Remote status
|
||||
extractedAccountAndNext = section.split('</a></span>');
|
||||
extractedAccountName = extractedAccountAndNext[0].split('@<span class="article-type">')[1].replace('<span>', '').replace('</span>', '');
|
||||
} else if (section.includes('class="u-url mention" rel="nofollow noopener noreferrer" target="_blank">@') && !section.includes('target="_blank">@<')) { //Misskey
|
||||
extractedAccountAndNext = section.split('</a>');
|
||||
extractedAccountName = extractedAccountAndNext[0].split('class="u-url mention" rel="nofollow noopener noreferrer" target="_blank">@')[1];
|
||||
|
||||
if (extractedAccountName.includes('@'))
|
||||
extractedAccountName = extractedAccountName.split('@')[0];
|
||||
} else if (section.includes(' class="u-url mention">@') && !section.includes(' class="u-url mention">@<')) { //Misskey in pleroma
|
||||
extractedAccountAndNext = section.split('</a>');
|
||||
extractedAccountName = extractedAccountAndNext[0].split(' class="u-url mention">@')[1];
|
||||
|
||||
if (extractedAccountName.includes('@'))
|
||||
extractedAccountName = extractedAccountName.split('@')[0];
|
||||
|
||||
} else if (!section.includes('@<span>')) { //GNU social
|
||||
extractedAccountAndNext = section.split('</a>');
|
||||
extractedAccountName = extractedAccountAndNext[0].split('>')[1];
|
||||
|
@ -121,9 +139,10 @@ export class DatabindedTextComponent implements OnInit {
|
|||
//let username = extractedAccountLink[extractedAccountLink.length - 1];
|
||||
|
||||
let extractedAccount = `@${extractedAccountName}@${domain}`;
|
||||
let extractedUrl = section.split('href="')[1].split('"')[0];
|
||||
|
||||
let classname = this.getClassNameForAccount(extractedAccount);
|
||||
this.processedText += `<a href class="${classname}" title="${extractedAccount}">@${extractedAccountName}</a>`;
|
||||
this.processedText += `<a href="${extractedUrl}" class="${classname}" title="${extractedAccount}" target="_blank" rel="noopener noreferrer">@${extractedAccountName}</a>`;
|
||||
|
||||
if (extractedAccountAndNext[1])
|
||||
this.processedText += extractedAccountAndNext[1];
|
||||
|
@ -136,7 +155,7 @@ export class DatabindedTextComponent implements OnInit {
|
|||
}
|
||||
|
||||
private processLink(section: string) {
|
||||
if(!section.includes('</a>')){
|
||||
if (!section.includes('</a>')) {
|
||||
this.processedText += section;
|
||||
return;
|
||||
}
|
||||
|
@ -144,30 +163,37 @@ export class DatabindedTextComponent implements OnInit {
|
|||
let extractedLinkAndNext = section.split('</a>')
|
||||
let extractedUrl = extractedLinkAndNext[0].split('"')[1];
|
||||
|
||||
let extractedName = '';
|
||||
try {
|
||||
extractedName = extractedLinkAndNext[0].split('<span class="ellipsis">')[1].split('</span>')[0];
|
||||
} catch (err) {
|
||||
let extractedName = '';
|
||||
|
||||
if(extractedLinkAndNext[0].includes('<span class="article-type">')){
|
||||
extractedName = extractedLinkAndNext[0].split('<span class="article-type">')[2].split('</span>')[0];
|
||||
} else {
|
||||
try {
|
||||
extractedName = extractedLinkAndNext[0].split(`<span class="">`)[1].split('</span>')[0];
|
||||
}
|
||||
catch (err) {
|
||||
extractedName = extractedLinkAndNext[0].split('<span class="ellipsis">')[1].split('</span>')[0];
|
||||
} catch (err) {
|
||||
try {
|
||||
extractedName = extractedLinkAndNext[0].split(' target="_blank">')[1].split('</span>')[0];
|
||||
} catch (err) { // Pleroma
|
||||
extractedName = extractedLinkAndNext[0].split(`<span class="">`)[1].split('</span>')[0];
|
||||
}
|
||||
catch (err) {
|
||||
try {
|
||||
extractedName = extractedLinkAndNext[0].split('</span><span>')[1].split('</span>')[0];
|
||||
} catch (err) {
|
||||
extractedName = extractedLinkAndNext[0].split('">')[1];
|
||||
extractedName = extractedLinkAndNext[0].split(' target="_blank">')[1].split('</span>')[0];
|
||||
} catch (err) { // Pleroma
|
||||
try {
|
||||
extractedName = extractedLinkAndNext[0].split('</span><span>')[1].split('</span>')[0];
|
||||
} catch (err) {
|
||||
extractedName = extractedLinkAndNext[0].split('">')[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.links.push(extractedUrl);
|
||||
let classname = this.getClassNameForLink(extractedUrl);
|
||||
|
||||
this.processedText += `<a href class="${classname}" title="open link">${extractedName}</a>`;
|
||||
let sanitizedLink = this.sanitizeLink(extractedUrl);
|
||||
|
||||
this.processedText += `<a href="${sanitizedLink}" class="${classname}" title="open link" target="_blank" rel="noopener noreferrer">${extractedName}</a>`;
|
||||
if (extractedLinkAndNext.length > 1) this.processedText += extractedLinkAndNext[1];
|
||||
}
|
||||
|
||||
|
@ -179,6 +205,10 @@ export class DatabindedTextComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.processEventBindings();
|
||||
}
|
||||
|
||||
processEventBindings(){
|
||||
for (const hashtag of this.hashtags) {
|
||||
let classname = this.getClassNameForHastag(hashtag);
|
||||
let els = <Element[]>this.contentElement.nativeElement.querySelectorAll(`.${classname}`);
|
||||
|
@ -219,20 +249,18 @@ export class DatabindedTextComponent implements OnInit {
|
|||
this.renderer.listen(el, 'click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
window.open(sanitizedLink, '_blank');
|
||||
window.open(sanitizedLink, '_blank', 'noopener');
|
||||
return false;
|
||||
});
|
||||
|
||||
this.renderer.listen(el, 'mouseup', (event) => {
|
||||
if (event.which === 2) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
window.open(sanitizedLink, '_blank');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
// this.renderer.listen(el, 'mouseup', (event) => {
|
||||
// if (event.which === 2) {
|
||||
// event.preventDefault();
|
||||
// event.stopImmediatePropagation();
|
||||
// window.open(sanitizedLink, '_blank', 'noopener');
|
||||
// return false;
|
||||
// }
|
||||
// });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,9 @@
|
|||
<button href *ngIf="!poll.voted && !poll.expired && !pollLocked" class="btn btn-sm btn-custom-primary poll__btn-vote"
|
||||
title="don't boo, vote!" (click)="vote()">Vote</button>
|
||||
<a href class="poll__refresh" *ngIf="(poll.voted || poll.expired) && !pollLocked" title="refresh poll" (click)="refresh()">refresh</a>
|
||||
<div class="poll__statistics"><span *ngIf="(poll.voted || poll.expired) && !pollLocked" class="poll__separator">·</span>{{poll.votes_count}} votes<span *ngIf="!poll.expired" class="poll__separator" title="{{ poll.expires_at | timeLeft | async }}">· {{ poll.expires_at | timeLeft | async }}</span></div>
|
||||
<div class="poll__statistics"><span *ngIf="(poll.voted || poll.expired) && !pollLocked && !(poll.voters_count && poll.voters_count > 0)" class="poll__separator">·</span><span *ngIf="!(poll.voters_count && poll.voters_count > 0)">{{poll.votes_count}} votes</span><span *ngIf="poll.voters_count > 0 && !pollLocked" class="poll__separator">·</span><span *ngIf="poll.voters_count > 0">{{poll.voters_count}} people</span><span *ngIf="!poll.expired" class="poll__separator" title="{{ poll.expires_at | timeLeft | async }}">· {{ poll.expires_at | timeLeft | async }}</span></div>
|
||||
</div>
|
||||
<div class="poll__error" *ngIf="errorOccuredWhenRetrievingPoll">
|
||||
Error occured when retrieving the poll
|
||||
</div>
|
||||
</div>
|
|
@ -27,6 +27,11 @@
|
|||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&__error {
|
||||
font-size: 12px;
|
||||
color: red;
|
||||
}
|
||||
|
||||
&__refresh {
|
||||
font-size: 0.8em;
|
||||
color: rgb(101, 121, 160);
|
||||
|
|
|
@ -22,6 +22,8 @@ export class PollComponent implements OnInit {
|
|||
choiceType: string;
|
||||
pollLocked: boolean;
|
||||
|
||||
errorOccuredWhenRetrievingPoll: boolean;
|
||||
|
||||
private pollSelection: number[] = [];
|
||||
options: PollOptionWrapper[] = [];
|
||||
|
||||
|
@ -30,7 +32,7 @@ export class PollComponent implements OnInit {
|
|||
private _poll: Poll;
|
||||
@Input('poll')
|
||||
set poll(value: Poll) {
|
||||
if(!value) return;
|
||||
if (!value) return;
|
||||
|
||||
this._poll = value;
|
||||
|
||||
|
@ -43,10 +45,18 @@ export class PollComponent implements OnInit {
|
|||
}
|
||||
|
||||
this.options.length = 0;
|
||||
const maxVotes = Math.max(...this.poll.options.map(x => x.votes_count));
|
||||
|
||||
let maxVotes = Math.max(...this.poll.options.map(x => x.votes_count));
|
||||
|
||||
if(!this.poll.multiple){ //Fix for absurd values in pleroma
|
||||
this.poll.voters_count = this.poll.votes_count;
|
||||
} else if(this.poll.voters_count * this.poll.options.length < this.poll.votes_count){
|
||||
this.poll.voters_count = this.poll.votes_count;
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
for (let opt of this.poll.options) {
|
||||
let optWrapper = new PollOptionWrapper(i, opt, this.poll.votes_count, opt.votes_count === maxVotes);
|
||||
let optWrapper = new PollOptionWrapper(i, opt, this.poll.votes_count, this.poll.voters_count, opt.votes_count === maxVotes);
|
||||
this.options.push(optWrapper);
|
||||
i++;
|
||||
}
|
||||
|
@ -83,6 +93,7 @@ export class PollComponent implements OnInit {
|
|||
|
||||
private checkStatus(accounts: AccountInfo[]): void {
|
||||
this.pollLocked = false;
|
||||
this.errorOccuredWhenRetrievingPoll = false;
|
||||
var newSelectedAccount = accounts.find(x => x.isSelected);
|
||||
|
||||
const accountChanged = this.selectedAccount.id !== newSelectedAccount.id;
|
||||
|
@ -92,7 +103,7 @@ export class PollComponent implements OnInit {
|
|||
let statusWrapper = new StatusWrapper(this.statusWrapper.status, this.statusWrapper.provider, this.statusWrapper.applyCw, this.statusWrapper.hide);
|
||||
this.pollPerAccountId[newSelectedAccount.id] = this.toolsService.getStatusUsableByAccount(newSelectedAccount, statusWrapper)
|
||||
.then((status: Status) => {
|
||||
if(!status || !(status.poll)) return null;
|
||||
if (!status || !(status.poll)) return null;
|
||||
return this.mastodonService.getPoll(newSelectedAccount, status.poll.id);
|
||||
})
|
||||
.then((poll: Poll) => {
|
||||
|
@ -100,7 +111,9 @@ export class PollComponent implements OnInit {
|
|||
return poll;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, newSelectedAccount);
|
||||
//this.notificationService.notifyHttpError(err, newSelectedAccount);
|
||||
this.errorOccuredWhenRetrievingPoll = true;
|
||||
this.pollPerAccountId[newSelectedAccount.id] = null;
|
||||
return null;
|
||||
});
|
||||
} else if (this.statusWrapper.status.visibility !== 'public' && this.statusWrapper.status.visibility !== 'unlisted' && this.statusWrapper.provider.id !== newSelectedAccount.id) {
|
||||
|
@ -115,8 +128,9 @@ export class PollComponent implements OnInit {
|
|||
this.selectedAccount = newSelectedAccount;
|
||||
}
|
||||
|
||||
|
||||
vote(): boolean {
|
||||
if (this.errorOccuredWhenRetrievingPoll) return false;
|
||||
|
||||
const selectedAccount = this.selectedAccount;
|
||||
const pollPromise = this.pollPerAccountId[selectedAccount.id];
|
||||
|
||||
|
@ -140,6 +154,8 @@ export class PollComponent implements OnInit {
|
|||
}
|
||||
|
||||
refresh(): boolean {
|
||||
if (this.errorOccuredWhenRetrievingPoll) return false;
|
||||
|
||||
this.setStatsAtZero();
|
||||
|
||||
const selectedAccount = this.selectedAccount;
|
||||
|
@ -175,14 +191,19 @@ export class PollComponent implements OnInit {
|
|||
}
|
||||
|
||||
class PollOptionWrapper implements PollOption {
|
||||
constructor(index: number, option: PollOption, totalVotes: number, isMax: boolean) {
|
||||
constructor(index: number, option: PollOption, totalVotes: number, totalVoters: number, isMax: boolean) {
|
||||
let votesDivider = totalVotes;
|
||||
if(totalVoters && totalVoters > 0){
|
||||
votesDivider = totalVoters;
|
||||
}
|
||||
|
||||
this.id = index;
|
||||
this.title = option.title;
|
||||
this.votes_count = option.votes_count;
|
||||
if (totalVotes === 0) {
|
||||
this.percentage = '0';
|
||||
} else {
|
||||
this.percentage = ((this.votes_count / totalVotes) * 100).toFixed(0);
|
||||
this.percentage = ((this.votes_count / votesDivider) * 100).toFixed(0);
|
||||
}
|
||||
this.isMax = isMax;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<div class="translation translation__button-display" *ngIf="isTranslationAvailable && showTranslationButton">
|
||||
<a href class="translation__link translation__button-display__link" (click)="translate()">Translate</a>
|
||||
</div>
|
||||
<div class="translation translation__display" *ngIf="isTranslationAvailable && !showTranslationButton">
|
||||
<span class="translation__by">Translated by {{translatedBy}}</span> <a href (click)="revertTranslation()" class="translation__link translation__display__link">revert</a>
|
||||
</div>
|
|
@ -0,0 +1,44 @@
|
|||
@import "variables";
|
||||
@import "commons";
|
||||
|
||||
$translation-color: #656b8f;
|
||||
$translation-color-hover: #9fa5ca;
|
||||
|
||||
.translation {
|
||||
margin: 0 10px 0 $avatar-column-space;
|
||||
color: $translation-color;
|
||||
font-size: 12px;
|
||||
|
||||
&__button-display {
|
||||
text-align: center;
|
||||
|
||||
&__link {
|
||||
display: block;
|
||||
padding: 5px 5px 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__display {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&__link {
|
||||
padding: 5px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__link {
|
||||
color: $translation-color;
|
||||
transition: all .2s;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: $translation-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&__by {
|
||||
display: block;
|
||||
text-align: left;
|
||||
padding: 5px 0 0 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { StatusTranslateComponent } from './status-translate.component';
|
||||
|
||||
xdescribe('StatusTranslateComponent', () => {
|
||||
let component: StatusTranslateComponent;
|
||||
let fixture: ComponentFixture<StatusTranslateComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ StatusTranslateComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(StatusTranslateComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,118 @@
|
|||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { StatusWrapper } from '../../../../models/common.model';
|
||||
import { ILanguage } from '../../../../states/settings.state';
|
||||
import { LanguageService } from '../../../../services/language.service';
|
||||
import { InstancesInfoService } from '../../../../services/instances-info.service';
|
||||
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
|
||||
import { Translation } from '../../../../services/models/mastodon.interfaces';
|
||||
import { NotificationService } from '../../../../services/notification.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'app-status-translate',
|
||||
templateUrl: './status-translate.component.html',
|
||||
styleUrls: ['./status-translate.component.scss']
|
||||
})
|
||||
export class StatusTranslateComponent implements OnInit, OnDestroy {
|
||||
|
||||
private languageSub: Subscription;
|
||||
private languagesSub: Subscription;
|
||||
private loadedTranslation: Translation;
|
||||
|
||||
selectedLanguage: ILanguage;
|
||||
configuredLanguages: ILanguage[] = [];
|
||||
|
||||
isTranslationAvailable: boolean;
|
||||
showTranslationButton: boolean = true;
|
||||
translatedBy: string;
|
||||
|
||||
@Input() status: StatusWrapper;
|
||||
@Output() translation = new EventEmitter<Translation>();
|
||||
|
||||
constructor(
|
||||
private readonly mastodonWrapperService: MastodonWrapperService,
|
||||
private readonly languageService: LanguageService,
|
||||
private readonly instancesInfoService: InstancesInfoService,
|
||||
private readonly notificationService: NotificationService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.languageSub = this.languageService.selectedLanguageChanged.subscribe(l => {
|
||||
if (l) {
|
||||
this.selectedLanguage = l;
|
||||
this.analyseAvailability();
|
||||
}
|
||||
});
|
||||
|
||||
this.languagesSub = this.languageService.configuredLanguagesChanged.subscribe(l => {
|
||||
if (l) {
|
||||
this.configuredLanguages = l;
|
||||
this.analyseAvailability();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.languageSub) this.languageSub.unsubscribe();
|
||||
if (this.languagesSub) this.languagesSub.unsubscribe();
|
||||
}
|
||||
|
||||
private analyseAvailability() {
|
||||
this.instancesInfoService.getTranslationAvailability(this.status.provider)
|
||||
.then(canTranslate => {
|
||||
if (canTranslate
|
||||
&& !this.status.isRemote
|
||||
&& this.status.status.language
|
||||
&& this.configuredLanguages.length > 0
|
||||
&& this.configuredLanguages.findIndex(x => x.iso639 === this.status.status.language) === -1) {
|
||||
|
||||
this.isTranslationAvailable = true;
|
||||
}
|
||||
else {
|
||||
this.isTranslationAvailable = false;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
this.isTranslationAvailable = false;
|
||||
});
|
||||
}
|
||||
|
||||
translate(): boolean {
|
||||
if(this.loadedTranslation){
|
||||
this.translation.next(this.loadedTranslation);
|
||||
this.showTranslationButton = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.mastodonWrapperService.translate(this.status.provider, this.status.status.id, this.selectedLanguage.iso639)
|
||||
.then(x => {
|
||||
this.loadedTranslation = x;
|
||||
this.translation.next(x);
|
||||
this.translatedBy = x.provider;
|
||||
this.showTranslationButton = false;
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
console.error(err);
|
||||
this.notificationService.notifyHttpError(err, this.status.provider);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
revertTranslation(): boolean {
|
||||
let revertTranslate: Translation;
|
||||
revertTranslate = {
|
||||
content: this.status.status.content,
|
||||
language: this.loadedTranslation.detected_source_language,
|
||||
detected_source_language: this.loadedTranslation.language,
|
||||
provider: this.loadedTranslation.provider,
|
||||
spoiler_text: this.status.status.spoiler_text
|
||||
};
|
||||
this.translation.next(revertTranslate);
|
||||
|
||||
this.showTranslationButton = true;
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
<div class="reblog" *ngIf="reblog">
|
||||
<a class="reblog__profile-link" href title="{{ status.account.acct }}"
|
||||
(click)="openAccount(status.account)"
|
||||
(auxclick)="openUrl(status.account.url)"><span innerHTML="{{ status.account | accountEmoji }}"></span> <img *ngIf="reblog" class="reblog__avatar" src="{{ status.account.avatar | ensureHttps }}" /></a> boosted
|
||||
(auxclick)="openUrl(status.account.url)"><span innerHTML="{{ status.account | accountEmoji }}"></span> <img *ngIf="reblog" class="reblog__avatar" src="{{ getAvatar(status.account) | ensureHttps }}" /></a> boosted
|
||||
</div>
|
||||
<div *ngIf="statusWrapper.status.pinned && !notificationType" class="pinned">
|
||||
<div class="notification--icon">
|
||||
|
@ -34,6 +34,17 @@
|
|||
boosted your status
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="notificationType === 'update'">
|
||||
<div class="notification--icon">
|
||||
<fa-icon class="update" [icon]="faEdit"></fa-icon>
|
||||
</div>
|
||||
<div class="notification--label">
|
||||
<a href class="notification--link" title="{{ notificationAccount.acct }}"
|
||||
(click)="openAccount(notificationAccount)"
|
||||
(auxclick)="openUrl(notificationAccount.url)" innerHTML="{{ notificationAccount | accountEmoji }}"></a>
|
||||
edited the status you boosted
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="notificationType === 'poll'">
|
||||
<div class="notification--icon">
|
||||
<fa-icon class="boost" [icon]="faList"></fa-icon>
|
||||
|
@ -49,9 +60,9 @@
|
|||
<div [ngClass]="{'notification--status': notificationAccount }">
|
||||
<a href class="status__profile-link" title="{{displayedStatus.account.acct}}"
|
||||
(click)="openAccount(displayedStatus.account)" (auxclick)="openUrl(displayedStatus.account.url)">
|
||||
<img [class.status__avatar--boosted]="reblog || notificationAccount" class="status__avatar" src="{{ displayedStatus.account.avatar | ensureHttps }}" />
|
||||
<img [class.status__avatar--boosted]="reblog || notificationAccount" class="status__avatar" src="{{ getAvatar(displayedStatus.account) | ensureHttps }}" />
|
||||
<!-- <img *ngIf="reblog" class="status__avatar--reblog" src="{{ status.account.avatar }}" /> -->
|
||||
<img *ngIf="notificationAccount" class="notification--avatar" src="{{ notificationAccount.avatar | ensureHttps }}" />
|
||||
<img *ngIf="notificationAccount" class="notification--avatar" src="{{ getAvatar(notificationAccount) | ensureHttps }}" />
|
||||
<span class="status__name">
|
||||
<span class="status__name--displayname"
|
||||
innerHTML="{{displayedStatus.account | accountEmoji}}"></span><span
|
||||
|
@ -85,6 +96,9 @@
|
|||
<div class="status__labels--label status__labels--remote" title="this status isn't federated with this instance" *ngIf="isRemote">
|
||||
remote
|
||||
</div>
|
||||
<div class="status__labels--label status__labels--edited" title="this status was edited" *ngIf="statusWrapper.status.edited_at">
|
||||
edited
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -95,10 +109,17 @@
|
|||
<span class="status__content-warning--title">sensitive content</span>
|
||||
<span innerHTML="{{ contentWarningText }}"></span>
|
||||
</a>
|
||||
<app-databinded-text class="status__content" *ngIf="!isContentWarned" [text]="statusContent" [selected]="isSelected"
|
||||
|
||||
<div class="status__content-warning__closed" *ngIf="!isContentWarned && contentWarningText" title="content warning">
|
||||
<span innerHTML="{{ contentWarningText }}"></span>
|
||||
</div>
|
||||
|
||||
<app-databinded-text #databindedtext class="status__content" *ngIf="!isContentWarned" [text]="statusContent" [selected]="isSelected"
|
||||
(accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)"
|
||||
(textSelected)="textSelected()"></app-databinded-text>
|
||||
|
||||
<app-status-translate [status]="displayedStatusWrapper" (translation)="onTranslation($event)"></app-status-translate>
|
||||
|
||||
<app-poll class="status__poll" *ngIf="!isContentWarned && displayedStatus.poll"
|
||||
[poll]="displayedStatus.poll" [statusWrapper]="displayedStatusWrapper"></app-poll>
|
||||
|
||||
|
|
|
@ -105,6 +105,17 @@
|
|||
background-color: rgb(33, 69, 136);
|
||||
background-color: rgb(38, 77, 148);
|
||||
}
|
||||
&--edited {
|
||||
background-color: rgb(167, 0, 153);
|
||||
background-color: rgb(0, 128, 167);
|
||||
background-color: rgb(65, 65, 71);
|
||||
background-color: rgb(144, 184, 0);
|
||||
background-color: rgb(82, 105, 0);
|
||||
|
||||
background-color: rgb(95, 95, 95);
|
||||
|
||||
// color: black;
|
||||
}
|
||||
}
|
||||
&__name {
|
||||
display: inline-block;
|
||||
|
@ -150,7 +161,8 @@
|
|||
min-height: 80px;
|
||||
display: block;
|
||||
margin: 0 10px 0 $avatar-column-space;
|
||||
padding: 3px 5px 12px 5px;
|
||||
padding: 3px 5px 14px 5px;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
|
@ -160,6 +172,26 @@
|
|||
border: 3px solid $status-secondary-color;
|
||||
color: whitesmoke;
|
||||
|
||||
&__closed {
|
||||
//margin: 0 5px 0 $avatar-column-space;
|
||||
margin: 0 5px 0 calc(#{$avatar-column-space} - 1px);
|
||||
padding: 3px 5px 3px 5px;
|
||||
margin-bottom: 5px;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
// color: #6d8fd3;
|
||||
// color: #7282a1;
|
||||
// color: #838da1;
|
||||
color: #919bb1;
|
||||
// background-color: #273149;
|
||||
// background-color: #1f273a;
|
||||
background-color: #171d2b;
|
||||
}
|
||||
|
||||
&--title {
|
||||
color: $content-warning-font-color;
|
||||
font-size: 11px;
|
||||
|
@ -246,6 +278,10 @@
|
|||
color: $boost-color;
|
||||
}
|
||||
|
||||
.update {
|
||||
color: $update-color;
|
||||
}
|
||||
|
||||
.favorite {
|
||||
color: $favorite-color;
|
||||
}
|
||||
|
@ -260,4 +296,4 @@
|
|||
&__label{
|
||||
color: $status-secondary-color;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,16 @@
|
|||
import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef } from "@angular/core";
|
||||
import { faStar, faRetweet, faList, faThumbtack } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faStar, faRetweet, faList, faThumbtack, faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Subscription } from "rxjs";
|
||||
|
||||
import { Status, Account } from "../../../services/models/mastodon.interfaces";
|
||||
import { Status, Account, Translation } from "../../../services/models/mastodon.interfaces";
|
||||
import { OpenThreadEvent, ToolsService } from "../../../services/tools.service";
|
||||
import { ActionBarComponent } from "./action-bar/action-bar.component";
|
||||
import { StatusWrapper } from '../../../models/common.model';
|
||||
import { EmojiConverter, EmojiTypeEnum } from '../../../tools/emoji.tools';
|
||||
import { ContentWarningPolicyEnum } from '../../../states/settings.state';
|
||||
import { stat } from 'fs';
|
||||
import { StatusesStateService, StatusState } from "../../../services/statuses-state.service";
|
||||
import { DatabindedTextComponent } from "./databinded-text/databinded-text.component";
|
||||
import { SettingsService } from "../../../services/settings.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-status",
|
||||
|
@ -21,6 +24,7 @@ export class StatusComponent implements OnInit {
|
|||
faRetweet = faRetweet;
|
||||
faList = faList;
|
||||
faThumbtack = faThumbtack;
|
||||
faEdit = faEdit;
|
||||
|
||||
displayedStatus: Status;
|
||||
displayedStatusWrapper: StatusWrapper;
|
||||
|
@ -41,6 +45,8 @@ export class StatusComponent implements OnInit {
|
|||
isSelected: boolean;
|
||||
isRemote: boolean;
|
||||
|
||||
private freezeAvatarEnabled: boolean;
|
||||
|
||||
hideStatus: boolean = false;
|
||||
|
||||
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||
|
@ -50,12 +56,16 @@ export class StatusComponent implements OnInit {
|
|||
|
||||
@Input() isThreadDisplay: boolean;
|
||||
|
||||
@Input() notificationType: 'mention' | 'reblog' | 'favourite' | 'poll';
|
||||
@Input() notificationType: 'mention' | 'reblog' | 'favourite' | 'poll' | 'update';
|
||||
@Input() notificationAccount: Account;
|
||||
|
||||
@Input() context: 'home' | 'notifications' | 'public' | 'thread' | 'account';
|
||||
|
||||
private _statusWrapper: StatusWrapper;
|
||||
status: Status;
|
||||
|
||||
private statusesStateServiceSub: Subscription;
|
||||
|
||||
@Input('statusWrapper')
|
||||
set statusWrapper(value: StatusWrapper) {
|
||||
this._statusWrapper = value;
|
||||
|
@ -88,7 +98,10 @@ export class StatusComponent implements OnInit {
|
|||
|
||||
// const instanceUrl = 'https://' + this.status.uri.split('https://')[1].split('/')[0];
|
||||
// this.statusAccountName = this.emojiConverter.applyEmojis(this.displayedStatus.account.emojis, this.displayedStatus.account.display_name, EmojiTypeEnum.small);
|
||||
this.statusContent = this.emojiConverter.applyEmojis(this.displayedStatus.emojis, this.displayedStatus.content, EmojiTypeEnum.medium);
|
||||
let statusContent = this.emojiConverter.applyEmojis(this.displayedStatus.emojis, this.displayedStatus.content, EmojiTypeEnum.medium);
|
||||
this.statusContent = this.ensureMentionAreDisplayed(statusContent);
|
||||
|
||||
this.validateFilteringStatus();
|
||||
}
|
||||
get statusWrapper(): StatusWrapper {
|
||||
return this._statusWrapper;
|
||||
|
@ -96,58 +109,91 @@ export class StatusComponent implements OnInit {
|
|||
|
||||
constructor(
|
||||
public elem: ElementRef,
|
||||
private readonly toolsService: ToolsService) { }
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly statusesStateService: StatusesStateService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.statusesStateServiceSub = this.statusesStateService.stateNotification.subscribe(notification => {
|
||||
if (this._statusWrapper.status.url === notification.statusId && notification.isEdited) {
|
||||
this.statusWrapper = notification.editedStatus;
|
||||
}
|
||||
});
|
||||
|
||||
this.freezeAvatarEnabled = this.settingsService.getSettings().enableFreezeAvatar;
|
||||
}
|
||||
|
||||
// private checkContentWarning(status: Status) {
|
||||
// let cwPolicy = this.toolsService.getSettings().contentWarningPolicy;
|
||||
ngOnDestroy() {
|
||||
if (this.statusesStateServiceSub) this.statusesStateServiceSub.unsubscribe();
|
||||
}
|
||||
|
||||
// let splittedContent = [];
|
||||
// if ((cwPolicy.policy === ContentWarningPolicyEnum.HideAll && cwPolicy.addCwOnContent.length > 0)
|
||||
// || (cwPolicy.policy === ContentWarningPolicyEnum.AddOnAllContent && cwPolicy.removeCwOnContent.length > 0)
|
||||
// || (cwPolicy.hideCompletlyContent && cwPolicy.hideCompletlyContent.length > 0)) {
|
||||
// let parser = new DOMParser();
|
||||
// let dom = parser.parseFromString((status.content + ' ' + status.spoiler_text).replace("<br/>", " ").replace("<br>", " ").replace(/\n/g, ' '), 'text/html')
|
||||
// let contentToParse = dom.body.textContent;
|
||||
// splittedContent = contentToParse.toLowerCase().split(' ');
|
||||
// }
|
||||
private validateFilteringStatus(){
|
||||
const filterStatus = this.displayedStatus.filtered;
|
||||
|
||||
// if (cwPolicy.policy === ContentWarningPolicyEnum.None && (status.sensitive || status.spoiler_text)) {
|
||||
// this.setContentWarning(status);
|
||||
// } else if (cwPolicy.policy === ContentWarningPolicyEnum.HideAll) {
|
||||
// let detected = cwPolicy.addCwOnContent.filter(x => splittedContent.find(y => y == x || y == `#${x}`));
|
||||
// if (!detected || detected.length === 0) {
|
||||
// this.status.sensitive = false;
|
||||
// } else {
|
||||
// if (!status.spoiler_text) {
|
||||
// status.spoiler_text = detected.join(' ');
|
||||
// }
|
||||
// this.setContentWarning(status);
|
||||
// }
|
||||
// } else if (cwPolicy.policy === ContentWarningPolicyEnum.AddOnAllContent) {
|
||||
// let detected = cwPolicy.removeCwOnContent.filter(x => splittedContent.find(y => y == x || y == `#${x}`));
|
||||
if(!filterStatus || filterStatus.length === 0) return;
|
||||
|
||||
// if (detected && detected.length > 0) {
|
||||
// this.status.sensitive = false;
|
||||
// } else {
|
||||
// this.setContentWarning(status);
|
||||
// }
|
||||
// }
|
||||
// if(!this.context){
|
||||
// console.warn('this.context not found');
|
||||
// console.warn(this.context);
|
||||
// }
|
||||
|
||||
// if (cwPolicy.hideCompletlyContent && cwPolicy.hideCompletlyContent.length > 0) {
|
||||
// let detected = cwPolicy.hideCompletlyContent.filter(x => splittedContent.find(y => y == x || y == `#${x}`));
|
||||
// if (detected && detected.length > 0) {
|
||||
// this.hideStatus = true;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
for (let filter of filterStatus) {
|
||||
if(this.context && filter.filter.context && filter.filter.context.length > 0){
|
||||
if(!filter.filter.context.includes(this.context)) continue;
|
||||
}
|
||||
|
||||
if(filter.filter.filter_action === 'warn'){
|
||||
this.isContentWarned = true;
|
||||
|
||||
let filterTxt = `FILTERED:`;
|
||||
for(let w of filter.keyword_matches){
|
||||
filterTxt += ` ${w}`;
|
||||
}
|
||||
|
||||
this.contentWarningText = filterTxt;
|
||||
} else if (filter.filter.filter_action === 'hide'){
|
||||
this.hideStatus = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAvatar(acc: Account): string {
|
||||
if(this.freezeAvatarEnabled){
|
||||
return acc.avatar_static;
|
||||
} else {
|
||||
return acc.avatar;
|
||||
}
|
||||
}
|
||||
|
||||
private ensureMentionAreDisplayed(data: string): string {
|
||||
const mentions = this.displayedStatus.mentions;
|
||||
if (!mentions || mentions.length === 0) return data;
|
||||
|
||||
let textMentions = '';
|
||||
for (const m of mentions) {
|
||||
if (!data.includes(m.url)) {
|
||||
textMentions += `<span class="h-card"><a class="u-url mention" data-user="${m.id}" href="${m.url}" rel="ugc">@<span>${m.username}</span></a></span> `
|
||||
}
|
||||
}
|
||||
if (textMentions !== '') {
|
||||
data = textMentions + data;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private setContentWarning(status: StatusWrapper) {
|
||||
this.hideStatus = status.hide;
|
||||
this.isContentWarned = status.applyCw;
|
||||
this.contentWarningText = this.emojiConverter.applyEmojis(this.displayedStatus.emojis, status.status.spoiler_text, EmojiTypeEnum.medium);
|
||||
|
||||
let spoiler = this.htmlEncode(status.status.spoiler_text);
|
||||
this.contentWarningText = this.emojiConverter.applyEmojis(this.displayedStatus.emojis, spoiler, EmojiTypeEnum.medium);
|
||||
}
|
||||
|
||||
private htmlEncode(str: string): string {
|
||||
var encodedStr = str.replace(/[\u00A0-\u9999<>\&]/gim, function (i) {
|
||||
return '&#' + i.charCodeAt(0) + ';';
|
||||
});
|
||||
return encodedStr;
|
||||
}
|
||||
|
||||
removeContentWarning(): boolean {
|
||||
|
@ -159,6 +205,31 @@ export class StatusComponent implements OnInit {
|
|||
changeCw(cwIsActive: boolean) {
|
||||
this.isContentWarned = cwIsActive;
|
||||
}
|
||||
|
||||
|
||||
@ViewChild('databindedtext') public databindedText: DatabindedTextComponent;
|
||||
|
||||
onTranslation(translation: Translation) {
|
||||
let statusContent = translation.content;
|
||||
|
||||
// clean up a bit some issues (not reliable)
|
||||
while (statusContent.includes('<span>@')) {
|
||||
statusContent = statusContent.replace('<span>@', '@<span>');
|
||||
}
|
||||
while (statusContent.includes('h<span class="invisible">')){
|
||||
statusContent = statusContent.replace('h<span class="invisible">', '<span class="invisible">h');
|
||||
}
|
||||
while (statusContent.includes('<span>#')){
|
||||
statusContent = statusContent.replace('<span>#', '#<span>');
|
||||
}
|
||||
|
||||
statusContent = this.emojiConverter.applyEmojis(this.displayedStatus.emojis, statusContent, EmojiTypeEnum.medium);
|
||||
this.statusContent = this.ensureMentionAreDisplayed(statusContent);
|
||||
|
||||
setTimeout(x => {
|
||||
this.databindedText.processEventBindings();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private checkLabels(status: Status) {
|
||||
//since API is limited with federated status...
|
||||
|
|
|
@ -122,18 +122,21 @@ export class StreamNotificationsComponent extends BrowseBase {
|
|||
loadNotifications(): any {
|
||||
this.account = this.toolsService.getAccountById(this.streamElement.accountId);
|
||||
|
||||
this.mentionsSubscription = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
|
||||
this.mentionsSubscription = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
|
||||
this.loadMentions(userNotifications);
|
||||
});
|
||||
|
||||
this.mastodonService.getNotifications(this.account, null, null, null, 10)
|
||||
this.mastodonService.getNotifications(this.account, [], null, null, 10)
|
||||
.then((notifications: Notification[]) => {
|
||||
this.isNotificationsLoading = false;
|
||||
|
||||
this.notifications = notifications.map(x => {
|
||||
let wrappedNotification= notifications.map(x => {
|
||||
let cwPolicy = this.toolsService.checkContentWarning(x.status);
|
||||
return new NotificationWrapper(x, this.account, cwPolicy.applyCw, cwPolicy.hide);
|
||||
});
|
||||
|
||||
this.notifications = wrappedNotification.filter(x => x.type !== 'mention' || (x.type === 'mention' && x.status.status !== null));
|
||||
|
||||
this.lastNotificationId = this.notifications[this.notifications.length - 1].notification.id;
|
||||
})
|
||||
.catch(err => {
|
||||
|
@ -201,7 +204,7 @@ export class StreamNotificationsComponent extends BrowseBase {
|
|||
|
||||
this.isNotificationsLoading = true;
|
||||
|
||||
this.mastodonService.getNotifications(this.account, null, this.lastNotificationId)
|
||||
this.mastodonService.getNotifications(this.account, ['update'], this.lastNotificationId)
|
||||
.then((result: Notification[]) => {
|
||||
if (result.length === 0) {
|
||||
this.notificationsMaxReached = true;
|
||||
|
@ -235,7 +238,7 @@ export class StreamNotificationsComponent extends BrowseBase {
|
|||
|
||||
this.isMentionsLoading = true;
|
||||
|
||||
this.mastodonService.getNotifications(this.account, ['follow', 'favourite', 'reblog', 'poll'], this.lastMentionId)
|
||||
this.mastodonService.getNotifications(this.account, ['follow', 'favourite', 'reblog', 'poll', 'follow_request', 'move', 'update'], this.lastMentionId)
|
||||
.then((result: Notification[]) => {
|
||||
if (result.length === 0) {
|
||||
this.mentionsMaxReached = true;
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
<div class="overlay">
|
||||
<div class="overlay__header">
|
||||
<a href class="overlay__button overlay-close" title="close" (click)="close()">
|
||||
<fa-icon class="overlay-close__icon" [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a href class="overlay__button overlay-previous"
|
||||
[ngClass]="{'overlay__button--focus': hasPreviousElements }" title="previous" (click)="previous()">
|
||||
<fa-icon class="overlay-previous__icon" [icon]="faAngleLeft"></fa-icon>
|
||||
|
@ -12,13 +8,17 @@
|
|||
title="refresh" (click)="refresh()">
|
||||
<fa-icon class="overlay-refresh__icon" [icon]="faRedoAlt"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a href class="overlay__button overlay-next" [ngClass]="{'overlay__button--focus': hasNextElements }"
|
||||
title="next" (click)="next()">
|
||||
<fa-icon class="overlay-next__icon" [icon]="faAngleRight"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a href title="return to top" class="overlay-gototop" (click)="goToTop()">
|
||||
</a>
|
||||
|
||||
<a href class="overlay__button overlay-next" [ngClass]="{'overlay__button--focus': hasNextElements }"
|
||||
title="next" (click)="next()">
|
||||
<fa-icon class="overlay-next__icon" [icon]="faAngleRight"></fa-icon>
|
||||
<a href class="overlay__button overlay-close" title="close" (click)="close()">
|
||||
<fa-icon class="overlay-close__icon" [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -11,7 +11,9 @@ $header-content-height: 40px;
|
|||
width: calc(100%);
|
||||
height: $header-content-height;
|
||||
background-color: $column-header-background-color;
|
||||
border-bottom: 1px solid #222736;
|
||||
border-bottom: 1px solid #222736;
|
||||
|
||||
display: flex;
|
||||
}
|
||||
&__content-wrapper {
|
||||
transition: all .2s;
|
||||
|
@ -44,11 +46,17 @@ $header-content-height: 40px;
|
|||
}
|
||||
|
||||
&__button {
|
||||
// outline: 1px dotted orange;
|
||||
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
|
||||
width: $header-content-height;
|
||||
height: $header-content-height;
|
||||
|
||||
color: #354060;
|
||||
transition: all .2s;
|
||||
margin: 8px 0 0 8px;
|
||||
|
||||
&:hover {
|
||||
color: #536599;
|
||||
color: #7a8dc7;
|
||||
|
@ -68,19 +76,8 @@ $header-content-height: 40px;
|
|||
|
||||
&__icon {
|
||||
position: relative;
|
||||
left: 7px;
|
||||
top: -1px
|
||||
}
|
||||
}
|
||||
&-next {
|
||||
display: block;
|
||||
float: left;
|
||||
font-size: 18px;
|
||||
|
||||
&__icon {
|
||||
position: relative;
|
||||
left: 8px;
|
||||
top: -1px
|
||||
left: 17px;
|
||||
top: 7px
|
||||
}
|
||||
}
|
||||
&-refresh {
|
||||
|
@ -90,29 +87,38 @@ $header-content-height: 40px;
|
|||
|
||||
&__icon {
|
||||
position: relative;
|
||||
left: 5px;
|
||||
top: 1px
|
||||
left: 13px;
|
||||
top: 9px
|
||||
}
|
||||
}
|
||||
&-next {
|
||||
display: block;
|
||||
float: left;
|
||||
font-size: 18px;
|
||||
|
||||
&__icon {
|
||||
position: relative;
|
||||
left: 13px;
|
||||
top: 7px
|
||||
}
|
||||
}
|
||||
&-gototop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 110px;
|
||||
right: 40px;
|
||||
// outline: 1px dotted orange;
|
||||
|
||||
flex-grow: 1;
|
||||
|
||||
display: block;
|
||||
height: $header-content-height;
|
||||
}
|
||||
&-close {
|
||||
display: block;
|
||||
float: right;
|
||||
font-size: 13px;
|
||||
color: white;
|
||||
margin-right: 8px;
|
||||
|
||||
&__icon {
|
||||
position: relative;
|
||||
left: 7px;
|
||||
top: 1px
|
||||
left: 15px;
|
||||
top: 9px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,20 +5,23 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div class="stream-toots__new-notification"
|
||||
<div class="stream-toots__new-notification"
|
||||
[class.stream-toots__new-notification--display]="bufferStream && bufferStream.length > 0 && !streamPositionnedAtTop"></div>
|
||||
|
||||
<div class="stream-toots__content flexcroll" #statusstream (scroll)="onScroll()" tabindex="0">
|
||||
<div class="stream-toots__content flexcroll" #statusstream (scroll)="onScroll()" tabindex="0">
|
||||
<div *ngIf="displayError" class="stream-toots__error">{{displayError}}</div>
|
||||
|
||||
<div *ngIf="timelineLoadingMode === 3 && bufferStream && bufferStream.length > 0">
|
||||
<a href (click)="loadBuffer()" class="stream-toots__load-buffer" title="load new items">{{ bufferStream.length }} new item<span *ngIf="bufferStream.length > 1">s</span></a>
|
||||
<div *ngIf="timelineLoadingMode === 3 && bufferStream && numNewItems > 0">
|
||||
<a href (click)="loadBuffer()" class="stream-toots__load-buffer" title="load new items">{{ numNewItems }} new item<span *ngIf="numNewItems > 1">s</span></a>
|
||||
</div>
|
||||
|
||||
<div class="stream-toots__status" *ngFor="let statusWrapper of statuses" #status>
|
||||
<app-status
|
||||
[statusWrapper]="statusWrapper" [isThreadDisplay]="isThread"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
<app-status
|
||||
[statusWrapper]="statusWrapper"
|
||||
[isThreadDisplay]="isThread"
|
||||
[context]="context"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-status>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { HttpErrorResponse } from '@angular/common/http';
|
|||
import { Observable, Subscription } from 'rxjs';
|
||||
import { Store } from '@ngxs/store';
|
||||
|
||||
import { StreamElement } from '../../../states/streams.state';
|
||||
import { StreamElement, StreamTypeEnum } from '../../../states/streams.state';
|
||||
import { AccountInfo } from '../../../states/accounts.state';
|
||||
import { StreamingService, EventEnum, StatusUpdate } from '../../../services/streaming.service';
|
||||
import { Status } from '../../../services/models/mastodon.interfaces';
|
||||
|
@ -13,15 +13,18 @@ import { ToolsService } from '../../../services/tools.service';
|
|||
import { StatusWrapper } from '../../../models/common.model';
|
||||
import { TimeLineModeEnum } from '../../../states/settings.state';
|
||||
import { TimelineBase } from '../../common/timeline-base';
|
||||
import { SettingsService } from '../../../services/settings.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-stream-statuses',
|
||||
templateUrl: './stream-statuses.component.html',
|
||||
styleUrls: ['./stream-statuses.component.scss']
|
||||
})
|
||||
export class StreamStatusesComponent extends TimelineBase {
|
||||
export class StreamStatusesComponent extends TimelineBase {
|
||||
protected _streamElement: StreamElement;
|
||||
|
||||
context: 'home' | 'notifications' | 'public' | 'thread' | 'account';
|
||||
|
||||
@Input()
|
||||
set streamElement(streamElement: StreamElement) {
|
||||
this._streamElement = streamElement;
|
||||
|
@ -31,6 +34,8 @@ export class StreamStatusesComponent extends TimelineBase {
|
|||
this.hideReplies = streamElement.hideReplies;
|
||||
|
||||
this.load(this._streamElement);
|
||||
|
||||
this.setContext(this._streamElement);
|
||||
}
|
||||
get streamElement(): StreamElement {
|
||||
return this._streamElement;
|
||||
|
@ -43,6 +48,7 @@ export class StreamStatusesComponent extends TimelineBase {
|
|||
private streams$: Observable<StreamElement[]>;
|
||||
|
||||
constructor(
|
||||
protected readonly settingsService: SettingsService,
|
||||
protected readonly store: Store,
|
||||
protected readonly toolsService: ToolsService,
|
||||
protected readonly notificationService: NotificationService,
|
||||
|
@ -54,7 +60,7 @@ export class StreamStatusesComponent extends TimelineBase {
|
|||
}
|
||||
|
||||
ngOnInit() {
|
||||
let settings = this.toolsService.getSettings();
|
||||
let settings = this.settingsService.getSettings();
|
||||
this.timelineLoadingMode = settings.timelineMode;
|
||||
|
||||
this.goToTopSubscription = this.goToTop.subscribe(() => {
|
||||
|
@ -99,6 +105,8 @@ export class StreamStatusesComponent extends TimelineBase {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.numNewItems = 0;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
@ -108,6 +116,24 @@ export class StreamStatusesComponent extends TimelineBase {
|
|||
if (this.deleteStatusSubscription) this.deleteStatusSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
private setContext(streamElement: StreamElement) {
|
||||
switch(streamElement.type){
|
||||
case StreamTypeEnum.global:
|
||||
case StreamTypeEnum.local:
|
||||
case StreamTypeEnum.tag:
|
||||
this.context = 'public';
|
||||
break;
|
||||
case StreamTypeEnum.personnal:
|
||||
case StreamTypeEnum.list:
|
||||
this.context = 'home';
|
||||
break;
|
||||
case StreamTypeEnum.activity:
|
||||
case StreamTypeEnum.directmessages:
|
||||
this.context = 'notifications';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
refresh(): any {
|
||||
this.load(this._streamElement);
|
||||
}
|
||||
|
@ -131,6 +157,7 @@ export class StreamStatusesComponent extends TimelineBase {
|
|||
private resetStream() {
|
||||
this.statuses.length = 0;
|
||||
this.bufferStream.length = 0;
|
||||
this.numNewItems = 0;
|
||||
if (this.websocketStreaming) this.websocketStreaming.dispose();
|
||||
}
|
||||
|
||||
|
@ -152,6 +179,7 @@ export class StreamStatusesComponent extends TimelineBase {
|
|||
this.statuses.unshift(wrapper);
|
||||
} else {
|
||||
this.bufferStream.push(update.status);
|
||||
this.numNewItems++;
|
||||
}
|
||||
}
|
||||
} else if (update.type === EventEnum.delete) {
|
||||
|
@ -199,6 +227,7 @@ export class StreamStatusesComponent extends TimelineBase {
|
|||
}
|
||||
|
||||
this.bufferStream.length = 0;
|
||||
this.numNewItems = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -210,7 +239,7 @@ export class StreamStatusesComponent extends TimelineBase {
|
|||
return status.filter(x => !this.isFiltered(x));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private isFiltered(status: Status): boolean {
|
||||
if (this.streamElement.hideBoosts) {
|
||||
if (status.reblog) {
|
||||
|
|
|
@ -7,11 +7,13 @@
|
|||
<!-- <div> -->
|
||||
<div class="stream-column__stream-header">
|
||||
<a class="stream-column__stream-selector" href title="return to top" (click)="goToTop()">
|
||||
<img *ngIf="timelineHeader === 3 || timelineHeader === 4" class="stream-column__stream-selector--avatar" src="{{avatar}}" />
|
||||
<img *ngIf="timelineHeader === 3 || timelineHeader === 4 || timelineHeader === 6" class="stream-column__stream-selector--avatar" src="{{avatar}}" />
|
||||
<fa-icon class="stream-column__stream-selector--icon" [icon]="columnFaIcon"></fa-icon>
|
||||
<span class="stream-column__stream-selector--text">
|
||||
<h1 class="stream-column__stream-selector--title" [class.stream-column__stream-selector--title--only]="timelineHeader === 4 || timelineHeader === 5">{{ streamElement.name.toUpperCase() }}</h1>
|
||||
<span class="stream-column__stream-selector--subtitle" *ngIf="streamElement.instance && timelineHeader !== 4 && timelineHeader !== 5"><span *ngIf="timelineHeader === 2">{{account.username}}@</span>{{ streamElement.instance.toLowerCase() }}</span>
|
||||
<span class="stream-column__stream-selector--subtitle" *ngIf="streamElement.instance && timelineHeader !== 4 && timelineHeader !== 5">
|
||||
<span *ngIf="timelineHeader === 2 || timelineHeader === 6">{{account.username}}@</span>{{ streamElement.instance.toLowerCase() }}
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
<a class="stream-column__open-menu" href title="edit column" (click)="openEditionMenu()">
|
||||
|
|
|
@ -8,6 +8,7 @@ import { StreamStatusesComponent } from './stream-statuses/stream-statuses.compo
|
|||
import { StreamNotificationsComponent } from './stream-notifications/stream-notifications.component';
|
||||
import { TimeLineHeaderEnum } from '../../states/settings.state';
|
||||
import { AccountInfo } from '../../states/accounts.state';
|
||||
import { SettingsService } from '../../services/settings.service';
|
||||
|
||||
@Component({
|
||||
selector: "app-stream",
|
||||
|
@ -72,10 +73,12 @@ export class StreamComponent implements OnInit {
|
|||
return this._streamElement;
|
||||
}
|
||||
|
||||
constructor(private toolsService: ToolsService) { }
|
||||
constructor(
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly toolsService: ToolsService) { }
|
||||
|
||||
ngOnInit() {
|
||||
let settings = this.toolsService.getSettings();
|
||||
let settings = this.settingsService.getSettings();
|
||||
this.timelineHeader = settings.timelineHeader;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import scrollIntoView from 'scroll-into-view-if-needed';
|
|||
import { UserNotificationService, UserNotification } from '../../../services/user-notification.service';
|
||||
import { TimeLineModeEnum } from '../../../states/settings.state';
|
||||
import { BrowseBase } from '../../common/browse-base';
|
||||
import { SettingsService } from '../../../services/settings.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-thread',
|
||||
|
@ -27,6 +28,9 @@ export class ThreadComponent extends BrowseBase {
|
|||
hasContentWarnings = false;
|
||||
private remoteStatusFetchingDisabled = false;
|
||||
|
||||
context = 'thread';
|
||||
|
||||
numNewItems: number; //html compatibility only
|
||||
bufferStream: Status[] = []; //html compatibility only
|
||||
streamPositionnedAtTop: boolean = true; //html compatibility only
|
||||
timelineLoadingMode: TimeLineModeEnum = TimeLineModeEnum.OnTop; //html compatibility only
|
||||
|
@ -54,6 +58,7 @@ export class ThreadComponent extends BrowseBase {
|
|||
private responseSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly httpClient: HttpClient,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly userNotificationService: UserNotificationService,
|
||||
|
@ -63,7 +68,7 @@ export class ThreadComponent extends BrowseBase {
|
|||
}
|
||||
|
||||
ngOnInit() {
|
||||
let settings = this.toolsService.getSettings();
|
||||
let settings = this.settingsService.getSettings();
|
||||
this.remoteStatusFetchingDisabled = settings.disableRemoteStatusFetching;
|
||||
|
||||
if (this.refreshEventEmitter) {
|
||||
|
|
|
@ -96,7 +96,6 @@ export class UserFollowsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
})
|
||||
.then((result: FollowingResult) => {
|
||||
console.warn(result);
|
||||
this.maxId = result.maxId;
|
||||
this.accounts = result.follows;
|
||||
})
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<h2 class="profile__floating-header__names__display-name"
|
||||
innerHTML="{{displayedAccount | accountEmoji }}" title="{{displayedAccount.display_name}}"></h2>
|
||||
<a class="profile__floating-header__names__fullhandle" href="{{displayedAccount.url}}"
|
||||
target="_blank" title="{{displayedAccount.acct}}">@{{displayedAccount.acct}}</a>
|
||||
target="_blank" title="{{displayedAccount.acct}}">@{{displayedAccount.acct}}</a> <fa-icon class="fa-lock" *ngIf="displayedAccount.locked" [icon]="faLock" title="account locked"></fa-icon>
|
||||
</div>
|
||||
|
||||
<div class="profile__floating-header__follow" *ngIf="relationship && !displayedAccount.moved">
|
||||
|
@ -107,7 +107,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<app-status-user-context-menu class="profile-header__more" [displayedAccount]="displayedAccount">
|
||||
<app-status-user-context-menu class="profile-header__more"
|
||||
[displayedAccount]="displayedAccount" [relationship]="relationship"
|
||||
(relationshipChanged)="relationshipChanged($event)">
|
||||
</app-status-user-context-menu>
|
||||
</div>
|
||||
|
||||
|
@ -118,7 +120,7 @@
|
|||
<h2 class="profile-name__link profile-name__display-name"
|
||||
innerHTML="{{displayedAccount | accountEmoji }}" title="{{displayedAccount.display_name}}"></h2>
|
||||
<h2 class="profile-name__link profile-name__fullhandle"><a href="{{displayedAccount.url}}"
|
||||
target="_blank" title="{{displayedAccount.acct}}">@{{displayedAccount.acct}}</a></h2>
|
||||
target="_blank" title="{{displayedAccount.acct}}">@{{displayedAccount.acct}}</a> <fa-icon class="fa-lock" *ngIf="displayedAccount.locked" [icon]="faLock" title="account locked"></fa-icon></h2>
|
||||
</div>
|
||||
|
||||
<div class="profile-follows">
|
||||
|
@ -164,39 +166,27 @@
|
|||
|
||||
<div class="profile__extra-info profile__extra-info__preparefloating" *ngIf="!isLoading"
|
||||
[class.profile__extra-info__floating]="showFloatingStatusMenu">
|
||||
<div class="profile__extra-info__section">
|
||||
<a href class="profile__extra-info__links" (click)="switchStatusSection('status')" title="Status"
|
||||
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('status')" title="Status"
|
||||
[class.profile__extra-info__links--selected]="statusSection === 'status'">Status</a>
|
||||
</div>
|
||||
<div class="profile__extra-info__section">
|
||||
<a href class="profile__extra-info__links" (click)="switchStatusSection('replies')"
|
||||
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('replies')"
|
||||
title="Status & Replies"
|
||||
[class.profile__extra-info__links--selected]="statusSection === 'replies'">Status &
|
||||
Replies</a>
|
||||
</div>
|
||||
<div class="profile__extra-info__section">
|
||||
<a href class="profile__extra-info__links" (click)="switchStatusSection('media')" title="Media"
|
||||
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('media')" title="Media"
|
||||
[class.profile__extra-info__links--selected]="statusSection === 'media'">Media</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-statuses" #profilestatuses>
|
||||
<div class="profile__extra-info" *ngIf="!isLoading">
|
||||
<div class="profile__extra-info__section">
|
||||
<a href class="profile__extra-info__links" (click)="switchStatusSection('status')"
|
||||
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('status')"
|
||||
title="Status"
|
||||
[class.profile__extra-info__links--selected]="statusSection === 'status'">Status</a>
|
||||
</div>
|
||||
<div class="profile__extra-info__section">
|
||||
<a href class="profile__extra-info__links" (click)="switchStatusSection('replies')"
|
||||
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('replies')"
|
||||
title="Status & Replies"
|
||||
[class.profile__extra-info__links--selected]="statusSection === 'replies'">Status &
|
||||
Replies</a>
|
||||
</div>
|
||||
<div class="profile__extra-info__section">
|
||||
<a href class="profile__extra-info__links" (click)="switchStatusSection('media')" title="Media"
|
||||
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('media')" title="Media"
|
||||
[class.profile__extra-info__links--selected]="statusSection === 'media'">Media</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div [class.profile__status-switching-section]="isSwitchingSection">
|
||||
|
@ -206,21 +196,29 @@
|
|||
|
||||
<div *ngIf="statusSection === 'status' && !statusLoading">
|
||||
<div *ngFor="let statusWrapper of pinnedStatuses">
|
||||
<app-status [statusWrapper]="statusWrapper" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseThreadEvent)="browseThread($event)">
|
||||
<app-status
|
||||
[statusWrapper]="statusWrapper"
|
||||
[context]="'account'"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseThreadEvent)="browseThread($event)">
|
||||
</app-status>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngFor="let statusWrapper of statuses">
|
||||
<div *ngIf="statusSection !== 'media'">
|
||||
<app-status [statusWrapper]="statusWrapper" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseThreadEvent)="browseThread($event)">
|
||||
<app-status
|
||||
[statusWrapper]="statusWrapper"
|
||||
[context]="'account'"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseThreadEvent)="browseThread($event)">
|
||||
</app-status>
|
||||
</div>
|
||||
<div *ngIf="statusSection === 'media'" class="status-media">
|
||||
<div *ngFor="let media of statusWrapper.status.media_attachments">
|
||||
<app-attachement-image *ngIf="media.type === 'image' || media.type === 'gifv'" class="status-media__image" [attachment]="media" (openEvent)="openAttachment(media)"></app-attachement-image>
|
||||
<app-attachement-image *ngIf="media.type === 'image' || media.type === 'gifv'" class="status-media__image" [attachment]="media" [status]="statusWrapper" (openEvent)="openAttachment(media)" (browseThreadEvent)="browseThread($event)"></app-attachement-image>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -275,14 +275,15 @@ $floating-header-height: 60px;
|
|||
&-follows {
|
||||
width: calc(100%);
|
||||
font-size: 13px;
|
||||
border-bottom: 1px solid #0f111a;;
|
||||
border-bottom: 1px solid #0f111a;
|
||||
|
||||
display: flex;
|
||||
|
||||
&__link {
|
||||
color: white;
|
||||
width: calc(50%);
|
||||
flex-grow: 1;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
background-color: #1a1f2e;
|
||||
transition: all .2s;
|
||||
|
||||
|
@ -311,15 +312,15 @@ $floating-header-height: 60px;
|
|||
font-size: 13px;
|
||||
transition: all .4s;
|
||||
|
||||
&__section {
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
width: calc(33.333% - 5px);
|
||||
padding: 5px 0 7px 0;
|
||||
display: flex;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
&__section {
|
||||
// outline: 1px dotted orange;
|
||||
|
||||
flex-grow: 1;
|
||||
|
||||
text-align: center;
|
||||
padding: 5px 0 7px 0;
|
||||
}
|
||||
|
||||
&__preparefloating {
|
||||
|
@ -402,6 +403,12 @@ $floating-header-height: 60px;
|
|||
}
|
||||
}
|
||||
|
||||
.fa-lock {
|
||||
margin-left: 5px;
|
||||
color: gray;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
//Mastodon styling
|
||||
:host ::ng-deep .profile-fields__field--value {
|
||||
// font-size: 14px;
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { faUser, faHourglassHalf, faUserCheck, faExclamationTriangle, faLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faUser, faHourglassHalf, faUserCheck, faExclamationTriangle, faLink, faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faUser as faUserRegular } from "@fortawesome/free-regular-svg-icons";
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { Store } from '@ngxs/store';
|
||||
|
||||
import { Account, Status, Relationship, Attachment } from "../../../services/models/mastodon.interfaces";
|
||||
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
|
||||
import { ToolsService, OpenThreadEvent } from '../../../services/tools.service';
|
||||
import { ToolsService, OpenThreadEvent, InstanceType } from '../../../services/tools.service';
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { AccountInfo } from '../../../states/accounts.state';
|
||||
import { StatusWrapper, OpenMediaEvent } from '../../../models/common.model';
|
||||
|
@ -29,6 +29,7 @@ export class UserProfileComponent extends BrowseBase {
|
|||
faUserCheck = faUserCheck;
|
||||
faExclamationTriangle = faExclamationTriangle;
|
||||
faLink = faLink;
|
||||
faLock = faLock;
|
||||
|
||||
displayedAccount: Account;
|
||||
hasNote: boolean;
|
||||
|
@ -267,6 +268,10 @@ export class UserProfileComponent extends BrowseBase {
|
|||
this.showFloatingStatusMenu = false;
|
||||
this.load(this.lastAccountName);
|
||||
}
|
||||
|
||||
relationshipChanged(relationship: Relationship){
|
||||
this.relationship = relationship;
|
||||
}
|
||||
|
||||
browseAccount(accountName: string): void {
|
||||
if (accountName === this.toolsService.getAccountFullHandle(this.displayedAccount)) return;
|
||||
|
@ -281,21 +286,44 @@ export class UserProfileComponent extends BrowseBase {
|
|||
}
|
||||
|
||||
follow(): boolean {
|
||||
this.loadingRelationShip = true;
|
||||
|
||||
const userAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
let foundAccountToFollow: Account;
|
||||
this.toolsService.findAccount(userAccount, this.lastAccountName)
|
||||
.then((account: Account) => {
|
||||
foundAccountToFollow = account;
|
||||
return this.mastodonService.follow(userAccount, account);
|
||||
})
|
||||
.then((relationship: Relationship) => {
|
||||
this.relationship = relationship;
|
||||
this.relationship = relationship;
|
||||
})
|
||||
.then(async () => {
|
||||
// Double check for pleroma users
|
||||
const instanceInfo = await this.toolsService.getInstanceInfo(userAccount);
|
||||
if(instanceInfo.type === InstanceType.Pleroma || instanceInfo.type === InstanceType.Akkoma){
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
const relationships = await this.mastodonService.getRelationships(userAccount, [foundAccountToFollow]);
|
||||
const relationship = relationships.find(x => x.id === foundAccountToFollow.id);
|
||||
if(relationship){
|
||||
this.relationship = relationship;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.notificationService.notifyHttpError(err, userAccount);
|
||||
})
|
||||
.then(() => {
|
||||
this.loadingRelationShip = false;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
unfollow(): boolean {
|
||||
this.loadingRelationShip = true;
|
||||
|
||||
const userAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
this.toolsService.findAccount(userAccount, this.lastAccountName)
|
||||
.then((account: Account) => {
|
||||
|
@ -306,6 +334,9 @@ export class UserProfileComponent extends BrowseBase {
|
|||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.notificationService.notifyHttpError(err, userAccount);
|
||||
})
|
||||
.then(() => {
|
||||
this.loadingRelationShip = false;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { HotkeysService, Hotkey } from 'angular2-hotkeys';
|
|||
|
||||
import { StreamElement, StreamTypeEnum } from '../../states/streams.state';
|
||||
import { NavigationService } from '../../services/navigation.service';
|
||||
import { ToolsService } from '../../services/tools.service';
|
||||
import { SettingsService } from '../../services/settings.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-streams-selection-footer',
|
||||
|
@ -17,14 +17,14 @@ export class StreamsSelectionFooterComponent implements OnInit {
|
|||
private streams$: Observable<StreamElement[]>;
|
||||
|
||||
constructor(
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly hotkeysService: HotkeysService,
|
||||
private readonly navigationService: NavigationService,
|
||||
private readonly store: Store) {
|
||||
|
||||
this.streams$ = this.store.select(state => state.streamsstatemodel.streams);
|
||||
|
||||
const settings = this.toolsService.getSettings();
|
||||
const settings = this.settingsService.getSettings();
|
||||
if(!settings.columnSwitchingWinAlt) {
|
||||
this.hotkeysService.add(new Hotkey('ctrl+right', (event: KeyboardEvent): boolean => {
|
||||
this.nextColumnSelected();
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
<div class="tutorial-content flexcroll">
|
||||
<h2 class="tutorial-content__title-important">Labels</h2>
|
||||
<p>
|
||||
Sengi uses labels on statuses to describe them properly: <br />
|
||||
<br />
|
||||
<img class="content__expand" src="assets/img/labels.png" /><br />
|
||||
</p>
|
||||
|
||||
|
||||
<h3 class="tutorial-content__subtitle">Definitions</h3>
|
||||
<br />
|
||||
<div class="doc-label-wrapper">
|
||||
<div class="doc-label doc-label__bot">
|
||||
BOT
|
||||
</div>
|
||||
<div class="doc-label-definition">
|
||||
The account is tagged as a bot
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-label-wrapper">
|
||||
<div class="doc-label doc-label__replies">
|
||||
REPLIES
|
||||
</div>
|
||||
<div class="doc-label-definition">
|
||||
The status has replies
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-label-wrapper">
|
||||
<div class="doc-label doc-label__thread">
|
||||
THREAD
|
||||
</div>
|
||||
<div class="doc-label-definition">
|
||||
The status is part of a thread
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-label-wrapper">
|
||||
<div class="doc-label doc-label__xpost">
|
||||
X-POST
|
||||
</div>
|
||||
<div class="doc-label-definition">
|
||||
The status was cross-posted using a software like <a href="https://moa.party/" target="_blank">MOA</a>.<br/>
|
||||
<span class="doc-label-definition__gray">This functionality is limited to Local TL/Accounts due to API limitation.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-label-wrapper">
|
||||
<div class="doc-label doc-label__old">
|
||||
OLD
|
||||
</div>
|
||||
<div class="doc-label-definition">
|
||||
The status was posted more than 3 months ago.
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-label-wrapper">
|
||||
<div class="doc-label doc-label__remote">
|
||||
REMOTE
|
||||
</div>
|
||||
<div class="doc-label-definition">
|
||||
The status wasn't federated with your instance and was fetched remotely.<br/>
|
||||
<span class="doc-label-definition__gray">This functionality bypasses the blocking rules of your account/instance, you can disable it in the settings.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<p>
|
||||
|
||||
</p>
|
||||
</div>
|
|
@ -0,0 +1,60 @@
|
|||
.doc-label-wrapper {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.doc-label {
|
||||
width: 70px;
|
||||
// height: 15px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 3px 0 2px 0;
|
||||
font-size: 11px;
|
||||
border-radius: 3px;
|
||||
float: left;
|
||||
|
||||
&__bot {
|
||||
background-color: #017282;
|
||||
}
|
||||
|
||||
&__replies {
|
||||
background-color: #59028f;
|
||||
}
|
||||
|
||||
&__thread {
|
||||
background-color: #007233;
|
||||
}
|
||||
|
||||
&__xpost {
|
||||
background-color: #9f5d00;
|
||||
}
|
||||
|
||||
&__old {
|
||||
background-color: #960000;
|
||||
}
|
||||
|
||||
&__remote {
|
||||
background-color: #264d94;
|
||||
}
|
||||
}
|
||||
|
||||
.doc-label-definition {
|
||||
margin-left: 85px;
|
||||
position: relative;
|
||||
// top: -1px;
|
||||
|
||||
& a {
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
color: rgb(218, 218, 218);
|
||||
}
|
||||
}
|
||||
|
||||
&__gray {
|
||||
display: inline-block;
|
||||
margin-top: 5px;
|
||||
color: #0f111a;
|
||||
color: #9497a5;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LabelsTutorialComponent } from './labels-tutorial.component';
|
||||
|
||||
xdescribe('LabelsTutorialComponent', () => {
|
||||
let component: LabelsTutorialComponent;
|
||||
let fixture: ComponentFixture<LabelsTutorialComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LabelsTutorialComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LabelsTutorialComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-labels-tutorial',
|
||||
templateUrl: './labels-tutorial.component.html',
|
||||
styleUrls: ['../tutorial-enhanced.component.scss', 'labels-tutorial.component.scss']
|
||||
})
|
||||
export class LabelsTutorialComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<div class="tutorial-content flexcroll">
|
||||
<h2 class="tutorial-content__title-important">And... you're all set! 🎉</h2>
|
||||
<p>
|
||||
Please find below more information about Sengi.<br />
|
||||
<br />
|
||||
</p>
|
||||
|
||||
<h3 class="tutorial-content__subtitle">Notifications</h3>
|
||||
<p>
|
||||
By default, Sengi doesn't need notification timelines: it has its own way to do it.<br />
|
||||
<br />
|
||||
When your account will receive a new mention, follow, boost, favorite and other kind of notifications, your
|
||||
avatar's icon will start flashing this way:<br />
|
||||
<br />
|
||||
<img class="content__center" src="assets/video/flashing.gif">
|
||||
<!-- <video class="content__center clean-outline" autoplay muted loop>
|
||||
<source src="assets/video/flashing.mp4" type="video/mp4">
|
||||
</video> -->
|
||||
<br />
|
||||
If you right-click on your avatar, the account panel will automatically open and switch to the related
|
||||
subsection (mentions or notifications) and show you the last entries.<br />
|
||||
The flashing animation will then automatically stop:<br />
|
||||
<br />
|
||||
<video class="content__expand clean-outline" autoplay muted loop controls>
|
||||
<source src="assets/video/notification.mp4" type="video/mp4">
|
||||
</video>
|
||||
<br />
|
||||
The aim of this mechanism is to prevent the flood of notification timelines (one per account) in the interface,
|
||||
especially if you add a lot of accounts in Sengi. <br />
|
||||
<br />
|
||||
Of course, you can disable various parts of this mechanism in the settings like "disable avatar notification", "disable account's panel autofocus", etc.<br />
|
||||
</p>
|
||||
</div>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue