aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+accounts
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/+accounts')
-rw-r--r--client/src/app/+accounts/account-about/account-about.component.html15
-rw-r--r--client/src/app/+accounts/account-about/account-about.component.scss12
-rw-r--r--client/src/app/+accounts/account-about/account-about.component.ts40
-rw-r--r--client/src/app/+accounts/account-search/account-search.component.ts7
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.html43
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.scss172
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.ts47
-rw-r--r--client/src/app/+accounts/account-videos/account-videos.component.ts10
-rw-r--r--client/src/app/+accounts/accounts-routing.module.ts16
-rw-r--r--client/src/app/+accounts/accounts.component.html114
-rw-r--r--client/src/app/+accounts/accounts.component.scss179
-rw-r--r--client/src/app/+accounts/accounts.component.ts121
-rw-r--r--client/src/app/+accounts/accounts.module.ts8
13 files changed, 510 insertions, 274 deletions
diff --git a/client/src/app/+accounts/account-about/account-about.component.html b/client/src/app/+accounts/account-about/account-about.component.html
deleted file mode 100644
index e9e0e4079..000000000
--- a/client/src/app/+accounts/account-about/account-about.component.html
+++ /dev/null
@@ -1,15 +0,0 @@
1<h1 class="sr-only" i18n>About</h1>
2<div class="margin-content">
3 <div *ngIf="account" class="row no-gutters">
4 <div class="block col-md-6 col-sm-12 pr-2">
5 <h2 i18n class="small-title">DESCRIPTION</h2>
6 <div class="content" [innerHtml]="getAccountDescription()"></div>
7 </div>
8
9 <div class="block col-md-6 col-sm-12">
10 <h2 i18n class="small-title">STATS</h2>
11
12 <div i18n class="content">Joined {{ account.createdAt | date }}</div>
13 </div>
14 </div>
15</div>
diff --git a/client/src/app/+accounts/account-about/account-about.component.scss b/client/src/app/+accounts/account-about/account-about.component.scss
deleted file mode 100644
index 5bcd4b561..000000000
--- a/client/src/app/+accounts/account-about/account-about.component.scss
+++ /dev/null
@@ -1,12 +0,0 @@
1@import '_variables';
2@import '_mixins';
3
4.block {
5 margin-bottom: 40px;
6
7 .small-title {
8 @include in-content-small-title;
9
10 margin-bottom: 20px;
11 }
12}
diff --git a/client/src/app/+accounts/account-about/account-about.component.ts b/client/src/app/+accounts/account-about/account-about.component.ts
deleted file mode 100644
index 6cf846d72..000000000
--- a/client/src/app/+accounts/account-about/account-about.component.ts
+++ /dev/null
@@ -1,40 +0,0 @@
1import { Subscription } from 'rxjs'
2import { Component, OnDestroy, OnInit } from '@angular/core'
3import { MarkdownService } from '@app/core'
4import { Account, AccountService } from '@app/shared/shared-main'
5
6@Component({
7 selector: 'my-account-about',
8 templateUrl: './account-about.component.html',
9 styleUrls: [ './account-about.component.scss' ]
10})
11export class AccountAboutComponent implements OnInit, OnDestroy {
12 account: Account
13 descriptionHTML = ''
14
15 private accountSub: Subscription
16
17 constructor (
18 private accountService: AccountService,
19 private markdownService: MarkdownService
20 ) { }
21
22 ngOnInit () {
23 // Parent get the account for us
24 this.accountSub = this.accountService.accountLoaded
25 .subscribe(async account => {
26 this.account = account
27 this.descriptionHTML = await this.markdownService.textMarkdownToHTML(this.account.description, true)
28 })
29 }
30
31 ngOnDestroy () {
32 if (this.accountSub) this.accountSub.unsubscribe()
33 }
34
35 getAccountDescription () {
36 if (this.descriptionHTML) return this.descriptionHTML
37
38 return $localize`No description`
39 }
40}
diff --git a/client/src/app/+accounts/account-search/account-search.component.ts b/client/src/app/+accounts/account-search/account-search.component.ts
index dda4bf0c7..f54ab846a 100644
--- a/client/src/app/+accounts/account-search/account-search.component.ts
+++ b/client/src/app/+accounts/account-search/account-search.component.ts
@@ -64,9 +64,14 @@ export class AccountSearchComponent extends AbstractVideoList implements OnInit,
64 } 64 }
65 65
66 updateSearch (value: string) { 66 updateSearch (value: string) {
67 if (value === '') this.router.navigate(['../videos'], { relativeTo: this.route })
68 this.search = value 67 this.search = value
69 68
69 if (!this.search) {
70 this.router.navigate([ '../videos' ], { relativeTo: this.route })
71 return
72 }
73
74 this.videos = []
70 this.reloadVideos() 75 this.reloadVideos()
71 } 76 }
72 77
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
index 5dbb341d2..19a4b3c9c 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
@@ -1,33 +1,50 @@
1<h1 class="sr-only" i18n>Video channels</h1> 1<h1 class="sr-only" i18n>Video channels</h1>
2
2<div class="margin-content"> 3<div class="margin-content">
3 4
4 <div class="no-results" i18n *ngIf="channelPagination.totalItems === 0">This account does not have channels.</div> 5 <div class="no-results" i18n *ngIf="channelPagination.totalItems === 0">This account does not have channels.</div>
5 6
6 <div class="channels" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onChannelDataSubject.asObservable()"> 7 <div class="channels" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onChannelDataSubject.asObservable()">
7 <div class="section channel" *ngFor="let videoChannel of videoChannels"> 8 <div class="channel" *ngFor="let videoChannel of videoChannels">
8 <div class="section-title"> 9
9 <a [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel"> 10 <div class="channel-avatar-row">
11 <a class="avatar-link" [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel">
10 <img [src]="videoChannel.avatarUrl" alt="Avatar" /> 12 <img [src]="videoChannel.avatarUrl" alt="Avatar" />
13 </a>
11 14
12 <h2 class="section-title">{{ videoChannel.displayName }}</h2> 15 <h2>
16 <a [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel">
17 {{ videoChannel.displayName }}
18 </a>
19 </h2>
20
21 <div class="actor-counters">
13 <div class="followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> 22 <div class="followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
14 </a>
15 23
16 <my-subscribe-button [videoChannels]="[videoChannel]"></my-subscribe-button> 24 <span class="videos-count" *ngIf="getTotalVideosOf(videoChannel) !== undefined" i18n>
25 {getTotalVideosOf(videoChannel), plural, =1 {1 videos} other {{{ getTotalVideosOf(videoChannel) }} videos}}
26 </span>
27 </div>
28
29 <div class="description-html" [innerHTML]="getChannelDescription(videoChannel)"></div>
17 </div> 30 </div>
18 31
19 <div *ngIf="getVideosOf(videoChannel)" class="videos"> 32 <my-subscribe-button [videoChannels]="[videoChannel]"></my-subscribe-button>
20 <div class="no-results my-5" i18n *ngIf="getVideosOf(videoChannel).length === 0">This channel doesn't have any videos.</div> 33
34 <a i18n class="button-show-channel peertube-button-link orange-button-inverted" [routerLink]="getVideoChannelLink(videoChannel)">Show this channel</a>
35
36 <div class="videos">
37 <div class="no-results" i18n *ngIf="getTotalVideosOf(videoChannel) === 0">This channel doesn't have any videos.</div>
21 38
22 <my-video-miniature 39 <my-video-miniature
23 *ngFor="let video of getVideosOf(videoChannel)" 40 *ngFor="let video of getVideosOf(videoChannel)"
24 [video]="video" [user]="userMiniature" [displayVideoActions]="true" 41 [video]="video" [user]="userMiniature" [displayVideoActions]="true" [displayOptions]="miniatureDisplayOptions"
25 ></my-video-miniature> 42 ></my-video-miniature>
26 </div>
27 43
28 <a *ngIf="getVideosOf(videoChannel).length !== 0" class="show-more" i18n [routerLink]="getVideoChannelLink(videoChannel)"> 44 <div *ngIf="getTotalVideosOf(videoChannel)" class="miniature-show-channel">
29 SHOW THIS CHANNEL 45 <a i18n [routerLink]="getVideoChannelLink(videoChannel)">SHOW THIS CHANNEL ></a>
30 </a> 46 </div>
47 </div>
31 </div> 48 </div>
32 </div> 49 </div>
33</div> 50</div>
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
index 4957e91d7..7e88802f3 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
@@ -3,37 +3,175 @@
3@import '_miniature'; 3@import '_miniature';
4 4
5.margin-content { 5.margin-content {
6 @include fluid-videos-miniature-layout; 6 @include grid-videos-miniature-margins;
7} 7}
8 8
9.section { 9.channel {
10 @include miniature-rows; 10 max-width: $max-channels-width;
11 background-color: pvar(--channelBackgroundColor);
12 padding: 30px;
11 13
12 padding-top: 0 !important; 14 margin: 30px 0;
13 15
14 .section-title { 16 display: grid;
17 grid-template-columns: 1fr auto;
18 grid-template-rows: auto auto;
19 column-gap: 15px;
20}
21
22.channel-avatar-row {
23 grid-column: 1;
24 grid-row: 1;
25
26 display: grid;
27 grid-template-columns: auto auto 1fr;
28 grid-template-rows: auto 1fr;
29
30 .avatar-link {
31 grid-column: 1;
32 grid-row: 1 / 3;
33 margin-right: 30px;
34 }
35
36 img {
37 @include channel-avatar(75px);
38 }
39
40 a {
41 color: pvar(--mainForegroundColor);
42 }
43
44 h2 {
45 grid-row: 1;
46 grid-column: 2;
47 font-size: 20px;
48 line-height: 1;
49 font-weight: $font-bold;
50 margin: 0;
51 }
52
53 .actor-counters {
54 grid-row: 1;
55 grid-column: 3;
56 color: pvar(--greyForegroundColor);
57 font-size: 16px;
58 display: flex;
15 align-items: center; 59 align-items: center;
60 margin-left: 15px;
16 } 61 }
17 62
18 .videos { 63 .actor-counters > *:not(:last-child)::after {
19 overflow: hidden; 64 content: '•';
65 margin: 0 10px;
66 color: pvar(--mainColor);
67 }
20 68
21 .no-results { 69 .description-html {
22 height: 50px; 70 grid-column: 2 / 4;
23 } 71 grid-row: 2;
72
73 max-height: 80px;
74 font-size: 16px;
75
76 @include fade-text(30px, pvar(--channelBackgroundColor));
77 }
78}
79
80my-subscribe-button {
81 grid-row: 1;
82 grid-column: 2;
83}
84
85.videos {
86 display: flex;
87 grid-column: 1 / 3;
88 grid-row: 2;
89 margin-top: 30px;
90
91 position: relative;
92 overflow: hidden;
93
94 my-video-miniature {
95 margin-right: 15px;
96 min-width: $video-thumbnail-medium-width;
97 max-width: $video-thumbnail-medium-width;
98 }
99
100 .no-results {
101 height: auto;
24 } 102 }
103}
25 104
26 my-video-miniature ::ng-deep my-video-actions-dropdown > my-action-dropdown { 105.miniature-show-channel {
27 // Fix our overflow 106 height: 100%;
28 position: absolute; 107 position: absolute;
108 right: 0;
109 background: linear-gradient(90deg, transparent 0, pvar(--channelBackgroundColor) 45px);
110 padding: ($video-thumbnail-medium-height / 2 - 10px) 15px 0 60px;
111 z-index: z(miniature) + 1;
112
113 a {
114 color: pvar(--mainColor);
115 font-size: 16px;
116 font-weight: $font-semibold;
29 } 117 }
30} 118}
31 119
120.button-show-channel {
121 display: none;
122}
123
32@media screen and (max-width: $mobile-view) { 124@media screen and (max-width: $mobile-view) {
33 .section { 125 .channel {
34 .section-title { 126 padding: 15px;
35 flex-direction: column; 127 }
36 align-items: normal; 128
129 .channel-avatar-row {
130 grid-template-columns: auto auto auto 1fr;
131
132 .avatar-link {
133 grid-row: 1 / 4;
134 }
135
136 h2 {
137 font-size: 16px;
37 } 138 }
139
140 .actor-counters {
141 margin: 0;
142 font-size: 13px;
143 grid-row: 2;
144 grid-column: 2 / 4;
145 }
146
147 .description-html {
148 grid-row: 3;
149 font-size: 14px;
150 }
151 }
152
153 .show-channel a {
154 @include peertube-button-link;
155 @include orange-button-inverted;
156 }
157
158 .videos {
159 display: none;
160 }
161
162 my-subscribe-button,
163 .button-show-channel {
164 grid-column: 1 / 4;
165 grid-row: 3;
166 margin-top: 15px;
167 }
168
169 my-subscribe-button {
170 justify-self: start;
171 }
172
173 .button-show-channel {
174 display: block;
175 justify-self: end;
38 } 176 }
39} 177}
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
index f2beb6689..0628c7a96 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
@@ -1,9 +1,10 @@
1import { from, Subject, Subscription } from 'rxjs' 1import { from, Subject, Subscription } from 'rxjs'
2import { concatMap, map, switchMap, tap } from 'rxjs/operators' 2import { concatMap, map, switchMap, tap } from 'rxjs/operators'
3import { Component, OnDestroy, OnInit } from '@angular/core' 3import { Component, OnDestroy, OnInit } from '@angular/core'
4import { ComponentPagination, hasMoreItems, ScreenService, User, UserService } from '@app/core' 4import { ComponentPagination, hasMoreItems, MarkdownService, ScreenService, User, UserService } from '@app/core'
5import { Account, AccountService, Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' 5import { Account, AccountService, Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
6import { NSFWPolicyType, VideoSortField } from '@shared/models' 6import { NSFWPolicyType, VideoSortField } from '@shared/models'
7import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature'
7 8
8@Component({ 9@Component({
9 selector: 'my-account-video-channels', 10 selector: 'my-account-video-channels',
@@ -13,7 +14,10 @@ import { NSFWPolicyType, VideoSortField } from '@shared/models'
13export class AccountVideoChannelsComponent implements OnInit, OnDestroy { 14export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
14 account: Account 15 account: Account
15 videoChannels: VideoChannel[] = [] 16 videoChannels: VideoChannel[] = []
16 videos: { [id: number]: Video[] } = {} 17
18 videos: { [id: number]: { total: number, videos: Video[] } } = {}
19
20 channelsDescriptionHTML: { [ id: number ]: string } = {}
17 21
18 channelPagination: ComponentPagination = { 22 channelPagination: ComponentPagination = {
19 currentPage: 1, 23 currentPage: 1,
@@ -23,7 +27,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
23 27
24 videosPagination: ComponentPagination = { 28 videosPagination: ComponentPagination = {
25 currentPage: 1, 29 currentPage: 1,
26 itemsPerPage: 12, 30 itemsPerPage: 5,
27 totalItems: null 31 totalItems: null
28 } 32 }
29 videosSort: VideoSortField = '-publishedAt' 33 videosSort: VideoSortField = '-publishedAt'
@@ -32,6 +36,16 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
32 36
33 userMiniature: User 37 userMiniature: User
34 nsfwPolicy: NSFWPolicyType 38 nsfwPolicy: NSFWPolicyType
39 miniatureDisplayOptions: MiniatureDisplayOptions = {
40 date: true,
41 views: true,
42 by: false,
43 avatar: false,
44 privacyLabel: false,
45 privacyText: false,
46 state: false,
47 blacklistInfo: false
48 }
35 49
36 private accountSub: Subscription 50 private accountSub: Subscription
37 51
@@ -39,7 +53,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
39 private accountService: AccountService, 53 private accountService: AccountService,
40 private videoChannelService: VideoChannelService, 54 private videoChannelService: VideoChannelService,
41 private videoService: VideoService, 55 private videoService: VideoService,
42 private screenService: ScreenService, 56 private markdown: MarkdownService,
43 private userService: UserService 57 private userService: UserService
44 ) { } 58 ) { }
45 59
@@ -78,23 +92,36 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
78 } 92 }
79 93
80 return this.videoService.getVideoChannelVideos(options) 94 return this.videoService.getVideoChannelVideos(options)
81 .pipe(map(data => ({ videoChannel, videos: data.data }))) 95 .pipe(map(data => ({ videoChannel, videos: data.data, total: data.total })))
82 }) 96 })
83 ) 97 )
84 .subscribe(({ videoChannel, videos }) => { 98 .subscribe(async ({ videoChannel, videos, total }) => {
99 this.channelsDescriptionHTML[videoChannel.id] = await this.markdown.textMarkdownToHTML(videoChannel.description)
100
85 this.videoChannels.push(videoChannel) 101 this.videoChannels.push(videoChannel)
86 102
87 this.videos[videoChannel.id] = videos 103 this.videos[videoChannel.id] = { videos, total }
88 104
89 this.onChannelDataSubject.next([ videoChannel ]) 105 this.onChannelDataSubject.next([ videoChannel ])
90 }) 106 })
91 } 107 }
92 108
93 getVideosOf (videoChannel: VideoChannel) { 109 getVideosOf (videoChannel: VideoChannel) {
94 const numberOfVideos = this.screenService.getNumberOfAvailableMiniatures() 110 const obj = this.videos[ videoChannel.id ]
111 if (!obj) return []
112
113 return obj.videos
114 }
115
116 getTotalVideosOf (videoChannel: VideoChannel) {
117 const obj = this.videos[ videoChannel.id ]
118 if (!obj) return undefined
119
120 return obj.total
121 }
95 122
96 // 2 rows 123 getChannelDescription (videoChannel: VideoChannel) {
97 return this.videos[ videoChannel.id ].slice(0, numberOfVideos * 2) 124 return this.channelsDescriptionHTML[videoChannel.id]
98 } 125 }
99 126
100 onNearOfBottom () { 127 onNearOfBottom () {
diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts
index 484d60e25..75af45e90 100644
--- a/client/src/app/+accounts/account-videos/account-videos.component.ts
+++ b/client/src/app/+accounts/account-videos/account-videos.component.ts
@@ -16,6 +16,7 @@ import { VideoFilter } from '@shared/models'
16 ] 16 ]
17}) 17})
18export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { 18export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
19 // No value because we don't want a page title
19 titlePage: string 20 titlePage: string
20 loadOnInit = false 21 loadOnInit = false
21 loadUserVideoPreferences = true 22 loadUserVideoPreferences = true
@@ -77,11 +78,6 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
77 78
78 return this.videoService 79 return this.videoService
79 .getAccountVideos(options) 80 .getAccountVideos(options)
80 .pipe(
81 tap(({ total }) => {
82 this.titlePage = $localize`Published ${total} videos`
83 })
84 )
85 } 81 }
86 82
87 toggleModerationDisplay () { 83 toggleModerationDisplay () {
@@ -93,4 +89,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
93 generateSyndicationList () { 89 generateSyndicationList () {
94 this.syndicationItems = this.videoService.getAccountFeedUrls(this.account.id) 90 this.syndicationItems = this.videoService.getAccountFeedUrls(this.account.id)
95 } 91 }
92
93 displayAsRow () {
94 return this.screenService.isInMobileView()
95 }
96} 96}
diff --git a/client/src/app/+accounts/accounts-routing.module.ts b/client/src/app/+accounts/accounts-routing.module.ts
index 15937a67b..3bf0f7185 100644
--- a/client/src/app/+accounts/accounts-routing.module.ts
+++ b/client/src/app/+accounts/accounts-routing.module.ts
@@ -1,11 +1,10 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core' 3import { MetaGuard } from '@ngx-meta/core'
4import { AccountsComponent } from './accounts.component'
5import { AccountVideosComponent } from './account-videos/account-videos.component'
6import { AccountAboutComponent } from './account-about/account-about.component'
7import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
8import { AccountSearchComponent } from './account-search/account-search.component' 4import { AccountSearchComponent } from './account-search/account-search.component'
5import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
6import { AccountVideosComponent } from './account-videos/account-videos.component'
7import { AccountsComponent } from './accounts.component'
9 8
10const accountsRoutes: Routes = [ 9const accountsRoutes: Routes = [
11 { 10 {
@@ -32,15 +31,6 @@ const accountsRoutes: Routes = [
32 } 31 }
33 }, 32 },
34 { 33 {
35 path: 'about',
36 component: AccountAboutComponent,
37 data: {
38 meta: {
39 title: $localize`About account`
40 }
41 }
42 },
43 {
44 path: 'videos', 34 path: 'videos',
45 component: AccountVideosComponent, 35 component: AccountVideosComponent,
46 data: { 36 data: {
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index 5bd7b0824..ea7a317eb 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -1,57 +1,87 @@
1<div *ngIf="account" class="row"> 1<div *ngIf="account" class="root">
2 <div class="sub-menu"> 2 <div class="account-info">
3 3
4 <div class="actor"> 4 <div class="account-avatar-row">
5 <img [src]="account.avatarUrl" alt="Avatar" /> 5 <my-account-avatar [account]="account" size="120"></my-account-avatar>
6 6
7 <div class="actor-info"> 7 <div>
8 <div class="actor-names"> 8 <div class="section-label" i18n>PEERTUBE ACCOUNT</div>
9 <div class="actor-display-name">{{ account.displayName }}</div> 9
10 <div class="actor-name"> 10 <div class="actor-info">
11 <span>{{ account.nameWithHost }}</span> 11 <div>
12 <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()" 12 <div class="actor-display-name">
13 class="btn btn-outline-secondary btn-sm copy-button" 13 <h1 i18n-title [title]="'Created on ' + (account.createdAt | date)">{{ account.displayName }}</h1>
14 > 14
15 <span class="glyphicon glyphicon-copy"></span> 15 <my-user-moderation-dropdown
16 </button> 16 [prependActions]="prependModerationActions"
17 buttonSize="small" [account]="account" [user]="accountUser" placement="bottom-left auto"
18 (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
19 ></my-user-moderation-dropdown>
20
21 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
22 <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
23 <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span>
24 <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
25 <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
26 </div>
27
28 <div class="actor-handle">
29 <span>@{{ account.nameWithHost }}</span>
30 <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
31 class="btn btn-outline-secondary btn-sm copy-button" title="Copy account handle" i18n-title
32 >
33 <span class="glyphicon glyphicon-duplicate"></span>
34 </button>
35 </div>
36
37 <div class="actor-counters">
38 <span i18n>{naiveAggregatedSubscribers(), plural, =1 {1 subscriber} other {{{ naiveAggregatedSubscribers() }} subscribers}}</span>
39
40 <span class="videos-count" *ngIf="accountVideosCount !== undefined" i18n>
41 {accountVideosCount, plural, =1 {1 videos} other {{{ accountVideosCount }} videos}}
42 </span>
43 </div>
17 </div> 44 </div>
18 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
19 <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
20 <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span>
21 <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
22 <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
23
24 <my-user-moderation-dropdown
25 [prependActions]="prependModerationActions"
26 buttonSize="small" [account]="account" [user]="accountUser" placement="bottom-left auto"
27 (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
28 ></my-user-moderation-dropdown>
29 </div>
30 <div class="actor-followers" [title]="accountFollowerTitle">
31 {{ subscribersDisplayFor(naiveAggregatedSubscribers) }}
32 </div> 45 </div>
33 </div> 46 </div>
47 </div>
34 48
35 <div class="right-buttons"> 49 <div class="description" [ngClass]="{ expanded: accountDescriptionExpanded }">
36 <a *ngIf="isAccountManageable && !isInSmallView" routerLink="/my-account" class="btn btn-outline-tertiary mr-2" i18n>Manage account</a> 50 <div class="description-html" [innerHTML]="accountDescriptionHTML"></div>
37 <my-subscribe-button *ngIf="videoChannels" [account]="account" [videoChannels]="videoChannels"></my-subscribe-button>
38 </div>
39 </div> 51 </div>
40 52
41 <div class="links w-100"> 53 <div *ngIf="hasShowMoreDescription()" class="show-more" role="button"
42 <ng-template #linkTemplate let-item="item"> 54 (click)="accountDescriptionExpanded = !accountDescriptionExpanded"
43 <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> 55 title="Show the complete description" i18n-title i18n
44 </ng-template> 56 >
57 Show more...
58 </div>
45 59
46 <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> 60 <div class="buttons">
61 <a *ngIf="isManageable()" routerLink="/my-account" class="peertube-button-link orange-button" i18n>
62 Manage account
63 </a>
47 64
48 <simple-search-input (searchChanged)="searchChanged($event)" name="search-videos" i18n-placeholder placeholder="Search videos"></simple-search-input> 65 <my-subscribe-button *ngIf="hasVideoChannels() && !isManageable()" [account]="account" [videoChannels]="videoChannels"></my-subscribe-button>
49 </div> 66 </div>
50 </div> 67 </div>
51 68
52 <div class="margin-content"> 69 <div class="links">
53 <router-outlet (activate)="onOutletLoaded($event)"></router-outlet> 70 <ng-template #linkTemplate let-item="item">
71 <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
72 </ng-template>
73
74 <list-overflow [hidden]="hideMenu" [items]="links" [itemTemplate]="linkTemplate"></list-overflow>
75
76 <simple-search-input
77 [alwaysShow]="!isInSmallView()" (searchChanged)="searchChanged($event)"
78 (inputDisplayChanged)="onSearchInputDisplayChanged($event)" name="search-videos"
79 i18n-iconTitle icon-title="Search account videos"
80 i18n-placeholder placeholder="Search account videos"
81 ></simple-search-input>
54 </div> 82 </div>
83
84 <router-outlet (activate)="onOutletLoaded($event)"></router-outlet>
55</div> 85</div>
56 86
57<ng-container *ngIf="prependModerationActions"> 87<ng-container *ngIf="prependModerationActions">
diff --git a/client/src/app/+accounts/accounts.component.scss b/client/src/app/+accounts/accounts.component.scss
index 40c6b6493..56927dea6 100644
--- a/client/src/app/+accounts/accounts.component.scss
+++ b/client/src/app/+accounts/accounts.component.scss
@@ -1,48 +1,29 @@
1// Bootstrap grid utilities require functions, variables and mixins
2@import 'node_modules/bootstrap/scss/functions';
3@import 'node_modules/bootstrap/scss/variables';
4@import 'node_modules/bootstrap/scss/mixins';
5@import 'node_modules/bootstrap/scss/grid';
6
7@import '_variables'; 1@import '_variables';
8@import '_mixins'; 2@import '_mixins';
9 3@import '_actor';
10.sub-menu { 4@import '_miniature';
11 @include sub-menu-with-actor; 5
12 6.root {
13 .actor { 7 --myGlobalTopPadding: 60px;
14 width: 100%; 8 --myImgMargin: 30px;
15 } 9 --myFontSize: 16px;
10 --myGreyFontSize: 16px;
16} 11}
17 12
18.margin-content { 13.section-label {
19 // margin-content is required, but child views have their own margins 14 @include section-label-responsive;
20 // that match views outside the scope of accounts, so we only align
21 // them with the margins of .sub-menu when required.
22 margin: 0;
23} 15}
24 16
25.right-buttons { 17.links {
26 display: flex; 18 @include grid-videos-miniature-margins;
27 height: max-content;
28 margin-left: auto;
29 margin-top: 10px;
30
31 @include media-breakpoint-down(lg) {
32 flex-flow: column-reverse;
33 19
34 a { 20 display: flex;
35 margin-top: 0.25rem; 21 justify-content: space-between;
36 margin-right: 0 !important; 22 align-items: center;
37 } 23 max-width: $max-channels-width;
38 }
39
40 a {
41 @include peertube-button-outline;
42 }
43 24
44 my-subscribe-button { 25 simple-search-input {
45 min-height: 30px; 26 margin-left: auto;
46 } 27 }
47} 28}
48 29
@@ -60,39 +41,101 @@ my-user-moderation-dropdown,
60 41
61.copy-button { 42.copy-button {
62 border: none; 43 border: none;
63 padding: 5px; 44}
64 margin-top: -2px; 45
46.account-info {
47 @include grid-videos-miniature-margins(false, 15px);
48
49 display: grid;
50 grid-template-columns: 1fr min-content;
51 grid-template-rows: auto auto;
52
53 background-color: pvar(--submenuBackgroundColor);
54 margin-bottom: 45px;
55 padding-top: var(--myGlobalTopPadding);
56 padding-bottom: var(--myGlobalTopPadding);
57 font-size: var(--myFontSize);
58}
59
60.account-avatar-row {
61 @include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize));
62}
63
64.description {
65 grid-column: 1 / 3;
66 max-width: 1000px;
67 word-break: break-word;
68}
69
70.show-more {
71 @include show-more-description;
72
73 display: none;
74 text-align: center;
75}
76
77.buttons {
78 grid-column: 2;
79 grid-row: 1;
80
81 display: flex;
82 flex-wrap: wrap;
83 justify-content: flex-end;
84 align-content: flex-start;
85
86 > *:not(:last-child) {
87 margin-bottom: 15px;
88 }
89
90 > a {
91 white-space: nowrap;
92 }
93}
94
95@media screen and (max-width: $small-view) {
96 .root {
97 --myGlobalTopPadding: 45px;
98 --myChannelImgMargin: 15px;
99 }
100
101 .account-info {
102 display: block;
103 padding-bottom: 60px;
104 }
105
106 .description:not(.expanded) {
107 max-height: 70px;
108
109 @include fade-text(30px, pvar(--submenuBackgroundColor));
110 }
111
112 .show-more {
113 display: block;
114 }
115
116 .buttons {
117 justify-content: center;
118 }
65} 119}
66 120
67@media screen and (max-width: $mobile-view) { 121@media screen and (max-width: $mobile-view) {
68 .sub-menu { 122 .root {
69 .actor { 123 --myGlobalTopPadding: 15px;
70 flex-direction: column; 124 --myFontSize: 14px;
71 align-items: center; 125 --myGreyFontSize: 13px;
72 126 }
73 img, 127
74 .actor-info .actor-names .actor-display-name { 128 .account-info {
75 margin-right: 0; 129 display: block;
76 } 130 padding-bottom: 30px;
77 131 }
78 .actor-info { 132
79 .actor-names { 133 .links {
80 flex-direction: column; 134 margin: auto !important;
81 align-items: center; 135 width: min-content;
82 } 136 }
83 137
84 my-user-moderation-dropdown { 138 .show-more {
85 margin-left: 0; 139 margin-bottom: 30px;
86 }
87
88 .actor-followers {
89 text-align: center;
90 }
91 }
92
93 .right-buttons {
94 margin-left: 0;
95 }
96 }
97 } 140 }
98} 141}
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts
index e6a5a5d5e..fbd7380a9 100644
--- a/client/src/app/+accounts/accounts.component.ts
+++ b/client/src/app/+accounts/accounts.component.ts
@@ -2,11 +2,19 @@ import { Subscription } from 'rxjs'
2import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators' 2import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'
3import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' 3import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
4import { ActivatedRoute } from '@angular/router' 4import { ActivatedRoute } from '@angular/router'
5import { AuthService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core' 5import { AuthService, MarkdownService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core'
6import { Account, AccountService, DropdownAction, ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main' 6import {
7 Account,
8 AccountService,
9 DropdownAction,
10 ListOverflowItem,
11 VideoChannel,
12 VideoChannelService,
13 VideoService
14} from '@app/shared/shared-main'
7import { AccountReportComponent } from '@app/shared/shared-moderation' 15import { AccountReportComponent } from '@app/shared/shared-moderation'
8import { User, UserRight } from '@shared/models'
9import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 16import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
17import { User, UserRight } from '@shared/models'
10import { AccountSearchComponent } from './account-search/account-search.component' 18import { AccountSearchComponent } from './account-search/account-search.component'
11 19
12@Component({ 20@Component({
@@ -15,16 +23,23 @@ import { AccountSearchComponent } from './account-search/account-search.componen
15}) 23})
16export class AccountsComponent implements OnInit, OnDestroy { 24export class AccountsComponent implements OnInit, OnDestroy {
17 @ViewChild('accountReportModal') accountReportModal: AccountReportComponent 25 @ViewChild('accountReportModal') accountReportModal: AccountReportComponent
26
18 accountSearch: AccountSearchComponent 27 accountSearch: AccountSearchComponent
19 28
20 account: Account 29 account: Account
21 accountUser: User 30 accountUser: User
31
22 videoChannels: VideoChannel[] = [] 32 videoChannels: VideoChannel[] = []
33
23 links: ListOverflowItem[] = [] 34 links: ListOverflowItem[] = []
35 hideMenu = false
24 36
25 isAccountManageable = false
26 accountFollowerTitle = '' 37 accountFollowerTitle = ''
27 38
39 accountVideosCount: number
40 accountDescriptionHTML = ''
41 accountDescriptionExpanded = false
42
28 prependModerationActions: DropdownAction<any>[] 43 prependModerationActions: DropdownAction<any>[]
29 44
30 private routeSub: Subscription 45 private routeSub: Subscription
@@ -38,6 +53,8 @@ export class AccountsComponent implements OnInit, OnDestroy {
38 private restExtractor: RestExtractor, 53 private restExtractor: RestExtractor,
39 private redirectService: RedirectService, 54 private redirectService: RedirectService,
40 private authService: AuthService, 55 private authService: AuthService,
56 private videoService: VideoService,
57 private markdown: MarkdownService,
41 private screenService: ScreenService 58 private screenService: ScreenService
42 ) { 59 ) {
43 } 60 }
@@ -62,9 +79,8 @@ export class AccountsComponent implements OnInit, OnDestroy {
62 ) 79 )
63 80
64 this.links = [ 81 this.links = [
65 { label: $localize`VIDEO CHANNELS`, routerLink: 'video-channels' }, 82 { label: $localize`CHANNELS`, routerLink: 'video-channels' },
66 { label: $localize`VIDEOS`, routerLink: 'videos' }, 83 { label: $localize`VIDEOS`, routerLink: 'videos' }
67 { label: $localize`ABOUT`, routerLink: 'about' }
68 ] 84 ]
69 } 85 }
70 86
@@ -72,19 +88,29 @@ export class AccountsComponent implements OnInit, OnDestroy {
72 if (this.routeSub) this.routeSub.unsubscribe() 88 if (this.routeSub) this.routeSub.unsubscribe()
73 } 89 }
74 90
75 get naiveAggregatedSubscribers () { 91 naiveAggregatedSubscribers () {
76 return this.videoChannels.reduce( 92 return this.videoChannels.reduce(
77 (acc, val) => acc + val.followersCount, 93 (acc, val) => acc + val.followersCount,
78 this.account.followersCount // accumulator starts with the base number of subscribers the account has 94 this.account.followersCount // accumulator starts with the base number of subscribers the account has
79 ) 95 )
80 } 96 }
81 97
82 get isInSmallView () { 98 isUserLoggedIn () {
99 return this.authService.isLoggedIn()
100 }
101
102 isInSmallView () {
83 return this.screenService.isInSmallView() 103 return this.screenService.isInSmallView()
84 } 104 }
85 105
106 isManageable () {
107 if (!this.isUserLoggedIn()) return false
108
109 return this.account?.userId === this.authService.getUser().id
110 }
111
86 onUserChanged () { 112 onUserChanged () {
87 this.getUserIfNeeded(this.account) 113 this.loadUserIfNeeded(this.account)
88 } 114 }
89 115
90 onUserDeleted () { 116 onUserDeleted () {
@@ -113,40 +139,38 @@ export class AccountsComponent implements OnInit, OnDestroy {
113 if (this.accountSearch) this.accountSearch.updateSearch(search) 139 if (this.accountSearch) this.accountSearch.updateSearch(search)
114 } 140 }
115 141
116 private onAccount (account: Account) { 142 onSearchInputDisplayChanged (displayed: boolean) {
143 this.hideMenu = this.isInSmallView() && displayed
144 }
145
146 hasVideoChannels () {
147 return this.videoChannels.length !== 0
148 }
149
150 hasShowMoreDescription () {
151 return !this.accountDescriptionExpanded && this.accountDescriptionHTML.length > 100
152 }
153
154 private async onAccount (account: Account) {
155 this.accountFollowerTitle = $localize`${account.followersCount} direct account followers`
156
117 this.prependModerationActions = undefined 157 this.prependModerationActions = undefined
118 158
119 this.account = account 159 this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML(account.description)
120 160
121 if (this.authService.isLoggedIn()) { 161 // After the markdown renderer to avoid layout changes
122 this.authService.userInformationLoaded.subscribe( 162 this.account = account
123 () => {
124 this.isAccountManageable = this.account.userId && this.account.userId === this.authService.getUser().id
125
126 const followers = this.subscribersDisplayFor(account.followersCount)
127 this.accountFollowerTitle = $localize`${followers} direct account followers`
128
129 // It's not our account, we can report it
130 if (!this.isAccountManageable) {
131 this.prependModerationActions = [
132 {
133 label: $localize`Report this account`,
134 handler: () => this.showReportModal()
135 }
136 ]
137 }
138 }
139 )
140 }
141 163
142 this.getUserIfNeeded(account) 164 this.updateModerationActions()
165 this.loadUserIfNeeded(account)
166 this.loadAccountVideosCount()
143 } 167 }
144 168
145 private showReportModal () { 169 private showReportModal () {
146 this.accountReportModal.show() 170 this.accountReportModal.show()
147 } 171 }
148 172
149 private getUserIfNeeded (account: Account) { 173 private loadUserIfNeeded (account: Account) {
150 if (!account.userId || !this.authService.isLoggedIn()) return 174 if (!account.userId || !this.authService.isLoggedIn()) return
151 175
152 const user = this.authService.getUser() 176 const user = this.authService.getUser()
@@ -158,4 +182,33 @@ export class AccountsComponent implements OnInit, OnDestroy {
158 ) 182 )
159 } 183 }
160 } 184 }
185
186 private updateModerationActions () {
187 if (!this.authService.isLoggedIn()) return
188
189 this.authService.userInformationLoaded.subscribe(
190 () => {
191 if (this.isManageable()) return
192
193 // It's not our account, we can report it
194 this.prependModerationActions = [
195 {
196 label: $localize`Report this account`,
197 handler: () => this.showReportModal()
198 }
199 ]
200 }
201 )
202 }
203
204 private loadAccountVideosCount () {
205 this.videoService.getAccountVideos({
206 account: this.account,
207 videoPagination: {
208 currentPage: 1,
209 itemsPerPage: 0
210 },
211 sort: '-publishedAt'
212 }).subscribe(res => this.accountVideosCount = res.total)
213 }
161} 214}
diff --git a/client/src/app/+accounts/accounts.module.ts b/client/src/app/+accounts/accounts.module.ts
index 6da65cbc1..22cdd0642 100644
--- a/client/src/app/+accounts/accounts.module.ts
+++ b/client/src/app/+accounts/accounts.module.ts
@@ -5,12 +5,12 @@ import { SharedMainModule } from '@app/shared/shared-main'
5import { SharedModerationModule } from '@app/shared/shared-moderation' 5import { SharedModerationModule } from '@app/shared/shared-moderation'
6import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' 6import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
7import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' 7import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
8import { AccountAboutComponent } from './account-about/account-about.component' 8import { AccountSearchComponent } from './account-search/account-search.component'
9import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' 9import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
10import { AccountVideosComponent } from './account-videos/account-videos.component' 10import { AccountVideosComponent } from './account-videos/account-videos.component'
11import { AccountSearchComponent } from './account-search/account-search.component'
12import { AccountsRoutingModule } from './accounts-routing.module' 11import { AccountsRoutingModule } from './accounts-routing.module'
13import { AccountsComponent } from './accounts.component' 12import { AccountsComponent } from './accounts.component'
13import { SharedAccountAvatarModule } from '../shared/shared-account-avatar/shared-account-avatar.module'
14 14
15@NgModule({ 15@NgModule({
16 imports: [ 16 imports: [
@@ -21,14 +21,14 @@ import { AccountsComponent } from './accounts.component'
21 SharedUserSubscriptionModule, 21 SharedUserSubscriptionModule,
22 SharedModerationModule, 22 SharedModerationModule,
23 SharedVideoMiniatureModule, 23 SharedVideoMiniatureModule,
24 SharedGlobalIconModule 24 SharedGlobalIconModule,
25 SharedAccountAvatarModule
25 ], 26 ],
26 27
27 declarations: [ 28 declarations: [
28 AccountsComponent, 29 AccountsComponent,
29 AccountVideosComponent, 30 AccountVideosComponent,
30 AccountVideoChannelsComponent, 31 AccountVideoChannelsComponent,
31 AccountAboutComponent,
32 AccountSearchComponent 32 AccountSearchComponent
33 ], 33 ],
34 34