]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - client/src/app/app.component.ts
Add welcome modal
[github/Chocobozzz/PeerTube.git] / client / src / app / app.component.ts
CommitLineData
43d0ea7f 1import { Component, OnInit, ViewChild } from '@angular/core'
00b5556c 2import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
489290b8 3import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular/router'
1a00c561 4import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core'
989e526a 5import { is18nPath } from '../../../shared/models/i18n'
bbe0f064 6import { ScreenService } from '@app/shared/misc/screen.service'
43d0ea7f 7import { debounceTime, filter, map, pairwise, skip, switchMap } from 'rxjs/operators'
489290b8 8import { Hotkey, HotkeysService } from 'angular2-hotkeys'
e33f888b 9import { I18n } from '@ngx-translate/i18n-polyfill'
a5858241 10import { fromEvent } from 'rxjs'
4334445d 11import { PlatformLocation, ViewportScroller } from '@angular/common'
18a6f04c 12import { PluginService } from '@app/core/plugins/plugin.service'
93cae479 13import { HooksService } from '@app/core/plugins/hooks.service'
4334445d 14import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
60c2bc80 15import { POP_STATE_MODAL_DISMISS } from '@app/shared/misc/constants'
43d0ea7f
C
16import { WelcomeModalComponent } from '@app/modal/welcome-modal.component'
17import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
18import { UserRole } from '@shared/models'
19import { User } from '@app/shared'
20import { InstanceService } from '@app/shared/instance/instance.service'
e2a2d6c8 21
dc8bc31b 22@Component({
3154f382
C
23 selector: 'my-app',
24 templateUrl: './app.component.html',
25 styleUrls: [ './app.component.scss' ]
dc8bc31b 26})
e2a2d6c8 27export class AppComponent implements OnInit {
43d0ea7f
C
28 @ViewChild('welcomeModal', { static: false }) welcomeModal: WelcomeModalComponent
29 @ViewChild('instanceConfigWarningModal', { static: false }) instanceConfigWarningModal: InstanceConfigWarningModalComponent
30
df98563e 31 isMenuDisplayed = true
a5858241 32 isMenuChangedByUser = false
67167390 33
00b5556c
C
34 customCSS: SafeHtml
35
df98563e 36 constructor (
e33f888b 37 private i18n: I18n,
489290b8 38 private viewportScroller: ViewportScroller,
3154f382 39 private router: Router,
e2a2d6c8 40 private authService: AuthService,
00b5556c 41 private serverService: ServerService,
18a6f04c 42 private pluginService: PluginService,
43d0ea7f 43 private instanceService: InstanceService,
901637bb 44 private domSanitizer: DomSanitizer,
bbe0f064 45 private redirectService: RedirectService,
ee1fc23a 46 private screenService: ScreenService,
1a00c561 47 private hotkeysService: HotkeysService,
93cae479 48 private themeService: ThemeService,
4334445d
C
49 private hooks: HooksService,
50 private location: PlatformLocation,
51 private modalService: NgbModal
989e526a 52 ) { }
a99593ed 53
915c5bbe
C
54 get serverVersion () {
55 return this.serverService.getConfig().serverVersion
56 }
57
abb2c792
RK
58 get serverCommit () {
59 const commit = this.serverService.getConfig().serverCommit || ''
60 return (commit !== '') ? '...' + commit : commit
61 }
62
36f9424f
C
63 get instanceName () {
64 return this.serverService.getConfig().instance.name
65 }
66
29f9b562
C
67 get defaultRoute () {
68 return RedirectService.DEFAULT_ROUTE
69 }
70
df98563e 71 ngOnInit () {
b3eeb529 72 document.getElementById('incompatible-browser').className += ' browser-ok'
73e09f27 73
d592e0a9
C
74 this.authService.loadClientCredentials()
75
d414207f 76 if (this.isUserLoggedIn()) {
e2a2d6c8 77 // The service will automatically redirect to the login page if the token is not valid anymore
bcd9f81e 78 this.authService.refreshUserInformation()
e2a2d6c8 79 }
6e07c3de 80
db7af09b
C
81 // Load custom data from server
82 this.serverService.loadConfig()
83 this.serverService.loadVideoCategories()
84 this.serverService.loadVideoLanguages()
85 this.serverService.loadVideoLicences()
fd45e8f4 86 this.serverService.loadVideoPrivacies()
830b4faf 87 this.serverService.loadVideoPlaylistPrivacies()
3eeeb87f 88
18a6f04c 89 this.loadPlugins()
ffb321be 90 this.themeService.initialize()
18a6f04c 91
3eeeb87f 92 // Do not display menu on small screens
bbe0f064 93 if (this.screenService.isInSmallView()) {
df98563e 94 this.isMenuDisplayed = false
3eeeb87f 95 }
c8cf5952 96
489290b8
C
97 this.initRouteEvents()
98 this.injectJS()
99 this.injectCSS()
100
101 this.initHotkeys()
102
103 fromEvent(window, 'resize')
104 .pipe(debounceTime(200))
105 .subscribe(() => this.onResize())
4334445d 106
60c2bc80 107 this.location.onPopState(() => this.modalService.dismissAll(POP_STATE_MODAL_DISMISS))
43d0ea7f
C
108
109 this.openModalsIfNeeded()
489290b8
C
110 }
111
112 isUserLoggedIn () {
113 return this.authService.isLoggedIn()
114 }
115
116 toggleMenu () {
117 this.isMenuDisplayed = !this.isMenuDisplayed
118 this.isMenuChangedByUser = true
119 }
120
121 onResize () {
122 this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser
123 }
124
125 private initRouteEvents () {
126 let resetScroll = true
127 const eventsObs = this.router.events
128
129 const scrollEvent = eventsObs.pipe(filter((e: Event): e is Scroll => e instanceof Scroll))
489290b8
C
130
131 scrollEvent.subscribe(e => {
132 if (e.position) {
133 return this.viewportScroller.scrollToPosition(e.position)
c8cf5952 134 }
00b5556c 135
489290b8
C
136 if (e.anchor) {
137 return this.viewportScroller.scrollToAnchor(e.anchor)
138 }
139
140 if (resetScroll) {
141 return this.viewportScroller.scrollToPosition([ 0, 0 ])
142 }
143 })
144
60c2bc80
C
145 const navigationEndEvent = eventsObs.pipe(filter((e: Event): e is NavigationEnd => e instanceof NavigationEnd))
146
489290b8
C
147 // When we add the a-state parameter, we don't want to alter the scroll
148 navigationEndEvent.pipe(pairwise())
149 .subscribe(([ e1, e2 ]) => {
150 try {
151 resetScroll = false
152
722bca90
C
153 const previousUrl = new URL(window.location.origin + e1.urlAfterRedirects)
154 const nextUrl = new URL(window.location.origin + e2.urlAfterRedirects)
489290b8
C
155
156 if (previousUrl.pathname !== nextUrl.pathname) {
157 resetScroll = true
158 return
159 }
160
161 const nextSearchParams = nextUrl.searchParams
162 nextSearchParams.delete('a-state')
163
164 const previousSearchParams = previousUrl.searchParams
165
166 nextSearchParams.sort()
167 previousSearchParams.sort()
168
169 if (nextSearchParams.toString() !== previousSearchParams.toString()) {
170 resetScroll = true
171 }
172 } catch (e) {
173 console.error('Cannot parse URL to check next scroll.', e)
174 resetScroll = true
175 }
176 })
177
178 navigationEndEvent.pipe(
179 map(() => window.location.pathname),
180 filter(pathname => !pathname || pathname === '/' || is18nPath(pathname))
181 ).subscribe(() => this.redirectService.redirectToHomepage(true))
182
23bdacf8
C
183 navigationEndEvent.subscribe(e => {
184 this.hooks.runAction('action:router.navigation-end', 'common', { path: e.url })
185 })
186
489290b8
C
187 eventsObs.pipe(
188 filter((e: Event): e is GuardsCheckStart => e instanceof GuardsCheckStart),
189 filter(() => this.screenService.isInSmallView())
190 ).subscribe(() => this.isMenuDisplayed = false) // User clicked on a link in the menu, change the page
191 }
192
193 private injectJS () {
e032aec9 194 // Inject JS
00b5556c 195 this.serverService.configLoaded
e032aec9
C
196 .subscribe(() => {
197 const config = this.serverService.getConfig()
198
199 if (config.instance.customizations.javascript) {
200 try {
201 // tslint:disable:no-eval
202 eval(config.instance.customizations.javascript)
203 } catch (err) {
204 console.error('Cannot eval custom JavaScript.', err)
205 }
206 }
207 })
489290b8 208 }
00b5556c 209
489290b8 210 private injectCSS () {
e032aec9
C
211 // Inject CSS if modified (admin config settings)
212 this.serverService.configLoaded
213 .pipe(skip(1)) // We only want to subscribe to reloads, because the CSS is already injected by the server
214 .subscribe(() => {
215 const headStyle = document.querySelector('style.custom-css-style')
216 if (headStyle) headStyle.parentNode.removeChild(headStyle)
00b5556c 217
e032aec9
C
218 const config = this.serverService.getConfig()
219
220 // We test customCSS if the admin removed the css
221 if (this.customCSS || config.instance.customizations.css) {
222 const styleTag = '<style>' + config.instance.customizations.css + '</style>'
223 this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag)
00b5556c 224 }
e032aec9 225 })
489290b8 226 }
ee1fc23a 227
18a6f04c
C
228 private async loadPlugins () {
229 this.pluginService.initializePlugins()
230
c9e3eeed 231 this.hooks.runAction('action:application.init', 'common')
18a6f04c
C
232 }
233
43d0ea7f
C
234 private async openModalsIfNeeded () {
235 this.serverService.configLoaded
236 .pipe(
237 switchMap(() => this.authService.userInformationLoaded),
238 map(() => this.authService.getUser()),
239 filter(user => user.role === UserRole.ADMINISTRATOR)
240 ).subscribe(user => setTimeout(() => this.openAdminModals(user))) // setTimeout because of ngIf in template
241 }
242
243 private async openAdminModals (user: User) {
244 if (user.noWelcomeModal !== true) return this.welcomeModal.show()
245
246 const config = this.serverService.getConfig()
247
248 if (user.noInstanceConfigWarningModal !== true && config.signup.allowed && config.instance.name.toLowerCase() === 'peertube') {
249 this.instanceService.getAbout()
250 .subscribe(about => {
251 if (!about.instance.terms) {
252 this.instanceConfigWarningModal.show()
253 }
254 })
255 }
256 }
257
489290b8 258 private initHotkeys () {
ee1fc23a 259 this.hotkeysService.add([
8542dc33 260 new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => {
ee1fc23a 261 document.getElementById('search-video').focus()
8542dc33 262 return false
e33f888b 263 }, undefined, this.i18n('Focus the search bar')),
43d0ea7f 264
8542dc33
RK
265 new Hotkey('b', (event: KeyboardEvent): boolean => {
266 this.toggleMenu()
267 return false
e33f888b 268 }, undefined, this.i18n('Toggle the left menu')),
43d0ea7f 269
20d21199 270 new Hotkey('g o', (event: KeyboardEvent): boolean => {
a54991da
RK
271 this.router.navigate([ '/videos/overview' ])
272 return false
79a89941 273 }, undefined, this.i18n('Go to the discover videos page')),
43d0ea7f 274
20d21199 275 new Hotkey('g t', (event: KeyboardEvent): boolean => {
ee1fc23a
RK
276 this.router.navigate([ '/videos/trending' ])
277 return false
e33f888b 278 }, undefined, this.i18n('Go to the trending videos page')),
43d0ea7f 279
20d21199 280 new Hotkey('g r', (event: KeyboardEvent): boolean => {
ee1fc23a
RK
281 this.router.navigate([ '/videos/recently-added' ])
282 return false
e33f888b 283 }, undefined, this.i18n('Go to the recently added videos page')),
43d0ea7f 284
20d21199 285 new Hotkey('g l', (event: KeyboardEvent): boolean => {
ee1fc23a
RK
286 this.router.navigate([ '/videos/local' ])
287 return false
e33f888b 288 }, undefined, this.i18n('Go to the local videos page')),
43d0ea7f 289
20d21199 290 new Hotkey('g u', (event: KeyboardEvent): boolean => {
ee1fc23a
RK
291 this.router.navigate([ '/videos/upload' ])
292 return false
ffb321be 293 }, undefined, this.i18n('Go to the videos upload page'))
ee1fc23a 294 ])
67167390 295 }
dc8bc31b 296}