Merge pull request #123 from NicolasConstant/topic_quality-of-life

Topic quality of life
This commit is contained in:
Nicolas Constant 2019-06-24 15:02:21 -04:00 committed by GitHub
commit cd699b9da4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 775 additions and 328 deletions

View File

@ -9,166 +9,181 @@ const fs = require("fs");
let win; let win;
function createWindow() { function createWindow() {
// Create the browser window. // Create the browser window.
win = new BrowserWindow({ win = new BrowserWindow({
width: 377, width: 377,
height: 800, height: 800,
title: "Sengi", title: "Sengi",
backgroundColor: "#FFF", backgroundColor: "#FFF",
useContentSize: true useContentSize: true
}); });
var server = http.createServer(requestHandler).listen(9527); var server = http.createServer(requestHandler).listen(9527);
win.loadURL("http://localhost:9527"); win.loadURL("http://localhost:9527");
const template = [ const template = [
{
label: "View",
submenu: [
{ role: "reload" },
{ role: "forcereload" },
{ type: "separator" },
{ role: "close" },
{ role: 'quit' }
]
},
{
role: "help",
submenu: [
{ role: "toggledevtools" },
{ {
label: "Open GitHub project", label: "View",
click() { submenu: [
require("electron").shell.openExternal( { role: "reload" },
"https://github.com/NicolasConstant/sengi" { role: "forcereload" },
); { 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: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
{ role: "pasteandmatchstyle" },
{ role: "delete" },
{ role: "selectall" },
{ type: "separator" },
{ role: "close" },
{ role: 'quit' }
]
},
{
label: "View",
submenu: [{ role: "reload" }, { role: "forcereload" }]
},
{
role: "help",
submenu: [
{ role: "toggledevtools" },
{
label: "Open GitHub project",
click() {
require("electron").shell.openExternal(
"https://github.com/NicolasConstant/sengi"
);
}
}
]
}
])
);
} }
];
const menu = Menu.buildFromTemplate(template); // Open the DevTools.
win.setMenu(menu); // win.webContents.openDevTools()
// Check if we are on a MAC //open external links to browser
if (process.platform === "darwin") { win.webContents.on("new-window", function (event, url) {
// Create our menu entries so that we can use MAC shortcuts event.preventDefault();
Menu.setApplicationMenu( shell.openExternal(url);
Menu.buildFromTemplate([ });
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
{ role: "pasteandmatchstyle" },
{ role: "delete" },
{ role: "selectall" },
{ type: "separator" },
{ role: "close" },
{ role: 'quit' }
]
},
{
label: "View",
submenu: [{ role: "reload" }, { role: "forcereload" }]
},
{
role: "help",
submenu: [
{ role: "toggledevtools" },
{
label: "Open GitHub project",
click() {
require("electron").shell.openExternal(
"https://github.com/NicolasConstant/sengi"
);
}
}
]
}
])
);
}
// Open the DevTools. // Emitted when the window is closed.
// win.webContents.openDevTools() win.on("closed", () => {
// Dereference the window object, usually you would store windows
//open external links to browser // in an array if your app supports multi windows, this is the time
win.webContents.on("new-window", function(event, url) { // when you should delete the corresponding element.
event.preventDefault(); win = null;
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;
});
} }
function requestHandler(req, res) { function requestHandler(req, res) {
var file = req.url == "/" ? "/index.html" : req.url, var file = req.url == "/" ? "/index.html" : req.url,
root = __dirname + "/dist", root = __dirname + "/dist",
page404 = root + "/404.html"; page404 = root + "/404.html";
if (file.includes("register") || file.includes("home")) file = "/index.html"; if (file.includes("register") || file.includes("home")) file = "/index.html";
getFile(root + file, res, page404); getFile(root + file, res, page404);
} }
function getFile(filePath, res, page404) { function getFile(filePath, res, page404) {
console.warn(`filePath: ${filePath}`); console.warn(`filePath: ${filePath}`);
fs.exists(filePath, function(exists) { fs.exists(filePath, function (exists) {
if (exists) { if (exists) {
fs.readFile(filePath, function(err, contents) { fs.readFile(filePath, function (err, contents) {
if (!err) { if (!err) {
res.end(contents); res.end(contents);
} else {
console.dir(err);
}
});
} else { } else {
console.dir(err); fs.readFile(page404, function (err, contents) {
if (!err) {
res.writeHead(404, { "Content-Type": "text/html" });
res.end(contents);
} else {
console.dir(err);
}
});
} }
}); });
} else {
fs.readFile(page404, function(err, contents) {
if (!err) {
res.writeHead(404, { "Content-Type": "text/html" });
res.end(contents);
} else {
console.dir(err);
}
});
}
});
} }
app.commandLine.appendSwitch("force-color-profile", "srgb"); app.commandLine.appendSwitch("force-color-profile", "srgb");
// This method will be called when Electron has finished
// initialization and is ready to create browser windows. const gotTheLock = app.requestSingleInstanceLock();
// Some APIs can only be used after this event occurs.
app.on("ready", createWindow); 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. // Quit when all windows are closed.
app.on("window-all-closed", () => { app.on("window-all-closed", () => {
// On macOS it is common for applications and their menu bar // On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q // to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== "darwin") { if (process.platform !== "darwin") {
app.quit(); app.quit();
} }
}); });
app.on("activate", () => { app.on("activate", () => {
// On macOS it's common to re-create a window in the app when the // 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. // dock icon is clicked and there are no other windows open.
if (win === null) { if (win === null) {
createWindow(); createWindow();
} }
}); });
// In this file you can include the rest of your app's specific main process // In this file you can include the rest of your app's specific main process

View File

