From 27d3507922384f587fd9e4424edf1cfd3fea84b8 Mon Sep 17 00:00:00 2001 From: Nicolas Constant Date: Thu, 15 Nov 2018 23:57:52 -0500 Subject: [PATCH] creation of a dedicated controller for loading the statuses --- src/app/app.module.ts | 4 +- .../floating-column.component.ts | 1 - .../stream-statuses.component.html | 6 + .../stream-statuses.component.scss | 13 ++ .../stream-statuses.component.spec.ts | 25 +++ .../stream-statuses.component.ts | 198 ++++++++++++++++++ .../components/stream/stream.component.html | 6 +- .../components/stream/stream.component.scss | 18 +- src/app/components/stream/stream.component.ts | 167 ++------------- 9 files changed, 275 insertions(+), 163 deletions(-) create mode 100644 src/app/components/stream/stream-statuses/stream-statuses.component.html create mode 100644 src/app/components/stream/stream-statuses/stream-statuses.component.scss create mode 100644 src/app/components/stream/stream-statuses/stream-statuses.component.spec.ts create mode 100644 src/app/components/stream/stream-statuses/stream-statuses.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 50e84682..a58959f7 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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, diff --git a/src/app/components/floating-column/floating-column.component.ts b/src/app/components/floating-column/floating-column.component.ts index b35edbbd..8f5116eb 100644 --- a/src/app/components/floating-column/floating-column.component.ts +++ b/src/app/components/floating-column/floating-column.component.ts @@ -16,7 +16,6 @@ export class FloatingColumnComponent implements OnInit { openPanel: string; - constructor(private readonly navigationService: NavigationService) { } ngOnInit() { diff --git a/src/app/components/stream/stream-statuses/stream-statuses.component.html b/src/app/components/stream/stream-statuses/stream-statuses.component.html new file mode 100644 index 00000000..43918a72 --- /dev/null +++ b/src/app/components/stream/stream-statuses/stream-statuses.component.html @@ -0,0 +1,6 @@ +
+ +
+ +
+
\ No newline at end of file diff --git a/src/app/components/stream/stream-statuses/stream-statuses.component.scss b/src/app/components/stream/stream-statuses/stream-statuses.component.scss new file mode 100644 index 00000000..b7f225a0 --- /dev/null +++ b/src/app/components/stream/stream-statuses/stream-statuses.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/src/app/components/stream/stream-statuses/stream-statuses.component.spec.ts b/src/app/components/stream/stream-statuses/stream-statuses.component.spec.ts new file mode 100644 index 00000000..3a391ce1 --- /dev/null +++ b/src/app/components/stream/stream-statuses/stream-statuses.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ StreamStatusesComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(StreamStatusesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/stream/stream-statuses/stream-statuses.component.ts b/src/app/components/stream/stream-statuses/stream-statuses.component.ts new file mode 100644 index 00000000..74ef3465 --- /dev/null +++ b/src/app/components/stream/stream-statuses/stream-statuses.component.ts @@ -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(); + @Output() browseHashtag = new EventEmitter(); + @Output() browseThread = new EventEmitter(); + + @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; + + 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 = 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; + } + } +} + diff --git a/src/app/components/stream/stream.component.html b/src/app/components/stream/stream.component.html index a9f2099e..cd7b94dd 100644 --- a/src/app/components/stream/stream.component.html +++ b/src/app/components/stream/stream.component.html @@ -10,10 +10,10 @@

{{ streamElement.name.toUpperCase() }}

-
- + +
\ No newline at end of file diff --git a/src/app/components/stream/stream.component.scss b/src/app/components/stream/stream.component.scss index aeb3219c..ae66203e 100644 --- a/src/app/components/stream/stream.component.scss +++ b/src/app/components/stream/stream.component.scss @@ -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; diff --git a/src/app/components/stream/stream.component.ts b/src/app/components/stream/stream.component.ts index 33e379f7..c0456de4 100644 --- a/src/app/components/stream/stream.component.ts +++ b/src/app/components/stream/stream.component.ts @@ -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 = new Subject(); - 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 = 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 - ) {} + ) { } } \ No newline at end of file