From d8b34ee55b654912f86bb8b472d391ced8c28f64 Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Wed, 13 Jan 2021 09:16:15 +0100 Subject: [PATCH] Allow user to search through their watch history (#3576) * allow user to search through their watch history * add tests for search in watch history * Update client/src/app/shared/shared-main/users/user-history.service.ts --- .../my-history/my-history.component.html | 19 ++++++++--- .../my-history/my-history.component.scss | 10 ++++-- .../my-history/my-history.component.ts | 34 +++++++++++++++++-- .../my-subscriptions.component.html | 2 +- .../shared-main/users/user-history.service.ts | 3 +- server/controllers/api/users/my-history.ts | 4 ++- server/middlewares/validators/user-history.ts | 19 +++++++++-- server/models/account/user-video-history.ts | 3 +- server/models/video/video.ts | 4 ++- server/tests/api/videos/videos-history.ts | 9 +++++ shared/extra-utils/videos/video-history.ts | 5 ++- support/doc/api/openapi.yaml | 1 + 12 files changed, 97 insertions(+), 16 deletions(-) diff --git a/client/src/app/+my-library/my-history/my-history.component.html b/client/src/app/+my-library/my-history/my-history.component.html index 58b874ebf..c180161e7 100644 --- a/client/src/app/+my-library/my-history/my-history.component.html +++ b/client/src/app/+my-library/my-history/my-history.component.html @@ -1,12 +1,23 @@

- My history + My watch history {{ pagination.totalItems }}

-
+
+
+ + + Clear filters +
+
+ +
- +
-
You don't have any video history yet.
+
You don't have any video in your watch history yet.
diff --git a/client/src/app/+my-library/my-history/my-history.component.scss b/client/src/app/+my-library/my-history/my-history.component.scss index 9eeeaf310..928a8a3da 100644 --- a/client/src/app/+my-library/my-history/my-history.component.scss +++ b/client/src/app/+my-library/my-history/my-history.component.scss @@ -10,17 +10,23 @@ } .top-buttons { - margin-bottom: 20px; + margin-bottom: 30px; display: flex; align-items: center; flex-wrap: wrap; + #history-search { + @include peertube-input-text(250px); + } + .history-switch { display: flex; - flex-grow: 1; label { margin: 0 0 0 5px; + color: var(--greyForegroundColor); + font-size: 15px; + font-weight: $font-semibold; } } diff --git a/client/src/app/+my-library/my-history/my-history.component.ts b/client/src/app/+my-library/my-history/my-history.component.ts index 4ba95124d..0c8e4b83f 100644 --- a/client/src/app/+my-library/my-history/my-history.component.ts +++ b/client/src/app/+my-library/my-history/my-history.component.ts @@ -13,6 +13,8 @@ import { import { immutableAssign } from '@app/helpers' import { UserHistoryService } from '@app/shared/shared-main' import { AbstractVideoList } from '@app/shared/shared-video-miniature' +import { Subject } from 'rxjs' +import { debounceTime, tap, distinctUntilChanged } from 'rxjs/operators' @Component({ templateUrl: './my-history.component.html', @@ -26,6 +28,9 @@ export class MyHistoryComponent extends AbstractVideoList implements OnInit, OnD totalItems: null } videosHistoryEnabled: boolean + search: string + + protected searchStream: Subject constructor ( protected router: Router, @@ -41,7 +46,7 @@ export class MyHistoryComponent extends AbstractVideoList implements OnInit, OnD ) { super() - this.titlePage = $localize`My videos history` + this.titlePage = $localize`My watch history` } ngOnInit () { @@ -52,6 +57,28 @@ export class MyHistoryComponent extends AbstractVideoList implements OnInit, OnD this.videosHistoryEnabled = this.authService.getUser().videosHistoryEnabled }) + this.searchStream = new Subject() + + this.searchStream + .pipe( + debounceTime(400), + distinctUntilChanged() + ) + .subscribe(search => { + this.search = search + this.reloadVideos() + }) + } + + onSearch (event: Event) { + const target = event.target as HTMLInputElement + this.searchStream.next(target.value) + } + + resetSearch () { + const searchInput = document.getElementById('history-search') as HTMLInputElement + searchInput.value = '' + this.searchStream.next('') } ngOnDestroy () { @@ -61,7 +88,10 @@ export class MyHistoryComponent extends AbstractVideoList implements OnInit, OnD getVideosObservable (page: number) { const newPagination = immutableAssign(this.pagination, { currentPage: page }) - return this.userHistoryService.getUserVideosHistory(newPagination) + return this.userHistoryService.getUserVideosHistory(newPagination, this.search) + .pipe( + tap(res => this.pagination.totalItems = res.total) + ) } generateSyndicationList () { diff --git a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html index 6ab3826ba..510b400c0 100644 --- a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html +++ b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html @@ -15,7 +15,7 @@
-
You don't have any subscriptions yet.
+
You don't have any subscription yet.
diff --git a/client/src/app/shared/shared-main/users/user-history.service.ts b/client/src/app/shared/shared-main/users/user-history.service.ts index 43970dc5b..bb87dcba8 100644 --- a/client/src/app/shared/shared-main/users/user-history.service.ts +++ b/client/src/app/shared/shared-main/users/user-history.service.ts @@ -18,11 +18,12 @@ export class UserHistoryService { private videoService: VideoService ) {} - getUserVideosHistory (historyPagination: ComponentPaginationLight) { + getUserVideosHistory (historyPagination: ComponentPaginationLight, search?: string) { const pagination = this.restService.componentPaginationToRestPagination(historyPagination) let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination) + params = params.append('search', search) return this.authHttp .get>(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL, { params }) diff --git a/server/controllers/api/users/my-history.ts b/server/controllers/api/users/my-history.ts index 80d4dc748..72c7da373 100644 --- a/server/controllers/api/users/my-history.ts +++ b/server/controllers/api/users/my-history.ts @@ -5,6 +5,7 @@ import { authenticate, paginationValidator, setDefaultPagination, + userHistoryListValidator, userHistoryRemoveValidator } from '../../../middlewares' import { getFormattedObjects } from '../../../helpers/utils' @@ -18,6 +19,7 @@ myVideosHistoryRouter.get('/me/history/videos', authenticate, paginationValidator, setDefaultPagination, + userHistoryListValidator, asyncMiddleware(listMyVideosHistory) ) @@ -38,7 +40,7 @@ export { async function listMyVideosHistory (req: express.Request, res: express.Response) { const user = res.locals.oauth.token.User - const resultList = await UserVideoHistoryModel.listForApi(user, req.query.start, req.query.count) + const resultList = await UserVideoHistoryModel.listForApi(user, req.query.start, req.query.count, req.query.search) return res.json(getFormattedObjects(resultList.data, resultList.total)) } diff --git a/server/middlewares/validators/user-history.ts b/server/middlewares/validators/user-history.ts index 2f1d3cc41..058bf7758 100644 --- a/server/middlewares/validators/user-history.ts +++ b/server/middlewares/validators/user-history.ts @@ -1,8 +1,22 @@ import * as express from 'express' -import { body } from 'express-validator' +import { body, query } from 'express-validator' import { logger } from '../../helpers/logger' import { areValidationErrors } from './utils' -import { isDateValid } from '../../helpers/custom-validators/misc' +import { exists, isDateValid } from '../../helpers/custom-validators/misc' + +const userHistoryListValidator = [ + query('search') + .optional() + .custom(exists).withMessage('Should have a valid search'), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking userHistoryListValidator parameters', { parameters: req.query }) + + if (areValidationErrors(req, res)) return + + return next() + } +] const userHistoryRemoveValidator = [ body('beforeDate') @@ -21,5 +35,6 @@ const userHistoryRemoveValidator = [ // --------------------------------------------------------------------------- export { + userHistoryListValidator, userHistoryRemoveValidator } diff --git a/server/models/account/user-video-history.ts b/server/models/account/user-video-history.ts index 45171fc60..6be1d65ea 100644 --- a/server/models/account/user-video-history.ts +++ b/server/models/account/user-video-history.ts @@ -55,10 +55,11 @@ export class UserVideoHistoryModel extends Model { }) User: UserModel - static listForApi (user: MUserAccountId, start: number, count: number) { + static listForApi (user: MUserAccountId, start: number, count: number, search?: string) { return VideoModel.listForApi({ start, count, + search, sort: '-"userVideoHistory"."updatedAt"', nsfw: null, // All includeLocalVideos: true, diff --git a/server/models/video/video.ts b/server/models/video/video.ts index abf823d4b..5027e980d 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -1087,6 +1087,7 @@ export class VideoModel extends Model { user?: MUserAccountId historyOfUser?: MUserId countVideos?: boolean + search?: string }) { 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') @@ -1123,7 +1124,8 @@ export class VideoModel extends Model { includeLocalVideos: options.includeLocalVideos, user: options.user, historyOfUser: options.historyOfUser, - trendingDays + trendingDays, + search: options.search } return VideoModel.getAvailableForApi(queryOptions, options.countVideos) diff --git a/server/tests/api/videos/videos-history.ts b/server/tests/api/videos/videos-history.ts index 661d603cb..b25cff879 100644 --- a/server/tests/api/videos/videos-history.ts +++ b/server/tests/api/videos/videos-history.ts @@ -152,6 +152,15 @@ describe('Test videos history', function () { expect(res.body.data).to.have.lengthOf(0) }) + it('Should be able to search through videos in my history', async function () { + const res = await listMyVideosHistory(server.url, server.accessToken, '2') + + expect(res.body.total).to.equal(1) + + const videos: Video[] = res.body.data + expect(videos[0].name).to.equal('video 2') + }) + it('Should clear my history', async function () { await removeMyVideosHistory(server.url, server.accessToken, video3WatchedDate.toISOString()) }) diff --git a/shared/extra-utils/videos/video-history.ts b/shared/extra-utils/videos/video-history.ts index 0dd3afb24..b989e14dc 100644 --- a/shared/extra-utils/videos/video-history.ts +++ b/shared/extra-utils/videos/video-history.ts @@ -14,13 +14,16 @@ function userWatchVideo ( return makePutBodyRequest({ url, path, token, fields, statusCodeExpected }) } -function listMyVideosHistory (url: string, token: string) { +function listMyVideosHistory (url: string, token: string, search?: string) { const path = '/api/v1/users/me/history/videos' return makeGetRequest({ url, path, token, + query: { + search + }, statusCodeExpected: HttpStatusCode.OK_200 }) } diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index fe4552ff7..8ad98a9a9 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml @@ -933,6 +933,7 @@ paths: parameters: - $ref: '#/components/parameters/start' - $ref: '#/components/parameters/count' + - $ref: '#/components/parameters/search' responses: '200': description: successful operation