commit
9595e6f5db
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "sengi",
|
||||
"version": "0.27.1",
|
||||
"version": "0.28.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "main-electron.js",
|
||||
"description": "A multi-account desktop client for Mastodon and Pleroma",
|
||||
|
|
|
@ -10,9 +10,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auto-update" [class.auto-update__activated]="updateAvailable">
|
||||
<div *ngIf="showRestartNotification" class="auto-update" [class.auto-update__activated]="restartNotificationAvailable">
|
||||
<div class="auto-update__display">
|
||||
<div class="auto-update__display--text">A new version is available!</div> <a href class="auto-update__display--reload" (click)="loadNewVersion()">reload</a> <a href class="auto-update__display--close" (click)="closeAutoUpdate()"><fa-icon [icon]="faTimes"></fa-icon></a>
|
||||
<div class="auto-update__display--text">{{restartNotificationLabel}}</div> <a href class="auto-update__display--reload" (click)="loadNewVersion()" title="reload">reload</a> <a href class="auto-update__display--close" (click)="closeRestartNotification()" title="close"><fa-icon [icon]="faTimes"></fa-icon></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -104,17 +104,20 @@ app-streams-selection-footer {
|
|||
transition-timing-function: ease-in;
|
||||
|
||||
position: absolute;
|
||||
height: 70px;
|
||||
height: 50px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -80px;
|
||||
bottom: 0;
|
||||
//bottom: -80px;
|
||||
opacity: 0;
|
||||
z-index: 999999999;
|
||||
|
||||
&__activated {
|
||||
// opacity: 1;
|
||||
transition: all .25s;
|
||||
transition-timing-function: ease-out;
|
||||
bottom: 0px;
|
||||
opacity: 1;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
&__display {
|
||||
|
|
|
@ -32,7 +32,10 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
floatingColumnActive: boolean;
|
||||
tutorialActive: boolean;
|
||||
openedMediaEvent: OpenMediaEvent
|
||||
updateAvailable: boolean;
|
||||
|
||||
restartNotificationLabel: string;
|
||||
restartNotificationAvailable: boolean;
|
||||
showRestartNotification: boolean;
|
||||
|
||||
private authStorageKey: string = 'tempAuth';
|
||||
|
||||
|
@ -40,8 +43,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
private openMediaSub: Subscription;
|
||||
private streamSub: Subscription;
|
||||
private dragoverSub: Subscription;
|
||||
private updateAvailableSub: Subscription;
|
||||
private paramsSub: Subscription;
|
||||
private restartNotificationSub: Subscription;
|
||||
|
||||
@Select(state => state.streamsstatemodel.streams) streamElements$: Observable<StreamElement[]>;
|
||||
|
||||
|
@ -52,14 +55,17 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
private readonly mastodonService: MastodonWrapperService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly activatedRoute: ActivatedRoute,
|
||||
private readonly serviceWorkerService: ServiceWorkerService,
|
||||
private readonly serviceWorkerService: ServiceWorkerService, // Ensure update checks
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mediaService: MediaService,
|
||||
private readonly navigationService: NavigationService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.paramsSub = this.activatedRoute.queryParams.subscribe(params => {
|
||||
// disable tutorial for future update
|
||||
localStorage.setItem('tutorial', JSON.stringify(true));
|
||||
|
||||
this.paramsSub = this.activatedRoute.queryParams.subscribe(params => {
|
||||
const code = params['code'];
|
||||
if (!code) {
|
||||
return;
|
||||
|
@ -77,7 +83,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
this.authService.getToken(appDataWrapper.instance, appInfo.app.client_id, appInfo.app.client_secret, code, appInfo.app.redirect_uri)
|
||||
.then((tokenData: TokenData) => {
|
||||
|
||||
if(tokenData.refresh_token && !tokenData.created_at){
|
||||
if (tokenData.refresh_token && !tokenData.created_at) {
|
||||
const nowEpoch = Date.now() / 1000 | 0;
|
||||
tokenData.created_at = nowEpoch;
|
||||
}
|
||||
|
@ -90,7 +96,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
var username = account.username.toLowerCase();
|
||||
var instance = appDataWrapper.instance.toLowerCase();
|
||||
|
||||
if(this.isAccountAlreadyPresent(username, instance)){
|
||||
if (this.isAccountAlreadyPresent(username, instance)) {
|
||||
this.notificationService.notify(null, null, `Account @${username}@${instance} is already registered`, true);
|
||||
this.router.navigate(['/']);
|
||||
return;
|
||||
|
@ -113,10 +119,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
});
|
||||
|
||||
this.updateAvailableSub = this.serviceWorkerService.newAppVersionIsAvailable.subscribe((updateAvailable) => {
|
||||
this.updateAvailable = updateAvailable;
|
||||
});
|
||||
|
||||
this.streamSub = this.streamElements$.subscribe((streams: StreamElement[]) => {
|
||||
if (streams && streams.length === 0) {
|
||||
this.tutorialActive = true;
|
||||
|
@ -147,7 +149,13 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
)
|
||||
.subscribe(() => {
|
||||
this.drag = false;
|
||||
})
|
||||
});
|
||||
|
||||
this.restartNotificationSub = this.notificationService.restartNotificationStream.subscribe((label: string) => {
|
||||
if (label) {
|
||||
this.displayRestartNotification(label);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
@ -155,8 +163,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
this.columnEditorSub.unsubscribe();
|
||||
this.openMediaSub.unsubscribe();
|
||||
this.dragoverSub.unsubscribe();
|
||||
this.updateAvailableSub.unsubscribe();
|
||||
this.paramsSub.unsubscribe();
|
||||
this.restartNotificationSub.unsubscribe();
|
||||
}
|
||||
|
||||
closeMedia() {
|
||||
|
@ -195,19 +203,34 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
loadNewVersion(): boolean {
|
||||
this.serviceWorkerService.loadNewAppVersion();
|
||||
document.location.reload();
|
||||
// this.serviceWorkerService.loadNewAppVersion();
|
||||
return false;
|
||||
}
|
||||
|
||||
closeAutoUpdate(): boolean {
|
||||
this.updateAvailable = false;
|
||||
displayRestartNotification(label: string): boolean {
|
||||
this.restartNotificationLabel = label;
|
||||
this.showRestartNotification = true;
|
||||
setTimeout(() => {
|
||||
this.restartNotificationAvailable = true;
|
||||
}, 200);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private isAccountAlreadyPresent(username: string, instance: string): boolean{
|
||||
closeRestartNotification(): boolean {
|
||||
this.restartNotificationAvailable = false;
|
||||
setTimeout(() => {
|
||||
this.showRestartNotification = false;
|
||||
}, 250);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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){
|
||||
if (acc.instance === instance && acc.username == username) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</a>
|
||||
|
||||
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm status-editor__content" (paste)="onPaste($event)"
|
||||
rows="5" required title="content" placeholder="What's in your mind?" (keydown.control.enter)="onCtrlEnter()"
|
||||
rows="5" required title="content" placeholder="What's on your mind?" (keydown.control.enter)="onCtrlEnter()"
|
||||
(keydown)="handleKeyDown($event)" (blur)="statusTextEditorLostFocus()" dir="auto">
|
||||
</textarea>
|
||||
|
||||
|
|
|
@ -735,7 +735,11 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
|
||||
if (isVisible) {
|
||||
setTimeout(() => {
|
||||
this.footerElement.nativeElement.scrollIntoViewIfNeeded({ behavior: 'instant', block: 'end', inline: 'start' });
|
||||
try{
|
||||
this.footerElement.nativeElement.scrollIntoViewIfNeeded({ behavior: 'instant', block: 'end', inline: 'start' });
|
||||
}catch(err) {
|
||||
this.footerElement.nativeElement.scrollIntoView({ behavior: 'instant', block: 'end', inline: 'start' });
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,4 +127,15 @@ export class BookmarksComponent implements OnInit {
|
|||
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
}
|
||||
|
||||
applyGoToTop(): boolean {
|
||||
const stream = this.statustream.nativeElement as HTMLElement;
|
||||
setTimeout(() => {
|
||||
stream.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,6 +121,17 @@ export class DirectMessagesComponent implements OnInit {
|
|||
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
}
|
||||
|
||||
applyGoToTop(): boolean {
|
||||
const stream = this.statustream.nativeElement as HTMLElement;
|
||||
setTimeout(() => {
|
||||
stream.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class ConversationWrapper {
|
||||
|
|
|
@ -128,4 +128,15 @@ export class FavoritesComponent implements OnInit {
|
|||
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
}
|
||||
|
||||
applyGoToTop(): boolean {
|
||||
const stream = this.statustream.nativeElement as HTMLElement;
|
||||
setTimeout(() => {
|
||||
stream.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,20 +35,20 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<app-bookmarks class="account__body" *ngIf="subPanel === 'bookmarks'" [account]="account"
|
||||
<app-bookmarks #bookmarks class="account__body" *ngIf="subPanel === 'bookmarks'" [account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-bookmarks>
|
||||
<app-direct-messages class="account__body" *ngIf="subPanel === 'dm'" [account]="account"
|
||||
<app-direct-messages #dm 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"
|
||||
<app-favorites #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"
|
||||
<app-mentions #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"
|
||||
<app-notifications #notifications class="account__body" *ngIf="subPanel === 'notifications'" [account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-notifications>
|
||||
</div>
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ViewChild } from '@angular/core';
|
||||
import { faAt, faUserPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faBell, faEnvelope, faUser, faStar, faBookmark } from "@fortawesome/free-regular-svg-icons";
|
||||
import { Subscription } from 'rxjs';
|
||||
|
@ -10,6 +10,11 @@ import { MastodonWrapperService } from '../../../services/mastodon-wrapper.servi
|
|||
import { Account } from "../../../services/models/mastodon.interfaces";
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { AccountInfo } from '../../../states/accounts.state';
|
||||
import { BookmarksComponent } from './bookmarks/bookmarks.component';
|
||||
import { NotificationsComponent } from './notifications/notifications.component';
|
||||
import { MentionsComponent } from './mentions/mentions.component';
|
||||
import { DirectMessagesComponent } from './direct-messages/direct-messages.component';
|
||||
import { FavoritesComponent } from './favorites/favorites.component';
|
||||
|
||||
|
||||
@Component({
|
||||
|
@ -122,8 +127,35 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
loadSubPanel(subpanel: 'account' | 'notifications' | 'mentions' | 'dm' | 'favorites'): boolean {
|
||||
@ViewChild('bookmarks') bookmarksComp:BookmarksComponent;
|
||||
@ViewChild('notifications') notificationsComp:NotificationsComponent;
|
||||
@ViewChild('mentions') mentionsComp:MentionsComponent;
|
||||
@ViewChild('dm') dmComp:DirectMessagesComponent;
|
||||
@ViewChild('favorites') favoritesComp:FavoritesComponent;
|
||||
|
||||
loadSubPanel(subpanel: 'account' | 'notifications' | 'mentions' | 'dm' | 'favorites' | 'bookmarks'): boolean {
|
||||
if(this.subPanel === subpanel){
|
||||
switch(subpanel){
|
||||
case 'bookmarks':
|
||||
this.bookmarksComp.applyGoToTop();
|
||||
break;
|
||||
case 'notifications':
|
||||
this.notificationsComp.applyGoToTop();
|
||||
break;
|
||||
case 'mentions':
|
||||
this.mentionsComp.applyGoToTop();
|
||||
break;
|
||||
case 'dm':
|
||||
this.dmComp.applyGoToTop();
|
||||
break;
|
||||
case 'favorites':
|
||||
this.favoritesComp.applyGoToTop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.subPanel = subpanel;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -146,4 +146,15 @@ export class MentionsComponent implements OnInit, OnDestroy {
|
|||
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
}
|
||||
|
||||
applyGoToTop(): boolean {
|
||||
const stream = this.statustream.nativeElement as HTMLElement;
|
||||
setTimeout(() => {
|
||||
stream.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
<div class="my-account__body flexcroll">
|
||||
<h4 class="my-account__label">add timeline:</h4>
|
||||
<a class="my-account__link my-account__link--margin-bottom my-account__blue" href
|
||||
*ngFor="let stream of availableStreams" (click)="addStream(stream)"
|
||||
title="{{ stream.isAdded ? '' : 'add timeline'}}" [class.my-account__link--disabled]="stream.isAdded">
|
||||
{{ stream.name }} <fa-icon class="my-account__link--icon" *ngIf="stream.isAdded" [icon]="faCheckSquare">
|
||||
</fa-icon>
|
||||
</a>
|
||||
<div class="my-account__link--margin-bottom" *ngFor="let stream of availableStreams">
|
||||
<a href *ngIf="stream.isAdded" class="my-account__list--button" title="remove timeline" (click)="removeStream(stream)">
|
||||
<fa-icon class="my-account__link--icon my-account__link--remove" [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
<a href class="my-account__link my-account__link--margin-bottom my-account__blue" (click)="addStream(stream)"
|
||||
title="{{ stream.isAdded ? '' : 'add timeline'}}"
|
||||
[class.my-account__link--disabled]="stream.isAdded">
|
||||
{{ stream.name }} <!-- <fa-icon class="my-account__link--icon" *ngIf="stream.isAdded" [icon]="faCheckSquare"> </fa-icon> -->
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
<h4 class="my-account__label my-account__margin-top">manage list:</h4>
|
||||
<div class="my-account__link--margin-bottom" *ngFor="let list of availableLists">
|
||||
|
@ -26,11 +31,12 @@
|
|||
*ngIf="list.confirmDeletion">
|
||||
<fa-icon class="my-account__link--icon" [icon]="faCheck"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a href *ngIf="list.isAdded" class="my-account__list--button" title="remove list" (click)="removeStream(list)">
|
||||
<fa-icon class="my-account__link--icon my-account__list--remove" [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
<a class="my-account__link my-account__list my-account__blue" href (click)="addStream(list)"
|
||||
title="{{ list.isAdded ? '' : 'add list'}}" [class.my-account__link--disabled]="list.isAdded">
|
||||
{{ list.name }} <fa-icon class="my-account__link--icon" *ngIf="list.isAdded" [icon]="faCheckSquare">
|
||||
</fa-icon>
|
||||
title="{{ list.isAdded ? '' : 'add list'}}" [class.my-account__list--disabled]="list.isAdded">
|
||||
{{ list.name }} <!--<fa-icon class="my-account__link--icon" *ngIf="list.isAdded" [icon]="faCheckSquare"> </fa-icon>-->
|
||||
</a>
|
||||
|
||||
<app-list-editor *ngIf="list.editList" [list]="list" [account]="account"></app-list-editor>
|
||||
|
@ -44,7 +50,9 @@
|
|||
<h4 class="my-account__label my-account__margin-top">advanced settings:</h4>
|
||||
<div class="advanced-settings">
|
||||
<input class="advanced-settings__checkbox" [(ngModel)]="avatarNotificationDisabled"
|
||||
(change)="onDisableAvatarNotificationChanged()" type="checkbox" name="avatarNotification" value="avatarNotification" id="avatarNotification"> <label class="noselect advanced-settings__label" for="avatarNotification">disable avatar notifications</label><br>
|
||||
(change)="onDisableAvatarNotificationChanged()" type="checkbox" name="avatarNotification"
|
||||
value="avatarNotification" id="avatarNotification"> <label class="noselect advanced-settings__label"
|
||||
for="avatarNotification">disable avatar notifications</label><br>
|
||||
<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">custom char
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
@import "variables";
|
||||
@import "commons";
|
||||
|
||||
|
||||
$list-width: 60px;
|
||||
$button-width: $list-width/2;
|
||||
|
||||
.my-account {
|
||||
transition: all .2s;
|
||||
|
||||
|
@ -50,10 +54,19 @@
|
|||
float: right;
|
||||
}
|
||||
|
||||
&--remove {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
cursor: default;
|
||||
background-color: darken($color-primary, 4);
|
||||
|
||||
width: calc(100% - #{$button-width} - 1px);
|
||||
//outline: 1px solid greenyellow;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($color-primary, 4);
|
||||
}
|
||||
|
@ -67,9 +80,27 @@
|
|||
}
|
||||
|
||||
&__list {
|
||||
$list-width: 60px;
|
||||
|
||||
width: calc(100% - #{$list-width} - 2px);
|
||||
|
||||
&--remove {
|
||||
position: relative;
|
||||
top: 0px;
|
||||
right: 1px;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
cursor: default;
|
||||
background-color: darken($color-primary, 4);
|
||||
|
||||
width: calc(100% - #{$button-width} * 3 - 3px);
|
||||
//outline: 1px solid greenyellow;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($color-primary, 4);
|
||||
}
|
||||
}
|
||||
|
||||
&--button {
|
||||
margin-left: 1px;
|
||||
width: calc(#{$list-width}/2);
|
||||
|
|
|
@ -104,18 +104,22 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
});
|
||||
|
||||
this.availableLists.length = 0;
|
||||
// this.availableLists.length = 0;
|
||||
this.mastodonService.getLists(account.info)
|
||||
.then((streams: StreamElement[]) => {
|
||||
this.availableLists.length = 0;
|
||||
// this.availableLists.length = 0;
|
||||
for (let stream of streams) {
|
||||
let wrappedStream = new StreamWrapper(stream);
|
||||
let wrappedStream = this.availableLists.find(x => x.id === stream.id);
|
||||
if(!wrappedStream){
|
||||
wrappedStream = new StreamWrapper(stream);
|
||||
this.availableLists.push(wrappedStream);
|
||||
}
|
||||
|
||||
if(loadedStreams.find(x => x.id == stream.id)){
|
||||
wrappedStream.isAdded = true;
|
||||
} else {
|
||||
wrappedStream.isAdded = false;
|
||||
}
|
||||
this.availableLists.push(wrappedStream);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
|
@ -133,6 +137,16 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
|||
return false;
|
||||
}
|
||||
|
||||
removeStream(stream: StreamWrapper): boolean {
|
||||
if (stream && stream.isAdded) {
|
||||
this.store.dispatch([new RemoveStream(stream.id)]).toPromise()
|
||||
.then(() => {
|
||||
stream.isAdded = false;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
removeAccount(): boolean {
|
||||
const accountId = this.account.info.id;
|
||||
this.store.dispatch([new RemoveAllStreams(accountId), new RemoveAccount(accountId)]);
|
||||
|
|
|
@ -135,6 +135,17 @@ export class NotificationsComponent implements OnInit, OnDestroy {
|
|||
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
}
|
||||
|
||||
applyGoToTop(): boolean {
|
||||
const stream = this.statustream.nativeElement as HTMLElement;
|
||||
setTimeout(() => {
|
||||
stream.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class NotificationWrapper {
|
||||
|
|
|
@ -48,9 +48,6 @@
|
|||
type="radio" name="colmun-win" value="colmun-win" id="colmun-win">
|
||||
<label class="noselect sub-section__label" for="colmun-win">Win + Alt + Left | Win + Alt + Right</label>
|
||||
<br>
|
||||
|
||||
<span class="sub-section__title" *ngIf="columnShortcutChanged">this settings needs a <a href
|
||||
(click)="reload()">reload</a> to be effective.</span>
|
||||
</div>
|
||||
|
||||
<h4 class="panel__subtitle">Content-Warning Policies</h4>
|
||||
|
@ -90,9 +87,6 @@
|
|||
<input type="text" class="form-control form-control-sm sub_section__text-input"
|
||||
[(ngModel)]="setContentHidedCompletely" placeholder="example;other example" />
|
||||
</div>
|
||||
|
||||
<span class="sub-section__title" *ngIf="contentWarningPolicyChanged"><br />this settings needs a <a href
|
||||
(click)="reload()">reload</a> to be effective.</span>
|
||||
</div>
|
||||
|
||||
<h4 class="panel__subtitle">Timelines</h4>
|
||||
|
@ -123,9 +117,6 @@
|
|||
<label class="noselect sub-section__label" for="timelineheader-5">Title</label>
|
||||
<br>
|
||||
|
||||
<span class="sub-section__title" *ngIf="timeLineHeaderChanged">this settings needs a <a href
|
||||
(click)="reload()">reload</a> to be effective.</span>
|
||||
|
||||
<span class="sub-section__title">loading behavior:</span><br />
|
||||
|
||||
<input class="sub-section__checkbox" [checked]="timeLineMode === 1" (change)="onTimeLineModeChange(1)"
|
||||
|
@ -142,9 +133,6 @@
|
|||
type="radio" name="timelinemode-3" value="timelinemode-3" id="timelinemode-3">
|
||||
<label class="noselect sub-section__label" for="timelinemode-3">Slow mode (manual loading)</label>
|
||||
<br>
|
||||
|
||||
<span class="sub-section__title" *ngIf="timeLineModeChanged">this settings needs a <a href
|
||||
(click)="reload()">reload</a> to be effective.</span>
|
||||
</div>
|
||||
|
||||
<h4 class="panel__subtitle">Other</h4>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { ToolsService } from '../../../services/tools.service';
|
|||
import { UserNotificationService, NotificationSoundDefinition } from '../../../services/user-notification.service';
|
||||
import { ServiceWorkerService } from '../../../services/service-worker.service';
|
||||
import { ContentWarningPolicy, ContentWarningPolicyEnum, TimeLineModeEnum, TimeLineHeaderEnum } from '../../../states/settings.state';
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
|
@ -27,16 +28,9 @@ export class SettingsComponent implements OnInit {
|
|||
version: string;
|
||||
|
||||
columnShortcutEnabled: ColumnShortcut = ColumnShortcut.Ctrl;
|
||||
columnShortcutChanged = false;
|
||||
|
||||
timeLineHeader: TimeLineHeaderEnum = TimeLineHeaderEnum.Title_DomainName;
|
||||
timeLineHeaderChanged = false;
|
||||
|
||||
timeLineMode: TimeLineModeEnum = TimeLineModeEnum.OnTop;
|
||||
timeLineModeChanged = false;
|
||||
|
||||
contentWarningPolicy: ContentWarningPolicyEnum = ContentWarningPolicyEnum.None;
|
||||
contentWarningPolicyChanged = false;
|
||||
|
||||
private addCwOnContent: string;
|
||||
set setAddCwOnContent(value: string) {
|
||||
|
@ -69,6 +63,7 @@ export class SettingsComponent implements OnInit {
|
|||
private formBuilder: FormBuilder,
|
||||
private serviceWorkersService: ServiceWorkerService,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly userNotificationsService: UserNotificationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -104,7 +99,7 @@ export class SettingsComponent implements OnInit {
|
|||
|
||||
onShortcutChange(id: ColumnShortcut) {
|
||||
this.columnShortcutEnabled = id;
|
||||
this.columnShortcutChanged = true;
|
||||
this.notifyRestartNeeded();
|
||||
|
||||
let settings = this.toolsService.getSettings();
|
||||
settings.columnSwitchingWinAlt = id === ColumnShortcut.Win;
|
||||
|
@ -113,7 +108,7 @@ export class SettingsComponent implements OnInit {
|
|||
|
||||
onTimeLineHeaderChange(id: TimeLineHeaderEnum){
|
||||
this.timeLineHeader = id;
|
||||
this.timeLineHeaderChanged = true;
|
||||
this.notifyRestartNeeded();
|
||||
|
||||
let settings = this.toolsService.getSettings();
|
||||
settings.timelineHeader = id;
|
||||
|
@ -122,7 +117,7 @@ export class SettingsComponent implements OnInit {
|
|||
|
||||
onTimeLineModeChange(id: TimeLineModeEnum){
|
||||
this.timeLineMode = id;
|
||||
this.timeLineModeChanged = true;
|
||||
this.notifyRestartNeeded();
|
||||
|
||||
let settings = this.toolsService.getSettings();
|
||||
settings.timelineMode = id;
|
||||
|
@ -131,13 +126,13 @@ export class SettingsComponent implements OnInit {
|
|||
|
||||
onCwPolicyChange(id: ContentWarningPolicyEnum) {
|
||||
this.contentWarningPolicy = id;
|
||||
this.contentWarningPolicyChanged = true;
|
||||
this.notifyRestartNeeded();
|
||||
|
||||
this.setCwPolicy(id);
|
||||
}
|
||||
|
||||
private setCwPolicy(id: ContentWarningPolicyEnum = null, addCw: string = null, removeCw: string = null, hide: string = null){
|
||||
this.contentWarningPolicyChanged = true;
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.toolsService.getSettings();
|
||||
let cwPolicySettings = new ContentWarningPolicy();
|
||||
|
||||
|
@ -172,10 +167,10 @@ export class SettingsComponent implements OnInit {
|
|||
return data.split(';').map(x => x.trim().toLowerCase()).filter((value, index, self) => self.indexOf(value) === index).filter(y => y !== '');
|
||||
}
|
||||
|
||||
reload(): boolean {
|
||||
window.location.reload();
|
||||
return false;
|
||||
}
|
||||
// reload(): boolean {
|
||||
// window.location.reload();
|
||||
// return false;
|
||||
// }
|
||||
|
||||
onChange(soundId: string) {
|
||||
this.notificationSoundId = soundId;
|
||||
|
@ -196,18 +191,21 @@ export class SettingsComponent implements OnInit {
|
|||
}
|
||||
|
||||
onDisableAutofocusChanged() {
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.toolsService.getSettings();
|
||||
settings.disableAutofocus = this.disableAutofocusEnabled;
|
||||
this.toolsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onDisableRemoteStatusFetchingChanged() {
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.toolsService.getSettings();
|
||||
settings.disableRemoteStatusFetching = this.disableRemoteStatusFetchingEnabled;
|
||||
this.toolsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onDisableAvatarNotificationsChanged() {
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.toolsService.getSettings();
|
||||
settings.disableAvatarNotifications = this.disableAvatarNotificationsEnabled;
|
||||
this.toolsService.saveSettings(settings);
|
||||
|
@ -248,6 +246,10 @@ export class SettingsComponent implements OnInit {
|
|||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
notifyRestartNeeded(){
|
||||
this.notificationService.notifyRestartNotification('Reload to apply changes');
|
||||
}
|
||||
}
|
||||
|
||||
enum ColumnShortcut {
|
||||
|
|
|
@ -31,6 +31,12 @@ describe('DatabindedTextComponent', () => {
|
|||
expect(component.processedText).toContain(sample);
|
||||
});
|
||||
|
||||
it('should parse href text', () => {
|
||||
const sample = '<p>href<p>';
|
||||
component.text = sample;
|
||||
expect(component.processedText).toBe(sample);
|
||||
});
|
||||
|
||||
it('should parse hashtag', () => {
|
||||
const hashtag = 'programmers';
|
||||
const url = 'https://test.social/tags/programmers';
|
||||
|
|
|
@ -28,7 +28,7 @@ export class DatabindedTextComponent implements OnInit {
|
|||
|
||||
@Input('text')
|
||||
set text(value: string) {
|
||||
//console.warn(value);
|
||||
// console.warn(value);
|
||||
|
||||
let parser = new DOMParser();
|
||||
var dom = parser.parseFromString(value, 'text/html')
|
||||
|
@ -133,6 +133,11 @@ export class DatabindedTextComponent implements OnInit {
|
|||
}
|
||||
|
||||
private processLink(section: string) {
|
||||
if(!section.includes('</a>')){
|
||||
this.processedText += section;
|
||||
return;
|
||||
}
|
||||
|
||||
let extractedLinkAndNext = section.split('</a>')
|
||||
let extractedUrl = extractedLinkAndNext[0].split('"')[1];
|
||||
|
||||
|
|
|
@ -266,6 +266,7 @@ export class UserProfileComponent implements OnInit {
|
|||
|
||||
refresh(): any {
|
||||
this.showFloatingHeader = false;
|
||||
this.showFloatingStatusMenu = false;
|
||||
this.load(this.lastAccountName);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,19 +4,22 @@ import { EmojiConverter, EmojiTypeEnum } from '../tools/emoji.tools';
|
|||
import { Account } from '../services/models/mastodon.interfaces';
|
||||
|
||||
@Pipe({
|
||||
name: "accountEmoji"
|
||||
name: "accountEmoji"
|
||||
})
|
||||
export class AccountEmojiPipe implements PipeTransform {
|
||||
private emojiConverter = new EmojiConverter();
|
||||
private emojiConverter = new EmojiConverter();
|
||||
|
||||
transform(value: Account, text?: string): any {
|
||||
transform(value: Account, text?: string): any {
|
||||
try {
|
||||
let textToTransform = text;
|
||||
if (!text) {
|
||||
if (value.display_name) textToTransform = value.display_name;
|
||||
else textToTransform = value.acct.split('@')[0];
|
||||
}
|
||||
|
||||
let textToTransform = text;
|
||||
if(!text){
|
||||
if(value.display_name) textToTransform = value.display_name;
|
||||
else textToTransform = value.acct.split('@')[0];
|
||||
return this.emojiConverter.applyEmojis(value.emojis, textToTransform, EmojiTypeEnum.small);
|
||||
} catch (err){
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return this.emojiConverter.applyEmojis(value.emojis, textToTransform, EmojiTypeEnum.small)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { AppInfo, RegisteredAppsStateModel } from '../states/registered-apps.sta
|
|||
providedIn: 'root'
|
||||
})
|
||||
export class MastodonWrapperService {
|
||||
private refreshingToken: { [id: string]: Promise<AccountInfo> } = {};
|
||||
|
||||
constructor(
|
||||
private readonly store: Store,
|
||||
|
@ -19,6 +20,10 @@ export class MastodonWrapperService {
|
|||
private readonly mastodonService: MastodonService) { }
|
||||
|
||||
refreshAccountIfNeeded(accountInfo: AccountInfo): Promise<AccountInfo> {
|
||||
if(this.refreshingToken[accountInfo.id]){
|
||||
return this.refreshingToken[accountInfo.id];
|
||||
}
|
||||
|
||||
let isExpired = false;
|
||||
let storedAccountInfo = this.getStoreAccountInfo(accountInfo.id);
|
||||
|
||||
|
@ -47,11 +52,8 @@ export class MastodonWrapperService {
|
|||
}
|
||||
|
||||
if (storedAccountInfo.token.refresh_token && isExpired) {
|
||||
console.log('>>> MARTY!! ------------');
|
||||
console.log('>>> RENEW TOKEN FFS ----');
|
||||
|
||||
const app = this.getAllSavedApps().find(x => x.instance === storedAccountInfo.instance);
|
||||
return this.authService.refreshToken(storedAccountInfo.instance, app.app.client_id, app.app.client_secret, storedAccountInfo.token.refresh_token)
|
||||
let p = this.authService.refreshToken(storedAccountInfo.instance, app.app.client_id, app.app.client_secret, storedAccountInfo.token.refresh_token)
|
||||
.then((tokenData: TokenData) => {
|
||||
if (tokenData.refresh_token && !tokenData.created_at) {
|
||||
const nowEpoch = Date.now() / 1000 | 0;
|
||||
|
@ -66,6 +68,13 @@ export class MastodonWrapperService {
|
|||
.catch(err => {
|
||||
return Promise.resolve(storedAccountInfo);
|
||||
});
|
||||
|
||||
p.then(() => {
|
||||
this.refreshingToken[accountInfo.id] = null;
|
||||
});
|
||||
|
||||
this.refreshingToken[accountInfo.id] = p;
|
||||
return p;
|
||||
} else {
|
||||
return Promise.resolve(storedAccountInfo);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { ToolsService } from './tools.service';
|
|||
|
||||
@Injectable()
|
||||
export class NotificationService {
|
||||
public restartNotificationStream = new Subject<string>();
|
||||
public notifactionStream = new Subject<NotificatioData>();
|
||||
public newRespondPostedStream = new Subject<NewReplyData>();
|
||||
public hideAccountUrlStream = new Subject<string>();
|
||||
|
@ -60,6 +61,10 @@ export class NotificationService {
|
|||
public deleteStatus(status: StatusWrapper) {
|
||||
this.deletedStatusStream.next(status);
|
||||
}
|
||||
|
||||
public notifyRestartNotification(label: string){
|
||||
this.restartNotificationStream.next(label);
|
||||
}
|
||||
}
|
||||
|
||||
export class NotificatioData {
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import { Injectable, ApplicationRef } from '@angular/core';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { interval, concat, BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { NotificationService } from './notification.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ServiceWorkerService {
|
||||
newAppVersionIsAvailable = new BehaviorSubject<boolean>(false);
|
||||
|
||||
private isListening = false;
|
||||
|
||||
constructor(appRef: ApplicationRef, private updates: SwUpdate) {
|
||||
constructor(
|
||||
appRef: ApplicationRef,
|
||||
private updates: SwUpdate,
|
||||
private notificationService: NotificationService) {
|
||||
|
||||
//https://angular.io/guide/service-worker-communications
|
||||
|
||||
|
@ -19,7 +21,7 @@ export class ServiceWorkerService {
|
|||
console.log('current version is', event.current);
|
||||
console.log('available version is', event.available);
|
||||
|
||||
this.newAppVersionIsAvailable.next(true);
|
||||
this.notificationService.notifyRestartNotification('A new version is available!');
|
||||
});
|
||||
|
||||
// Allow the app to stabilize first, before starting polling for updates with `interval()`.
|
||||
|
|
|
@ -25,6 +25,9 @@ export class UserNotificationService {
|
|||
private soundJustPlayed = false;
|
||||
private soundFileId: string;
|
||||
|
||||
private accountSub: Subscription;
|
||||
private loadedAccounts: AccountInfo[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly streamingService: StreamingService,
|
||||
private readonly toolsService: ToolsService,
|
||||
|
@ -37,49 +40,58 @@ export class UserNotificationService {
|
|||
|
||||
private fetchNotifications() {
|
||||
let accounts = this.store.snapshot().registeredaccounts.accounts;
|
||||
// let promises: Promise<any>[] = [];
|
||||
|
||||
accounts.forEach((account: AccountInfo) => {
|
||||
// let sinceId = null;
|
||||
// if (this.sinceIds[account.id]) {
|
||||
// sinceId = this.sinceIds[account.id];
|
||||
// }
|
||||
|
||||
let getMentionsPromise = this.mastodonService.getNotifications(account, ['favourite', 'follow', 'reblog', 'poll'], null, null, 10)
|
||||
.then((notifications: Notification[]) => {
|
||||
this.processMentionsAndNotifications(account, notifications, NotificationTypeEnum.UserMention);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, account);
|
||||
});
|
||||
|
||||
let getNotificationPromise = this.mastodonService.getNotifications(account, ['mention'], null, null, 10)
|
||||
.then((notifications: Notification[]) => {
|
||||
this.processMentionsAndNotifications(account, notifications, NotificationTypeEnum.UserNotification);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, account);
|
||||
});
|
||||
|
||||
Promise.all([getMentionsPromise, getNotificationPromise])
|
||||
.then(() => {
|
||||
let streamElement = new StreamElement(StreamTypeEnum.personnal, 'activity', account.id, null, null, null, account.instance);
|
||||
|
||||
let streaming = this.streamingService.getStreaming(account, streamElement);
|
||||
streaming.statusUpdateSubjet.subscribe((notification: StatusUpdate) => {
|
||||
if (notification && notification.type === EventEnum.notification) {
|
||||
this.processNewUpdate(account, notification);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(err => { });
|
||||
this.loadedAccounts.push(account);
|
||||
this.startFetchingNotifications(account);
|
||||
});
|
||||
|
||||
this.accountSub = this.store.select(state => state.registeredaccounts.accounts)
|
||||
.subscribe((accounts: AccountInfo[]) => {
|
||||
accounts.forEach(a => {
|
||||
if(!this.loadedAccounts.find(x => x.id === a.id)){
|
||||
this.loadedAccounts.push(a);
|
||||
this.startFetchingNotifications(a);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private startFetchingNotifications(account: AccountInfo) {
|
||||
let getMentionsPromise = this.mastodonService.getNotifications(account, ['favourite', 'follow', 'reblog', 'poll'], null, null, 10)
|
||||
.then((notifications: Notification[]) => {
|
||||
this.processMentionsAndNotifications(account, notifications, NotificationTypeEnum.UserMention);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, account);
|
||||
});
|
||||
|
||||
let getNotificationPromise = this.mastodonService.getNotifications(account, ['mention'], null, null, 10)
|
||||
.then((notifications: Notification[]) => {
|
||||
this.processMentionsAndNotifications(account, notifications, NotificationTypeEnum.UserNotification);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, account);
|
||||
});
|
||||
|
||||
Promise.all([getMentionsPromise, getNotificationPromise])
|
||||
.then(() => {
|
||||
let streamElement = new StreamElement(StreamTypeEnum.personnal, 'activity', account.id, null, null, null, account.instance);
|
||||
|
||||
let streaming = this.streamingService.getStreaming(account, streamElement);
|
||||
streaming.statusUpdateSubjet.subscribe((notification: StatusUpdate) => {
|
||||
if (notification && notification.type === EventEnum.notification) {
|
||||
this.processNewUpdate(account, notification);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(err => { });
|
||||
}
|
||||
|
||||
private playSoundNotification() {
|
||||
const settings = this.toolsService.getSettings();
|
||||
if(settings.disableSounds) return;
|
||||
if(this.soundJustPlayed) return;
|
||||
if (settings.disableSounds) return;
|
||||
if (this.soundJustPlayed) return;
|
||||
this.soundJustPlayed = true;
|
||||
|
||||
this.setNotificationSound();
|
||||
|
@ -94,13 +106,13 @@ export class UserNotificationService {
|
|||
let settings = this.toolsService.getSettings();
|
||||
let soundId = settings.notificationSoundFileId;
|
||||
|
||||
if(!soundId){
|
||||
if (!soundId) {
|
||||
soundId = '0';
|
||||
settings.notificationSoundFileId = '0';
|
||||
this.toolsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
if(this.soundFileId === soundId) return;
|
||||
if (this.soundFileId === soundId) return;
|
||||
|
||||
var sound = this.getAllNotificationSounds().find(x => x.id === soundId);
|
||||
this.sound = new Howl({
|
||||
|
@ -110,9 +122,9 @@ export class UserNotificationService {
|
|||
}
|
||||
|
||||
private processNewUpdate(account: AccountInfo, notification: StatusUpdate) {
|
||||
if(!notification && !notification.notification) return;
|
||||
if (!notification && !notification.notification) return;
|
||||
|
||||
if(!notification.muteSound){
|
||||
if (!notification.muteSound) {
|
||||
this.playSoundNotification();
|
||||
}
|
||||
|
||||
|
@ -135,9 +147,9 @@ export class UserNotificationService {
|
|||
currentAccountNotifications = this.analyseNotifications(account, currentAccountNotifications, notifications, type);
|
||||
|
||||
//if (currentAccountNotifications.hasNewMentions || currentAccountNotifications.hasNewNotifications) {
|
||||
currentNotifications = currentNotifications.filter(x => x.account.id !== account.id);
|
||||
currentNotifications.push(currentAccountNotifications);
|
||||
this.userNotifications.next(currentNotifications);
|
||||
currentNotifications = currentNotifications.filter(x => x.account.id !== account.id);
|
||||
currentNotifications.push(currentAccountNotifications);
|
||||
this.userNotifications.next(currentNotifications);
|
||||
//}
|
||||
} else {
|
||||
let newNotifications = new UserNotification();
|
||||
|
@ -261,5 +273,5 @@ export class NotificationSoundDefinition {
|
|||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly path: string,
|
||||
public readonly name: string) {}
|
||||
public readonly name: string) { }
|
||||
}
|
Loading…
Reference in New Issue