From 4f5d045960b042eb27e10bac1bdaf1c074c9fa2a Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Thu, 23 Jul 2020 21:30:04 +0200 Subject: [PATCH] harmonize search for libraries --- .../users/user-list/user-list.component.html | 6 ++- .../users/user-list/user-list.component.ts | 17 +++++--- .../my-account-video-channels.component.html | 19 +++++--- .../my-account-video-channels.component.scss | 9 ++-- .../my-account-video-channels.component.ts | 26 ++++++++++- .../my-account-notifications.component.html | 6 +-- .../my-account-notifications.component.ts | 18 +++++++- .../my-account-ownership.component.html | 6 ++- .../my-account-ownership.component.scss | 4 ++ .../my-account-ownership.component.ts | 13 +++++- .../my-account-subscriptions.component.html | 15 +++++-- .../my-account-subscriptions.component.scss | 4 ++ .../my-account-subscriptions.component.ts | 43 ++++++++++++++----- .../my-account-video-imports.component.html | 7 ++- .../my-account-video-imports.component.scss | 6 ++- .../my-account-video-imports.component.ts | 13 ++++++ .../my-account-video-playlists.component.html | 19 ++++---- .../my-account-video-playlists.component.scss | 14 ++---- .../my-account-video-playlists.component.ts | 5 +++ .../my-account-videos.component.html | 19 +++++--- .../my-account-videos.component.scss | 10 +---- .../my-account-videos.component.ts | 8 +++- client/src/app/core/users/user.service.ts | 10 ++--- .../user-subscription.service.ts | 11 +++-- client/src/sass/bootstrap.scss | 1 + client/src/sass/include/_mixins.scss | 3 +- server/controllers/api/accounts.ts | 3 +- .../controllers/api/users/my-subscriptions.ts | 11 ++++- server/controllers/api/video-channel.ts | 3 +- .../validators/user-subscriptions.ts | 13 ++++++ server/models/activitypub/actor-follow.ts | 31 ++++++++++--- server/models/video/video-channel.ts | 24 +++++++++-- 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 @@ - - {{ user.email }} + + + {{ user.email }} + 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' import { ServerConfig, User, UserRole } from '@shared/models' import { Params, Router, ActivatedRoute } from '@angular/router' +type UserForList = User & { + rawVideoQuota: number + rawVideoQuotaUsed: number + rawVideoQuotaDaily: number + rawVideoQuotaUsedDaily: number +} + @Component({ selector: 'my-user-list', templateUrl: './user-list.component.html', @@ -24,8 +31,8 @@ export class UserListComponent extends RestTable implements OnInit { selectedUsers: User[] = [] bulkUserActions: DropdownAction[][] = [] columns: { key: string, label: string }[] - _selectedColumns: { key: string, label: string }[] + private _selectedColumns: { key: string, label: string }[] private serverConfig: ServerConfig constructor ( @@ -111,7 +118,7 @@ export class UserListComponent extends RestTable implements OnInit { { key: 'role', label: 'Role' }, { key: 'createdAt', label: 'Created' } ] - this.selectedColumns = [...this.columns] + this.selectedColumns = [ ...this.columns ] // make a full copy of the array this.columns.push({ key: 'quotaDaily', label: 'Daily quota' }) this.columns.push({ key: 'pluginAuth', label: 'Auth plugin' }) this.columns.push({ key: 'lastLoginDate', label: 'Last login' }) @@ -133,14 +140,14 @@ export class UserListComponent extends RestTable implements OnInit { } getColumn (key: string) { - return this.selectedColumns.find((col: any) => col.key === key) + return this.selectedColumns.find((col: { key: string }) => col.key === key) } - getUserVideoQuotaPercentage (user: User & { rawVideoQuota: number, rawVideoQuotaUsed: number}) { + getUserVideoQuotaPercentage (user: UserForList) { return user.rawVideoQuotaUsed * 100 / user.rawVideoQuota } - getUserVideoQuotaDailyPercentage (user: User & { rawVideoQuotaDaily: number, rawVideoQuotaUsedDaily: number}) { + getUserVideoQuotaDailyPercentage (user: UserForList) { return user.rawVideoQuotaUsedDaily * 100 / user.rawVideoQuotaDaily } 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 @@ -

- - My channels -

+

+ + + My channels + {{ totalItems }} + + +
+ + + Clear filters +
- +

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 @@ @include create-button; } +input[type=text] { + @include peertube-input-text(300px); +} + ::ng-deep .action-button { &.action-button-edit { margin-right: 10px; @@ -55,11 +59,6 @@ } } -.video-channels-header { - text-align: right; - margin: 20px 0 50px; -} - ::ng-deep .chartjs-render-monitor { position: relative; 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 @@ import { ChartData } from 'chart.js' import { max, maxBy, min, minBy } from 'lodash-es' -import { flatMap } from 'rxjs/operators' +import { flatMap, debounceTime } from 'rxjs/operators' import { Component, OnInit } from '@angular/core' import { AuthService, ConfirmService, Notifier, ScreenService, User } from '@app/core' import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' import { I18n } from '@ngx-translate/i18n-polyfill' +import { Subject } from 'rxjs' @Component({ selector: 'my-account-video-channels', @@ -12,11 +13,16 @@ import { I18n } from '@ngx-translate/i18n-polyfill' styleUrls: [ './my-account-video-channels.component.scss' ] }) export class MyAccountVideoChannelsComponent implements OnInit { + totalItems: number + videoChannels: VideoChannel[] = [] videoChannelsChartData: ChartData[] videoChannelsMinimumDailyViews = 0 videoChannelsMaximumDailyViews: number + channelsSearch: string + channelsSearchChanged = new Subject() + private user: User constructor ( @@ -32,6 +38,12 @@ export class MyAccountVideoChannelsComponent implements OnInit { this.user = this.authService.getUser() this.loadVideoChannels() + + this.channelsSearchChanged + .pipe(debounceTime(500)) + .subscribe(() => { + this.loadVideoChannels() + }) } get isInSmallView () { @@ -87,6 +99,15 @@ export class MyAccountVideoChannelsComponent implements OnInit { } } + resetSearch() { + this.channelsSearch = '' + this.onChannelsSearchChanged() + } + + onChannelsSearchChanged () { + this.channelsSearchChanged.next() + } + async deleteVideoChannel (videoChannel: VideoChannel) { const res = await this.confirmService.confirmWithInput( this.i18n( @@ -118,9 +139,10 @@ export class MyAccountVideoChannelsComponent implements OnInit { private loadVideoChannels () { this.authService.userInformationLoaded - .pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true))) + .pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true, this.channelsSearch))) .subscribe(res => { this.videoChannels = res.data + this.totalItems = res.total // chart data 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 @@
- - - + +
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 @@ import { Component, ViewChild } from '@angular/core' import { UserNotificationsComponent } from '@app/shared/shared-main' +type NotificationSortType = 'createdAt' | 'read' + @Component({ templateUrl: './my-account-notifications.component.html', styleUrls: [ './my-account-notifications.component.scss' ] @@ -8,7 +10,17 @@ import { UserNotificationsComponent } from '@app/shared/shared-main' export class MyAccountNotificationsComponent { @ViewChild('userNotification', { static: true }) userNotification: UserNotificationsComponent - notificationSortType = 'created' + _notificationSortType: NotificationSortType = 'createdAt' + + get notificationSortType () { + return !this.hasUnreadNotifications() + ? 'createdAt' + : this._notificationSortType + } + + set notificationSortType (type: NotificationSortType) { + this._notificationSortType = type + } markAllAsRead () { this.userNotification.markAllAsRead() @@ -17,4 +29,8 @@ export class MyAccountNotificationsComponent { hasUnreadNotifications () { return this.userNotification.notifications.filter(n => n.read === false).length !== 0 } + + onChangeSortColumn () { + this.userNotification.changeSortColumn(this.notificationSortType) + } } 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 @@ {{ videoChangeOwnership.createdAt | date: 'short' }} - {{ videoChangeOwnership.status }} + + + {{ videoChangeOwnership.status }} + + 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 @@ @include chip; } +.badge { + @include table-badge; +} + .video-table-video { display: inline-flex; 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' import { Component, OnInit, ViewChild } from '@angular/core' import { Notifier, RestPagination, RestTable } from '@app/core' import { VideoOwnershipService, Actor, Video, Account } from '@app/shared/shared-main' -import { VideoChangeOwnership } from '@shared/models' +import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '@shared/models' import { MyAccountAcceptOwnershipComponent } from './my-account-accept-ownership/my-account-accept-ownership.component' import { getAbsoluteAPIUrl } from '@app/helpers' @@ -34,6 +34,17 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit { return 'MyAccountOwnershipComponent' } + getStatusClass (status: VideoChangeOwnershipStatus) { + switch (status) { + case VideoChangeOwnershipStatus.ACCEPTED: + return 'badge-green' + case VideoChangeOwnershipStatus.REFUSED: + return 'badge-red' + default: + return 'badge-yellow' + } + } + switchToDefaultAvatar ($event: Event) { ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() } 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 @@ -

- - My subscriptions +

+ + + My subscriptions + {{ pagination.totalItems }} + + +
+ + + Clear filters +

You don't have any subscriptions yet.
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 @@ @import '_variables'; @import '_mixins'; +input[type=text] { + @include peertube-input-text(300px); +} + .video-channel { @include row-blocks; 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' import { ComponentPagination, Notifier } from '@app/core' import { VideoChannel } from '@app/shared/shared-main' import { UserSubscriptionService } from '@app/shared/shared-user-subscription' +import { debounceTime } from 'rxjs/operators' @Component({ selector: 'my-account-subscriptions', @@ -20,6 +21,9 @@ export class MyAccountSubscriptionsComponent implements OnInit { onDataSubject = new Subject() + subscriptionsSearch: string + subscriptionsSearchChanged = new Subject() + constructor ( private userSubscriptionService: UserSubscriptionService, private notifier: Notifier @@ -27,20 +31,22 @@ export class MyAccountSubscriptionsComponent implements OnInit { ngOnInit () { this.loadSubscriptions() - } - loadSubscriptions () { - this.userSubscriptionService.listSubscriptions(this.pagination) - .subscribe( - res => { - this.videoChannels = this.videoChannels.concat(res.data) - this.pagination.totalItems = res.total + this.subscriptionsSearchChanged + .pipe(debounceTime(500)) + .subscribe(() => { + this.pagination.currentPage = 1 + this.loadSubscriptions(false) + }) + } - this.onDataSubject.next(res.data) - }, + resetSearch () { + this.subscriptionsSearch = '' + this.onSubscriptionsSearchChanged() + } - error => this.notifier.error(error.message) - ) + onSubscriptionsSearchChanged () { + this.subscriptionsSearchChanged.next() } onNearOfBottom () { @@ -51,4 +57,19 @@ export class MyAccountSubscriptionsComponent implements OnInit { this.loadSubscriptions() } + private loadSubscriptions (more = true) { + this.userSubscriptionService.listSubscriptions({ pagination: this.pagination, search: this.subscriptionsSearch }) + .subscribe( + res => { + this.videoChannels = more + ? this.videoChannels.concat(res.data) + : res.data + this.pagination.totalItems = res.total + + this.onDataSubject.next(res.data) + }, + + error => this.notifier.error(error.message) + ) + } } 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 @@ - {{ videoImport.state.label }} + + + {{ videoImport.state.label }} + + + {{ videoImport.createdAt | date: 'short' }} 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 { .video-import-error { color: red; -} \ No newline at end of file +} + +.badge { + @include table-badge; +} 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 return 'MyAccountVideoImportsComponent' } + getVideoImportStateClass (state: VideoImportState) { + switch (state) { + case VideoImportState.FAILED: + return 'badge-red' + case VideoImportState.REJECTED: + return 'badge-banned' + case VideoImportState.PENDING: + return 'badge-yellow' + default: + return 'badge-green' + } + } + isVideoImportSuccess (videoImport: VideoImport) { return videoImport.state.id === VideoImportState.SUCCESS } 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 @@ -

- - My playlists {{ pagination.totalItems }} -

- +

+ + + My playlists {{ pagination.totalItems }} + -
- +
+ + + Clear filters +
Create 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 @@ @include create-button; } +input[type=text] { + @include peertube-input-text(300px); +} + ::ng-deep .action-button { &.action-button-delete { margin-right: 10px; @@ -33,16 +37,6 @@ } } -.video-playlists-header { - display: flex; - justify-content: space-between; - margin: 20px 0 50px; - - input[type=text] { - @include peertube-input-text(300px); - } -} - @media screen and (max-width: $small-view) { .video-playlists-header { 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 { this.loadVideoPlaylists() } + resetSearch () { + this.videoPlaylistsSearch = '' + this.onVideoPlaylistSearchChanged() + } + onVideoPlaylistSearchChanged () { this.videoPlaylistSearchChanged.next() } 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 @@ -

- - My videos {{ pagination.totalItems }} -

+

+ + + My videos + {{ pagination.totalItems }} + -
- -
+
+ + + Clear filters +
+

{ this.videosSelection.reloadVideos() }) } + resetSearch () { + this.videosSearch = '' + this.onVideosSearchChanged() + } + onVideosSearchChanged () { this.videosSearchChanged.next() } 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 { const videoQuotaUsed = this.bytesPipe.transform(user.videoQuotaUsed, 0) - let videoQuotaDaily - let videoQuotaUsedDaily + let videoQuotaDaily: string + let videoQuotaUsedDaily: string if (user.videoQuotaDaily === -1) { videoQuotaDaily = '∞' - videoQuotaUsedDaily = this.bytesPipe.transform(0, 0) + videoQuotaUsedDaily = this.bytesPipe.transform(0, 0) + '' } else { - videoQuotaDaily = this.bytesPipe.transform(user.videoQuotaDaily, 0) - videoQuotaUsedDaily = this.bytesPipe.transform(user.videoQuotaUsedDaily || 0, 0) + videoQuotaDaily = this.bytesPipe.transform(user.videoQuotaDaily, 0) + '' + videoQuotaUsedDaily = this.bytesPipe.transform(user.videoQuotaUsedDaily || 0, 0) + '' } 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 { ) } - listSubscriptions (componentPagination: ComponentPaginationLight): Observable> { + listSubscriptions (parameters: { + pagination: ComponentPaginationLight + search: string + }): Observable> { + const { pagination, search } = parameters const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL - const pagination = this.restService.componentPaginationToRestPagination(componentPagination) + const restPagination = this.restService.componentPaginationToRestPagination(pagination) let params = new HttpParams() - params = this.restService.addRestGetParams(params, pagination) + params = this.restService.addRestGetParams(params, restPagination) + if (search) params = params.append('search', search) return this.authHttp.get>(url, { params }) .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 { position: absolute; right: .5rem; height: 95%; + font-size: 14px; &:hover { 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 @@ overflow: hidden; font-size: 0.75rem; border-radius: 0.25rem; - isolation: isolate; position: relative; span { position: absolute; - color: rgb(92, 92, 92); + color: $grey-foreground-color; top: -1px; &: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) start: req.query.start, count: req.query.count, sort: req.query.sort, - withStats: req.query.withStats + withStats: req.query.withStats, + search: req.query.search } 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 { userSubscriptionAddValidator, userSubscriptionGetValidator } from '../../../middlewares' -import { areSubscriptionsExistValidator, userSubscriptionsSortValidator, videosSortValidator } from '../../../middlewares/validators' +import { areSubscriptionsExistValidator, userSubscriptionsSortValidator, videosSortValidator, userSubscriptionListValidator } from '../../../middlewares/validators' import { VideoModel } from '../../../models/video/video' import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' import { VideoFilter } from '../../../../shared/models/videos/video-query.type' @@ -45,6 +45,7 @@ mySubscriptionsRouter.get('/me/subscriptions', userSubscriptionsSortValidator, setDefaultSort, setDefaultPagination, + userSubscriptionListValidator, asyncMiddleware(getUserSubscriptions) ) @@ -141,7 +142,13 @@ async function getUserSubscriptions (req: express.Request, res: express.Response const user = res.locals.oauth.token.User const actorId = user.Account.Actor.id - const resultList = await ActorFollowModel.listSubscriptionsForApi(actorId, req.query.start, req.query.count, req.query.sort) + const resultList = await ActorFollowModel.listSubscriptionsForApi({ + actorId, + start: req.query.start, + count: req.query.count, + sort: req.query.sort, + search: req.query.search + }) return res.json(getFormattedObjects(resultList.data, resultList.total)) } 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) { actorId: serverActor.id, start: req.query.start, count: req.query.count, - sort: req.query.sort, - search: req.query.search + sort: req.query.sort }) 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 import { toArray } from '../../helpers/custom-validators/misc' import { WEBSERVER } from '../../initializers/constants' +const userSubscriptionListValidator = [ + query('search').optional().not().isEmpty().withMessage('Should have a valid search'), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking userSubscriptionListValidator parameters', { parameters: req.query }) + + if (areValidationErrors(req, res)) return + + return next() + } +] + const userSubscriptionAddValidator = [ body('uri').custom(isValidActorHandle).withMessage('Should have a valid URI to follow (username@domain)'), @@ -64,6 +76,7 @@ const userSubscriptionGetValidator = [ export { areSubscriptionsExistValidator, + userSubscriptionListValidator, userSubscriptionAddValidator, userSubscriptionGetValidator } 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 { Max, Model, Table, - UpdatedAt + UpdatedAt, + Sequelize } from 'sequelize-typescript' import { FollowState } from '../../../shared/models/actors' import { ActorFollow } from '../../../shared/models/actors/follow.model' import { logger } from '../../helpers/logger' import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' import { ServerModel } from '../server/server' -import { createSafeIn, getFollowsSort, getSort } from '../utils' +import { createSafeIn, getFollowsSort, getSort, searchAttribute } from '../utils' import { ActorModel, unusedActorAttributesForAPI } from './actor' import { VideoChannelModel } from '../video/video-channel' import { AccountModel } from '../account/account' @@ -440,16 +441,34 @@ export class ActorFollowModel extends Model { }) } - static listSubscriptionsForApi (actorId: number, start: number, count: number, sort: string) { + static listSubscriptionsForApi (options: { + actorId: number + start: number + count: number + sort: string + search?: string + }) { + const { actorId, start, count, sort } = options + const where = { + actorId: actorId + } + + if (options.search) { + Object.assign(where, { + [Op.or]: [ + searchAttribute(options.search, '$ActorFollowing.preferredUsername$'), + searchAttribute(options.search, '$ActorFollowing.VideoChannel.name$') + ] + }) + } + const query = { attributes: [], distinct: true, offset: start, limit: count, order: getSort(sort), - where: { - actorId: actorId - }, + where, include: [ { 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 { start: number count: number sort: string - search?: string }) { - const { actorId, search } = parameters + const { actorId } = parameters const query = { offset: parameters.start, @@ -326,7 +325,7 @@ export class VideoChannelModel extends Model { } const scopes = { - method: [ ScopeNames.FOR_API, { actorId, search } as AvailableForListOptions ] + method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ] } return VideoChannelModel .scope(scopes) @@ -405,7 +404,23 @@ export class VideoChannelModel extends Model { count: number sort: string withStats?: boolean + search?: string }) { + const escapedSearch = VideoModel.sequelize.escape(options.search) + const escapedLikeSearch = VideoModel.sequelize.escape('%' + options.search + '%') + const where = options.search + ? { + [Op.or]: [ + Sequelize.literal( + 'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' + ), + Sequelize.literal( + 'lower(immutable_unaccent("VideoChannelModel"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' + ) + ] + } + : null + const query = { offset: options.start, limit: options.count, @@ -418,7 +433,8 @@ export class VideoChannelModel extends Model { }, required: true } - ] + ], + where } const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ] -- 2.41.0