From 9270ccf6dca5b2955ad126947d4296deb385fdcb Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Wed, 8 Jan 2020 22:13:47 +0100 Subject: [PATCH] Make subscribe buttons observe subscription statuses to synchronise --- .../account-video-channels.component.html | 6 +- .../src/app/+accounts/accounts.component.html | 7 +- .../src/app/+accounts/accounts.component.ts | 12 +-- .../subscribe-button.component.html | 15 +-- .../subscribe-button.component.scss | 15 +++ .../subscribe-button.component.ts | 86 +++++++++-------- .../user-subscription.service.ts | 92 +++++++++++++++++-- .../video/video-miniature.component.html | 6 +- 8 files changed, 152 insertions(+), 87 deletions(-) diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html index 93f43a350..781156840 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html @@ -9,11 +9,7 @@ Avatar
{{ videoChannel.displayName }}
-
{{ videoChannel.followersCount }} - - subscriber - subscribers -
+
{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html index 1b6eb480e..be40b63ca 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html @@ -28,12 +28,7 @@ > -
- {{ account.followersCount }} - - subscriber - subscribers -
+
{account.followersCount, plural, =1 {1 subscriber} other {{{ account.followersCount }} subscribers}}
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts index e3a503f4c..8bde7ad07 100644 --- a/client/src/app/+accounts/accounts.component.ts +++ b/client/src/app/+accounts/accounts.component.ts @@ -40,15 +40,15 @@ export class AccountsComponent implements OnInit, OnDestroy { map(params => params[ 'accountId' ]), distinctUntilChanged(), switchMap(accountId => this.accountService.getAccount(accountId)), - tap(account => this.getUserIfNeeded(account)), + tap(account => { + this.account = account + this.getUserIfNeeded(account) + }), + switchMap(account => this.videoChannelService.listAccountVideoChannels(account)), catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])) ) .subscribe( - account => { - this.account = account - this.videoChannelService.listAccountVideoChannels(account) - .subscribe(videoChannels => this.videoChannels = videoChannels.data) - }, + videoChannels => this.videoChannels = videoChannels.data, err => this.notifier.error(err.message) ) diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.html b/client/src/app/shared/user-subscription/subscribe-button.component.html index 6ac8af3de..275349b7f 100644 --- a/client/src/app/shared/user-subscription/subscribe-button.component.html +++ b/client/src/app/shared/user-subscription/subscribe-button.component.html @@ -1,13 +1,13 @@
+ [ngClass]="{'subscribe-button': !isAllChannelsSubscribed(), 'unsubscribe-button': isAllChannelsSubscribed(), 'big': isBigButton() }"> - + Subscribe Subscribe to all channels - {{subscribeStatus(true).length}}/{{subscribed.size}} + {{subscribeStatus(true).length}}/{{subscribed.size}} channels subscribed @@ -27,13 +27,8 @@ diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.scss b/client/src/app/shared/user-subscription/subscribe-button.component.scss index 164b917b8..d5b3796a1 100644 --- a/client/src/app/shared/user-subscription/subscribe-button.component.scss +++ b/client/src/app/shared/user-subscription/subscribe-button.component.scss @@ -13,6 +13,20 @@ font-size: 15px; } + &.big { + height: 35px; + + button .extra-text { + span:first-child { + line-height: 80%; + } + + span:not(:first-child) { + font-size: 75%; + } + } + } + // Unlogged & > .dropdown > .dropdown-toggle span { padding-right: 3px; @@ -80,5 +94,6 @@ span:not(:first-child) { font-size: 60%; + text-align: left; } } diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.ts b/client/src/app/shared/user-subscription/subscribe-button.component.ts index f0bee9d47..14a6bfe66 100644 --- a/client/src/app/shared/user-subscription/subscribe-button.component.ts +++ b/client/src/app/shared/user-subscription/subscribe-button.component.ts @@ -7,7 +7,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill' import { VideoService } from '@app/shared/video/video.service' import { FeedFormat } from '../../../../../shared/models/feeds' import { Account } from '@app/shared/account/account.model' -import { forkJoin } from 'rxjs' +import { forkJoin, merge } from 'rxjs' @Component({ selector: 'my-subscribe-button', @@ -26,7 +26,7 @@ export class SubscribeButtonComponent implements OnInit { @Input() displayFollowers = false @Input() size: 'small' | 'normal' = 'normal' - subscribed: Map + subscribed = new Map() constructor ( private authService: AuthService, @@ -35,9 +35,7 @@ export class SubscribeButtonComponent implements OnInit { private userSubscriptionService: UserSubscriptionService, private i18n: I18n, private videoService: VideoService - ) { - this.subscribed = new Map() - } + ) { } get handle () { return this.account @@ -68,19 +66,7 @@ export class SubscribeButtonComponent implements OnInit { } ngOnInit () { - if (this.isUserLoggedIn()) { - - forkJoin(this.videoChannels.map(videoChannel => { - const handle = this.getChannelHandler(videoChannel) - this.subscribed.set(handle, false) - this.userSubscriptionService.doesSubscriptionExist(handle) - .subscribe( - res => this.subscribed.set(handle, res[handle]), - - err => this.notifier.error(err.message) - ) - })) - } + this.loadSubscribedStatus() } subscribe () { @@ -92,31 +78,22 @@ export class SubscribeButtonComponent implements OnInit { } localSubscribe () { - const observableBatch: any = [] - - this.videoChannels - .filter(videoChannel => this.subscribeStatus(false).includes(this.getChannelHandler(videoChannel))) - .forEach(videoChannel => observableBatch.push( - this.userSubscriptionService.addSubscription(this.getChannelHandler(videoChannel)) - )) + const observableBatch = this.videoChannels + .map(videoChannel => this.getChannelHandler(videoChannel)) + .filter(handle => this.subscribeStatus(false).includes(handle)) + .map(handle => this.userSubscriptionService.addSubscription(handle)) forkJoin(observableBatch) .subscribe( () => { - [...this.subscribed.keys()].forEach((key) => { - this.subscribed.set(key, true) - }) - this.notifier.success( this.account ? this.i18n( - 'Subscribed to all current channels of {{nameWithHost}}. ' + - 'You will be notified of all their new videos.', + 'Subscribed to all current channels of {{nameWithHost}}. You will be notified of all their new videos.', { nameWithHost: this.account.displayName } ) : this.i18n( - 'Subscribed to {{nameWithHost}}. ' + - 'You will be notified of all their new videos.', + 'Subscribed to {{nameWithHost}}. You will be notified of all their new videos.', { nameWithHost: this.videoChannels[0].displayName } ) , @@ -135,21 +112,14 @@ export class SubscribeButtonComponent implements OnInit { } localUnsubscribe () { - const observableBatch: any = [] - - this.videoChannels - .filter(videoChannel => this.subscribeStatus(true).includes(this.getChannelHandler(videoChannel))) - .forEach(videoChannel => observableBatch.push( - this.userSubscriptionService.deleteSubscription(this.getChannelHandler(videoChannel)) - )) + const observableBatch = this.videoChannels + .map(videoChannel => this.getChannelHandler(videoChannel)) + .filter(handle => this.subscribeStatus(true).includes(handle)) + .map(handle => this.userSubscriptionService.deleteSubscription(handle)) forkJoin(observableBatch) .subscribe( () => { - [...this.subscribed.keys()].forEach((key) => { - this.subscribed.set(key, false) - }) - this.notifier.success( this.account ? this.i18n('Unsubscribed from all channels of {{nameWithHost}}', { nameWithHost: this.account.nameWithHost }) @@ -171,6 +141,14 @@ export class SubscribeButtonComponent implements OnInit { return !Array.from(this.subscribed.values()).includes(false) } + isAtLeastOneChannelSubscribed () { + return this.subscribeStatus(true).length > 0 + } + + isBigButton () { + return this.videoChannels.length > 1 && this.isAtLeastOneChannelSubscribed() + } + gotoLogin () { this.router.navigate([ '/login' ]) } @@ -180,10 +158,28 @@ export class SubscribeButtonComponent implements OnInit { } private subscribeStatus (subscribed: boolean) { - const accumulator = [] + const accumulator: string[] = [] for (const [key, value] of this.subscribed.entries()) { if (value === subscribed) accumulator.push(key) } return accumulator } + + private loadSubscribedStatus () { + if (!this.isUserLoggedIn()) return + + for (const videoChannel of this.videoChannels) { + const handle = this.getChannelHandler(videoChannel) + this.subscribed.set(handle, false) + merge( + this.userSubscriptionService.listenToSubscriptionCacheChange(handle), + this.userSubscriptionService.doesSubscriptionExist(handle) + ) + .subscribe( + res => this.subscribed.set(handle, res), + + err => this.notifier.error(err.message) + ) + } + } } diff --git a/client/src/app/shared/user-subscription/user-subscription.service.ts b/client/src/app/shared/user-subscription/user-subscription.service.ts index 83df40a43..bfb5848bc 100644 --- a/client/src/app/shared/user-subscription/user-subscription.service.ts +++ b/client/src/app/shared/user-subscription/user-subscription.service.ts @@ -1,44 +1,67 @@ -import { bufferTime, catchError, filter, first, map, share, switchMap } from 'rxjs/operators' +import { bufferTime, catchError, filter, map, tap, share, switchMap } from 'rxjs/operators' +import { Observable, ReplaySubject, Subject, of, merge } from 'rxjs' import { HttpClient, HttpParams } from '@angular/common/http' import { Injectable } from '@angular/core' import { ResultList } from '../../../../../shared' import { environment } from '../../../environments/environment' import { RestExtractor, RestService } from '../rest' -import { Observable, ReplaySubject, Subject } from 'rxjs' import { VideoChannel } from '@app/shared/video-channel/video-channel.model' import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' import { VideoChannel as VideoChannelServer } from '../../../../../shared/models/videos' import { ComponentPaginationLight } from '@app/shared/rest/component-pagination.model' +import { uniq } from 'lodash-es' +import * as debug from 'debug' + +const logger = debug('peertube:subscriptions:UserSubscriptionService') type SubscriptionExistResult = { [ uri: string ]: boolean } +type SubscriptionExistResultObservable = { [ uri: string ]: Observable } @Injectable() export class UserSubscriptionService { static BASE_USER_SUBSCRIPTIONS_URL = environment.apiUrl + '/api/v1/users/me/subscriptions' // Use a replay subject because we "next" a value before subscribing - private existsSubject: Subject = new ReplaySubject(1) + private existsSubject = new ReplaySubject(1) private readonly existsObservable: Observable + private myAccountSubscriptionCache: SubscriptionExistResult = {} + private myAccountSubscriptionCacheObservable: SubscriptionExistResultObservable = {} + private myAccountSubscriptionCacheSubject = new Subject() + constructor ( private authHttp: HttpClient, private restExtractor: RestExtractor, private restService: RestService ) { - this.existsObservable = this.existsSubject.pipe( - bufferTime(500), - filter(uris => uris.length !== 0), - switchMap(uris => this.doSubscriptionsExist(uris)), - share() + this.existsObservable = merge( + this.existsSubject.pipe( + bufferTime(500), + filter(uris => uris.length !== 0), + map(uris => uniq(uris)), + switchMap(uris => this.doSubscriptionsExist(uris)), + share() + ), + + this.myAccountSubscriptionCacheSubject ) } + /** + * Subscription part + */ + deleteSubscription (nameWithHost: string) { const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL + '/' + nameWithHost return this.authHttp.delete(url) .pipe( map(this.restExtractor.extractDataBool), + tap(() => { + this.myAccountSubscriptionCache[nameWithHost] = false + + this.myAccountSubscriptionCacheSubject.next(this.myAccountSubscriptionCache) + }), catchError(err => this.restExtractor.handleError(err)) ) } @@ -50,6 +73,11 @@ export class UserSubscriptionService { return this.authHttp.post(url, body) .pipe( map(this.restExtractor.extractDataBool), + tap(() => { + this.myAccountSubscriptionCache[nameWithHost] = true + + this.myAccountSubscriptionCacheSubject.next(this.myAccountSubscriptionCache) + }), catchError(err => this.restExtractor.handleError(err)) ) } @@ -69,10 +97,46 @@ export class UserSubscriptionService { ) } + /** + * SubscriptionExist part + */ + + listenToMyAccountSubscriptionCacheSubject () { + return this.myAccountSubscriptionCacheSubject.asObservable() + } + + listenToSubscriptionCacheChange (nameWithHost: string) { + if (nameWithHost in this.myAccountSubscriptionCacheObservable) { + return this.myAccountSubscriptionCacheObservable[ nameWithHost ] + } + + const obs = this.existsObservable + .pipe( + filter(existsResult => existsResult[ nameWithHost ] !== undefined), + map(existsResult => existsResult[ nameWithHost ]) + ) + + this.myAccountSubscriptionCacheObservable[ nameWithHost ] = obs + return obs + } + doesSubscriptionExist (nameWithHost: string) { + logger('Running subscription check for %d.', nameWithHost) + + if (nameWithHost in this.myAccountSubscriptionCache) { + logger('Found cache for %d.', nameWithHost) + + return of(this.myAccountSubscriptionCache[ nameWithHost ]) + } + this.existsSubject.next(nameWithHost) - return this.existsObservable.pipe(first()) + logger('Fetching from network for %d.', nameWithHost) + return this.existsObservable.pipe( + filter(existsResult => existsResult[ nameWithHost ] !== undefined), + map(existsResult => existsResult[ nameWithHost ]), + tap(result => this.myAccountSubscriptionCache[ nameWithHost ] = result) + ) } private doSubscriptionsExist (uris: string[]): Observable { @@ -82,6 +146,14 @@ export class UserSubscriptionService { params = this.restService.addObjectParams(params, { uris }) return this.authHttp.get(url, { params }) - .pipe(catchError(err => this.restExtractor.handleError(err))) + .pipe( + tap(res => { + this.myAccountSubscriptionCache = { + ...this.myAccountSubscriptionCache, + ...res + } + }), + catchError(err => this.restExtractor.handleError(err)) + ) } } diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html index a31165a41..ce977b3e6 100644 --- a/client/src/app/shared/video/video-miniature.component.html +++ b/client/src/app/shared/video/video-miniature.component.html @@ -24,11 +24,7 @@ • - {{ video.views | myNumberFormatter }} - - view - views - + {video.views, plural, =1 {1 view} other {{{ video.views | myNumberFormatter }} views}} -- 2.25.1