Merge pull request #300 from NicolasConstant/topic_fix-spamming

Topic fix spamming
This commit is contained in:
Nicolas Constant 2020-06-14 10:22:05 +02:00 committed by GitHub
commit 675dd0a3a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 349 additions and 523 deletions

View File

@ -0,0 +1,25 @@
import { Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { OpenThreadEvent } from '../../services/tools.service';
export abstract class BrowseBase implements OnInit, OnDestroy {
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
abstract ngOnInit();
abstract ngOnDestroy();
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
}

View File

@ -0,0 +1,115 @@
import { OnInit, Input, OnDestroy, Output, EventEmitter, ElementRef, ViewChild } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { MastodonWrapperService } from '../../services/mastodon-wrapper.service';
import { AccountInfo } from '../../states/accounts.state';
import { StreamingWrapper } from '../../services/streaming.service';
import { NotificationService } from '../../services/notification.service';
import { ToolsService, OpenThreadEvent } from '../../services/tools.service';
import { StatusWrapper } from '../../models/common.model';
import { Status } from '../../services/models/mastodon.interfaces';
import { TimeLineModeEnum } from '../../states/settings.state';
import { BrowseBase } from './browse-base';
export abstract class TimelineBase extends BrowseBase {
isLoading = true;
protected maxReached = false;
isThread = false;
displayError: string;
hasContentWarnings = false;
timelineLoadingMode: TimeLineModeEnum = TimeLineModeEnum.OnTop;
protected account: AccountInfo;
protected websocketStreaming: StreamingWrapper;
statuses: StatusWrapper[] = [];
bufferStream: Status[] = [];
protected bufferWasCleared: boolean;
streamPositionnedAtTop: boolean = true;
protected isProcessingInfiniteScroll: boolean;
protected hideBoosts: boolean;
protected hideReplies: boolean;
protected hideBots: boolean;
@Input() goToTop: Observable<void>;
@Input() userLocked = true;
@ViewChild('statusstream') public statustream: ElementRef;
constructor(
protected readonly toolsService: ToolsService,
protected readonly notificationService: NotificationService,
protected readonly mastodonService: MastodonWrapperService) {
super();
}
abstract ngOnInit();
abstract ngOnDestroy();
protected abstract scrolledToTop();
protected abstract statusProcessOnGoToTop();
protected abstract getNextStatuses(): Promise<Status[]>;
onScroll() {
var element = this.statustream.nativeElement as HTMLElement;
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
const atTop = element.scrollTop === 0;
this.streamPositionnedAtTop = false;
if (atBottom && !this.isProcessingInfiniteScroll) {
this.scrolledToBottom();
} else if (atTop) {
this.scrolledToTop();
}
}
private scrolledErrorOccured = false;
protected scrolledToBottom() {
if (this.isLoading || this.maxReached || this.scrolledErrorOccured) return;
this.isLoading = true;
this.isProcessingInfiniteScroll = true;
this.getNextStatuses()
.then((status: Status[]) => {
if (!status || status.length === 0 || this.maxReached) {
this.maxReached = true;
return;
}
for (const s of status) {
let cwPolicy = this.toolsService.checkContentWarning(s);
const wrapper = new StatusWrapper(cwPolicy.status, this.account, cwPolicy.applyCw, cwPolicy.hide);
this.statuses.push(wrapper);
}
})
.catch((err: HttpErrorResponse) => {
this.scrolledErrorOccured = true;
setTimeout(() => {
this.scrolledErrorOccured = false;
}, 5000);
this.notificationService.notifyHttpError(err, this.account);
})
.then(() => {
this.isLoading = false;
this.isProcessingInfiniteScroll = false;
});
}
applyGoToTop(): boolean {
this.statusProcessOnGoToTop();
const stream = this.statustream.nativeElement as HTMLElement;
setTimeout(() => {
stream.scrollTo({
top: 0,
behavior: 'smooth'
});
}, 0);
return false;
}
}

View File

@ -1,3 +0,0 @@
<p>
bookmarks works!
</p>

View File

@ -1,57 +1,49 @@
import { Component, OnInit, Output, EventEmitter, Input, ViewChild, ElementRef } from '@angular/core';
import { Component, Input, ViewChild, ElementRef } from '@angular/core';
import { StatusWrapper } from '../../../../models/common.model';
import { OpenThreadEvent, ToolsService } from '../../../../services/tools.service';
import { ToolsService } from '../../../../services/tools.service';
import { AccountWrapper } from '../../../../models/account.models';
import { FavoriteResult, BookmarkResult } from '../../../../services/mastodon.service';
import { BookmarkResult } from '../../../../services/mastodon.service';
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
import { Status } from '../../../../services/models/mastodon.interfaces';
import { NotificationService } from '../../../../services/notification.service';
import { TimeLineModeEnum } from '../../../../states/settings.state';
import { TimelineBase } from '../../../../components/common/timeline-base';
@Component({
selector: 'app-bookmarks',
templateUrl: '../../../stream/stream-statuses/stream-statuses.component.html',
styleUrls: ['../../../stream/stream-statuses/stream-statuses.component.scss', './bookmarks.component.scss']
})
export class BookmarksComponent implements OnInit {
statuses: StatusWrapper[] = [];
displayError: string;
isLoading = true;
isThread = false;
hasContentWarnings = false;
bufferStream: Status[] = []; //html compatibility only
streamPositionnedAtTop: boolean = true; //html compatibility only
timelineLoadingMode: TimeLineModeEnum = TimeLineModeEnum.OnTop; //html compatibility only
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
private maxReached = false;
export class BookmarksComponent extends TimelineBase {
private maxId: string;
private _account: AccountWrapper;
private _accountWrapper: AccountWrapper;
@Input('account')
set account(acc: AccountWrapper) {
this._account = acc;
set accountWrapper(acc: AccountWrapper) {
this._accountWrapper = acc;
this.account = acc.info;
this.getBookmarks();
}
get account(): AccountWrapper {
return this._account;
get accountWrapper(): AccountWrapper {
return this._accountWrapper;
}
@ViewChild('statusstream') public statustream: ElementRef;
constructor(
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly mastodonService: MastodonWrapperService) { }
protected readonly toolsService: ToolsService,
protected readonly notificationService: NotificationService,
protected readonly mastodonService: MastodonWrapperService) {
super(toolsService, notificationService, mastodonService);
}
ngOnInit() {
}
ngOnDestroy() {
}
private reset() {
this.isLoading = true;
this.statuses.length = 0;
@ -62,80 +54,38 @@ export class BookmarksComponent implements OnInit {
private getBookmarks() {
this.reset();
this.mastodonService.getBookmarks(this.account.info)
this.mastodonService.getBookmarks(this.account)
.then((result: BookmarkResult) => {
this.maxId = result.max_id;
for (const s of result.bookmarked) {
let cwPolicy = this.toolsService.checkContentWarning(s);
const wrapper = new StatusWrapper(cwPolicy.status, this.account.info, cwPolicy.applyCw, cwPolicy.hide);
const wrapper = new StatusWrapper(cwPolicy.status, this.account, cwPolicy.applyCw, cwPolicy.hide);
this.statuses.push(wrapper);
}
})
.catch(err => {
this.notificationService.notifyHttpError(err, this.account.info);
this.notificationService.notifyHttpError(err, this.account);
})
.then(() => {
this.isLoading = false;
});
}
onScroll() {
var element = this.statustream.nativeElement as HTMLElement;
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
if (atBottom) {
this.scrolledToBottom();
}
}
private scrolledToBottom() {
if (this.isLoading || this.maxReached) return;
this.isLoading = true;
this.mastodonService.getBookmarks(this.account.info, this.maxId)
protected getNextStatuses(): Promise<Status[]> {
return this.mastodonService.getBookmarks(this.account, this.maxId)
.then((result: BookmarkResult) => {
const statuses = result.bookmarked;
if (statuses.length === 0 || !this.maxId) {
this.maxReached = true;
return;
}
this.maxId = result.max_id;
for (const s of statuses) {
let cwPolicy = this.toolsService.checkContentWarning(s);
const wrapper = new StatusWrapper(cwPolicy.status, this.account.info, cwPolicy.applyCw, cwPolicy.hide);
this.statuses.push(wrapper);
if(!this.maxId){
this.maxReached = true;
}
})
.catch(err => {
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
this.isLoading = false;
return statuses;
});
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
protected scrolledToTop() {}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
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;
}
protected statusProcessOnGoToTop(){}
}

View File

@ -8,13 +8,14 @@ import { NotificationService } from '../../../../services/notification.service';
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
import { Conversation } from '../../../../services/models/mastodon.interfaces';
import { AccountInfo } from '../../../../states/accounts.state';
import { BrowseBase } from '../../../common/browse-base';
@Component({
selector: 'app-direct-messages',
templateUrl: './direct-messages.component.html',
styleUrls: ['../../../stream/stream-statuses/stream-statuses.component.scss', './direct-messages.component.scss']
})
export class DirectMessagesComponent implements OnInit {
export class DirectMessagesComponent extends BrowseBase {
faUserFriends = faUserFriends;
conversations: ConversationWrapper[] = [];
@ -23,9 +24,7 @@ export class DirectMessagesComponent implements OnInit {
isThread = false;
hasContentWarnings = false;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
private isProcessingInfiniteScroll: boolean;
private maxReached = false;
private _account: AccountWrapper;
@ -44,11 +43,16 @@ export class DirectMessagesComponent implements OnInit {
constructor(
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly mastodonService: MastodonWrapperService) { }
private readonly mastodonService: MastodonWrapperService) {
super();
}
ngOnInit() {
}
ngOnDestroy() {
}
private reset() {
this.isLoading = true;
this.conversations.length = 0;
@ -78,17 +82,19 @@ export class DirectMessagesComponent implements OnInit {
var element = this.statustream.nativeElement as HTMLElement;
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
if (atBottom) {
if (atBottom && !this.isProcessingInfiniteScroll) {
this.scrolledToBottom();
}
}
private scrolledErrorOccured = false;
private scrolledToBottom() {
if (this.isLoading || this.maxReached) return;
if (this.isLoading || this.maxReached || this.scrolledErrorOccured) return;
const maxId = this.conversations[this.conversations.length - 1].conversation.last_status.id;
this.isLoading = true;
this.isProcessingInfiniteScroll = true;
this.mastodonService.getConversations(this.account.info, maxId)
.then((conversations: Conversation[]) => {
if (conversations.length === 0) {
@ -103,25 +109,19 @@ export class DirectMessagesComponent implements OnInit {
}
})
.catch(err => {
this.scrolledErrorOccured = true;
setTimeout(() => {
this.scrolledErrorOccured = false;
}, 5000);
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
this.isLoading = false;
this.isProcessingInfiniteScroll = false;
});
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
applyGoToTop(): boolean {
const stream = this.statustream.nativeElement as HTMLElement;
setTimeout(() => {

View File

@ -1,3 +0,0 @@
<p>
favorites works!
</p>

View File

@ -1,58 +1,48 @@
import { Component, OnInit, Output, EventEmitter, Input, ViewChild, ElementRef } from '@angular/core';
import { Component, Input } from '@angular/core';
import { StatusWrapper } from '../../../../models/common.model';
import { OpenThreadEvent, ToolsService } from '../../../../services/tools.service';
import { ToolsService } from '../../../../services/tools.service';
import { AccountWrapper } from '../../../../models/account.models';
import { FavoriteResult } from '../../../../services/mastodon.service';
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
import { Status } from '../../../../services/models/mastodon.interfaces';
import { NotificationService } from '../../../../services/notification.service';
import { TimeLineModeEnum } from '../../../../states/settings.state';
import { TimelineBase } from '../../../../components/common/timeline-base';
@Component({
selector: 'app-favorites',
templateUrl: '../../../stream/stream-statuses/stream-statuses.component.html',
styleUrls: ['../../../stream/stream-statuses/stream-statuses.component.scss', './favorites.component.scss']
})
export class FavoritesComponent implements OnInit {
statuses: StatusWrapper[] = [];
displayError: string;
isLoading = true;
isThread = false;
hasContentWarnings = false;
bufferStream: Status[] = []; //html compatibility only
streamPositionnedAtTop: boolean = true; //html compatibility only
timelineLoadingMode: TimeLineModeEnum = TimeLineModeEnum.OnTop; //html compatibility only
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
private maxReached = false;
export class FavoritesComponent extends TimelineBase {
private maxId: string;
private _account: AccountWrapper;
@Input('account')
set account(acc: AccountWrapper) {
set accountWrapper(acc: AccountWrapper) {
this._account = acc;
this.account = acc.info;
this.getFavorites();
}
get account(): AccountWrapper {
get accountWrapper(): AccountWrapper {
return this._account;
}
@ViewChild('statusstream') public statustream: ElementRef;
constructor(
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly mastodonService: MastodonWrapperService) { }
protected readonly toolsService: ToolsService,
protected readonly notificationService: NotificationService,
protected readonly mastodonService: MastodonWrapperService) {
super(toolsService, notificationService, mastodonService);
}
ngOnInit() {
}
private reset(){
ngOnDestroy() {
}
private reset() {
this.isLoading = true;
this.statuses.length = 0;
this.maxReached = false;
@ -62,17 +52,17 @@ export class FavoritesComponent implements OnInit {
private getFavorites() {
this.reset();
this.mastodonService.getFavorites(this.account.info)
this.mastodonService.getFavorites(this.account)
.then((result: FavoriteResult) => {
this.maxId = result.max_id;
for (const s of result.favorites) {
let cwPolicy = this.toolsService.checkContentWarning(s);
const wrapper = new StatusWrapper(cwPolicy.status, this.account.info, cwPolicy.applyCw, cwPolicy.hide);
const wrapper = new StatusWrapper(cwPolicy.status, this.account, cwPolicy.applyCw, cwPolicy.hide);
this.statuses.push(wrapper);
}
})
.catch(err => {
this.notificationService.notifyHttpError(err, this.account.info);
this.notificationService.notifyHttpError(err, this.account);
})
.then(() => {
this.isLoading = false;
@ -80,63 +70,21 @@ export class FavoritesComponent implements OnInit {
}
onScroll() {
var element = this.statustream.nativeElement as HTMLElement;
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
if (atBottom) {
this.scrolledToBottom();
}
}
private scrolledToBottom() {
if (this.isLoading || this.maxReached) return;
this.isLoading = true;
this.mastodonService.getFavorites(this.account.info, this.maxId)
protected getNextStatuses(): Promise<Status[]> {
return this.mastodonService.getFavorites(this.account, this.maxId)
.then((result: FavoriteResult) => {
const statuses = result.favorites;
if (statuses.length === 0 || !this.maxId) {
this.maxReached = true;
return;
}
this.maxId = result.max_id;
for (const s of statuses) {
let cwPolicy = this.toolsService.checkContentWarning(s);
const wrapper = new StatusWrapper(cwPolicy.status, this.account.info, cwPolicy.applyCw, cwPolicy.hide);
this.statuses.push(wrapper);
if(!this.maxId){
this.maxReached = true;
}
})
.catch(err => {
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
this.isLoading = false;
return statuses;
});
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
protected scrolledToTop() {}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
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;
}
protected statusProcessOnGoToTop(){}
}

View File

@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ViewChild } from '@angular/core';
import { Component, 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';
@ -15,6 +15,7 @@ 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';
import { BrowseBase } from '../../common/browse-base';
@Component({
@ -22,7 +23,7 @@ import { FavoritesComponent } from './favorites/favorites.component';
templateUrl: './manage-account.component.html',
styleUrls: ['./manage-account.component.scss']
})
export class ManageAccountComponent implements OnInit, OnDestroy {
export class ManageAccountComponent extends BrowseBase {
faAt = faAt;
faBell = faBell;
faEnvelope = faEnvelope;
@ -38,10 +39,6 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
userAccount: Account;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@Input('account')
set account(acc: AccountWrapper) {
this._account = acc;
@ -60,7 +57,9 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
private readonly toolsService: ToolsService,
private readonly mastodonService: MastodonWrapperService,
private readonly notificationService: NotificationService,
private readonly userNotificationService: UserNotificationService) { }
private readonly userNotificationService: UserNotificationService) {
super();
}
ngOnInit() {
}
@ -159,10 +158,6 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
return false;
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
browseLocalAccount(): boolean {
var accountName = `@${this.account.info.username}@${this.account.info.instance}`;
this.browseAccountEvent.next(accountName);
@ -173,12 +168,4 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
window.open(this.userAccount.url, '_blank');
return false;
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
}

View File

@ -1,3 +0,0 @@
<p>
mentions works!
</p>

View File

@ -1,6 +1,4 @@
@import "variables";
@import "commons";
@import "mixins";
.stream-toots {
background-color: $column-background;

View File

@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy, Input, ViewChild, ElementRef, Output, EventEmitter } from '@angular/core';
import { Component, Input } from '@angular/core';
import { Subscription } from 'rxjs';
import { AccountWrapper } from '../../../../models/account.models';
@ -7,8 +7,8 @@ import { StatusWrapper } from '../../../../models/common.model';
import { Status, Notification } from '../../../../services/models/mastodon.interfaces';
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
import { NotificationService } from '../../../../services/notification.service';
import { OpenThreadEvent, ToolsService } from '../../../../services/tools.service';
import { TimeLineModeEnum } from '../../../../states/settings.state';
import { ToolsService } from '../../../../services/tools.service';
import { TimelineBase } from '../../../../components/common/timeline-base';
@Component({
@ -16,43 +16,29 @@ import { TimeLineModeEnum } from '../../../../states/settings.state';
templateUrl: '../../../stream/stream-statuses/stream-statuses.component.html',
styleUrls: ['../../../stream/stream-statuses/stream-statuses.component.scss', './mentions.component.scss']
})
export class MentionsComponent implements OnInit, OnDestroy {
statuses: StatusWrapper[] = [];
displayError: string;
isLoading = false;
isThread = false;
hasContentWarnings = false;
bufferStream: Status[] = []; //html compatibility only
streamPositionnedAtTop: boolean = true; //html compatibility only
timelineLoadingMode: TimeLineModeEnum = TimeLineModeEnum.OnTop; //html compatibility only
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
export class MentionsComponent extends TimelineBase {
private lastId: string;
private _account: AccountWrapper;
@Input('account')
set account(acc: AccountWrapper) {
set accountWrapper(acc: AccountWrapper) {
this._account = acc;
this.account = acc.info;
this.loadMentions();
}
get account(): AccountWrapper {
get accountWrapper(): AccountWrapper {
return this._account;
}
@ViewChild('statusstream') public statustream: ElementRef;
private maxReached = false;
private _account: AccountWrapper;
private userNotificationServiceSub: Subscription;
private lastId: string;
private userNotificationServiceSub: Subscription;
constructor(
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly userNotificationService: UserNotificationService,
private readonly mastodonService: MastodonWrapperService) {
protected readonly toolsService: ToolsService,
protected readonly notificationService: NotificationService,
protected readonly userNotificationService: UserNotificationService,
protected readonly mastodonService: MastodonWrapperService) {
super(toolsService, notificationService, mastodonService);
}
ngOnInit() {
@ -70,7 +56,7 @@ export class MentionsComponent implements OnInit, OnDestroy {
}
this.statuses.length = 0;
this.userNotificationService.markMentionsAsRead(this.account.info);
this.userNotificationService.markMentionsAsRead(this.account);
this.userNotificationServiceSub = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
this.processNewMentions(userNotifications);
@ -79,82 +65,33 @@ export class MentionsComponent implements OnInit, OnDestroy {
}
private processNewMentions(userNotifications: UserNotification[]) {
const userNotification = userNotifications.find(x => x.account.id === this.account.info.id);
const userNotification = userNotifications.find(x => x.account.id === this.account.id);
if (userNotification && userNotification.mentions) {
let orderedMentions = [...userNotification.mentions.map(x => x.status)].reverse();
for (let m of orderedMentions) {
if (!this.statuses.find(x => x.status.id === m.id)) {
let cwPolicy = this.toolsService.checkContentWarning(m);
const statusWrapper = new StatusWrapper(cwPolicy.status, this.account.info, cwPolicy.applyCw, cwPolicy.hide);
const statusWrapper = new StatusWrapper(cwPolicy.status, this.account, cwPolicy.applyCw, cwPolicy.hide);
this.statuses.unshift(statusWrapper);
}
}
}
this.lastId = userNotification.lastMentionsId;
this.userNotificationService.markMentionsAsRead(this.account.info);
this.userNotificationService.markMentionsAsRead(this.account);
}
onScroll() {
var element = this.statustream.nativeElement as HTMLElement;
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
if (atBottom) {
this.scrolledToBottom();
}
}
private scrolledToBottom() {
if (this.isLoading || this.maxReached || this.statuses.length === 0) return;
this.isLoading = true;
this.mastodonService.getNotifications(this.account.info, ['follow', 'favourite', 'reblog', 'poll'], this.lastId)
.then((result: Notification[]) => {
protected getNextStatuses(): Promise<Status[]> {
return this.mastodonService.getNotifications(this.account, ['follow', 'favourite', 'reblog', 'poll'], this.lastId)
.then((result: Notification[]) => {
const statuses = result.map(x => x.status);
if (statuses.length === 0) {
this.maxReached = true;
return;
}
this.lastId = result[result.length - 1].id;
return statuses;
});
}
protected scrolledToTop() {}
for (const s of statuses) {
let cwPolicy = this.toolsService.checkContentWarning(s);
const wrapper = new StatusWrapper(cwPolicy.status, this.account.info, cwPolicy.applyCw, cwPolicy.hide);
if (!this.statuses.find(x => x.status.id === s.id)) {
this.statuses.push(wrapper);
}
}
this.lastId = result[result.length - 1].id;
})
.catch(err => {
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
this.isLoading = false;
});
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
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;
}
protected statusProcessOnGoToTop(){}
}

View File

@ -1,39 +1,29 @@
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
import { Component, Input } from '@angular/core';
import { faUserPlus } from "@fortawesome/free-solid-svg-icons";
import { NotificationWrapper } from '../notifications.component';
import { OpenThreadEvent, ToolsService } from '../../../../../services/tools.service';
import { ToolsService } from '../../../../../services/tools.service';
import { Account } from '../../../../../services/models/mastodon.interfaces';
import { BrowseBase } from '../../../../../components/common/browse-base';
@Component({
selector: 'app-notification',
templateUrl: './notification.component.html',
styleUrls: ['./notification.component.scss']
})
export class NotificationComponent implements OnInit {
export class NotificationComponent extends BrowseBase {
faUserPlus = faUserPlus;
@Input() notification: NotificationWrapper;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
constructor(private readonly toolsService: ToolsService) { }
constructor(private readonly toolsService: ToolsService) {
super();
}
ngOnInit() {
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
ngOnDestroy() {
}
openAccount(account: Account): boolean {

View File

@ -1,4 +1,4 @@
import { Component, OnInit, Input, ViewChild, ElementRef, OnDestroy, Output, EventEmitter } from '@angular/core';
import { Component, Input, ViewChild, ElementRef } from '@angular/core';
import { Subscription } from 'rxjs';
import { AccountWrapper } from '../../../../models/account.models';
@ -8,21 +8,19 @@ import { Notification, Account } from '../../../../services/models/mastodon.inte
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
import { NotificationService } from '../../../../services/notification.service';
import { AccountInfo } from '../../../../states/accounts.state';
import { OpenThreadEvent, ToolsService } from '../../../../services/tools.service';
import { ToolsService } from '../../../../services/tools.service';
import { BrowseBase } from '../../../../components/common/browse-base';
@Component({
selector: 'app-notifications',
templateUrl: './notifications.component.html',
styleUrls: ['./notifications.component.scss']
})
export class NotificationsComponent implements OnInit, OnDestroy {
export class NotificationsComponent extends BrowseBase {
notifications: NotificationWrapper[] = [];
private isProcessingInfiniteScroll: boolean;
isLoading = false;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@Input('account')
set account(acc: AccountWrapper) {
this._account = acc;
@ -43,7 +41,9 @@ export class NotificationsComponent implements OnInit, OnDestroy {
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly userNotificationService: UserNotificationService,
private readonly mastodonService: MastodonWrapperService) { }
private readonly mastodonService: MastodonWrapperService) {
super();
}
ngOnInit() {
}
@ -89,15 +89,17 @@ export class NotificationsComponent implements OnInit, OnDestroy {
var element = this.statustream.nativeElement as HTMLElement;
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
if (atBottom) {
if (atBottom && !this.isProcessingInfiniteScroll) {
this.scrolledToBottom();
}
}
private scrolledErrorOccured = false;
private scrolledToBottom() {
if (this.isLoading || this.maxReached || this.notifications.length === 0) return;
if (this.isLoading || this.maxReached || this.notifications.length === 0 || this.scrolledErrorOccured) return;
this.isLoading = true;
this.isProcessingInfiniteScroll = true;
this.mastodonService.getNotifications(this.account.info, ['mention'], this.lastId)
.then((notifications: Notification[]) => {
@ -117,25 +119,19 @@ export class NotificationsComponent implements OnInit, OnDestroy {
this.lastId = notifications[notifications.length - 1].id;
})
.catch(err => {
this.scrolledErrorOccured = true;
setTimeout(() => {
this.scrolledErrorOccured = false;
}, 5000);
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
this.isLoading = false;
this.isProcessingInfiniteScroll = false;
});
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
applyGoToTop(): boolean {
const stream = this.statustream.nativeElement as HTMLElement;
setTimeout(() => {

View File

@ -10,13 +10,14 @@ import { NotificationWrapper } from '../../floating-column/manage-account/notifi
import { AccountInfo } from '../../../states/accounts.state';
import { NotificationService } from '../../../services/notification.service';
import { StreamingService, StatusUpdate, EventEnum } from '../../../services/streaming.service';
import { BrowseBase } from '../../common/browse-base';
@Component({
selector: 'app-stream-notifications',
templateUrl: './stream-notifications.component.html',
styleUrls: ['./stream-notifications.component.scss']
})
export class StreamNotificationsComponent implements OnInit, OnDestroy {
export class StreamNotificationsComponent extends BrowseBase {
displayingNotifications = true;
displayingMentions = false;
@ -26,10 +27,6 @@ export class StreamNotificationsComponent implements OnInit, OnDestroy {
@Input() streamElement: StreamElement;
@Input() goToTop: Observable<void>;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@ViewChild('notificationstream') public notificationstream: ElementRef;
@ViewChild('mentionstream') public mentionstream: ElementRef;
@ -53,7 +50,9 @@ export class StreamNotificationsComponent implements OnInit, OnDestroy {
private readonly notificationService: NotificationService,
private readonly userNotificationService: UserNotificationService,
private readonly mastodonService: MastodonService,
private readonly toolsService: ToolsService) { }
private readonly toolsService: ToolsService) {
super();
}
ngOnInit() {
this.goToTopSubscription = this.goToTop.subscribe(() => {
@ -195,8 +194,9 @@ export class StreamNotificationsComponent implements OnInit, OnDestroy {
}
}
private scrolledErrorOccured = false;
notificationsScrolledToBottom(): any {
if (this.isNotificationsLoading || this.notificationsMaxReached || this.notifications.length === 0)
if (this.isNotificationsLoading || this.notificationsMaxReached || this.notifications.length === 0 || this.scrolledErrorOccured)
return;
this.isNotificationsLoading = true;
@ -217,6 +217,11 @@ export class StreamNotificationsComponent implements OnInit, OnDestroy {
this.lastNotificationId = result[result.length - 1].id;
})
.catch(err => {
this.scrolledErrorOccured = true;
setTimeout(() => {
this.scrolledErrorOccured = false;
}, 5000);
this.notificationService.notifyHttpError(err, this.account);
})
.then(() => {
@ -225,7 +230,7 @@ export class StreamNotificationsComponent implements OnInit, OnDestroy {
}
mentionsScrolledToBottom(): any {
if (this.isMentionsLoading || this.mentionsMaxReached || this.mentions.length === 0)
if (this.isMentionsLoading || this.mentionsMaxReached || this.mentions.length === 0 || this.scrolledErrorOccured)
return;
this.isMentionsLoading = true;
@ -246,6 +251,11 @@ export class StreamNotificationsComponent implements OnInit, OnDestroy {
this.lastMentionId = result[result.length - 1].id;
})
.catch(err => {
this.scrolledErrorOccured = true;
setTimeout(() => {
this.scrolledErrorOccured = false;
}, 5000);
this.notificationService.notifyHttpError(err, this.account);
})
.then(() => {
@ -265,16 +275,4 @@ export class StreamNotificationsComponent implements OnInit, OnDestroy {
}, 0);
return false;
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
}

View File

@ -1,49 +1,26 @@
import { Component, OnInit, Input, ViewChild, ElementRef, OnDestroy, EventEmitter, Output, ViewChildren, QueryList } from '@angular/core';
import { Component, Input } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Observable, Subscription } from 'rxjs';
import { Store } from '@ngxs/store';
import { StreamElement } from '../../../states/streams.state';
import { AccountInfo } from '../../../states/accounts.state';
import { StreamingService, EventEnum, StreamingWrapper, StatusUpdate } from '../../../services/streaming.service';
import { StreamingService, EventEnum, StatusUpdate } from '../../../services/streaming.service';
import { Status } from '../../../services/models/mastodon.interfaces';
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
import { NotificationService } from '../../../services/notification.service';
import { OpenThreadEvent, ToolsService } from '../../../services/tools.service';
import { ToolsService } from '../../../services/tools.service';
import { StatusWrapper } from '../../../models/common.model';
import { TimeLineModeEnum } from '../../../states/settings.state';
import { TimelineBase } from '../../common/timeline-base';
@Component({
selector: 'app-stream-statuses',
templateUrl: './stream-statuses.component.html',
styleUrls: ['./stream-statuses.component.scss']
})
export class StreamStatusesComponent implements OnInit, OnDestroy {
isLoading = true;
private lastInfinityFetchReturnedNothing = false;
isThread = false;
displayError: string;
hasContentWarnings = false;
timelineLoadingMode: TimeLineModeEnum;
private _streamElement: StreamElement;
private account: AccountInfo;
private websocketStreaming: StreamingWrapper;
statuses: StatusWrapper[] = [];
bufferStream: Status[] = [];
private bufferWasCleared: boolean;
streamPositionnedAtTop: boolean = true;
private isProcessingInfiniteScroll: 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>();
export class StreamStatusesComponent extends TimelineBase {
protected _streamElement: StreamElement;
@Input()
set streamElement(streamElement: StreamElement) {
@ -59,10 +36,6 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
return this._streamElement;
}
@Input() goToTop: Observable<void>;
@Input() userLocked = true;
private goToTopSubscription: Subscription;
private streamsSubscription: Subscription;
private hideAccountSubscription: Subscription;
@ -70,12 +43,13 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
private streams$: Observable<StreamElement[]>;
constructor(
private readonly store: Store,
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly streamingService: StreamingService,
private readonly mastodonService: MastodonWrapperService) {
protected readonly store: Store,
protected readonly toolsService: ToolsService,
protected readonly notificationService: NotificationService,
protected readonly streamingService: StreamingService,
protected readonly mastodonService: MastodonWrapperService) {
super(toolsService, notificationService, mastodonService);
this.streams$ = this.store.select(state => state.streamsstatemodel.streams);
}
@ -138,7 +112,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
this.load(this._streamElement);
}
private load(streamElement: StreamElement) {
protected load(streamElement: StreamElement) {
this.resetStream();
if (this.userLocked) {
@ -190,54 +164,17 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
});
}
@ViewChild('statusstream') public statustream: ElementRef;
private applyGoToTop(): boolean {
// this.loadBuffer();
protected statusProcessOnGoToTop(){
if (this.statuses.length > 2 * this.streamingService.nbStatusPerIteration) {
this.statuses.length = 2 * this.streamingService.nbStatusPerIteration;
}
const stream = this.statustream.nativeElement as HTMLElement;
setTimeout(() => {
stream.scrollTo({
top: 0,
behavior: 'smooth'
});
}, 0);
return false;
}
onScroll() {
var element = this.statustream.nativeElement as HTMLElement;
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
const atTop = element.scrollTop === 0;
this.streamPositionnedAtTop = false;
if (atBottom && !this.isProcessingInfiniteScroll) {
this.scrolledToBottom();
} else if (atTop) {
this.scrolledToTop();
}
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
textSelected(): void {
console.warn(`status comp: textSelected`); //TODO
}
private scrolledToTop() {
protected scrolledToTop() {
this.streamPositionnedAtTop = true;
if (this.timelineLoadingMode !== TimeLineModeEnum.SlowMode) {
@ -265,37 +202,33 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
return false;
}
private scrolledToBottom() {
if (this.isLoading || this.lastInfinityFetchReturnedNothing) return;
this.isLoading = true;
this.isProcessingInfiniteScroll = true;
protected getNextStatuses(): Promise<Status[]> {
const lastStatus = this.statuses[this.statuses.length - 1];
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;
}
let cwPolicy = this.toolsService.checkContentWarning(s);
const wrapper = new StatusWrapper(cwPolicy.status, this.account, cwPolicy.applyCw, cwPolicy.hide);
this.statuses.push(wrapper);
}
if (!status || status.length === 0) {
this.lastInfinityFetchReturnedNothing = true;
}
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err, this.account);
})
.then(() => {
this.isLoading = false;
this.isProcessingInfiniteScroll = false;
return this.mastodonService.getTimeline(this.account, this._streamElement.type, lastStatus.status.id, null, this.streamingService.nbStatusPerIteration, this._streamElement.tag, this._streamElement.listId)
.then((status: Status[]) =>{
return status.filter(x => !this.isFiltered(x));
});
}
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;
}
private getRegisteredAccounts(): AccountInfo[] {
var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
@ -306,7 +239,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
focus(): boolean {
setTimeout(() => {
var element = this.statustream.nativeElement as HTMLElement;
element.focus({preventScroll:true});
element.focus({ preventScroll: true });
}, 0);
return false;
}
@ -333,7 +266,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
private checkAndCleanUpStream(): void {
if (this.streamPositionnedAtTop && this.statuses.length > 3 * this.streamingService.nbStatusPerIteration) {
this.statuses.length = 2 * this.streamingService.nbStatusPerIteration;
this.lastInfinityFetchReturnedNothing = false;
this.maxReached = false;
}
if (this.bufferStream.length > 3 * this.streamingService.nbStatusPerIteration) {
@ -341,24 +274,5 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
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

@ -12,13 +12,14 @@ import { StatusComponent } from '../status/status.component';
import scrollIntoView from 'scroll-into-view-if-needed';
import { UserNotificationService, UserNotification } from '../../../services/user-notification.service';
import { TimeLineModeEnum } from '../../../states/settings.state';
import { BrowseBase } from '../../common/browse-base';
@Component({
selector: 'app-thread',
templateUrl: '../stream-statuses/stream-statuses.component.html',
styleUrls: ['../stream-statuses/stream-statuses.component.scss']
})
export class ThreadComponent implements OnInit, OnDestroy {
export class ThreadComponent extends BrowseBase {
statuses: StatusWrapper[] = [];
displayError: string;
isLoading = true;
@ -32,10 +33,6 @@ export class ThreadComponent implements OnInit, OnDestroy {
private lastThreadEvent: OpenThreadEvent;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@Input() refreshEventEmitter: EventEmitter<any>;
@Input() goToTopEventEmitter: EventEmitter<any>;
@ -61,7 +58,9 @@ export class ThreadComponent implements OnInit, OnDestroy {
private readonly notificationService: NotificationService,
private readonly userNotificationService: UserNotificationService,
private readonly toolsService: ToolsService,
private readonly mastodonService: MastodonWrapperService) { }
private readonly mastodonService: MastodonWrapperService) {
super();
}
ngOnInit() {
let settings = this.toolsService.getSettings();
@ -282,18 +281,6 @@ export class ThreadComponent implements OnInit, OnDestroy {
//Do nothing
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
removeCw(): boolean {
const statuses = this.statusChildren.toArray();
statuses.forEach(x => {

View File

@ -13,13 +13,14 @@ import { AccountInfo } from '../../../states/accounts.state';
import { StatusWrapper, OpenMediaEvent } from '../../../models/common.model';
import { EmojiConverter, EmojiTypeEnum } from '../../../tools/emoji.tools';
import { NavigationService } from '../../../services/navigation.service';
import { BrowseBase } from '../../common/browse-base';
@Component({
selector: 'app-user-profile',
templateUrl: './user-profile.component.html',
styleUrls: ['./user-profile.component.scss']
})
export class UserProfileComponent implements OnInit {
export class UserProfileComponent extends BrowseBase {
private emojiConverter = new EmojiConverter();
faUser = faUser;
@ -64,10 +65,6 @@ export class UserProfileComponent implements OnInit {
@ViewChild('statusstream') public statustream: ElementRef;
@ViewChild('profilestatuses') public profilestatuses: ElementRef;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@Input() refreshEventEmitter: EventEmitter<any>;
@Input() goToTopEventEmitter: EventEmitter<any>;
@ -83,6 +80,7 @@ export class UserProfileComponent implements OnInit {
private readonly mastodonService: MastodonWrapperService,
private readonly toolsService: ToolsService) {
super();
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
}
@ -282,14 +280,6 @@ export class UserProfileComponent implements OnInit {
return false;
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
follow(): boolean {
const userAccount = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(userAccount, this.lastAccountName)