Compare commits

...

149 Commits

Author SHA1 Message Date
Nicolas Constant 822ef21985
Merge pull request #639 from NicolasConstant/develop
1.7.0 PR
2024-03-09 15:32:25 -05:00
Nicolas Constant a154028a53 road to 1.7.0 2024-03-09 15:10:04 -05:00
Nicolas Constant 6a8d85f40c
Merge pull request #638 from NicolasConstant/topic_enhance-filters
Topic enhance filters
2024-03-09 02:36:17 -05:00
Nicolas Constant 04153543a9 fix filtering message 2024-03-09 02:00:05 -05:00
Nicolas Constant 92ec089eab fix error spamming, fix #605 2024-03-09 01:46:26 -05:00
Nicolas Constant 12ce0a3a4a added context filtering 2024-03-09 00:40:37 -05:00
Nicolas Constant 7a6eb9c3d2 starting handling server filters 2024-03-08 03:13:58 -05:00
Nicolas Constant 63b7c6fdf1 added filter params in status interface 2024-03-08 02:12:00 -05:00
Nicolas Constant bd75317417 retrieve full handle on post edition, fix #630 2024-03-08 01:53:28 -05:00
Nicolas Constant 74eed7e8ba fix pleroma vote count, fix #398 2024-03-08 00:51:27 -05:00
Nicolas Constant ebce6282c5 better follow workflow, fix #629 2024-03-08 00:19:39 -05:00
Nicolas Constant 702e4daa44
Merge pull request #637 from NicolasConstant/topic_refine-css-zones
Topic refine css zones
2024-03-07 18:44:40 -05:00
Nicolas Constant d2221d539c fix undefined image description, fix #632 2024-03-07 18:23:26 -05:00
Nicolas Constant c4de387f86 sort list in alphabetical order, fix #633 2024-03-07 01:17:04 -05:00
Nicolas Constant c0f84ddc11 open status from image galery, fix #627 2024-03-07 00:59:00 -05:00
Nicolas Constant 1830212a91 flex migration of account navigation 2024-03-07 00:24:27 -05:00
Nicolas Constant 46adf207bb flex migration floating column + hover timeline 2024-03-06 23:13:46 -05:00
Nicolas Constant 909b190b33 fix tests 2024-03-06 19:14:30 -05:00
Nicolas Constant cfc4d5f915 GoToSocial max char handling, fix #576 2024-03-05 18:15:43 -05:00
Nicolas Constant 0f58252c61
Merge pull request #614 from NicolasConstant/develop
1.6.0 PR
2023-08-24 00:30:44 -04:00
Nicolas Constant 0d2ac6b569
road to 1.6.0 2023-08-23 23:43:50 -04:00
Nicolas Constant e62987b11a
Merge pull request #611 from NicolasConstant/topic_reorganize-accounts
Topic reorganize accounts
2023-08-20 03:35:34 -04:00
Nicolas Constant 8cee7289eb
Merge pull request #612 from NicolasConstant/topic_add-more-user-actions
Topic add more user actions
2023-08-20 03:33:32 -04:00
Nicolas Constant 0305cc6ac7
added button to unlock icons, fix #489 2023-08-20 02:33:03 -04:00
Nicolas Constant f215d027f9
added draggable aspect in service 2023-08-20 01:44:46 -04:00
Nicolas Constant 335cbf4956
added reorder state changing 2023-08-20 01:33:07 -04:00
Nicolas Constant b41c31b4ac
better draggable design 2023-08-20 01:16:41 -04:00
Nicolas Constant 41faa36087
working drag&drop 2023-08-20 00:36:52 -04:00
Nicolas Constant 024042959e
fix poll exception on creation 2023-08-19 21:33:43 -04:00
Nicolas Constant f4c87df078
added multi account faq, fix #608 2023-08-19 21:28:20 -04:00
Nicolas Constant d24441343a
always show cw text, fix #480 2023-08-19 20:39:37 -04:00
Nicolas Constant 8c9685045e
added hide boosts, block domain, fix #487 2023-08-19 20:02:37 -04:00
Nicolas Constant a0cb240446
Merge pull request #610 from NicolasConstant/topic_enhance-language-feature
Topic enhance language feature
2023-08-19 16:45:03 -04:00
Nicolas Constant 2def5725f5
option to stop animated avatar, fix #335 2023-08-12 18:41:41 -04:00
Nicolas Constant 450a0088d5
added alt label display option, fix #387 2023-08-12 17:59:34 -04:00
Nicolas Constant d7f988ecb9
added lang autodetect disable option 2023-08-12 17:16:57 -04:00
Nicolas Constant 8703df27d5
display years, fix #603 2023-08-12 17:05:25 -04:00
Nicolas Constant 10fa412173
ensure language is set to provide translation 2023-08-12 16:50:35 -04:00
Nicolas Constant 0b93ed7307
language autodetection functionnal 2023-08-12 01:33:07 -04:00
Nicolas Constant c3cd6fe79e
Merge pull request #604 from NicolasConstant/develop
1.5.0 PR
2023-08-07 20:09:31 -04:00
Nicolas Constant 14287b476c
ensure lang exists before using it 2023-08-07 19:56:46 -04:00
Nicolas Constant 2b106ba546
clean up 2023-08-07 19:48:23 -04:00
Nicolas Constant 4a2b408c1b
added some translation cleanup 2023-08-07 19:47:54 -04:00
Nicolas Constant 92a3ac6ae3
road to 1.5.0 2023-08-07 01:18:08 -04:00
Nicolas Constant ec0bed4606
wording: expand CW instead of hide, fix #459 2023-08-06 17:39:28 -04:00
Nicolas Constant 62d4140d63
close panel and unfocus on esc, fix #429 2023-08-06 17:15:00 -04:00
Nicolas Constant 4a34063dc8
focus on seach, fix #429 (partialy) 2023-08-06 17:06:36 -04:00
Nicolas Constant 9cd709f44c
better follow hastag positionning 2023-08-06 17:02:05 -04:00
Nicolas Constant 64ceb3e095
fix translate button spacing 2023-08-06 16:59:03 -04:00
Nicolas Constant cb58be5bd8
fine tunning prewrap 2023-08-06 16:50:49 -04:00
Nicolas Constant 7a8dfd0c6b
notify edited boosted status, fix #542 2023-08-06 04:41:10 -04:00
Nicolas Constant 89c5c33de2
load poll on edition, fix #587 2023-08-06 02:53:24 -04:00
Nicolas Constant 590627bc58
fix spacing, fix #597 2023-08-06 02:01:53 -04:00
Nicolas Constant 7013d9174c
enhance bookmarks support, fix #583 2023-08-06 01:17:04 -04:00
Nicolas Constant ba08c0d0b2
fine tuning dropdown menu 2023-08-05 18:59:53 -04:00
Nicolas Constant 26a01b5c30
refactoring 2023-08-05 18:40:46 -04:00
Nicolas Constant 73ac37a8f4
added translation revert 2023-08-05 18:32:23 -04:00
Nicolas Constant 38b052f06b
Merge pull request #602 from NicolasConstant/topic_lang-electron-integration
Topic lang electron integration
2023-08-05 18:13:07 -04:00
Nicolas Constant 4511363408
changed ipc channel name 2023-08-05 18:03:22 -04:00
Nicolas Constant c0f03570a0
clean up 2023-08-05 02:48:24 -04:00
Nicolas Constant 3d5c91a12b
working poc 2023-08-05 02:07:34 -04:00
Nicolas Constant 27b22338c9
Merge pull request #601 from NicolasConstant/topic_language-translation
Topic language translation
2023-08-05 00:20:39 -04:00
Nicolas Constant 191bd936aa
Merge pull request #600 from NicolasConstant/topic_language-support
Topic language support
2023-08-05 00:20:18 -04:00
Nicolas Constant 1c42f54db0
added languages 2023-08-05 00:00:00 -04:00
Nicolas Constant e8dbe214f4
sort and slice results 2023-08-04 23:45:34 -04:00
Nicolas Constant 8cd4d30ac8
notify error 2023-08-04 23:38:29 -04:00
Nicolas Constant 30f678af04
translation working 2023-08-04 23:36:21 -04:00
Nicolas Constant 16bbf9aa2f
displaying translation link 2023-08-04 22:57:06 -04:00
Nicolas Constant 74af61ad78
added warning 2023-08-04 20:23:40 -04:00
Nicolas Constant 449506092a
better language handling in settings + ui fix 2023-08-04 20:12:23 -04:00
Nicolas Constant b37a2a2f0c
fix impot 2023-08-04 03:26:24 -04:00
Nicolas Constant 32efac5aa4
language posting working 2023-08-04 03:20:48 -04:00
Nicolas Constant 91b2f4a0f0
changing lang working 2023-08-04 02:39:59 -04:00
Nicolas Constant 0d7821cd01
added selected language data 2023-08-04 01:08:00 -04:00
Nicolas Constant 18d6b8d96c
added lang change 2023-08-04 00:48:38 -04:00
Nicolas Constant 503cb6c9d4
fix language saving 2023-08-03 22:44:14 -04:00
Nicolas Constant 98e7d54c33
starting lang ui settings 2023-08-03 18:45:55 -04:00
Nicolas Constant dbb5d8e71b
added language service 2023-08-03 03:27:21 -04:00
Nicolas Constant a77b46755f
Merge pull request #599 from NicolasConstant/develop
1.4.0 PR
2023-08-02 19:15:28 -04:00
Nicolas Constant a5f9feb10b
road to 1.4.0 2023-08-02 18:58:09 -04:00
Nicolas Constant 95c4d8b249
small refactoring of the domain name display 2023-08-02 18:57:43 -04:00
Nicolas Constant 128dfd7fe5
Merge branch 'develop' of https://github.com/NicolasConstant/sengis into develop 2023-08-02 18:49:47 -04:00
Nicolas Constant 2dc77dd39a
Merge pull request #595 from HamzaFarooqArif/589_add_items_to_header
added a new option for timelines
2023-08-02 18:50:25 -04:00
Nicolas Constant f71e175375
better wording 2023-08-02 18:48:29 -04:00
HamzaFarooqArif a1a56e49f5 reverted code that breaks spacing 2023-07-31 10:06:59 +05:00
HamzaFarooqArif 5dc98c677e added a new option for timelines 2023-07-23 00:04:20 +05:00
Nicolas Constant b00c52ff83
Merge pull request #575 from NicolasConstant/develop
1.3.0 PR
2023-04-24 18:34:37 -04:00
Nicolas Constant f46d7d433a
Merge pull request #574 from NicolasConstant/topic_fixing-things
Topic fixing things
2023-04-24 01:34:42 -04:00
Nicolas Constant 06dbdef1dc
Merge branch 'topic_fixing-things' of https://github.com/NicolasConstant/sengis into topic_fixing-things 2023-04-24 01:09:50 -04:00
Nicolas Constant 5e865ed9a4
road to 1.3.0 2023-04-24 01:09:48 -04:00
Nicolas Constant 253ea52590
Update README.md 2023-04-24 01:08:59 -04:00
Nicolas Constant 84a4b8c00a
disable notification on unsupported type 2023-04-24 01:04:44 -04:00
Nicolas Constant 982a670352
keep attachments on edition, fix #563 2023-04-24 00:17:13 -04:00
Nicolas Constant 314c736cf4
only apply block with current acc, better interact 2023-04-23 19:49:08 -04:00
Nicolas Constant 9999944d1f
added unmute / unblock, fix #341 2023-04-23 19:21:54 -04:00
Nicolas Constant 2bcac4622a
fix error message, fix #561 2023-04-23 18:40:42 -04:00
Nicolas Constant 5d6672f379
added macos cmd to send toot, fix #515 2023-04-23 18:32:42 -04:00
Nicolas Constant eac8c6120a
ensure all alt media are updated, fix #430 2023-04-23 18:27:47 -04:00
Nicolas Constant 22cad9e22d
supporting new instance v2 model 2023-04-23 16:48:07 -04:00
Nicolas Constant 232a86566c
support v2 instance, fix #504 2023-04-23 16:20:00 -04:00
Nicolas Constant 2cb443dd4d
fix streaming url retrieval, fix #565 2023-04-23 16:09:44 -04:00
Nicolas Constant cb342ce9b5
Merge pull request #571 from NicolasConstant/topic_updated-docs
updated electron build
2023-04-23 15:45:28 -04:00
Nicolas Constant 8c9fe07109
Merge pull request #573 from NicolasConstant/topic_fixing-things
Topic fixing things
2023-04-23 15:32:32 -04:00
Nicolas Constant 00134a7407
electron removal 2023-04-23 15:23:43 -04:00
Nicolas Constant db6b37eef3
add auth on emoji fetch, fix #391 2023-04-23 15:09:34 -04:00
Nicolas Constant e14852e087
Merge branch 'topic_fixing-things' of https://github.com/NicolasConstant/sengis into topic_fixing-things 2023-04-23 15:08:39 -04:00
Nicolas Constant 6001a26f02
fix dependancies 2023-04-23 15:08:35 -04:00
Nicolas Constant 48677e8e6c
Delete build.yml 2023-04-23 02:53:12 -04:00
Nicolas Constant 1ca603f211
removed electron 2023-04-23 02:52:33 -04:00
Nicolas Constant d60bf804b8
Delete build.yml 2023-04-23 01:59:40 -04:00
Nicolas Constant 8bd71afc55
updated electron build 2023-04-23 01:55:02 -04:00
Nicolas Constant ed8c935285
Update build.yml 2023-04-13 00:52:02 -04:00
Nicolas Constant b1cd975422
updated electron 2023-04-12 23:56:27 -04:00
Nicolas Constant c5e3f4abac
added new CICD for Electron builds 2023-04-12 23:38:46 -04:00
Nicolas Constant 4599d64c60
Merge pull request #517 from NicolasConstant/develop
1.2.0 PR
2022-12-10 22:53:48 -05:00
Nicolas Constant 522c1c0133
road to 1.2.0 2022-12-10 22:34:47 -05:00
Nicolas Constant b6ea1d8d43
fix autocomplete error 2022-12-10 22:26:06 -05:00
Nicolas Constant 55a855d046
align post button text 2022-12-10 22:19:12 -05:00
Nicolas Constant 410007dc25
added edit text 2022-12-10 22:11:25 -05:00
Nicolas Constant 54d4b300f4
Merge pull request #516 from NicolasConstant/topic_edit-status_enhancements
Topic edit status enhancements
2022-12-10 19:57:48 -05:00
Nicolas Constant f4ba3a168f
removing editstatus panel 2022-12-10 19:41:43 -05:00
Nicolas Constant f2e1478cfa
refactoring panels for edition 2022-12-10 19:40:17 -05:00
Nicolas Constant ce71965b5c
refactoring 2022-12-10 19:19:02 -05:00
Nicolas Constant 65c147bc6f
clean up 2022-12-10 18:55:20 -05:00
Nicolas Constant 57f863e2a1
added edition notification logic 2022-12-10 18:31:54 -05:00
Nicolas Constant 0ce8be99bd
Merge branch 'develop' into topic_edit-status 2022-12-08 00:11:14 -05:00
Nicolas Constant f5de97993b
Merge pull request #478 from rpetti/add-edit-posts
Support Mastodon 4.0.0's Edit Feature
2022-12-07 23:31:00 -05:00
Nicolas Constant 0777c23124
Merge pull request #513 from NicolasConstant/fix_item-count-slow-mode
Fix item count slow mode
2022-12-07 23:04:22 -05:00
Nicolas Constant 70c9e2564b
Update appveyor.yml 2022-12-07 22:47:27 -05:00
Nicolas Constant 54772d8487
fix build 2022-12-07 22:30:04 -05:00
Nicolas Constant 30c81ae143
Merge pull request #499 from rpetti/fix-slow-mode-new-item-count
fix new item count in slow mode
2022-12-07 22:21:29 -05:00
Nicolas Constant 9cc2324fd2
remove duplicate tag 2022-12-02 00:01:30 -05:00
Nicolas Constant c912f12db5
Merge pull request #472 from rpetti/add-tag-following
hashtag following support for mastodon >= 4.0
2022-12-02 00:00:45 -05:00
Rob Petti 513bb1e684 fix new item count in slow mode 2022-11-26 13:48:33 -07:00
Rob Petti ec233754dd add post edit functionality 2022-11-19 10:16:31 -07:00
Rob Petti 39187c82fb initial implementation of tag following 2022-11-19 08:58:33 -07:00
Nicolas Constant 78f0f3ab5f
Merge pull request #474 from NicolasConstant/develop
1.1.6 PR
2022-11-18 20:34:03 +01:00
Nicolas Constant 39abd6a175
road to 1.1.6 2022-11-18 19:53:36 +01:00
Nicolas Constant 644b0d0b86
Merge pull request #471 from rpetti/fix-bookmarks
fixing the version check on the bookmarks feature
2022-11-18 19:51:44 +01:00
Rob Petti 83f52391ae fixing the version check on the bookmarks feature 2022-11-17 16:08:13 -07:00
Nicolas Constant 33a61f7347
Merge pull request #404 from NicolasConstant/develop
1.1.5 PR
2022-01-21 22:53:49 -05:00
Nicolas Constant 0409431105
removing travis 2022-01-21 21:40:16 -05:00
Nicolas Constant 42fb269c24
road to 1.1.5 2022-01-19 23:30:38 -05:00
Nicolas Constant c3a5306e56
Merge pull request #403 from NicolasConstant/fix_soapbox-mess
Fix missing mentions
2022-01-19 23:29:54 -05:00
Nicolas Constant 76b911351c
Merge pull request #402 from NicolasConstant/fix_cicd
Fix CICD
2022-01-19 23:05:39 -05:00
Nicolas Constant 7cb0887749
disable cache 2022-01-19 22:39:44 -05:00
Nicolas Constant 5c52c9c4f2
bump nodejs version 2022-01-19 22:37:58 -05:00
Nicolas Constant 59c3b19271
fix npm version 2022-01-19 22:32:50 -05:00
Nicolas Constant 2f84471a3e
add missing mentions, fix #399 2022-01-19 22:08:19 -05:00
91 changed files with 3849 additions and 2428 deletions

