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'
11 PeerTubeRouterService,
18 UserLocalStorageService
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'
38 templateUrl: './app.component.html',
39 styleUrls: [ './app.component.scss' ]
41 export class AppComponent implements OnInit, AfterViewInit {
42 private static BROADCAST_MESSAGE_KEY = 'app-broadcast-message-dismissed'
44 @ViewChild('accountSetupWarningModal') accountSetupWarningModal: AccountSetupWarningModalComponent
45 @ViewChild('adminWelcomeModal') adminWelcomeModal: AdminWelcomeModalComponent
46 @ViewChild('instanceConfigWarningModal') instanceConfigWarningModal: InstanceConfigWarningModalComponent
47 @ViewChild('customModal') customModal: CustomModalComponent
50 broadcastMessage: { message: string, dismissable: boolean, class: string } | null = null
52 private serverConfig: HTMLServerConfig
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
78 this.ngbConfig.animation = false
82 return this.serverConfig.instance.name
86 return this.router.navigateByUrl(this.redirectService.getDefaultRoute())
90 document.getElementById('incompatible-browser').className += ' browser-ok'
94 this.serverConfig = this.serverService.getHTMLConfig()
96 this.hooks.runAction('action:application.init', 'common')
97 this.themeService.initialize()
99 this.authService.loadClientCredentials()
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()
106 this.initRouteEvents()
107 this.scrollService.enableScrollRestoration()
111 this.injectBroadcastMessage()
113 this.serverService.configReloaded
114 .subscribe(config => {
115 this.serverConfig = config
117 this.injectBroadcastMessage()
120 // Don't reinject JS since it could conflict with existing one
125 this.location.onPopState(() => this.modalService.dismissAll(POP_STATE_MODAL_DISMISS))
127 this.openModalsIfNeeded()
129 this.document.documentElement.lang = getShortLocale(this.localeId)
130 this.document.documentElement.dir = getLocaleDirection(this.localeId)
134 this.pluginService.initializeCustomModal(this.customModal)
138 if (this.menu.isDisplayed()) return $localize`Close the left menu`
140 return $localize`Open the left menu`
144 return this.authService.isLoggedIn()
147 hideBroadcastMessage () {
148 peertubeLocalStorage.setItem(AppComponent.BROADCAST_MESSAGE_KEY, this.serverConfig.broadcastMessage.message)
150 this.broadcastMessage = null
151 this.screenService.isBroadcastMessageDisplayed = false
154 getNotificationIcon (message: { severity: 'success' | 'error' | 'info' }): GlobalIconName {
155 switch (message.severity) {
165 private initRouteEvents () {
166 const eventsObs = this.router.events
169 this.peertubeRouter.getNavigationEndEvents().subscribe(e => {
170 this.hooks.runAction('action:router.navigation-end', 'common', { path: e.url })
173 // Automatically hide/display the menu
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
179 // Handle lazy loaded module
181 filter((e: Event): e is RouteConfigLoadStart => e instanceof RouteConfigLoadStart)
182 ).subscribe(() => this.loadingBar.useRef().start())
185 filter((e: Event): e is RouteConfigLoadEnd => e instanceof RouteConfigLoadEnd)
186 ).subscribe(() => this.loadingBar.useRef().complete())
189 private async injectBroadcastMessage () {
190 this.broadcastMessage = null
191 this.screenService.isBroadcastMessageDisplayed = false
193 const messageConfig = this.serverConfig.broadcastMessage
195 if (messageConfig.enabled) {
196 // Already dismissed this message?
197 if (messageConfig.dismissable && localStorage.getItem(AppComponent.BROADCAST_MESSAGE_KEY) === messageConfig.message) {
201 const classes: { [id in BroadcastMessageLevel]: string } = {
203 warning: 'alert-warning',
204 error: 'alert-danger'
207 this.broadcastMessage = {
208 message: await this.markdownService.unsafeMarkdownToHTML(messageConfig.message, true),
209 dismissable: messageConfig.dismissable,
210 class: classes[messageConfig.level]
213 this.screenService.isBroadcastMessageDisplayed = true
217 private injectJS () {
219 if (this.serverConfig.instance.customizations.javascript) {
221 /* eslint-disable no-eval */
222 eval(this.serverConfig.instance.customizations.javascript)
224 console.error('Cannot eval custom JavaScript.', err)
229 private injectCSS () {
230 const headStyle = document.querySelector('style.custom-css-style')
231 if (headStyle) headStyle.parentNode.removeChild(headStyle)
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)
240 private openModalsIfNeeded () {
241 const userSub = this.authService.userInformationLoaded
243 delay(0), // Wait for modals creations
244 map(() => this.authService.getUser())
249 filter(user => user.role === UserRole.ADMINISTRATOR)
250 ).subscribe(user => this.openAdminModalsIfNeeded(user))
254 filter(user => user.role !== UserRole.ADMINISTRATOR)
255 ).subscribe(user => this.openAccountModalsIfNeeded(user))
258 private openAdminModalsIfNeeded (user: User) {
259 if (this.adminWelcomeModal.shouldOpen(user)) {
260 return this.adminWelcomeModal.show()
263 if (!this.instanceConfigWarningModal.shouldOpenByUser(user)) return
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)
275 private openAccountModalsIfNeeded (user: User) {
276 if (this.accountSetupWarningModal.shouldOpen(user)) {
277 this.accountSetupWarningModal.show(user)
281 private initHotkeys () {
282 this.hotkeysService.add([
283 new Hotkey([ '/', 's' ], (event: KeyboardEvent): boolean => {
284 document.getElementById('search-video').focus()
286 }, undefined, $localize`Focus the search bar`),
288 new Hotkey('b', (event: KeyboardEvent): boolean => {
289 this.menu.toggleMenu()
291 }, undefined, $localize`Toggle the left menu`),
293 new Hotkey('g o', (event: KeyboardEvent): boolean => {
294 this.router.navigate([ '/videos/overview' ])
296 }, undefined, $localize`Go to the discover videos page`),
298 new Hotkey('g t', (event: KeyboardEvent): boolean => {
299 this.router.navigate([ '/videos/trending' ])
301 }, undefined, $localize`Go to the trending videos page`),
303 new Hotkey('g r', (event: KeyboardEvent): boolean => {
304 this.router.navigate([ '/videos/recently-added' ])
306 }, undefined, $localize`Go to the recently added videos page`),
308 new Hotkey('g l', (event: KeyboardEvent): boolean => {
309 this.router.navigate([ '/videos/local' ])
311 }, undefined, $localize`Go to the local videos page`),
313 new Hotkey('g u', (event: KeyboardEvent): boolean => {
314 this.router.navigate([ '/videos/upload' ])
316 }, undefined, $localize`Go to the videos upload page`)
320 private loadUser () {
321 const tokens = this.userLocalStorage.getTokens()
324 const user = this.userLocalStorage.getLoggedInUser()
328 this.authService.buildAuthUser(user, tokens)