@ -1,6 +1,6 @@
{ {
"name": "sengi", "name": "sengi",
"version": "0.10.1", "version": "0.11.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",

View File

@ -20,7 +20,14 @@ import { identifierModuleUrl } from '@angular/compiler';
styleUrls: ['./create-status.component.scss'] styleUrls: ['./create-status.component.scss']
}) })
export class CreateStatusComponent implements OnInit, OnDestroy { export class CreateStatusComponent implements OnInit, OnDestroy {
title: string; private _title: string;
set title(value: string){
this._title = value;
this.countStatusChar(this.status);
}
get title(): string {
return this._title;
}
private _status: string = ''; private _status: string = '';
set status(value: string) { set status(value: string) {
@ -106,14 +113,21 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private accountChanged(accounts: AccountInfo[]): void { private accountChanged(accounts: AccountInfo[]): void {
if (accounts && accounts.length > 0) { if (accounts && accounts.length > 0) {
const selectedAccount = accounts.filter(x => x.isSelected)[0]; const selectedAccount = accounts.filter(x => x.isSelected)[0];
this.instancesInfoService.getMaxStatusChars(selectedAccount.instance)
.then((maxChars: number) => { const settings = this.toolsService.getAccountSettings(selectedAccount);
this.maxCharLength = maxChars; if (settings.customStatusCharLengthEnabled) {
this.countStatusChar(this.status); this.maxCharLength = settings.customStatusCharLength;
}) this.countStatusChar(this.status);
.catch((err: HttpErrorResponse) => { } else {
this.notificationService.notifyHttpError(err); this.instancesInfoService.getMaxStatusChars(selectedAccount.instance)
}); .then((maxChars: number) => {
this.maxCharLength = maxChars;
this.countStatusChar(this.status);
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
}
if (!this.statusReplyingToWrapper) { if (!this.statusReplyingToWrapper) {
this.instancesInfoService.getDefaultPrivacy(selectedAccount) this.instancesInfoService.getDefaultPrivacy(selectedAccount)
@ -169,10 +183,18 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
const statusExtraChars = this.getMentionExtraChars(status); const statusExtraChars = this.getMentionExtraChars(status);
const statusLength = currentStatus.length - statusExtraChars; const statusLength = currentStatus.length - statusExtraChars;
this.charCountLeft = this.maxCharLength - statusLength; this.charCountLeft = this.maxCharLength - statusLength - this.getCwLength();
this.postCounts = parseStatus.length; this.postCounts = parseStatus.length;
} }
private getCwLength(): number {
let cwLength = 0;
if (this.title) {
cwLength = this.title.length;
}
return cwLength;
}
private getMentions(status: Status, providerInfo: AccountInfo): string[] { private getMentions(status: Status, providerInfo: AccountInfo): string[] {
const mentions = [...status.mentions.map(x => x.acct), status.account.acct]; const mentions = [...status.mentions.map(x => x.acct), status.account.acct];
@ -238,9 +260,6 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
return this.sendStatus(acc, this.status, visibility, this.title, status, mediaAttachments); return this.sendStatus(acc, this.status, visibility, this.title, status, mediaAttachments);
}) })
.then((res: Status) => { .then((res: Status) => {
if (this.statusReplyingToWrapper) {
this.notificationService.newStatusPosted(this.statusReplyingToWrapper.status.id, new StatusWrapper(res, acc));
}
this.title = ''; this.title = '';
this.status = ''; this.status = '';
this.onClose.emit(); this.onClose.emit();
@ -261,22 +280,30 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
for (let i = 0; i < parsedStatus.length; i++) { for (let i = 0; i < parsedStatus.length; i++) {
let s = parsedStatus[i]; let s = parsedStatus[i];
resultPromise = resultPromise.then((pStatus: Status) => { resultPromise = resultPromise
let inReplyToId = null; .then((pStatus: Status) => {
if (pStatus) { let inReplyToId = null;
inReplyToId = pStatus.id; if (pStatus) {
} inReplyToId = pStatus.id;
}
if (i === 0) { if (i === 0) {
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id)) return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id))
.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, []); return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, []);
} }
}); })
.then((status: Status) => {
if (this.statusReplyingToWrapper) {
this.notificationService.newStatusPosted(this.statusReplyingToWrapper.status.id, new StatusWrapper(status, account));
}
return status;
});
} }
return resultPromise; return resultPromise;
@ -293,7 +320,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
aggregateMention += `${x} `; aggregateMention += `${x} `;
}); });
const currentMaxCharLength = this.maxCharLength + mentionExtraChars; const currentMaxCharLength = this.maxCharLength + mentionExtraChars - this.getCwLength();
const maxChars = currentMaxCharLength - 6; const maxChars = currentMaxCharLength - 6;
while (trucatedStatus.length > currentMaxCharLength) { while (trucatedStatus.length > currentMaxCharLength) {

View File

@ -2,7 +2,9 @@
<h3 class="panel__title">Manage Account</h3> <h3 class="panel__title">Manage Account</h3>
<div class="account__header"> <div class="account__header">
<img class="account__avatar" src="{{account.avatar}}" title="{{ account.info.id }} " /> <a href (click)="browseLocalAccount()" (auxclick)="openLocalAccount()" title="open {{ account.info.id }}">
<img class="account__avatar" src="{{account.avatar}}"/>
</a>
<!-- <a href class="account__header--button"><fa-icon [icon]="faUserPlus"></fa-icon></a> --> <!-- <a href class="account__header--button"><fa-icon [icon]="faUserPlus"></fa-icon></a> -->
<a href class="account__header--button" title="favorites" (click)="loadSubPanel('favorites')" <a href class="account__header--button" title="favorites" (click)="loadSubPanel('favorites')"
@ -17,8 +19,7 @@
[ngClass]="{ 'account__header--button--selected': subPanel === 'mentions', 'account__header--button--notification': hasMentions }"> [ngClass]="{ 'account__header--button--selected': subPanel === 'mentions', 'account__header--button--notification': hasMentions }">
<fa-icon [icon]="faAt"></fa-icon> <fa-icon [icon]="faAt"></fa-icon>
</a> </a>
<a href class="account__header--button" title="notifications" (click)="loadSubPanel('notifications')" <a href class="account__header--button" title="notifications" (click)="loadSubPanel('notifications')" [ngClass]="{ 'account__header--button--selected': subPanel === 'notifications',
[ngClass]="{ 'account__header--button--selected': subPanel === 'notifications',
'account__header--button--notification': hasNotifications }"> 'account__header--button--notification': hasNotifications }">
<fa-icon [icon]="faBell"></fa-icon> <fa-icon [icon]="faBell"></fa-icon>
</a> </a>
@ -27,27 +28,18 @@
<fa-icon [icon]="faUser"></fa-icon> <fa-icon [icon]="faUser"></fa-icon>
</a> </a>
</div> </div>
<app-direct-messages class="account__body" *ngIf="subPanel === 'dm'" <app-direct-messages class="account__body" *ngIf="subPanel === 'dm'" [account]="account"
[account]="account" (browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-direct-messages> (browseThreadEvent)="browseThread($event)"></app-direct-messages>
<app-favorites class="account__body" *ngIf="subPanel === 'favorites'" <app-favorites class="account__body" *ngIf="subPanel === 'favorites'" [account]="account"
[account]="account" (browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-favorites> (browseThreadEvent)="browseThread($event)"></app-favorites>
<app-mentions class="account__body" *ngIf="subPanel === 'mentions'" <app-mentions class="account__body" *ngIf="subPanel === 'mentions'" [account]="account"
[account]="account" (browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-mentions> (browseThreadEvent)="browseThread($event)"></app-mentions>
<app-my-account class="account__body" *ngIf="subPanel === 'account'" <app-my-account class="account__body" *ngIf="subPanel === 'account'" [account]="account"></app-my-account>
[account]="account"></app-my-account> <app-notifications class="account__body" *ngIf="subPanel === 'notifications'" [account]="account"
<app-notifications class="account__body" *ngIf="subPanel === 'notifications'" (browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
[account]="account"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-notifications> (browseThreadEvent)="browseThread($event)"></app-notifications>
</div> </div>

View File

@ -6,6 +6,10 @@ import { Subscription } from 'rxjs';
import { AccountWrapper } from '../../../models/account.models'; import { AccountWrapper } from '../../../models/account.models';
import { UserNotificationService, UserNotification } from '../../../services/user-notification.service'; import { UserNotificationService, UserNotification } from '../../../services/user-notification.service';
import { OpenThreadEvent } from '../../../services/tools.service'; import { OpenThreadEvent } from '../../../services/tools.service';
import { MastodonService } from '../../../services/mastodon.service';
import { Account } from "../../../services/models/mastodon.interfaces";
import { NotificationService } from '../../../services/notification.service';
import { AccountInfo } from '../../../states/accounts.state';
@Component({ @Component({
@ -25,6 +29,8 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
hasNotifications = false; hasNotifications = false;
hasMentions = false; hasMentions = false;
userAccount: Account;
@Output() browseAccountEvent = new EventEmitter<string>(); @Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>(); @Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>(); @Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@ -33,6 +39,7 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
set account(acc: AccountWrapper) { set account(acc: AccountWrapper) {
this._account = acc; this._account = acc;
this.checkNotifications(); this.checkNotifications();
this.getUserUrl(acc.info);
} }
get account(): AccountWrapper { get account(): AccountWrapper {
return this._account; return this._account;
@ -42,6 +49,8 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
private _account: AccountWrapper; private _account: AccountWrapper;
constructor( constructor(
private readonly mastodonService: MastodonService,
private readonly notificationService: NotificationService,
private readonly userNotificationService: UserNotificationService) { } private readonly userNotificationService: UserNotificationService) { }
ngOnInit() { ngOnInit() {
@ -52,6 +61,16 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
this.userNotificationServiceSub.unsubscribe(); this.userNotificationServiceSub.unsubscribe();
} }
private getUserUrl(account: AccountInfo){
this.mastodonService.retrieveAccountDetails(this.account.info)
.then((acc: Account) => {
this.userAccount = acc;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
});
}
private checkNotifications(){ private checkNotifications(){
if(this.userNotificationServiceSub){ if(this.userNotificationServiceSub){
this.userNotificationServiceSub.unsubscribe(); this.userNotificationServiceSub.unsubscribe();
@ -75,6 +94,17 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
this.browseAccountEvent.next(accountName); this.browseAccountEvent.next(accountName);
} }
browseLocalAccount(): boolean {
var accountName = `@${this.account.info.username}@${this.account.info.instance}`;
this.browseAccountEvent.next(accountName);
return false;
}
openLocalAccount(): boolean {
window.open(this.userAccount.url, '_blank');
return false;
}
browseHashtag(hashtag: string): void { browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag); this.browseHashtagEvent.next(hashtag);
} }

View File

@ -9,21 +9,21 @@
<h4 class="my-account__label my-account__margin-top">manage list:</h4> <h4 class="my-account__label my-account__margin-top">manage list:</h4>
<div class="my-account__link--margin-bottom" *ngFor="let list of availableLists"> <div class="my-account__link--margin-bottom" *ngFor="let list of availableLists">
<a href class="my-account__list--button" title="delete list" <a href class="my-account__list--button" title="delete list" (click)="openCloseDeleteConfirmation(list, true)"
(click)="openCloseDeleteConfirmation(list, true)" *ngIf="!list.confirmDeletion"> *ngIf="!list.confirmDeletion">
<fa-icon class="my-account__link--icon" [icon]="faTrash"></fa-icon> <fa-icon class="my-account__link--icon" [icon]="faTrash"></fa-icon>
</a> </a>
<a href class="my-account__list--button" title="edit list" <a href class="my-account__list--button" title="edit list" (click)="editList(list)"
(click)="editList(list)" *ngIf="!list.confirmDeletion"> *ngIf="!list.confirmDeletion">
<fa-icon class="my-account__link--icon" [icon]="faPenAlt"></fa-icon> <fa-icon class="my-account__link--icon" [icon]="faPenAlt"></fa-icon>
</a> </a>
<a href class="my-account__list--button" title="cancel" <a href class="my-account__list--button" title="cancel" (click)="openCloseDeleteConfirmation(list, false)"
(click)="openCloseDeleteConfirmation(list, false)" *ngIf="list.confirmDeletion"> *ngIf="list.confirmDeletion">
<fa-icon class="my-account__link--icon" [icon]="faTimes"></fa-icon> <fa-icon class="my-account__link--icon" [icon]="faTimes"></fa-icon>
</a> </a>
<a href class="my-account__list--button my-account__red" title="delete list" <a href class="my-account__list--button my-account__red" title="delete list" (click)="deleteList(list)"
(click)="deleteList(list)" *ngIf="list.confirmDeletion"> *ngIf="list.confirmDeletion">
<fa-icon class="my-account__link--icon" [icon]="faCheck"></fa-icon> <fa-icon class="my-account__link--icon" [icon]="faCheck"></fa-icon>
</a> </a>
@ -38,7 +38,17 @@
<a href class="my-account__list--button" title="create list" (click)="createList()"> <a href class="my-account__list--button" title="create list" (click)="createList()">
<fa-icon class="my-account__link--icon" [icon]="faPlus"></fa-icon> <fa-icon class="my-account__link--icon" [icon]="faPlus"></fa-icon>
</a> </a>
<input class="my-account__list--new-list-title" placeholder="new list title" [(ngModel)]="listTitle" (keyup.enter)="createList()" [disabled]="creationLoading"/> <input class="my-account__list--new-list-title" placeholder="new list title" [(ngModel)]="listTitle"
(keyup.enter)="createList()" [disabled]="creationLoading" />
<h4 class="my-account__label my-account__margin-top">advanced settings:</h4>
<div class="advanced-settings">
<input class="advanced-settings__checkbox" [(ngModel)]="customStatusLengthEnabled" (change)="onCustomLengthEnabledChanged()"
type="checkbox" name="customCharLength" value="customCharLength" id="customCharLength"> <label class="noselect advanced-settings__label" for="customCharLength">status'
custom length</label><br>
<p *ngIf="customStatusLengthEnabled" class="advanced-settings__text">use this only if your instance doesn't support custom length detection (i.e. not a Pleroma or glitch-soc instance)</p>
<input *ngIf="customStatusLengthEnabled" [(ngModel)]="customStatusLength" class="themed-form advanced-settings__input" type="number" (keyup)="customStatusLengthChanged($event)" />
</div>
<h4 class="my-account__label my-account__margin-top">remove account from sengi:</h4> <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()">

View File

@ -102,4 +102,30 @@
&__margin-top { &__margin-top {
margin-top: 25px; margin-top: 25px;
} }
}
.advanced-settings {
position: relative;
&__checkbox{
position: relative;
top:3px;
left: 5px;
margin-right: 7px;
}
&__label {
}
&__text {
display: block;
margin: 0 6px 9px 6px;
color: rgb(140, 152, 173);
}
&__input {
margin-left: 5px;
}
} }

View File

@ -10,6 +10,8 @@ import { AccountWrapper } from '../../../../models/account.models';
import { RemoveAccount } from '../../../../states/accounts.state'; import { RemoveAccount } from '../../../../states/accounts.state';
import { NavigationService } from '../../../../services/navigation.service'; import { NavigationService } from '../../../../services/navigation.service';
import { MastodonService } from '../../../../services/mastodon.service'; import { MastodonService } from '../../../../services/mastodon.service';
import { ToolsService } from '../../../../services/tools.service';
import { AccountSettings } from '../../../../states/settings.state';
@Component({ @Component({
selector: 'app-my-account', selector: 'app-my-account',
@ -23,6 +25,10 @@ export class MyAccountComponent implements OnInit, OnDestroy {
faCheckSquare = faCheckSquare; faCheckSquare = faCheckSquare;
faCheck = faCheck; faCheck = faCheck;
faTimes = faTimes; faTimes = faTimes;
customStatusLengthEnabled: boolean;
customStatusLength: number;
private accountSettings: AccountSettings;
availableStreams: StreamWrapper[] = []; availableStreams: StreamWrapper[] = [];
availableLists: StreamWrapper[] = []; availableLists: StreamWrapper[] = [];
@ -32,6 +38,7 @@ export class MyAccountComponent implements OnInit, OnDestroy {
set account(acc: AccountWrapper) { set account(acc: AccountWrapper) {
this._account = acc; this._account = acc;
this.loadStreams(acc); this.loadStreams(acc);
this.loadAccountSettings();
} }
get account(): AccountWrapper { get account(): AccountWrapper {
return this._account; return this._account;
@ -42,6 +49,7 @@ export class MyAccountComponent implements OnInit, OnDestroy {
constructor( constructor(
private readonly store: Store, private readonly store: Store,
private readonly toolsService: ToolsService,
private readonly navigationService: NavigationService, private readonly navigationService: NavigationService,
private readonly mastodonService: MastodonService, private readonly mastodonService: MastodonService,
private readonly notificationService: NotificationService) { } private readonly notificationService: NotificationService) { }
@ -49,7 +57,7 @@ export class MyAccountComponent implements OnInit, OnDestroy {
ngOnInit() { ngOnInit() {
this.streamChangedSub = this.streamElements$.subscribe((streams: StreamElement[]) => { this.streamChangedSub = this.streamElements$.subscribe((streams: StreamElement[]) => {
this.loadStreams(this.account); this.loadStreams(this.account);
}); });
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -58,6 +66,24 @@ export class MyAccountComponent implements OnInit, OnDestroy {
} }
} }
private loadAccountSettings(){
this.accountSettings = this.toolsService.getAccountSettings(this.account.info);
this.customStatusLengthEnabled = this.accountSettings.customStatusCharLengthEnabled; this.customStatusLength = this.accountSettings.customStatusCharLength;
}
onCustomLengthEnabledChanged(): boolean {
this.accountSettings.customStatusCharLengthEnabled = this.customStatusLengthEnabled;
this.toolsService.saveAccountSettings(this.accountSettings);
return false;
}
customStatusLengthChanged(event): boolean{
this.accountSettings.customStatusCharLength = this.customStatusLength;
this.toolsService.saveAccountSettings(this.accountSettings);
return false;
}
private loadStreams(account: AccountWrapper){ private loadStreams(account: AccountWrapper){
const instance = account.info.instance; const instance = account.info.instance;
this.availableStreams.length = 0; this.availableStreams.length = 0;

View File

@ -1,23 +1,34 @@
<div class="media-viewer-canvas" (click)="close()"> <div class="media-viewer-canvas noselect">
<button class="media-viewer-canvas__close media-viewer-canvas__button" title="close"> <div class="background__close" (click)="close()"></div>
<button class="media-viewer-canvas__close media-viewer-canvas__button" title="close" (click)="close()">
<fa-icon [icon]="faTimes"></fa-icon> <fa-icon [icon]="faTimes"></fa-icon>
</button> </button>
<button class="media-viewer-canvas__previous media-viewer-canvas__button" title="previous" (click)="previous($event)" *ngIf="previousAvailable"> <button class="media-viewer-canvas__previous media-viewer-canvas__button" title="previous"
(click)="previous($event)" *ngIf="previousAvailable">
<fa-icon [icon]="faAngleLeft"></fa-icon> <fa-icon [icon]="faAngleLeft"></fa-icon>
</button> </button>
<button class="media-viewer-canvas__next media-viewer-canvas__button" title="next" (click)="next($event)" *ngIf="nextAvailable"> <button class="media-viewer-canvas__next media-viewer-canvas__button" title="next" (click)="next($event)"
*ngIf="nextAvailable">
<fa-icon [icon]="faAngleRight"></fa-icon> <fa-icon [icon]="faAngleRight"></fa-icon>
</button> </button>
<img class="media-viewer-canvas__image" *ngIf="imageUrl" src="{{imageUrl}}" (click)="blockClick($event)"/> <div *ngFor="let att of attachments" class="media-viewer-canvas__attachement"
<video class="media-viewer-canvas__image" *ngIf="gifvUrl" role="application" loop autoplay (click)="blockClick($event)"> [ngClass]="{ 'collapsed': currentIndex !== att.index }">
<source src="{{ gifvUrl }}" type="video/mp4"> <a href="{{att.url}}" target="_blank" title="open image">
</video> <img *ngIf="att.type === 'image'" src="{{att.url}}" class="media-viewer-canvas__image" />
<video class="media-viewer-canvas__image" *ngIf="videoUrl" role="application" loop controls="controls" (click)="blockClick($event)"> </a>
<source src="{{ videoUrl }}" type="video/mp4">
</video> <video *ngIf="att.type === 'gifv'" class="media-viewer-canvas__image" role="application" loop autoplay>
<source src="{{att.url}}" type="video/mp4">
</video>
<video *ngIf="att.type === 'video'" class="media-viewer-canvas__image" role="application" loop autoplay
controls="controls">
<source src="{{att.url}}" type="video/mp4">
</video>
</div>
<div #video *ngIf="html" class="media-viewer-canvas__image media-viewer-canvas__iframe" [innerHTML]="html"> <div #video *ngIf="html" class="media-viewer-canvas__image media-viewer-canvas__iframe" [innerHTML]="html">
</div> </div>

View File

@ -1,13 +1,14 @@
@import "variables"; @import "variables";
@import "commons";
@import "mixins"; @import "mixins";
.media-viewer-canvas { .media-viewer-canvas {
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.8); // background-color: rgba(0, 0, 0, 0.8);
overflow: hidden; overflow: hidden;
position: relative; position: relative;
&__button { &__button {
@include clearButton; @include clearButton;
padding: 5px; padding: 5px;
@ -60,7 +61,14 @@
padding: 10px; padding: 10px;
} }
&__attachement {
// max-width: 100%;
// height: 100vh;
}
&__image { &__image {
z-index: 10;
@media screen and (min-width: $screen-break) { @media screen and (min-width: $screen-break) {
max-width: 85%; max-width: 85%;
} }
@ -72,6 +80,8 @@
margin-top: 50vh; margin-top: 50vh;
margin-left: 50vw; margin-left: 50vw;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
visibility: visible;
} }
&__iframe { &__iframe {
@ -80,4 +90,18 @@
max-height: 600px; max-height: 600px;
max-width: 950px; max-width: 950px;
} }
} }
.background__close {
position: absolute;
top:0;
bottom: 0;
left: 0;
right: 0;
z-index: 0;
background-color: rgba(0, 0, 0, 0.8);
}
.collapsed {
height: 0;
}

View File

@ -18,26 +18,30 @@ export class MediaViewerComponent implements OnInit {
faAngleLeft = faAngleLeft; faAngleLeft = faAngleLeft;
faAngleRight = faAngleRight; faAngleRight = faAngleRight;
imageUrl: string; attachments: AttachmentsWrapper[] = [];
gifvUrl: string;
videoUrl: string;
html: SafeHtml; html: SafeHtml;
previousAvailable: boolean; previousAvailable: boolean;
nextAvailable: boolean; nextAvailable: boolean;
private currentIndex: number; currentIndex: number;
@Input('openedMediaEvent') @Input('openedMediaEvent')
set openedMediaEvent(value: OpenMediaEvent) { set openedMediaEvent(value: OpenMediaEvent) {
this._mediaEvent = value; this._mediaEvent = value;
this.attachments.length = 0;
if (value.iframe) { if (value.iframe) {
this.html = value.iframe; this.html = value.iframe;
this.autoplayIframe(); this.autoplayIframe();
} else { } else {
const attachment = value.attachments[value.selectedIndex];
for(let i = 0; i < value.attachments.length; i++){
let att = value.attachments[i];
this.attachments.push(new AttachmentsWrapper(att, i));
}
this.currentIndex = value.selectedIndex; this.currentIndex = value.selectedIndex;
this.loadAttachment(attachment);
this.setBrowsing(); this.setBrowsing();
} }
} }
@ -52,7 +56,7 @@ export class MediaViewerComponent implements OnInit {
handleKeyboardEvent(event: KeyboardEvent) { handleKeyboardEvent(event: KeyboardEvent) {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
if (event.key === 'ArrowRight') { if (event.key === 'ArrowRight') {
this.next(event); this.next(event);
} else if (event.key === 'ArrowLeft') { } else if (event.key === 'ArrowLeft') {
@ -65,24 +69,10 @@ export class MediaViewerComponent implements OnInit {
ngOnInit() { ngOnInit() {
} }
private loadAttachment(attachment: Attachment) {
if (attachment.type === 'image') {
this.imageUrl = attachment.url;
} else if (attachment.type === 'gifv') {
this.gifvUrl = attachment.url;
} else if (attachment.type === 'video') {
this.videoUrl = attachment.url;
}
}
private setBrowsing() { private setBrowsing() {
var index = this.currentIndex; var index = this.currentIndex;
var attachments = this.openedMediaEvent.attachments;
console.log(`index ${index}`); if (index < this.attachments.length - 1) {
console.log(`attachments.length ${attachments.length}`);
if (index < attachments.length - 1) {
this.nextAvailable = true; this.nextAvailable = true;
} else { } else {
this.nextAvailable = false; this.nextAvailable = false;
@ -120,7 +110,6 @@ export class MediaViewerComponent implements OnInit {
if (this.currentIndex <= 0) return false; if (this.currentIndex <= 0) return false;
this.currentIndex--; this.currentIndex--;
this.imageUrl = this.openedMediaEvent.attachments[this.currentIndex].url;
this.setBrowsing(); this.setBrowsing();
return false; return false;
@ -131,9 +120,34 @@ export class MediaViewerComponent implements OnInit {
if (this.currentIndex >= this.openedMediaEvent.attachments.length - 1) return false; if (this.currentIndex >= this.openedMediaEvent.attachments.length - 1) return false;
this.currentIndex++; this.currentIndex++;
this.imageUrl = this.openedMediaEvent.attachments[this.currentIndex].url;
this.setBrowsing(); this.setBrowsing();
return false; return false;
} }
} }
class AttachmentsWrapper implements Attachment {
constructor(attachment: Attachment, index: number) {
this.id = attachment.id;
this.type = attachment.type;
this.url = attachment.url;
this.remote_url = attachment.remote_url;
this.preview_url = attachment.preview_url;
this.text_url = attachment.text_url;
this.meta = attachment.meta;
this.description = attachment.description;
this.index = index;
}
id: string;
type: "image" | "video" | "gifv";
url: string;
remote_url: string;
preview_url: string;
text_url: string;
meta: any;
description: string;
index: number;
}

View File

@ -1,7 +1,8 @@
<div class="poll"> <div class="poll">
<div *ngIf="!poll.voted && !poll.expired"> <div *ngIf="!poll.voted && !poll.expired">
<div *ngFor="let o of options"> <div *ngFor="let o of options">
<label class="poll__container">{{o.title}} <label class="poll__container">
<span class="poll__container__title">{{o.title}}</span>
<input class="poll__container__input" type="{{choiceType}}" name="{{pollName}}" value="{{o.title}}" <input class="poll__container__input" type="{{choiceType}}" name="{{pollName}}" value="{{o.title}}"
(change)="onSelectionChange(o)"> (change)="onSelectionChange(o)">
<span class="poll__container__checkmark" *ngIf="!pollLocked" <span class="poll__container__checkmark" *ngIf="!pollLocked"
@ -15,7 +16,7 @@
<div class="poll__result" title="{{ o.votes_count }} votes"> <div class="poll__result" title="{{ o.votes_count }} votes">
<div class="poll__result--progress-bar" [style.width]="o.percentage + '%'" [ngClass]="{ 'poll__result--progress-bar--most-votes': o.isMax }"></div> <div class="poll__result--progress-bar" [style.width]="o.percentage + '%'" [ngClass]="{ 'poll__result--progress-bar--most-votes': o.isMax }"></div>
<div class="poll__result--data"> <span class="poll__result--percentage">{{ o.percentage }}%</span> <div class="poll__result--data"> <span class="poll__result--percentage">{{ o.percentage }}%</span>
{{o.title}}</div> <span class="poll__container__title">{{o.title}}</span></div>
</div> </div>
</div> </div>

View File

@ -1,10 +1,10 @@
// @import "variables"; @import "variables";
// @import "commons"; @import "commons";
// @import "panel"; // @import "panel";
@import "buttons"; @import "buttons";
.poll { .poll {
color: white; color: white;
color: rgb(228, 228, 228); color: rgb(228, 228, 228);
@ -44,6 +44,11 @@
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
&__title {
text-overflow: ellipsis;
white-space: nowrap;
}
&:hover &__input~&__checkmark { &:hover &__input~&__checkmark {
background-color: #ccc; background-color: #ccc;
} }
@ -125,7 +130,7 @@
&__result { &__result {
transition: width 2s; transition: width 2s;
margin: 0 0 5px 5px; margin: 0 0 5px 5px;
padding: 0 5px 0 5px; padding: 0 5px 0 5px;
position: relative; position: relative;
@ -141,15 +146,15 @@
color: white; color: white;
display: inline; display: inline;
margin-right: 10px; margin-right: 10px;
} }
&--progress-bar { &--progress-bar {
position: absolute; position: absolute;
background-color: rgb(47, 68, 100); background-color: rgb(47, 68, 100);
// background-color: rgb(43, 62, 92); // background-color: rgb(43, 62, 92);
top:0; top: 0;
left:0; left: 0;
width: calc(100%); width: calc(100%);
height: 22px; height: 22px;
z-index: 1; z-index: 1;
@ -166,19 +171,4 @@
display: inline-block; display: inline-block;
margin: 0 5px; margin: 0 5px;
} }
}
.noselect {
-webkit-touch-callout: none;
/* iOS Safari */
-webkit-user-select: none;
/* Safari */
-khtml-user-select: none;
/* Konqueror HTML */
-moz-user-select: none;
/* Firefox */
-ms-user-select: none;
/* Internet Explorer/Edge */
user-select: none;
/* Non-prefixed version, currently supported by Chrome and Opera */
} }

View File

@ -53,8 +53,8 @@ export class PollComponent implements OnInit {
return this._poll; return this._poll;
} }
@Input() provider: AccountInfo; // @Input() provider: AccountInfo;
@Input() status: Status; @Input() statusWrapper: StatusWrapper;
private accounts$: Observable<AccountInfo[]>; private accounts$: Observable<AccountInfo[]>;
private accountSub: Subscription; private accountSub: Subscription;
@ -71,8 +71,8 @@ export class PollComponent implements OnInit {
} }
ngOnInit() { ngOnInit() {
this.pollPerAccountId[this.provider.id] = Promise.resolve(this.poll); this.pollPerAccountId[this.statusWrapper.provider.id] = Promise.resolve(this.poll);
this.selectedAccount = this.provider; this.selectedAccount = this.statusWrapper.provider;
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => { this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
this.checkStatus(accounts); this.checkStatus(accounts);
@ -84,10 +84,10 @@ export class PollComponent implements OnInit {
var newSelectedAccount = accounts.find(x => x.isSelected); var newSelectedAccount = accounts.find(x => x.isSelected);
const accountChanged = this.selectedAccount.id !== newSelectedAccount.id; const accountChanged = this.selectedAccount.id !== newSelectedAccount.id;
if (accountChanged && !this.pollPerAccountId[newSelectedAccount.id] && (this.status.visibility === 'public' || this.status.visibility === 'unlisted')) { if (accountChanged && !this.pollPerAccountId[newSelectedAccount.id] && (this.statusWrapper.status.visibility === 'public' || this.statusWrapper.status.visibility === 'unlisted')) {
this.setStatsAtZero(); this.setStatsAtZero();
this.pollPerAccountId[newSelectedAccount.id] = this.toolsService.getStatusUsableByAccount(newSelectedAccount, new StatusWrapper(this.status, this.provider)) this.pollPerAccountId[newSelectedAccount.id] = this.toolsService.getStatusUsableByAccount(newSelectedAccount, new StatusWrapper(this.statusWrapper.status, this.statusWrapper.provider))
.then((status: Status) => { .then((status: Status) => {
return this.mastodonService.getPoll(newSelectedAccount, status.poll.id); return this.mastodonService.getPoll(newSelectedAccount, status.poll.id);
}) })
@ -99,7 +99,7 @@ export class PollComponent implements OnInit {
this.notificationService.notifyHttpError(err); this.notificationService.notifyHttpError(err);
return null; return null;
}); });
} else if (this.status.visibility !== 'public' && this.status.visibility !== 'unlisted' && this.provider.id !== newSelectedAccount.id) { } else if (this.statusWrapper.status.visibility !== 'public' && this.statusWrapper.status.visibility !== 'unlisted' && this.statusWrapper.provider.id !== newSelectedAccount.id) {
this.pollLocked = true; this.pollLocked = true;
} else { } else {
this.pollPerAccountId[newSelectedAccount.id] this.pollPerAccountId[newSelectedAccount.id]

View File

@ -1,7 +1,7 @@
<div class="reblog" *ngIf="reblog"> <div class="reblog" *ngIf="reblog">
<a class="reblog__profile-link" href (click)="openAccount(status.account)"><span <a class="reblog__profile-link" href (click)="openAccount(status.account)"
innerHTML="{{ status.account | accountEmoji }}"></span> <img *ngIf="reblog" class="reblog__avatar" (auxclick)="openUrl(status.account.url)"><span innerHTML="{{ status.account | accountEmoji }}"></span> <img
src="{{ status.account.avatar }}" /></a> boosted *ngIf="reblog" class="reblog__avatar" src="{{ status.account.avatar }}" /></a> boosted
</div> </div>
<div *ngIf="notificationType === 'favourite'"> <div *ngIf="notificationType === 'favourite'">
<div class="notification--icon"> <div class="notification--icon">
@ -30,9 +30,12 @@
</div> </div>
</div> </div>
<div class="status"> <div class="status">
<a href class="status__navigation" title="open status" (click)="textSelected()">
</a>
<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)"> (click)="openAccount(displayedStatus.account)" (auxclick)="openUrl(displayedStatus.account.url)">
<img [class.status__avatar--boosted]="reblog || notificationAccount" class="status__avatar" <img [class.status__avatar--boosted]="reblog || notificationAccount" class="status__avatar"
src="{{ displayedStatus.account.avatar }}" /> src="{{ displayedStatus.account.avatar }}" />
<!-- <img *ngIf="reblog" class="status__avatar--reblog" src="{{ status.account.avatar }}" /> --> <!-- <img *ngIf="reblog" class="status__avatar--reblog" src="{{ status.account.avatar }}" /> -->
@ -44,12 +47,12 @@
</span> </span>
</a> </a>
<div class="status__created-at" title="{{ displayedStatus.created_at | date: 'full' }}"> <div class="status__created-at" title="{{ displayedStatus.created_at | date: 'full' }}">
<a href class="status__created-at--link" (click)="textSelected()" (auxclick)="openUrl()"> <a href class="status__created-at--link" (click)="textSelected()" (auxclick)="openUrl(displayedStatus.url)">
{{ status.created_at | timeAgo | async }} {{ displayedStatus.created_at | timeAgo | async }}
</a> </a>
</div> </div>
<div class="status__labels"> <div class="status__labels">
<div class="status__labels--label status__labels--bot" title="bot" *ngIf="status.account.bot"> <div class="status__labels--label status__labels--bot" title="bot" *ngIf="displayedStatus.account.bot">
bot bot
</div> </div>
<div class="status__labels--label status__labels--xpost" title="this status was cross-posted" <div class="status__labels--label status__labels--xpost" title="this status was cross-posted"
@ -63,7 +66,12 @@
*ngIf="hasReply"> *ngIf="hasReply">
replies replies
</div> </div>
<div class="status__labels--label status__labels--old" title="this status is old" *ngIf="isOld">
old
</div>
</div> </div>
<!-- <div #content class="status__content" innerHTML="{{displayedStatus.content}}"></div> --> <!-- <div #content class="status__content" innerHTML="{{displayedStatus.content}}"></div> -->
<a href class="status__content-warning" *ngIf="isContentWarned" title="show content" <a href class="status__content-warning" *ngIf="isContentWarned" title="show content"
@ -75,18 +83,20 @@
(accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)" (accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)"
(textSelected)="textSelected()"></app-databinded-text> (textSelected)="textSelected()"></app-databinded-text>
<app-poll class="status__poll" *ngIf="!isContentWarned && displayedStatus.poll" <app-poll class="status__poll" *ngIf="!isContentWarned && displayedStatus.poll" [poll]="displayedStatus.poll"
[poll]="displayedStatus.poll" [status]="displayedStatus" [provider]="statusWrapper.provider"></app-poll> [statusWrapper]="displayedStatusWrapper"></app-poll>
<app-card class="status__card" *ngIf="!isContentWarned && displayedStatus.card && !hasAttachments" [card]="displayedStatus.card"></app-card> <app-card class="status__card" *ngIf="!isContentWarned && displayedStatus.card && !hasAttachments"
[card]="displayedStatus.card"></app-card>
<app-attachements *ngIf="!isContentWarned && hasAttachments" class="attachments" <app-attachements *ngIf="!isContentWarned && hasAttachments" class="attachments"
[attachments]="displayedStatus.media_attachments"> [attachments]="displayedStatus.media_attachments">
</app-attachements> </app-attachements>
<app-action-bar #appActionBar [statusWrapper]="statusWrapper" (cwIsActiveEvent)="changeCw($event)" <app-action-bar #appActionBar [statusWrapper]="displayedStatusWrapper" (cwIsActiveEvent)="changeCw($event)"
(replyEvent)="openReply()"></app-action-bar> (replyEvent)="openReply()"></app-action-bar>
</div> </div>
<app-create-status *ngIf="replyingToStatus" [statusReplyingToWrapper]="statusWrapper" (onClose)="closeReply()"> <app-create-status *ngIf="replyingToStatus" [statusReplyingToWrapper]="displayedStatusWrapper"
(onClose)="closeReply()">
</app-create-status> </app-create-status>
</div> </div>

View File

@ -80,6 +80,9 @@
&--discuss { &--discuss {
background-color: rgb(90, 0, 143); background-color: rgb(90, 0, 143);
} }
&--old {
background-color: rgb(150, 0, 0);
}
} }
&__name { &__name {
display: inline-block; display: inline-block;
@ -123,9 +126,9 @@
} }
&__content-warning { &__content-warning {
min-height: 80px; min-height: 80px;
display: block; // border: 1px solid greenyellow; display: block;
margin: 0 10px 0 $avatar-column-space; margin: 0 10px 0 $avatar-column-space;
padding: 3px 5px 3px 5px; padding: 3px 5px 12px 5px;
text-decoration: none; text-decoration: none;
text-align: center; text-align: center;
@ -159,6 +162,16 @@
} }
} }
} }
&__navigation{
display: block;
position: absolute;
top:65px;
bottom: 40px;
width: 65px;
min-height: 40px;
// outline: 1px solid greenyellow;
}
} }
.attachments { .attachments {

View File

@ -6,6 +6,7 @@ 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 { TrustedString } from '@angular/core/src/sanitization/bypass';
@Component({ @Component({
selector: "app-status", selector: "app-status",
@ -20,6 +21,7 @@ export class StatusComponent implements OnInit {
faList = faList; faList = faList;
displayedStatus: Status; displayedStatus: Status;
displayedStatusWrapper: StatusWrapper;
// statusAccountName: string; // statusAccountName: string;
statusContent: string; statusContent: string;
@ -29,6 +31,7 @@ export class StatusComponent implements OnInit {
replyingToStatus: boolean; replyingToStatus: boolean;
isCrossPoster: boolean; isCrossPoster: boolean;
isThread: boolean; isThread: boolean;
isOld: boolean;
isContentWarned: boolean; isContentWarned: boolean;
hasReply: boolean; hasReply: boolean;
contentWarningText: string; contentWarningText: string;
@ -58,6 +61,8 @@ export class StatusComponent implements OnInit {
this.displayedStatus = this.status; this.displayedStatus = this.status;
} }
this.displayedStatusWrapper = new StatusWrapper(this.displayedStatus, value.provider);
this.checkLabels(this.displayedStatus); this.checkLabels(this.displayedStatus);
this.checkContentWarning(this.displayedStatus); this.checkContentWarning(this.displayedStatus);
@ -120,6 +125,13 @@ export class StatusComponent implements OnInit {
} }
this.hasReply = status.replies_count > 0; this.hasReply = status.replies_count > 0;
let createdAt = new Date(status.created_at);
let now = new Date();
now.setMonth(now.getMonth() - 3);
if (now > createdAt) {
this.isOld = true;
}
} }
openAccount(account: Account): boolean { openAccount(account: Account): boolean {
@ -165,9 +177,9 @@ export class StatusComponent implements OnInit {
return false; return false;
} }
openUrl(): boolean { openUrl(url: string): boolean {
event.preventDefault(); event.preventDefault();
window.open(this.displayedStatus.url, "_blank"); window.open(url, "_blank");
return false; return false;
} }
} }

View File

@ -1,5 +1,33 @@
<div class="stream-edition"> <div class="stream-edition">
<button (click)="moveLeft()" class="stream-edition__button" title="move left"><fa-icon [icon]="faChevronLeft"></fa-icon></button> <div class="stream-edition__settings">
<button (click)="moveRight()" class="stream-edition__button" title="move right"><fa-icon [icon]="faChevronRight"></fa-icon></button> <div class="stream-edition__setting">
<button (click)="delete()" class="stream-edition__button stream-edition__button--delete" title="remove column"><fa-icon [icon]="faTimes"></fa-icon></button> <input [(ngModel)]="hideBoosts" (change)="settingsChanged()"
class="stream-edition__setting--checkbox" type="checkbox" id="hideBoosts"> <label for="hideBoosts" class="noselect">hide
boosts</label><br />
</div>
<div class="stream-edition__setting">
<input [(ngModel)]="hideReplies" (change)="settingsChanged()"
class="stream-edition__setting--checkbox" type="checkbox" id="hideReplies"> <label for="hideReplies" class="noselect">hide
replies</label><br />
</div>
<div class="stream-edition__setting">
<input [(ngModel)]="hideBots" (change)="settingsChanged()"
class="stream-edition__setting--checkbox" type="checkbox" id="hideBots" > <label for="hideBots" class="noselect">hide
bots</label><br />
</div>
</div>
<div>
<button (click)="delete()" class="stream-edition__button stream-edition__button--delete" title="remove column">
<fa-icon [icon]="faTimes"></fa-icon> <span class="stream-edition__button--delete--label">remove</span>
</button>
<button (click)="moveRight()"
class="stream-edition__button stream-edition__button--move stream-edition__button--move--right"
title="move right">
<fa-icon [icon]="faChevronRight"></fa-icon>
</button>
<button (click)="moveLeft()" class="stream-edition__button stream-edition__button--move" title="move left">
<fa-icon [icon]="faChevronLeft"></fa-icon>
</button>
</div>
</div> </div>

View File

@ -1,17 +1,51 @@
@import "variables"; @import "variables";
@import "commons";
@import "mixins"; @import "mixins";
.stream-edition { .stream-edition {
width: 100%; width: 100%;
// min-height: 50px; // min-height: 50px;
background-color: #222736; background-color: #222736;
border-bottom: 1px solid $color-secondary;
&__settings{
padding: 5px 5px 0 5px;
border-bottom: 1px solid $color-secondary;
}
&__setting{
// outline: 1px solid greenyellow;
padding: 0 10px 0 10px;
&--checkbox {
position: relative;
top: 2px;
margin-right: 5px;
}
}
&__button { &__button {
@include clearButton; @include clearButton;
padding: 5px 10px 5px 10px; padding: 5px 10px 5px 10px;
margin: 3px 0; margin: 3px 0;
&--delete{
&--delete {
// float: right;
margin-left: 5px;
&--label {
position: relative;
top: -1px;
left: 7px;
}
}
&--move {
float: right; float: right;
margin-right: 5px;
&--right {
margin-right: 5px;
}
} }
&:hover { &:hover {

View File

@ -2,7 +2,7 @@ import { Component, OnInit, Input } from '@angular/core';
import { Store } from '@ngxs/store'; import { Store } from '@ngxs/store';
import { faChevronLeft, faChevronRight, faTimes } from "@fortawesome/free-solid-svg-icons"; import { faChevronLeft, faChevronRight, faTimes } from "@fortawesome/free-solid-svg-icons";
import { StreamElement, RemoveStream, MoveStreamUp, MoveStreamDown } from '../../../states/streams.state'; import { StreamElement, RemoveStream, MoveStreamUp, MoveStreamDown, UpdateStream } from '../../../states/streams.state';
@Component({ @Component({
selector: 'app-stream-edition', selector: 'app-stream-edition',
@ -14,11 +14,27 @@ export class StreamEditionComponent implements OnInit {
faChevronRight = faChevronRight; faChevronRight = faChevronRight;
faTimes = faTimes; faTimes = faTimes;
hideBoosts: boolean;
hideReplies: boolean;
hideBots: boolean;
@Input() streamElement: StreamElement; @Input() streamElement: StreamElement;
constructor(private readonly store: Store) { } constructor(private readonly store: Store) { }
ngOnInit() { ngOnInit() {
this.hideBoosts = this.streamElement.hideBoosts;
this.hideReplies = this.streamElement.hideReplies;
this.hideBots = this.streamElement.hideBots;
}
settingsChanged(): boolean {
this.streamElement.hideBoosts = this.hideBoosts;
this.streamElement.hideReplies = this.hideReplies;
this.streamElement.hideBots = this.hideBots;
this.store.dispatch([new UpdateStream(this.streamElement)]);
return false;
} }
moveLeft(): boolean { moveLeft(): boolean {

View File

@ -18,7 +18,7 @@ import { StatusWrapper } from '../../../models/common.model';
styleUrls: ['./stream-statuses.component.scss'] styleUrls: ['./stream-statuses.component.scss']
}) })
export class StreamStatusesComponent implements OnInit, OnDestroy { export class StreamStatusesComponent implements OnInit, OnDestroy {
isLoading = true; isLoading = true;
isThread = false; isThread = false;
displayError: string; displayError: string;
hasContentWarnings = false; hasContentWarnings = false;
@ -31,6 +31,10 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
private bufferStream: Status[] = []; private bufferStream: Status[] = [];
private bufferWasCleared: boolean; private bufferWasCleared: boolean;
private hideBoosts: boolean;
private hideReplies: boolean;
private hideBots: boolean;
@Output() browseAccountEvent = new EventEmitter<string>(); @Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>(); @Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>(); @Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@ -38,6 +42,11 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
@Input() @Input()
set streamElement(streamElement: StreamElement) { set streamElement(streamElement: StreamElement) {
this._streamElement = streamElement; this._streamElement = streamElement;
this.hideBoosts = streamElement.hideBoosts;
this.hideBots = streamElement.hideBots;
this.hideReplies = streamElement.hideReplies;
this.load(this._streamElement); this.load(this._streamElement);
} }
get streamElement(): StreamElement { get streamElement(): StreamElement {
@ -49,6 +58,8 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
@Input() userLocked = true; @Input() userLocked = true;
private goToTopSubscription: Subscription; private goToTopSubscription: Subscription;
private streamsSubscription: Subscription;
private streams$: Observable<StreamElement[]>;
constructor( constructor(
private readonly store: Store, private readonly store: Store,
@ -56,16 +67,29 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
private readonly notificationService: NotificationService, private readonly notificationService: NotificationService,
private readonly streamingService: StreamingService, private readonly streamingService: StreamingService,
private readonly mastodonService: MastodonService) { private readonly mastodonService: MastodonService) {
this.streams$ = this.store.select(state => state.streamsstatemodel.streams);
} }
ngOnInit() { ngOnInit() {
this.goToTopSubscription = this.goToTop.subscribe(() => { this.goToTopSubscription = this.goToTop.subscribe(() => {
this.applyGoToTop(); this.applyGoToTop();
}); });
this.streamsSubscription = this.streams$.subscribe((streams: StreamElement[]) => {
let updatedStream = streams.find(x => x.id === this.streamElement.id);
if (this.hideBoosts !== updatedStream.hideBoosts
|| this.hideBots !== updatedStream.hideBots
|| this.hideReplies !== updatedStream.hideReplies) {
this.streamElement = updatedStream;
}
});
} }
ngOnDestroy() { ngOnDestroy() {
if (this.goToTopSubscription) this.goToTopSubscription.unsubscribe(); if (this.goToTopSubscription) this.goToTopSubscription.unsubscribe();
if (this.streamsSubscription) this.streamsSubscription.unsubscribe();
} }
refresh(): any { refresh(): any {
@ -101,6 +125,10 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
if (update.type === EventEnum.update) { if (update.type === EventEnum.update) {
if (!this.statuses.find(x => x.status.id == update.status.id)) { if (!this.statuses.find(x => x.status.id == update.status.id)) {
if (this.streamPositionnedAtTop) { if (this.streamPositionnedAtTop) {
if (this.isFiltered(update.status)) {
return;
}
const wrapper = new StatusWrapper(update.status, this.account); const wrapper = new StatusWrapper(update.status, this.account);
this.statuses.unshift(wrapper); this.statuses.unshift(wrapper);
} else { } else {
@ -173,6 +201,10 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
} }
for (const status of this.bufferStream) { for (const status of this.bufferStream) {
if (this.isFiltered(status)) {
continue;
}
const wrapper = new StatusWrapper(status, this.account); const wrapper = new StatusWrapper(status, this.account);
this.statuses.unshift(wrapper); this.statuses.unshift(wrapper);
} }
@ -181,7 +213,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
} }
private scrolledToBottom() { private scrolledToBottom() {
if(this.isLoading) return; if (this.isLoading) return;
this.isLoading = true; this.isLoading = true;
this.isProcessingInfiniteScroll = true; this.isProcessingInfiniteScroll = true;
@ -190,6 +222,10 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
this.mastodonService.getTimeline(this.account, this._streamElement.type, lastStatus.status.id, null, this.streamingService.nbStatusPerIteration, this._streamElement.tag, this._streamElement.listId) this.mastodonService.getTimeline(this.account, this._streamElement.type, lastStatus.status.id, null, this.streamingService.nbStatusPerIteration, this._streamElement.tag, this._streamElement.listId)
.then((status: Status[]) => { .then((status: Status[]) => {
for (const s of status) { for (const s of status) {
if (this.isFiltered(s)) {
continue;
}
const wrapper = new StatusWrapper(s, this.account); const wrapper = new StatusWrapper(s, this.account);
this.statuses.push(wrapper); this.statuses.push(wrapper);
} }
@ -214,6 +250,10 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
.then((results: Status[]) => { .then((results: Status[]) => {
this.isLoading = false; this.isLoading = false;
for (const s of results) { for (const s of results) {
if (this.isFiltered(s)) {
continue;
}
const wrapper = new StatusWrapper(s, this.account); const wrapper = new StatusWrapper(s, this.account);
this.statuses.push(wrapper); this.statuses.push(wrapper);
} }
@ -232,6 +272,25 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
this.bufferWasCleared = true; this.bufferWasCleared = true;
this.bufferStream.length = 2 * this.streamingService.nbStatusPerIteration; this.bufferStream.length = 2 * this.streamingService.nbStatusPerIteration;
} }
} }
private isFiltered(status: Status): boolean {
if (this.streamElement.hideBoosts) {
if (status.reblog) {
return true;
}
}
if (this.streamElement.hideBots) {
if (status.account.bot) {
return true;
}
}
if (this.streamElement.hideReplies) {
if (status.in_reply_to_account_id && status.account.id !== status.in_reply_to_account_id) {
return true;
}
}
return false;
}
} }

View File

@ -2,11 +2,15 @@
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation> <app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
<div *ngIf="displayedAccount" class="profile-header" [ngStyle]="{'background-image':'url('+displayedAccount.header+')'}"> <div *ngIf="displayedAccount" class="profile-header"
[ngStyle]="{'background-image':'url('+displayedAccount.header+')'}">
<div class="profile-header__inner"> <div class="profile-header__inner">
<img class="profile-header__avatar" src="{{displayedAccount.avatar}}" alt="header" /> <a href (click)="showAvatar(displayedAccount.avatar)" title="open avatar">
<img class="profile-header__avatar" src="{{displayedAccount.avatar}}" alt="header" />
</a>
<h2 class="profile-header__display-name" innerHTML="{{displayedAccount | accountEmoji }}"></h2> <h2 class="profile-header__display-name" innerHTML="{{displayedAccount | accountEmoji }}"></h2>
<h2 class="profile-header__fullhandle"><a href="{{displayedAccount.url}}" target="_blank">@{{displayedAccount.acct}}</a></h2> <h2 class="profile-header__fullhandle"><a href="{{displayedAccount.url}}"
target="_blank">@{{displayedAccount.acct}}</a></h2>
<div class="profile-header__follow" *ngIf="relationship"> <div class="profile-header__follow" *ngIf="relationship">
<button class="profile-header__follow--button profile-header__follow--unfollowed" title="follow" <button class="profile-header__follow--button profile-header__follow--unfollowed" title="follow"
@ -38,7 +42,8 @@
</div> </div>
<div class="profile-fields" *ngIf="displayedAccount && displayedAccount.fields.length > 0"> <div class="profile-fields" *ngIf="displayedAccount && displayedAccount.fields.length > 0">
<div class="profile-fields__field" *ngFor="let field of displayedAccount.fields"> <div class="profile-fields__field" *ngFor="let field of displayedAccount.fields">
<div class="profile-fields__field--value" innerHTML="{{ displayedAccount | accountEmoji:field.value}}" [ngClass]="{'profile-fields__field--validated': field.verified_at }"> <div class="profile-fields__field--value" innerHTML="{{ displayedAccount | accountEmoji:field.value}}"
[ngClass]="{'profile-fields__field--validated': field.verified_at }">
</div> </div>
<div class="profile-fields__field--name"> <div class="profile-fields__field--name">
{{ field.name }} {{ field.name }}
@ -57,7 +62,7 @@
</app-status> </app-status>
</div> </div>
<app-waiting-animation *ngIf="statusLoading" class="waiting-icon"></app-waiting-animation> <app-waiting-animation *ngIf="statusLoading" class="waiting-icon"></app-waiting-animation>
</div> </div>
</div> </div>

View File

@ -5,13 +5,14 @@ import { faUser as faUserRegular } from "@fortawesome/free-regular-svg-icons";
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { Store } from '@ngxs/store'; import { Store } from '@ngxs/store';
import { Account, Status, Relationship } from "../../../services/models/mastodon.interfaces"; import { Account, Status, Relationship, Attachment } from "../../../services/models/mastodon.interfaces";
import { MastodonService } from '../../../services/mastodon.service'; import { MastodonService } from '../../../services/mastodon.service';
import { ToolsService, OpenThreadEvent } from '../../../services/tools.service'; import { ToolsService, OpenThreadEvent } 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 } 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 { NavigationService } from '../../../services/navigation.service';
@Component({ @Component({
selector: 'app-user-profile', selector: 'app-user-profile',
@ -32,7 +33,7 @@ export class UserProfileComponent implements OnInit {
note: string; note: string;
isLoading: boolean; isLoading: boolean;
private maxReached = false; private maxReached = false;
private maxId: string; private maxId: string;
statusLoading: boolean; statusLoading: boolean;
@ -60,6 +61,7 @@ export class UserProfileComponent implements OnInit {
constructor( constructor(
private readonly store: Store, private readonly store: Store,
private readonly navigationService: NavigationService,
private readonly notificationService: NotificationService, private readonly notificationService: NotificationService,
private readonly mastodonService: MastodonService, private readonly mastodonService: MastodonService,
private readonly toolsService: ToolsService) { private readonly toolsService: ToolsService) {
@ -103,7 +105,7 @@ export class UserProfileComponent implements OnInit {
this.displayedAccount = account; this.displayedAccount = account;
this.hasNote = account && account.note && account.note !== '<p></p>'; this.hasNote = account && account.note && account.note !== '<p></p>';
if(this.hasNote){ if (this.hasNote) {
this.note = this.emojiConverter.applyEmojis(account.emojis, account.note, EmojiTypeEnum.medium); this.note = this.emojiConverter.applyEmojis(account.emojis, account.note, EmojiTypeEnum.medium);
} }
@ -155,6 +157,26 @@ export class UserProfileComponent implements OnInit {
}); });
} }
showAvatar(avatarUrl: string): boolean {
const att: Attachment = {
id: '',
type: 'image',
remote_url: avatarUrl,
preview_url: avatarUrl,
url: avatarUrl,
meta: null,
text_url: '',
description: ''
}
this.navigationService.openMedia({
selectedIndex: 0,
attachments: [att],
iframe: null
});
return false;
}
refresh(): any { refresh(): any {
this.load(this.lastAccountName); this.load(this.lastAccountName);
} }
@ -187,7 +209,7 @@ export class UserProfileComponent implements OnInit {
} }
unfollow(): boolean { unfollow(): boolean {
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) => {
return this.mastodonService.unfollow(userAccount, account); return this.mastodonService.unfollow(userAccount, account);
@ -209,7 +231,7 @@ export class UserProfileComponent implements OnInit {
this.scrolledToBottom(); this.scrolledToBottom();
} }
} }
private scrolledToBottom() { private scrolledToBottom() {
if (this.statusLoading || this.maxReached) return; if (this.statusLoading || this.maxReached) return;
@ -227,7 +249,7 @@ export class UserProfileComponent implements OnInit {
}); });
} }
private loadStatus(userAccount: AccountInfo, statuses: Status[]){ private loadStatus(userAccount: AccountInfo, statuses: Status[]) {
if (statuses.length === 0) { if (statuses.length === 0) {
this.maxReached = true; this.maxReached = true;
return; return;

View File

@ -5,10 +5,6 @@ export class AccountWrapper {
constructor() { constructor() {
} }
// id: number;
// username: string;
// display_name: string;
info: AccountInfo; info: AccountInfo;
avatar: string; avatar: string;
} }

View File

@ -4,10 +4,11 @@ import { ActivatedRoute, Router } from "@angular/router";
import { HttpErrorResponse } from "@angular/common/http"; import { HttpErrorResponse } from "@angular/common/http";
import { AuthService, CurrentAuthProcess } from "../../services/auth.service"; import { AuthService, CurrentAuthProcess } from "../../services/auth.service";
import { TokenData } from "../../services/models/mastodon.interfaces"; import { TokenData, Account } from "../../services/models/mastodon.interfaces";
import { RegisteredAppsStateModel, AppInfo } from "../../states/registered-apps.state"; import { RegisteredAppsStateModel, AppInfo } from "../../states/registered-apps.state";
import { AccountInfo, AddAccount } from "../../states/accounts.state"; import { AccountInfo, AddAccount, AccountsStateModel } from "../../states/accounts.state";
import { NotificationService } from "../../services/notification.service"; import { NotificationService } from "../../services/notification.service";
import { MastodonService } from '../../services/mastodon.service';
@Component({ @Component({
selector: "app-register-new-account", selector: "app-register-new-account",
@ -23,6 +24,7 @@ export class RegisterNewAccountComponent implements OnInit {
private authStorageKey: string = 'tempAuth'; private authStorageKey: string = 'tempAuth';
constructor( constructor(
private readonly mastodonService: MastodonService,
private readonly notificationService: NotificationService, private readonly notificationService: NotificationService,
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly store: Store, private readonly store: Store,
@ -45,13 +47,26 @@ export class RegisterNewAccountComponent implements OnInit {
} }
const appInfo = this.getAllSavedApps().filter(x => x.instance === appDataWrapper.instance)[0]; const appInfo = this.getAllSavedApps().filter(x => x.instance === appDataWrapper.instance)[0];
let usedTokenData: TokenData;
this.authService.getToken(appDataWrapper.instance, appInfo.app.client_id, appInfo.app.client_secret, code, appInfo.app.redirect_uri) this.authService.getToken(appDataWrapper.instance, appInfo.app.client_id, appInfo.app.client_secret, code, appInfo.app.redirect_uri)
.then((tokenData: TokenData) => { .then((tokenData: TokenData) => {
usedTokenData = tokenData;
return this.mastodonService.retrieveAccountDetails({ 'instance': appDataWrapper.instance, 'id': '', 'username': '', 'order': 0, 'isSelected': true, 'token': tokenData });
})
.then((account: Account) => {
var username = account.username.toLowerCase();
var instance = appDataWrapper.instance.toLowerCase();
if(this.isAccountAlreadyPresent(username, instance)){
this.notificationService.notify(`Account @${username}@${instance} is already registered`, true);
this.router.navigate(['/home']);
return;
}
const accountInfo = new AccountInfo(); const accountInfo = new AccountInfo();
accountInfo.username = appDataWrapper.username.toLowerCase(); accountInfo.username = username;
accountInfo.instance = appDataWrapper.instance.toLowerCase(); accountInfo.instance = instance;
accountInfo.token = tokenData; accountInfo.token = usedTokenData;
this.store.dispatch([new AddAccount(accountInfo)]) this.store.dispatch([new AddAccount(accountInfo)])
.subscribe(() => { .subscribe(() => {
@ -68,6 +83,16 @@ export class RegisterNewAccountComponent implements OnInit {
ngOnInit() { ngOnInit() {
} }
private isAccountAlreadyPresent(username: string, instance: string): boolean{
const accounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
for (let acc of accounts) {
if(acc.instance === instance && acc.username == username){
return true;
}
}
return false;
}
private displayError(type: RegistrationErrorTypes) { private displayError(type: RegistrationErrorTypes) {
this.hasError = true; this.hasError = true;
switch (type) { switch (type) {

View File

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

View File

@ -31,11 +31,11 @@ export class TimeLeftPipe implements PipeTransform {
if (seconds < 0) { if (seconds < 0) {
text = '0 seconds left'; text = '0 seconds left';
} else if (seconds <= 60) { } else if (seconds <= 59) {
text = Math.round(seconds) + ' seconds left'; text = Math.round(seconds) + ' seconds left';
} else if (minutes <= 90) { } else if (minutes <= 59) {
text = Math.round(minutes) + ' minutes left'; text = Math.round(minutes) + ' minutes left';
} else if (hours <= 24) { } else if (hours <= 23) {
text = Math.round(hours) + ' hours left'; text = Math.round(hours) + ' hours left';
} else { } else {
text = Math.round(days) + ' days left'; text = Math.round(days) + ' days left';

View File

@ -30,6 +30,10 @@ export class ToolsService {
accountSettings.accountId = account.id; accountSettings.accountId = account.id;
this.saveAccountSettings(accountSettings); this.saveAccountSettings(accountSettings);
} }
if(!accountSettings.customStatusCharLength){
accountSettings.customStatusCharLength = 500;
this.saveAccountSettings(accountSettings);
}
return accountSettings; return accountSettings;
} }

View File

@ -44,22 +44,16 @@ export class AccountsState {
@Action(SelectAccount) @Action(SelectAccount)
SelectAccount(ctx: StateContext<AccountsStateModel>, action: SelectAccount){ SelectAccount(ctx: StateContext<AccountsStateModel>, action: SelectAccount){
const state = ctx.getState(); const state = ctx.getState();
// const multiSelection = action.multiselection;
const selectedAccount = action.account; const selectedAccount = action.account;
// const copyAccounts = [...state.accounts];
// copyAccounts
// .filter(x => x.id !== selectedAccount.id)
// .forEach(x => x.isSelected = false);
const oldSelectedAccount = state.accounts.find(x => x.isSelected); const oldSelectedAccount = state.accounts.find(x => x.isSelected);
if(selectedAccount.id === oldSelectedAccount.id) return; if(oldSelectedAccount != null && selectedAccount.id === oldSelectedAccount.id) return;
const acc = state.accounts.find(x => x.id === selectedAccount.id); const acc = state.accounts.find(x => x.id === selectedAccount.id);
acc.isSelected = true; acc.isSelected = true;
oldSelectedAccount.isSelected = false;
if(oldSelectedAccount != null) oldSelectedAccount.isSelected = false;
ctx.patchState({ ctx.patchState({
accounts: [...state.accounts] accounts: [...state.accounts]

View File

@ -21,6 +21,9 @@ export class AccountSettings {
displayNotifications: boolean = true; displayNotifications: boolean = true;
lastMentionCreationDate: string; lastMentionCreationDate: string;
lastNotificationCreationDate: string; lastNotificationCreationDate: string;
customStatusCharLengthEnabled: boolean = false;
customStatusCharLength: number = 500;
} }
export class GlobalSettings { export class GlobalSettings {

View File

@ -5,6 +5,11 @@ export class AddStream {
constructor(public stream: StreamElement) {} constructor(public stream: StreamElement) {}
} }
export class UpdateStream {
static readonly type = '[Streams] Update stream';
constructor(public stream: StreamElement) {}
}
export class RemoveAllStreams { export class RemoveAllStreams {
static readonly type = '[Streams] Remove all streams'; static readonly type = '[Streams] Remove all streams';
constructor(public accountId :string) {} constructor(public accountId :string) {}
@ -43,6 +48,20 @@ export class StreamsState {
streams: [...state.streams, action.stream] streams: [...state.streams, action.stream]
}); });
} }
@Action(UpdateStream)
UpdateStream(ctx: StateContext<StreamsStateModel>, action: UpdateStream){
const state = ctx.getState();
const updatedStream = state.streams.find(x => x.id === action.stream.id);
updatedStream.hideBoosts = action.stream.hideBoosts;
updatedStream.hideReplies = action.stream.hideReplies;
updatedStream.hideBots = action.stream.hideBots;
ctx.patchState({
streams: [...state.streams]
});
}
@Action(RemoveAllStreams) @Action(RemoveAllStreams)
RemoveAllStreams(ctx: StateContext<StreamsStateModel>, action: RemoveAllStreams){ RemoveAllStreams(ctx: StateContext<StreamsStateModel>, action: RemoveAllStreams){
const state = ctx.getState(); const state = ctx.getState();
@ -92,6 +111,10 @@ export class StreamsState {
export class StreamElement { export class StreamElement {
public id: string; public id: string;
public hideBoosts: boolean = false;
public hideReplies: boolean = false;
public hideBots: boolean = false;
constructor( constructor(
public type: StreamTypeEnum, public type: StreamTypeEnum,
public name: string, public name: string,

View File

@ -1,7 +1,27 @@
@import "variables";
.themed-form{
background-color: $column-color;
border: 1px $button-border-color solid;
padding: 2px 0 2px 5px;
// border-width: 1px;
color: #fff;
&:focus {
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
outline: 0px;
}
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
.waiting-icon { .waiting-icon {
width: 40px; width: 40px;
display: block; display: block;
margin: 5px auto; margin: 5px auto;
} }
.flexcroll { .flexcroll {
@ -12,9 +32,11 @@
scrollbar-darkshadow-color: #08090d; scrollbar-darkshadow-color: #08090d;
scrollbar-track-color: #08090d; scrollbar-track-color: #08090d;
scrollbar-arrow-color: #08090d; scrollbar-arrow-color: #08090d;
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 7px; width: 7px;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
-webkit-border-radius: 0px; -webkit-border-radius: 0px;
border-radius: 0px; border-radius: 0px;
@ -35,4 +57,19 @@
width: 20px; width: 20px;
height: 20px; height: 20px;
vertical-align: middle; vertical-align: middle;
}
.noselect {
-webkit-touch-callout: none;
/* iOS Safari */
-webkit-user-select: none;
/* Safari */
-khtml-user-select: none;
/* Konqueror HTML */
-moz-user-select: none;
/* Firefox */
-ms-user-select: none;
/* Internet Explorer/Edge */
user-select: none;
/* Non-prefixed version, currently supported by Chrome and Opera */
} }