Files
start-os/ui/src/app/app.component.ts
2022-01-21 20:35:52 -07:00

313 lines
8.6 KiB
TypeScript

import { Component } from '@angular/core'
import { Storage } from '@ionic/storage'
import { AuthService, AuthState } from './services/auth.service'
import { ApiService } from './services/api/api.service'
import { Router, RoutesRecognized } from '@angular/router'
import { debounceTime, distinctUntilChanged, filter, finalize, takeWhile } from 'rxjs/operators'
import { AlertController, ToastController } from '@ionic/angular'
import { LoaderService } from './services/loader.service'
import { Emver } from './services/emver.service'
import { SplitPaneTracker } from './services/split-pane.service'
import { LoadingOptions } from '@ionic/core'
import { PatchDbModel } from './services/patch-db/patch-db.service'
import { HttpService } from './services/http.service'
import { ServerStatus } from './services/patch-db/data-model'
import { ConnectionFailure, ConnectionService } from './services/connection.service'
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent {
ServerStatus = ServerStatus
showMenu = false
selectedIndex = 0
offlineToast: HTMLIonToastElement
serverName: string
unreadCount: number
appPages = [
{
title: 'Installed Services',
url: '/services',
icon: 'grid-outline',
},
{
title: 'Embassy',
url: '/embassy',
icon: 'cube-outline',
},
{
title: 'Service Marketplace',
url: '/marketplace',
icon: 'storefront-outline',
},
{
title: 'Notifications',
url: '/notifications',
icon: 'notifications-outline',
},
]
constructor (
private readonly storage: Storage,
private readonly authService: AuthService,
private readonly router: Router,
private readonly api: ApiService,
private readonly http: HttpService,
private readonly alertCtrl: AlertController,
private readonly loader: LoaderService,
private readonly emver: Emver,
private readonly connectionService: ConnectionService,
private readonly toastCtrl: ToastController,
private readonly patch: PatchDbModel,
readonly splitPane: SplitPaneTracker,
) {
// set dark theme
document.body.classList.toggle('dark', true)
this.init()
}
async init () {
await this.storage.create()
await this.authService.init()
await this.emver.init()
await this.patch.init()
this.router.initialNavigation()
// watch auth
this.authService.watch$()
.subscribe(auth => {
// VERIFIED
if (auth === AuthState.VERIFIED) {
this.http.authReqEnabled = true
this.showMenu = true
this.patch.start()
// watch patch DB to display name and unread count
this.watchPatch()
this.connectionService.start()
// watch connection to display connectivity issues
this.watchConnection(auth)
// watch router to highlight selected menu item
this.watchRouter(auth)
// watch status to display/hide maintenance page
this.watchStatus(auth)
// watch unread notification count to display toast
this.watchNotifications(auth)
// UNVERIFIED
} else if (auth === AuthState.UNVERIFIED) {
this.http.authReqEnabled = false
this.showMenu = false
this.connectionService.stop()
this.patch.stop()
this.storage.clear()
this.router.navigate(['/login'], { replaceUrl: true })
}
})
this.http.watchUnauth$().subscribe(() => {
this.authService.setUnverified()
})
}
watchPatch (): void {
this.patch.watch$('server-info', 'unread-notification-count')
.subscribe(unread => {
this.unreadCount = unread
})
}
private watchConnection (auth: AuthState): void {
this.connectionService.watchFailure$()
.pipe(
distinctUntilChanged(),
debounceTime(500),
takeWhile(() => auth === AuthState.VERIFIED),
)
.subscribe(async connectionFailure => {
if (connectionFailure === ConnectionFailure.None) {
if (this.offlineToast) {
this.offlineToast.dismiss()
this.offlineToast = undefined
}
} else {
let message: string
switch (connectionFailure) {
case ConnectionFailure.Network:
message = 'No network'
break
case ConnectionFailure.Diagnosing:
message = 'Diagnosing'
break
case ConnectionFailure.Embassy:
message = 'Embassy is unreachable'
break
case ConnectionFailure.Tor:
message = 'Tor issues'
break
case ConnectionFailure.Internet:
message = 'No Internet'
break
}
await this.presentToastOffline(message)
}
})
}
private watchRouter (auth: AuthState): void {
this.router.events
.pipe(
filter((e: RoutesRecognized) => !!e.urlAfterRedirects),
takeWhile(() => auth === AuthState.VERIFIED),
)
.subscribe(e => {
const appPageIndex = this.appPages.findIndex(
appPage => e.urlAfterRedirects.startsWith(appPage.url),
)
if (appPageIndex > -1) this.selectedIndex = appPageIndex
})
}
private watchStatus (auth: AuthState): void {
this.patch.watch$('server-info', 'status')
.pipe(
takeWhile(() => auth === AuthState.VERIFIED),
)
.subscribe(status => {
const maintenance = '/maintenance'
const url = this.router.url
if (status === ServerStatus.Running && url.startsWith(maintenance)) {
this.router.navigate([''], { replaceUrl: true })
}
if ([ServerStatus.Updating, ServerStatus.BackingUp].includes(status) && !url.startsWith(maintenance)) {
this.router.navigate([maintenance], { replaceUrl: true })
}
})
}
private watchNotifications (auth: AuthState): void {
let previous: number
this.patch.watch$('server-info', 'unread-notification-count')
.pipe(
takeWhile(() => auth === AuthState.VERIFIED),
finalize(() => console.log('FINALIZING!!!')),
)
.subscribe(count => {
if (previous !== undefined && count > previous) this.presentToastNotifications()
previous = count
})
}
async presentAlertLogout () {
const alert = await this.alertCtrl.create({
backdropDismiss: false,
header: 'Caution',
message: 'Are you sure you want to logout?',
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Logout',
cssClass: 'alert-danger',
handler: () => {
this.logout()
},
},
],
})
await alert.present()
}
private async logout () {
this.loader.of(LoadingSpinner('Logging out...'))
.displayDuringP(this.api.logout({ }))
.then(() => this.authService.setUnverified())
.catch(e => this.setError(e))
}
private async presentToastNotifications () {
const toast = await this.toastCtrl.create({
header: 'Embassy',
message: `New notifications`,
position: 'bottom',
duration: 4000,
cssClass: 'notification-toast',
buttons: [
{
side: 'start',
icon: 'close',
handler: () => {
return true
},
},
{
side: 'end',
text: 'View',
handler: () => {
this.router.navigate(['/notifications'], { queryParams: { toast: true } })
},
},
],
})
await toast.present()
}
private async presentToastOffline (message: string) {
if (this.offlineToast) {
this.offlineToast.message = message
return
}
this.offlineToast = await this.toastCtrl.create({
header: 'Connection Issue',
message,
position: 'bottom',
duration: 0,
buttons: [
{
side: 'start',
icon: 'close',
handler: () => {
return true
},
},
],
})
await this.offlineToast.present()
}
private async setError (e: Error) {
console.error(e)
await this.presentError(e.message)
}
private async presentError (e: string) {
const alert = await this.alertCtrl.create({
backdropDismiss: true,
message: `Exception on logout: ${e}`,
buttons: [
{
text: 'Dismiss',
role: 'cancel',
},
],
})
await alert.present()
}
splitPaneVisible (e: any) {
this.splitPane.sidebarOpen$.next(e.detail.visible)
}
}
const LoadingSpinner: (m?: string) => LoadingOptions = (m) => {
const toMergeIn = m ? { message: m } : { }
return {
spinner: 'lines',
cssClass: 'loader',
...toMergeIn,
} as LoadingOptions
}