diff options
author | Chocobozzz <me@florianbigard.com> | 2021-03-26 13:20:37 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2021-03-31 09:05:51 +0200 |
commit | 67264e060b6068399dae9a67abae035a73b84af1 (patch) | |
tree | dabb735a530c9389c941f7ff1d44fa4b5f6db03e /client/src/app | |
parent | 60c35932f6a14cfe83bb0e54407427cce70171ea (diff) | |
download | PeerTube-67264e060b6068399dae9a67abae035a73b84af1.tar.gz PeerTube-67264e060b6068399dae9a67abae035a73b84af1.tar.zst PeerTube-67264e060b6068399dae9a67abae035a73b84af1.zip |
Redesign account page
Diffstat (limited to 'client/src/app')
15 files changed, 343 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 @@ | |||
1 | import { Subscription } from 'rxjs' | ||
2 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
3 | import { MarkdownService } from '@app/core' | ||
4 | import { 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 | }) | ||
11 | export 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 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | 3 | import { MetaGuard } from '@ngx-meta/core' |
4 | import { AccountsComponent } from './accounts.component' | ||
5 | import { AccountVideosComponent } from './account-videos/account-videos.component' | ||
6 | import { AccountAboutComponent } from './account-about/account-about.component' | ||
7 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' | ||
8 | import { AccountSearchComponent } from './account-search/account-search.component' | 4 | import { AccountSearchComponent } from './account-search/account-search.component' |
5 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' | ||
6 | import { AccountVideosComponent } from './account-videos/account-videos.component' | ||
7 | import { AccountsComponent } from './accounts.component' | ||
9 | 8 | ||
10 | const accountsRoutes: Routes = [ | 9 | const 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 | ||
49 | my-user-moderation-dropdown, | 26 | my-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' | |||
2 | import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators' | 2 | import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators' |
3 | import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' | 3 | import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' |
4 | import { ActivatedRoute } from '@angular/router' | 4 | import { ActivatedRoute } from '@angular/router' |
5 | import { AuthService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core' | 5 | import { AuthService, MarkdownService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core' |
6 | import { Account, AccountService, DropdownAction, ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 6 | import { |
7 | Account, | ||
8 | AccountService, | ||
9 | DropdownAction, | ||
10 | ListOverflowItem, | ||
11 | VideoChannel, | ||
12 | VideoChannelService, | ||
13 | VideoService | ||
14 | } from '@app/shared/shared-main' | ||
7 | import { AccountReportComponent } from '@app/shared/shared-moderation' | 15 | import { AccountReportComponent } from '@app/shared/shared-moderation' |
8 | import { User, UserRight } from '@shared/models' | ||
9 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 16 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
17 | import { User, UserRight } from '@shared/models' | ||
10 | import { AccountSearchComponent } from './account-search/account-search.component' | 18 | import { 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 | }) |
16 | export class AccountsComponent implements OnInit, OnDestroy { | 24 | export 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' | |||
5 | import { SharedModerationModule } from '@app/shared/shared-moderation' | 5 | import { SharedModerationModule } from '@app/shared/shared-moderation' |
6 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' | 6 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' |
7 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' | 7 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' |
8 | import { AccountAboutComponent } from './account-about/account-about.component' | 8 | import { AccountSearchComponent } from './account-search/account-search.component' |
9 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' | 9 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' |
10 | import { AccountVideosComponent } from './account-videos/account-videos.component' | 10 | import { AccountVideosComponent } from './account-videos/account-videos.component' |
11 | import { AccountSearchComponent } from './account-search/account-search.component' | ||
12 | import { AccountsRoutingModule } from './accounts-routing.module' | 11 | import { AccountsRoutingModule } from './accounts-routing.module' |
13 | import { AccountsComponent } from './accounts.component' | 12 | import { 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 | ||
4 | span { | 4 | .root { |
5 | opacity: .6; | 5 | display: flex; |
6 | |||
7 | &:focus-within { | ||
8 | opacity: 1; | ||
9 | } | ||
10 | } | 6 | } |
11 | 7 | ||
12 | my-global-icon { | 8 | my-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 | ||
18 | input { | 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 | |||
27 | input { | ||
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 @@ | |||
1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { Subject } from 'rxjs' | 1 | import { Subject } from 'rxjs' |
4 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | 2 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' |
3 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | ||
4 | import { 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 | } |