import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { SwyxTrayClient, type ConnectionStatus, type RawMessage } from '../swyx/SwyxTrayClient' import { mergeSnapshot, type CallEvent, type SnapshotMessage } from '../swyx/protocol' export interface LogEntry extends RawMessage { id: number } /** Zustand der App selbst, aus dem Snapshot – unabhängig von den Leitungen. */ export interface TrayState { connected?: boolean serverUp?: boolean overall?: string statusText?: string user?: string server?: string } const MAX_LOG_ENTRIES = 300 const MAX_HISTORY = 50 /** * Bindet den SwyxTrayClient an React: Verbindungsstatus, Zustand der Leitungen, * Ereignisverlauf und die Telefonie-Kommandos. */ export function useSwyxTray() { const [status, setStatus] = useState('idle') const [error, setError] = useState(null) const [log, setLog] = useState([]) const [history, setHistory] = useState([]) // Belegte Leitungen, nach Leitungsnummer; jeder Snapshot schreibt sie neu. const [lines, setLines] = useState>({}) const [tray, setTray] = useState({}) const [busy, setBusy] = useState(false) const clientRef = useRef(null) const logIdRef = useRef(0) // Stand der Leitungen für den Snapshot-Vergleich. const linesRef = useRef>({}) if (clientRef.current === null) { clientRef.current = new SwyxTrayClient() } const client = clientRef.current useEffect(() => { const unsubscribe = [ client.on('status', (next) => { setStatus(next) if (next === 'open') setError(null) // Der Leitungszustand der alten Verbindung ist nach dem Abbruch wertlos; // die App schickt beim Verbinden ohnehin einen Snapshot. if (next === 'closed') { linesRef.current = {} setLines({}) setTray({}) } }), client.on('error', (message) => setError(message)), client.on('raw', (message) => { setLog((entries) => { const next = [...entries, { ...message, id: logIdRef.current++ }] return next.length > MAX_LOG_ENTRIES ? next.slice(next.length - MAX_LOG_ENTRIES) : next }) }), client.on('snapshot', (snapshot: SnapshotMessage) => { setTray({ connected: snapshot.connected, serverUp: snapshot.serverUp, overall: snapshot.overall, statusText: snapshot.statusText, user: snapshot.user, server: snapshot.server, }) // Der Vollzustand ersetzt den bisherigen Stand; die Anruf-Ereignisse für // Verlauf und Benachrichtigung entstehen aus dem Vergleich mit ihm. // Der Vergleich läuft über die Ref, nicht im State-Updater – der wird // im StrictMode doppelt ausgeführt und würde den Verlauf verdoppeln. const { lines: next, events } = mergeSnapshot(linesRef.current, snapshot.lines) linesRef.current = next setLines(next) if (events.length > 0) { setHistory((entries) => [...[...events].reverse(), ...entries].slice(0, MAX_HISTORY)) } }), ] return () => { unsubscribe.forEach((off) => off()) } }, [client]) // Beim Verlassen der Seite Verbindung und Timer aufräumen. useEffect(() => () => client.dispose(), [client]) const connect = useCallback((url: string) => client.connect(url), [client]) const disconnect = useCallback(() => client.disconnect(), [client]) /** Führt ein Kommando aus und übernimmt Fehleranzeige und Busy-Zustand. */ const run = useCallback(async (action: () => Promise): Promise => { setBusy(true) setError(null) try { return await action() } catch (e) { setError(e instanceof Error ? e.message : String(e)) return null } finally { setBusy(false) } }, []) const dial = useCallback((number: string) => run(() => client.dial(number)), [client, run]) const answer = useCallback((line: number) => run(() => client.answer(line)), [client, run]) const hangup = useCallback((line: number) => run(() => client.hangup(line)), [client, run]) const refresh = useCallback(() => run(() => client.requestStatus()), [client, run]) const focusWindow = useCallback( (title: string, url?: string) => run(() => client.focusWindow(title, url)), [client, run], ) // Tabs des Firefox-Plugins; die App reicht diese drei Kommandos nur durch. const listTabs = useCallback(() => run(() => client.listTabs()), [client, run]) const openTab = useCallback((url: string) => run(() => client.openTab(url)), [client, run]) const closeTab = useCallback((tabId: number) => run(() => client.closeTab(tabId)), [client, run]) const sendRaw = useCallback( (text: string) => { try { client.sendRaw(text) return true } catch (e) { setError(e instanceof Error ? e.message : String(e)) return false } }, [client], ) const clearLog = useCallback(() => setLog([]), []) const calls = useMemo( () => Object.values(lines).sort((a, b) => a.line - b.line), [lines], ) /** Erste klingelnde Leitung eines eingehenden Anrufs. */ const ringingCall = useMemo( () => calls.find((call) => call.event === 'incoming') ?? null, [calls], ) return { status, error, log, history, calls, ringingCall, tray, busy, connect, disconnect, dial, answer, hangup, refresh, focusWindow, listTabs, openTab, closeTab, sendRaw, clearLog, } }