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
75 lines
2.9 KiB
TypeScript
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
|
|
}
|
|
};
|