View File

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

View File

@ -12,6 +12,8 @@ It is strongly focused on the following points:
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/)

View File

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

View File

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

View File

@ -1,238 +0,0 @@
const { join } = require("path");
const { app, Menu, MenuItem, 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;
const globalAny = global;
if (process.env.NODE_ENV !== 'development') {
globalAny.__static = require('path').join(__dirname, '/assets/icons').replace(/\\/g, '\\\\');
}
function createWindow() {
// Set icon
let icon = join(globalAny.__static, '/png/512x512.png');
if (process.platform === "win32") {
icon = join(globalAny.__static, '/win/icon.ico');
}
// Create the browser window
win = new BrowserWindow({
width: 377,
height: 800,
title: "Sengi",
icon: icon,
backgroundColor: "#131925",
useContentSize: true,
webPreferences: {
spellcheck: false
}
// 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;
});
// win.webContents.on('context-menu', (event, params) => {
// const menu = new Menu();
// // Add each spelling suggestion
// for (const suggestion of params.dictionarySuggestions) {
// menu.append(new MenuItem({
// label: suggestion,
// click: () => mainWindow.webContents.replaceMisspelling(suggestion)
// }));
// }
// // Allow users to add the misspelled word to the dictionary
// if (params.misspelledWord) {
// menu.append(
// new MenuItem({
// label: 'Add to dictionary',
// click: () => mainWindow.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
// }));
// }
// menu.popup();
// });
}
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();
}
});

2900
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "sengi",
"version": "1.1.4",
"version": "1.7.0",
"license": "AGPL-3.0-or-later",
"main": "main-electron.js",
"description": "A multi-account desktop client for Mastodon and Pleroma",
@ -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": "^10.1.1",
"electron-builder": "^20.39.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~1.7.1",

View File

@ -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';
@ -90,6 +90,7 @@ import { TutorialEnhancedComponent } from './components/tutorial-enhanced/tutori
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 },
@ -159,7 +160,8 @@ const routes: Routes = [
TutorialEnhancedComponent,
NotificationsTutorialComponent,
LabelsTutorialComponent,
ThankyouTutorialComponent
ThankyouTutorialComponent,
StatusTranslateComponent
],
entryComponents: [
EmojiPickerComponent
@ -173,9 +175,11 @@ const routes: Routes = [
FormsModule,
ReactiveFormsModule,
PickerModule,
OwlDateTimeModule,
OwlDateTimeModule,
OwlNativeDateTimeModule,
OverlayModule,
DragDropModule,
// NgxElectronModule,
RouterModule.forRoot(routes),
NgxsModule.forRoot([

View File

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

View File

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

View File

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

View File

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

View File

@ -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';
@ -25,6 +25,9 @@ import { StatusSchedulerComponent } from './status-scheduler/status-scheduler.co
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',
@ -65,6 +68,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.detectAutosuggestion(value);
this._status = value;
this.languageService.autoDetectLang(value);
setTimeout(() => {
this.autoGrow();
}, 0);
@ -83,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));
@ -112,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;
@ -130,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;
@ -141,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;
@ -150,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;
@ -184,11 +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 readonly navigationService: NavigationService,
private readonly languageService: LanguageService,
private readonly settingsService: SettingsService,
private statusStateService: StatusesStateService,
private readonly statusStateService: StatusesStateService,
private readonly scheduledStatusService: ScheduledStatusService,
private readonly contextMenuService: ContextMenuService,
private readonly store: Store,
@ -198,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);
}
@ -250,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) {
@ -308,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;
}
@ -436,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:
@ -494,14 +571,14 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private getMentions(status: Status): string[] {
let acct = status.account.acct;
if(!acct.includes('@')) {
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('@')){
if (!mentionAcct.includes('@')) {
mentionAcct += `@${m.url.replace('https://', '').split('/')[0]}`;
}
mentions.push(mentionAcct);
@ -525,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;
@ -546,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);
@ -572,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 = '';
@ -599,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);
@ -613,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) => {
@ -628,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;
});
}
@ -636,8 +744,6 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
}
private parseStatus(status: string): string[] {
//console.error(status.toString());
let mentionExtraChars = this.getMentionExtraChars(status);
let urlExtraChar = this.getLinksExtraChars(status);
let trucatedStatus = `${status}`;
@ -654,8 +760,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
while (trucatedStatus.length > currentMaxCharLength) {
const nextIndex = trucatedStatus.lastIndexOf(' ', maxChars);
if(nextIndex === -1){
if (nextIndex === -1) {
break;
}
@ -706,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;
@ -756,7 +862,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
w++;
result += `${word}`;
if(w < wordCount || i === nberLines){
if (w < wordCount || i === nberLines) {
result += ' ';
}
});
@ -768,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);
}
@ -853,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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -59,11 +59,11 @@ export class ManageAccountComponent extends BrowseBase {
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 {
@ -71,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;
@ -128,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':
@ -151,12 +147,12 @@ export class ManageAccountComponent extends BrowseBase {
break;
case 'favorites':
this.favoritesComp.applyGoToTop();
break;
break;
}
}
this.subPanel = subpanel;
return false;
}

View File

