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;
function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 377,
height: 800,
title: "Sengi",
backgroundColor: "#FFF",
useContentSize: true
});
// Create the browser window.
win = new BrowserWindow({
width: 377,
height: 800,
title: "Sengi",
backgroundColor: "#FFF",
useContentSize: true
});
var server = http.createServer(requestHandler).listen(9527);
win.loadURL("http://localhost:9527");
var server = http.createServer(requestHandler).listen(9527);
win.loadURL("http://localhost:9527");
const template = [
{
label: "View",
submenu: [
{ role: "reload" },
{ role: "forcereload" },
{ type: "separator" },
{ role: "close" },
{ role: 'quit' }
]
},
{
role: "help",
submenu: [
{ role: "toggledevtools" },
const template = [
{
label: "Open GitHub project",
click() {
require("electron").shell.openExternal(
"https://github.com/NicolasConstant/sengi"
);
}
label: "View",
submenu: [
{ role: "reload" },
{ 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);
win.setMenu(menu);
// Open the DevTools.
// win.webContents.openDevTools()
// 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"
);
}
}
]
}
])
);
}
//open external links to browser
win.webContents.on("new-window", function (event, url) {
event.preventDefault();
shell.openExternal(url);
});
// Open the DevTools.
// win.webContents.openDevTools()
//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;
});
// 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) {
var file = req.url == "/" ? "/index.html" : req.url,
root = __dirname + "/dist",
page404 = root + "/404.html";
var file = req.url == "/" ? "/index.html" : req.url,
root = __dirname + "/dist",
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) {
console.warn(`filePath: ${filePath}`);
fs.exists(filePath, function(exists) {
if (exists) {
fs.readFile(filePath, function(err, contents) {
if (!err) {
res.end(contents);
console.warn(`filePath: ${filePath}`);
fs.exists(filePath, function (exists) {
if (exists) {
fs.readFile(filePath, function (err, contents) {
if (!err) {
res.end(contents);
} else {
console.dir(err);
}
});
} 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");
// 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);
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();
}
// 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();
}
// 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();
}
});
// In this file you can include the rest of your app's specific main process

View File

@ -1,6 +1,6 @@
{
"name": "sengi",
"version": "0.10.1",
"version": "0.11.0",
"license": "AGPL-3.0-or-later",
"main": "main-electron.js",
"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']
})
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 = '';
set status(value: string) {
@ -106,14 +113,21 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private accountChanged(accounts: AccountInfo[]): void {
if (accounts && accounts.length > 0) {
const selectedAccount = accounts.filter(x => x.isSelected)[0];
this.instancesInfoService.getMaxStatusChars(selectedAccount.instance)
.then((maxChars: number) => {
this.maxCharLength = maxChars;
this.countStatusChar(this.status);
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
const settings = this.toolsService.getAccountSettings(selectedAccount);
if (settings.customStatusCharLengthEnabled) {
this.maxCharLength = settings.customStatusCharLength;
this.countStatusChar(this.status);
} else {
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) {
this.instancesInfoService.getDefaultPrivacy(selectedAccount)
@ -169,10 +183,18 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
const statusExtraChars = this.getMentionExtraChars(status);
const statusLength = currentStatus.length - statusExtraChars;
this.charCountLeft = this.maxCharLength - statusLength;
this.charCountLeft = this.maxCharLength - statusLength - this.getCwLength();
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[] {
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);
})
.then((res: Status) => {
if (this.statusReplyingToWrapper) {
this.notificationService.newStatusPosted(this.statusReplyingToWrapper.status.id, new StatusWrapper(res, acc));
}
this.title = '';
this.status = '';
this.onClose.emit();
@ -261,22 +280,30 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
for (let i = 0; i < parsedStatus.length; i++) {
let s = parsedStatus[i];
resultPromise = resultPromise.then((pStatus: Status) => {
let inReplyToId = null;
if (pStatus) {
inReplyToId = pStatus.id;
}
resultPromise = resultPromise
.then((pStatus: Status) => {
let inReplyToId = null;
if (pStatus) {
inReplyToId = pStatus.id;
}
if (i === 0) {
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id))
.then((status: Status) => {
this.mediaService.clearMedia();
return status;
});
} else {
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, []);
}
});
if (i === 0) {
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id))
.then((status: Status) => {
this.mediaService.clearMedia();
return status;
});
} else {
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;
@ -293,7 +320,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
aggregateMention += `${x} `;
});
const currentMaxCharLength = this.maxCharLength + mentionExtraChars;
const currentMaxCharLength = this.maxCharLength + mentionExtraChars - this.getCwLength();
const maxChars = currentMaxCharLength - 6;
while (trucatedStatus.length > currentMaxCharLength) {

View File

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

View File

@ -6,6 +6,10 @@ import { Subscription } from 'rxjs';
import { AccountWrapper } from '../../../models/account.models';
import { UserNotificationService, UserNotification } from '../../../services/user-notification.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({
@ -25,6 +29,8 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
hasNotifications = false;
hasMentions = false;
userAccount: Account;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@ -33,6 +39,7 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
set account(acc: AccountWrapper) {
this._account = acc;
this.checkNotifications();
this.getUserUrl(acc.info);
}
get account(): AccountWrapper {
return this._account;
@ -42,6 +49,8 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
private _account: AccountWrapper;
constructor(
private readonly mastodonService: MastodonService,
private readonly notificationService: NotificationService,
private readonly userNotificationService: UserNotificationService) { }
ngOnInit() {
@ -52,6 +61,16 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
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(){
if(this.userNotificationServiceSub){
this.userNotificationServiceSub.unsubscribe();
@ -75,6 +94,17 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
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 {
this.browseHashtagEvent.next(hashtag);
}

View File

@ -9,21 +9,21 @@
<h4 class="my-account__label my-account__margin-top">manage list:</h4>
<div class="my-account__link--margin-bottom" *ngFor="let list of availableLists">
<a href class="my-account__list--button" title="delete list"
(click)="openCloseDeleteConfirmation(list, true)" *ngIf="!list.confirmDeletion">
<a href class="my-account__list--button" title="delete list" (click)="openCloseDeleteConfirmation(list, true)"
*ngIf="!list.confirmDeletion">
<fa-icon class="my-account__link--icon" [icon]="faTrash"></fa-icon>
</a>
<a href class="my-account__list--button" title="edit list"
(click)="editList(list)" *ngIf="!list.confirmDeletion">
<a href class="my-account__list--button" title="edit list" (click)="editList(list)"
*ngIf="!list.confirmDeletion">
<fa-icon class="my-account__link--icon" [icon]="faPenAlt"></fa-icon>
</a>
<a href class="my-account__list--button" title="cancel"
(click)="openCloseDeleteConfirmation(list, false)" *ngIf="list.confirmDeletion">
<a href class="my-account__list--button" title="cancel" (click)="openCloseDeleteConfirmation(list, false)"
*ngIf="list.confirmDeletion">
<fa-icon class="my-account__link--icon" [icon]="faTimes"></fa-icon>
</a>
<a href class="my-account__list--button my-account__red" title="delete list"
(click)="deleteList(list)" *ngIf="list.confirmDeletion">
<a href class="my-account__list--button my-account__red" title="delete list" (click)="deleteList(list)"
*ngIf="list.confirmDeletion">
<fa-icon class="my-account__link--icon" [icon]="faCheck"></fa-icon>
</a>
@ -38,7 +38,17 @@
<a href class="my-account__list--button" title="create list" (click)="createList()">
<fa-icon class="my-account__link--icon" [icon]="faPlus"></fa-icon>
</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>
<a class="my-account__link my-account__red" href (click)="removeAccount()">

View File

@ -102,4 +102,30 @@
&__margin-top {
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 { NavigationService } from '../../../../services/navigation.service';
import { MastodonService } from '../../../../services/mastodon.service';
import { ToolsService } from '../../../../services/tools.service';
import { AccountSettings } from '../../../../states/settings.state';
@Component({
selector: 'app-my-account',
@ -23,6 +25,10 @@ export class MyAccountComponent implements OnInit, OnDestroy {
faCheckSquare = faCheckSquare;
faCheck = faCheck;
faTimes = faTimes;
customStatusLengthEnabled: boolean;
customStatusLength: number;
private accountSettings: AccountSettings;
availableStreams: StreamWrapper[] = [];
availableLists: StreamWrapper[] = [];
@ -32,6 +38,7 @@ export class MyAccountComponent implements OnInit, OnDestroy {
set account(acc: AccountWrapper) {
this._account = acc;
this.loadStreams(acc);
this.loadAccountSettings();
}
get account(): AccountWrapper {
return this._account;
@ -42,6 +49,7 @@ export class MyAccountComponent implements OnInit, OnDestroy {
constructor(
private readonly store: Store,
private readonly toolsService: ToolsService,
private readonly navigationService: NavigationService,
private readonly mastodonService: MastodonService,
private readonly notificationService: NotificationService) { }
@ -49,7 +57,7 @@ export class MyAccountComponent implements OnInit, OnDestroy {
ngOnInit() {
this.streamChangedSub = this.streamElements$.subscribe((streams: StreamElement[]) => {
this.loadStreams(this.account);
});
});
}
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){
const instance = account.info.instance;
this.availableStreams.length = 0;

View File

@ -1,23 +1,34 @@
<div class="media-viewer-canvas" (click)="close()">
<button class="media-viewer-canvas__close media-viewer-canvas__button" title="close">
<div class="media-viewer-canvas noselect">
<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>
</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>
</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>
</button>
<img class="media-viewer-canvas__image" *ngIf="imageUrl" src="{{imageUrl}}" (click)="blockClick($event)"/>
<video class="media-viewer-canvas__image" *ngIf="gifvUrl" role="application" loop autoplay (click)="blockClick($event)">
<source src="{{ gifvUrl }}" type="video/mp4">
</video>
<video class="media-viewer-canvas__image" *ngIf="videoUrl" role="application" loop controls="controls" (click)="blockClick($event)">
<source src="{{ videoUrl }}" type="video/mp4">
</video>
<div *ngFor="let att of attachments" class="media-viewer-canvas__attachement"
[ngClass]="{ 'collapsed': currentIndex !== att.index }">
<a href="{{att.url}}" target="_blank" title="open image">
<img *ngIf="att.type === 'image'" src="{{att.url}}" class="media-viewer-canvas__image" />
</a>
<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>

View File

@ -1,13 +1,14 @@
@import "variables";
@import "commons";
@import "mixins";
.media-viewer-canvas {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
// background-color: rgba(0, 0, 0, 0.8);
overflow: hidden;
position: relative;
position: relative;
&__button {
@include clearButton;
padding: 5px;
@ -60,7 +61,14 @@
padding: 10px;
}
&__attachement {
// max-width: 100%;
// height: 100vh;
}
&__image {
z-index: 10;
@media screen and (min-width: $screen-break) {
max-width: 85%;
}
@ -72,6 +80,8 @@
margin-top: 50vh;
margin-left: 50vw;
transform: translate(-50%, -50%);
visibility: visible;
}
&__iframe {
@ -80,4 +90,18 @@
max-height: 600px;
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;
faAngleRight = faAngleRight;
imageUrl: string;
gifvUrl: string;
videoUrl: string;
attachments: AttachmentsWrapper[] = [];
html: SafeHtml;
previousAvailable: boolean;
nextAvailable: boolean;
private currentIndex: number;
currentIndex: number;
@Input('openedMediaEvent')
set openedMediaEvent(value: OpenMediaEvent) {
this._mediaEvent = value;
this.attachments.length = 0;
if (value.iframe) {
this.html = value.iframe;
this.autoplayIframe();
} 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.loadAttachment(attachment);
this.setBrowsing();
}
}
@ -52,7 +56,7 @@ export class MediaViewerComponent implements OnInit {
handleKeyboardEvent(event: KeyboardEvent) {
event.stopPropagation();
event.preventDefault();
if (event.key === 'ArrowRight') {
this.next(event);
} else if (event.key === 'ArrowLeft') {
@ -65,24 +69,10 @@ export class MediaViewerComponent implements OnInit {
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() {
var index = this.currentIndex;
var attachments = this.openedMediaEvent.attachments;
console.log(`index ${index}`);
console.log(`attachments.length ${attachments.length}`);
if (index < attachments.length - 1) {
if (index < this.attachments.length - 1) {
this.nextAvailable = true;
} else {
this.nextAvailable = false;
@ -120,7 +110,6 @@ export class MediaViewerComponent implements OnInit {
if (this.currentIndex <= 0) return false;
this.currentIndex--;
this.imageUrl = this.openedMediaEvent.attachments[this.currentIndex].url;
this.setBrowsing();
return false;
@ -131,9 +120,34 @@ export class MediaViewerComponent implements OnInit {
if (this.currentIndex >= this.openedMediaEvent.attachments.length - 1) return false;
this.currentIndex++;
this.imageUrl = this.openedMediaEvent.attachments[this.currentIndex].url;
this.setBrowsing();
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 *ngIf="!poll.voted && !poll.expired">
<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}}"
(change)="onSelectionChange(o)">
<span class="poll__container__checkmark" *ngIf="!pollLocked"
@ -15,7 +16,7 @@
<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--data"> <span class="poll__result--percentage">{{ o.percentage }}%</span>
{{o.title}}</div>
<span class="poll__container__title">{{o.title}}</span></div>
</div>
</div>

View File

@ -1,10 +1,10 @@
// @import "variables";
// @import "commons";
@import "variables";
@import "commons";
// @import "panel";
@import "buttons";
.poll {
color: white;
color: rgb(228, 228, 228);
@ -44,6 +44,11 @@
-ms-user-select: none;
user-select: none;
&__title {
text-overflow: ellipsis;
white-space: nowrap;
}
&:hover &__input~&__checkmark {
background-color: #ccc;
}
@ -125,7 +130,7 @@
&__result {
transition: width 2s;
margin: 0 0 5px 5px;
padding: 0 5px 0 5px;
position: relative;
@ -141,15 +146,15 @@
color: white;
display: inline;
margin-right: 10px;
}
&--progress-bar {
position: absolute;
background-color: rgb(47, 68, 100);
// background-color: rgb(43, 62, 92);
top:0;
left:0;
top: 0;
left: 0;
width: calc(100%);
height: 22px;
z-index: 1;
@ -166,19 +171,4 @@
display: inline-block;
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;
}
@Input() provider: AccountInfo;
@Input() status: Status;
// @Input() provider: AccountInfo;
@Input() statusWrapper: StatusWrapper;
private accounts$: Observable<AccountInfo[]>;
private accountSub: Subscription;
@ -71,8 +71,8 @@ export class PollComponent implements OnInit {
}
ngOnInit() {
this.pollPerAccountId[this.provider.id] = Promise.resolve(this.poll);
this.selectedAccount = this.provider;
this.pollPerAccountId[this.statusWrapper.provider.id] = Promise.resolve(this.poll);
this.selectedAccount = this.statusWrapper.provider;
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
this.checkStatus(accounts);
@ -84,10 +84,10 @@ export class PollComponent implements OnInit {
var newSelectedAccount = accounts.find(x => x.isSelected);
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.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) => {
return this.mastodonService.getPoll(newSelectedAccount, status.poll.id);
})
@ -99,7 +99,7 @@ export class PollComponent implements OnInit {
this.notificationService.notifyHttpError(err);
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;
} else {
this.pollPerAccountId[newSelectedAccount.id]

View File

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

View File

@ -80,6 +80,9 @@
&--discuss {
background-color: rgb(90, 0, 143);
}
&--old {
background-color: rgb(150, 0, 0);
}
}
&__name {
display: inline-block;
@ -123,9 +126,9 @@
}
&__content-warning {
min-height: 80px;
display: block; // border: 1px solid greenyellow;
display: block;
margin: 0 10px 0 $avatar-column-space;
padding: 3px 5px 3px 5px;
padding: 3px 5px 12px 5px;
text-decoration: none;
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 {

View File

@ -6,6 +6,7 @@ import { OpenThreadEvent, ToolsService } from "../../../services/tools.service";
import { ActionBarComponent } from "./action-bar/action-bar.component";
import { StatusWrapper } from '../../../models/common.model';
import { EmojiConverter, EmojiTypeEnum } from '../../../tools/emoji.tools';
import { TrustedString } from '@angular/core/src/sanitization/bypass';
@Component({
selector: "app-status",
@ -20,6 +21,7 @@ export class StatusComponent implements OnInit {
faList = faList;
displayedStatus: Status;
displayedStatusWrapper: StatusWrapper;
// statusAccountName: string;
statusContent: string;
@ -29,6 +31,7 @@ export class StatusComponent implements OnInit {
replyingToStatus: boolean;
isCrossPoster: boolean;
isThread: boolean;
isOld: boolean;
isContentWarned: boolean;
hasReply: boolean;
contentWarningText: string;
@ -58,6 +61,8 @@ export class StatusComponent implements OnInit {
this.displayedStatus = this.status;
}
this.displayedStatusWrapper = new StatusWrapper(this.displayedStatus, value.provider);
this.checkLabels(this.displayedStatus);
this.checkContentWarning(this.displayedStatus);
@ -120,6 +125,13 @@ export class StatusComponent implements OnInit {
}
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 {
@ -165,9 +177,9 @@ export class StatusComponent implements OnInit {
return false;
}
openUrl(): boolean {
openUrl(url: string): boolean {
event.preventDefault();
window.open(this.displayedStatus.url, "_blank");
window.open(url, "_blank");
return false;
}
}

View File

@ -1,5 +1,33 @@
<div class="stream-edition">
<button (click)="moveLeft()" class="stream-edition__button" title="move left"><fa-icon [icon]="faChevronLeft"></fa-icon></button>
<button (click)="moveRight()" class="stream-edition__button" title="move right"><fa-icon [icon]="faChevronRight"></fa-icon></button>
<button (click)="delete()" class="stream-edition__button stream-edition__button--delete" title="remove column"><fa-icon [icon]="faTimes"></fa-icon></button>
<div class="stream-edition__settings">
<div class="stream-edition__setting">
<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>

View File

@ -1,17 +1,51 @@
@import "variables";
@import "commons";
@import "mixins";
.stream-edition {
width: 100%;
// min-height: 50px;
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 {
@include clearButton;
padding: 5px 10px 5px 10px;
margin: 3px 0;
&--delete{
&--delete {
// float: right;
margin-left: 5px;
&--label {
position: relative;
top: -1px;
left: 7px;
}
}
&--move {
float: right;
margin-right: 5px;
&--right {
margin-right: 5px;
}
}
&:hover {

View File

@ -2,7 +2,7 @@ import { Component, OnInit, Input } from '@angular/core';
import { Store } from '@ngxs/store';
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({
selector: 'app-stream-edition',
@ -14,11 +14,27 @@ export class StreamEditionComponent implements OnInit {
faChevronRight = faChevronRight;
faTimes = faTimes;
hideBoosts: boolean;
hideReplies: boolean;
hideBots: boolean;
@Input() streamElement: StreamElement;
constructor(private readonly store: Store) { }
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 {

View File

@ -18,7 +18,7 @@ import { StatusWrapper } from '../../../models/common.model';
styleUrls: ['./stream-statuses.component.scss']
})
export class StreamStatusesComponent implements OnInit, OnDestroy {
isLoading = true;
isLoading = true;
isThread = false;
displayError: string;
hasContentWarnings = false;
@ -31,6 +31,10 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
private bufferStream: Status[] = [];
private bufferWasCleared: boolean;
private hideBoosts: boolean;
private hideReplies: boolean;
private hideBots: boolean;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@ -38,6 +42,11 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
@Input()
set streamElement(streamElement: StreamElement) {
this._streamElement = streamElement;
this.hideBoosts = streamElement.hideBoosts;
this.hideBots = streamElement.hideBots;
this.hideReplies = streamElement.hideReplies;
this.load(this._streamElement);
}
get streamElement(): StreamElement {
@ -49,6 +58,8 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
@Input() userLocked = true;
private goToTopSubscription: Subscription;
private streamsSubscription: Subscription;
private streams$: Observable<StreamElement[]>;
constructor(
private readonly store: Store,
@ -56,16 +67,29 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
private readonly notificationService: NotificationService,
private readonly streamingService: StreamingService,
private readonly mastodonService: MastodonService) {
this.streams$ = this.store.select(state => state.streamsstatemodel.streams);
}
ngOnInit() {
this.goToTopSubscription = this.goToTop.subscribe(() => {
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() {
if (this.goToTopSubscription) this.goToTopSubscription.unsubscribe();
if (this.streamsSubscription) this.streamsSubscription.unsubscribe();
}
refresh(): any {
@ -101,6 +125,10 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
if (update.type === EventEnum.update) {
if (!this.statuses.find(x => x.status.id == update.status.id)) {
if (this.streamPositionnedAtTop) {
if (this.isFiltered(update.status)) {
return;
}
const wrapper = new StatusWrapper(update.status, this.account);
this.statuses.unshift(wrapper);
} else {
@ -173,6 +201,10 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
}
for (const status of this.bufferStream) {
if (this.isFiltered(status)) {
continue;
}
const wrapper = new StatusWrapper(status, this.account);
this.statuses.unshift(wrapper);
}
@ -181,7 +213,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
}
private scrolledToBottom() {
if(this.isLoading) return;
if (this.isLoading) return;
this.isLoading = 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)
.then((status: Status[]) => {
for (const s of status) {
if (this.isFiltered(s)) {
continue;
}
const wrapper = new StatusWrapper(s, this.account);
this.statuses.push(wrapper);
}
@ -214,6 +250,10 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
.then((results: Status[]) => {
this.isLoading = false;
for (const s of results) {
if (this.isFiltered(s)) {
continue;
}
const wrapper = new StatusWrapper(s, this.account);
this.statuses.push(wrapper);
}
@ -232,6 +272,25 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
this.bufferWasCleared = true;
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>
<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">
<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__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">
<button class="profile-header__follow--button profile-header__follow--unfollowed" title="follow"
@ -38,7 +42,8 @@
</div>
<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--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 class="profile-fields__field--name">
{{ field.name }}
@ -57,7 +62,7 @@
</app-status>
</div>
<app-waiting-animation *ngIf="statusLoading" class="waiting-icon"></app-waiting-animation>
</div>
</div>

View File

@ -5,13 +5,14 @@ import { faUser as faUserRegular } from "@fortawesome/free-regular-svg-icons";
import { Observable, Subscription } from 'rxjs';
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 { ToolsService, OpenThreadEvent } from '../../../services/tools.service';
import { NotificationService } from '../../../services/notification.service';
import { AccountInfo } from '../../../states/accounts.state';
import { StatusWrapper } from '../../../models/common.model';
import { EmojiConverter, EmojiTypeEnum } from '../../../tools/emoji.tools';
import { NavigationService } from '../../../services/navigation.service';
@Component({
selector: 'app-user-profile',
@ -32,7 +33,7 @@ export class UserProfileComponent implements OnInit {
note: string;
isLoading: boolean;
private maxReached = false;
private maxId: string;
statusLoading: boolean;
@ -60,6 +61,7 @@ export class UserProfileComponent implements OnInit {
constructor(
private readonly store: Store,
private readonly navigationService: NavigationService,
private readonly notificationService: NotificationService,
private readonly mastodonService: MastodonService,
private readonly toolsService: ToolsService) {
@ -103,7 +105,7 @@ export class UserProfileComponent implements OnInit {
this.displayedAccount = account;
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);
}
@ -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 {
this.load(this.lastAccountName);
}
@ -187,7 +209,7 @@ export class UserProfileComponent implements OnInit {
}
unfollow(): boolean {
const userAccount = this.toolsService.getSelectedAccounts()[0];
const userAccount = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(userAccount, this.lastAccountName)
.then((account: Account) => {
return this.mastodonService.unfollow(userAccount, account);
@ -209,7 +231,7 @@ export class UserProfileComponent implements OnInit {
this.scrolledToBottom();
}
}
private scrolledToBottom() {
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) {
this.maxReached = true;
return;

View File

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

View File

@ -4,10 +4,11 @@ import { ActivatedRoute, Router } from "@angular/router";
import { HttpErrorResponse } from "@angular/common/http";
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 { AccountInfo, AddAccount } from "../../states/accounts.state";
import { AccountInfo, AddAccount, AccountsStateModel } from "../../states/accounts.state";
import { NotificationService } from "../../services/notification.service";
import { MastodonService } from '../../services/mastodon.service';
@Component({
selector: "app-register-new-account",
@ -23,6 +24,7 @@ export class RegisterNewAccountComponent implements OnInit {
private authStorageKey: string = 'tempAuth';
constructor(
private readonly mastodonService: MastodonService,
private readonly notificationService: NotificationService,
private readonly authService: AuthService,
private readonly store: Store,
@ -45,13 +47,26 @@ export class RegisterNewAccountComponent implements OnInit {
}
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)
.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();
accountInfo.username = appDataWrapper.username.toLowerCase();
accountInfo.instance = appDataWrapper.instance.toLowerCase();
accountInfo.token = tokenData;
accountInfo.username = username;
accountInfo.instance = instance;
accountInfo.token = usedTokenData;
this.store.dispatch([new AddAccount(accountInfo)])
.subscribe(() => {
@ -68,6 +83,16 @@ export class RegisterNewAccountComponent implements OnInit {
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) {
this.hasError = true;
switch (type) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,11 @@ export class AddStream {
constructor(public stream: StreamElement) {}
}
export class UpdateStream {
static readonly type = '[Streams] Update stream';
constructor(public stream: StreamElement) {}
}
export class RemoveAllStreams {
static readonly type = '[Streams] Remove all streams';
constructor(public accountId :string) {}
@ -43,6 +48,20 @@ export class StreamsState {
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)
RemoveAllStreams(ctx: StateContext<StreamsStateModel>, action: RemoveAllStreams){
const state = ctx.getState();
@ -92,6 +111,10 @@ export class StreamsState {
export class StreamElement {
public id: string;
public hideBoosts: boolean = false;
public hideReplies: boolean = false;
public hideBots: boolean = false;
constructor(
public type: StreamTypeEnum,
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 {
width: 40px;
display: block;
margin: 5px auto;
margin: 5px auto;
}
.flexcroll {
@ -12,9 +32,11 @@
scrollbar-darkshadow-color: #08090d;
scrollbar-track-color: #08090d;
scrollbar-arrow-color: #08090d;
&::-webkit-scrollbar {
width: 7px;
}
&::-webkit-scrollbar-thumb {
-webkit-border-radius: 0px;
border-radius: 0px;
@ -35,4 +57,19 @@
width: 20px;
height: 20px;
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 */
}