/** * [Gitea #121] The fork's expected-device check. Before joining, EC reports * the user's usual mic / output (the one they last picked) as missing; we ask * "Join anyway" or "Cancel". In a call, losing it is a toast. Pure helpers. */ export type DeviceKind = 'audioinput' | 'audiooutput'; export type MissingDevice = { kind: DeviceKind; expected: string; fallback: string | null }; const isKind = (k: unknown): k is DeviceKind => k === 'audioinput' || k === 'audiooutput'; const parseOne = (v: unknown): MissingDevice | undefined => { if (!v || typeof v !== 'object') return undefined; const { kind, expected, fallback } = v as Record; if (!isKind(kind) || typeof expected !== 'string' || !expected) return undefined; return { kind, expected, fallback: typeof fallback === 'string' && fallback ? fallback : null }; }; /** `io.lotus.device_missing` → the missing devices (empty if malformed). */ export const parseDeviceMissing = (data: unknown): MissingDevice[] => { const list = (data as { missing?: unknown } | undefined)?.missing; if (!Array.isArray(list)) return []; return list.map(parseOne).filter((d): d is MissingDevice => !!d); }; /** `io.lotus.device_lost` → the device that went away. */ export const parseDeviceLost = (data: unknown): MissingDevice | undefined => parseOne(data); const what = (kind: DeviceKind): string => (kind === 'audioinput' ? 'microphone' : 'audio output'); /** "Your usual microphone **Arctis 7** isn't connected." lines + the fallback. */ export const describeMissing = ( d: MissingDevice, ): { title: string; expected: string; fallback: string } => ({ title: `Your usual ${what(d.kind)} isn't connected`, expected: d.expected, fallback: d.fallback ?? (d.kind === 'audioinput' ? 'the default microphone' : 'the default output'), }); export const describeLost = (d: MissingDevice): string => `${d.expected} disconnected — now using ${ d.fallback ?? (d.kind === 'audioinput' ? 'the default microphone' : 'the default output') }.`; // "Join anyway" holds for the rest of the session for the same device(s). const ACCEPTED_KEY = 'lotus-device-check-accepted'; const keyOf = (missing: MissingDevice[]): string => missing .map((d) => `${d.kind}:${d.expected}`) .sort() .join('|'); const readAccepted = (): string[] => { try { const v: unknown = JSON.parse(sessionStorage.getItem(ACCEPTED_KEY) ?? '[]'); return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : []; } catch { return []; } }; export const isAcceptedThisSession = (missing: MissingDevice[]): boolean => readAccepted().includes(keyOf(missing)); export const acceptForSession = (missing: MissingDevice[]): void => { try { const list = readAccepted(); const k = keyOf(missing); if (!list.includes(k)) sessionStorage.setItem(ACCEPTED_KEY, JSON.stringify([...list, k])); } catch { // storage unavailable: ask again next time } };