Compare commits
160 Commits
Author | SHA1 | Date |
---|---|---|
Nicolas Constant | 822ef21985 | |
Nicolas Constant | a154028a53 | |
Nicolas Constant | 6a8d85f40c | |
Nicolas Constant | 04153543a9 | |
Nicolas Constant | 92ec089eab | |
Nicolas Constant | 12ce0a3a4a | |
Nicolas Constant | 7a6eb9c3d2 | |
Nicolas Constant | 63b7c6fdf1 | |
Nicolas Constant | bd75317417 | |
Nicolas Constant | 74eed7e8ba | |
Nicolas Constant | ebce6282c5 | |
Nicolas Constant | 702e4daa44 | |
Nicolas Constant | d2221d539c | |
Nicolas Constant | c4de387f86 | |
Nicolas Constant | c0f84ddc11 | |
Nicolas Constant | 1830212a91 | |
Nicolas Constant | 46adf207bb | |
Nicolas Constant | 909b190b33 | |
Nicolas Constant | cfc4d5f915 | |
Nicolas Constant | 0f58252c61 | |
Nicolas Constant | 0d2ac6b569 | |
Nicolas Constant | e62987b11a | |
Nicolas Constant | 8cee7289eb | |
Nicolas Constant | 0305cc6ac7 | |
Nicolas Constant | f215d027f9 | |
Nicolas Constant | 335cbf4956 | |
Nicolas Constant | b41c31b4ac | |
Nicolas Constant | 41faa36087 | |
Nicolas Constant | 024042959e | |
Nicolas Constant | f4c87df078 | |
Nicolas Constant | d24441343a | |
Nicolas Constant | 8c9685045e | |
Nicolas Constant | a0cb240446 | |
Nicolas Constant | 2def5725f5 | |
Nicolas Constant | 450a0088d5 | |
Nicolas Constant | d7f988ecb9 | |
Nicolas Constant | 8703df27d5 | |
Nicolas Constant | 10fa412173 | |
Nicolas Constant | 0b93ed7307 | |
Nicolas Constant | c3cd6fe79e | |
Nicolas Constant | 14287b476c | |
Nicolas Constant | 2b106ba546 | |
Nicolas Constant | 4a2b408c1b | |
Nicolas Constant | 92a3ac6ae3 | |
Nicolas Constant | ec0bed4606 | |
Nicolas Constant | 62d4140d63 | |
Nicolas Constant | 4a34063dc8 | |
Nicolas Constant | 9cd709f44c | |
Nicolas Constant | 64ceb3e095 | |
Nicolas Constant | cb58be5bd8 | |
Nicolas Constant | 7a8dfd0c6b | |
Nicolas Constant | 89c5c33de2 | |
Nicolas Constant | 590627bc58 | |
Nicolas Constant | 7013d9174c | |
Nicolas Constant | ba08c0d0b2 | |
Nicolas Constant | 26a01b5c30 | |
Nicolas Constant | 73ac37a8f4 | |
Nicolas Constant | 38b052f06b | |
Nicolas Constant | 4511363408 | |
Nicolas Constant | c0f03570a0 | |
Nicolas Constant | 3d5c91a12b | |
Nicolas Constant | 27b22338c9 | |
Nicolas Constant | 191bd936aa | |
Nicolas Constant | 1c42f54db0 | |
Nicolas Constant | e8dbe214f4 | |
Nicolas Constant | 8cd4d30ac8 | |
Nicolas Constant | 30f678af04 | |
Nicolas Constant | 16bbf9aa2f | |
Nicolas Constant | 74af61ad78 | |
Nicolas Constant | 449506092a | |
Nicolas Constant | b37a2a2f0c | |
Nicolas Constant | 32efac5aa4 | |
Nicolas Constant | 91b2f4a0f0 | |
Nicolas Constant | 0d7821cd01 | |
Nicolas Constant | 18d6b8d96c | |
Nicolas Constant | 503cb6c9d4 | |
Nicolas Constant | 98e7d54c33 | |
Nicolas Constant | dbb5d8e71b | |
Nicolas Constant | a77b46755f | |
Nicolas Constant | a5f9feb10b | |
Nicolas Constant | 95c4d8b249 | |
Nicolas Constant | 128dfd7fe5 | |
Nicolas Constant | 2dc77dd39a | |
Nicolas Constant | f71e175375 | |
HamzaFarooqArif | a1a56e49f5 | |
HamzaFarooqArif | 5dc98c677e | |
Nicolas Constant | b00c52ff83 | |
Nicolas Constant | f46d7d433a | |
Nicolas Constant | 06dbdef1dc | |
Nicolas Constant | 5e865ed9a4 | |
Nicolas Constant | 253ea52590 | |
Nicolas Constant | 84a4b8c00a | |
Nicolas Constant | 982a670352 | |
Nicolas Constant | 314c736cf4 | |
Nicolas Constant | 9999944d1f | |
Nicolas Constant | 2bcac4622a | |
Nicolas Constant | 5d6672f379 | |
Nicolas Constant | eac8c6120a | |
Nicolas Constant | 22cad9e22d | |
Nicolas Constant | 232a86566c | |
Nicolas Constant | 2cb443dd4d | |
Nicolas Constant | cb342ce9b5 | |
Nicolas Constant | 8c9fe07109 | |
Nicolas Constant | 00134a7407 | |
Nicolas Constant | db6b37eef3 | |
Nicolas Constant | e14852e087 | |
Nicolas Constant | 6001a26f02 | |
Nicolas Constant | 48677e8e6c | |
Nicolas Constant | 1ca603f211 | |
Nicolas Constant | d60bf804b8 | |
Nicolas Constant | 8bd71afc55 | |
Nicolas Constant | ed8c935285 | |
Nicolas Constant | b1cd975422 | |
Nicolas Constant | c5e3f4abac | |
Nicolas Constant | 4599d64c60 | |
Nicolas Constant | 522c1c0133 | |
Nicolas Constant | b6ea1d8d43 | |
Nicolas Constant | 55a855d046 | |
Nicolas Constant | 410007dc25 | |
Nicolas Constant | 54d4b300f4 | |
Nicolas Constant | f4ba3a168f | |
Nicolas Constant | f2e1478cfa | |
Nicolas Constant | ce71965b5c | |
Nicolas Constant | 65c147bc6f | |
Nicolas Constant | 57f863e2a1 | |
Nicolas Constant | 0ce8be99bd | |
Nicolas Constant | f5de97993b | |
Nicolas Constant | 0777c23124 | |
Nicolas Constant | 70c9e2564b | |
Nicolas Constant | 54772d8487 | |
Nicolas Constant | 30c81ae143 | |
Nicolas Constant | 9cc2324fd2 | |
Nicolas Constant | c912f12db5 | |
Rob Petti | 513bb1e684 | |
Rob Petti | ec233754dd | |
Rob Petti | 39187c82fb | |
Nicolas Constant | 78f0f3ab5f | |
Nicolas Constant | 39abd6a175 | |
Nicolas Constant | 644b0d0b86 | |
Rob Petti | 83f52391ae | |
Nicolas Constant | 33a61f7347 | |
Nicolas Constant | 0409431105 | |
Nicolas Constant | 42fb269c24 | |
Nicolas Constant | c3a5306e56 | |
Nicolas Constant | 76b911351c | |
Nicolas Constant | 7cb0887749 | |
Nicolas Constant | 5c52c9c4f2 | |
Nicolas Constant | 59c3b19271 | |
Nicolas Constant | 2f84471a3e | |
Nicolas Constant | 640028ca08 | |
Nicolas Constant | 3f01c70bc9 | |
Nicolas Constant | 70bef7b98e | |
Nicolas Constant | 0956b623ce | |
Nicolas Constant | 6554a359b5 | |
Nicolas Constant | 1ebbece7ab | |
Nicolas Constant | a85e24b77f | |
Nicolas Constant | c2812fae43 | |
Nicolas Constant | 9426bc9e38 | |
Nicolas Constant | 06d142c4a5 | |
Nicolas Constant | eb74e34cb0 |
47
.travis.yml
47
.travis.yml
|
@ -1,47 +0,0 @@
|
|||
sudo: required
|
||||
dist: trusty
|
||||
|
||||
language: c
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
- os: linux
|
||||
env: CC=clang CXX=clang++ npm_config_clang=1
|
||||
compiler: clang
|
||||
|
||||
node_js:
|
||||
- 10.9.0
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
- icnsutils
|
||||
- graphicsmagick
|
||||
- libgnome-keyring-dev
|
||||
- xz-utils
|
||||
- xorriso
|
||||
- xvfb
|
||||
|
||||
install:
|
||||
- nvm install 10.9.0
|
||||
- npm install electron-builder@next
|
||||
- npm install
|
||||
- npm rebuild node-sass
|
||||
- export DISPLAY=':99.0'
|
||||
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||
|
||||
before_script:
|
||||
- export DISPLAY=:99.0
|
||||
- sh -e /etc/init.d/xvfb start &
|
||||
- sleep 3
|
||||
|
||||
script:
|
||||
- npm run travis
|
|
@ -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/)
|
||||
|
|
|
@ -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:
|
||||
|
@ -44,4 +44,5 @@ deploy:
|
|||
folder: /
|
||||
application: dist.zip
|
||||
on:
|
||||
APPVEYOR_REPO_TAG: true
|
||||
branch: master
|
||||
# APPVEYOR_REPO_TAG: true
|
||||
|
|
|
@ -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();
|
||||
|
|
238
main-electron.js
238
main-electron.js
|
@ -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();
|
||||
}
|
||||
});
|
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "sengi",
|
||||
"version": "1.1.2",
|
||||
"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",
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -28,6 +28,7 @@ export abstract class TimelineBase extends BrowseBase {
|
|||
statuses: StatusWrapper[] = [];
|
||||
bufferStream: Status[] = [];
|
||||
protected bufferWasCleared: boolean;
|
||||
numNewItems: number;
|
||||
streamPositionnedAtTop: boolean = true;
|
||||
protected isProcessingInfiniteScroll: boolean;
|
||||
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
<form class="status-editor" (ngSubmit)="onSubmit()">
|
||||
<input [(ngModel)]="title" type="text" class="form-control form-control-sm status-editor__title" name="title"
|
||||
autocomplete="off" placeholder="Title, Content Warning (optional)" title="title, content warning (optional)" dir="auto" />
|
||||
<input #mytitle [(ngModel)]="title" type="text" class="form-control form-control-sm status-editor__title"
|
||||
name="title" autocomplete="off" placeholder="Title, Content Warning (optional)"
|
||||
title="title, content warning (optional)" dir="auto"
|
||||
(keydown.escape)="mytitle.blur()" />
|
||||
|
||||
<a class="status-editor__emoji" title="Insert Emoji"
|
||||
#emojiButton href (click)="openEmojiPicker($event)">
|
||||
<img class="status-editor__emoji--image" src="/assets/emoji/72x72/1f636.png">
|
||||
</a>
|
||||
|
||||
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm status-editor__content" (paste)="onPaste($event)"
|
||||
rows="5" required title="content" placeholder="What's on your mind?" (keydown.control.enter)="onCtrlEnter()"
|
||||
<a class="status-editor__lang" title="Change language" href *ngIf="configuredLanguages && configuredLanguages.length > 1" (click)="onLangContextMenu($event)">
|
||||
{{ selectedLanguage.iso639 }}
|
||||
</a>
|
||||
|
||||
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm status-editor__content" (paste)="onPaste($event)"
|
||||
rows="5" required title="content" placeholder="What's on your mind?"
|
||||
(keydown.control.enter)="onCtrlEnter()"
|
||||
(keydown.meta.enter)="onCtrlEnter()"
|
||||
(keydown.escape)="reply.blur()"
|
||||
(keydown)="handleKeyDown($event)" (blur)="statusTextEditorLostFocus()" dir="auto">
|
||||
</textarea>
|
||||
|
||||
|
@ -21,19 +30,21 @@
|
|||
(suggestionSelectedEvent)="suggestionSelected($event)" (hasSuggestionsEvent)="suggestionsChanged($event)">
|
||||
</app-autosuggest>
|
||||
|
||||
<app-poll-editor *ngIf="instanceSupportsPoll && pollIsActive"></app-poll-editor>
|
||||
<app-poll-editor *ngIf="instanceSupportsPoll && pollIsActive" [oldPoll]="oldPoll"></app-poll-editor>
|
||||
|
||||
<app-status-scheduler class="scheduler" *ngIf="instanceSupportsScheduling && scheduleIsActive"></app-status-scheduler>
|
||||
|
||||
<div class="status-editor__footer" #footer>
|
||||
<button type="submit" title="reply" class="status-editor__footer--send-button" *ngIf="statusReplyingToWrapper">
|
||||
<span *ngIf="!isSending && !scheduleIsActive">REPLY!</span>
|
||||
<span *ngIf="!isSending && scheduleIsActive">PLAN!</span>
|
||||
<span *ngIf="!isSending && !scheduleIsActive && !isEditing">REPLY!</span>
|
||||
<span *ngIf="!isSending && scheduleIsActive && !isEditing">PLAN!</span>
|
||||
<span *ngIf="!isSending && isEditing">EDIT!</span>
|
||||
<app-waiting-animation class="waiting-icon" *ngIf="isSending"></app-waiting-animation>
|
||||
</button>
|
||||
<button type="submit" title="post" class="status-editor__footer--send-button" *ngIf="!statusReplyingToWrapper">
|
||||
<span *ngIf="!isSending && !scheduleIsActive">POST!</span>
|
||||
<span *ngIf="!isSending && scheduleIsActive">PLAN!</span>
|
||||
<span *ngIf="!isSending && !scheduleIsActive && !isEditing">POST!</span>
|
||||
<span *ngIf="!isSending && scheduleIsActive && !isEditing">PLAN!</span>
|
||||
<span *ngIf="!isSending && isEditing">EDIT!</span>
|
||||
<app-waiting-animation class="waiting-icon" *ngIf="isSending"></app-waiting-animation>
|
||||
</button>
|
||||
<div class="status-editor__footer__counter">
|
||||
|
@ -64,6 +75,10 @@
|
|||
<fa-icon [icon]="faClock"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="language-warning" *ngIf="!configuredLanguages || configuredLanguages.length === 0">
|
||||
You haven't set your language(s) yet, please <a href class="language-warning__link" (click)="onNavigateToSettings()">go in the settings</a> to provide it.
|
||||
</div>
|
||||
|
||||
<context-menu #contextMenu>
|
||||
<ng-template contextMenuItem (execute)="changePrivacy('Public')">
|
||||
|
@ -79,5 +94,12 @@
|
|||
<fa-icon [icon]="faEnvelope" class="context-menu-icon"></fa-icon> Direct
|
||||
</ng-template>
|
||||
</context-menu>
|
||||
|
||||
<context-menu #langContextMenu>
|
||||
<ng-template contextMenuItem (execute)="setLanguage(l)" *ngFor="let l of configuredLanguages">
|
||||
{{ l.name }}
|
||||
</ng-template>
|
||||
</context-menu>
|
||||
|
||||
<app-media></app-media>
|
||||
</form>
|
||||
|
|
|
@ -70,6 +70,32 @@ $counter-width: 90px;
|
|||
}
|
||||
}
|
||||
|
||||
&__lang {
|
||||
position: absolute;
|
||||
top: 64px;
|
||||
right: 12px;
|
||||
|
||||
font-weight: bolder;
|
||||
font-size: 12px;
|
||||
color: #a5a5a5;
|
||||
text-decoration: none;
|
||||
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 19px;
|
||||
border-radius: 2px;
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
|
||||
padding: 1px 0 0 2px;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color:black;
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
border-width: 0;
|
||||
background-color: $status-editor-background;
|
||||
|
@ -154,6 +180,9 @@ $counter-width: 90px;
|
|||
}
|
||||
|
||||
& span {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -204,6 +233,20 @@ $counter-width: 90px;
|
|||
border-bottom: 1px solid whitesmoke;
|
||||
}
|
||||
|
||||
.language-warning {
|
||||
padding: 5px 10px;
|
||||
color: orange;
|
||||
|
||||
&__link {
|
||||
text-decoration: underline;
|
||||
color: #f0d124;
|
||||
|
||||
&:hover {
|
||||
color: #d18800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@import '~@angular/cdk/overlay-prebuilt.css';
|
||||
// ::ng-deep .cdk-overlay-backdrop {
|
||||
// // width: 100%;
|
||||
|
|
|
@ -15,7 +15,7 @@ import { NavigationService } from '../../services/navigation.service';
|
|||
import { NotificationService } from '../../services/notification.service';
|
||||
import { MastodonService } from '../../services/mastodon.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
|
||||
import { SettingsState } from '../../states/settings.state';
|
||||
|
||||
describe('CreateStatusComponent', () => {
|
||||
let component: CreateStatusComponent;
|
||||
|
@ -33,7 +33,8 @@ describe('CreateStatusComponent', () => {
|
|||
NgxsModule.forRoot([
|
||||
RegisteredAppsState,
|
||||
AccountsState,
|
||||
StreamsState
|
||||
StreamsState,
|
||||
SettingsState
|
||||
]),
|
||||
],
|
||||
providers: [NavigationService, NotificationService, MastodonService, AuthService],
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
@ -225,7 +295,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
// this.status = state;
|
||||
// } else {
|
||||
if (!this.status || this.status === '') {
|
||||
const uniqueMentions = this.getMentions(this.statusReplyingTo, this.statusReplyingToWrapper.provider);
|
||||
const uniqueMentions = this.getMentions(this.statusReplyingTo);
|
||||
for (const mention of uniqueMentions) {
|
||||
this.status += `@${mention} `;
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -492,8 +569,20 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
return cwLength;
|
||||
}
|
||||
|
||||
private getMentions(status: Status, providerInfo: AccountInfo): string[] {
|
||||
const mentions = [status.account.acct, ...status.mentions.map(x => x.acct)];
|
||||
private getMentions(status: Status): string[] {
|
||||
let acct = status.account.acct;
|
||||
if (!acct.includes('@')) {
|
||||
acct += `@${status.account.url.replace('https://', '').split('/')[0]}`
|
||||
}
|
||||
|
||||
const mentions = [acct];
|
||||
status.mentions.forEach(m => {
|
||||
let mentionAcct = m.acct;
|
||||
if (!mentionAcct.includes('@')) {
|
||||
mentionAcct += `@${m.url.replace('https://', '').split('/')[0]}`;
|
||||
}
|
||||
mentions.push(mentionAcct);
|
||||
});
|
||||
|
||||
let uniqueMentions = [];
|
||||
for (let mention of mentions) {
|
||||
|
@ -502,22 +591,10 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
let globalUniqueMentions = [];
|
||||
for (let mention of uniqueMentions) {
|
||||
if (!mention.includes('@')) {
|
||||
if (providerInfo) {
|
||||
mention += `@${providerInfo.instance}`;
|
||||
} else {
|
||||
mention += `@${status.url.replace('https://', '').split('/')[0]}`;
|
||||
}
|
||||
}
|
||||
globalUniqueMentions.push(mention);
|
||||
}
|
||||
|
||||
const selectedUser = this.toolsService.getSelectedAccounts()[0];
|
||||
globalUniqueMentions = globalUniqueMentions.filter(x => x.toLowerCase() !== `${selectedUser.username}@${selectedUser.instance}`.toLowerCase());
|
||||
uniqueMentions = uniqueMentions.filter(x => x.toLowerCase() !== `${selectedUser.username}@${selectedUser.instance}`.toLowerCase());
|
||||
|
||||
return globalUniqueMentions;
|
||||
return uniqueMentions;
|
||||
}
|
||||
|
||||
onCtrlEnter(): boolean {
|
||||
|
@ -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;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<div *ngFor="let m of media" class="media">
|
||||
<div *ngIf="m.attachment === null" class="media__loading" title="{{m.file.name}}">
|
||||
<div *ngIf="m.attachment === null" class="media__loading" title="{{getName(m)}}">
|
||||
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
||||
</div>
|
||||
<div *ngIf="m.attachment !== null && m.attachment.type !== 'audio'" class="media__loaded" title="{{m.file.name}}"
|
||||
<div *ngIf="m.attachment !== null && m.attachment.type !== 'audio'" class="media__loaded" title="{{getName(m)}}"
|
||||
(mouseleave)="updateMedia(m)">
|
||||
<div class="media__loaded--migrating" *ngIf="m.isMigrating">
|
||||
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
||||
|
|
|
@ -56,4 +56,13 @@ export class MediaComponent implements OnInit, OnDestroy {
|
|||
this.mediaService.update(account, media);
|
||||
return false;
|
||||
}
|
||||
|
||||
getName(media: MediaWrapper): string {
|
||||
if(media && media.file && media.file.name){
|
||||
return media.file.name;
|
||||
}
|
||||
if(media.attachment && media.attachment.description){
|
||||
return media.attachment.description;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, Input, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { PollEntry } from './poll-entry/poll-entry.component';
|
||||
import { PollParameters } from '../../../services/mastodon.service';
|
||||
import { retry } from 'rxjs/operators';
|
||||
import { Poll } from '../../../services/models/mastodon.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-poll-editor',
|
||||
|
@ -19,6 +19,8 @@ export class PollEditorComponent implements OnInit {
|
|||
selectedId: string;
|
||||
private multiSelected: boolean;
|
||||
|
||||
@Input() oldPoll: Poll;
|
||||
|
||||
constructor() {
|
||||
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
|
||||
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
|
||||
|
@ -40,6 +42,12 @@ export class PollEditorComponent implements OnInit {
|
|||
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['oldPoll']) {
|
||||
this.loadPollParameters(this.oldPoll);
|
||||
}
|
||||
}
|
||||
|
||||
private getEntryUuid(): number {
|
||||
this.entryUuid++;
|
||||
return this.entryUuid;
|
||||
|
@ -50,7 +58,7 @@ export class PollEditorComponent implements OnInit {
|
|||
return false;
|
||||
}
|
||||
|
||||
removeElement(entry: PollEntry){
|
||||
removeElement(entry: PollEntry) {
|
||||
this.entries = this.entries.filter(x => x.id != entry.id);
|
||||
}
|
||||
|
||||
|
@ -69,6 +77,19 @@ export class PollEditorComponent implements OnInit {
|
|||
params.hide_totals = false;
|
||||
return params;
|
||||
}
|
||||
|
||||
private loadPollParameters(poll: Poll) {
|
||||
if(!this.oldPoll) return;
|
||||
|
||||
const isMulti = poll.multiple;
|
||||
|
||||
this.entries.length = 0;
|
||||
for (let o of poll.options) {
|
||||
const entry = new PollEntry(this.getEntryUuid(), isMulti);
|
||||
entry.label = o.title;
|
||||
this.entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Delay {
|
||||
|
|
|
@ -15,7 +15,9 @@
|
|||
<button type="submit" class="form-button"
|
||||
title="add account"
|
||||
[class.comrade__button]="isComrade">
|
||||
<span *ngIf="!isLoading">Submit</span>
|
||||
|
||||
<span *ngIf="!isLoading && !this.isInstanceMultiAccountLoading">Submit</span>
|
||||
<span *ngIf="!isLoading && this.isInstanceMultiAccountLoading" class="faq__warning">See FAQ</span>
|
||||
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
|
||||
</button>
|
||||
|
||||
|
@ -29,5 +31,12 @@
|
|||
allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
<div class="faq" *ngIf="isInstanceMultiAccount">
|
||||
<p>
|
||||
FAQ<br/>
|
||||
<a href="https://github.com/NicolasConstant/sengi/wiki/How-to-add-multiple-accounts-from-the-same-instance" target="_blank">How to add multiple accounts from the same instance?</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -109,4 +109,21 @@ $comrade_red: #a50000;
|
|||
background-color: $comrade_red;
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.faq {
|
||||
margin: 20px 0 0 0;
|
||||
|
||||
& a {
|
||||
color: #ffcc00;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: #ffe88a;
|
||||
}
|
||||
}
|
||||
|
||||
&__warning {
|
||||
color: #ffdc52;
|
||||
}
|
||||
}
|
|
@ -6,13 +6,14 @@ import { RegisteredAppsStateModel, AppInfo, AddRegisteredApp } from '../../../st
|
|||
import { AuthService, CurrentAuthProcess } from '../../../services/auth.service';
|
||||
import { AppData } from '../../../services/models/mastodon.interfaces';
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { ToolsService } from '../../../services/tools.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-new-account',
|
||||
templateUrl: './add-new-account.component.html',
|
||||
styleUrls: ['./add-new-account.component.scss']
|
||||
})
|
||||
export class AddNewAccountComponent implements OnInit {
|
||||
export class AddNewAccountComponent implements OnInit {
|
||||
private blockList = ['gab.com', 'gab.ai', 'cyzed.com'];
|
||||
private comradeList = ['juche.town'];
|
||||
|
||||
|
@ -24,12 +25,14 @@ export class AddNewAccountComponent implements OnInit {
|
|||
set setInstance(value: string) {
|
||||
this.instance = value.replace('http://', '').replace('https://', '').replace('/', '').toLowerCase().trim();
|
||||
this.checkComrad();
|
||||
this.checkInstanceMultiAccount(value);
|
||||
}
|
||||
get setInstance(): string {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly store: Store) { }
|
||||
|
@ -51,8 +54,27 @@ export class AddNewAccountComponent implements OnInit {
|
|||
this.isComrade = false;
|
||||
}
|
||||
|
||||
isInstanceMultiAccount: boolean;
|
||||
isInstanceMultiAccountLoading: boolean;
|
||||
checkInstanceMultiAccount(value: string) {
|
||||
if(value) {
|
||||
const instances: string[] = this.toolsService.getAllAccounts().map(x => x.instance);
|
||||
if(instances && instances.indexOf(value) > -1){
|
||||
this.isInstanceMultiAccount = true;
|
||||
this.isInstanceMultiAccountLoading = true;
|
||||
|
||||
setTimeout(() => {
|
||||
this.isInstanceMultiAccountLoading = false;
|
||||
}, 2000);
|
||||
} else {
|
||||
this.isInstanceMultiAccount = false;
|
||||
this.isInstanceMultiAccountLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
if(this.isLoading || !this.instance) return false;
|
||||
if(this.isLoading || !this.instance || this.isInstanceMultiAccountLoading) return false;
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
<div class=" new-message-body flexcroll">
|
||||
<app-create-status (onClose)="closeColumn()" [isDirectMention]="isDirectMention"
|
||||
[replyingUserHandle]="userHandle" [redraftedStatus]="redraftedStatus"></app-create-status>
|
||||
|
||||
[replyingUserHandle]="userHandle" [statusToEdit]="statusToEdit" [redraftedStatus]="redraftedStatus"></app-create-status>
|
||||
</div>
|
||||
</div>
|
|
@ -13,6 +13,7 @@ export class AddNewStatusComponent implements OnInit {
|
|||
@Input() isDirectMention: boolean;
|
||||
@Input() userHandle: string;
|
||||
@Input() redraftedStatus: StatusWrapper;
|
||||
@Input() statusToEdit: StatusWrapper;
|
||||
|
||||
constructor(private readonly navigationService: NavigationService) {
|
||||
}
|
||||
|
|
|
@ -1,29 +1,31 @@
|
|||
<div class="floating-column">
|
||||
<div class="floating-column__inner">
|
||||
<div class="sliding-column" [class.sliding-column__right-display]="overlayActive">
|
||||
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive"
|
||||
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive"
|
||||
(closeOverlay)="closeOverlay()"
|
||||
[browseAccountData]="overlayAccountToBrowse"
|
||||
[browseAccountData]="overlayAccountToBrowse"
|
||||
[browseHashtagData]="overlayHashtagToBrowse"
|
||||
[browseThreadData]="overlayThreadToBrowse"></app-stream-overlay>
|
||||
|
||||
<div class="floating-column__inner--left">
|
||||
<div class="floating-column__header">
|
||||
<a class="close-button" href (click)="closePanel()" title="close">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
<fa-icon class="close-button__icon" [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<app-manage-account *ngIf="openPanel === 'manageAccount'" [account]="userAccountUsed"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-manage-account>
|
||||
<app-add-new-status *ngIf="openPanel === 'createNewStatus'" [isDirectMention]="isDirectMention"
|
||||
[userHandle]="userHandle" [redraftedStatus]="redraftedStatus"></app-add-new-status>
|
||||
[userHandle]="userHandle"
|
||||
[redraftedStatus]="redraftedStatus"
|
||||
[statusToEdit]="statusToEdit"></app-add-new-status>
|
||||
<app-add-new-account *ngIf="openPanel === 'addNewAccount'"></app-add-new-account>
|
||||
<app-search *ngIf="openPanel === 'search'"
|
||||
<app-search *ngIf="openPanel === 'search'"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)">
|
||||
</app-search>
|
||||
<app-settings *ngIf="openPanel === 'settings'"></app-settings>
|
||||
|
|
|
@ -29,9 +29,20 @@
|
|||
}
|
||||
|
||||
.close-button {
|
||||
// outline: 1px dotted orange;
|
||||
|
||||
display: block;
|
||||
float: right;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
margin: 10px 16px 0 0;
|
||||
margin: 5px 5px 0 0;
|
||||
|
||||
width: 40px;
|
||||
height: 34px;
|
||||
|
||||
&__icon {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
left: 17px;
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
|
|||
isDirectMention: boolean;
|
||||
userHandle: string;
|
||||
redraftedStatus: StatusWrapper;
|
||||
statusToEdit: StatusWrapper;
|
||||
|
||||
openPanel: string = '';
|
||||
|
||||
|
@ -49,12 +50,21 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
break;
|
||||
case LeftPanelType.CreateNewStatus:
|
||||
case LeftPanelType.EditStatus:
|
||||
if (this.openPanel === 'createNewStatus' && !event.userHandle) {
|
||||
this.closePanel();
|
||||
} else {
|
||||
this.isDirectMention = event.action === LeftPanelAction.DM;
|
||||
this.userHandle = event.userHandle;
|
||||
this.redraftedStatus = event.status;
|
||||
|
||||
if(event.type === LeftPanelType.CreateNewStatus){
|
||||
this.redraftedStatus = event.status;
|
||||
this.statusToEdit = null;
|
||||
} else {
|
||||
this.redraftedStatus = null;
|
||||
this.statusToEdit = event.status;
|
||||
}
|
||||
|
||||
this.openPanel = 'createNewStatus';
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -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';
|
||||
}
|
|
@ -4,8 +4,8 @@
|
|||
<h3 class="panel__title">search</h3>
|
||||
|
||||
<form class="form-section" (ngSubmit)="onSubmit()">
|
||||
<input type="text" class="form-control form-control-sm form-with-button" [(ngModel)]="searchHandle"
|
||||
name="searchHandle" placeholder="Search" autocomplete="off" />
|
||||
<input #search type="text" class="form-control form-control-sm form-with-button" [(ngModel)]="searchHandle"
|
||||
name="searchHandle" placeholder="Search" autocomplete="off" (keydown.escape)="search.blur()"/>
|
||||
<button class="form-button" type="submit" title="search">GO</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
|
||||
|
@ -26,12 +26,15 @@ export class SearchComponent implements OnInit {
|
|||
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
|
||||
@ViewChild('search') searchElement: ElementRef;
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mastodonService: MastodonWrapperService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.searchElement.nativeElement.focus();
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
|
|
|
@ -35,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>
|
||||
|
|
|
@ -31,6 +31,13 @@
|
|||
padding: 0 5px 15px 5px;
|
||||
position: relative;
|
||||
|
||||
&__content {
|
||||
display: block;
|
||||
padding: 0 0 0 5px;
|
||||
|
||||
// outline: 1px dotted greenyellow;
|
||||
}
|
||||
|
||||
&__checkbox {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
|
@ -68,6 +75,41 @@
|
|||
}
|
||||
}
|
||||
|
||||
.language {
|
||||
&__warning {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
&__entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
|
||||
&:not(:last-child){
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
display: block;
|
||||
align-items: stretch;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
&__action {
|
||||
align-items: stretch;
|
||||
min-width: 70px;
|
||||
text-align: center;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__search {
|
||||
display: block;
|
||||
margin: 5px 0 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: 1px solid $settings-text-input-border;
|
||||
color: $settings-text-input-foreground;
|
||||
|
@ -111,4 +153,22 @@
|
|||
background-color: #32384d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toogle-lock-icon-menu {
|
||||
display: block;
|
||||
padding: 3px 40px;
|
||||
width: 170px;
|
||||
|
||||
float: right;
|
||||
|
||||
text-align: center;
|
||||
|
||||
color: white;
|
||||
background-color: #1f2330;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: #32384d;
|
||||
}
|
||||
}
|
|
@ -1,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();
|
||||
|
|
|
@ -8,27 +8,36 @@
|
|||
<fa-icon [icon]="faSearch"></fa-icon>
|
||||
</a>
|
||||
|
||||
<div *ngFor="let account of accounts">
|
||||
<app-account-icon [account]="account" (toogleAccountNotify)="onToogleAccountNotify($event)"
|
||||
(openMenuNotify)="onOpenMenuNotify($event)">
|
||||
</app-account-icon>
|
||||
<div *ngIf="!iconMenuIsDraggable">
|
||||
<div *ngFor="let account of accounts">
|
||||
<app-account-icon [account]="account" (toogleAccountNotify)="onToogleAccountNotify($event)"
|
||||
(openMenuNotify)="onOpenMenuNotify($event)">
|
||||
</app-account-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="iconMenuIsDraggable" cdkDropList [cdkDropListData]="accounts" (cdkDropListDropped)="onDrop($event)">
|
||||
<div *ngFor="let account of accounts" cdkDrag class="draggable">
|
||||
<fa-icon class="draggable__icon" [icon]="faArrowsAltV"></fa-icon>
|
||||
<img class="draggable__avatar" src="{{ account.avatar }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<a class="left-bar-button left-bar-button--add left-bar-link" [ngClass]="{'no-accounts': hasAccounts === false }"
|
||||
href title="add new account" (click)="addNewAccount()" (contextmenu)="addNewAccount()">
|
||||
<fa-icon [icon]="faPlus"></fa-icon>
|
||||
</a>
|
||||
|
||||
|
||||
<a class="left-bar-button left-bar-button--scheduled left-bar-button--bottom left-bar-link" href title="scheduled statuses"
|
||||
*ngIf="hasAccounts && hasScheduledStatuses"
|
||||
(click)="openScheduledStatuses()"
|
||||
<a class="left-bar-button left-bar-button--scheduled left-bar-button--bottom left-bar-link" href
|
||||
title="scheduled statuses" *ngIf="hasAccounts && hasScheduledStatuses" (click)="openScheduledStatuses()"
|
||||
(contextmenu)="openScheduledStatuses()">
|
||||
<fa-icon [icon]="faCalendarAlt"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a class="left-bar-button left-bar-button--cog left-bar-button--bottom left-bar-link" href title="settings" (click)="openSettings()"
|
||||
(contextmenu)="openSettings()">
|
||||
<a class="left-bar-button left-bar-button--cog left-bar-button--bottom left-bar-link" href title="settings"
|
||||
(click)="openSettings()" (contextmenu)="openSettings()">
|
||||
<fa-icon [icon]="faCog"></fa-icon>
|
||||
</a>
|
||||
</div>
|
|
@ -82,4 +82,38 @@ $height-button: 40px;
|
|||
.no-accounts {
|
||||
padding-top: 10px;
|
||||
// color: cornflowerblue;
|
||||
}
|
||||
|
||||
|
||||
$draggable-accent-color: #47e927;
|
||||
// $draggable-accent-color: #a8ff97;
|
||||
.draggable {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: auto;
|
||||
margin-bottom: 5px;
|
||||
|
||||
border: 2px solid #df0adf;
|
||||
border: 2px solid $draggable-accent-color;
|
||||
border-radius: 2px;
|
||||
|
||||
position: relative;
|
||||
|
||||
&__avatar {
|
||||
width: calc(100%);
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
position: absolute;
|
||||
|
||||
float: left;
|
||||
z-index: 5;
|
||||
color:$draggable-accent-color;
|
||||
|
||||
top: 6px;
|
||||
left: 12px;
|
||||
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
|
@ -1,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) {
|
||||
|
|
|
@ -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>
|
|
@ -40,6 +40,14 @@ $inner-column-size: 320px;
|
|||
border: 1px solid black;
|
||||
color: white;
|
||||
}
|
||||
&__follow-button {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 114px;
|
||||
padding: 0 10px 0 10px;
|
||||
border: 1px solid black;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.hashtag-stream {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { Component, OnInit, Output, EventEmitter, Input, ViewChild, OnDestroy } from '@angular/core';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { Subject, Subscription, Observable } from 'rxjs';
|
||||
import { Store } from '@ngxs/store';
|
||||
|
||||
import { StreamElement, StreamTypeEnum, AddStream } from '../../../states/streams.state';
|
||||
import { OpenThreadEvent, ToolsService } from '../../../services/tools.service';
|
||||
import { StreamStatusesComponent } from '../stream-statuses/stream-statuses.component';
|
||||
import { AccountInfo } from '../../../states/accounts.state';
|
||||
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hashtag',
|
||||
|
@ -21,7 +22,7 @@ export class HashtagComponent implements OnInit, OnDestroy {
|
|||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
|
||||
private _hashtagElement: StreamElement;
|
||||
@Input()
|
||||
@Input()
|
||||
set hashtagElement(hashtagElement: StreamElement){
|
||||
this._hashtagElement = hashtagElement;
|
||||
this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
|
@ -29,7 +30,7 @@ export class HashtagComponent implements OnInit, OnDestroy {
|
|||
get hashtagElement(): StreamElement{
|
||||
return this._hashtagElement;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ViewChild('appStreamStatuses') appStreamStatuses: StreamStatusesComponent;
|
||||
|
||||
|
@ -38,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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<a href class="context-menu-link" (click)="onContextMenu($event)"
|
||||
<a href class="context-menu-link" (click)="onContextMenu($event)"
|
||||
[class.context-menu-link__status]="statusWrapper"
|
||||
[class.context-menu-link__profile]="displayedAccount"
|
||||
title="More">
|
||||
|
@ -27,19 +27,42 @@
|
|||
<ng-template contextMenuItem (execute)="unmuteConversation()" *ngIf="statusWrapper && isOwnerSelected && displayedStatus.muted">
|
||||
Unmute conversation
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem divider="true"></ng-template>
|
||||
<ng-template contextMenuItem (execute)="muteAccount()" *ngIf="!isOwnerSelected">
|
||||
<ng-template contextMenuItem (execute)="hideBoosts()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.following && this.relationship.showing_reblogs">
|
||||
Hide boosts from @{{ this.username }}
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="unhideBoosts()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.following && !this.relationship.showing_reblogs">
|
||||
Unhide boosts from @{{ this.username }}
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem divider="true" *ngIf="!isOwnerSelected"></ng-template>
|
||||
<ng-template contextMenuItem (execute)="muteAccount()" *ngIf="!isOwnerSelected && this.relationship && !this.relationship.muting">
|
||||
Mute @{{ this.username }}
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="blockAccount()" *ngIf="!isOwnerSelected">
|
||||
Block @{{ this.username }}
|
||||
<ng-template contextMenuItem (execute)="unmuteAccount()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.muting">
|
||||
Unmute @{{ this.username }}
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="blockAccount()" *ngIf="!isOwnerSelected && this.relationship && !this.relationship.blocking">
|
||||
Block @{{ this.username }}
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="unblockAccount()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.blocking">
|
||||
Unblock @{{ this.username }}
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem divider="true" *ngIf="!isOwnerSelected"></ng-template>
|
||||
<ng-template contextMenuItem (execute)="blockDomain()" *ngIf="!isOwnerSelected && this.relationship && !this.relationship.domain_blocking">
|
||||
Block domain {{ this.domain }}
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="unblockDomain()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.domain_blocking">
|
||||
Unblock domain {{ this.domain }}
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem divider="true" *ngIf="isOwnerSelected"></ng-template>
|
||||
<ng-template contextMenuItem (execute)="pinOnProfile()" *ngIf="statusWrapper && isOwnerSelected && !displayedStatus.pinned && displayedStatus.visibility === 'public'">
|
||||
Pin on profile
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="unpinFromProfile()" *ngIf="statusWrapper && isOwnerSelected && displayedStatus.pinned && displayedStatus.visibility === 'public'">
|
||||
Unpin from profile
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="edit()" *ngIf="statusWrapper && isOwnerSelected && isEditingAvailable">
|
||||
Edit
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="delete(false)" *ngIf="statusWrapper && isOwnerSelected">
|
||||
Delete
|
||||
</ng-template>
|
||||
|
|
|
@ -4,8 +4,8 @@ import { ContextMenuComponent, ContextMenuService } from 'ngx-contextmenu';
|
|||
import { Observable, Subscription } from 'rxjs';
|
||||
import { Store } from '@ngxs/store';
|
||||
|
||||
import { Status, Account, Results } from '../../../../../services/models/mastodon.interfaces';
|
||||
import { ToolsService, OpenThreadEvent } from '../../../../../services/tools.service';
|
||||
import { Status, Account, Results, Relationship } from '../../../../../services/models/mastodon.interfaces';
|
||||
import { ToolsService, OpenThreadEvent, InstanceInfo } from '../../../../../services/tools.service';
|
||||
import { StatusWrapper } from '../../../../../models/common.model';
|
||||
import { NavigationService } from '../../../../../services/navigation.service';
|
||||
import { AccountInfo } from '../../../../../states/accounts.state';
|
||||
|
@ -25,12 +25,17 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
|
|||
private loadedAccounts: AccountInfo[];
|
||||
displayedStatus: Status;
|
||||
username: string;
|
||||
domain: string;
|
||||
isOwnerSelected: boolean;
|
||||
|
||||
isEditingAvailable: boolean;
|
||||
|
||||
@Input() statusWrapper: StatusWrapper;
|
||||
@Input() displayedAccount: Account;
|
||||
@Input() relationship: Relationship;
|
||||
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
@Output() relationshipChanged = new EventEmitter<Relationship>();
|
||||
|
||||
@ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent;
|
||||
|
||||
|
@ -70,6 +75,7 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
this.username = account.acct.split('@')[0];
|
||||
this.domain = account.acct.split('@')[1];
|
||||
this.fullHandle = this.toolsService.getAccountFullHandle(account);
|
||||
}
|
||||
|
||||
|
@ -78,6 +84,14 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
|
|||
|
||||
this.isOwnerSelected = selectedAccount.username.toLowerCase() === this.displayedStatus.account.username.toLowerCase()
|
||||
&& selectedAccount.instance.toLowerCase() === this.displayedStatus.account.url.replace('https://', '').split('/')[0].toLowerCase();
|
||||
|
||||
this.toolsService.getInstanceInfo(selectedAccount).then((instanceInfo: InstanceInfo) => {
|
||||
if (instanceInfo.major >= 4) {
|
||||
this.isEditingAvailable = true;
|
||||
} else {
|
||||
this.isEditingAvailable = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -155,38 +169,139 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
|
|||
return false;
|
||||
}
|
||||
|
||||
hideBoosts(): boolean {
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
this.toolsService.findAccount(acc, this.fullHandle)
|
||||
.then(async (target: Account) => {
|
||||
const relationship = await this.mastodonService.hideBoosts(acc, target);
|
||||
this.relationship = relationship;
|
||||
this.relationshipChanged.next(relationship);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
unhideBoosts(): boolean {
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
this.toolsService.findAccount(acc, this.fullHandle)
|
||||
.then(async (target: Account) => {
|
||||
const relationship = await this.mastodonService.unhideBoosts(acc, target);
|
||||
this.relationship = relationship;
|
||||
this.relationshipChanged.next(relationship);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
muteAccount(): boolean {
|
||||
this.loadedAccounts.forEach(acc => {
|
||||
this.toolsService.findAccount(acc, this.fullHandle)
|
||||
.then((target: Account) => {
|
||||
this.mastodonService.mute(acc, target.id);
|
||||
return target;
|
||||
})
|
||||
.then((target: Account) => {
|
||||
this.notificationService.hideAccount(target);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
});
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
this.toolsService.findAccount(acc, this.fullHandle)
|
||||
.then(async (target: Account) => {
|
||||
const relationship = await this.mastodonService.mute(acc, target.id);
|
||||
this.relationship = relationship;
|
||||
this.relationshipChanged.next(relationship);
|
||||
return target;
|
||||
})
|
||||
.then((target: Account) => {
|
||||
this.notificationService.hideAccount(target);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
unmuteAccount(): boolean {
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
this.toolsService.findAccount(acc, this.fullHandle)
|
||||
.then(async (target: Account) => {
|
||||
const relationship = await this.mastodonService.unmute(acc, target.id);
|
||||
this.relationship = relationship;
|
||||
this.relationshipChanged.next(relationship);
|
||||
return target;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
blockAccount(): boolean {
|
||||
this.loadedAccounts.forEach(acc => {
|
||||
this.toolsService.findAccount(acc, this.fullHandle)
|
||||
.then((target: Account) => {
|
||||
this.mastodonService.block(acc, target.id);
|
||||
return target;
|
||||
})
|
||||
.then((target: Account) => {
|
||||
this.notificationService.hideAccount(target);
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
this.toolsService.findAccount(acc, this.fullHandle)
|
||||
.then(async (target: Account) => {
|
||||
const relationship = await this.mastodonService.block(acc, target.id);
|
||||
this.relationship = relationship;
|
||||
this.relationshipChanged.next(relationship);
|
||||
return target;
|
||||
})
|
||||
.then((target: Account) => {
|
||||
this.notificationService.hideAccount(target);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
unblockAccount(): boolean {
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
this.toolsService.findAccount(acc, this.fullHandle)
|
||||
.then(async (target: Account) => {
|
||||
const relationship = await this.mastodonService.unblock(acc, target.id);
|
||||
this.relationship = relationship;
|
||||
this.relationshipChanged.next(relationship);
|
||||
return target;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
blockDomain(): boolean {
|
||||
const response = confirm(`Are you really sure you want to block the entire ${this.domain} domain? You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.`);
|
||||
|
||||
if (response) {
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
this.mastodonService.blockDomain(acc, this.domain)
|
||||
.then(_ => {
|
||||
this.relationship.domain_blocking = true;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
unblockDomain(): boolean {
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
this.mastodonService.blockDomain(acc, this.domain)
|
||||
.then(_ => {
|
||||
this.relationship.domain_blocking = false;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -282,6 +397,18 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
|
|||
return false;
|
||||
}
|
||||
|
||||
edit(): boolean {
|
||||
const selectedAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
this.getStatus(selectedAccount)
|
||||
.then(() => {
|
||||
this.navigationService.edit(this.statusWrapper);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, selectedAccount);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
private getStatus(account: AccountInfo): Promise<Status> {
|
||||
let statusPromise: Promise<Status> = Promise.resolve(this.statusWrapper.status);
|
||||
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
<div class="image">
|
||||
<div class="image">
|
||||
<div class="image__alt" *ngIf="displayAltLabel && attachment.description" title="{{ attachment.description }}">ALT</div>
|
||||
<a *ngIf="status" href class="image__status" (click)="openStatus()" (auxclick)="openStatus()" title="open status">
|
||||
<fa-icon class="image__status--icon" [icon]="faExternalLinkAlt"></fa-icon>
|
||||
</a>
|
||||
<a href class="image__link" (click)="openExternal()" (auxclick)="openExternal()" title="open image">
|
||||
<fa-icon class="image__link--icon" [icon]="faLink"></fa-icon>
|
||||
</a>
|
||||
|
|
|
@ -25,10 +25,48 @@
|
|||
// }
|
||||
}
|
||||
|
||||
&__status {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 25px;
|
||||
padding: 5px 5px 8px 8px;
|
||||
transition: all .2s;
|
||||
opacity: 0;
|
||||
color: white;
|
||||
|
||||
&--icon {
|
||||
filter: drop-shadow(0 0 3px rgb(78, 78, 78));
|
||||
}
|
||||
}
|
||||
|
||||
&:hover &__link {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover &__status {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__alt {
|
||||
display: inline;
|
||||
color: white;
|
||||
|
||||
z-index: 10;
|
||||
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 5px;
|
||||
|
||||
font-size: 10px;
|
||||
font-weight: bolder;
|
||||
|
||||
background-color: rgba($color: #000000, $alpha: 0.5);
|
||||
border-radius: 3px;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
img,
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { faLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faLink, faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { SettingsService } from '../../../../../services/settings.service';
|
||||
import { Attachment } from '../../../../../services/models/mastodon.interfaces';
|
||||
import { StatusWrapper } from '../../../../../models/common.model';
|
||||
import { OpenThreadEvent } from '../../../../../services/tools.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-attachement-image',
|
||||
|
@ -10,11 +13,19 @@ import { Attachment } from '../../../../../services/models/mastodon.interfaces';
|
|||
})
|
||||
export class AttachementImageComponent implements OnInit {
|
||||
faLink = faLink;
|
||||
faExternalLinkAlt = faExternalLinkAlt;
|
||||
displayAltLabel: boolean;
|
||||
|
||||
@Input() attachment: Attachment;
|
||||
@Input() status: StatusWrapper;
|
||||
@Output() openEvent = new EventEmitter();
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
|
||||
constructor() { }
|
||||
constructor(
|
||||
private readonly settingsService: SettingsService
|
||||
) {
|
||||
this.displayAltLabel = this.settingsService.getSettings().enableAltLabel;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
@ -28,4 +39,13 @@ export class AttachementImageComponent implements OnInit {
|
|||
window.open(this.attachment.url, '_blank');
|
||||
return false;
|
||||
}
|
||||
|
||||
openStatus(): boolean {
|
||||
if(!this.status) return false;
|
||||
|
||||
const openThreadEvent = new OpenThreadEvent(this.status.status, this.status.provider);
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,10 @@ export class AttachementsComponent implements OnInit {
|
|||
|
||||
@Input('attachments')
|
||||
set attachments(value: Attachment[]) {
|
||||
this.imageAttachments = [];
|
||||
this.videoAttachments = [];
|
||||
this.audioAttachments = [];
|
||||
|
||||
this._attachments = value;
|
||||
this.setAttachments(value);
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ $expand-color: $column-color;
|
|||
|
||||
& p {
|
||||
margin: 0px;
|
||||
white-space: pre-wrap;
|
||||
//font-size: .9em;
|
||||
// font-size: 14px;
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<div class="translation translation__button-display" *ngIf="isTranslationAvailable && showTranslationButton">
|
||||
<a href class="translation__link translation__button-display__link" (click)="translate()">Translate</a>
|
||||
</div>
|
||||
<div class="translation translation__display" *ngIf="isTranslationAvailable && !showTranslationButton">
|
||||
<span class="translation__by">Translated by {{translatedBy}}</span> <a href (click)="revertTranslation()" class="translation__link translation__display__link">revert</a>
|
||||
</div>
|
|
@ -0,0 +1,44 @@
|
|||
@import "variables";
|
||||
@import "commons";
|
||||
|
||||
$translation-color: #656b8f;
|
||||
$translation-color-hover: #9fa5ca;
|
||||
|
||||
.translation {
|
||||
margin: 0 10px 0 $avatar-column-space;
|
||||
color: $translation-color;
|
||||
font-size: 12px;
|
||||
|
||||
&__button-display {
|
||||
text-align: center;
|
||||
|
||||
&__link {
|
||||
display: block;
|
||||
padding: 5px 5px 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__display {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&__link {
|
||||
padding: 5px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__link {
|
||||
color: $translation-color;
|
||||
transition: all .2s;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: $translation-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&__by {
|
||||
display: block;
|
||||
text-align: left;
|
||||
padding: 5px 0 0 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { StatusTranslateComponent } from './status-translate.component';
|
||||
|
||||
xdescribe('StatusTranslateComponent', () => {
|
||||
let component: StatusTranslateComponent;
|
||||
let fixture: ComponentFixture<StatusTranslateComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ StatusTranslateComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(StatusTranslateComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,118 @@
|
|||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { StatusWrapper } from '../../../../models/common.model';
|
||||
import { ILanguage } from '../../../../states/settings.state';
|
||||
import { LanguageService } from '../../../../services/language.service';
|
||||
import { InstancesInfoService } from '../../../../services/instances-info.service';
|
||||
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
|
||||
import { Translation } from '../../../../services/models/mastodon.interfaces';
|
||||
import { NotificationService } from '../../../../services/notification.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'app-status-translate',
|
||||
templateUrl: './status-translate.component.html',
|
||||
styleUrls: ['./status-translate.component.scss']
|
||||
})
|
||||
export class StatusTranslateComponent implements OnInit, OnDestroy {
|
||||
|
||||
private languageSub: Subscription;
|
||||
private languagesSub: Subscription;
|
||||
private loadedTranslation: Translation;
|
||||
|
||||
selectedLanguage: ILanguage;
|
||||
configuredLanguages: ILanguage[] = [];
|
||||
|
||||
isTranslationAvailable: boolean;
|
||||
showTranslationButton: boolean = true;
|
||||
translatedBy: string;
|
||||
|
||||
@Input() status: StatusWrapper;
|
||||
@Output() translation = new EventEmitter<Translation>();
|
||||
|
||||
constructor(
|
||||
private readonly mastodonWrapperService: MastodonWrapperService,
|
||||
private readonly languageService: LanguageService,
|
||||
private readonly instancesInfoService: InstancesInfoService,
|
||||
private readonly notificationService: NotificationService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.languageSub = this.languageService.selectedLanguageChanged.subscribe(l => {
|
||||
if (l) {
|
||||
this.selectedLanguage = l;
|
||||
this.analyseAvailability();
|
||||
}
|
||||
});
|
||||
|
||||
this.languagesSub = this.languageService.configuredLanguagesChanged.subscribe(l => {
|
||||
if (l) {
|
||||
this.configuredLanguages = l;
|
||||
this.analyseAvailability();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.languageSub) this.languageSub.unsubscribe();
|
||||
if (this.languagesSub) this.languagesSub.unsubscribe();
|
||||
}
|
||||
|
||||
private analyseAvailability() {
|
||||
this.instancesInfoService.getTranslationAvailability(this.status.provider)
|
||||
.then(canTranslate => {
|
||||
if (canTranslate
|
||||
&& !this.status.isRemote
|
||||
&& this.status.status.language
|
||||
&& this.configuredLanguages.length > 0
|
||||
&& this.configuredLanguages.findIndex(x => x.iso639 === this.status.status.language) === -1) {
|
||||
|
||||
this.isTranslationAvailable = true;
|
||||
}
|
||||
else {
|
||||
this.isTranslationAvailable = false;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
this.isTranslationAvailable = false;
|
||||
});
|
||||
}
|
||||
|
||||
translate(): boolean {
|
||||
if(this.loadedTranslation){
|
||||
this.translation.next(this.loadedTranslation);
|
||||
this.showTranslationButton = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.mastodonWrapperService.translate(this.status.provider, this.status.status.id, this.selectedLanguage.iso639)
|
||||
.then(x => {
|
||||
this.loadedTranslation = x;
|
||||
this.translation.next(x);
|
||||
this.translatedBy = x.provider;
|
||||
this.showTranslationButton = false;
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
console.error(err);
|
||||
this.notificationService.notifyHttpError(err, this.status.provider);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
revertTranslation(): boolean {
|
||||
let revertTranslate: Translation;
|
||||
revertTranslate = {
|
||||
content: this.status.status.content,
|
||||
language: this.loadedTranslation.detected_source_language,
|
||||
detected_source_language: this.loadedTranslation.language,
|
||||
provider: this.loadedTranslation.provider,
|
||||
spoiler_text: this.status.status.spoiler_text
|
||||
};
|
||||
this.translation.next(revertTranslate);
|
||||
|
||||
this.showTranslationButton = true;
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
<div class="reblog" *ngIf="reblog">
|
||||
<a class="reblog__profile-link" href title="{{ status.account.acct }}"
|
||||
(click)="openAccount(status.account)"
|
||||
(auxclick)="openUrl(status.account.url)"><span innerHTML="{{ status.account | accountEmoji }}"></span> <img *ngIf="reblog" class="reblog__avatar" src="{{ status.account.avatar | ensureHttps }}" /></a> boosted
|
||||
(auxclick)="openUrl(status.account.url)"><span innerHTML="{{ status.account | accountEmoji }}"></span> <img *ngIf="reblog" class="reblog__avatar" src="{{ getAvatar(status.account) | ensureHttps }}" /></a> boosted
|
||||
</div>
|
||||
<div *ngIf="statusWrapper.status.pinned && !notificationType" class="pinned">
|
||||
<div class="notification--icon">
|
||||
|
@ -34,6 +34,17 @@
|
|||
boosted your status
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="notificationType === 'update'">
|
||||
<div class="notification--icon">
|
||||
<fa-icon class="update" [icon]="faEdit"></fa-icon>
|
||||
</div>
|
||||
<div class="notification--label">
|
||||
<a href class="notification--link" title="{{ notificationAccount.acct }}"
|
||||
(click)="openAccount(notificationAccount)"
|
||||
(auxclick)="openUrl(notificationAccount.url)" innerHTML="{{ notificationAccount | accountEmoji }}"></a>
|
||||
edited the status you boosted
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="notificationType === 'poll'">
|
||||
<div class="notification--icon">
|
||||
<fa-icon class="boost" [icon]="faList"></fa-icon>
|
||||
|
@ -49,9 +60,9 @@
|
|||
<div [ngClass]="{'notification--status': notificationAccount }">
|
||||
<a href class="status__profile-link" title="{{displayedStatus.account.acct}}"
|
||||
(click)="openAccount(displayedStatus.account)" (auxclick)="openUrl(displayedStatus.account.url)">
|
||||
<img [class.status__avatar--boosted]="reblog || notificationAccount" class="status__avatar" src="{{ displayedStatus.account.avatar | ensureHttps }}" />
|
||||
<img [class.status__avatar--boosted]="reblog || notificationAccount" class="status__avatar" src="{{ getAvatar(displayedStatus.account) | ensureHttps }}" />
|
||||
<!-- <img *ngIf="reblog" class="status__avatar--reblog" src="{{ status.account.avatar }}" /> -->
|
||||
<img *ngIf="notificationAccount" class="notification--avatar" src="{{ notificationAccount.avatar | ensureHttps }}" />
|
||||
<img *ngIf="notificationAccount" class="notification--avatar" src="{{ getAvatar(notificationAccount) | ensureHttps }}" />
|
||||
<span class="status__name">
|
||||
<span class="status__name--displayname"
|
||||
innerHTML="{{displayedStatus.account | accountEmoji}}"></span><span
|
||||
|
@ -85,6 +96,9 @@
|
|||
<div class="status__labels--label status__labels--remote" title="this status isn't federated with this instance" *ngIf="isRemote">
|
||||
remote
|
||||
</div>
|
||||
<div class="status__labels--label status__labels--edited" title="this status was edited" *ngIf="statusWrapper.status.edited_at">
|
||||
edited
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -95,10 +109,17 @@
|
|||
<span class="status__content-warning--title">sensitive content</span>
|
||||
<span innerHTML="{{ contentWarningText }}"></span>
|
||||
</a>
|
||||
<app-databinded-text class="status__content" *ngIf="!isContentWarned" [text]="statusContent" [selected]="isSelected"
|
||||
|
||||
<div class="status__content-warning__closed" *ngIf="!isContentWarned && contentWarningText" title="content warning">
|
||||
<span innerHTML="{{ contentWarningText }}"></span>
|
||||
</div>
|
||||
|
||||
<app-databinded-text #databindedtext class="status__content" *ngIf="!isContentWarned" [text]="statusContent" [selected]="isSelected"
|
||||
(accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)"
|
||||
(textSelected)="textSelected()"></app-databinded-text>
|
||||
|
||||
<app-status-translate [status]="displayedStatusWrapper" (translation)="onTranslation($event)"></app-status-translate>
|
||||
|
||||
<app-poll class="status__poll" *ngIf="!isContentWarned && displayedStatus.poll"
|
||||
[poll]="displayedStatus.poll" [statusWrapper]="displayedStatusWrapper"></app-poll>
|
||||
|
||||
|
|
|
@ -105,6 +105,17 @@
|
|||
background-color: rgb(33, 69, 136);
|
||||
background-color: rgb(38, 77, 148);
|
||||
}
|
||||
&--edited {
|
||||
background-color: rgb(167, 0, 153);
|
||||
background-color: rgb(0, 128, 167);
|
||||
background-color: rgb(65, 65, 71);
|
||||
background-color: rgb(144, 184, 0);
|
||||
background-color: rgb(82, 105, 0);
|
||||
|
||||
background-color: rgb(95, 95, 95);
|
||||
|
||||
// color: black;
|
||||
}
|
||||
}
|
||||
&__name {
|
||||
display: inline-block;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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...
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
<div class="overlay">
|
||||
<div class="overlay__header">
|
||||
<a href class="overlay__button overlay-close" title="close" (click)="close()">
|
||||
<fa-icon class="overlay-close__icon" [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a href class="overlay__button overlay-previous"
|
||||
[ngClass]="{'overlay__button--focus': hasPreviousElements }" title="previous" (click)="previous()">
|
||||
<fa-icon class="overlay-previous__icon" [icon]="faAngleLeft"></fa-icon>
|
||||
|
@ -12,13 +8,17 @@
|
|||
title="refresh" (click)="refresh()">
|
||||
<fa-icon class="overlay-refresh__icon" [icon]="faRedoAlt"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a href class="overlay__button overlay-next" [ngClass]="{'overlay__button--focus': hasNextElements }"
|
||||
title="next" (click)="next()">
|
||||
<fa-icon class="overlay-next__icon" [icon]="faAngleRight"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a href title="return to top" class="overlay-gototop" (click)="goToTop()">
|
||||
</a>
|
||||
|
||||
<a href class="overlay__button overlay-next" [ngClass]="{'overlay__button--focus': hasNextElements }"
|
||||
title="next" (click)="next()">
|
||||
<fa-icon class="overlay-next__icon" [icon]="faAngleRight"></fa-icon>
|
||||
<a href class="overlay__button overlay-close" title="close" (click)="close()">
|
||||
<fa-icon class="overlay-close__icon" [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -11,7 +11,9 @@ $header-content-height: 40px;
|
|||
width: calc(100%);
|
||||
height: $header-content-height;
|
||||
background-color: $column-header-background-color;
|
||||
border-bottom: 1px solid #222736;
|
||||
border-bottom: 1px solid #222736;
|
||||
|
||||
display: flex;
|
||||
}
|
||||
&__content-wrapper {
|
||||
transition: all .2s;
|
||||
|
@ -44,11 +46,17 @@ $header-content-height: 40px;
|
|||
}
|
||||
|
||||
&__button {
|
||||
// outline: 1px dotted orange;
|
||||
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
|
||||
width: $header-content-height;
|
||||
height: $header-content-height;
|
||||
|
||||
color: #354060;
|
||||
transition: all .2s;
|
||||
margin: 8px 0 0 8px;
|
||||
|
||||
&:hover {
|
||||
color: #536599;
|
||||
color: #7a8dc7;
|
||||
|
@ -68,19 +76,8 @@ $header-content-height: 40px;
|
|||
|
||||
&__icon {
|
||||
position: relative;
|
||||
left: 7px;
|
||||
top: -1px
|
||||
}
|
||||
}
|
||||
&-next {
|
||||
display: block;
|
||||
float: left;
|
||||
font-size: 18px;
|
||||
|
||||
&__icon {
|
||||
position: relative;
|
||||
left: 8px;
|
||||
top: -1px
|
||||
left: 17px;
|
||||
top: 7px
|
||||
}
|
||||
}
|
||||
&-refresh {
|
||||
|
@ -90,29 +87,38 @@ $header-content-height: 40px;
|
|||
|
||||
&__icon {
|
||||
position: relative;
|
||||
left: 5px;
|
||||
top: 1px
|
||||
left: 13px;
|
||||
top: 9px
|
||||
}
|
||||
}
|
||||
&-next {
|
||||
display: block;
|
||||
float: left;
|
||||
font-size: 18px;
|
||||
|
||||
&__icon {
|
||||
position: relative;
|
||||
left: 13px;
|
||||
top: 7px
|
||||
}
|
||||
}
|
||||
&-gototop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 110px;
|
||||
right: 40px;
|
||||
// outline: 1px dotted orange;
|
||||
|
||||
flex-grow: 1;
|
||||
|
||||
display: block;
|
||||
height: $header-content-height;
|
||||
}
|
||||
&-close {
|
||||
display: block;
|
||||
float: right;
|
||||
font-size: 13px;
|
||||
color: white;
|
||||
margin-right: 8px;
|
||||
|
||||
&__icon {
|
||||
position: relative;
|
||||
left: 7px;
|
||||
top: 1px
|
||||
left: 15px;
|
||||
top: 9px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,20 +5,23 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div class="stream-toots__new-notification"
|
||||
<div class="stream-toots__new-notification"
|
||||
[class.stream-toots__new-notification--display]="bufferStream && bufferStream.length > 0 && !streamPositionnedAtTop"></div>
|
||||
|
||||
<div class="stream-toots__content flexcroll" #statusstream (scroll)="onScroll()" tabindex="0">
|
||||
<div class="stream-toots__content flexcroll" #statusstream (scroll)="onScroll()" tabindex="0">
|
||||
<div *ngIf="displayError" class="stream-toots__error">{{displayError}}</div>
|
||||
|
||||
<div *ngIf="timelineLoadingMode === 3 && bufferStream && bufferStream.length > 0">
|
||||
<a href (click)="loadBuffer()" class="stream-toots__load-buffer" title="load new items">{{ bufferStream.length }} new item<span *ngIf="bufferStream.length > 1">s</span></a>
|
||||
<div *ngIf="timelineLoadingMode === 3 && bufferStream && numNewItems > 0">
|
||||
<a href (click)="loadBuffer()" class="stream-toots__load-buffer" title="load new items">{{ numNewItems }} new item<span *ngIf="numNewItems > 1">s</span></a>
|
||||
</div>
|
||||
|
||||
<div class="stream-toots__status" *ngFor="let statusWrapper of statuses" #status>
|
||||
<app-status
|
||||
[statusWrapper]="statusWrapper" [isThreadDisplay]="isThread"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
<app-status
|
||||
[statusWrapper]="statusWrapper"
|
||||
[isThreadDisplay]="isThread"
|
||||
[context]="context"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-status>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { HttpErrorResponse } from '@angular/common/http';
|
|||
import { Observable, Subscription } from 'rxjs';
|
||||
import { Store } from '@ngxs/store';
|
||||
|
||||
import { StreamElement } from '../../../states/streams.state';
|
||||
import { StreamElement, StreamTypeEnum } from '../../../states/streams.state';
|
||||
import { AccountInfo } from '../../../states/accounts.state';
|
||||
import { StreamingService, EventEnum, StatusUpdate } from '../../../services/streaming.service';
|
||||
import { Status } from '../../../services/models/mastodon.interfaces';
|
||||
|
@ -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) {
|
||||
|
|
|
@ -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()">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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
|
||||
) {}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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) { }
|
||||
|
||||
|
|
|
@ -33,6 +33,11 @@ export class SettingsService {
|
|||
this.saveSettings(settings);
|
||||
}
|
||||
|
||||
if(!settings.configuredLanguages){
|
||||
settings.configuredLanguages = [];
|
||||
this.saveSettings(settings);
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Reference in New Issue