commit
ece9182e99
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "sengi",
|
||||
"version": "0.29.1",
|
||||
"version": "0.30.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "main-electron.js",
|
||||
"description": "A multi-account desktop client for Mastodon and Pleroma",
|
||||
|
|
|
@ -84,6 +84,8 @@ import { environment } from '../environments/environment';
|
|||
import { BookmarksComponent } from './components/floating-column/manage-account/bookmarks/bookmarks.component';
|
||||
import { AttachementImageComponent } from './components/stream/status/attachements/attachement-image/attachement-image.component';
|
||||
import { EnsureHttpsPipe } from './pipes/ensure-https.pipe';
|
||||
import { UserFollowsComponent } from './components/stream/user-follows/user-follows.component';
|
||||
import { AccountComponent } from './components/common/account/account.component';
|
||||
|
||||
|
||||
const routes: Routes = [
|
||||
|
@ -148,7 +150,9 @@ const routes: Routes = [
|
|||
NotificationComponent,
|
||||
BookmarksComponent,
|
||||
AttachementImageComponent,
|
||||
EnsureHttpsPipe
|
||||
EnsureHttpsPipe,
|
||||
UserFollowsComponent,
|
||||
AccountComponent
|
||||
],
|
||||
entryComponents: [
|
||||
EmojiPickerComponent
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<a href class="account" title="open account" (click)="selected()">
|
||||
<img src="{{account.avatar}}" class="account__avatar" />
|
||||
<div class="account__name" innerHTML="{{ account | accountEmoji }}"></div>
|
||||
<div class="account__fullhandle">@{{ account.acct }}</div>
|
||||
</a>
|
|
@ -0,0 +1,48 @@
|
|||
@import "variables";
|
||||
@import "mixins";
|
||||
|
||||
.account {
|
||||
font-size: $small-font-size;
|
||||
|
||||
display: block;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
transition: all .3s;
|
||||
border-top: 1px solid $separator-color;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 1px solid $separator-color;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 40px;
|
||||
margin: 5px 10px 5px 5px;
|
||||
float: left;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
margin: 7px 0 0 0;
|
||||
}
|
||||
|
||||
&__fullhandle {
|
||||
margin: 0 0 5px 0;
|
||||
color: $status-secondary-color;
|
||||
transition: all .3s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:hover &__fullhandle {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $button-background-color-hover;
|
||||
}
|
||||
|
||||
@include clearfix;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AccountComponent } from './account.component';
|
||||
|
||||
xdescribe('AccountComponent', () => {
|
||||
let component: AccountComponent;
|
||||
let fixture: ComponentFixture<AccountComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AccountComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AccountComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
|
||||
|
||||
import { Account } from '../../../services/models/mastodon.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
templateUrl: './account.component.html',
|
||||
styleUrls: ['./account.component.scss']
|
||||
})
|
||||
export class AccountComponent implements OnInit {
|
||||
|
||||
@Input() account: Account;
|
||||
@Output() accountSelected = new EventEmitter<Account>();
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
selected(): boolean{
|
||||
this.accountSelected.next(this.account);
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
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;
|
||||
protected lastCallReachedMax = 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;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if(this.lastCallReachedMax){
|
||||
this.maxReached = true;
|
||||
}
|
||||
})
|
||||
.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;
|
||||
}
|
||||
}
|
|
@ -179,7 +179,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
private statusReplyingTo: Status;
|
||||
|
||||
selectedPrivacy = 'Public';
|
||||
// privacyList: string[] = ['Public', 'Unlisted', 'Follows-only', 'DM'];
|
||||
private selectedPrivacySetByRedraft = false;
|
||||
|
||||
private accounts$: Observable<AccountInfo[]>;
|
||||
private accountSub: Subscription;
|
||||
|
@ -429,9 +429,13 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
this.setVisibility(VisibilityEnum.Direct);
|
||||
break;
|
||||
}
|
||||
|
||||
this.selectedPrivacySetByRedraft = true;
|
||||
}
|
||||
|
||||
private setVisibility(defaultPrivacy: VisibilityEnum) {
|
||||
if(this.selectedPrivacySetByRedraft) return;
|
||||
|
||||
switch (defaultPrivacy) {
|
||||
case VisibilityEnum.Public:
|
||||
this.selectedPrivacy = 'Public';
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<p>
|
||||
bookmarks works!
|
||||
</p>
|
|
@ -1,141 +1,92 @@
|
|||
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;
|
||||
this.maxReached = false;
|
||||
this.maxReached = false;
|
||||
this.lastCallReachedMax = false;
|
||||
this.maxId = null;
|
||||
}
|
||||
|
||||
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.lastCallReachedMax = 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(){}
|
||||
}
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<p>
|
||||
favorites works!
|
||||
</p>
|
|
@ -1,78 +1,69 @@
|
|||
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;
|
||||
this.lastCallReachedMax = false;
|
||||
this.maxId = null;
|
||||
}
|
||||
|
||||
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 +71,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.lastCallReachedMax = 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(){}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<p>
|
||||
mentions works!
|
||||
</p>
|
|
@ -1,6 +1,4 @@
|
|||
@import "variables";
|
||||
@import "commons";
|
||||
@import "mixins";
|
||||
|
||||
.stream-toots {
|
||||
background-color: $column-background;
|
||||
|
|
|
@ -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,46 +16,33 @@ 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() {
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
@ -70,7 +57,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 +66,34 @@ 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[]> {
|
||||
console.warn('MENTIONS get next 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(){}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -15,12 +15,17 @@
|
|||
|
||||
<div *ngIf="accounts.length > 0" class="search-results">
|
||||
<h3 class="search-results__title">Accounts</h3>
|
||||
<a href *ngFor="let account of accounts" class="account" title="open account"
|
||||
|
||||
<app-account class="account" *ngFor="let account of accounts"
|
||||
[account]="account"
|
||||
(accountSelected)="processAndBrowseAccount($event)"></app-account>
|
||||
|
||||
<!-- <a href *ngFor="let account of accounts" class="account" title="open account"
|
||||
(click)="processAndBrowseAccount(account)">
|
||||
<img src="{{account.avatar}}" class="account__avatar" />
|
||||
<div class="account__name">{{ account.username }}</div>
|
||||
<div class="account__fullhandle">@{{ account.acct }}</div>
|
||||
</a>
|
||||
</a> -->
|
||||
</div>
|
||||
|
||||
<div *ngIf="hashtags.length > 0" class="search-results">
|
||||
|
|
|
@ -96,7 +96,7 @@ $search-form-height: 70px;
|
|||
// outline: 1px solid greenyellow;
|
||||
margin-top: 10px; // &:first-of-type{
|
||||
padding-left: 10px; // margin-top: 10px;
|
||||
padding-right: 10px; // margin-top: 10px;
|
||||
//padding-right: 10px; // margin-top: 10px;
|
||||
|
||||
// }
|
||||
&__title {
|
||||
|
@ -135,42 +135,4 @@ $search-form-height: 70px;
|
|||
|
||||
.account {
|
||||
display: block;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
transition: all .3s; // &:hover &__name {
|
||||
// text-decoration: underline;
|
||||
// }
|
||||
border-top: 1px solid $separator-color;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 1px solid $separator-color;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 40px;
|
||||
margin: 5px 10px 5px 5px;
|
||||
float: left;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
margin: 7px 0 0 0;
|
||||
}
|
||||
|
||||
&__fullhandle {
|
||||
margin: 0 0 5px 0;
|
||||
color: $status-secondary-color;
|
||||
transition: all .3s; // &:hover {
|
||||
// color: white;
|
||||
// }
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:hover &__fullhandle {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
background-color: $button-background-color-hover;
|
||||
}
|
||||
|
||||
@include clearfix;
|
||||
}
|
|
@ -65,6 +65,13 @@ describe('DatabindedTextComponent', () => {
|
|||
expect(component.processedText).toContain('bla2');
|
||||
});
|
||||
|
||||
it('should parse remote mention', () => {
|
||||
const sample = `<p><span class="article-type"><a href="https://domain.name/@username" class="u-url mention" rel="nofollow noopener noreferrer" target="_blank">@<span class="article-type">username</span></a></span> <br>Yes, indeed.</p>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toBe('<p><span class="article-type"><a href class="account--username-domain-name" title="@username@domain.name">@username</a> <br>Yes, indeed.</p>');
|
||||
});
|
||||
|
||||
it('should parse link', () => {
|
||||
const url = 'mydomain.co/test';
|
||||
const sample = `<p>bla1 <a href="https://${url}" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="">${url}</span><span class="invisible"></span></a> bla2</p>`;
|
||||
|
@ -126,7 +133,7 @@ describe('DatabindedTextComponent', () => {
|
|||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href class="account--user-instance-club" title="@user@instance.club">@user</a>');
|
||||
expect(component.processedText).toContain('bla1');
|
||||
})
|
||||
});
|
||||
|
||||
it('should parse mention - Pleroma in Mastodon - 2', () => {
|
||||
const sample = `<div><span><a class="mention status-link" href="https://pleroma.site/users/kaniini" rel="noopener" target="_blank" title="kaniini@pleroma.site">@<span>kaniini</span></a></span> <span><a class="mention status-link" href="https://mastodon.social/@Gargron" rel="noopener" target="_blank" title="Gargron@mastodon.social">@<span>Gargron</span></a></span> bla1?</div>`;
|
||||
|
|
|
@ -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')
|
||||
|
@ -104,6 +104,9 @@ export class DatabindedTextComponent implements OnInit {
|
|||
if (section.includes('<span class="mention">')) { //Friendica
|
||||
extractedAccountAndNext = section.split('</a>');
|
||||
extractedAccountName = extractedAccountAndNext[0].split('<span class="mention">')[1].split('</span>')[0];
|
||||
} else if(section.includes('>@<span class="article-type">')){ //Remote status
|
||||
extractedAccountAndNext = section.split('</a></span>');
|
||||
extractedAccountName = extractedAccountAndNext[0].split('@<span class="article-type">')[1].replace('<span>', '').replace('</span>', '');
|
||||
} else if (!section.includes('@<span>')) { //GNU social
|
||||
extractedAccountAndNext = section.split('</a>');
|
||||
extractedAccountName = extractedAccountAndNext[0].split('>')[1];
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,16 @@
|
|||
class="overlay__content"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-user-profile>
|
||||
(browseThreadEvent)="browseThread($event)"
|
||||
(browseFollowsEvent)="browseFollows($event)"
|
||||
(browseFollowersEvent)="browseFollowers($event)"></app-user-profile>
|
||||
<app-user-follows *ngIf="e.type === 'follows' || e.type === 'followers'"
|
||||
[currentAccount]="e.account"
|
||||
[type]="e.type"
|
||||
[refreshEventEmitter]="e.refreshEventEmitter"
|
||||
[goToTopEventEmitter]="e.goToTopEventEmitter"
|
||||
class="overlay__content"
|
||||
(browseAccountEvent)="browseAccount($event)"></app-user-follows>
|
||||
<app-hashtag #appHashtag *ngIf="e.type === 'hashtag'"
|
||||
[hashtagElement]="e.hashtag"
|
||||
[refreshEventEmitter]="e.refreshEventEmitter"
|
||||
|
|
|
@ -123,7 +123,7 @@ export class StreamOverlayComponent implements OnInit, OnDestroy {
|
|||
browseAccount(accountName: string): void {
|
||||
if (!accountName) return;
|
||||
|
||||
const newElement = new OverlayBrowsing(null, accountName, null);
|
||||
const newElement = new OverlayBrowsing('account', null, accountName, null);
|
||||
this.loadElement(newElement);
|
||||
}
|
||||
|
||||
|
@ -132,14 +132,28 @@ export class StreamOverlayComponent implements OnInit, OnDestroy {
|
|||
|
||||
const selectedAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
const hashTagElement = new StreamElement(StreamTypeEnum.tag, hashtag, selectedAccount.id, hashtag, null, null, selectedAccount.instance);
|
||||
const newElement = new OverlayBrowsing(hashTagElement, null, null);
|
||||
const newElement = new OverlayBrowsing('hashtag', hashTagElement, null, null);
|
||||
this.loadElement(newElement);
|
||||
}
|
||||
|
||||
browseThread(openThread: OpenThreadEvent): any {
|
||||
if (!openThread) return;
|
||||
|
||||
const newElement = new OverlayBrowsing(null, null, openThread);
|
||||
const newElement = new OverlayBrowsing('thread', null, null, openThread);
|
||||
this.loadElement(newElement);
|
||||
}
|
||||
|
||||
browseFollows(accountName: string): void {
|
||||
if (!accountName) return;
|
||||
|
||||
const newElement = new OverlayBrowsing('follows', null, accountName, null);
|
||||
this.loadElement(newElement);
|
||||
}
|
||||
|
||||
browseFollowers(accountName: string): void {
|
||||
if (!accountName) return;
|
||||
|
||||
const newElement = new OverlayBrowsing('followers', null, accountName, null);
|
||||
this.loadElement(newElement);
|
||||
}
|
||||
|
||||
|
@ -167,19 +181,10 @@ class OverlayBrowsing {
|
|||
goToTopEventEmitter = new EventEmitter();
|
||||
|
||||
constructor(
|
||||
public readonly type: 'hashtag' | 'account' | 'thread' | 'follows' | 'followers',
|
||||
public readonly hashtag: StreamElement,
|
||||
public readonly account: string,
|
||||
public readonly thread: OpenThreadEvent) {
|
||||
|
||||
if (hashtag) {
|
||||
this.type = 'hashtag';
|
||||
} else if (account) {
|
||||
this.type = 'account';
|
||||
} else if (thread) {
|
||||
this.type = 'thread';
|
||||
} else {
|
||||
throw Error('NotImplemented');
|
||||
}
|
||||
}
|
||||
|
||||
show(): any {
|
||||
|
@ -198,5 +203,4 @@ class OverlayBrowsing {
|
|||
}
|
||||
|
||||
isVisible: boolean;
|
||||
type: 'hashtag' | 'account' | 'thread';
|
||||
}
|
||||
|
|
|
@ -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,8 @@ 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;
|
||||
this.lastCallReachedMax = false;
|
||||
}
|
||||
|
||||
if (this.bufferStream.length > 3 * this.streamingService.nbStatusPerIteration) {
|
||||
|
@ -341,24 +275,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<div class="follow flexcroll" #accountslist (scroll)="onScroll()" >
|
||||
<div class="accounts" *ngIf="accounts.length > 0">
|
||||
<app-account class="account" *ngFor="let account of accounts"
|
||||
[account]="account"
|
||||
(accountSelected)="browseAccount($event)"></app-account>
|
||||
</div>
|
||||
<div *ngIf="accounts.length === 0 && !isLoading" class="follow__empty"> There is nothing here! </div>
|
||||
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
|
||||
</div>
|
|
@ -0,0 +1,20 @@
|
|||
@import "variables";
|
||||
@import "commons";
|
||||
|
||||
.follow {
|
||||
height: calc(100%);
|
||||
width: calc(100%);
|
||||
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
&__empty {
|
||||
padding: 20px 0 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.account {
|
||||
display: block;
|
||||
margin: 0 1px;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UserFollowsComponent } from './user-follows.component';
|
||||
|
||||
xdescribe('UserFollowsComponent', () => {
|
||||
let component: UserFollowsComponent;
|
||||
let fixture: ComponentFixture<UserFollowsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ UserFollowsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(UserFollowsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,195 @@
|
|||
import { Component, OnInit, Input, EventEmitter, Output, OnDestroy, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Subscription, Observable } from 'rxjs';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
|
||||
import { ToolsService } from '../../../services/tools.service';
|
||||
import { Account } from "../../../services/models/mastodon.interfaces";
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { FollowingResult } from '../../../services/mastodon.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-follows',
|
||||
templateUrl: './user-follows.component.html',
|
||||
styleUrls: ['./user-follows.component.scss']
|
||||
})
|
||||
export class UserFollowsComponent implements OnInit, OnDestroy {
|
||||
|
||||
private _type: 'follows' | 'followers';
|
||||
private _currentAccount: string;
|
||||
|
||||
private maxId: string;
|
||||
isLoading: boolean = true;
|
||||
accounts: Account[] = [];
|
||||
|
||||
@Input('type')
|
||||
set setType(type: 'follows' | 'followers') {
|
||||
this._type = type;
|
||||
this.load(this._type, this._currentAccount);
|
||||
}
|
||||
get setType(): 'follows' | 'followers' {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
@Input('currentAccount')
|
||||
set currentAccount(accountName: string) {
|
||||
this._currentAccount = accountName;
|
||||
this.load(this._type, this._currentAccount);
|
||||
}
|
||||
get currentAccount(): string {
|
||||
return this._currentAccount;
|
||||
}
|
||||
|
||||
@Input() refreshEventEmitter: EventEmitter<any>;
|
||||
@Input() goToTopEventEmitter: EventEmitter<any>;
|
||||
|
||||
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||
|
||||
@ViewChild('accountslist') public accountslist: ElementRef;
|
||||
|
||||
private refreshSubscription: Subscription;
|
||||
private goToTopSubscription: Subscription;
|
||||
// private accountSub: Subscription;
|
||||
// private accounts$: Observable<AccountInfo[]>;
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mastodonService: MastodonWrapperService) {
|
||||
// this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.refreshEventEmitter) {
|
||||
this.refreshSubscription = this.refreshEventEmitter.subscribe(() => {
|
||||
this.refresh();
|
||||
})
|
||||
}
|
||||
|
||||
if (this.goToTopEventEmitter) {
|
||||
this.goToTopSubscription = this.goToTopEventEmitter.subscribe(() => {
|
||||
this.goToTop();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.refreshSubscription) this.refreshSubscription.unsubscribe();
|
||||
if (this.goToTopSubscription) this.goToTopSubscription.unsubscribe();
|
||||
// if (this.accountSub) this.accountSub.unsubscribe();
|
||||
}
|
||||
|
||||
private load(type: 'follows' | 'followers', accountName: string) {
|
||||
if (type && accountName) {
|
||||
this.accounts = [];
|
||||
this.isLoading = true;
|
||||
|
||||
let currentAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
this.toolsService.findAccount(currentAccount, accountName)
|
||||
.then((acc: Account) => {
|
||||
if (type === 'followers') {
|
||||
return this.mastodonService.getFollowers(currentAccount, acc.id, null, null);
|
||||
} else if (type === 'follows') {
|
||||
return this.mastodonService.getFollowing(currentAccount, acc.id, null, null);
|
||||
} else {
|
||||
throw Error('not implemented');
|
||||
}
|
||||
})
|
||||
.then((result: FollowingResult) => {
|
||||
console.warn(result);
|
||||
this.maxId = result.maxId;
|
||||
this.accounts = result.follows;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, currentAccount);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private scrolledToBottom() {
|
||||
if (this.isLoading || this.maxReached || this.scrolledErrorOccured || this.accounts.length === 0) return;
|
||||
|
||||
this.isLoading = true;
|
||||
this.isProcessingInfiniteScroll = true;
|
||||
|
||||
let currentAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
this.toolsService.findAccount(currentAccount, this._currentAccount)
|
||||
.then((acc: Account) => {
|
||||
if (this._type === 'followers') {
|
||||
return this.mastodonService.getFollowers(currentAccount, acc.id, this.maxId, null);
|
||||
} else if (this._type === 'follows') {
|
||||
return this.mastodonService.getFollowing(currentAccount, acc.id, this.maxId, null);
|
||||
} else {
|
||||
throw Error('not implemented');
|
||||
}
|
||||
})
|
||||
.then((result: FollowingResult) => {
|
||||
if(!result) return;
|
||||
|
||||
let accounts = result.follows;
|
||||
|
||||
if (!accounts || accounts.length === 0 || this.maxReached) {
|
||||
this.maxReached = true;
|
||||
return;
|
||||
}
|
||||
|
||||
for (let a of accounts) {
|
||||
this.accounts.push(a);
|
||||
}
|
||||
|
||||
this.maxId = result.maxId;
|
||||
if(!this.maxId) {
|
||||
this.maxReached = true;
|
||||
}
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.scrolledErrorOccured = true;
|
||||
setTimeout(() => {
|
||||
this.scrolledErrorOccured = false;
|
||||
}, 5000);
|
||||
|
||||
this.notificationService.notifyHttpError(err, currentAccount);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
this.isProcessingInfiniteScroll = false;
|
||||
});
|
||||
}
|
||||
|
||||
refresh(): any {
|
||||
this.load(this._type, this._currentAccount);
|
||||
}
|
||||
|
||||
goToTop(): any {
|
||||
const stream = this.accountslist.nativeElement as HTMLElement;
|
||||
setTimeout(() => {
|
||||
stream.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
browseAccount(account: Account) {
|
||||
let acc = this.toolsService.getAccountFullHandle(account);
|
||||
this.browseAccountEvent.next(acc);
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
var element = this.accountslist.nativeElement as HTMLElement;
|
||||
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
|
||||
const atTop = element.scrollTop === 0;
|
||||
|
||||
if (atBottom) {
|
||||
this.scrolledToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
private scrolledErrorOccured = false;
|
||||
private maxReached = false;
|
||||
private isProcessingInfiniteScroll = false;
|
||||
|
||||
}
|
|
@ -121,6 +121,11 @@
|
|||
target="_blank" title="{{displayedAccount.acct}}">@{{displayedAccount.acct}}</a></h2>
|
||||
</div>
|
||||
|
||||
<div class="profile-follows">
|
||||
<a href class="profile-follows__link" title="show following" (click)="browseFollows()" >Following</a>
|
||||
<a href class="profile-follows__link" title="show followers" (click)="browseFollowers()">Followers</a>
|
||||
</div>
|
||||
|
||||
<!-- <div class="profile__extra-info">
|
||||
<div class="profile__extra-info__section">
|
||||
<a href class="profile__extra-info__links"
|
||||
|
|
|
@ -15,7 +15,7 @@ $floating-header-height: 60px;
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.profile {
|
||||
.profile {
|
||||
// overflow: auto;
|
||||
height: calc(100%);
|
||||
// width: $stream-column-width;
|
||||
|
@ -175,7 +175,8 @@ $floating-header-height: 60px;
|
|||
display: inline-block;
|
||||
transition: all .2s;
|
||||
color: white;
|
||||
&:hover{
|
||||
|
||||
&:hover {
|
||||
color: rgb(216, 216, 216);
|
||||
}
|
||||
}
|
||||
|
@ -188,7 +189,7 @@ $floating-header-height: 60px;
|
|||
color: #5fbcff;
|
||||
color: #85ccff;
|
||||
|
||||
&:hover{
|
||||
&:hover {
|
||||
color: #85ccff;
|
||||
color: #38abff;
|
||||
}
|
||||
|
@ -212,7 +213,7 @@ $floating-header-height: 60px;
|
|||
font-size: 12px;
|
||||
// max-width: 150px;
|
||||
width: 265px;
|
||||
|
||||
|
||||
//outline: 1px solid greenyellow;
|
||||
|
||||
&--data {
|
||||
|
@ -221,7 +222,7 @@ $floating-header-height: 60px;
|
|||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
margin: 0 2px 2px 0;
|
||||
margin: 0 2px 2px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -252,7 +253,7 @@ $floating-header-height: 60px;
|
|||
overflow: hidden;
|
||||
margin: 0;
|
||||
|
||||
&:not(:last-child){
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
}
|
||||
|
@ -271,6 +272,27 @@ $floating-header-height: 60px;
|
|||
}
|
||||
}
|
||||
|
||||
&-follows {
|
||||
width: calc(100%);
|
||||
font-size: 13px;
|
||||
border-bottom: 1px solid #0f111a;;
|
||||
|
||||
&__link {
|
||||
color: white;
|
||||
width: calc(50%);
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
background-color: #1a1f2e;
|
||||
transition: all .2s;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: #131722;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-description {
|
||||
padding: 9px 10px 15px 10px;
|
||||
font-size: 13px;
|
||||
|
@ -360,7 +382,7 @@ $floating-header-height: 60px;
|
|||
}
|
||||
|
||||
&__status-switching-section {
|
||||
height: calc(100vh - 35px - #{$floating-header-height} - #{$stream-header-height} - #{$stream-selector-height});
|
||||
height: calc(100vh - 35px - #{$floating-header-height} - #{$stream-header-height} - #{$stream-selector-height});
|
||||
}
|
||||
|
||||
&-no-toots {
|
||||
|
|
|
@ -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)
|
||||
|
@ -428,4 +418,19 @@ export class UserProfileComponent implements OnInit {
|
|||
this.navigationService.openMedia(openMediaEvent);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Output() browseFollowsEvent = new EventEmitter<string>();
|
||||
@Output() browseFollowersEvent = new EventEmitter<string>();
|
||||
|
||||
browseFollows(): boolean {
|
||||
let accountName = this.toolsService.getAccountFullHandle(this.displayedAccount);
|
||||
this.browseFollowsEvent.next(accountName);
|
||||
return false;
|
||||
}
|
||||
|
||||
browseFollowers(): boolean {
|
||||
let accountName = this.toolsService.getAccountFullHandle(this.displayedAccount);
|
||||
this.browseFollowersEvent.next(accountName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,14 +4,14 @@ import { Store } from '@ngxs/store';
|
|||
import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus, TokenData } from "./models/mastodon.interfaces";
|
||||
import { AccountInfo, UpdateAccount } from '../states/accounts.state';
|
||||
import { StreamTypeEnum, StreamElement } from '../states/streams.state';
|
||||
import { FavoriteResult, VisibilityEnum, PollParameters, MastodonService, BookmarkResult } from './mastodon.service';
|
||||
import { FavoriteResult, VisibilityEnum, PollParameters, MastodonService, BookmarkResult, FollowingResult } from './mastodon.service';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AppInfo, RegisteredAppsStateModel } from '../states/registered-apps.state';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MastodonWrapperService {
|
||||
export class MastodonWrapperService {
|
||||
private refreshingToken: { [id: string]: Promise<AccountInfo> } = {};
|
||||
|
||||
constructor(
|
||||
|
@ -391,4 +391,18 @@ export class MastodonWrapperService {
|
|||
return this.mastodonService.deleteScheduledStatus(refreshedAccount, statusId);
|
||||
});
|
||||
}
|
||||
|
||||
getFollowing(account: AccountInfo, accountId: number, maxId: string, sinceId: string, limit: number = 40): Promise<FollowingResult> {
|
||||
return this.refreshAccountIfNeeded(account)
|
||||
.then((refreshedAccount: AccountInfo) => {
|
||||
return this.mastodonService.getFollowing(refreshedAccount, accountId, maxId, sinceId, limit);
|
||||
});
|
||||
}
|
||||
|
||||
getFollowers(account: AccountInfo, accountId: number, maxId: string, sinceId: string, limit: number = 40): Promise<FollowingResult> {
|
||||
return this.refreshAccountIfNeeded(account)
|
||||
.then((refreshedAccount: AccountInfo) => {
|
||||
return this.mastodonService.getFollowers(refreshedAccount, accountId, maxId, sinceId, limit);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -468,6 +468,49 @@ export class MastodonService {
|
|||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.delete<ScheduledStatus>(route, { headers: headers }).toPromise();
|
||||
}
|
||||
|
||||
getFollowers(account: AccountInfo, targetAccountId: number, maxId: string, sinceId: string, limit: number = 40): Promise<FollowingResult> {
|
||||
const route = `https://${account.instance}${this.apiRoutes.getFollowers}`.replace('{0}', targetAccountId.toString());
|
||||
|
||||
let params = `?limit=${limit}`;
|
||||
if (maxId) params += `&max_id=${maxId}`;
|
||||
if (sinceId) params += `&since_id=${sinceId}`;
|
||||
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.get<Account[]>(route + params, { headers: headers, observe: "response" }).toPromise()
|
||||
.then((res: HttpResponse<Account[]>) => {
|
||||
const link = res.headers.get('Link');
|
||||
let lastId = null;
|
||||
if (link) {
|
||||
const maxId = link.split('max_id=')[1];
|
||||
if (maxId) {
|
||||
lastId = maxId.split('>;')[0];
|
||||
}
|
||||
}
|
||||
return new FollowingResult(lastId, res.body)
|
||||
});
|
||||
}
|
||||
getFollowing(account: AccountInfo, targetAccountId: number, maxId: string, sinceId: string, limit: number = 40): Promise<FollowingResult> {
|
||||
const route = `https://${account.instance}${this.apiRoutes.getFollowing}`.replace('{0}', targetAccountId.toString());
|
||||
|
||||
let params = `?limit=${limit}`;
|
||||
if (maxId) params += `&max_id=${maxId}`;
|
||||
if (sinceId) params += `&since_id=${sinceId}`;
|
||||
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.get<Account[]>(route + params, { headers: headers, observe: "response" }).toPromise()
|
||||
.then((res: HttpResponse<Account[]>) => {
|
||||
const link = res.headers.get('Link');
|
||||
let lastId = null;
|
||||
if (link) {
|
||||
const maxId = link.split('max_id=')[1];
|
||||
if (maxId) {
|
||||
lastId = maxId.split('>;')[0];
|
||||
}
|
||||
}
|
||||
return new FollowingResult(lastId, res.body)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export enum VisibilityEnum {
|
||||
|
@ -506,4 +549,10 @@ export class BookmarkResult {
|
|||
constructor(
|
||||
public max_id: string,
|
||||
public bookmarked: Status[]) { }
|
||||
}
|
||||
|
||||
export class FollowingResult {
|
||||
constructor(
|
||||
public maxId: string,
|
||||
public follows: Account[]) { }
|
||||
}
|
|
@ -73,4 +73,6 @@ export class ApiRoutes {
|
|||
bookmarkingStatus = '/api/v1/statuses/{0}/bookmark';
|
||||
unbookmarkingStatus = '/api/v1/statuses/{0}/unbookmark';
|
||||
getBookmarks = '/api/v1/bookmarks';
|
||||
getFollowers = '/api/v1/accounts/{0}/followers';
|
||||
getFollowing = '/api/v1/accounts/{0}/following';
|
||||
}
|
||||
|
|
|
@ -29,8 +29,11 @@ export class NotificationService {
|
|||
|
||||
try {
|
||||
code = err.status;
|
||||
message = err.error.error; //Mastodon
|
||||
if (!message) {
|
||||
if(err.message){
|
||||
message = err.message;
|
||||
} else if(err.error && err.error.error) {
|
||||
message = err.error.error; //Mastodon
|
||||
} else if(err.error && err.error.errors && err.error.errors.detail){
|
||||
message = err.error.errors.detail; //Pleroma
|
||||
}
|
||||
} catch (err) { }
|
||||
|
|
Loading…
Reference in New Issue