Redesign account page
This commit is contained in:
parent
60c35932f6
commit
67264e060b
|
@ -1,15 +0,0 @@
|
||||||
<h1 class="sr-only" i18n>About</h1>
|
|
||||||
<div class="margin-content">
|
|
||||||
<div *ngIf="account" class="row no-gutters">
|
|
||||||
<div class="block col-md-6 col-sm-12 pr-2">
|
|
||||||
<h2 i18n class="small-title">DESCRIPTION</h2>
|
|
||||||
<div class="content" [innerHtml]="getAccountDescription()"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="block col-md-6 col-sm-12">
|
|
||||||
<h2 i18n class="small-title">STATS</h2>
|
|
||||||
|
|
||||||
<div i18n class="content">Joined {{ account.createdAt | date }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,12 +0,0 @@
|
||||||
@import '_variables';
|
|
||||||
@import '_mixins';
|
|
||||||
|
|
||||||
.block {
|
|
||||||
margin-bottom: 40px;
|
|
||||||
|
|
||||||
.small-title {
|
|
||||||
@include in-content-small-title;
|
|
||||||
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { Subscription } from 'rxjs'
|
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
|
||||||
import { MarkdownService } from '@app/core'
|
|
||||||
import { Account, AccountService } from '@app/shared/shared-main'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'my-account-about',
|
|
||||||
templateUrl: './account-about.component.html',
|
|
||||||
styleUrls: [ './account-about.component.scss' ]
|
|
||||||
})
|
|
||||||
export class AccountAboutComponent implements OnInit, OnDestroy {
|
|
||||||
account: Account
|
|
||||||
descriptionHTML = ''
|
|
||||||
|
|
||||||
private accountSub: Subscription
|
|
||||||
|
|
||||||
constructor (
|
|
||||||
private accountService: AccountService,
|
|
||||||
private markdownService: MarkdownService
|
|
||||||
) { }
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
// Parent get the account for us
|
|
||||||
this.accountSub = this.accountService.accountLoaded
|
|
||||||
.subscribe(async account => {
|
|
||||||
this.account = account
|
|
||||||
this.descriptionHTML = await this.markdownService.textMarkdownToHTML(this.account.description, true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy () {
|
|
||||||
if (this.accountSub) this.accountSub.unsubscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
getAccountDescription () {
|
|
||||||
if (this.descriptionHTML) return this.descriptionHTML
|
|
||||||
|
|
||||||
return $localize`No description`
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -64,9 +64,14 @@ export class AccountSearchComponent extends AbstractVideoList implements OnInit,
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSearch (value: string) {
|
updateSearch (value: string) {
|
||||||
if (value === '') this.router.navigate(['../videos'], { relativeTo: this.route })
|
|
||||||
this.search = value
|
this.search = value
|
||||||
|
|
||||||
|
if (!this.search) {
|
||||||
|
this.router.navigate([ '../videos' ], { relativeTo: this.route })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.videos = []
|
||||||
this.reloadVideos()
|
this.reloadVideos()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
import { MetaGuard } from '@ngx-meta/core'
|
import { MetaGuard } from '@ngx-meta/core'
|
||||||
import { AccountsComponent } from './accounts.component'
|
|
||||||
import { AccountVideosComponent } from './account-videos/account-videos.component'
|
|
||||||
import { AccountAboutComponent } from './account-about/account-about.component'
|
|
||||||
import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
|
|
||||||
import { AccountSearchComponent } from './account-search/account-search.component'
|
import { AccountSearchComponent } from './account-search/account-search.component'
|
||||||
|
import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
|
||||||
|
import { AccountVideosComponent } from './account-videos/account-videos.component'
|
||||||
|
import { AccountsComponent } from './accounts.component'
|
||||||
|
|
||||||
const accountsRoutes: Routes = [
|
const accountsRoutes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -31,15 +30,6 @@ const accountsRoutes: Routes = [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'about',
|
|
||||||
component: AccountAboutComponent,
|
|
||||||
data: {
|
|
||||||
meta: {
|
|
||||||
title: $localize`About account`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'videos',
|
path: 'videos',
|
||||||
component: AccountVideosComponent,
|
component: AccountVideosComponent,
|
||||||
|
|
|
@ -1,57 +1,89 @@
|
||||||
<div *ngIf="account" class="row">
|
<div *ngIf="account" class="root">
|
||||||
<div class="sub-menu">
|
<div class="account-info">
|
||||||
|
|
||||||
<div class="actor">
|
<div class="account-avatar-row">
|
||||||
<img [src]="account.avatarUrl" alt="Avatar" />
|
<img class="account-avatar" [src]="account.avatarUrl" alt="Avatar" />
|
||||||
|
|
||||||
<div class="actor-info">
|
<div>
|
||||||
<div class="actor-names">
|
<div class="section-label" i18n>PEERTUBE ACCOUNT</div>
|
||||||
<div class="actor-display-name">{{ account.displayName }}</div>
|
|
||||||
<div class="actor-name">
|
<div class="actor-info">
|
||||||
<span>{{ account.nameWithHost }}</span>
|
<div>
|
||||||
<button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
|
<div class="actor-display-name">
|
||||||
class="btn btn-outline-secondary btn-sm copy-button"
|
<h1>{{ account.displayName }}</h1>
|
||||||
>
|
|
||||||
<span class="glyphicon glyphicon-duplicate"></span>
|
<my-user-moderation-dropdown
|
||||||
</button>
|
[prependActions]="prependModerationActions"
|
||||||
|
buttonSize="small" [account]="account" [user]="accountUser" placement="bottom-left auto"
|
||||||
|
(userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
|
||||||
|
></my-user-moderation-dropdown>
|
||||||
|
|
||||||
|
<span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
|
||||||
|
<span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
|
||||||
|
<span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span>
|
||||||
|
<span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
|
||||||
|
<span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actor-handle">
|
||||||
|
<span>@{{ account.nameWithHost }}</span>
|
||||||
|
<button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
|
||||||
|
class="btn btn-outline-secondary btn-sm copy-button" title="Copy account handle" i18n-title
|
||||||
|
>
|
||||||
|
<span class="glyphicon glyphicon-duplicate"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actor-counters">
|
||||||
|
<span i18n>{naiveAggregatedSubscribers(), plural, =1 {1 subscriber} other {{{ naiveAggregatedSubscribers() }} subscribers}}</span>
|
||||||
|
|
||||||
|
<span class="videos-count" *ngIf="accountVideosCount !== undefined" i18n>
|
||||||
|
{accountVideosCount, plural, =1 {1 videos} other {{{ accountVideosCount }} videos}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
|
|
||||||
<span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
|
|
||||||
<span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span>
|
|
||||||
<span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
|
|
||||||
<span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
|
|
||||||
|
|
||||||
<my-user-moderation-dropdown
|
|
||||||
[prependActions]="prependModerationActions"
|
|
||||||
buttonSize="small" [account]="account" [user]="accountUser" placement="bottom-left auto"
|
|
||||||
(userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
|
|
||||||
></my-user-moderation-dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="actor-followers" [title]="accountFollowerTitle">
|
|
||||||
{{ subscribersDisplayFor(naiveAggregatedSubscribers) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="right-buttons">
|
|
||||||
<a *ngIf="isAccountManageable && !isInSmallView" routerLink="/my-account" class="btn btn-outline-tertiary mr-2" i18n>Manage account</a>
|
|
||||||
<my-subscribe-button *ngIf="videoChannels" [account]="account" [videoChannels]="videoChannels"></my-subscribe-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="links w-100">
|
<div class="description" [ngClass]="{ expanded: accountDescriptionExpanded }">
|
||||||
<ng-template #linkTemplate let-item="item">
|
<div class="description-html" [innerHTML]="accountDescriptionHTML"></div>
|
||||||
<a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow>
|
<div class="created-at" i18n>Account created on {{ account.createdAt | date }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<simple-search-input (searchChanged)="searchChanged($event)" name="search-videos" i18n-placeholder placeholder="Search videos"></simple-search-input>
|
<div *ngIf="!accountDescriptionExpanded" class="show-more" role="button"
|
||||||
|
(click)="accountDescriptionExpanded = !accountDescriptionExpanded"
|
||||||
|
title="Show the complete description" i18n-title i18n
|
||||||
|
>
|
||||||
|
Show more...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<a *ngIf="isManageable() && !isInSmallView()" routerLink="/my-account" class="peertube-button-link orange-button" i18n>
|
||||||
|
Manage account
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<my-subscribe-button *ngIf="videoChannels" [account]="account" [videoChannels]="videoChannels"></my-subscribe-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="margin-content">
|
<div class="links">
|
||||||
<router-outlet (activate)="onOutletLoaded($event)"></router-outlet>
|
<ng-template #linkTemplate let-item="item">
|
||||||
|
<a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<list-overflow [hidden]="hideMenu" [items]="links" [itemTemplate]="linkTemplate"></list-overflow>
|
||||||
|
|
||||||
|
<simple-search-input
|
||||||
|
[alwaysShow]="!isInSmallView()" (searchChanged)="searchChanged($event)"
|
||||||
|
(inputDisplayChanged)="onSearchInputDisplayChanged($event)" name="search-videos"
|
||||||
|
i18n-iconTitle icon-title="Search account videos"
|
||||||
|
i18n-placeholder placeholder="Search account videos"
|
||||||
|
></simple-search-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<router-outlet (activate)="onOutletLoaded($event)"></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="prependModerationActions">
|
<ng-container *ngIf="prependModerationActions">
|
||||||
|
|
|
@ -1,49 +1,26 @@
|
||||||
// Bootstrap grid utilities require functions, variables and mixins
|
|
||||||
@import 'node_modules/bootstrap/scss/functions';
|
|
||||||
@import 'node_modules/bootstrap/scss/variables';
|
|
||||||
@import 'node_modules/bootstrap/scss/mixins';
|
|
||||||
@import 'node_modules/bootstrap/scss/grid';
|
|
||||||
|
|
||||||
@import '_variables';
|
@import '_variables';
|
||||||
@import '_mixins';
|
@import '_mixins';
|
||||||
|
@import '_actor';
|
||||||
|
@import '_miniature';
|
||||||
|
|
||||||
.sub-menu {
|
.root {
|
||||||
@include sub-menu-with-actor;
|
--myGlobalPadding: 60px;
|
||||||
|
--myImgMargin: 30px;
|
||||||
.actor {
|
--myFontSize: 16px;
|
||||||
width: 100%;
|
--myGreyFontSize: 16px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.margin-content {
|
.section-label {
|
||||||
// margin-content is required, but child views have their own margins
|
@include section-label-responsive;
|
||||||
// that match views outside the scope of accounts, so we only align
|
|
||||||
// them with the margins of .sub-menu when required.
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-buttons {
|
.links {
|
||||||
|
@include fluid-videos-miniature-layout;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
height: max-content;
|
justify-content: space-between;
|
||||||
margin-left: auto;
|
align-items: center;
|
||||||
margin-top: 10px;
|
max-width: 800px;
|
||||||
|
|
||||||
@include media-breakpoint-down(lg) {
|
|
||||||
flex-flow: column-reverse;
|
|
||||||
|
|
||||||
a {
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
margin-right: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
@include peertube-button-outline;
|
|
||||||
}
|
|
||||||
|
|
||||||
my-subscribe-button {
|
|
||||||
min-height: 30px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
my-user-moderation-dropdown,
|
my-user-moderation-dropdown,
|
||||||
|
@ -60,39 +37,98 @@ my-user-moderation-dropdown,
|
||||||
|
|
||||||
.copy-button {
|
.copy-button {
|
||||||
border: none;
|
border: none;
|
||||||
padding: 5px;
|
}
|
||||||
margin-top: -2px;
|
|
||||||
|
.account-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr min-content;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
|
||||||
|
background-color: pvar(--submenuColor);
|
||||||
|
margin-bottom: 45px;
|
||||||
|
padding: var(--myGlobalPadding) var(--myGlobalPadding) 0 var(--myGlobalPadding);
|
||||||
|
font-size: var(--myFontSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-avatar-row {
|
||||||
|
@include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.created-at {
|
||||||
|
margin-top: 15px;
|
||||||
|
color: pvar(--greyForegroundColor);
|
||||||
|
padding-bottom: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-more {
|
||||||
|
@include show-more-description;
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-content: flex-start;
|
||||||
|
|
||||||
|
> *:not(:last-child) {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $small-view) {
|
||||||
|
.root {
|
||||||
|
--myGlobalPadding: 45px;
|
||||||
|
--myChannelImgMargin: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-info {
|
||||||
|
display: block;
|
||||||
|
padding-bottom: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description:not(.expanded) {
|
||||||
|
max-height: 70px;
|
||||||
|
|
||||||
|
@include fade-text(30px, pvar(--submenuColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-more {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $mobile-view) {
|
@media screen and (max-width: $mobile-view) {
|
||||||
.sub-menu {
|
.root {
|
||||||
.actor {
|
--myGlobalPadding: 15px;
|
||||||
flex-direction: column;
|
--myFontSize: 14px;
|
||||||
align-items: center;
|
--myGreyFontSize: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
img,
|
.account-info {
|
||||||
.actor-info .actor-names .actor-display-name {
|
display: block;
|
||||||
margin-right: 0;
|
padding-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actor-info {
|
.links {
|
||||||
.actor-names {
|
margin: auto !important;
|
||||||
flex-direction: column;
|
width: min-content;
|
||||||
align-items: center;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
my-user-moderation-dropdown {
|
.show-more {
|
||||||
margin-left: 0;
|
margin-bottom: 30px;
|
||||||
}
|
|
||||||
|
|
||||||
.actor-followers {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-buttons {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,19 @@ import { Subscription } from 'rxjs'
|
||||||
import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'
|
import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'
|
||||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { AuthService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core'
|
import { AuthService, MarkdownService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core'
|
||||||
import { Account, AccountService, DropdownAction, ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main'
|
import {
|
||||||
|
Account,
|
||||||
|
AccountService,
|
||||||
|
DropdownAction,
|
||||||
|
ListOverflowItem,
|
||||||
|
VideoChannel,
|
||||||
|
VideoChannelService,
|
||||||
|
VideoService
|
||||||
|
} from '@app/shared/shared-main'
|
||||||
import { AccountReportComponent } from '@app/shared/shared-moderation'
|
import { AccountReportComponent } from '@app/shared/shared-moderation'
|
||||||
import { User, UserRight } from '@shared/models'
|
|
||||||
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
|
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
|
||||||
|
import { User, UserRight } from '@shared/models'
|
||||||
import { AccountSearchComponent } from './account-search/account-search.component'
|
import { AccountSearchComponent } from './account-search/account-search.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -15,16 +23,23 @@ import { AccountSearchComponent } from './account-search/account-search.componen
|
||||||
})
|
})
|
||||||
export class AccountsComponent implements OnInit, OnDestroy {
|
export class AccountsComponent implements OnInit, OnDestroy {
|
||||||
@ViewChild('accountReportModal') accountReportModal: AccountReportComponent
|
@ViewChild('accountReportModal') accountReportModal: AccountReportComponent
|
||||||
|
|
||||||
accountSearch: AccountSearchComponent
|
accountSearch: AccountSearchComponent
|
||||||
|
|
||||||
account: Account
|
account: Account
|
||||||
accountUser: User
|
accountUser: User
|
||||||
videoChannels: VideoChannel[] = []
|
|
||||||
links: ListOverflowItem[] = []
|
|
||||||
|
|
||||||
isAccountManageable = false
|
videoChannels: VideoChannel[] = []
|
||||||
|
|
||||||
|
links: ListOverflowItem[] = []
|
||||||
|
hideMenu = false
|
||||||
|
|
||||||
accountFollowerTitle = ''
|
accountFollowerTitle = ''
|
||||||
|
|
||||||
|
accountVideosCount: number
|
||||||
|
accountDescriptionHTML = ''
|
||||||
|
accountDescriptionExpanded = false
|
||||||
|
|
||||||
prependModerationActions: DropdownAction<any>[]
|
prependModerationActions: DropdownAction<any>[]
|
||||||
|
|
||||||
private routeSub: Subscription
|
private routeSub: Subscription
|
||||||
|
@ -38,6 +53,8 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
||||||
private restExtractor: RestExtractor,
|
private restExtractor: RestExtractor,
|
||||||
private redirectService: RedirectService,
|
private redirectService: RedirectService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
|
private videoService: VideoService,
|
||||||
|
private markdown: MarkdownService,
|
||||||
private screenService: ScreenService
|
private screenService: ScreenService
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
@ -63,8 +80,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
this.links = [
|
this.links = [
|
||||||
{ label: $localize`VIDEO CHANNELS`, routerLink: 'video-channels' },
|
{ label: $localize`VIDEO CHANNELS`, routerLink: 'video-channels' },
|
||||||
{ label: $localize`VIDEOS`, routerLink: 'videos' },
|
{ label: $localize`VIDEOS`, routerLink: 'videos' }
|
||||||
{ label: $localize`ABOUT`, routerLink: 'about' }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,19 +88,29 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
||||||
if (this.routeSub) this.routeSub.unsubscribe()
|
if (this.routeSub) this.routeSub.unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
get naiveAggregatedSubscribers () {
|
naiveAggregatedSubscribers () {
|
||||||
return this.videoChannels.reduce(
|
return this.videoChannels.reduce(
|
||||||
(acc, val) => acc + val.followersCount,
|
(acc, val) => acc + val.followersCount,
|
||||||
this.account.followersCount // accumulator starts with the base number of subscribers the account has
|
this.account.followersCount // accumulator starts with the base number of subscribers the account has
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
get isInSmallView () {
|
isUserLoggedIn () {
|
||||||
|
return this.authService.isLoggedIn()
|
||||||
|
}
|
||||||
|
|
||||||
|
isInSmallView () {
|
||||||
return this.screenService.isInSmallView()
|
return this.screenService.isInSmallView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isManageable () {
|
||||||
|
if (!this.isUserLoggedIn()) return false
|
||||||
|
|
||||||
|
return this.account?.userId === this.authService.getUser().id
|
||||||
|
}
|
||||||
|
|
||||||
onUserChanged () {
|
onUserChanged () {
|
||||||
this.getUserIfNeeded(this.account)
|
this.loadUserIfNeeded(this.account)
|
||||||
}
|
}
|
||||||
|
|
||||||
onUserDeleted () {
|
onUserDeleted () {
|
||||||
|
@ -113,40 +139,30 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
||||||
if (this.accountSearch) this.accountSearch.updateSearch(search)
|
if (this.accountSearch) this.accountSearch.updateSearch(search)
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAccount (account: Account) {
|
onSearchInputDisplayChanged (displayed: boolean) {
|
||||||
|
this.hideMenu = this.isInSmallView() && displayed
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onAccount (account: Account) {
|
||||||
|
this.accountFollowerTitle = $localize`${account.followersCount} direct account followers`
|
||||||
|
|
||||||
this.prependModerationActions = undefined
|
this.prependModerationActions = undefined
|
||||||
|
|
||||||
|
this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML(account.description)
|
||||||
|
|
||||||
|
// After the markdown renderer to avoid layout changes
|
||||||
this.account = account
|
this.account = account
|
||||||
|
|
||||||
if (this.authService.isLoggedIn()) {
|
this.updateModerationActions()
|
||||||
this.authService.userInformationLoaded.subscribe(
|
this.loadUserIfNeeded(account)
|
||||||
() => {
|
this.loadAccountVideosCount()
|
||||||
this.isAccountManageable = this.account.userId && this.account.userId === this.authService.getUser().id
|
|
||||||
|
|
||||||
const followers = this.subscribersDisplayFor(account.followersCount)
|
|
||||||
this.accountFollowerTitle = $localize`${followers} direct account followers`
|
|
||||||
|
|
||||||
// It's not our account, we can report it
|
|
||||||
if (!this.isAccountManageable) {
|
|
||||||
this.prependModerationActions = [
|
|
||||||
{
|
|
||||||
label: $localize`Report this account`,
|
|
||||||
handler: () => this.showReportModal()
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getUserIfNeeded(account)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private showReportModal () {
|
private showReportModal () {
|
||||||
this.accountReportModal.show()
|
this.accountReportModal.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private getUserIfNeeded (account: Account) {
|
private loadUserIfNeeded (account: Account) {
|
||||||
if (!account.userId || !this.authService.isLoggedIn()) return
|
if (!account.userId || !this.authService.isLoggedIn()) return
|
||||||
|
|
||||||
const user = this.authService.getUser()
|
const user = this.authService.getUser()
|
||||||
|
@ -158,4 +174,33 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updateModerationActions () {
|
||||||
|
if (!this.authService.isLoggedIn()) return
|
||||||
|
|
||||||
|
this.authService.userInformationLoaded.subscribe(
|
||||||
|
() => {
|
||||||
|
if (this.isManageable()) return
|
||||||
|
|
||||||
|
// It's not our account, we can report it
|
||||||
|
this.prependModerationActions = [
|
||||||
|
{
|
||||||
|
label: $localize`Report this account`,
|
||||||
|
handler: () => this.showReportModal()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadAccountVideosCount () {
|
||||||
|
this.videoService.getAccountVideos({
|
||||||
|
account: this.account,
|
||||||
|
videoPagination: {
|
||||||
|
currentPage: 1,
|
||||||
|
itemsPerPage: 0
|
||||||
|
},
|
||||||
|
sort: '-publishedAt'
|
||||||
|
}).subscribe(res => this.accountVideosCount = res.total)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,9 @@ import { SharedMainModule } from '@app/shared/shared-main'
|
||||||
import { SharedModerationModule } from '@app/shared/shared-moderation'
|
import { SharedModerationModule } from '@app/shared/shared-moderation'
|
||||||
import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
|
import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
|
||||||
import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
|
import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
|
||||||
import { AccountAboutComponent } from './account-about/account-about.component'
|
import { AccountSearchComponent } from './account-search/account-search.component'
|
||||||
import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
|
import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
|
||||||
import { AccountVideosComponent } from './account-videos/account-videos.component'
|
import { AccountVideosComponent } from './account-videos/account-videos.component'
|
||||||
import { AccountSearchComponent } from './account-search/account-search.component'
|
|
||||||
import { AccountsRoutingModule } from './accounts-routing.module'
|
import { AccountsRoutingModule } from './accounts-routing.module'
|
||||||
import { AccountsComponent } from './accounts.component'
|
import { AccountsComponent } from './accounts.component'
|
||||||
|
|
||||||
|
@ -28,7 +27,6 @@ import { AccountsComponent } from './accounts.component'
|
||||||
AccountsComponent,
|
AccountsComponent,
|
||||||
AccountVideosComponent,
|
AccountVideosComponent,
|
||||||
AccountVideoChannelsComponent,
|
AccountVideoChannelsComponent,
|
||||||
AccountAboutComponent,
|
|
||||||
AccountSearchComponent
|
AccountSearchComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<ng-template #ownerTemplate>
|
<ng-template #ownerTemplate>
|
||||||
<div class="owner-block">
|
<div class="owner-block">
|
||||||
<div class="avatar-row">
|
<div class="avatar-row">
|
||||||
<img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
|
<img class="channel-avatar" [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
|
||||||
|
|
||||||
<div class="actor-info">
|
<div class="actor-info">
|
||||||
<h4>{{ videoChannel.ownerAccount.displayName }}</h4>
|
<h4>{{ videoChannel.ownerAccount.displayName }}</h4>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
@import '_variables';
|
@import '_variables';
|
||||||
@import '_mixins';
|
@import '_mixins';
|
||||||
|
@import '_actor';
|
||||||
@import '_miniature';
|
@import '_miniature';
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
|
@ -11,11 +12,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-label {
|
.section-label {
|
||||||
color: pvar(--mainColor);
|
@include section-label-responsive;
|
||||||
font-size: 12px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-weight: $font-bold;
|
|
||||||
letter-spacing: 2.5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.links {
|
.links {
|
||||||
|
@ -34,48 +31,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-avatar-row {
|
.channel-avatar-row {
|
||||||
display: flex;
|
@include avatar-row-responsive(var(--myChannelImgMargin), var(--myGreyChannelFontSize));
|
||||||
grid-column: 1;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
@include channel-avatar(120px);
|
|
||||||
}
|
|
||||||
|
|
||||||
> div {
|
|
||||||
margin-left: var(--myChannelImgMargin);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor-info {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
> div:first-child {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor-display-name {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: $font-bold;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor-handle,
|
|
||||||
.actor-counters {
|
|
||||||
color: pvar(--greyForegroundColor);
|
|
||||||
font-size: var(--myGreyChannelFontSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor-counters > *:not(:last-child)::after {
|
|
||||||
content: '•';
|
|
||||||
margin: 0 10px;
|
|
||||||
color: pvar(--mainColor);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-description {
|
.channel-description {
|
||||||
|
@ -83,12 +39,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.show-more {
|
.show-more {
|
||||||
display: none;
|
@include show-more-description;
|
||||||
color: pvar(--mainColor);
|
|
||||||
cursor: pointer;
|
|
||||||
margin: 10px auto 45px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.channel-buttons {
|
.channel-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -280,24 +234,6 @@
|
||||||
width: min-content;
|
width: min-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-label {
|
|
||||||
font-size: 10px;
|
|
||||||
letter-spacing: 2.1px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.channel-avatar-row {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
@include channel-avatar(80px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.show-more {
|
.show-more {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,7 +94,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
|
||||||
isManageable () {
|
isManageable () {
|
||||||
if (!this.isUserLoggedIn()) return false
|
if (!this.isUserLoggedIn()) return false
|
||||||
|
|
||||||
return this.videoChannel.ownerAccount.userId === this.authService.getUser().id
|
return this.videoChannel?.ownerAccount.userId === this.authService.getUser().id
|
||||||
}
|
}
|
||||||
|
|
||||||
activateCopiedMessage () {
|
activateCopiedMessage () {
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
<span>
|
<div class="root">
|
||||||
<my-global-icon iconName="search" aria-label="Search" role="button" (click)="showInput()"></my-global-icon>
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
#ref
|
#ref
|
||||||
type="text"
|
type="text"
|
||||||
[(ngModel)]="value"
|
[(ngModel)]="value"
|
||||||
(focusout)="focusLost()"
|
|
||||||
(keyup.enter)="searchChange()"
|
(keyup.enter)="searchChange()"
|
||||||
[hidden]="!shown"
|
[hidden]="!inputShown"
|
||||||
[name]="name"
|
[name]="name"
|
||||||
[placeholder]="placeholder"
|
[placeholder]="placeholder"
|
||||||
>
|
>
|
||||||
</span>
|
|
||||||
|
<my-global-icon iconName="search" aria-label="Search" role="button" (click)="onIconClick()" [title]="iconTitle"></my-global-icon>
|
||||||
|
|
||||||
|
<my-global-icon *ngIf="!alwaysShow && inputShown" i18n-title title="Close search" iconName="cross" (click)="hideInput()"></my-global-icon>
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,29 +1,29 @@
|
||||||
@import '_variables';
|
@import '_variables';
|
||||||
@import '_mixins';
|
@import '_mixins';
|
||||||
|
|
||||||
span {
|
.root {
|
||||||
opacity: .6;
|
display: flex;
|
||||||
|
|
||||||
&:focus-within {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
my-global-icon {
|
my-global-icon {
|
||||||
height: 18px;
|
height: 26px;
|
||||||
position: relative;
|
width: 26px;
|
||||||
top: -2px;
|
margin-left: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: pvar(--mainHoverColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[iconName=search] {
|
||||||
|
color: pvar(--mainColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[iconName=cross] {
|
||||||
|
color: pvar(--mainForegroundColor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
@include peertube-input-text(150px);
|
@include peertube-input-text(200px);
|
||||||
|
|
||||||
height: 22px; // maximum height for the account/video-channels links
|
|
||||||
padding-left: 10px;
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
|
||||||
import { Subject } from 'rxjs'
|
import { Subject } from 'rxjs'
|
||||||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
|
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
|
||||||
|
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'simple-search-input',
|
selector: 'simple-search-input',
|
||||||
|
@ -13,11 +13,14 @@ export class SimpleSearchInputComponent implements OnInit {
|
||||||
|
|
||||||
@Input() name = 'search'
|
@Input() name = 'search'
|
||||||
@Input() placeholder = $localize`Search`
|
@Input() placeholder = $localize`Search`
|
||||||
|
@Input() iconTitle = $localize`Search`
|
||||||
|
@Input() alwaysShow = true
|
||||||
|
|
||||||
@Output() searchChanged = new EventEmitter<string>()
|
@Output() searchChanged = new EventEmitter<string>()
|
||||||
|
@Output() inputDisplayChanged = new EventEmitter<boolean>()
|
||||||
|
|
||||||
value = ''
|
value = ''
|
||||||
shown: boolean
|
inputShown: boolean
|
||||||
|
|
||||||
private searchSubject = new Subject<string>()
|
private searchSubject = new Subject<string>()
|
||||||
|
|
||||||
|
@ -35,20 +38,51 @@ export class SimpleSearchInputComponent implements OnInit {
|
||||||
.subscribe(value => this.searchChanged.emit(value))
|
.subscribe(value => this.searchChanged.emit(value))
|
||||||
|
|
||||||
this.searchSubject.next(this.value)
|
this.searchSubject.next(this.value)
|
||||||
|
|
||||||
|
if (this.isInputShown()) this.showInput(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
showInput () {
|
isInputShown () {
|
||||||
this.shown = true
|
if (this.alwaysShow) return true
|
||||||
setTimeout(() => this.input.nativeElement.focus())
|
|
||||||
|
return this.inputShown
|
||||||
|
}
|
||||||
|
|
||||||
|
onIconClick () {
|
||||||
|
if (!this.isInputShown()) {
|
||||||
|
this.showInput()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
showInput (focus = true) {
|
||||||
|
this.inputShown = true
|
||||||
|
this.inputDisplayChanged.emit(this.inputShown)
|
||||||
|
|
||||||
|
if (focus) {
|
||||||
|
setTimeout(() => this.input.nativeElement.focus())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideInput () {
|
||||||
|
this.inputShown = false
|
||||||
|
|
||||||
|
if (this.isInputShown() === false) {
|
||||||
|
this.inputDisplayChanged.emit(this.inputShown)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
focusLost () {
|
focusLost () {
|
||||||
if (this.value !== '') return
|
if (this.value) return
|
||||||
this.shown = false
|
|
||||||
|
this.hideInput()
|
||||||
}
|
}
|
||||||
|
|
||||||
searchChange () {
|
searchChange () {
|
||||||
this.router.navigate(['./search'], { relativeTo: this.route })
|
this.router.navigate([ './search' ], { relativeTo: this.route })
|
||||||
|
|
||||||
this.searchSubject.next(this.value)
|
this.searchSubject.next(this.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
@import '_variables';
|
||||||
|
|
||||||
|
@mixin section-label-responsive {
|
||||||
|
color: pvar(--mainColor);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-weight: $font-bold;
|
||||||
|
letter-spacing: 2.5px;
|
||||||
|
|
||||||
|
@media screen and (max-width: $mobile-view) {
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 2.1px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin show-more-description {
|
||||||
|
color: pvar(--mainColor);
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 10px auto 45px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin avatar-row-responsive ($img-margin, $grey-font-size) {
|
||||||
|
display: flex;
|
||||||
|
grid-column: 1;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
.channel-avatar {
|
||||||
|
@include channel-avatar(120px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-avatar {
|
||||||
|
@include avatar(120px);
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-left: $img-margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actor-info {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
> div:first-child {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actor-display-name {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: $font-bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actor-handle,
|
||||||
|
.actor-counters {
|
||||||
|
color: pvar(--greyForegroundColor);
|
||||||
|
font-size: $grey-font-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actor-counters > *:not(:last-child)::after {
|
||||||
|
content: '•';
|
||||||
|
margin: 0 10px;
|
||||||
|
color: pvar(--mainColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $mobile-view) {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-avatar {
|
||||||
|
@include channel-avatar(80px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-avatar {
|
||||||
|
@include avatar(120px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue