aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts1
-rw-r--r--client/src/app/core/menu/menu.service.ts106
-rw-r--r--client/src/app/menu/menu.component.html41
-rw-r--r--client/src/app/menu/menu.component.scss3
-rw-r--r--client/src/app/menu/menu.component.ts73
-rw-r--r--shared/models/plugins/client/client-hook.model.ts5
-rw-r--r--support/doc/plugins/guide.md6
7 files changed, 135 insertions, 100 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
index bac1015fc..671e734ac 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
@@ -82,6 +82,7 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
82 82
83 buildLandingPageOptions () { 83 buildLandingPageOptions () {
84 this.defaultLandingPageOptions = this.menuService.buildCommonLinks(this.serverConfig) 84 this.defaultLandingPageOptions = this.menuService.buildCommonLinks(this.serverConfig)
85 .links
85 .map(o => ({ 86 .map(o => ({
86 id: o.path, 87 id: o.path,
87 label: o.label, 88 label: o.label,
diff --git a/client/src/app/core/menu/menu.service.ts b/client/src/app/core/menu/menu.service.ts
index a30766b29..60130382f 100644
--- a/client/src/app/core/menu/menu.service.ts
+++ b/client/src/app/core/menu/menu.service.ts
@@ -2,16 +2,23 @@ import { fromEvent } from 'rxjs'
2import { debounceTime } from 'rxjs/operators' 2import { debounceTime } from 'rxjs/operators'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { GlobalIconName } from '@app/shared/shared-icons' 4import { GlobalIconName } from '@app/shared/shared-icons'
5import { sortObjectComparator } from '@shared/core-utils/miscs/miscs'
6import { HTMLServerConfig } from '@shared/models/server' 5import { HTMLServerConfig } from '@shared/models/server'
7import { ScreenService } from '../wrappers' 6import { ScreenService } from '../wrappers'
8 7
9export type MenuLink = { 8export type MenuLink = {
10 icon: GlobalIconName 9 icon: GlobalIconName
10
11 label: string 11 label: string
12 menuLabel: string 12 // Used by the left menu for example
13 shortLabel: string
14
13 path: string 15 path: string
14 priority: number 16}
17
18export type MenuSection = {
19 key: string
20 title: string
21 links: MenuLink[]
15} 22}
16 23
17@Injectable() 24@Injectable()
@@ -59,51 +66,90 @@ export class MenuService {
59 this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser 66 this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser
60 } 67 }
61 68
62 buildCommonLinks (config: HTMLServerConfig) { 69 buildLibraryLinks (userCanSeeVideosLink: boolean): MenuSection {
63 let entries: MenuLink[] = [ 70 let links: MenuLink[] = []
71
72 if (userCanSeeVideosLink) {
73 links.push({
74 path: '/my-library/videos',
75 icon: 'videos' as GlobalIconName,
76 shortLabel: $localize`Videos`,
77 label: $localize`My videos`
78 })
79 }
80
81 links = links.concat([
82 {
83 path: '/my-library/video-playlists',
84 icon: 'playlists' as GlobalIconName,
85 shortLabel: $localize`Playlists`,
86 label: $localize`My playlists`
87 },
88 {
89 path: '/videos/subscriptions',
90 icon: 'subscriptions' as GlobalIconName,
91 shortLabel: $localize`Subscriptions`,
92 label: $localize`My subscriptions`
93 },
94 {
95 path: '/my-library/history/videos',
96 icon: 'history' as GlobalIconName,
97 shortLabel: $localize`History`,
98 label: $localize`My history`
99 }
100 ])
101
102 return {
103 key: 'in-my-library',
104 title: 'In my library',
105 links
106 }
107 }
108
109 buildCommonLinks (config: HTMLServerConfig): MenuSection {
110 let links: MenuLink[] = []
111
112 if (config.homepage.enabled) {
113 links.push({
114 icon: 'home' as 'home',
115 label: $localize`Home`,
116 shortLabel: $localize`Home`,
117 path: '/home'
118 })
119 }
120
121 links = links.concat([
64 { 122 {
65 icon: 'globe' as 'globe', 123 icon: 'globe' as 'globe',
66 label: $localize`Discover videos`, 124 label: $localize`Discover videos`,
67 menuLabel: $localize`Discover`, 125 shortLabel: $localize`Discover`,
68 path: '/videos/overview', 126 path: '/videos/overview'
69 priority: 150
70 }, 127 },
71 { 128 {
72 icon: 'trending' as 'trending', 129 icon: 'trending' as 'trending',
73 label: $localize`Trending videos`, 130 label: $localize`Trending videos`,
74 menuLabel: $localize`Trending`, 131 shortLabel: $localize`Trending`,
75 path: '/videos/trending', 132 path: '/videos/trending'
76 priority: 140
77 }, 133 },
78 { 134 {
79 icon: 'recently-added' as 'recently-added', 135 icon: 'recently-added' as 'recently-added',
80 label: $localize`Recently added videos`, 136 label: $localize`Recently added videos`,
81 menuLabel: $localize`Recently added`, 137 shortLabel: $localize`Recently added`,
82 path: '/videos/recently-added', 138 path: '/videos/recently-added'
83 priority: 130
84 }, 139 },
85 { 140 {
86 icon: 'local' as 'local', 141 icon: 'local' as 'local',
87 label: $localize`Local videos`, 142 label: $localize`Local videos`,
88 menuLabel: $localize`Local videos`, 143 shortLabel: $localize`Local videos`,
89 path: '/videos/local', 144 path: '/videos/local'
90 priority: 120
91 } 145 }
92 ] 146 ])
93 147
94 if (config.homepage.enabled) { 148 return {
95 entries.push({ 149 key: 'on-instance',
96 icon: 'home' as 'home', 150 title: $localize`ON ${config.instance.name}`,
97 label: $localize`Home`, 151 links
98 menuLabel: $localize`Home`,
99 path: '/home',
100 priority: 160
101 })
102 } 152 }
103
104 entries = entries.sort(sortObjectComparator('priority', 'desc'))
105
106 return entries
107 } 153 }
108 154
109 private handleWindowResize () { 155 private handleWindowResize () {
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index 099ee8f36..16c79efc1 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -95,39 +95,16 @@
95 <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">Create an account</a> 95 <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">Create an account</a>
96 </div> 96 </div>
97 97
98 <div *ngIf="isLoggedIn" class="in-my-library"> 98 <ng-container *ngFor="let menuSection of menuSections" >
99 <div i18n class="block-title">IN MY LIBRARY</div> 99 <div [ngClass]="[ menuSection.key, 'menu-block' ]">
100 <div i18n class="block-title">{{ menuSection.title }}</div>
100 101
101 <a *ngIf="user.canSeeVideosLink" class="menu-link" routerLink="/my-library/videos" routerLinkActive="active"> 102 <a class="menu-link" *ngFor="let link of menuSection.links" [routerLink]="link.path" routerLinkActive="active">
102 <my-global-icon iconName="videos" aria-hidden="true"></my-global-icon> 103 <my-global-icon [iconName]="link.icon" aria-hidden="true"></my-global-icon>
103 <ng-container i18n>Videos</ng-container> 104 <ng-container>{{ link.shortLabel }}</ng-container>
104 </a> 105 </a>
105 106 </div>
106 <a class="menu-link" routerLink="/my-library/video-playlists" routerLinkActive="active"> 107 </ng-container>
107 <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon>
108 <ng-container i18n>Playlists</ng-container>
109 </a>
110
111 <a class="menu-link" routerLink="/videos/subscriptions" routerLinkActive="active">
112 <my-global-icon iconName="subscriptions" aria-hidden="true"></my-global-icon>
113 <ng-container i18n>Subscriptions</ng-container>
114 </a>
115
116 <a class="menu-link" routerLink="/my-library/history/videos" routerLinkActive="active">
117 <my-global-icon iconName="history" aria-hidden="true"></my-global-icon>
118 <ng-container i18n>History</ng-container>
119 </a>
120
121 </div>
122
123 <div class="on-instance">
124 <div i18n class="block-title">ON {{instanceName}}</div>
125
126 <a class="menu-link" *ngFor="let commonLink of commonMenuLinks" [routerLink]="commonLink.path" routerLinkActive="active">
127 <my-global-icon [iconName]="commonLink.icon" aria-hidden="true"></my-global-icon>
128 <ng-container>{{ commonLink.menuLabel }}</ng-container>
129 </a>
130 </div>
131 </div> 108 </div>
132 109
133 <div class="footer"> 110 <div class="footer">
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss
index 6da80191a..daaed2d32 100644
--- a/client/src/app/menu/menu.component.scss
+++ b/client/src/app/menu/menu.component.scss
@@ -274,8 +274,7 @@ my-actor-avatar {
274 } 274 }
275} 275}
276 276
277.in-my-library, 277.menu-block,
278.on-instance,
279.footer-block { 278.footer-block {
280 margin-bottom: 15px; 279 margin-bottom: 15px;
281 280
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts
index c767f19b2..fa104cc43 100644
--- a/client/src/app/menu/menu.component.ts
+++ b/client/src/app/menu/menu.component.ts
@@ -8,7 +8,8 @@ import {
8 AuthService, 8 AuthService,
9 AuthStatus, 9 AuthStatus,
10 AuthUser, 10 AuthUser,
11 MenuLink, 11 HooksService,
12 MenuSection,
12 MenuService, 13 MenuService,
13 RedirectService, 14 RedirectService,
14 ScreenService, 15 ScreenService,
@@ -45,7 +46,7 @@ export class MenuComponent implements OnInit {
45 46
46 currentInterfaceLanguage: string 47 currentInterfaceLanguage: string
47 48
48 commonMenuLinks: MenuLink[] = [] 49 menuSections: MenuSection[] = []
49 50
50 private languages: VideoConstant<string>[] = [] 51 private languages: VideoConstant<string>[] = []
51 52
@@ -71,7 +72,8 @@ export class MenuComponent implements OnInit {
71 private screenService: ScreenService, 72 private screenService: ScreenService,
72 private menuService: MenuService, 73 private menuService: MenuService,
73 private modalService: PeertubeModalService, 74 private modalService: PeertubeModalService,
74 private router: Router 75 private router: Router,
76 private hooks: HooksService
75 ) { } 77 ) { }
76 78
77 get isInMobileView () { 79 get isInMobileView () {
@@ -88,46 +90,23 @@ export class MenuComponent implements OnInit {
88 return this.languageChooserModal.getCurrentLanguage() 90 return this.languageChooserModal.getCurrentLanguage()
89 } 91 }
90 92
91 get instanceName () {
92 return this.htmlServerConfig.instance.name
93 }
94
95 ngOnInit () { 93 ngOnInit () {
96 this.htmlServerConfig = this.serverService.getHTMLConfig() 94 this.htmlServerConfig = this.serverService.getHTMLConfig()
97 this.buildMenuLinks()
98
99 this.isLoggedIn = this.authService.isLoggedIn()
100 if (this.isLoggedIn === true) {
101 this.user = this.authService.getUser()
102
103 this.computeNSFWPolicy()
104 this.computeVideosLink()
105 }
106
107 this.computeAdminAccess()
108
109 this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage() 95 this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage()
110 96
97 this.updateUserState()
98 this.buildMenuSections()
99
111 this.authService.loginChangedSource.subscribe( 100 this.authService.loginChangedSource.subscribe(
112 status => { 101 status => {
113 if (status === AuthStatus.LoggedIn) { 102 if (status === AuthStatus.LoggedIn) {
114 this.isLoggedIn = true 103 this.isLoggedIn = true
115 this.user = this.authService.getUser()
116
117 this.computeAdminAccess()
118 this.computeVideosLink()
119
120 logger('Logged in.')
121 } else if (status === AuthStatus.LoggedOut) { 104 } else if (status === AuthStatus.LoggedOut) {
122 this.isLoggedIn = false 105 this.isLoggedIn = false
123 this.user = undefined
124
125 this.computeAdminAccess()
126
127 logger('Logged out.')
128 } else {
129 console.error('Unknown auth status: ' + status)
130 } 106 }
107
108 this.updateUserState()
109 this.buildMenuSections()
131 } 110 }
132 ) 111 )
133 112
@@ -257,8 +236,20 @@ export class MenuComponent implements OnInit {
257 } 236 }
258 } 237 }
259 238
260 private buildMenuLinks () { 239 private async buildMenuSections () {
261 this.commonMenuLinks = this.menuService.buildCommonLinks(this.htmlServerConfig) 240 const menuSections = []
241
242 if (this.isLoggedIn) {
243 menuSections.push(
244 this.menuService.buildLibraryLinks(this.user?.canSeeVideosLink)
245 )
246 }
247
248 menuSections.push(
249 this.menuService.buildCommonLinks(this.htmlServerConfig)
250 )
251
252 this.menuSections = await this.hooks.wrapObject(menuSections, 'common', 'filter:left-menu.links.create.result')
262 } 253 }
263 254
264 private buildUserLanguages () { 255 private buildUserLanguages () {
@@ -268,7 +259,7 @@ export class MenuComponent implements OnInit {
268 } 259 }
269 260
270 if (!this.user.videoLanguages) { 261 if (!this.user.videoLanguages) {
271 this.videoLanguages = [$localize`any language`] 262 this.videoLanguages = [ $localize`any language` ]
272 return 263 return
273 } 264 }
274 265
@@ -284,6 +275,8 @@ export class MenuComponent implements OnInit {
284 } 275 }
285 276
286 private computeVideosLink () { 277 private computeVideosLink () {
278 if (!this.isLoggedIn) return
279
287 this.authService.userInformationLoaded 280 this.authService.userInformationLoaded
288 .pipe( 281 .pipe(
289 switchMap(() => this.user.computeCanSeeVideosLink(this.userService.getMyVideoQuotaUsed())) 282 switchMap(() => this.user.computeCanSeeVideosLink(this.userService.getMyVideoQuotaUsed()))
@@ -313,4 +306,14 @@ export class MenuComponent implements OnInit {
313 break 306 break
314 } 307 }
315 } 308 }
309
310 private updateUserState () {
311 this.user = this.isLoggedIn
312 ? this.authService.getUser()
313 : undefined
314
315 this.computeAdminAccess()
316 this.computeNSFWPolicy()
317 this.computeVideosLink()
318 }
316} 319}
diff --git a/shared/models/plugins/client/client-hook.model.ts b/shared/models/plugins/client/client-hook.model.ts
index 620651051..3eedd670b 100644
--- a/shared/models/plugins/client/client-hook.model.ts
+++ b/shared/models/plugins/client/client-hook.model.ts
@@ -50,7 +50,10 @@ export const clientFilterHookObject = {
50 50
51 // Filter our SVG icons content 51 // Filter our SVG icons content
52 'filter:internal.common.svg-icons.get-content.params': true, 52 'filter:internal.common.svg-icons.get-content.params': true,
53 'filter:internal.common.svg-icons.get-content.result': true 53 'filter:internal.common.svg-icons.get-content.result': true,
54
55 // Filter left menu links
56 'filter:left-menu.links.create.result': true
54} 57}
55 58
56export type ClientFilterHookName = keyof typeof clientFilterHookObject 59export type ClientFilterHookName = keyof typeof clientFilterHookObject
diff --git a/support/doc/plugins/guide.md b/support/doc/plugins/guide.md
index db1f1863c..f4d3c4800 100644
--- a/support/doc/plugins/guide.md
+++ b/support/doc/plugins/guide.md
@@ -29,6 +29,7 @@
29 - [Add custom fields to video form](#add-custom-fields-to-video-form) 29 - [Add custom fields to video form](#add-custom-fields-to-video-form)
30 - [Register settings script](#register-settings-script) 30 - [Register settings script](#register-settings-script)
31 - [HTML placeholder elements](#html-placeholder-elements) 31 - [HTML placeholder elements](#html-placeholder-elements)
32 - [Add/remove left menu links](#addremove-left-menu-links)
32 - [Publishing](#publishing) 33 - [Publishing](#publishing)
33- [Write a plugin/theme](#write-a-plugintheme) 34- [Write a plugin/theme](#write-a-plugintheme)
34 - [Clone the quickstart repository](#clone-the-quickstart-repository) 35 - [Clone the quickstart repository](#clone-the-quickstart-repository)
@@ -744,6 +745,11 @@ async function register (...) {
744 745
745See the complete list on https://docs.joinpeertube.org/api-plugins 746See the complete list on https://docs.joinpeertube.org/api-plugins
746 747
748#### Add/remove left menu links
749
750Left menu links can be filtered (add/remove a section or add/remove links) using the `filter:left-menu.links.create.result` client hook.
751
752
747### Publishing 753### Publishing
748 754
749PeerTube plugins and themes should be published on [NPM](https://www.npmjs.com/) so that PeerTube indexes 755PeerTube plugins and themes should be published on [NPM](https://www.npmjs.com/) so that PeerTube indexes