import { Injectable, OnDestroy, NgZone, inject } from '@angular/core'; import { Store } from '@ngrx/store'; import { UsersActions } from '../../store/users/users.actions'; import { selectManualStatus, selectCurrentUser } from '../../store/users/users.selectors'; import { RealtimeSessionFacade } from '../realtime'; import { NotificationAudioService } from './notification-audio.service'; import { UserStatus } from '../../shared-kernel'; const BROWSER_IDLE_POLL_MS = 10_000; const BROWSER_IDLE_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes /** * Orchestrates user status based on idle detection (Electron powerMonitor * or browser-fallback) and manual overrides (e.g. Do Not Disturb). * * Manual status always takes priority over automatic idle detection. * When manual status is cleared, the service falls back to automatic. */ @Injectable({ providedIn: 'root' }) export class UserStatusService implements OnDestroy { private store = inject(Store); private zone = inject(NgZone); private webrtc = inject(RealtimeSessionFacade); private audio = inject(NotificationAudioService); private electronCleanup: (() => void) | null = null; private browserPollTimer: ReturnType | null = null; private lastActivityTimestamp = Date.now(); private browserActivityListeners: (() => void)[] = []; private currentAutoStatus: UserStatus = 'online'; private started = false; start(): void { if (this.started) return; this.started = true; if ((window as any).electronAPI?.onIdleStateChanged) { this.startElectronIdleDetection(); } else { this.startBrowserIdleDetection(); } } /** Set a manual status override (e.g. DND = 'busy'). Pass `null` to clear. */ setManualStatus(status: UserStatus | null): void { this.store.dispatch(UsersActions.setManualStatus({ status })); this.audio.dndMuted.set(status === 'busy'); this.broadcastStatus(this.resolveEffectiveStatus(status)); } ngOnDestroy(): void { this.cleanup(); } private cleanup(): void { this.electronCleanup?.(); this.electronCleanup = null; if (this.browserPollTimer) { clearInterval(this.browserPollTimer); this.browserPollTimer = null; } for (const remove of this.browserActivityListeners) { remove(); } this.browserActivityListeners = []; this.started = false; } private startElectronIdleDetection(): void { const api = (window as { electronAPI?: { onIdleStateChanged: (cb: (state: 'active' | 'idle') => void) => () => void; getIdleState: () => Promise<'active' | 'idle'>; }; }).electronAPI!; this.electronCleanup = api.onIdleStateChanged((idleState: 'active' | 'idle') => { this.zone.run(() => { this.currentAutoStatus = idleState === 'idle' ? 'away' : 'online'; this.applyAutoStatusIfAllowed(); }); }); // Check initial state api.getIdleState().then((idleState: 'active' | 'idle') => { this.zone.run(() => { this.currentAutoStatus = idleState === 'idle' ? 'away' : 'online'; this.applyAutoStatusIfAllowed(); }); }); } private startBrowserIdleDetection(): void { this.lastActivityTimestamp = Date.now(); const onActivity = () => { this.lastActivityTimestamp = Date.now(); const wasAway = this.currentAutoStatus === 'away'; if (wasAway) { this.currentAutoStatus = 'online'; this.zone.run(() => this.applyAutoStatusIfAllowed()); } }; const events = [ 'mousemove', 'keydown', 'mousedown', 'touchstart', 'scroll' ] as const; for (const evt of events) { document.addEventListener(evt, onActivity, { passive: true }); this.browserActivityListeners.push(() => document.removeEventListener(evt, onActivity) ); } this.zone.runOutsideAngular(() => { this.browserPollTimer = setInterval(() => { const idle = Date.now() - this.lastActivityTimestamp >= BROWSER_IDLE_THRESHOLD_MS; if (idle && this.currentAutoStatus !== 'away') { this.currentAutoStatus = 'away'; this.zone.run(() => this.applyAutoStatusIfAllowed()); } }, BROWSER_IDLE_POLL_MS); }); } private applyAutoStatusIfAllowed(): void { const manualStatus = this.store.selectSignal(selectManualStatus)(); // Manual status overrides automatic if (manualStatus) return; const currentUser = this.store.selectSignal(selectCurrentUser)(); if (currentUser?.status !== this.currentAutoStatus) { this.store.dispatch(UsersActions.setManualStatus({ status: null })); this.store.dispatch(UsersActions.updateCurrentUser({ updates: { status: this.currentAutoStatus } })); this.broadcastStatus(this.currentAutoStatus); } } private resolveEffectiveStatus(manualStatus: UserStatus | null): UserStatus { return manualStatus ?? this.currentAutoStatus; } private broadcastStatus(status: UserStatus): void { this.webrtc.sendRawMessage({ type: 'status_update', status }); } }