]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Redesign account page
authorChocobozzz <me@florianbigard.com>
Fri, 26 Mar 2021 12:20:37 +0000 (13:20 +0100)
committerChocobozzz <chocobozzz@cpy.re>
Wed, 31 Mar 2021 07:05:51 +0000 (09:05 +0200)
16 files changed:
client/src/app/+accounts/account-about/account-about.component.html [deleted file]
client/src/app/+accounts/account-about/account-about.component.scss [deleted file]
client/src/app/+accounts/account-about/account-about.component.ts [deleted file]
client/src/app/+accounts/account-search/account-search.component.ts
client/src/app/+accounts/accounts-routing.module.ts
client/src/app/+accounts/accounts.component.html
client/src/app/+accounts/accounts.component.scss
client/src/app/+accounts/accounts.component.ts
client/src/app/+accounts/accounts.module.ts
client/src/app/+video-channels/video-channels.component.html
client/src/app/+video-channels/video-channels.component.scss
client/src/app/+video-channels/video-channels.component.ts
client/src/app/shared/shared-main/misc/simple-search-input.component.html
client/src/app/shared/shared-main/misc/simple-search-input.component.scss
client/src/app/shared/shared-main/misc/simple-search-input.component.ts
client/src/sass/include/_actor.scss [new file with mode: 0644]

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 (file)
index e9e0e40..0000000
+++ /dev/null
@@ -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>
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 (file)
index 5bcd4b5..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-@import '_variables';
-@import '_mixins';
-
-.block {
-  margin-bottom: 40px;
-
-  .small-title {
-    @include in-content-small-title;
-
-    margin-bottom: 20px;
-  }
-}
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 (file)
index 6cf846d..0000000
+++ /dev/null
@@ -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`
-  }
-}
index dda4bf0c76544351f1982e47698f53bcefe2af80..f54ab846a5a39a2335b54b6a259684c95809b692 100644 (file)
@@ -64,9 +64,14 @@ export class AccountSearchComponent extends AbstractVideoList implements OnInit,
   }
 
   updateSearch (value: string) {
-    if (value === '') this.router.navigate(['../videos'], { relativeTo: this.route })
     this.search = value
 
+    if (!this.search) {
+      this.router.navigate([ '../videos' ], { relativeTo: this.route })
+      return
+    }
+
+    this.videos = []
     this.reloadVideos()
   }
 
index 15937a67b5482f91152945920f86d6f2aeb46588..3bf0f7185c9eb395736a40c1cbb0041d2783e2b5 100644 (file)
@@ -1,11 +1,10 @@
 import { NgModule } from '@angular/core'
 import { RouterModule, Routes } from '@angular/router'
 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 { 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 = [
   {
@@ -31,15 +30,6 @@ const accountsRoutes: Routes = [
           }
         }
       },
-      {
-        path: 'about',
-        component: AccountAboutComponent,
-        data: {
-          meta: {
-            title: $localize`About account`
-          }
-        }
-      },
       {
         path: 'videos',
         component: AccountVideosComponent,
index 1903bb36f10a0ce65145406b33a92f194975c7ff..92d24ce9446316c1276a837054f526b69075ab53 100644 (file)
@@ -1,57 +1,89 @@
-<div *ngIf="account" class="row">
-  <div class="sub-menu">
-
-    <div class="actor">
-      <img [src]="account.avatarUrl" alt="Avatar" />
-
-      <div class="actor-info">
-        <div class="actor-names">
-          <div class="actor-display-name">{{ account.displayName }}</div>
-          <div class="actor-name">
-            <span>{{ account.nameWithHost }}</span>
-            <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
-                    class="btn btn-outline-secondary btn-sm copy-button"
-            >
-              <span class="glyphicon glyphicon-duplicate"></span>
-            </button>
+<div *ngIf="account" class="root">
+  <div class="account-info">
+
+    <div class="account-avatar-row">
+      <img class="account-avatar" [src]="account.avatarUrl" alt="Avatar" />
+
+      <div>
+        <div class="section-label" i18n>PEERTUBE ACCOUNT</div>
+
+        <div class="actor-info">
+          <div>
+            <div class="actor-display-name">
+              <h1>{{ account.displayName }}</h1>
+
+              <my-user-moderation-dropdown
+                [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>
-          <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 class="actor-followers" [title]="accountFollowerTitle">
-          {{ subscribersDisplayFor(naiveAggregatedSubscribers) }}
         </div>
       </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 class="description" [ngClass]="{ expanded: accountDescriptionExpanded }">
+      <div class="description-html" [innerHTML]="accountDescriptionHTML"></div>
+
+      <div class="created-at" i18n>Account created on {{ account.createdAt | date }}</div>
     </div>
 
-    <div class="links w-100">
-      <ng-template #linkTemplate let-item="item">
-        <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
-      </ng-template>
+    <div *ngIf="!accountDescriptionExpanded" class="show-more" role="button"
+      (click)="accountDescriptionExpanded = !accountDescriptionExpanded"
+      title="Show the complete description" i18n-title i18n
+    >
+      Show more...
+    </div>
 
-      <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow>
+    <div class="buttons">
+      <a *ngIf="isManageable() && !isInSmallView()" routerLink="/my-account" class="peertube-button-link orange-button" i18n>
+        Manage account
+      </a>
 
-      <simple-search-input (searchChanged)="searchChanged($event)" name="search-videos" i18n-placeholder placeholder="Search videos"></simple-search-input>
+      <my-subscribe-button *ngIf="videoChannels" [account]="account" [videoChannels]="videoChannels"></my-subscribe-button>
     </div>
   </div>
 
-  <div class="margin-content">
-    <router-outlet (activate)="onOutletLoaded($event)"></router-outlet>
+  <div class="links">
+    <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>
+
+  <router-outlet (activate)="onOutletLoaded($event)"></router-outlet>
 </div>
 
 <ng-container *ngIf="prependModerationActions">
index 40c6b649368c9136c30e54efe9aad5487a28ca06..c1cf53f3a70e587f042509cf3ee08943a1e9d442 100644 (file)
@@ -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 '_mixins';
-
-.sub-menu {
-  @include sub-menu-with-actor;
-
-  .actor {
-    width: 100%;
-  }
+@import '_actor';
+@import '_miniature';
+
+.root {
+  --myGlobalPadding: 60px;
+  --myImgMargin: 30px;
+  --myFontSize: 16px;
+  --myGreyFontSize: 16px;
 }
 
-.margin-content {
-  // margin-content is required, but child views have their own margins
-  // that match views outside the scope of accounts, so we only align
-  // them with the margins of .sub-menu when required.
-  margin: 0;
+.section-label {
+  @include section-label-responsive;
 }
 
-.right-buttons {
-  display: flex;
-  height: max-content;
-  margin-left: auto;
-  margin-top: 10px;
-
-  @include media-breakpoint-down(lg) {
-    flex-flow: column-reverse;
+.links {
+  @include fluid-videos-miniature-layout;
 
-    a {
-      margin-top: 0.25rem;
-      margin-right: 0 !important;
-    }
-  }
-
-  a {
-    @include peertube-button-outline;
-  }
-
-  my-subscribe-button {
-    min-height: 30px;
-  }
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  max-width: 800px;
 }
 
 my-user-moderation-dropdown,
@@ -60,39 +37,98 @@ my-user-moderation-dropdown,
 
 .copy-button {
   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) {
-  .sub-menu {
-    .actor {
-      flex-direction: column;
-      align-items: center;
-
-      img,
-      .actor-info .actor-names .actor-display-name {
-        margin-right: 0;
-      }
-
-      .actor-info  {
-        .actor-names {
-          flex-direction: column;
-          align-items: center;
-        }
-
-        my-user-moderation-dropdown {
-          margin-left: 0;
-        }
-
-        .actor-followers {
-          text-align: center;
-        }
-      }
-
-      .right-buttons {
-        margin-left: 0;
-      }
-    }
+  .root {
+    --myGlobalPadding: 15px;
+    --myFontSize: 14px;
+    --myGreyFontSize: 13px;
+  }
+
+  .account-info {
+    display: block;
+    padding-bottom: 30px;
+  }
+
+  .links {
+    margin: auto !important;
+    width: min-content;
+  }
+
+  .show-more {
+    margin-bottom: 30px;
   }
 }
index e6a5a5d5ec2f3a7337fbddef15a0d065549b6d3e..a0006312961a922cbd0e5675f89cdfd805750f22 100644 (file)
@@ -2,11 +2,19 @@ import { Subscription } from 'rxjs'
 import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'
 import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
 import { ActivatedRoute } from '@angular/router'
-import { AuthService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core'
-import { Account, AccountService, DropdownAction, ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main'
+import { AuthService, MarkdownService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core'
+import {
+  Account,
+  AccountService,
+  DropdownAction,
+  ListOverflowItem,
+  VideoChannel,
+  VideoChannelService,
+  VideoService
+} from '@app/shared/shared-main'
 import { AccountReportComponent } from '@app/shared/shared-moderation'
-import { User, UserRight } from '@shared/models'
 import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
+import { User, UserRight } from '@shared/models'
 import { AccountSearchComponent } from './account-search/account-search.component'
 
 @Component({
@@ -15,16 +23,23 @@ import { AccountSearchComponent } from './account-search/account-search.componen
 })
 export class AccountsComponent implements OnInit, OnDestroy {
   @ViewChild('accountReportModal') accountReportModal: AccountReportComponent
+
   accountSearch: AccountSearchComponent
 
   account: Account
   accountUser: User
+
   videoChannels: VideoChannel[] = []
+
   links: ListOverflowItem[] = []
+  hideMenu = false
 
-  isAccountManageable = false
   accountFollowerTitle = ''
 
+  accountVideosCount: number
+  accountDescriptionHTML = ''
+  accountDescriptionExpanded = false
+
   prependModerationActions: DropdownAction<any>[]
 
   private routeSub: Subscription
@@ -38,6 +53,8 @@ export class AccountsComponent implements OnInit, OnDestroy {
     private restExtractor: RestExtractor,
     private redirectService: RedirectService,
     private authService: AuthService,
+    private videoService: VideoService,
+    private markdown: MarkdownService,
     private screenService: ScreenService
   ) {
   }
@@ -63,8 +80,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
 
     this.links = [
       { label: $localize`VIDEO CHANNELS`, routerLink: 'video-channels' },
-      { label: $localize`VIDEOS`, routerLink: 'videos' },
-      { label: $localize`ABOUT`, routerLink: 'about' }
+      { label: $localize`VIDEOS`, routerLink: 'videos' }
     ]
   }
 
@@ -72,19 +88,29 @@ export class AccountsComponent implements OnInit, OnDestroy {
     if (this.routeSub) this.routeSub.unsubscribe()
   }
 
-  get naiveAggregatedSubscribers () {
+  naiveAggregatedSubscribers () {
     return this.videoChannels.reduce(
       (acc, val) => acc + val.followersCount,
       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()
   }
 
+  isManageable () {
+    if (!this.isUserLoggedIn()) return false
+
+    return this.account?.userId === this.authService.getUser().id
+  }
+
   onUserChanged () {
-    this.getUserIfNeeded(this.account)
+    this.loadUserIfNeeded(this.account)
   }
 
   onUserDeleted () {
@@ -113,40 +139,30 @@ export class AccountsComponent implements OnInit, OnDestroy {
     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.account = account
+    this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML(account.description)
 
-    if (this.authService.isLoggedIn()) {
-      this.authService.userInformationLoaded.subscribe(
-        () => {
-          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()
-              }
-            ]
-          }
-        }
-      )
-    }
+    // After the markdown renderer to avoid layout changes
+    this.account = account
 
-    this.getUserIfNeeded(account)
+    this.updateModerationActions()
+    this.loadUserIfNeeded(account)
+    this.loadAccountVideosCount()
   }
 
   private showReportModal () {
     this.accountReportModal.show()
   }
 
-  private getUserIfNeeded (account: Account) {
+  private loadUserIfNeeded (account: Account) {
     if (!account.userId || !this.authService.isLoggedIn()) return
 
     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)
+  }
 }
index 6da65cbc1489f227544e31406a2d5222e882ea0a..3354b4189f8650ee5a242e19489d0e6b10cc5771 100644 (file)
@@ -5,10 +5,9 @@ import { SharedMainModule } from '@app/shared/shared-main'
 import { SharedModerationModule } from '@app/shared/shared-moderation'
 import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
 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 { AccountVideosComponent } from './account-videos/account-videos.component'
-import { AccountSearchComponent } from './account-search/account-search.component'
 import { AccountsRoutingModule } from './accounts-routing.module'
 import { AccountsComponent } from './accounts.component'
 
@@ -28,7 +27,6 @@ import { AccountsComponent } from './accounts.component'
     AccountsComponent,
     AccountVideosComponent,
     AccountVideoChannelsComponent,
-    AccountAboutComponent,
     AccountSearchComponent
   ],
 
index f63110bf5dd7e1544cfd64adb67fa5323a7cf119..d1eb15dff0aa1eb6ae098f55e150f8cd8df47ae5 100644 (file)
@@ -12,7 +12,7 @@
     <ng-template #ownerTemplate>
       <div class="owner-block">
         <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">
             <h4>{{ videoChannel.ownerAccount.displayName }}</h4>
index 16e13c5780c0e35261487062d7625b39d542522a..f5547b4e9062b9f7ea1e024bdbf9a538419415ed 100644 (file)
@@ -1,5 +1,6 @@
 @import '_variables';
 @import '_mixins';
+@import '_actor';
 @import '_miniature';
 
 .root {
 }
 
 .section-label {
-  color: pvar(--mainColor);
-  font-size: 12px;
-  margin-bottom: 15px;
-  font-weight: $font-bold;
-  letter-spacing: 2.5px;
+  @include section-label-responsive;
 }
 
 .links {
 }
 
 .channel-avatar-row {
-  display: flex;
-  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);
-  }
+  @include avatar-row-responsive(var(--myChannelImgMargin), var(--myGreyChannelFontSize));
 }
 
 .channel-description {
 }
 
 .show-more {
+  @include show-more-description;
+
   display: none;
-  color: pvar(--mainColor);
-  cursor: pointer;
-  margin: 10px auto 45px auto;
 }
 
-
 .channel-buttons {
   display: flex;
   flex-wrap: wrap;
     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 {
     margin-bottom: 30px;
   }
index 037c108f2c4ebee6141e867931ababe38a233580..4fcc4210374205fa77560ce4e75e97488bc446de 100644 (file)
@@ -94,7 +94,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
   isManageable () {
     if (!this.isUserLoggedIn()) return false
 
-    return this.videoChannel.ownerAccount.userId === this.authService.getUser().id
+    return this.videoChannel?.ownerAccount.userId === this.authService.getUser().id
   }
 
   activateCopiedMessage () {
index fb0d97122cf744d9aa1c67f36aa5aa86f549cf72..c20c02e23150655ca54045c5516386613052a216 100644 (file)
@@ -1,14 +1,15 @@
-<span>
-  <my-global-icon iconName="search" aria-label="Search" role="button" (click)="showInput()"></my-global-icon>
-
+<div class="root">
   <input
     #ref
     type="text"
     [(ngModel)]="value"
-    (focusout)="focusLost()"
     (keyup.enter)="searchChange()"
-    [hidden]="!shown"
+    [hidden]="!inputShown"
     [name]="name"
     [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>
index 591b04fb2f60514f09baf44cdc3fdfabbaa4d3fc..037937f800042c1b8693eb3df26b34421bcf43fd 100644 (file)
@@ -1,29 +1,29 @@
 @import '_variables';
 @import '_mixins';
 
-span {
-  opacity: .6;
-  
-  &:focus-within {
-    opacity: 1;
-  }
+.root {
+  display: flex;
 }
 
 my-global-icon {
-  height: 18px;
-  position: relative;
-  top: -2px;
-}
+  height: 26px;
+  width: 26px;
+  margin-left: 10px;
+  cursor: pointer;
 
-input {
-  @include peertube-input-text(150px);
+  &:hover {
+    color: pvar(--mainHoverColor);
+  }
 
-  height: 22px; // maximum height for the account/video-channels links
-  padding-left: 10px;
-  background-color: transparent;
-  border: none;
+  &[iconName=search] {
+    color: pvar(--mainColor);
+  }
 
-  &::placeholder {
-    font-size: 15px;
+  &[iconName=cross] {
+    color: pvar(--mainForegroundColor);
   }
 }
+
+input {
+  @include peertube-input-text(200px);
+}
index 86ae9ab4222cf7d21e103f35121fdae8652a4054..224d71134535697db729c6034cd82b6c12c149ec 100644 (file)
@@ -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 { debounceTime, distinctUntilChanged } from 'rxjs/operators'
+import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
 
 @Component({
   selector: 'simple-search-input',
@@ -13,11 +13,14 @@ export class SimpleSearchInputComponent implements OnInit {
 
   @Input() name = 'search'
   @Input() placeholder = $localize`Search`
+  @Input() iconTitle = $localize`Search`
+  @Input() alwaysShow = true
 
   @Output() searchChanged = new EventEmitter<string>()
+  @Output() inputDisplayChanged = new EventEmitter<boolean>()
 
   value = ''
-  shown: boolean
+  inputShown: boolean
 
   private searchSubject = new Subject<string>()
 
@@ -35,20 +38,51 @@ export class SimpleSearchInputComponent implements OnInit {
         .subscribe(value => this.searchChanged.emit(value))
 
     this.searchSubject.next(this.value)
+
+    if (this.isInputShown()) this.showInput(false)
   }
 
-  showInput () {
-    this.shown = true
-    setTimeout(() => this.input.nativeElement.focus())
+  isInputShown () {
+    if (this.alwaysShow) return true
+
+    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 () {
-    if (this.value !== '') return
-    this.shown = false
+    if (this.value) return
+
+    this.hideInput()
   }
 
   searchChange () {
-    this.router.navigate(['./search'], { relativeTo: this.route })
+    this.router.navigate([ './search' ], { relativeTo: this.route })
+
     this.searchSubject.next(this.value)
   }
 }
diff --git a/client/src/sass/include/_actor.scss b/client/src/sass/include/_actor.scss
new file mode 100644 (file)
index 0000000..5e96073
--- /dev/null
@@ -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);
+    }
+  }
+}