065bdcaa45d88bccbea9064439bc4470d4af8813
[oweals/peertube.git] / client / src / app / app.component.ts
1 import { Component, OnInit } from '@angular/core'
2 import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
3 import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular/router'
4 import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core'
5 import { is18nPath } from '../../../shared/models/i18n'
6 import { ScreenService } from '@app/shared/misc/screen.service'
7 import { debounceTime, filter, map, pairwise, skip } from 'rxjs/operators'
8 import { Hotkey, HotkeysService } from 'angular2-hotkeys'
9 import { I18n } from '@ngx-translate/i18n-polyfill'
10 import { fromEvent } from 'rxjs'
11 import { PlatformLocation, ViewportScroller } from '@angular/common'
12 import { PluginService } from '@app/core/plugins/plugin.service'
13 import { HooksService } from '@app/core/plugins/hooks.service'
14 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
15
16 @Component({
17   selector: 'my-app',
18   templateUrl: './app.component.html',
19   styleUrls: [ './app.component.scss' ]
20 })
21 export class AppComponent implements OnInit {
22   isMenuDisplayed = true
23   isMenuChangedByUser = false
24
25   customCSS: SafeHtml
26
27   constructor (
28     private i18n: I18n,
29     private viewportScroller: ViewportScroller,
30     private router: Router,
31     private authService: AuthService,
32     private serverService: ServerService,
33     private pluginService: PluginService,
34     private domSanitizer: DomSanitizer,
35     private redirectService: RedirectService,
36     private screenService: ScreenService,
37     private hotkeysService: HotkeysService,
38     private themeService: ThemeService,
39     private hooks: HooksService,
40     private location: PlatformLocation,
41     private modalService: NgbModal
42   ) { }
43
44   get serverVersion () {
45     return this.serverService.getConfig().serverVersion
46   }
47
48   get serverCommit () {
49     const commit = this.serverService.getConfig().serverCommit || ''
50     return (commit !== '') ? '...' + commit : commit
51   }
52
53   get instanceName () {
54     return this.serverService.getConfig().instance.name
55   }
56
57   get defaultRoute () {
58     return RedirectService.DEFAULT_ROUTE
59   }
60
61   ngOnInit () {
62     document.getElementById('incompatible-browser').className += ' browser-ok'
63
64     this.authService.loadClientCredentials()
65
66     if (this.isUserLoggedIn()) {
67       // The service will automatically redirect to the login page if the token is not valid anymore
68       this.authService.refreshUserInformation()
69     }
70
71     // Load custom data from server
72     this.serverService.loadConfig()
73     this.serverService.loadVideoCategories()
74     this.serverService.loadVideoLanguages()
75     this.serverService.loadVideoLicences()
76     this.serverService.loadVideoPrivacies()
77     this.serverService.loadVideoPlaylistPrivacies()
78
79     this.loadPlugins()
80     this.themeService.initialize()
81
82     // Do not display menu on small screens
83     if (this.screenService.isInSmallView()) {
84       this.isMenuDisplayed = false
85     }
86
87     this.initRouteEvents()
88     this.injectJS()
89     this.injectCSS()
90
91     this.initHotkeys()
92
93     fromEvent(window, 'resize')
94       .pipe(debounceTime(200))
95       .subscribe(() => this.onResize())
96
97     this.location.onPopState(() => this.modalService.dismissAll())
98   }
99
100   isUserLoggedIn () {
101     return this.authService.isLoggedIn()
102   }
103
104   toggleMenu () {
105     this.isMenuDisplayed = !this.isMenuDisplayed
106     this.isMenuChangedByUser = true
107   }
108
109   onResize () {
110     this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser
111   }
112
113   private initRouteEvents () {
114     let resetScroll = true
115     const eventsObs = this.router.events
116
117     const scrollEvent = eventsObs.pipe(filter((e: Event): e is Scroll => e instanceof Scroll))
118     const navigationEndEvent = eventsObs.pipe(filter((e: Event): e is NavigationEnd => e instanceof NavigationEnd))
119
120     scrollEvent.subscribe(e => {
121       if (e.position) {
122         return this.viewportScroller.scrollToPosition(e.position)
123       }
124
125       if (e.anchor) {
126         return this.viewportScroller.scrollToAnchor(e.anchor)
127       }
128
129       if (resetScroll) {
130         return this.viewportScroller.scrollToPosition([ 0, 0 ])
131       }
132     })
133
134     // When we add the a-state parameter, we don't want to alter the scroll
135     navigationEndEvent.pipe(pairwise())
136                       .subscribe(([ e1, e2 ]) => {
137                         try {
138                           resetScroll = false
139
140                           const previousUrl = new URL(window.location.origin + e1.urlAfterRedirects)
141                           const nextUrl = new URL(window.location.origin + e2.urlAfterRedirects)
142
143                           if (previousUrl.pathname !== nextUrl.pathname) {
144                             resetScroll = true
145                             return
146                           }
147
148                           const nextSearchParams = nextUrl.searchParams
149                           nextSearchParams.delete('a-state')
150
151                           const previousSearchParams = previousUrl.searchParams
152
153                           nextSearchParams.sort()
154                           previousSearchParams.sort()
155
156                           if (nextSearchParams.toString() !== previousSearchParams.toString()) {
157                             resetScroll = true
158                           }
159                         } catch (e) {
160                           console.error('Cannot parse URL to check next scroll.', e)
161                           resetScroll = true
162                         }
163                       })
164
165     navigationEndEvent.pipe(
166       map(() => window.location.pathname),
167       filter(pathname => !pathname || pathname === '/' || is18nPath(pathname))
168     ).subscribe(() => this.redirectService.redirectToHomepage(true))
169
170     navigationEndEvent.subscribe(e => {
171       this.hooks.runAction('action:router.navigation-end', 'common', { path: e.url })
172     })
173
174     eventsObs.pipe(
175       filter((e: Event): e is GuardsCheckStart => e instanceof GuardsCheckStart),
176       filter(() => this.screenService.isInSmallView())
177     ).subscribe(() => this.isMenuDisplayed = false) // User clicked on a link in the menu, change the page
178   }
179
180   private injectJS () {
181     // Inject JS
182     this.serverService.configLoaded
183         .subscribe(() => {
184           const config = this.serverService.getConfig()
185
186           if (config.instance.customizations.javascript) {
187             try {
188               // tslint:disable:no-eval
189               eval(config.instance.customizations.javascript)
190             } catch (err) {
191               console.error('Cannot eval custom JavaScript.', err)
192             }
193           }
194         })
195   }
196
197   private injectCSS () {
198     // Inject CSS if modified (admin config settings)
199     this.serverService.configLoaded
200         .pipe(skip(1)) // We only want to subscribe to reloads, because the CSS is already injected by the server
201         .subscribe(() => {
202           const headStyle = document.querySelector('style.custom-css-style')
203           if (headStyle) headStyle.parentNode.removeChild(headStyle)
204
205           const config = this.serverService.getConfig()
206
207           // We test customCSS if the admin removed the css
208           if (this.customCSS || config.instance.customizations.css) {
209             const styleTag = '<style>' + config.instance.customizations.css + '</style>'
210             this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag)
211           }
212         })
213   }
214
215   private async loadPlugins () {
216     this.pluginService.initializePlugins()
217
218     this.hooks.runAction('action:application.init', 'common')
219   }
220
221   private initHotkeys () {
222     this.hotkeysService.add([
223       new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => {
224         document.getElementById('search-video').focus()
225         return false
226       }, undefined, this.i18n('Focus the search bar')),
227       new Hotkey('b', (event: KeyboardEvent): boolean => {
228         this.toggleMenu()
229         return false
230       }, undefined, this.i18n('Toggle the left menu')),
231       new Hotkey('g o', (event: KeyboardEvent): boolean => {
232         this.router.navigate([ '/videos/overview' ])
233         return false
234       }, undefined, this.i18n('Go to the discover videos page')),
235       new Hotkey('g t', (event: KeyboardEvent): boolean => {
236         this.router.navigate([ '/videos/trending' ])
237         return false
238       }, undefined, this.i18n('Go to the trending videos page')),
239       new Hotkey('g r', (event: KeyboardEvent): boolean => {
240         this.router.navigate([ '/videos/recently-added' ])
241         return false
242       }, undefined, this.i18n('Go to the recently added videos page')),
243       new Hotkey('g l', (event: KeyboardEvent): boolean => {
244         this.router.navigate([ '/videos/local' ])
245         return false
246       }, undefined, this.i18n('Go to the local videos page')),
247       new Hotkey('g u', (event: KeyboardEvent): boolean => {
248         this.router.navigate([ '/videos/upload' ])
249         return false
250       }, undefined, this.i18n('Go to the videos upload page'))
251     ])
252   }
253 }