Files
cinny/src/app/plugins/call/expectedDevices.ts
T
Lotus CIandClaude Opus 5.5 7bd1c14a3c feat(call): ask before joining when your usual mic / headphones are missing (#121)
The fork now checks, before joining (lotusDeviceCheck=1), that the mic /
output the user last picked are plugged in. When one is missing it asks
us (io.lotus.device_missing): "Your usual microphone isn't connected.
Arctis 7 isn't connected. Join with Laptop Mic instead?" [Join anyway]
[Cancel]. Join anyway isn't asked again this session for the same
device(s); Cancel closes the call before it starts. A device lost during
a call is a toast ("Arctis 7 disconnected — now using Laptop Mic").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-24 19:49:50 -04:00

75 lines
2.9 KiB
TypeScript

/**
* [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<string, unknown>;
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
}
};