From 51a83970061b4005343d2bfc4edb883318ef2ca6 Mon Sep 17 00:00:00 2001 From: Kimsible <1877318+kimsible@users.noreply.github.com> Date: Sun, 13 Dec 2020 14:54:12 +0100 Subject: 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 Co-authored-by: Rigel Kent --- .../my-account-notifications.component.html | 2 +- client/src/app/app.module.ts | 4 +- .../app/menu/avatar-notification.component.html | 43 ------ .../app/menu/avatar-notification.component.scss | 134 ----------------- .../src/app/menu/avatar-notification.component.ts | 87 ----------- client/src/app/menu/index.ts | 2 +- client/src/app/menu/menu.component.html | 40 +++-- client/src/app/menu/menu.component.scss | 133 ++++++++++++++--- client/src/app/menu/menu.component.ts | 52 ++++++- client/src/app/menu/notification.component.html | 52 +++++++ client/src/app/menu/notification.component.scss | 164 +++++++++++++++++++++ client/src/app/menu/notification.component.ts | 107 ++++++++++++++ .../shared/shared-icons/global-icon.component.ts | 2 +- 13 files changed, 502 insertions(+), 320 deletions(-) delete mode 100644 client/src/app/menu/avatar-notification.component.html delete mode 100644 client/src/app/menu/avatar-notification.component.scss delete mode 100644 client/src/app/menu/avatar-notification.component.ts create mode 100644 client/src/app/menu/notification.component.html create mode 100644 client/src/app/menu/notification.component.scss create mode 100644 client/src/app/menu/notification.component.ts (limited to 'client/src/app') diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html index a60ed885d..f0e9f4010 100644 --- a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html @@ -15,7 +15,7 @@ - - - - -
- -
- - - - - - See all your notifications - - - diff --git a/client/src/app/menu/avatar-notification.component.scss b/client/src/app/menu/avatar-notification.component.scss deleted file mode 100644 index 88f2b6296..000000000 --- a/client/src/app/menu/avatar-notification.component.scss +++ /dev/null @@ -1,134 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -::ng-deep { - .popover-notifications.popover { - max-width: none; - left: 7px !important; - - .popover-body { - padding: 0; - font-size: 14px; - font-family: $main-fonts; - width: 400px; - box-shadow: 0 6px 14px rgba(0, 0, 0, 0.30); - - .loader { - display: flex; - align-items: center; - justify-content: center; - - padding: 5px 0; - } - - .content { - max-height: 150px; - transition: max-height 0.15s ease-out; - display: flex; - height: 500px; - flex-direction: column; - - &.loaded { - max-height: 500px; - } - - & > my-user-notifications:nth-child(2) { - overflow-y: auto; - flex-grow: 1; - } - } - - .notifications-header { - display: flex; - justify-content: space-between; - - background-color: rgba(0, 0, 0, 0.10); - align-items: center; - padding: 0 10px; - font-size: 16px; - min-height: 50px; - - a { - @include disable-default-a-behaviour; - } - - button { - @include peertube-button; - - padding: 0; - background: transparent; - } - - a, button { - color: rgba(20, 20, 20, 0.5); - - &:hover:not(:disabled) { - color: rgba(20, 20, 20, 0.8); - } - } - } - - .all-notifications { - display: flex; - align-items: center; - justify-content: center; - font-weight: $font-semibold; - color: $fg-color; - padding: 7px 0; - margin-top: auto; - text-decoration: none; - } - } - } -} - -.notification-avatar { - 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; - - display: flex; - align-items: center; - justify-content: center; - - background-color: pvar(--mainColor); - color: #fff; - font-size: 10px; - font-weight: $font-semibold; - - border-radius: 15px; - width: 15px; - 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; - } - } - } -} diff --git a/client/src/app/menu/avatar-notification.component.ts b/client/src/app/menu/avatar-notification.component.ts deleted file mode 100644 index ed3ffc2d8..000000000 --- a/client/src/app/menu/avatar-notification.component.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Subject, Subscription } from 'rxjs' -import { filter } from 'rxjs/operators' -import { Component, EventEmitter, Input, Output, OnDestroy, OnInit, ViewChild } from '@angular/core' -import { NavigationEnd, Router } from '@angular/router' -import { Notifier, User, PeerTubeSocket } 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' ] -}) -export class AvatarNotificationComponent implements OnInit, OnDestroy { - @ViewChild('popover', { static: true }) popover: NgbPopover - - @Input() user: User - @Output() navigate = new EventEmitter() - - unreadNotifications = 0 - loaded = false - - markAllAsReadSubject = new Subject() - - private notificationSub: Subscription - private routeSub: Subscription - - constructor ( - private userNotificationService: UserNotificationService, - private peertubeSocket: PeerTubeSocket, - private notifier: Notifier, - private router: Router - ) { - } - - ngOnInit () { - this.userNotificationService.countUnreadNotifications() - .subscribe( - result => { - this.unreadNotifications = Math.min(result, 99) // Limit number to 99 - this.subscribeToNotifications() - }, - - err => this.notifier.error(err.message) - ) - - this.routeSub = this.router.events - .pipe(filter(event => event instanceof NavigationEnd)) - .subscribe(() => this.closePopover()) - } - - ngOnDestroy () { - if (this.notificationSub) this.notificationSub.unsubscribe() - if (this.routeSub) this.routeSub.unsubscribe() - } - - closePopover () { - this.popover.close() - } - - onPopoverHidden () { - this.loaded = false - } - - onNotificationLoaded () { - this.loaded = true - } - - onNavigate (link: HTMLAnchorElement) { - this.navigate.emit(link) - } - - markAllAsRead () { - this.markAllAsReadSubject.next(true) - } - - private async subscribeToNotifications () { - const obs = await this.peertubeSocket.getMyNotificationsSocket() - - this.notificationSub = obs.subscribe(data => { - if (data.type === 'new') return this.unreadNotifications++ - if (data.type === 'read') return this.unreadNotifications-- - if (data.type === 'read-all') return this.unreadNotifications = 0 - }) - } - -} diff --git a/client/src/app/menu/index.ts b/client/src/app/menu/index.ts index 39dbde750..7f62c1100 100644 --- a/client/src/app/menu/index.ts +++ b/client/src/app/menu/index.ts @@ -1,3 +1,3 @@ export * from './language-chooser.component' -export * from './avatar-notification.component' +export * from './notification.component' export * from './menu.component' diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index bba5fdadf..5b1c24c7a 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html @@ -3,32 +3,28 @@
- - - - -
- +
+
+ Avatar +
+
{{ user.account?.displayName }}
+ +
@{{ user.username }}
+
+ + +
+ +
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss index e79ecb5c7..89dc26e87 100644 --- a/client/src/app/menu/menu.component.scss +++ b/client/src/app/menu/menu.component.scss @@ -88,47 +88,118 @@ menu { height: 80px; display: flex; align-items: center; - justify-content: center; + justify-content: left; - .logged-in-info { - @include ellipsis; + .logged-in-more { + $main-radius: 25px; - flex-grow: 1; + margin-left: 13px; + border-radius: $main-radius; + transition: all .1s ease-in-out; + cursor: pointer; - .logged-in-display-name { - font-size: 16px; - font-weight: $font-semibold; - color: pvar(--menuForegroundColor); - cursor: pointer; + *, & { + line-height: 1; + } - @include disable-default-a-behaviour; + &.show { + background-color: rgba(255, 255, 255, 0.20); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325); } - .logged-in-username { - @include ellipsis; + @mixin display-hints($is-mobile: false) { + background-color: rgba(255, 255, 255, 0.15); - font-size: 13px; - color: #C6C6C6; - max-width: 140px; - cursor: pointer; + @if $is-mobile { + .dropdown-toggle-indicator { + display: inherit !important; + } + .dropdown-toggle:first-child { + padding-right: 30px !important; + } + } } - } - .logged-in-more { - margin-right: 20px; + &:hover { + @include display-hints; + } - my-global-icon.dropdown-toggle { - cursor: pointer; + /* 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; + } + } } diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index bdc95127b..50ff0e2b3 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts @@ -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[] = [] @@ -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 = [] diff --git a/client/src/app/menu/notification.component.html b/client/src/app/menu/notification.component.html new file mode 100644 index 000000000..beda1c43c --- /dev/null +++ b/client/src/app/menu/notification.component.html @@ -0,0 +1,52 @@ +
+
{{ unreadNotifications }}
+ + +
+ + + + +
+
+
Notifications
+ +
+ + +
+
+ +
+ +
+ + + + + + See all your notifications + +
+
diff --git a/client/src/app/menu/notification.component.scss b/client/src/app/menu/notification.component.scss new file mode 100644 index 000000000..40feb9e66 --- /dev/null +++ b/client/src/app/menu/notification.component.scss @@ -0,0 +1,164 @@ +@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 2px 6px rgba(0, 0, 0, 0.30); + + .loader { + display: flex; + align-items: center; + justify-content: center; + + padding: 5px 0; + } + + .content { + max-height: 150px; + transition: max-height 0.15s ease-out; + display: flex; + height: 500px; + flex-direction: column; + + &.loaded { + max-height: 500px; + } + + & > my-user-notifications:nth-child(2) { + overflow-y: auto; + flex-grow: 1; + } + } + + .notifications-header { + display: flex; + justify-content: space-between; + + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + align-items: center; + 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; + } + + a, button { + color: rgba(20, 20, 20, 0.5); + + &:hover:not(:disabled) { + color: rgba(20, 20, 20, 0.8); + } + } + } + + .all-notifications { + display: flex; + align-items: center; + justify-content: center; + font-weight: $font-semibold; + color: $fg-color; + padding: 7px 0; + margin-top: auto; + text-decoration: none; + } + } + } +} + +.notification-inbox-popover, .notification-inbox-link { + cursor: pointer; + position: relative; + + .unread-notifications { + margin-left: 20px; + } + + .unread-notifications { + position: absolute; + top: 6px; + left: 0; + + @media screen and (max-width: $mobile-view) { + top: -4px; + left: -2px; + } + + display: flex; + align-items: center; + justify-content: center; + + background-color: pvar(--mainColor); + color: #fff; + font-size: 10px; + font-weight: $font-semibold; + + border-radius: 15px; + width: 15px; + height: 15px; + } +} diff --git a/client/src/app/menu/notification.component.ts b/client/src/app/menu/notification.component.ts new file mode 100644 index 000000000..b7d9e9abb --- /dev/null +++ b/client/src/app/menu/notification.component.ts @@ -0,0 +1,107 @@ +import { Subject, Subscription } from 'rxjs' +import { filter } from 'rxjs/operators' +import { Component, EventEmitter, Output, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { NavigationEnd, Router } from '@angular/router' +import { Notifier, PeerTubeSocket, ScreenService } from '@app/core' +import { UserNotificationService } from '@app/shared/shared-main' +import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' + +@Component({ + selector: 'my-notification', + templateUrl: './notification.component.html', + styleUrls: [ './notification.component.scss' ] +}) +export class NotificationComponent implements OnInit, OnDestroy { + @ViewChild('popover', { static: true }) popover: NgbPopover + + @Output() navigate = new EventEmitter() + + unreadNotifications = 0 + loaded = false + opened = false + + markAllAsReadSubject = new Subject() + + private notificationSub: Subscription + private routeSub: Subscription + + constructor ( + private userNotificationService: UserNotificationService, + private screenService: ScreenService, + private peertubeSocket: PeerTubeSocket, + private notifier: Notifier, + private router: Router + ) { + } + + ngOnInit () { + this.userNotificationService.countUnreadNotifications() + .subscribe( + result => { + this.unreadNotifications = Math.min(result, 99) // Limit number to 99 + this.subscribeToNotifications() + }, + + err => this.notifier.error(err.message) + ) + + this.routeSub = this.router.events + .pipe(filter(event => event instanceof NavigationEnd)) + .subscribe(() => this.closePopover()) + } + + ngOnDestroy () { + if (this.notificationSub) this.notificationSub.unsubscribe() + 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 () { + this.loaded = true + } + + onNavigate (link: HTMLAnchorElement) { + this.closePopover() + this.navigate.emit(link) + } + + markAllAsRead () { + this.markAllAsReadSubject.next(true) + } + + private async subscribeToNotifications () { + const obs = await this.peertubeSocket.getMyNotificationsSocket() + + this.notificationSub = obs.subscribe(data => { + if (data.type === 'new') return this.unreadNotifications++ + if (data.type === 'read') return this.unreadNotifications-- + if (data.type === 'read-all') return this.unreadNotifications = 0 + }) + } +} diff --git a/client/src/app/shared/shared-icons/global-icon.component.ts b/client/src/app/shared/shared-icons/global-icon.component.ts index 53a2aee9a..0924b8119 100644 --- a/client/src/app/shared/shared-icons/global-icon.component.ts +++ b/client/src/app/shared/shared-icons/global-icon.component.ts @@ -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, -- cgit v1.2.3