From 0aa52e170727ac6bdf441bcaa2353ae0b8a354ed Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 18 Nov 2020 15:29:38 +0100 Subject: [PATCH] Add ability to display all channel/account videos --- .../account-video-channels.component.ts | 18 +++++++++-- .../account-videos.component.ts | 20 +++++++++++- .../video-channel-videos.component.ts | 20 +++++++++++- .../video-list/video-local.component.ts | 8 ++--- .../shared/shared-main/video/video.service.ts | 27 +++++++++++++--- .../abstract-video-list.html | 2 +- .../abstract-video-list.scss | 12 +++++-- .../abstract-video-list.ts | 32 ++++++++++++++++--- client/src/sass/bootstrap.scss | 4 +++ server/helpers/custom-validators/videos.ts | 2 +- .../middlewares/validators/videos/videos.ts | 5 ++- server/models/video/video-query-builder.ts | 2 +- server/models/video/video.ts | 2 +- .../tests/api/check-params/videos-filter.ts | 29 ++++++++++------- server/tests/api/videos/videos-filter.ts | 14 ++++++++ shared/models/videos/video-query.type.ts | 2 +- support/doc/api/openapi.yaml | 3 +- 17 files changed, 161 insertions(+), 41 deletions(-) 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 205245675..f2beb6689 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 @@ -3,7 +3,7 @@ import { concatMap, map, switchMap, tap } from 'rxjs/operators' import { Component, OnDestroy, OnInit } from '@angular/core' import { ComponentPagination, hasMoreItems, ScreenService, User, UserService } from '@app/core' import { Account, AccountService, Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' -import { VideoSortField } from '@shared/models' +import { NSFWPolicyType, VideoSortField } from '@shared/models' @Component({ selector: 'my-account-video-channels', @@ -31,6 +31,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { onChannelDataSubject = new Subject() userMiniature: User + nsfwPolicy: NSFWPolicyType private accountSub: Subscription @@ -52,7 +53,11 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { }) this.userService.getAnonymousOrLoggedUser() - .subscribe(user => this.userMiniature = user) + .subscribe(user => { + this.userMiniature = user + + this.nsfwPolicy = user.nsfwPolicy + }) } ngOnDestroy () { @@ -65,7 +70,14 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { tap(res => this.channelPagination.totalItems = res.total), switchMap(res => from(res.data)), concatMap(videoChannel => { - return this.videoService.getVideoChannelVideos(videoChannel, this.videosPagination, this.videosSort) + const options = { + videoChannel, + videoPagination: this.videosPagination, + sort: this.videosSort, + nsfwPolicy: this.nsfwPolicy + } + + return this.videoService.getVideoChannelVideos(options) .pipe(map(data => ({ videoChannel, videos: data.data }))) }) ) 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 3134a8ee2..58d0719fd 100644 --- a/client/src/app/+accounts/account-videos/account-videos.component.ts +++ b/client/src/app/+accounts/account-videos/account-videos.component.ts @@ -6,6 +6,7 @@ import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenServi import { immutableAssign } from '@app/helpers' import { Account, AccountService, VideoService } from '@app/shared/shared-main' import { AbstractVideoList } from '@app/shared/shared-video-miniature' +import { VideoFilter } from '@shared/models' @Component({ selector: 'my-account-videos', @@ -18,6 +19,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, titlePage: string loadOnInit = false + filter: VideoFilter = null + private account: Account private accountSub: Subscription @@ -40,6 +43,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, ngOnInit () { super.ngOnInit() + this.enableAllFilterIfPossible() + // Parent get the account for us this.accountSub = this.accountService.accountLoaded .pipe(first()) @@ -59,9 +64,16 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, getVideosObservable (page: number) { const newPagination = immutableAssign(this.pagination, { currentPage: page }) + const options = { + account: this.account, + videoPagination: newPagination, + sort: this.sort, + nsfwPolicy: this.nsfwPolicy, + videoFilter: this.filter + } return this.videoService - .getAccountVideos(this.account, newPagination, this.sort) + .getAccountVideos(options) .pipe( tap(({ total }) => { this.titlePage = $localize`Published ${total} videos` @@ -69,6 +81,12 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, ) } + toggleModerationDisplay () { + this.filter = this.buildLocalFilter(this.filter, null) + + this.reloadVideos() + } + generateSyndicationList () { this.syndicationItems = this.videoService.getAccountFeedUrls(this.account.id) } 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 e1ec6bbcb..645696f48 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 @@ -6,6 +6,7 @@ import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenServi import { immutableAssign } from '@app/helpers' import { VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' import { AbstractVideoList } from '@app/shared/shared-video-miniature' +import { VideoFilter } from '@shared/models' @Component({ selector: 'my-video-channel-videos', @@ -18,6 +19,8 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On titlePage: string loadOnInit = false + filter: VideoFilter = null + private videoChannel: VideoChannel private videoChannelSub: Subscription @@ -46,6 +49,8 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On ngOnInit () { super.ngOnInit() + this.enableAllFilterIfPossible() + // Parent get the video channel for us this.videoChannelSub = this.videoChannelService.videoChannelLoaded .pipe(first()) @@ -65,9 +70,16 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On getVideosObservable (page: number) { const newPagination = immutableAssign(this.pagination, { currentPage: page }) + const options = { + videoChannel: this.videoChannel, + videoPagination: newPagination, + sort: this.sort, + nsfwPolicy: this.nsfwPolicy, + videoFilter: this.filter + } return this.videoService - .getVideoChannelVideos(this.videoChannel, newPagination, this.sort, this.nsfwPolicy) + .getVideoChannelVideos(options) .pipe( tap(({ total }) => { this.titlePage = total === 1 @@ -80,4 +92,10 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On generateSyndicationList () { this.syndicationItems = this.videoService.getVideoChannelFeedUrls(this.videoChannel.id) } + + toggleModerationDisplay () { + this.filter = this.buildLocalFilter(this.filter, null) + + this.reloadVideos() + } } 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 07063d4d4..20dd61db9 100644 --- a/client/src/app/+videos/video-list/video-local.component.ts +++ b/client/src/app/+videos/video-list/video-local.component.ts @@ -39,11 +39,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On ngOnInit () { super.ngOnInit() - if (this.authService.isLoggedIn()) { - const user = this.authService.getUser() - this.displayModerationBlock = user.hasRight(UserRight.SEE_ALL_VIDEOS) - } - + this.enableAllFilterIfPossible() this.generateSyndicationList() } @@ -77,7 +73,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On } toggleModerationDisplay () { - this.filter = this.filter === 'local' ? 'all-local' as 'all-local' : 'local' as 'local' + this.filter = this.buildLocalFilter(this.filter, 'local') this.reloadVideos() } diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts index 0e2d36081..c8a3ec043 100644 --- a/client/src/app/shared/shared-main/video/video.service.ts +++ b/client/src/app/shared/shared-main/video/video.service.ts @@ -134,16 +134,28 @@ export class VideoService implements VideosProvider { ) } - getAccountVideos ( + getAccountVideos (parameters: { account: Account, videoPagination: ComponentPaginationLight, sort: VideoSortField - ): Observable> { + nsfwPolicy?: NSFWPolicyType + videoFilter?: VideoFilter + }): Observable> { + const { account, videoPagination, sort, videoFilter, nsfwPolicy } = parameters + const pagination = this.restService.componentPaginationToRestPagination(videoPagination) let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination, sort) + if (nsfwPolicy) { + params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy)) + } + + if (videoFilter) { + params = params.set('filter', videoFilter) + } + return this.authHttp .get>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params }) .pipe( @@ -152,12 +164,15 @@ export class VideoService implements VideosProvider { ) } - getVideoChannelVideos ( + getVideoChannelVideos (parameters: { videoChannel: VideoChannel, videoPagination: ComponentPaginationLight, sort: VideoSortField, nsfwPolicy?: NSFWPolicyType - ): Observable> { + videoFilter?: VideoFilter + }): Observable> { + const { videoChannel, videoPagination, sort, nsfwPolicy, videoFilter } = parameters + const pagination = this.restService.componentPaginationToRestPagination(videoPagination) let params = new HttpParams() @@ -167,6 +182,10 @@ export class VideoService implements VideosProvider { params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy)) } + if (videoFilter) { + params = params.set('filter', videoFilter) + } + return this.authHttp .get>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params }) .pipe( diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.html b/client/src/app/shared/shared-video-miniature/abstract-video-list.html index 08962dff8..b1ac757db 100644 --- a/client/src/app/shared/shared-video-miniature/abstract-video-list.html +++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.html @@ -21,7 +21,7 @@ diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.scss b/client/src/app/shared/shared-video-miniature/abstract-video-list.scss index 7841b60f7..9077e2f75 100644 --- a/client/src/app/shared/shared-video-miniature/abstract-video-list.scss +++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.scss @@ -31,13 +31,21 @@ $iconSize: 16px; .moderation-block { div { - @include button-with-icon($iconSize, 3px, -1px); + @include button-with-icon($iconSize, 3px, -2px); } - margin-left: .2rem; + margin-left: .4rem; display: flex; justify-content: flex-end; align-items: center; + + .dropdown-item { + padding: 0; + + ::ng-deep my-peertube-checkbox label { + padding: 3px 15px; + } + } } } diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts index da05e15fb..2219ced30 100644 --- a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts +++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts @@ -15,7 +15,7 @@ import { import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' import { GlobalIconName } from '@app/shared/shared-icons' import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@shared/core-utils/miscs/date' -import { ServerConfig, VideoSortField } from '@shared/models' +import { ServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/models' import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type' import { Syndication, Video } from '../shared-main' import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component' @@ -205,10 +205,6 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor this.loadMoreVideos(true) } - toggleModerationDisplay () { - throw new Error('toggleModerationDisplay is not implemented') - } - removeVideoFromArray (video: Video) { this.videos = this.videos.filter(v => v.id !== video.id) } @@ -268,6 +264,10 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor return this.groupedDateLabels[this.groupedDates[video.id]] } + toggleModerationDisplay () { + throw new Error('toggleModerationDisplay is not implemented') + } + // On videos hook for children that want to do something protected onMoreVideos () { /* empty */ } @@ -277,6 +277,28 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor this.angularState = routeParams[ 'a-state' ] } + protected buildLocalFilter (existing: VideoFilter, base: VideoFilter) { + if (base === 'local') { + return existing === 'local' + ? 'all-local' as 'all-local' + : 'local' as 'local' + } + + return existing === 'all' + ? null + : 'all' + } + + protected enableAllFilterIfPossible () { + if (!this.authService.isLoggedIn()) return + + this.authService.userInformationLoaded + .subscribe(() => { + const user = this.authService.getUser() + this.displayModerationBlock = user.hasRight(UserRight.SEE_ALL_VIDEOS) + }) + } + private calcPageSizes () { if (this.screenService.isInMobileView()) { this.pagination.itemsPerPage = 5 diff --git a/client/src/sass/bootstrap.scss b/client/src/sass/bootstrap.scss index 259af7a77..7cb149f5f 100644 --- a/client/src/sass/bootstrap.scss +++ b/client/src/sass/bootstrap.scss @@ -65,6 +65,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/'; opacity: .9; } + &:active { + color: pvar(--mainForegroundColor) !important; + } + &::after { display: none; } diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index e99992236..8b309ae42 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts @@ -17,7 +17,7 @@ import * as magnetUtil from 'magnet-uri' const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS function isVideoFilterValid (filter: VideoFilter) { - return filter === 'local' || filter === 'all-local' + return filter === 'local' || filter === 'all-local' || filter === 'all' } function isVideoCategoryValid (value: any) { diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index ff90e347a..efab67a01 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts @@ -429,7 +429,10 @@ const commonVideosFiltersValidator = [ if (areValidationErrors(req, res)) return const user = res.locals.oauth ? res.locals.oauth.token.User : undefined - if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) { + if ( + (req.query.filter === 'all-local' || req.query.filter === 'all') && + (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false) + ) { res.status(401) .json({ error: 'You are not allowed to see all local videos.' }) diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts index b14bb16d6..25d5042b7 100644 --- a/server/models/video/video-query-builder.ts +++ b/server/models/video/video-query-builder.ts @@ -89,7 +89,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions) } // Only list public/published videos - if (!options.filter || options.filter !== 'all-local') { + if (!options.filter || (options.filter !== 'all-local' && options.filter !== 'all')) { and.push( `("video"."state" = ${VideoState.PUBLISHED} OR ` + `("video"."state" = ${VideoState.TO_TRANSCODE} AND "video"."waitTranscoding" IS false))` diff --git a/server/models/video/video.ts b/server/models/video/video.ts index edf757697..f365d3d51 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -1085,7 +1085,7 @@ export class VideoModel extends Model { historyOfUser?: MUserId countVideos?: boolean }) { - if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { + if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { throw new Error('Try to filter all-local but no user has not the see all videos right') } diff --git a/server/tests/api/check-params/videos-filter.ts b/server/tests/api/check-params/videos-filter.ts index ec8654db2..bf8248b0e 100644 --- a/server/tests/api/check-params/videos-filter.ts +++ b/server/tests/api/check-params/videos-filter.ts @@ -78,28 +78,33 @@ describe('Test videos filters', function () { await testEndpoints(server, server.accessToken, 'local', 200) }) - it('Should fail to list all-local with a simple user', async function () { + it('Should fail to list all-local/all with a simple user', async function () { await testEndpoints(server, userAccessToken, 'all-local', 401) + await testEndpoints(server, userAccessToken, 'all', 401) }) - it('Should succeed to list all-local with a moderator', async function () { + it('Should succeed to list all-local/all with a moderator', async function () { await testEndpoints(server, moderatorAccessToken, 'all-local', 200) + await testEndpoints(server, moderatorAccessToken, 'all', 200) }) - it('Should succeed to list all-local with an admin', async function () { + it('Should succeed to list all-local/all with an admin', async function () { await testEndpoints(server, server.accessToken, 'all-local', 200) + await testEndpoints(server, server.accessToken, 'all', 200) }) // Because we cannot authenticate the user on the RSS endpoint - it('Should fail on the feeds endpoint with the all-local filter', async function () { - await makeGetRequest({ - url: server.url, - path: '/feeds/videos.json', - statusCodeExpected: 401, - query: { - filter: 'all-local' - } - }) + it('Should fail on the feeds endpoint with the all-local/all filter', async function () { + for (const filter of [ 'all', 'all-local' ]) { + await makeGetRequest({ + url: server.url, + path: '/feeds/videos.json', + statusCodeExpected: 401, + query: { + filter + } + }) + } }) it('Should succeed on the feeds endpoint with the local filter', async function () { diff --git a/server/tests/api/videos/videos-filter.ts b/server/tests/api/videos/videos-filter.ts index 95e12e43c..6b9a4b6d4 100644 --- a/server/tests/api/videos/videos-filter.ts +++ b/server/tests/api/videos/videos-filter.ts @@ -116,6 +116,20 @@ describe('Test videos filter validator', function () { } } }) + + it('Should display all videos by the admin or the moderator', async function () { + for (const server of servers) { + for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) { + + const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames(server, token, 'all') + expect(channelVideos).to.have.lengthOf(3) + expect(accountVideos).to.have.lengthOf(3) + + expect(videos).to.have.lengthOf(5) + expect(searchVideos).to.have.lengthOf(5) + } + } + }) }) after(async function () { diff --git a/shared/models/videos/video-query.type.ts b/shared/models/videos/video-query.type.ts index f76a91aad..e641a401c 100644 --- a/shared/models/videos/video-query.type.ts +++ b/shared/models/videos/video-query.type.ts @@ -1 +1 @@ -export type VideoFilter = 'local' | 'all-local' +export type VideoFilter = 'local' | 'all-local' | 'all' diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 4a178e4d7..c16914eb2 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml @@ -3681,9 +3681,10 @@ components: in: query required: false description: > - Special filters (local for instance) which might require special rights: + Special filters which might require special rights: * `local` - only videos local to the instance * `all-local` - only videos local to the instance, but showing private and unlisted videos (requires Admin privileges) + * `all` - all videos, showing private and unlisted videos (requires Admin privileges) schema: type: string enum: -- 2.41.0