Merge pull request #278 from NicolasConstant/develop

0.28.0 PR
This commit is contained in:
Nicolas Constant 2020-05-06 18:44:16 -04:00 committed by GitHub
commit 9595e6f5db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 359 additions and 156 deletions

View File

@ -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",

View File

@ -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>

View File

@ -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 {

View File

@ -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,9 +43,9 @@ 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[]>;
constructor(
@ -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;
@ -76,10 +82,10 @@ export class AppComponent implements OnInit, OnDestroy {
let usedTokenData: TokenData;
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;
tokenData.created_at = nowEpoch;
}
usedTokenData = tokenData;
@ -87,17 +93,17 @@ export class AppComponent implements OnInit, OnDestroy {
return this.mastodonService.retrieveAccountDetails({ 'instance': appDataWrapper.instance, 'id': '', 'username': '', 'order': 0, 'isSelected': true, 'token': tokenData });
})
.then((account: Account) => {
var username = account.username.toLowerCase();
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;
}
const accountInfo = new AccountInfo();
accountInfo.username = username;
accountInfo.username = username;
accountInfo.instance = instance;
accountInfo.token = usedTokenData;
@ -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;
}
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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);

View File

@ -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)]);

View File

@ -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 {

View File

@ -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>

View File

@ -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 {

View File

@ -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';

View File

@ -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];

View File

@ -266,6 +266,7 @@ export class UserProfileComponent implements OnInit {
refresh(): any {
this.showFloatingHeader = false;
this.showFloatingStatusMenu = false;
this.load(this.lastAccountName);
}

View File

@ -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)
}
return this.emojiConverter.applyEmojis(value.emojis, textToTransform, EmojiTypeEnum.small);
} catch (err){
return '';
}
}
}

View File

@ -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);
}

View File

@ -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 {

View File

@ -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()`.
@ -47,6 +49,6 @@ export class ServiceWorkerService {
}
checkForUpdates(): Promise<void> {
return this.updates.checkForUpdate();
return this.updates.checkForUpdate();
}
}

View File

@ -25,82 +25,94 @@ export class UserNotificationService {
private soundJustPlayed = false;
private soundFileId: string;
private accountSub: Subscription;
private loadedAccounts: AccountInfo[] = [];
constructor(
private readonly streamingService: StreamingService,
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly mastodonService: MastodonWrapperService,
private readonly store: Store) {
this.fetchNotifications();
}
}
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();
this.sound.play();
setTimeout(() => {
setTimeout(() => {
this.soundJustPlayed = false;
}, 2000);
}
private setNotificationSound() {
let settings = this.toolsService.getSettings();
let soundId = settings.notificationSoundFileId;
if(!soundId){
let soundId = settings.notificationSoundFileId;
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();
}
@ -129,15 +141,15 @@ export class UserNotificationService {
}
let currentNotifications = this.userNotifications.value;
let currentAccountNotifications = currentNotifications.find(x => x.account.id === account.id);
let currentAccountNotifications = currentNotifications.find(x => x.account.id === account.id);
if (currentAccountNotifications) {
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();
@ -230,7 +242,7 @@ export class UserNotificationService {
new NotificationSoundDefinition('0', 'assets/audio/all-eyes-on-me.mp3', 'All eyes on me'),
new NotificationSoundDefinition('1', 'assets/audio/exquisite.mp3', 'Exquisite'),
new NotificationSoundDefinition('2', 'assets/audio/appointed.mp3', 'Appointed'),
new NotificationSoundDefinition('3', 'assets/audio/boop.mp3', 'Mastodon boop'),
new NotificationSoundDefinition('3', 'assets/audio/boop.mp3', 'Mastodon boop'),
];
return defs;
}
@ -261,5 +273,5 @@ export class NotificationSoundDefinition {
constructor(
public readonly id: string,
public readonly path: string,
public readonly name: string) {}
public readonly name: string) { }
}