From 93cae47925e4dd68b7d34a41927b2740b4fab1b4 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 22 Jul 2019 15:40:13 +0200 Subject: [PATCH] Add client hooks --- .../account-video-channels.component.ts | 2 +- .../account-videos.component.ts | 4 +- ...count-video-playlist-elements.component.ts | 6 +- .../video-channel-videos.component.ts | 4 +- client/src/app/app.component.ts | 6 +- client/src/app/core/core.module.ts | 2 + client/src/app/core/plugins/hooks.service.ts | 44 ++++++++++++++ client/src/app/core/plugins/plugin.service.ts | 58 ++++++++++--------- client/src/app/search/search.component.ts | 46 ++++++++++++--- client/src/app/search/search.service.ts | 14 +++-- .../app/shared/overview/overview.service.ts | 2 +- .../app/shared/video/abstract-video-list.ts | 13 ++--- client/src/app/shared/video/video.service.ts | 23 ++++---- .../video/videos-selection.component.ts | 3 +- .../+video-edit/video-update.resolver.ts | 2 +- .../comment/video-comment.service.ts | 12 +++- .../comment/video-comments.component.ts | 55 +++++++++++++----- .../video-watch-playlist.component.ts | 8 +-- .../+video-watch/video-watch.component.ts | 16 ++++- .../recent-videos-recommendation.service.ts | 26 +++++---- .../video-list/video-local.component.ts | 17 ++++-- .../video-recently-added.component.ts | 18 ++++-- .../video-list/video-trending.component.ts | 17 ++++-- .../video-user-subscriptions.component.ts | 16 ++++- client/tsconfig.json | 1 + shared/core-utils/plugins/hooks.ts | 2 +- shared/models/plugins/client-hook.model.ts | 39 +++++++++++++ .../plugins/plugin-client-scope.type.ts | 1 + shared/models/plugins/plugin-scope.type.ts | 1 - shared/models/plugins/server-hook.model.ts | 2 +- shared/models/server/server-config.model.ts | 3 +- 31 files changed, 339 insertions(+), 124 deletions(-) create mode 100644 client/src/app/core/plugins/hooks.service.ts create mode 100644 shared/models/plugins/client-hook.model.ts create mode 100644 shared/models/plugins/plugin-client-scope.type.ts delete mode 100644 shared/models/plugins/plugin-scope.type.ts diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts index a8d4237e8..4d07d653f 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts @@ -68,7 +68,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { switchMap(res => from(res.data)), concatMap(videoChannel => { return this.videoService.getVideoChannelVideos(videoChannel, this.videosPagination, this.videosSort) - .pipe(map(data => ({ videoChannel, videos: data.videos }))) + .pipe(map(data => ({ videoChannel, videos: data.data }))) }) ) .subscribe(({ videoChannel, videos }) => { diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts index 5a99aadce..ac4477c18 100644 --- a/client/src/app/+accounts/account-videos/account-videos.component.ts +++ b/client/src/app/+accounts/account-videos/account-videos.component.ts @@ -69,8 +69,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, return this.videoService .getAccountVideos(this.account, newPagination, this.sort) .pipe( - tap(({ totalVideos }) => { - this.titlePage = this.i18n('Published {{totalVideos}} videos', { totalVideos }) + tap(({ total }) => { + this.titlePage = this.i18n('Published {{total}} videos', { total }) }) ) } diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts index 03f34412c..d5122aeba 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts @@ -131,9 +131,9 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro private loadElements () { this.videoService.getPlaylistVideos(this.videoPlaylistId, this.pagination) - .subscribe(({ totalVideos, videos }) => { - this.videos = this.videos.concat(videos) - this.pagination.totalItems = totalVideos + .subscribe(({ total, data }) => { + this.videos = this.videos.concat(data) + this.pagination.totalItems = total }) } diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts index 629fd4450..c1dc25aaf 100644 --- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts +++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts @@ -71,8 +71,8 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On return this.videoService .getVideoChannelVideos(this.videoChannel, newPagination, this.sort) .pipe( - tap(({ totalVideos }) => { - this.titlePage = this.i18n('Published {{totalVideos}} videos', { totalVideos }) + tap(({ total }) => { + this.titlePage = this.i18n('Published {{total}} videos', { total }) }) ) } diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 0ebd628fc..bde97c68b 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -10,6 +10,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill' import { fromEvent } from 'rxjs' import { ViewportScroller } from '@angular/common' import { PluginService } from '@app/core/plugins/plugin.service' +import { HooksService } from '@app/core/plugins/hooks.service' @Component({ selector: 'my-app', @@ -33,7 +34,8 @@ export class AppComponent implements OnInit { private redirectService: RedirectService, private screenService: ScreenService, private hotkeysService: HotkeysService, - private themeService: ThemeService + private themeService: ThemeService, + private hooks: HooksService ) { } get serverVersion () { @@ -206,7 +208,7 @@ export class AppComponent implements OnInit { await this.pluginService.loadPluginsByScope('common') - this.pluginService.runHook('action:application.loaded') + this.hooks.runAction('action:application.init') } private initHotkeys () { diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts index 436c0dfb8..5943af4da 100644 --- a/client/src/app/core/core.module.ts +++ b/client/src/app/core/core.module.ts @@ -22,6 +22,7 @@ import { UserNotificationSocket } from '@app/core/notification/user-notification import { ServerConfigResolver } from './routing/server-config-resolver.service' import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service' import { PluginService } from '@app/core/plugins/plugin.service' +import { HooksService } from '@app/core/plugins/hooks.service' @NgModule({ imports: [ @@ -63,6 +64,7 @@ import { PluginService } from '@app/core/plugins/plugin.service' UnloggedGuard, PluginService, + HooksService, RedirectService, Notifier, diff --git a/client/src/app/core/plugins/hooks.service.ts b/client/src/app/core/plugins/hooks.service.ts new file mode 100644 index 000000000..80c57869c --- /dev/null +++ b/client/src/app/core/plugins/hooks.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@angular/core' +import { PluginService } from '@app/core/plugins/plugin.service' +import { ClientActionHookName, ClientFilterHookName } from '@shared/models/plugins/client-hook.model' +import { from, Observable } from 'rxjs' +import { mergeMap, switchMap } from 'rxjs/operators' +import { ServerService } from '@app/core/server' +import { PluginClientScope } from '@shared/models/plugins/plugin-client-scope.type' + +type RawFunction = (params: U) => T +type ObservableFunction = RawFunction> + +@Injectable() +export class HooksService { + constructor ( + private server: ServerService, + private pluginService: PluginService + ) { } + + wrapObject (result: T, hookName: U) { + return this.pluginService.runHook(hookName, result) + } + + wrapObsFun + + (fun: ObservableFunction, params: P, scope: PluginClientScope, hookParamName: H1, hookResultName: H2) { + return from(this.pluginService.ensurePluginsAreLoaded(scope)) + .pipe( + mergeMap(() => this.wrapObject(params, hookParamName)), + switchMap(params => fun(params)), + mergeMap(result => this.pluginService.runHook(hookResultName, result, params)) + ) + } + + async wrapFun (fun: RawFunction, params: U, hookName: V) { + const result = fun(params) + + return this.pluginService.runHook(hookName, result, params) + } + + runAction (hookName: U, params?: T) { + this.pluginService.runHook(hookName, params) + .catch((err: any) => console.error('Fatal hook error.', { err })) + } +} diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts index af330c2eb..14310f093 100644 --- a/client/src/app/core/plugins/plugin.service.ts +++ b/client/src/app/core/plugins/plugin.service.ts @@ -3,11 +3,13 @@ import { Router } from '@angular/router' import { ServerConfigPlugin } from '@shared/models' import { ServerService } from '@app/core/server/server.service' import { ClientScript } from '@shared/models/plugins/plugin-package-json.model' -import { PluginScope } from '@shared/models/plugins/plugin-scope.type' import { environment } from '../../../environments/environment' import { RegisterHookOptions } from '@shared/models/plugins/register-hook.model' import { ReplaySubject } from 'rxjs' import { first, shareReplay } from 'rxjs/operators' +import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' +import { ClientHook, ClientHookName } from '@shared/models/plugins/client-hook.model' +import { PluginClientScope } from '@shared/models/plugins/plugin-client-scope.type' interface HookStructValue extends RegisterHookOptions { plugin: ServerConfigPlugin @@ -21,14 +23,18 @@ type PluginInfo = { } @Injectable() -export class PluginService { - pluginsLoaded = new ReplaySubject(1) +export class PluginService implements ClientHook { + pluginsBuilt = new ReplaySubject(1) + + pluginsLoaded: { [ scope in PluginClientScope ]: ReplaySubject } = { + common: new ReplaySubject(1), + 'video-watch': new ReplaySubject(1) + } private plugins: ServerConfigPlugin[] = [] private scopes: { [ scopeName: string ]: PluginInfo[] } = {} - private loadedPlugins: { [ name: string ]: boolean } = {} private loadedScripts: { [ script: string ]: boolean } = {} - private loadedScopes: PluginScope[] = [] + private loadedScopes: PluginClientScope[] = [] private hooks: { [ name: string ]: HookStructValue[] } = {} @@ -45,12 +51,18 @@ export class PluginService { this.buildScopeStruct() - this.pluginsLoaded.next(true) + this.pluginsBuilt.next(true) }) } - ensurePluginsAreLoaded () { - return this.pluginsLoaded.asObservable() + ensurePluginsAreBuilt () { + return this.pluginsBuilt.asObservable() + .pipe(first(), shareReplay()) + .toPromise() + } + + ensurePluginsAreLoaded (scope: PluginClientScope) { + return this.pluginsLoaded[scope].asObservable() .pipe(first(), shareReplay()) .toPromise() } @@ -90,9 +102,9 @@ export class PluginService { } } - async loadPluginsByScope (scope: PluginScope, isReload = false) { + async loadPluginsByScope (scope: PluginClientScope, isReload = false) { try { - await this.ensurePluginsAreLoaded() + await this.ensurePluginsAreBuilt() if (!isReload) this.loadedScopes.push(scope) @@ -111,32 +123,24 @@ export class PluginService { } await Promise.all(promises) + + this.pluginsLoaded[scope].next(true) } catch (err) { console.error('Cannot load plugins by scope %s.', scope, err) } } - async runHook (hookName: string, param?: any) { - let result = param + async runHook (hookName: ClientHookName, result?: T, params?: any): Promise { + if (!this.hooks[hookName]) return Promise.resolve(result) - if (!this.hooks[hookName]) return result - - const wait = hookName.startsWith('static:') + const hookType = getHookType(hookName) for (const hook of this.hooks[hookName]) { - try { - const p = hook.handler(param) + console.log('Running hook %s of plugin %s.', hookName, hook.plugin.name) - if (wait) { - result = await p - } else if (p.catch) { - p.catch((err: Error) => { - console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.plugin, hook.clientScript, err) - }) - } - } catch (err) { - console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.plugin, hook.clientScript, err) - } + result = await internalRunHook(hook.handler, hookType, result, params, err => { + console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.clientScript.script, hook.plugin.name, err) + }) } return result diff --git a/client/src/app/search/search.component.ts b/client/src/app/search/search.component.ts index a7ddbe1f8..b1d732d68 100644 --- a/client/src/app/search/search.component.ts +++ b/client/src/app/search/search.component.ts @@ -10,6 +10,7 @@ import { AdvancedSearch } from '@app/search/advanced-search.model' import { VideoChannel } from '@app/shared/video-channel/video-channel.model' import { immutableAssign } from '@app/shared/misc/utils' import { Video } from '@app/shared/video/video.model' +import { HooksService } from '@app/core/plugins/hooks.service' @Component({ selector: 'my-search', @@ -41,7 +42,8 @@ export class SearchComponent implements OnInit, OnDestroy { private metaService: MetaService, private notifier: Notifier, private searchService: SearchService, - private authService: AuthService + private authService: AuthService, + private hooks: HooksService ) { } get user () { @@ -93,18 +95,18 @@ export class SearchComponent implements OnInit, OnDestroy { search () { forkJoin([ - this.searchService.searchVideos(this.currentSearch, this.pagination, this.advancedSearch), - this.searchService.searchVideoChannels(this.currentSearch, immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage })) + this.getVideosObs(), + this.getVideoChannelObs() ]) .subscribe( ([ videosResult, videoChannelsResult ]) => { this.results = this.results .concat(videoChannelsResult.data) - .concat(videosResult.videos) - this.pagination.totalItems = videosResult.totalVideos + videoChannelsResult.total + .concat(videosResult.data) + this.pagination.totalItems = videosResult.total + videoChannelsResult.total // Focus on channels if there are no enough videos - if (this.firstSearch === true && videosResult.videos.length < this.pagination.itemsPerPage) { + if (this.firstSearch === true && videosResult.data.length < this.pagination.itemsPerPage) { this.resetPagination() this.firstSearch = false @@ -117,7 +119,6 @@ export class SearchComponent implements OnInit, OnDestroy { err => this.notifier.error(err.message) ) - } onNearOfBottom () { @@ -163,4 +164,35 @@ export class SearchComponent implements OnInit, OnDestroy { queryParams: Object.assign({}, this.advancedSearch.toUrlObject(), { search }) }) } + + private getVideosObs () { + const params = { + search: this.currentSearch, + componentPagination: this.pagination, + advancedSearch: this.advancedSearch + } + + return this.hooks.wrapObsFun( + this.searchService.searchVideos.bind(this.searchService), + params, + 'common', + 'filter:api.search.videos.list.params', + 'filter:api.search.videos.list.result' + ) + } + + private getVideoChannelObs () { + const params = { + search: this.currentSearch, + componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }) + } + + return this.hooks.wrapObsFun( + this.searchService.searchVideoChannels.bind(this.searchService), + params, + 'common', + 'filter:api.search.video-channels.list.params', + 'filter:api.search.video-channels.list.result' + ) + } } diff --git a/client/src/app/search/search.service.ts b/client/src/app/search/search.service.ts index cd3bdad35..8f137a321 100644 --- a/client/src/app/search/search.service.ts +++ b/client/src/app/search/search.service.ts @@ -23,13 +23,14 @@ export class SearchService { private videoService: VideoService ) {} - searchVideos ( + searchVideos (parameters: { search: string, componentPagination: ComponentPagination, advancedSearch: AdvancedSearch - ): Observable<{ videos: Video[], totalVideos: number }> { - const url = SearchService.BASE_SEARCH_URL + 'videos' + }): Observable> { + const { search, componentPagination, advancedSearch } = parameters + const url = SearchService.BASE_SEARCH_URL + 'videos' const pagination = this.restService.componentPaginationToRestPagination(componentPagination) let params = new HttpParams() @@ -48,12 +49,13 @@ export class SearchService { ) } - searchVideoChannels ( + searchVideoChannels (parameters: { search: string, componentPagination: ComponentPagination - ): Observable<{ data: VideoChannel[], total: number }> { - const url = SearchService.BASE_SEARCH_URL + 'video-channels' + }): Observable> { + const { search, componentPagination } = parameters + const url = SearchService.BASE_SEARCH_URL + 'video-channels' const pagination = this.restService.componentPaginationToRestPagination(componentPagination) let params = new HttpParams() diff --git a/client/src/app/shared/overview/overview.service.ts b/client/src/app/shared/overview/overview.service.ts index 98dba2d97..bd4068925 100644 --- a/client/src/app/shared/overview/overview.service.ts +++ b/client/src/app/shared/overview/overview.service.ts @@ -45,7 +45,7 @@ export class OverviewService { of(object.videos) .pipe( switchMap(videos => this.videosService.extractVideos({ total: 0, data: videos })), - map(result => result.videos), + map(result => result.data), tap(videos => { videosOverviewResult[key].push(immutableAssign(object, { videos })) }) diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts index cf4b5ef8e..8a247a9af 100644 --- a/client/src/app/shared/video/abstract-video-list.ts +++ b/client/src/app/shared/video/abstract-video-list.ts @@ -13,6 +13,7 @@ import { Notifier, ServerService } from '@app/core' import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' import { I18n } from '@ngx-translate/i18n-polyfill' import { isLastMonth, isLastWeek, isToday, isYesterday } from '@shared/core-utils/miscs/date' +import { ResultList } from '@shared/models' enum GroupDate { UNKNOWN = 0, @@ -73,7 +74,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor private groupedDateLabels: { [id in GroupDate]: string } private groupedDates: { [id: number]: GroupDate } = {} - abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number }> + abstract getVideosObservable (page: number): Observable> abstract generateSyndicationList (): void @@ -138,12 +139,10 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor } loadMoreVideos () { - const observable = this.getVideosObservable(this.pagination.currentPage) - - observable.subscribe( - ({ videos, totalVideos }) => { - this.pagination.totalItems = totalVideos - this.videos = this.videos.concat(videos) + this.getVideosObservable(this.pagination.currentPage).subscribe( + ({ data, total }) => { + this.pagination.totalItems = total + this.videos = this.videos.concat(data) if (this.groupByDate) this.buildGroupedDateLabels() diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index 871bc9e46..d1af13c93 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts @@ -41,7 +41,7 @@ export interface VideosProvider { filter?: VideoFilter, categoryOneOf?: number, languageOneOf?: string[] - }): Observable<{ videos: Video[], totalVideos: number }> + }): Observable> } @Injectable() @@ -65,11 +65,11 @@ export class VideoService implements VideosProvider { return VideoService.BASE_VIDEO_URL + uuid + '/watching' } - getVideo (uuid: string): Observable { + getVideo (options: { videoId: string }): Observable { return this.serverService.localeObservable .pipe( switchMap(translations => { - return this.authHttp.get(VideoService.BASE_VIDEO_URL + uuid) + return this.authHttp.get(VideoService.BASE_VIDEO_URL + options.videoId) .pipe(map(videoHash => ({ videoHash, translations }))) }), map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)), @@ -123,7 +123,7 @@ export class VideoService implements VideosProvider { .pipe(catchError(err => this.restExtractor.handleError(err))) } - getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField): Observable<{ videos: Video[], totalVideos: number }> { + getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField): Observable> { const pagination = this.restService.componentPaginationToRestPagination(videoPagination) let params = new HttpParams() @@ -141,7 +141,7 @@ export class VideoService implements VideosProvider { account: Account, videoPagination: ComponentPagination, sort: VideoSortField - ): Observable<{ videos: Video[], totalVideos: number }> { + ): Observable> { const pagination = this.restService.componentPaginationToRestPagination(videoPagination) let params = new HttpParams() @@ -159,7 +159,7 @@ export class VideoService implements VideosProvider { videoChannel: VideoChannel, videoPagination: ComponentPagination, sort: VideoSortField - ): Observable<{ videos: Video[], totalVideos: number }> { + ): Observable> { const pagination = this.restService.componentPaginationToRestPagination(videoPagination) let params = new HttpParams() @@ -176,7 +176,7 @@ export class VideoService implements VideosProvider { getPlaylistVideos ( videoPlaylistId: number | string, videoPagination: ComponentPagination - ): Observable<{ videos: Video[], totalVideos: number }> { + ): Observable> { const pagination = this.restService.componentPaginationToRestPagination(videoPagination) let params = new HttpParams() @@ -190,10 +190,11 @@ export class VideoService implements VideosProvider { ) } - getUserSubscriptionVideos ( + getUserSubscriptionVideos (parameters: { videoPagination: ComponentPagination, sort: VideoSortField - ): Observable<{ videos: Video[], totalVideos: number }> { + }): Observable> { + const { videoPagination, sort } = parameters const pagination = this.restService.componentPaginationToRestPagination(videoPagination) let params = new HttpParams() @@ -213,7 +214,7 @@ export class VideoService implements VideosProvider { filter?: VideoFilter, categoryOneOf?: number, languageOneOf?: string[] - }): Observable<{ videos: Video[], totalVideos: number }> { + }): Observable> { const { videoPagination, sort, filter, categoryOneOf, languageOneOf } = parameters const pagination = this.restService.componentPaginationToRestPagination(videoPagination) @@ -344,7 +345,7 @@ export class VideoService implements VideosProvider { videos.push(new Video(videoJson, translations)) } - return { videos, totalVideos } + return { total: totalVideos, data: videos } }) ) } diff --git a/client/src/app/shared/video/videos-selection.component.ts b/client/src/app/shared/video/videos-selection.component.ts index d69f7b70e..994e0fa1e 100644 --- a/client/src/app/shared/video/videos-selection.component.ts +++ b/client/src/app/shared/video/videos-selection.component.ts @@ -21,6 +21,7 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template import { VideoSortField } from '@app/shared/video/sort-field.type' import { ComponentPagination } from '@app/shared/rest/component-pagination.model' import { I18n } from '@ngx-translate/i18n-polyfill' +import { ResultList } from '@shared/models' export type SelectionType = { [ id: number ]: boolean } @@ -33,7 +34,7 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni @Input() pagination: ComponentPagination @Input() titlePage: string @Input() miniatureDisplayOptions: MiniatureDisplayOptions - @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<{ videos: Video[], totalVideos: number }> + @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable> @ContentChildren(PeerTubeTemplateDirective) templates: QueryList @Output() selectionChange = new EventEmitter() diff --git a/client/src/app/videos/+video-edit/video-update.resolver.ts b/client/src/app/videos/+video-edit/video-update.resolver.ts index 384458127..4ac517d96 100644 --- a/client/src/app/videos/+video-edit/video-update.resolver.ts +++ b/client/src/app/videos/+video-edit/video-update.resolver.ts @@ -18,7 +18,7 @@ export class VideoUpdateResolver implements Resolve { resolve (route: ActivatedRouteSnapshot) { const uuid: string = route.params[ 'uuid' ] - return this.videoService.getVideo(uuid) + return this.videoService.getVideo({ videoId: uuid }) .pipe( switchMap(video => { return forkJoin([ diff --git a/client/src/app/videos/+video-watch/comment/video-comment.service.ts b/client/src/app/videos/+video-watch/comment/video-comment.service.ts index b8e5878c5..eb608a1a3 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment.service.ts +++ b/client/src/app/videos/+video-watch/comment/video-comment.service.ts @@ -48,11 +48,13 @@ export class VideoCommentService { ) } - getVideoCommentThreads ( + getVideoCommentThreads (parameters: { videoId: number | string, componentPagination: ComponentPagination, sort: VideoSortField - ): Observable<{ comments: VideoComment[], totalComments: number}> { + }): Observable<{ comments: VideoComment[], totalComments: number}> { + const { videoId, componentPagination, sort } = parameters + const pagination = this.restService.componentPaginationToRestPagination(componentPagination) let params = new HttpParams() @@ -67,7 +69,11 @@ export class VideoCommentService { ) } - getVideoThreadComments (videoId: number | string, threadId: number): Observable { + getVideoThreadComments (parameters: { + videoId: number | string, + threadId: number + }): Observable { + const { videoId, threadId } = parameters const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}` return this.authHttp diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.ts b/client/src/app/videos/+video-watch/comment/video-comments.component.ts index 3acddbe6a..3c1a0986c 100644 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.ts +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.ts @@ -12,6 +12,7 @@ import { VideoComment } from './video-comment.model' import { VideoCommentService } from './video-comment.service' import { I18n } from '@ngx-translate/i18n-polyfill' import { Syndication } from '@app/shared/video/syndication.model' +import { HooksService } from '@app/core/plugins/hooks.service' @Component({ selector: 'my-video-comments', @@ -45,7 +46,8 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { private confirmService: ConfirmService, private videoCommentService: VideoCommentService, private activatedRoute: ActivatedRoute, - private i18n: I18n + private i18n: I18n, + private hooks: HooksService ) {} ngOnInit () { @@ -73,8 +75,20 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { viewReplies (commentId: number, highlightThread = false) { this.threadLoading[commentId] = true - this.videoCommentService.getVideoThreadComments(this.video.id, commentId) - .subscribe( + const params = { + videoId: this.video.id, + threadId: commentId + } + + const obs = this.hooks.wrapObsFun( + this.videoCommentService.getVideoThreadComments.bind(this.videoCommentService), + params, + 'video-watch', + 'filter:api.video-watch.video-thread-replies.list.params', + 'filter:api.video-watch.video-thread-replies.list.result' + ) + + obs.subscribe( res => { this.threadComments[commentId] = res this.threadLoading[commentId] = false @@ -91,16 +105,29 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { ) } - loadMoreComments () { - this.videoCommentService.getVideoCommentThreads(this.video.id, this.componentPagination, this.sort) - .subscribe( - res => { - this.comments = this.comments.concat(res.comments) - this.componentPagination.totalItems = res.totalComments - }, + loadMoreThreads () { + const params = { + videoId: this.video.id, + componentPagination: this.componentPagination, + sort: this.sort + } - err => this.notifier.error(err.message) - ) + const obs = this.hooks.wrapObsFun( + this.videoCommentService.getVideoCommentThreads.bind(this.videoCommentService), + params, + 'video-watch', + 'filter:api.video-watch.video-threads.list.params', + 'filter:api.video-watch.video-threads.list.result' + ) + + obs.subscribe( + res => { + this.comments = this.comments.concat(res.comments) + this.componentPagination.totalItems = res.totalComments + }, + + err => this.notifier.error(err.message) + ) } onCommentThreadCreated (comment: VideoComment) { @@ -169,7 +196,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { this.componentPagination.currentPage++ if (hasMoreItems(this.componentPagination)) { - this.loadMoreComments() + this.loadMoreThreads() } } @@ -197,7 +224,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { this.syndicationItems = this.videoCommentService.getVideoCommentsFeeds(this.video.uuid) - this.loadMoreComments() + this.loadMoreThreads() } } diff --git a/client/src/app/videos/+video-watch/video-watch-playlist.component.ts b/client/src/app/videos/+video-watch/video-watch-playlist.component.ts index bccdaf7b2..2fb0cb0e5 100644 --- a/client/src/app/videos/+video-watch/video-watch-playlist.component.ts +++ b/client/src/app/videos/+video-watch/video-watch-playlist.component.ts @@ -66,11 +66,11 @@ export class VideoWatchPlaylistComponent { loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false) { this.videoService.getPlaylistVideos(playlist.uuid, this.playlistPagination) - .subscribe(({ totalVideos, videos }) => { - this.playlistVideos = this.playlistVideos.concat(videos) - this.playlistPagination.totalItems = totalVideos + .subscribe(({ total, data }) => { + this.playlistVideos = this.playlistVideos.concat(data) + this.playlistPagination.totalItems = total - if (totalVideos === 0) { + if (total === 0) { this.noPlaylistVideos = true return } diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index 6d8bb4b3f..eed2ec048 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts @@ -33,6 +33,7 @@ import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils' import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component' import { getStoredTheater } from '../../../assets/player/peertube-player-local-storage' import { PluginService } from '@app/core/plugins/plugin.service' +import { HooksService } from '@app/core/plugins/hooks.service' @Component({ selector: 'my-video-watch', @@ -93,6 +94,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { private videoCaptionService: VideoCaptionService, private i18n: I18n, private hotkeysService: HotkeysService, + private hooks: HooksService, @Inject(LOCALE_ID) private localeId: string ) {} @@ -131,7 +133,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { this.theaterEnabled = getStoredTheater() - this.pluginService.runHook('action:video-watch.loaded') + this.hooks.runAction('action:video-watch.init') } ngOnDestroy () { @@ -246,9 +248,17 @@ export class VideoWatchComponent implements OnInit, OnDestroy { if (this.player) this.player.pause() + const videoObs = this.hooks.wrapObsFun( + this.videoService.getVideo.bind(this.videoService), + { videoId }, + 'video-watch', + 'filter:api.video-watch.video.get.params', + 'filter:api.video-watch.video.get.result' + ) + // Video did change forkJoin( - this.videoService.getVideo(videoId), + videoObs, this.videoCaptionService.listCaptions(videoId) ) .pipe( @@ -486,6 +496,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { this.setOpenGraphTags() this.checkUserRating() + + this.hooks.runAction('action:video-watch.video.loaded') } private setRating (nextRating: UserVideoRateType) { diff --git a/client/src/app/videos/recommendations/recent-videos-recommendation.service.ts b/client/src/app/videos/recommendations/recent-videos-recommendation.service.ts index f975ff6ef..a1e65c27c 100644 --- a/client/src/app/videos/recommendations/recent-videos-recommendation.service.ts +++ b/client/src/app/videos/recommendations/recent-videos-recommendation.service.ts @@ -33,20 +33,24 @@ export class RecentVideosRecommendationService implements RecommendationService private fetchPage (page: number, recommendation: RecommendationInfo): Observable { const pagination = { currentPage: page, itemsPerPage: this.pageSize + 1 } const defaultSubscription = this.videos.getVideos({ videoPagination: pagination, sort: '-createdAt' }) - .pipe(map(v => v.videos)) + .pipe(map(v => v.data)) if (!recommendation.tags || recommendation.tags.length === 0) return defaultSubscription - return this.searchService.searchVideos('', - pagination, - new AdvancedSearch({ tagsOneOf: recommendation.tags.join(','), sort: '-createdAt' }) - ).pipe( - map(v => v.videos), - switchMap(videos => { - if (videos.length <= 1) return defaultSubscription + const params = { + search: '', + componentPagination: pagination, + advancedSearch: new AdvancedSearch({ tagsOneOf: recommendation.tags.join(','), sort: '-createdAt' }) + } - return of(videos) - }) - ) + return this.searchService.searchVideos(params) + .pipe( + map(v => v.data), + switchMap(videos => { + if (videos.length <= 1) return defaultSubscription + + return of(videos) + }) + ) } } diff --git a/client/src/app/videos/video-list/video-local.component.ts b/client/src/app/videos/video-list/video-local.component.ts index 5de4a13af..81b6ce493 100644 --- a/client/src/app/videos/video-list/video-local.component.ts +++ b/client/src/app/videos/video-list/video-local.component.ts @@ -10,6 +10,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill' import { ScreenService } from '@app/shared/misc/screen.service' import { UserRight } from '../../../../../shared/models/users' import { Notifier, ServerService } from '@app/core' +import { HooksService } from '@app/core/plugins/hooks.service' @Component({ selector: 'my-videos-local', @@ -31,7 +32,8 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On protected notifier: Notifier, protected authService: AuthService, protected screenService: ScreenService, - private videoService: VideoService + private videoService: VideoService, + private hooks: HooksService ) { super() @@ -55,14 +57,21 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On getVideosObservable (page: number) { const newPagination = immutableAssign(this.pagination, { currentPage: page }) - - return this.videoService.getVideos({ + const params = { videoPagination: newPagination, sort: this.sort, filter: this.filter, categoryOneOf: this.categoryOneOf, languageOneOf: this.languageOneOf - }) + } + + return this.hooks.wrapObsFun( + this.videoService.getVideos.bind(this.videoService), + params, + 'common', + 'filter:api.videos.list.local.params', + 'filter:api.videos.list.local.result' + ) } generateSyndicationList () { diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts index 19522e6b4..638e7caed 100644 --- a/client/src/app/videos/video-list/video-recently-added.component.ts +++ b/client/src/app/videos/video-list/video-recently-added.component.ts @@ -8,6 +8,7 @@ import { VideoService } from '../../shared/video/video.service' import { I18n } from '@ngx-translate/i18n-polyfill' import { ScreenService } from '@app/shared/misc/screen.service' import { Notifier, ServerService } from '@app/core' +import { HooksService } from '@app/core/plugins/hooks.service' @Component({ selector: 'my-videos-recently-added', @@ -29,7 +30,8 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On protected notifier: Notifier, protected authService: AuthService, protected screenService: ScreenService, - private videoService: VideoService + private videoService: VideoService, + private hooks: HooksService ) { super() @@ -48,14 +50,20 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On getVideosObservable (page: number) { const newPagination = immutableAssign(this.pagination, { currentPage: page }) - - return this.videoService.getVideos({ + const params = { videoPagination: newPagination, sort: this.sort, - filter: undefined, categoryOneOf: this.categoryOneOf, languageOneOf: this.languageOneOf - }) + } + + return this.hooks.wrapObsFun( + this.videoService.getVideos.bind(this.videoService), + params, + 'common', + 'filter:api.videos.list.recently-added.params', + 'filter:api.videos.list.recently-added.result' + ) } generateSyndicationList () { diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts index 5f1d5055b..0e69bfd64 100644 --- a/client/src/app/videos/video-list/video-trending.component.ts +++ b/client/src/app/videos/video-list/video-trending.component.ts @@ -8,6 +8,7 @@ import { VideoService } from '../../shared/video/video.service' import { I18n } from '@ngx-translate/i18n-polyfill' import { ScreenService } from '@app/shared/misc/screen.service' import { Notifier, ServerService } from '@app/core' +import { HooksService } from '@app/core/plugins/hooks.service' @Component({ selector: 'my-videos-trending', @@ -28,7 +29,8 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit, protected notifier: Notifier, protected authService: AuthService, protected screenService: ScreenService, - private videoService: VideoService + private videoService: VideoService, + private hooks: HooksService ) { super() } @@ -61,13 +63,20 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit, getVideosObservable (page: number) { const newPagination = immutableAssign(this.pagination, { currentPage: page }) - return this.videoService.getVideos({ + const params = { videoPagination: newPagination, sort: this.sort, - filter: undefined, categoryOneOf: this.categoryOneOf, languageOneOf: this.languageOneOf - }) + } + + return this.hooks.wrapObsFun( + this.videoService.getVideos.bind(this.videoService), + params, + 'common', + 'filter:api.videos.list.trending.params', + 'filter:api.videos.list.trending.result' + ) } generateSyndicationList () { diff --git a/client/src/app/videos/video-list/video-user-subscriptions.component.ts b/client/src/app/videos/video-list/video-user-subscriptions.component.ts index 3caa371d8..ac325aeff 100644 --- a/client/src/app/videos/video-list/video-user-subscriptions.component.ts +++ b/client/src/app/videos/video-list/video-user-subscriptions.component.ts @@ -9,6 +9,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill' import { ScreenService } from '@app/shared/misc/screen.service' import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' import { Notifier, ServerService } from '@app/core' +import { HooksService } from '@app/core/plugins/hooks.service' @Component({ selector: 'my-videos-user-subscriptions', @@ -29,7 +30,8 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement protected notifier: Notifier, protected authService: AuthService, protected screenService: ScreenService, - private videoService: VideoService + private videoService: VideoService, + private hooks: HooksService ) { super() @@ -46,8 +48,18 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement getVideosObservable (page: number) { const newPagination = immutableAssign(this.pagination, { currentPage: page }) + const params = { + videoPagination: newPagination, + sort: this.sort + } - return this.videoService.getUserSubscriptionVideos(newPagination, this.sort) + return this.hooks.wrapObsFun( + this.videoService.getUserSubscriptionVideos.bind(this.videoService), + params, + 'common', + 'filter:api.videos.list.user-subscriptions.params', + 'filter:api.videos.list.user-subscriptions.result' + ) } generateSyndicationList () { diff --git a/client/tsconfig.json b/client/tsconfig.json index e46528d1c..ac9a3522d 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -12,6 +12,7 @@ "noImplicitThis": true, "suppressImplicitAnyIndexErrors":true, "alwaysStrict": true, + "strictBindCallApply": true, "target": "es5", "typeRoots": [ "node_modules/@types" diff --git a/shared/core-utils/plugins/hooks.ts b/shared/core-utils/plugins/hooks.ts index 3d59a7428..5405e0529 100644 --- a/shared/core-utils/plugins/hooks.ts +++ b/shared/core-utils/plugins/hooks.ts @@ -29,7 +29,7 @@ async function internalRunHook (handler: Function, hookType: HookType, resul } if (hookType === HookType.ACTION) { - if (isCatchable(p)) p.catch(err => onError(err)) + if (isCatchable(p)) p.catch((err: any) => onError(err)) return undefined } diff --git a/shared/models/plugins/client-hook.model.ts b/shared/models/plugins/client-hook.model.ts new file mode 100644 index 000000000..89400003e --- /dev/null +++ b/shared/models/plugins/client-hook.model.ts @@ -0,0 +1,39 @@ +export type ClientFilterHookName = + 'filter:api.videos.list.trending.params' | + 'filter:api.videos.list.trending.result' | + + 'filter:api.videos.list.local.params' | + 'filter:api.videos.list.local.result' | + + 'filter:api.videos.list.recently-added.params' | + 'filter:api.videos.list.recently-added.result' | + + 'filter:api.videos.list.user-subscriptions.params' | + 'filter:api.videos.list.user-subscriptions.result' | + + 'filter:api.video-watch.video.get.params' | + 'filter:api.video-watch.video.get.result' | + + 'filter:api.video-watch.video-threads.list.params' | + 'filter:api.video-watch.video-threads.list.result' | + + 'filter:api.video-watch.video-thread-replies.list.params' | + 'filter:api.video-watch.video-thread-replies.list.result' | + + 'filter:api.search.videos.list.params' | + 'filter:api.search.videos.list.result' | + 'filter:api.search.video-channels.list.params' | + 'filter:api.search.video-channels.list.result' + +export type ClientActionHookName = + 'action:application.init' | + + 'action:video-watch.init' | + + 'action:video-watch.video.loaded' + +export type ClientHookName = ClientActionHookName | ClientFilterHookName + +export interface ClientHook { + runHook (hookName: ClientHookName, result?: T, params?: any): Promise +} diff --git a/shared/models/plugins/plugin-client-scope.type.ts b/shared/models/plugins/plugin-client-scope.type.ts new file mode 100644 index 000000000..a2112eed7 --- /dev/null +++ b/shared/models/plugins/plugin-client-scope.type.ts @@ -0,0 +1 @@ +export type PluginClientScope = 'common' | 'video-watch' diff --git a/shared/models/plugins/plugin-scope.type.ts b/shared/models/plugins/plugin-scope.type.ts deleted file mode 100644 index b63ae43ec..000000000 --- a/shared/models/plugins/plugin-scope.type.ts +++ /dev/null @@ -1 +0,0 @@ -export type PluginScope = 'common' | 'video-watch' diff --git a/shared/models/plugins/server-hook.model.ts b/shared/models/plugins/server-hook.model.ts index a7f88f3c4..6729e2dab 100644 --- a/shared/models/plugins/server-hook.model.ts +++ b/shared/models/plugins/server-hook.model.ts @@ -30,5 +30,5 @@ export type ServerActionHookName = export type ServerHookName = ServerFilterHookName | ServerActionHookName export interface ServerHook { - runHook (hookName: ServerHookName, params?: any) + runHook (hookName: ServerHookName, result?: T, params?: any): Promise } diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts index 3498f86d7..49bb01708 100644 --- a/shared/models/server/server-config.model.ts +++ b/shared/models/server/server-config.model.ts @@ -1,11 +1,12 @@ import { NSFWPolicyType } from '../videos/nsfw-policy.type' import { ClientScript } from '../plugins/plugin-package-json.model' +import { PluginClientScope } from '../plugins/plugin-scope.type' export interface ServerConfigPlugin { name: string version: string description: string - clientScripts: { [name: string]: ClientScript } + clientScripts: { [name in PluginClientScope]: ClientScript } } export interface ServerConfigTheme extends ServerConfigPlugin {