diff options
32 files changed, 295 insertions, 102 deletions
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 571c780d6..e8a084259 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 | |||
@@ -112,8 +112,10 @@ | |||
112 | </a> | 112 | </a> |
113 | </td> | 113 | </td> |
114 | 114 | ||
115 | <td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus" [title]="user.email"> | 115 | <td *ngIf="getColumn('email')" [title]="user.email"> |
116 | <a class="table-email" [href]="'mailto:' + user.email">{{ user.email }}</a> | 116 | <ng-container *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus"> |
117 | <a class="table-email" [href]="'mailto:' + user.email">{{ user.email }}</a> | ||
118 | </ng-container> | ||
117 | </td> | 119 | </td> |
118 | 120 | ||
119 | <ng-template #emailWithVerificationStatus> | 121 | <ng-template #emailWithVerificationStatus> |
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 b2978212e..699b2a6da 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 | |||
@@ -7,6 +7,13 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
7 | import { ServerConfig, User, UserRole } from '@shared/models' | 7 | import { ServerConfig, User, UserRole } from '@shared/models' |
8 | import { Params, Router, ActivatedRoute } from '@angular/router' | 8 | import { Params, Router, ActivatedRoute } from '@angular/router' |
9 | 9 | ||
10 | type UserForList = User & { | ||
11 | rawVideoQuota: number | ||
12 | rawVideoQuotaUsed: number | ||
13 | rawVideoQuotaDaily: number | ||
14 | rawVideoQuotaUsedDaily: number | ||
15 | } | ||
16 | |||
10 | @Component({ | 17 | @Component({ |
11 | selector: 'my-user-list', | 18 | selector: 'my-user-list', |
12 | templateUrl: './user-list.component.html', | 19 | templateUrl: './user-list.component.html', |
@@ -24,8 +31,8 @@ export class UserListComponent extends RestTable implements OnInit { | |||
24 | selectedUsers: User[] = [] | 31 | selectedUsers: User[] = [] |
25 | bulkUserActions: DropdownAction<User[]>[][] = [] | 32 | bulkUserActions: DropdownAction<User[]>[][] = [] |
26 | columns: { key: string, label: string }[] | 33 | columns: { key: string, label: string }[] |
27 | _selectedColumns: { key: string, label: string }[] | ||
28 | 34 | ||
35 | private _selectedColumns: { key: string, label: string }[] | ||
29 | private serverConfig: ServerConfig | 36 | private serverConfig: ServerConfig |
30 | 37 | ||
31 | constructor ( | 38 | constructor ( |
@@ -111,7 +118,7 @@ export class UserListComponent extends RestTable implements OnInit { | |||
111 | { key: 'role', label: 'Role' }, | 118 | { key: 'role', label: 'Role' }, |
112 | { key: 'createdAt', label: 'Created' } | 119 | { key: 'createdAt', label: 'Created' } |
113 | ] | 120 | ] |
114 | this.selectedColumns = [...this.columns] | 121 | this.selectedColumns = [ ...this.columns ] // make a full copy of the array |
115 | this.columns.push({ key: 'quotaDaily', label: 'Daily quota' }) | 122 | this.columns.push({ key: 'quotaDaily', label: 'Daily quota' }) |
116 | this.columns.push({ key: 'pluginAuth', label: 'Auth plugin' }) | 123 | this.columns.push({ key: 'pluginAuth', label: 'Auth plugin' }) |
117 | this.columns.push({ key: 'lastLoginDate', label: 'Last login' }) | 124 | this.columns.push({ key: 'lastLoginDate', label: 'Last login' }) |
@@ -133,14 +140,14 @@ export class UserListComponent extends RestTable implements OnInit { | |||
133 | } | 140 | } |
134 | 141 | ||
135 | getColumn (key: string) { | 142 | getColumn (key: string) { |
136 | return this.selectedColumns.find((col: any) => col.key === key) | 143 | return this.selectedColumns.find((col: { key: string }) => col.key === key) |
137 | } | 144 | } |
138 | 145 | ||
139 | getUserVideoQuotaPercentage (user: User & { rawVideoQuota: number, rawVideoQuotaUsed: number}) { | 146 | getUserVideoQuotaPercentage (user: UserForList) { |
140 | return user.rawVideoQuotaUsed * 100 / user.rawVideoQuota | 147 | return user.rawVideoQuotaUsed * 100 / user.rawVideoQuota |
141 | } | 148 | } |
142 | 149 | ||
143 | getUserVideoQuotaDailyPercentage (user: User & { rawVideoQuotaDaily: number, rawVideoQuotaUsedDaily: number}) { | 150 | getUserVideoQuotaDailyPercentage (user: UserForList) { |
144 | return user.rawVideoQuotaUsedDaily * 100 / user.rawVideoQuotaDaily | 151 | return user.rawVideoQuotaUsedDaily * 100 / user.rawVideoQuotaDaily |
145 | } | 152 | } |
146 | 153 | ||
diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html index e8d44a45e..c20215cf9 100644 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html +++ b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html | |||
@@ -1,14 +1,21 @@ | |||
1 | <h1> | 1 | <h1 class="d-flex justify-content-between"> |
2 | <my-global-icon iconName="channel" aria-hidden="true"></my-global-icon> | 2 | <span> |
3 | <ng-container i18n>My channels</ng-container> | 3 | <my-global-icon iconName="channel" aria-hidden="true"></my-global-icon> |
4 | </h1> | 4 | <ng-container i18n>My channels</ng-container> |
5 | <span class="badge badge-secondary">{{ totalItems }}</span> | ||
6 | </span> | ||
7 | |||
8 | <div class="has-feedback has-clear"> | ||
9 | <input type="text" placeholder="Search your channels" i18n-placeholder [(ngModel)]="channelsSearch" (ngModelChange)="onChannelsSearchChanged()" /> | ||
10 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> | ||
11 | <span class="sr-only" i18n>Clear filters</span> | ||
12 | </div> | ||
5 | 13 | ||
6 | <div class="video-channels-header"> | ||
7 | <a class="create-button" routerLink="create"> | 14 | <a class="create-button" routerLink="create"> |
8 | <my-global-icon iconName="add" aria-hidden="true"></my-global-icon> | 15 | <my-global-icon iconName="add" aria-hidden="true"></my-global-icon> |
9 | <ng-container i18n>Create video channel</ng-container> | 16 | <ng-container i18n>Create video channel</ng-container> |
10 | </a> | 17 | </a> |
11 | </div> | 18 | </h1> |
12 | 19 | ||
13 | <div class="video-channels"> | 20 | <div class="video-channels"> |
14 | <div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel"> | 21 | <div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel"> |
diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.scss b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.scss index 76fb2cde0..4ecb4f408 100644 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.scss +++ b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.scss | |||
@@ -5,6 +5,10 @@ | |||
5 | @include create-button; | 5 | @include create-button; |
6 | } | 6 | } |
7 | 7 | ||
8 | input[type=text] { | ||
9 | @include peertube-input-text(300px); | ||
10 | } | ||
11 | |||
8 | ::ng-deep .action-button { | 12 | ::ng-deep .action-button { |
9 | &.action-button-edit { | 13 | &.action-button-edit { |
10 | margin-right: 10px; | 14 | margin-right: 10px; |
@@ -55,11 +59,6 @@ | |||
55 | } | 59 | } |
56 | } | 60 | } |
57 | 61 | ||
58 | .video-channels-header { | ||
59 | text-align: right; | ||
60 | margin: 20px 0 50px; | ||
61 | } | ||
62 | |||
63 | ::ng-deep .chartjs-render-monitor { | 62 | ::ng-deep .chartjs-render-monitor { |
64 | position: relative; | 63 | position: relative; |
65 | top: 1px; | 64 | top: 1px; |
diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts index 70510d7c9..da8c7298f 100644 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts +++ b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts | |||
@@ -1,10 +1,11 @@ | |||
1 | import { ChartData } from 'chart.js' | 1 | import { ChartData } from 'chart.js' |
2 | import { max, maxBy, min, minBy } from 'lodash-es' | 2 | import { max, maxBy, min, minBy } from 'lodash-es' |
3 | import { flatMap } from 'rxjs/operators' | 3 | import { flatMap, debounceTime } from 'rxjs/operators' |
4 | import { Component, OnInit } from '@angular/core' | 4 | import { Component, OnInit } from '@angular/core' |
5 | import { AuthService, ConfirmService, Notifier, ScreenService, User } from '@app/core' | 5 | import { AuthService, ConfirmService, Notifier, ScreenService, User } from '@app/core' |
6 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 6 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' |
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 7 | import { I18n } from '@ngx-translate/i18n-polyfill' |
8 | import { Subject } from 'rxjs' | ||
8 | 9 | ||
9 | @Component({ | 10 | @Component({ |
10 | selector: 'my-account-video-channels', | 11 | selector: 'my-account-video-channels', |
@@ -12,11 +13,16 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
12 | styleUrls: [ './my-account-video-channels.component.scss' ] | 13 | styleUrls: [ './my-account-video-channels.component.scss' ] |
13 | }) | 14 | }) |
14 | export class MyAccountVideoChannelsComponent implements OnInit { | 15 | export class MyAccountVideoChannelsComponent implements OnInit { |
16 | totalItems: number | ||
17 | |||
15 | videoChannels: VideoChannel[] = [] | 18 | videoChannels: VideoChannel[] = [] |
16 | videoChannelsChartData: ChartData[] | 19 | videoChannelsChartData: ChartData[] |
17 | videoChannelsMinimumDailyViews = 0 | 20 | videoChannelsMinimumDailyViews = 0 |
18 | videoChannelsMaximumDailyViews: number | 21 | videoChannelsMaximumDailyViews: number |
19 | 22 | ||
23 | channelsSearch: string | ||
24 | channelsSearchChanged = new Subject<string>() | ||
25 | |||
20 | private user: User | 26 | private user: User |
21 | 27 | ||
22 | constructor ( | 28 | constructor ( |
@@ -32,6 +38,12 @@ export class MyAccountVideoChannelsComponent implements OnInit { | |||
32 | this.user = this.authService.getUser() | 38 | this.user = this.authService.getUser() |
33 | 39 | ||
34 | this.loadVideoChannels() | 40 | this.loadVideoChannels() |
41 | |||
42 | this.channelsSearchChanged | ||
43 | .pipe(debounceTime(500)) | ||
44 | .subscribe(() => { | ||
45 | this.loadVideoChannels() | ||
46 | }) | ||
35 | } | 47 | } |
36 | 48 | ||
37 | get isInSmallView () { | 49 | get isInSmallView () { |
@@ -87,6 +99,15 @@ export class MyAccountVideoChannelsComponent implements OnInit { | |||
87 | } | 99 | } |
88 | } | 100 | } |
89 | 101 | ||
102 | resetSearch() { | ||
103 | this.channelsSearch = '' | ||
104 | this.onChannelsSearchChanged() | ||
105 | } | ||
106 | |||
107 | onChannelsSearchChanged () { | ||
108 | this.channelsSearchChanged.next() | ||
109 | } | ||
110 | |||
90 | async deleteVideoChannel (videoChannel: VideoChannel) { | 111 | async deleteVideoChannel (videoChannel: VideoChannel) { |
91 | const res = await this.confirmService.confirmWithInput( | 112 | const res = await this.confirmService.confirmWithInput( |
92 | this.i18n( | 113 | this.i18n( |
@@ -118,9 +139,10 @@ export class MyAccountVideoChannelsComponent implements OnInit { | |||
118 | 139 | ||
119 | private loadVideoChannels () { | 140 | private loadVideoChannels () { |
120 | this.authService.userInformationLoaded | 141 | this.authService.userInformationLoaded |
121 | .pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true))) | 142 | .pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true, this.channelsSearch))) |
122 | .subscribe(res => { | 143 | .subscribe(res => { |
123 | this.videoChannels = res.data | 144 | this.videoChannels = res.data |
145 | this.totalItems = res.total | ||
124 | 146 | ||
125 | // chart data | 147 | // chart data |
126 | this.videoChannelsChartData = this.videoChannels.map(v => ({ | 148 | this.videoChannelsChartData = this.videoChannels.map(v => ({ |
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html index 9b5f2dd2f..8de152b5e 100644 --- a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html | |||
@@ -6,10 +6,10 @@ | |||
6 | </a> | 6 | </a> |
7 | 7 | ||
8 | <div class="peertube-select-container peertube-select-button ml-2"> | 8 | <div class="peertube-select-container peertube-select-button ml-2"> |
9 | <select [(ngModel)]="notificationSortType" class="form-control"> | 9 | <select [(ngModel)]="notificationSortType" (ngModelChange)="onChangeSortColumn()" class="form-control"> |
10 | <option value="undefined" disabled>Sort by</option> | 10 | <option value="undefined" disabled>Sort by</option> |
11 | <option value="created" i18n>Newest first</option> | 11 | <option value="createdAt" i18n>Newest first</option> |
12 | <option value="unread-created" i18n>Unread first</option> | 12 | <option value="read" [disabled]="!hasUnreadNotifications()" i18n>Unread first</option> |
13 | </select> | 13 | </select> |
14 | </div> | 14 | </div> |
15 | 15 | ||
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts index 8a51319fe..0ec67d401 100644 --- a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts | |||
@@ -1,6 +1,8 @@ | |||
1 | import { Component, ViewChild } from '@angular/core' | 1 | import { Component, ViewChild } from '@angular/core' |
2 | import { UserNotificationsComponent } from '@app/shared/shared-main' | 2 | import { UserNotificationsComponent } from '@app/shared/shared-main' |
3 | 3 | ||
4 | type NotificationSortType = 'createdAt' | 'read' | ||
5 | |||
4 | @Component({ | 6 | @Component({ |
5 | templateUrl: './my-account-notifications.component.html', | 7 | templateUrl: './my-account-notifications.component.html', |
6 | styleUrls: [ './my-account-notifications.component.scss' ] | 8 | styleUrls: [ './my-account-notifications.component.scss' ] |
@@ -8,7 +10,17 @@ import { UserNotificationsComponent } from '@app/shared/shared-main' | |||
8 | export class MyAccountNotificationsComponent { | 10 | export class MyAccountNotificationsComponent { |
9 | @ViewChild('userNotification', { static: true }) userNotification: UserNotificationsComponent | 11 | @ViewChild('userNotification', { static: true }) userNotification: UserNotificationsComponent |
10 | 12 | ||
11 | notificationSortType = 'created' | 13 | _notificationSortType: NotificationSortType = 'createdAt' |
14 | |||
15 | get notificationSortType () { | ||
16 | return !this.hasUnreadNotifications() | ||
17 | ? 'createdAt' | ||
18 | : this._notificationSortType | ||
19 | } | ||
20 | |||
21 | set notificationSortType (type: NotificationSortType) { | ||
22 | this._notificationSortType = type | ||
23 | } | ||
12 | 24 | ||
13 | markAllAsRead () { | 25 | markAllAsRead () { |
14 | this.userNotification.markAllAsRead() | 26 | this.userNotification.markAllAsRead() |
@@ -17,4 +29,8 @@ export class MyAccountNotificationsComponent { | |||
17 | hasUnreadNotifications () { | 29 | hasUnreadNotifications () { |
18 | return this.userNotification.notifications.filter(n => n.read === false).length !== 0 | 30 | return this.userNotification.notifications.filter(n => n.read === false).length !== 0 |
19 | } | 31 | } |
32 | |||
33 | onChangeSortColumn () { | ||
34 | this.userNotification.changeSortColumn(this.notificationSortType) | ||
35 | } | ||
20 | } | 36 | } |
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html index be5d41f3b..4475178c7 100644 --- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html +++ b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html | |||
@@ -62,7 +62,11 @@ | |||
62 | </td> | 62 | </td> |
63 | 63 | ||
64 | <td>{{ videoChangeOwnership.createdAt | date: 'short' }}</td> | 64 | <td>{{ videoChangeOwnership.createdAt | date: 'short' }}</td> |
65 | <td i18n>{{ videoChangeOwnership.status }}</td> | 65 | |
66 | <td> | ||
67 | <span class="badge" [ngClass]="getStatusClass(videoChangeOwnership.status)">{{ videoChangeOwnership.status }}</span> | ||
68 | </td> | ||
69 | |||
66 | <td class="action-cell"> | 70 | <td class="action-cell"> |
67 | <ng-container *ngIf="videoChangeOwnership.status === 'WAITING'"> | 71 | <ng-container *ngIf="videoChangeOwnership.status === 'WAITING'"> |
68 | <my-button i18n-label label="Accept" icon="tick" (click)="openAcceptModal(videoChangeOwnership)"></my-button> | 72 | <my-button i18n-label label="Accept" icon="tick" (click)="openAcceptModal(videoChangeOwnership)"></my-button> |
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.scss b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.scss index c04e26374..7cac9c9f3 100644 --- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.scss +++ b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.scss | |||
@@ -5,6 +5,10 @@ | |||
5 | @include chip; | 5 | @include chip; |
6 | } | 6 | } |
7 | 7 | ||
8 | .badge { | ||
9 | @include table-badge; | ||
10 | } | ||
11 | |||
8 | .video-table-video { | 12 | .video-table-video { |
9 | display: inline-flex; | 13 | display: inline-flex; |
10 | 14 | ||
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts index 98360dfb3..7473470aa 100644 --- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts +++ b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts | |||
@@ -2,7 +2,7 @@ import { SortMeta } from 'primeng/api' | |||
2 | import { Component, OnInit, ViewChild } from '@angular/core' | 2 | import { Component, OnInit, ViewChild } from '@angular/core' |
3 | import { Notifier, RestPagination, RestTable } from '@app/core' | 3 | import { Notifier, RestPagination, RestTable } from '@app/core' |
4 | import { VideoOwnershipService, Actor, Video, Account } from '@app/shared/shared-main' | 4 | import { VideoOwnershipService, Actor, Video, Account } from '@app/shared/shared-main' |
5 | import { VideoChangeOwnership } from '@shared/models' | 5 | import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '@shared/models' |
6 | import { MyAccountAcceptOwnershipComponent } from './my-account-accept-ownership/my-account-accept-ownership.component' | 6 | import { MyAccountAcceptOwnershipComponent } from './my-account-accept-ownership/my-account-accept-ownership.component' |
7 | import { getAbsoluteAPIUrl } from '@app/helpers' | 7 | import { getAbsoluteAPIUrl } from '@app/helpers' |
8 | 8 | ||
@@ -34,6 +34,17 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit { | |||
34 | return 'MyAccountOwnershipComponent' | 34 | return 'MyAccountOwnershipComponent' |
35 | } | 35 | } |
36 | 36 | ||
37 | getStatusClass (status: VideoChangeOwnershipStatus) { | ||
38 | switch (status) { | ||
39 | case VideoChangeOwnershipStatus.ACCEPTED: | ||
40 | return 'badge-green' | ||
41 | case VideoChangeOwnershipStatus.REFUSED: | ||
42 | return 'badge-red' | ||
43 | default: | ||
44 | return 'badge-yellow' | ||
45 | } | ||
46 | } | ||
47 | |||
37 | switchToDefaultAvatar ($event: Event) { | 48 | switchToDefaultAvatar ($event: Event) { |
38 | ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() | 49 | ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() |
39 | } | 50 | } |
diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html index 3b4c3022e..6cec7c0d5 100644 --- a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html +++ b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html | |||
@@ -1,6 +1,15 @@ | |||
1 | <h1> | 1 | <h1 class="d-flex justify-content-between"> |
2 | <my-global-icon iconName="subscriptions" aria-hidden="true"></my-global-icon> | 2 | <span> |
3 | <ng-container i18n>My subscriptions</ng-container> | 3 | <my-global-icon iconName="subscriptions" aria-hidden="true"></my-global-icon> |
4 | <ng-container i18n>My subscriptions</ng-container> | ||
5 | <span class="badge badge-secondary"> {{ pagination.totalItems }}</span> | ||
6 | </span> | ||
7 | |||
8 | <div class="has-feedback has-clear"> | ||
9 | <input type="text" placeholder="Search your subscriptions" i18n-placeholder [(ngModel)]="subscriptionsSearch" (ngModelChange)="onSubscriptionsSearchChanged()" /> | ||
10 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> | ||
11 | <span class="sr-only" i18n>Clear filters</span> | ||
12 | </div> | ||
4 | </h1> | 13 | </h1> |
5 | 14 | ||
6 | <div class="no-results" i18n *ngIf="pagination.totalItems === 0">You don't have any subscriptions yet.</div> | 15 | <div class="no-results" i18n *ngIf="pagination.totalItems === 0">You don't have any subscriptions yet.</div> |
diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss index dd990c42b..884959070 100644 --- a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss +++ b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss | |||
@@ -1,6 +1,10 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | input[type=text] { | ||
5 | @include peertube-input-text(300px); | ||
6 | } | ||
7 | |||
4 | .video-channel { | 8 | .video-channel { |
5 | @include row-blocks; | 9 | @include row-blocks; |
6 | 10 | ||
diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts index 390293a28..994fe5142 100644 --- a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts +++ b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts | |||
@@ -3,6 +3,7 @@ import { Component, OnInit } from '@angular/core' | |||
3 | import { ComponentPagination, Notifier } from '@app/core' | 3 | import { ComponentPagination, Notifier } from '@app/core' |
4 | import { VideoChannel } from '@app/shared/shared-main' | 4 | import { VideoChannel } from '@app/shared/shared-main' |
5 | import { UserSubscriptionService } from '@app/shared/shared-user-subscription' | 5 | import { UserSubscriptionService } from '@app/shared/shared-user-subscription' |
6 | import { debounceTime } from 'rxjs/operators' | ||
6 | 7 | ||
7 | @Component({ | 8 | @Component({ |
8 | selector: 'my-account-subscriptions', | 9 | selector: 'my-account-subscriptions', |
@@ -20,6 +21,9 @@ export class MyAccountSubscriptionsComponent implements OnInit { | |||
20 | 21 | ||
21 | onDataSubject = new Subject<any[]>() | 22 | onDataSubject = new Subject<any[]>() |
22 | 23 | ||
24 | subscriptionsSearch: string | ||
25 | subscriptionsSearchChanged = new Subject<string>() | ||
26 | |||
23 | constructor ( | 27 | constructor ( |
24 | private userSubscriptionService: UserSubscriptionService, | 28 | private userSubscriptionService: UserSubscriptionService, |
25 | private notifier: Notifier | 29 | private notifier: Notifier |
@@ -27,20 +31,22 @@ export class MyAccountSubscriptionsComponent implements OnInit { | |||
27 | 31 | ||
28 | ngOnInit () { | 32 | ngOnInit () { |
29 | this.loadSubscriptions() | 33 | this.loadSubscriptions() |
30 | } | ||
31 | 34 | ||
32 | loadSubscriptions () { | 35 | this.subscriptionsSearchChanged |
33 | this.userSubscriptionService.listSubscriptions(this.pagination) | 36 | .pipe(debounceTime(500)) |
34 | .subscribe( | 37 | .subscribe(() => { |
35 | res => { | 38 | this.pagination.currentPage = 1 |
36 | this.videoChannels = this.videoChannels.concat(res.data) | 39 | this.loadSubscriptions(false) |
37 | this.pagination.totalItems = res.total | 40 | }) |
41 | } | ||
38 | 42 | ||
39 | this.onDataSubject.next(res.data) | 43 | resetSearch () { |
40 | }, | 44 | this.subscriptionsSearch = '' |
45 | this.onSubscriptionsSearchChanged() | ||
46 | } | ||
41 | 47 | ||
42 | error => this.notifier.error(error.message) | 48 | onSubscriptionsSearchChanged () { |
43 | ) | 49 | this.subscriptionsSearchChanged.next() |
44 | } | 50 | } |
45 | 51 | ||
46 | onNearOfBottom () { | 52 | onNearOfBottom () { |
@@ -51,4 +57,19 @@ export class MyAccountSubscriptionsComponent implements OnInit { | |||
51 | this.loadSubscriptions() | 57 | this.loadSubscriptions() |
52 | } | 58 | } |
53 | 59 | ||
60 | private loadSubscriptions (more = true) { | ||
61 | this.userSubscriptionService.listSubscriptions({ pagination: this.pagination, search: this.subscriptionsSearch }) | ||
62 | .subscribe( | ||
63 | res => { | ||
64 | this.videoChannels = more | ||
65 | ? this.videoChannels.concat(res.data) | ||
66 | : res.data | ||
67 | this.pagination.totalItems = res.total | ||
68 | |||
69 | this.onDataSubject.next(res.data) | ||
70 | }, | ||
71 | |||
72 | error => this.notifier.error(error.message) | ||
73 | ) | ||
74 | } | ||
54 | } | 75 | } |
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html index 98a2039cc..854126443 100644 --- a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html +++ b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html | |||
@@ -45,7 +45,12 @@ | |||
45 | <ng-container *ngIf="isVideoImportFailed(videoImport)"></ng-container> | 45 | <ng-container *ngIf="isVideoImportFailed(videoImport)"></ng-container> |
46 | </td> | 46 | </td> |
47 | 47 | ||
48 | <td>{{ videoImport.state.label }}</td> | 48 | <td> |
49 | <span class="badge" [ngClass]="getVideoImportStateClass(videoImport.state)"> | ||
50 | {{ videoImport.state.label }} | ||
51 | </span> | ||
52 | </td> | ||
53 | |||
49 | <td>{{ videoImport.createdAt | date: 'short' }}</td> | 54 | <td>{{ videoImport.createdAt | date: 'short' }}</td> |
50 | 55 | ||
51 | <td class="action-cell"> | 56 | <td class="action-cell"> |
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss index bdd2f8270..a93c28028 100644 --- a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss +++ b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss | |||
@@ -7,4 +7,8 @@ pre { | |||
7 | 7 | ||
8 | .video-import-error { | 8 | .video-import-error { |
9 | color: red; | 9 | color: red; |
10 | } \ No newline at end of file | 10 | } |
11 | |||
12 | .badge { | ||
13 | @include table-badge; | ||
14 | } | ||
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts index 42ddb0ee2..9dd5ef142 100644 --- a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts +++ b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts | |||
@@ -30,6 +30,19 @@ export class MyAccountVideoImportsComponent extends RestTable implements OnInit | |||
30 | return 'MyAccountVideoImportsComponent' | 30 | return 'MyAccountVideoImportsComponent' |
31 | } | 31 | } |
32 | 32 | ||
33 | getVideoImportStateClass (state: VideoImportState) { | ||
34 | switch (state) { | ||
35 | case VideoImportState.FAILED: | ||
36 | return 'badge-red' | ||
37 | case VideoImportState.REJECTED: | ||
38 | return 'badge-banned' | ||
39 | case VideoImportState.PENDING: | ||
40 | return 'badge-yellow' | ||
41 | default: | ||
42 | return 'badge-green' | ||
43 | } | ||
44 | } | ||
45 | |||
33 | isVideoImportSuccess (videoImport: VideoImport) { | 46 | isVideoImportSuccess (videoImport: VideoImport) { |
34 | return videoImport.state.id === VideoImportState.SUCCESS | 47 | return videoImport.state.id === VideoImportState.SUCCESS |
35 | } | 48 | } |
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html index 8d69c3a5a..d8e3fb2fa 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html | |||
@@ -1,17 +1,20 @@ | |||
1 | <h1> | 1 | <h1 class="d-flex justify-content-between"> |
2 | <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon> | 2 | <span> |
3 | <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span> | 3 | <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon> |
4 | </h1> | 4 | <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span> |
5 | 5 | </span> | |
6 | 6 | ||
7 | <div class="video-playlists-header"> | 7 | <div class="has-feedback has-clear"> |
8 | <input type="text" placeholder="Search your playlists" i18n-placeholder [(ngModel)]="videoPlaylistsSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" /> | 8 | <input type="text" placeholder="Search your playlists" i18n-placeholder [(ngModel)]="videoPlaylistsSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" /> |
9 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> | ||
10 | <span class="sr-only" i18n>Clear filters</span> | ||
11 | </div> | ||
9 | 12 | ||
10 | <a class="create-button" routerLink="create"> | 13 | <a class="create-button" routerLink="create"> |
11 | <my-global-icon iconName="add" aria-hidden="true"></my-global-icon> | 14 | <my-global-icon iconName="add" aria-hidden="true"></my-global-icon> |
12 | <ng-container i18n>Create playlist</ng-container> | 15 | <ng-container i18n>Create playlist</ng-container> |
13 | </a> | 16 | </a> |
14 | </div> | 17 | </h1> |
15 | 18 | ||
16 | <div class="video-playlists" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> | 19 | <div class="video-playlists" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> |
17 | <div *ngFor="let playlist of videoPlaylists" class="video-playlist"> | 20 | <div *ngFor="let playlist of videoPlaylists" class="video-playlist"> |
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss index 4381d74b0..ade28c70b 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss | |||
@@ -5,6 +5,10 @@ | |||
5 | @include create-button; | 5 | @include create-button; |
6 | } | 6 | } |
7 | 7 | ||
8 | input[type=text] { | ||
9 | @include peertube-input-text(300px); | ||
10 | } | ||
11 | |||
8 | ::ng-deep .action-button { | 12 | ::ng-deep .action-button { |
9 | &.action-button-delete { | 13 | &.action-button-delete { |
10 | margin-right: 10px; | 14 | margin-right: 10px; |
@@ -33,16 +37,6 @@ | |||
33 | } | 37 | } |
34 | } | 38 | } |
35 | 39 | ||
36 | .video-playlists-header { | ||
37 | display: flex; | ||
38 | justify-content: space-between; | ||
39 | margin: 20px 0 50px; | ||
40 | |||
41 | input[type=text] { | ||
42 | @include peertube-input-text(300px); | ||
43 | } | ||
44 | } | ||
45 | |||
46 | @media screen and (max-width: $small-view) { | 40 | @media screen and (max-width: $small-view) { |
47 | .video-playlists-header { | 41 | .video-playlists-header { |
48 | text-align: center; | 42 | text-align: center; |
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts index ea3bcde4f..668a23d8f 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts | |||
@@ -84,6 +84,11 @@ export class MyAccountVideoPlaylistsComponent implements OnInit { | |||
84 | this.loadVideoPlaylists() | 84 | this.loadVideoPlaylists() |
85 | } | 85 | } |
86 | 86 | ||
87 | resetSearch () { | ||
88 | this.videoPlaylistsSearch = '' | ||
89 | this.onVideoPlaylistSearchChanged() | ||
90 | } | ||
91 | |||
87 | onVideoPlaylistSearchChanged () { | 92 | onVideoPlaylistSearchChanged () { |
88 | this.videoPlaylistSearchChanged.next() | 93 | this.videoPlaylistSearchChanged.next() |
89 | } | 94 | } |
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html index 6d098b507..faeb3b56c 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html | |||
@@ -1,11 +1,16 @@ | |||
1 | <h1> | 1 | <h1 class="d-flex justify-content-between"> |
2 | <my-global-icon iconName="videos" aria-hidden="true"></my-global-icon> | 2 | <span> |
3 | <ng-container i18n>My videos</ng-container><span class="badge badge-secondary"> {{ pagination.totalItems }}</span> | 3 | <my-global-icon iconName="videos" aria-hidden="true"></my-global-icon> |
4 | </h1> | 4 | <ng-container i18n>My videos</ng-container> |
5 | <span class="badge badge-secondary"> {{ pagination.totalItems }}</span> | ||
6 | </span> | ||
5 | 7 | ||
6 | <div class="videos-header"> | 8 | <div class="has-feedback has-clear"> |
7 | <input type="text" placeholder="Search your videos" i18n-placeholder [(ngModel)]="videosSearch" (ngModelChange)="onVideosSearchChanged()" /> | 9 | <input type="text" placeholder="Search your videos" i18n-placeholder [(ngModel)]="videosSearch" (ngModelChange)="onVideosSearchChanged()" /> |
8 | </div> | 10 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> |
11 | <span class="sr-only" i18n>Clear filters</span> | ||
12 | </div> | ||
13 | </h1> | ||
9 | 14 | ||
10 | <my-videos-selection | 15 | <my-videos-selection |
11 | [pagination]="pagination" | 16 | [pagination]="pagination" |
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss index 9225fc5fd..0930b1959 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss | |||
@@ -1,14 +1,8 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .videos-header { | 4 | input[type=text] { |
5 | display: flex; | 5 | @include peertube-input-text(300px); |
6 | justify-content: space-between; | ||
7 | margin: 20px 0 50px; | ||
8 | |||
9 | input[type=text] { | ||
10 | @include peertube-input-text(300px); | ||
11 | } | ||
12 | } | 6 | } |
13 | 7 | ||
14 | .action-button-delete-selection { | 8 | .action-button-delete-selection { |
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts index 3cfe8fb38..2274c6a7b 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts | |||
@@ -59,13 +59,17 @@ export class MyAccountVideosComponent implements OnInit, DisableForReuseHook { | |||
59 | 59 | ||
60 | ngOnInit () { | 60 | ngOnInit () { |
61 | this.videosSearchChanged | 61 | this.videosSearchChanged |
62 | .pipe( | 62 | .pipe(debounceTime(500)) |
63 | debounceTime(500)) | ||
64 | .subscribe(() => { | 63 | .subscribe(() => { |
65 | this.videosSelection.reloadVideos() | 64 | this.videosSelection.reloadVideos() |
66 | }) | 65 | }) |
67 | } | 66 | } |
68 | 67 | ||
68 | resetSearch () { | ||
69 | this.videosSearch = '' | ||
70 | this.onVideosSearchChanged() | ||
71 | } | ||
72 | |||
69 | onVideosSearchChanged () { | 73 | onVideosSearchChanged () { |
70 | this.videosSearchChanged.next() | 74 | this.videosSearchChanged.next() |
71 | } | 75 | } |
diff --git a/client/src/app/core/users/user.service.ts b/client/src/app/core/users/user.service.ts index 5f9300bec..c98b3844c 100644 --- a/client/src/app/core/users/user.service.ts +++ b/client/src/app/core/users/user.service.ts | |||
@@ -381,14 +381,14 @@ export class UserService { | |||
381 | 381 | ||
382 | const videoQuotaUsed = this.bytesPipe.transform(user.videoQuotaUsed, 0) | 382 | const videoQuotaUsed = this.bytesPipe.transform(user.videoQuotaUsed, 0) |
383 | 383 | ||
384 | let videoQuotaDaily | 384 | let videoQuotaDaily: string |
385 | let videoQuotaUsedDaily | 385 | let videoQuotaUsedDaily: string |
386 | if (user.videoQuotaDaily === -1) { | 386 | if (user.videoQuotaDaily === -1) { |
387 | videoQuotaDaily = '∞' | 387 | videoQuotaDaily = '∞' |
388 | videoQuotaUsedDaily = this.bytesPipe.transform(0, 0) | 388 | videoQuotaUsedDaily = this.bytesPipe.transform(0, 0) + '' |
389 | } else { | 389 | } else { |
390 | videoQuotaDaily = this.bytesPipe.transform(user.videoQuotaDaily, 0) | 390 | videoQuotaDaily = this.bytesPipe.transform(user.videoQuotaDaily, 0) + '' |
391 | videoQuotaUsedDaily = this.bytesPipe.transform(user.videoQuotaUsedDaily || 0, 0) | 391 | videoQuotaUsedDaily = this.bytesPipe.transform(user.videoQuotaUsedDaily || 0, 0) + '' |
392 | } | 392 | } |
393 | 393 | ||
394 | const roleLabels: { [ id in UserRole ]: string } = { | 394 | const roleLabels: { [ id in UserRole ]: string } = { |
diff --git a/client/src/app/shared/shared-user-subscription/user-subscription.service.ts b/client/src/app/shared/shared-user-subscription/user-subscription.service.ts index 732ed6bcb..eb1fdf91c 100644 --- a/client/src/app/shared/shared-user-subscription/user-subscription.service.ts +++ b/client/src/app/shared/shared-user-subscription/user-subscription.service.ts | |||
@@ -105,13 +105,18 @@ export class UserSubscriptionService { | |||
105 | ) | 105 | ) |
106 | } | 106 | } |
107 | 107 | ||
108 | listSubscriptions (componentPagination: ComponentPaginationLight): Observable<ResultList<VideoChannel>> { | 108 | listSubscriptions (parameters: { |
109 | pagination: ComponentPaginationLight | ||
110 | search: string | ||
111 | }): Observable<ResultList<VideoChannel>> { | ||
112 | const { pagination, search } = parameters | ||
109 | const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL | 113 | const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL |
110 | 114 | ||
111 | const pagination = this.restService.componentPaginationToRestPagination(componentPagination) | 115 | const restPagination = this.restService.componentPaginationToRestPagination(pagination) |
112 | 116 | ||
113 | let params = new HttpParams() | 117 | let params = new HttpParams() |
114 | params = this.restService.addRestGetParams(params, pagination) | 118 | params = this.restService.addRestGetParams(params, restPagination) |
119 | if (search) params = params.append('search', search) | ||
115 | 120 | ||
116 | return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params }) | 121 | return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params }) |
117 | .pipe( | 122 | .pipe( |
diff --git a/client/src/sass/bootstrap.scss b/client/src/sass/bootstrap.scss index 897182e53..8b7eab366 100644 --- a/client/src/sass/bootstrap.scss +++ b/client/src/sass/bootstrap.scss | |||
@@ -310,6 +310,7 @@ ngb-tooltip-window { | |||
310 | position: absolute; | 310 | position: absolute; |
311 | right: .5rem; | 311 | right: .5rem; |
312 | height: 95%; | 312 | height: 95%; |
313 | font-size: 14px; | ||
313 | 314 | ||
314 | &:hover { | 315 | &:hover { |
315 | color: rgba(0, 0, 0, 0.7); | 316 | color: rgba(0, 0, 0, 0.7); |
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index 75fe2ab11..0fb54f121 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -690,12 +690,11 @@ | |||
690 | overflow: hidden; | 690 | overflow: hidden; |
691 | font-size: 0.75rem; | 691 | font-size: 0.75rem; |
692 | border-radius: 0.25rem; | 692 | border-radius: 0.25rem; |
693 | isolation: isolate; | ||
694 | position: relative; | 693 | position: relative; |
695 | 694 | ||
696 | span { | 695 | span { |
697 | position: absolute; | 696 | position: absolute; |
698 | color: rgb(92, 92, 92); | 697 | color: $grey-foreground-color; |
699 | top: -1px; | 698 | top: -1px; |
700 | 699 | ||
701 | &:nth-of-type(1) { | 700 | &:nth-of-type(1) { |
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index ccdc610a2..b1c05c6c0 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts | |||
@@ -120,7 +120,8 @@ async function listAccountChannels (req: express.Request, res: express.Response) | |||
120 | start: req.query.start, | 120 | start: req.query.start, |
121 | count: req.query.count, | 121 | count: req.query.count, |
122 | sort: req.query.sort, | 122 | sort: req.query.sort, |
123 | withStats: req.query.withStats | 123 | withStats: req.query.withStats, |
124 | search: req.query.search | ||
124 | } | 125 | } |
125 | 126 | ||
126 | const resultList = await VideoChannelModel.listByAccount(options) | 127 | const resultList = await VideoChannelModel.listByAccount(options) |
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts index efe1b9bc3..d207a19ae 100644 --- a/server/controllers/api/users/my-subscriptions.ts +++ b/server/controllers/api/users/my-subscriptions.ts | |||
@@ -13,7 +13,7 @@ import { | |||
13 | userSubscriptionAddValidator, | 13 | userSubscriptionAddValidator, |
14 | userSubscriptionGetValidator | 14 | userSubscriptionGetValidator |
15 | } from '../../../middlewares' | 15 | } from '../../../middlewares' |
16 | import { areSubscriptionsExistValidator, userSubscriptionsSortValidator, videosSortValidator } from '../../../middlewares/validators' | 16 | import { areSubscriptionsExistValidator, userSubscriptionsSortValidator, videosSortValidator, userSubscriptionListValidator } from '../../../middlewares/validators' |
17 | import { VideoModel } from '../../../models/video/video' | 17 | import { VideoModel } from '../../../models/video/video' |
18 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' | 18 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' |
19 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' | 19 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' |
@@ -45,6 +45,7 @@ mySubscriptionsRouter.get('/me/subscriptions', | |||
45 | userSubscriptionsSortValidator, | 45 | userSubscriptionsSortValidator, |
46 | setDefaultSort, | 46 | setDefaultSort, |
47 | setDefaultPagination, | 47 | setDefaultPagination, |
48 | userSubscriptionListValidator, | ||
48 | asyncMiddleware(getUserSubscriptions) | 49 | asyncMiddleware(getUserSubscriptions) |
49 | ) | 50 | ) |
50 | 51 | ||
@@ -141,7 +142,13 @@ async function getUserSubscriptions (req: express.Request, res: express.Response | |||
141 | const user = res.locals.oauth.token.User | 142 | const user = res.locals.oauth.token.User |
142 | const actorId = user.Account.Actor.id | 143 | const actorId = user.Account.Actor.id |
143 | 144 | ||
144 | const resultList = await ActorFollowModel.listSubscriptionsForApi(actorId, req.query.start, req.query.count, req.query.sort) | 145 | const resultList = await ActorFollowModel.listSubscriptionsForApi({ |
146 | actorId, | ||
147 | start: req.query.start, | ||
148 | count: req.query.count, | ||
149 | sort: req.query.sort, | ||
150 | search: req.query.search | ||
151 | }) | ||
145 | 152 | ||
146 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 153 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
147 | } | 154 | } |
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index d96998209..f705034fd 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -119,8 +119,7 @@ async function listVideoChannels (req: express.Request, res: express.Response) { | |||
119 | actorId: serverActor.id, | 119 | actorId: serverActor.id, |
120 | start: req.query.start, | 120 | start: req.query.start, |
121 | count: req.query.count, | 121 | count: req.query.count, |
122 | sort: req.query.sort, | 122 | sort: req.query.sort |
123 | search: req.query.search | ||
124 | }) | 123 | }) |
125 | 124 | ||
126 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 125 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
diff --git a/server/middlewares/validators/user-subscriptions.ts b/server/middlewares/validators/user-subscriptions.ts index 5d4cc94c5..a54ecb704 100644 --- a/server/middlewares/validators/user-subscriptions.ts +++ b/server/middlewares/validators/user-subscriptions.ts | |||
@@ -7,6 +7,18 @@ import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-v | |||
7 | import { toArray } from '../../helpers/custom-validators/misc' | 7 | import { toArray } from '../../helpers/custom-validators/misc' |
8 | import { WEBSERVER } from '../../initializers/constants' | 8 | import { WEBSERVER } from '../../initializers/constants' |
9 | 9 | ||
10 | const userSubscriptionListValidator = [ | ||
11 | query('search').optional().not().isEmpty().withMessage('Should have a valid search'), | ||
12 | |||
13 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
14 | logger.debug('Checking userSubscriptionListValidator parameters', { parameters: req.query }) | ||
15 | |||
16 | if (areValidationErrors(req, res)) return | ||
17 | |||
18 | return next() | ||
19 | } | ||
20 | ] | ||
21 | |||
10 | const userSubscriptionAddValidator = [ | 22 | const userSubscriptionAddValidator = [ |
11 | body('uri').custom(isValidActorHandle).withMessage('Should have a valid URI to follow (username@domain)'), | 23 | body('uri').custom(isValidActorHandle).withMessage('Should have a valid URI to follow (username@domain)'), |
12 | 24 | ||
@@ -64,6 +76,7 @@ const userSubscriptionGetValidator = [ | |||
64 | 76 | ||
65 | export { | 77 | export { |
66 | areSubscriptionsExistValidator, | 78 | areSubscriptionsExistValidator, |
79 | userSubscriptionListValidator, | ||
67 | userSubscriptionAddValidator, | 80 | userSubscriptionAddValidator, |
68 | userSubscriptionGetValidator | 81 | userSubscriptionGetValidator |
69 | } | 82 | } |
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index 3e85cc329..529cb35cc 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/activitypub/actor-follow.ts | |||
@@ -15,14 +15,15 @@ import { | |||
15 | Max, | 15 | Max, |
16 | Model, | 16 | Model, |
17 | Table, | 17 | Table, |
18 | UpdatedAt | 18 | UpdatedAt, |
19 | Sequelize | ||
19 | } from 'sequelize-typescript' | 20 | } from 'sequelize-typescript' |
20 | import { FollowState } from '../../../shared/models/actors' | 21 | import { FollowState } from '../../../shared/models/actors' |
21 | import { ActorFollow } from '../../../shared/models/actors/follow.model' | 22 | import { ActorFollow } from '../../../shared/models/actors/follow.model' |
22 | import { logger } from '../../helpers/logger' | 23 | import { logger } from '../../helpers/logger' |
23 | import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' | 24 | import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' |
24 | import { ServerModel } from '../server/server' | 25 | import { ServerModel } from '../server/server' |
25 | import { createSafeIn, getFollowsSort, getSort } from '../utils' | 26 | import { createSafeIn, getFollowsSort, getSort, searchAttribute } from '../utils' |
26 | import { ActorModel, unusedActorAttributesForAPI } from './actor' | 27 | import { ActorModel, unusedActorAttributesForAPI } from './actor' |
27 | import { VideoChannelModel } from '../video/video-channel' | 28 | import { VideoChannelModel } from '../video/video-channel' |
28 | import { AccountModel } from '../account/account' | 29 | import { AccountModel } from '../account/account' |
@@ -440,16 +441,34 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
440 | }) | 441 | }) |
441 | } | 442 | } |
442 | 443 | ||
443 | static listSubscriptionsForApi (actorId: number, start: number, count: number, sort: string) { | 444 | static listSubscriptionsForApi (options: { |
445 | actorId: number | ||
446 | start: number | ||
447 | count: number | ||
448 | sort: string | ||
449 | search?: string | ||
450 | }) { | ||
451 | const { actorId, start, count, sort } = options | ||
452 | const where = { | ||
453 | actorId: actorId | ||
454 | } | ||
455 | |||
456 | if (options.search) { | ||
457 | Object.assign(where, { | ||
458 | [Op.or]: [ | ||
459 | searchAttribute(options.search, '$ActorFollowing.preferredUsername$'), | ||
460 | searchAttribute(options.search, '$ActorFollowing.VideoChannel.name$') | ||
461 | ] | ||
462 | }) | ||
463 | } | ||
464 | |||
444 | const query = { | 465 | const query = { |
445 | attributes: [], | 466 | attributes: [], |
446 | distinct: true, | 467 | distinct: true, |
447 | offset: start, | 468 | offset: start, |
448 | limit: count, | 469 | limit: count, |
449 | order: getSort(sort), | 470 | order: getSort(sort), |
450 | where: { | 471 | where, |
451 | actorId: actorId | ||
452 | }, | ||
453 | include: [ | 472 | include: [ |
454 | { | 473 | { |
455 | attributes: [ 'id' ], | 474 | attributes: [ 'id' ], |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index f3401fb9c..9da965bbc 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -315,9 +315,8 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
315 | start: number | 315 | start: number |
316 | count: number | 316 | count: number |
317 | sort: string | 317 | sort: string |
318 | search?: string | ||
319 | }) { | 318 | }) { |
320 | const { actorId, search } = parameters | 319 | const { actorId } = parameters |
321 | 320 | ||
322 | const query = { | 321 | const query = { |
323 | offset: parameters.start, | 322 | offset: parameters.start, |
@@ -326,7 +325,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
326 | } | 325 | } |
327 | 326 | ||
328 | const scopes = { | 327 | const scopes = { |
329 | method: [ ScopeNames.FOR_API, { actorId, search } as AvailableForListOptions ] | 328 | method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ] |
330 | } | 329 | } |
331 | return VideoChannelModel | 330 | return VideoChannelModel |
332 | .scope(scopes) | 331 | .scope(scopes) |
@@ -405,7 +404,23 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
405 | count: number | 404 | count: number |
406 | sort: string | 405 | sort: string |
407 | withStats?: boolean | 406 | withStats?: boolean |
407 | search?: string | ||
408 | }) { | 408 | }) { |
409 | const escapedSearch = VideoModel.sequelize.escape(options.search) | ||
410 | const escapedLikeSearch = VideoModel.sequelize.escape('%' + options.search + '%') | ||
411 | const where = options.search | ||
412 | ? { | ||
413 | [Op.or]: [ | ||
414 | Sequelize.literal( | ||
415 | 'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' | ||
416 | ), | ||
417 | Sequelize.literal( | ||
418 | 'lower(immutable_unaccent("VideoChannelModel"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' | ||
419 | ) | ||
420 | ] | ||
421 | } | ||
422 | : null | ||
423 | |||
409 | const query = { | 424 | const query = { |
410 | offset: options.start, | 425 | offset: options.start, |
411 | limit: options.count, | 426 | limit: options.count, |
@@ -418,7 +433,8 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
418 | }, | 433 | }, |
419 | required: true | 434 | required: true |
420 | } | 435 | } |
421 | ] | 436 | ], |
437 | where | ||
422 | } | 438 | } |
423 | 439 | ||
424 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ] | 440 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ] |