aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.ts4
-rw-r--r--client/src/app/+accounts/accounts.component.scss2
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html2
-rw-r--r--client/src/app/+admin/plugins/shared/plugin-api.service.ts4
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.component.html7
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss4
-rw-r--r--client/src/app/+my-library/my-follows/my-followers.component.html31
-rw-r--r--client/src/app/+my-library/my-follows/my-followers.component.scss26
-rw-r--r--client/src/app/+my-library/my-follows/my-followers.component.ts76
-rw-r--r--client/src/app/+my-library/my-follows/my-subscriptions.component.html (renamed from client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html)14
-rw-r--r--client/src/app/+my-library/my-follows/my-subscriptions.component.scss16
-rw-r--r--client/src/app/+my-library/my-follows/my-subscriptions.component.ts (renamed from client/src/app/+my-library/my-subscriptions/my-subscriptions.component.ts)0
-rw-r--r--client/src/app/+my-library/my-library-routing.module.ts12
-rw-r--r--client/src/app/+my-library/my-library.component.ts15
-rw-r--r--client/src/app/+my-library/my-library.module.ts6
-rw-r--r--client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss84
-rw-r--r--client/src/app/+video-channels/video-channels.component.scss2
-rw-r--r--client/src/app/core/rest/rest.service.ts16
-rw-r--r--client/src/app/shared/shared-custom-markup/custom-markup-help.component.html2
-rw-r--r--client/src/app/shared/shared-forms/advanced-input-filter.component.ts2
-rw-r--r--client/src/app/shared/shared-main/users/user-history.service.ts2
-rw-r--r--client/src/app/shared/shared-main/users/user-notification.service.ts2
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.html2
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.service.ts2
-rw-r--r--client/src/app/shared/shared-main/video/video.service.ts4
-rw-r--r--client/src/app/shared/shared-search/search.service.ts6
-rw-r--r--client/src/app/shared/shared-user-subscription/user-subscription.service.ts41
-rw-r--r--client/src/app/shared/shared-video-comment/video-comment.service.ts2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist.service.ts6
29 files changed, 264 insertions, 128 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
10input[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 @@
1import { Subject } from 'rxjs'
2import { Component, OnInit } from '@angular/core'
3import { ActivatedRoute } from '@angular/router'
4import { AuthService, ComponentPagination, Notifier } from '@app/core'
5import { UserSubscriptionService } from '@app/shared/shared-user-subscription'
6import { ActorFollow } from '@shared/models'
7
8@Component({
9 templateUrl: './my-followers.component.html',
10 styleUrls: [ './my-followers.component.scss' ]
11})
12export 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
10input[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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { LoginGuard } from '../core' 3import { LoginGuard } from '../core'
4import { MyFollowersComponent } from './my-follows/my-followers.component'
5import { MySubscriptionsComponent } from './my-follows/my-subscriptions.component'
4import { MyHistoryComponent } from './my-history/my-history.component' 6import { MyHistoryComponent } from './my-history/my-history.component'
5import { MyLibraryComponent } from './my-library.component' 7import { MyLibraryComponent } from './my-library.component'
6import { MyOwnershipComponent } from './my-ownership/my-ownership.component' 8import { MyOwnershipComponent } from './my-ownership/my-ownership.component'
7import { MySubscriptionsComponent } from './my-subscriptions/my-subscriptions.component'
8import { MyVideoImportsComponent } from './my-video-imports/my-video-imports.component' 9import { MyVideoImportsComponent } from './my-video-imports/my-video-imports.component'
9import { MyVideoPlaylistCreateComponent } from './my-video-playlists/my-video-playlist-create.component' 10import { MyVideoPlaylistCreateComponent } from './my-video-playlists/my-video-playlist-create.component'
10import { MyVideoPlaylistElementsComponent } from './my-video-playlists/my-video-playlist-elements.component' 11import { 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
13import { SharedVideoLiveModule } from '@app/shared/shared-video-live' 13import { SharedVideoLiveModule } from '@app/shared/shared-video-live'
14import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' 14import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
15import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist/shared-video-playlist.module' 15import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist/shared-video-playlist.module'
16import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
17import { MySubscriptionsComponent } from './my-follows/my-subscriptions.component'
16import { MyHistoryComponent } from './my-history/my-history.component' 18import { MyHistoryComponent } from './my-history/my-history.component'
17import { MyLibraryRoutingModule } from './my-library-routing.module' 19import { MyLibraryRoutingModule } from './my-library-routing.module'
18import { MyLibraryComponent } from './my-library.component' 20import { MyLibraryComponent } from './my-library.component'
19import { MyAcceptOwnershipComponent } from './my-ownership/my-accept-ownership/my-accept-ownership.component' 21import { MyAcceptOwnershipComponent } from './my-ownership/my-accept-ownership/my-accept-ownership.component'
20import { MyOwnershipComponent } from './my-ownership/my-ownership.component' 22import { MyOwnershipComponent } from './my-ownership/my-ownership.component'
21import { MySubscriptionsComponent } from './my-subscriptions/my-subscriptions.component'
22import { MyVideoImportsComponent } from './my-video-imports/my-video-imports.component' 23import { MyVideoImportsComponent } from './my-video-imports/my-video-imports.component'
23import { MyVideoPlaylistCreateComponent } from './my-video-playlists/my-video-playlist-create.component' 24import { MyVideoPlaylistCreateComponent } from './my-video-playlists/my-video-playlist-create.component'
24import { MyVideoPlaylistElementsComponent } from './my-video-playlists/my-video-playlist-elements.component' 25import { MyVideoPlaylistElementsComponent } from './my-video-playlists/my-video-playlist-elements.component'
@@ -26,7 +27,7 @@ import { MyVideoPlaylistUpdateComponent } from './my-video-playlists/my-video-pl
26import { MyVideoPlaylistsComponent } from './my-video-playlists/my-video-playlists.component' 27import { MyVideoPlaylistsComponent } from './my-video-playlists/my-video-playlists.component'
27import { VideoChangeOwnershipComponent } from './my-videos/modals/video-change-ownership.component' 28import { VideoChangeOwnershipComponent } from './my-videos/modals/video-change-ownership.component'
28import { MyVideosComponent } from './my-videos/my-videos.component' 29import { MyVideosComponent } from './my-videos/my-videos.component'
29import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module' 30import { 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
4input[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
16type ParseQueryStringFilterResult = { 16type ParseQueryStringFilters <K extends keyof any> = Partial<Record<K, string | number | boolean | (string | number | boolean)[]>>
17 [key: string]: string | number | boolean | (string | number | boolean)[] 17type ParseQueryStringFiltersResult <K extends keyof any> = ParseQueryStringFilters<K> & { search?: string }
18}
19 18
20@Injectable() 19@Injectable()
21export class RestService { 20export 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'
6import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' 6import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core'
7import { buildBulkObservable } from '@app/helpers' 7import { buildBulkObservable } from '@app/helpers'
8import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' 8import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
9import { ResultList, VideoChannel as VideoChannelServer, VideoSortField } from '@shared/models' 9import { ActorFollow, ResultList, VideoChannel as VideoChannelServer, VideoSortField } from '@shared/models'
10import { environment } from '../../../environments/environment' 10import { environment } from '../../../environments/environment'
11 11
12const logger = debug('peertube:subscriptions:UserSubscriptionService') 12const logger = debug('peertube:subscriptions:UserSubscriptionService')
@@ -17,6 +17,8 @@ type SubscriptionExistResultObservable = { [ uri: string ]: Observable<boolean>
17@Injectable() 17@Injectable()
18export class UserSubscriptionService { 18export 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)