303 lines
13 KiB
TypeScript
303 lines
13 KiB
TypeScript
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ViewChildren, QueryList, ViewChild, ElementRef } from '@angular/core';
|
|
import { HttpErrorResponse, HttpClient, HttpHeaders } from '@angular/common/http';
|
|
import { Subscription } from 'rxjs';
|
|
|
|
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
|
|
import { ToolsService, OpenThreadEvent } from '../../../services/tools.service';
|
|
import { Results, Context, Status } from '../../../services/models/mastodon.interfaces';
|
|
import { NotificationService, NewReplyData } from '../../../services/notification.service';
|
|
import { AccountInfo } from '../../../states/accounts.state';
|
|
import { StatusWrapper } from '../../../models/common.model';
|
|
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';
|
|
import { SettingsService } from '../../../services/settings.service';
|
|
|
|
@Component({
|
|
selector: 'app-thread',
|
|
templateUrl: '../stream-statuses/stream-statuses.component.html',
|
|
styleUrls: ['../stream-statuses/stream-statuses.component.scss']
|
|
})
|
|
export class ThreadComponent extends BrowseBase {
|
|
statuses: StatusWrapper[] = [];
|
|
displayError: string;
|
|
isLoading = true;
|
|
isThread = true;
|
|
hasContentWarnings = false;
|
|
private remoteStatusFetchingDisabled = false;
|
|
|
|
context = 'thread';
|
|
|
|
numNewItems: number; //html compatibility only
|
|
bufferStream: Status[] = []; //html compatibility only
|
|
streamPositionnedAtTop: boolean = true; //html compatibility only
|
|
timelineLoadingMode: TimeLineModeEnum = TimeLineModeEnum.OnTop; //html compatibility only
|
|
|
|
private lastThreadEvent: OpenThreadEvent;
|
|
|
|
@Input() refreshEventEmitter: EventEmitter<any>;
|
|
@Input() goToTopEventEmitter: EventEmitter<any>;
|
|
|
|
@Input('currentThread')
|
|
set currentThread(thread: OpenThreadEvent) {
|
|
if (thread) {
|
|
this.lastThreadEvent = thread;
|
|
this.getThread(thread);
|
|
}
|
|
}
|
|
|
|
@ViewChildren(StatusComponent) statusChildren: QueryList<StatusComponent>;
|
|
|
|
private newPostSub: Subscription;
|
|
private hideAccountSubscription: Subscription;
|
|
private deleteStatusSubscription: Subscription;
|
|
private refreshSubscription: Subscription;
|
|
private goToTopSubscription: Subscription;
|
|
private responseSubscription: Subscription;
|
|
|
|
constructor(
|
|
private readonly settingsService: SettingsService,
|
|
private readonly httpClient: HttpClient,
|
|
private readonly notificationService: NotificationService,
|
|
private readonly userNotificationService: UserNotificationService,
|
|
private readonly toolsService: ToolsService,
|
|
private readonly mastodonService: MastodonWrapperService) {
|
|
super();
|
|
}
|
|
|
|
ngOnInit() {
|
|
let settings = this.settingsService.getSettings();
|
|
this.remoteStatusFetchingDisabled = settings.disableRemoteStatusFetching;
|
|
|
|
if (this.refreshEventEmitter) {
|
|
this.refreshSubscription = this.refreshEventEmitter.subscribe(() => {
|
|
this.refresh();
|
|
})
|
|
}
|
|
|
|
if (this.goToTopEventEmitter) {
|
|
this.goToTopSubscription = this.goToTopEventEmitter.subscribe(() => {
|
|
this.goToTop();
|
|
})
|
|
}
|
|
|
|
this.newPostSub = this.notificationService.newRespondPostedStream.subscribe((replyData: NewReplyData) => {
|
|
if (replyData) {
|
|
const repondingStatus = this.statuses.find(x => x.status.id === replyData.uiStatusId);
|
|
const responseStatus = replyData.response;
|
|
if (repondingStatus && this.statuses[0]) {
|
|
this.statuses.push(responseStatus);
|
|
}
|
|
}
|
|
});
|
|
|
|
this.hideAccountSubscription = this.notificationService.hideAccountUrlStream.subscribe((accountUrl: string) => {
|
|
if (accountUrl) {
|
|
this.statuses = this.statuses.filter(x => {
|
|
if (x.status.reblog) {
|
|
return x.status.reblog.account.url != accountUrl;
|
|
} else {
|
|
return x.status.account.url != accountUrl;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
this.deleteStatusSubscription = this.notificationService.deletedStatusStream.subscribe((status: StatusWrapper) => {
|
|
if (status) {
|
|
this.statuses = this.statuses.filter(x => {
|
|
return !(x.status.url.replace('https://', '').split('/')[0] === status.provider.instance && x.status.id === status.status.id);
|
|
});
|
|
}
|
|
});
|
|
|
|
this.responseSubscription = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
|
|
userNotifications.forEach(x => {
|
|
x.mentions.forEach(y => {
|
|
if(y.status){
|
|
if(this.statuses.map(z => z.status.id).includes(y.status.in_reply_to_id) && !this.statuses.map(z => z.status.uri).includes(y.status.uri)) {
|
|
let cwResult = this.toolsService.checkContentWarning(y.status);
|
|
this.statuses.push(new StatusWrapper(y.status, x.account, cwResult.applyCw, cwResult.hide));
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
if (this.newPostSub) this.newPostSub.unsubscribe();
|
|
if (this.hideAccountSubscription) this.hideAccountSubscription.unsubscribe();
|
|
if (this.deleteStatusSubscription) this.deleteStatusSubscription.unsubscribe();
|
|
if (this.refreshSubscription) this.refreshSubscription.unsubscribe();
|
|
if (this.goToTopSubscription) this.goToTopSubscription.unsubscribe();
|
|
if (this.responseSubscription) this.responseSubscription.unsubscribe();
|
|
}
|
|
|
|
@ViewChild('statusstream') public statustream: ElementRef;
|
|
goToTop(): any {
|
|
const stream = this.statustream.nativeElement as HTMLElement;
|
|
setTimeout(() => {
|
|
stream.scrollTo({
|
|
top: 0,
|
|
behavior: 'smooth'
|
|
});
|
|
}, 0);
|
|
}
|
|
|
|
private getThread(openThreadEvent: OpenThreadEvent) {
|
|
this.statuses.length = 0;
|
|
this.displayError = null;
|
|
|
|
let currentAccount = this.toolsService.getSelectedAccounts()[0];
|
|
|
|
const status = openThreadEvent.status;
|
|
const sourceAccount = openThreadEvent.sourceAccount;
|
|
|
|
if (status.visibility === 'public' || status.visibility === 'unlisted') {
|
|
// var statusPromise: Promise<Status> = Promise.resolve(status);
|
|
// if (!sourceAccount || sourceAccount.id !== currentAccount.id) {
|
|
var statusPromise = this.toolsService.getInstanceInfo(currentAccount)
|
|
.then(instance => {
|
|
let version: 'v1' | 'v2' = 'v1';
|
|
if (instance.major >= 3) version = 'v2';
|
|
return this.mastodonService.search(currentAccount, status.uri, version, true);
|
|
})
|
|
.then((result: Results) => {
|
|
if (result.statuses.length === 1) {
|
|
const retrievedStatus = result.statuses[0];
|
|
return retrievedStatus;
|
|
}
|
|
throw new Error('could not find status');
|
|
});
|
|
// }
|
|
|
|
this.retrieveThread(currentAccount, statusPromise);
|
|
|
|
} else if (sourceAccount && sourceAccount.id === currentAccount.id) {
|
|
var statusPromise = Promise.resolve(status);
|
|
this.retrieveThread(currentAccount, statusPromise);
|
|
} else {
|
|
this.isLoading = false;
|
|
this.displayError = `You need to use your account ${sourceAccount.username}@${sourceAccount.instance} to show this thread`;
|
|
}
|
|
}
|
|
|
|
private retrieveThread(currentAccount: AccountInfo, pipeline: Promise<Status>) {
|
|
pipeline
|
|
.then((status: Status) => {
|
|
return this.mastodonService.getStatusContext(currentAccount, status.id)
|
|
.then((context: Context) => {
|
|
let contextStatuses = [...context.ancestors, status, ...context.descendants]
|
|
const position = context.ancestors.length;
|
|
|
|
let localStatuses = [];
|
|
for (let i = 0; i < contextStatuses.length; i++) {
|
|
let s = contextStatuses[i];
|
|
let cwPolicy = this.toolsService.checkContentWarning(s);
|
|
const wrapper = new StatusWrapper(cwPolicy.status, currentAccount, cwPolicy.applyCw, cwPolicy.hide);
|
|
|
|
if (i == position) wrapper.isSelected = true;
|
|
|
|
// this.statuses.push(wrapper);
|
|
localStatuses.push(wrapper);
|
|
}
|
|
return localStatuses;
|
|
})
|
|
.then(async (localStatuses: StatusWrapper[]) => {
|
|
|
|
let remoteStatuses = await this.retrieveRemoteThread(status);
|
|
let unknownStatuses = remoteStatuses.filter(x => !localStatuses.find(y => y.status.uri == x.uri));
|
|
|
|
for(let s of unknownStatuses){
|
|
let cwPolicy = this.toolsService.checkContentWarning(s);
|
|
let wrapper = new StatusWrapper(s, null, cwPolicy.applyCw, cwPolicy.hide);
|
|
wrapper.isRemote = true;
|
|
localStatuses.push(wrapper);
|
|
}
|
|
|
|
localStatuses.sort((a,b) => {
|
|
if(a.status.created_at > b.status.created_at) return 1;
|
|
if(a.status.created_at < b.status.created_at) return -1;
|
|
return 0;
|
|
});
|
|
|
|
this.statuses = localStatuses;
|
|
this.hasContentWarnings = this.statuses.filter(x => x.applyCw).length > 1;
|
|
let newPosition = this.statuses.findIndex(x => x.isSelected);
|
|
return newPosition;
|
|
});
|
|
})
|
|
.then((position: number) => {
|
|
setTimeout(() => {
|
|
const el = this.statusChildren.toArray()[position];
|
|
//el.elem.nativeElement.scrollIntoViewIfNeeded({ behavior: 'auto', block: 'start', inline: 'nearest' });
|
|
scrollIntoView(el.elem.nativeElement, { behavior: 'smooth', block: 'nearest' });
|
|
}, 250);
|
|
})
|
|
.catch((err: HttpErrorResponse) => {
|
|
this.notificationService.notifyHttpError(err, currentAccount);
|
|
})
|
|
.then(() => {
|
|
this.isLoading = false;
|
|
});
|
|
}
|
|
|
|
private async retrieveRemoteThread(status: Status): Promise<Status[]> {
|
|
if(this.remoteStatusFetchingDisabled) return [];
|
|
|
|
try {
|
|
let url = status.url;
|
|
let splitUrl = url.replace('https://', '').split('/');
|
|
let id = splitUrl[splitUrl.length - 1];
|
|
let instance = splitUrl[0];
|
|
|
|
//Pleroma
|
|
if(url.includes('/objects/')){
|
|
var webpage = await this.httpClient.get(url, { responseType: 'text' }).toPromise();
|
|
id = webpage.split(`<meta content="https://${instance}/notice/`)[1].split('" property="og:url"')[0];
|
|
}
|
|
|
|
let context = await this.mastodonService.getRemoteStatusContext(instance, id);
|
|
let remoteStatuses = [...context.ancestors, ...context.descendants];
|
|
remoteStatuses.forEach(s => {
|
|
if(!s.account.acct.includes('@')){
|
|
s.account.acct += `@${instance}`;
|
|
}
|
|
});
|
|
return remoteStatuses;
|
|
|
|
} catch (err) {
|
|
return [];
|
|
};
|
|
}
|
|
|
|
refresh(): any {
|
|
this.isLoading = true;
|
|
this.displayError = null;
|
|
this.statuses.length = 0;
|
|
this.getThread(this.lastThreadEvent);
|
|
}
|
|
|
|
onScroll() {
|
|
//Do nothing
|
|
}
|
|
|
|
removeCw(): boolean {
|
|
const statuses = this.statusChildren.toArray();
|
|
statuses.forEach(x => {
|
|
x.removeContentWarning();
|
|
if (x.isSelected) {
|
|
setTimeout(() => {
|
|
scrollIntoView(x.elem.nativeElement, { behavior: 'auto', block: 'nearest' });
|
|
}, 0);
|
|
}
|
|
});
|
|
this.hasContentWarnings = false;
|
|
return false;
|
|
}
|
|
}
|