]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/app/app.component.ts
Merge branch 'release/4.2.0' into develop
[github/Chocobozzz/PeerTube.git] / client / src / app / app.component.ts
1 import { Hotkey, HotkeysService } from 'angular2-hotkeys'
2 import { forkJoin, delay } from 'rxjs'
3 import { filter, first, map } from 'rxjs/operators'
4 import { DOCUMENT, getLocaleDirection, PlatformLocation } from '@angular/common'
5 import { AfterViewInit, Component, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core'
6 import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
7 import { Event, GuardsCheckStart, RouteConfigLoadEnd, RouteConfigLoadStart, Router } from '@angular/router'
8 import {
9 AuthService,
10 MarkdownService,
11 PeerTubeRouterService,
12 RedirectService,
13 ScreenService,
14 ScrollService,
15 ServerService,
16 ThemeService,
17 User,
18 UserLocalStorageService
19 } from '@app/core'
20 import { HooksService } from '@app/core/plugins/hooks.service'
21 import { PluginService } from '@app/core/plugins/plugin.service'
22 import { AccountSetupWarningModalComponent } from '@app/modal/account-setup-warning-modal.component'
23 import { CustomModalComponent } from '@app/modal/custom-modal.component'
24 import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
25 import { AdminWelcomeModalComponent } from '@app/modal/admin-welcome-modal.component'
26 import { NgbConfig, NgbModal } from '@ng-bootstrap/ng-bootstrap'
27 import { LoadingBarService } from '@ngx-loading-bar/core'
28 import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
29 import { getShortLocale } from '@shared/core-utils/i18n'
30 import { BroadcastMessageLevel, HTMLServerConfig, UserRole } from '@shared/models'
31 import { MenuService } from './core/menu/menu.service'
32 import { POP_STATE_MODAL_DISMISS } from './helpers'
33 import { InstanceService } from './shared/shared-instance'
34 import { GlobalIconName } from './shared/shared-icons'
35
36 @Component({
37 selector: 'my-app',
38 templateUrl: './app.component.html',
39 styleUrls: [ './app.component.scss' ]
40 })
41 export class AppComponent implements OnInit, AfterViewInit {
42 private static BROADCAST_MESSAGE_KEY = 'app-broadcast-message-dismissed'
43
44 @ViewChild('accountSetupWarningModal') accountSetupWarningModal: AccountSetupWarningModalComponent
45 @ViewChild('adminWelcomeModal') adminWelcomeModal: AdminWelcomeModalComponent
46 @ViewChild('instanceConfigWarningModal') instanceConfigWarningModal: InstanceConfigWarningModalComponent
47 @ViewChild('customModal') customModal: CustomModalComponent
48
49 customCSS: SafeHtml
50 broadcastMessage: { message: string, dismissable: boolean, class: string } | null = null
51
52 private serverConfig: HTMLServerConfig
53
54 constructor (
55 @Inject(DOCUMENT) private document: Document,
56 @Inject(LOCALE_ID) private localeId: string,
57 private router: Router,
58 private authService: AuthService,
59 private serverService: ServerService,
60 private peertubeRouter: PeerTubeRouterService,
61 private pluginService: PluginService,
62 private instanceService: InstanceService,
63 private domSanitizer: DomSanitizer,
64 private redirectService: RedirectService,
65 private screenService: ScreenService,
66 private hotkeysService: HotkeysService,
67 private themeService: ThemeService,
68 private hooks: HooksService,
69 private location: PlatformLocation,
70 private modalService: NgbModal,
71 private markdownService: MarkdownService,
72 private ngbConfig: NgbConfig,
73 private loadingBar: LoadingBarService,
74 private scrollService: ScrollService,
75 private userLocalStorage: UserLocalStorageService,
76 public menu: MenuService
77 ) {
78 this.ngbConfig.animation = false
79 }
80
81 get instanceName () {
82 return this.serverConfig.instance.name
83 }
84
85 goToDefaultRoute () {
86 return this.router.navigateByUrl(this.redirectService.getDefaultRoute())
87 }
88
89 ngOnInit () {
90 document.getElementById('incompatible-browser').className += ' browser-ok'
91
92 this.loadUser()
93
94 this.serverConfig = this.serverService.getHTMLConfig()
95
96 this.hooks.runAction('action:application.init', 'common')
97 this.themeService.initialize()
98
99 this.authService.loadClientCredentials()
100
101 if (this.isUserLoggedIn()) {
102 // The service will automatically redirect to the login page if the token is not valid anymore
103 this.authService.refreshUserInformation()
104 }
105
106 this.initRouteEvents()
107 this.scrollService.enableScrollRestoration()
108
109 this.injectJS()
110 this.injectCSS()
111 this.injectBroadcastMessage()
112
113 this.serverService.configReloaded
114 .subscribe(config => {
115 this.serverConfig = config
116
117 this.injectBroadcastMessage()
118 this.injectCSS()
119
120 // Don't reinject JS since it could conflict with existing one
121 })
122
123 this.initHotkeys()
124
125 this.location.onPopState(() => this.modalService.dismissAll(POP_STATE_MODAL_DISMISS))
126
127 this.openModalsIfNeeded()
128
129 this.document.documentElement.lang = getShortLocale(this.localeId)
130 this.document.documentElement.dir = getLocaleDirection(this.localeId)
131 }
132
133 ngAfterViewInit () {
134 this.pluginService.initializeCustomModal(this.customModal)
135 }
136
137 getToggleTitle () {
138 if (this.menu.isDisplayed()) return $localize`Close the left menu`
139
140 return $localize`Open the left menu`
141 }
142
143 isUserLoggedIn () {
144 return this.authService.isLoggedIn()
145 }
146
147 hideBroadcastMessage () {
148 peertubeLocalStorage.setItem(AppComponent.BROADCAST_MESSAGE_KEY, this.serverConfig.broadcastMessage.message)
149
150 this.broadcastMessage = null
151 this.screenService.isBroadcastMessageDisplayed = false
152 }
153
154 getNotificationIcon (message: { severity: 'success' | 'error' | 'info' }): GlobalIconName {
155 switch (message.severity) {
156 case 'error':
157 return 'cross'
158 case 'success':
159 return 'tick'
160 case 'info':
161 return 'help'
162 }
163 }
164
165 private initRouteEvents () {
166 const eventsObs = this.router.events
167
168 // Plugin hooks
169 this.peertubeRouter.getNavigationEndEvents().subscribe(e => {
170 this.hooks.runAction('action:router.navigation-end', 'common', { path: e.url })
171 })
172
173 // Automatically hide/display the menu
174 eventsObs.pipe(
175 filter((e: Event): e is GuardsCheckStart => e instanceof GuardsCheckStart),
176 filter(() => this.screenService.isInSmallView() || this.screenService.isInTouchScreen())
177 ).subscribe(() => this.menu.setMenuDisplay(false)) // User clicked on a link in the menu, change the page
178
179 // Handle lazy loaded module
180 eventsObs.pipe(
181 filter((e: Event): e is RouteConfigLoadStart => e instanceof RouteConfigLoadStart)
182 ).subscribe(() => this.loadingBar.useRef().start())
183
184 eventsObs.pipe(
185 filter((e: Event): e is RouteConfigLoadEnd => e instanceof RouteConfigLoadEnd)
186 ).subscribe(() => this.loadingBar.useRef().complete())
187 }
188
189 private async injectBroadcastMessage () {
190 this.broadcastMessage = null
191 this.screenService.isBroadcastMessageDisplayed = false
192
193 const messageConfig = this.serverConfig.broadcastMessage
194
195 if (messageConfig.enabled) {
196 // Already dismissed this message?
197 if (messageConfig.dismissable && localStorage.getItem(AppComponent.BROADCAST_MESSAGE_KEY) === messageConfig.message) {
198 return
199 }
200
201 const classes: { [id in BroadcastMessageLevel]: string } = {
202 info: 'alert-info',
203 warning: 'alert-warning',
204 error: 'alert-danger'
205 }
206
207 this.broadcastMessage = {
208 message: await this.markdownService.unsafeMarkdownToHTML(messageConfig.message, true),
209 dismissable: messageConfig.dismissable,
210 class: classes[messageConfig.level]
211 }
212
213 this.screenService.isBroadcastMessageDisplayed = true
214 }
215 }
216
217 private injectJS () {
218 // Inject JS
219 if (this.serverConfig.instance.customizations.javascript) {
220 try {
221 /* eslint-disable no-eval */
222 eval(this.serverConfig.instance.customizations.javascript)
223 } catch (err) {
224 console.error('Cannot eval custom JavaScript.', err)
225 }
226 }
227 }
228
229 private injectCSS () {
230 const headStyle = document.querySelector('style.custom-css-style')
231 if (headStyle) headStyle.parentNode.removeChild(headStyle)
232
233 // We test customCSS if the admin removed the css
234 if (this.customCSS || this.serverConfig.instance.customizations.css) {
235 const styleTag = '<style>' + this.serverConfig.instance.customizations.css + '</style>'
236 this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag)
237 }
238 }
239
240 private openModalsIfNeeded () {
241 const userSub = this.authService.userInformationLoaded
242 .pipe(
243 delay(0), // Wait for modals creations
244 map(() => this.authService.getUser())
245 )
246
247 // Admin modal
248 userSub.pipe(
249 filter(user => user.role === UserRole.ADMINISTRATOR)
250 ).subscribe(user => this.openAdminModalsIfNeeded(user))
251
252 // Account modal
253 userSub.pipe(
254 filter(user => user.role !== UserRole.ADMINISTRATOR)
255 ).subscribe(user => this.openAccountModalsIfNeeded(user))
256 }
257
258 private openAdminModalsIfNeeded (user: User) {
259 if (this.adminWelcomeModal.shouldOpen(user)) {
260 return this.adminWelcomeModal.show()
261 }
262
263 if (!this.instanceConfigWarningModal.shouldOpenByUser(user)) return
264
265 forkJoin([
266 this.serverService.getConfig().pipe(first()),
267 this.instanceService.getAbout().pipe(first())
268 ]).subscribe(([ config, about ]) => {
269 if (this.instanceConfigWarningModal.shouldOpen(config, about)) {
270 this.instanceConfigWarningModal.show(about)
271 }
272 })
273 }
274
275 private openAccountModalsIfNeeded (user: User) {
276 if (this.accountSetupWarningModal.shouldOpen(user)) {
277 this.accountSetupWarningModal.show(user)
278 }
279 }
280
281 private initHotkeys () {
282 this.hotkeysService.add([
283 new Hotkey([ '/', 's' ], (event: KeyboardEvent): boolean => {
284 document.getElementById('search-video').focus()
285 return false
286 }, undefined, $localize`Focus the search bar`),
287
288 new Hotkey('b', (event: KeyboardEvent): boolean => {
289 this.menu.toggleMenu()
290 return false
291 }, undefined, $localize`Toggle the left menu`),
292
293 new Hotkey('g o', (event: KeyboardEvent): boolean => {
294 this.router.navigate([ '/videos/overview' ])
295 return false
296 }, undefined, $localize`Go to the discover videos page`),
297
298 new Hotkey('g t', (event: KeyboardEvent): boolean => {
299 this.router.navigate([ '/videos/trending' ])
300 return false
301 }, undefined, $localize`Go to the trending videos page`),
302
303 new Hotkey('g r', (event: KeyboardEvent): boolean => {
304 this.router.navigate([ '/videos/recently-added' ])
305 return false
306 }, undefined, $localize`Go to the recently added videos page`),
307
308 new Hotkey('g l', (event: KeyboardEvent): boolean => {
309 this.router.navigate([ '/videos/local' ])
310 return false
311 }, undefined, $localize`Go to the local videos page`),
312
313 new Hotkey('g u', (event: KeyboardEvent): boolean => {
314 this.router.navigate([ '/videos/upload' ])
315 return false
316 }, undefined, $localize`Go to the videos upload page`)
317 ])
318 }
319
320 private loadUser () {
321 const tokens = this.userLocalStorage.getTokens()
322 if (!tokens) return
323
324 const user = this.userLocalStorage.getLoggedInUser()
325 if (!user) return
326
327 // Initialize user
328 this.authService.buildAuthUser(user, tokens)
329 }
330 }