aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-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/accounts-routing.module.ts16
-rw-r--r--client/src/app/+accounts/accounts.component.html116
-rw-r--r--client/src/app/+accounts/accounts.component.scss176
-rw-r--r--client/src/app/+accounts/accounts.component.ts111
-rw-r--r--client/src/app/+accounts/accounts.module.ts4
-rw-r--r--client/src/app/+video-channels/video-channels.component.html2
-rw-r--r--client/src/app/+video-channels/video-channels.component.scss74
-rw-r--r--client/src/app/+video-channels/video-channels.component.ts2
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.html13
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.scss36
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.ts52
-rw-r--r--client/src/sass/include/_actor.scss86
16 files changed, 429 insertions, 333 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/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 1903bb36f..92d24ce94 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -1,57 +1,89 @@
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 <img class="account-avatar" [src]="account.avatarUrl" alt="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>{{ account.displayName }}</h1>
14 > 14
15 <span class="glyphicon glyphicon-duplicate"></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> 51
38 </div> 52 <div class="created-at" i18n>Account created on {{ account.createdAt | date }}</div>
39 </div> 53 </div>
40 54
41 <div class="links w-100"> 55 <div *ngIf="!accountDescriptionExpanded" class="show-more" role="button"
42 <ng-template #linkTemplate let-item="item"> 56 (click)="accountDescriptionExpanded = !accountDescriptionExpanded"
43 <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> 57 title="Show the complete description" i18n-title i18n
44 </ng-template> 58 >
59 Show more...
60 </div>
45 61
46 <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> 62 <div class="buttons">
63 <a *ngIf="isManageable() && !isInSmallView()" routerLink="/my-account" class="peertube-button-link orange-button" i18n>
64 Manage account
65 </a>
47 66
48 <simple-search-input (searchChanged)="searchChanged($event)" name="search-videos" i18n-placeholder placeholder="Search videos"></simple-search-input> 67 <my-subscribe-button *ngIf="videoChannels" [account]="account" [videoChannels]="videoChannels"></my-subscribe-button>
49 </div> 68 </div>
50 </div> 69 </div>
51 70
52 <div class="margin-content"> 71 <div class="links">
53 <router-outlet (activate)="onOutletLoaded($event)"></router-outlet> 72 <ng-template #linkTemplate let-item="item">
73 <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
74 </ng-template>
75
76 <list-overflow [hidden]="hideMenu" [items]="links" [itemTemplate]="linkTemplate"></list-overflow>
77
78 <simple-search-input
79 [alwaysShow]="!isInSmallView()" (searchChanged)="searchChanged($event)"
80 (inputDisplayChanged)="onSearchInputDisplayChanged($event)" name="search-videos"
81 i18n-iconTitle icon-title="Search account videos"
82 i18n-placeholder placeholder="Search account videos"
83 ></simple-search-input>
54 </div> 84 </div>
85
86 <router-outlet (activate)="onOutletLoaded($event)"></router-outlet>
55</div> 87</div>
56 88
57<ng-container *ngIf="prependModerationActions"> 89<ng-container *ngIf="prependModerationActions">
diff --git a/client/src/app/+accounts/accounts.component.scss b/client/src/app/+accounts/accounts.component.scss
index 40c6b6493..c1cf53f3a 100644
--- a/client/src/app/+accounts/accounts.component.scss
+++ b/client/src/app/+accounts/accounts.component.scss
@@ -1,49 +1,26 @@
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 --myGlobalPadding: 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 fluid-videos-miniature-layout;
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: 800px;
38 }
39
40 a {
41 @include peertube-button-outline;
42 }
43
44 my-subscribe-button {
45 min-height: 30px;
46 }
47} 24}
48 25
49my-user-moderation-dropdown, 26my-user-moderation-dropdown,
@@ -60,39 +37,98 @@ my-user-moderation-dropdown,
60 37
61.copy-button { 38.copy-button {
62 border: none; 39 border: none;
63 padding: 5px; 40}
64 margin-top: -2px; 41
42.account-info {
43 display: grid;
44 grid-template-columns: 1fr min-content;
45 grid-template-rows: auto auto;
46
47 background-color: pvar(--submenuColor);
48 margin-bottom: 45px;
49 padding: var(--myGlobalPadding) var(--myGlobalPadding) 0 var(--myGlobalPadding);
50 font-size: var(--myFontSize);
51}
52
53.account-avatar-row {
54 @include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize));
55}
56
57.description {
58 grid-column: 1 / 3;
59}
60
61.created-at {
62 margin-top: 15px;
63 color: pvar(--greyForegroundColor);
64 padding-bottom: 60px;
65}
66
67.show-more {
68 @include show-more-description;
69
70 display: none;
71 text-align: center;
72}
73
74.buttons {
75 grid-column: 2;
76 grid-row: 1;
77
78 display: flex;
79 flex-wrap: wrap;
80 justify-content: flex-end;
81 align-content: flex-start;
82
83 > *:not(:last-child) {
84 margin-bottom: 15px;
85 }
86}
87
88@media screen and (max-width: $small-view) {
89 .root {
90 --myGlobalPadding: 45px;
91 --myChannelImgMargin: 15px;
92 }
93
94 .account-info {
95 display: block;
96 padding-bottom: 60px;
97 }
98
99 .description:not(.expanded) {
100 max-height: 70px;
101
102 @include fade-text(30px, pvar(--submenuColor));
103 }
104
105 .show-more {
106 display: block;
107 }
108
109 .buttons {
110 justify-content: center;
111 }
65} 112}
66 113
67@media screen and (max-width: $mobile-view) { 114@media screen and (max-width: $mobile-view) {
68 .sub-menu { 115 .root {
69 .actor { 116 --myGlobalPadding: 15px;
70 flex-direction: column; 117 --myFontSize: 14px;
71 align-items: center; 118 --myGreyFontSize: 13px;
72 119 }
73 img, 120
74 .actor-info .actor-names .actor-display-name { 121 .account-info {
75 margin-right: 0; 122 display: block;
76 } 123 padding-bottom: 30px;
77 124 }
78 .actor-info { 125
79 .actor-names { 126 .links {
80 flex-direction: column; 127 margin: auto !important;
81 align-items: center; 128 width: min-content;
82 } 129 }
83 130
84 my-user-moderation-dropdown { 131 .show-more {
85 margin-left: 0; 132 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 } 133 }
98} 134}
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts
index e6a5a5d5e..a00063129 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 }
@@ -63,8 +80,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
63 80
64 this.links = [ 81 this.links = [
65 { label: $localize`VIDEO CHANNELS`, routerLink: 'video-channels' }, 82 { label: $localize`VIDEO 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,30 @@ 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 private async onAccount (account: Account) {
147 this.accountFollowerTitle = $localize`${account.followersCount} direct account followers`
148
117 this.prependModerationActions = undefined 149 this.prependModerationActions = undefined
118 150
119 this.account = account 151 this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML(account.description)
120 152
121 if (this.authService.isLoggedIn()) { 153 // After the markdown renderer to avoid layout changes
122 this.authService.userInformationLoaded.subscribe( 154 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 155
142 this.getUserIfNeeded(account) 156 this.updateModerationActions()
157 this.loadUserIfNeeded(account)
158 this.loadAccountVideosCount()
143 } 159 }
144 160
145 private showReportModal () { 161 private showReportModal () {
146 this.accountReportModal.show() 162 this.accountReportModal.show()
147 } 163 }
148 164
149 private getUserIfNeeded (account: Account) { 165 private loadUserIfNeeded (account: Account) {
150 if (!account.userId || !this.authService.isLoggedIn()) return 166 if (!account.userId || !this.authService.isLoggedIn()) return
151 167
152 const user = this.authService.getUser() 168 const user = this.authService.getUser()
@@ -158,4 +174,33 @@ export class AccountsComponent implements OnInit, OnDestroy {
158 ) 174 )
159 } 175 }
160 } 176 }
177
178 private updateModerationActions () {
179 if (!this.authService.isLoggedIn()) return
180
181 this.authService.userInformationLoaded.subscribe(
182 () => {
183 if (this.isManageable()) return
184
185 // It's not our account, we can report it
186 this.prependModerationActions = [
187 {
188 label: $localize`Report this account`,
189 handler: () => this.showReportModal()
190 }
191 ]
192 }
193 )
194 }
195
196 private loadAccountVideosCount () {
197 this.videoService.getAccountVideos({
198 account: this.account,
199 videoPagination: {
200 currentPage: 1,
201 itemsPerPage: 0
202 },
203 sort: '-publishedAt'
204 }).subscribe(res => this.accountVideosCount = res.total)
205 }
161} 206}
diff --git a/client/src/app/+accounts/accounts.module.ts b/client/src/app/+accounts/accounts.module.ts
index 6da65cbc1..3354b4189 100644
--- a/client/src/app/+accounts/accounts.module.ts
+++ b/client/src/app/+accounts/accounts.module.ts
@@ -5,10 +5,9 @@ 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'
14 13
@@ -28,7 +27,6 @@ import { AccountsComponent } from './accounts.component'
28 AccountsComponent, 27 AccountsComponent,
29 AccountVideosComponent, 28 AccountVideosComponent,
30 AccountVideoChannelsComponent, 29 AccountVideoChannelsComponent,
31 AccountAboutComponent,
32 AccountSearchComponent 30 AccountSearchComponent
33 ], 31 ],
34 32
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html
index f63110bf5..d1eb15dff 100644
--- a/client/src/app/+video-channels/video-channels.component.html
+++ b/client/src/app/+video-channels/video-channels.component.html
@@ -12,7 +12,7 @@
12 <ng-template #ownerTemplate> 12 <ng-template #ownerTemplate>
13 <div class="owner-block"> 13 <div class="owner-block">
14 <div class="avatar-row"> 14 <div class="avatar-row">
15 <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> 15 <img class="channel-avatar" [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
16 16
17 <div class="actor-info"> 17 <div class="actor-info">
18 <h4>{{ videoChannel.ownerAccount.displayName }}</h4> 18 <h4>{{ videoChannel.ownerAccount.displayName }}</h4>
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss
index 16e13c578..f5547b4e9 100644
--- a/client/src/app/+video-channels/video-channels.component.scss
+++ b/client/src/app/+video-channels/video-channels.component.scss
@@ -1,5 +1,6 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3@import '_actor';
3@import '_miniature'; 4@import '_miniature';
4 5
5.root { 6.root {
@@ -11,11 +12,7 @@
11} 12}
12 13
13.section-label { 14.section-label {
14 color: pvar(--mainColor); 15 @include section-label-responsive;
15 font-size: 12px;
16 margin-bottom: 15px;
17 font-weight: $font-bold;
18 letter-spacing: 2.5px;
19} 16}
20 17
21.links { 18.links {
@@ -34,48 +31,7 @@
34} 31}
35 32
36.channel-avatar-row { 33.channel-avatar-row {
37 display: flex; 34 @include avatar-row-responsive(var(--myChannelImgMargin), var(--myGreyChannelFontSize));
38 grid-column: 1;
39 margin-bottom: 30px;
40
41 img {
42 @include channel-avatar(120px);
43 }
44
45 > div {
46 margin-left: var(--myChannelImgMargin);
47 }
48
49 .actor-info {
50 display: flex;
51
52 > div:first-child {
53 flex-grow: 1;
54 }
55 }
56
57 .actor-display-name {
58 display: flex;
59 flex-wrap: wrap;
60 }
61
62 h1 {
63 font-size: 28px;
64 font-weight: $font-bold;
65 margin: 0;
66 }
67
68 .actor-handle,
69 .actor-counters {
70 color: pvar(--greyForegroundColor);
71 font-size: var(--myGreyChannelFontSize);
72 }
73
74 .actor-counters > *:not(:last-child)::after {
75 content: '•';
76 margin: 0 10px;
77 color: pvar(--mainColor);
78 }
79} 35}
80 36
81.channel-description { 37.channel-description {
@@ -83,13 +39,11 @@
83} 39}
84 40
85.show-more { 41.show-more {
42 @include show-more-description;
43
86 display: none; 44 display: none;
87 color: pvar(--mainColor);
88 cursor: pointer;
89 margin: 10px auto 45px auto;
90} 45}
91 46
92
93.channel-buttons { 47.channel-buttons {
94 display: flex; 48 display: flex;
95 flex-wrap: wrap; 49 flex-wrap: wrap;
@@ -280,24 +234,6 @@
280 width: min-content; 234 width: min-content;
281 } 235 }
282 236
283 .section-label {
284 font-size: 10px;
285 letter-spacing: 2.1px;
286 margin-bottom: 5px;
287 }
288
289 .channel-avatar-row {
290 margin-bottom: 15px;
291
292 h1 {
293 font-size: 22px;
294 }
295
296 img {
297 @include channel-avatar(80px);
298 }
299 }
300
301 .show-more { 237 .show-more {
302 margin-bottom: 30px; 238 margin-bottom: 30px;
303 } 239 }
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts
index 037c108f2..4fcc42103 100644
--- a/client/src/app/+video-channels/video-channels.component.ts
+++ b/client/src/app/+video-channels/video-channels.component.ts
@@ -94,7 +94,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
94 isManageable () { 94 isManageable () {
95 if (!this.isUserLoggedIn()) return false 95 if (!this.isUserLoggedIn()) return false
96 96
97 return this.videoChannel.ownerAccount.userId === this.authService.getUser().id 97 return this.videoChannel?.ownerAccount.userId === this.authService.getUser().id
98 } 98 }
99 99
100 activateCopiedMessage () { 100 activateCopiedMessage () {
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.html b/client/src/app/shared/shared-main/misc/simple-search-input.component.html
index fb0d97122..c20c02e23 100644
--- a/client/src/app/shared/shared-main/misc/simple-search-input.component.html
+++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.html
@@ -1,14 +1,15 @@
1<span> 1<div class="root">
2 <my-global-icon iconName="search" aria-label="Search" role="button" (click)="showInput()"></my-global-icon>
3
4 <input 2 <input
5 #ref 3 #ref
6 type="text" 4 type="text"
7 [(ngModel)]="value" 5 [(ngModel)]="value"
8 (focusout)="focusLost()"
9 (keyup.enter)="searchChange()" 6 (keyup.enter)="searchChange()"
10 [hidden]="!shown" 7 [hidden]="!inputShown"
11 [name]="name" 8 [name]="name"
12 [placeholder]="placeholder" 9 [placeholder]="placeholder"
13 > 10 >
14</span> 11
12 <my-global-icon iconName="search" aria-label="Search" role="button" (click)="onIconClick()" [title]="iconTitle"></my-global-icon>
13
14 <my-global-icon *ngIf="!alwaysShow && inputShown" i18n-title title="Close search" iconName="cross" (click)="hideInput()"></my-global-icon>
15</div>
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.scss b/client/src/app/shared/shared-main/misc/simple-search-input.component.scss
index 591b04fb2..037937f80 100644
--- a/client/src/app/shared/shared-main/misc/simple-search-input.component.scss
+++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.scss
@@ -1,29 +1,29 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4span { 4.root {
5 opacity: .6; 5 display: flex;
6
7 &:focus-within {
8 opacity: 1;
9 }
10} 6}
11 7
12my-global-icon { 8my-global-icon {
13 height: 18px; 9 height: 26px;
14 position: relative; 10 width: 26px;
15 top: -2px; 11 margin-left: 10px;
16} 12 cursor: pointer;
17 13
18input { 14 &:hover {
19 @include peertube-input-text(150px); 15 color: pvar(--mainHoverColor);
16 }
20 17
21 height: 22px; // maximum height for the account/video-channels links 18 &[iconName=search] {
22 padding-left: 10px; 19 color: pvar(--mainColor);
23 background-color: transparent; 20 }
24 border: none;
25 21
26 &::placeholder { 22 &[iconName=cross] {
27 font-size: 15px; 23 color: pvar(--mainForegroundColor);
28 } 24 }
29} 25}
26
27input {
28 @include peertube-input-text(200px);
29}
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.ts b/client/src/app/shared/shared-main/misc/simple-search-input.component.ts
index 86ae9ab42..224d71134 100644
--- a/client/src/app/shared/shared-main/misc/simple-search-input.component.ts
+++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.ts
@@ -1,7 +1,7 @@
1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { Subject } from 'rxjs' 1import { Subject } from 'rxjs'
4import { debounceTime, distinctUntilChanged } from 'rxjs/operators' 2import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
3import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router'
5 5
6@Component({ 6@Component({
7 selector: 'simple-search-input', 7 selector: 'simple-search-input',
@@ -13,11 +13,14 @@ export class SimpleSearchInputComponent implements OnInit {
13 13
14 @Input() name = 'search' 14 @Input() name = 'search'
15 @Input() placeholder = $localize`Search` 15 @Input() placeholder = $localize`Search`
16 @Input() iconTitle = $localize`Search`
17 @Input() alwaysShow = true
16 18
17 @Output() searchChanged = new EventEmitter<string>() 19 @Output() searchChanged = new EventEmitter<string>()
20 @Output() inputDisplayChanged = new EventEmitter<boolean>()
18 21
19 value = '' 22 value = ''
20 shown: boolean 23 inputShown: boolean
21 24
22 private searchSubject = new Subject<string>() 25 private searchSubject = new Subject<string>()
23 26
@@ -35,20 +38,51 @@ export class SimpleSearchInputComponent implements OnInit {
35 .subscribe(value => this.searchChanged.emit(value)) 38 .subscribe(value => this.searchChanged.emit(value))
36 39
37 this.searchSubject.next(this.value) 40 this.searchSubject.next(this.value)
41
42 if (this.isInputShown()) this.showInput(false)
38 } 43 }
39 44
40 showInput () { 45 isInputShown () {
41 this.shown = true 46 if (this.alwaysShow) return true
42 setTimeout(() => this.input.nativeElement.focus()) 47
48 return this.inputShown
49 }
50
51 onIconClick () {
52 if (!this.isInputShown()) {
53 this.showInput()
54 return
55 }
56
57 this.searchChange()
58 }
59
60 showInput (focus = true) {
61 this.inputShown = true
62 this.inputDisplayChanged.emit(this.inputShown)
63
64 if (focus) {
65 setTimeout(() => this.input.nativeElement.focus())
66 }
67 }
68
69 hideInput () {
70 this.inputShown = false
71
72 if (this.isInputShown() === false) {
73 this.inputDisplayChanged.emit(this.inputShown)
74 }
43 } 75 }
44 76
45 focusLost () { 77 focusLost () {
46 if (this.value !== '') return 78 if (this.value) return
47 this.shown = false 79
80 this.hideInput()
48 } 81 }
49 82
50 searchChange () { 83 searchChange () {
51 this.router.navigate(['./search'], { relativeTo: this.route }) 84 this.router.navigate([ './search' ], { relativeTo: this.route })
85
52 this.searchSubject.next(this.value) 86 this.searchSubject.next(this.value)
53 } 87 }
54} 88}
diff --git a/client/src/sass/include/_actor.scss b/client/src/sass/include/_actor.scss
new file mode 100644
index 000000000..5e9607318
--- /dev/null
+++ b/client/src/sass/include/_actor.scss
@@ -0,0 +1,86 @@
1@import '_variables';
2
3@mixin section-label-responsive {
4 color: pvar(--mainColor);
5 font-size: 12px;
6 margin-bottom: 15px;
7 font-weight: $font-bold;
8 letter-spacing: 2.5px;
9
10 @media screen and (max-width: $mobile-view) {
11 font-size: 10px;
12 letter-spacing: 2.1px;
13 margin-bottom: 5px;
14 }
15}
16
17@mixin show-more-description {
18 color: pvar(--mainColor);
19 cursor: pointer;
20 margin: 10px auto 45px auto;
21}
22
23@mixin avatar-row-responsive ($img-margin, $grey-font-size) {
24 display: flex;
25 grid-column: 1;
26 margin-bottom: 30px;
27
28 .channel-avatar {
29 @include channel-avatar(120px);
30 }
31
32 .account-avatar {
33 @include avatar(120px);
34 }
35
36 > div {
37 margin-left: $img-margin;
38 }
39
40 .actor-info {
41 display: flex;
42
43 > div:first-child {
44 flex-grow: 1;
45 }
46 }
47
48 .actor-display-name {
49 display: flex;
50 flex-wrap: wrap;
51 }
52
53 h1 {
54 font-size: 28px;
55 font-weight: $font-bold;
56 margin: 0;
57 }
58
59 .actor-handle,
60 .actor-counters {
61 color: pvar(--greyForegroundColor);
62 font-size: $grey-font-size;
63 }
64
65 .actor-counters > *:not(:last-child)::after {
66 content: '•';
67 margin: 0 10px;
68 color: pvar(--mainColor);
69 }
70
71 @media screen and (max-width: $mobile-view) {
72 margin-bottom: 15px;
73
74 h1 {
75 font-size: 22px;
76 }
77
78 .channel-avatar {
79 @include channel-avatar(80px);
80 }
81
82 .account-avatar {
83 @include avatar(120px);
84 }
85 }
86}