@ -82,7 +82,7 @@ export class MentionsComponent extends TimelineBase {
}
protected getNextStatuses(): Promise<Status[]> {
return this.mastodonService.getNotifications(this.account, ['follow', 'favourite', 'reblog', 'poll', 'move'], 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);

View File

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

View File

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

View File

@ -22,7 +22,7 @@
(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"
<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>
@ -69,12 +69,30 @@
</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)"
<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>

View File

@ -152,6 +152,7 @@ export class NotificationWrapper {
case 'reblog':
case 'favourite':
case 'poll':
case 'update':
this.status = new StatusWrapper(notification.status, provider, applyCw, hideStatus);
break;
}
@ -168,5 +169,5 @@ export class NotificationWrapper {
account: Account;
target: Account;
status: StatusWrapper;
type: 'mention' | 'reblog' | 'favourite' | 'follow' | 'poll' | 'follow_request' | 'move';
type: 'mention' | 'reblog' | 'favourite' | 'follow' | 'poll' | 'follow_request' | 'move' | 'update';
}

View File

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

View File

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

View File

@ -35,7 +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 />
@ -51,21 +51,50 @@
<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">
(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>
<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>
<a href="https://github.com/NicolasConstant/sengi/wiki/BirdsiteLIVE-integration" target="_blank"
class="version__link">What is this?</a>
</div>
</div>
@ -79,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 />
@ -136,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)"
@ -160,7 +195,8 @@
<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>
<label class="noselect sub-section__label" for="onAutoFollowOnListChanged">autofollow accounts when
adding to list</label>
<br>
</div>
</div>
@ -173,6 +209,20 @@
<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>

View File

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

View File

@ -1,15 +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, 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',
@ -17,7 +19,7 @@ import { SettingsService } from '../../../services/settings.service';
styleUrls: ['./settings.component.scss']
})
export class SettingsComponent implements OnInit {
export class SettingsComponent implements OnInit, OnDestroy {
notificationSounds: NotificationSoundDefinition[];
notificationSoundId: string;
@ -27,6 +29,9 @@ export class SettingsComponent implements OnInit {
disableRemoteStatusFetchingEnabled: boolean;
disableAvatarNotificationsEnabled: boolean;
disableSoundsEnabled: boolean;
disableLangAutodetectEnabled: boolean;
enableAltLabelEnabled: boolean;
enableFreezeAvatarEnabled: boolean;
version: string;
hasPleromaAccount: boolean;
@ -39,6 +44,10 @@ export class SettingsComponent implements OnInit {
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);
@ -76,16 +85,25 @@ export class SettingsComponent implements OnInit {
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.settingsService.getSettings();
@ -129,6 +147,44 @@ export class SettingsComponent implements OnInit {
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) {
@ -230,6 +286,27 @@ 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.settingsService.getSettings();

View File

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

View File

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

View File

@ -1,12 +1,13 @@
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';
@ -24,6 +25,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
faPlus = faPlus;
faCog = faCog;
faCalendarAlt = faCalendarAlt;
faArrowsAltV = faArrowsAltV;
accounts: AccountWithNotificationWrapper[] = [];
hasAccounts: boolean;
@ -33,6 +35,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
private accountSub: Subscription;
private scheduledSub: Subscription;
private notificationSub: Subscription;
private draggableIconMenuSub: Subscription;
constructor(
private readonly settingsService: SettingsService,
@ -103,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
@ -164,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) {

View File

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

View File

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

View File

@ -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,12 +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) {
@ -57,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 {
@ -83,6 +108,10 @@ export class HashtagComponent implements OnInit, OnDestroy {
refresh(): any {
this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0];
this.updateHashtagFollowStatus(this.lastUsedAccount);
if (this.isHashtagFollowingAvailable) {
this.checkIfFollowingHashtag(this.lastUsedAccount);
}
this.appStreamStatuses.refresh();
}
@ -99,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
}
}

View File

@ -190,7 +190,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
this.boostPromise = Promise.resolve(true);
}
const account = this.toolsService.getSelectedAccounts()[0];
const account = this.toolsService.getSelectedAccounts()[0];
this.boostPromise = this.boostPromise
.then(() => {
this.boostIsLoading = true;
@ -231,11 +231,11 @@ export class ActionBarComponent implements OnInit, OnDestroy {
}
private favoritePromise: Promise<any>;
favorite(): boolean {
favorite(): boolean {
if (!this.favoritePromise) {
this.favoritePromise = Promise.resolve(true);
}
const account = this.toolsService.getSelectedAccounts()[0];
this.favoritePromise = this.favoritePromise
.then(() => {
@ -282,7 +282,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
.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);
@ -342,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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -64,6 +64,7 @@ $expand-color: $column-color;
& p {
margin: 0px;
white-space: pre-wrap;
//font-size: .9em;
// font-size: 14px;
}

View File

@ -202,7 +202,7 @@ describe('DatabindedTextComponent', () => {
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="https://ubuntu.social/tags/kubecon" class="hashtag-KubeCon" title="#KubeCon" target="_blank" rel="noopener noreferrer">#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', () => {

View File

@ -97,7 +97,7 @@ export class DatabindedTextComponent implements OnInit {
let extractedUrl = extractedLinkAndNext[0].split('href="')[1].split('"')[0];
let classname = this.getClassNameForHastag(extractedHashtag);
this.processedText += ` <a href="${extractedUrl}" class="${classname}" title="#${extractedHashtag}" target="_blank" rel="noopener noreferrer">#${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);
}
@ -205,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}`);

View File

@ -45,7 +45,15 @@ 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, this.poll.voters_count, opt.votes_count === maxVotes);
@ -195,7 +203,7 @@ class PollOptionWrapper implements PollOption {
if (totalVotes === 0) {
this.percentage = '0';
} else {
this.percentage = ((this.votes_count / votesDivider) * 100).toFixed(0);
this.percentage = ((this.votes_count / votesDivider) * 100).toFixed(0);
}
this.isMax = isMax;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
@ -161,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;
@ -247,6 +278,10 @@
color: $boost-color;
}
.update {
color: $update-color;
}
.favorite {
color: $favorite-color;
}
@ -261,4 +296,4 @@
&__label{
color: $status-secondary-color;
}
}
}

View File

@ -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,11 +109,78 @@ 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;
}
ngOnDestroy() {
if (this.statusesStateServiceSub) this.statusesStateServiceSub.unsubscribe();
}
private validateFilteringStatus(){
const filterStatus = this.displayedStatus.filtered;
if(!filterStatus || filterStatus.length === 0) return;
// if(!this.context){
// console.warn('this.context not found');
// console.warn(this.context);
// }
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;
@ -125,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...

View File

@ -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', 'follow_request', 'move'], 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;

View File

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

View File

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

View File

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

View File

@ -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';
@ -20,9 +20,11 @@ import { SettingsService } from '../../../services/settings.service';
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;
@ -32,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;
@ -101,6 +105,8 @@ export class StreamStatusesComponent extends TimelineBase {
});
}
});
this.numNewItems = 0;
}
ngOnDestroy() {
@ -110,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);
}
@ -133,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();
}
@ -154,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) {
@ -201,6 +227,7 @@ export class StreamStatusesComponent extends TimelineBase {
}
this.bufferStream.length = 0;
this.numNewItems = 0;
return false;
}
@ -212,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) {

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ 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';
@ -268,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;
@ -282,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) => {
@ -307,6 +334,9 @@ export class UserProfileComponent extends BrowseBase {
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err, userAccount);
})
.then(() => {
this.loadingRelationShip = false;
});
return false;
}

View File

@ -30,7 +30,7 @@ export class TimeAgoPipe implements PipeTransform {
const hours = minutes / 60;
const days = hours / 24;
// const months = days / 30.416;
// const years = days / 365;
const years = days / 365;
if (seconds <= 59) {
text = Math.round(seconds) + 's';
@ -38,8 +38,10 @@ export class TimeAgoPipe implements PipeTransform {
text = Math.round(minutes) + 'm';
} else if (hours <= 23) {
text = Math.round(hours) + 'h';
} else {
} else if (days < 365) {
text = Math.round(days) + 'd';
} else {
text = Math.round(years) + 'y';
}
if (minutes < 1) {

View File

@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { MyElectronService } from './electron.service';
xdescribe('MyElectronService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: MyElectronService = TestBed.get(MyElectronService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,58 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class MyElectronService {
detectedLangSubject = new Subject<DetectedLang[]>();
constructor() {
try {
if ((<any>window).api) {
(<any>window).api.receive("detectedLang", (data) => {
const result = [];
for (const l of data) {
let newLang = new DetectedLang(l[0], l[1]);
result.push(newLang);
}
this.detectedLangSubject.next(result);
});
}
}
catch (err) {
console.error(err);
}
this.detectLang("ceci est une phrase");
}
setLang(lang: string) {
try {
if ((<any>window).api) {
(<any>window).api.send("changeSpellchecker", lang);
}
}
catch (err) {
console.error(err);
}
}
detectLang(text: string) {
try {
if ((<any>window).api) {
(<any>window).api.send("detectLang", text);
}
}
catch (err) {
console.error(err);
}
}
}
export class DetectedLang {
constructor(
public lang: string,
public score: number
) {}
}

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { VisibilityEnum } from './mastodon.service';
import { MastodonWrapperService } from './mastodon-wrapper.service';
import { Instance, Account } from './models/mastodon.interfaces';
import { Instance, Instancev1, Instancev2, Account } from './models/mastodon.interfaces';
import { AccountInfo } from '../states/accounts.state';
@Injectable({
@ -11,6 +11,7 @@ import { AccountInfo } from '../states/accounts.state';
export class InstancesInfoService {
private defaultMaxChars = 500;
private cachedMaxInstanceChar: { [id: string]: Promise<number>; } = {};
private cachedTranslationAvailability: { [id: string]: Promise<boolean>; } = {};
private cachedDefaultPrivacy: { [id: string]: Promise<VisibilityEnum>; } = {};
constructor(private mastodonService: MastodonWrapperService) { }
@ -19,11 +20,22 @@ export class InstancesInfoService {
if (!this.cachedMaxInstanceChar[instance]) {
this.cachedMaxInstanceChar[instance] = this.mastodonService.getInstance(instance)
.then((instance: Instance) => {
if (instance.max_toot_chars) {
return instance.max_toot_chars;
if (+instance.version.split('.')[0] >= 4) {
const instanceV2 = <Instancev2>instance;
if (instanceV2
&& instanceV2.configuration
&& instanceV2.configuration.statuses
&& instanceV2.configuration.statuses.max_characters)
return instanceV2.configuration.statuses.max_characters;
} else {
return this.defaultMaxChars;
const instanceV1 = <Instancev1>instance;
if (instanceV1 && instanceV1.max_toot_chars)
return instanceV1.max_toot_chars;
if(instanceV1 && instanceV1.configuration && instanceV1.configuration.statuses && instanceV1.configuration.statuses.max_characters)
return instanceV1.configuration.statuses.max_characters;
}
return this.defaultMaxChars;
})
.catch(() => {
return this.defaultMaxChars;
@ -56,4 +68,30 @@ export class InstancesInfoService {
}
return this.cachedDefaultPrivacy[instance];
}
getTranslationAvailability(account: AccountInfo): Promise<boolean> {
const instance = account.instance;
if (!this.cachedTranslationAvailability[instance]) {
this.cachedTranslationAvailability[instance] = this.mastodonService.getInstance(instance)
.then((instance: Instance) => {
if (+instance.version.split('.')[0] >= 4) {
const instanceV2 = <Instancev2>instance;
if (instanceV2
&& instanceV2.configuration
&& instanceV2.configuration.translation)
return instanceV2.configuration.translation.enabled;
} else {
const instanceV1 = <Instancev1>instance;
if (instanceV1 && instanceV1.max_toot_chars)
return false;
}
return false;
})
.catch(() => {
return false;
});
}
return this.cachedTranslationAvailability[instance];
}
}

View File

@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { LanguageService } from './language.service';
xdescribe('LanguageService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: LanguageService = TestBed.get(LanguageService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,315 @@
import { T } from '@angular/cdk/keycodes';
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { ILanguage } from '../states/settings.state';
import { DetectedLang, MyElectronService } from './electron.service';
import { SettingsService } from './settings.service';
@Injectable({
providedIn: 'root'
})
export class LanguageService {
configuredLanguagesChanged = new BehaviorSubject<ILanguage[]>([]);
selectedLanguageChanged = new BehaviorSubject<ILanguage>(null);
constructor(
private settingsService: SettingsService,
private electronService: MyElectronService
) {
this.configuredLanguagesChanged.next(this.getConfiguredLanguages());
this.selectedLanguageChanged.next(this.getSelectedLanguage());
this.electronService.detectedLangSubject.subscribe(l => {
this.detectedLanguage(l);
});
}
private detectedLanguage(lang: DetectedLang[]) {
if (!lang) return;
if (lang.length >= 1) {
const languages = this.getConfiguredLanguages();
let firstLang = lang[0].lang;
let firstLocalLang = languages.find(x => x.iso639 == firstLang);
if (firstLocalLang) {
this.setSelectedLanguage(firstLocalLang);
return;
}
if (lang.length > 1) {
firstLang = lang[1].lang;
firstLocalLang = languages.find(x => x.iso639 == firstLang);
if (firstLocalLang) {
this.setSelectedLanguage(firstLocalLang);
return;
}
}
}
}
autoDetectLang(text: string): void {
if (!text || text.length < 5) return;
if (!this.settingsService.getSettings().disableLangAutodetec) {
this.electronService.detectLang(text);
}
}
getSelectedLanguage(): ILanguage {
const lang = this.settingsService.getSettings().selectedLanguage;
return lang;
}
setSelectedLanguage(lang: ILanguage): void {
var settings = this.settingsService.getSettings();
settings.selectedLanguage = lang;
this.settingsService.saveSettings(settings);
this.selectedLanguageChanged.next(lang);
if (lang) {
this.electronService.setLang(lang.iso639);
}
}
getConfiguredLanguages(): ILanguage[] {
const langs = this.settingsService.getSettings().configuredLanguages;
return langs;
}
addLanguage(lang: ILanguage) {
var settings = this.settingsService.getSettings();
settings.configuredLanguages.push(lang);
settings.configuredLanguages.sort((a, b) => a.name.localeCompare(b.name));
this.settingsService.saveSettings(settings);
this.configuredLanguagesChanged.next(settings.configuredLanguages);
if (settings.configuredLanguages.length === 1) {
this.setSelectedLanguage(lang);
}
}
removeLanguage(lang: ILanguage) {
var settings = this.settingsService.getSettings();
settings.configuredLanguages = settings.configuredLanguages.filter(x => x.iso639 !== lang.iso639);
this.settingsService.saveSettings(settings);
this.configuredLanguagesChanged.next(settings.configuredLanguages);
if (this.getSelectedLanguage().iso639 === lang.iso639) {
if (settings.configuredLanguages.length > 0) {
this.setSelectedLanguage(settings.configuredLanguages[0]);
} else {
this.setSelectedLanguage(null);
}
}
}
searchLanguage(input: string): ILanguage[] {
if (!input) return [];
const avLangs = this.getAllAvaialbleLaguages();
let found = avLangs.filter(x => x.name.toLowerCase().includes(input.toLowerCase()) || x.iso639.toLowerCase().includes(input.toLowerCase()));
found.sort((a, b) => a.name.localeCompare(b.name));
found = found.slice(0, 5);
return found;
}
private getAllAvaialbleLaguages(): Language[] {
return [
new Language("aa", "Afar"),
new Language("ab", "Abkhazian"),
new Language("af", "Afrikaans"),
new Language("ak", "Akan"),
new Language("am", "Amharic"),
new Language("an", "Aragonese"),
new Language("ar", "Arabic"),
new Language("as", "Assamese"),
new Language("av", "Avar"),
new Language("ay", "Aymara"),
new Language("az", "Azerbaijani"),
new Language("ba", "Bashkir"),
new Language("be", "Belarusian"),
new Language("bg", "Bulgarian"),
new Language("bh", "Bihari"),
new Language("bi", "Bislama"),
new Language("bm", "Bambara"),
new Language("bn", "Bengali"),
new Language("bo", "Tibetan"),
new Language("br", "Breton"),
new Language("bs", "Bosnian"),
new Language("ca", "Catalan"),
new Language("ce", "Chechen"),
new Language("ch", "Chamorro"),
new Language("co", "Corsican"),
new Language("cr", "Cree"),
new Language("cs", "Czech"),
new Language("cu", "Old Church Slavonic"),
new Language("cv", "Chuvash"),
new Language("cy", "Welsh"),
new Language("da", "Danish"),
new Language("de", "German"),
new Language("dv", "Divehi"),
new Language("dz", "Dzongkha"),
new Language("ee", "Ewe"),
new Language("el", "Greek"),
new Language("en", "English"),
new Language("eo", "Esperanto"),
new Language("es", "Spanish"),
new Language("et", "Estonian"),
new Language("eu", "Basque"),
new Language("fa", "Persian"),
new Language("ff", "Peul"),
new Language("fi", "Finnish"),
new Language("fj", "Fijian"),
new Language("fo", "Faroese"),
new Language("fr", "French"),
new Language("fy", "West Frisian"),
new Language("ga", "Irish"),
new Language("gd", "Scottish Gaelic"),
new Language("gl", "Galician"),
new Language("gn", "Guarani"),
new Language("gu", "Gujarati"),
new Language("gv", "Manx"),
new Language("ha", "Hausa"),
new Language("he", "Hebrew"),
new Language("hi", "Hindi"),
new Language("ho", "Hiri Motu"),
new Language("hr", "Croatian"),
new Language("ht", "Haitian"),
new Language("hu", "Hungarian"),
new Language("hy", "Armenian"),
new Language("hz", "Herero"),
new Language("ia", "Interlingua"),
new Language("id", "Indonesian"),
new Language("ie", "Interlingue"),
new Language("ig", "Igbo"),
new Language("ii", "Sichuan Yi"),
new Language("ik", "Inupiak"),
new Language("io", "Ido"),
new Language("is", "Icelandic"),
new Language("it", "Italian"),
new Language("iu", "Inuktitut"),
new Language("ja", "Japanese"),
new Language("jv", "Javanese"),
new Language("ka", "Georgian"),
new Language("kg", "Kongo"),
new Language("ki", "Kikuyu"),
new Language("kj", "Kuanyama"),
new Language("kk", "Kazakh"),
new Language("kl", "Greenlandic"),
new Language("km", "Cambodian"),
new Language("kn", "Kannada"),
new Language("ko", "Korean"),
new Language("kr", "Kanuri"),
new Language("ks", "Kashmiri"),
new Language("ku", "Kurdish"),
new Language("kv", "Komi"),
new Language("kw", "Cornish"),
new Language("ky", "Kirghiz"),
new Language("la", "Latin"),
new Language("lb", "Luxembourgish"),
new Language("lg", "Ganda"),
new Language("li", "Limburgian"),
new Language("ln", "Lingala"),
new Language("lo", "Laotian"),
new Language("lt", "Lithuanian"),
new Language("lu", "Luba-Katanga"),
new Language("lv", "Latvian"),
new Language("mg", "Malagasy"),
new Language("mh", "Marshallese"),
new Language("mi", "Maori"),
new Language("mk", "Macedonian"),
new Language("ml", "Malayalam"),
new Language("mn", "Mongolian"),
new Language("mo", "Moldovan"),
new Language("mr", "Marathi"),
new Language("ms", "Malay"),
new Language("mt", "Maltese"),
new Language("my", "Burmese"),
new Language("na", "Nauruan"),
new Language("nb", "Norwegian Bokmål"),
new Language("nd", "North Ndebele"),
new Language("ne", "Nepali"),
new Language("ng", "Ndonga"),
new Language("nl", "Dutch"),
new Language("nn", "Norwegian Nynorsk"),
new Language("no", "Norwegian"),
new Language("nr", "South Ndebele"),
new Language("nv", "Navajo"),
new Language("ny", "Chichewa"),
new Language("oc", "Occitan"),
new Language("oj", "Ojibwa"),
new Language("om", "Oromo"),
new Language("or", "Oriya"),
new Language("os", "Ossetian"),
new Language("pa", "Panjabi"),
new Language("pi", "Pali"),
new Language("pl", "Polish"),
new Language("ps", "Pashto"),
new Language("pt", "Portuguese"),
new Language("qu", "Quechua"),
new Language("rm", "Raeto Romance"),
new Language("rn", "Kirundi"),
new Language("ro", "Romanian"),
new Language("ru", "Russian"),
new Language("rw", "Rwandi"),
new Language("sa", "Sanskrit"),
new Language("sc", "Sardinian"),
new Language("sd", "Sindhi"),
new Language("se", "Northern Sami"),
new Language("sg", "Sango"),
new Language("sh", "Serbo-Croatian"),
new Language("si", "Sinhalese"),
new Language("sk", "Slovak"),
new Language("sl", "Slovenian"),
new Language("sm", "Samoan"),
new Language("sn", "Shona"),
new Language("so", "Somalia"),
new Language("sq", "Albanian"),
new Language("sr", "Serbian"),
new Language("ss", "Swati"),
new Language("st", "Southern Sotho"),
new Language("su", "Sundanese"),
new Language("sv", "Swedish"),
new Language("sw", "Swahili"),
new Language("ta", "Tamil"),
new Language("te", "Telugu"),
new Language("tg", "Tajik"),
new Language("th", "Thai"),
new Language("ti", "Tigrinya"),
new Language("tk", "Turkmen"),
new Language("tl", "Tagalog"),
new Language("tn", "Tswana"),
new Language("to", "Tonga"),
new Language("tr", "Turkish"),
new Language("ts", "Tsonga"),
new Language("tt", "Tatar"),
new Language("tw", "Twi"),
new Language("ty", "Tahitian"),
new Language("ug", "Uyghur"),
new Language("uk", "Ukrainian"),
new Language("ur", "Urdu"),
new Language("uz", "Uzbek"),
new Language("ve", "Venda"),
new Language("vi", "Vietnamese"),
new Language("vo", "Volapük"),
new Language("wa", "Walloon"),
new Language("wo", "Wolof"),
new Language("xh", "Xhosa"),
new Language("yi", "Yiddish"),
new Language("yo", "Yoruba"),
new Language("za", "Zhuang"),
new Language("zh", "Chinese"),
new Language("zu", "Zulu"),
];
}
}
export class Language {
constructor(public iso639: string, public name: string) {
}
}

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngxs/store';
import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus, TokenData } from "./models/mastodon.interfaces";
import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus, TokenData, Tag, Translation } from "./models/mastodon.interfaces";
import { AccountInfo, UpdateAccount } from '../states/accounts.state';
import { StreamTypeEnum, StreamElement } from '../states/streams.state';
import { FavoriteResult, VisibilityEnum, PollParameters, MastodonService, BookmarkResult, FollowingResult } from './mastodon.service';
@ -12,7 +12,7 @@ import { SettingsService } from './settings.service';
@Injectable({
providedIn: 'root'
})
export class MastodonWrapperService {
export class MastodonWrapperService {
private refreshingToken: { [id: string]: Promise<AccountInfo> } = {};
constructor(
@ -22,14 +22,14 @@ export class MastodonWrapperService {
private readonly mastodonService: MastodonService) { }
refreshAccountIfNeeded(accountInfo: AccountInfo): Promise<AccountInfo> {
if(this.refreshingToken[accountInfo.id]){
if (this.refreshingToken[accountInfo.id]) {
return this.refreshingToken[accountInfo.id];
}
let isExpired = false;
let storedAccountInfo = this.getStoreAccountInfo(accountInfo.id);
if(!storedAccountInfo || !(storedAccountInfo.token))
if (!storedAccountInfo || !(storedAccountInfo.token))
return Promise.resolve(accountInfo);
try {
@ -39,7 +39,7 @@ export class MastodonWrapperService {
} else {
const nowEpoch = Date.now() / 1000 | 0;
//Pleroma workaround
//Pleroma workaround
let expire_in = storedAccountInfo.token.expires_in;
if (expire_in < 3600) {
expire_in = 3600;
@ -74,7 +74,7 @@ export class MastodonWrapperService {
p.then(() => {
this.refreshingToken[accountInfo.id] = null;
});
this.refreshingToken[accountInfo.id] = p;
return p;
} else {
@ -96,6 +96,13 @@ export class MastodonWrapperService {
return this.mastodonService.getInstance(instance);
}
translate(account: AccountInfo, statusId: string, lang: string): Promise<Translation> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.translate(refreshedAccount, statusId, lang);
});
}
retrieveAccountDetails(account: AccountInfo): Promise<Account> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
@ -117,10 +124,17 @@ export class MastodonWrapperService {
});
}
postNewStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, mediaIds: string[], poll: PollParameters = null, scheduled_at: string = null): Promise<Status> {
postNewStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, mediaIds: string[], poll: PollParameters = null, scheduled_at: string = null, lang: string = null): Promise<Status> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.postNewStatus(refreshedAccount, status, visibility, spoiler, in_reply_to_id, mediaIds, poll, scheduled_at);
return this.mastodonService.postNewStatus(refreshedAccount, status, visibility, spoiler, in_reply_to_id, mediaIds, poll, scheduled_at, lang);
});
}
editStatus(account: AccountInfo, statusId: string, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, attachements: Attachment[], poll: PollParameters = null, scheduled_at: string = null, lang: string = null): Promise<Status> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.editStatus(refreshedAccount, statusId, status, visibility, spoiler, in_reply_to_id, attachements, poll, scheduled_at, lang);
});
}
@ -132,7 +146,7 @@ export class MastodonWrapperService {
}
search(account: AccountInfo, query: string, version: 'v1' | 'v2', resolve: boolean = false): Promise<Results> {
if(query.includes('twitter.com')){
if (query.includes('twitter.com')) {
query = this.processTwitterQuery(query);
}
@ -144,17 +158,17 @@ export class MastodonWrapperService {
private processTwitterQuery(query: string): string {
const settings = this.settingsService.getSettings();
if(!settings.twitterBridgeInstance) return query;
if (!settings.twitterBridgeInstance) return query;
let name;
if(query.includes('twitter.com/')){
if (query.includes('twitter.com/')) {
console.log(query.replace('https://', '').replace('http://', '').split('/'));
name = query.replace('https://', '').replace('http://', '').split('/')[1];
}
if(query.includes('@twitter.com')){
if (query.includes('@twitter.com')) {
console.log(query.split('@'));
name = query.split('@')[0];
if(name === '' || name == null){
if (name === '' || name == null) {
name = query.split('@')[1];
}
}
@ -194,7 +208,7 @@ export class MastodonWrapperService {
}
searchAccount(account: AccountInfo, query: string, limit: number = 40, following: boolean = false, resolve = true): Promise<Account[]> {
if(query.includes('twitter.com')){
if (query.includes('twitter.com')) {
query = this.processTwitterQuery(query);
}
@ -267,6 +281,41 @@ export class MastodonWrapperService {
});
}
hideBoosts(currentlyUsedAccount: AccountInfo, account: Account): Promise<Relationship> {
return this.refreshAccountIfNeeded(currentlyUsedAccount)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.hideBoosts(refreshedAccount, account);
});
}
unhideBoosts(currentlyUsedAccount: AccountInfo, account: Account): Promise<Relationship> {
return this.refreshAccountIfNeeded(currentlyUsedAccount)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.unhideBoosts(refreshedAccount, account);
});
}
followHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
return this.refreshAccountIfNeeded(currentlyUsedAccount)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.followHashtag(refreshedAccount, hashtag);
});
}
unfollowHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
return this.refreshAccountIfNeeded(currentlyUsedAccount)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.unfollowHashtag(refreshedAccount, hashtag);
});
}
getHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
return this.refreshAccountIfNeeded(currentlyUsedAccount)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.getHashtag(refreshedAccount, hashtag);
});
}
uploadMediaAttachment(account: AccountInfo, file: File, description: string): Promise<Attachment> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
@ -281,7 +330,7 @@ export class MastodonWrapperService {
});
}
getNotifications(account: AccountInfo, excludeTypes: ('follow' | 'favourite' | 'reblog' | 'mention' | 'poll' | 'follow_request' | 'move')[] = null, maxId: string = null, sinceId: string = null, limit: number = 15): Promise<Notification[]> {
getNotifications(account: AccountInfo, excludeTypes: ('follow' | 'favourite' | 'reblog' | 'mention' | 'poll' | 'follow_request' | 'move' | 'update')[] = null, maxId: string = null, sinceId: string = null, limit: number = 15): Promise<Notification[]> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.getNotifications(refreshedAccount, excludeTypes, maxId, sinceId, limit);
@ -351,6 +400,13 @@ export class MastodonWrapperService {
});
}
unmute(account: AccountInfo, accounId: number): Promise<Relationship> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.unmute(refreshedAccount, accounId);
});
}
block(account: AccountInfo, accounId: number): Promise<Relationship> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
@ -358,6 +414,27 @@ export class MastodonWrapperService {
});
}
unblock(account: AccountInfo, accounId: number): Promise<Relationship> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.unblock(refreshedAccount, accounId);
});
}
blockDomain(account: AccountInfo, domain: string): Promise<void> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.blockDomain(refreshedAccount, domain);
});
}
unblockDomain(account: AccountInfo, domain: string): Promise<void> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.unblockDomain(refreshedAccount, domain);
});
}
pinOnProfile(account: AccountInfo, statusId: string): Promise<Status> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
@ -421,14 +498,14 @@ export class MastodonWrapperService {
});
}
getFollowing(account: AccountInfo, accountId: number, maxId: string, sinceId: string, limit: number = 40): Promise<FollowingResult> {
getFollowing(account: AccountInfo, accountId: number, maxId: string, sinceId: string, limit: number = 40): Promise<FollowingResult> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.getFollowing(refreshedAccount, accountId, maxId, sinceId, limit);
});
}
getFollowers(account: AccountInfo, accountId: number, maxId: string, sinceId: string, limit: number = 40): Promise<FollowingResult> {
getFollowers(account: AccountInfo, accountId: number, maxId: string, sinceId: string, limit: number = 40): Promise<FollowingResult> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.getFollowers(refreshedAccount, accountId, maxId, sinceId, limit);

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { HttpHeaders, HttpClient, HttpResponse } from '@angular/common/http';
import { ApiRoutes } from './models/api.settings';
import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus } from "./models/mastodon.interfaces";
import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus, Tag, Instancev2, Instancev1, Translation } from "./models/mastodon.interfaces";
import { AccountInfo } from '../states/accounts.state';
import { StreamTypeEnum, StreamElement } from '../states/streams.state';
@ -13,8 +13,19 @@ export class MastodonService {
constructor(private readonly httpClient: HttpClient) { }
getInstance(instance: string): Promise<Instance> {
const route = `https://${instance}${this.apiRoutes.getInstance}`;
return this.httpClient.get<Instance>(route).toPromise();
let route = `https://${instance}${this.apiRoutes.getInstancev2}`;
return this.httpClient.get<Instancev2>(route).toPromise()
.catch(err => {
route = `https://${instance}${this.apiRoutes.getInstance}`;
return this.httpClient.get<Instancev1>(route).toPromise();
});
}
translate(account: AccountInfo, statusId: string, lang: string): Promise<Translation>{
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
let route = `https://${account.instance}${this.apiRoutes.translate.replace('{0}', statusId)}`;
return this.httpClient.post<Translation>(route, { 'lang': lang }, { headers: headers }).toPromise();
}
retrieveAccountDetails(account: AccountInfo): Promise<Account> {
@ -84,7 +95,7 @@ export class MastodonService {
return origString.replace(regEx, "");
};
postNewStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, mediaIds: string[], poll: PollParameters = null, scheduled_at: string = null): Promise<Status> {
postNewStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, mediaIds: string[], poll: PollParameters = null, scheduled_at: string = null, lang: string = null): Promise<Status> {
const url = `https://${account.instance}${this.apiRoutes.postNewStatus}`;
const statusData = new StatusData();
@ -102,10 +113,16 @@ export class MastodonService {
if (in_reply_to_id) {
statusData.in_reply_to_id = in_reply_to_id;
}
if (spoiler) {
statusData.sensitive = true;
statusData.spoiler_text = spoiler;
}
if(lang) {
statusData.language = lang;
}
switch (visibility) {
case VisibilityEnum.Public:
statusData.visibility = 'public';
@ -128,6 +145,57 @@ export class MastodonService {
return this.httpClient.post<Status>(url, statusData, { headers: headers }).toPromise();
}
editStatus(account: AccountInfo, statusId: string, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, attachements: Attachment[], poll: PollParameters = null, scheduled_at: string = null, lang: string = null): Promise<Status> {
const url = `https://${account.instance}${this.apiRoutes.editStatus.replace('{0}', statusId)}`;
const statusData = new StatusData();
statusData.status = status;
statusData.media_ids = attachements.map(x => x.id);
statusData.media_attributes = attachements.map(x => new MediaAttributes(x.id, x.description));
if (poll) {
statusData['poll'] = poll;
}
if (scheduled_at) {
statusData['scheduled_at'] = scheduled_at;
}
if (in_reply_to_id) {
statusData.in_reply_to_id = in_reply_to_id;
}
if (spoiler) {
statusData.sensitive = true;
statusData.spoiler_text = spoiler;
}
if(lang) {
statusData.language = lang;
}
switch (visibility) {
case VisibilityEnum.Public:
statusData.visibility = 'public';
break;
case VisibilityEnum.Unlisted:
statusData.visibility = 'unlisted';
break;
case VisibilityEnum.Private:
statusData.visibility = 'private';
break;
case VisibilityEnum.Direct:
statusData.visibility = 'direct';
break;
default:
statusData.visibility = 'private';
break;
}
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.put<Status>(url, statusData, { headers: headers }).toPromise();
}
getStatus(account: AccountInfo, statusId: string): Promise<Status> {
const route = `https://${account.instance}${this.apiRoutes.getStatus.replace('{0}', statusId)}`;
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
@ -289,14 +357,54 @@ export class MastodonService {
}
hideBoosts(currentlyUsedAccount: AccountInfo, account: Account): Promise<Relationship> {
const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.follow}`.replace('{0}', account.id.toString());
const headers = new HttpHeaders({ 'Authorization': `Bearer ${currentlyUsedAccount.token.access_token}` });
let input = new FormData();
input.append('reblogs', 'false');
return this.httpClient.post<Relationship>(route, input, { headers: headers }).toPromise();
}
unhideBoosts(currentlyUsedAccount: AccountInfo, account: Account): Promise<Relationship> {
const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.follow}`.replace('{0}', account.id.toString());
const headers = new HttpHeaders({ 'Authorization': `Bearer ${currentlyUsedAccount.token.access_token}` });
let input = new FormData();
input.append('reblogs', 'true');
return this.httpClient.post<Relationship>(route, input, { headers: headers }).toPromise();
}
followHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.followHashtag}`.replace('{0}', hashtag);
const headers = new HttpHeaders({ 'Authorization': `Bearer ${currentlyUsedAccount.token.access_token}` });
return this.httpClient.post<Tag>(route, null, { headers: headers }).toPromise();
}
unfollowHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.unfollowHashtag}`.replace('{0}', hashtag);
const headers = new HttpHeaders({ 'Authorization': `Bearer ${currentlyUsedAccount.token.access_token}` });
return this.httpClient.post<Tag>(route, null, { headers: headers }).toPromise();
}
getHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.getHashtag}`.replace('{0}', hashtag);
const headers = new HttpHeaders({ 'Authorization': `Bearer ${currentlyUsedAccount.token.access_token}` });
return this.httpClient.get<Tag>(route, { headers: headers }).toPromise();
}
uploadMediaAttachment(account: AccountInfo, file: File, description: string): Promise<Attachment> {
let input = new FormData();
input.append('file', file);
if (description !== null && description !== undefined) {
input.append('description', description);
} else {
input.append('description', '');
}
const route = `https://${account.instance}${this.apiRoutes.uploadMediaAttachment}`;
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.post<Attachment>(route, input, { headers: headers }).toPromise();
@ -305,13 +413,19 @@ export class MastodonService {
//TODO: add focus support
updateMediaAttachment(account: AccountInfo, mediaId: string, description: string): Promise<Attachment> {
let input = new FormData();
input.append('description', description);
if (description !== null && description !== undefined) {
input.append('description', description);
} else {
input.append('description', '');
}
const route = `https://${account.instance}${this.apiRoutes.updateMediaAttachment.replace('{0}', mediaId)}`;
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.put<Attachment>(route, input, { headers: headers }).toPromise();
}
getNotifications(account: AccountInfo, excludeTypes: ('follow' | 'favourite' | 'reblog' | 'mention' | 'poll' | 'follow_request' | 'move')[] = null, maxId: string = null, sinceId: string = null, limit: number = 15): Promise<Notification[]> {
getNotifications(account: AccountInfo, excludeTypes: ('follow' | 'favourite' | 'reblog' | 'mention' | 'poll' | 'follow_request' | 'move' | 'update')[] = null, maxId: string = null, sinceId: string = null, limit: number = 15): Promise<Notification[]> {
let route = `https://${account.instance}${this.apiRoutes.getNotifications}?limit=${limit}`;
if (maxId) {
@ -382,10 +496,10 @@ export class MastodonService {
addAccountToList(account: AccountInfo, listId: string, accountId: number): Promise<any> {
let route = `https://${account.instance}${this.apiRoutes.addAccountToList}`.replace('{0}', listId);
route += `?account_ids[]=${accountId}`;
let data = new ListAccountData();
data.account_ids.push(accountId.toString());
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.post(route, data, { headers: headers }).toPromise();
}
@ -420,12 +534,41 @@ export class MastodonService {
return this.httpClient.post<Relationship>(route, null, { headers: headers }).toPromise();
}
unmute(account: AccountInfo, accounId: number): Promise<Relationship> {
let route = `https://${account.instance}${this.apiRoutes.unmute}`.replace('{0}', accounId.toString());
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.post<Relationship>(route, null, { headers: headers }).toPromise();
}
block(account: AccountInfo, accounId: number): Promise<Relationship> {
let route = `https://${account.instance}${this.apiRoutes.block}`.replace('{0}', accounId.toString());
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.post<Relationship>(route, null, { headers: headers }).toPromise();
}
unblock(account: AccountInfo, accounId: number): Promise<Relationship> {
let route = `https://${account.instance}${this.apiRoutes.unblock}`.replace('{0}', accounId.toString());
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.post<Relationship>(route, null, { headers: headers }).toPromise();
}
blockDomain(account: AccountInfo, domain: string): Promise<void> {
let route = `https://${account.instance}${this.apiRoutes.blockDomain}`;
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
let input = new FormData();
input.append('domain', domain);
return this.httpClient.post<void>(route, input, { headers: headers }).toPromise();
}
unblockDomain(account: AccountInfo, domain: string): Promise<void> {
let route = `https://${account.instance}${this.apiRoutes.blockDomain}?domain=${domain}`;
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}`});
return this.httpClient.delete<void>(route, { headers: headers }).toPromise();
}
pinOnProfile(account: AccountInfo, statusId: string): Promise<Status> {
let route = `https://${account.instance}${this.apiRoutes.pinStatus}`.replace('{0}', statusId.toString());
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
@ -458,7 +601,8 @@ export class MastodonService {
getCustomEmojis(account: AccountInfo): Promise<Emoji[]> {
let route = `https://${account.instance}${this.apiRoutes.getCustomEmojis}`;
return this.httpClient.get<Emoji[]>(route).toPromise();
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.get<Emoji[]>(route, { headers: headers }).toPromise();
}
getScheduledStatuses(account: AccountInfo): Promise<ScheduledStatus[]> {
@ -564,11 +708,22 @@ class StatusData {
status: string;
in_reply_to_id: string;
media_ids: string[];
media_attributes: MediaAttributes[];
// poll: PollParameters;
sensitive: boolean;
spoiler_text: string;
visibility: string;
// scheduled_at: string;
language: string;
}
class MediaAttributes {
constructor(
public id: string,
public description: string){
}
}
export class PollParameters {

View File

@ -51,24 +51,63 @@ export class MediaService {
});
}
update(account: AccountInfo, media: MediaWrapper) {
if (media.attachment.description === media.description) return;
loadMedia(attachments: Attachment[]) {
const wrappers: MediaWrapper[] = [];
for (const att of attachments) {
const uniqueId = `${att.id}${Math.random()}`;
const wrapper = new MediaWrapper(uniqueId, null, att);
wrapper.description = att.description;
wrapper.isEdited = true;
wrappers.push(wrapper);
}
this.mediaSubject.next(wrappers);
this.mastodonService.updateMediaAttachment(account, media.attachment.id, media.description)
.then((att: Attachment) => {
let medias = this.mediaSubject.value;
let updatedMedia = medias.filter(x => x.id === media.id)[0];
updatedMedia.attachment.description = att.description;
this.mediaSubject.next(medias);
})
.catch((err) => {
this.notificationService.notifyHttpError(err, account);
});
}
addExistingMedia(media: MediaWrapper){
if(!this.fileCache[media.attachment.url]) return;
update(account: AccountInfo, media: MediaWrapper): Promise<void> {
if (media.attachment.description === media.description) return;
if (media.isEdited) {
media.attachment.description = media.description;
let medias = this.mediaSubject.value;
let updatedMedia = medias.filter(x => x.id === media.id)[0];
updatedMedia.attachment.description = media.attachment.description;
this.mediaSubject.next(medias);
} else {
return this.mastodonService.updateMediaAttachment(account, media.attachment.id, media.description)
.then((att: Attachment) => {
let medias = this.mediaSubject.value;
let updatedMedia = medias.filter(x => x.id === media.id)[0];
updatedMedia.attachment.description = att.description;
this.mediaSubject.next(medias);
})
.catch((err) => {
console.warn('failing update');
this.notificationService.notifyHttpError(err, account);
});
}
}
async retrieveUpToDateMedia(account: AccountInfo): Promise<MediaWrapper[]> {
const allMedia = this.mediaSubject.value;
let allPromises: Promise<any>[] = [];
for (const m of allMedia) {
let t = this.update(account, m);
allPromises.push(t);
}
await Promise.all(allPromises);
return allMedia;
}
addExistingMedia(media: MediaWrapper) {
if (!this.fileCache[media.attachment.url]) return;
media.file = this.fileCache[media.attachment.url];
let medias = this.mediaSubject.value;
medias.push(media);
@ -88,11 +127,15 @@ export class MediaService {
migrateMedias(account: AccountInfo) {
let medias = this.mediaSubject.value;
medias.forEach(media => {
media.isMigrating = true;
if (!media.isEdited) {
media.isMigrating = true;
}
});
this.mediaSubject.next(medias);
for (let media of medias) {
if (media.isEdited) continue;
this.mastodonService.uploadMediaAttachment(account, media.file, media.description)
.then((attachment: Attachment) => {
this.fileCache[attachment.url] = media.file;
@ -117,7 +160,7 @@ export class MediaWrapper {
public id: string,
public file: File,
attachment: Attachment) {
this.attachment = attachment;
this.attachment = attachment;
}
private _attachment: Attachment;
@ -125,7 +168,7 @@ export class MediaWrapper {
return this._attachment;
}
public set attachment(value: Attachment){
public set attachment(value: Attachment) {
if (value && value.meta && value.meta.audio_encode) {
this.audioType = `audio/${value.meta.audio_encode}`;
} else if (value && value.pleroma && value.pleroma.mime_type) {
@ -138,4 +181,6 @@ export class MediaWrapper {
public description: string;
public isMigrating: boolean;
public audioType: string;
public isEdited: boolean;
}

View File

@ -12,6 +12,7 @@ export class ApiRoutes {
unfollow = '/api/v1/accounts/{0}/unfollow';
block = '/api/v1/accounts/{0}/block';
unblock = '/api/v1/accounts/{0}/unblock';
blockDomain = '/api/v1/domain_blocks';
mute = '/api/v1/accounts/{0}/mute';
unmute = '/api/v1/accounts/{0}/unmute';
muteStatus = '/api/v1/statuses/{0}/mute';
@ -25,6 +26,7 @@ export class ApiRoutes {
rejectFollowRequest = '/api/v1/follow_requests/{0}/reject';
followRemote = '/api/v1/follows';
getInstance = '/api/v1/instance';
getInstancev2 = '/api/v2/instance';
uploadMediaAttachment = '/api/v1/media';
updateMediaAttachment = '/api/v1/media/{0}';
getMutes = '/api/v1/mutes';
@ -41,6 +43,7 @@ export class ApiRoutes {
getStatusRebloggedBy = '/api/v1/statuses/{0}/reblogged_by';
getStatusFavouritedBy = '/api/v1/statuses/{0}/favourited_by';
postNewStatus = '/api/v1/statuses';
editStatus = '/api/v1/statuses/{0}';
deleteStatus = '/api/v1/statuses/{0}';
reblogStatus = '/api/v1/statuses/{0}/reblog';
unreblogStatus = '/api/v1/statuses/{0}/unreblog';
@ -75,4 +78,8 @@ export class ApiRoutes {
getBookmarks = '/api/v1/bookmarks';
getFollowers = '/api/v1/accounts/{0}/followers';
getFollowing = '/api/v1/accounts/{0}/following';
followHashtag = '/api/v1/tags/{0}/follow';
unfollowHashtag = '/api/v1/tags/{0}/unfollow';
getHashtag = '/api/v1/tags/{0}';
translate = '/api/v1/statuses/{0}/translate';
}

View File

@ -110,15 +110,47 @@ export interface Error {
error: string;
}
export interface Instance {
uri: string;
title: string;
description: string;
email: string;
version: string;
urls: string[];
description: string;
}
export interface Instancev1 extends Instance {
uri: string;
email: string;
urls: InstanceUrls;
contact_account: Account;
max_toot_chars: number;
configuration: Instancev2Configuration;
}
export interface Instancev2 extends Instance {
configuration: Instancev2Configuration
}
export interface Instancev2Configuration {
urls: Instancev2Urls;
statuses: Instancev2Statuses;
translation: Instancev2Translation;
}
export interface InstanceUrls {
streaming_api: string;
}
export interface Instancev2Urls {
streaming: string;
}
export interface Instancev2Statuses {
max_characters: number;
}
export interface Instancev2Translation {
enabled: boolean;
}
export interface Mention {
@ -130,7 +162,7 @@ export interface Mention {
export interface Notification {
id: string;
type: 'mention' | 'reblog' | 'favourite' | 'follow' | 'poll' | 'follow_request' | 'move';
type: 'mention' | 'reblog' | 'favourite' | 'follow' | 'poll' | 'follow_request' | 'move' | 'update';
created_at: string;
account: Account;
status?: Status;
@ -141,7 +173,7 @@ export interface Relationship {
id: number;
following: boolean;
followed_by: boolean;
blocked_by: boolean;
blocked_by: boolean;
blocking: boolean;
domain_blocking: boolean;
muting: boolean;
@ -162,6 +194,33 @@ export interface Results {
hashtags: string[];
}
export interface FilterKeyword {
id: string;
keyword: string;
whole_word: boolean;
}
export interface FilterStatus {
id: string;
status_id: string;
}
export interface Filter {
id: string;
title: string;
context: string[]; //home notifications public thread account
expires_at: string;
filter_action: string; //warn hide
keywords: FilterKeyword[];
statuses: FilterStatus[];
}
export interface FilterResult {
filter: Filter;
keyword_matches: string[];
status_matches: string[];
}
export interface Status {
id: string;
uri: string;
@ -172,6 +231,7 @@ export interface Status {
reblog: Status;
content: string;
created_at: string;
edited_at: string;
reblogs_count: number;
replies_count: number;
favourites_count: string;
@ -190,7 +250,8 @@ export interface Status {
muted: boolean;
bookmarked: boolean;
card: Card;
poll: Poll;
poll: Poll;
filtered: FilterResult[];
pleroma: PleromaStatusInfo;
}
@ -207,11 +268,6 @@ export interface PleromaStatusInfo {
local: boolean;
}
export interface Tag {
name: string;
url: string;
}
export interface List {
id: string;
title: string;
@ -249,4 +305,25 @@ export interface StatusParams {
visibility: 'public' | 'unlisted' | 'private' | 'direct';
scheduled_at: string;
application_id: string;
}
export interface TagHistory {
day: string;
uses: number;
accounts: number;
}
export interface Tag {
name: string;
url: string;
history: TagHistory[];
following: boolean;
}
export interface Translation {
content: string;
language: string;
detected_source_language: string;
provider: string;
spoiler_text: string;
}

View File

@ -9,7 +9,8 @@ export class NavigationService {
private accountToManage: AccountWrapper;
activatedPanelSubject = new BehaviorSubject<OpenLeftPanelEvent>(new OpenLeftPanelEvent(LeftPanelType.Closed));
activatedMediaSubject: Subject<OpenMediaEvent> = new Subject<OpenMediaEvent>();
columnSelectedSubject = new BehaviorSubject<number>(-1);
columnSelectedSubject = new BehaviorSubject<number>(-1);
enableDraggableIconMenu = new BehaviorSubject<boolean>(false);
constructor() { }
@ -19,6 +20,10 @@ export class NavigationService {
this.activatedPanelSubject.next(newEvent);
}
changeIconMenuState(draggable: boolean) {
this.enableDraggableIconMenu.next(draggable);
}
openPanel(type: LeftPanelType){
const newEvent = new OpenLeftPanelEvent(type);
this.activatedPanelSubject.next(newEvent);
@ -41,6 +46,11 @@ export class NavigationService {
this.activatedPanelSubject.next(newEvent);
}
edit(status: StatusWrapper){
const newEvent = new OpenLeftPanelEvent(LeftPanelType.EditStatus, LeftPanelAction.Edit, null, status);
this.activatedPanelSubject.next(newEvent);
}
columnSelected(index: number): void {
this.columnSelectedSubject.next(index);
}
@ -68,6 +78,7 @@ export enum LeftPanelAction {
DM = 1,
Mention = 2,
Redraft = 3,
Edit = 4,
}
export enum LeftPanelType {
@ -77,5 +88,6 @@ export enum LeftPanelType {
Search = 3,
AddNewAccount = 4,
Settings = 5,
ScheduledStatuses = 6
ScheduledStatuses = 6,
EditStatus = 7,
}

View File

@ -26,15 +26,15 @@ export class NotificationService {
public notifyHttpError(err: HttpErrorResponse, account: AccountInfo) {
let message = 'Oops, Unknown Error';
let code: number;
try {
code = err.status;
if(err.message){
message = err.message;
} else if(err.error && err.error.error) {
if(err.error && err.error.error) {
message = err.error.error; //Mastodon
} else if(err.error && err.error.errors && err.error.errors.detail){
message = err.error.errors.detail; //Pleroma
} else if(err.message){
message = err.message;
}
} catch (err) { }

View File

@ -33,6 +33,11 @@ export class SettingsService {
this.saveSettings(settings);
}
if(!settings.configuredLanguages){
settings.configuredLanguages = [];
this.saveSettings(settings);
}
return settings;
}

View File

@ -6,15 +6,15 @@ import { StatusWrapper } from '../models/common.model';
@Injectable({
providedIn: 'root'
})
export class StatusesStateService {
private cachedStatusText: { [statusId: string]: string } = {};
private cachedStatusStates: { [statusId: string]: { [accountId: string]: StatusState } } = {};
export class StatusesStateService {
private cachedStatusText: { [statusId: string]: string } = {};
private cachedStatusStates: { [statusId: string]: { [accountId: string]: StatusState } } = {};
public stateNotification = new Subject<StatusState>();
constructor() { }
getStateForStatus(statusId: string): StatusState[] {
if(!this.cachedStatusStates[statusId])
if (!this.cachedStatusStates[statusId])
return null;
let results: StatusState[] = [];
@ -31,7 +31,7 @@ export class StatusesStateService {
this.cachedStatusStates[statusId] = {};
if (!this.cachedStatusStates[statusId][accountId]) {
this.cachedStatusStates[statusId][accountId] = new StatusState(statusId, accountId, isFavorited, null, null);
this.cachedStatusStates[statusId][accountId] = new StatusState(statusId, accountId, isFavorited, null, null, null, null);
} else {
this.cachedStatusStates[statusId][accountId].isFavorited = isFavorited;
}
@ -44,7 +44,7 @@ export class StatusesStateService {
this.cachedStatusStates[statusId] = {};
if (!this.cachedStatusStates[statusId][accountId]) {
this.cachedStatusStates[statusId][accountId] = new StatusState(statusId, accountId, null, isRebloged, null);
this.cachedStatusStates[statusId][accountId] = new StatusState(statusId, accountId, null, isRebloged, null, null, null);
} else {
this.cachedStatusStates[statusId][accountId].isRebloged = isRebloged;
}
@ -57,7 +57,7 @@ export class StatusesStateService {
this.cachedStatusStates[statusId] = {};
if (!this.cachedStatusStates[statusId][accountId]) {
this.cachedStatusStates[statusId][accountId] = new StatusState(statusId, accountId, null, null, isBookmarked);
this.cachedStatusStates[statusId][accountId] = new StatusState(statusId, accountId, null, null, isBookmarked, null, null);
} else {
this.cachedStatusStates[statusId][accountId].isBookmarked = isBookmarked;
}
@ -65,42 +65,58 @@ export class StatusesStateService {
this.stateNotification.next(this.cachedStatusStates[statusId][accountId]);
}
setStatusContent(data: string, replyingToStatus: StatusWrapper){
if(replyingToStatus){
statusEditedStatusChanged(statusId: string, accountId: string, editedStatus: StatusWrapper) {
if (!this.cachedStatusStates[statusId])
this.cachedStatusStates[statusId] = {};
if (!this.cachedStatusStates[statusId][accountId]) {
this.cachedStatusStates[statusId][accountId] = new StatusState(statusId, accountId, null, null, null, true, editedStatus);
} else {
this.cachedStatusStates[statusId][accountId].isEdited = true;
this.cachedStatusStates[statusId][accountId].editedStatus = editedStatus;
}
this.stateNotification.next(this.cachedStatusStates[statusId][accountId]);
}
setStatusContent(data: string, replyingToStatus: StatusWrapper) {
if (replyingToStatus) {
this.cachedStatusText[replyingToStatus.status.uri] = data;
} else {
this.cachedStatusText['none'] = data;
}
}
}
getStatusContent(replyingToStatus: StatusWrapper): string{
getStatusContent(replyingToStatus: StatusWrapper): string {
let data: string;
if(replyingToStatus){
if (replyingToStatus) {
data = this.cachedStatusText[replyingToStatus.status.uri];
} else {
data = this.cachedStatusText['none'];
}
if(!data) return '';
if (!data) return '';
return data;
}
resetStatusContent(replyingToStatus: StatusWrapper){
if(replyingToStatus){
resetStatusContent(replyingToStatus: StatusWrapper) {
if (replyingToStatus) {
this.cachedStatusText[replyingToStatus.status.uri] = '';
} else {
this.cachedStatusText['none'] = '';
}
}
}
}
export class StatusState {
constructor(
public statusId: string,
public accountId: string,
public isFavorited: boolean,
public statusId: string,
public accountId: string,
public isFavorited: boolean,
public isRebloged: boolean,
public isBookmarked: boolean) {
public isBookmarked: boolean,
public isEdited: boolean,
public editedStatus: StatusWrapper) {
}
}

View File

@ -6,6 +6,7 @@ import { ApiRoutes } from "./models/api.settings";
import { StreamTypeEnum, StreamElement } from "../states/streams.state";
import { MastodonWrapperService } from "./mastodon-wrapper.service";
import { AccountInfo } from "../states/accounts.state";
import { InstanceInfo, ToolsService } from "./tools.service";
@Injectable()
export class StreamingService {
@ -13,12 +14,13 @@ export class StreamingService {
public readonly nbStatusPerIteration: number = 20;
constructor(
private readonly mastodonService: MastodonWrapperService) { }
private readonly mastodonService: MastodonWrapperService,
private readonly toolsService: ToolsService) { }
getStreaming(accountInfo: AccountInfo, stream: StreamElement, since_id: string = null): StreamingWrapper {
//new EventSourceStreaminWrapper(accountInfo, stream);
return new StreamingWrapper(this.mastodonService, accountInfo, stream, this.nbStatusPerIteration);
return new StreamingWrapper(this.mastodonService, this.toolsService, accountInfo, stream, this.nbStatusPerIteration);
}
}
@ -33,6 +35,7 @@ export class StreamingWrapper {
constructor(
private readonly mastodonService: MastodonWrapperService,
private readonly toolsService: ToolsService,
private readonly account: AccountInfo,
private readonly stream: StreamElement,
private readonly nbStatusPerIteration: number,
@ -53,7 +56,13 @@ export class StreamingWrapper {
return account;
})
.then((refreshedAccount: AccountInfo) => {
const route = this.getRoute(refreshedAccount, stream);
let getInstanceProms = this.toolsService.getInstanceInfo(refreshedAccount);
return getInstanceProms.then(inst => {
return new StreamingAccountInfo(inst, refreshedAccount);
});
})
.then((account: StreamingAccountInfo) => {
const route = this.getRoute(account.instanceInfo, account.refreshedAccount, stream);
this.eventSource = new WebSocket(route);
this.eventSource.onmessage = x => {
if (x.data !== '') {
@ -62,7 +71,7 @@ export class StreamingWrapper {
}
this.eventSource.onerror = x => this.webSocketGotError(x);
this.eventSource.onopen = x => { };
this.eventSource.onclose = x => this.webSocketClosed(refreshedAccount, stream, x);
this.eventSource.onclose = x => this.webSocketClosed(account.refreshedAccount, stream, x);
});
}
@ -87,7 +96,7 @@ export class StreamingWrapper {
}
private pullNewNotifications() {
this.mastodonService.getNotifications(this.account, null, null, this.since_id_notifications, 10)
this.mastodonService.getNotifications(this.account, [], null, this.since_id_notifications, 10)
.then((notifications: Notification[]) => {
//notifications = notifications.sort((a, b) => a.id.localeCompare(b.id));
let soundMuted = !this.since_id_notifications;
@ -159,12 +168,19 @@ export class StreamingWrapper {
newUpdate.type = EventEnum.unknow;
}
this.statusUpdateSubjet.next(newUpdate);
}
private getRoute(account: AccountInfo, stream: StreamElement): string {
private getRoute(instanceInfo: InstanceInfo, account: AccountInfo, stream: StreamElement): string {
let streamingEndpoint = `wss://${account.instance}`;
if(instanceInfo.major >= 4){
streamingEndpoint = instanceInfo.streamingApi;
}
const streamingRouteType = this.getStreamingRouteType(stream.type);
let route = `wss://${account.instance}${this.apiRoutes.getStreaming}`.replace('{0}', account.token.access_token).replace('{1}', streamingRouteType);
let route = `${streamingEndpoint}${this.apiRoutes.getStreaming}`.replace('{0}', account.token.access_token).replace('{1}', streamingRouteType);
if (stream.tag) route = `${route}&tag=${stream.tag}`;
if (stream.list) route = `${route}&list=${stream.listId}`;
@ -274,6 +290,13 @@ class WebSocketEvent {
payload: any;
}
class StreamingAccountInfo {
constructor(
public instanceInfo: InstanceInfo,
public refreshedAccount: AccountInfo) {
}
}
export class StatusUpdate {
type: EventEnum;
status: Status;

View File

@ -3,7 +3,7 @@ import { Store } from '@ngxs/store';
import { AccountInfo } from '../states/accounts.state';
import { MastodonWrapperService } from './mastodon-wrapper.service';
import { Account, Results, Status, Emoji } from "./models/mastodon.interfaces";
import { Account, Results, Status, Emoji, Instancev2, Instancev1 } from "./models/mastodon.interfaces";
import { StatusWrapper } from '../models/common.model';
import { AccountSettings, SaveAccountSettings, GlobalSettings, SaveSettings, ContentWarningPolicy, SaveContentWarningPolicy, ContentWarningPolicyEnum, TimeLineModeEnum, TimeLineHeaderEnum } from '../states/settings.state';
import { SettingsService } from './settings.service';
@ -77,23 +77,64 @@ export class ToolsService {
return Promise.resolve(this.instanceInfos[acc.instance]);
} else {
return this.mastodonService.getInstance(acc.instance)
.then(instance => {
var type = InstanceType.Mastodon;
if (instance.version.toLowerCase().includes('pleroma')) {
.then(instance => {
const splittedVersion = instance.version.split('.');
let major = +splittedVersion[0];
let minor = +splittedVersion[1];
let altMajor = 0;
let altMinor = 0;
let type = InstanceType.Mastodon;
const version = instance.version.toLowerCase();
if (version.includes('pleroma')) {
type = InstanceType.Pleroma;
} else if (instance.version.toLowerCase().includes('+glitch')) {
const pleromaVersion = version.split('pleroma ')[1].split('.');
altMajor = +pleromaVersion[0];
altMinor = +pleromaVersion[1];
} else if (version.includes('+glitch')) {
type = InstanceType.GlitchSoc;
} else if (instance.version.toLowerCase().includes('+florence')) {
} else if (version.includes('+florence')) {
type = InstanceType.Florence;
} else if (instance.version.toLowerCase().includes('pixelfed')) {
} else if (version.includes('pixelfed')) {
type = InstanceType.Pixelfed;
} else if (version.includes('takahe')) {
type = InstanceType.Takahe;
major = 1; //FIXME: when a clearer set of feature are available
minor = 0; //FIXME: when a clearer set of feature are available
const takaheVersion = version.split('takahe/')[1].split('.');
altMajor = +takaheVersion[0];
altMinor = +takaheVersion[1];
} else if (version.includes('akkoma')) {
type = InstanceType.Akkoma;
const akkomaVersion = version.split('akkoma ')[1].split('.');
altMajor = +akkomaVersion[0];
altMinor = +akkomaVersion[1];
}
var splittedVersion = instance.version.split('.');
var major = +splittedVersion[0];
var minor = +splittedVersion[1];
let streamingApi = "";
var instanceInfo = new InstanceInfo(type, major, minor);
if (major >= 4) {
const instanceV2 = <Instancev2>instance;
if (instanceV2
&& instanceV2.configuration
&& instanceV2.configuration.urls)
streamingApi = instanceV2.configuration.urls.streaming;
} else {
const instanceV1 = <Instancev1>instance;
if (instanceV1 && instanceV1.urls)
streamingApi = instanceV1.urls.streaming_api;
}
let instanceInfo = new InstanceInfo(type, major, minor, streamingApi, altMajor, altMinor);
this.instanceInfos[acc.instance] = instanceInfo;
return instanceInfo;
@ -101,6 +142,25 @@ export class ToolsService {
}
}
isBookmarksAreAvailable(account: AccountInfo): Promise<boolean> {
return this.getInstanceInfo(account)
.then((instance: InstanceInfo) => {
if (instance.major == 3 && instance.minor >= 1
|| instance.major > 3
|| instance.type === InstanceType.Pleroma && instance.altMajor >= 2 && instance.altMinor >= 5
|| instance.type === InstanceType.Akkoma && instance.altMajor >= 3 && instance.altMinor >= 9
|| instance.type === InstanceType.Takahe && instance.altMajor >= 0 && instance.altMinor >= 9) {
return true;
} else {
return false;
}
})
.catch(err => {
console.error(err);
return false;
});
}
getAvatar(acc: AccountInfo): Promise<string> {
if (this.accountAvatar[acc.id]) {
return Promise.resolve(this.accountAvatar[acc.id]);
@ -231,16 +291,21 @@ export class InstanceInfo {
constructor(
public readonly type: InstanceType,
public readonly major: number,
public readonly minor: number) {
public readonly minor: number,
public readonly streamingApi: string,
public readonly altMajor: number,
public readonly altMinor: number) {
}
}
export enum InstanceType {
Mastodon = 1,
Pleroma = 2,
GlitchSoc = 3,
Pleroma = 2, // "2.7.2 (compatible; Pleroma 2.5.1)"
GlitchSoc = 3, // "4.1.5+glitch_0801_3b49b5a"
Florence = 4,
Pixelfed = 5
Pixelfed = 5,
Takahe = 6, // "takahe/0.9.0"
Akkoma = 7, // "2.7.2 (compatible; Akkoma 3.9.2-develop)"
}
export class StatusWithCwPolicyResult {

View File

@ -58,8 +58,10 @@ export class UserNotificationService {
}
private startFetchingNotifications(account: AccountInfo) {
let getMentionsPromise = this.mastodonService.getNotifications(account, ['favourite', 'follow', 'reblog', 'poll', 'follow_request', 'move'], null, null, 10)
let getMentionsPromise = this.mastodonService.getNotifications(account, ['favourite', 'follow', 'reblog', 'poll', 'follow_request', 'move', 'update'], null, null, 10)
.then((notifications: Notification[]) => {
notifications = notifications.filter(x => x.status !== null);
this.processMentionsAndNotifications(account, notifications, NotificationTypeEnum.UserMention);
})
.catch(err => {

View File

@ -6,6 +6,11 @@ export class AddAccount {
constructor(public account: AccountInfo) {}
}
export class ReorderAccounts {
static readonly type = '[Accounts] Reorder';
constructor(public accounts: AccountInfo[]) {}
}
export class SelectAccount {
static readonly type = '[Accounts] Select account';
constructor(public account: AccountInfo, public multiselection: boolean = false) {}
@ -46,6 +51,16 @@ export class AccountsState {
});
}
@Action(ReorderAccounts)
ReorderAccounts(ctx: StateContext<AccountsStateModel>, action: ReorderAccounts){
// const state = ctx.getState();
const reorderedAccounts = action.accounts;
ctx.patchState({
accounts: [...reorderedAccounts]
});
}
@Action(UpdateAccount)
UpdateAccount(ctx: StateContext<AccountsStateModel>, action: UpdateAccount){
const state = ctx.getState();

View File

@ -51,7 +51,8 @@ export enum TimeLineHeaderEnum {
Title_Username_DomainName = 2,
Title_AccountIcon_DomainName = 3,
Title_AccountIcon = 4,
Title = 5
Title = 5,
Title_AccountIcon_Username_DomainName = 6
}
export class ContentWarningPolicy {
@ -79,7 +80,19 @@ export class GlobalSettings {
columnSwitchingWinAlt = false;
accountSettings: AccountSettings[] = [];
accountSettings: AccountSettings[] = [];
configuredLanguages: ILanguage[] = [];
selectedLanguage: ILanguage;
disableLangAutodetec: boolean;
enableAltLabel: boolean;
enableFreezeAvatar: boolean;
}
export interface ILanguage {
iso639: string;
name: string;
}
export interface SettingsStateModel {
@ -170,6 +183,11 @@ export class SettingsState {
newSettings.autoFollowOnListEnabled = oldSettings.autoFollowOnListEnabled;
newSettings.twitterBridgeEnabled = oldSettings.twitterBridgeEnabled;
newSettings.twitterBridgeInstance = oldSettings.twitterBridgeInstance;
newSettings.configuredLanguages = oldSettings.configuredLanguages;
newSettings.selectedLanguage = oldSettings.selectedLanguage;
newSettings.disableLangAutodetec = oldSettings.disableLangAutodetec;
newSettings.enableAltLabel = oldSettings.enableAltLabel;
newSettings.enableFreezeAvatar = oldSettings.enableFreezeAvatar;
return newSettings;
}

View File

@ -1,17 +1,27 @@
@import "variables";
::ng-deep .ngx-contextmenu {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
-moz-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
-webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
-o-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
$shadow: 0.4;
box-shadow: 0 0 10px rgba(0, 0, 0, $shadow);
-moz-box-shadow: 0 0 10px rgba(0, 0, 0, $shadow);
-webkit-box-shadow: 0 0 10px rgba(0, 0, 0, $shadow);
-o-box-shadow: 0 0 10px rgba(0, 0, 0, $shadow);
padding: 0;
border-radius: 7px;
overflow: hidden;
& .dropdown-menu {
//border: solid 1px $context-menu-border-color;
border: none;
background-color: $context-menu-background;
padding: 0;
margin: 0;
border-radius: 0px;
border-radius: 7px;
overflow: hidden;
// padding: 2px 0;
// border-radius: 2px;
//border: solid 2px $context-menu-border-color;
@ -44,6 +54,6 @@
}
& .divider {
border-top: solid 2px $context-menu-border-color;
border-top: solid 1px $context-menu-border-color;
}
}

View File

@ -21,6 +21,7 @@ $status-primary-color: #fff;
$status-secondary-color: #4e5572;
$status-links-color: #d9e1e8;
$boost-color : #5098eb;
$update-color : #95e470;
$favorite-color: #ffc16f;
$bookmarked-color: #ff5050;
@ -52,9 +53,12 @@ $column-background: #0f111a;
$card-border-color: #2b344d;
$context-menu-background: #d9e1e8;
$context-menu-background: #ffffff;
$context-menu-background-hover: #a9c9e6;
$context-menu-background-hover: #d7dfeb;
$context-menu-font-color: #000000;
$context-menu-border-color: #c0cdd9;
$context-menu-border-color: #cbd3df;
$direct-message-background: #090a0f;