creation of a dedicated controller for loading the statuses

This commit is contained in:
Nicolas Constant 2018-11-15 23:57:52 -05:00
parent f33f2e37bb
commit 27d3507922
No known key found for this signature in database
GPG Key ID: 1E9F677FB01A5688
9 changed files with 275 additions and 163 deletions

View File

@ -41,6 +41,7 @@ import { HashtagComponent } from './components/stream/hashtag/hashtag.component'
import { StreamOverlayComponent } from './components/stream/stream-overlay/stream-overlay.component';
import { DatabindedTextComponent } from './components/stream/status/databinded-text/databinded-text.component';
import { TimeAgoPipe } from './pipes/time-ago.pipe';
import { StreamStatusesComponent } from './components/stream/stream-statuses/stream-statuses.component';
const routes: Routes = [
{ path: "", redirectTo: "home", pathMatch: "full" },
@ -74,7 +75,8 @@ const routes: Routes = [
HashtagComponent,
StreamOverlayComponent,
DatabindedTextComponent,
TimeAgoPipe
TimeAgoPipe,
StreamStatusesComponent
],
imports: [
BrowserModule,

View File

@ -16,7 +16,6 @@ export class FloatingColumnComponent implements OnInit {
openPanel: string;
constructor(private readonly navigationService: NavigationService) { }
ngOnInit() {

View File

@ -0,0 +1,6 @@
<div class="stream-toots flexcroll" #statusstream (scroll)="onScroll()">
<!-- data-simplebar -->
<div class="stream-toots__status" *ngFor="let statusWrapper of statuses">
<app-status [statusWrapper]="statusWrapper" (browseAccount)="accountSelected($event)" (browseHashtag)="hashtagSelected($event)"></app-status>
</div>
</div>

View File

@ -0,0 +1,13 @@
@import "variables";
@import "commons";
.stream-toots {
height: calc(100%);
width: calc(100%);
overflow: auto;
&__status:not(:last-child) {
border: solid #06070b;
border-width: 0 0 1px 0;
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { StreamStatusesComponent } from './stream-statuses.component';
describe('StreamStatusesComponent', () => {
let component: StreamStatusesComponent;
let fixture: ComponentFixture<StreamStatusesComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ StreamStatusesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(StreamStatusesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,198 @@
import { Component, OnInit, Input, ViewChild, ElementRef, OnDestroy, EventEmitter, Output } from '@angular/core';
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 { Status } from '../../../services/models/mastodon.interfaces';
import { MastodonService } from '../../../services/mastodon.service';
import { Observable, Subscription } from 'rxjs';
import { StatusWrapper } from '../stream.component';
@Component({
selector: 'app-stream-statuses',
templateUrl: './stream-statuses.component.html',
styleUrls: ['./stream-statuses.component.scss']
})
export class StreamStatusesComponent implements OnInit, OnDestroy {
private _streamElement: StreamElement;
private account: AccountInfo;
private websocketStreaming: StreamingWrapper;
statuses: StatusWrapper[] = [];
private bufferStream: Status[] = [];
private bufferWasCleared: boolean;
@Output() browseAccount = new EventEmitter<string>();
@Output() browseHashtag = new EventEmitter<string>();
@Output() browseThread = new EventEmitter<string>();
@Input()
set streamElement(streamElement: StreamElement) {
this._streamElement = streamElement;
const splitedUserName = streamElement.accountId.split('@');
const user = splitedUserName[0];
const instance = splitedUserName[1];
this.account = this.getRegisteredAccounts().find(x => x.username == user && x.instance == instance);
this.retrieveToots();
this.launchWebsocket();
}
get streamElement(): StreamElement {
return this._streamElement;
}
@Input() goToTop: Observable<void>;
private goToTopSubscription: Subscription;
constructor(
private readonly store: Store,
private readonly streamingService: StreamingService,
private readonly mastodonService: MastodonService) {
}
ngOnInit() {
this.goToTopSubscription = this.goToTop.subscribe(() => {
this.applyGoToTop();
});
}
ngOnDestroy(){
if( this.goToTopSubscription) this.goToTopSubscription.unsubscribe();
}
private launchWebsocket(): void {
this.websocketStreaming = this.streamingService.getStreaming(this.account, this._streamElement);
this.websocketStreaming.statusUpdateSubjet.subscribe((update: StatusUpdate) => {
if (update) {
if (update.type === EventEnum.update) {
if (!this.statuses.find(x => x.status.id == update.status.id)) {
if (this.streamPositionnedAtTop) {
const wrapper = new StatusWrapper(update.status, this.account);
this.statuses.unshift(wrapper);
} else {
this.bufferStream.push(update.status);
}
}
}
}
this.checkAndCleanUpStream();
});
}
@ViewChild('statusstream') public statustream: ElementRef;
private applyGoToTop(): boolean {
this.loadBuffer();
if (this.statuses.length > 2 * this.streamingService.nbStatusPerIteration) {
this.statuses.length = 2 * this.streamingService.nbStatusPerIteration;
}
const stream = this.statustream.nativeElement as HTMLElement;
stream.scrollTo({
top: 0,
behavior: 'smooth'
});
return false;
}
private streamPositionnedAtTop: boolean = true;
private isProcessingInfiniteScroll: boolean;
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();
}
}
accountSelected(accountName: string): void {
console.warn(`status comp: accountSelected ${accountName}`);
this.browseAccount.next(accountName);
}
hashtagSelected(hashtag: string): void {
console.warn(`status comp: hashtagSelected ${hashtag}`);
this.browseHashtag.next(hashtag);
}
textSelected(): void {
console.warn(`status comp: textSelected`);
}
private scrolledToTop() {
this.streamPositionnedAtTop = true;
this.loadBuffer();
}
private loadBuffer(){
if(this.bufferWasCleared) {
this.statuses.length = 0;
this.bufferWasCleared = false;
}
for (const status of this.bufferStream) {
const wrapper = new StatusWrapper(status, this.account);
this.statuses.unshift(wrapper);
}
this.bufferStream.length = 0;
}
private scrolledToBottom() {
this.isProcessingInfiniteScroll = true;
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.list)
.then((status: Status[]) => {
for (const s of status) {
const wrapper = new StatusWrapper(s, this.account);
this.statuses.push(wrapper);
}
})
.catch(err => {
console.error(err);
})
.then(() => {
this.isProcessingInfiniteScroll = false;
});
}
private getRegisteredAccounts(): AccountInfo[] {
var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
return regAccounts;
}
private retrieveToots(): void {
this.mastodonService.getTimeline(this.account, this._streamElement.type, null, null, this.streamingService.nbStatusPerIteration, this._streamElement.tag, this._streamElement.list)
.then((results: Status[]) => {
for (const s of results) {
const wrapper = new StatusWrapper(s, this.account);
this.statuses.push(wrapper);
}
});
}
private checkAndCleanUpStream(): void {
if (this.streamPositionnedAtTop && this.statuses.length > 3 * this.streamingService.nbStatusPerIteration) {
this.statuses.length = 2 * this.streamingService.nbStatusPerIteration;
}
if (this.bufferStream.length > 3 * this.streamingService.nbStatusPerIteration) {
this.bufferWasCleared = true;
this.bufferStream.length = 2 * this.streamingService.nbStatusPerIteration;
}
}
}

View File

@ -10,10 +10,10 @@
<h1>{{ streamElement.name.toUpperCase() }}</h1>
</a>
</div>
<div class="stream-toots flexcroll" #statusstream (scroll)="onScroll()">
<!-- data-simplebar -->
<app-stream-statuses class="stream-statuses" [streamElement]="streamElement" [goToTop]="goToTopSubject.asObservable()" (browseAccount)="browseAccount($event)" (browseHashtag)="browseHashtag($event)"></app-stream-statuses>
<!-- <div class="stream-toots flexcroll" #statusstream (scroll)="onScroll()">
<div class="stream-toots__status" *ngFor="let statusWrapper of statuses">
<app-status [statusWrapper]="statusWrapper" (browseAccount)="browseAccount($event)" (browseHashtag)="browseHashtag($event)"></app-status>
</div>
</div>
</div> -->
</div>

View File

@ -22,16 +22,22 @@
}
}
.stream-toots {
.stream-statuses {
display: block;
height: calc(100% - 30px);
width: 320px;
overflow: auto;
&__status:not(:last-child) {
border: solid #06070b;
border-width: 0 0 1px 0;
}
}
// .stream-toots {
// height: calc(100% - 30px);
// width: 320px;
// overflow: auto;
// &__status:not(:last-child) {
// border: solid #06070b;
// border-width: 0 0 1px 0;
// }
// }
.stream-overlay {
position: absolute;

View File

@ -1,11 +1,9 @@
import { Component, OnInit, Input, ElementRef, ViewChild, HostListener } from "@angular/core";
import { AccountWrapper } from "../../models/account.models";
import { StreamElement, StreamTypeEnum } from "../../states/streams.state";
import { StreamingService, StreamingWrapper, EventEnum, StatusUpdate } from "../../services/streaming.service";
import { Store } from "@ngxs/store";
import { AccountInfo } from "../../states/accounts.state";
import { Subject } from "rxjs";
import { StreamElement } from "../../states/streams.state";
import { Status } from "../../services/models/mastodon.interfaces";
import { MastodonService } from "../../services/mastodon.service";
import { AccountInfo } from "../../states/accounts.state";
@Component({
selector: "app-stream",
@ -13,54 +11,34 @@ import { MastodonService } from "../../services/mastodon.service";
styleUrls: ["./stream.component.scss"]
})
export class StreamComponent implements OnInit {
private _streamElement: StreamElement;
private account: AccountInfo;
private websocketStreaming: StreamingWrapper;
statuses: StatusWrapper[] = [];
private bufferStream: Status[] = [];
private bufferWasCleared: boolean;
overlayActive: boolean;
overlayAccountToBrowse: string;
overlayHashtagToBrowse: string;
@Input()
set streamElement(streamElement: StreamElement) {
this._streamElement = streamElement;
private goToTopSubject: Subject<void> = new Subject<void>();
const splitedUserName = streamElement.accountId.split('@');
const user = splitedUserName[0];
const instance = splitedUserName[1];
this.account = this.getRegisteredAccounts().find(x => x.username == user && x.instance == instance);
@Input() streamElement: StreamElement;
this.retrieveToots();
this.launchWebsocket();
}
get streamElement(): StreamElement {
return this._streamElement;
}
constructor(
private readonly store: Store,
private readonly streamingService: StreamingService,
private readonly mastodonService: MastodonService) {
}
constructor() { }
ngOnInit() {
}
goToTop(): boolean {
this.goToTopSubject.next();
return false;
}
browseAccount(account: string): void {
this.overlayAccountToBrowse = account;
this.overlayHashtagToBrowse = null;
this.overlayActive = true;
this.overlayActive = true;
}
browseHashtag(hashtag: string): void {
this.overlayAccountToBrowse = null;
this.overlayHashtagToBrowse = hashtag;
this.overlayActive = true;
this.overlayActive = true;
}
browseThread(thread: string): void {
@ -72,126 +50,11 @@ export class StreamComponent implements OnInit {
this.overlayAccountToBrowse = null;
this.overlayActive = false;
}
@ViewChild('statusstream') public statustream: ElementRef;
goToTop(): boolean {
this.loadBuffer();
if (this.statuses.length > 2 * this.streamingService.nbStatusPerIteration) {
this.statuses.length = 2 * this.streamingService.nbStatusPerIteration;
}
const stream = this.statustream.nativeElement as HTMLElement;
stream.scrollTo({
top: 0,
behavior: 'smooth'
});
return false;
}
private streamPositionnedAtTop: boolean = true;
private isProcessingInfiniteScroll: boolean;
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 scrolledToTop() {
this.streamPositionnedAtTop = true;
this.loadBuffer();
}
private loadBuffer(){
if(this.bufferWasCleared) {
this.statuses.length = 0;
this.bufferWasCleared = false;
}
for (const status of this.bufferStream) {
const wrapper = new StatusWrapper(status, this.account);
this.statuses.unshift(wrapper);
}
this.bufferStream.length = 0;
}
private scrolledToBottom() {
this.isProcessingInfiniteScroll = true;
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.list)
.then((status: Status[]) => {
for (const s of status) {
const wrapper = new StatusWrapper(s, this.account);
this.statuses.push(wrapper);
}
})
.catch(err => {
console.error(err);
})
.then(() => {
this.isProcessingInfiniteScroll = false;
});
}
private getRegisteredAccounts(): AccountInfo[] {
var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
return regAccounts;
}
private retrieveToots(): void {
this.mastodonService.getTimeline(this.account, this._streamElement.type, null, null, this.streamingService.nbStatusPerIteration, this._streamElement.tag, this._streamElement.list)
.then((results: Status[]) => {
for (const s of results) {
const wrapper = new StatusWrapper(s, this.account);
this.statuses.push(wrapper);
}
});
}
private launchWebsocket(): void {
this.websocketStreaming = this.streamingService.getStreaming(this.account, this._streamElement);
this.websocketStreaming.statusUpdateSubjet.subscribe((update: StatusUpdate) => {
if (update) {
if (update.type === EventEnum.update) {
if (!this.statuses.find(x => x.status.id == update.status.id)) {
if (this.streamPositionnedAtTop) {
const wrapper = new StatusWrapper(update.status, this.account);
this.statuses.unshift(wrapper);
} else {
this.bufferStream.push(update.status);
}
}
}
}
this.checkAndCleanUpStream();
});
}
private checkAndCleanUpStream(): void {
if (this.streamPositionnedAtTop && this.statuses.length > 3 * this.streamingService.nbStatusPerIteration) {
this.statuses.length = 2 * this.streamingService.nbStatusPerIteration;
}
if (this.bufferStream.length > 3 * this.streamingService.nbStatusPerIteration) {
this.bufferWasCleared = true;
this.bufferStream.length = 2 * this.streamingService.nbStatusPerIteration;
}
}
}
export class StatusWrapper {
constructor(
public status: Status,
public provider: AccountInfo
) {}
) { }
}