Compare commits
136 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 |
|
@ -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).
|
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
|
## Official project page
|
||||||
|
|
||||||
[Discover Sengi](https://nicolasconstant.github.io/sengi/)
|
[Discover Sengi](https://nicolasconstant.github.io/sengi/)
|
||||||
|
|
|
@ -3,7 +3,7 @@ cache:
|
||||||
#- node_modules
|
#- node_modules
|
||||||
environment:
|
environment:
|
||||||
GH_TOKEN:
|
GH_TOKEN:
|
||||||
secure: wRRBU0GXTmTBgZBs2PGSaEJWOflynAyvp3Nc/7e9xmciPfkUCQAXcpOn0jIYmzpb
|
secure: eXSiJiDFgLi4vixO5GS93lgrqZ+BzQNy7PKPCQCErHjCQD9mWiEtVQQnhvmUq1FPLUc3fNLmOFQu2nIWA9bnkHg5Yw9WiG2m7QSCPRB+xCnvSY6JbLqpzURZp5x5OLj6
|
||||||
matrix:
|
matrix:
|
||||||
- nodejs_version: 10.9.0
|
- nodejs_version: 10.9.0
|
||||||
install:
|
install:
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
|
|
||||||
<div class="header__download-box--buttons">
|
<div class="header__download-box--buttons">
|
||||||
<p>
|
<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"
|
<a href="#" class="download-button download-button__web"
|
||||||
title="what are you waiting for? click!"
|
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;"
|
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 />
|
||||||
<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;">
|
<div id="download-buttons" style="display: none;">
|
||||||
<a id="windows" href class="download-button" title="download client for windows">
|
<a id="windows" href class="download-button" title="download client for windows">
|
||||||
<i class="fab fa-windows"></i>
|
<i class="fab fa-windows"></i>
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<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>
|
title="browse previous releases">browse previous releases</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -215,6 +215,12 @@
|
||||||
return myJson;
|
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() {
|
function getOS() {
|
||||||
var userAgent = window.navigator.userAgent,
|
var userAgent = window.navigator.userAgent,
|
||||||
platform = window.navigator.platform,
|
platform = window.navigator.platform,
|
||||||
|
@ -242,6 +248,9 @@
|
||||||
let lastRelease = await getLastRelease();
|
let lastRelease = await getLastRelease();
|
||||||
let version = lastRelease.tag_name;
|
let version = lastRelease.tag_name;
|
||||||
|
|
||||||
|
let lastElectronRelease = await getLastElectronRelease();
|
||||||
|
let electronVersion = lastElectronRelease.tag_name;
|
||||||
|
|
||||||
var downloadButtons = document.getElementById('download-buttons');
|
var downloadButtons = document.getElementById('download-buttons');
|
||||||
downloadButtons.style.display = 'block';
|
downloadButtons.style.display = 'block';
|
||||||
|
|
||||||
|
@ -249,12 +258,15 @@
|
||||||
downloadButtonsNojs.style.display = 'none';
|
downloadButtonsNojs.style.display = 'none';
|
||||||
|
|
||||||
var sengiVersion = document.getElementById('sengi-version');
|
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`;
|
var htmlElectronVersion = document.getElementById('electron-version');
|
||||||
document.getElementById('mac').href = `https://github.com/NicolasConstant/sengi/releases/download/${version}/Sengi-${version}-mac.dmg`;
|
htmlElectronVersion.textContent = `(${electronVersion})`;
|
||||||
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`;
|
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();
|
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",
|
"name": "sengi",
|
||||||
"version": "1.1.6",
|
"version": "1.7.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"main": "main-electron.js",
|
"main": "main-electron.js",
|
||||||
"description": "A multi-account desktop client for Mastodon and Pleroma",
|
"description": "A multi-account desktop client for Mastodon and Pleroma",
|
||||||
|
@ -21,21 +21,18 @@
|
||||||
"test-nowatch": "ng test --watch=false",
|
"test-nowatch": "ng test --watch=false",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"e2e": "ng e2e",
|
"e2e": "ng e2e",
|
||||||
"electron": "electron .",
|
"dist": "npm run build"
|
||||||
"electron-prod": "ng build --prod && electron .",
|
|
||||||
"electron-debug": "ng build && electron .",
|
|
||||||
"dist": "npm run build && electron-builder --publish onTagOrDraft",
|
|
||||||
"travis": "electron-builder --publish onTagOrDraft"
|
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^7.2.7",
|
"@angular/animations": "^7.2.16",
|
||||||
"@angular/cdk": "^7.2.7",
|
"@angular/cdk": "^7.3.7",
|
||||||
"@angular/common": "^7.2.7",
|
"@angular/common": "^7.2.7",
|
||||||
"@angular/compiler": "^7.2.7",
|
"@angular/compiler": "^7.2.7",
|
||||||
"@angular/core": "^7.2.7",
|
"@angular/core": "^7.2.7",
|
||||||
"@angular/forms": "^7.2.7",
|
"@angular/forms": "^7.2.7",
|
||||||
"@angular/http": "^7.2.7",
|
"@angular/http": "^7.2.7",
|
||||||
|
"@angular/material": "^16.2.1",
|
||||||
"@angular/platform-browser": "^7.2.7",
|
"@angular/platform-browser": "^7.2.7",
|
||||||
"@angular/platform-browser-dynamic": "^7.2.7",
|
"@angular/platform-browser-dynamic": "^7.2.7",
|
||||||
"@angular/pwa": "^0.12.4",
|
"@angular/pwa": "^0.12.4",
|
||||||
|
@ -47,9 +44,9 @@
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.7.0",
|
"@fortawesome/free-brands-svg-icons": "^5.7.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.7.0",
|
"@fortawesome/free-regular-svg-icons": "^5.7.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.7.0",
|
"@fortawesome/free-solid-svg-icons": "^5.7.0",
|
||||||
"@ngxs/storage-plugin": "^3.2.0",
|
"@ngxs/storage-plugin": "~3.2.0",
|
||||||
"@ngxs/store": "^3.2.0",
|
"@ngxs/store": "~3.2.0",
|
||||||
"angular2-hotkeys": "^2.1.5",
|
"angular2-hotkeys": "~2.1.5",
|
||||||
"bootstrap": "^4.1.3",
|
"bootstrap": "^4.1.3",
|
||||||
"core-js": "^2.5.4",
|
"core-js": "^2.5.4",
|
||||||
"emojione": "~4.5.0",
|
"emojione": "~4.5.0",
|
||||||
|
@ -70,8 +67,6 @@
|
||||||
"@types/jasminewd2": "~2.0.3",
|
"@types/jasminewd2": "~2.0.3",
|
||||||
"@types/node": "~8.9.4",
|
"@types/node": "~8.9.4",
|
||||||
"codelyzer": "~4.2.1",
|
"codelyzer": "~4.2.1",
|
||||||
"electron": "^10.1.1",
|
|
||||||
"electron-builder": "^20.39.0",
|
|
||||||
"jasmine-core": "~2.99.1",
|
"jasmine-core": "~2.99.1",
|
||||||
"jasmine-spec-reporter": "~4.2.1",
|
"jasmine-spec-reporter": "~4.2.1",
|
||||||
"karma": "~1.7.1",
|
"karma": "~1.7.1",
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { HttpModule } from "@angular/http";
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
import { NgModule, APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
import { NgModule, APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
||||||
import { RouterModule, Routes } from "@angular/router";
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||||
// import { NgxElectronModule } from "ngx-electron";
|
// import { NgxElectronModule } from 'ngx-electron';
|
||||||
|
|
||||||
import { NgxsModule } from '@ngxs/store';
|
import { NgxsModule } from '@ngxs/store';
|
||||||
import { NgxsStoragePluginModule } from '@ngxs/storage-plugin';
|
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 { NotificationsTutorialComponent } from './components/tutorial-enhanced/notifications-tutorial/notifications-tutorial.component';
|
||||||
import { LabelsTutorialComponent } from './components/tutorial-enhanced/labels-tutorial/labels-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 { ThankyouTutorialComponent } from './components/tutorial-enhanced/thankyou-tutorial/thankyou-tutorial.component';
|
||||||
|
import { StatusTranslateComponent } from './components/stream/status/status-translate/status-translate.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: "", component: StreamsMainDisplayComponent },
|
{ path: "", component: StreamsMainDisplayComponent },
|
||||||
|
@ -159,7 +160,8 @@ const routes: Routes = [
|
||||||
TutorialEnhancedComponent,
|
TutorialEnhancedComponent,
|
||||||
NotificationsTutorialComponent,
|
NotificationsTutorialComponent,
|
||||||
LabelsTutorialComponent,
|
LabelsTutorialComponent,
|
||||||
ThankyouTutorialComponent
|
ThankyouTutorialComponent,
|
||||||
|
StatusTranslateComponent
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
EmojiPickerComponent
|
EmojiPickerComponent
|
||||||
|
@ -173,9 +175,11 @@ const routes: Routes = [
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
PickerModule,
|
PickerModule,
|
||||||
OwlDateTimeModule,
|
OwlDateTimeModule,
|
||||||
OwlNativeDateTimeModule,
|
OwlNativeDateTimeModule,
|
||||||
OverlayModule,
|
OverlayModule,
|
||||||
|
DragDropModule,
|
||||||
|
// NgxElectronModule,
|
||||||
RouterModule.forRoot(routes),
|
RouterModule.forRoot(routes),
|
||||||
|
|
||||||
NgxsModule.forRoot([
|
NgxsModule.forRoot([
|
||||||
|
|
|
@ -28,6 +28,7 @@ export abstract class TimelineBase extends BrowseBase {
|
||||||
statuses: StatusWrapper[] = [];
|
statuses: StatusWrapper[] = [];
|
||||||
bufferStream: Status[] = [];
|
bufferStream: Status[] = [];
|
||||||
protected bufferWasCleared: boolean;
|
protected bufferWasCleared: boolean;
|
||||||
|
numNewItems: number;
|
||||||
streamPositionnedAtTop: boolean = true;
|
streamPositionnedAtTop: boolean = true;
|
||||||
protected isProcessingInfiniteScroll: boolean;
|
protected isProcessingInfiniteScroll: boolean;
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,23 @@
|
||||||
<form class="status-editor" (ngSubmit)="onSubmit()">
|
<form class="status-editor" (ngSubmit)="onSubmit()">
|
||||||
<input [(ngModel)]="title" type="text" class="form-control form-control-sm status-editor__title" name="title"
|
<input #mytitle [(ngModel)]="title" type="text" class="form-control form-control-sm status-editor__title"
|
||||||
autocomplete="off" placeholder="Title, Content Warning (optional)" title="title, content warning (optional)" dir="auto" />
|
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"
|
<a class="status-editor__emoji" title="Insert Emoji"
|
||||||
#emojiButton href (click)="openEmojiPicker($event)">
|
#emojiButton href (click)="openEmojiPicker($event)">
|
||||||
<img class="status-editor__emoji--image" src="/assets/emoji/72x72/1f636.png">
|
<img class="status-editor__emoji--image" src="/assets/emoji/72x72/1f636.png">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm status-editor__content" (paste)="onPaste($event)"
|
<a class="status-editor__lang" title="Change language" href *ngIf="configuredLanguages && configuredLanguages.length > 1" (click)="onLangContextMenu($event)">
|
||||||
rows="5" required title="content" placeholder="What's on your mind?" (keydown.control.enter)="onCtrlEnter()"
|
{{ 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">
|
(keydown)="handleKeyDown($event)" (blur)="statusTextEditorLostFocus()" dir="auto">
|
||||||
</textarea>
|
</textarea>
|
||||||
|
|
||||||
|
@ -21,19 +30,21 @@
|
||||||
(suggestionSelectedEvent)="suggestionSelected($event)" (hasSuggestionsEvent)="suggestionsChanged($event)">
|
(suggestionSelectedEvent)="suggestionSelected($event)" (hasSuggestionsEvent)="suggestionsChanged($event)">
|
||||||
</app-autosuggest>
|
</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>
|
<app-status-scheduler class="scheduler" *ngIf="instanceSupportsScheduling && scheduleIsActive"></app-status-scheduler>
|
||||||
|
|
||||||
<div class="status-editor__footer" #footer>
|
<div class="status-editor__footer" #footer>
|
||||||
<button type="submit" title="reply" class="status-editor__footer--send-button" *ngIf="statusReplyingToWrapper">
|
<button type="submit" title="reply" class="status-editor__footer--send-button" *ngIf="statusReplyingToWrapper">
|
||||||
<span *ngIf="!isSending && !scheduleIsActive">REPLY!</span>
|
<span *ngIf="!isSending && !scheduleIsActive && !isEditing">REPLY!</span>
|
||||||
<span *ngIf="!isSending && scheduleIsActive">PLAN!</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>
|
<app-waiting-animation class="waiting-icon" *ngIf="isSending"></app-waiting-animation>
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" title="post" class="status-editor__footer--send-button" *ngIf="!statusReplyingToWrapper">
|
<button type="submit" title="post" class="status-editor__footer--send-button" *ngIf="!statusReplyingToWrapper">
|
||||||
<span *ngIf="!isSending && !scheduleIsActive">POST!</span>
|
<span *ngIf="!isSending && !scheduleIsActive && !isEditing">POST!</span>
|
||||||
<span *ngIf="!isSending && scheduleIsActive">PLAN!</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>
|
<app-waiting-animation class="waiting-icon" *ngIf="isSending"></app-waiting-animation>
|
||||||
</button>
|
</button>
|
||||||
<div class="status-editor__footer__counter">
|
<div class="status-editor__footer__counter">
|
||||||
|
@ -64,6 +75,10 @@
|
||||||
<fa-icon [icon]="faClock"></fa-icon>
|
<fa-icon [icon]="faClock"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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>
|
<context-menu #contextMenu>
|
||||||
<ng-template contextMenuItem (execute)="changePrivacy('Public')">
|
<ng-template contextMenuItem (execute)="changePrivacy('Public')">
|
||||||
|
@ -79,5 +94,12 @@
|
||||||
<fa-icon [icon]="faEnvelope" class="context-menu-icon"></fa-icon> Direct
|
<fa-icon [icon]="faEnvelope" class="context-menu-icon"></fa-icon> Direct
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</context-menu>
|
</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>
|
<app-media></app-media>
|
||||||
</form>
|
</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 {
|
&__content {
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
background-color: $status-editor-background;
|
background-color: $status-editor-background;
|
||||||
|
@ -154,6 +180,9 @@ $counter-width: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
& span {
|
& span {
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
@ -204,6 +233,20 @@ $counter-width: 90px;
|
||||||
border-bottom: 1px solid whitesmoke;
|
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';
|
@import '~@angular/cdk/overlay-prebuilt.css';
|
||||||
// ::ng-deep .cdk-overlay-backdrop {
|
// ::ng-deep .cdk-overlay-backdrop {
|
||||||
// // width: 100%;
|
// // width: 100%;
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { NavigationService } from '../../services/navigation.service';
|
||||||
import { NotificationService } from '../../services/notification.service';
|
import { NotificationService } from '../../services/notification.service';
|
||||||
import { MastodonService } from '../../services/mastodon.service';
|
import { MastodonService } from '../../services/mastodon.service';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
|
import { SettingsState } from '../../states/settings.state';
|
||||||
|
|
||||||
describe('CreateStatusComponent', () => {
|
describe('CreateStatusComponent', () => {
|
||||||
let component: CreateStatusComponent;
|
let component: CreateStatusComponent;
|
||||||
|
@ -33,7 +33,8 @@ describe('CreateStatusComponent', () => {
|
||||||
NgxsModule.forRoot([
|
NgxsModule.forRoot([
|
||||||
RegisteredAppsState,
|
RegisteredAppsState,
|
||||||
AccountsState,
|
AccountsState,
|
||||||
StreamsState
|
StreamsState,
|
||||||
|
SettingsState
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
providers: [NavigationService, NotificationService, MastodonService, AuthService],
|
providers: [NavigationService, NotificationService, MastodonService, AuthService],
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { ContextMenuService, ContextMenuComponent } from 'ngx-contextmenu';
|
||||||
|
|
||||||
import { VisibilityEnum, PollParameters } from '../../services/mastodon.service';
|
import { VisibilityEnum, PollParameters } from '../../services/mastodon.service';
|
||||||
import { MastodonWrapperService } from '../../services/mastodon-wrapper.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 { ToolsService, InstanceInfo, InstanceType } from '../../services/tools.service';
|
||||||
import { NotificationService } from '../../services/notification.service';
|
import { NotificationService } from '../../services/notification.service';
|
||||||
import { StatusWrapper } from '../../models/common.model';
|
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 { ScheduledStatusService } from '../../services/scheduled-status.service';
|
||||||
import { StatusesStateService } from '../../services/statuses-state.service';
|
import { StatusesStateService } from '../../services/statuses-state.service';
|
||||||
import { SettingsService } from '../../services/settings.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({
|
@Component({
|
||||||
selector: 'app-create-status',
|
selector: 'app-create-status',
|
||||||
|
@ -65,6 +68,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
this.detectAutosuggestion(value);
|
this.detectAutosuggestion(value);
|
||||||
this._status = value;
|
this._status = value;
|
||||||
|
|
||||||
|
this.languageService.autoDetectLang(value);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.autoGrow();
|
this.autoGrow();
|
||||||
}, 0);
|
}, 0);
|
||||||
|
@ -83,12 +88,22 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
return s;
|
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')
|
@Input('redraftedStatus')
|
||||||
set redraftedStatus(value: StatusWrapper) {
|
set redraftedStatus(value: StatusWrapper) {
|
||||||
if (value) {
|
if (value) {
|
||||||
this.isRedrafting = true;
|
this.isRedrafting = true;
|
||||||
this.statusLoaded = false;
|
this.statusLoaded = false;
|
||||||
|
|
||||||
if (value.status && value.status.media_attachments) {
|
if (value.status && value.status.media_attachments) {
|
||||||
for (const m of value.status.media_attachments) {
|
for (const m of value.status.media_attachments) {
|
||||||
this.mediaService.addExistingMedia(new MediaWrapper(m.id, null, m));
|
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);
|
// 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.setVisibilityFromStatus(value.status);
|
||||||
this.title = value.status.spoiler_text;
|
this.title = value.status.spoiler_text;
|
||||||
this.statusLoaded = true;
|
this.statusLoaded = true;
|
||||||
|
@ -130,9 +152,19 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
this.isSending = false;
|
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;
|
private maxCharLength: number;
|
||||||
charCountLeft: number;
|
charCountLeft: number;
|
||||||
postCounts: number = 1;
|
postCounts: number = 1;
|
||||||
|
@ -141,6 +173,10 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
autosuggestData: string = null;
|
autosuggestData: string = null;
|
||||||
instanceSupportsPoll = true;
|
instanceSupportsPoll = true;
|
||||||
instanceSupportsScheduling = true;
|
instanceSupportsScheduling = true;
|
||||||
|
isEditing: boolean;
|
||||||
|
editingStatusId: string;
|
||||||
|
configuredLanguages: ILanguage[] = [];
|
||||||
|
selectedLanguage: ILanguage;
|
||||||
private statusLoaded: boolean;
|
private statusLoaded: boolean;
|
||||||
private hasSuggestions: boolean;
|
private hasSuggestions: boolean;
|
||||||
|
|
||||||
|
@ -150,6 +186,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
@ViewChild('fileInput') fileInputElement: ElementRef;
|
@ViewChild('fileInput') fileInputElement: ElementRef;
|
||||||
@ViewChild('footer') footerElement: ElementRef;
|
@ViewChild('footer') footerElement: ElementRef;
|
||||||
@ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent;
|
@ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent;
|
||||||
|
@ViewChild('langContextMenu') public langContextMenu: ContextMenuComponent;
|
||||||
@ViewChild(PollEditorComponent) pollEditor: PollEditorComponent;
|
@ViewChild(PollEditorComponent) pollEditor: PollEditorComponent;
|
||||||
@ViewChild(StatusSchedulerComponent) statusScheduler: StatusSchedulerComponent;
|
@ViewChild(StatusSchedulerComponent) statusScheduler: StatusSchedulerComponent;
|
||||||
|
|
||||||
|
@ -184,11 +221,15 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
private accounts$: Observable<AccountInfo[]>;
|
private accounts$: Observable<AccountInfo[]>;
|
||||||
private accountSub: Subscription;
|
private accountSub: Subscription;
|
||||||
|
private langSub: Subscription;
|
||||||
|
private selectLangSub: Subscription;
|
||||||
private selectedAccount: AccountInfo;
|
private selectedAccount: AccountInfo;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly navigationService: NavigationService,
|
||||||
|
private readonly languageService: LanguageService,
|
||||||
private readonly settingsService: SettingsService,
|
private readonly settingsService: SettingsService,
|
||||||
private statusStateService: StatusesStateService,
|
private readonly statusStateService: StatusesStateService,
|
||||||
private readonly scheduledStatusService: ScheduledStatusService,
|
private readonly scheduledStatusService: ScheduledStatusService,
|
||||||
private readonly contextMenuService: ContextMenuService,
|
private readonly contextMenuService: ContextMenuService,
|
||||||
private readonly store: Store,
|
private readonly store: Store,
|
||||||
|
@ -198,12 +239,41 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
private readonly instancesInfoService: InstancesInfoService,
|
private readonly instancesInfoService: InstancesInfoService,
|
||||||
private readonly mediaService: MediaService,
|
private readonly mediaService: MediaService,
|
||||||
private readonly overlay: Overlay,
|
private readonly overlay: Overlay,
|
||||||
public viewContainerRef: ViewContainerRef) {
|
public viewContainerRef: ViewContainerRef,
|
||||||
|
private readonly statusesStateService: StatusesStateService) {
|
||||||
|
|
||||||
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
|
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() {
|
ngOnInit() {
|
||||||
|
this.initLanguages();
|
||||||
|
|
||||||
if (!this.isRedrafting) {
|
if (!this.isRedrafting) {
|
||||||
this.status = this.statusStateService.getStatusContent(this.statusReplyingToWrapper);
|
this.status = this.statusStateService.getStatusContent(this.statusReplyingToWrapper);
|
||||||
}
|
}
|
||||||
|
@ -250,6 +320,13 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.accountSub.unsubscribe();
|
this.accountSub.unsubscribe();
|
||||||
|
this.langSub.unsubscribe();
|
||||||
|
this.selectLangSub.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
onNavigateToSettings(): boolean {
|
||||||
|
this.navigationService.openPanel(LeftPanelType.Settings);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onPaste(e: any) {
|
onPaste(e: any) {
|
||||||
|
@ -308,7 +385,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
};
|
};
|
||||||
|
|
||||||
const word = this.getWordByPos(currentSection, caretPosition - offset);
|
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;
|
this.autosuggestData = word;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -436,7 +513,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private setVisibility(defaultPrivacy: VisibilityEnum) {
|
private setVisibility(defaultPrivacy: VisibilityEnum) {
|
||||||
if(this.selectedPrivacySetByRedraft) return;
|
if (this.selectedPrivacySetByRedraft) return;
|
||||||
|
|
||||||
switch (defaultPrivacy) {
|
switch (defaultPrivacy) {
|
||||||
case VisibilityEnum.Public:
|
case VisibilityEnum.Public:
|
||||||
|
@ -494,14 +571,14 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
private getMentions(status: Status): string[] {
|
private getMentions(status: Status): string[] {
|
||||||
let acct = status.account.acct;
|
let acct = status.account.acct;
|
||||||
if(!acct.includes('@')) {
|
if (!acct.includes('@')) {
|
||||||
acct += `@${status.account.url.replace('https://', '').split('/')[0]}`
|
acct += `@${status.account.url.replace('https://', '').split('/')[0]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const mentions = [acct];
|
const mentions = [acct];
|
||||||
status.mentions.forEach(m => {
|
status.mentions.forEach(m => {
|
||||||
let mentionAcct = m.acct;
|
let mentionAcct = m.acct;
|
||||||
if(!mentionAcct.includes('@')){
|
if (!mentionAcct.includes('@')) {
|
||||||
mentionAcct += `@${m.url.replace('https://', '').split('/')[0]}`;
|
mentionAcct += `@${m.url.replace('https://', '').split('/')[0]}`;
|
||||||
}
|
}
|
||||||
mentions.push(mentionAcct);
|
mentions.push(mentionAcct);
|
||||||
|
@ -525,7 +602,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit(): boolean {
|
async onSubmit(): Promise<boolean> {
|
||||||
if (this.isSending || this.mentionTooFarAwayError) return false;
|
if (this.isSending || this.mentionTooFarAwayError) return false;
|
||||||
|
|
||||||
this.isSending = true;
|
this.isSending = true;
|
||||||
|
@ -546,9 +623,10 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaAttachments = this.mediaService.mediaSubject.value.map(x => x.attachment);
|
|
||||||
|
|
||||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||||
|
|
||||||
|
const mediaAttachments = (await this.mediaService.retrieveUpToDateMedia(acc)).map(x => x.attachment);
|
||||||
|
|
||||||
let usableStatus: Promise<Status>;
|
let usableStatus: Promise<Status>;
|
||||||
if (this.statusReplyingToWrapper) {
|
if (this.statusReplyingToWrapper) {
|
||||||
usableStatus = this.toolsService.getStatusUsableByAccount(acc, this.statusReplyingToWrapper);
|
usableStatus = this.toolsService.getStatusUsableByAccount(acc, this.statusReplyingToWrapper);
|
||||||
|
@ -572,7 +650,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
usableStatus
|
usableStatus
|
||||||
.then((status: Status) => {
|
.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) => {
|
.then((res: Status) => {
|
||||||
this.title = '';
|
this.title = '';
|
||||||
|
@ -599,7 +677,15 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
return false;
|
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 parsedStatus = this.parseStatus(status);
|
||||||
let resultPromise = Promise.resolve(previousStatus);
|
let resultPromise = Promise.resolve(previousStatus);
|
||||||
|
|
||||||
|
@ -613,13 +699,25 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i === 0) {
|
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) => {
|
.then((status: Status) => {
|
||||||
this.mediaService.clearMedia();
|
this.mediaService.clearMedia();
|
||||||
return status;
|
return status;
|
||||||
});
|
});
|
||||||
} else {
|
} 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) => {
|
.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));
|
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;
|
return status;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -636,8 +744,6 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseStatus(status: string): string[] {
|
private parseStatus(status: string): string[] {
|
||||||
//console.error(status.toString());
|
|
||||||
|
|
||||||
let mentionExtraChars = this.getMentionExtraChars(status);
|
let mentionExtraChars = this.getMentionExtraChars(status);
|
||||||
let urlExtraChar = this.getLinksExtraChars(status);
|
let urlExtraChar = this.getLinksExtraChars(status);
|
||||||
let trucatedStatus = `${status}`;
|
let trucatedStatus = `${status}`;
|
||||||
|
@ -654,8 +760,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
while (trucatedStatus.length > currentMaxCharLength) {
|
while (trucatedStatus.length > currentMaxCharLength) {
|
||||||
const nextIndex = trucatedStatus.lastIndexOf(' ', maxChars);
|
const nextIndex = trucatedStatus.lastIndexOf(' ', maxChars);
|
||||||
|
|
||||||
if(nextIndex === -1){
|
if (nextIndex === -1) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -706,8 +812,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
suggestionSelected(selection: AutosuggestSelection) {
|
suggestionSelected(selection: AutosuggestSelection) {
|
||||||
if (this.status.includes(selection.pattern)) {
|
if (this.status.includes(selection.pattern)) {
|
||||||
this.status = this.replacePatternWithAutosuggest(this.status, selection.pattern, selection.autosuggest);
|
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;
|
let newCaretPosition = cleanStatus.indexOf(`${selection.autosuggest}`) + selection.autosuggest.length;
|
||||||
if (newCaretPosition > cleanStatus.length) newCaretPosition = cleanStatus.length;
|
if (newCaretPosition > cleanStatus.length) newCaretPosition = cleanStatus.length;
|
||||||
|
|
||||||
|
@ -756,7 +862,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
w++;
|
w++;
|
||||||
result += `${word}`;
|
result += `${word}`;
|
||||||
|
|
||||||
if(w < wordCount || i === nberLines){
|
if (w < wordCount || i === nberLines) {
|
||||||
result += ' ';
|
result += ' ';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -768,7 +874,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
result = result.replace(' ', ' ');
|
result = result.replace(' ', ' ');
|
||||||
|
|
||||||
let endRegex = new RegExp(`${autosuggest} $`, 'i');
|
let endRegex = new RegExp(`${autosuggest} $`, 'i');
|
||||||
if(!result.match(endRegex)){
|
if (!result.match(endRegex)) {
|
||||||
result = result.substring(0, result.length - 1);
|
result = result.substring(0, result.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -853,6 +959,17 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||||
$event.stopPropagation();
|
$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
|
//https://stackblitz.com/edit/overlay-demo
|
||||||
@ViewChild('emojiButton') emojiButtonElement: ElementRef;
|
@ViewChild('emojiButton') emojiButtonElement: ElementRef;
|
||||||
overlayRef: OverlayRef;
|
overlayRef: OverlayRef;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<div *ngFor="let m of media" class="media">
|
<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>
|
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
||||||
</div>
|
</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)">
|
(mouseleave)="updateMedia(m)">
|
||||||
<div class="media__loaded--migrating" *ngIf="m.isMigrating">
|
<div class="media__loaded--migrating" *ngIf="m.isMigrating">
|
||||||
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
||||||
|
|
|
@ -56,4 +56,13 @@ export class MediaComponent implements OnInit, OnDestroy {
|
||||||
this.mediaService.update(account, media);
|
this.mediaService.update(account, media);
|
||||||
return false;
|
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 { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
import { PollEntry } from './poll-entry/poll-entry.component';
|
import { PollEntry } from './poll-entry/poll-entry.component';
|
||||||
import { PollParameters } from '../../../services/mastodon.service';
|
import { PollParameters } from '../../../services/mastodon.service';
|
||||||
import { retry } from 'rxjs/operators';
|
import { Poll } from '../../../services/models/mastodon.interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-poll-editor',
|
selector: 'app-poll-editor',
|
||||||
|
@ -19,6 +19,8 @@ export class PollEditorComponent implements OnInit {
|
||||||
selectedId: string;
|
selectedId: string;
|
||||||
private multiSelected: boolean;
|
private multiSelected: boolean;
|
||||||
|
|
||||||
|
@Input() oldPoll: Poll;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
|
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
|
||||||
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 {
|
private getEntryUuid(): number {
|
||||||
this.entryUuid++;
|
this.entryUuid++;
|
||||||
return this.entryUuid;
|
return this.entryUuid;
|
||||||
|
@ -50,7 +58,7 @@ export class PollEditorComponent implements OnInit {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeElement(entry: PollEntry){
|
removeElement(entry: PollEntry) {
|
||||||
this.entries = this.entries.filter(x => x.id != entry.id);
|
this.entries = this.entries.filter(x => x.id != entry.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +77,19 @@ export class PollEditorComponent implements OnInit {
|
||||||
params.hide_totals = false;
|
params.hide_totals = false;
|
||||||
return params;
|
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 {
|
class Delay {
|
||||||
|
|
|
@ -15,7 +15,9 @@
|
||||||
<button type="submit" class="form-button"
|
<button type="submit" class="form-button"
|
||||||
title="add account"
|
title="add account"
|
||||||
[class.comrade__button]="isComrade">
|
[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>
|
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@ -29,5 +31,12 @@
|
||||||
allowfullscreen></iframe>
|
allowfullscreen></iframe>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
|
@ -109,4 +109,21 @@ $comrade_red: #a50000;
|
||||||
background-color: $comrade_red;
|
background-color: $comrade_red;
|
||||||
background-position: 0 0;
|
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 { AuthService, CurrentAuthProcess } from '../../../services/auth.service';
|
||||||
import { AppData } from '../../../services/models/mastodon.interfaces';
|
import { AppData } from '../../../services/models/mastodon.interfaces';
|
||||||
import { NotificationService } from '../../../services/notification.service';
|
import { NotificationService } from '../../../services/notification.service';
|
||||||
|
import { ToolsService } from '../../../services/tools.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-add-new-account',
|
selector: 'app-add-new-account',
|
||||||
templateUrl: './add-new-account.component.html',
|
templateUrl: './add-new-account.component.html',
|
||||||
styleUrls: ['./add-new-account.component.scss']
|
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 blockList = ['gab.com', 'gab.ai', 'cyzed.com'];
|
||||||
private comradeList = ['juche.town'];
|
private comradeList = ['juche.town'];
|
||||||
|
|
||||||
|
@ -24,12 +25,14 @@ export class AddNewAccountComponent implements OnInit {
|
||||||
set setInstance(value: string) {
|
set setInstance(value: string) {
|
||||||
this.instance = value.replace('http://', '').replace('https://', '').replace('/', '').toLowerCase().trim();
|
this.instance = value.replace('http://', '').replace('https://', '').replace('/', '').toLowerCase().trim();
|
||||||
this.checkComrad();
|
this.checkComrad();
|
||||||
|
this.checkInstanceMultiAccount(value);
|
||||||
}
|
}
|
||||||
get setInstance(): string {
|
get setInstance(): string {
|
||||||
return this.instance;
|
return this.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly toolsService: ToolsService,
|
||||||
private readonly notificationService: NotificationService,
|
private readonly notificationService: NotificationService,
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly store: Store) { }
|
private readonly store: Store) { }
|
||||||
|
@ -51,8 +54,27 @@ export class AddNewAccountComponent implements OnInit {
|
||||||
this.isComrade = false;
|
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 {
|
onSubmit(): boolean {
|
||||||
if(this.isLoading || !this.instance) return false;
|
if(this.isLoading || !this.instance || this.isInstanceMultiAccountLoading) return false;
|
||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
<div class=" new-message-body flexcroll">
|
<div class=" new-message-body flexcroll">
|
||||||
<app-create-status (onClose)="closeColumn()" [isDirectMention]="isDirectMention"
|
<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>
|
||||||
</div>
|
</div>
|
|
@ -13,6 +13,7 @@ export class AddNewStatusComponent implements OnInit {
|
||||||
@Input() isDirectMention: boolean;
|
@Input() isDirectMention: boolean;
|
||||||
@Input() userHandle: string;
|
@Input() userHandle: string;
|
||||||
@Input() redraftedStatus: StatusWrapper;
|
@Input() redraftedStatus: StatusWrapper;
|
||||||
|
@Input() statusToEdit: StatusWrapper;
|
||||||
|
|
||||||
constructor(private readonly navigationService: NavigationService) {
|
constructor(private readonly navigationService: NavigationService) {
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,31 @@
|
||||||
<div class="floating-column">
|
<div class="floating-column">
|
||||||
<div class="floating-column__inner">
|
<div class="floating-column__inner">
|
||||||
<div class="sliding-column" [class.sliding-column__right-display]="overlayActive">
|
<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()"
|
(closeOverlay)="closeOverlay()"
|
||||||
[browseAccountData]="overlayAccountToBrowse"
|
[browseAccountData]="overlayAccountToBrowse"
|
||||||
[browseHashtagData]="overlayHashtagToBrowse"
|
[browseHashtagData]="overlayHashtagToBrowse"
|
||||||
[browseThreadData]="overlayThreadToBrowse"></app-stream-overlay>
|
[browseThreadData]="overlayThreadToBrowse"></app-stream-overlay>
|
||||||
|
|
||||||
<div class="floating-column__inner--left">
|
<div class="floating-column__inner--left">
|
||||||
<div class="floating-column__header">
|
<div class="floating-column__header">
|
||||||
<a class="close-button" href (click)="closePanel()" title="close">
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-manage-account *ngIf="openPanel === 'manageAccount'" [account]="userAccountUsed"
|
<app-manage-account *ngIf="openPanel === 'manageAccount'" [account]="userAccountUsed"
|
||||||
(browseAccountEvent)="browseAccount($event)"
|
(browseAccountEvent)="browseAccount($event)"
|
||||||
(browseHashtagEvent)="browseHashtag($event)"
|
(browseHashtagEvent)="browseHashtag($event)"
|
||||||
(browseThreadEvent)="browseThread($event)"></app-manage-account>
|
(browseThreadEvent)="browseThread($event)"></app-manage-account>
|
||||||
<app-add-new-status *ngIf="openPanel === 'createNewStatus'" [isDirectMention]="isDirectMention"
|
<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-add-new-account *ngIf="openPanel === 'addNewAccount'"></app-add-new-account>
|
||||||
<app-search *ngIf="openPanel === 'search'"
|
<app-search *ngIf="openPanel === 'search'"
|
||||||
(browseAccountEvent)="browseAccount($event)"
|
(browseAccountEvent)="browseAccount($event)"
|
||||||
(browseHashtagEvent)="browseHashtag($event)"
|
(browseHashtagEvent)="browseHashtag($event)"
|
||||||
(browseThreadEvent)="browseThread($event)">
|
(browseThreadEvent)="browseThread($event)">
|
||||||
</app-search>
|
</app-search>
|
||||||
<app-settings *ngIf="openPanel === 'settings'"></app-settings>
|
<app-settings *ngIf="openPanel === 'settings'"></app-settings>
|
||||||
|
|
|
@ -29,9 +29,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-button {
|
.close-button {
|
||||||
|
// outline: 1px dotted orange;
|
||||||
|
|
||||||
display: block;
|
display: block;
|
||||||
float: right;
|
float: right;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: white;
|
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;
|
isDirectMention: boolean;
|
||||||
userHandle: string;
|
userHandle: string;
|
||||||
redraftedStatus: StatusWrapper;
|
redraftedStatus: StatusWrapper;
|
||||||
|
statusToEdit: StatusWrapper;
|
||||||
|
|
||||||
openPanel: string = '';
|
openPanel: string = '';
|
||||||
|
|
||||||
|
@ -49,12 +50,21 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case LeftPanelType.CreateNewStatus:
|
case LeftPanelType.CreateNewStatus:
|
||||||
|
case LeftPanelType.EditStatus:
|
||||||
if (this.openPanel === 'createNewStatus' && !event.userHandle) {
|
if (this.openPanel === 'createNewStatus' && !event.userHandle) {
|
||||||
this.closePanel();
|
this.closePanel();
|
||||||
} else {
|
} else {
|
||||||
this.isDirectMention = event.action === LeftPanelAction.DM;
|
this.isDirectMention = event.action === LeftPanelAction.DM;
|
||||||
this.userHandle = event.userHandle;
|
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';
|
this.openPanel = 'createNewStatus';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -60,8 +60,8 @@ export class ManageAccountComponent extends BrowseBase {
|
||||||
private readonly mastodonService: MastodonWrapperService,
|
private readonly mastodonService: MastodonWrapperService,
|
||||||
private readonly notificationService: NotificationService,
|
private readonly notificationService: NotificationService,
|
||||||
private readonly userNotificationService: UserNotificationService) {
|
private readonly userNotificationService: UserNotificationService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
}
|
}
|
||||||
|
@ -71,13 +71,9 @@ export class ManageAccountComponent extends BrowseBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkIfBookmarksAreAvailable() {
|
private checkIfBookmarksAreAvailable() {
|
||||||
this.toolsService.getInstanceInfo(this.account.info)
|
this.toolsService.isBookmarksAreAvailable(this.account.info)
|
||||||
.then((instance: InstanceInfo) => {
|
.then((isAvailable: boolean) => {
|
||||||
if (instance.major == 3 && instance.minor >= 1 || instance.major > 3) {
|
this.isBookmarksAvailable = isAvailable;
|
||||||
this.isBookmarksAvailable = true;
|
|
||||||
} else {
|
|
||||||
this.isBookmarksAvailable = false;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
this.isBookmarksAvailable = false;
|
this.isBookmarksAvailable = false;
|
||||||
|
@ -128,15 +124,15 @@ export class ManageAccountComponent extends BrowseBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewChild('bookmarks') bookmarksComp:BookmarksComponent;
|
@ViewChild('bookmarks') bookmarksComp: BookmarksComponent;
|
||||||
@ViewChild('notifications') notificationsComp:NotificationsComponent;
|
@ViewChild('notifications') notificationsComp: NotificationsComponent;
|
||||||
@ViewChild('mentions') mentionsComp:MentionsComponent;
|
@ViewChild('mentions') mentionsComp: MentionsComponent;
|
||||||
@ViewChild('dm') dmComp:DirectMessagesComponent;
|
@ViewChild('dm') dmComp: DirectMessagesComponent;
|
||||||
@ViewChild('favorites') favoritesComp:FavoritesComponent;
|
@ViewChild('favorites') favoritesComp: FavoritesComponent;
|
||||||
|
|
||||||
loadSubPanel(subpanel: 'account' | 'notifications' | 'mentions' | 'dm' | 'favorites' | 'bookmarks'): boolean {
|
loadSubPanel(subpanel: 'account' | 'notifications' | 'mentions' | 'dm' | 'favorites' | 'bookmarks'): boolean {
|
||||||
if(this.subPanel === subpanel){
|
if (this.subPanel === subpanel) {
|
||||||
switch(subpanel){
|
switch (subpanel) {
|
||||||
case 'bookmarks':
|
case 'bookmarks':
|
||||||
this.bookmarksComp.applyGoToTop();
|
this.bookmarksComp.applyGoToTop();
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -82,7 +82,7 @@ export class MentionsComponent extends TimelineBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getNextStatuses(): Promise<Status[]> {
|
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[]) => {
|
.then((result: Notification[]) => {
|
||||||
const statuses = result.map(x => x.status);
|
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>
|
<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()">
|
<a class="my-account__link my-account__red" href (click)="removeAccount()">
|
||||||
Delete
|
Remove
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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 => {
|
.catch(err => {
|
||||||
this.notificationService.notifyHttpError(err, this.account.info);
|
this.notificationService.notifyHttpError(err, this.account.info);
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
(click)="acceptFollowRequest()">
|
(click)="acceptFollowRequest()">
|
||||||
<fa-icon class="follow_request__icon" [icon]="faCheck"></fa-icon>
|
<fa-icon class="follow_request__icon" [icon]="faCheck"></fa-icon>
|
||||||
</a>
|
</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()">
|
(click)="refuseFollowRequest()">
|
||||||
<fa-icon class="follow_request__icon" [icon]="faTimes"></fa-icon>
|
<fa-icon class="follow_request__icon" [icon]="faTimes"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
|
@ -69,12 +69,30 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-status *ngIf="notification.status && notification.type !== 'mention'" class="stream__status" [statusWrapper]="notification.status"
|
<app-status *ngIf="notification.status && notification.type === 'update'" class="stream__status"
|
||||||
[notificationAccount]="notification.account" [notificationType]="notification.type"
|
[statusWrapper]="notification.status"
|
||||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
[notificationAccount]="notification.account"
|
||||||
|
[notificationType]="notification.type"
|
||||||
|
[context]="'notifications'"
|
||||||
|
(browseAccountEvent)="browseAccount($event)"
|
||||||
|
(browseHashtagEvent)="browseHashtag($event)"
|
||||||
(browseThreadEvent)="browseThread($event)"></app-status>
|
(browseThreadEvent)="browseThread($event)"></app-status>
|
||||||
|
|
||||||
<app-status *ngIf="notification.status && notification.type === 'mention'" class="stream__status" [statusWrapper]="notification.status"
|
<app-status *ngIf="notification.status && notification.type === 'mention'" class="stream__status"
|
||||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
[statusWrapper]="notification.status"
|
||||||
|
[context]="'notifications'"
|
||||||
|
(browseAccountEvent)="browseAccount($event)"
|
||||||
|
(browseHashtagEvent)="browseHashtag($event)"
|
||||||
(browseThreadEvent)="browseThread($event)"></app-status>
|
(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>
|
</div>
|
|
@ -152,6 +152,7 @@ export class NotificationWrapper {
|
||||||
case 'reblog':
|
case 'reblog':
|
||||||
case 'favourite':
|
case 'favourite':
|
||||||
case 'poll':
|
case 'poll':
|
||||||
|
case 'update':
|
||||||
this.status = new StatusWrapper(notification.status, provider, applyCw, hideStatus);
|
this.status = new StatusWrapper(notification.status, provider, applyCw, hideStatus);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -168,5 +169,5 @@ export class NotificationWrapper {
|
||||||
account: Account;
|
account: Account;
|
||||||
target: Account;
|
target: Account;
|
||||||
status: StatusWrapper;
|
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>
|
<h3 class="panel__title">search</h3>
|
||||||
|
|
||||||
<form class="form-section" (ngSubmit)="onSubmit()">
|
<form class="form-section" (ngSubmit)="onSubmit()">
|
||||||
<input type="text" class="form-control form-control-sm form-with-button" [(ngModel)]="searchHandle"
|
<input #search type="text" class="form-control form-control-sm form-with-button" [(ngModel)]="searchHandle"
|
||||||
name="searchHandle" placeholder="Search" autocomplete="off" />
|
name="searchHandle" placeholder="Search" autocomplete="off" (keydown.escape)="search.blur()"/>
|
||||||
<button class="form-button" type="submit" title="search">GO</button>
|
<button class="form-button" type="submit" title="search">GO</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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 { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
|
||||||
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
|
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
|
||||||
|
@ -26,12 +26,15 @@ export class SearchComponent implements OnInit {
|
||||||
@Output() browseHashtagEvent = new EventEmitter<string>();
|
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||||
|
|
||||||
|
@ViewChild('search') searchElement: ElementRef;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly notificationService: NotificationService,
|
private readonly notificationService: NotificationService,
|
||||||
private readonly toolsService: ToolsService,
|
private readonly toolsService: ToolsService,
|
||||||
private readonly mastodonService: MastodonWrapperService) { }
|
private readonly mastodonService: MastodonWrapperService) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.searchElement.nativeElement.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit(): boolean {
|
onSubmit(): boolean {
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
</form>
|
</form>
|
||||||
<a href class="form-button sound__play" type="submit" (click)="playNotificationSound()">play</a>
|
<a href class="form-button sound__play" type="submit" (click)="playNotificationSound()">play</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 class="panel__subtitle">Shortcuts</h4>
|
<h4 class="panel__subtitle">Shortcuts</h4>
|
||||||
<div class="sub-section">
|
<div class="sub-section">
|
||||||
<span class="sub-section__title">switch column:</span><br />
|
<span class="sub-section__title">switch column:</span><br />
|
||||||
|
@ -51,21 +51,50 @@
|
||||||
<br>
|
<br>
|
||||||
</div>
|
</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>
|
<h4 class="panel__subtitle">Twitter Bridge</h4>
|
||||||
<div class="sub-section">
|
<div class="sub-section">
|
||||||
<input class="sub-section__checkbox" [(ngModel)]="twitterBridgeEnabled"
|
<input class="sub-section__checkbox" [(ngModel)]="twitterBridgeEnabled"
|
||||||
(change)="onTwitterBridgeEnabledChanged()" type="checkbox" name="onTwitterBridgeEnabled"
|
(change)="onTwitterBridgeEnabledChanged()" type="checkbox" name="onTwitterBridgeEnabled"
|
||||||
value="onTwitterBridgeEnabled" id="onTwitterBridgeEnabled">
|
value="onTwitterBridgeEnabled" id="onTwitterBridgeEnabled">
|
||||||
<label class="noselect sub-section__label" for="onTwitterBridgeEnabled">enable bridge</label>
|
<label class="noselect sub-section__label" for="onTwitterBridgeEnabled">enable bridge</label>
|
||||||
<br>
|
<br>
|
||||||
<div *ngIf="twitterBridgeEnabled">
|
<div *ngIf="twitterBridgeEnabled">
|
||||||
<p>Please provide your bridge instance:
|
<p>Please provide your bridge instance:
|
||||||
<input type="text" class="form-control form-control-sm sub_section__text-input"
|
<input type="text" class="form-control form-control-sm sub_section__text-input"
|
||||||
[(ngModel)]="setTwitterBridgeInstance" placeholder="bridge.tld" />
|
[(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>
|
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>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -79,7 +108,7 @@
|
||||||
|
|
||||||
<input class="sub-section__checkbox" [checked]="contentWarningPolicy === 2" (change)="onCwPolicyChange(2)"
|
<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">
|
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>
|
<br>
|
||||||
<div class="sub-section__cw-settings" *ngIf="contentWarningPolicy === 2">
|
<div class="sub-section__cw-settings" *ngIf="contentWarningPolicy === 2">
|
||||||
<span class="sub-section__title">but add CW on content containing:</span><br />
|
<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>
|
<label class="noselect sub-section__label" for="timelineheader-5">Title</label>
|
||||||
<br>
|
<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 />
|
<span class="sub-section__title">loading behavior:</span><br />
|
||||||
|
|
||||||
<input class="sub-section__checkbox" [checked]="timeLineMode === 1" (change)="onTimeLineModeChange(1)"
|
<input class="sub-section__checkbox" [checked]="timeLineMode === 1" (change)="onTimeLineModeChange(1)"
|
||||||
|
@ -160,7 +195,8 @@
|
||||||
<input class="sub-section__checkbox" [(ngModel)]="autoFollowOnListEnabled"
|
<input class="sub-section__checkbox" [(ngModel)]="autoFollowOnListEnabled"
|
||||||
(change)="onAutoFollowOnListChanged()" type="checkbox" name="onAutoFollowOnListChanged"
|
(change)="onAutoFollowOnListChanged()" type="checkbox" name="onAutoFollowOnListChanged"
|
||||||
value="onAutoFollowOnListChanged" id="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>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -173,6 +209,20 @@
|
||||||
<label class="noselect sub-section__label" for="disableRemoteFetching">disable remote status
|
<label class="noselect sub-section__label" for="disableRemoteFetching">disable remote status
|
||||||
fetching</label>
|
fetching</label>
|
||||||
<br>
|
<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>
|
</div>
|
||||||
|
|
||||||
<h4 class="panel__subtitle">About</h4>
|
<h4 class="panel__subtitle">About</h4>
|
||||||
|
|
|
@ -31,6 +31,13 @@
|
||||||
padding: 0 5px 15px 5px;
|
padding: 0 5px 15px 5px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: block;
|
||||||
|
padding: 0 0 0 5px;
|
||||||
|
|
||||||
|
// outline: 1px dotted greenyellow;
|
||||||
|
}
|
||||||
|
|
||||||
&__checkbox {
|
&__checkbox {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 3px;
|
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 {
|
.form-control {
|
||||||
border: 1px solid $settings-text-input-border;
|
border: 1px solid $settings-text-input-border;
|
||||||
color: $settings-text-input-foreground;
|
color: $settings-text-input-foreground;
|
||||||
|
@ -111,4 +153,22 @@
|
||||||
background-color: #32384d;
|
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 { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
import { Howl } from 'howler';
|
import { Howl } from 'howler';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { ToolsService, InstanceType } from '../../../services/tools.service';
|
import { ToolsService, InstanceType } from '../../../services/tools.service';
|
||||||
import { UserNotificationService, NotificationSoundDefinition } from '../../../services/user-notification.service';
|
import { UserNotificationService, NotificationSoundDefinition } from '../../../services/user-notification.service';
|
||||||
import { ServiceWorkerService } from '../../../services/service-worker.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 { NotificationService } from '../../../services/notification.service';
|
||||||
import { NavigationService } from '../../../services/navigation.service';
|
import { NavigationService } from '../../../services/navigation.service';
|
||||||
import { SettingsService } from '../../../services/settings.service';
|
import { SettingsService } from '../../../services/settings.service';
|
||||||
|
import { LanguageService } from '../../../services/language.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-settings',
|
selector: 'app-settings',
|
||||||
|
@ -17,7 +19,7 @@ import { SettingsService } from '../../../services/settings.service';
|
||||||
styleUrls: ['./settings.component.scss']
|
styleUrls: ['./settings.component.scss']
|
||||||
})
|
})
|
||||||
|
|
||||||
export class SettingsComponent implements OnInit {
|
export class SettingsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
notificationSounds: NotificationSoundDefinition[];
|
notificationSounds: NotificationSoundDefinition[];
|
||||||
notificationSoundId: string;
|
notificationSoundId: string;
|
||||||
|
@ -27,6 +29,9 @@ export class SettingsComponent implements OnInit {
|
||||||
disableRemoteStatusFetchingEnabled: boolean;
|
disableRemoteStatusFetchingEnabled: boolean;
|
||||||
disableAvatarNotificationsEnabled: boolean;
|
disableAvatarNotificationsEnabled: boolean;
|
||||||
disableSoundsEnabled: boolean;
|
disableSoundsEnabled: boolean;
|
||||||
|
disableLangAutodetectEnabled: boolean;
|
||||||
|
enableAltLabelEnabled: boolean;
|
||||||
|
enableFreezeAvatarEnabled: boolean;
|
||||||
version: string;
|
version: string;
|
||||||
|
|
||||||
hasPleromaAccount: boolean;
|
hasPleromaAccount: boolean;
|
||||||
|
@ -39,6 +44,10 @@ export class SettingsComponent implements OnInit {
|
||||||
timeLineMode: TimeLineModeEnum = TimeLineModeEnum.OnTop;
|
timeLineMode: TimeLineModeEnum = TimeLineModeEnum.OnTop;
|
||||||
contentWarningPolicy: ContentWarningPolicyEnum = ContentWarningPolicyEnum.None;
|
contentWarningPolicy: ContentWarningPolicyEnum = ContentWarningPolicyEnum.None;
|
||||||
|
|
||||||
|
configuredLangs: ILanguage[] = [];
|
||||||
|
searchedLangs: ILanguage[] = [];
|
||||||
|
searchLang: string;
|
||||||
|
|
||||||
private addCwOnContent: string;
|
private addCwOnContent: string;
|
||||||
set setAddCwOnContent(value: string) {
|
set setAddCwOnContent(value: string) {
|
||||||
this.setCwPolicy(null, value, null, null);
|
this.setCwPolicy(null, value, null, null);
|
||||||
|
@ -76,16 +85,25 @@ export class SettingsComponent implements OnInit {
|
||||||
return this.twitterBridgeInstance;
|
return this.twitterBridgeInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private languageSub: Subscription;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly languageService: LanguageService,
|
||||||
private readonly settingsService: SettingsService,
|
private readonly settingsService: SettingsService,
|
||||||
private readonly navigationService: NavigationService,
|
private readonly navigationService: NavigationService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private serviceWorkersService: ServiceWorkerService,
|
private serviceWorkersService: ServiceWorkerService,
|
||||||
private readonly toolsService: ToolsService,
|
private readonly toolsService: ToolsService,
|
||||||
private readonly notificationService: NotificationService,
|
private readonly notificationService: NotificationService,
|
||||||
private readonly userNotificationsService: UserNotificationService) { }
|
private readonly userNotificationsService: UserNotificationService) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.languageSub = this.languageService.configuredLanguagesChanged.subscribe(l => {
|
||||||
|
if(l){
|
||||||
|
this.configuredLangs = l;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.version = environment.VERSION;
|
this.version = environment.VERSION;
|
||||||
|
|
||||||
const settings = this.settingsService.getSettings();
|
const settings = this.settingsService.getSettings();
|
||||||
|
@ -129,6 +147,44 @@ export class SettingsComponent implements OnInit {
|
||||||
|
|
||||||
this.twitterBridgeEnabled = settings.twitterBridgeEnabled;
|
this.twitterBridgeEnabled = settings.twitterBridgeEnabled;
|
||||||
this.twitterBridgeInstance = settings.twitterBridgeInstance;
|
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) {
|
onShortcutChange(id: ColumnShortcut) {
|
||||||
|
@ -230,6 +286,27 @@ export class SettingsComponent implements OnInit {
|
||||||
return false;
|
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() {
|
onDisableAutofocusChanged() {
|
||||||
this.notifyRestartNeeded();
|
this.notifyRestartNeeded();
|
||||||
let settings = this.settingsService.getSettings();
|
let settings = this.settingsService.getSettings();
|
||||||
|
|
|
@ -8,27 +8,36 @@
|
||||||
<fa-icon [icon]="faSearch"></fa-icon>
|
<fa-icon [icon]="faSearch"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div *ngFor="let account of accounts">
|
<div *ngIf="!iconMenuIsDraggable">
|
||||||
<app-account-icon [account]="account" (toogleAccountNotify)="onToogleAccountNotify($event)"
|
<div *ngFor="let account of accounts">
|
||||||
(openMenuNotify)="onOpenMenuNotify($event)">
|
<app-account-icon [account]="account" (toogleAccountNotify)="onToogleAccountNotify($event)"
|
||||||
</app-account-icon>
|
(openMenuNotify)="onOpenMenuNotify($event)">
|
||||||
|
</app-account-icon>
|
||||||
|
</div>
|
||||||
</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 }"
|
<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()">
|
href title="add new account" (click)="addNewAccount()" (contextmenu)="addNewAccount()">
|
||||||
<fa-icon [icon]="faPlus"></fa-icon>
|
<fa-icon [icon]="faPlus"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
<a class="left-bar-button left-bar-button--scheduled left-bar-button--bottom left-bar-link" href title="scheduled statuses"
|
<a class="left-bar-button left-bar-button--scheduled left-bar-button--bottom left-bar-link" href
|
||||||
*ngIf="hasAccounts && hasScheduledStatuses"
|
title="scheduled statuses" *ngIf="hasAccounts && hasScheduledStatuses" (click)="openScheduledStatuses()"
|
||||||
(click)="openScheduledStatuses()"
|
|
||||||
(contextmenu)="openScheduledStatuses()">
|
(contextmenu)="openScheduledStatuses()">
|
||||||
<fa-icon [icon]="faCalendarAlt"></fa-icon>
|
<fa-icon [icon]="faCalendarAlt"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="left-bar-button left-bar-button--cog left-bar-button--bottom left-bar-link" href title="settings" (click)="openSettings()"
|
<a class="left-bar-button left-bar-button--cog left-bar-button--bottom left-bar-link" href title="settings"
|
||||||
(contextmenu)="openSettings()">
|
(click)="openSettings()" (contextmenu)="openSettings()">
|
||||||
<fa-icon [icon]="faCog"></fa-icon>
|
<fa-icon [icon]="faCog"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
|
@ -82,4 +82,38 @@ $height-button: 40px;
|
||||||
.no-accounts {
|
.no-accounts {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
// color: cornflowerblue;
|
// 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 { Component, OnInit, OnDestroy } from "@angular/core";
|
||||||
|
import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop";
|
||||||
import { Subscription, Observable } from "rxjs";
|
import { Subscription, Observable } from "rxjs";
|
||||||
import { Store } from "@ngxs/store";
|
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 { faCommentAlt, faCalendarAlt } from "@fortawesome/free-regular-svg-icons";
|
||||||
import { HotkeysService, Hotkey } from 'angular2-hotkeys';
|
import { HotkeysService, Hotkey } from 'angular2-hotkeys';
|
||||||
|
|
||||||
import { AccountWrapper } from "../../models/account.models";
|
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 { NavigationService, LeftPanelType } from "../../services/navigation.service";
|
||||||
import { UserNotificationService, UserNotification } from '../../services/user-notification.service';
|
import { UserNotificationService, UserNotification } from '../../services/user-notification.service';
|
||||||
import { ToolsService } from '../../services/tools.service';
|
import { ToolsService } from '../../services/tools.service';
|
||||||
|
@ -24,6 +25,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
||||||
faPlus = faPlus;
|
faPlus = faPlus;
|
||||||
faCog = faCog;
|
faCog = faCog;
|
||||||
faCalendarAlt = faCalendarAlt;
|
faCalendarAlt = faCalendarAlt;
|
||||||
|
faArrowsAltV = faArrowsAltV;
|
||||||
|
|
||||||
accounts: AccountWithNotificationWrapper[] = [];
|
accounts: AccountWithNotificationWrapper[] = [];
|
||||||
hasAccounts: boolean;
|
hasAccounts: boolean;
|
||||||
|
@ -33,6 +35,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
||||||
private accountSub: Subscription;
|
private accountSub: Subscription;
|
||||||
private scheduledSub: Subscription;
|
private scheduledSub: Subscription;
|
||||||
private notificationSub: Subscription;
|
private notificationSub: Subscription;
|
||||||
|
private draggableIconMenuSub: Subscription;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly settingsService: SettingsService,
|
private readonly settingsService: SettingsService,
|
||||||
|
@ -103,7 +106,13 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
iconMenuIsDraggable = false;
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.draggableIconMenuSub = this.navigationService.enableDraggableIconMenu.subscribe(x => {
|
||||||
|
this.iconMenuIsDraggable = x;
|
||||||
|
});
|
||||||
|
|
||||||
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
|
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
|
||||||
if (accounts) {
|
if (accounts) {
|
||||||
//Update and Add
|
//Update and Add
|
||||||
|
@ -164,6 +173,17 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
||||||
this.accountSub.unsubscribe();
|
this.accountSub.unsubscribe();
|
||||||
this.notificationSub.unsubscribe();
|
this.notificationSub.unsubscribe();
|
||||||
this.scheduledSub.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) {
|
onToogleAccountNotify(acc: AccountWrapper) {
|
||||||
|
|
|
@ -2,15 +2,16 @@
|
||||||
<div class="hashtag-header">
|
<div class="hashtag-header">
|
||||||
<a href (click)="goToTop()" class="hashtag-header__gototop" title="go to top">
|
<a href (click)="goToTop()" class="hashtag-header__gototop" title="go to top">
|
||||||
<h3 class="hashtag-header__title">#{{hashtagElement.tag}}</h3>
|
<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>
|
<button class="btn-custom-secondary hashtag-header__add-column" (click)="addColumn($event)" title="add column to board" [hidden]="columnAdded">add column</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<app-stream-statuses #appStreamStatuses class="hashtag-stream" *ngIf="hashtagElement"
|
<app-stream-statuses #appStreamStatuses class="hashtag-stream" *ngIf="hashtagElement"
|
||||||
[streamElement]="hashtagElement"
|
[streamElement]="hashtagElement"
|
||||||
[goToTop]="goToTopSubject.asObservable()"
|
[goToTop]="goToTopSubject.asObservable()"
|
||||||
[userLocked]="false"
|
[userLocked]="false"
|
||||||
(browseAccountEvent)="browseAccount($event)"
|
(browseAccountEvent)="browseAccount($event)"
|
||||||
(browseHashtagEvent)="browseHashtag($event)"
|
(browseHashtagEvent)="browseHashtag($event)"
|
||||||
(browseThreadEvent)="browseThread($event)"></app-stream-statuses>
|
(browseThreadEvent)="browseThread($event)"></app-stream-statuses>
|
||||||
</div>
|
</div>
|
|
@ -40,6 +40,14 @@ $inner-column-size: 320px;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
&__follow-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 7px;
|
||||||
|
right: 114px;
|
||||||
|
padding: 0 10px 0 10px;
|
||||||
|
border: 1px solid black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hashtag-stream {
|
.hashtag-stream {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { Component, OnInit, Output, EventEmitter, Input, ViewChild, OnDestroy } from '@angular/core';
|
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 { Store } from '@ngxs/store';
|
||||||
|
|
||||||
import { StreamElement, StreamTypeEnum, AddStream } from '../../../states/streams.state';
|
import { StreamElement, StreamTypeEnum, AddStream } from '../../../states/streams.state';
|
||||||
import { OpenThreadEvent, ToolsService } from '../../../services/tools.service';
|
import { OpenThreadEvent, ToolsService } from '../../../services/tools.service';
|
||||||
import { StreamStatusesComponent } from '../stream-statuses/stream-statuses.component';
|
import { StreamStatusesComponent } from '../stream-statuses/stream-statuses.component';
|
||||||
import { AccountInfo } from '../../../states/accounts.state';
|
import { AccountInfo } from '../../../states/accounts.state';
|
||||||
|
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-hashtag',
|
selector: 'app-hashtag',
|
||||||
|
@ -21,7 +22,7 @@ export class HashtagComponent implements OnInit, OnDestroy {
|
||||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||||
|
|
||||||
private _hashtagElement: StreamElement;
|
private _hashtagElement: StreamElement;
|
||||||
@Input()
|
@Input()
|
||||||
set hashtagElement(hashtagElement: StreamElement){
|
set hashtagElement(hashtagElement: StreamElement){
|
||||||
this._hashtagElement = hashtagElement;
|
this._hashtagElement = hashtagElement;
|
||||||
this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0];
|
this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0];
|
||||||
|
@ -29,7 +30,7 @@ export class HashtagComponent implements OnInit, OnDestroy {
|
||||||
get hashtagElement(): StreamElement{
|
get hashtagElement(): StreamElement{
|
||||||
return this._hashtagElement;
|
return this._hashtagElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ViewChild('appStreamStatuses') appStreamStatuses: StreamStatusesComponent;
|
@ViewChild('appStreamStatuses') appStreamStatuses: StreamStatusesComponent;
|
||||||
|
|
||||||
|
@ -38,12 +39,25 @@ export class HashtagComponent implements OnInit, OnDestroy {
|
||||||
private lastUsedAccount: AccountInfo;
|
private lastUsedAccount: AccountInfo;
|
||||||
private refreshSubscription: Subscription;
|
private refreshSubscription: Subscription;
|
||||||
private goToTopSubscription: Subscription;
|
private goToTopSubscription: Subscription;
|
||||||
|
isHashtagFollowingAvailable: boolean;
|
||||||
|
isFollowingHashtag: boolean;
|
||||||
|
|
||||||
|
private accounts$: Observable<AccountInfo[]>;
|
||||||
|
|
||||||
|
private accountSub: Subscription;
|
||||||
|
|
||||||
|
followingLoading: boolean;
|
||||||
|
unfollowingLoading: boolean;
|
||||||
|
|
||||||
columnAdded: boolean;
|
columnAdded: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly store: Store,
|
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() {
|
ngOnInit() {
|
||||||
if(this.refreshEventEmitter) {
|
if(this.refreshEventEmitter) {
|
||||||
|
@ -57,11 +71,22 @@ export class HashtagComponent implements OnInit, OnDestroy {
|
||||||
this.goToTop();
|
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 {
|
ngOnDestroy(): void {
|
||||||
if(this.refreshSubscription) this.refreshSubscription.unsubscribe();
|
if(this.refreshSubscription) this.refreshSubscription.unsubscribe();
|
||||||
if (this.goToTopSubscription) this.goToTopSubscription.unsubscribe();
|
if (this.goToTopSubscription) this.goToTopSubscription.unsubscribe();
|
||||||
|
if (this.accountSub) this.accountSub.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
goToTop(): boolean {
|
goToTop(): boolean {
|
||||||
|
@ -83,6 +108,10 @@ export class HashtagComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
refresh(): any {
|
refresh(): any {
|
||||||
this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0];
|
this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0];
|
||||||
|
this.updateHashtagFollowStatus(this.lastUsedAccount);
|
||||||
|
if (this.isHashtagFollowingAvailable) {
|
||||||
|
this.checkIfFollowingHashtag(this.lastUsedAccount);
|
||||||
|
}
|
||||||
this.appStreamStatuses.refresh();
|
this.appStreamStatuses.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,4 +128,41 @@ export class HashtagComponent implements OnInit, OnDestroy {
|
||||||
browseThread(openThreadEvent: OpenThreadEvent): void {
|
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||||
this.browseThreadEvent.next(openThreadEvent);
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -342,13 +342,9 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkIfBookmarksAreAvailable(account: AccountInfo) {
|
private checkIfBookmarksAreAvailable(account: AccountInfo) {
|
||||||
this.toolsService.getInstanceInfo(account)
|
this.toolsService.isBookmarksAreAvailable(account)
|
||||||
.then((instance: InstanceInfo) => {
|
.then((isAvailable: boolean) => {
|
||||||
if (instance.major == 3 && instance.minor >= 1 || instance.major > 3) {
|
this.isBookmarksAvailable = isAvailable;
|
||||||
this.isBookmarksAvailable = true;
|
|
||||||
} else {
|
|
||||||
this.isBookmarksAvailable = false;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
this.isBookmarksAvailable = false;
|
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__status]="statusWrapper"
|
||||||
[class.context-menu-link__profile]="displayedAccount"
|
[class.context-menu-link__profile]="displayedAccount"
|
||||||
title="More">
|
title="More">
|
||||||
|
@ -27,19 +27,42 @@
|
||||||
<ng-template contextMenuItem (execute)="unmuteConversation()" *ngIf="statusWrapper && isOwnerSelected && displayedStatus.muted">
|
<ng-template contextMenuItem (execute)="unmuteConversation()" *ngIf="statusWrapper && isOwnerSelected && displayedStatus.muted">
|
||||||
Unmute conversation
|
Unmute conversation
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template contextMenuItem divider="true"></ng-template>
|
<ng-template contextMenuItem (execute)="hideBoosts()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.following && this.relationship.showing_reblogs">
|
||||||
<ng-template contextMenuItem (execute)="muteAccount()" *ngIf="!isOwnerSelected">
|
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 }}
|
Mute @{{ this.username }}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template contextMenuItem (execute)="blockAccount()" *ngIf="!isOwnerSelected">
|
<ng-template contextMenuItem (execute)="unmuteAccount()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.muting">
|
||||||
Block @{{ this.username }}
|
Unmute @{{ this.username }}
|
||||||
</ng-template>
|
</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'">
|
<ng-template contextMenuItem (execute)="pinOnProfile()" *ngIf="statusWrapper && isOwnerSelected && !displayedStatus.pinned && displayedStatus.visibility === 'public'">
|
||||||
Pin on profile
|
Pin on profile
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template contextMenuItem (execute)="unpinFromProfile()" *ngIf="statusWrapper && isOwnerSelected && displayedStatus.pinned && displayedStatus.visibility === 'public'">
|
<ng-template contextMenuItem (execute)="unpinFromProfile()" *ngIf="statusWrapper && isOwnerSelected && displayedStatus.pinned && displayedStatus.visibility === 'public'">
|
||||||
Unpin from profile
|
Unpin from profile
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
<ng-template contextMenuItem (execute)="edit()" *ngIf="statusWrapper && isOwnerSelected && isEditingAvailable">
|
||||||
|
Edit
|
||||||
|
</ng-template>
|
||||||
<ng-template contextMenuItem (execute)="delete(false)" *ngIf="statusWrapper && isOwnerSelected">
|
<ng-template contextMenuItem (execute)="delete(false)" *ngIf="statusWrapper && isOwnerSelected">
|
||||||
Delete
|
Delete
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { ContextMenuComponent, ContextMenuService } from 'ngx-contextmenu';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { Store } from '@ngxs/store';
|
import { Store } from '@ngxs/store';
|
||||||
|
|
||||||
import { Status, Account, Results } from '../../../../../services/models/mastodon.interfaces';
|
import { Status, Account, Results, Relationship } from '../../../../../services/models/mastodon.interfaces';
|
||||||
import { ToolsService, OpenThreadEvent } from '../../../../../services/tools.service';
|
import { ToolsService, OpenThreadEvent, InstanceInfo } from '../../../../../services/tools.service';
|
||||||
import { StatusWrapper } from '../../../../../models/common.model';
|
import { StatusWrapper } from '../../../../../models/common.model';
|
||||||
import { NavigationService } from '../../../../../services/navigation.service';
|
import { NavigationService } from '../../../../../services/navigation.service';
|
||||||
import { AccountInfo } from '../../../../../states/accounts.state';
|
import { AccountInfo } from '../../../../../states/accounts.state';
|
||||||
|
@ -25,12 +25,17 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
|
||||||
private loadedAccounts: AccountInfo[];
|
private loadedAccounts: AccountInfo[];
|
||||||
displayedStatus: Status;
|
displayedStatus: Status;
|
||||||
username: string;
|
username: string;
|
||||||
|
domain: string;
|
||||||
isOwnerSelected: boolean;
|
isOwnerSelected: boolean;
|
||||||
|
|
||||||
|
isEditingAvailable: boolean;
|
||||||
|
|
||||||
@Input() statusWrapper: StatusWrapper;
|
@Input() statusWrapper: StatusWrapper;
|
||||||
@Input() displayedAccount: Account;
|
@Input() displayedAccount: Account;
|
||||||
|
@Input() relationship: Relationship;
|
||||||
|
|
||||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||||
|
@Output() relationshipChanged = new EventEmitter<Relationship>();
|
||||||
|
|
||||||
@ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent;
|
@ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent;
|
||||||
|
|
||||||
|
@ -70,6 +75,7 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.username = account.acct.split('@')[0];
|
this.username = account.acct.split('@')[0];
|
||||||
|
this.domain = account.acct.split('@')[1];
|
||||||
this.fullHandle = this.toolsService.getAccountFullHandle(account);
|
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()
|
this.isOwnerSelected = selectedAccount.username.toLowerCase() === this.displayedStatus.account.username.toLowerCase()
|
||||||
&& selectedAccount.instance.toLowerCase() === this.displayedStatus.account.url.replace('https://', '').split('/')[0].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;
|
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 {
|
muteAccount(): boolean {
|
||||||
this.loadedAccounts.forEach(acc => {
|
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||||
this.toolsService.findAccount(acc, this.fullHandle)
|
|
||||||
.then((target: Account) => {
|
this.toolsService.findAccount(acc, this.fullHandle)
|
||||||
this.mastodonService.mute(acc, target.id);
|
.then(async (target: Account) => {
|
||||||
return target;
|
const relationship = await this.mastodonService.mute(acc, target.id);
|
||||||
})
|
this.relationship = relationship;
|
||||||
.then((target: Account) => {
|
this.relationshipChanged.next(relationship);
|
||||||
this.notificationService.hideAccount(target);
|
return target;
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.then((target: Account) => {
|
||||||
this.notificationService.notifyHttpError(err, acc);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockAccount(): boolean {
|
blockAccount(): boolean {
|
||||||
this.loadedAccounts.forEach(acc => {
|
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||||
this.toolsService.findAccount(acc, this.fullHandle)
|
|
||||||
.then((target: Account) => {
|
this.toolsService.findAccount(acc, this.fullHandle)
|
||||||
this.mastodonService.block(acc, target.id);
|
.then(async (target: Account) => {
|
||||||
return target;
|
const relationship = await this.mastodonService.block(acc, target.id);
|
||||||
})
|
this.relationship = relationship;
|
||||||
.then((target: Account) => {
|
this.relationshipChanged.next(relationship);
|
||||||
this.notificationService.hideAccount(target);
|
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 => {
|
.catch(err => {
|
||||||
this.notificationService.notifyHttpError(err, acc);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -282,6 +397,18 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
|
||||||
return false;
|
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> {
|
private getStatus(account: AccountInfo): Promise<Status> {
|
||||||
let statusPromise: Promise<Status> = Promise.resolve(this.statusWrapper.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">
|
<a href class="image__link" (click)="openExternal()" (auxclick)="openExternal()" title="open image">
|
||||||
<fa-icon class="image__link--icon" [icon]="faLink"></fa-icon>
|
<fa-icon class="image__link--icon" [icon]="faLink"></fa-icon>
|
||||||
</a>
|
</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 {
|
&:hover &__link {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
cursor: pointer;
|
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,
|
img,
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
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 { Attachment } from '../../../../../services/models/mastodon.interfaces';
|
||||||
|
import { StatusWrapper } from '../../../../../models/common.model';
|
||||||
|
import { OpenThreadEvent } from '../../../../../services/tools.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-attachement-image',
|
selector: 'app-attachement-image',
|
||||||
|
@ -10,11 +13,19 @@ import { Attachment } from '../../../../../services/models/mastodon.interfaces';
|
||||||
})
|
})
|
||||||
export class AttachementImageComponent implements OnInit {
|
export class AttachementImageComponent implements OnInit {
|
||||||
faLink = faLink;
|
faLink = faLink;
|
||||||
|
faExternalLinkAlt = faExternalLinkAlt;
|
||||||
|
displayAltLabel: boolean;
|
||||||
|
|
||||||
@Input() attachment: Attachment;
|
@Input() attachment: Attachment;
|
||||||
|
@Input() status: StatusWrapper;
|
||||||
@Output() openEvent = new EventEmitter();
|
@Output() openEvent = new EventEmitter();
|
||||||
|
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||||
|
|
||||||
constructor() { }
|
constructor(
|
||||||
|
private readonly settingsService: SettingsService
|
||||||
|
) {
|
||||||
|
this.displayAltLabel = this.settingsService.getSettings().enableAltLabel;
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
}
|
}
|
||||||
|
@ -28,4 +39,13 @@ export class AttachementImageComponent implements OnInit {
|
||||||
window.open(this.attachment.url, '_blank');
|
window.open(this.attachment.url, '_blank');
|
||||||
return false;
|
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')
|
@Input('attachments')
|
||||||
set attachments(value: Attachment[]) {
|
set attachments(value: Attachment[]) {
|
||||||
|
this.imageAttachments = [];
|
||||||
|
this.videoAttachments = [];
|
||||||
|
this.audioAttachments = [];
|
||||||
|
|
||||||
this._attachments = value;
|
this._attachments = value;
|
||||||
this.setAttachments(value);
|
this.setAttachments(value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,7 @@ $expand-color: $column-color;
|
||||||
|
|
||||||
& p {
|
& p {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
white-space: pre-wrap;
|
||||||
//font-size: .9em;
|
//font-size: .9em;
|
||||||
// font-size: 14px;
|
// 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>`;
|
const sample = `<p>Bla <a href="https://ubuntu.social/tags/kubecon" rel="tag">#<span>KubeCon</span></a> Bla</p>`;
|
||||||
|
|
||||||
component.text = sample;
|
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', () => {
|
it('should parse link - Pleroma', () => {
|
||||||
|
|
|
@ -97,7 +97,7 @@ export class DatabindedTextComponent implements OnInit {
|
||||||
let extractedUrl = extractedLinkAndNext[0].split('href="')[1].split('"')[0];
|
let extractedUrl = extractedLinkAndNext[0].split('href="')[1].split('"')[0];
|
||||||
|
|
||||||
let classname = this.getClassNameForHastag(extractedHashtag);
|
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];
|
if (extractedLinkAndNext[1]) this.processedText += extractedLinkAndNext[1];
|
||||||
this.hashtags.push(extractedHashtag);
|
this.hashtags.push(extractedHashtag);
|
||||||
}
|
}
|
||||||
|
@ -205,6 +205,10 @@ export class DatabindedTextComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
|
this.processEventBindings();
|
||||||
|
}
|
||||||
|
|
||||||
|
processEventBindings(){
|
||||||
for (const hashtag of this.hashtags) {
|
for (const hashtag of this.hashtags) {
|
||||||
let classname = this.getClassNameForHastag(hashtag);
|
let classname = this.getClassNameForHastag(hashtag);
|
||||||
let els = <Element[]>this.contentElement.nativeElement.querySelectorAll(`.${classname}`);
|
let els = <Element[]>this.contentElement.nativeElement.querySelectorAll(`.${classname}`);
|
||||||
|
|
|
@ -45,7 +45,15 @@ export class PollComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.options.length = 0;
|
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;
|
let i = 0;
|
||||||
for (let opt of this.poll.options) {
|
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);
|
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) {
|
if (totalVotes === 0) {
|
||||||
this.percentage = '0';
|
this.percentage = '0';
|
||||||
} else {
|
} else {
|
||||||
this.percentage = ((this.votes_count / votesDivider) * 100).toFixed(0);
|
this.percentage = ((this.votes_count / votesDivider) * 100).toFixed(0);
|
||||||
}
|
}
|
||||||
this.isMax = isMax;
|
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">
|
<div class="reblog" *ngIf="reblog">
|
||||||
<a class="reblog__profile-link" href title="{{ status.account.acct }}"
|
<a class="reblog__profile-link" href title="{{ status.account.acct }}"
|
||||||
(click)="openAccount(status.account)"
|
(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>
|
||||||
<div *ngIf="statusWrapper.status.pinned && !notificationType" class="pinned">
|
<div *ngIf="statusWrapper.status.pinned && !notificationType" class="pinned">
|
||||||
<div class="notification--icon">
|
<div class="notification--icon">
|
||||||
|
@ -34,6 +34,17 @@
|
||||||
boosted your status
|
boosted your status
|
||||||
</div>
|
</div>
|
||||||
</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 *ngIf="notificationType === 'poll'">
|
||||||
<div class="notification--icon">
|
<div class="notification--icon">
|
||||||
<fa-icon class="boost" [icon]="faList"></fa-icon>
|
<fa-icon class="boost" [icon]="faList"></fa-icon>
|
||||||
|
@ -49,9 +60,9 @@
|
||||||
<div [ngClass]="{'notification--status': notificationAccount }">
|
<div [ngClass]="{'notification--status': notificationAccount }">
|
||||||
<a href class="status__profile-link" title="{{displayedStatus.account.acct}}"
|
<a href class="status__profile-link" title="{{displayedStatus.account.acct}}"
|
||||||
(click)="openAccount(displayedStatus.account)" (auxclick)="openUrl(displayedStatus.account.url)">
|
(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="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">
|
||||||
<span class="status__name--displayname"
|
<span class="status__name--displayname"
|
||||||
innerHTML="{{displayedStatus.account | accountEmoji}}"></span><span
|
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">
|
<div class="status__labels--label status__labels--remote" title="this status isn't federated with this instance" *ngIf="isRemote">
|
||||||
remote
|
remote
|
||||||
</div>
|
</div>
|
||||||
|
<div class="status__labels--label status__labels--edited" title="this status was edited" *ngIf="statusWrapper.status.edited_at">
|
||||||
|
edited
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@ -95,10 +109,17 @@
|
||||||
<span class="status__content-warning--title">sensitive content</span>
|
<span class="status__content-warning--title">sensitive content</span>
|
||||||
<span innerHTML="{{ contentWarningText }}"></span>
|
<span innerHTML="{{ contentWarningText }}"></span>
|
||||||
</a>
|
</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)"
|
(accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)"
|
||||||
(textSelected)="textSelected()"></app-databinded-text>
|
(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"
|
<app-poll class="status__poll" *ngIf="!isContentWarned && displayedStatus.poll"
|
||||||
[poll]="displayedStatus.poll" [statusWrapper]="displayedStatusWrapper"></app-poll>
|
[poll]="displayedStatus.poll" [statusWrapper]="displayedStatusWrapper"></app-poll>
|
||||||
|
|
||||||
|
|
|
@ -105,6 +105,17 @@
|
||||||
background-color: rgb(33, 69, 136);
|
background-color: rgb(33, 69, 136);
|
||||||
background-color: rgb(38, 77, 148);
|
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 {
|
&__name {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -161,6 +172,26 @@
|
||||||
border: 3px solid $status-secondary-color;
|
border: 3px solid $status-secondary-color;
|
||||||
color: whitesmoke;
|
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 {
|
&--title {
|
||||||
color: $content-warning-font-color;
|
color: $content-warning-font-color;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
@ -247,6 +278,10 @@
|
||||||
color: $boost-color;
|
color: $boost-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.update {
|
||||||
|
color: $update-color;
|
||||||
|
}
|
||||||
|
|
||||||
.favorite {
|
.favorite {
|
||||||
color: $favorite-color;
|
color: $favorite-color;
|
||||||
}
|
}
|
||||||
|
@ -261,4 +296,4 @@
|
||||||
&__label{
|
&__label{
|
||||||
color: $status-secondary-color;
|
color: $status-secondary-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,13 +1,16 @@
|
||||||
import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef } from "@angular/core";
|
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 { OpenThreadEvent, ToolsService } from "../../../services/tools.service";
|
||||||
import { ActionBarComponent } from "./action-bar/action-bar.component";
|
import { ActionBarComponent } from "./action-bar/action-bar.component";
|
||||||
import { StatusWrapper } from '../../../models/common.model';
|
import { StatusWrapper } from '../../../models/common.model';
|
||||||
import { EmojiConverter, EmojiTypeEnum } from '../../../tools/emoji.tools';
|
import { EmojiConverter, EmojiTypeEnum } from '../../../tools/emoji.tools';
|
||||||
import { ContentWarningPolicyEnum } from '../../../states/settings.state';
|
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({
|
@Component({
|
||||||
selector: "app-status",
|
selector: "app-status",
|
||||||
|
@ -21,6 +24,7 @@ export class StatusComponent implements OnInit {
|
||||||
faRetweet = faRetweet;
|
faRetweet = faRetweet;
|
||||||
faList = faList;
|
faList = faList;
|
||||||
faThumbtack = faThumbtack;
|
faThumbtack = faThumbtack;
|
||||||
|
faEdit = faEdit;
|
||||||
|
|
||||||
displayedStatus: Status;
|
displayedStatus: Status;
|
||||||
displayedStatusWrapper: StatusWrapper;
|
displayedStatusWrapper: StatusWrapper;
|
||||||
|
@ -41,6 +45,8 @@ export class StatusComponent implements OnInit {
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isRemote: boolean;
|
isRemote: boolean;
|
||||||
|
|
||||||
|
private freezeAvatarEnabled: boolean;
|
||||||
|
|
||||||
hideStatus: boolean = false;
|
hideStatus: boolean = false;
|
||||||
|
|
||||||
@Output() browseAccountEvent = new EventEmitter<string>();
|
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||||
|
@ -50,12 +56,16 @@ export class StatusComponent implements OnInit {
|
||||||
|
|
||||||
@Input() isThreadDisplay: boolean;
|
@Input() isThreadDisplay: boolean;
|
||||||
|
|
||||||
@Input() notificationType: 'mention' | 'reblog' | 'favourite' | 'poll';
|
@Input() notificationType: 'mention' | 'reblog' | 'favourite' | 'poll' | 'update';
|
||||||
@Input() notificationAccount: Account;
|
@Input() notificationAccount: Account;
|
||||||
|
|
||||||
|
@Input() context: 'home' | 'notifications' | 'public' | 'thread' | 'account';
|
||||||
|
|
||||||
private _statusWrapper: StatusWrapper;
|
private _statusWrapper: StatusWrapper;
|
||||||
status: Status;
|
status: Status;
|
||||||
|
|
||||||
|
private statusesStateServiceSub: Subscription;
|
||||||
|
|
||||||
@Input('statusWrapper')
|
@Input('statusWrapper')
|
||||||
set statusWrapper(value: StatusWrapper) {
|
set statusWrapper(value: StatusWrapper) {
|
||||||
this._statusWrapper = value;
|
this._statusWrapper = value;
|
||||||
|
@ -90,6 +100,8 @@ export class StatusComponent implements OnInit {
|
||||||
// this.statusAccountName = this.emojiConverter.applyEmojis(this.displayedStatus.account.emojis, this.displayedStatus.account.display_name, EmojiTypeEnum.small);
|
// this.statusAccountName = this.emojiConverter.applyEmojis(this.displayedStatus.account.emojis, this.displayedStatus.account.display_name, EmojiTypeEnum.small);
|
||||||
let 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.statusContent = this.ensureMentionAreDisplayed(statusContent);
|
||||||
|
|
||||||
|
this.validateFilteringStatus();
|
||||||
}
|
}
|
||||||
get statusWrapper(): StatusWrapper {
|
get statusWrapper(): StatusWrapper {
|
||||||
return this._statusWrapper;
|
return this._statusWrapper;
|
||||||
|
@ -97,22 +109,73 @@ export class StatusComponent implements OnInit {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public elem: ElementRef,
|
public elem: ElementRef,
|
||||||
private readonly toolsService: ToolsService) { }
|
private readonly toolsService: ToolsService,
|
||||||
|
private readonly settingsService: SettingsService,
|
||||||
|
private readonly statusesStateService: StatusesStateService) { }
|
||||||
|
|
||||||
ngOnInit() {
|
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 {
|
private ensureMentionAreDisplayed(data: string): string {
|
||||||
const mentions = this.displayedStatus.mentions;
|
const mentions = this.displayedStatus.mentions;
|
||||||
if(!mentions || mentions.length === 0) return data;
|
if (!mentions || mentions.length === 0) return data;
|
||||||
|
|
||||||
let textMentions = '';
|
let textMentions = '';
|
||||||
for (const m of mentions) {
|
for (const m of mentions) {
|
||||||
if(!data.includes(m.url)){
|
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> `
|
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 !== ''){
|
if (textMentions !== '') {
|
||||||
data = textMentions + data;
|
data = textMentions + data;
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
|
@ -142,6 +205,31 @@ export class StatusComponent implements OnInit {
|
||||||
changeCw(cwIsActive: boolean) {
|
changeCw(cwIsActive: boolean) {
|
||||||
this.isContentWarned = cwIsActive;
|
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) {
|
private checkLabels(status: Status) {
|
||||||
//since API is limited with federated status...
|
//since API is limited with federated status...
|
||||||
|
|
|
@ -122,18 +122,21 @@ export class StreamNotificationsComponent extends BrowseBase {
|
||||||
loadNotifications(): any {
|
loadNotifications(): any {
|
||||||
this.account = this.toolsService.getAccountById(this.streamElement.accountId);
|
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.loadMentions(userNotifications);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.mastodonService.getNotifications(this.account, null, null, null, 10)
|
this.mastodonService.getNotifications(this.account, [], null, null, 10)
|
||||||
.then((notifications: Notification[]) => {
|
.then((notifications: Notification[]) => {
|
||||||
this.isNotificationsLoading = false;
|
this.isNotificationsLoading = false;
|
||||||
|
|
||||||
this.notifications = notifications.map(x => {
|
let wrappedNotification= notifications.map(x => {
|
||||||
let cwPolicy = this.toolsService.checkContentWarning(x.status);
|
let cwPolicy = this.toolsService.checkContentWarning(x.status);
|
||||||
return new NotificationWrapper(x, this.account, cwPolicy.applyCw, cwPolicy.hide);
|
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;
|
this.lastNotificationId = this.notifications[this.notifications.length - 1].notification.id;
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
@ -201,7 +204,7 @@ export class StreamNotificationsComponent extends BrowseBase {
|
||||||
|
|
||||||
this.isNotificationsLoading = true;
|
this.isNotificationsLoading = true;
|
||||||
|
|
||||||
this.mastodonService.getNotifications(this.account, null, this.lastNotificationId)
|
this.mastodonService.getNotifications(this.account, ['update'], this.lastNotificationId)
|
||||||
.then((result: Notification[]) => {
|
.then((result: Notification[]) => {
|
||||||
if (result.length === 0) {
|
if (result.length === 0) {
|
||||||
this.notificationsMaxReached = true;
|
this.notificationsMaxReached = true;
|
||||||
|
@ -235,7 +238,7 @@ export class StreamNotificationsComponent extends BrowseBase {
|
||||||
|
|
||||||
this.isMentionsLoading = true;
|
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[]) => {
|
.then((result: Notification[]) => {
|
||||||
if (result.length === 0) {
|
if (result.length === 0) {
|
||||||
this.mentionsMaxReached = true;
|
this.mentionsMaxReached = true;
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
<div class="overlay">
|
<div class="overlay">
|
||||||
<div class="overlay__header">
|
<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"
|
<a href class="overlay__button overlay-previous"
|
||||||
[ngClass]="{'overlay__button--focus': hasPreviousElements }" title="previous" (click)="previous()">
|
[ngClass]="{'overlay__button--focus': hasPreviousElements }" title="previous" (click)="previous()">
|
||||||
<fa-icon class="overlay-previous__icon" [icon]="faAngleLeft"></fa-icon>
|
<fa-icon class="overlay-previous__icon" [icon]="faAngleLeft"></fa-icon>
|
||||||
|
@ -12,13 +8,17 @@
|
||||||
title="refresh" (click)="refresh()">
|
title="refresh" (click)="refresh()">
|
||||||
<fa-icon class="overlay-refresh__icon" [icon]="faRedoAlt"></fa-icon>
|
<fa-icon class="overlay-refresh__icon" [icon]="faRedoAlt"></fa-icon>
|
||||||
</a>
|
</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 href title="return to top" class="overlay-gototop" (click)="goToTop()">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href class="overlay__button overlay-next" [ngClass]="{'overlay__button--focus': hasNextElements }"
|
<a href class="overlay__button overlay-close" title="close" (click)="close()">
|
||||||
title="next" (click)="next()">
|
<fa-icon class="overlay-close__icon" [icon]="faTimes"></fa-icon>
|
||||||
<fa-icon class="overlay-next__icon" [icon]="faAngleRight"></fa-icon>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,9 @@ $header-content-height: 40px;
|
||||||
width: calc(100%);
|
width: calc(100%);
|
||||||
height: $header-content-height;
|
height: $header-content-height;
|
||||||
background-color: $column-header-background-color;
|
background-color: $column-header-background-color;
|
||||||
border-bottom: 1px solid #222736;
|
border-bottom: 1px solid #222736;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
&__content-wrapper {
|
&__content-wrapper {
|
||||||
transition: all .2s;
|
transition: all .2s;
|
||||||
|
@ -44,11 +46,17 @@ $header-content-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__button {
|
&__button {
|
||||||
|
// outline: 1px dotted orange;
|
||||||
|
|
||||||
width: 25px;
|
width: 25px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
|
|
||||||
|
width: $header-content-height;
|
||||||
|
height: $header-content-height;
|
||||||
|
|
||||||
color: #354060;
|
color: #354060;
|
||||||
transition: all .2s;
|
transition: all .2s;
|
||||||
margin: 8px 0 0 8px;
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #536599;
|
color: #536599;
|
||||||
color: #7a8dc7;
|
color: #7a8dc7;
|
||||||
|
@ -68,19 +76,8 @@ $header-content-height: 40px;
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
position: relative;
|
position: relative;
|
||||||
left: 7px;
|
left: 17px;
|
||||||
top: -1px
|
top: 7px
|
||||||
}
|
|
||||||
}
|
|
||||||
&-next {
|
|
||||||
display: block;
|
|
||||||
float: left;
|
|
||||||
font-size: 18px;
|
|
||||||
|
|
||||||
&__icon {
|
|
||||||
position: relative;
|
|
||||||
left: 8px;
|
|
||||||
top: -1px
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&-refresh {
|
&-refresh {
|
||||||
|
@ -90,29 +87,38 @@ $header-content-height: 40px;
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
position: relative;
|
position: relative;
|
||||||
left: 5px;
|
left: 13px;
|
||||||
top: 1px
|
top: 9px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-next {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
font-size: 18px;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
position: relative;
|
||||||
|
left: 13px;
|
||||||
|
top: 7px
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&-gototop {
|
&-gototop {
|
||||||
position: absolute;
|
// outline: 1px dotted orange;
|
||||||
top: 0;
|
|
||||||
left: 110px;
|
flex-grow: 1;
|
||||||
right: 40px;
|
|
||||||
display: block;
|
display: block;
|
||||||
height: $header-content-height;
|
height: $header-content-height;
|
||||||
}
|
}
|
||||||
&-close {
|
&-close {
|
||||||
display: block;
|
display: block;
|
||||||
float: right;
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: white;
|
color: white;
|
||||||
margin-right: 8px;
|
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
position: relative;
|
position: relative;
|
||||||
left: 7px;
|
left: 15px;
|
||||||
top: 1px
|
top: 9px
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,20 +5,23 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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>
|
[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="displayError" class="stream-toots__error">{{displayError}}</div>
|
||||||
|
|
||||||
<div *ngIf="timelineLoadingMode === 3 && bufferStream && bufferStream.length > 0">
|
<div *ngIf="timelineLoadingMode === 3 && bufferStream && numNewItems > 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>
|
<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>
|
||||||
|
|
||||||
<div class="stream-toots__status" *ngFor="let statusWrapper of statuses" #status>
|
<div class="stream-toots__status" *ngFor="let statusWrapper of statuses" #status>
|
||||||
<app-status
|
<app-status
|
||||||
[statusWrapper]="statusWrapper" [isThreadDisplay]="isThread"
|
[statusWrapper]="statusWrapper"
|
||||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
[isThreadDisplay]="isThread"
|
||||||
|
[context]="context"
|
||||||
|
(browseAccountEvent)="browseAccount($event)"
|
||||||
|
(browseHashtagEvent)="browseHashtag($event)"
|
||||||
(browseThreadEvent)="browseThread($event)"></app-status>
|
(browseThreadEvent)="browseThread($event)"></app-status>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { Store } from '@ngxs/store';
|
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 { AccountInfo } from '../../../states/accounts.state';
|
||||||
import { StreamingService, EventEnum, StatusUpdate } from '../../../services/streaming.service';
|
import { StreamingService, EventEnum, StatusUpdate } from '../../../services/streaming.service';
|
||||||
import { Status } from '../../../services/models/mastodon.interfaces';
|
import { Status } from '../../../services/models/mastodon.interfaces';
|
||||||
|
@ -20,9 +20,11 @@ import { SettingsService } from '../../../services/settings.service';
|
||||||
templateUrl: './stream-statuses.component.html',
|
templateUrl: './stream-statuses.component.html',
|
||||||
styleUrls: ['./stream-statuses.component.scss']
|
styleUrls: ['./stream-statuses.component.scss']
|
||||||
})
|
})
|
||||||
export class StreamStatusesComponent extends TimelineBase {
|
export class StreamStatusesComponent extends TimelineBase {
|
||||||
protected _streamElement: StreamElement;
|
protected _streamElement: StreamElement;
|
||||||
|
|
||||||
|
context: 'home' | 'notifications' | 'public' | 'thread' | 'account';
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
set streamElement(streamElement: StreamElement) {
|
set streamElement(streamElement: StreamElement) {
|
||||||
this._streamElement = streamElement;
|
this._streamElement = streamElement;
|
||||||
|
@ -32,6 +34,8 @@ export class StreamStatusesComponent extends TimelineBase {
|
||||||
this.hideReplies = streamElement.hideReplies;
|
this.hideReplies = streamElement.hideReplies;
|
||||||
|
|
||||||
this.load(this._streamElement);
|
this.load(this._streamElement);
|
||||||
|
|
||||||
|
this.setContext(this._streamElement);
|
||||||
}
|
}
|
||||||
get streamElement(): StreamElement {
|
get streamElement(): StreamElement {
|
||||||
return this._streamElement;
|
return this._streamElement;
|
||||||
|
@ -101,6 +105,8 @@ export class StreamStatusesComponent extends TimelineBase {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.numNewItems = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
@ -110,6 +116,24 @@ export class StreamStatusesComponent extends TimelineBase {
|
||||||
if (this.deleteStatusSubscription) this.deleteStatusSubscription.unsubscribe();
|
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 {
|
refresh(): any {
|
||||||
this.load(this._streamElement);
|
this.load(this._streamElement);
|
||||||
}
|
}
|
||||||
|
@ -133,6 +157,7 @@ export class StreamStatusesComponent extends TimelineBase {
|
||||||
private resetStream() {
|
private resetStream() {
|
||||||
this.statuses.length = 0;
|
this.statuses.length = 0;
|
||||||
this.bufferStream.length = 0;
|
this.bufferStream.length = 0;
|
||||||
|
this.numNewItems = 0;
|
||||||
if (this.websocketStreaming) this.websocketStreaming.dispose();
|
if (this.websocketStreaming) this.websocketStreaming.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,6 +179,7 @@ export class StreamStatusesComponent extends TimelineBase {
|
||||||
this.statuses.unshift(wrapper);
|
this.statuses.unshift(wrapper);
|
||||||
} else {
|
} else {
|
||||||
this.bufferStream.push(update.status);
|
this.bufferStream.push(update.status);
|
||||||
|
this.numNewItems++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (update.type === EventEnum.delete) {
|
} else if (update.type === EventEnum.delete) {
|
||||||
|
@ -201,6 +227,7 @@ export class StreamStatusesComponent extends TimelineBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bufferStream.length = 0;
|
this.bufferStream.length = 0;
|
||||||
|
this.numNewItems = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,7 +239,7 @@ export class StreamStatusesComponent extends TimelineBase {
|
||||||
return status.filter(x => !this.isFiltered(x));
|
return status.filter(x => !this.isFiltered(x));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private isFiltered(status: Status): boolean {
|
private isFiltered(status: Status): boolean {
|
||||||
if (this.streamElement.hideBoosts) {
|
if (this.streamElement.hideBoosts) {
|
||||||
if (status.reblog) {
|
if (status.reblog) {
|
||||||
|
|
|
@ -7,11 +7,13 @@
|
||||||
<!-- <div> -->
|
<!-- <div> -->
|
||||||
<div class="stream-column__stream-header">
|
<div class="stream-column__stream-header">
|
||||||
<a class="stream-column__stream-selector" href title="return to top" (click)="goToTop()">
|
<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>
|
<fa-icon class="stream-column__stream-selector--icon" [icon]="columnFaIcon"></fa-icon>
|
||||||
<span class="stream-column__stream-selector--text">
|
<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>
|
<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>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="stream-column__open-menu" href title="edit column" (click)="openEditionMenu()">
|
<a class="stream-column__open-menu" href title="edit column" (click)="openEditionMenu()">
|
||||||
|
|
|
@ -28,6 +28,9 @@ export class ThreadComponent extends BrowseBase {
|
||||||
hasContentWarnings = false;
|
hasContentWarnings = false;
|
||||||
private remoteStatusFetchingDisabled = false;
|
private remoteStatusFetchingDisabled = false;
|
||||||
|
|
||||||
|
context = 'thread';
|
||||||
|
|
||||||
|
numNewItems: number; //html compatibility only
|
||||||
bufferStream: Status[] = []; //html compatibility only
|
bufferStream: Status[] = []; //html compatibility only
|
||||||
streamPositionnedAtTop: boolean = true; //html compatibility only
|
streamPositionnedAtTop: boolean = true; //html compatibility only
|
||||||
timelineLoadingMode: TimeLineModeEnum = TimeLineModeEnum.OnTop; //html compatibility only
|
timelineLoadingMode: TimeLineModeEnum = TimeLineModeEnum.OnTop; //html compatibility only
|
||||||
|
|
|
@ -107,7 +107,9 @@
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</app-status-user-context-menu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -164,39 +166,27 @@
|
||||||
|
|
||||||
<div class="profile__extra-info profile__extra-info__preparefloating" *ngIf="!isLoading"
|
<div class="profile__extra-info profile__extra-info__preparefloating" *ngIf="!isLoading"
|
||||||
[class.profile__extra-info__floating]="showFloatingStatusMenu">
|
[class.profile__extra-info__floating]="showFloatingStatusMenu">
|
||||||
<div class="profile__extra-info__section">
|
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('status')" title="Status"
|
||||||
<a href class="profile__extra-info__links" (click)="switchStatusSection('status')" title="Status"
|
|
||||||
[class.profile__extra-info__links--selected]="statusSection === 'status'">Status</a>
|
[class.profile__extra-info__links--selected]="statusSection === 'status'">Status</a>
|
||||||
</div>
|
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('replies')"
|
||||||
<div class="profile__extra-info__section">
|
|
||||||
<a href class="profile__extra-info__links" (click)="switchStatusSection('replies')"
|
|
||||||
title="Status & Replies"
|
title="Status & Replies"
|
||||||
[class.profile__extra-info__links--selected]="statusSection === 'replies'">Status &
|
[class.profile__extra-info__links--selected]="statusSection === 'replies'">Status &
|
||||||
Replies</a>
|
Replies</a>
|
||||||
</div>
|
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('media')" title="Media"
|
||||||
<div class="profile__extra-info__section">
|
|
||||||
<a href class="profile__extra-info__links" (click)="switchStatusSection('media')" title="Media"
|
|
||||||
[class.profile__extra-info__links--selected]="statusSection === 'media'">Media</a>
|
[class.profile__extra-info__links--selected]="statusSection === 'media'">Media</a>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="profile-statuses" #profilestatuses>
|
<div class="profile-statuses" #profilestatuses>
|
||||||
<div class="profile__extra-info" *ngIf="!isLoading">
|
<div class="profile__extra-info" *ngIf="!isLoading">
|
||||||
<div class="profile__extra-info__section">
|
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('status')"
|
||||||
<a href class="profile__extra-info__links" (click)="switchStatusSection('status')"
|
|
||||||
title="Status"
|
title="Status"
|
||||||
[class.profile__extra-info__links--selected]="statusSection === 'status'">Status</a>
|
[class.profile__extra-info__links--selected]="statusSection === 'status'">Status</a>
|
||||||
</div>
|
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('replies')"
|
||||||
<div class="profile__extra-info__section">
|
|
||||||
<a href class="profile__extra-info__links" (click)="switchStatusSection('replies')"
|
|
||||||
title="Status & Replies"
|
title="Status & Replies"
|
||||||
[class.profile__extra-info__links--selected]="statusSection === 'replies'">Status &
|
[class.profile__extra-info__links--selected]="statusSection === 'replies'">Status &
|
||||||
Replies</a>
|
Replies</a>
|
||||||
</div>
|
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('media')" title="Media"
|
||||||
<div class="profile__extra-info__section">
|
|
||||||
<a href class="profile__extra-info__links" (click)="switchStatusSection('media')" title="Media"
|
|
||||||
[class.profile__extra-info__links--selected]="statusSection === 'media'">Media</a>
|
[class.profile__extra-info__links--selected]="statusSection === 'media'">Media</a>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div [class.profile__status-switching-section]="isSwitchingSection">
|
<div [class.profile__status-switching-section]="isSwitchingSection">
|
||||||
|
@ -206,21 +196,29 @@
|
||||||
|
|
||||||
<div *ngIf="statusSection === 'status' && !statusLoading">
|
<div *ngIf="statusSection === 'status' && !statusLoading">
|
||||||
<div *ngFor="let statusWrapper of pinnedStatuses">
|
<div *ngFor="let statusWrapper of pinnedStatuses">
|
||||||
<app-status [statusWrapper]="statusWrapper" (browseHashtagEvent)="browseHashtag($event)"
|
<app-status
|
||||||
(browseAccountEvent)="browseAccount($event)" (browseThreadEvent)="browseThread($event)">
|
[statusWrapper]="statusWrapper"
|
||||||
|
[context]="'account'"
|
||||||
|
(browseHashtagEvent)="browseHashtag($event)"
|
||||||
|
(browseAccountEvent)="browseAccount($event)"
|
||||||
|
(browseThreadEvent)="browseThread($event)">
|
||||||
</app-status>
|
</app-status>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngFor="let statusWrapper of statuses">
|
<div *ngFor="let statusWrapper of statuses">
|
||||||
<div *ngIf="statusSection !== 'media'">
|
<div *ngIf="statusSection !== 'media'">
|
||||||
<app-status [statusWrapper]="statusWrapper" (browseHashtagEvent)="browseHashtag($event)"
|
<app-status
|
||||||
(browseAccountEvent)="browseAccount($event)" (browseThreadEvent)="browseThread($event)">
|
[statusWrapper]="statusWrapper"
|
||||||
|
[context]="'account'"
|
||||||
|
(browseHashtagEvent)="browseHashtag($event)"
|
||||||
|
(browseAccountEvent)="browseAccount($event)"
|
||||||
|
(browseThreadEvent)="browseThread($event)">
|
||||||
</app-status>
|
</app-status>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="statusSection === 'media'" class="status-media">
|
<div *ngIf="statusSection === 'media'" class="status-media">
|
||||||
<div *ngFor="let media of statusWrapper.status.media_attachments">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -275,14 +275,15 @@ $floating-header-height: 60px;
|
||||||
&-follows {
|
&-follows {
|
||||||
width: calc(100%);
|
width: calc(100%);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
border-bottom: 1px solid #0f111a;;
|
border-bottom: 1px solid #0f111a;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
&__link {
|
&__link {
|
||||||
color: white;
|
color: white;
|
||||||
width: calc(50%);
|
flex-grow: 1;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: inline-block;
|
|
||||||
background-color: #1a1f2e;
|
background-color: #1a1f2e;
|
||||||
transition: all .2s;
|
transition: all .2s;
|
||||||
|
|
||||||
|
@ -311,15 +312,15 @@ $floating-header-height: 60px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
transition: all .4s;
|
transition: all .4s;
|
||||||
|
|
||||||
&__section {
|
display: flex;
|
||||||
text-align: center;
|
|
||||||
display: inline-block;
|
|
||||||
width: calc(33.333% - 5px);
|
|
||||||
padding: 5px 0 7px 0;
|
|
||||||
|
|
||||||
&:not(:last-child) {
|
&__section {
|
||||||
margin-right: 5px;
|
// outline: 1px dotted orange;
|
||||||
}
|
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px 0 7px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__preparefloating {
|
&__preparefloating {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Store } from '@ngxs/store';
|
||||||
|
|
||||||
import { Account, Status, Relationship, Attachment } from "../../../services/models/mastodon.interfaces";
|
import { Account, Status, Relationship, Attachment } from "../../../services/models/mastodon.interfaces";
|
||||||
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
|
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 { NotificationService } from '../../../services/notification.service';
|
||||||
import { AccountInfo } from '../../../states/accounts.state';
|
import { AccountInfo } from '../../../states/accounts.state';
|
||||||
import { StatusWrapper, OpenMediaEvent } from '../../../models/common.model';
|
import { StatusWrapper, OpenMediaEvent } from '../../../models/common.model';
|
||||||
|
@ -268,6 +268,10 @@ export class UserProfileComponent extends BrowseBase {
|
||||||
this.showFloatingStatusMenu = false;
|
this.showFloatingStatusMenu = false;
|
||||||
this.load(this.lastAccountName);
|
this.load(this.lastAccountName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
relationshipChanged(relationship: Relationship){
|
||||||
|
this.relationship = relationship;
|
||||||
|
}
|
||||||
|
|
||||||
browseAccount(accountName: string): void {
|
browseAccount(accountName: string): void {
|
||||||
if (accountName === this.toolsService.getAccountFullHandle(this.displayedAccount)) return;
|
if (accountName === this.toolsService.getAccountFullHandle(this.displayedAccount)) return;
|
||||||
|
@ -282,21 +286,44 @@ export class UserProfileComponent extends BrowseBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
follow(): boolean {
|
follow(): boolean {
|
||||||
|
this.loadingRelationShip = true;
|
||||||
|
|
||||||
const userAccount = this.toolsService.getSelectedAccounts()[0];
|
const userAccount = this.toolsService.getSelectedAccounts()[0];
|
||||||
|
|
||||||
|
let foundAccountToFollow: Account;
|
||||||
this.toolsService.findAccount(userAccount, this.lastAccountName)
|
this.toolsService.findAccount(userAccount, this.lastAccountName)
|
||||||
.then((account: Account) => {
|
.then((account: Account) => {
|
||||||
|
foundAccountToFollow = account;
|
||||||
return this.mastodonService.follow(userAccount, account);
|
return this.mastodonService.follow(userAccount, account);
|
||||||
})
|
})
|
||||||
.then((relationship: Relationship) => {
|
.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) => {
|
.catch((err: HttpErrorResponse) => {
|
||||||
this.notificationService.notifyHttpError(err, userAccount);
|
this.notificationService.notifyHttpError(err, userAccount);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.loadingRelationShip = false;
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
unfollow(): boolean {
|
unfollow(): boolean {
|
||||||
|
this.loadingRelationShip = true;
|
||||||
|
|
||||||
const userAccount = this.toolsService.getSelectedAccounts()[0];
|
const userAccount = this.toolsService.getSelectedAccounts()[0];
|
||||||
this.toolsService.findAccount(userAccount, this.lastAccountName)
|
this.toolsService.findAccount(userAccount, this.lastAccountName)
|
||||||
.then((account: Account) => {
|
.then((account: Account) => {
|
||||||
|
@ -307,6 +334,9 @@ export class UserProfileComponent extends BrowseBase {
|
||||||
})
|
})
|
||||||
.catch((err: HttpErrorResponse) => {
|
.catch((err: HttpErrorResponse) => {
|
||||||
this.notificationService.notifyHttpError(err, userAccount);
|
this.notificationService.notifyHttpError(err, userAccount);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.loadingRelationShip = false;
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ export class TimeAgoPipe implements PipeTransform {
|
||||||
const hours = minutes / 60;
|
const hours = minutes / 60;
|
||||||
const days = hours / 24;
|
const days = hours / 24;
|
||||||
// const months = days / 30.416;
|
// const months = days / 30.416;
|
||||||
// const years = days / 365;
|
const years = days / 365;
|
||||||
|
|
||||||
if (seconds <= 59) {
|
if (seconds <= 59) {
|
||||||
text = Math.round(seconds) + 's';
|
text = Math.round(seconds) + 's';
|
||||||
|
@ -38,8 +38,10 @@ export class TimeAgoPipe implements PipeTransform {
|
||||||
text = Math.round(minutes) + 'm';
|
text = Math.round(minutes) + 'm';
|
||||||
} else if (hours <= 23) {
|
} else if (hours <= 23) {
|
||||||
text = Math.round(hours) + 'h';
|
text = Math.round(hours) + 'h';
|
||||||
} else {
|
} else if (days < 365) {
|
||||||
text = Math.round(days) + 'd';
|
text = Math.round(days) + 'd';
|
||||||
|
} else {
|
||||||
|
text = Math.round(years) + 'y';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minutes < 1) {
|
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 { VisibilityEnum } from './mastodon.service';
|
||||||
import { MastodonWrapperService } from './mastodon-wrapper.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';
|
import { AccountInfo } from '../states/accounts.state';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
|
@ -11,6 +11,7 @@ import { AccountInfo } from '../states/accounts.state';
|
||||||
export class InstancesInfoService {
|
export class InstancesInfoService {
|
||||||
private defaultMaxChars = 500;
|
private defaultMaxChars = 500;
|
||||||
private cachedMaxInstanceChar: { [id: string]: Promise<number>; } = {};
|
private cachedMaxInstanceChar: { [id: string]: Promise<number>; } = {};
|
||||||
|
private cachedTranslationAvailability: { [id: string]: Promise<boolean>; } = {};
|
||||||
private cachedDefaultPrivacy: { [id: string]: Promise<VisibilityEnum>; } = {};
|
private cachedDefaultPrivacy: { [id: string]: Promise<VisibilityEnum>; } = {};
|
||||||
|
|
||||||
constructor(private mastodonService: MastodonWrapperService) { }
|
constructor(private mastodonService: MastodonWrapperService) { }
|
||||||
|
@ -19,11 +20,22 @@ export class InstancesInfoService {
|
||||||
if (!this.cachedMaxInstanceChar[instance]) {
|
if (!this.cachedMaxInstanceChar[instance]) {
|
||||||
this.cachedMaxInstanceChar[instance] = this.mastodonService.getInstance(instance)
|
this.cachedMaxInstanceChar[instance] = this.mastodonService.getInstance(instance)
|
||||||
.then((instance: Instance) => {
|
.then((instance: Instance) => {
|
||||||
if (instance.max_toot_chars) {
|
if (+instance.version.split('.')[0] >= 4) {
|
||||||
return instance.max_toot_chars;
|
const instanceV2 = <Instancev2>instance;
|
||||||
|
if (instanceV2
|
||||||
|
&& instanceV2.configuration
|
||||||
|
&& instanceV2.configuration.statuses
|
||||||
|
&& instanceV2.configuration.statuses.max_characters)
|
||||||
|
return instanceV2.configuration.statuses.max_characters;
|
||||||
} else {
|
} 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(() => {
|
.catch(() => {
|
||||||
return this.defaultMaxChars;
|
return this.defaultMaxChars;
|
||||||
|
@ -56,4 +68,30 @@ export class InstancesInfoService {
|
||||||
}
|
}
|
||||||
return this.cachedDefaultPrivacy[instance];
|
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 { Injectable } from '@angular/core';
|
||||||
import { Store } from '@ngxs/store';
|
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 { AccountInfo, UpdateAccount } from '../states/accounts.state';
|
||||||
import { StreamTypeEnum, StreamElement } from '../states/streams.state';
|
import { StreamTypeEnum, StreamElement } from '../states/streams.state';
|
||||||
import { FavoriteResult, VisibilityEnum, PollParameters, MastodonService, BookmarkResult, FollowingResult } from './mastodon.service';
|
import { FavoriteResult, VisibilityEnum, PollParameters, MastodonService, BookmarkResult, FollowingResult } from './mastodon.service';
|
||||||
|
@ -12,7 +12,7 @@ import { SettingsService } from './settings.service';
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class MastodonWrapperService {
|
export class MastodonWrapperService {
|
||||||
private refreshingToken: { [id: string]: Promise<AccountInfo> } = {};
|
private refreshingToken: { [id: string]: Promise<AccountInfo> } = {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -22,14 +22,14 @@ export class MastodonWrapperService {
|
||||||
private readonly mastodonService: MastodonService) { }
|
private readonly mastodonService: MastodonService) { }
|
||||||
|
|
||||||
refreshAccountIfNeeded(accountInfo: AccountInfo): Promise<AccountInfo> {
|
refreshAccountIfNeeded(accountInfo: AccountInfo): Promise<AccountInfo> {
|
||||||
if(this.refreshingToken[accountInfo.id]){
|
if (this.refreshingToken[accountInfo.id]) {
|
||||||
return this.refreshingToken[accountInfo.id];
|
return this.refreshingToken[accountInfo.id];
|
||||||
}
|
}
|
||||||
|
|
||||||
let isExpired = false;
|
let isExpired = false;
|
||||||
let storedAccountInfo = this.getStoreAccountInfo(accountInfo.id);
|
let storedAccountInfo = this.getStoreAccountInfo(accountInfo.id);
|
||||||
|
|
||||||
if(!storedAccountInfo || !(storedAccountInfo.token))
|
if (!storedAccountInfo || !(storedAccountInfo.token))
|
||||||
return Promise.resolve(accountInfo);
|
return Promise.resolve(accountInfo);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -39,7 +39,7 @@ export class MastodonWrapperService {
|
||||||
} else {
|
} else {
|
||||||
const nowEpoch = Date.now() / 1000 | 0;
|
const nowEpoch = Date.now() / 1000 | 0;
|
||||||
|
|
||||||
//Pleroma workaround
|
//Pleroma workaround
|
||||||
let expire_in = storedAccountInfo.token.expires_in;
|
let expire_in = storedAccountInfo.token.expires_in;
|
||||||
if (expire_in < 3600) {
|
if (expire_in < 3600) {
|
||||||
expire_in = 3600;
|
expire_in = 3600;
|
||||||
|
@ -74,7 +74,7 @@ export class MastodonWrapperService {
|
||||||
p.then(() => {
|
p.then(() => {
|
||||||
this.refreshingToken[accountInfo.id] = null;
|
this.refreshingToken[accountInfo.id] = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.refreshingToken[accountInfo.id] = p;
|
this.refreshingToken[accountInfo.id] = p;
|
||||||
return p;
|
return p;
|
||||||
} else {
|
} else {
|
||||||
|
@ -96,6 +96,13 @@ export class MastodonWrapperService {
|
||||||
return this.mastodonService.getInstance(instance);
|
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> {
|
retrieveAccountDetails(account: AccountInfo): Promise<Account> {
|
||||||
return this.refreshAccountIfNeeded(account)
|
return this.refreshAccountIfNeeded(account)
|
||||||
.then((refreshedAccount: AccountInfo) => {
|
.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)
|
return this.refreshAccountIfNeeded(account)
|
||||||
.then((refreshedAccount: AccountInfo) => {
|
.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> {
|
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);
|
query = this.processTwitterQuery(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,17 +158,17 @@ export class MastodonWrapperService {
|
||||||
|
|
||||||
private processTwitterQuery(query: string): string {
|
private processTwitterQuery(query: string): string {
|
||||||
const settings = this.settingsService.getSettings();
|
const settings = this.settingsService.getSettings();
|
||||||
if(!settings.twitterBridgeInstance) return query;
|
if (!settings.twitterBridgeInstance) return query;
|
||||||
|
|
||||||
let name;
|
let name;
|
||||||
if(query.includes('twitter.com/')){
|
if (query.includes('twitter.com/')) {
|
||||||
console.log(query.replace('https://', '').replace('http://', '').split('/'));
|
console.log(query.replace('https://', '').replace('http://', '').split('/'));
|
||||||
name = query.replace('https://', '').replace('http://', '').split('/')[1];
|
name = query.replace('https://', '').replace('http://', '').split('/')[1];
|
||||||
}
|
}
|
||||||
if(query.includes('@twitter.com')){
|
if (query.includes('@twitter.com')) {
|
||||||
console.log(query.split('@'));
|
console.log(query.split('@'));
|
||||||
name = query.split('@')[0];
|
name = query.split('@')[0];
|
||||||
if(name === '' || name == null){
|
if (name === '' || name == null) {
|
||||||
name = query.split('@')[1];
|
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[]> {
|
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);
|
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> {
|
uploadMediaAttachment(account: AccountInfo, file: File, description: string): Promise<Attachment> {
|
||||||
return this.refreshAccountIfNeeded(account)
|
return this.refreshAccountIfNeeded(account)
|
||||||
.then((refreshedAccount: AccountInfo) => {
|
.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)
|
return this.refreshAccountIfNeeded(account)
|
||||||
.then((refreshedAccount: AccountInfo) => {
|
.then((refreshedAccount: AccountInfo) => {
|
||||||
return this.mastodonService.getNotifications(refreshedAccount, excludeTypes, maxId, sinceId, limit);
|
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> {
|
block(account: AccountInfo, accounId: number): Promise<Relationship> {
|
||||||
return this.refreshAccountIfNeeded(account)
|
return this.refreshAccountIfNeeded(account)
|
||||||
.then((refreshedAccount: AccountInfo) => {
|
.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> {
|
pinOnProfile(account: AccountInfo, statusId: string): Promise<Status> {
|
||||||
return this.refreshAccountIfNeeded(account)
|
return this.refreshAccountIfNeeded(account)
|
||||||
.then((refreshedAccount: AccountInfo) => {
|
.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)
|
return this.refreshAccountIfNeeded(account)
|
||||||
.then((refreshedAccount: AccountInfo) => {
|
.then((refreshedAccount: AccountInfo) => {
|
||||||
return this.mastodonService.getFollowing(refreshedAccount, accountId, maxId, sinceId, limit);
|
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)
|
return this.refreshAccountIfNeeded(account)
|
||||||
.then((refreshedAccount: AccountInfo) => {
|
.then((refreshedAccount: AccountInfo) => {
|
||||||
return this.mastodonService.getFollowers(refreshedAccount, accountId, maxId, sinceId, limit);
|
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 { HttpHeaders, HttpClient, HttpResponse } from '@angular/common/http';
|
||||||
|
|
||||||
import { ApiRoutes } from './models/api.settings';
|
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 { AccountInfo } from '../states/accounts.state';
|
||||||
import { StreamTypeEnum, StreamElement } from '../states/streams.state';
|
import { StreamTypeEnum, StreamElement } from '../states/streams.state';
|
||||||
|
|
||||||
|
@ -13,8 +13,19 @@ export class MastodonService {
|
||||||
constructor(private readonly httpClient: HttpClient) { }
|
constructor(private readonly httpClient: HttpClient) { }
|
||||||
|
|
||||||
getInstance(instance: string): Promise<Instance> {
|
getInstance(instance: string): Promise<Instance> {
|
||||||
const route = `https://${instance}${this.apiRoutes.getInstance}`;
|
let route = `https://${instance}${this.apiRoutes.getInstancev2}`;
|
||||||
return this.httpClient.get<Instance>(route).toPromise();
|
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> {
|
retrieveAccountDetails(account: AccountInfo): Promise<Account> {
|
||||||
|
@ -84,7 +95,7 @@ export class MastodonService {
|
||||||
return origString.replace(regEx, "");
|
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 url = `https://${account.instance}${this.apiRoutes.postNewStatus}`;
|
||||||
|
|
||||||
const statusData = new StatusData();
|
const statusData = new StatusData();
|
||||||
|
@ -102,10 +113,16 @@ export class MastodonService {
|
||||||
if (in_reply_to_id) {
|
if (in_reply_to_id) {
|
||||||
statusData.in_reply_to_id = in_reply_to_id;
|
statusData.in_reply_to_id = in_reply_to_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spoiler) {
|
if (spoiler) {
|
||||||
statusData.sensitive = true;
|
statusData.sensitive = true;
|
||||||
statusData.spoiler_text = spoiler;
|
statusData.spoiler_text = spoiler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(lang) {
|
||||||
|
statusData.language = lang;
|
||||||
|
}
|
||||||
|
|
||||||
switch (visibility) {
|
switch (visibility) {
|
||||||
case VisibilityEnum.Public:
|
case VisibilityEnum.Public:
|
||||||
statusData.visibility = 'public';
|
statusData.visibility = 'public';
|
||||||
|
@ -128,6 +145,57 @@ export class MastodonService {
|
||||||
return this.httpClient.post<Status>(url, statusData, { headers: headers }).toPromise();
|
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> {
|
getStatus(account: AccountInfo, statusId: string): Promise<Status> {
|
||||||
const route = `https://${account.instance}${this.apiRoutes.getStatus.replace('{0}', statusId)}`;
|
const route = `https://${account.instance}${this.apiRoutes.getStatus.replace('{0}', statusId)}`;
|
||||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
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> {
|
uploadMediaAttachment(account: AccountInfo, file: File, description: string): Promise<Attachment> {
|
||||||
let input = new FormData();
|
let input = new FormData();
|
||||||
input.append('file', file);
|
input.append('file', file);
|
||||||
|
|
||||||
if (description !== null && description !== undefined) {
|
if (description !== null && description !== undefined) {
|
||||||
input.append('description', description);
|
input.append('description', description);
|
||||||
} else {
|
} else {
|
||||||
input.append('description', '');
|
input.append('description', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const route = `https://${account.instance}${this.apiRoutes.uploadMediaAttachment}`;
|
const route = `https://${account.instance}${this.apiRoutes.uploadMediaAttachment}`;
|
||||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||||
return this.httpClient.post<Attachment>(route, input, { headers: headers }).toPromise();
|
return this.httpClient.post<Attachment>(route, input, { headers: headers }).toPromise();
|
||||||
|
@ -305,13 +413,19 @@ export class MastodonService {
|
||||||
//TODO: add focus support
|
//TODO: add focus support
|
||||||
updateMediaAttachment(account: AccountInfo, mediaId: string, description: string): Promise<Attachment> {
|
updateMediaAttachment(account: AccountInfo, mediaId: string, description: string): Promise<Attachment> {
|
||||||
let input = new FormData();
|
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 route = `https://${account.instance}${this.apiRoutes.updateMediaAttachment.replace('{0}', mediaId)}`;
|
||||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||||
return this.httpClient.put<Attachment>(route, input, { headers: headers }).toPromise();
|
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}`;
|
let route = `https://${account.instance}${this.apiRoutes.getNotifications}?limit=${limit}`;
|
||||||
|
|
||||||
if (maxId) {
|
if (maxId) {
|
||||||
|
@ -382,10 +496,10 @@ export class MastodonService {
|
||||||
addAccountToList(account: AccountInfo, listId: string, accountId: number): Promise<any> {
|
addAccountToList(account: AccountInfo, listId: string, accountId: number): Promise<any> {
|
||||||
let route = `https://${account.instance}${this.apiRoutes.addAccountToList}`.replace('{0}', listId);
|
let route = `https://${account.instance}${this.apiRoutes.addAccountToList}`.replace('{0}', listId);
|
||||||
route += `?account_ids[]=${accountId}`;
|
route += `?account_ids[]=${accountId}`;
|
||||||
|
|
||||||
let data = new ListAccountData();
|
let data = new ListAccountData();
|
||||||
data.account_ids.push(accountId.toString());
|
data.account_ids.push(accountId.toString());
|
||||||
|
|
||||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||||
return this.httpClient.post(route, data, { headers: headers }).toPromise();
|
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();
|
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> {
|
block(account: AccountInfo, accounId: number): Promise<Relationship> {
|
||||||
let route = `https://${account.instance}${this.apiRoutes.block}`.replace('{0}', accounId.toString());
|
let route = `https://${account.instance}${this.apiRoutes.block}`.replace('{0}', accounId.toString());
|
||||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||||
return this.httpClient.post<Relationship>(route, null, { headers: headers }).toPromise();
|
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> {
|
pinOnProfile(account: AccountInfo, statusId: string): Promise<Status> {
|
||||||
let route = `https://${account.instance}${this.apiRoutes.pinStatus}`.replace('{0}', statusId.toString());
|
let route = `https://${account.instance}${this.apiRoutes.pinStatus}`.replace('{0}', statusId.toString());
|
||||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||||
|
@ -458,7 +601,8 @@ export class MastodonService {
|
||||||
|
|
||||||
getCustomEmojis(account: AccountInfo): Promise<Emoji[]> {
|
getCustomEmojis(account: AccountInfo): Promise<Emoji[]> {
|
||||||
let route = `https://${account.instance}${this.apiRoutes.getCustomEmojis}`;
|
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[]> {
|
getScheduledStatuses(account: AccountInfo): Promise<ScheduledStatus[]> {
|
||||||
|
@ -564,11 +708,22 @@ class StatusData {
|
||||||
status: string;
|
status: string;
|
||||||
in_reply_to_id: string;
|
in_reply_to_id: string;
|
||||||
media_ids: string[];
|
media_ids: string[];
|
||||||
|
media_attributes: MediaAttributes[];
|
||||||
|
|
||||||
// poll: PollParameters;
|
// poll: PollParameters;
|
||||||
sensitive: boolean;
|
sensitive: boolean;
|
||||||
spoiler_text: string;
|
spoiler_text: string;
|
||||||
visibility: string;
|
visibility: string;
|
||||||
// scheduled_at: string;
|
// scheduled_at: string;
|
||||||
|
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MediaAttributes {
|
||||||
|
constructor(
|
||||||
|
public id: string,
|
||||||
|
public description: string){
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PollParameters {
|
export class PollParameters {
|
||||||
|
|
|
@ -51,24 +51,63 @@ export class MediaService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
update(account: AccountInfo, media: MediaWrapper) {
|
loadMedia(attachments: Attachment[]) {
|
||||||
if (media.attachment.description === media.description) return;
|
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){
|
update(account: AccountInfo, media: MediaWrapper): Promise<void> {
|
||||||
if(!this.fileCache[media.attachment.url]) return;
|
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];
|
media.file = this.fileCache[media.attachment.url];
|
||||||
let medias = this.mediaSubject.value;
|
let medias = this.mediaSubject.value;
|
||||||
medias.push(media);
|
medias.push(media);
|
||||||
|
@ -88,11 +127,15 @@ export class MediaService {
|
||||||
migrateMedias(account: AccountInfo) {
|
migrateMedias(account: AccountInfo) {
|
||||||
let medias = this.mediaSubject.value;
|
let medias = this.mediaSubject.value;
|
||||||
medias.forEach(media => {
|
medias.forEach(media => {
|
||||||
media.isMigrating = true;
|
if (!media.isEdited) {
|
||||||
|
media.isMigrating = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.mediaSubject.next(medias);
|
this.mediaSubject.next(medias);
|
||||||
|
|
||||||
for (let media of medias) {
|
for (let media of medias) {
|
||||||
|
if (media.isEdited) continue;
|
||||||
|
|
||||||
this.mastodonService.uploadMediaAttachment(account, media.file, media.description)
|
this.mastodonService.uploadMediaAttachment(account, media.file, media.description)
|
||||||
.then((attachment: Attachment) => {
|
.then((attachment: Attachment) => {
|
||||||
this.fileCache[attachment.url] = media.file;
|
this.fileCache[attachment.url] = media.file;
|
||||||
|
@ -117,7 +160,7 @@ export class MediaWrapper {
|
||||||
public id: string,
|
public id: string,
|
||||||
public file: File,
|
public file: File,
|
||||||
attachment: Attachment) {
|
attachment: Attachment) {
|
||||||
this.attachment = attachment;
|
this.attachment = attachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _attachment: Attachment;
|
private _attachment: Attachment;
|
||||||
|
@ -125,7 +168,7 @@ export class MediaWrapper {
|
||||||
return this._attachment;
|
return this._attachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
public set attachment(value: Attachment){
|
public set attachment(value: Attachment) {
|
||||||
if (value && value.meta && value.meta.audio_encode) {
|
if (value && value.meta && value.meta.audio_encode) {
|
||||||
this.audioType = `audio/${value.meta.audio_encode}`;
|
this.audioType = `audio/${value.meta.audio_encode}`;
|
||||||
} else if (value && value.pleroma && value.pleroma.mime_type) {
|
} else if (value && value.pleroma && value.pleroma.mime_type) {
|
||||||
|
@ -138,4 +181,6 @@ export class MediaWrapper {
|
||||||
public description: string;
|
public description: string;
|
||||||
public isMigrating: boolean;
|
public isMigrating: boolean;
|
||||||
public audioType: string;
|
public audioType: string;
|
||||||
|
|
||||||
|
public isEdited: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ export class ApiRoutes {
|
||||||
unfollow = '/api/v1/accounts/{0}/unfollow';
|
unfollow = '/api/v1/accounts/{0}/unfollow';
|
||||||
block = '/api/v1/accounts/{0}/block';
|
block = '/api/v1/accounts/{0}/block';
|
||||||
unblock = '/api/v1/accounts/{0}/unblock';
|
unblock = '/api/v1/accounts/{0}/unblock';
|
||||||
|
blockDomain = '/api/v1/domain_blocks';
|
||||||
mute = '/api/v1/accounts/{0}/mute';
|
mute = '/api/v1/accounts/{0}/mute';
|
||||||
unmute = '/api/v1/accounts/{0}/unmute';
|
unmute = '/api/v1/accounts/{0}/unmute';
|
||||||
muteStatus = '/api/v1/statuses/{0}/mute';
|
muteStatus = '/api/v1/statuses/{0}/mute';
|
||||||
|
@ -25,6 +26,7 @@ export class ApiRoutes {
|
||||||
rejectFollowRequest = '/api/v1/follow_requests/{0}/reject';
|
rejectFollowRequest = '/api/v1/follow_requests/{0}/reject';
|
||||||
followRemote = '/api/v1/follows';
|
followRemote = '/api/v1/follows';
|
||||||
getInstance = '/api/v1/instance';
|
getInstance = '/api/v1/instance';
|
||||||
|
getInstancev2 = '/api/v2/instance';
|
||||||
uploadMediaAttachment = '/api/v1/media';
|
uploadMediaAttachment = '/api/v1/media';
|
||||||
updateMediaAttachment = '/api/v1/media/{0}';
|
updateMediaAttachment = '/api/v1/media/{0}';
|
||||||
getMutes = '/api/v1/mutes';
|
getMutes = '/api/v1/mutes';
|
||||||
|
@ -41,6 +43,7 @@ export class ApiRoutes {
|
||||||
getStatusRebloggedBy = '/api/v1/statuses/{0}/reblogged_by';
|
getStatusRebloggedBy = '/api/v1/statuses/{0}/reblogged_by';
|
||||||
getStatusFavouritedBy = '/api/v1/statuses/{0}/favourited_by';
|
getStatusFavouritedBy = '/api/v1/statuses/{0}/favourited_by';
|
||||||
postNewStatus = '/api/v1/statuses';
|
postNewStatus = '/api/v1/statuses';
|
||||||
|
editStatus = '/api/v1/statuses/{0}';
|
||||||
deleteStatus = '/api/v1/statuses/{0}';
|
deleteStatus = '/api/v1/statuses/{0}';
|
||||||
reblogStatus = '/api/v1/statuses/{0}/reblog';
|
reblogStatus = '/api/v1/statuses/{0}/reblog';
|
||||||
unreblogStatus = '/api/v1/statuses/{0}/unreblog';
|
unreblogStatus = '/api/v1/statuses/{0}/unreblog';
|
||||||
|
@ -75,4 +78,8 @@ export class ApiRoutes {
|
||||||
getBookmarks = '/api/v1/bookmarks';
|
getBookmarks = '/api/v1/bookmarks';
|
||||||
getFollowers = '/api/v1/accounts/{0}/followers';
|
getFollowers = '/api/v1/accounts/{0}/followers';
|
||||||
getFollowing = '/api/v1/accounts/{0}/following';
|
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;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface Instance {
|
export interface Instance {
|
||||||
uri: string;
|
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
|
||||||
email: string;
|
|
||||||
version: string;
|
version: string;
|
||||||
urls: string[];
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Instancev1 extends Instance {
|
||||||
|
uri: string;
|
||||||
|
email: string;
|
||||||
|
urls: InstanceUrls;
|
||||||
contact_account: Account;
|
contact_account: Account;
|
||||||
max_toot_chars: number;
|
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 {
|
export interface Mention {
|
||||||
|
@ -130,7 +162,7 @@ export interface Mention {
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'mention' | 'reblog' | 'favourite' | 'follow' | 'poll' | 'follow_request' | 'move';
|
type: 'mention' | 'reblog' | 'favourite' | 'follow' | 'poll' | 'follow_request' | 'move' | 'update';
|
||||||
created_at: string;
|
created_at: string;
|
||||||
account: Account;
|
account: Account;
|
||||||
status?: Status;
|
status?: Status;
|
||||||
|
@ -141,7 +173,7 @@ export interface Relationship {
|
||||||
id: number;
|
id: number;
|
||||||
following: boolean;
|
following: boolean;
|
||||||
followed_by: boolean;
|
followed_by: boolean;
|
||||||
blocked_by: boolean;
|
blocked_by: boolean;
|
||||||
blocking: boolean;
|
blocking: boolean;
|
||||||
domain_blocking: boolean;
|
domain_blocking: boolean;
|
||||||
muting: boolean;
|
muting: boolean;
|
||||||
|
@ -162,6 +194,33 @@ export interface Results {
|
||||||
hashtags: string[];
|
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 {
|
export interface Status {
|
||||||
id: string;
|
id: string;
|
||||||
uri: string;
|
uri: string;
|
||||||
|
@ -172,6 +231,7 @@ export interface Status {
|
||||||
reblog: Status;
|
reblog: Status;
|
||||||
content: string;
|
content: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
edited_at: string;
|
||||||
reblogs_count: number;
|
reblogs_count: number;
|
||||||
replies_count: number;
|
replies_count: number;
|
||||||
favourites_count: string;
|
favourites_count: string;
|
||||||
|
@ -190,7 +250,8 @@ export interface Status {
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
bookmarked: boolean;
|
bookmarked: boolean;
|
||||||
card: Card;
|
card: Card;
|
||||||
poll: Poll;
|
poll: Poll;
|
||||||
|
filtered: FilterResult[];
|
||||||
|
|
||||||
pleroma: PleromaStatusInfo;
|
pleroma: PleromaStatusInfo;
|
||||||
}
|
}
|
||||||
|
@ -207,11 +268,6 @@ export interface PleromaStatusInfo {
|
||||||
local: boolean;
|
local: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Tag {
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface List {
|
export interface List {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -249,4 +305,25 @@ export interface StatusParams {
|
||||||
visibility: 'public' | 'unlisted' | 'private' | 'direct';
|
visibility: 'public' | 'unlisted' | 'private' | 'direct';
|
||||||
scheduled_at: string;
|
scheduled_at: string;
|
||||||
application_id: 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;
|
private accountToManage: AccountWrapper;
|
||||||
activatedPanelSubject = new BehaviorSubject<OpenLeftPanelEvent>(new OpenLeftPanelEvent(LeftPanelType.Closed));
|
activatedPanelSubject = new BehaviorSubject<OpenLeftPanelEvent>(new OpenLeftPanelEvent(LeftPanelType.Closed));
|
||||||
activatedMediaSubject: Subject<OpenMediaEvent> = new Subject<OpenMediaEvent>();
|
activatedMediaSubject: Subject<OpenMediaEvent> = new Subject<OpenMediaEvent>();
|
||||||
columnSelectedSubject = new BehaviorSubject<number>(-1);
|
columnSelectedSubject = new BehaviorSubject<number>(-1);
|
||||||
|
enableDraggableIconMenu = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
|
@ -19,6 +20,10 @@ export class NavigationService {
|
||||||
this.activatedPanelSubject.next(newEvent);
|
this.activatedPanelSubject.next(newEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeIconMenuState(draggable: boolean) {
|
||||||
|
this.enableDraggableIconMenu.next(draggable);
|
||||||
|
}
|
||||||
|
|
||||||
openPanel(type: LeftPanelType){
|
openPanel(type: LeftPanelType){
|
||||||
const newEvent = new OpenLeftPanelEvent(type);
|
const newEvent = new OpenLeftPanelEvent(type);
|
||||||
this.activatedPanelSubject.next(newEvent);
|
this.activatedPanelSubject.next(newEvent);
|
||||||
|
@ -41,6 +46,11 @@ export class NavigationService {
|
||||||
this.activatedPanelSubject.next(newEvent);
|
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 {
|
columnSelected(index: number): void {
|
||||||
this.columnSelectedSubject.next(index);
|
this.columnSelectedSubject.next(index);
|
||||||
}
|
}
|
||||||
|
@ -68,6 +78,7 @@ export enum LeftPanelAction {
|
||||||
DM = 1,
|
DM = 1,
|
||||||
Mention = 2,
|
Mention = 2,
|
||||||
Redraft = 3,
|
Redraft = 3,
|
||||||
|
Edit = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LeftPanelType {
|
export enum LeftPanelType {
|
||||||
|
@ -77,5 +88,6 @@ export enum LeftPanelType {
|
||||||
Search = 3,
|
Search = 3,
|
||||||
AddNewAccount = 4,
|
AddNewAccount = 4,
|
||||||
Settings = 5,
|
Settings = 5,
|
||||||
ScheduledStatuses = 6
|
ScheduledStatuses = 6,
|
||||||
|
EditStatus = 7,
|
||||||
}
|
}
|
|
@ -26,15 +26,15 @@ export class NotificationService {
|
||||||
public notifyHttpError(err: HttpErrorResponse, account: AccountInfo) {
|
public notifyHttpError(err: HttpErrorResponse, account: AccountInfo) {
|
||||||
let message = 'Oops, Unknown Error';
|
let message = 'Oops, Unknown Error';
|
||||||
let code: number;
|
let code: number;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
code = err.status;
|
code = err.status;
|
||||||
if(err.message){
|
if(err.error && err.error.error) {
|
||||||
message = err.message;
|
|
||||||
} else if(err.error && err.error.error) {
|
|
||||||
message = err.error.error; //Mastodon
|
message = err.error.error; //Mastodon
|
||||||
} else if(err.error && err.error.errors && err.error.errors.detail){
|
} else if(err.error && err.error.errors && err.error.errors.detail){
|
||||||
message = err.error.errors.detail; //Pleroma
|
message = err.error.errors.detail; //Pleroma
|
||||||
|
} else if(err.message){
|
||||||
|
message = err.message;
|
||||||
}
|
}
|
||||||
} catch (err) { }
|
} catch (err) { }
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,11 @@ export class SettingsService {
|
||||||
this.saveSettings(settings);
|
this.saveSettings(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!settings.configuredLanguages){
|
||||||
|
settings.configuredLanguages = [];
|
||||||
|
this.saveSettings(settings);
|
||||||
|
}
|
||||||
|
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,15 +6,15 @@ import { StatusWrapper } from '../models/common.model';
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class StatusesStateService {
|
export class StatusesStateService {
|
||||||
private cachedStatusText: { [statusId: string]: string } = {};
|
private cachedStatusText: { [statusId: string]: string } = {};
|
||||||
private cachedStatusStates: { [statusId: string]: { [accountId: string]: StatusState } } = {};
|
private cachedStatusStates: { [statusId: string]: { [accountId: string]: StatusState } } = {};
|
||||||
public stateNotification = new Subject<StatusState>();
|
public stateNotification = new Subject<StatusState>();
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
getStateForStatus(statusId: string): StatusState[] {
|
getStateForStatus(statusId: string): StatusState[] {
|
||||||
if(!this.cachedStatusStates[statusId])
|
if (!this.cachedStatusStates[statusId])
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
let results: StatusState[] = [];
|
let results: StatusState[] = [];
|
||||||
|
@ -31,7 +31,7 @@ export class StatusesStateService {
|
||||||
this.cachedStatusStates[statusId] = {};
|
this.cachedStatusStates[statusId] = {};
|
||||||
|
|
||||||
if (!this.cachedStatusStates[statusId][accountId]) {
|
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 {
|
} else {
|
||||||
this.cachedStatusStates[statusId][accountId].isFavorited = isFavorited;
|
this.cachedStatusStates[statusId][accountId].isFavorited = isFavorited;
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ export class StatusesStateService {
|
||||||
this.cachedStatusStates[statusId] = {};
|
this.cachedStatusStates[statusId] = {};
|
||||||
|
|
||||||
if (!this.cachedStatusStates[statusId][accountId]) {
|
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 {
|
} else {
|
||||||
this.cachedStatusStates[statusId][accountId].isRebloged = isRebloged;
|
this.cachedStatusStates[statusId][accountId].isRebloged = isRebloged;
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ export class StatusesStateService {
|
||||||
this.cachedStatusStates[statusId] = {};
|
this.cachedStatusStates[statusId] = {};
|
||||||
|
|
||||||
if (!this.cachedStatusStates[statusId][accountId]) {
|
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 {
|
} else {
|
||||||
this.cachedStatusStates[statusId][accountId].isBookmarked = isBookmarked;
|
this.cachedStatusStates[statusId][accountId].isBookmarked = isBookmarked;
|
||||||
}
|
}
|
||||||
|
@ -65,42 +65,58 @@ export class StatusesStateService {
|
||||||
this.stateNotification.next(this.cachedStatusStates[statusId][accountId]);
|
this.stateNotification.next(this.cachedStatusStates[statusId][accountId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatusContent(data: string, replyingToStatus: StatusWrapper){
|
statusEditedStatusChanged(statusId: string, accountId: string, editedStatus: StatusWrapper) {
|
||||||
if(replyingToStatus){
|
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;
|
this.cachedStatusText[replyingToStatus.status.uri] = data;
|
||||||
} else {
|
} else {
|
||||||
this.cachedStatusText['none'] = data;
|
this.cachedStatusText['none'] = data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatusContent(replyingToStatus: StatusWrapper): string{
|
getStatusContent(replyingToStatus: StatusWrapper): string {
|
||||||
let data: string;
|
let data: string;
|
||||||
if(replyingToStatus){
|
if (replyingToStatus) {
|
||||||
data = this.cachedStatusText[replyingToStatus.status.uri];
|
data = this.cachedStatusText[replyingToStatus.status.uri];
|
||||||
} else {
|
} else {
|
||||||
data = this.cachedStatusText['none'];
|
data = this.cachedStatusText['none'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!data) return '';
|
if (!data) return '';
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
resetStatusContent(replyingToStatus: StatusWrapper){
|
resetStatusContent(replyingToStatus: StatusWrapper) {
|
||||||
if(replyingToStatus){
|
if (replyingToStatus) {
|
||||||
this.cachedStatusText[replyingToStatus.status.uri] = '';
|
this.cachedStatusText[replyingToStatus.status.uri] = '';
|
||||||
} else {
|
} else {
|
||||||
this.cachedStatusText['none'] = '';
|
this.cachedStatusText['none'] = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StatusState {
|
export class StatusState {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public statusId: string,
|
public statusId: string,
|
||||||
public accountId: string,
|
public accountId: string,
|
||||||
public isFavorited: boolean,
|
public isFavorited: boolean,
|
||||||
public isRebloged: 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 { StreamTypeEnum, StreamElement } from "../states/streams.state";
|
||||||
import { MastodonWrapperService } from "./mastodon-wrapper.service";
|
import { MastodonWrapperService } from "./mastodon-wrapper.service";
|
||||||
import { AccountInfo } from "../states/accounts.state";
|
import { AccountInfo } from "../states/accounts.state";
|
||||||
|
import { InstanceInfo, ToolsService } from "./tools.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StreamingService {
|
export class StreamingService {
|
||||||
|
@ -13,12 +14,13 @@ export class StreamingService {
|
||||||
public readonly nbStatusPerIteration: number = 20;
|
public readonly nbStatusPerIteration: number = 20;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly mastodonService: MastodonWrapperService) { }
|
private readonly mastodonService: MastodonWrapperService,
|
||||||
|
private readonly toolsService: ToolsService) { }
|
||||||
|
|
||||||
getStreaming(accountInfo: AccountInfo, stream: StreamElement, since_id: string = null): StreamingWrapper {
|
getStreaming(accountInfo: AccountInfo, stream: StreamElement, since_id: string = null): StreamingWrapper {
|
||||||
//new EventSourceStreaminWrapper(accountInfo, stream);
|
//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(
|
constructor(
|
||||||
private readonly mastodonService: MastodonWrapperService,
|
private readonly mastodonService: MastodonWrapperService,
|
||||||
|
private readonly toolsService: ToolsService,
|
||||||
private readonly account: AccountInfo,
|
private readonly account: AccountInfo,
|
||||||
private readonly stream: StreamElement,
|
private readonly stream: StreamElement,
|
||||||
private readonly nbStatusPerIteration: number,
|
private readonly nbStatusPerIteration: number,
|
||||||
|
@ -53,7 +56,13 @@ export class StreamingWrapper {
|
||||||
return account;
|
return account;
|
||||||
})
|
})
|
||||||
.then((refreshedAccount: AccountInfo) => {
|
.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 = new WebSocket(route);
|
||||||
this.eventSource.onmessage = x => {
|
this.eventSource.onmessage = x => {
|
||||||
if (x.data !== '') {
|
if (x.data !== '') {
|
||||||
|
@ -62,7 +71,7 @@ export class StreamingWrapper {
|
||||||
}
|
}
|
||||||
this.eventSource.onerror = x => this.webSocketGotError(x);
|
this.eventSource.onerror = x => this.webSocketGotError(x);
|
||||||
this.eventSource.onopen = 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() {
|
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[]) => {
|
.then((notifications: Notification[]) => {
|
||||||
//notifications = notifications.sort((a, b) => a.id.localeCompare(b.id));
|
//notifications = notifications.sort((a, b) => a.id.localeCompare(b.id));
|
||||||
let soundMuted = !this.since_id_notifications;
|
let soundMuted = !this.since_id_notifications;
|
||||||
|
@ -159,12 +168,19 @@ export class StreamingWrapper {
|
||||||
newUpdate.type = EventEnum.unknow;
|
newUpdate.type = EventEnum.unknow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.statusUpdateSubjet.next(newUpdate);
|
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);
|
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.tag) route = `${route}&tag=${stream.tag}`;
|
||||||
if (stream.list) route = `${route}&list=${stream.listId}`;
|
if (stream.list) route = `${route}&list=${stream.listId}`;
|
||||||
|
@ -274,6 +290,13 @@ class WebSocketEvent {
|
||||||
payload: any;
|
payload: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StreamingAccountInfo {
|
||||||
|
constructor(
|
||||||
|
public instanceInfo: InstanceInfo,
|
||||||
|
public refreshedAccount: AccountInfo) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class StatusUpdate {
|
export class StatusUpdate {
|
||||||
type: EventEnum;
|
type: EventEnum;
|
||||||
status: Status;
|
status: Status;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Store } from '@ngxs/store';
|
||||||
|
|
||||||
import { AccountInfo } from '../states/accounts.state';
|
import { AccountInfo } from '../states/accounts.state';
|
||||||
import { MastodonWrapperService } from './mastodon-wrapper.service';
|
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 { StatusWrapper } from '../models/common.model';
|
||||||
import { AccountSettings, SaveAccountSettings, GlobalSettings, SaveSettings, ContentWarningPolicy, SaveContentWarningPolicy, ContentWarningPolicyEnum, TimeLineModeEnum, TimeLineHeaderEnum } from '../states/settings.state';
|
import { AccountSettings, SaveAccountSettings, GlobalSettings, SaveSettings, ContentWarningPolicy, SaveContentWarningPolicy, ContentWarningPolicyEnum, TimeLineModeEnum, TimeLineHeaderEnum } from '../states/settings.state';
|
||||||
import { SettingsService } from './settings.service';
|
import { SettingsService } from './settings.service';
|
||||||
|
@ -77,23 +77,64 @@ export class ToolsService {
|
||||||
return Promise.resolve(this.instanceInfos[acc.instance]);
|
return Promise.resolve(this.instanceInfos[acc.instance]);
|
||||||
} else {
|
} else {
|
||||||
return this.mastodonService.getInstance(acc.instance)
|
return this.mastodonService.getInstance(acc.instance)
|
||||||
.then(instance => {
|
.then(instance => {
|
||||||
var type = InstanceType.Mastodon;
|
const splittedVersion = instance.version.split('.');
|
||||||
if (instance.version.toLowerCase().includes('pleroma')) {
|
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;
|
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;
|
type = InstanceType.GlitchSoc;
|
||||||
} else if (instance.version.toLowerCase().includes('+florence')) {
|
} else if (version.includes('+florence')) {
|
||||||
type = InstanceType.Florence;
|
type = InstanceType.Florence;
|
||||||
} else if (instance.version.toLowerCase().includes('pixelfed')) {
|
} else if (version.includes('pixelfed')) {
|
||||||
type = InstanceType.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('.');
|
let streamingApi = "";
|
||||||
var major = +splittedVersion[0];
|
|
||||||
var minor = +splittedVersion[1];
|
|
||||||
|
|
||||||
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;
|
this.instanceInfos[acc.instance] = instanceInfo;
|
||||||
|
|
||||||
return 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> {
|
getAvatar(acc: AccountInfo): Promise<string> {
|
||||||
if (this.accountAvatar[acc.id]) {
|
if (this.accountAvatar[acc.id]) {
|
||||||
return Promise.resolve(this.accountAvatar[acc.id]);
|
return Promise.resolve(this.accountAvatar[acc.id]);
|
||||||
|
@ -231,16 +291,21 @@ export class InstanceInfo {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly type: InstanceType,
|
public readonly type: InstanceType,
|
||||||
public readonly major: number,
|
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 {
|
export enum InstanceType {
|
||||||
Mastodon = 1,
|
Mastodon = 1,
|
||||||
Pleroma = 2,
|
Pleroma = 2, // "2.7.2 (compatible; Pleroma 2.5.1)"
|
||||||
GlitchSoc = 3,
|
GlitchSoc = 3, // "4.1.5+glitch_0801_3b49b5a"
|
||||||
Florence = 4,
|
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 {
|
export class StatusWithCwPolicyResult {
|
||||||
|
|
|
@ -58,8 +58,10 @@ export class UserNotificationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private startFetchingNotifications(account: AccountInfo) {
|
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[]) => {
|
.then((notifications: Notification[]) => {
|
||||||
|
notifications = notifications.filter(x => x.status !== null);
|
||||||
|
|
||||||
this.processMentionsAndNotifications(account, notifications, NotificationTypeEnum.UserMention);
|
this.processMentionsAndNotifications(account, notifications, NotificationTypeEnum.UserMention);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
|
|
@ -6,6 +6,11 @@ export class AddAccount {
|
||||||
constructor(public account: AccountInfo) {}
|
constructor(public account: AccountInfo) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ReorderAccounts {
|
||||||
|
static readonly type = '[Accounts] Reorder';
|
||||||
|
constructor(public accounts: AccountInfo[]) {}
|
||||||
|
}
|
||||||
|
|
||||||
export class SelectAccount {
|
export class SelectAccount {
|
||||||
static readonly type = '[Accounts] Select account';
|
static readonly type = '[Accounts] Select account';
|
||||||
constructor(public account: AccountInfo, public multiselection: boolean = false) {}
|
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)
|
@Action(UpdateAccount)
|
||||||
UpdateAccount(ctx: StateContext<AccountsStateModel>, action: UpdateAccount){
|
UpdateAccount(ctx: StateContext<AccountsStateModel>, action: UpdateAccount){
|
||||||
const state = ctx.getState();
|
const state = ctx.getState();
|
||||||
|
|
|
@ -51,7 +51,8 @@ export enum TimeLineHeaderEnum {
|
||||||
Title_Username_DomainName = 2,
|
Title_Username_DomainName = 2,
|
||||||
Title_AccountIcon_DomainName = 3,
|
Title_AccountIcon_DomainName = 3,
|
||||||
Title_AccountIcon = 4,
|
Title_AccountIcon = 4,
|
||||||
Title = 5
|
Title = 5,
|
||||||
|
Title_AccountIcon_Username_DomainName = 6
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContentWarningPolicy {
|
export class ContentWarningPolicy {
|
||||||
|
@ -79,7 +80,19 @@ export class GlobalSettings {
|
||||||
|
|
||||||
columnSwitchingWinAlt = false;
|
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 {
|
export interface SettingsStateModel {
|
||||||
|
@ -170,6 +183,11 @@ export class SettingsState {
|
||||||
newSettings.autoFollowOnListEnabled = oldSettings.autoFollowOnListEnabled;
|
newSettings.autoFollowOnListEnabled = oldSettings.autoFollowOnListEnabled;
|
||||||
newSettings.twitterBridgeEnabled = oldSettings.twitterBridgeEnabled;
|
newSettings.twitterBridgeEnabled = oldSettings.twitterBridgeEnabled;
|
||||||
newSettings.twitterBridgeInstance = oldSettings.twitterBridgeInstance;
|
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;
|
return newSettings;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,27 @@
|
||||||
@import "variables";
|
@import "variables";
|
||||||
|
|
||||||
::ng-deep .ngx-contextmenu {
|
::ng-deep .ngx-contextmenu {
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
$shadow: 0.4;
|
||||||
-moz-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 0 10px rgba(0, 0, 0, $shadow);
|
||||||
-webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
-moz-box-shadow: 0 0 10px rgba(0, 0, 0, $shadow);
|
||||||
-o-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
-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 {
|
& .dropdown-menu {
|
||||||
//border: solid 1px $context-menu-border-color;
|
//border: solid 1px $context-menu-border-color;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: $context-menu-background;
|
background-color: $context-menu-background;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
|
|
||||||
|
border-radius: 7px;
|
||||||
|
overflow: hidden;
|
||||||
// padding: 2px 0;
|
// padding: 2px 0;
|
||||||
// border-radius: 2px;
|
// border-radius: 2px;
|
||||||
//border: solid 2px $context-menu-border-color;
|
//border: solid 2px $context-menu-border-color;
|
||||||
|
@ -44,6 +54,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
& .divider {
|
& .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-secondary-color: #4e5572;
|
||||||
$status-links-color: #d9e1e8;
|
$status-links-color: #d9e1e8;
|
||||||
$boost-color : #5098eb;
|
$boost-color : #5098eb;
|
||||||
|
$update-color : #95e470;
|
||||||
$favorite-color: #ffc16f;
|
$favorite-color: #ffc16f;
|
||||||
$bookmarked-color: #ff5050;
|
$bookmarked-color: #ff5050;
|
||||||
|
|
||||||
|
@ -52,9 +53,12 @@ $column-background: #0f111a;
|
||||||
$card-border-color: #2b344d;
|
$card-border-color: #2b344d;
|
||||||
|
|
||||||
$context-menu-background: #d9e1e8;
|
$context-menu-background: #d9e1e8;
|
||||||
|
$context-menu-background: #ffffff;
|
||||||
$context-menu-background-hover: #a9c9e6;
|
$context-menu-background-hover: #a9c9e6;
|
||||||
|
$context-menu-background-hover: #d7dfeb;
|
||||||
$context-menu-font-color: #000000;
|
$context-menu-font-color: #000000;
|
||||||
$context-menu-border-color: #c0cdd9;
|
$context-menu-border-color: #c0cdd9;
|
||||||
|
$context-menu-border-color: #cbd3df;
|
||||||
|
|
||||||
$direct-message-background: #090a0f;
|
$direct-message-background: #090a0f;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue