aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html6
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.ts17
-rw-r--r--client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html19
-rw-r--r--client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.scss9
-rw-r--r--client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts26
-rw-r--r--client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html6
-rw-r--r--client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts18
-rw-r--r--client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html6
-rw-r--r--client/src/app/+my-account/my-account-ownership/my-account-ownership.component.scss4
-rw-r--r--client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts13
-rw-r--r--client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html15
-rw-r--r--client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss4
-rw-r--r--client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts43
-rw-r--r--client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html7
-rw-r--r--client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss6
-rw-r--r--client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts13
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html19
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss14
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts5
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.html19
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.scss10
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.ts8
-rw-r--r--client/src/app/core/users/user.service.ts10
-rw-r--r--client/src/app/shared/shared-user-subscription/user-subscription.service.ts11
-rw-r--r--client/src/sass/bootstrap.scss1
-rw-r--r--client/src/sass/include/_mixins.scss3
-rw-r--r--server/controllers/api/accounts.ts3
-rw-r--r--server/controllers/api/users/my-subscriptions.ts11
-rw-r--r--server/controllers/api/video-channel.ts3
-rw-r--r--server/middlewares/validators/user-subscriptions.ts13
-rw-r--r--server/models/activitypub/actor-follow.ts31
-rw-r--r--server/models/video/video-channel.ts24
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'
7import { ServerConfig, User, UserRole } from '@shared/models' 7import { ServerConfig, User, UserRole } from '@shared/models'
8import { Params, Router, ActivatedRoute } from '@angular/router' 8import { Params, Router, ActivatedRoute } from '@angular/router'
9 9
10type 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
8input[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 @@
1import { ChartData } from 'chart.js' 1import { ChartData } from 'chart.js'
2import { max, maxBy, min, minBy } from 'lodash-es' 2import { max, maxBy, min, minBy } from 'lodash-es'
3import { flatMap } from 'rxjs/operators' 3import { flatMap, debounceTime } from 'rxjs/operators'
4import { Component, OnInit } from '@angular/core' 4import { Component, OnInit } from '@angular/core'
5import { AuthService, ConfirmService, Notifier, ScreenService, User } from '@app/core' 5import { AuthService, ConfirmService, Notifier, ScreenService, User } from '@app/core'
6import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' 6import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { 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})
14export class MyAccountVideoChannelsComponent implements OnInit { 15export 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 @@
1import { Component, ViewChild } from '@angular/core' 1import { Component, ViewChild } from '@angular/core'
2import { UserNotificationsComponent } from '@app/shared/shared-main' 2import { UserNotificationsComponent } from '@app/shared/shared-main'
3 3
4type 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'
8export class MyAccountNotificationsComponent { 10export 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'
2import { Component, OnInit, ViewChild } from '@angular/core' 2import { Component, OnInit, ViewChild } from '@angular/core'
3import { Notifier, RestPagination, RestTable } from '@app/core' 3import { Notifier, RestPagination, RestTable } from '@app/core'
4import { VideoOwnershipService, Actor, Video, Account } from '@app/shared/shared-main' 4import { VideoOwnershipService, Actor, Video, Account } from '@app/shared/shared-main'
5import { VideoChangeOwnership } from '@shared/models' 5import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '@shared/models'
6import { MyAccountAcceptOwnershipComponent } from './my-account-accept-ownership/my-account-accept-ownership.component' 6import { MyAccountAcceptOwnershipComponent } from './my-account-accept-ownership/my-account-accept-ownership.component'
7import { getAbsoluteAPIUrl } from '@app/helpers' 7import { 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
4input[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'
3import { ComponentPagination, Notifier } from '@app/core' 3import { ComponentPagination, Notifier } from '@app/core'
4import { VideoChannel } from '@app/shared/shared-main' 4import { VideoChannel } from '@app/shared/shared-main'
5import { UserSubscriptionService } from '@app/shared/shared-user-subscription' 5import { UserSubscriptionService } from '@app/shared/shared-user-subscription'
6import { 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
8input[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 { 4input[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'
16import { areSubscriptionsExistValidator, userSubscriptionsSortValidator, videosSortValidator } from '../../../middlewares/validators' 16import { areSubscriptionsExistValidator, userSubscriptionsSortValidator, videosSortValidator, userSubscriptionListValidator } from '../../../middlewares/validators'
17import { VideoModel } from '../../../models/video/video' 17import { VideoModel } from '../../../models/video/video'
18import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' 18import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
19import { VideoFilter } from '../../../../shared/models/videos/video-query.type' 19import { 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
7import { toArray } from '../../helpers/custom-validators/misc' 7import { toArray } from '../../helpers/custom-validators/misc'
8import { WEBSERVER } from '../../initializers/constants' 8import { WEBSERVER } from '../../initializers/constants'
9 9
10const 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
10const userSubscriptionAddValidator = [ 22const 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
65export { 77export {
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'
20import { FollowState } from '../../../shared/models/actors' 21import { FollowState } from '../../../shared/models/actors'
21import { ActorFollow } from '../../../shared/models/actors/follow.model' 22import { ActorFollow } from '../../../shared/models/actors/follow.model'
22import { logger } from '../../helpers/logger' 23import { logger } from '../../helpers/logger'
23import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' 24import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants'
24import { ServerModel } from '../server/server' 25import { ServerModel } from '../server/server'
25import { createSafeIn, getFollowsSort, getSort } from '../utils' 26import { createSafeIn, getFollowsSort, getSort, searchAttribute } from '../utils'
26import { ActorModel, unusedActorAttributesForAPI } from './actor' 27import { ActorModel, unusedActorAttributesForAPI } from './actor'
27import { VideoChannelModel } from '../video/video-channel' 28import { VideoChannelModel } from '../video/video-channel'
28import { AccountModel } from '../account/account' 29import { 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 ]