/* eslint-disable @angular-eslint/component-class-suffix */ import { Component, OnInit, OnDestroy, inject, HostListener } from '@angular/core'; import { Router, RouterOutlet, NavigationEnd } from '@angular/router'; import { CommonModule } from '@angular/common'; import { Store } from '@ngrx/store'; import { DatabaseService } from './infrastructure/persistence'; import { DesktopAppUpdateService } from './core/services/desktop-app-update.service'; import { ServerDirectoryFacade } from './domains/server-directory'; import { TimeSyncService } from './core/services/time-sync.service'; import { VoiceSessionFacade } from './domains/voice-session'; import { ExternalLinkService } from './core/platform'; import { SettingsModalService } from './core/services/settings-modal.service'; import { ElectronBridgeService } from './core/platform/electron/electron-bridge.service'; import { ServersRailComponent } from './features/servers/servers-rail.component'; import { TitleBarComponent } from './features/shell/title-bar.component'; import { FloatingVoiceControlsComponent } from './domains/voice-session/feature/floating-voice-controls/floating-voice-controls.component'; import { SettingsModalComponent } from './features/settings/settings-modal/settings-modal.component'; import { DebugConsoleComponent } from './shared/components/debug-console/debug-console.component'; import { ScreenShareSourcePickerComponent } from './shared/components/screen-share-source-picker/screen-share-source-picker.component'; import { UsersActions } from './store/users/users.actions'; import { RoomsActions } from './store/rooms/rooms.actions'; import { selectCurrentRoom } from './store/rooms/rooms.selectors'; import { ROOM_URL_PATTERN, STORAGE_KEY_CURRENT_USER_ID, STORAGE_KEY_LAST_VISITED_ROUTE } from './core/constants'; @Component({ selector: 'app-root', imports: [ CommonModule, RouterOutlet, ServersRailComponent, TitleBarComponent, FloatingVoiceControlsComponent, SettingsModalComponent, DebugConsoleComponent, ScreenShareSourcePickerComponent ], templateUrl: './app.html', styleUrl: './app.scss' }) export class App implements OnInit, OnDestroy { store = inject(Store); currentRoom = this.store.selectSignal(selectCurrentRoom); desktopUpdates = inject(DesktopAppUpdateService); desktopUpdateState = this.desktopUpdates.state; private databaseService = inject(DatabaseService); private router = inject(Router); private servers = inject(ServerDirectoryFacade); private settingsModal = inject(SettingsModalService); private timeSync = inject(TimeSyncService); private voiceSession = inject(VoiceSessionFacade); private externalLinks = inject(ExternalLinkService); private electronBridge = inject(ElectronBridgeService); private deepLinkCleanup: (() => void) | null = null; @HostListener('document:click', ['$event']) onGlobalLinkClick(evt: MouseEvent): void { this.externalLinks.handleClick(evt); } async ngOnInit(): Promise { void this.desktopUpdates.initialize(); await this.databaseService.initialize(); try { const apiBase = this.servers.getApiBaseUrl(); await this.timeSync.syncWithEndpoint(apiBase); } catch {} await this.setupDesktopDeepLinks(); this.store.dispatch(UsersActions.loadCurrentUser()); this.store.dispatch(RoomsActions.loadRooms()); const currentUserId = localStorage.getItem(STORAGE_KEY_CURRENT_USER_ID); if (!currentUserId) { if (!this.isPublicRoute(this.router.url)) { this.router.navigate(['/login'], { queryParams: { returnUrl: this.router.url } }).catch(() => {}); } } else { const last = localStorage.getItem(STORAGE_KEY_LAST_VISITED_ROUTE); if (last && typeof last === 'string') { const current = this.router.url; if (current === '/' || current === '/search') { this.router.navigate([last], { replaceUrl: true }).catch(() => {}); } } } this.router.events.subscribe((evt) => { if (evt instanceof NavigationEnd) { const url = evt.urlAfterRedirects || evt.url; localStorage.setItem(STORAGE_KEY_LAST_VISITED_ROUTE, url); const roomMatch = url.match(ROOM_URL_PATTERN); const currentRoomId = roomMatch ? roomMatch[1] : null; this.voiceSession.checkCurrentRoute(currentRoomId); } }); } ngOnDestroy(): void { this.deepLinkCleanup?.(); this.deepLinkCleanup = null; } openNetworkSettings(): void { this.settingsModal.open('network'); } openUpdatesSettings(): void { this.settingsModal.open('updates'); } async refreshDesktopUpdateContext(): Promise { await this.desktopUpdates.refreshServerContext(); } async restartToApplyUpdate(): Promise { await this.desktopUpdates.restartToApplyUpdate(); } private async setupDesktopDeepLinks(): Promise { const electronApi = this.electronBridge.getApi(); if (!electronApi) { return; } this.deepLinkCleanup = electronApi.onDeepLinkReceived?.((url) => { void this.handleDesktopDeepLink(url); }) || null; const pendingDeepLink = await electronApi.consumePendingDeepLink?.(); if (pendingDeepLink) { await this.handleDesktopDeepLink(pendingDeepLink); } } private async handleDesktopDeepLink(url: string): Promise { const invite = this.parseDesktopInviteUrl(url); if (!invite) { return; } await this.router.navigate(['/invite', invite.inviteId], { queryParams: { server: invite.sourceUrl } }); } private isPublicRoute(url: string): boolean { return url === '/login' || url === '/register' || url.startsWith('/invite/'); } private parseDesktopInviteUrl(url: string): { inviteId: string; sourceUrl: string } | null { try { const parsedUrl = new URL(url); if (parsedUrl.protocol !== 'toju:') { return null; } const pathSegments = [parsedUrl.hostname, ...parsedUrl.pathname.split('/').filter(Boolean)] .map((segment) => decodeURIComponent(segment)); if (pathSegments[0] !== 'invite' || !pathSegments[1]) { return null; } const sourceUrl = parsedUrl.searchParams.get('server')?.trim(); if (!sourceUrl) { return null; } return { inviteId: pathSegments[1], sourceUrl }; } catch { return null; } } }