diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2021-01-19 13:43:33 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-19 13:43:33 +0100 |
commit | 370240824e2fb28b314255f6c23f5ea7d6b08625 (patch) | |
tree | 6d4350fd93ea0b960bd278a948cecbdfbd2b67d7 /client/src/app | |
parent | 2264c1ceedcf27998108b8f8b706e51ed910d4fb (diff) | |
download | PeerTube-370240824e2fb28b314255f6c23f5ea7d6b08625.tar.gz PeerTube-370240824e2fb28b314255f6c23f5ea7d6b08625.tar.zst PeerTube-370240824e2fb28b314255f6c23f5ea7d6b08625.zip |
Allow users/visitors to search through an account's videos (#3589)
* WIP: account search
* add search to account view
* add tests for account search
Diffstat (limited to 'client/src/app')
11 files changed, 253 insertions, 16 deletions
diff --git a/client/src/app/+accounts/account-search/account-search.component.ts b/client/src/app/+accounts/account-search/account-search.component.ts new file mode 100644 index 000000000..10c7a12d8 --- /dev/null +++ b/client/src/app/+accounts/account-search/account-search.component.ts | |||
@@ -0,0 +1,104 @@ | |||
1 | import { Subscription } from 'rxjs' | ||
2 | import { first, tap } from 'rxjs/operators' | ||
3 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
4 | import { ActivatedRoute, Router } from '@angular/router' | ||
5 | import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core' | ||
6 | import { immutableAssign } from '@app/helpers' | ||
7 | import { Account, AccountService, VideoService } from '@app/shared/shared-main' | ||
8 | import { AbstractVideoList } from '@app/shared/shared-video-miniature' | ||
9 | import { VideoFilter } from '@shared/models' | ||
10 | |||
11 | @Component({ | ||
12 | selector: 'my-account-search', | ||
13 | templateUrl: '../../shared/shared-video-miniature/abstract-video-list.html', | ||
14 | styleUrls: [ | ||
15 | '../../shared/shared-video-miniature/abstract-video-list.scss' | ||
16 | ] | ||
17 | }) | ||
18 | export class AccountSearchComponent extends AbstractVideoList implements OnInit, OnDestroy { | ||
19 | titlePage: string | ||
20 | loadOnInit = false | ||
21 | |||
22 | search = '' | ||
23 | filter: VideoFilter = null | ||
24 | |||
25 | private account: Account | ||
26 | private accountSub: Subscription | ||
27 | |||
28 | constructor ( | ||
29 | protected router: Router, | ||
30 | protected serverService: ServerService, | ||
31 | protected route: ActivatedRoute, | ||
32 | protected authService: AuthService, | ||
33 | protected userService: UserService, | ||
34 | protected notifier: Notifier, | ||
35 | protected confirmService: ConfirmService, | ||
36 | protected screenService: ScreenService, | ||
37 | protected storageService: LocalStorageService, | ||
38 | private accountService: AccountService, | ||
39 | private videoService: VideoService | ||
40 | ) { | ||
41 | super() | ||
42 | } | ||
43 | |||
44 | ngOnInit () { | ||
45 | super.ngOnInit() | ||
46 | |||
47 | this.enableAllFilterIfPossible() | ||
48 | |||
49 | // Parent get the account for us | ||
50 | this.accountSub = this.accountService.accountLoaded | ||
51 | .pipe(first()) | ||
52 | .subscribe(account => { | ||
53 | this.account = account | ||
54 | |||
55 | this.reloadVideos() | ||
56 | this.generateSyndicationList() | ||
57 | }) | ||
58 | } | ||
59 | |||
60 | ngOnDestroy () { | ||
61 | if (this.accountSub) this.accountSub.unsubscribe() | ||
62 | |||
63 | super.ngOnDestroy() | ||
64 | } | ||
65 | |||
66 | updateSearch (value: string) { | ||
67 | if (value === '') this.router.navigate(['../videos'], { relativeTo: this.route }) | ||
68 | this.search = value | ||
69 | |||
70 | this.reloadVideos() | ||
71 | } | ||
72 | |||
73 | getVideosObservable (page: number) { | ||
74 | const newPagination = immutableAssign(this.pagination, { currentPage: page }) | ||
75 | const options = { | ||
76 | account: this.account, | ||
77 | videoPagination: newPagination, | ||
78 | sort: this.sort, | ||
79 | nsfwPolicy: this.nsfwPolicy, | ||
80 | videoFilter: this.filter, | ||
81 | search: this.search | ||
82 | } | ||
83 | |||
84 | return this.videoService | ||
85 | .getAccountVideos(options) | ||
86 | .pipe( | ||
87 | tap(({ total }) => { | ||
88 | this.titlePage = this.search | ||
89 | ? $localize`Published ${total} videos matching "${this.search}"` | ||
90 | : $localize`Published ${total} videos` | ||
91 | }) | ||
92 | ) | ||
93 | } | ||
94 | |||
95 | toggleModerationDisplay () { | ||
96 | this.filter = this.buildLocalFilter(this.filter, null) | ||
97 | |||
98 | this.reloadVideos() | ||
99 | } | ||
100 | |||
101 | generateSyndicationList () { | ||
102 | /* disable syndication */ | ||
103 | } | ||
104 | } | ||
diff --git a/client/src/app/+accounts/accounts-routing.module.ts b/client/src/app/+accounts/accounts-routing.module.ts index d2ca784b0..15937a67b 100644 --- a/client/src/app/+accounts/accounts-routing.module.ts +++ b/client/src/app/+accounts/accounts-routing.module.ts | |||
@@ -5,6 +5,7 @@ import { AccountsComponent } from './accounts.component' | |||
5 | import { AccountVideosComponent } from './account-videos/account-videos.component' | 5 | import { AccountVideosComponent } from './account-videos/account-videos.component' |
6 | import { AccountAboutComponent } from './account-about/account-about.component' | 6 | import { AccountAboutComponent } from './account-about/account-about.component' |
7 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' | 7 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' |
8 | import { AccountSearchComponent } from './account-search/account-search.component' | ||
8 | 9 | ||
9 | const accountsRoutes: Routes = [ | 10 | const accountsRoutes: Routes = [ |
10 | { | 11 | { |
@@ -22,6 +23,24 @@ const accountsRoutes: Routes = [ | |||
22 | pathMatch: 'full' | 23 | pathMatch: 'full' |
23 | }, | 24 | }, |
24 | { | 25 | { |
26 | path: 'video-channels', | ||
27 | component: AccountVideoChannelsComponent, | ||
28 | data: { | ||
29 | meta: { | ||
30 | title: $localize`Account video channels` | ||
31 | } | ||
32 | } | ||
33 | }, | ||
34 | { | ||
35 | path: 'about', | ||
36 | component: AccountAboutComponent, | ||
37 | data: { | ||
38 | meta: { | ||
39 | title: $localize`About account` | ||
40 | } | ||
41 | } | ||
42 | }, | ||
43 | { | ||
25 | path: 'videos', | 44 | path: 'videos', |
26 | component: AccountVideosComponent, | 45 | component: AccountVideosComponent, |
27 | data: { | 46 | data: { |
@@ -35,20 +54,11 @@ const accountsRoutes: Routes = [ | |||
35 | } | 54 | } |
36 | }, | 55 | }, |
37 | { | 56 | { |
38 | path: 'video-channels', | 57 | path: 'search', |
39 | component: AccountVideoChannelsComponent, | 58 | component: AccountSearchComponent, |
40 | data: { | 59 | data: { |
41 | meta: { | 60 | meta: { |
42 | title: $localize`Account video channels` | 61 | title: $localize`Search videos within account` |
43 | } | ||
44 | } | ||
45 | }, | ||
46 | { | ||
47 | path: 'about', | ||
48 | component: AccountAboutComponent, | ||
49 | data: { | ||
50 | meta: { | ||
51 | title: $localize`About account` | ||
52 | } | 62 | } |
53 | } | 63 | } |
54 | } | 64 | } |
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html index 31c8e3a8e..5bd7b0824 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html | |||
@@ -44,11 +44,13 @@ | |||
44 | </ng-template> | 44 | </ng-template> |
45 | 45 | ||
46 | <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> | 46 | <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> |
47 | |||
48 | <simple-search-input (searchChanged)="searchChanged($event)" name="search-videos" i18n-placeholder placeholder="Search videos"></simple-search-input> | ||
47 | </div> | 49 | </div> |
48 | </div> | 50 | </div> |
49 | 51 | ||
50 | <div class="margin-content"> | 52 | <div class="margin-content"> |
51 | <router-outlet></router-outlet> | 53 | <router-outlet (activate)="onOutletLoaded($event)"></router-outlet> |
52 | </div> | 54 | </div> |
53 | </div> | 55 | </div> |
54 | 56 | ||
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts index 4820eaf32..1458ea59c 100644 --- a/client/src/app/+accounts/accounts.component.ts +++ b/client/src/app/+accounts/accounts.component.ts | |||
@@ -7,6 +7,7 @@ import { Account, AccountService, DropdownAction, ListOverflowItem, VideoChannel | |||
7 | import { AccountReportComponent } from '@app/shared/shared-moderation' | 7 | import { AccountReportComponent } from '@app/shared/shared-moderation' |
8 | import { User, UserRight } from '@shared/models' | 8 | import { User, UserRight } from '@shared/models' |
9 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 9 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
10 | import { AccountSearchComponent } from './account-search/account-search.component' | ||
10 | 11 | ||
11 | @Component({ | 12 | @Component({ |
12 | templateUrl: './accounts.component.html', | 13 | templateUrl: './accounts.component.html', |
@@ -14,6 +15,7 @@ import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | |||
14 | }) | 15 | }) |
15 | export class AccountsComponent implements OnInit, OnDestroy { | 16 | export class AccountsComponent implements OnInit, OnDestroy { |
16 | @ViewChild('accountReportModal') accountReportModal: AccountReportComponent | 17 | @ViewChild('accountReportModal') accountReportModal: AccountReportComponent |
18 | accountSearch: AccountSearchComponent | ||
17 | 19 | ||
18 | account: Account | 20 | account: Account |
19 | accountUser: User | 21 | accountUser: User |
@@ -99,6 +101,18 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
99 | return $localize`${count} subscribers` | 101 | return $localize`${count} subscribers` |
100 | } | 102 | } |
101 | 103 | ||
104 | onOutletLoaded (component: Component) { | ||
105 | if (component instanceof AccountSearchComponent) { | ||
106 | this.accountSearch = component | ||
107 | } else { | ||
108 | this.accountSearch = undefined | ||
109 | } | ||
110 | } | ||
111 | |||
112 | searchChanged (search: string) { | ||
113 | if (this.accountSearch) this.accountSearch.updateSearch(search) | ||
114 | } | ||
115 | |||
102 | private onAccount (account: Account) { | 116 | private onAccount (account: Account) { |
103 | this.prependModerationActions = undefined | 117 | this.prependModerationActions = undefined |
104 | 118 | ||
diff --git a/client/src/app/+accounts/accounts.module.ts b/client/src/app/+accounts/accounts.module.ts index 815360341..6da65cbc1 100644 --- a/client/src/app/+accounts/accounts.module.ts +++ b/client/src/app/+accounts/accounts.module.ts | |||
@@ -8,6 +8,7 @@ import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' | |||
8 | import { AccountAboutComponent } from './account-about/account-about.component' | 8 | import { AccountAboutComponent } from './account-about/account-about.component' |
9 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' | 9 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' |
10 | import { AccountVideosComponent } from './account-videos/account-videos.component' | 10 | import { AccountVideosComponent } from './account-videos/account-videos.component' |
11 | import { AccountSearchComponent } from './account-search/account-search.component' | ||
11 | import { AccountsRoutingModule } from './accounts-routing.module' | 12 | import { AccountsRoutingModule } from './accounts-routing.module' |
12 | import { AccountsComponent } from './accounts.component' | 13 | import { AccountsComponent } from './accounts.component' |
13 | 14 | ||
@@ -27,7 +28,8 @@ import { AccountsComponent } from './accounts.component' | |||
27 | AccountsComponent, | 28 | AccountsComponent, |
28 | AccountVideosComponent, | 29 | AccountVideosComponent, |
29 | AccountVideoChannelsComponent, | 30 | AccountVideoChannelsComponent, |
30 | AccountAboutComponent | 31 | AccountAboutComponent, |
32 | AccountSearchComponent | ||
31 | ], | 33 | ], |
32 | 34 | ||
33 | exports: [ | 35 | exports: [ |
diff --git a/client/src/app/shared/shared-main/misc/index.ts b/client/src/app/shared/shared-main/misc/index.ts index e806fd2f2..dc8ef9754 100644 --- a/client/src/app/shared/shared-main/misc/index.ts +++ b/client/src/app/shared/shared-main/misc/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | export * from './help.component' | 1 | export * from './help.component' |
2 | export * from './list-overflow.component' | 2 | export * from './list-overflow.component' |
3 | export * from './top-menu-dropdown.component' | 3 | export * from './top-menu-dropdown.component' |
4 | export * from './simple-search-input.component' | ||
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.html b/client/src/app/shared/shared-main/misc/simple-search-input.component.html new file mode 100644 index 000000000..fb0d97122 --- /dev/null +++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.html | |||
@@ -0,0 +1,14 @@ | |||
1 | <span> | ||
2 | <my-global-icon iconName="search" aria-label="Search" role="button" (click)="showInput()"></my-global-icon> | ||
3 | |||
4 | <input | ||
5 | #ref | ||
6 | type="text" | ||
7 | [(ngModel)]="value" | ||
8 | (focusout)="focusLost()" | ||
9 | (keyup.enter)="searchChange()" | ||
10 | [hidden]="!shown" | ||
11 | [name]="name" | ||
12 | [placeholder]="placeholder" | ||
13 | > | ||
14 | </span> | ||
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.scss b/client/src/app/shared/shared-main/misc/simple-search-input.component.scss new file mode 100644 index 000000000..591b04fb2 --- /dev/null +++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.scss | |||
@@ -0,0 +1,29 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | span { | ||
5 | opacity: .6; | ||
6 | |||
7 | &:focus-within { | ||
8 | opacity: 1; | ||
9 | } | ||
10 | } | ||
11 | |||
12 | my-global-icon { | ||
13 | height: 18px; | ||
14 | position: relative; | ||
15 | top: -2px; | ||
16 | } | ||
17 | |||
18 | input { | ||
19 | @include peertube-input-text(150px); | ||
20 | |||
21 | height: 22px; // maximum height for the account/video-channels links | ||
22 | padding-left: 10px; | ||
23 | background-color: transparent; | ||
24 | border: none; | ||
25 | |||
26 | &::placeholder { | ||
27 | font-size: 15px; | ||
28 | } | ||
29 | } | ||
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.ts b/client/src/app/shared/shared-main/misc/simple-search-input.component.ts new file mode 100644 index 000000000..86ae9ab42 --- /dev/null +++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.ts | |||
@@ -0,0 +1,54 @@ | |||
1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { Subject } from 'rxjs' | ||
4 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | ||
5 | |||
6 | @Component({ | ||
7 | selector: 'simple-search-input', | ||
8 | templateUrl: './simple-search-input.component.html', | ||
9 | styleUrls: [ './simple-search-input.component.scss' ] | ||
10 | }) | ||
11 | export class SimpleSearchInputComponent implements OnInit { | ||
12 | @ViewChild('ref') input: ElementRef | ||
13 | |||
14 | @Input() name = 'search' | ||
15 | @Input() placeholder = $localize`Search` | ||
16 | |||
17 | @Output() searchChanged = new EventEmitter<string>() | ||
18 | |||
19 | value = '' | ||
20 | shown: boolean | ||
21 | |||
22 | private searchSubject = new Subject<string>() | ||
23 | |||
24 | constructor ( | ||
25 | private router: Router, | ||
26 | private route: ActivatedRoute | ||
27 | ) {} | ||
28 | |||
29 | ngOnInit () { | ||
30 | this.searchSubject | ||
31 | .pipe( | ||
32 | debounceTime(400), | ||
33 | distinctUntilChanged() | ||
34 | ) | ||
35 | .subscribe(value => this.searchChanged.emit(value)) | ||
36 | |||
37 | this.searchSubject.next(this.value) | ||
38 | } | ||
39 | |||
40 | showInput () { | ||
41 | this.shown = true | ||
42 | setTimeout(() => this.input.nativeElement.focus()) | ||
43 | } | ||
44 | |||
45 | focusLost () { | ||
46 | if (this.value !== '') return | ||
47 | this.shown = false | ||
48 | } | ||
49 | |||
50 | searchChange () { | ||
51 | this.router.navigate(['./search'], { relativeTo: this.route }) | ||
52 | this.searchSubject.next(this.value) | ||
53 | } | ||
54 | } | ||
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts index 123b5a3e3..c69a4c8b2 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts | |||
@@ -30,7 +30,7 @@ import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditBu | |||
30 | import { DateToggleComponent } from './date' | 30 | import { DateToggleComponent } from './date' |
31 | import { FeedComponent } from './feeds' | 31 | import { FeedComponent } from './feeds' |
32 | import { LoaderComponent, SmallLoaderComponent } from './loaders' | 32 | import { LoaderComponent, SmallLoaderComponent } from './loaders' |
33 | import { HelpComponent, ListOverflowComponent, TopMenuDropdownComponent } from './misc' | 33 | import { HelpComponent, ListOverflowComponent, TopMenuDropdownComponent, SimpleSearchInputComponent } from './misc' |
34 | import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' | 34 | import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' |
35 | import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' | 35 | import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' |
36 | import { VideoCaptionService } from './video-caption' | 36 | import { VideoCaptionService } from './video-caption' |
@@ -88,6 +88,7 @@ import { VideoChannelService } from './video-channel' | |||
88 | HelpComponent, | 88 | HelpComponent, |
89 | ListOverflowComponent, | 89 | ListOverflowComponent, |
90 | TopMenuDropdownComponent, | 90 | TopMenuDropdownComponent, |
91 | SimpleSearchInputComponent, | ||
91 | 92 | ||
92 | UserQuotaComponent, | 93 | UserQuotaComponent, |
93 | UserNotificationsComponent | 94 | UserNotificationsComponent |
@@ -140,6 +141,7 @@ import { VideoChannelService } from './video-channel' | |||
140 | HelpComponent, | 141 | HelpComponent, |
141 | ListOverflowComponent, | 142 | ListOverflowComponent, |
142 | TopMenuDropdownComponent, | 143 | TopMenuDropdownComponent, |
144 | SimpleSearchInputComponent, | ||
143 | 145 | ||
144 | UserQuotaComponent, | 146 | UserQuotaComponent, |
145 | UserNotificationsComponent | 147 | UserNotificationsComponent |
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 59860c5cb..0b708b692 100644 --- a/client/src/app/shared/shared-main/video/video.service.ts +++ b/client/src/app/shared/shared-main/video/video.service.ts | |||
@@ -140,8 +140,9 @@ export class VideoService implements VideosProvider { | |||
140 | sort: VideoSortField | 140 | sort: VideoSortField |
141 | nsfwPolicy?: NSFWPolicyType | 141 | nsfwPolicy?: NSFWPolicyType |
142 | videoFilter?: VideoFilter | 142 | videoFilter?: VideoFilter |
143 | search?: string | ||
143 | }): Observable<ResultList<Video>> { | 144 | }): Observable<ResultList<Video>> { |
144 | const { account, videoPagination, sort, videoFilter, nsfwPolicy } = parameters | 145 | const { account, videoPagination, sort, videoFilter, nsfwPolicy, search } = parameters |
145 | 146 | ||
146 | const pagination = this.restService.componentPaginationToRestPagination(videoPagination) | 147 | const pagination = this.restService.componentPaginationToRestPagination(videoPagination) |
147 | 148 | ||
@@ -156,6 +157,10 @@ export class VideoService implements VideosProvider { | |||
156 | params = params.set('filter', videoFilter) | 157 | params = params.set('filter', videoFilter) |
157 | } | 158 | } |
158 | 159 | ||
160 | if (search) { | ||
161 | params = params.set('search', search) | ||
162 | } | ||
163 | |||
159 | return this.authHttp | 164 | return this.authHttp |
160 | .get<ResultList<Video>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params }) | 165 | .get<ResultList<Video>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params }) |
161 | .pipe( | 166 | .pipe( |