diff options
46 files changed, 568 insertions, 335 deletions
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html index 9a6c124e1..a9e0931f8 100644 --- a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html +++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html | |||
@@ -3,4 +3,4 @@ | |||
3 | <ng-container i18n>Reports</ng-container> | 3 | <ng-container i18n>Reports</ng-container> |
4 | </h1> | 4 | </h1> |
5 | 5 | ||
6 | <my-abuse-list-table viewType="admin" baseRoute="/admin/moderation/abuses/list"></my-abuse-list-table> | 6 | <my-abuse-list-table viewType="admin"></my-abuse-list-table> |
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html index c7275de1b..cf2466bdb 100644 --- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html +++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html | |||
@@ -13,25 +13,7 @@ | |||
13 | <ng-template pTemplate="caption"> | 13 | <ng-template pTemplate="caption"> |
14 | <div class="caption"> | 14 | <div class="caption"> |
15 | <div class="ml-auto"> | 15 | <div class="ml-auto"> |
16 | <div class="input-group has-feedback has-clear"> | 16 | <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter> |
17 | <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body"> | ||
18 | <div class="input-group-text" ngbDropdownToggle> | ||
19 | <span class="caret" aria-haspopup="menu" role="button"></span> | ||
20 | </div> | ||
21 | |||
22 | <div role="menu" ngbDropdownMenu> | ||
23 | <h6 class="dropdown-header" i18n>Advanced block filters</h6> | ||
24 | <a [routerLink]="[ '/admin/moderation/video-blocks/list' ]" [queryParams]="{ 'search': 'type:auto' }" class="dropdown-item" i18n>Automatic blocks</a> | ||
25 | <a [routerLink]="[ '/admin/moderation/video-blocks/list' ]" [queryParams]="{ 'search': 'type:manual' }" class="dropdown-item" i18n>Manual blocks</a> | ||
26 | </div> | ||
27 | </div> | ||
28 | <input | ||
29 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | ||
30 | (keyup)="onSearch($event)" | ||
31 | > | ||
32 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a> | ||
33 | <span class="sr-only" i18n>Clear filters</span> | ||
34 | </div> | ||
35 | </div> | 17 | </div> |
36 | </div> | 18 | </div> |
37 | </ng-template> | 19 | </ng-template> |
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts index d6aca10e7..dfd8dc745 100644 --- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts +++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts | |||
@@ -6,6 +6,7 @@ import { AfterViewInit, Component, OnInit } from '@angular/core' | |||
6 | import { DomSanitizer } from '@angular/platform-browser' | 6 | import { DomSanitizer } from '@angular/platform-browser' |
7 | import { ActivatedRoute, Params, Router } from '@angular/router' | 7 | import { ActivatedRoute, Params, Router } from '@angular/router' |
8 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' | 8 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' |
9 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | ||
9 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 10 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
10 | import { VideoBlockService } from '@app/shared/shared-moderation' | 11 | import { VideoBlockService } from '@app/shared/shared-moderation' |
11 | import { VideoBlacklist, VideoBlacklistType } from '@shared/models' | 12 | import { VideoBlacklist, VideoBlacklistType } from '@shared/models' |
@@ -24,6 +25,17 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV | |||
24 | 25 | ||
25 | videoBlocklistActions: DropdownAction<VideoBlacklist>[][] = [] | 26 | videoBlocklistActions: DropdownAction<VideoBlacklist>[][] = [] |
26 | 27 | ||
28 | inputFilters: AdvancedInputFilter[] = [ | ||
29 | { | ||
30 | queryParams: { 'search': 'type:auto' }, | ||
31 | label: $localize`Automatic blocks` | ||
32 | }, | ||
33 | { | ||
34 | queryParams: { 'search': 'type:manual' }, | ||
35 | label: $localize`Manual blocks` | ||
36 | } | ||
37 | ] | ||
38 | |||
27 | constructor ( | 39 | constructor ( |
28 | protected route: ActivatedRoute, | 40 | protected route: ActivatedRoute, |
29 | protected router: Router, | 41 | protected router: Router, |
@@ -111,25 +123,6 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV | |||
111 | if (this.search) this.setTableFilter(this.search, false) | 123 | if (this.search) this.setTableFilter(this.search, false) |
112 | } | 124 | } |
113 | 125 | ||
114 | /* Table filter functions */ | ||
115 | onBlockSearch (event: Event) { | ||
116 | this.onSearch(event) | ||
117 | this.setQueryParams((event.target as HTMLInputElement).value) | ||
118 | } | ||
119 | |||
120 | setQueryParams (search: string) { | ||
121 | const queryParams: Params = {} | ||
122 | if (search) Object.assign(queryParams, { search }) | ||
123 | this.router.navigate([ '/admin/moderation/video-blocks/list' ], { queryParams }) | ||
124 | } | ||
125 | |||
126 | resetTableFilter () { | ||
127 | this.setTableFilter('') | ||
128 | this.setQueryParams('') | ||
129 | this.resetSearch() | ||
130 | } | ||
131 | /* END Table filter functions */ | ||
132 | |||
133 | getIdentifier () { | 126 | getIdentifier () { |
134 | return 'VideoBlockListComponent' | 127 | return 'VideoBlockListComponent' |
135 | } | 128 | } |
diff --git a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html index b6cec9c51..5cc0ff137 100644 --- a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html +++ b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html | |||
@@ -26,25 +26,7 @@ | |||
26 | </div> | 26 | </div> |
27 | 27 | ||
28 | <div class="ml-auto"> | 28 | <div class="ml-auto"> |
29 | <div class="input-group has-feedback has-clear"> | 29 | <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter> |
30 | <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body"> | ||
31 | <div class="input-group-text" ngbDropdownToggle> | ||
32 | <span class="caret" aria-haspopup="menu" role="button"></span> | ||
33 | </div> | ||
34 | |||
35 | <div role="menu" ngbDropdownMenu> | ||
36 | <h6 class="dropdown-header" i18n>Advanced comments filters</h6> | ||
37 | <a [routerLink]="[ '/admin/moderation/video-comments/list' ]" [queryParams]="{ 'search': 'local:true' }" class="dropdown-item" i18n>Local comments</a> | ||
38 | <a [routerLink]="[ '/admin/moderation/video-comments/list' ]" [queryParams]="{ 'search': 'local:false' }" class="dropdown-item" i18n>Remote comments</a> | ||
39 | </div> | ||
40 | </div> | ||
41 | <input | ||
42 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | ||
43 | (keyup)="onSearch($event)" | ||
44 | > | ||
45 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a> | ||
46 | <span class="sr-only" i18n>Clear filters</span> | ||
47 | </div> | ||
48 | </div> | 30 | </div> |
49 | </div> | 31 | </div> |
50 | </ng-template> | 32 | </ng-template> |
diff --git a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts index 63493d00d..ebbbddb43 100644 --- a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts +++ b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts | |||
@@ -2,6 +2,7 @@ import { SortMeta } from 'primeng/api' | |||
2 | import { AfterViewInit, Component, OnInit } from '@angular/core' | 2 | import { AfterViewInit, Component, OnInit } from '@angular/core' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { AuthService, ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' | 4 | import { AuthService, ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' |
5 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | ||
5 | import { DropdownAction } from '@app/shared/shared-main' | 6 | import { DropdownAction } from '@app/shared/shared-main' |
6 | import { BulkService } from '@app/shared/shared-moderation' | 7 | import { BulkService } from '@app/shared/shared-moderation' |
7 | import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment' | 8 | import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment' |
@@ -43,6 +44,17 @@ export class VideoCommentListComponent extends RestTable implements OnInit, Afte | |||
43 | selectedComments: VideoCommentAdmin[] = [] | 44 | selectedComments: VideoCommentAdmin[] = [] |
44 | bulkCommentActions: DropdownAction<VideoCommentAdmin[]>[] = [] | 45 | bulkCommentActions: DropdownAction<VideoCommentAdmin[]>[] = [] |
45 | 46 | ||
47 | inputFilters: AdvancedInputFilter[] = [ | ||
48 | { | ||
49 | queryParams: { 'search': 'local:true' }, | ||
50 | label: $localize`Local comments` | ||
51 | }, | ||
52 | { | ||
53 | queryParams: { 'search': 'local:false' }, | ||
54 | label: $localize`Remote comments` | ||
55 | } | ||
56 | ] | ||
57 | |||
46 | get authUser () { | 58 | get authUser () { |
47 | return this.auth.getUser() | 59 | return this.auth.getUser() |
48 | } | 60 | } |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html index f84d3fd0c..7170d7019 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.html +++ b/client/src/app/+admin/users/user-list/user-list.component.html | |||
@@ -22,24 +22,7 @@ | |||
22 | </div> | 22 | </div> |
23 | 23 | ||
24 | <div class="ml-auto"> | 24 | <div class="ml-auto"> |
25 | <div class="input-group has-feedback has-clear"> | 25 | <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter> |
26 | <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body"> | ||
27 | <div class="input-group-text" ngbDropdownToggle> | ||
28 | <span class="caret" aria-haspopup="menu" role="button"></span> | ||
29 | </div> | ||
30 | |||
31 | <div role="menu" ngbDropdownMenu> | ||
32 | <h6 class="dropdown-header" i18n>Advanced user filters</h6> | ||
33 | <a [routerLink]="[ '/admin/users/list' ]" [queryParams]="{ 'search': 'banned:true' }" class="dropdown-item" i18n>Banned users</a> | ||
34 | </div> | ||
35 | </div> | ||
36 | <input | ||
37 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | ||
38 | (keyup)="onSearch($event)" | ||
39 | > | ||
40 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a> | ||
41 | <span class="sr-only" i18n>Clear filters</span> | ||
42 | </div> | ||
43 | </div> | 26 | </div> |
44 | 27 | ||
45 | </div> | 28 | </div> |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.scss b/client/src/app/+admin/users/user-list/user-list.component.scss index f18747ec3..db4979a51 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.scss +++ b/client/src/app/+admin/users/user-list/user-list.component.scss | |||
@@ -11,6 +11,7 @@ tr.banned > td { | |||
11 | 11 | ||
12 | .table-email { | 12 | .table-email { |
13 | @include disable-default-a-behaviour; | 13 | @include disable-default-a-behaviour; |
14 | |||
14 | color: pvar(--mainForegroundColor); | 15 | color: pvar(--mainForegroundColor); |
15 | } | 16 | } |
16 | 17 | ||
@@ -28,14 +29,6 @@ tr.banned > td { | |||
28 | margin-left: 0.1rem; | 29 | margin-left: 0.1rem; |
29 | } | 30 | } |
30 | 31 | ||
31 | .caption { | ||
32 | justify-content: space-between; | ||
33 | |||
34 | input { | ||
35 | @include peertube-input-text(250px); | ||
36 | } | ||
37 | } | ||
38 | |||
39 | p-tableCheckbox { | 32 | p-tableCheckbox { |
40 | position: relative; | 33 | position: relative; |
41 | top: -2.5px; | 34 | top: -2.5px; |
@@ -55,18 +48,7 @@ my-global-icon { | |||
55 | 48 | ||
56 | .progress { | 49 | .progress { |
57 | @include progressbar($small: true); | 50 | @include progressbar($small: true); |
51 | |||
58 | width: auto; | 52 | width: auto; |
59 | max-width: 100%; | 53 | max-width: 100%; |
60 | } | 54 | } |
61 | |||
62 | .input-group { | ||
63 | @include peertube-input-group(300px); | ||
64 | |||
65 | input { | ||
66 | flex: 1; | ||
67 | } | ||
68 | |||
69 | .dropdown-toggle::after { | ||
70 | margin-left: 0; | ||
71 | } | ||
72 | } | ||
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts index 339e18206..435bc17d7 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/users/user-list/user-list.component.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { SortMeta } from 'primeng/api' | 1 | import { SortMeta } from 'primeng/api' |
2 | import { Component, OnInit, ViewChild } from '@angular/core' | 2 | import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core' |
3 | import { ActivatedRoute, Params, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { AuthService, ConfirmService, Notifier, RestPagination, RestTable, ServerService, UserService } from '@app/core' | 4 | import { AuthService, ConfirmService, Notifier, RestPagination, RestTable, ServerService, UserService } from '@app/core' |
5 | import { Account, DropdownAction } from '@app/shared/shared-main' | 5 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
6 | import { DropdownAction } from '@app/shared/shared-main' | ||
6 | import { UserBanModalComponent } from '@app/shared/shared-moderation' | 7 | import { UserBanModalComponent } from '@app/shared/shared-moderation' |
7 | import { ServerConfig, User, UserRole } from '@shared/models' | 8 | import { ServerConfig, User, UserRole } from '@shared/models' |
8 | 9 | ||
@@ -18,19 +19,28 @@ type UserForList = User & { | |||
18 | templateUrl: './user-list.component.html', | 19 | templateUrl: './user-list.component.html', |
19 | styleUrls: [ './user-list.component.scss' ] | 20 | styleUrls: [ './user-list.component.scss' ] |
20 | }) | 21 | }) |
21 | export class UserListComponent extends RestTable implements OnInit { | 22 | export class UserListComponent extends RestTable implements OnInit, AfterViewInit { |
22 | @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent | 23 | @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent |
23 | 24 | ||
24 | users: User[] = [] | 25 | users: User[] = [] |
26 | |||
25 | totalRecords = 0 | 27 | totalRecords = 0 |
26 | sort: SortMeta = { field: 'createdAt', order: 1 } | 28 | sort: SortMeta = { field: 'createdAt', order: 1 } |
27 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | 29 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
30 | |||
28 | highlightBannedUsers = false | 31 | highlightBannedUsers = false |
29 | 32 | ||
30 | selectedUsers: User[] = [] | 33 | selectedUsers: User[] = [] |
31 | bulkUserActions: DropdownAction<User[]>[][] = [] | 34 | bulkUserActions: DropdownAction<User[]>[][] = [] |
32 | columns: { id: string, label: string }[] | 35 | columns: { id: string, label: string }[] |
33 | 36 | ||
37 | inputFilters: AdvancedInputFilter[] = [ | ||
38 | { | ||
39 | queryParams: { 'search': 'banned:true' }, | ||
40 | label: $localize`Banned users` | ||
41 | } | ||
42 | ] | ||
43 | |||
34 | private _selectedColumns: string[] | 44 | private _selectedColumns: string[] |
35 | private serverConfig: ServerConfig | 45 | private serverConfig: ServerConfig |
36 | 46 | ||
@@ -117,6 +127,10 @@ export class UserListComponent extends RestTable implements OnInit { | |||
117 | this.columns.push({ id: 'lastLoginDate', label: 'Last login' }) | 127 | this.columns.push({ id: 'lastLoginDate', label: 'Last login' }) |
118 | } | 128 | } |
119 | 129 | ||
130 | ngAfterViewInit () { | ||
131 | if (this.search) this.setTableFilter(this.search, false) | ||
132 | } | ||
133 | |||
120 | getIdentifier () { | 134 | getIdentifier () { |
121 | return 'UserListComponent' | 135 | return 'UserListComponent' |
122 | } | 136 | } |
diff --git a/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.html b/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.html index 59ca61be6..e83b59019 100644 --- a/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.html +++ b/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.html | |||
@@ -3,4 +3,4 @@ | |||
3 | <ng-container i18n>Reports</ng-container> | 3 | <ng-container i18n>Reports</ng-container> |
4 | </h1> | 4 | </h1> |
5 | 5 | ||
6 | <my-abuse-list-table viewType="user" baseRoute="/my-account/abuses"></my-abuse-list-table> | 6 | <my-abuse-list-table viewType="user"></my-abuse-list-table> |
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.html b/client/src/app/+my-library/my-videos/my-videos.component.html index e9f436378..7c1cdb511 100644 --- a/client/src/app/+my-library/my-videos/my-videos.component.html +++ b/client/src/app/+my-library/my-videos/my-videos.component.html | |||
@@ -19,12 +19,7 @@ | |||
19 | </h1> | 19 | </h1> |
20 | 20 | ||
21 | <div class="videos-header d-flex justify-content-between"> | 21 | <div class="videos-header d-flex justify-content-between"> |
22 | <div class="has-feedback has-clear"> | 22 | <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter> |
23 | <input type="text" placeholder="Search your videos" i18n-placeholder [(ngModel)]="videosSearch" | ||
24 | (ngModelChange)="onVideosSearchChanged()" /> | ||
25 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> | ||
26 | <span class="sr-only" i18n>Clear filters</span> | ||
27 | </div> | ||
28 | 23 | ||
29 | <div class="peertube-select-container peertube-select-button"> | 24 | <div class="peertube-select-container peertube-select-button"> |
30 | <select [(ngModel)]="sort" (ngModelChange)="onChangeSortColumn()" class="form-control"> | 25 | <select [(ngModel)]="sort" (ngModelChange)="onChangeSortColumn()" class="form-control"> |
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.ts b/client/src/app/+my-library/my-videos/my-videos.component.ts index 356e158d6..f9c1b32b0 100644 --- a/client/src/app/+my-library/my-videos/my-videos.component.ts +++ b/client/src/app/+my-library/my-videos/my-videos.component.ts | |||
@@ -1,10 +1,11 @@ | |||
1 | import { concat, Observable, Subject } from 'rxjs' | 1 | import { concat, Observable } from 'rxjs' |
2 | import { debounceTime, tap, toArray } from 'rxjs/operators' | 2 | import { tap, toArray } from 'rxjs/operators' |
3 | import { Component, OnInit, ViewChild } from '@angular/core' | 3 | import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core' |
4 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
5 | import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core' | 5 | import { AuthService, ComponentPagination, ConfirmService, Notifier, RouteFilter, ScreenService, ServerService, User } from '@app/core' |
6 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' | 6 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' |
7 | import { immutableAssign } from '@app/helpers' | 7 | import { immutableAssign } from '@app/helpers' |
8 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | ||
8 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 9 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
9 | import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' | 10 | import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' |
10 | import { MiniatureDisplayOptions, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' | 11 | import { MiniatureDisplayOptions, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' |
@@ -15,7 +16,7 @@ import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.c | |||
15 | templateUrl: './my-videos.component.html', | 16 | templateUrl: './my-videos.component.html', |
16 | styleUrls: [ './my-videos.component.scss' ] | 17 | styleUrls: [ './my-videos.component.scss' ] |
17 | }) | 18 | }) |
18 | export class MyVideosComponent implements OnInit, DisableForReuseHook { | 19 | export class MyVideosComponent extends RouteFilter implements OnInit, AfterViewInit, DisableForReuseHook { |
19 | @ViewChild('videosSelection', { static: true }) videosSelection: VideosSelectionComponent | 20 | @ViewChild('videosSelection', { static: true }) videosSelection: VideosSelectionComponent |
20 | @ViewChild('videoChangeOwnershipModal', { static: true }) videoChangeOwnershipModal: VideoChangeOwnershipComponent | 21 | @ViewChild('videoChangeOwnershipModal', { static: true }) videoChangeOwnershipModal: VideoChangeOwnershipComponent |
21 | @ViewChild('liveStreamInformationModal', { static: true }) liveStreamInformationModal: LiveStreamInformationComponent | 22 | @ViewChild('liveStreamInformationModal', { static: true }) liveStreamInformationModal: LiveStreamInformationComponent |
@@ -40,13 +41,18 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
40 | videoActions: DropdownAction<{ video: Video }>[] = [] | 41 | videoActions: DropdownAction<{ video: Video }>[] = [] |
41 | 42 | ||
42 | videos: Video[] = [] | 43 | videos: Video[] = [] |
43 | videosSearch: string | ||
44 | videosSearchChanged = new Subject<string>() | ||
45 | getVideosObservableFunction = this.getVideosObservable.bind(this) | 44 | getVideosObservableFunction = this.getVideosObservable.bind(this) |
46 | sort: VideoSortField = '-publishedAt' | 45 | sort: VideoSortField = '-publishedAt' |
47 | 46 | ||
48 | user: User | 47 | user: User |
49 | 48 | ||
49 | inputFilters: AdvancedInputFilter[] = [ | ||
50 | { | ||
51 | queryParams: { 'search': 'isLive:true' }, | ||
52 | label: $localize`Only live videos` | ||
53 | } | ||
54 | ] | ||
55 | |||
50 | constructor ( | 56 | constructor ( |
51 | protected router: Router, | 57 | protected router: Router, |
52 | protected serverService: ServerService, | 58 | protected serverService: ServerService, |
@@ -57,6 +63,8 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
57 | private confirmService: ConfirmService, | 63 | private confirmService: ConfirmService, |
58 | private videoService: VideoService | 64 | private videoService: VideoService |
59 | ) { | 65 | ) { |
66 | super() | ||
67 | |||
60 | this.titlePage = $localize`My videos` | 68 | this.titlePage = $localize`My videos` |
61 | } | 69 | } |
62 | 70 | ||
@@ -65,20 +73,16 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
65 | 73 | ||
66 | this.user = this.authService.getUser() | 74 | this.user = this.authService.getUser() |
67 | 75 | ||
68 | this.videosSearchChanged | 76 | this.initSearch() |
69 | .pipe(debounceTime(500)) | 77 | this.listenToSearchChange() |
70 | .subscribe(() => { | ||
71 | this.videosSelection.reloadVideos() | ||
72 | }) | ||
73 | } | 78 | } |
74 | 79 | ||
75 | resetSearch () { | 80 | ngAfterViewInit () { |
76 | this.videosSearch = '' | 81 | if (this.search) this.setTableFilter(this.search, false) |
77 | this.onVideosSearchChanged() | ||
78 | } | 82 | } |
79 | 83 | ||
80 | onVideosSearchChanged () { | 84 | loadData () { |
81 | this.videosSearchChanged.next() | 85 | this.videosSelection.reloadVideos() |
82 | } | 86 | } |
83 | 87 | ||
84 | onChangeSortColumn () { | 88 | onChangeSortColumn () { |
@@ -96,7 +100,7 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
96 | getVideosObservable (page: number) { | 100 | getVideosObservable (page: number) { |
97 | const newPagination = immutableAssign(this.pagination, { currentPage: page }) | 101 | const newPagination = immutableAssign(this.pagination, { currentPage: page }) |
98 | 102 | ||
99 | return this.videoService.getMyVideos(newPagination, this.sort, this.videosSearch) | 103 | return this.videoService.getMyVideos(newPagination, this.sort, this.search) |
100 | .pipe( | 104 | .pipe( |
101 | tap(res => this.pagination.totalItems = res.total) | 105 | tap(res => this.pagination.totalItems = res.total) |
102 | ) | 106 | ) |
diff --git a/client/src/app/core/rest/rest-table.ts b/client/src/app/core/rest/rest-table.ts index 32c1db446..9baab8a39 100644 --- a/client/src/app/core/rest/rest-table.ts +++ b/client/src/app/core/rest/rest-table.ts | |||
@@ -1,14 +1,14 @@ | |||
1 | import * as debug from 'debug' | 1 | import * as debug from 'debug' |
2 | import { LazyLoadEvent, SortMeta } from 'primeng/api' | 2 | import { LazyLoadEvent, SortMeta } from 'primeng/api' |
3 | import { Subject } from 'rxjs' | 3 | import { Subject } from 'rxjs' |
4 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
5 | import { ActivatedRoute, Params, Router } from '@angular/router' | ||
6 | import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' | 5 | import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' |
6 | import { RouteFilter } from '../routing' | ||
7 | import { RestPagination } from './rest-pagination' | 7 | import { RestPagination } from './rest-pagination' |
8 | 8 | ||
9 | const logger = debug('peertube:tables:RestTable') | 9 | const logger = debug('peertube:tables:RestTable') |
10 | 10 | ||
11 | export abstract class RestTable { | 11 | export abstract class RestTable extends RouteFilter { |
12 | 12 | ||
13 | abstract totalRecords: number | 13 | abstract totalRecords: number |
14 | abstract sort: SortMeta | 14 | abstract sort: SortMeta |
@@ -19,8 +19,6 @@ export abstract class RestTable { | |||
19 | rowsPerPage = this.rowsPerPageOptions[0] | 19 | rowsPerPage = this.rowsPerPageOptions[0] |
20 | expandedRows = {} | 20 | expandedRows = {} |
21 | 21 | ||
22 | baseRoute: string | ||
23 | |||
24 | protected searchStream: Subject<string> | 22 | protected searchStream: Subject<string> |
25 | 23 | ||
26 | protected route: ActivatedRoute | 24 | protected route: ActivatedRoute |
@@ -66,55 +64,6 @@ export abstract class RestTable { | |||
66 | peertubeLocalStorage.setItem(this.getSortLocalStorageKey(), JSON.stringify(this.sort)) | 64 | peertubeLocalStorage.setItem(this.getSortLocalStorageKey(), JSON.stringify(this.sort)) |
67 | } | 65 | } |
68 | 66 | ||
69 | initSearch () { | ||
70 | this.searchStream = new Subject() | ||
71 | |||
72 | this.searchStream | ||
73 | .pipe( | ||
74 | debounceTime(400), | ||
75 | distinctUntilChanged() | ||
76 | ) | ||
77 | .subscribe(search => { | ||
78 | this.search = search | ||
79 | |||
80 | logger('On search %s.', this.search) | ||
81 | |||
82 | this.loadData() | ||
83 | }) | ||
84 | } | ||
85 | |||
86 | onSearch (event: Event) { | ||
87 | const target = event.target as HTMLInputElement | ||
88 | this.searchStream.next(target.value) | ||
89 | |||
90 | this.setQueryParams((event.target as HTMLInputElement).value) | ||
91 | } | ||
92 | |||
93 | setQueryParams (search: string) { | ||
94 | if (!this.baseRoute) return | ||
95 | |||
96 | const queryParams: Params = {} | ||
97 | |||
98 | if (search) Object.assign(queryParams, { search }) | ||
99 | this.router.navigate([ this.baseRoute ], { queryParams }) | ||
100 | } | ||
101 | |||
102 | resetTableFilter () { | ||
103 | this.setTableFilter('') | ||
104 | this.setQueryParams('') | ||
105 | this.resetSearch() | ||
106 | } | ||
107 | |||
108 | listenToSearchChange () { | ||
109 | this.route.queryParams | ||
110 | .subscribe(params => { | ||
111 | this.search = params.search || '' | ||
112 | |||
113 | // Primeng table will run an event to load data | ||
114 | this.setTableFilter(this.search) | ||
115 | }) | ||
116 | } | ||
117 | |||
118 | onPage (event: { first: number, rows: number }) { | 67 | onPage (event: { first: number, rows: number }) { |
119 | logger('On page %o.', event) | 68 | logger('On page %o.', event) |
120 | 69 | ||
@@ -131,21 +80,6 @@ export abstract class RestTable { | |||
131 | this.expandedRows = {} | 80 | this.expandedRows = {} |
132 | } | 81 | } |
133 | 82 | ||
134 | setTableFilter (filter: string, triggerEvent = true) { | ||
135 | // FIXME: cannot use ViewChild, so create a component for the filter input | ||
136 | const filterInput = document.getElementById('table-filter') as HTMLInputElement | ||
137 | if (!filterInput) return | ||
138 | |||
139 | filterInput.value = filter | ||
140 | |||
141 | if (triggerEvent) filterInput.dispatchEvent(new Event('keyup')) | ||
142 | } | ||
143 | |||
144 | resetSearch () { | ||
145 | this.searchStream.next('') | ||
146 | this.setTableFilter('') | ||
147 | } | ||
148 | |||
149 | protected abstract loadData (): void | 83 | protected abstract loadData (): void |
150 | 84 | ||
151 | private getSortLocalStorageKey () { | 85 | private getSortLocalStorageKey () { |
diff --git a/client/src/app/core/routing/index.ts b/client/src/app/core/routing/index.ts index 239c27caf..d53a4ae2c 100644 --- a/client/src/app/core/routing/index.ts +++ b/client/src/app/core/routing/index.ts | |||
@@ -5,6 +5,7 @@ export * from './login-guard.service' | |||
5 | export * from './menu-guard.service' | 5 | export * from './menu-guard.service' |
6 | export * from './preload-selected-modules-list' | 6 | export * from './preload-selected-modules-list' |
7 | export * from './redirect.service' | 7 | export * from './redirect.service' |
8 | export * from './route-filter' | ||
8 | export * from './server-config-resolver.service' | 9 | export * from './server-config-resolver.service' |
9 | export * from './unlogged-guard.service' | 10 | export * from './unlogged-guard.service' |
10 | export * from './user-right-guard.service' | 11 | export * from './user-right-guard.service' |
diff --git a/client/src/app/core/routing/route-filter.ts b/client/src/app/core/routing/route-filter.ts new file mode 100644 index 000000000..f783a0c40 --- /dev/null +++ b/client/src/app/core/routing/route-filter.ts | |||
@@ -0,0 +1,79 @@ | |||
1 | import * as debug from 'debug' | ||
2 | import { Subject } from 'rxjs' | ||
3 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | ||
4 | import { ActivatedRoute, Params, Router } from '@angular/router' | ||
5 | |||
6 | const logger = debug('peertube:tables:RouteFilter') | ||
7 | |||
8 | export abstract class RouteFilter { | ||
9 | search: string | ||
10 | |||
11 | protected searchStream: Subject<string> | ||
12 | |||
13 | protected route: ActivatedRoute | ||
14 | protected router: Router | ||
15 | |||
16 | initSearch () { | ||
17 | this.searchStream = new Subject() | ||
18 | |||
19 | this.searchStream | ||
20 | .pipe( | ||
21 | debounceTime(400), | ||
22 | distinctUntilChanged() | ||
23 | ) | ||
24 | .subscribe(search => { | ||
25 | this.search = search | ||
26 | |||
27 | logger('On search %s.', this.search) | ||
28 | |||
29 | this.loadData() | ||
30 | }) | ||
31 | } | ||
32 | |||
33 | onSearch (event: Event) { | ||
34 | const target = event.target as HTMLInputElement | ||
35 | this.searchStream.next(target.value) | ||
36 | |||
37 | this.setQueryParams(target.value) | ||
38 | } | ||
39 | |||
40 | resetTableFilter () { | ||
41 | this.setTableFilter('') | ||
42 | this.setQueryParams('') | ||
43 | this.resetSearch() | ||
44 | } | ||
45 | |||
46 | resetSearch () { | ||
47 | this.searchStream.next('') | ||
48 | this.setTableFilter('') | ||
49 | } | ||
50 | |||
51 | listenToSearchChange () { | ||
52 | this.route.queryParams | ||
53 | .subscribe(params => { | ||
54 | this.search = params.search || '' | ||
55 | |||
56 | // Primeng table will run an event to load data | ||
57 | this.setTableFilter(this.search) | ||
58 | }) | ||
59 | } | ||
60 | |||
61 | setTableFilter (filter: string, triggerEvent = true) { | ||
62 | // FIXME: cannot use ViewChild, so create a component for the filter input | ||
63 | const filterInput = document.getElementById('table-filter') as HTMLInputElement | ||
64 | if (!filterInput) return | ||
65 | |||
66 | filterInput.value = filter | ||
67 | |||
68 | if (triggerEvent) filterInput.dispatchEvent(new Event('keyup')) | ||
69 | } | ||
70 | |||
71 | protected abstract loadData (): void | ||
72 | |||
73 | private setQueryParams (search: string) { | ||
74 | const queryParams: Params = {} | ||
75 | |||
76 | if (search) Object.assign(queryParams, { search }) | ||
77 | this.router.navigate([ ], { queryParams }) | ||
78 | } | ||
79 | } | ||
diff --git a/client/src/app/shared/shared-abuse-list/abuse-details.component.html b/client/src/app/shared/shared-abuse-list/abuse-details.component.html index f2eaeb32f..110f574fa 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-details.component.html +++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.html | |||
@@ -7,7 +7,7 @@ | |||
7 | <span class="col-3 moderation-expanded-label" i18n>Reporter</span> | 7 | <span class="col-3 moderation-expanded-label" i18n>Reporter</span> |
8 | 8 | ||
9 | <span class="col-9 moderation-expanded-text"> | 9 | <span class="col-9 moderation-expanded-text"> |
10 | <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" | 10 | <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" |
11 | class="chip" | 11 | class="chip" |
12 | > | 12 | > |
13 | <my-actor-avatar [account]="abuse.reporterAccount"></my-actor-avatar> | 13 | <my-actor-avatar [account]="abuse.reporterAccount"></my-actor-avatar> |
@@ -16,7 +16,7 @@ | |||
16 | </div> | 16 | </div> |
17 | </a> | 17 | </a> |
18 | 18 | ||
19 | <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" | 19 | <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" |
20 | class="ml-auto text-muted abuse-details-links" i18n | 20 | class="ml-auto text-muted abuse-details-links" i18n |
21 | > | 21 | > |
22 | {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1Â glyphicon glyphicon-flag"></span> | 22 | {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1Â glyphicon glyphicon-flag"></span> |
@@ -27,7 +27,7 @@ | |||
27 | <div class="d-flex" *ngIf="abuse.flaggedAccount"> | 27 | <div class="d-flex" *ngIf="abuse.flaggedAccount"> |
28 | <span class="col-3 moderation-expanded-label" i18n>Reportee</span> | 28 | <span class="col-3 moderation-expanded-label" i18n>Reportee</span> |
29 | <span class="col-9 moderation-expanded-text"> | 29 | <span class="col-9 moderation-expanded-text"> |
30 | <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }" | 30 | <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }" |
31 | class="chip" | 31 | class="chip" |
32 | > | 32 | > |
33 | <my-actor-avatar [account]="abuse.flaggedAccount"></my-actor-avatar> | 33 | <my-actor-avatar [account]="abuse.flaggedAccount"></my-actor-avatar> |
@@ -36,7 +36,7 @@ | |||
36 | </div> | 36 | </div> |
37 | </a> | 37 | </a> |
38 | 38 | ||
39 | <a *ngIf="isAdminView" [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }" | 39 | <a *ngIf="isAdminView" [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }" |
40 | class="ml-auto text-muted abuse-details-links" i18n | 40 | class="ml-auto text-muted abuse-details-links" i18n |
41 | > | 41 | > |
42 | {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1Â glyphicon glyphicon-flag"></span> | 42 | {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1Â glyphicon glyphicon-flag"></span> |
@@ -53,7 +53,7 @@ | |||
53 | <div class="mt-3 d-flex"> | 53 | <div class="mt-3 d-flex"> |
54 | <span class="col-3 moderation-expanded-label"> | 54 | <span class="col-3 moderation-expanded-label"> |
55 | <ng-container i18n>Report</ng-container> | 55 | <ng-container i18n>Report</ng-container> |
56 | <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 text-muted">#{{ abuse.id }}</a> | 56 | <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 text-muted">#{{ abuse.id }}</a> |
57 | </span> | 57 | </span> |
58 | <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span> | 58 | <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span> |
59 | </div> | 59 | </div> |
@@ -61,7 +61,7 @@ | |||
61 | <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex"> | 61 | <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex"> |
62 | <span class="col-3"></span> | 62 | <span class="col-3"></span> |
63 | <span class="col-9"> | 63 | <span class="col-9"> |
64 | <a *ngFor="let reason of getPredefinedReasons()" [routerLink]="[ baseRoute ]" | 64 | <a *ngFor="let reason of getPredefinedReasons()" [routerLink]="[ '.' ]" |
65 | [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" | 65 | [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" |
66 | > | 66 | > |
67 | <div>{{ reason.label }}</div> | 67 | <div>{{ reason.label }}</div> |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-details.component.ts b/client/src/app/shared/shared-abuse-list/abuse-details.component.ts index e8ce7e678..14674c5f0 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-details.component.ts +++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { durationToString } from '@app/helpers' | 2 | import { durationToString } from '@app/helpers' |
3 | import { Account } from '@app/shared/shared-main' | ||
4 | import { AbusePredefinedReasonsString } from '@shared/models' | 3 | import { AbusePredefinedReasonsString } from '@shared/models' |
5 | import { ProcessedAbuse } from './processed-abuse.model' | 4 | import { ProcessedAbuse } from './processed-abuse.model' |
6 | 5 | ||
@@ -12,7 +11,6 @@ import { ProcessedAbuse } from './processed-abuse.model' | |||
12 | export class AbuseDetailsComponent { | 11 | export class AbuseDetailsComponent { |
13 | @Input() abuse: ProcessedAbuse | 12 | @Input() abuse: ProcessedAbuse |
14 | @Input() isAdminView: boolean | 13 | @Input() isAdminView: boolean |
15 | @Input() baseRoute: string | ||
16 | 14 | ||
17 | private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string } | 15 | private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string } |
18 | 16 | ||
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html index b41bc75d4..22f84a96e 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html | |||
@@ -8,28 +8,7 @@ | |||
8 | <ng-template pTemplate="caption"> | 8 | <ng-template pTemplate="caption"> |
9 | <div class="caption"> | 9 | <div class="caption"> |
10 | <div class="ml-auto"> | 10 | <div class="ml-auto"> |
11 | <div class="input-group has-feedback has-clear"> | 11 | <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter> |
12 | <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body"> | ||
13 | <div class="input-group-text" ngbDropdownToggle> | ||
14 | <span class="caret" aria-haspopup="menu" role="button"></span> | ||
15 | </div> | ||
16 | |||
17 | <div role="menu" ngbDropdownMenu> | ||
18 | <h6 class="dropdown-header" i18n>Advanced report filters</h6> | ||
19 | <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a> | ||
20 | <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a> | ||
21 | <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a> | ||
22 | <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a> | ||
23 | <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a> | ||
24 | </div> | ||
25 | </div> | ||
26 | <input | ||
27 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | ||
28 | (keyup)="onSearch($event)" | ||
29 | > | ||
30 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a> | ||
31 | <span class="sr-only" i18n>Clear filters</span> | ||
32 | </div> | ||
33 | </div> | 12 | </div> |
34 | </div> | 13 | </div> |
35 | </ng-template> | 14 | </ng-template> |
@@ -171,7 +150,7 @@ | |||
171 | <ng-template pTemplate="rowexpansion" let-abuse> | 150 | <ng-template pTemplate="rowexpansion" let-abuse> |
172 | <tr> | 151 | <tr> |
173 | <td class="expand-cell" colspan="8"> | 152 | <td class="expand-cell" colspan="8"> |
174 | <my-abuse-details [abuse]="abuse" [baseRoute]="baseRoute" [isAdminView]="isAdminView()"></my-abuse-details> | 153 | <my-abuse-details [abuse]="abuse" [isAdminView]="isAdminView()"></my-abuse-details> |
175 | </td> | 154 | </td> |
176 | </tr> | 155 | </tr> |
177 | </ng-template> | 156 | </ng-template> |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts index 8b5771237..f393c0d1e 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts | |||
@@ -14,6 +14,7 @@ import { AbuseState, AdminAbuse } from '@shared/models' | |||
14 | import { AbuseMessageModalComponent } from './abuse-message-modal.component' | 14 | import { AbuseMessageModalComponent } from './abuse-message-modal.component' |
15 | import { ModerationCommentModalComponent } from './moderation-comment-modal.component' | 15 | import { ModerationCommentModalComponent } from './moderation-comment-modal.component' |
16 | import { ProcessedAbuse } from './processed-abuse.model' | 16 | import { ProcessedAbuse } from './processed-abuse.model' |
17 | import { AdvancedInputFilter } from '../shared-forms' | ||
17 | 18 | ||
18 | const logger = debug('peertube:moderation:AbuseListTableComponent') | 19 | const logger = debug('peertube:moderation:AbuseListTableComponent') |
19 | 20 | ||
@@ -24,7 +25,6 @@ const logger = debug('peertube:moderation:AbuseListTableComponent') | |||
24 | }) | 25 | }) |
25 | export class AbuseListTableComponent extends RestTable implements OnInit, AfterViewInit { | 26 | export class AbuseListTableComponent extends RestTable implements OnInit, AfterViewInit { |
26 | @Input() viewType: 'admin' | 'user' | 27 | @Input() viewType: 'admin' | 'user' |
27 | @Input() baseRoute: string | ||
28 | 28 | ||
29 | @ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent | 29 | @ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent |
30 | @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent | 30 | @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent |
@@ -36,6 +36,29 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV | |||
36 | 36 | ||
37 | abuseActions: DropdownAction<ProcessedAbuse>[][] = [] | 37 | abuseActions: DropdownAction<ProcessedAbuse>[][] = [] |
38 | 38 | ||
39 | inputFilters: AdvancedInputFilter[] = [ | ||
40 | { | ||
41 | queryParams: { 'search': 'state:pending' }, | ||
42 | label: $localize`Unsolved reports` | ||
43 | }, | ||
44 | { | ||
45 | queryParams: { 'search': 'state:accepted' }, | ||
46 | label: $localize`Accepted reports` | ||
47 | }, | ||
48 | { | ||
49 | queryParams: { 'search': 'state:rejected' }, | ||
50 | label: $localize`Refused reports` | ||
51 | }, | ||
52 | { | ||
53 | queryParams: { 'search': 'videoIs:blacklisted' }, | ||
54 | label: $localize`Reports with blocked videos` | ||
55 | }, | ||
56 | { | ||
57 | queryParams: { 'search': 'videoIs:deleted' }, | ||
58 | label: $localize`Reports with deleted videos` | ||
59 | } | ||
60 | ] | ||
61 | |||
39 | constructor ( | 62 | constructor ( |
40 | protected route: ActivatedRoute, | 63 | protected route: ActivatedRoute, |
41 | protected router: Router, | 64 | protected router: Router, |
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar.component.ts b/client/src/app/shared/shared-actor-image/actor-avatar.component.ts index e4efbe475..835e15110 100644 --- a/client/src/app/shared/shared-actor-image/actor-avatar.component.ts +++ b/client/src/app/shared/shared-actor-image/actor-avatar.component.ts | |||
@@ -98,7 +98,7 @@ export class ActorAvatarComponent { | |||
98 | jkl: 'gray', | 98 | jkl: 'gray', |
99 | mno: 'yellow', | 99 | mno: 'yellow', |
100 | pqr: 'orange', | 100 | pqr: 'orange', |
101 | stv: 'red', | 101 | stvu: 'red', |
102 | wxyz: 'dark-blue' | 102 | wxyz: 'dark-blue' |
103 | } | 103 | } |
104 | 104 | ||
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.html b/client/src/app/shared/shared-forms/advanced-input-filter.component.html new file mode 100644 index 000000000..03c4f127b --- /dev/null +++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.html | |||
@@ -0,0 +1,22 @@ | |||
1 | <div class="input-group has-feedback has-clear"> | ||
2 | <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body"> | ||
3 | <div class="input-group-text" ngbDropdownToggle> | ||
4 | <span class="caret" aria-haspopup="menu" role="button"></span> | ||
5 | </div> | ||
6 | |||
7 | <div role="menu" ngbDropdownMenu> | ||
8 | <h6 class="dropdown-header" i18n>Advanced filters</h6> | ||
9 | |||
10 | <a *ngFor="let filter of filters" [routerLink]="[ '.' ]" [queryParams]="filter.queryParams" class="dropdown-item"> | ||
11 | {{ filter.label }} | ||
12 | </a> | ||
13 | |||
14 | </div> | ||
15 | </div> | ||
16 | <input | ||
17 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | ||
18 | (keyup)="onSearch($event)" | ||
19 | > | ||
20 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="onResetTableFilter()"></a> | ||
21 | <span class="sr-only" i18n>Clear filters</span> | ||
22 | </div> | ||
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.scss b/client/src/app/shared/shared-forms/advanced-input-filter.component.scss new file mode 100644 index 000000000..7c2198927 --- /dev/null +++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.scss | |||
@@ -0,0 +1,10 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | input { | ||
5 | @include peertube-input-text(250px); | ||
6 | } | ||
7 | |||
8 | .input-group-text { | ||
9 | background-color: transparent; | ||
10 | } | ||
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.ts b/client/src/app/shared/shared-forms/advanced-input-filter.component.ts new file mode 100644 index 000000000..394090751 --- /dev/null +++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import { Component, EventEmitter, Input, Output } from '@angular/core' | ||
2 | import { Params } from '@angular/router' | ||
3 | |||
4 | export type AdvancedInputFilter = { | ||
5 | label: string | ||
6 | queryParams: Params | ||
7 | } | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-advanced-input-filter', | ||
11 | templateUrl: './advanced-input-filter.component.html', | ||
12 | styleUrls: [ './advanced-input-filter.component.scss' ] | ||
13 | }) | ||
14 | export class AdvancedInputFilterComponent { | ||
15 | @Input() filters: AdvancedInputFilter[] = [] | ||
16 | |||
17 | @Output() resetTableFilter = new EventEmitter<void>() | ||
18 | @Output() search = new EventEmitter<Event>() | ||
19 | |||
20 | onSearch (event: Event) { | ||
21 | this.search.emit(event) | ||
22 | } | ||
23 | |||
24 | onResetTableFilter () { | ||
25 | this.resetTableFilter.emit() | ||
26 | } | ||
27 | } | ||
diff --git a/client/src/app/shared/shared-forms/index.ts b/client/src/app/shared/shared-forms/index.ts index 1d859b991..727416a40 100644 --- a/client/src/app/shared/shared-forms/index.ts +++ b/client/src/app/shared/shared-forms/index.ts | |||
@@ -1,12 +1,14 @@ | |||
1 | export * from './form-validator.service' | 1 | export * from './advanced-input-filter.component' |
2 | export * from './form-reactive' | 2 | export * from './form-reactive' |
3 | export * from './select' | 3 | export * from './form-validator.service' |
4 | export * from './input-toggle-hidden.component' | 4 | export * from './form-validator.service' |
5 | export * from './input-switch.component' | 5 | export * from './input-switch.component' |
6 | export * from './input-toggle-hidden.component' | ||
6 | export * from './markdown-textarea.component' | 7 | export * from './markdown-textarea.component' |
7 | export * from './peertube-checkbox.component' | 8 | export * from './peertube-checkbox.component' |
8 | export * from './preview-upload.component' | 9 | export * from './preview-upload.component' |
9 | export * from './reactive-file.component' | 10 | export * from './reactive-file.component' |
11 | export * from './select' | ||
12 | export * from './shared-form.module' | ||
10 | export * from './textarea-autoresize.directive' | 13 | export * from './textarea-autoresize.directive' |
11 | export * from './timestamp-input.component' | 14 | export * from './timestamp-input.component' |
12 | export * from './shared-form.module' | ||
diff --git a/client/src/app/shared/shared-forms/shared-form.module.ts b/client/src/app/shared/shared-forms/shared-form.module.ts index 9bdd138a1..5417f7342 100644 --- a/client/src/app/shared/shared-forms/shared-form.module.ts +++ b/client/src/app/shared/shared-forms/shared-form.module.ts | |||
@@ -5,6 +5,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms' | |||
5 | import { NgSelectModule } from '@ng-select/ng-select' | 5 | import { NgSelectModule } from '@ng-select/ng-select' |
6 | import { SharedGlobalIconModule } from '../shared-icons' | 6 | import { SharedGlobalIconModule } from '../shared-icons' |
7 | import { SharedMainModule } from '../shared-main/shared-main.module' | 7 | import { SharedMainModule } from '../shared-main/shared-main.module' |
8 | import { AdvancedInputFilterComponent } from './advanced-input-filter.component' | ||
8 | import { DynamicFormFieldComponent } from './dynamic-form-field.component' | 9 | import { DynamicFormFieldComponent } from './dynamic-form-field.component' |
9 | import { FormValidatorService } from './form-validator.service' | 10 | import { FormValidatorService } from './form-validator.service' |
10 | import { InputSwitchComponent } from './input-switch.component' | 11 | import { InputSwitchComponent } from './input-switch.component' |
@@ -52,7 +53,9 @@ import { TimestampInputComponent } from './timestamp-input.component' | |||
52 | SelectCheckboxComponent, | 53 | SelectCheckboxComponent, |
53 | SelectCustomValueComponent, | 54 | SelectCustomValueComponent, |
54 | 55 | ||
55 | DynamicFormFieldComponent | 56 | DynamicFormFieldComponent, |
57 | |||
58 | AdvancedInputFilterComponent | ||
56 | ], | 59 | ], |
57 | 60 | ||
58 | exports: [ | 61 | exports: [ |
@@ -78,7 +81,9 @@ import { TimestampInputComponent } from './timestamp-input.component' | |||
78 | SelectCheckboxComponent, | 81 | SelectCheckboxComponent, |
79 | SelectCustomValueComponent, | 82 | SelectCustomValueComponent, |
80 | 83 | ||
81 | DynamicFormFieldComponent | 84 | DynamicFormFieldComponent, |
85 | |||
86 | AdvancedInputFilterComponent | ||
82 | ], | 87 | ], |
83 | 88 | ||
84 | providers: [ | 89 | providers: [ |
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 0b708b692..668e51f73 100644 --- a/client/src/app/shared/shared-main/video/video.service.ts +++ b/client/src/app/shared/shared-main/video/video.service.ts | |||
@@ -124,7 +124,23 @@ export class VideoService implements VideosProvider { | |||
124 | 124 | ||
125 | let params = new HttpParams() | 125 | let params = new HttpParams() |
126 | params = this.restService.addRestGetParams(params, pagination, sort) | 126 | params = this.restService.addRestGetParams(params, pagination, sort) |
127 | params = this.restService.addObjectParams(params, { search }) | 127 | |
128 | if (search) { | ||
129 | const filters = this.restService.parseQueryStringFilter(search, { | ||
130 | isLive: { | ||
131 | prefix: 'isLive:', | ||
132 | isBoolean: true, | ||
133 | handler: v => { | ||
134 | if (v === 'true') return v | ||
135 | if (v === 'false') return v | ||
136 | |||
137 | return undefined | ||
138 | } | ||
139 | } | ||
140 | }) | ||
141 | |||
142 | params = this.restService.addObjectParams(params, filters) | ||
143 | } | ||
128 | 144 | ||
129 | return this.authHttp | 145 | return this.authHttp |
130 | .get<ResultList<Video>>(UserService.BASE_USERS_URL + 'me/videos', { params }) | 146 | .get<ResultList<Video>>(UserService.BASE_USERS_URL + 'me/videos', { params }) |
diff --git a/client/src/app/shared/shared-search/advanced-search.model.ts b/client/src/app/shared/shared-search/advanced-search.model.ts index 30badc8fa..0e3924841 100644 --- a/client/src/app/shared/shared-search/advanced-search.model.ts +++ b/client/src/app/shared/shared-search/advanced-search.model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { NSFWQuery, SearchTargetType } from '@shared/models' | 1 | import { BooleanBothQuery, SearchTargetType } from '@shared/models' |
2 | 2 | ||
3 | export class AdvancedSearch { | 3 | export class AdvancedSearch { |
4 | startDate: string // ISO 8601 | 4 | startDate: string // ISO 8601 |
@@ -7,7 +7,7 @@ export class AdvancedSearch { | |||
7 | originallyPublishedStartDate: string // ISO 8601 | 7 | originallyPublishedStartDate: string // ISO 8601 |
8 | originallyPublishedEndDate: string // ISO 8601 | 8 | originallyPublishedEndDate: string // ISO 8601 |
9 | 9 | ||
10 | nsfw: NSFWQuery | 10 | nsfw: BooleanBothQuery |
11 | 11 | ||
12 | categoryOneOf: string | 12 | categoryOneOf: string |
13 | 13 | ||
@@ -33,7 +33,7 @@ export class AdvancedSearch { | |||
33 | endDate?: string | 33 | endDate?: string |
34 | originallyPublishedStartDate?: string | 34 | originallyPublishedStartDate?: string |
35 | originallyPublishedEndDate?: string | 35 | originallyPublishedEndDate?: string |
36 | nsfw?: NSFWQuery | 36 | nsfw?: BooleanBothQuery |
37 | categoryOneOf?: string | 37 | categoryOneOf?: string |
38 | licenceOneOf?: string | 38 | licenceOneOf?: string |
39 | languageOneOf?: string | 39 | languageOneOf?: string |
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss index 1abcd30e4..6a4d89dff 100644 --- a/client/src/sass/primeng-custom.scss +++ b/client/src/sass/primeng-custom.scss | |||
@@ -9,6 +9,10 @@ input[type=button] { | |||
9 | border-radius: inherit; | 9 | border-radius: inherit; |
10 | } | 10 | } |
11 | 11 | ||
12 | p-table .p-datatable-header .caption { | ||
13 | margin-bottom: 15px; | ||
14 | } | ||
15 | |||
12 | // Taken from old nova light theme | 16 | // Taken from old nova light theme |
13 | 17 | ||
14 | body .p-disabled { | 18 | body .p-disabled { |
@@ -512,10 +516,6 @@ p-table { | |||
512 | .left-buttons { | 516 | .left-buttons { |
513 | padding-left: 15px; | 517 | padding-left: 15px; |
514 | } | 518 | } |
515 | |||
516 | .input-group-text { | ||
517 | background-color: transparent; | ||
518 | } | ||
519 | } | 519 | } |
520 | } | 520 | } |
521 | 521 | ||
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index e31924a94..49a8e3195 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { getServerActor } from '@server/models/application/application' | 2 | import { getServerActor } from '@server/models/application/application' |
3 | import { VideosWithSearchCommonQuery } from '@shared/models' | ||
3 | import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' | 4 | import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' |
4 | import { getFormattedObjects } from '../../helpers/utils' | 5 | import { getFormattedObjects } from '../../helpers/utils' |
5 | import { Hooks } from '../../lib/plugins/hooks' | ||
6 | import { JobQueue } from '../../lib/job-queue' | 6 | import { JobQueue } from '../../lib/job-queue' |
7 | import { Hooks } from '../../lib/plugins/hooks' | ||
7 | import { | 8 | import { |
8 | asyncMiddleware, | 9 | asyncMiddleware, |
9 | authenticate, | 10 | authenticate, |
@@ -158,25 +159,27 @@ async function listAccountVideos (req: express.Request, res: express.Response) { | |||
158 | const account = res.locals.account | 159 | const account = res.locals.account |
159 | const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined | 160 | const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined |
160 | const countVideos = getCountVideos(req) | 161 | const countVideos = getCountVideos(req) |
162 | const query = req.query as VideosWithSearchCommonQuery | ||
161 | 163 | ||
162 | const apiOptions = await Hooks.wrapObject({ | 164 | const apiOptions = await Hooks.wrapObject({ |
163 | followerActorId, | 165 | followerActorId, |
164 | start: req.query.start, | 166 | start: query.start, |
165 | count: req.query.count, | 167 | count: query.count, |
166 | sort: req.query.sort, | 168 | sort: query.sort, |
167 | includeLocalVideos: true, | 169 | includeLocalVideos: true, |
168 | categoryOneOf: req.query.categoryOneOf, | 170 | categoryOneOf: query.categoryOneOf, |
169 | licenceOneOf: req.query.licenceOneOf, | 171 | licenceOneOf: query.licenceOneOf, |
170 | languageOneOf: req.query.languageOneOf, | 172 | languageOneOf: query.languageOneOf, |
171 | tagsOneOf: req.query.tagsOneOf, | 173 | tagsOneOf: query.tagsOneOf, |
172 | tagsAllOf: req.query.tagsAllOf, | 174 | tagsAllOf: query.tagsAllOf, |
173 | filter: req.query.filter, | 175 | filter: query.filter, |
174 | nsfw: buildNSFWFilter(res, req.query.nsfw), | 176 | isLive: query.isLive, |
177 | nsfw: buildNSFWFilter(res, query.nsfw), | ||
175 | withFiles: false, | 178 | withFiles: false, |
176 | accountId: account.id, | 179 | accountId: account.id, |
177 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined, | 180 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined, |
178 | countVideos, | 181 | countVideos, |
179 | search: req.query.search | 182 | search: query.search |
180 | }, 'filter:api.accounts.videos.list.params') | 183 | }, 'filter:api.accounts.videos.list.params') |
181 | 184 | ||
182 | const resultList = await Hooks.wrapPromiseFun( | 185 | const resultList = await Hooks.wrapPromiseFun( |
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 9f9d2d77f..0763d1900 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts | |||
@@ -111,7 +111,8 @@ async function getUserVideos (req: express.Request, res: express.Response) { | |||
111 | start: req.query.start, | 111 | start: req.query.start, |
112 | count: req.query.count, | 112 | count: req.query.count, |
113 | sort: req.query.sort, | 113 | sort: req.query.sort, |
114 | search: req.query.search | 114 | search: req.query.search, |
115 | isLive: req.query.isLive | ||
115 | }, 'filter:api.user.me.videos.list.params') | 116 | }, 'filter:api.user.me.videos.list.params') |
116 | 117 | ||
117 | const resultList = await Hooks.wrapPromiseFun( | 118 | const resultList = await Hooks.wrapPromiseFun( |
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts index e8949ee59..56b93276f 100644 --- a/server/controllers/api/users/my-subscriptions.ts +++ b/server/controllers/api/users/my-subscriptions.ts | |||
@@ -2,8 +2,8 @@ import 'multer' | |||
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { sendUndoFollow } from '@server/lib/activitypub/send' | 3 | import { sendUndoFollow } from '@server/lib/activitypub/send' |
4 | import { VideoChannelModel } from '@server/models/video/video-channel' | 4 | import { VideoChannelModel } from '@server/models/video/video-channel' |
5 | import { VideosCommonQuery } from '@shared/models' | ||
5 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
6 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' | ||
7 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' | 7 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' |
8 | import { getFormattedObjects } from '../../../helpers/utils' | 8 | import { getFormattedObjects } from '../../../helpers/utils' |
9 | import { WEBSERVER } from '../../../initializers/constants' | 9 | import { WEBSERVER } from '../../../initializers/constants' |
@@ -170,19 +170,20 @@ async function getUserSubscriptions (req: express.Request, res: express.Response | |||
170 | async function getUserSubscriptionVideos (req: express.Request, res: express.Response) { | 170 | async function getUserSubscriptionVideos (req: express.Request, res: express.Response) { |
171 | const user = res.locals.oauth.token.User | 171 | const user = res.locals.oauth.token.User |
172 | const countVideos = getCountVideos(req) | 172 | const countVideos = getCountVideos(req) |
173 | const query = req.query as VideosCommonQuery | ||
173 | 174 | ||
174 | const resultList = await VideoModel.listForApi({ | 175 | const resultList = await VideoModel.listForApi({ |
175 | start: req.query.start, | 176 | start: query.start, |
176 | count: req.query.count, | 177 | count: query.count, |
177 | sort: req.query.sort, | 178 | sort: query.sort, |
178 | includeLocalVideos: false, | 179 | includeLocalVideos: false, |
179 | categoryOneOf: req.query.categoryOneOf, | 180 | categoryOneOf: query.categoryOneOf, |
180 | licenceOneOf: req.query.licenceOneOf, | 181 | licenceOneOf: query.licenceOneOf, |
181 | languageOneOf: req.query.languageOneOf, | 182 | languageOneOf: query.languageOneOf, |
182 | tagsOneOf: req.query.tagsOneOf, | 183 | tagsOneOf: query.tagsOneOf, |
183 | tagsAllOf: req.query.tagsAllOf, | 184 | tagsAllOf: query.tagsAllOf, |
184 | nsfw: buildNSFWFilter(res, req.query.nsfw), | 185 | nsfw: buildNSFWFilter(res, query.nsfw), |
185 | filter: req.query.filter as VideoFilter, | 186 | filter: query.filter, |
186 | withFiles: false, | 187 | withFiles: false, |
187 | followerActorId: user.Account.Actor.id, | 188 | followerActorId: user.Account.Actor.id, |
188 | user, | 189 | user, |
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index 149d6cfb4..a755d7e57 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -2,7 +2,7 @@ import * as express from 'express' | |||
2 | import { Hooks } from '@server/lib/plugins/hooks' | 2 | import { Hooks } from '@server/lib/plugins/hooks' |
3 | import { getServerActor } from '@server/models/application/application' | 3 | import { getServerActor } from '@server/models/application/application' |
4 | import { MChannelBannerAccountDefault } from '@server/types/models' | 4 | import { MChannelBannerAccountDefault } from '@server/types/models' |
5 | import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared' | 5 | import { ActorImageType, VideoChannelCreate, VideoChannelUpdate, VideosCommonQuery } from '../../../shared' |
6 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
7 | import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' | 7 | import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' |
8 | import { resetSequelizeInstance } from '../../helpers/database-utils' | 8 | import { resetSequelizeInstance } from '../../helpers/database-utils' |
@@ -312,20 +312,21 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon | |||
312 | const videoChannelInstance = res.locals.videoChannel | 312 | const videoChannelInstance = res.locals.videoChannel |
313 | const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined | 313 | const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined |
314 | const countVideos = getCountVideos(req) | 314 | const countVideos = getCountVideos(req) |
315 | const query = req.query as VideosCommonQuery | ||
315 | 316 | ||
316 | const apiOptions = await Hooks.wrapObject({ | 317 | const apiOptions = await Hooks.wrapObject({ |
317 | followerActorId, | 318 | followerActorId, |
318 | start: req.query.start, | 319 | start: query.start, |
319 | count: req.query.count, | 320 | count: query.count, |
320 | sort: req.query.sort, | 321 | sort: query.sort, |
321 | includeLocalVideos: true, | 322 | includeLocalVideos: true, |
322 | categoryOneOf: req.query.categoryOneOf, | 323 | categoryOneOf: query.categoryOneOf, |
323 | licenceOneOf: req.query.licenceOneOf, | 324 | licenceOneOf: query.licenceOneOf, |
324 | languageOneOf: req.query.languageOneOf, | 325 | languageOneOf: query.languageOneOf, |
325 | tagsOneOf: req.query.tagsOneOf, | 326 | tagsOneOf: query.tagsOneOf, |
326 | tagsAllOf: req.query.tagsAllOf, | 327 | tagsAllOf: query.tagsAllOf, |
327 | filter: req.query.filter, | 328 | filter: query.filter, |
328 | nsfw: buildNSFWFilter(res, req.query.nsfw), | 329 | nsfw: buildNSFWFilter(res, query.nsfw), |
329 | withFiles: false, | 330 | withFiles: false, |
330 | videoChannelId: videoChannelInstance.id, | 331 | videoChannelId: videoChannelInstance.id, |
331 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined, | 332 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined, |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 7fee278f2..6ec6478e4 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -10,9 +10,8 @@ import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnail | |||
10 | import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | 10 | import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' |
11 | import { getServerActor } from '@server/models/application/application' | 11 | import { getServerActor } from '@server/models/application/application' |
12 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | 12 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
13 | import { VideoCreate, VideoState, VideoUpdate } from '../../../../shared' | 13 | import { VideoCreate, VideosCommonQuery, VideoState, VideoUpdate } from '../../../../shared' |
14 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 14 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
15 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' | ||
16 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 15 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
17 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' | 16 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' |
18 | import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' | 17 | import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' |
@@ -494,20 +493,22 @@ async function getVideoFileMetadata (req: express.Request, res: express.Response | |||
494 | } | 493 | } |
495 | 494 | ||
496 | async function listVideos (req: express.Request, res: express.Response) { | 495 | async function listVideos (req: express.Request, res: express.Response) { |
496 | const query = req.query as VideosCommonQuery | ||
497 | const countVideos = getCountVideos(req) | 497 | const countVideos = getCountVideos(req) |
498 | 498 | ||
499 | const apiOptions = await Hooks.wrapObject({ | 499 | const apiOptions = await Hooks.wrapObject({ |
500 | start: req.query.start, | 500 | start: query.start, |
501 | count: req.query.count, | 501 | count: query.count, |
502 | sort: req.query.sort, | 502 | sort: query.sort, |
503 | includeLocalVideos: true, | 503 | includeLocalVideos: true, |
504 | categoryOneOf: req.query.categoryOneOf, | 504 | categoryOneOf: query.categoryOneOf, |
505 | licenceOneOf: req.query.licenceOneOf, | 505 | licenceOneOf: query.licenceOneOf, |
506 | languageOneOf: req.query.languageOneOf, | 506 | languageOneOf: query.languageOneOf, |
507 | tagsOneOf: req.query.tagsOneOf, | 507 | tagsOneOf: query.tagsOneOf, |
508 | tagsAllOf: req.query.tagsAllOf, | 508 | tagsAllOf: query.tagsAllOf, |
509 | nsfw: buildNSFWFilter(res, req.query.nsfw), | 509 | nsfw: buildNSFWFilter(res, query.nsfw), |
510 | filter: req.query.filter as VideoFilter, | 510 | isLive: query.isLive, |
511 | filter: query.filter, | ||
511 | withFiles: false, | 512 | withFiles: false, |
512 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined, | 513 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined, |
513 | countVideos | 514 | countVideos |
diff --git a/server/helpers/custom-validators/search.ts b/server/helpers/custom-validators/search.ts index 429fcafcf..a8f258838 100644 --- a/server/helpers/custom-validators/search.ts +++ b/server/helpers/custom-validators/search.ts | |||
@@ -11,7 +11,7 @@ function isStringArray (value: any) { | |||
11 | return isArray(value) && value.every(v => typeof v === 'string') | 11 | return isArray(value) && value.every(v => typeof v === 'string') |
12 | } | 12 | } |
13 | 13 | ||
14 | function isNSFWQueryValid (value: any) { | 14 | function isBooleanBothQueryValid (value: any) { |
15 | return value === 'true' || value === 'false' || value === 'both' | 15 | return value === 'true' || value === 'false' || value === 'both' |
16 | } | 16 | } |
17 | 17 | ||
@@ -32,6 +32,6 @@ function isSearchTargetValid (value: SearchTargetType) { | |||
32 | export { | 32 | export { |
33 | isNumberArray, | 33 | isNumberArray, |
34 | isStringArray, | 34 | isStringArray, |
35 | isNSFWQueryValid, | 35 | isBooleanBothQueryValid, |
36 | isSearchTargetValid | 36 | isSearchTargetValid |
37 | } | 37 | } |
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 4d31d3dcb..bb617d77c 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -20,7 +20,7 @@ import { | |||
20 | toIntOrNull, | 20 | toIntOrNull, |
21 | toValueOrNull | 21 | toValueOrNull |
22 | } from '../../../helpers/custom-validators/misc' | 22 | } from '../../../helpers/custom-validators/misc' |
23 | import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' | 23 | import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' |
24 | import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership' | 24 | import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership' |
25 | import { | 25 | import { |
26 | isScheduleVideoUpdatePrivacyValid, | 26 | isScheduleVideoUpdatePrivacyValid, |
@@ -439,7 +439,11 @@ const commonVideosFiltersValidator = [ | |||
439 | .custom(isStringArray).withMessage('Should have a valid all of tags array'), | 439 | .custom(isStringArray).withMessage('Should have a valid all of tags array'), |
440 | query('nsfw') | 440 | query('nsfw') |
441 | .optional() | 441 | .optional() |
442 | .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'), | 442 | .custom(isBooleanBothQueryValid).withMessage('Should have a valid NSFW attribute'), |
443 | query('isLive') | ||
444 | .optional() | ||
445 | .customSanitizer(toBooleanOrNull) | ||
446 | .custom(isBooleanValid).withMessage('Should have a valid live boolean'), | ||
443 | query('filter') | 447 | query('filter') |
444 | .optional() | 448 | .optional() |
445 | .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'), | 449 | .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'), |
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts index 4d95ddee2..155afe64b 100644 --- a/server/models/video/video-query-builder.ts +++ b/server/models/video/video-query-builder.ts | |||
@@ -16,9 +16,11 @@ export type BuildVideosQueryOptions = { | |||
16 | start: number | 16 | start: number |
17 | sort: string | 17 | sort: string |
18 | 18 | ||
19 | nsfw?: boolean | ||
19 | filter?: VideoFilter | 20 | filter?: VideoFilter |
21 | isLive?: boolean | ||
22 | |||
20 | categoryOneOf?: number[] | 23 | categoryOneOf?: number[] |
21 | nsfw?: boolean | ||
22 | licenceOneOf?: number[] | 24 | licenceOneOf?: number[] |
23 | languageOneOf?: string[] | 25 | languageOneOf?: string[] |
24 | tagsOneOf?: string[] | 26 | tagsOneOf?: string[] |
@@ -199,10 +201,14 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions) | |||
199 | 201 | ||
200 | if (options.nsfw === true) { | 202 | if (options.nsfw === true) { |
201 | and.push('"video"."nsfw" IS TRUE') | 203 | and.push('"video"."nsfw" IS TRUE') |
204 | } else if (options.nsfw === false) { | ||
205 | and.push('"video"."nsfw" IS FALSE') | ||
202 | } | 206 | } |
203 | 207 | ||
204 | if (options.nsfw === false) { | 208 | if (options.isLive === true) { |
205 | and.push('"video"."nsfw" IS FALSE') | 209 | and.push('"video"."isLive" IS TRUE') |
210 | } else if (options.isLive === false) { | ||
211 | and.push('"video"."isLive" IS FALSE') | ||
206 | } | 212 | } |
207 | 213 | ||
208 | if (options.categoryOneOf) { | 214 | if (options.categoryOneOf) { |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 422bf6deb..e55a21a6b 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1021,14 +1021,28 @@ export class VideoModel extends Model { | |||
1021 | start: number | 1021 | start: number |
1022 | count: number | 1022 | count: number |
1023 | sort: string | 1023 | sort: string |
1024 | isLive?: boolean | ||
1024 | search?: string | 1025 | search?: string |
1025 | }) { | 1026 | }) { |
1026 | const { accountId, start, count, sort, search } = options | 1027 | const { accountId, start, count, sort, search, isLive } = options |
1027 | 1028 | ||
1028 | function buildBaseQuery (): FindOptions { | 1029 | function buildBaseQuery (): FindOptions { |
1029 | let baseQuery = { | 1030 | const where: WhereOptions = {} |
1031 | |||
1032 | if (search) { | ||
1033 | where.name = { | ||
1034 | [Op.iLike]: '%' + search + '%' | ||
1035 | } | ||
1036 | } | ||
1037 | |||
1038 | if (isLive) { | ||
1039 | where.isLive = isLive | ||
1040 | } | ||
1041 | |||
1042 | const baseQuery = { | ||
1030 | offset: start, | 1043 | offset: start, |
1031 | limit: count, | 1044 | limit: count, |
1045 | where, | ||
1032 | order: getVideoSort(sort), | 1046 | order: getVideoSort(sort), |
1033 | include: [ | 1047 | include: [ |
1034 | { | 1048 | { |
@@ -1047,16 +1061,6 @@ export class VideoModel extends Model { | |||
1047 | ] | 1061 | ] |
1048 | } | 1062 | } |
1049 | 1063 | ||
1050 | if (search) { | ||
1051 | baseQuery = Object.assign(baseQuery, { | ||
1052 | where: { | ||
1053 | name: { | ||
1054 | [Op.iLike]: '%' + search + '%' | ||
1055 | } | ||
1056 | } | ||
1057 | }) | ||
1058 | } | ||
1059 | |||
1060 | return baseQuery | 1064 | return baseQuery |
1061 | } | 1065 | } |
1062 | 1066 | ||
@@ -1084,23 +1088,34 @@ export class VideoModel extends Model { | |||
1084 | start: number | 1088 | start: number |
1085 | count: number | 1089 | count: number |
1086 | sort: string | 1090 | sort: string |
1091 | |||
1087 | nsfw: boolean | 1092 | nsfw: boolean |
1093 | filter?: VideoFilter | ||
1094 | isLive?: boolean | ||
1095 | |||
1088 | includeLocalVideos: boolean | 1096 | includeLocalVideos: boolean |
1089 | withFiles: boolean | 1097 | withFiles: boolean |
1098 | |||
1090 | categoryOneOf?: number[] | 1099 | categoryOneOf?: number[] |
1091 | licenceOneOf?: number[] | 1100 | licenceOneOf?: number[] |
1092 | languageOneOf?: string[] | 1101 | languageOneOf?: string[] |
1093 | tagsOneOf?: string[] | 1102 | tagsOneOf?: string[] |
1094 | tagsAllOf?: string[] | 1103 | tagsAllOf?: string[] |
1095 | filter?: VideoFilter | 1104 | |
1096 | accountId?: number | 1105 | accountId?: number |
1097 | videoChannelId?: number | 1106 | videoChannelId?: number |
1107 | |||
1098 | followerActorId?: number | 1108 | followerActorId?: number |
1109 | |||
1099 | videoPlaylistId?: number | 1110 | videoPlaylistId?: number |
1111 | |||
1100 | trendingDays?: number | 1112 | trendingDays?: number |
1113 | |||
1101 | user?: MUserAccountId | 1114 | user?: MUserAccountId |
1102 | historyOfUser?: MUserId | 1115 | historyOfUser?: MUserId |
1116 | |||
1103 | countVideos?: boolean | 1117 | countVideos?: boolean |
1118 | |||
1104 | search?: string | 1119 | search?: string |
1105 | }) { | 1120 | }) { |
1106 | if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { | 1121 | if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { |
@@ -1128,6 +1143,7 @@ export class VideoModel extends Model { | |||
1128 | followerActorId, | 1143 | followerActorId, |
1129 | serverAccountId: serverActor.Account.id, | 1144 | serverAccountId: serverActor.Account.id, |
1130 | nsfw: options.nsfw, | 1145 | nsfw: options.nsfw, |
1146 | isLive: options.isLive, | ||
1131 | categoryOneOf: options.categoryOneOf, | 1147 | categoryOneOf: options.categoryOneOf, |
1132 | licenceOneOf: options.licenceOneOf, | 1148 | licenceOneOf: options.licenceOneOf, |
1133 | languageOneOf: options.languageOneOf, | 1149 | languageOneOf: options.languageOneOf, |
@@ -1160,6 +1176,7 @@ export class VideoModel extends Model { | |||
1160 | originallyPublishedStartDate?: string | 1176 | originallyPublishedStartDate?: string |
1161 | originallyPublishedEndDate?: string | 1177 | originallyPublishedEndDate?: string |
1162 | nsfw?: boolean | 1178 | nsfw?: boolean |
1179 | isLive?: boolean | ||
1163 | categoryOneOf?: number[] | 1180 | categoryOneOf?: number[] |
1164 | licenceOneOf?: number[] | 1181 | licenceOneOf?: number[] |
1165 | languageOneOf?: string[] | 1182 | languageOneOf?: string[] |
@@ -1171,23 +1188,32 @@ export class VideoModel extends Model { | |||
1171 | filter?: VideoFilter | 1188 | filter?: VideoFilter |
1172 | }) { | 1189 | }) { |
1173 | const serverActor = await getServerActor() | 1190 | const serverActor = await getServerActor() |
1191 | |||
1174 | const queryOptions = { | 1192 | const queryOptions = { |
1175 | followerActorId: serverActor.id, | 1193 | followerActorId: serverActor.id, |
1176 | serverAccountId: serverActor.Account.id, | 1194 | serverAccountId: serverActor.Account.id, |
1195 | |||
1177 | includeLocalVideos: options.includeLocalVideos, | 1196 | includeLocalVideos: options.includeLocalVideos, |
1178 | nsfw: options.nsfw, | 1197 | nsfw: options.nsfw, |
1198 | isLive: options.isLive, | ||
1199 | |||
1179 | categoryOneOf: options.categoryOneOf, | 1200 | categoryOneOf: options.categoryOneOf, |
1180 | licenceOneOf: options.licenceOneOf, | 1201 | licenceOneOf: options.licenceOneOf, |
1181 | languageOneOf: options.languageOneOf, | 1202 | languageOneOf: options.languageOneOf, |
1203 | |||
1182 | tagsOneOf: options.tagsOneOf, | 1204 | tagsOneOf: options.tagsOneOf, |
1183 | tagsAllOf: options.tagsAllOf, | 1205 | tagsAllOf: options.tagsAllOf, |
1206 | |||
1184 | user: options.user, | 1207 | user: options.user, |
1185 | filter: options.filter, | 1208 | filter: options.filter, |
1209 | |||
1186 | start: options.start, | 1210 | start: options.start, |
1187 | count: options.count, | 1211 | count: options.count, |
1188 | sort: options.sort, | 1212 | sort: options.sort, |
1213 | |||
1189 | startDate: options.startDate, | 1214 | startDate: options.startDate, |
1190 | endDate: options.endDate, | 1215 | endDate: options.endDate, |
1216 | |||
1191 | originallyPublishedStartDate: options.originallyPublishedStartDate, | 1217 | originallyPublishedStartDate: options.originallyPublishedStartDate, |
1192 | originallyPublishedEndDate: options.originallyPublishedEndDate, | 1218 | originallyPublishedEndDate: options.originallyPublishedEndDate, |
1193 | 1219 | ||
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index d48e2a8ee..57fb58150 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts | |||
@@ -19,10 +19,12 @@ import { | |||
19 | doubleFollow, | 19 | doubleFollow, |
20 | flushAndRunMultipleServers, | 20 | flushAndRunMultipleServers, |
21 | getLive, | 21 | getLive, |
22 | getMyVideosWithFilter, | ||
22 | getPlaylist, | 23 | getPlaylist, |
23 | getVideo, | 24 | getVideo, |
24 | getVideoIdFromUUID, | 25 | getVideoIdFromUUID, |
25 | getVideosList, | 26 | getVideosList, |
27 | getVideosWithFilters, | ||
26 | killallServers, | 28 | killallServers, |
27 | makeRawRequest, | 29 | makeRawRequest, |
28 | removeVideo, | 30 | removeVideo, |
@@ -37,6 +39,7 @@ import { | |||
37 | testImage, | 39 | testImage, |
38 | updateCustomSubConfig, | 40 | updateCustomSubConfig, |
39 | updateLive, | 41 | updateLive, |
42 | uploadVideoAndGetId, | ||
40 | viewVideo, | 43 | viewVideo, |
41 | wait, | 44 | wait, |
42 | waitJobs, | 45 | waitJobs, |
@@ -229,6 +232,68 @@ describe('Test live', function () { | |||
229 | }) | 232 | }) |
230 | }) | 233 | }) |
231 | 234 | ||
235 | describe('Live filters', function () { | ||
236 | let command: any | ||
237 | let liveVideoId: string | ||
238 | let vodVideoId: string | ||
239 | |||
240 | before(async function () { | ||
241 | this.timeout(120000) | ||
242 | |||
243 | vodVideoId = (await uploadVideoAndGetId({ server: servers[0], videoName: 'vod video' })).uuid | ||
244 | |||
245 | const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: servers[0].videoChannel.id } | ||
246 | const resLive = await createLive(servers[0].url, servers[0].accessToken, liveOptions) | ||
247 | liveVideoId = resLive.body.video.uuid | ||
248 | |||
249 | command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId) | ||
250 | await waitUntilLivePublishedOnAllServers(liveVideoId) | ||
251 | await waitJobs(servers) | ||
252 | }) | ||
253 | |||
254 | it('Should only display lives', async function () { | ||
255 | const res = await getVideosWithFilters(servers[0].url, { isLive: true }) | ||
256 | |||
257 | expect(res.body.total).to.equal(1) | ||
258 | expect(res.body.data).to.have.lengthOf(1) | ||
259 | expect(res.body.data[0].name).to.equal('live') | ||
260 | }) | ||
261 | |||
262 | it('Should not display lives', async function () { | ||
263 | const res = await getVideosWithFilters(servers[0].url, { isLive: false }) | ||
264 | |||
265 | expect(res.body.total).to.equal(1) | ||
266 | expect(res.body.data).to.have.lengthOf(1) | ||
267 | expect(res.body.data[0].name).to.equal('vod video') | ||
268 | }) | ||
269 | |||
270 | it('Should display my lives', async function () { | ||
271 | this.timeout(60000) | ||
272 | |||
273 | await stopFfmpeg(command) | ||
274 | await waitJobs(servers) | ||
275 | |||
276 | const res = await getMyVideosWithFilter(servers[0].url, servers[0].accessToken, { isLive: true }) | ||
277 | const videos = res.body.data as Video[] | ||
278 | |||
279 | const result = videos.every(v => v.isLive) | ||
280 | expect(result).to.be.true | ||
281 | }) | ||
282 | |||
283 | it('Should not display my lives', async function () { | ||
284 | const res = await getMyVideosWithFilter(servers[0].url, servers[0].accessToken, { isLive: false }) | ||
285 | const videos = res.body.data as Video[] | ||
286 | |||
287 | const result = videos.every(v => !v.isLive) | ||
288 | expect(result).to.be.true | ||
289 | }) | ||
290 | |||
291 | after(async function () { | ||
292 | await removeVideo(servers[0].url, servers[0].accessToken, vodVideoId) | ||
293 | await removeVideo(servers[0].url, servers[0].accessToken, liveVideoId) | ||
294 | }) | ||
295 | }) | ||
296 | |||
232 | describe('Stream checks', function () { | 297 | describe('Stream checks', function () { |
233 | let liveVideo: LiveVideo & VideoDetails | 298 | let liveVideo: LiveVideo & VideoDetails |
234 | let rtmpUrl: string | 299 | let rtmpUrl: string |
diff --git a/server/tests/api/search/search-videos.ts b/server/tests/api/search/search-videos.ts index e05c3a269..5b8907961 100644 --- a/server/tests/api/search/search-videos.ts +++ b/server/tests/api/search/search-videos.ts | |||
@@ -1,17 +1,24 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | ||
5 | import { VideoPrivacy } from '@shared/models' | ||
5 | import { | 6 | import { |
6 | advancedVideosSearch, | 7 | advancedVideosSearch, |
7 | cleanupTests, | 8 | cleanupTests, |
9 | createLive, | ||
8 | flushAndRunServer, | 10 | flushAndRunServer, |
9 | immutableAssign, | 11 | immutableAssign, |
10 | searchVideo, | 12 | searchVideo, |
13 | sendRTMPStreamInVideo, | ||
11 | ServerInfo, | 14 | ServerInfo, |
12 | setAccessTokensToServers, | 15 | setAccessTokensToServers, |
16 | setDefaultVideoChannel, | ||
17 | stopFfmpeg, | ||
18 | updateCustomSubConfig, | ||
13 | uploadVideo, | 19 | uploadVideo, |
14 | wait | 20 | wait, |
21 | waitUntilLivePublished | ||
15 | } from '../../../../shared/extra-utils' | 22 | } from '../../../../shared/extra-utils' |
16 | import { createVideoCaption } from '../../../../shared/extra-utils/videos/video-captions' | 23 | import { createVideoCaption } from '../../../../shared/extra-utils/videos/video-captions' |
17 | 24 | ||
@@ -28,6 +35,7 @@ describe('Test videos search', function () { | |||
28 | server = await flushAndRunServer(1) | 35 | server = await flushAndRunServer(1) |
29 | 36 | ||
30 | await setAccessTokensToServers([ server ]) | 37 | await setAccessTokensToServers([ server ]) |
38 | await setDefaultVideoChannel([ server ]) | ||
31 | 39 | ||
32 | { | 40 | { |
33 | const attributes1 = { | 41 | const attributes1 = { |
@@ -449,6 +457,43 @@ describe('Test videos search', function () { | |||
449 | expect(res.body.data[0].name).to.equal('1111 2222 3333 - 3') | 457 | expect(res.body.data[0].name).to.equal('1111 2222 3333 - 3') |
450 | }) | 458 | }) |
451 | 459 | ||
460 | it('Should search by live', async function () { | ||
461 | this.timeout(30000) | ||
462 | |||
463 | { | ||
464 | const options = { | ||
465 | search: { | ||
466 | searchIndex: { enabled: false } | ||
467 | }, | ||
468 | live: { enabled: true } | ||
469 | } | ||
470 | await updateCustomSubConfig(server.url, server.accessToken, options) | ||
471 | } | ||
472 | |||
473 | { | ||
474 | const res = await advancedVideosSearch(server.url, { isLive: true }) | ||
475 | |||
476 | expect(res.body.total).to.equal(0) | ||
477 | expect(res.body.data).to.have.lengthOf(0) | ||
478 | } | ||
479 | |||
480 | { | ||
481 | const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: server.videoChannel.id } | ||
482 | const resLive = await createLive(server.url, server.accessToken, liveOptions) | ||
483 | const liveVideoId = resLive.body.video.uuid | ||
484 | |||
485 | const command = await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId) | ||
486 | await waitUntilLivePublished(server.url, server.accessToken, liveVideoId) | ||
487 | |||
488 | const res = await advancedVideosSearch(server.url, { isLive: true }) | ||
489 | |||
490 | expect(res.body.total).to.equal(1) | ||
491 | expect(res.body.data[0].name).to.equal('live') | ||
492 | |||
493 | await stopFfmpeg(command) | ||
494 | } | ||
495 | }) | ||
496 | |||
452 | after(async function () { | 497 | after(async function () { |
453 | await cleanupTests([ server ]) | 498 | await cleanupTests([ server ]) |
454 | }) | 499 | }) |
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts index da90223b8..a79648bf7 100644 --- a/server/tests/api/videos/single-server.ts +++ b/server/tests/api/videos/single-server.ts | |||
@@ -387,11 +387,11 @@ describe('Test a single server', function () { | |||
387 | }) | 387 | }) |
388 | 388 | ||
389 | it('Should filter by tags and category', async function () { | 389 | it('Should filter by tags and category', async function () { |
390 | const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: 4 }) | 390 | const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 4 ] }) |
391 | expect(res1.body.total).to.equal(1) | 391 | expect(res1.body.total).to.equal(1) |
392 | expect(res1.body.data[0].name).to.equal('my super video updated') | 392 | expect(res1.body.data[0].name).to.equal('my super video updated') |
393 | 393 | ||
394 | const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: 3 }) | 394 | const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 3 ] }) |
395 | expect(res2.body.total).to.equal(0) | 395 | expect(res2.body.total).to.equal(0) |
396 | }) | 396 | }) |
397 | 397 | ||
diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts index 67fe82d41..a0143b0ef 100644 --- a/shared/extra-utils/videos/videos.ts +++ b/shared/extra-utils/videos/videos.ts | |||
@@ -8,6 +8,7 @@ import * as request from 'supertest' | |||
8 | import { v4 as uuidv4 } from 'uuid' | 8 | import { v4 as uuidv4 } from 'uuid' |
9 | import validator from 'validator' | 9 | import validator from 'validator' |
10 | import { HttpStatusCode } from '@shared/core-utils' | 10 | import { HttpStatusCode } from '@shared/core-utils' |
11 | import { VideosCommonQuery } from '@shared/models' | ||
11 | import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants' | 12 | import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants' |
12 | import { VideoDetails, VideoPrivacy } from '../../models/videos' | 13 | import { VideoDetails, VideoPrivacy } from '../../models/videos' |
13 | import { | 14 | import { |
@@ -195,6 +196,18 @@ function getMyVideos (url: string, accessToken: string, start: number, count: nu | |||
195 | .expect('Content-Type', /json/) | 196 | .expect('Content-Type', /json/) |
196 | } | 197 | } |
197 | 198 | ||
199 | function getMyVideosWithFilter (url: string, accessToken: string, query: { isLive?: boolean }) { | ||
200 | const path = '/api/v1/users/me/videos' | ||
201 | |||
202 | return makeGetRequest({ | ||
203 | url, | ||
204 | path, | ||
205 | token: accessToken, | ||
206 | query, | ||
207 | statusCodeExpected: HttpStatusCode.OK_200 | ||
208 | }) | ||
209 | } | ||
210 | |||
198 | function getAccountVideos ( | 211 | function getAccountVideos ( |
199 | url: string, | 212 | url: string, |
200 | accessToken: string, | 213 | accessToken: string, |
@@ -295,7 +308,7 @@ function getVideosListSort (url: string, sort: string) { | |||
295 | .expect('Content-Type', /json/) | 308 | .expect('Content-Type', /json/) |
296 | } | 309 | } |
297 | 310 | ||
298 | function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) { | 311 | function getVideosWithFilters (url: string, query: VideosCommonQuery) { |
299 | const path = '/api/v1/videos' | 312 | const path = '/api/v1/videos' |
300 | 313 | ||
301 | return request(url) | 314 | return request(url) |
@@ -751,6 +764,7 @@ export { | |||
751 | completeVideoCheck, | 764 | completeVideoCheck, |
752 | checkVideoFilesWereRemoved, | 765 | checkVideoFilesWereRemoved, |
753 | getPlaylistVideos, | 766 | getPlaylistVideos, |
767 | getMyVideosWithFilter, | ||
754 | uploadVideoAndGetId, | 768 | uploadVideoAndGetId, |
755 | getLocalIdByUUID, | 769 | getLocalIdByUUID, |
756 | getVideoIdFromUUID | 770 | getVideoIdFromUUID |
diff --git a/shared/models/search/boolean-both-query.model.ts b/shared/models/search/boolean-both-query.model.ts new file mode 100644 index 000000000..57b0e8d44 --- /dev/null +++ b/shared/models/search/boolean-both-query.model.ts | |||
@@ -0,0 +1 @@ | |||
export type BooleanBothQuery = 'true' | 'false' | 'both' | |||
diff --git a/shared/models/search/index.ts b/shared/models/search/index.ts index e2d0ab620..697ceccb1 100644 --- a/shared/models/search/index.ts +++ b/shared/models/search/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export * from './nsfw-query.model' | 1 | export * from './boolean-both-query.model' |
2 | export * from './search-target-query.model' | 2 | export * from './search-target-query.model' |
3 | export * from './videos-common-query.model' | ||
3 | export * from './videos-search-query.model' | 4 | export * from './videos-search-query.model' |
4 | export * from './video-channels-search-query.model' | 5 | export * from './video-channels-search-query.model' |
diff --git a/shared/models/search/nsfw-query.model.ts b/shared/models/search/nsfw-query.model.ts deleted file mode 100644 index 6b6ad1991..000000000 --- a/shared/models/search/nsfw-query.model.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export type NSFWQuery = 'true' | 'false' | 'both' | ||
diff --git a/shared/models/search/videos-common-query.model.ts b/shared/models/search/videos-common-query.model.ts new file mode 100644 index 000000000..bd02489ea --- /dev/null +++ b/shared/models/search/videos-common-query.model.ts | |||
@@ -0,0 +1,28 @@ | |||
1 | import { VideoFilter } from '../videos' | ||
2 | import { BooleanBothQuery } from './boolean-both-query.model' | ||
3 | |||
4 | // These query parameters can be used with any endpoint that list videos | ||
5 | export interface VideosCommonQuery { | ||
6 | start?: number | ||
7 | count?: number | ||
8 | sort?: string | ||
9 | |||
10 | nsfw?: BooleanBothQuery | ||
11 | |||
12 | isLive?: boolean | ||
13 | |||
14 | categoryOneOf?: number[] | ||
15 | |||
16 | licenceOneOf?: number[] | ||
17 | |||
18 | languageOneOf?: string[] | ||
19 | |||
20 | tagsOneOf?: string[] | ||
21 | tagsAllOf?: string[] | ||
22 | |||
23 | filter?: VideoFilter | ||
24 | } | ||
25 | |||
26 | export interface VideosWithSearchCommonQuery extends VideosCommonQuery { | ||
27 | search?: string | ||
28 | } | ||
diff --git a/shared/models/search/videos-search-query.model.ts b/shared/models/search/videos-search-query.model.ts index 3ce4ff73e..406f6cab2 100644 --- a/shared/models/search/videos-search-query.model.ts +++ b/shared/models/search/videos-search-query.model.ts | |||
@@ -1,33 +1,15 @@ | |||
1 | import { VideoFilter } from '../videos' | ||
2 | import { NSFWQuery } from './nsfw-query.model' | ||
3 | import { SearchTargetQuery } from './search-target-query.model' | 1 | import { SearchTargetQuery } from './search-target-query.model' |
2 | import { VideosCommonQuery } from './videos-common-query.model' | ||
4 | 3 | ||
5 | export interface VideosSearchQuery extends SearchTargetQuery { | 4 | export interface VideosSearchQuery extends SearchTargetQuery, VideosCommonQuery { |
6 | search?: string | 5 | search?: string |
7 | 6 | ||
8 | start?: number | ||
9 | count?: number | ||
10 | sort?: string | ||
11 | |||
12 | startDate?: string // ISO 8601 | 7 | startDate?: string // ISO 8601 |
13 | endDate?: string // ISO 8601 | 8 | endDate?: string // ISO 8601 |
14 | 9 | ||
15 | originallyPublishedStartDate?: string // ISO 8601 | 10 | originallyPublishedStartDate?: string // ISO 8601 |
16 | originallyPublishedEndDate?: string // ISO 8601 | 11 | originallyPublishedEndDate?: string // ISO 8601 |
17 | 12 | ||
18 | nsfw?: NSFWQuery | ||
19 | |||
20 | categoryOneOf?: number[] | ||
21 | |||
22 | licenceOneOf?: number[] | ||
23 | |||
24 | languageOneOf?: string[] | ||
25 | |||
26 | tagsOneOf?: string[] | ||
27 | tagsAllOf?: string[] | ||
28 | |||
29 | durationMin?: number // seconds | 13 | durationMin?: number // seconds |
30 | durationMax?: number // seconds | 14 | durationMax?: number // seconds |
31 | |||
32 | filter?: VideoFilter | ||
33 | } | 15 | } |
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 1fffe7ddf..da51732ad 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml | |||
@@ -210,6 +210,7 @@ paths: | |||
210 | parameters: | 210 | parameters: |
211 | - $ref: '#/components/parameters/name' | 211 | - $ref: '#/components/parameters/name' |
212 | - $ref: '#/components/parameters/categoryOneOf' | 212 | - $ref: '#/components/parameters/categoryOneOf' |
213 | - $ref: '#/components/parameters/isLive' | ||
213 | - $ref: '#/components/parameters/tagsOneOf' | 214 | - $ref: '#/components/parameters/tagsOneOf' |
214 | - $ref: '#/components/parameters/tagsAllOf' | 215 | - $ref: '#/components/parameters/tagsAllOf' |
215 | - $ref: '#/components/parameters/licenceOneOf' | 216 | - $ref: '#/components/parameters/licenceOneOf' |
@@ -781,6 +782,7 @@ paths: | |||
781 | - Videos | 782 | - Videos |
782 | parameters: | 783 | parameters: |
783 | - $ref: '#/components/parameters/categoryOneOf' | 784 | - $ref: '#/components/parameters/categoryOneOf' |
785 | - $ref: '#/components/parameters/isLive' | ||
784 | - $ref: '#/components/parameters/tagsOneOf' | 786 | - $ref: '#/components/parameters/tagsOneOf' |
785 | - $ref: '#/components/parameters/tagsAllOf' | 787 | - $ref: '#/components/parameters/tagsAllOf' |
786 | - $ref: '#/components/parameters/licenceOneOf' | 788 | - $ref: '#/components/parameters/licenceOneOf' |
@@ -1086,6 +1088,7 @@ paths: | |||
1086 | - Video | 1088 | - Video |
1087 | parameters: | 1089 | parameters: |
1088 | - $ref: '#/components/parameters/categoryOneOf' | 1090 | - $ref: '#/components/parameters/categoryOneOf' |
1091 | - $ref: '#/components/parameters/isLive' | ||
1089 | - $ref: '#/components/parameters/tagsOneOf' | 1092 | - $ref: '#/components/parameters/tagsOneOf' |
1090 | - $ref: '#/components/parameters/tagsAllOf' | 1093 | - $ref: '#/components/parameters/tagsAllOf' |
1091 | - $ref: '#/components/parameters/licenceOneOf' | 1094 | - $ref: '#/components/parameters/licenceOneOf' |
@@ -2194,6 +2197,7 @@ paths: | |||
2194 | parameters: | 2197 | parameters: |
2195 | - $ref: '#/components/parameters/channelHandle' | 2198 | - $ref: '#/components/parameters/channelHandle' |
2196 | - $ref: '#/components/parameters/categoryOneOf' | 2199 | - $ref: '#/components/parameters/categoryOneOf' |
2200 | - $ref: '#/components/parameters/isLive' | ||
2197 | - $ref: '#/components/parameters/tagsOneOf' | 2201 | - $ref: '#/components/parameters/tagsOneOf' |
2198 | - $ref: '#/components/parameters/tagsAllOf' | 2202 | - $ref: '#/components/parameters/tagsAllOf' |
2199 | - $ref: '#/components/parameters/licenceOneOf' | 2203 | - $ref: '#/components/parameters/licenceOneOf' |
@@ -2841,6 +2845,7 @@ paths: | |||
2841 | schema: | 2845 | schema: |
2842 | type: string | 2846 | type: string |
2843 | - $ref: '#/components/parameters/categoryOneOf' | 2847 | - $ref: '#/components/parameters/categoryOneOf' |
2848 | - $ref: '#/components/parameters/isLive' | ||
2844 | - $ref: '#/components/parameters/tagsOneOf' | 2849 | - $ref: '#/components/parameters/tagsOneOf' |
2845 | - $ref: '#/components/parameters/tagsAllOf' | 2850 | - $ref: '#/components/parameters/tagsAllOf' |
2846 | - $ref: '#/components/parameters/licenceOneOf' | 2851 | - $ref: '#/components/parameters/licenceOneOf' |
@@ -3809,6 +3814,13 @@ components: | |||
3809 | description: The comment id | 3814 | description: The comment id |
3810 | schema: | 3815 | schema: |
3811 | type: integer | 3816 | type: integer |
3817 | isLive: | ||
3818 | name: isLive | ||
3819 | in: query | ||
3820 | required: false | ||
3821 | description: whether or not the video is a live | ||
3822 | schema: | ||
3823 | type: boolean | ||
3812 | categoryOneOf: | 3824 | categoryOneOf: |
3813 | name: categoryOneOf | 3825 | name: categoryOneOf |
3814 | in: query | 3826 | in: query |