Repository navigation
Expand file tree
/
Copy pathavailability.ts
More file actions
77 lines (66 loc) · 2.77 KB
/
Copy pathavailability.ts
File metadata and controls
77 lines (66 loc) · 2.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import { useEffect } from 'react';
import { create } from 'zustand';
// Whether voice rounds can run in this browser. Voice needs the Web Speech
// recogniser and a microphone the student has not blocked. When either is
// missing, voice mode is hidden with a short reason and typing carries on.
export type VoiceUnavailableReason = 'unsupported' | 'denied';
const NOTICE_STORAGE_KEY = 'debatelab-voice-notice-v1';
function recognitionSupported(): boolean {
return typeof window !== 'undefined' && Boolean(window.SpeechRecognition || window.webkitSpeechRecognition);
}
function noticeAcknowledged(): boolean {
try {
return window.localStorage.getItem(NOTICE_STORAGE_KEY) === 'acknowledged';
} catch {
return false;
}
}
interface VoiceAvailabilityState {
microphoneDenied: boolean;
/** The student has read the first-use privacy notice in this browser. */
noticeAcknowledged: boolean;
markMicrophoneDenied: () => void;
setMicrophoneDenied: (denied: boolean) => void;
acknowledgeNotice: () => void;
}
export const useVoiceAvailabilityStore = create<VoiceAvailabilityState>((set) => ({
microphoneDenied: false,
noticeAcknowledged: typeof window !== 'undefined' && noticeAcknowledged(),
markMicrophoneDenied: () => set({ microphoneDenied: true }),
setMicrophoneDenied: (microphoneDenied) => set({ microphoneDenied }),
acknowledgeNotice: () => {
try { window.localStorage.setItem(NOTICE_STORAGE_KEY, 'acknowledged'); } catch { /* storage unavailable */ }
set({ noticeAcknowledged: true });
},
}));
export interface VoiceAvailability {
available: boolean;
reason: VoiceUnavailableReason | null;
}
export function useVoiceAvailability(): VoiceAvailability {
const microphoneDenied = useVoiceAvailabilityStore((state) => state.microphoneDenied);
const setMicrophoneDenied = useVoiceAvailabilityStore((state) => state.setMicrophoneDenied);
const supported = recognitionSupported();
useEffect(() => {
if (!supported || !navigator.permissions?.query) return;
let status: PermissionStatus | undefined;
let cancelled = false;
const sync = () => {
if (status) setMicrophoneDenied(status.state === 'denied');
};
// Some browsers do not know the "microphone" permission name and reject.
navigator.permissions.query({ name: 'microphone' as PermissionName }).then((result) => {
if (cancelled) return;
status = result;
sync();
result.addEventListener('change', sync);
}).catch((): void => {});
return () => {
cancelled = true;
status?.removeEventListener('change', sync);
};
}, [setMicrophoneDenied, supported]);
if (!supported) return { available: false, reason: 'unsupported' };
if (microphoneDenied) return { available: false, reason: 'denied' };
return { available: true, reason: null };
}