diff options
author | Kimsible <1877318+kimsible@users.noreply.github.com> | 2020-12-13 14:54:12 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-12-13 14:54:12 +0100 |
commit | 51a83970061b4005343d2bfc4edb883318ef2ca6 (patch) | |
tree | c33753d0dc51bc534706b0839cf567d673294f29 /client/src | |
parent | 75594f474a3e04086b37a014d23e89780ca67458 (diff) | |
download | PeerTube-51a83970061b4005343d2bfc4edb883318ef2ca6.tar.gz PeerTube-51a83970061b4005343d2bfc4edb883318ef2ca6.tar.zst PeerTube-51a83970061b4005343d2bfc4edb883318ef2ca6.zip |
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>
Diffstat (limited to 'client/src')
-rw-r--r-- | client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html | 2 | ||||
-rw-r--r-- | client/src/app/app.module.ts | 4 | ||||
-rw-r--r-- | client/src/app/menu/index.ts | 2 | ||||
-rw-r--r-- | client/src/app/menu/menu.component.html | 40 | ||||
-rw-r--r-- | client/src/app/menu/menu.component.scss | 133 | ||||
-rw-r--r-- | client/src/app/menu/menu.component.ts | 52 | ||||
-rw-r--r-- | client/src/app/menu/notification.component.html (renamed from client/src/app/menu/avatar-notification.component.html) | 19 | ||||
-rw-r--r-- | client/src/app/menu/notification.component.scss (renamed from client/src/app/menu/avatar-notification.component.scss) | 96 | ||||
-rw-r--r-- | client/src/app/menu/notification.component.ts (renamed from client/src/app/menu/avatar-notification.component.ts) | 36 | ||||
-rw-r--r-- | client/src/app/shared/shared-icons/global-icon.component.ts | 2 | ||||
-rw-r--r-- | client/src/assets/images/feather/bell.svg | 1 | ||||
-rw-r--r-- | client/src/assets/images/feather/inbox-full.svg | 1 |
12 files changed, 285 insertions, 103 deletions
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 @@ | |||
15 | 15 | ||
16 | <button class="btn ml-auto" [disabled]="!hasUnreadNotifications()" (click)="markAllAsRead()"> | 16 | <button class="btn ml-auto" [disabled]="!hasUnreadNotifications()" (click)="markAllAsRead()"> |
17 | <ng-container *ngIf="hasUnreadNotifications()"> | 17 | <ng-container *ngIf="hasUnreadNotifications()"> |
18 | <my-global-icon iconName="inbox-full" aria-hidden="true"></my-global-icon> | 18 | <my-global-icon iconName="tick" aria-hidden="true"></my-global-icon> |
19 | 19 | ||
20 | <span i18n>Mark all as read</span> | 20 | <span i18n>Mark all as read</span> |
21 | </ng-container> | 21 | </ng-container> |
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 3f874856d..98dec4244 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts | |||
@@ -11,7 +11,7 @@ import { CoreModule } from './core' | |||
11 | import { EmptyComponent } from './empty.component' | 11 | import { EmptyComponent } from './empty.component' |
12 | import { HeaderComponent, SearchTypeaheadComponent, SuggestionComponent } from './header' | 12 | import { HeaderComponent, SearchTypeaheadComponent, SuggestionComponent } from './header' |
13 | import { HighlightPipe } from './header/highlight.pipe' | 13 | import { HighlightPipe } from './header/highlight.pipe' |
14 | import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' | 14 | import { NotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' |
15 | import { ConfirmComponent } from './modal/confirm.component' | 15 | import { ConfirmComponent } from './modal/confirm.component' |
16 | import { CustomModalComponent } from './modal/custom-modal.component' | 16 | import { CustomModalComponent } from './modal/custom-modal.component' |
17 | import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component' | 17 | import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component' |
@@ -35,7 +35,7 @@ registerLocaleData(localeOc, 'oc') | |||
35 | MenuComponent, | 35 | MenuComponent, |
36 | LanguageChooserComponent, | 36 | LanguageChooserComponent, |
37 | QuickSettingsModalComponent, | 37 | QuickSettingsModalComponent, |
38 | AvatarNotificationComponent, | 38 | NotificationComponent, |
39 | HeaderComponent, | 39 | HeaderComponent, |
40 | SearchTypeaheadComponent, | 40 | SearchTypeaheadComponent, |
41 | SuggestionComponent, | 41 | SuggestionComponent, |
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 @@ | |||
1 | export * from './language-chooser.component' | 1 | export * from './language-chooser.component' |
2 | export * from './avatar-notification.component' | 2 | export * from './notification.component' |
3 | export * from './menu.component' | 3 | 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 @@ | |||
3 | <div class="top-menu"> | 3 | <div class="top-menu"> |
4 | <div *ngIf="isLoggedIn" class="logged-in-block"> | 4 | <div *ngIf="isLoggedIn" class="logged-in-block"> |
5 | <div> | 5 | <div> |
6 | <my-avatar-notification [user]="user" (navigate)="onActiveLinkScrollToAnchor($event)"></my-avatar-notification> | 6 | <div class="logged-in-more" ngbDropdown #dropdown="ngbDropdown" placement="bottom-left" [container]="dropdownContainer" (openChange)="onDropdownOpenChange($event)" autoClose="outside"> |
7 | 7 | <div ngbDropdownToggle> | |
8 | <div class="logged-in-info"> | 8 | <img [src]="user.accountAvatarUrl" alt="Avatar" /> |
9 | <a *ngIf="user.account" [routerLink]="[ '/accounts', user.account.nameWithHost ]" class="logged-in-display-name">{{ user.account?.displayName }}</a> | 9 | <div class="logged-in-info"> |
10 | <a *ngIf="!user.account" routerLink="/my-account/settings" class="logged-in-display-name">{{ user.account?.displayName }}</a> | 10 | <div class="logged-in-display-name">{{ user.account?.displayName }}</div> |
11 | 11 | ||
12 | <div class="logged-in-username">@{{ user.username }}</div> | 12 | <div class="logged-in-username">@{{ user.username }}</div> |
13 | </div> | 13 | </div> |
14 | 14 | ||
15 | <div class="logged-in-more" ngbDropdown [placement]="loggedInMorePlacement" container="body" autoClose="outside"> | 15 | <div class="dropdown-toggle-indicator"> |
16 | <my-global-icon iconName="more-vertical" ngbDropdownToggle role="button"></my-global-icon> | 16 | <span class="glyphicon glyphicon-chevron-down"></span> |
17 | </div> | ||
18 | </div> | ||
17 | 19 | ||
18 | <div ngbDropdownMenu> | 20 | <div ngbDropdownMenu> |
19 | <a *ngIf="user.account" ngbDropdownItem ngbDropdownToggle class="dropdown-item" [routerLink]="[ '/accounts', user.account.nameWithHost ]"> | 21 | <a *ngIf="user.account" ngbDropdownItem ngbDropdownToggle class="dropdown-item" [routerLink]="[ '/accounts', user.account.nameWithHost ]" |
22 | #profile (click)="onActiveLinkScrollToAnchor(profile)"> | ||
20 | <my-global-icon iconName="go" aria-hidden="true"></my-global-icon> <ng-container i18n>Public profile</ng-container> | 23 | <my-global-icon iconName="go" aria-hidden="true"></my-global-icon> <ng-container i18n>Public profile</ng-container> |
21 | </a> | 24 | </a> |
22 | 25 | ||
23 | <div class="dropdown-divider"></div> | 26 | <div class="dropdown-divider"></div> |
24 | 27 | ||
25 | <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" routerLink="/my-account/notifications" | ||
26 | #notifications (click)="onActiveLinkScrollToAnchor(notifications)"> | ||
27 | <my-global-icon iconName="inbox-full" aria-hidden="true"></my-global-icon> <ng-container i18n>My notifications</ng-container> | ||
28 | </a> | ||
29 | |||
30 | <div class="dropdown-divider"></div> | ||
31 | |||
32 | <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openLanguageChooser()"> | 28 | <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openLanguageChooser()"> |
33 | <my-global-icon iconName="language" aria-hidden="true"></my-global-icon> | 29 | <my-global-icon iconName="language" aria-hidden="true"></my-global-icon> |
34 | <span i18n>Interface:</span> | 30 | <span i18n>Interface:</span> |
@@ -42,7 +38,7 @@ | |||
42 | <span class="ml-auto text-muted">{{ videoLanguages.join(', ') }}</span> | 38 | <span class="ml-auto text-muted">{{ videoLanguages.join(', ') }}</span> |
43 | </a> | 39 | </a> |
44 | 40 | ||
45 | <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" routerLink="/my-account/settings" | 41 | <a ngbDropdownItem ngbDropdownToggle class="dropdown-item settings-sensitive" routerLink="/my-account/settings" |
46 | fragment="video-sensitive-content-policy" #settingsSensitiveContentPolicy | 42 | fragment="video-sensitive-content-policy" #settingsSensitiveContentPolicy |
47 | (click)="onActiveLinkScrollToAnchor(settingsSensitiveContentPolicy)"> | 43 | (click)="onActiveLinkScrollToAnchor(settingsSensitiveContentPolicy)"> |
48 | <my-global-icon class="hover-display-toggle" [ngClass]="{ 'not-displayed': user.nsfwPolicy === 'display' }" iconName="sensitive" aria-hidden="true"></my-global-icon> | 44 | <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 @@ | |||
60 | 56 | ||
61 | <div class="dropdown-divider"></div> | 57 | <div class="dropdown-divider"></div> |
62 | 58 | ||
63 | <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openHotkeysCheatSheet()"> | 59 | <a *ngIf="!isInMobileView" ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openHotkeysCheatSheet()"> |
64 | <my-global-icon iconName="command" aria-hidden="true"></my-global-icon> | 60 | <my-global-icon iconName="command" aria-hidden="true"></my-global-icon> |
65 | <ng-container i18n>Keyboard shortcuts</ng-container> | 61 | <ng-container i18n>Keyboard shortcuts</ng-container> |
66 | </a> | 62 | </a> |
@@ -71,6 +67,8 @@ | |||
71 | </a> | 67 | </a> |
72 | </div> | 68 | </div> |
73 | </div> | 69 | </div> |
70 | |||
71 | <my-notification (navigate)="onActiveLinkScrollToAnchor($event)"></my-notification> | ||
74 | </div> | 72 | </div> |
75 | 73 | ||
76 | <div class="logged-in-menu"> | 74 | <div class="logged-in-menu"> |
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 { | |||
88 | height: 80px; | 88 | height: 80px; |
89 | display: flex; | 89 | display: flex; |
90 | align-items: center; | 90 | align-items: center; |
91 | justify-content: center; | 91 | justify-content: left; |
92 | 92 | ||
93 | .logged-in-info { | 93 | .logged-in-more { |
94 | @include ellipsis; | 94 | $main-radius: 25px; |
95 | 95 | ||
96 | flex-grow: 1; | 96 | margin-left: 13px; |
97 | border-radius: $main-radius; | ||
98 | transition: all .1s ease-in-out; | ||
99 | cursor: pointer; | ||
97 | 100 | ||
98 | .logged-in-display-name { | 101 | *, & { |
99 | font-size: 16px; | 102 | line-height: 1; |
100 | font-weight: $font-semibold; | 103 | } |
101 | color: pvar(--menuForegroundColor); | ||
102 | cursor: pointer; | ||
103 | 104 | ||
104 | @include disable-default-a-behaviour; | 105 | &.show { |
106 | background-color: rgba(255, 255, 255, 0.20); | ||
107 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325); | ||
105 | } | 108 | } |
106 | 109 | ||
107 | .logged-in-username { | 110 | @mixin display-hints($is-mobile: false) { |
108 | @include ellipsis; | 111 | background-color: rgba(255, 255, 255, 0.15); |
109 | 112 | ||
110 | font-size: 13px; | 113 | @if $is-mobile { |
111 | color: #C6C6C6; | 114 | .dropdown-toggle-indicator { |
112 | max-width: 140px; | 115 | display: inherit !important; |
113 | cursor: pointer; | 116 | } |
117 | .dropdown-toggle:first-child { | ||
118 | padding-right: 30px !important; | ||
119 | } | ||
120 | } | ||
114 | } | 121 | } |
115 | } | ||
116 | 122 | ||
117 | .logged-in-more { | 123 | &:hover { |
118 | margin-right: 20px; | 124 | @include display-hints; |
125 | } | ||
119 | 126 | ||
120 | my-global-icon.dropdown-toggle { | 127 | /* smartphones and touchscreens */ |
121 | cursor: pointer; | 128 | @media (hover: none) and (pointer: coarse) { |
129 | @include display-hints($is-mobile: true); | ||
122 | 130 | ||
131 | /* fill space when on mobile */ | ||
132 | max-width: calc(100% - 80px); | ||
133 | .dropdown-toggle { | ||
134 | max-width: 100%; | ||
135 | } | ||
136 | .logged-in-info { | ||
137 | max-width: calc(100% - 45px) !important; | ||
138 | } | ||
139 | |||
140 | } | ||
141 | |||
142 | .dropdown-toggle-indicator { | ||
143 | position: relative; | ||
144 | width: 0; | ||
145 | display: none; | ||
146 | |||
147 | span { | ||
148 | position: absolute; | ||
149 | right: -35px; | ||
150 | top: -8px; | ||
151 | color: grey; | ||
152 | width: $main-radius; | ||
153 | } | ||
154 | } | ||
155 | |||
156 | .dropdown-toggle { | ||
123 | &::after { | 157 | &::after { |
124 | border: none; | 158 | border: none; |
125 | } | 159 | } |
160 | } | ||
126 | 161 | ||
127 | ::ng-deep { | 162 | .dropdown-toggle:first-child { |
128 | @include apply-svg-color(pvar(--menuForegroundColor)); | 163 | display: inline-flex; |
164 | align-items: center; | ||
165 | padding: 5px 7px; | ||
166 | } | ||
167 | |||
168 | img { | ||
169 | @include avatar(34px); | ||
170 | |||
171 | margin-right: 10px; | ||
172 | } | ||
173 | |||
174 | .logged-in-info { | ||
175 | max-width: 105px; | ||
176 | |||
177 | flex-grow: 1; | ||
178 | |||
179 | .logged-in-display-name, | ||
180 | .logged-in-username { | ||
181 | @include ellipsis; | ||
182 | } | ||
183 | |||
184 | .logged-in-display-name { | ||
185 | font-size: 16px; | ||
186 | font-weight: $font-semibold; | ||
187 | color: pvar(--menuForegroundColor); | ||
188 | |||
189 | @include disable-default-a-behaviour; | ||
190 | } | ||
191 | |||
192 | .logged-in-username { | ||
193 | font-size: 13px; | ||
194 | color: #C6C6C6; | ||
129 | } | 195 | } |
130 | } | 196 | } |
131 | } | 197 | } |
198 | |||
199 | my-notification { | ||
200 | margin-left: auto; | ||
201 | margin-right: 15px; | ||
202 | } | ||
132 | } | 203 | } |
133 | 204 | ||
134 | .logged-in-menu { | 205 | .logged-in-menu { |
@@ -343,6 +414,12 @@ menu { | |||
343 | my-global-icon.hover-display-toggle { | 414 | my-global-icon.hover-display-toggle { |
344 | display: none; | 415 | display: none; |
345 | } | 416 | } |
417 | |||
418 | &.settings-sensitive { | ||
419 | my-global-icon ::ng-deep svg { | ||
420 | margin-top: 2px !important; | ||
421 | } | ||
422 | } | ||
346 | } | 423 | } |
347 | } | 424 | } |
348 | 425 | ||
@@ -364,4 +441,14 @@ menu { | |||
364 | .top-menu, .footer { | 441 | .top-menu, .footer { |
365 | width: 100% !important; | 442 | width: 100% !important; |
366 | } | 443 | } |
444 | |||
445 | .dropdown-menu { | ||
446 | width: calc(100vw - 30px); | ||
447 | } | ||
448 | |||
449 | .dropdown-item:hover, .dropdown-item:active { | ||
450 | &.settings-sensitive my-global-icon ::ng-deep svg { | ||
451 | margin-top: 0px !important; | ||
452 | } | ||
453 | } | ||
367 | } | 454 | } |
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 | |||
9 | import { LanguageChooserComponent } from '@app/menu/language-chooser.component' | 9 | import { LanguageChooserComponent } from '@app/menu/language-chooser.component' |
10 | import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component' | 10 | import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component' |
11 | import { ServerConfig, UserRight, VideoConstant } from '@shared/models' | 11 | import { ServerConfig, UserRight, VideoConstant } from '@shared/models' |
12 | import { NgbDropdown, NgbDropdownConfig } from '@ng-bootstrap/ng-bootstrap' | ||
12 | 13 | ||
13 | const logger = debug('peertube:menu:MenuComponent') | 14 | const logger = debug('peertube:menu:MenuComponent') |
14 | 15 | ||
@@ -20,6 +21,7 @@ const logger = debug('peertube:menu:MenuComponent') | |||
20 | export class MenuComponent implements OnInit { | 21 | export class MenuComponent implements OnInit { |
21 | @ViewChild('languageChooserModal', { static: true }) languageChooserModal: LanguageChooserComponent | 22 | @ViewChild('languageChooserModal', { static: true }) languageChooserModal: LanguageChooserComponent |
22 | @ViewChild('quickSettingsModal', { static: true }) quickSettingsModal: QuickSettingsModalComponent | 23 | @ViewChild('quickSettingsModal', { static: true }) quickSettingsModal: QuickSettingsModalComponent |
24 | @ViewChild('dropdown') dropdown: NgbDropdown | ||
23 | 25 | ||
24 | user: AuthUser | 26 | user: AuthUser |
25 | isLoggedIn: boolean | 27 | isLoggedIn: boolean |
@@ -30,8 +32,6 @@ export class MenuComponent implements OnInit { | |||
30 | videoLanguages: string[] = [] | 32 | videoLanguages: string[] = [] |
31 | nsfwPolicy: string | 33 | nsfwPolicy: string |
32 | 34 | ||
33 | loggedInMorePlacement: string | ||
34 | |||
35 | currentInterfaceLanguage: string | 35 | currentInterfaceLanguage: string |
36 | 36 | ||
37 | private languages: VideoConstant<string>[] = [] | 37 | private languages: VideoConstant<string>[] = [] |
@@ -54,8 +54,27 @@ export class MenuComponent implements OnInit { | |||
54 | private hotkeysService: HotkeysService, | 54 | private hotkeysService: HotkeysService, |
55 | private screenService: ScreenService, | 55 | private screenService: ScreenService, |
56 | private menuService: MenuService, | 56 | private menuService: MenuService, |
57 | private dropdownConfig: NgbDropdownConfig, | ||
57 | private router: Router | 58 | private router: Router |
58 | ) { } | 59 | ) { |
60 | this.dropdownConfig.container = 'body' | ||
61 | } | ||
62 | |||
63 | get isInMobileView () { | ||
64 | return this.screenService.isInMobileView() | ||
65 | } | ||
66 | |||
67 | get dropdownContainer () { | ||
68 | if (this.isInMobileView) { | ||
69 | return null | ||
70 | } else { | ||
71 | return this.dropdownConfig.container | ||
72 | } | ||
73 | } | ||
74 | |||
75 | get language () { | ||
76 | return this.languageChooserModal.getCurrentLanguage() | ||
77 | } | ||
59 | 78 | ||
60 | get instanceName () { | 79 | get instanceName () { |
61 | return this.serverConfig.instance.name | 80 | return this.serverConfig.instance.name |
@@ -76,10 +95,6 @@ export class MenuComponent implements OnInit { | |||
76 | 95 | ||
77 | this.computeAdminAccess() | 96 | this.computeAdminAccess() |
78 | 97 | ||
79 | this.loggedInMorePlacement = this.screenService.isInMobileView() | ||
80 | ? 'left-top auto' | ||
81 | : 'right-top auto' | ||
82 | |||
83 | this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage() | 98 | this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage() |
84 | 99 | ||
85 | this.authService.loginChangedSource.subscribe( | 100 | this.authService.loginChangedSource.subscribe( |
@@ -203,6 +218,29 @@ export class MenuComponent implements OnInit { | |||
203 | } | 218 | } |
204 | } | 219 | } |
205 | 220 | ||
221 | // Lock menu scroll when menu scroll to avoid fleeing / detached dropdown | ||
222 | onMenuScrollEvent () { | ||
223 | document.querySelector('menu').scrollTo(0, 0) | ||
224 | } | ||
225 | |||
226 | onDropdownOpenChange (opened: boolean) { | ||
227 | if (this.screenService.isInMobileView()) return | ||
228 | |||
229 | // Close dropdown when window scroll to avoid dropdown quick jump for re-position | ||
230 | const onWindowScroll = () => { | ||
231 | this.dropdown.close() | ||
232 | window.removeEventListener('scroll', onWindowScroll) | ||
233 | } | ||
234 | |||
235 | if (opened) { | ||
236 | window.addEventListener('scroll', onWindowScroll) | ||
237 | document.querySelector('menu').scrollTo(0, 0) // Reset menu scroll to easy lock | ||
238 | document.querySelector('menu').addEventListener('scroll', this.onMenuScrollEvent) | ||
239 | } else { | ||
240 | document.querySelector('menu').removeEventListener('scroll', this.onMenuScrollEvent) | ||
241 | } | ||
242 | } | ||
243 | |||
206 | private buildUserLanguages () { | 244 | private buildUserLanguages () { |
207 | if (!this.user) { | 245 | if (!this.user) { |
208 | this.videoLanguages = [] | 246 | this.videoLanguages = [] |
diff --git a/client/src/app/menu/avatar-notification.component.html b/client/src/app/menu/notification.component.html index b24bd0309..beda1c43c 100644 --- a/client/src/app/menu/avatar-notification.component.html +++ b/client/src/app/menu/notification.component.html | |||
@@ -1,10 +1,19 @@ | |||
1 | <div | 1 | <div |
2 | [ngbPopover]="popContent" autoClose="outside" placement="bottom-left" container="body" popoverClass="popover-notifications" | 2 | [ngbPopover]="popContent" autoClose="outside" placement="bottom" container={this} popoverClass="popover-notifications" |
3 | i18n-title title="View your notifications" class="notification-avatar" #popover="ngbPopover" (hidden)="onPopoverHidden()" | 3 | i18n-title title="View your notifications" [ngClass]="{ 'notification-inbox-popover': true, 'shown': opened, 'hidden': isInMobileView }" |
4 | #popover="ngbPopover" (shown)="onPopoverShown()" (hidden)="onPopoverHidden()" | ||
4 | > | 5 | > |
5 | <div *ngIf="unreadNotifications > 0" class="unread-notifications">{{ unreadNotifications }}</div> | 6 | <div *ngIf="unreadNotifications > 0" class="unread-notifications">{{ unreadNotifications }}</div> |
6 | 7 | ||
7 | <img [src]="user.accountAvatarUrl" alt="Avatar" /> | 8 | <my-global-icon iconName="bell"></my-global-icon> |
9 | </div> | ||
10 | |||
11 | <div *ngIf="isInMobileView" i18n-title title="View your notifications" class="notification-inbox-link"> | ||
12 | <div *ngIf="unreadNotifications > 0" class="unread-notifications">{{ unreadNotifications }}</div> | ||
13 | |||
14 | <a routerLink="/my-account/notifications" routerLinkActive="active" #link (click)="onNavigate(link)"> | ||
15 | <my-global-icon iconName="bell"></my-global-icon> | ||
16 | </a> | ||
8 | </div> | 17 | </div> |
9 | 18 | ||
10 | <ng-template #popContent> | 19 | <ng-template #popContent> |
@@ -15,7 +24,7 @@ | |||
15 | <div> | 24 | <div> |
16 | <button | 25 | <button |
17 | *ngIf="unreadNotifications" | 26 | *ngIf="unreadNotifications" |
18 | i18n-title title="Mark all as read" class="glyphicon glyphicon-inbox mr-2" | 27 | i18n-title title="Mark all as read" class="glyphicon glyphicon-ok mr-2" |
19 | (click)="markAllAsRead()" | 28 | (click)="markAllAsRead()" |
20 | ></button> | 29 | ></button> |
21 | <a | 30 | <a |
@@ -36,7 +45,7 @@ | |||
36 | ></my-user-notifications> | 45 | ></my-user-notifications> |
37 | 46 | ||
38 | <a *ngIf="loaded" class="all-notifications" routerLink="/my-account/notifications" #notifications (click)="onNavigate(notifications)"> | 47 | <a *ngIf="loaded" class="all-notifications" routerLink="/my-account/notifications" #notifications (click)="onNavigate(notifications)"> |
39 | <my-global-icon class="mr-1" iconName="inbox-full" aria-hidden="true"></my-global-icon> | 48 | <my-global-icon class="mr-1" iconName="bell" aria-hidden="true"></my-global-icon> |
40 | <span i18n>See all your notifications</span> | 49 | <span i18n>See all your notifications</span> |
41 | </a> | 50 | </a> |
42 | </div> | 51 | </div> |
diff --git a/client/src/app/menu/avatar-notification.component.scss b/client/src/app/menu/notification.component.scss index 88f2b6296..40feb9e66 100644 --- a/client/src/app/menu/avatar-notification.component.scss +++ b/client/src/app/menu/notification.component.scss | |||
@@ -1,17 +1,62 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | |||
5 | .notification-inbox-popover { | ||
6 | padding: 10px; | ||
7 | } | ||
8 | |||
9 | .notification-inbox-link a { | ||
10 | padding: 13px 10px; | ||
11 | } | ||
12 | |||
13 | .notification-inbox-popover, | ||
14 | .notification-inbox-link a { | ||
15 | @include apply-svg-color(#808080); | ||
16 | ::ng-deep { | ||
17 | svg { | ||
18 | transition: color .1s ease-in-out; | ||
19 | } | ||
20 | } | ||
21 | |||
22 | transition: all .1s ease-in-out; | ||
23 | border-radius: 25px; | ||
24 | cursor: pointer; | ||
25 | |||
26 | &:hover, &:active { | ||
27 | background-color: rgba(255, 255, 255, 0.15); | ||
28 | @include apply-svg-color(#fff); | ||
29 | } | ||
30 | } | ||
31 | |||
32 | .notification-inbox-popover.shown, | ||
33 | .notification-inbox-link a.active { | ||
34 | @include apply-svg-color(#fff); | ||
35 | |||
36 | background-color: rgba(255, 255, 255, 0.28); | ||
37 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325); | ||
38 | } | ||
39 | |||
40 | .notification-inbox-popover.hidden { | ||
41 | display: none; | ||
42 | } | ||
43 | |||
4 | ::ng-deep { | 44 | ::ng-deep { |
5 | .popover-notifications.popover { | 45 | .popover-notifications.popover { |
6 | max-width: none; | 46 | max-width: none; |
47 | top: -6px !important; | ||
7 | left: 7px !important; | 48 | left: 7px !important; |
8 | 49 | ||
50 | .arrow { | ||
51 | display: none; | ||
52 | } | ||
53 | |||
9 | .popover-body { | 54 | .popover-body { |
10 | padding: 0; | 55 | padding: 0; |
11 | font-size: 14px; | 56 | font-size: 14px; |
12 | font-family: $main-fonts; | 57 | font-family: $main-fonts; |
13 | width: 400px; | 58 | width: 400px; |
14 | box-shadow: 0 6px 14px rgba(0, 0, 0, 0.30); | 59 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.30); |
15 | 60 | ||
16 | .loader { | 61 | .loader { |
17 | display: flex; | 62 | display: flex; |
@@ -42,19 +87,22 @@ | |||
42 | display: flex; | 87 | display: flex; |
43 | justify-content: space-between; | 88 | justify-content: space-between; |
44 | 89 | ||
45 | background-color: rgba(0, 0, 0, 0.10); | 90 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); |
46 | align-items: center; | 91 | align-items: center; |
47 | padding: 0 10px; | 92 | padding: 0 12px; |
48 | font-size: 16px; | 93 | font-size: 14px; |
49 | min-height: 50px; | 94 | font-weight: bold; |
95 | color: rgba(0, 0, 0, 0.5); | ||
96 | text-transform: uppercase; | ||
97 | min-height: 40px; | ||
50 | 98 | ||
51 | a { | 99 | a { |
52 | @include disable-default-a-behaviour; | 100 | @include disable-default-a-behaviour; |
53 | } | 101 | } |
54 | 102 | ||
55 | button { | 103 | button { |
56 | @include peertube-button; | 104 | @include peertube-button; |
57 | 105 | ||
58 | padding: 0; | 106 | padding: 0; |
59 | background: transparent; | 107 | background: transparent; |
60 | } | 108 | } |
@@ -82,25 +130,23 @@ | |||
82 | } | 130 | } |
83 | } | 131 | } |
84 | 132 | ||
85 | .notification-avatar { | 133 | .notification-inbox-popover, .notification-inbox-link { |
86 | cursor: pointer; | 134 | cursor: pointer; |
87 | position: relative; | 135 | position: relative; |
88 | 136 | ||
89 | img, | ||
90 | .unread-notifications { | 137 | .unread-notifications { |
91 | margin-left: 20px; | 138 | margin-left: 20px; |
92 | } | 139 | } |
93 | 140 | ||
94 | img { | ||
95 | @include avatar(34px); | ||
96 | |||
97 | margin-right: 10px; | ||
98 | } | ||
99 | |||
100 | .unread-notifications { | 141 | .unread-notifications { |
101 | position: absolute; | 142 | position: absolute; |
102 | top: -5px; | 143 | top: 6px; |
103 | left: -5px; | 144 | left: 0; |
145 | |||
146 | @media screen and (max-width: $mobile-view) { | ||
147 | top: -4px; | ||
148 | left: -2px; | ||
149 | } | ||
104 | 150 | ||
105 | display: flex; | 151 | display: flex; |
106 | align-items: center; | 152 | align-items: center; |
@@ -116,19 +162,3 @@ | |||
116 | height: 15px; | 162 | height: 15px; |
117 | } | 163 | } |
118 | } | 164 | } |
119 | |||
120 | @media screen and (max-width: $mobile-view) { | ||
121 | ::ng-deep { | ||
122 | .popover-notifications.popover { | ||
123 | left: unset !important; | ||
124 | |||
125 | .arrow { | ||
126 | left: calc(2em + 7px); | ||
127 | } | ||
128 | |||
129 | .popover-body { | ||
130 | width: 100vw; | ||
131 | } | ||
132 | } | ||
133 | } | ||
134 | } | ||
diff --git a/client/src/app/menu/avatar-notification.component.ts b/client/src/app/menu/notification.component.ts index ed3ffc2d8..b7d9e9abb 100644 --- a/client/src/app/menu/avatar-notification.component.ts +++ b/client/src/app/menu/notification.component.ts | |||
@@ -1,24 +1,24 @@ | |||
1 | import { Subject, Subscription } from 'rxjs' | 1 | import { Subject, Subscription } from 'rxjs' |
2 | import { filter } from 'rxjs/operators' | 2 | import { filter } from 'rxjs/operators' |
3 | import { Component, EventEmitter, Input, Output, OnDestroy, OnInit, ViewChild } from '@angular/core' | 3 | import { Component, EventEmitter, Output, OnDestroy, OnInit, ViewChild } from '@angular/core' |
4 | import { NavigationEnd, Router } from '@angular/router' | 4 | import { NavigationEnd, Router } from '@angular/router' |
5 | import { Notifier, User, PeerTubeSocket } from '@app/core' | 5 | import { Notifier, PeerTubeSocket, ScreenService } from '@app/core' |
6 | import { UserNotificationService } from '@app/shared/shared-main' | 6 | import { UserNotificationService } from '@app/shared/shared-main' |
7 | import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' | 7 | import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' |
8 | 8 | ||
9 | @Component({ | 9 | @Component({ |
10 | selector: 'my-avatar-notification', | 10 | selector: 'my-notification', |
11 | templateUrl: './avatar-notification.component.html', | 11 | templateUrl: './notification.component.html', |
12 | styleUrls: [ './avatar-notification.component.scss' ] | 12 | styleUrls: [ './notification.component.scss' ] |
13 | }) | 13 | }) |
14 | export class AvatarNotificationComponent implements OnInit, OnDestroy { | 14 | export class NotificationComponent implements OnInit, OnDestroy { |
15 | @ViewChild('popover', { static: true }) popover: NgbPopover | 15 | @ViewChild('popover', { static: true }) popover: NgbPopover |
16 | 16 | ||
17 | @Input() user: User | ||
18 | @Output() navigate = new EventEmitter<HTMLAnchorElement>() | 17 | @Output() navigate = new EventEmitter<HTMLAnchorElement>() |
19 | 18 | ||
20 | unreadNotifications = 0 | 19 | unreadNotifications = 0 |
21 | loaded = false | 20 | loaded = false |
21 | opened = false | ||
22 | 22 | ||
23 | markAllAsReadSubject = new Subject<boolean>() | 23 | markAllAsReadSubject = new Subject<boolean>() |
24 | 24 | ||
@@ -27,6 +27,7 @@ export class AvatarNotificationComponent implements OnInit, OnDestroy { | |||
27 | 27 | ||
28 | constructor ( | 28 | constructor ( |
29 | private userNotificationService: UserNotificationService, | 29 | private userNotificationService: UserNotificationService, |
30 | private screenService: ScreenService, | ||
30 | private peertubeSocket: PeerTubeSocket, | 31 | private peertubeSocket: PeerTubeSocket, |
31 | private notifier: Notifier, | 32 | private notifier: Notifier, |
32 | private router: Router | 33 | private router: Router |
@@ -54,12 +55,31 @@ export class AvatarNotificationComponent implements OnInit, OnDestroy { | |||
54 | if (this.routeSub) this.routeSub.unsubscribe() | 55 | if (this.routeSub) this.routeSub.unsubscribe() |
55 | } | 56 | } |
56 | 57 | ||
58 | get isInMobileView () { | ||
59 | return this.screenService.isInMobileView() | ||
60 | } | ||
61 | |||
57 | closePopover () { | 62 | closePopover () { |
58 | this.popover.close() | 63 | this.popover.close() |
59 | } | 64 | } |
60 | 65 | ||
66 | onPopoverShown () { | ||
67 | this.opened = true | ||
68 | |||
69 | document.querySelector('menu').scrollTo(0, 0) // Reset menu scroll to easy lock | ||
70 | document.querySelector('menu').addEventListener('scroll', this.onMenuScrollEvent) | ||
71 | } | ||
72 | |||
61 | onPopoverHidden () { | 73 | onPopoverHidden () { |
62 | this.loaded = false | 74 | this.loaded = false |
75 | this.opened = false | ||
76 | |||
77 | document.querySelector('menu').removeEventListener('scroll', this.onMenuScrollEvent) | ||
78 | } | ||
79 | |||
80 | // Lock menu scroll when menu scroll to avoid fleeing / detached dropdown | ||
81 | onMenuScrollEvent () { | ||
82 | document.querySelector('menu').scrollTo(0, 0) | ||
63 | } | 83 | } |
64 | 84 | ||
65 | onNotificationLoaded () { | 85 | onNotificationLoaded () { |
@@ -67,6 +87,7 @@ export class AvatarNotificationComponent implements OnInit, OnDestroy { | |||
67 | } | 87 | } |
68 | 88 | ||
69 | onNavigate (link: HTMLAnchorElement) { | 89 | onNavigate (link: HTMLAnchorElement) { |
90 | this.closePopover() | ||
70 | this.navigate.emit(link) | 91 | this.navigate.emit(link) |
71 | } | 92 | } |
72 | 93 | ||
@@ -83,5 +104,4 @@ export class AvatarNotificationComponent implements OnInit, OnDestroy { | |||
83 | if (data.type === 'read-all') return this.unreadNotifications = 0 | 104 | if (data.type === 'read-all') return this.unreadNotifications = 0 |
84 | }) | 105 | }) |
85 | } | 106 | } |
86 | |||
87 | } | 107 | } |
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 = { | |||
36 | 'clock': require('!!raw-loader?!../../../assets/images/feather/clock.svg').default, | 36 | 'clock': require('!!raw-loader?!../../../assets/images/feather/clock.svg').default, |
37 | 'cog': require('!!raw-loader?!../../../assets/images/feather/cog.svg').default, | 37 | 'cog': require('!!raw-loader?!../../../assets/images/feather/cog.svg').default, |
38 | 'delete': require('!!raw-loader?!../../../assets/images/feather/delete.svg').default, | 38 | 'delete': require('!!raw-loader?!../../../assets/images/feather/delete.svg').default, |
39 | 'inbox-full': require('!!raw-loader?!../../../assets/images/feather/inbox-full.svg').default, | 39 | 'bell': require('!!raw-loader?!../../../assets/images/feather/bell.svg').default, |
40 | 'sign-out': require('!!raw-loader?!../../../assets/images/feather/log-out.svg').default, | 40 | 'sign-out': require('!!raw-loader?!../../../assets/images/feather/log-out.svg').default, |
41 | 'sign-in': require('!!raw-loader?!../../../assets/images/feather/log-in.svg').default, | 41 | 'sign-in': require('!!raw-loader?!../../../assets/images/feather/log-in.svg').default, |
42 | 'download': require('!!raw-loader?!../../../assets/images/feather/download.svg').default, | 42 | 'download': require('!!raw-loader?!../../../assets/images/feather/download.svg').default, |
diff --git a/client/src/assets/images/feather/bell.svg b/client/src/assets/images/feather/bell.svg new file mode 100644 index 000000000..bba561c19 --- /dev/null +++ b/client/src/assets/images/feather/bell.svg | |||
@@ -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> \ No newline at end of file | |||
diff --git a/client/src/assets/images/feather/inbox-full.svg b/client/src/assets/images/feather/inbox-full.svg deleted file mode 100644 index 03a13b4e4..000000000 --- a/client/src/assets/images/feather/inbox-full.svg +++ /dev/null | |||
@@ -1 +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-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> \ No newline at end of file | ||