Files
start-os/frontend/projects/ui/src/app/app.component.ts
Drew Ansbacher 0c0cd9d0a0 start9 marketplace config
shift not unshift

move eos updates to embassy tab

selected id sub

roughly working

keep name in sync in case of change

delete commented code

64 img
2022-02-09 10:18:22 -07:00

474 lines
13 KiB
TypeScript

import { Component, HostListener, NgZone } from '@angular/core'
import { Storage } from '@ionic/storage-angular'
import { AuthService, AuthState } from './services/auth.service'
import { ApiService } from './services/api/embassy-api.service'
import { Router, RoutesRecognized } from '@angular/router'
import {
debounceTime,
distinctUntilChanged,
filter,
take,
} from 'rxjs/operators'
import {
AlertController,
IonicSafeString,
LoadingController,
ToastController,
} from '@ionic/angular'
import { Emver } from './services/emver.service'
import { SplitPaneTracker } from './services/split-pane.service'
import { ToastButton } from '@ionic/core'
import { PatchDbService } from './services/patch-db/patch-db.service'
import {
ServerStatus,
UIData,
UIMarketplaceData,
} from './services/patch-db/data-model'
import {
ConnectionFailure,
ConnectionService,
} from './services/connection.service'
import { StartupAlertsService } from './services/startup-alerts.service'
import { ConfigService } from './services/config.service'
import { debounce, isEmptyObject } from './util/misc.util'
import { ErrorToastService } from './services/error-toast.service'
import { Subscription } from 'rxjs'
import { LocalStorageService } from './services/local-storage.service'
import { EOSService } from './services/eos.service'
import { v4 } from 'uuid'
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent {
@HostListener('document:keydown.enter', ['$event'])
@debounce()
handleKeyboardEvent () {
const elems = document.getElementsByClassName('enter-click')
const elem = elems[elems.length - 1] as HTMLButtonElement
if (!elem || elem.classList.contains('no-click') || elem.disabled) return
if (elem) elem.click()
}
ServerStatus = ServerStatus
showMenu = false
selectedIndex = 0
offlineToast: HTMLIonToastElement
updateToast: HTMLIonToastElement
notificationToast: HTMLIonToastElement
serverName: string
unreadCount: number
subscriptions: Subscription[] = []
osUpdateProgress: { size: number; downloaded: number }
appPages = [
{
title: 'Services',
url: '/services',
icon: 'grid-outline',
},
{
title: 'Embassy',
url: '/embassy',
icon: 'cube-outline',
},
{
title: 'Marketplace',
url: '/marketplace',
icon: 'storefront-outline',
},
{
title: 'Notifications',
url: '/notifications',
icon: 'notifications-outline',
},
{
title: 'Developer Tools',
url: '/developer',
icon: 'hammer-outline',
},
]
constructor (
private readonly storage: Storage,
private readonly authService: AuthService,
private readonly router: Router,
private readonly embassyApi: ApiService,
private readonly alertCtrl: AlertController,
private readonly loadingCtrl: LoadingController,
private readonly emver: Emver,
private readonly connectionService: ConnectionService,
private readonly startupAlertsService: StartupAlertsService,
private readonly toastCtrl: ToastController,
private readonly errToast: ErrorToastService,
private readonly config: ConfigService,
private readonly zone: NgZone,
public readonly splitPane: SplitPaneTracker,
public readonly patch: PatchDbService,
public readonly localStorageService: LocalStorageService,
public readonly eosService: EOSService,
) {
this.init()
}
async init () {
await this.storage.create()
await this.authService.init()
await this.localStorageService.init()
this.router.initialNavigation()
// watch auth
this.authService.watch$().subscribe(async auth => {
// VERIFIED
if (auth === AuthState.VERIFIED) {
await this.patch.start()
this.showMenu = true
// if on the login screen, route to dashboard
if (this.router.url.startsWith('/login')) {
this.router.navigate([''], { replaceUrl: true })
}
this.subscriptions = this.subscriptions.concat([
// start the connection monitor
...this.connectionService.start(),
// watch connection to display connectivity issues
this.watchConnection(),
// watch router to highlight selected menu item
this.watchRouter(),
// watch status to display/hide maintenance page
])
this.patch
.watch$()
.pipe(
filter(obj => !isEmptyObject(obj)),
take(1),
)
.subscribe(data => {
// check for updates to EOS
this.checkForEosUpdate(data.ui)
// seed EOS marketplace as default for services too
this.seedMarketplace(data.ui.marketplace)
this.subscriptions = this.subscriptions.concat([
// watch status to present toast for updated state
this.watchStatus(),
// watch update-progress to present progress bar when server is updating
this.watchUpdateProgress(),
// watch version to refresh browser window
this.watchVersion(),
// watch unread notification count to display toast
this.watchNotifications(),
// run startup alerts
this.startupAlertsService.runChecks(),
])
})
// UNVERIFIED
} else if (auth === AuthState.UNVERIFIED) {
this.subscriptions.forEach(sub => sub.unsubscribe())
this.subscriptions = []
this.showMenu = false
this.patch.stop()
this.storage.clear()
if (this.errToast) this.errToast.dismiss()
if (this.updateToast) this.updateToast.dismiss()
if (this.notificationToast) this.notificationToast.dismiss()
if (this.offlineToast) this.offlineToast.dismiss()
this.zone.run(() => {
this.router.navigate(['/login'], { replaceUrl: true })
})
}
})
}
async goToWebsite (): Promise<void> {
let url: string
if (this.config.isTor()) {
url =
'http://privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion'
} else {
url = 'https://start9.com'
}
window.open(url, '_blank', 'noreferrer')
}
async presentAlertLogout () {
const alert = await this.alertCtrl.create({
header: 'Caution',
message:
'Do you know your password? If you log out and forget your password, you may permanently lose access to your Embassy.',
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Logout',
cssClass: 'enter-click',
handler: () => {
this.logout()
},
},
],
})
await alert.present()
}
private async checkForEosUpdate (ui: UIData): Promise<void> {
if (ui['auto-check-updates']) {
await this.eosService.getEOS()
}
}
private async seedMarketplace(marketplace: UIMarketplaceData): Promise<void> {
if (
!marketplace ||
!marketplace['known-hosts'] ||
!marketplace['selected-id']
) {
const uuid = v4()
const value: UIMarketplaceData = {
'selected-id': uuid,
'known-hosts': {
[uuid]: {
url: this.config.eosMarketplaceUrl,
name: 'Start9 Embassy Marketplace',
},
},
}
await this.embassyApi.setDbValue({ pointer: '/marketplace', value })
}
}
// should wipe cache independant of actual BE logout
private async logout () {
this.embassyApi.logout({})
this.authService.setUnverified()
}
private watchConnection (): Subscription {
return this.connectionService
.watchFailure$()
.pipe(distinctUntilChanged(), debounceTime(500))
.subscribe(async connectionFailure => {
if (connectionFailure === ConnectionFailure.None) {
if (this.offlineToast) {
await this.offlineToast.dismiss()
this.offlineToast = undefined
}
} else {
let message: string | IonicSafeString
let link: string
switch (connectionFailure) {
case ConnectionFailure.Network:
message = 'Phone or computer has no network connection.'
break
case ConnectionFailure.Tor:
message = 'Browser unable to connect over Tor.'
link =
'https://docs.start9.com/support/FAQ/troubleshooting.html#tor-failure'
break
case ConnectionFailure.Lan:
message = 'Embassy not found on Local Area Network.'
link =
'https://docs.start9.com/support/FAQ/troubleshooting.html#lan-failure'
break
}
await this.presentToastOffline(message, link)
}
})
}
private watchRouter (): Subscription {
return this.router.events
.pipe(filter((e: RoutesRecognized) => !!e.urlAfterRedirects))
.subscribe(e => {
const appPageIndex = this.appPages.findIndex(appPage =>
e.urlAfterRedirects.startsWith(appPage.url),
)
if (appPageIndex > -1) this.selectedIndex = appPageIndex
})
}
private watchStatus (): Subscription {
return this.patch.watch$('server-info', 'status').subscribe(status => {
if (status === ServerStatus.Updated && !this.updateToast) {
this.presentToastUpdated()
}
})
}
private watchUpdateProgress (): Subscription {
return this.patch
.watch$('server-info', 'update-progress')
.subscribe(progress => {
this.osUpdateProgress = progress
})
}
private watchVersion (): Subscription {
return this.patch.watch$('server-info', 'version').subscribe(version => {
if (this.emver.compare(this.config.version, version) !== 0) {
this.presentAlertRefreshNeeded()
}
})
}
private watchNotifications (): Subscription {
let previous: number
return this.patch
.watch$('server-info', 'unread-notification-count')
.subscribe(count => {
this.unreadCount = count
if (previous !== undefined && count > previous)
this.presentToastNotifications()
previous = count
})
}
private async presentAlertRefreshNeeded () {
const alert = await this.alertCtrl.create({
backdropDismiss: false,
header: 'Refresh Needed',
message:
'Your user interface is cached and out of date. Hard refresh the page to get the latest UI.',
buttons: [
{
text: 'Refresh Page',
cssClass: 'enter-click',
handler: () => {
location.reload()
},
},
],
})
await alert.present()
}
private async presentToastUpdated () {
if (this.updateToast) return
this.updateToast = await this.toastCtrl.create({
header: 'EOS download complete!',
message:
'Restart your Embassy for these updates to take effect. It can take several minutes to come back online.',
position: 'bottom',
duration: 0,
cssClass: 'success-toast',
buttons: [
{
side: 'start',
icon: 'close',
handler: () => {
return true
},
},
{
side: 'end',
text: 'Restart',
handler: () => {
this.restart()
},
},
],
})
await this.updateToast.present()
}
private async presentToastNotifications () {
if (this.notificationToast) return
this.notificationToast = await this.toastCtrl.create({
header: 'Embassy',
message: `New notifications`,
position: 'bottom',
duration: 4000,
buttons: [
{
side: 'start',
icon: 'close',
handler: () => {
return true
},
},
{
side: 'end',
text: 'View',
handler: () => {
this.router.navigate(['/notifications'], {
queryParams: { toast: true },
})
},
},
],
})
await this.notificationToast.present()
}
private async presentToastOffline (
message: string | IonicSafeString,
link?: string,
) {
if (this.offlineToast) {
this.offlineToast.message = message
return
}
let buttons: ToastButton[] = [
{
side: 'start',
icon: 'close',
handler: () => {
return true
},
},
]
if (link) {
buttons.push({
side: 'end',
text: 'View solutions',
handler: () => {
window.open(link, '_blank', 'noreferrer')
return false
},
})
}
this.offlineToast = await this.toastCtrl.create({
header: 'Unable to Connect',
cssClass: 'warning-toast',
message,
position: 'bottom',
duration: 0,
buttons,
})
await this.offlineToast.present()
}
private async restart (): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Restarting...',
cssClass: 'loader',
})
await loader.present()
try {
await this.embassyApi.restartServer({})
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
splitPaneVisible (e: any) {
this.splitPane.sidebarOpen$.next(e.detail.visible)
}
}