1
0
Fork 0

User dropdown and notifications popover improvements (#3344)

* hove user dropdown on avatar and username
* rename avatar-notification to notification component
* use a link on mobile for notification component
* add profile user dropdown and mobile notifications link as reusable active link
* replace markAllAsRead inbox glyphicon to ok in notification popover
* remove keyboard shortcuts from user dropdown on mobile
* use common bell icon instead of inbox-full for notifications
* remove duplicated notification in user dropdown since the bell appears on the right
* adjust sensitive icon in user dropdown
* align vertically user buttons popover and dropdown
* adjust ellipsis on user display name and username in menu
* adjust notification bell for mobile in menu
* display background of user dropdown avatar and username for touchscreens
* add right arrow indicator on mobile

Co-authored-by: kimsible <kimsible@users.noreply.github.com>
Co-authored-by: Rigel Kent <sendmemail@rigelk.eu>
This commit is contained in:
Kimsible 2020-12-13 14:54:12 +01:00 committed by GitHub
parent 75594f474a
commit 51a8397006
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 290 additions and 108 deletions

View file

@ -15,7 +15,7 @@
<button class="btn ml-auto" [disabled]="!hasUnreadNotifications()" (click)="markAllAsRead()">
<ng-container *ngIf="hasUnreadNotifications()">
<my-global-icon iconName="inbox-full" aria-hidden="true"></my-global-icon>
<my-global-icon iconName="tick" aria-hidden="true"></my-global-icon>
<span i18n>Mark all as read</span>
</ng-container>

View file

@ -11,7 +11,7 @@ import { CoreModule } from './core'
import { EmptyComponent } from './empty.component'
import { HeaderComponent, SearchTypeaheadComponent, SuggestionComponent } from './header'
import { HighlightPipe } from './header/highlight.pipe'
import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu'
import { NotificationComponent, LanguageChooserComponent, MenuComponent } from './menu'
import { ConfirmComponent } from './modal/confirm.component'
import { CustomModalComponent } from './modal/custom-modal.component'
import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component'
@ -35,7 +35,7 @@ registerLocaleData(localeOc, 'oc')
MenuComponent,
LanguageChooserComponent,
QuickSettingsModalComponent,
AvatarNotificationComponent,
NotificationComponent,
HeaderComponent,
SearchTypeaheadComponent,
SuggestionComponent,

View file

@ -1,3 +1,3 @@
export * from './language-chooser.component'
export * from './avatar-notification.component'
export * from './notification.component'
export * from './menu.component'

View file

@ -3,32 +3,28 @@
<div class="top-menu">
<div *ngIf="isLoggedIn" class="logged-in-block">
<div>
<my-avatar-notification [user]="user" (navigate)="onActiveLinkScrollToAnchor($event)"></my-avatar-notification>
<div class="logged-in-more" ngbDropdown #dropdown="ngbDropdown" placement="bottom-left" [container]="dropdownContainer" (openChange)="onDropdownOpenChange($event)" autoClose="outside">
<div ngbDropdownToggle>
<img [src]="user.accountAvatarUrl" alt="Avatar" />
<div class="logged-in-info">
<div class="logged-in-display-name">{{ user.account?.displayName }}</div>
<div class="logged-in-info">
<a *ngIf="user.account" [routerLink]="[ '/accounts', user.account.nameWithHost ]" class="logged-in-display-name">{{ user.account?.displayName }}</a>
<a *ngIf="!user.account" routerLink="/my-account/settings" class="logged-in-display-name">{{ user.account?.displayName }}</a>
<div class="logged-in-username">@{{ user.username }}</div>
</div>
<div class="logged-in-username">@{{ user.username }}</div>
</div>
<div class="logged-in-more" ngbDropdown [placement]="loggedInMorePlacement" container="body" autoClose="outside">
<my-global-icon iconName="more-vertical" ngbDropdownToggle role="button"></my-global-icon>
<div class="dropdown-toggle-indicator">
<span class="glyphicon glyphicon-chevron-down"></span>
</div>
</div>
<div ngbDropdownMenu>
<a *ngIf="user.account" ngbDropdownItem ngbDropdownToggle class="dropdown-item" [routerLink]="[ '/accounts', user.account.nameWithHost ]">
<a *ngIf="user.account" ngbDropdownItem ngbDropdownToggle class="dropdown-item" [routerLink]="[ '/accounts', user.account.nameWithHost ]"
#profile (click)="onActiveLinkScrollToAnchor(profile)">
<my-global-icon iconName="go" aria-hidden="true"></my-global-icon> <ng-container i18n>Public profile</ng-container>
</a>
<div class="dropdown-divider"></div>
<a ngbDropdownItem ngbDropdownToggle class="dropdown-item" routerLink="/my-account/notifications"
#notifications (click)="onActiveLinkScrollToAnchor(notifications)">
<my-global-icon iconName="inbox-full" aria-hidden="true"></my-global-icon> <ng-container i18n>My notifications</ng-container>
</a>
<div class="dropdown-divider"></div>
<a ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openLanguageChooser()">
<my-global-icon iconName="language" aria-hidden="true"></my-global-icon>
<span i18n>Interface:</span>
@ -42,7 +38,7 @@
<span class="ml-auto text-muted">{{ videoLanguages.join(', ') }}</span>
</a>
<a ngbDropdownItem ngbDropdownToggle class="dropdown-item" routerLink="/my-account/settings"
<a ngbDropdownItem ngbDropdownToggle class="dropdown-item settings-sensitive" routerLink="/my-account/settings"
fragment="video-sensitive-content-policy" #settingsSensitiveContentPolicy
(click)="onActiveLinkScrollToAnchor(settingsSensitiveContentPolicy)">
<my-global-icon class="hover-display-toggle" [ngClass]="{ 'not-displayed': user.nsfwPolicy === 'display' }" iconName="sensitive" aria-hidden="true"></my-global-icon>
@ -60,7 +56,7 @@
<div class="dropdown-divider"></div>
<a ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openHotkeysCheatSheet()">
<a *ngIf="!isInMobileView" ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openHotkeysCheatSheet()">
<my-global-icon iconName="command" aria-hidden="true"></my-global-icon>
<ng-container i18n>Keyboard shortcuts</ng-container>
</a>
@ -71,6 +67,8 @@
</a>
</div>
</div>
<my-notification (navigate)="onActiveLinkScrollToAnchor($event)"></my-notification>
</div>
<div class="logged-in-menu">

View file

@ -88,47 +88,118 @@ menu {
height: 80px;
display: flex;
align-items: center;
justify-content: center;
.logged-in-info {
@include ellipsis;
flex-grow: 1;
.logged-in-display-name {
font-size: 16px;
font-weight: $font-semibold;
color: pvar(--menuForegroundColor);
cursor: pointer;
@include disable-default-a-behaviour;
}
.logged-in-username {
@include ellipsis;
font-size: 13px;
color: #C6C6C6;
max-width: 140px;
cursor: pointer;
}
}
justify-content: left;
.logged-in-more {
margin-right: 20px;
$main-radius: 25px;
my-global-icon.dropdown-toggle {
cursor: pointer;
margin-left: 13px;
border-radius: $main-radius;
transition: all .1s ease-in-out;
cursor: pointer;
*, & {
line-height: 1;
}
&.show {
background-color: rgba(255, 255, 255, 0.20);
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325);
}
@mixin display-hints($is-mobile: false) {
background-color: rgba(255, 255, 255, 0.15);
@if $is-mobile {
.dropdown-toggle-indicator {
display: inherit !important;
}
.dropdown-toggle:first-child {
padding-right: 30px !important;
}
}
}
&:hover {
@include display-hints;
}
/* smartphones and touchscreens */
@media (hover: none) and (pointer: coarse) {
@include display-hints($is-mobile: true);
/* fill space when on mobile */
max-width: calc(100% - 80px);
.dropdown-toggle {
max-width: 100%;
}
.logged-in-info {
max-width: calc(100% - 45px) !important;
}
}
.dropdown-toggle-indicator {
position: relative;
width: 0;
display: none;
span {
position: absolute;
right: -35px;
top: -8px;
color: grey;
width: $main-radius;
}
}
.dropdown-toggle {
&::after {
border: none;
}
}
::ng-deep {
@include apply-svg-color(pvar(--menuForegroundColor));
.dropdown-toggle:first-child {
display: inline-flex;
align-items: center;
padding: 5px 7px;
}
img {
@include avatar(34px);
margin-right: 10px;
}
.logged-in-info {
max-width: 105px;
flex-grow: 1;
.logged-in-display-name,
.logged-in-username {
@include ellipsis;
}
.logged-in-display-name {
font-size: 16px;
font-weight: $font-semibold;
color: pvar(--menuForegroundColor);
@include disable-default-a-behaviour;
}
.logged-in-username {
font-size: 13px;
color: #C6C6C6;
}
}
}
my-notification {
margin-left: auto;
margin-right: 15px;
}
}
.logged-in-menu {
@ -343,6 +414,12 @@ menu {
my-global-icon.hover-display-toggle {
display: none;
}
&.settings-sensitive {
my-global-icon ::ng-deep svg {
margin-top: 2px !important;
}
}
}
}
@ -364,4 +441,14 @@ menu {
.top-menu, .footer {
width: 100% !important;
}
.dropdown-menu {
width: calc(100vw - 30px);
}
.dropdown-item:hover, .dropdown-item:active {
&.settings-sensitive my-global-icon ::ng-deep svg {
margin-top: 0px !important;
}
}
}

View file

@ -9,6 +9,7 @@ import { AuthService, AuthStatus, AuthUser, MenuService, RedirectService, Screen
import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component'
import { ServerConfig, UserRight, VideoConstant } from '@shared/models'
import { NgbDropdown, NgbDropdownConfig } from '@ng-bootstrap/ng-bootstrap'
const logger = debug('peertube:menu:MenuComponent')
@ -20,6 +21,7 @@ const logger = debug('peertube:menu:MenuComponent')
export class MenuComponent implements OnInit {
@ViewChild('languageChooserModal', { static: true }) languageChooserModal: LanguageChooserComponent
@ViewChild('quickSettingsModal', { static: true }) quickSettingsModal: QuickSettingsModalComponent
@ViewChild('dropdown') dropdown: NgbDropdown
user: AuthUser
isLoggedIn: boolean
@ -30,8 +32,6 @@ export class MenuComponent implements OnInit {
videoLanguages: string[] = []
nsfwPolicy: string
loggedInMorePlacement: string
currentInterfaceLanguage: string
private languages: VideoConstant<string>[] = []
@ -54,8 +54,27 @@ export class MenuComponent implements OnInit {
private hotkeysService: HotkeysService,
private screenService: ScreenService,
private menuService: MenuService,
private dropdownConfig: NgbDropdownConfig,
private router: Router
) { }
) {
this.dropdownConfig.container = 'body'
}
get isInMobileView () {
return this.screenService.isInMobileView()
}
get dropdownContainer () {
if (this.isInMobileView) {
return null
} else {
return this.dropdownConfig.container
}
}
get language () {
return this.languageChooserModal.getCurrentLanguage()
}
get instanceName () {
return this.serverConfig.instance.name
@ -76,10 +95,6 @@ export class MenuComponent implements OnInit {
this.computeAdminAccess()
this.loggedInMorePlacement = this.screenService.isInMobileView()
? 'left-top auto'
: 'right-top auto'
this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage()
this.authService.loginChangedSource.subscribe(
@ -203,6 +218,29 @@ export class MenuComponent implements OnInit {
}
}
// Lock menu scroll when menu scroll to avoid fleeing / detached dropdown
onMenuScrollEvent () {
document.querySelector('menu').scrollTo(0, 0)
}
onDropdownOpenChange (opened: boolean) {
if (this.screenService.isInMobileView()) return
// Close dropdown when window scroll to avoid dropdown quick jump for re-position
const onWindowScroll = () => {
this.dropdown.close()
window.removeEventListener('scroll', onWindowScroll)
}
if (opened) {
window.addEventListener('scroll', onWindowScroll)
document.querySelector('menu').scrollTo(0, 0) // Reset menu scroll to easy lock
document.querySelector('menu').addEventListener('scroll', this.onMenuScrollEvent)
} else {
document.querySelector('menu').removeEventListener('scroll', this.onMenuScrollEvent)
}
}
private buildUserLanguages () {
if (!this.user) {
this.videoLanguages = []

View file

@ -1,10 +1,19 @@
<div
[ngbPopover]="popContent" autoClose="outside" placement="bottom-left" container="body" popoverClass="popover-notifications"
i18n-title title="View your notifications" class="notification-avatar" #popover="ngbPopover" (hidden)="onPopoverHidden()"
[ngbPopover]="popContent" autoClose="outside" placement="bottom" container={this} popoverClass="popover-notifications"
i18n-title title="View your notifications" [ngClass]="{ 'notification-inbox-popover': true, 'shown': opened, 'hidden': isInMobileView }"
#popover="ngbPopover" (shown)="onPopoverShown()" (hidden)="onPopoverHidden()"
>
<div *ngIf="unreadNotifications > 0" class="unread-notifications">{{ unreadNotifications }}</div>
<img [src]="user.accountAvatarUrl" alt="Avatar" />
<my-global-icon iconName="bell"></my-global-icon>
</div>
<div *ngIf="isInMobileView" i18n-title title="View your notifications" class="notification-inbox-link">
<div *ngIf="unreadNotifications > 0" class="unread-notifications">{{ unreadNotifications }}</div>
<a routerLink="/my-account/notifications" routerLinkActive="active" #link (click)="onNavigate(link)">
<my-global-icon iconName="bell"></my-global-icon>
</a>
</div>
<ng-template #popContent>
@ -15,7 +24,7 @@
<div>
<button
*ngIf="unreadNotifications"
i18n-title title="Mark all as read" class="glyphicon glyphicon-inbox mr-2"
i18n-title title="Mark all as read" class="glyphicon glyphicon-ok mr-2"
(click)="markAllAsRead()"
></button>
<a
@ -36,7 +45,7 @@
></my-user-notifications>
<a *ngIf="loaded" class="all-notifications" routerLink="/my-account/notifications" #notifications (click)="onNavigate(notifications)">
<my-global-icon class="mr-1" iconName="inbox-full" aria-hidden="true"></my-global-icon>
<my-global-icon class="mr-1" iconName="bell" aria-hidden="true"></my-global-icon>
<span i18n>See all your notifications</span>
</a>
</div>

View file

@ -1,17 +1,62 @@
@import '_variables';
@import '_mixins';
.notification-inbox-popover {
padding: 10px;
}
.notification-inbox-link a {
padding: 13px 10px;
}
.notification-inbox-popover,
.notification-inbox-link a {
@include apply-svg-color(#808080);
::ng-deep {
svg {
transition: color .1s ease-in-out;
}
}
transition: all .1s ease-in-out;
border-radius: 25px;
cursor: pointer;
&:hover, &:active {
background-color: rgba(255, 255, 255, 0.15);
@include apply-svg-color(#fff);
}
}
.notification-inbox-popover.shown,
.notification-inbox-link a.active {
@include apply-svg-color(#fff);
background-color: rgba(255, 255, 255, 0.28);
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325);
}
.notification-inbox-popover.hidden {
display: none;
}
::ng-deep {
.popover-notifications.popover {
max-width: none;
top: -6px !important;
left: 7px !important;
.arrow {
display: none;
}
.popover-body {
padding: 0;
font-size: 14px;
font-family: $main-fonts;
width: 400px;
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.30);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.30);
.loader {
display: flex;
@ -42,19 +87,22 @@
display: flex;
justify-content: space-between;
background-color: rgba(0, 0, 0, 0.10);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
align-items: center;
padding: 0 10px;
font-size: 16px;
min-height: 50px;
padding: 0 12px;
font-size: 14px;
font-weight: bold;
color: rgba(0, 0, 0, 0.5);
text-transform: uppercase;
min-height: 40px;
a {
@include disable-default-a-behaviour;
}
button {
@include peertube-button;
padding: 0;
background: transparent;
}
@ -82,25 +130,23 @@
}
}
.notification-avatar {
.notification-inbox-popover, .notification-inbox-link {
cursor: pointer;
position: relative;
img,
.unread-notifications {
margin-left: 20px;
}
img {
@include avatar(34px);
margin-right: 10px;
}
.unread-notifications {
position: absolute;
top: -5px;
left: -5px;
top: 6px;
left: 0;
@media screen and (max-width: $mobile-view) {
top: -4px;
left: -2px;
}
display: flex;
align-items: center;
@ -116,19 +162,3 @@
height: 15px;
}
}
@media screen and (max-width: $mobile-view) {
::ng-deep {
.popover-notifications.popover {
left: unset !important;
.arrow {
left: calc(2em + 7px);
}
.popover-body {
width: 100vw;
}
}
}
}

View file

@ -1,24 +1,24 @@
import { Subject, Subscription } from 'rxjs'
import { filter } from 'rxjs/operators'
import { Component, EventEmitter, Input, Output, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { Component, EventEmitter, Output, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { NavigationEnd, Router } from '@angular/router'
import { Notifier, User, PeerTubeSocket } from '@app/core'
import { Notifier, PeerTubeSocket, ScreenService } from '@app/core'
import { UserNotificationService } from '@app/shared/shared-main'
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
@Component({
selector: 'my-avatar-notification',
templateUrl: './avatar-notification.component.html',
styleUrls: [ './avatar-notification.component.scss' ]
selector: 'my-notification',
templateUrl: './notification.component.html',
styleUrls: [ './notification.component.scss' ]
})
export class AvatarNotificationComponent implements OnInit, OnDestroy {
export class NotificationComponent implements OnInit, OnDestroy {
@ViewChild('popover', { static: true }) popover: NgbPopover
@Input() user: User
@Output() navigate = new EventEmitter<HTMLAnchorElement>()
unreadNotifications = 0
loaded = false
opened = false
markAllAsReadSubject = new Subject<boolean>()
@ -27,6 +27,7 @@ export class AvatarNotificationComponent implements OnInit, OnDestroy {
constructor (
private userNotificationService: UserNotificationService,
private screenService: ScreenService,
private peertubeSocket: PeerTubeSocket,
private notifier: Notifier,
private router: Router
@ -54,12 +55,31 @@ export class AvatarNotificationComponent implements OnInit, OnDestroy {
if (this.routeSub) this.routeSub.unsubscribe()
}
get isInMobileView () {
return this.screenService.isInMobileView()
}
closePopover () {
this.popover.close()
}
onPopoverShown () {
this.opened = true
document.querySelector('menu').scrollTo(0, 0) // Reset menu scroll to easy lock
document.querySelector('menu').addEventListener('scroll', this.onMenuScrollEvent)
}
onPopoverHidden () {
this.loaded = false
this.opened = false
document.querySelector('menu').removeEventListener('scroll', this.onMenuScrollEvent)
}
// Lock menu scroll when menu scroll to avoid fleeing / detached dropdown
onMenuScrollEvent () {
document.querySelector('menu').scrollTo(0, 0)
}
onNotificationLoaded () {
@ -67,6 +87,7 @@ export class AvatarNotificationComponent implements OnInit, OnDestroy {
}
onNavigate (link: HTMLAnchorElement) {
this.closePopover()
this.navigate.emit(link)
}
@ -83,5 +104,4 @@ export class AvatarNotificationComponent implements OnInit, OnDestroy {
if (data.type === 'read-all') return this.unreadNotifications = 0
})
}
}

View file

@ -36,7 +36,7 @@ const icons = {
'clock': require('!!raw-loader?!../../../assets/images/feather/clock.svg').default,
'cog': require('!!raw-loader?!../../../assets/images/feather/cog.svg').default,
'delete': require('!!raw-loader?!../../../assets/images/feather/delete.svg').default,
'inbox-full': require('!!raw-loader?!../../../assets/images/feather/inbox-full.svg').default,
'bell': require('!!raw-loader?!../../../assets/images/feather/bell.svg').default,
'sign-out': require('!!raw-loader?!../../../assets/images/feather/log-out.svg').default,
'sign-in': require('!!raw-loader?!../../../assets/images/feather/log-in.svg').default,
'download': require('!!raw-loader?!../../../assets/images/feather/download.svg').default,

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bell"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>

After

Width:  |  Height:  |  Size: 321 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-inbox"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"></polyline><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path></svg>

Before

Width:  |  Height:  |  Size: 405 B