diff options
47 files changed, 796 insertions, 245 deletions
diff --git a/client/src/app/+about/about-follows/about-follows.component.ts b/client/src/app/+about/about-follows/about-follows.component.ts index a35272681..84b47e967 100644 --- a/client/src/app/+about/about-follows/about-follows.component.ts +++ b/client/src/app/+about/about-follows/about-follows.component.ts | |||
@@ -88,7 +88,7 @@ export class AboutFollowsComponent implements OnInit { | |||
88 | } | 88 | } |
89 | 89 | ||
90 | private loadMoreFollowers (reset = false) { | 90 | private loadMoreFollowers (reset = false) { |
91 | const pagination = this.restService.componentPaginationToRestPagination(this.followersPagination) | 91 | const pagination = this.restService.componentToRestPagination(this.followersPagination) |
92 | 92 | ||
93 | this.followService.getFollowers({ pagination, sort: this.sort, state: 'accepted' }) | 93 | this.followService.getFollowers({ pagination, sort: this.sort, state: 'accepted' }) |
94 | .subscribe({ | 94 | .subscribe({ |
@@ -106,7 +106,7 @@ export class AboutFollowsComponent implements OnInit { | |||
106 | } | 106 | } |
107 | 107 | ||
108 | private loadMoreFollowings (reset = false) { | 108 | private loadMoreFollowings (reset = false) { |
109 | const pagination = this.restService.componentPaginationToRestPagination(this.followingsPagination) | 109 | const pagination = this.restService.componentToRestPagination(this.followingsPagination) |
110 | 110 | ||
111 | this.followService.getFollowing({ pagination, sort: this.sort, state: 'accepted' }) | 111 | this.followService.getFollowing({ pagination, sort: this.sort, state: 'accepted' }) |
112 | .subscribe({ | 112 | .subscribe({ |
diff --git a/client/src/app/+accounts/accounts.component.scss b/client/src/app/+accounts/accounts.component.scss index c4e2159d1..cdd00487b 100644 --- a/client/src/app/+accounts/accounts.component.scss +++ b/client/src/app/+accounts/accounts.component.scss | |||
@@ -1,6 +1,6 @@ | |||
1 | @use '_variables' as *; | 1 | @use '_variables' as *; |
2 | @use '_mixins' as *; | 2 | @use '_mixins' as *; |
3 | @use '_actor' as *; | 3 | @use '_account-channel-page' as *; |
4 | @use '_miniature' as *; | 4 | @use '_miniature' as *; |
5 | 5 | ||
6 | .root { | 6 | .root { |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html index 1f542e458..537e06d4d 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html | |||
@@ -416,7 +416,7 @@ | |||
416 | <p i18n>⚠️ This functionality requires a lot of attention and extra moderation.</p> | 416 | <p i18n>⚠️ This functionality requires a lot of attention and extra moderation.</p> |
417 | 417 | ||
418 | <span i18n> | 418 | <span i18n> |
419 | See <a href="https://docs.joinpeertube.org/admin-following-instances?id=automatically-follow-other-instances" rel="noopener noreferer" target="_blank">the documentation</a> for more information about the expected URL | 419 | See <a href="https://docs.joinpeertube.org/admin-following-instances?id=automatically-follow-other-instances" rel="noopener noreferrer" target="_blank">the documentation</a> for more information about the expected URL |
420 | </span> | 420 | </span> |
421 | </ng-container> | 421 | </ng-container> |
422 | 422 | ||
diff --git a/client/src/app/+admin/plugins/shared/plugin-api.service.ts b/client/src/app/+admin/plugins/shared/plugin-api.service.ts index c4f480cae..b95ee0c9d 100644 --- a/client/src/app/+admin/plugins/shared/plugin-api.service.ts +++ b/client/src/app/+admin/plugins/shared/plugin-api.service.ts | |||
@@ -51,7 +51,7 @@ export class PluginApiService { | |||
51 | componentPagination: ComponentPagination, | 51 | componentPagination: ComponentPagination, |
52 | sort: string | 52 | sort: string |
53 | ) { | 53 | ) { |
54 | const pagination = this.restService.componentPaginationToRestPagination(componentPagination) | 54 | const pagination = this.restService.componentToRestPagination(componentPagination) |
55 | 55 | ||
56 | let params = new HttpParams() | 56 | let params = new HttpParams() |
57 | params = this.restService.addRestGetParams(params, pagination, sort) | 57 | params = this.restService.addRestGetParams(params, pagination, sort) |
@@ -67,7 +67,7 @@ export class PluginApiService { | |||
67 | sort: string, | 67 | sort: string, |
68 | search?: string | 68 | search?: string |
69 | ) { | 69 | ) { |
70 | const pagination = this.restService.componentPaginationToRestPagination(componentPagination) | 70 | const pagination = this.restService.componentToRestPagination(componentPagination) |
71 | 71 | ||
72 | let params = new HttpParams() | 72 | let params = new HttpParams() |
73 | params = this.restService.addRestGetParams(params, pagination, sort) | 73 | params = this.restService.addRestGetParams(params, pagination, sort) |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html index 4c5b46d5b..bbe583971 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html | |||
@@ -27,7 +27,12 @@ | |||
27 | <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div> | 27 | <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div> |
28 | </a> | 28 | </a> |
29 | 29 | ||
30 | <div i18n class="video-channel-followers">{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> | 30 | <a |
31 | i18n class="video-channel-followers" | ||
32 | [routerLink]="[ '/my-library', 'followers' ]" [queryParams]="{ search: 'channel:' + videoChannel.name }" | ||
33 | > | ||
34 | {videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}} | ||
35 | </a> | ||
31 | 36 | ||
32 | <div i18n class="video-channel-videos">{videoChannel.videosCount, plural, =0 {No videos} =1 {1 video} other {{{ videoChannel.videosCount }} videos}}</div> | 37 | <div i18n class="video-channel-videos">{videoChannel.videosCount, plural, =0 {No videos} =1 {1 video} other {{{ videoChannel.videosCount }} videos}}</div> |
33 | 38 | ||
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss index 9ef5513b6..998e46cb2 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss | |||
@@ -54,6 +54,10 @@ my-edit-button { | |||
54 | color: $grey-actor-name; | 54 | color: $grey-actor-name; |
55 | } | 55 | } |
56 | 56 | ||
57 | .video-channel-followers { | ||
58 | color: pvar(--mainForegroundColor); | ||
59 | } | ||
60 | |||
57 | .video-channel-buttons { | 61 | .video-channel-buttons { |
58 | margin-top: 10px; | 62 | margin-top: 10px; |
59 | min-width: 190px; | 63 | min-width: 190px; |
diff --git a/client/src/app/+my-library/my-follows/my-followers.component.html b/client/src/app/+my-library/my-follows/my-followers.component.html new file mode 100644 index 000000000..d2b2dccb6 --- /dev/null +++ b/client/src/app/+my-library/my-follows/my-followers.component.html | |||
@@ -0,0 +1,31 @@ | |||
1 | <h1> | ||
2 | <span> | ||
3 | <my-global-icon iconName="follower" aria-hidden="true"></my-global-icon> | ||
4 | <ng-container i18n>My followers</ng-container> | ||
5 | <span class="badge badge-secondary"> {{ pagination.totalItems }}</span> | ||
6 | </span> | ||
7 | </h1> | ||
8 | |||
9 | <div class="followers-header"> | ||
10 | <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> | ||
11 | </div> | ||
12 | |||
13 | <div class="no-results" i18n *ngIf="pagination.totalItems === 0">No follower found.</div> | ||
14 | |||
15 | <div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> | ||
16 | <div *ngFor="let follow of follows" class="actor"> | ||
17 | <my-actor-avatar [account]="follow.follower" [href]="follow.follower.url"></my-actor-avatar> | ||
18 | |||
19 | <div class="actor-info"> | ||
20 | <a [href]="follow.follower.url" class="actor-names" rel="noopener noreferrer" target="_blank" i18n-title title="Follower page"> | ||
21 | <div class="actor-display-name">{{ follow.follower.name + '@' + follow.follower.host }}</div> | ||
22 | <span class="glyphicon glyphicon-new-window"></span> | ||
23 | </a> | ||
24 | |||
25 | <div class="text-muted"> | ||
26 | <ng-container *ngIf="isFollowingAccount(follow)" i18n>Is following all your channels</ng-container> | ||
27 | <ng-container *ngIf="!isFollowingAccount(follow)" i18n>Is following your channel {{ follow.following.name }}</ng-container> | ||
28 | </div> | ||
29 | </div> | ||
30 | </div> | ||
31 | </div> | ||
diff --git a/client/src/app/+my-library/my-follows/my-followers.component.scss b/client/src/app/+my-library/my-follows/my-followers.component.scss new file mode 100644 index 000000000..15b51c419 --- /dev/null +++ b/client/src/app/+my-library/my-follows/my-followers.component.scss | |||
@@ -0,0 +1,26 @@ | |||
1 | @use '_variables' as *; | ||
2 | @use '_mixins' as *; | ||
3 | @use '_actor' as *; | ||
4 | |||
5 | .followers-header { | ||
6 | margin-bottom: 30px; | ||
7 | display: flex; | ||
8 | } | ||
9 | |||
10 | input[type=text] { | ||
11 | @include peertube-input-text(300px); | ||
12 | } | ||
13 | |||
14 | .actor { | ||
15 | @include actor-row($avatar-size: 40px, $min-height: auto, $separator: true); | ||
16 | |||
17 | .actor-display-name { | ||
18 | font-size: 16px; | ||
19 | |||
20 | + .glyphicon { | ||
21 | @include margin-left(5px); | ||
22 | |||
23 | font-size: 12px; | ||
24 | } | ||
25 | } | ||
26 | } | ||
diff --git a/client/src/app/+my-library/my-follows/my-followers.component.ts b/client/src/app/+my-library/my-follows/my-followers.component.ts new file mode 100644 index 000000000..a7bbe6d99 --- /dev/null +++ b/client/src/app/+my-library/my-follows/my-followers.component.ts | |||
@@ -0,0 +1,76 @@ | |||
1 | import { Subject } from 'rxjs' | ||
2 | import { Component, OnInit } from '@angular/core' | ||
3 | import { ActivatedRoute } from '@angular/router' | ||
4 | import { AuthService, ComponentPagination, Notifier } from '@app/core' | ||
5 | import { UserSubscriptionService } from '@app/shared/shared-user-subscription' | ||
6 | import { ActorFollow } from '@shared/models' | ||
7 | |||
8 | @Component({ | ||
9 | templateUrl: './my-followers.component.html', | ||
10 | styleUrls: [ './my-followers.component.scss' ] | ||
11 | }) | ||
12 | export class MyFollowersComponent implements OnInit { | ||
13 | follows: ActorFollow[] = [] | ||
14 | |||
15 | pagination: ComponentPagination = { | ||
16 | currentPage: 1, | ||
17 | itemsPerPage: 10, | ||
18 | totalItems: null | ||
19 | } | ||
20 | |||
21 | onDataSubject = new Subject<any[]>() | ||
22 | search: string | ||
23 | |||
24 | constructor ( | ||
25 | private route: ActivatedRoute, | ||
26 | private auth: AuthService, | ||
27 | private userSubscriptionService: UserSubscriptionService, | ||
28 | private notifier: Notifier | ||
29 | ) {} | ||
30 | |||
31 | ngOnInit () { | ||
32 | if (this.route.snapshot.queryParams['search']) { | ||
33 | this.search = this.route.snapshot.queryParams['search'] | ||
34 | } | ||
35 | } | ||
36 | |||
37 | onNearOfBottom () { | ||
38 | // Last page | ||
39 | if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return | ||
40 | |||
41 | this.pagination.currentPage += 1 | ||
42 | this.loadFollowers() | ||
43 | } | ||
44 | |||
45 | onSearch (search: string) { | ||
46 | this.search = search | ||
47 | this.loadFollowers(false) | ||
48 | } | ||
49 | |||
50 | isFollowingAccount (follow: ActorFollow) { | ||
51 | return follow.following.name === this.getUsername() | ||
52 | } | ||
53 | |||
54 | private loadFollowers (more = true) { | ||
55 | this.userSubscriptionService.listFollowers({ | ||
56 | pagination: this.pagination, | ||
57 | nameWithHost: this.getUsername(), | ||
58 | search: this.search | ||
59 | }).subscribe({ | ||
60 | next: res => { | ||
61 | this.follows = more | ||
62 | ? this.follows.concat(res.data) | ||
63 | : res.data | ||
64 | this.pagination.totalItems = res.total | ||
65 | |||
66 | this.onDataSubject.next(res.data) | ||
67 | }, | ||
68 | |||
69 | error: err => this.notifier.error(err.message) | ||
70 | }) | ||
71 | } | ||
72 | |||
73 | private getUsername () { | ||
74 | return this.auth.getUser().username | ||
75 | } | ||
76 | } | ||
diff --git a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html b/client/src/app/+my-library/my-follows/my-subscriptions.component.html index ca5ad794a..775f0e783 100644 --- a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html +++ b/client/src/app/+my-library/my-follows/my-subscriptions.component.html | |||
@@ -12,17 +12,17 @@ | |||
12 | 12 | ||
13 | <div class="no-results" i18n *ngIf="pagination.totalItems === 0">You don't have any subscription yet.</div> | 13 | <div class="no-results" i18n *ngIf="pagination.totalItems === 0">You don't have any subscription yet.</div> |
14 | 14 | ||
15 | <div class="video-channels" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> | 15 | <div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> |
16 | <div *ngFor="let videoChannel of videoChannels" class="video-channel"> | 16 | <div *ngFor="let videoChannel of videoChannels" class="actor"> |
17 | <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]"></my-actor-avatar> | 17 | <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]"></my-actor-avatar> |
18 | 18 | ||
19 | <div class="video-channel-info"> | 19 | <div class="actor-info"> |
20 | <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page"> | 20 | <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="actor-names" i18n-title title="Channel page"> |
21 | <div class="video-channel-display-name">{{ videoChannel.displayName }}</div> | 21 | <div class="actor-display-name">{{ videoChannel.displayName }}</div> |
22 | <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div> | 22 | <div class="actor-name">{{ videoChannel.nameWithHost }}</div> |
23 | </a> | 23 | </a> |
24 | 24 | ||
25 | <div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div> | 25 | <div i18n class="actor-followers">{{ videoChannel.followersCount }} subscribers</div> |
26 | 26 | ||
27 | <a [routerLink]="[ '/a', videoChannel.ownerBy ]" i18n-title title="Owner account page" class="actor-owner"> | 27 | <a [routerLink]="[ '/a', videoChannel.ownerBy ]" i18n-title title="Owner account page" class="actor-owner"> |
28 | <span i18n>Created by {{ videoChannel.ownerBy }}</span> | 28 | <span i18n>Created by {{ videoChannel.ownerBy }}</span> |
diff --git a/client/src/app/+my-library/my-follows/my-subscriptions.component.scss b/client/src/app/+my-library/my-follows/my-subscriptions.component.scss new file mode 100644 index 000000000..310e11cb0 --- /dev/null +++ b/client/src/app/+my-library/my-follows/my-subscriptions.component.scss | |||
@@ -0,0 +1,16 @@ | |||
1 | @use '_variables' as *; | ||
2 | @use '_mixins' as *; | ||
3 | @use '_actor' as *; | ||
4 | |||
5 | .video-subscriptions-header { | ||
6 | margin-bottom: 30px; | ||
7 | display: flex; | ||
8 | } | ||
9 | |||
10 | input[type=text] { | ||
11 | @include peertube-input-text(300px); | ||
12 | } | ||
13 | |||
14 | .actor { | ||
15 | @include actor-row; | ||
16 | } | ||
diff --git a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.ts b/client/src/app/+my-library/my-follows/my-subscriptions.component.ts index f676aa014..f676aa014 100644 --- a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.ts +++ b/client/src/app/+my-library/my-follows/my-subscriptions.component.ts | |||
diff --git a/client/src/app/+my-library/my-library-routing.module.ts b/client/src/app/+my-library/my-library-routing.module.ts index 76894bed8..73858fb82 100644 --- a/client/src/app/+my-library/my-library-routing.module.ts +++ b/client/src/app/+my-library/my-library-routing.module.ts | |||
@@ -1,10 +1,11 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { LoginGuard } from '../core' | 3 | import { LoginGuard } from '../core' |
4 | import { MyFollowersComponent } from './my-follows/my-followers.component' | ||
5 | import { MySubscriptionsComponent } from './my-follows/my-subscriptions.component' | ||
4 | import { MyHistoryComponent } from './my-history/my-history.component' | 6 | import { MyHistoryComponent } from './my-history/my-history.component' |
5 | import { MyLibraryComponent } from './my-library.component' | 7 | import { MyLibraryComponent } from './my-library.component' |
6 | import { MyOwnershipComponent } from './my-ownership/my-ownership.component' | 8 | import { MyOwnershipComponent } from './my-ownership/my-ownership.component' |
7 | import { MySubscriptionsComponent } from './my-subscriptions/my-subscriptions.component' | ||
8 | import { MyVideoImportsComponent } from './my-video-imports/my-video-imports.component' | 9 | import { MyVideoImportsComponent } from './my-video-imports/my-video-imports.component' |
9 | import { MyVideoPlaylistCreateComponent } from './my-video-playlists/my-video-playlist-create.component' | 10 | import { MyVideoPlaylistCreateComponent } from './my-video-playlists/my-video-playlist-create.component' |
10 | import { MyVideoPlaylistElementsComponent } from './my-video-playlists/my-video-playlist-elements.component' | 11 | import { MyVideoPlaylistElementsComponent } from './my-video-playlists/my-video-playlist-elements.component' |
@@ -100,6 +101,15 @@ const myLibraryRoutes: Routes = [ | |||
100 | } | 101 | } |
101 | }, | 102 | }, |
102 | { | 103 | { |
104 | path: 'followers', | ||
105 | component: MyFollowersComponent, | ||
106 | data: { | ||
107 | meta: { | ||
108 | title: $localize`My followers` | ||
109 | } | ||
110 | } | ||
111 | }, | ||
112 | { | ||
103 | path: 'ownership', | 113 | path: 'ownership', |
104 | component: MyOwnershipComponent, | 114 | component: MyOwnershipComponent, |
105 | data: { | 115 | data: { |
diff --git a/client/src/app/+my-library/my-library.component.ts b/client/src/app/+my-library/my-library.component.ts index 16a7f63e3..ff901952f 100644 --- a/client/src/app/+my-library/my-library.component.ts +++ b/client/src/app/+my-library/my-library.component.ts | |||
@@ -61,8 +61,19 @@ export class MyLibraryComponent implements OnInit { | |||
61 | }, | 61 | }, |
62 | 62 | ||
63 | { | 63 | { |
64 | label: $localize`Subscriptions`, | 64 | label: $localize`Follows`, |
65 | routerLink: '/my-library/subscriptions' | 65 | children: [ |
66 | { | ||
67 | label: $localize`Subscriptions`, | ||
68 | iconName: 'subscriptions', | ||
69 | routerLink: '/my-library/subscriptions' | ||
70 | }, | ||
71 | { | ||
72 | label: $localize`Followers`, | ||
73 | iconName: 'follower', | ||
74 | routerLink: '/my-library/followers' | ||
75 | } | ||
76 | ] | ||
66 | }, | 77 | }, |
67 | 78 | ||
68 | { | 79 | { |
diff --git a/client/src/app/+my-library/my-library.module.ts b/client/src/app/+my-library/my-library.module.ts index 264ad03f7..360c53589 100644 --- a/client/src/app/+my-library/my-library.module.ts +++ b/client/src/app/+my-library/my-library.module.ts | |||
@@ -13,12 +13,13 @@ import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscripti | |||
13 | import { SharedVideoLiveModule } from '@app/shared/shared-video-live' | 13 | import { SharedVideoLiveModule } from '@app/shared/shared-video-live' |
14 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' | 14 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' |
15 | import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist/shared-video-playlist.module' | 15 | import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist/shared-video-playlist.module' |
16 | import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module' | ||
17 | import { MySubscriptionsComponent } from './my-follows/my-subscriptions.component' | ||
16 | import { MyHistoryComponent } from './my-history/my-history.component' | 18 | import { MyHistoryComponent } from './my-history/my-history.component' |
17 | import { MyLibraryRoutingModule } from './my-library-routing.module' | 19 | import { MyLibraryRoutingModule } from './my-library-routing.module' |
18 | import { MyLibraryComponent } from './my-library.component' | 20 | import { MyLibraryComponent } from './my-library.component' |
19 | import { MyAcceptOwnershipComponent } from './my-ownership/my-accept-ownership/my-accept-ownership.component' | 21 | import { MyAcceptOwnershipComponent } from './my-ownership/my-accept-ownership/my-accept-ownership.component' |
20 | import { MyOwnershipComponent } from './my-ownership/my-ownership.component' | 22 | import { MyOwnershipComponent } from './my-ownership/my-ownership.component' |
21 | import { MySubscriptionsComponent } from './my-subscriptions/my-subscriptions.component' | ||
22 | import { MyVideoImportsComponent } from './my-video-imports/my-video-imports.component' | 23 | import { MyVideoImportsComponent } from './my-video-imports/my-video-imports.component' |
23 | import { MyVideoPlaylistCreateComponent } from './my-video-playlists/my-video-playlist-create.component' | 24 | import { MyVideoPlaylistCreateComponent } from './my-video-playlists/my-video-playlist-create.component' |
24 | import { MyVideoPlaylistElementsComponent } from './my-video-playlists/my-video-playlist-elements.component' | 25 | import { MyVideoPlaylistElementsComponent } from './my-video-playlists/my-video-playlist-elements.component' |
@@ -26,7 +27,7 @@ import { MyVideoPlaylistUpdateComponent } from './my-video-playlists/my-video-pl | |||
26 | import { MyVideoPlaylistsComponent } from './my-video-playlists/my-video-playlists.component' | 27 | import { MyVideoPlaylistsComponent } from './my-video-playlists/my-video-playlists.component' |
27 | import { VideoChangeOwnershipComponent } from './my-videos/modals/video-change-ownership.component' | 28 | import { VideoChangeOwnershipComponent } from './my-videos/modals/video-change-ownership.component' |
28 | import { MyVideosComponent } from './my-videos/my-videos.component' | 29 | import { MyVideosComponent } from './my-videos/my-videos.component' |
29 | import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module' | 30 | import { MyFollowersComponent } from './my-follows/my-followers.component' |
30 | 31 | ||
31 | @NgModule({ | 32 | @NgModule({ |
32 | imports: [ | 33 | imports: [ |
@@ -61,6 +62,7 @@ import { SharedActorImageModule } from '../shared/shared-actor-image/shared-acto | |||
61 | MyAcceptOwnershipComponent, | 62 | MyAcceptOwnershipComponent, |
62 | MyVideoImportsComponent, | 63 | MyVideoImportsComponent, |
63 | MySubscriptionsComponent, | 64 | MySubscriptionsComponent, |
65 | MyFollowersComponent, | ||
64 | MyHistoryComponent, | 66 | MyHistoryComponent, |
65 | 67 | ||
66 | MyVideoPlaylistCreateComponent, | 68 | MyVideoPlaylistCreateComponent, |
diff --git a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss deleted file mode 100644 index edca06a66..000000000 --- a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss +++ /dev/null | |||
@@ -1,84 +0,0 @@ | |||
1 | @use '_variables' as *; | ||
2 | @use '_mixins' as *; | ||
3 | |||
4 | input[type=text] { | ||
5 | @include peertube-input-text(300px); | ||
6 | } | ||
7 | |||
8 | .video-channel { | ||
9 | @include row-blocks; | ||
10 | |||
11 | > my-actor-avatar { | ||
12 | @include actor-avatar-size(80px); | ||
13 | |||
14 | @include margin-right(10px); | ||
15 | } | ||
16 | } | ||
17 | |||
18 | .video-channel-info { | ||
19 | flex-grow: 1; | ||
20 | |||
21 | a.video-channel-names { | ||
22 | @include disable-default-a-behaviour; | ||
23 | |||
24 | width: fit-content; | ||
25 | display: flex; | ||
26 | align-items: baseline; | ||
27 | color: pvar(--mainForegroundColor); | ||
28 | |||
29 | .video-channel-display-name { | ||
30 | font-weight: $font-semibold; | ||
31 | font-size: 18px; | ||
32 | } | ||
33 | |||
34 | .video-channel-name { | ||
35 | @include margin-left(5px); | ||
36 | |||
37 | font-size: 14px; | ||
38 | color: $grey-actor-name; | ||
39 | } | ||
40 | } | ||
41 | } | ||
42 | |||
43 | .actor-owner { | ||
44 | @include disable-default-a-behaviour; | ||
45 | |||
46 | font-size: 13px; | ||
47 | color: pvar(--mainForegroundColor); | ||
48 | |||
49 | span:hover { | ||
50 | opacity: 0.8; | ||
51 | } | ||
52 | |||
53 | my-actor-avatar { | ||
54 | @include margin-left(7px); | ||
55 | display: inline-block; | ||
56 | vertical-align: top; | ||
57 | } | ||
58 | } | ||
59 | |||
60 | .video-subscriptions-header { | ||
61 | margin-bottom: 30px; | ||
62 | display: flex; | ||
63 | } | ||
64 | |||
65 | @media screen and (max-width: $small-view) { | ||
66 | .video-subscriptions-header input[type=text] { | ||
67 | width: 100% !important; | ||
68 | } | ||
69 | |||
70 | .video-channel-info { | ||
71 | padding-bottom: 10px; | ||
72 | text-align: center; | ||
73 | |||
74 | .video-channel-names { | ||
75 | flex-direction: column; | ||
76 | align-items: center !important; | ||
77 | margin: auto; | ||
78 | } | ||
79 | } | ||
80 | |||
81 | img { | ||
82 | @include margin-right(0); | ||
83 | } | ||
84 | } | ||
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss index d174dcd62..72ee2d7bb 100644 --- a/client/src/app/+video-channels/video-channels.component.scss +++ b/client/src/app/+video-channels/video-channels.component.scss | |||
@@ -1,6 +1,6 @@ | |||
1 | @use '_variables' as *; | 1 | @use '_variables' as *; |
2 | @use '_mixins' as *; | 2 | @use '_mixins' as *; |
3 | @use '_actor' as *; | 3 | @use '_account-channel-page' as *; |
4 | @use '_miniature' as *; | 4 | @use '_miniature' as *; |
5 | 5 | ||
6 | .root { | 6 | .root { |
diff --git a/client/src/app/core/rest/rest.service.ts b/client/src/app/core/rest/rest.service.ts index 98e45ffc0..93b5f56b2 100644 --- a/client/src/app/core/rest/rest.service.ts +++ b/client/src/app/core/rest/rest.service.ts | |||
@@ -13,9 +13,8 @@ interface QueryStringFilterPrefixes { | |||
13 | } | 13 | } |
14 | } | 14 | } |
15 | 15 | ||
16 | type ParseQueryStringFilterResult = { | 16 | type ParseQueryStringFilters <K extends keyof any> = Partial<Record<K, string | number | boolean | (string | number | boolean)[]>> |
17 | [key: string]: string | number | boolean | (string | number | boolean)[] | 17 | type ParseQueryStringFiltersResult <K extends keyof any> = ParseQueryStringFilters<K> & { search?: string } |
18 | } | ||
19 | 18 | ||
20 | @Injectable() | 19 | @Injectable() |
21 | export class RestService { | 20 | export class RestService { |
@@ -67,14 +66,17 @@ export class RestService { | |||
67 | return params | 66 | return params |
68 | } | 67 | } |
69 | 68 | ||
70 | componentPaginationToRestPagination (componentPagination: ComponentPaginationLight): RestPagination { | 69 | componentToRestPagination (componentPagination: ComponentPaginationLight): RestPagination { |
71 | const start: number = (componentPagination.currentPage - 1) * componentPagination.itemsPerPage | 70 | const start: number = (componentPagination.currentPage - 1) * componentPagination.itemsPerPage |
72 | const count: number = componentPagination.itemsPerPage | 71 | const count: number = componentPagination.itemsPerPage |
73 | 72 | ||
74 | return { start, count } | 73 | return { start, count } |
75 | } | 74 | } |
76 | 75 | ||
77 | parseQueryStringFilter (q: string, prefixes: QueryStringFilterPrefixes): ParseQueryStringFilterResult { | 76 | /* |
77 | * Returns an object containing the filters and the remaining search | ||
78 | */ | ||
79 | parseQueryStringFilter <T extends QueryStringFilterPrefixes> (q: string, prefixes: T): ParseQueryStringFiltersResult<keyof T> { | ||
78 | if (!q) return {} | 80 | if (!q) return {} |
79 | 81 | ||
80 | // Tokenize the strings using spaces that are not in quotes | 82 | // Tokenize the strings using spaces that are not in quotes |
@@ -90,9 +92,9 @@ export class RestService { | |||
90 | return prefixeStrings.every(prefixString => t.startsWith(prefixString) === false) | 92 | return prefixeStrings.every(prefixString => t.startsWith(prefixString) === false) |
91 | }) | 93 | }) |
92 | 94 | ||
93 | const additionalFilters: ParseQueryStringFilterResult = {} | 95 | const additionalFilters: ParseQueryStringFilters<keyof T> = {} |
94 | 96 | ||
95 | for (const prefixKey of Object.keys(prefixes)) { | 97 | for (const prefixKey of Object.keys(prefixes) as (keyof T)[]) { |
96 | const prefixObj = prefixes[prefixKey] | 98 | const prefixObj = prefixes[prefixKey] |
97 | const prefix = prefixObj.prefix | 99 | const prefix = prefixObj.prefix |
98 | 100 | ||
diff --git a/client/src/app/shared/shared-custom-markup/custom-markup-help.component.html b/client/src/app/shared/shared-custom-markup/custom-markup-help.component.html index dd7a56d7d..0ca84ff78 100644 --- a/client/src/app/shared/shared-custom-markup/custom-markup-help.component.html +++ b/client/src/app/shared/shared-custom-markup/custom-markup-help.component.html | |||
@@ -1,3 +1,3 @@ | |||
1 | <ng-container i18n> | 1 | <ng-container i18n> |
2 | <a href="https://en.wikipedia.org/wiki/Markdown#Example" target="_blank" rel="noreferer noopener">Markdown compatible</a> that also supports <a href="https://docs.joinpeertube.org/api-custom-client-markup" target="_blank" rel="noreferer noopener">custom PeerTube HTML tags</a> | 2 | <a href="https://en.wikipedia.org/wiki/Markdown#Example" target="_blank" rel="noreferrer noopener">Markdown compatible</a> that also supports <a href="https://docs.joinpeertube.org/api-custom-client-markup" target="_blank" rel="noreferrer noopener">custom PeerTube HTML tags</a> |
3 | </ng-container> | 3 | </ng-container> |
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.ts b/client/src/app/shared/shared-forms/advanced-input-filter.component.ts index 72cd6d460..113219f48 100644 --- a/client/src/app/shared/shared-forms/advanced-input-filter.component.ts +++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.ts | |||
@@ -77,6 +77,8 @@ export class AdvancedInputFilterComponent implements OnInit, AfterViewInit { | |||
77 | 77 | ||
78 | logger('On route search change "%s".', search) | 78 | logger('On route search change "%s".', search) |
79 | 79 | ||
80 | if (this.searchValue === search) return | ||
81 | |||
80 | this.searchValue = search | 82 | this.searchValue = search |
81 | this.emitSearch() | 83 | this.emitSearch() |
82 | }) | 84 | }) |
diff --git a/client/src/app/shared/shared-main/users/user-history.service.ts b/client/src/app/shared/shared-main/users/user-history.service.ts index 91268af8c..a4841897d 100644 --- a/client/src/app/shared/shared-main/users/user-history.service.ts +++ b/client/src/app/shared/shared-main/users/user-history.service.ts | |||
@@ -19,7 +19,7 @@ export class UserHistoryService { | |||
19 | ) {} | 19 | ) {} |
20 | 20 | ||
21 | getUserVideosHistory (historyPagination: ComponentPaginationLight, search?: string) { | 21 | getUserVideosHistory (historyPagination: ComponentPaginationLight, search?: string) { |
22 | const pagination = this.restService.componentPaginationToRestPagination(historyPagination) | 22 | const pagination = this.restService.componentToRestPagination(historyPagination) |
23 | 23 | ||
24 | let params = new HttpParams() | 24 | let params = new HttpParams() |
25 | params = this.restService.addRestGetParams(params, pagination) | 25 | params = this.restService.addRestGetParams(params, pagination) |
diff --git a/client/src/app/shared/shared-main/users/user-notification.service.ts b/client/src/app/shared/shared-main/users/user-notification.service.ts index 09fee87a3..e27dab21a 100644 --- a/client/src/app/shared/shared-main/users/user-notification.service.ts +++ b/client/src/app/shared/shared-main/users/user-notification.service.ts | |||
@@ -29,7 +29,7 @@ export class UserNotificationService { | |||
29 | const { pagination, ignoreLoadingBar, unread, sort } = parameters | 29 | const { pagination, ignoreLoadingBar, unread, sort } = parameters |
30 | 30 | ||
31 | let params = new HttpParams() | 31 | let params = new HttpParams() |
32 | params = this.restService.addRestGetParams(params, this.restService.componentPaginationToRestPagination(pagination), sort) | 32 | params = this.restService.addRestGetParams(params, this.restService.componentToRestPagination(pagination), sort) |
33 | 33 | ||
34 | if (unread) params = params.append('unread', `${unread}`) | 34 | if (unread) params = params.append('unread', `${unread}`) |
35 | 35 | ||
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html index ee8df864a..9af6da784 100644 --- a/client/src/app/shared/shared-main/users/user-notifications.component.html +++ b/client/src/app/shared/shared-main/users/user-notifications.component.html | |||
@@ -203,7 +203,7 @@ | |||
203 | <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon> | 203 | <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon> |
204 | 204 | ||
205 | <div class="message" i18n> | 205 | <div class="message" i18n> |
206 | <a (click)="markAsRead(notification)" [href]="notification.peertubeVersionLink" target="_blank" rel="noopener noreferer">A new version of PeerTube</a> is available: {{ notification.peertube.latestVersion }} | 206 | <a (click)="markAsRead(notification)" [href]="notification.peertubeVersionLink" target="_blank" rel="noopener noreferrer">A new version of PeerTube</a> is available: {{ notification.peertube.latestVersion }} |
207 | </div> | 207 | </div> |
208 | </ng-container> | 208 | </ng-container> |
209 | 209 | ||
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts index 7560a35a8..dc00fabdc 100644 --- a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts +++ b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts | |||
@@ -50,7 +50,7 @@ export class VideoChannelService { | |||
50 | const { account, componentPagination, withStats = false, sort, search } = options | 50 | const { account, componentPagination, withStats = false, sort, search } = options |
51 | 51 | ||
52 | const pagination = componentPagination | 52 | const pagination = componentPagination |
53 | ? this.restService.componentPaginationToRestPagination(componentPagination) | 53 | ? this.restService.componentToRestPagination(componentPagination) |
54 | : { start: 0, count: 20 } | 54 | : { start: 0, count: 20 } |
55 | 55 | ||
56 | let params = new HttpParams() | 56 | let params = new HttpParams() |
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts index 3481b116f..2f43f1b9d 100644 --- a/client/src/app/shared/shared-main/video/video.service.ts +++ b/client/src/app/shared/shared-main/video/video.service.ts | |||
@@ -123,7 +123,7 @@ export class VideoService { | |||
123 | } | 123 | } |
124 | 124 | ||
125 | getMyVideos (videoPagination: ComponentPaginationLight, sort: VideoSortField, search?: string): Observable<ResultList<Video>> { | 125 | getMyVideos (videoPagination: ComponentPaginationLight, sort: VideoSortField, search?: string): Observable<ResultList<Video>> { |
126 | const pagination = this.restService.componentPaginationToRestPagination(videoPagination) | 126 | const pagination = this.restService.componentToRestPagination(videoPagination) |
127 | 127 | ||
128 | let params = new HttpParams() | 128 | let params = new HttpParams() |
129 | params = this.restService.addRestGetParams(params, pagination, sort) | 129 | params = this.restService.addRestGetParams(params, pagination, sort) |
@@ -377,7 +377,7 @@ export class VideoService { | |||
377 | private buildCommonVideosParams (options: CommonVideoParams & { params: HttpParams }) { | 377 | private buildCommonVideosParams (options: CommonVideoParams & { params: HttpParams }) { |
378 | const { params, videoPagination, sort, filter, categoryOneOf, languageOneOf, skipCount, nsfwPolicy, isLive, nsfw } = options | 378 | const { params, videoPagination, sort, filter, categoryOneOf, languageOneOf, skipCount, nsfwPolicy, isLive, nsfw } = options |
379 | 379 | ||
380 | const pagination = this.restService.componentPaginationToRestPagination(videoPagination) | 380 | const pagination = this.restService.componentToRestPagination(videoPagination) |
381 | let newParams = this.restService.addRestGetParams(params, pagination, sort) | 381 | let newParams = this.restService.addRestGetParams(params, pagination, sort) |
382 | 382 | ||
383 | if (filter) newParams = newParams.set('filter', filter) | 383 | if (filter) newParams = newParams.set('filter', filter) |
diff --git a/client/src/app/shared/shared-search/search.service.ts b/client/src/app/shared/shared-search/search.service.ts index fdfab0e0e..71350c733 100644 --- a/client/src/app/shared/shared-search/search.service.ts +++ b/client/src/app/shared/shared-search/search.service.ts | |||
@@ -43,7 +43,7 @@ export class SearchService { | |||
43 | let pagination: RestPagination | 43 | let pagination: RestPagination |
44 | 44 | ||
45 | if (componentPagination) { | 45 | if (componentPagination) { |
46 | pagination = this.restService.componentPaginationToRestPagination(componentPagination) | 46 | pagination = this.restService.componentToRestPagination(componentPagination) |
47 | } | 47 | } |
48 | 48 | ||
49 | let params = new HttpParams() | 49 | let params = new HttpParams() |
@@ -77,7 +77,7 @@ export class SearchService { | |||
77 | 77 | ||
78 | let pagination: RestPagination | 78 | let pagination: RestPagination |
79 | if (componentPagination) { | 79 | if (componentPagination) { |
80 | pagination = this.restService.componentPaginationToRestPagination(componentPagination) | 80 | pagination = this.restService.componentToRestPagination(componentPagination) |
81 | } | 81 | } |
82 | 82 | ||
83 | let params = new HttpParams() | 83 | let params = new HttpParams() |
@@ -111,7 +111,7 @@ export class SearchService { | |||
111 | 111 | ||
112 | let pagination: RestPagination | 112 | let pagination: RestPagination |
113 | if (componentPagination) { | 113 | if (componentPagination) { |
114 | pagination = this.restService.componentPaginationToRestPagination(componentPagination) | 114 | pagination = this.restService.componentToRestPagination(componentPagination) |
115 | } | 115 | } |
116 | 116 | ||
117 | let params = new HttpParams() | 117 | let params = new HttpParams() |
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 f289fb6cf..ede65ff39 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 | |||
@@ -6,7 +6,7 @@ import { Injectable } from '@angular/core' | |||
6 | import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' | 6 | import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' |
7 | import { buildBulkObservable } from '@app/helpers' | 7 | import { buildBulkObservable } from '@app/helpers' |
8 | import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' | 8 | import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' |
9 | import { ResultList, VideoChannel as VideoChannelServer, VideoSortField } from '@shared/models' | 9 | import { ActorFollow, ResultList, VideoChannel as VideoChannelServer, VideoSortField } from '@shared/models' |
10 | import { environment } from '../../../environments/environment' | 10 | import { environment } from '../../../environments/environment' |
11 | 11 | ||
12 | const logger = debug('peertube:subscriptions:UserSubscriptionService') | 12 | const logger = debug('peertube:subscriptions:UserSubscriptionService') |
@@ -17,6 +17,8 @@ type SubscriptionExistResultObservable = { [ uri: string ]: Observable<boolean> | |||
17 | @Injectable() | 17 | @Injectable() |
18 | export class UserSubscriptionService { | 18 | export class UserSubscriptionService { |
19 | static BASE_USER_SUBSCRIPTIONS_URL = environment.apiUrl + '/api/v1/users/me/subscriptions' | 19 | static BASE_USER_SUBSCRIPTIONS_URL = environment.apiUrl + '/api/v1/users/me/subscriptions' |
20 | static BASE_VIDEO_CHANNELS_URL = environment.apiUrl + '/api/v1/video-channels' | ||
21 | static BASE_ACCOUNTS_URL = environment.apiUrl + '/api/v1/accounts' | ||
20 | 22 | ||
21 | // Use a replay subject because we "next" a value before subscribing | 23 | // Use a replay subject because we "next" a value before subscribing |
22 | private existsSubject = new ReplaySubject<string>(1) | 24 | private existsSubject = new ReplaySubject<string>(1) |
@@ -43,13 +45,46 @@ export class UserSubscriptionService { | |||
43 | ) | 45 | ) |
44 | } | 46 | } |
45 | 47 | ||
48 | listFollowers (parameters: { | ||
49 | pagination: ComponentPaginationLight | ||
50 | nameWithHost: string | ||
51 | search?: string | ||
52 | }) { | ||
53 | const { pagination, nameWithHost, search } = parameters | ||
54 | |||
55 | let url = `${UserSubscriptionService.BASE_ACCOUNTS_URL}/${nameWithHost}/followers` | ||
56 | |||
57 | let params = new HttpParams() | ||
58 | params = this.restService.addRestGetParams(params, this.restService.componentToRestPagination(pagination), '-createdAt') | ||
59 | |||
60 | if (search) { | ||
61 | const filters = this.restService.parseQueryStringFilter(search, { | ||
62 | channel: { | ||
63 | prefix: 'channel:' | ||
64 | } | ||
65 | }) | ||
66 | |||
67 | if (filters.channel) { | ||
68 | url = `${UserSubscriptionService.BASE_VIDEO_CHANNELS_URL}/${filters.channel}/followers` | ||
69 | } | ||
70 | |||
71 | params = this.restService.addObjectParams(params, { search: filters.search }) | ||
72 | } | ||
73 | |||
74 | return this.authHttp | ||
75 | .get<ResultList<ActorFollow>>(url, { params }) | ||
76 | .pipe( | ||
77 | catchError(err => this.restExtractor.handleError(err)) | ||
78 | ) | ||
79 | } | ||
80 | |||
46 | getUserSubscriptionVideos (parameters: { | 81 | getUserSubscriptionVideos (parameters: { |
47 | videoPagination: ComponentPaginationLight | 82 | videoPagination: ComponentPaginationLight |
48 | sort: VideoSortField | 83 | sort: VideoSortField |
49 | skipCount?: boolean | 84 | skipCount?: boolean |
50 | }): Observable<ResultList<Video>> { | 85 | }): Observable<ResultList<Video>> { |
51 | const { videoPagination, sort, skipCount } = parameters | 86 | const { videoPagination, sort, skipCount } = parameters |
52 | const pagination = this.restService.componentPaginationToRestPagination(videoPagination) | 87 | const pagination = this.restService.componentToRestPagination(videoPagination) |
53 | 88 | ||
54 | let params = new HttpParams() | 89 | let params = new HttpParams() |
55 | params = this.restService.addRestGetParams(params, pagination, sort) | 90 | params = this.restService.addRestGetParams(params, pagination, sort) |
@@ -106,7 +141,7 @@ export class UserSubscriptionService { | |||
106 | const { pagination, search } = parameters | 141 | const { pagination, search } = parameters |
107 | const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL | 142 | const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL |
108 | 143 | ||
109 | const restPagination = this.restService.componentPaginationToRestPagination(pagination) | 144 | const restPagination = this.restService.componentToRestPagination(pagination) |
110 | 145 | ||
111 | let params = new HttpParams() | 146 | let params = new HttpParams() |
112 | params = this.restService.addRestGetParams(params, restPagination) | 147 | params = this.restService.addRestGetParams(params, restPagination) |
diff --git a/client/src/app/shared/shared-video-comment/video-comment.service.ts b/client/src/app/shared/shared-video-comment/video-comment.service.ts index 5550c96e4..fd1cae7f8 100644 --- a/client/src/app/shared/shared-video-comment/video-comment.service.ts +++ b/client/src/app/shared/shared-video-comment/video-comment.service.ts | |||
@@ -81,7 +81,7 @@ export class VideoCommentService { | |||
81 | }): Observable<ThreadsResultList<VideoComment>> { | 81 | }): Observable<ThreadsResultList<VideoComment>> { |
82 | const { videoId, componentPagination, sort } = parameters | 82 | const { videoId, componentPagination, sort } = parameters |
83 | 83 | ||
84 | const pagination = this.restService.componentPaginationToRestPagination(componentPagination) | 84 | const pagination = this.restService.componentToRestPagination(componentPagination) |
85 | 85 | ||
86 | let params = new HttpParams() | 86 | let params = new HttpParams() |
87 | params = this.restService.addRestGetParams(params, pagination, sort) | 87 | params = this.restService.addRestGetParams(params, pagination, sort) |
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts index fc291329a..3faf81d11 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts +++ b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts | |||
@@ -62,7 +62,7 @@ export class VideoPlaylistService { | |||
62 | 62 | ||
63 | listChannelPlaylists (videoChannel: VideoChannel, componentPagination: ComponentPaginationLight): Observable<ResultList<VideoPlaylist>> { | 63 | listChannelPlaylists (videoChannel: VideoChannel, componentPagination: ComponentPaginationLight): Observable<ResultList<VideoPlaylist>> { |
64 | const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/video-playlists' | 64 | const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/video-playlists' |
65 | const pagination = this.restService.componentPaginationToRestPagination(componentPagination) | 65 | const pagination = this.restService.componentToRestPagination(componentPagination) |
66 | 66 | ||
67 | let params = new HttpParams() | 67 | let params = new HttpParams() |
68 | params = this.restService.addRestGetParams(params, pagination) | 68 | params = this.restService.addRestGetParams(params, pagination) |
@@ -103,7 +103,7 @@ export class VideoPlaylistService { | |||
103 | ): Observable<ResultList<VideoPlaylist>> { | 103 | ): Observable<ResultList<VideoPlaylist>> { |
104 | const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-playlists' | 104 | const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-playlists' |
105 | const pagination = componentPagination | 105 | const pagination = componentPagination |
106 | ? this.restService.componentPaginationToRestPagination(componentPagination) | 106 | ? this.restService.componentToRestPagination(componentPagination) |
107 | : undefined | 107 | : undefined |
108 | 108 | ||
109 | let params = new HttpParams() | 109 | let params = new HttpParams() |
@@ -259,7 +259,7 @@ export class VideoPlaylistService { | |||
259 | componentPagination: ComponentPaginationLight | 259 | componentPagination: ComponentPaginationLight |
260 | }): Observable<ResultList<VideoPlaylistElement>> { | 260 | }): Observable<ResultList<VideoPlaylistElement>> { |
261 | const path = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + options.videoPlaylistId + '/videos' | 261 | const path = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + options.videoPlaylistId + '/videos' |
262 | const pagination = this.restService.componentPaginationToRestPagination(options.componentPagination) | 262 | const pagination = this.restService.componentToRestPagination(options.componentPagination) |
263 | 263 | ||
264 | let params = new HttpParams() | 264 | let params = new HttpParams() |
265 | params = this.restService.addRestGetParams(params, pagination) | 265 | params = this.restService.addRestGetParams(params, pagination) |
diff --git a/client/src/sass/include/_account-channel-page.scss b/client/src/sass/include/_account-channel-page.scss new file mode 100644 index 000000000..b135bbb6d --- /dev/null +++ b/client/src/sass/include/_account-channel-page.scss | |||
@@ -0,0 +1,88 @@ | |||
1 | @use '_variables' as *; | ||
2 | @use '_mixins' as *; | ||
3 | |||
4 | @mixin section-label-responsive { | ||
5 | color: pvar(--mainColor); | ||
6 | font-size: 12px; | ||
7 | margin-bottom: 15px; | ||
8 | font-weight: $font-bold; | ||
9 | letter-spacing: 2.5px; | ||
10 | |||
11 | @media screen and (max-width: $mobile-view) { | ||
12 | font-size: 10px; | ||
13 | letter-spacing: 2.1px; | ||
14 | margin-bottom: 5px; | ||
15 | } | ||
16 | } | ||
17 | |||
18 | @mixin show-more-description { | ||
19 | color: pvar(--mainColor); | ||
20 | cursor: pointer; | ||
21 | margin: 10px auto 45px; | ||
22 | } | ||
23 | |||
24 | @mixin avatar-row-responsive ($img-margin, $grey-font-size) { | ||
25 | display: flex; | ||
26 | grid-column: 1; | ||
27 | margin-bottom: 30px; | ||
28 | |||
29 | .main-avatar { | ||
30 | @include actor-avatar-size(120px); | ||
31 | } | ||
32 | |||
33 | > div { | ||
34 | @include margin-left($img-margin); | ||
35 | |||
36 | min-width: 1px; | ||
37 | } | ||
38 | |||
39 | .actor-info { | ||
40 | display: flex; | ||
41 | |||
42 | > div:first-child { | ||
43 | flex-grow: 1; | ||
44 | min-width: 1px; | ||
45 | } | ||
46 | } | ||
47 | |||
48 | .actor-display-name { | ||
49 | @include peertube-word-wrap; | ||
50 | |||
51 | display: flex; | ||
52 | flex-wrap: wrap; | ||
53 | } | ||
54 | |||
55 | h1 { | ||
56 | font-size: 28px; | ||
57 | font-weight: $font-bold; | ||
58 | margin: 0; | ||
59 | } | ||
60 | |||
61 | .actor-handle { | ||
62 | @include ellipsis; | ||
63 | } | ||
64 | |||
65 | .actor-handle, | ||
66 | .actor-counters { | ||
67 | color: pvar(--greyForegroundColor); | ||
68 | font-size: $grey-font-size; | ||
69 | } | ||
70 | |||
71 | .actor-counters > *:not(:last-child)::after { | ||
72 | content: '•'; | ||
73 | margin: 0 10px; | ||
74 | color: pvar(--mainColor); | ||
75 | } | ||
76 | |||
77 | @media screen and (max-width: $mobile-view) { | ||
78 | margin-bottom: 15px; | ||
79 | |||
80 | h1 { | ||
81 | font-size: 22px; | ||
82 | } | ||
83 | |||
84 | .main-avatar { | ||
85 | @include actor-avatar-size(80px); | ||
86 | } | ||
87 | } | ||
88 | } | ||
diff --git a/client/src/sass/include/_actor.scss b/client/src/sass/include/_actor.scss index b135bbb6d..f9e44b8ad 100644 --- a/client/src/sass/include/_actor.scss +++ b/client/src/sass/include/_actor.scss | |||
@@ -1,88 +1,68 @@ | |||
1 | @use '_variables' as *; | 1 | @use '_variables' as *; |
2 | @use '_mixins' as *; | 2 | @use '_mixins' as *; |
3 | 3 | ||
4 | @mixin section-label-responsive { | 4 | @mixin actor-row ($avatar-size: 80px, $avatar-margin-right: 10px, $min-height: 130px, $separator: true) { |
5 | color: pvar(--mainColor); | 5 | @include row-blocks($min-height: $min-height, $separator: $separator); |
6 | font-size: 12px; | ||
7 | margin-bottom: 15px; | ||
8 | font-weight: $font-bold; | ||
9 | letter-spacing: 2.5px; | ||
10 | |||
11 | @media screen and (max-width: $mobile-view) { | ||
12 | font-size: 10px; | ||
13 | letter-spacing: 2.1px; | ||
14 | margin-bottom: 5px; | ||
15 | } | ||
16 | } | ||
17 | |||
18 | @mixin show-more-description { | ||
19 | color: pvar(--mainColor); | ||
20 | cursor: pointer; | ||
21 | margin: 10px auto 45px; | ||
22 | } | ||
23 | |||
24 | @mixin avatar-row-responsive ($img-margin, $grey-font-size) { | ||
25 | display: flex; | ||
26 | grid-column: 1; | ||
27 | margin-bottom: 30px; | ||
28 | 6 | ||
29 | .main-avatar { | 7 | > my-actor-avatar { |
30 | @include actor-avatar-size(120px); | 8 | @include actor-avatar-size($avatar-size); |
31 | } | ||
32 | |||
33 | > div { | ||
34 | @include margin-left($img-margin); | ||
35 | 9 | ||
36 | min-width: 1px; | 10 | @include margin-right($avatar-margin-right); |
37 | } | 11 | } |
38 | 12 | ||
39 | .actor-info { | 13 | .actor-info { |
40 | display: flex; | 14 | flex-grow: 1; |
41 | |||
42 | > div:first-child { | ||
43 | flex-grow: 1; | ||
44 | min-width: 1px; | ||
45 | } | ||
46 | } | 15 | } |
47 | 16 | ||
48 | .actor-display-name { | 17 | .actor-names { |
49 | @include peertube-word-wrap; | 18 | @include disable-default-a-behaviour; |
50 | 19 | ||
20 | width: fit-content; | ||
51 | display: flex; | 21 | display: flex; |
52 | flex-wrap: wrap; | 22 | align-items: baseline; |
23 | color: pvar(--mainForegroundColor); | ||
53 | } | 24 | } |
54 | 25 | ||
55 | h1 { | 26 | .actor-display-name { |
56 | font-size: 28px; | 27 | font-weight: $font-semibold; |
57 | font-weight: $font-bold; | 28 | font-size: 18px; |
58 | margin: 0; | ||
59 | } | 29 | } |
60 | 30 | ||
61 | .actor-handle { | 31 | .actor-name { |
62 | @include ellipsis; | 32 | @include margin-left(5px); |
63 | } | ||
64 | 33 | ||
65 | .actor-handle, | 34 | font-size: 14px; |
66 | .actor-counters { | 35 | color: $grey-actor-name; |
67 | color: pvar(--greyForegroundColor); | ||
68 | font-size: $grey-font-size; | ||
69 | } | 36 | } |
70 | 37 | ||
71 | .actor-counters > *:not(:last-child)::after { | 38 | .actor-owner { |
72 | content: '•'; | 39 | @include disable-default-a-behaviour; |
73 | margin: 0 10px; | ||
74 | color: pvar(--mainColor); | ||
75 | } | ||
76 | 40 | ||
77 | @media screen and (max-width: $mobile-view) { | 41 | font-size: 13px; |
78 | margin-bottom: 15px; | 42 | color: pvar(--mainForegroundColor); |
79 | 43 | ||
80 | h1 { | 44 | span:hover { |
81 | font-size: 22px; | 45 | opacity: 0.8; |
82 | } | 46 | } |
83 | 47 | ||
84 | .main-avatar { | 48 | my-actor-avatar { |
85 | @include actor-avatar-size(80px); | 49 | @include margin-left(7px); |
50 | |||
51 | display: inline-block; | ||
52 | vertical-align: top; | ||
53 | } | ||
54 | } | ||
55 | |||
56 | @media screen and (max-width: $small-view) { | ||
57 | .actor-info { | ||
58 | padding-bottom: 10px; | ||
59 | text-align: center; | ||
60 | |||
61 | .actor-names { | ||
62 | flex-direction: column; | ||
63 | align-items: center !important; | ||
64 | margin: auto; | ||
65 | } | ||
86 | } | 66 | } |
87 | } | 67 | } |
88 | } | 68 | } |
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index 9e510771e..2f436d787 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -653,12 +653,15 @@ | |||
653 | @include button-with-icon(20px, 5px, -1px); | 653 | @include button-with-icon(20px, 5px, -1px); |
654 | } | 654 | } |
655 | 655 | ||
656 | @mixin row-blocks ($column-responsive: true) { | 656 | @mixin row-blocks ($column-responsive: true, $min-height: 130px, $separator: true) { |
657 | display: flex; | 657 | display: flex; |
658 | min-height: 130px; | 658 | min-height: $min-height; |
659 | padding-bottom: 20px; | 659 | padding-bottom: 20px; |
660 | margin-bottom: 20px; | 660 | margin-bottom: 20px; |
661 | border-bottom: 1px solid #C6C6C6; | 661 | |
662 | @if $separator { | ||
663 | border-bottom: 1px solid #C6C6C6; | ||
664 | } | ||
662 | 665 | ||
663 | @media screen and (max-width: $small-view) { | 666 | @media screen and (max-width: $small-view) { |
664 | @if $column-responsive { | 667 | @if $column-responsive { |
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index 75679b0f4..77edfa7c2 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { pickCommonVideoQuery } from '@server/helpers/query' | 2 | import { pickCommonVideoQuery } from '@server/helpers/query' |
3 | import { ActorFollowModel } from '@server/models/actor/actor-follow' | ||
3 | import { getServerActor } from '@server/models/application/application' | 4 | import { getServerActor } from '@server/models/application/application' |
4 | import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' | 5 | import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' |
5 | import { getFormattedObjects } from '../../helpers/utils' | 6 | import { getFormattedObjects } from '../../helpers/utils' |
@@ -20,6 +21,7 @@ import { | |||
20 | } from '../../middlewares' | 21 | } from '../../middlewares' |
21 | import { | 22 | import { |
22 | accountNameWithHostGetValidator, | 23 | accountNameWithHostGetValidator, |
24 | accountsFollowersSortValidator, | ||
23 | accountsSortValidator, | 25 | accountsSortValidator, |
24 | ensureAuthUserOwnsAccountValidator, | 26 | ensureAuthUserOwnsAccountValidator, |
25 | videoChannelsSortValidator, | 27 | videoChannelsSortValidator, |
@@ -93,6 +95,17 @@ accountsRouter.get('/:accountName/ratings', | |||
93 | asyncMiddleware(listAccountRatings) | 95 | asyncMiddleware(listAccountRatings) |
94 | ) | 96 | ) |
95 | 97 | ||
98 | accountsRouter.get('/:accountName/followers', | ||
99 | authenticate, | ||
100 | asyncMiddleware(accountNameWithHostGetValidator), | ||
101 | ensureAuthUserOwnsAccountValidator, | ||
102 | paginationValidator, | ||
103 | accountsFollowersSortValidator, | ||
104 | setDefaultSort, | ||
105 | setDefaultPagination, | ||
106 | asyncMiddleware(listAccountFollowers) | ||
107 | ) | ||
108 | |||
96 | // --------------------------------------------------------------------------- | 109 | // --------------------------------------------------------------------------- |
97 | 110 | ||
98 | export { | 111 | export { |
@@ -127,7 +140,7 @@ async function listAccountChannels (req: express.Request, res: express.Response) | |||
127 | search: req.query.search | 140 | search: req.query.search |
128 | } | 141 | } |
129 | 142 | ||
130 | const resultList = await VideoChannelModel.listByAccount(options) | 143 | const resultList = await VideoChannelModel.listByAccountForAPI(options) |
131 | 144 | ||
132 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 145 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
133 | } | 146 | } |
@@ -195,3 +208,21 @@ async function listAccountRatings (req: express.Request, res: express.Response) | |||
195 | }) | 208 | }) |
196 | return res.json(getFormattedObjects(resultList.rows, resultList.count)) | 209 | return res.json(getFormattedObjects(resultList.rows, resultList.count)) |
197 | } | 210 | } |
211 | |||
212 | async function listAccountFollowers (req: express.Request, res: express.Response) { | ||
213 | const account = res.locals.account | ||
214 | |||
215 | const channels = await VideoChannelModel.listAllByAccount(account.id) | ||
216 | const actorIds = [ account.actorId ].concat(channels.map(c => c.actorId)) | ||
217 | |||
218 | const resultList = await ActorFollowModel.listFollowersForApi({ | ||
219 | actorIds, | ||
220 | start: req.query.start, | ||
221 | count: req.query.count, | ||
222 | sort: req.query.sort, | ||
223 | search: req.query.search, | ||
224 | state: 'accepted', | ||
225 | }) | ||
226 | |||
227 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
228 | } | ||
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts index 76ed75186..c613386b2 100644 --- a/server/controllers/api/server/follows.ts +++ b/server/controllers/api/server/follows.ts | |||
@@ -98,7 +98,7 @@ export { | |||
98 | 98 | ||
99 | async function listFollowing (req: express.Request, res: express.Response) { | 99 | async function listFollowing (req: express.Request, res: express.Response) { |
100 | const serverActor = await getServerActor() | 100 | const serverActor = await getServerActor() |
101 | const resultList = await ActorFollowModel.listFollowingForApi({ | 101 | const resultList = await ActorFollowModel.listInstanceFollowingForApi({ |
102 | id: serverActor.id, | 102 | id: serverActor.id, |
103 | start: req.query.start, | 103 | start: req.query.start, |
104 | count: req.query.count, | 104 | count: req.query.count, |
@@ -114,7 +114,7 @@ async function listFollowing (req: express.Request, res: express.Response) { | |||
114 | async function listFollowers (req: express.Request, res: express.Response) { | 114 | async function listFollowers (req: express.Request, res: express.Response) { |
115 | const serverActor = await getServerActor() | 115 | const serverActor = await getServerActor() |
116 | const resultList = await ActorFollowModel.listFollowersForApi({ | 116 | const resultList = await ActorFollowModel.listFollowersForApi({ |
117 | actorId: serverActor.id, | 117 | actorIds: [ serverActor.id ], |
118 | start: req.query.start, | 118 | start: req.query.start, |
119 | count: req.query.count, | 119 | count: req.query.count, |
120 | sort: req.query.sort, | 120 | sort: req.query.sort, |
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts index e3c0cf089..b2b441673 100644 --- a/server/controllers/api/users/my-subscriptions.ts +++ b/server/controllers/api/users/my-subscriptions.ts | |||
@@ -95,7 +95,7 @@ async function areSubscriptionsExist (req: express.Request, res: express.Respons | |||
95 | return { name, host, uri: u } | 95 | return { name, host, uri: u } |
96 | }) | 96 | }) |
97 | 97 | ||
98 | const results = await ActorFollowModel.listSubscribedIn(user.Account.Actor.id, handles) | 98 | const results = await ActorFollowModel.listSubscriptionsOf(user.Account.Actor.id, handles) |
99 | 99 | ||
100 | const existObject: { [id: string ]: boolean } = {} | 100 | const existObject: { [id: string ]: boolean } = {} |
101 | for (const handle of handles) { | 101 | for (const handle of handles) { |
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index b79dc5933..f370c7004 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { pickCommonVideoQuery } from '@server/helpers/query' | 2 | import { pickCommonVideoQuery } from '@server/helpers/query' |
3 | import { Hooks } from '@server/lib/plugins/hooks' | 3 | import { Hooks } from '@server/lib/plugins/hooks' |
4 | import { ActorFollowModel } from '@server/models/actor/actor-follow' | ||
4 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
5 | import { MChannelBannerAccountDefault } from '@server/types/models' | 6 | import { MChannelBannerAccountDefault } from '@server/types/models' |
6 | import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared' | 7 | import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared' |
@@ -33,7 +34,13 @@ import { | |||
33 | videoChannelsUpdateValidator, | 34 | videoChannelsUpdateValidator, |
34 | videoPlaylistsSortValidator | 35 | videoPlaylistsSortValidator |
35 | } from '../../middlewares' | 36 | } from '../../middlewares' |
36 | import { videoChannelsListValidator, videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators' | 37 | import { |
38 | ensureAuthUserOwnsChannelValidator, | ||
39 | videoChannelsFollowersSortValidator, | ||
40 | videoChannelsListValidator, | ||
41 | videoChannelsNameWithHostValidator, | ||
42 | videosSortValidator | ||
43 | } from '../../middlewares/validators' | ||
37 | import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image' | 44 | import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image' |
38 | import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' | 45 | import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' |
39 | import { AccountModel } from '../../models/account/account' | 46 | import { AccountModel } from '../../models/account/account' |
@@ -65,8 +72,8 @@ videoChannelRouter.post('/', | |||
65 | videoChannelRouter.post('/:nameWithHost/avatar/pick', | 72 | videoChannelRouter.post('/:nameWithHost/avatar/pick', |
66 | authenticate, | 73 | authenticate, |
67 | reqAvatarFile, | 74 | reqAvatarFile, |
68 | // Check the rights | 75 | asyncMiddleware(videoChannelsNameWithHostValidator), |
69 | asyncMiddleware(videoChannelsUpdateValidator), | 76 | ensureAuthUserOwnsChannelValidator, |
70 | updateAvatarValidator, | 77 | updateAvatarValidator, |
71 | asyncMiddleware(updateVideoChannelAvatar) | 78 | asyncMiddleware(updateVideoChannelAvatar) |
72 | ) | 79 | ) |
@@ -74,29 +81,31 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick', | |||
74 | videoChannelRouter.post('/:nameWithHost/banner/pick', | 81 | videoChannelRouter.post('/:nameWithHost/banner/pick', |
75 | authenticate, | 82 | authenticate, |
76 | reqBannerFile, | 83 | reqBannerFile, |
77 | // Check the rights | 84 | asyncMiddleware(videoChannelsNameWithHostValidator), |
78 | asyncMiddleware(videoChannelsUpdateValidator), | 85 | ensureAuthUserOwnsChannelValidator, |
79 | updateBannerValidator, | 86 | updateBannerValidator, |
80 | asyncMiddleware(updateVideoChannelBanner) | 87 | asyncMiddleware(updateVideoChannelBanner) |
81 | ) | 88 | ) |
82 | 89 | ||
83 | videoChannelRouter.delete('/:nameWithHost/avatar', | 90 | videoChannelRouter.delete('/:nameWithHost/avatar', |
84 | authenticate, | 91 | authenticate, |
85 | // Check the rights | 92 | asyncMiddleware(videoChannelsNameWithHostValidator), |
86 | asyncMiddleware(videoChannelsUpdateValidator), | 93 | ensureAuthUserOwnsChannelValidator, |
87 | asyncMiddleware(deleteVideoChannelAvatar) | 94 | asyncMiddleware(deleteVideoChannelAvatar) |
88 | ) | 95 | ) |
89 | 96 | ||
90 | videoChannelRouter.delete('/:nameWithHost/banner', | 97 | videoChannelRouter.delete('/:nameWithHost/banner', |
91 | authenticate, | 98 | authenticate, |
92 | // Check the rights | 99 | asyncMiddleware(videoChannelsNameWithHostValidator), |
93 | asyncMiddleware(videoChannelsUpdateValidator), | 100 | ensureAuthUserOwnsChannelValidator, |
94 | asyncMiddleware(deleteVideoChannelBanner) | 101 | asyncMiddleware(deleteVideoChannelBanner) |
95 | ) | 102 | ) |
96 | 103 | ||
97 | videoChannelRouter.put('/:nameWithHost', | 104 | videoChannelRouter.put('/:nameWithHost', |
98 | authenticate, | 105 | authenticate, |
99 | asyncMiddleware(videoChannelsUpdateValidator), | 106 | asyncMiddleware(videoChannelsNameWithHostValidator), |
107 | ensureAuthUserOwnsChannelValidator, | ||
108 | videoChannelsUpdateValidator, | ||
100 | asyncRetryTransactionMiddleware(updateVideoChannel) | 109 | asyncRetryTransactionMiddleware(updateVideoChannel) |
101 | ) | 110 | ) |
102 | 111 | ||
@@ -132,6 +141,17 @@ videoChannelRouter.get('/:nameWithHost/videos', | |||
132 | asyncMiddleware(listVideoChannelVideos) | 141 | asyncMiddleware(listVideoChannelVideos) |
133 | ) | 142 | ) |
134 | 143 | ||
144 | videoChannelRouter.get('/:nameWithHost/followers', | ||
145 | authenticate, | ||
146 | asyncMiddleware(videoChannelsNameWithHostValidator), | ||
147 | ensureAuthUserOwnsChannelValidator, | ||
148 | paginationValidator, | ||
149 | videoChannelsFollowersSortValidator, | ||
150 | setDefaultSort, | ||
151 | setDefaultPagination, | ||
152 | asyncMiddleware(listVideoChannelFollowers) | ||
153 | ) | ||
154 | |||
135 | // --------------------------------------------------------------------------- | 155 | // --------------------------------------------------------------------------- |
136 | 156 | ||
137 | export { | 157 | export { |
@@ -332,3 +352,18 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon | |||
332 | 352 | ||
333 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 353 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
334 | } | 354 | } |
355 | |||
356 | async function listVideoChannelFollowers (req: express.Request, res: express.Response) { | ||
357 | const channel = res.locals.videoChannel | ||
358 | |||
359 | const resultList = await ActorFollowModel.listFollowersForApi({ | ||
360 | actorIds: [ channel.actorId ], | ||
361 | start: req.query.start, | ||
362 | count: req.query.count, | ||
363 | sort: req.query.sort, | ||
364 | search: req.query.search, | ||
365 | state: 'accepted', | ||
366 | }) | ||
367 | |||
368 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
369 | } | ||
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 029984559..dcbad9264 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -69,8 +69,11 @@ const SORTABLE_COLUMNS = { | |||
69 | 69 | ||
70 | VIDEO_RATES: [ 'createdAt' ], | 70 | VIDEO_RATES: [ 'createdAt' ], |
71 | BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ], | 71 | BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ], |
72 | |||
72 | INSTANCE_FOLLOWERS: [ 'createdAt', 'state', 'score' ], | 73 | INSTANCE_FOLLOWERS: [ 'createdAt', 'state', 'score' ], |
73 | INSTANCE_FOLLOWING: [ 'createdAt', 'redundancyAllowed', 'state' ], | 74 | INSTANCE_FOLLOWING: [ 'createdAt', 'redundancyAllowed', 'state' ], |
75 | ACCOUNT_FOLLOWERS: [ 'createdAt' ], | ||
76 | CHANNEL_FOLLOWERS: [ 'createdAt' ], | ||
74 | 77 | ||
75 | VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot', 'best' ], | 78 | VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot', 'best' ], |
76 | 79 | ||
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index ce8df8fee..3ba668460 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -53,6 +53,9 @@ const pluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.PLUGINS) | |||
53 | const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS) | 53 | const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS) |
54 | const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES) | 54 | const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES) |
55 | 55 | ||
56 | const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS) | ||
57 | const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS) | ||
58 | |||
56 | // --------------------------------------------------------------------------- | 59 | // --------------------------------------------------------------------------- |
57 | 60 | ||
58 | export { | 61 | export { |
@@ -79,5 +82,7 @@ export { | |||
79 | videoPlaylistsSortValidator, | 82 | videoPlaylistsSortValidator, |
80 | videoRedundanciesSortValidator, | 83 | videoRedundanciesSortValidator, |
81 | videoPlaylistsSearchSortValidator, | 84 | videoPlaylistsSearchSortValidator, |
85 | accountsFollowersSortValidator, | ||
86 | videoChannelsFollowersSortValidator, | ||
82 | pluginsSortValidator | 87 | pluginsSortValidator |
83 | } | 88 | } |
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index c06b85862..c6eeeaf18 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -3,9 +3,7 @@ import { body, param, query } from 'express-validator' | |||
3 | import { omit } from 'lodash' | 3 | import { omit } from 'lodash' |
4 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { MUserDefault } from '@server/types/models' | 5 | import { MUserDefault } from '@server/types/models' |
6 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | 6 | import { HttpStatusCode, UserRegister, UserRole } from '@shared/models' |
7 | import { UserRole } from '../../../shared/models/users' | ||
8 | import { UserRegister } from '../../../shared/models/users/user-register.model' | ||
9 | import { toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' | 7 | import { toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' |
10 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' | 8 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' |
11 | import { | 9 | import { |
@@ -462,7 +460,22 @@ const ensureAuthUserOwnsAccountValidator = [ | |||
462 | if (res.locals.account.id !== user.Account.id) { | 460 | if (res.locals.account.id !== user.Account.id) { |
463 | return res.fail({ | 461 | return res.fail({ |
464 | status: HttpStatusCode.FORBIDDEN_403, | 462 | status: HttpStatusCode.FORBIDDEN_403, |
465 | message: 'Only owner can access ratings list.' | 463 | message: 'Only owner of this account can access this ressource.' |
464 | }) | ||
465 | } | ||
466 | |||
467 | return next() | ||
468 | } | ||
469 | ] | ||
470 | |||
471 | const ensureAuthUserOwnsChannelValidator = [ | ||
472 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
473 | const user = res.locals.oauth.token.User | ||
474 | |||
475 | if (res.locals.videoChannel.Account.userId !== user.id) { | ||
476 | return res.fail({ | ||
477 | status: HttpStatusCode.FORBIDDEN_403, | ||
478 | message: 'Only owner of this video channel can access this ressource' | ||
466 | }) | 479 | }) |
467 | } | 480 | } |
468 | 481 | ||
@@ -506,6 +519,7 @@ export { | |||
506 | usersVerifyEmailValidator, | 519 | usersVerifyEmailValidator, |
507 | userAutocompleteValidator, | 520 | userAutocompleteValidator, |
508 | ensureAuthUserOwnsAccountValidator, | 521 | ensureAuthUserOwnsAccountValidator, |
522 | ensureAuthUserOwnsChannelValidator, | ||
509 | ensureCanManageUser | 523 | ensureCanManageUser |
510 | } | 524 | } |
511 | 525 | ||
diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts index fc717abf6..ec107fa51 100644 --- a/server/middlewares/validators/videos/video-channels.ts +++ b/server/middlewares/validators/videos/video-channels.ts | |||
@@ -65,22 +65,6 @@ const videoChannelsUpdateValidator = [ | |||
65 | logger.debug('Checking videoChannelsUpdate parameters', { parameters: req.body }) | 65 | logger.debug('Checking videoChannelsUpdate parameters', { parameters: req.body }) |
66 | 66 | ||
67 | if (areValidationErrors(req, res)) return | 67 | if (areValidationErrors(req, res)) return |
68 | if (!await doesVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return | ||
69 | |||
70 | // We need to make additional checks | ||
71 | if (res.locals.videoChannel.Actor.isOwned() === false) { | ||
72 | return res.fail({ | ||
73 | status: HttpStatusCode.FORBIDDEN_403, | ||
74 | message: 'Cannot update video channel of another server' | ||
75 | }) | ||
76 | } | ||
77 | |||
78 | if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) { | ||
79 | return res.fail({ | ||
80 | status: HttpStatusCode.FORBIDDEN_403, | ||
81 | message: 'Cannot update video channel of another user' | ||
82 | }) | ||
83 | } | ||
84 | 68 | ||
85 | return next() | 69 | return next() |
86 | } | 70 | } |
diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts index fc1cc7499..d6a2387a5 100644 --- a/server/models/actor/actor-follow.ts +++ b/server/models/actor/actor-follow.ts | |||
@@ -143,7 +143,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
143 | * @deprecated Use `findOrCreateCustom` instead | 143 | * @deprecated Use `findOrCreateCustom` instead |
144 | */ | 144 | */ |
145 | static findOrCreate (): any { | 145 | static findOrCreate (): any { |
146 | throw new Error('Should not be called') | 146 | throw new Error('Must not be called') |
147 | } | 147 | } |
148 | 148 | ||
149 | // findOrCreate has issues with actor follow hooks | 149 | // findOrCreate has issues with actor follow hooks |
@@ -288,7 +288,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
288 | return ActorFollowModel.findOne(query) | 288 | return ActorFollowModel.findOne(query) |
289 | } | 289 | } |
290 | 290 | ||
291 | static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> { | 291 | static listSubscriptionsOf (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> { |
292 | const whereTab = targets | 292 | const whereTab = targets |
293 | .map(t => { | 293 | .map(t => { |
294 | if (t.host) { | 294 | if (t.host) { |
@@ -348,7 +348,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
348 | return ActorFollowModel.findAll(query) | 348 | return ActorFollowModel.findAll(query) |
349 | } | 349 | } |
350 | 350 | ||
351 | static listFollowingForApi (options: { | 351 | static listInstanceFollowingForApi (options: { |
352 | id: number | 352 | id: number |
353 | start: number | 353 | start: number |
354 | count: number | 354 | count: number |
@@ -415,7 +415,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
415 | } | 415 | } |
416 | 416 | ||
417 | static listFollowersForApi (options: { | 417 | static listFollowersForApi (options: { |
418 | actorId: number | 418 | actorIds: number[] |
419 | start: number | 419 | start: number |
420 | count: number | 420 | count: number |
421 | sort: string | 421 | sort: string |
@@ -423,7 +423,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
423 | actorType?: ActivityPubActorType | 423 | actorType?: ActivityPubActorType |
424 | search?: string | 424 | search?: string |
425 | }) { | 425 | }) { |
426 | const { actorId, start, count, sort, search, state, actorType } = options | 426 | const { actorIds, start, count, sort, search, state, actorType } = options |
427 | 427 | ||
428 | const followWhere = state ? { state } : {} | 428 | const followWhere = state ? { state } : {} |
429 | const followerWhere: WhereOptions = {} | 429 | const followerWhere: WhereOptions = {} |
@@ -452,20 +452,16 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
452 | model: ActorModel, | 452 | model: ActorModel, |
453 | required: true, | 453 | required: true, |
454 | as: 'ActorFollower', | 454 | as: 'ActorFollower', |
455 | where: followerWhere, | 455 | where: followerWhere |
456 | include: [ | ||
457 | { | ||
458 | model: ServerModel, | ||
459 | required: true | ||
460 | } | ||
461 | ] | ||
462 | }, | 456 | }, |
463 | { | 457 | { |
464 | model: ActorModel, | 458 | model: ActorModel, |
465 | as: 'ActorFollowing', | 459 | as: 'ActorFollowing', |
466 | required: true, | 460 | required: true, |
467 | where: { | 461 | where: { |
468 | id: actorId | 462 | id: { |
463 | [Op.in]: actorIds | ||
464 | } | ||
469 | } | 465 | } |
470 | } | 466 | } |
471 | ] | 467 | ] |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 8ccb818b3..a151ad61c 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -26,7 +26,7 @@ import { | |||
26 | isVideoChannelDisplayNameValid, | 26 | isVideoChannelDisplayNameValid, |
27 | isVideoChannelSupportValid | 27 | isVideoChannelSupportValid |
28 | } from '../../helpers/custom-validators/video-channels' | 28 | } from '../../helpers/custom-validators/video-channels' |
29 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' | 29 | import { CONSTRAINTS_FIELDS, VIDEO_CHANNELS, WEBSERVER } from '../../initializers/constants' |
30 | import { sendDeleteActor } from '../../lib/activitypub/send' | 30 | import { sendDeleteActor } from '../../lib/activitypub/send' |
31 | import { | 31 | import { |
32 | MChannelActor, | 32 | MChannelActor, |
@@ -527,7 +527,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
527 | }) | 527 | }) |
528 | } | 528 | } |
529 | 529 | ||
530 | static listByAccount (options: { | 530 | static listByAccountForAPI (options: { |
531 | accountId: number | 531 | accountId: number |
532 | start: number | 532 | start: number |
533 | count: number | 533 | count: number |
@@ -582,6 +582,26 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
582 | }) | 582 | }) |
583 | } | 583 | } |
584 | 584 | ||
585 | |||
586 | static listAllByAccount (accountId: number) { | ||
587 | const query = { | ||
588 | limit: VIDEO_CHANNELS.MAX_PER_USER, | ||
589 | include: [ | ||
590 | { | ||
591 | attributes: [], | ||
592 | model: AccountModel, | ||
593 | where: { | ||
594 | id: accountId | ||
595 | }, | ||
596 | required: true | ||
597 | } | ||
598 | ] | ||
599 | } | ||
600 | |||
601 | return VideoChannelModel.findAll(query) | ||
602 | } | ||
603 | |||
604 | |||
585 | static loadAndPopulateAccount (id: number, transaction?: Transaction): Promise<MChannelBannerAccountDefault> { | 605 | static loadAndPopulateAccount (id: number, transaction?: Transaction): Promise<MChannelBannerAccountDefault> { |
586 | return VideoChannelModel.unscoped() | 606 | return VideoChannelModel.unscoped() |
587 | .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ]) | 607 | .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ]) |
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index 58b360f92..517e2f423 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts | |||
@@ -840,6 +840,34 @@ describe('Test users API validators', function () { | |||
840 | }) | 840 | }) |
841 | }) | 841 | }) |
842 | 842 | ||
843 | describe('When getting my global followers', function () { | ||
844 | const path = '/api/v1/accounts/user1/followers' | ||
845 | |||
846 | it('Should fail with a bad start pagination', async function () { | ||
847 | await checkBadStartPagination(server.url, path, userToken) | ||
848 | }) | ||
849 | |||
850 | it('Should fail with a bad count pagination', async function () { | ||
851 | await checkBadCountPagination(server.url, path, userToken) | ||
852 | }) | ||
853 | |||
854 | it('Should fail with an incorrect sort', async function () { | ||
855 | await checkBadSortPagination(server.url, path, userToken) | ||
856 | }) | ||
857 | |||
858 | it('Should fail with a unauthenticated user', async function () { | ||
859 | await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
860 | }) | ||
861 | |||
862 | it('Should fail with a another user', async function () { | ||
863 | await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
864 | }) | ||
865 | |||
866 | it('Should succeed with the correct params', async function () { | ||
867 | await makeGetRequest({ url: server.url, path, token: userToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
868 | }) | ||
869 | }) | ||
870 | |||
843 | describe('When blocking/unblocking/removing user', function () { | 871 | describe('When blocking/unblocking/removing user', function () { |
844 | 872 | ||
845 | it('Should fail with an incorrect id', async function () { | 873 | it('Should fail with an incorrect id', async function () { |
diff --git a/server/tests/api/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts index 2e63916d4..e86c315fa 100644 --- a/server/tests/api/check-params/video-channels.ts +++ b/server/tests/api/check-params/video-channels.ts | |||
@@ -321,6 +321,34 @@ describe('Test video channels API validator', function () { | |||
321 | }) | 321 | }) |
322 | }) | 322 | }) |
323 | 323 | ||
324 | describe('When getting channel followers', function () { | ||
325 | const path = '/api/v1/video-channels/super_channel/followers' | ||
326 | |||
327 | it('Should fail with a bad start pagination', async function () { | ||
328 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
329 | }) | ||
330 | |||
331 | it('Should fail with a bad count pagination', async function () { | ||
332 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
333 | }) | ||
334 | |||
335 | it('Should fail with an incorrect sort', async function () { | ||
336 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
337 | }) | ||
338 | |||
339 | it('Should fail with a unauthenticated user', async function () { | ||
340 | await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
341 | }) | ||
342 | |||
343 | it('Should fail with a another user', async function () { | ||
344 | await makeGetRequest({ url: server.url, path, token: accessTokenUser, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
345 | }) | ||
346 | |||
347 | it('Should succeed with the correct params', async function () { | ||
348 | await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
349 | }) | ||
350 | }) | ||
351 | |||
324 | describe('When deleting a video channel', function () { | 352 | describe('When deleting a video channel', function () { |
325 | it('Should fail with a non authenticated user', async function () { | 353 | it('Should fail with a non authenticated user', async function () { |
326 | await command.delete({ token: 'coucou', channelName: 'super_channel', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | 354 | await command.delete({ token: 'coucou', channelName: 'super_channel', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) |
diff --git a/server/tests/api/users/user-subscriptions.ts b/server/tests/api/users/user-subscriptions.ts index 441f70d07..b49367be6 100644 --- a/server/tests/api/users/user-subscriptions.ts +++ b/server/tests/api/users/user-subscriptions.ts | |||
@@ -368,6 +368,162 @@ describe('Test users subscriptions', function () { | |||
368 | } | 368 | } |
369 | }) | 369 | }) |
370 | 370 | ||
371 | it('Should follow user channels of server 3 by root of server 3', async function () { | ||
372 | this.timeout(60000) | ||
373 | |||
374 | await servers[2].channels.create({ token: users[2].accessToken, attributes: { name: 'user3_channel2' } }) | ||
375 | |||
376 | await servers[2].subscriptions.add({ token: servers[2].accessToken, targetUri: 'user3_channel@localhost:' + servers[2].port }) | ||
377 | await servers[2].subscriptions.add({ token: servers[2].accessToken, targetUri: 'user3_channel2@localhost:' + servers[2].port }) | ||
378 | |||
379 | await waitJobs(servers) | ||
380 | }) | ||
381 | |||
382 | it('Should list user 3 followers', async function () { | ||
383 | { | ||
384 | const { total, data } = await servers[2].accounts.listFollowers({ | ||
385 | token: users[2].accessToken, | ||
386 | accountName: 'user3', | ||
387 | start: 0, | ||
388 | count: 5, | ||
389 | sort: 'createdAt' | ||
390 | }) | ||
391 | |||
392 | expect(total).to.equal(3) | ||
393 | expect(data[0].following.host).to.equal(servers[2].host) | ||
394 | expect(data[0].following.name).to.equal('user3_channel') | ||
395 | expect(data[0].follower.host).to.equal(servers[0].host) | ||
396 | expect(data[0].follower.name).to.equal('user1') | ||
397 | |||
398 | expect(data[1].following.host).to.equal(servers[2].host) | ||
399 | expect(data[1].following.name).to.equal('user3_channel') | ||
400 | expect(data[1].follower.host).to.equal(servers[2].host) | ||
401 | expect(data[1].follower.name).to.equal('root') | ||
402 | |||
403 | expect(data[2].following.host).to.equal(servers[2].host) | ||
404 | expect(data[2].following.name).to.equal('user3_channel2') | ||
405 | expect(data[2].follower.host).to.equal(servers[2].host) | ||
406 | expect(data[2].follower.name).to.equal('root') | ||
407 | } | ||
408 | |||
409 | { | ||
410 | const { total, data } = await servers[2].accounts.listFollowers({ | ||
411 | token: users[2].accessToken, | ||
412 | accountName: 'user3', | ||
413 | start: 0, | ||
414 | count: 1, | ||
415 | sort: '-createdAt' | ||
416 | }) | ||
417 | |||
418 | expect(total).to.equal(3) | ||
419 | expect(data[0].following.host).to.equal(servers[2].host) | ||
420 | expect(data[0].following.name).to.equal('user3_channel2') | ||
421 | expect(data[0].follower.host).to.equal(servers[2].host) | ||
422 | expect(data[0].follower.name).to.equal('root') | ||
423 | } | ||
424 | |||
425 | { | ||
426 | const { total, data } = await servers[2].accounts.listFollowers({ | ||
427 | token: users[2].accessToken, | ||
428 | accountName: 'user3', | ||
429 | start: 1, | ||
430 | count: 1, | ||
431 | sort: '-createdAt' | ||
432 | }) | ||
433 | |||
434 | expect(total).to.equal(3) | ||
435 | expect(data[0].following.host).to.equal(servers[2].host) | ||
436 | expect(data[0].following.name).to.equal('user3_channel') | ||
437 | expect(data[0].follower.host).to.equal(servers[2].host) | ||
438 | expect(data[0].follower.name).to.equal('root') | ||
439 | } | ||
440 | |||
441 | { | ||
442 | const { total, data } = await servers[2].accounts.listFollowers({ | ||
443 | token: users[2].accessToken, | ||
444 | accountName: 'user3', | ||
445 | search: 'user1', | ||
446 | sort: '-createdAt' | ||
447 | }) | ||
448 | |||
449 | expect(total).to.equal(1) | ||
450 | expect(data[0].following.host).to.equal(servers[2].host) | ||
451 | expect(data[0].following.name).to.equal('user3_channel') | ||
452 | expect(data[0].follower.host).to.equal(servers[0].host) | ||
453 | expect(data[0].follower.name).to.equal('user1') | ||
454 | } | ||
455 | }) | ||
456 | |||
457 | it('Should list user3_channel followers', async function () { | ||
458 | { | ||
459 | const { total, data } = await servers[2].channels.listFollowers({ | ||
460 | token: users[2].accessToken, | ||
461 | channelName: 'user3_channel', | ||
462 | start: 0, | ||
463 | count: 5, | ||
464 | sort: 'createdAt' | ||
465 | }) | ||
466 | |||
467 | expect(total).to.equal(2) | ||
468 | expect(data[0].following.host).to.equal(servers[2].host) | ||
469 | expect(data[0].following.name).to.equal('user3_channel') | ||
470 | expect(data[0].follower.host).to.equal(servers[0].host) | ||
471 | expect(data[0].follower.name).to.equal('user1') | ||
472 | |||
473 | expect(data[1].following.host).to.equal(servers[2].host) | ||
474 | expect(data[1].following.name).to.equal('user3_channel') | ||
475 | expect(data[1].follower.host).to.equal(servers[2].host) | ||
476 | expect(data[1].follower.name).to.equal('root') | ||
477 | } | ||
478 | |||
479 | { | ||
480 | const { total, data } = await servers[2].channels.listFollowers({ | ||
481 | token: users[2].accessToken, | ||
482 | channelName: 'user3_channel', | ||
483 | start: 0, | ||
484 | count: 1, | ||
485 | sort: '-createdAt' | ||
486 | }) | ||
487 | |||
488 | expect(total).to.equal(2) | ||
489 | expect(data[0].following.host).to.equal(servers[2].host) | ||
490 | expect(data[0].following.name).to.equal('user3_channel') | ||
491 | expect(data[0].follower.host).to.equal(servers[2].host) | ||
492 | expect(data[0].follower.name).to.equal('root') | ||
493 | } | ||
494 | |||
495 | { | ||
496 | const { total, data } = await servers[2].channels.listFollowers({ | ||
497 | token: users[2].accessToken, | ||
498 | channelName: 'user3_channel', | ||
499 | start: 1, | ||
500 | count: 1, | ||
501 | sort: '-createdAt' | ||
502 | }) | ||
503 | |||
504 | expect(total).to.equal(2) | ||
505 | expect(data[0].following.host).to.equal(servers[2].host) | ||
506 | expect(data[0].following.name).to.equal('user3_channel') | ||
507 | expect(data[0].follower.host).to.equal(servers[0].host) | ||
508 | expect(data[0].follower.name).to.equal('root') | ||
509 | } | ||
510 | |||
511 | { | ||
512 | const { total, data } = await servers[2].channels.listFollowers({ | ||
513 | token: users[2].accessToken, | ||
514 | channelName: 'user3_channel', | ||
515 | search: 'user1', | ||
516 | sort: '-createdAt' | ||
517 | }) | ||
518 | |||
519 | expect(total).to.equal(1) | ||
520 | expect(data[0].following.host).to.equal(servers[2].host) | ||
521 | expect(data[0].following.name).to.equal('user3_channel') | ||
522 | expect(data[0].follower.host).to.equal(servers[0].host) | ||
523 | expect(data[0].follower.name).to.equal('user1') | ||
524 | } | ||
525 | }) | ||
526 | |||
371 | after(async function () { | 527 | after(async function () { |
372 | await cleanupTests(servers) | 528 | await cleanupTests(servers) |
373 | }) | 529 | }) |
diff --git a/shared/extra-utils/users/accounts-command.ts b/shared/extra-utils/users/accounts-command.ts index 2f586104e..98d9d5927 100644 --- a/shared/extra-utils/users/accounts-command.ts +++ b/shared/extra-utils/users/accounts-command.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { HttpStatusCode, ResultList } from '@shared/models' | 1 | import { HttpStatusCode, ResultList } from '@shared/models' |
2 | import { Account } from '../../models/actors' | 2 | import { Account, ActorFollow } from '../../models/actors' |
3 | import { AccountVideoRate, VideoRateType } from '../../models/videos' | 3 | import { AccountVideoRate, VideoRateType } from '../../models/videos' |
4 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | 4 | import { AbstractCommand, OverrideCommandOptions } from '../shared' |
5 | 5 | ||
@@ -53,4 +53,26 @@ export class AccountsCommand extends AbstractCommand { | |||
53 | defaultExpectedStatus: HttpStatusCode.OK_200 | 53 | defaultExpectedStatus: HttpStatusCode.OK_200 |
54 | }) | 54 | }) |
55 | } | 55 | } |
56 | |||
57 | listFollowers (options: OverrideCommandOptions & { | ||
58 | accountName: string | ||
59 | start?: number | ||
60 | count?: number | ||
61 | sort?: string | ||
62 | search?: string | ||
63 | }) { | ||
64 | const { accountName, start, count, sort, search } = options | ||
65 | const path = '/api/v1/accounts/' + accountName + '/followers' | ||
66 | |||
67 | const query = { start, count, sort, search } | ||
68 | |||
69 | return this.getRequestBody<ResultList<ActorFollow>>({ | ||
70 | ...options, | ||
71 | |||
72 | path, | ||
73 | query, | ||
74 | implicitToken: true, | ||
75 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
76 | }) | ||
77 | } | ||
56 | } | 78 | } |
diff --git a/shared/extra-utils/videos/channels-command.ts b/shared/extra-utils/videos/channels-command.ts index 255e1d62d..e406e570b 100644 --- a/shared/extra-utils/videos/channels-command.ts +++ b/shared/extra-utils/videos/channels-command.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { pick } from '@shared/core-utils' | 1 | import { pick } from '@shared/core-utils' |
2 | import { HttpStatusCode, ResultList, VideoChannel, VideoChannelCreateResult } from '@shared/models' | 2 | import { ActorFollow, HttpStatusCode, ResultList, VideoChannel, VideoChannelCreateResult } from '@shared/models' |
3 | import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model' | 3 | import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model' |
4 | import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model' | 4 | import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model' |
5 | import { unwrapBody } from '../requests' | 5 | import { unwrapBody } from '../requests' |
@@ -47,7 +47,7 @@ export class ChannelsCommand extends AbstractCommand { | |||
47 | } | 47 | } |
48 | 48 | ||
49 | async create (options: OverrideCommandOptions & { | 49 | async create (options: OverrideCommandOptions & { |
50 | attributes: VideoChannelCreate | 50 | attributes: Partial<VideoChannelCreate> |
51 | }) { | 51 | }) { |
52 | const path = '/api/v1/video-channels/' | 52 | const path = '/api/v1/video-channels/' |
53 | 53 | ||
@@ -153,4 +153,26 @@ export class ChannelsCommand extends AbstractCommand { | |||
153 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | 153 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 |
154 | }) | 154 | }) |
155 | } | 155 | } |
156 | |||
157 | listFollowers (options: OverrideCommandOptions & { | ||
158 | channelName: string | ||
159 | start?: number | ||
160 | count?: number | ||
161 | sort?: string | ||
162 | search?: string | ||
163 | }) { | ||
164 | const { channelName, start, count, sort, search } = options | ||
165 | const path = '/api/v1/video-channels/' + channelName + '/followers' | ||
166 | |||
167 | const query = { start, count, sort, search } | ||
168 | |||
169 | return this.getRequestBody<ResultList<ActorFollow>>({ | ||
170 | ...options, | ||
171 | |||
172 | path, | ||
173 | query, | ||
174 | implicitToken: true, | ||
175 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
176 | }) | ||
177 | } | ||
156 | } | 178 | } |