Add ability to view my followers
This commit is contained in:
parent
9593a78ae1
commit
4beda9e12a
|
@ -88,7 +88,7 @@ export class AboutFollowsComponent implements OnInit {
|
|||
}
|
||||
|
||||
private loadMoreFollowers (reset = false) {
|
||||
const pagination = this.restService.componentPaginationToRestPagination(this.followersPagination)
|
||||
const pagination = this.restService.componentToRestPagination(this.followersPagination)
|
||||
|
||||
this.followService.getFollowers({ pagination, sort: this.sort, state: 'accepted' })
|
||||
.subscribe({
|
||||
|
@ -106,7 +106,7 @@ export class AboutFollowsComponent implements OnInit {
|
|||
}
|
||||
|
||||
private loadMoreFollowings (reset = false) {
|
||||
const pagination = this.restService.componentPaginationToRestPagination(this.followingsPagination)
|
||||
const pagination = this.restService.componentToRestPagination(this.followingsPagination)
|
||||
|
||||
this.followService.getFollowing({ pagination, sort: this.sort, state: 'accepted' })
|
||||
.subscribe({
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_actor' as *;
|
||||
@use '_account-channel-page' as *;
|
||||
@use '_miniature' as *;
|
||||
|
||||
.root {
|
||||
|
|
|
@ -416,7 +416,7 @@
|
|||
<p i18n>⚠️ This functionality requires a lot of attention and extra moderation.</p>
|
||||
|
||||
<span i18n>
|
||||
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
|
||||
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
|
||||
</span>
|
||||
</ng-container>
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ export class PluginApiService {
|
|||
componentPagination: ComponentPagination,
|
||||
sort: string
|
||||
) {
|
||||
const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
|
||||
const pagination = this.restService.componentToRestPagination(componentPagination)
|
||||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||
|
@ -67,7 +67,7 @@ export class PluginApiService {
|
|||
sort: string,
|
||||
search?: string
|
||||
) {
|
||||
const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
|
||||
const pagination = this.restService.componentToRestPagination(componentPagination)
|
||||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||
|
|
|
@ -27,7 +27,12 @@
|
|||
<div class="video-channel-name">{{ videoChannel.nameWithHost }}</div>
|
||||
</a>
|
||||
|
||||
<div i18n class="video-channel-followers">{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
|
||||
<a
|
||||
i18n class="video-channel-followers"
|
||||
[routerLink]="[ '/my-library', 'followers' ]" [queryParams]="{ search: 'channel:' + videoChannel.name }"
|
||||
>
|
||||
{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}
|
||||
</a>
|
||||
|
||||
<div i18n class="video-channel-videos">{videoChannel.videosCount, plural, =0 {No videos} =1 {1 video} other {{{ videoChannel.videosCount }} videos}}</div>
|
||||
|
||||
|
|
|
@ -54,6 +54,10 @@ my-edit-button {
|
|||
color: $grey-actor-name;
|
||||
}
|
||||
|
||||
.video-channel-followers {
|
||||
color: pvar(--mainForegroundColor);
|
||||
}
|
||||
|
||||
.video-channel-buttons {
|
||||
margin-top: 10px;
|
||||
min-width: 190px;
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<h1>
|
||||
<span>
|
||||
<my-global-icon iconName="follower" aria-hidden="true"></my-global-icon>
|
||||
<ng-container i18n>My followers</ng-container>
|
||||
<span class="badge badge-secondary"> {{ pagination.totalItems }}</span>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<div class="followers-header">
|
||||
<my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
|
||||
</div>
|
||||
|
||||
<div class="no-results" i18n *ngIf="pagination.totalItems === 0">No follower found.</div>
|
||||
|
||||
<div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
|
||||
<div *ngFor="let follow of follows" class="actor">
|
||||
<my-actor-avatar [account]="follow.follower" [href]="follow.follower.url"></my-actor-avatar>
|
||||
|
||||
<div class="actor-info">
|
||||
<a [href]="follow.follower.url" class="actor-names" rel="noopener noreferrer" target="_blank" i18n-title title="Follower page">
|
||||
<div class="actor-display-name">{{ follow.follower.name + '@' + follow.follower.host }}</div>
|
||||
<span class="glyphicon glyphicon-new-window"></span>
|
||||
</a>
|
||||
|
||||
<div class="text-muted">
|
||||
<ng-container *ngIf="isFollowingAccount(follow)" i18n>Is following all your channels</ng-container>
|
||||
<ng-container *ngIf="!isFollowingAccount(follow)" i18n>Is following your channel {{ follow.following.name }}</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,26 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_actor' as *;
|
||||
|
||||
.followers-header {
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
@include peertube-input-text(300px);
|
||||
}
|
||||
|
||||
.actor {
|
||||
@include actor-row($avatar-size: 40px, $min-height: auto, $separator: true);
|
||||
|
||||
.actor-display-name {
|
||||
font-size: 16px;
|
||||
|
||||
+ .glyphicon {
|
||||
@include margin-left(5px);
|
||||
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import { Subject } from 'rxjs'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { AuthService, ComponentPagination, Notifier } from '@app/core'
|
||||
import { UserSubscriptionService } from '@app/shared/shared-user-subscription'
|
||||
import { ActorFollow } from '@shared/models'
|
||||
|
||||
@Component({
|
||||
templateUrl: './my-followers.component.html',
|
||||
styleUrls: [ './my-followers.component.scss' ]
|
||||
})
|
||||
export class MyFollowersComponent implements OnInit {
|
||||
follows: ActorFollow[] = []
|
||||
|
||||
pagination: ComponentPagination = {
|
||||
currentPage: 1,
|
||||
itemsPerPage: 10,
|
||||
totalItems: null
|
||||
}
|
||||
|
||||
onDataSubject = new Subject<any[]>()
|
||||
search: string
|
||||
|
||||
constructor (
|
||||
private route: ActivatedRoute,
|
||||
private auth: AuthService,
|
||||
private userSubscriptionService: UserSubscriptionService,
|
||||
private notifier: Notifier
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
if (this.route.snapshot.queryParams['search']) {
|
||||
this.search = this.route.snapshot.queryParams['search']
|
||||
}
|
||||
}
|
||||
|
||||
onNearOfBottom () {
|
||||
// Last page
|
||||
if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
|
||||
|
||||
this.pagination.currentPage += 1
|
||||
this.loadFollowers()
|
||||
}
|
||||
|
||||
onSearch (search: string) {
|
||||
this.search = search
|
||||
this.loadFollowers(false)
|
||||
}
|
||||
|
||||
isFollowingAccount (follow: ActorFollow) {
|
||||
return follow.following.name === this.getUsername()
|
||||
}
|
||||
|
||||
private loadFollowers (more = true) {
|
||||
this.userSubscriptionService.listFollowers({
|
||||
pagination: this.pagination,
|
||||
nameWithHost: this.getUsername(),
|
||||
search: this.search
|
||||
}).subscribe({
|
||||
next: res => {
|
||||
this.follows = more
|
||||
? this.follows.concat(res.data)
|
||||
: res.data
|
||||
this.pagination.totalItems = res.total
|
||||
|
||||
this.onDataSubject.next(res.data)
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
private getUsername () {
|
||||
return this.auth.getUser().username
|
||||
}
|
||||
}
|
|
@ -12,17 +12,17 @@
|
|||
|
||||
<div class="no-results" i18n *ngIf="pagination.totalItems === 0">You don't have any subscription yet.</div>
|
||||
|
||||
<div class="video-channels" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
|
||||
<div *ngFor="let videoChannel of videoChannels" class="video-channel">
|
||||
<div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
|
||||
<div *ngFor="let videoChannel of videoChannels" class="actor">
|
||||
<my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]"></my-actor-avatar>
|
||||
|
||||
<div class="video-channel-info">
|
||||
<a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page">
|
||||
<div class="video-channel-display-name">{{ videoChannel.displayName }}</div>
|
||||
<div class="video-channel-name">{{ videoChannel.nameWithHost }}</div>
|
||||
<div class="actor-info">
|
||||
<a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="actor-names" i18n-title title="Channel page">
|
||||
<div class="actor-display-name">{{ videoChannel.displayName }}</div>
|
||||
<div class="actor-name">{{ videoChannel.nameWithHost }}</div>
|
||||
</a>
|
||||
|
||||
<div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div>
|
||||
<div i18n class="actor-followers">{{ videoChannel.followersCount }} subscribers</div>
|
||||
|
||||
<a [routerLink]="[ '/a', videoChannel.ownerBy ]" i18n-title title="Owner account page" class="actor-owner">
|
||||
<span i18n>Created by {{ videoChannel.ownerBy }}</span>
|
|
@ -0,0 +1,16 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_actor' as *;
|
||||
|
||||
.video-subscriptions-header {
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
@include peertube-input-text(300px);
|
||||
}
|
||||
|
||||
.actor {
|
||||
@include actor-row;
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { LoginGuard } from '../core'
|
||||
import { MyFollowersComponent } from './my-follows/my-followers.component'
|
||||
import { MySubscriptionsComponent } from './my-follows/my-subscriptions.component'
|
||||
import { MyHistoryComponent } from './my-history/my-history.component'
|
||||
import { MyLibraryComponent } from './my-library.component'
|
||||
import { MyOwnershipComponent } from './my-ownership/my-ownership.component'
|
||||
import { MySubscriptionsComponent } from './my-subscriptions/my-subscriptions.component'
|
||||
import { MyVideoImportsComponent } from './my-video-imports/my-video-imports.component'
|
||||
import { MyVideoPlaylistCreateComponent } from './my-video-playlists/my-video-playlist-create.component'
|
||||
import { MyVideoPlaylistElementsComponent } from './my-video-playlists/my-video-playlist-elements.component'
|
||||
|
@ -99,6 +100,15 @@ const myLibraryRoutes: Routes = [
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'followers',
|
||||
component: MyFollowersComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`My followers`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'ownership',
|
||||
component: MyOwnershipComponent,
|
||||
|
|
|
@ -61,8 +61,19 @@ export class MyLibraryComponent implements OnInit {
|
|||
},
|
||||
|
||||
{
|
||||
label: $localize`Subscriptions`,
|
||||
routerLink: '/my-library/subscriptions'
|
||||
label: $localize`Follows`,
|
||||
children: [
|
||||
{
|
||||
label: $localize`Subscriptions`,
|
||||
iconName: 'subscriptions',
|
||||
routerLink: '/my-library/subscriptions'
|
||||
},
|
||||
{
|
||||
label: $localize`Followers`,
|
||||
iconName: 'follower',
|
||||
routerLink: '/my-library/followers'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
|
|
|
@ -13,12 +13,13 @@ import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscripti
|
|||
import { SharedVideoLiveModule } from '@app/shared/shared-video-live'
|
||||
import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
|
||||
import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist/shared-video-playlist.module'
|
||||
import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
|
||||
import { MySubscriptionsComponent } from './my-follows/my-subscriptions.component'
|
||||
import { MyHistoryComponent } from './my-history/my-history.component'
|
||||
import { MyLibraryRoutingModule } from './my-library-routing.module'
|
||||
import { MyLibraryComponent } from './my-library.component'
|
||||
import { MyAcceptOwnershipComponent } from './my-ownership/my-accept-ownership/my-accept-ownership.component'
|
||||
import { MyOwnershipComponent } from './my-ownership/my-ownership.component'
|
||||
import { MySubscriptionsComponent } from './my-subscriptions/my-subscriptions.component'
|
||||
import { MyVideoImportsComponent } from './my-video-imports/my-video-imports.component'
|
||||
import { MyVideoPlaylistCreateComponent } from './my-video-playlists/my-video-playlist-create.component'
|
||||
import { MyVideoPlaylistElementsComponent } from './my-video-playlists/my-video-playlist-elements.component'
|
||||
|
@ -26,7 +27,7 @@ import { MyVideoPlaylistUpdateComponent } from './my-video-playlists/my-video-pl
|
|||
import { MyVideoPlaylistsComponent } from './my-video-playlists/my-video-playlists.component'
|
||||
import { VideoChangeOwnershipComponent } from './my-videos/modals/video-change-ownership.component'
|
||||
import { MyVideosComponent } from './my-videos/my-videos.component'
|
||||
import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
|
||||
import { MyFollowersComponent } from './my-follows/my-followers.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -61,6 +62,7 @@ import { SharedActorImageModule } from '../shared/shared-actor-image/shared-acto
|
|||
MyAcceptOwnershipComponent,
|
||||
MyVideoImportsComponent,
|
||||
MySubscriptionsComponent,
|
||||
MyFollowersComponent,
|
||||
MyHistoryComponent,
|
||||
|
||||
MyVideoPlaylistCreateComponent,
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
input[type=text] {
|
||||
@include peertube-input-text(300px);
|
||||
}
|
||||
|
||||
.video-channel {
|
||||
@include row-blocks;
|
||||
|
||||
> my-actor-avatar {
|
||||
@include actor-avatar-size(80px);
|
||||
|
||||
@include margin-right(10px);
|
||||
}
|
||||
}
|
||||
|
||||
.video-channel-info {
|
||||
flex-grow: 1;
|
||||
|
||||
a.video-channel-names {
|
||||
@include disable-default-a-behaviour;
|
||||
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
color: pvar(--mainForegroundColor);
|
||||
|
||||
.video-channel-display-name {
|
||||
font-weight: $font-semibold;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.video-channel-name {
|
||||
@include margin-left(5px);
|
||||
|
||||
font-size: 14px;
|
||||
color: $grey-actor-name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actor-owner {
|
||||
@include disable-default-a-behaviour;
|
||||
|
||||
font-size: 13px;
|
||||
color: pvar(--mainForegroundColor);
|
||||
|
||||
span:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
my-actor-avatar {
|
||||
@include margin-left(7px);
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.video-subscriptions-header {
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-view) {
|
||||
.video-subscriptions-header input[type=text] {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.video-channel-info {
|
||||
padding-bottom: 10px;
|
||||
text-align: center;
|
||||
|
||||
.video-channel-names {
|
||||
flex-direction: column;
|
||||
align-items: center !important;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
@include margin-right(0);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_actor' as *;
|
||||
@use '_account-channel-page' as *;
|
||||
@use '_miniature' as *;
|
||||
|
||||
.root {
|
||||
|
|
|
@ -13,9 +13,8 @@ interface QueryStringFilterPrefixes {
|
|||
}
|
||||
}
|
||||
|
||||
type ParseQueryStringFilterResult = {
|
||||
[key: string]: string | number | boolean | (string | number | boolean)[]
|
||||
}
|
||||
type ParseQueryStringFilters <K extends keyof any> = Partial<Record<K, string | number | boolean | (string | number | boolean)[]>>
|
||||
type ParseQueryStringFiltersResult <K extends keyof any> = ParseQueryStringFilters<K> & { search?: string }
|
||||
|
||||
@Injectable()
|
||||
export class RestService {
|
||||
|
@ -67,14 +66,17 @@ export class RestService {
|
|||
return params
|
||||
}
|
||||
|
||||
componentPaginationToRestPagination (componentPagination: ComponentPaginationLight): RestPagination {
|
||||
componentToRestPagination (componentPagination: ComponentPaginationLight): RestPagination {
|
||||
const start: number = (componentPagination.currentPage - 1) * componentPagination.itemsPerPage
|
||||
const count: number = componentPagination.itemsPerPage
|
||||
|
||||
return { start, count }
|
||||
}
|
||||
|
||||
parseQueryStringFilter (q: string, prefixes: QueryStringFilterPrefixes): ParseQueryStringFilterResult {
|
||||
/*
|
||||
* Returns an object containing the filters and the remaining search
|
||||
*/
|
||||
parseQueryStringFilter <T extends QueryStringFilterPrefixes> (q: string, prefixes: T): ParseQueryStringFiltersResult<keyof T> {
|
||||
if (!q) return {}
|
||||
|
||||
// Tokenize the strings using spaces that are not in quotes
|
||||
|
@ -90,9 +92,9 @@ export class RestService {
|
|||
return prefixeStrings.every(prefixString => t.startsWith(prefixString) === false)
|
||||
})
|
||||
|
||||
const additionalFilters: ParseQueryStringFilterResult = {}
|
||||
const additionalFilters: ParseQueryStringFilters<keyof T> = {}
|
||||
|
||||
for (const prefixKey of Object.keys(prefixes)) {
|
||||
for (const prefixKey of Object.keys(prefixes) as (keyof T)[]) {
|
||||
const prefixObj = prefixes[prefixKey]
|
||||
const prefix = prefixObj.prefix
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<ng-container i18n>
|
||||
<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>
|
||||
<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>
|
||||
</ng-container>
|
||||
|
|
|
@ -77,6 +77,8 @@ export class AdvancedInputFilterComponent implements OnInit, AfterViewInit {
|
|||
|
||||
logger('On route search change "%s".', search)
|
||||
|
||||
if (this.searchValue === search) return
|
||||
|
||||
this.searchValue = search
|
||||
this.emitSearch()
|
||||
})
|
||||
|
|
|
@ -19,7 +19,7 @@ export class UserHistoryService {
|
|||
) {}
|
||||
|
||||
getUserVideosHistory (historyPagination: ComponentPaginationLight, search?: string) {
|
||||
const pagination = this.restService.componentPaginationToRestPagination(historyPagination)
|
||||
const pagination = this.restService.componentToRestPagination(historyPagination)
|
||||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination)
|
||||
|
|
|
@ -29,7 +29,7 @@ export class UserNotificationService {
|
|||
const { pagination, ignoreLoadingBar, unread, sort } = parameters
|
||||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, this.restService.componentPaginationToRestPagination(pagination), sort)
|
||||
params = this.restService.addRestGetParams(params, this.restService.componentToRestPagination(pagination), sort)
|
||||
|
||||
if (unread) params = params.append('unread', `${unread}`)
|
||||
|
||||
|
|
|
@ -203,7 +203,7 @@
|
|||
<my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
|
||||
|
||||
<div class="message" i18n>
|
||||
<a (click)="markAsRead(notification)" [href]="notification.peertubeVersionLink" target="_blank" rel="noopener noreferer">A new version of PeerTube</a> is available: {{ notification.peertube.latestVersion }}
|
||||
<a (click)="markAsRead(notification)" [href]="notification.peertubeVersionLink" target="_blank" rel="noopener noreferrer">A new version of PeerTube</a> is available: {{ notification.peertube.latestVersion }}
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ export class VideoChannelService {
|
|||
const { account, componentPagination, withStats = false, sort, search } = options
|
||||
|
||||
const pagination = componentPagination
|
||||
? this.restService.componentPaginationToRestPagination(componentPagination)
|
||||
? this.restService.componentToRestPagination(componentPagination)
|
||||
: { start: 0, count: 20 }
|
||||
|
||||
let params = new HttpParams()
|
||||
|
|
|
@ -123,7 +123,7 @@ export class VideoService {
|
|||
}
|
||||
|
||||
getMyVideos (videoPagination: ComponentPaginationLight, sort: VideoSortField, search?: string): Observable<ResultList<Video>> {
|
||||
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
|
||||
const pagination = this.restService.componentToRestPagination(videoPagination)
|
||||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||
|
@ -377,7 +377,7 @@ export class VideoService {
|
|||
private buildCommonVideosParams (options: CommonVideoParams & { params: HttpParams }) {
|
||||
const { params, videoPagination, sort, filter, categoryOneOf, languageOneOf, skipCount, nsfwPolicy, isLive, nsfw } = options
|
||||
|
||||
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
|
||||
const pagination = this.restService.componentToRestPagination(videoPagination)
|
||||
let newParams = this.restService.addRestGetParams(params, pagination, sort)
|
||||
|
||||
if (filter) newParams = newParams.set('filter', filter)
|
||||
|
|
|
@ -43,7 +43,7 @@ export class SearchService {
|
|||
let pagination: RestPagination
|
||||
|
||||
if (componentPagination) {
|
||||
pagination = this.restService.componentPaginationToRestPagination(componentPagination)
|
||||
pagination = this.restService.componentToRestPagination(componentPagination)
|
||||
}
|
||||
|
||||
let params = new HttpParams()
|
||||
|
@ -77,7 +77,7 @@ export class SearchService {
|
|||
|
||||
let pagination: RestPagination
|
||||
if (componentPagination) {
|
||||
pagination = this.restService.componentPaginationToRestPagination(componentPagination)
|
||||
pagination = this.restService.componentToRestPagination(componentPagination)
|
||||
}
|
||||
|
||||
let params = new HttpParams()
|
||||
|
@ -111,7 +111,7 @@ export class SearchService {
|
|||
|
||||
let pagination: RestPagination
|
||||
if (componentPagination) {
|
||||
pagination = this.restService.componentPaginationToRestPagination(componentPagination)
|
||||
pagination = this.restService.componentToRestPagination(componentPagination)
|
||||
}
|
||||
|
||||
let params = new HttpParams()
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Injectable } from '@angular/core'
|
|||
import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core'
|
||||
import { buildBulkObservable } from '@app/helpers'
|
||||
import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
|
||||
import { ResultList, VideoChannel as VideoChannelServer, VideoSortField } from '@shared/models'
|
||||
import { ActorFollow, ResultList, VideoChannel as VideoChannelServer, VideoSortField } from '@shared/models'
|
||||
import { environment } from '../../../environments/environment'
|
||||
|
||||
const logger = debug('peertube:subscriptions:UserSubscriptionService')
|
||||
|
@ -17,6 +17,8 @@ type SubscriptionExistResultObservable = { [ uri: string ]: Observable<boolean>
|
|||
@Injectable()
|
||||
export class UserSubscriptionService {
|
||||
static BASE_USER_SUBSCRIPTIONS_URL = environment.apiUrl + '/api/v1/users/me/subscriptions'
|
||||
static BASE_VIDEO_CHANNELS_URL = environment.apiUrl + '/api/v1/video-channels'
|
||||
static BASE_ACCOUNTS_URL = environment.apiUrl + '/api/v1/accounts'
|
||||
|
||||
// Use a replay subject because we "next" a value before subscribing
|
||||
private existsSubject = new ReplaySubject<string>(1)
|
||||
|
@ -43,13 +45,46 @@ export class UserSubscriptionService {
|
|||
)
|
||||
}
|
||||
|
||||
listFollowers (parameters: {
|
||||
pagination: ComponentPaginationLight
|
||||
nameWithHost: string
|
||||
search?: string
|
||||
}) {
|
||||
const { pagination, nameWithHost, search } = parameters
|
||||
|
||||
let url = `${UserSubscriptionService.BASE_ACCOUNTS_URL}/${nameWithHost}/followers`
|
||||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, this.restService.componentToRestPagination(pagination), '-createdAt')
|
||||
|
||||
if (search) {
|
||||
const filters = this.restService.parseQueryStringFilter(search, {
|
||||
channel: {
|
||||
prefix: 'channel:'
|
||||
}
|
||||
})
|
||||
|
||||
if (filters.channel) {
|
||||
url = `${UserSubscriptionService.BASE_VIDEO_CHANNELS_URL}/${filters.channel}/followers`
|
||||
}
|
||||
|
||||
params = this.restService.addObjectParams(params, { search: filters.search })
|
||||
}
|
||||
|
||||
return this.authHttp
|
||||
.get<ResultList<ActorFollow>>(url, { params })
|
||||
.pipe(
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
}
|
||||
|
||||
getUserSubscriptionVideos (parameters: {
|
||||
videoPagination: ComponentPaginationLight
|
||||
sort: VideoSortField
|
||||
skipCount?: boolean
|
||||
}): Observable<ResultList<Video>> {
|
||||
const { videoPagination, sort, skipCount } = parameters
|
||||
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
|
||||
const pagination = this.restService.componentToRestPagination(videoPagination)
|
||||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||
|
@ -106,7 +141,7 @@ export class UserSubscriptionService {
|
|||
const { pagination, search } = parameters
|
||||
const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL
|
||||
|
||||
const restPagination = this.restService.componentPaginationToRestPagination(pagination)
|
||||
const restPagination = this.restService.componentToRestPagination(pagination)
|
||||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, restPagination)
|
||||
|
|
|
@ -81,7 +81,7 @@ export class VideoCommentService {
|
|||
}): Observable<ThreadsResultList<VideoComment>> {
|
||||
const { videoId, componentPagination, sort } = parameters
|
||||
|
||||
const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
|
||||
const pagination = this.restService.componentToRestPagination(componentPagination)
|
||||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||
|
|
|
@ -62,7 +62,7 @@ export class VideoPlaylistService {
|
|||
|
||||
listChannelPlaylists (videoChannel: VideoChannel, componentPagination: ComponentPaginationLight): Observable<ResultList<VideoPlaylist>> {
|
||||
const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/video-playlists'
|
||||
const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
|
||||
const pagination = this.restService.componentToRestPagination(componentPagination)
|
||||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination)
|
||||
|
@ -103,7 +103,7 @@ export class VideoPlaylistService {
|
|||
): Observable<ResultList<VideoPlaylist>> {
|
||||
const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-playlists'
|
||||
const pagination = componentPagination
|
||||
? this.restService.componentPaginationToRestPagination(componentPagination)
|
||||
? this.restService.componentToRestPagination(componentPagination)
|
||||
: undefined
|
||||
|
||||
let params = new HttpParams()
|
||||
|
@ -259,7 +259,7 @@ export class VideoPlaylistService {
|
|||
componentPagination: ComponentPaginationLight
|
||||
}): Observable<ResultList<VideoPlaylistElement>> {
|
||||
const path = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + options.videoPlaylistId + '/videos'
|
||||
const pagination = this.restService.componentPaginationToRestPagination(options.componentPagination)
|
||||
const pagination = this.restService.componentToRestPagination(options.componentPagination)
|
||||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination)
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
@mixin section-label-responsive {
|
||||
color: pvar(--mainColor);
|
||||
font-size: 12px;
|
||||
margin-bottom: 15px;
|
||||
font-weight: $font-bold;
|
||||
letter-spacing: 2.5px;
|
||||
|
||||
@media screen and (max-width: $mobile-view) {
|
||||
font-size: 10px;
|
||||
letter-spacing: 2.1px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin show-more-description {
|
||||
color: pvar(--mainColor);
|
||||
cursor: pointer;
|
||||
margin: 10px auto 45px;
|
||||
}
|
||||
|
||||
@mixin avatar-row-responsive ($img-margin, $grey-font-size) {
|
||||
display: flex;
|
||||
grid-column: 1;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.main-avatar {
|
||||
@include actor-avatar-size(120px);
|
||||
}
|
||||
|
||||
> div {
|
||||
@include margin-left($img-margin);
|
||||
|
||||
min-width: 1px;
|
||||
}
|
||||
|
||||
.actor-info {
|
||||
display: flex;
|
||||
|
||||
> div:first-child {
|
||||
flex-grow: 1;
|
||||
min-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.actor-display-name {
|
||||
@include peertube-word-wrap;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
font-weight: $font-bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actor-handle {
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.actor-handle,
|
||||
.actor-counters {
|
||||
color: pvar(--greyForegroundColor);
|
||||
font-size: $grey-font-size;
|
||||
}
|
||||
|
||||
.actor-counters > *:not(:last-child)::after {
|
||||
content: '•';
|
||||
margin: 0 10px;
|
||||
color: pvar(--mainColor);
|
||||
}
|
||||
|
||||
@media screen and (max-width: $mobile-view) {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.main-avatar {
|
||||
@include actor-avatar-size(80px);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,88 +1,68 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
@mixin section-label-responsive {
|
||||
color: pvar(--mainColor);
|
||||
font-size: 12px;
|
||||
margin-bottom: 15px;
|
||||
font-weight: $font-bold;
|
||||
letter-spacing: 2.5px;
|
||||
@mixin actor-row ($avatar-size: 80px, $avatar-margin-right: 10px, $min-height: 130px, $separator: true) {
|
||||
@include row-blocks($min-height: $min-height, $separator: $separator);
|
||||
|
||||
@media screen and (max-width: $mobile-view) {
|
||||
font-size: 10px;
|
||||
letter-spacing: 2.1px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
> my-actor-avatar {
|
||||
@include actor-avatar-size($avatar-size);
|
||||
|
||||
@mixin show-more-description {
|
||||
color: pvar(--mainColor);
|
||||
cursor: pointer;
|
||||
margin: 10px auto 45px;
|
||||
}
|
||||
|
||||
@mixin avatar-row-responsive ($img-margin, $grey-font-size) {
|
||||
display: flex;
|
||||
grid-column: 1;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.main-avatar {
|
||||
@include actor-avatar-size(120px);
|
||||
}
|
||||
|
||||
> div {
|
||||
@include margin-left($img-margin);
|
||||
|
||||
min-width: 1px;
|
||||
@include margin-right($avatar-margin-right);
|
||||
}
|
||||
|
||||
.actor-info {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
> div:first-child {
|
||||
flex-grow: 1;
|
||||
min-width: 1px;
|
||||
}
|
||||
.actor-names {
|
||||
@include disable-default-a-behaviour;
|
||||
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
color: pvar(--mainForegroundColor);
|
||||
}
|
||||
|
||||
.actor-display-name {
|
||||
@include peertube-word-wrap;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-weight: $font-semibold;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
font-weight: $font-bold;
|
||||
margin: 0;
|
||||
.actor-name {
|
||||
@include margin-left(5px);
|
||||
|
||||
font-size: 14px;
|
||||
color: $grey-actor-name;
|
||||
}
|
||||
|
||||
.actor-handle {
|
||||
@include ellipsis;
|
||||
}
|
||||
.actor-owner {
|
||||
@include disable-default-a-behaviour;
|
||||
|
||||
.actor-handle,
|
||||
.actor-counters {
|
||||
color: pvar(--greyForegroundColor);
|
||||
font-size: $grey-font-size;
|
||||
}
|
||||
font-size: 13px;
|
||||
color: pvar(--mainForegroundColor);
|
||||
|
||||
.actor-counters > *:not(:last-child)::after {
|
||||
content: '•';
|
||||
margin: 0 10px;
|
||||
color: pvar(--mainColor);
|
||||
}
|
||||
|
||||
@media screen and (max-width: $mobile-view) {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
span:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.main-avatar {
|
||||
@include actor-avatar-size(80px);
|
||||
my-actor-avatar {
|
||||
@include margin-left(7px);
|
||||
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-view) {
|
||||
.actor-info {
|
||||
padding-bottom: 10px;
|
||||
text-align: center;
|
||||
|
||||
.actor-names {
|
||||
flex-direction: column;
|
||||
align-items: center !important;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -653,12 +653,15 @@
|
|||
@include button-with-icon(20px, 5px, -1px);
|
||||
}
|
||||
|
||||
@mixin row-blocks ($column-responsive: true) {
|
||||
@mixin row-blocks ($column-responsive: true, $min-height: 130px, $separator: true) {
|
||||
display: flex;
|
||||
min-height: 130px;
|
||||
min-height: $min-height;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #C6C6C6;
|
||||
|
||||
@if $separator {
|
||||
border-bottom: 1px solid #C6C6C6;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-view) {
|
||||
@if $column-responsive {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import express from 'express'
|
||||
import { pickCommonVideoQuery } from '@server/helpers/query'
|
||||
import { ActorFollowModel } from '@server/models/actor/actor-follow'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
|
||||
import { getFormattedObjects } from '../../helpers/utils'
|
||||
|
@ -20,6 +21,7 @@ import {
|
|||
} from '../../middlewares'
|
||||
import {
|
||||
accountNameWithHostGetValidator,
|
||||
accountsFollowersSortValidator,
|
||||
accountsSortValidator,
|
||||
ensureAuthUserOwnsAccountValidator,
|
||||
videoChannelsSortValidator,
|
||||
|
@ -93,6 +95,17 @@ accountsRouter.get('/:accountName/ratings',
|
|||
asyncMiddleware(listAccountRatings)
|
||||
)
|
||||
|
||||
accountsRouter.get('/:accountName/followers',
|
||||
authenticate,
|
||||
asyncMiddleware(accountNameWithHostGetValidator),
|
||||
ensureAuthUserOwnsAccountValidator,
|
||||
paginationValidator,
|
||||
accountsFollowersSortValidator,
|
||||
setDefaultSort,
|
||||
setDefaultPagination,
|
||||
asyncMiddleware(listAccountFollowers)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
|
@ -127,7 +140,7 @@ async function listAccountChannels (req: express.Request, res: express.Response)
|
|||
search: req.query.search
|
||||
}
|
||||
|
||||
const resultList = await VideoChannelModel.listByAccount(options)
|
||||
const resultList = await VideoChannelModel.listByAccountForAPI(options)
|
||||
|
||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||
}
|
||||
|
@ -195,3 +208,21 @@ async function listAccountRatings (req: express.Request, res: express.Response)
|
|||
})
|
||||
return res.json(getFormattedObjects(resultList.rows, resultList.count))
|
||||
}
|
||||
|
||||
async function listAccountFollowers (req: express.Request, res: express.Response) {
|
||||
const account = res.locals.account
|
||||
|
||||
const channels = await VideoChannelModel.listAllByAccount(account.id)
|
||||
const actorIds = [ account.actorId ].concat(channels.map(c => c.actorId))
|
||||
|
||||
const resultList = await ActorFollowModel.listFollowersForApi({
|
||||
actorIds,
|
||||
start: req.query.start,
|
||||
count: req.query.count,
|
||||
sort: req.query.sort,
|
||||
search: req.query.search,
|
||||
state: 'accepted',
|
||||
})
|
||||
|
||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@ export {
|
|||
|
||||
async function listFollowing (req: express.Request, res: express.Response) {
|
||||
const serverActor = await getServerActor()
|
||||
const resultList = await ActorFollowModel.listFollowingForApi({
|
||||
const resultList = await ActorFollowModel.listInstanceFollowingForApi({
|
||||
id: serverActor.id,
|
||||
start: req.query.start,
|
||||
count: req.query.count,
|
||||
|
@ -114,7 +114,7 @@ async function listFollowing (req: express.Request, res: express.Response) {
|
|||
async function listFollowers (req: express.Request, res: express.Response) {
|
||||
const serverActor = await getServerActor()
|
||||
const resultList = await ActorFollowModel.listFollowersForApi({
|
||||
actorId: serverActor.id,
|
||||
actorIds: [ serverActor.id ],
|
||||
start: req.query.start,
|
||||
count: req.query.count,
|
||||
sort: req.query.sort,
|
||||
|
|
|
@ -95,7 +95,7 @@ async function areSubscriptionsExist (req: express.Request, res: express.Respons
|
|||
return { name, host, uri: u }
|
||||
})
|
||||
|
||||
const results = await ActorFollowModel.listSubscribedIn(user.Account.Actor.id, handles)
|
||||
const results = await ActorFollowModel.listSubscriptionsOf(user.Account.Actor.id, handles)
|
||||
|
||||
const existObject: { [id: string ]: boolean } = {}
|
||||
for (const handle of handles) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import express from 'express'
|
||||
import { pickCommonVideoQuery } from '@server/helpers/query'
|
||||
import { Hooks } from '@server/lib/plugins/hooks'
|
||||
import { ActorFollowModel } from '@server/models/actor/actor-follow'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
import { MChannelBannerAccountDefault } from '@server/types/models'
|
||||
import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
|
||||
|
@ -33,7 +34,13 @@ import {
|
|||
videoChannelsUpdateValidator,
|
||||
videoPlaylistsSortValidator
|
||||
} from '../../middlewares'
|
||||
import { videoChannelsListValidator, videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators'
|
||||
import {
|
||||
ensureAuthUserOwnsChannelValidator,
|
||||
videoChannelsFollowersSortValidator,
|
||||
videoChannelsListValidator,
|
||||
videoChannelsNameWithHostValidator,
|
||||
videosSortValidator
|
||||
} from '../../middlewares/validators'
|
||||
import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image'
|
||||
import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
|
||||
import { AccountModel } from '../../models/account/account'
|
||||
|
@ -65,8 +72,8 @@ videoChannelRouter.post('/',
|
|||
videoChannelRouter.post('/:nameWithHost/avatar/pick',
|
||||
authenticate,
|
||||
reqAvatarFile,
|
||||
// Check the rights
|
||||
asyncMiddleware(videoChannelsUpdateValidator),
|
||||
asyncMiddleware(videoChannelsNameWithHostValidator),
|
||||
ensureAuthUserOwnsChannelValidator,
|
||||
updateAvatarValidator,
|
||||
asyncMiddleware(updateVideoChannelAvatar)
|
||||
)
|
||||
|
@ -74,29 +81,31 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick',
|
|||
videoChannelRouter.post('/:nameWithHost/banner/pick',
|
||||
authenticate,
|
||||
reqBannerFile,
|
||||
// Check the rights
|
||||
asyncMiddleware(videoChannelsUpdateValidator),
|
||||
asyncMiddleware(videoChannelsNameWithHostValidator),
|
||||
ensureAuthUserOwnsChannelValidator,
|
||||
updateBannerValidator,
|
||||
asyncMiddleware(updateVideoChannelBanner)
|
||||
)
|
||||
|
||||
videoChannelRouter.delete('/:nameWithHost/avatar',
|
||||
authenticate,
|
||||
// Check the rights
|
||||
asyncMiddleware(videoChannelsUpdateValidator),
|
||||
asyncMiddleware(videoChannelsNameWithHostValidator),
|
||||
ensureAuthUserOwnsChannelValidator,
|
||||
asyncMiddleware(deleteVideoChannelAvatar)
|
||||
)
|
||||
|
||||
videoChannelRouter.delete('/:nameWithHost/banner',
|
||||
authenticate,
|
||||
// Check the rights
|
||||
asyncMiddleware(videoChannelsUpdateValidator),
|
||||
asyncMiddleware(videoChannelsNameWithHostValidator),
|
||||
ensureAuthUserOwnsChannelValidator,
|
||||
asyncMiddleware(deleteVideoChannelBanner)
|
||||
)
|
||||
|
||||
videoChannelRouter.put('/:nameWithHost',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsUpdateValidator),
|
||||
asyncMiddleware(videoChannelsNameWithHostValidator),
|
||||
ensureAuthUserOwnsChannelValidator,
|
||||
videoChannelsUpdateValidator,
|
||||
asyncRetryTransactionMiddleware(updateVideoChannel)
|
||||
)
|
||||
|
||||
|
@ -132,6 +141,17 @@ videoChannelRouter.get('/:nameWithHost/videos',
|
|||
asyncMiddleware(listVideoChannelVideos)
|
||||
)
|
||||
|
||||
videoChannelRouter.get('/:nameWithHost/followers',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsNameWithHostValidator),
|
||||
ensureAuthUserOwnsChannelValidator,
|
||||
paginationValidator,
|
||||
videoChannelsFollowersSortValidator,
|
||||
setDefaultSort,
|
||||
setDefaultPagination,
|
||||
asyncMiddleware(listVideoChannelFollowers)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
|
@ -332,3 +352,18 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
|
|||
|
||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||
}
|
||||
|
||||
async function listVideoChannelFollowers (req: express.Request, res: express.Response) {
|
||||
const channel = res.locals.videoChannel
|
||||
|
||||
const resultList = await ActorFollowModel.listFollowersForApi({
|
||||
actorIds: [ channel.actorId ],
|
||||
start: req.query.start,
|
||||
count: req.query.count,
|
||||
sort: req.query.sort,
|
||||
search: req.query.search,
|
||||
state: 'accepted',
|
||||
})
|
||||
|
||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||
}
|
||||
|
|
|
@ -69,8 +69,11 @@ const SORTABLE_COLUMNS = {
|
|||
|
||||
VIDEO_RATES: [ 'createdAt' ],
|
||||
BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ],
|
||||
|
||||
INSTANCE_FOLLOWERS: [ 'createdAt', 'state', 'score' ],
|
||||
INSTANCE_FOLLOWING: [ 'createdAt', 'redundancyAllowed', 'state' ],
|
||||
ACCOUNT_FOLLOWERS: [ 'createdAt' ],
|
||||
CHANNEL_FOLLOWERS: [ 'createdAt' ],
|
||||
|
||||
VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot', 'best' ],
|
||||
|
||||
|
|
|
@ -53,6 +53,9 @@ const pluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.PLUGINS)
|
|||
const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS)
|
||||
const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES)
|
||||
|
||||
const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS)
|
||||
const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
|
@ -79,5 +82,7 @@ export {
|
|||
videoPlaylistsSortValidator,
|
||||
videoRedundanciesSortValidator,
|
||||
videoPlaylistsSearchSortValidator,
|
||||
accountsFollowersSortValidator,
|
||||
videoChannelsFollowersSortValidator,
|
||||
pluginsSortValidator
|
||||
}
|
||||
|
|
|
@ -3,9 +3,7 @@ import { body, param, query } from 'express-validator'
|
|||
import { omit } from 'lodash'
|
||||
import { Hooks } from '@server/lib/plugins/hooks'
|
||||
import { MUserDefault } from '@server/types/models'
|
||||
import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
|
||||
import { UserRole } from '../../../shared/models/users'
|
||||
import { UserRegister } from '../../../shared/models/users/user-register.model'
|
||||
import { HttpStatusCode, UserRegister, UserRole } from '@shared/models'
|
||||
import { toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
|
||||
import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
|
||||
import {
|
||||
|
@ -462,7 +460,22 @@ const ensureAuthUserOwnsAccountValidator = [
|
|||
if (res.locals.account.id !== user.Account.id) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Only owner can access ratings list.'
|
||||
message: 'Only owner of this account can access this ressource.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const ensureAuthUserOwnsChannelValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
if (res.locals.videoChannel.Account.userId !== user.id) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Only owner of this video channel can access this ressource'
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -506,6 +519,7 @@ export {
|
|||
usersVerifyEmailValidator,
|
||||
userAutocompleteValidator,
|
||||
ensureAuthUserOwnsAccountValidator,
|
||||
ensureAuthUserOwnsChannelValidator,
|
||||
ensureCanManageUser
|
||||
}
|
||||
|
||||
|
|
|
@ -65,22 +65,6 @@ const videoChannelsUpdateValidator = [
|
|||
logger.debug('Checking videoChannelsUpdate parameters', { parameters: req.body })
|
||||
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return
|
||||
|
||||
// We need to make additional checks
|
||||
if (res.locals.videoChannel.Actor.isOwned() === false) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot update video channel of another server'
|
||||
})
|
||||
}
|
||||
|
||||
if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot update video channel of another user'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
|
|
@ -143,7 +143,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
|
|||
* @deprecated Use `findOrCreateCustom` instead
|
||||
*/
|
||||
static findOrCreate (): any {
|
||||
throw new Error('Should not be called')
|
||||
throw new Error('Must not be called')
|
||||
}
|
||||
|
||||
// findOrCreate has issues with actor follow hooks
|
||||
|
@ -288,7 +288,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
|
|||
return ActorFollowModel.findOne(query)
|
||||
}
|
||||
|
||||
static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> {
|
||||
static listSubscriptionsOf (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> {
|
||||
const whereTab = targets
|
||||
.map(t => {
|
||||
if (t.host) {
|
||||
|
@ -348,7 +348,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
|
|||
return ActorFollowModel.findAll(query)
|
||||
}
|
||||
|
||||
static listFollowingForApi (options: {
|
||||
static listInstanceFollowingForApi (options: {
|
||||
id: number
|
||||
start: number
|
||||
count: number
|
||||
|
@ -415,7 +415,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
|
|||
}
|
||||
|
||||
static listFollowersForApi (options: {
|
||||
actorId: number
|
||||
actorIds: number[]
|
||||
start: number
|
||||
count: number
|
||||
sort: string
|
||||
|
@ -423,7 +423,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
|
|||
actorType?: ActivityPubActorType
|
||||
search?: string
|
||||
}) {
|
||||
const { actorId, start, count, sort, search, state, actorType } = options
|
||||
const { actorIds, start, count, sort, search, state, actorType } = options
|
||||
|
||||
const followWhere = state ? { state } : {}
|
||||
const followerWhere: WhereOptions = {}
|
||||
|
@ -452,20 +452,16 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
|
|||
model: ActorModel,
|
||||
required: true,
|
||||
as: 'ActorFollower',
|
||||
where: followerWhere,
|
||||
include: [
|
||||
{
|
||||
model: ServerModel,
|
||||
required: true
|
||||
}
|
||||
]
|
||||
where: followerWhere
|
||||
},
|
||||
{
|
||||
model: ActorModel,
|
||||
as: 'ActorFollowing',
|
||||
required: true,
|
||||
where: {
|
||||
id: actorId
|
||||
id: {
|
||||
[Op.in]: actorIds
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
isVideoChannelDisplayNameValid,
|
||||
isVideoChannelSupportValid
|
||||
} from '../../helpers/custom-validators/video-channels'
|
||||
import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
|
||||
import { CONSTRAINTS_FIELDS, VIDEO_CHANNELS, WEBSERVER } from '../../initializers/constants'
|
||||
import { sendDeleteActor } from '../../lib/activitypub/send'
|
||||
import {
|
||||
MChannelActor,
|
||||
|
@ -527,7 +527,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
|
|||
})
|
||||
}
|
||||
|
||||
static listByAccount (options: {
|
||||
static listByAccountForAPI (options: {
|
||||
accountId: number
|
||||
start: number
|
||||
count: number
|
||||
|
@ -582,6 +582,26 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
static listAllByAccount (accountId: number) {
|
||||
const query = {
|
||||
limit: VIDEO_CHANNELS.MAX_PER_USER,
|
||||
include: [
|
||||
{
|
||||
attributes: [],
|
||||
model: AccountModel,
|
||||
where: {
|
||||
id: accountId
|
||||
},
|
||||
required: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return VideoChannelModel.findAll(query)
|
||||
}
|
||||
|
||||
|
||||
static loadAndPopulateAccount (id: number, transaction?: Transaction): Promise<MChannelBannerAccountDefault> {
|
||||
return VideoChannelModel.unscoped()
|
||||
.scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ])
|
||||
|
|
|
@ -840,6 +840,34 @@ describe('Test users API validators', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('When getting my global followers', function () {
|
||||
const path = '/api/v1/accounts/user1/followers'
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, userToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, userToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, userToken)
|
||||
})
|
||||
|
||||
it('Should fail with a unauthenticated user', async function () {
|
||||
await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a another user', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: userToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When blocking/unblocking/removing user', function () {
|
||||
|
||||
it('Should fail with an incorrect id', async function () {
|
||||
|
|
|
@ -321,6 +321,34 @@ describe('Test video channels API validator', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('When getting channel followers', function () {
|
||||
const path = '/api/v1/video-channels/super_channel/followers'
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a unauthenticated user', async function () {
|
||||
await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a another user', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: accessTokenUser, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When deleting a video channel', function () {
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await command.delete({ token: 'coucou', channelName: 'super_channel', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
|
|
|
@ -368,6 +368,162 @@ describe('Test users subscriptions', function () {
|
|||
}
|
||||
})
|
||||
|
||||
it('Should follow user channels of server 3 by root of server 3', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[2].channels.create({ token: users[2].accessToken, attributes: { name: 'user3_channel2' } })
|
||||
|
||||
await servers[2].subscriptions.add({ token: servers[2].accessToken, targetUri: 'user3_channel@localhost:' + servers[2].port })
|
||||
await servers[2].subscriptions.add({ token: servers[2].accessToken, targetUri: 'user3_channel2@localhost:' + servers[2].port })
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should list user 3 followers', async function () {
|
||||
{
|
||||
const { total, data } = await servers[2].accounts.listFollowers({
|
||||
token: users[2].accessToken,
|
||||
accountName: 'user3',
|
||||
start: 0,
|
||||
count: 5,
|
||||
sort: 'createdAt'
|
||||
})
|
||||
|
||||
expect(total).to.equal(3)
|
||||
expect(data[0].following.host).to.equal(servers[2].host)
|
||||
expect(data[0].following.name).to.equal('user3_channel')
|
||||
expect(data[0].follower.host).to.equal(servers[0].host)
|
||||
expect(data[0].follower.name).to.equal('user1')
|
||||
|
||||
expect(data[1].following.host).to.equal(servers[2].host)
|
||||
expect(data[1].following.name).to.equal('user3_channel')
|
||||
expect(data[1].follower.host).to.equal(servers[2].host)
|
||||
expect(data[1].follower.name).to.equal('root')
|
||||
|
||||
expect(data[2].following.host).to.equal(servers[2].host)
|
||||
expect(data[2].following.name).to.equal('user3_channel2')
|
||||
expect(data[2].follower.host).to.equal(servers[2].host)
|
||||
expect(data[2].follower.name).to.equal('root')
|
||||
}
|
||||
|
||||
{
|
||||
const { total, data } = await servers[2].accounts.listFollowers({
|
||||
token: users[2].accessToken,
|
||||
accountName: 'user3',
|
||||
start: 0,
|
||||
count: 1,
|
||||
sort: '-createdAt'
|
||||
})
|
||||
|
||||
expect(total).to.equal(3)
|
||||
expect(data[0].following.host).to.equal(servers[2].host)
|
||||
expect(data[0].following.name).to.equal('user3_channel2')
|
||||
expect(data[0].follower.host).to.equal(servers[2].host)
|
||||
expect(data[0].follower.name).to.equal('root')
|
||||
}
|
||||
|
||||
{
|
||||
const { total, data } = await servers[2].accounts.listFollowers({
|
||||
token: users[2].accessToken,
|
||||
accountName: 'user3',
|
||||
start: 1,
|
||||
count: 1,
|
||||
sort: '-createdAt'
|
||||
})
|
||||
|
||||
expect(total).to.equal(3)
|
||||
expect(data[0].following.host).to.equal(servers[2].host)
|
||||
expect(data[0].following.name).to.equal('user3_channel')
|
||||
expect(data[0].follower.host).to.equal(servers[2].host)
|
||||
expect(data[0].follower.name).to.equal('root')
|
||||
}
|
||||
|
||||
{
|
||||
const { total, data } = await servers[2].accounts.listFollowers({
|
||||
token: users[2].accessToken,
|
||||
accountName: 'user3',
|
||||
search: 'user1',
|
||||
sort: '-createdAt'
|
||||
})
|
||||
|
||||
expect(total).to.equal(1)
|
||||
expect(data[0].following.host).to.equal(servers[2].host)
|
||||
expect(data[0].following.name).to.equal('user3_channel')
|
||||
expect(data[0].follower.host).to.equal(servers[0].host)
|
||||
expect(data[0].follower.name).to.equal('user1')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should list user3_channel followers', async function () {
|
||||
{
|
||||
const { total, data } = await servers[2].channels.listFollowers({
|
||||
token: users[2].accessToken,
|
||||
channelName: 'user3_channel',
|
||||
start: 0,
|
||||
count: 5,
|
||||
sort: 'createdAt'
|
||||
})
|
||||
|
||||
expect(total).to.equal(2)
|
||||
expect(data[0].following.host).to.equal(servers[2].host)
|
||||
expect(data[0].following.name).to.equal('user3_channel')
|
||||
expect(data[0].follower.host).to.equal(servers[0].host)
|
||||
expect(data[0].follower.name).to.equal('user1')
|
||||
|
||||
expect(data[1].following.host).to.equal(servers[2].host)
|
||||
expect(data[1].following.name).to.equal('user3_channel')
|
||||
expect(data[1].follower.host).to.equal(servers[2].host)
|
||||
expect(data[1].follower.name).to.equal('root')
|
||||
}
|
||||
|
||||
{
|
||||
const { total, data } = await servers[2].channels.listFollowers({
|
||||
token: users[2].accessToken,
|
||||
channelName: 'user3_channel',
|
||||
start: 0,
|
||||
count: 1,
|
||||
sort: '-createdAt'
|
||||
})
|
||||
|
||||
expect(total).to.equal(2)
|
||||
expect(data[0].following.host).to.equal(servers[2].host)
|
||||
expect(data[0].following.name).to.equal('user3_channel')
|
||||
expect(data[0].follower.host).to.equal(servers[2].host)
|
||||
expect(data[0].follower.name).to.equal('root')
|
||||
}
|
||||
|
||||
{
|
||||
const { total, data } = await servers[2].channels.listFollowers({
|
||||
token: users[2].accessToken,
|
||||
channelName: 'user3_channel',
|
||||
start: 1,
|
||||
count: 1,
|
||||
sort: '-createdAt'
|
||||
})
|
||||
|
||||
expect(total).to.equal(2)
|
||||
expect(data[0].following.host).to.equal(servers[2].host)
|
||||
expect(data[0].following.name).to.equal('user3_channel')
|
||||
expect(data[0].follower.host).to.equal(servers[0].host)
|
||||
expect(data[0].follower.name).to.equal('root')
|
||||
}
|
||||
|
||||
{
|
||||
const { total, data } = await servers[2].channels.listFollowers({
|
||||
token: users[2].accessToken,
|
||||
channelName: 'user3_channel',
|
||||
search: 'user1',
|
||||
sort: '-createdAt'
|
||||
})
|
||||
|
||||
expect(total).to.equal(1)
|
||||
expect(data[0].following.host).to.equal(servers[2].host)
|
||||
expect(data[0].following.name).to.equal('user3_channel')
|
||||
expect(data[0].follower.host).to.equal(servers[0].host)
|
||||
expect(data[0].follower.name).to.equal('user1')
|
||||
}
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { HttpStatusCode, ResultList } from '@shared/models'
|
||||
import { Account } from '../../models/actors'
|
||||
import { Account, ActorFollow } from '../../models/actors'
|
||||
import { AccountVideoRate, VideoRateType } from '../../models/videos'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
||||
|
||||
|
@ -53,4 +53,26 @@ export class AccountsCommand extends AbstractCommand {
|
|||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
listFollowers (options: OverrideCommandOptions & {
|
||||
accountName: string
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
search?: string
|
||||
}) {
|
||||
const { accountName, start, count, sort, search } = options
|
||||
const path = '/api/v1/accounts/' + accountName + '/followers'
|
||||
|
||||
const query = { start, count, sort, search }
|
||||
|
||||
return this.getRequestBody<ResultList<ActorFollow>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { pick } from '@shared/core-utils'
|
||||
import { HttpStatusCode, ResultList, VideoChannel, VideoChannelCreateResult } from '@shared/models'
|
||||
import { ActorFollow, HttpStatusCode, ResultList, VideoChannel, VideoChannelCreateResult } from '@shared/models'
|
||||
import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model'
|
||||
import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model'
|
||||
import { unwrapBody } from '../requests'
|
||||
|
@ -47,7 +47,7 @@ export class ChannelsCommand extends AbstractCommand {
|
|||
}
|
||||
|
||||
async create (options: OverrideCommandOptions & {
|
||||
attributes: VideoChannelCreate
|
||||
attributes: Partial<VideoChannelCreate>
|
||||
}) {
|
||||
const path = '/api/v1/video-channels/'
|
||||
|
||||
|
@ -153,4 +153,26 @@ export class ChannelsCommand extends AbstractCommand {
|
|||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
listFollowers (options: OverrideCommandOptions & {
|
||||
channelName: string
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
search?: string
|
||||
}) {
|
||||
const { channelName, start, count, sort, search } = options
|
||||
const path = '/api/v1/video-channels/' + channelName + '/followers'
|
||||
|
||||
const query = { start, count, sort, search }
|
||||
|
||||
return this.getRequestBody<ResultList<ActorFollow>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue