Verified Commit 4beda9e1 authored by Chocobozzz's avatar Chocobozzz
Browse files

Add ability to view my followers

parent 9593a78a
......@@ -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({
......
@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;
......
<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>
@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;
}
}
}
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>
......
@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;
}
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,
......
@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);
}
}
@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
......
<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()
})
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment