aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2021-01-13 09:16:15 +0100
committerGitHub <noreply@github.com>2021-01-13 09:16:15 +0100
commitd8b34ee55b654912f86bb8b472d391ced8c28f64 (patch)
treeefa2b8ac36c00fa6e9b5af3f13e54a47bc7a7701
parent22078471fbe5a4dea6177bd1fa19da1cf887679e (diff)
downloadPeerTube-d8b34ee55b654912f86bb8b472d391ced8c28f64.tar.gz
PeerTube-d8b34ee55b654912f86bb8b472d391ced8c28f64.tar.zst
PeerTube-d8b34ee55b654912f86bb8b472d391ced8c28f64.zip
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
-rw-r--r--client/src/app/+my-library/my-history/my-history.component.html19
-rw-r--r--client/src/app/+my-library/my-history/my-history.component.scss10
-rw-r--r--client/src/app/+my-library/my-history/my-history.component.ts34
-rw-r--r--client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html2
-rw-r--r--client/src/app/shared/shared-main/users/user-history.service.ts3
-rw-r--r--server/controllers/api/users/my-history.ts4
-rw-r--r--server/middlewares/validators/user-history.ts19
-rw-r--r--server/models/account/user-video-history.ts3
-rw-r--r--server/models/video/video.ts4
-rw-r--r--server/tests/api/videos/videos-history.ts9
-rw-r--r--shared/extra-utils/videos/video-history.ts5
-rw-r--r--support/doc/api/openapi.yaml1
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 @@
1<h1> 1<h1>
2 <my-global-icon iconName="history" aria-hidden="true"></my-global-icon> 2 <my-global-icon iconName="history" aria-hidden="true"></my-global-icon>
3 <ng-container i18n>My history</ng-container> 3 <ng-container i18n>My watch history</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span>
4</h1> 4</h1>
5 5
6<div class="top-buttons"> 6<div class="top-buttons">
7 <div class="history-switch"> 7 <div>
8 <div class="input-group has-feedback has-clear">
9 <input
10 type="text" name="history-search" id="history-search" i18n-placeholder placeholder="Search your history"
11 (keyup)="onSearch($event)"
12 >
13 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
14 <span class="sr-only" i18n>Clear filters</span>
15 </div>
16 </div>
17
18 <div class="history-switch ml-auto mr-3">
8 <my-input-switch [(ngModel)]="videosHistoryEnabled" (ngModelChange)="onVideosHistoryChange()"></my-input-switch> 19 <my-input-switch [(ngModel)]="videosHistoryEnabled" (ngModelChange)="onVideosHistoryChange()"></my-input-switch>
9 <label i18n>Video history</label> 20 <label i18n>Track watch history</label>
10 </div> 21 </div>
11 22
12 <button class="delete-history" (click)="deleteHistory()" i18n> 23 <button class="delete-history" (click)="deleteHistory()" i18n>
@@ -16,7 +27,7 @@
16</div> 27</div>
17 28
18 29
19<div class="no-history" i18n *ngIf="hasDoneFirstQuery && videos.length === 0">You don't have any video history yet.</div> 30<div class="no-history" i18n *ngIf="hasDoneFirstQuery && videos.length === 0">You don't have any video in your watch history yet.</div>
20 31
21<div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()" class="videos"> 32<div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()" class="videos">
22 <div class="video" *ngFor="let video of videos"> 33 <div class="video" *ngFor="let video of videos">
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 @@
10} 10}
11 11
12.top-buttons { 12.top-buttons {
13 margin-bottom: 20px; 13 margin-bottom: 30px;
14 display: flex; 14 display: flex;
15 align-items: center; 15 align-items: center;
16 flex-wrap: wrap; 16 flex-wrap: wrap;
17 17
18 #history-search {
19 @include peertube-input-text(250px);
20 }
21
18 .history-switch { 22 .history-switch {
19 display: flex; 23 display: flex;
20 flex-grow: 1;
21 24
22 label { 25 label {
23 margin: 0 0 0 5px; 26 margin: 0 0 0 5px;
27 color: var(--greyForegroundColor);
28 font-size: 15px;
29 font-weight: $font-semibold;
24 } 30 }
25 } 31 }
26 32
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 {
13import { immutableAssign } from '@app/helpers' 13import { immutableAssign } from '@app/helpers'
14import { UserHistoryService } from '@app/shared/shared-main' 14import { UserHistoryService } from '@app/shared/shared-main'
15import { AbstractVideoList } from '@app/shared/shared-video-miniature' 15import { AbstractVideoList } from '@app/shared/shared-video-miniature'
16import { Subject } from 'rxjs'
17import { debounceTime, tap, distinctUntilChanged } from 'rxjs/operators'
16 18
17@Component({ 19@Component({
18 templateUrl: './my-history.component.html', 20 templateUrl: './my-history.component.html',
@@ -26,6 +28,9 @@ export class MyHistoryComponent extends AbstractVideoList implements OnInit, OnD
26 totalItems: null 28 totalItems: null
27 } 29 }
28 videosHistoryEnabled: boolean 30 videosHistoryEnabled: boolean
31 search: string
32
33 protected searchStream: Subject<string>
29 34
30 constructor ( 35 constructor (
31 protected router: Router, 36 protected router: Router,
@@ -41,7 +46,7 @@ export class MyHistoryComponent extends AbstractVideoList implements OnInit, OnD
41 ) { 46 ) {
42 super() 47 super()
43 48
44 this.titlePage = $localize`My videos history` 49 this.titlePage = $localize`My watch history`
45 } 50 }
46 51
47 ngOnInit () { 52 ngOnInit () {
@@ -52,6 +57,28 @@ export class MyHistoryComponent extends AbstractVideoList implements OnInit, OnD
52 this.videosHistoryEnabled = this.authService.getUser().videosHistoryEnabled 57 this.videosHistoryEnabled = this.authService.getUser().videosHistoryEnabled
53 }) 58 })
54 59
60 this.searchStream = new Subject()
61
62 this.searchStream
63 .pipe(
64 debounceTime(400),
65 distinctUntilChanged()
66 )
67 .subscribe(search => {
68 this.search = search
69 this.reloadVideos()
70 })
71 }
72
73 onSearch (event: Event) {
74 const target = event.target as HTMLInputElement
75 this.searchStream.next(target.value)
76 }
77
78 resetSearch () {
79 const searchInput = document.getElementById('history-search') as HTMLInputElement
80 searchInput.value = ''
81 this.searchStream.next('')
55 } 82 }
56 83
57 ngOnDestroy () { 84 ngOnDestroy () {
@@ -61,7 +88,10 @@ export class MyHistoryComponent extends AbstractVideoList implements OnInit, OnD
61 getVideosObservable (page: number) { 88 getVideosObservable (page: number) {
62 const newPagination = immutableAssign(this.pagination, { currentPage: page }) 89 const newPagination = immutableAssign(this.pagination, { currentPage: page })
63 90
64 return this.userHistoryService.getUserVideosHistory(newPagination) 91 return this.userHistoryService.getUserVideosHistory(newPagination, this.search)
92 .pipe(
93 tap(res => this.pagination.totalItems = res.total)
94 )
65 } 95 }
66 96
67 generateSyndicationList () { 97 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 @@
15 </div> 15 </div>
16</div> 16</div>
17 17
18<div class="no-results" i18n *ngIf="pagination.totalItems === 0">You don't have any subscriptions yet.</div> 18<div class="no-results" i18n *ngIf="pagination.totalItems === 0">You don't have any subscription yet.</div>
19 19
20<div class="video-channels" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> 20<div class="video-channels" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
21 <div *ngFor="let videoChannel of videoChannels" class="video-channel"> 21 <div *ngFor="let videoChannel of videoChannels" class="video-channel">
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 {
18 private videoService: VideoService 18 private videoService: VideoService
19 ) {} 19 ) {}
20 20
21 getUserVideosHistory (historyPagination: ComponentPaginationLight) { 21 getUserVideosHistory (historyPagination: ComponentPaginationLight, search?: string) {
22 const pagination = this.restService.componentPaginationToRestPagination(historyPagination) 22 const pagination = this.restService.componentPaginationToRestPagination(historyPagination)
23 23
24 let params = new HttpParams() 24 let params = new HttpParams()
25 params = this.restService.addRestGetParams(params, pagination) 25 params = this.restService.addRestGetParams(params, pagination)
26 params = params.append('search', search)
26 27
27 return this.authHttp 28 return this.authHttp
28 .get<ResultList<Video>>(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL, { params }) 29 .get<ResultList<Video>>(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 {
5 authenticate, 5 authenticate,
6 paginationValidator, 6 paginationValidator,
7 setDefaultPagination, 7 setDefaultPagination,
8 userHistoryListValidator,
8 userHistoryRemoveValidator 9 userHistoryRemoveValidator
9} from '../../../middlewares' 10} from '../../../middlewares'
10import { getFormattedObjects } from '../../../helpers/utils' 11import { getFormattedObjects } from '../../../helpers/utils'
@@ -18,6 +19,7 @@ myVideosHistoryRouter.get('/me/history/videos',
18 authenticate, 19 authenticate,
19 paginationValidator, 20 paginationValidator,
20 setDefaultPagination, 21 setDefaultPagination,
22 userHistoryListValidator,
21 asyncMiddleware(listMyVideosHistory) 23 asyncMiddleware(listMyVideosHistory)
22) 24)
23 25
@@ -38,7 +40,7 @@ export {
38async function listMyVideosHistory (req: express.Request, res: express.Response) { 40async function listMyVideosHistory (req: express.Request, res: express.Response) {
39 const user = res.locals.oauth.token.User 41 const user = res.locals.oauth.token.User
40 42
41 const resultList = await UserVideoHistoryModel.listForApi(user, req.query.start, req.query.count) 43 const resultList = await UserVideoHistoryModel.listForApi(user, req.query.start, req.query.count, req.query.search)
42 44
43 return res.json(getFormattedObjects(resultList.data, resultList.total)) 45 return res.json(getFormattedObjects(resultList.data, resultList.total))
44} 46}
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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body } from 'express-validator' 2import { body, query } from 'express-validator'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import { areValidationErrors } from './utils' 4import { areValidationErrors } from './utils'
5import { isDateValid } from '../../helpers/custom-validators/misc' 5import { exists, isDateValid } from '../../helpers/custom-validators/misc'
6
7const userHistoryListValidator = [
8 query('search')
9 .optional()
10 .custom(exists).withMessage('Should have a valid search'),
11
12 (req: express.Request, res: express.Response, next: express.NextFunction) => {
13 logger.debug('Checking userHistoryListValidator parameters', { parameters: req.query })
14
15 if (areValidationErrors(req, res)) return
16
17 return next()
18 }
19]
6 20
7const userHistoryRemoveValidator = [ 21const userHistoryRemoveValidator = [
8 body('beforeDate') 22 body('beforeDate')
@@ -21,5 +35,6 @@ const userHistoryRemoveValidator = [
21// --------------------------------------------------------------------------- 35// ---------------------------------------------------------------------------
22 36
23export { 37export {
38 userHistoryListValidator,
24 userHistoryRemoveValidator 39 userHistoryRemoveValidator
25} 40}
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 {
55 }) 55 })
56 User: UserModel 56 User: UserModel
57 57
58 static listForApi (user: MUserAccountId, start: number, count: number) { 58 static listForApi (user: MUserAccountId, start: number, count: number, search?: string) {
59 return VideoModel.listForApi({ 59 return VideoModel.listForApi({
60 start, 60 start,
61 count, 61 count,
62 search,
62 sort: '-"userVideoHistory"."updatedAt"', 63 sort: '-"userVideoHistory"."updatedAt"',
63 nsfw: null, // All 64 nsfw: null, // All
64 includeLocalVideos: true, 65 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 {
1087 user?: MUserAccountId 1087 user?: MUserAccountId
1088 historyOfUser?: MUserId 1088 historyOfUser?: MUserId
1089 countVideos?: boolean 1089 countVideos?: boolean
1090 search?: string
1090 }) { 1091 }) {
1091 if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { 1092 if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
1092 throw new Error('Try to filter all-local but no user has not the see all videos right') 1093 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 {
1123 includeLocalVideos: options.includeLocalVideos, 1124 includeLocalVideos: options.includeLocalVideos,
1124 user: options.user, 1125 user: options.user,
1125 historyOfUser: options.historyOfUser, 1126 historyOfUser: options.historyOfUser,
1126 trendingDays 1127 trendingDays,
1128 search: options.search
1127 } 1129 }
1128 1130
1129 return VideoModel.getAvailableForApi(queryOptions, options.countVideos) 1131 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 () {
152 expect(res.body.data).to.have.lengthOf(0) 152 expect(res.body.data).to.have.lengthOf(0)
153 }) 153 })
154 154
155 it('Should be able to search through videos in my history', async function () {
156 const res = await listMyVideosHistory(server.url, server.accessToken, '2')
157
158 expect(res.body.total).to.equal(1)
159
160 const videos: Video[] = res.body.data
161 expect(videos[0].name).to.equal('video 2')
162 })
163
155 it('Should clear my history', async function () { 164 it('Should clear my history', async function () {
156 await removeMyVideosHistory(server.url, server.accessToken, video3WatchedDate.toISOString()) 165 await removeMyVideosHistory(server.url, server.accessToken, video3WatchedDate.toISOString())
157 }) 166 })
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 (
14 return makePutBodyRequest({ url, path, token, fields, statusCodeExpected }) 14 return makePutBodyRequest({ url, path, token, fields, statusCodeExpected })
15} 15}
16 16
17function listMyVideosHistory (url: string, token: string) { 17function listMyVideosHistory (url: string, token: string, search?: string) {
18 const path = '/api/v1/users/me/history/videos' 18 const path = '/api/v1/users/me/history/videos'
19 19
20 return makeGetRequest({ 20 return makeGetRequest({
21 url, 21 url,
22 path, 22 path,
23 token, 23 token,
24 query: {
25 search
26 },
24 statusCodeExpected: HttpStatusCode.OK_200 27 statusCodeExpected: HttpStatusCode.OK_200
25 }) 28 })
26} 29}
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:
933 parameters: 933 parameters:
934 - $ref: '#/components/parameters/start' 934 - $ref: '#/components/parameters/start'
935 - $ref: '#/components/parameters/count' 935 - $ref: '#/components/parameters/count'
936 - $ref: '#/components/parameters/search'
936 responses: 937 responses:
937 '200': 938 '200':
938 description: successful operation 939 description: successful operation