From 9d4679d260226dff35de3751dc44e8546258cd6f Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Wed, 3 Jun 2026 00:45:43 -0400 Subject: [PATCH] feat: configurable keybindings for push-to-deafen and quick switcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add deafenKey (default M) and quickSwitcherKey (default P) to settingsAtom - Settings → Calls: Push to Deafen keybind tile using shared useKeyBind hook - Settings → Keyboard Shortcuts: new section with Quick Room Switcher keybind - Extract useKeyBind + keyLabel helpers to reduce duplication in Calls section - CallControls reads deafenKey from settings (reactive, re-registers on change) - ClientNonUIFeatures reads quickSwitcherKey from settings (same pattern) - QuickSwitcher now toggles open/closed on repeat press (Ctrl+key again closes) Co-Authored-By: Claude Sonnet 4.6 --- src/app/features/call/CallControls.tsx | 5 +- src/app/features/settings/general/General.tsx | 129 +++++++++++++----- src/app/pages/client/ClientNonUIFeatures.tsx | 7 +- src/app/state/settings.ts | 6 + 4 files changed, 107 insertions(+), 40 deletions(-) diff --git a/src/app/features/call/CallControls.tsx b/src/app/features/call/CallControls.tsx index 4ffccc7ff..1b9e73f5d 100644 --- a/src/app/features/call/CallControls.tsx +++ b/src/app/features/call/CallControls.tsx @@ -83,6 +83,7 @@ export function CallControls({ callEmbed }: CallControlsProps) { }, [shareConfirm]); const [pttMode] = useSetting(settingsAtom, 'pttMode'); const [pttKey] = useSetting(settingsAtom, 'pttKey'); + const [deafenKey] = useSetting(settingsAtom, 'deafenKey'); const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal'); const [pttActive, setPttActive] = useState(false); @@ -209,7 +210,7 @@ export function CallControls({ callEmbed }: CallControlsProps) { return false; }; const onKeyDown = (e: KeyboardEvent) => { - if (e.code !== 'KeyM') return; + if (e.code !== deafenKey) return; if (e.repeat) return; if (isEditable(e.target as HTMLElement)) return; e.preventDefault(); @@ -217,7 +218,7 @@ export function CallControls({ callEmbed }: CallControlsProps) { }; window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); - }, [callEmbed]); + }, [callEmbed, deafenKey]); const [hangupState, hangup] = useAsyncCallback( useCallback(() => callEmbed.hangup(), [callEmbed]), diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx index 3b6ea06eb..32cbe5bcc 100644 --- a/src/app/features/settings/general/General.tsx +++ b/src/app/features/settings/general/General.tsx @@ -900,6 +900,37 @@ function Privacy() { ); } +function useKeyBind(setter: (code: string) => void) { + const [listening, setListening] = useState(false); + const listenerRef = useRef<((e: KeyboardEvent) => void) | null>(null); + + useEffect( + () => () => { + if (listenerRef.current) window.removeEventListener('keydown', listenerRef.current, true); + }, + [], + ); + + const startListening = useCallback(() => { + if (listening) return; + setListening(true); + const onKey = (e: KeyboardEvent) => { + e.preventDefault(); + if (e.code !== 'Escape') setter(e.code); + setListening(false); + window.removeEventListener('keydown', onKey, true); + listenerRef.current = null; + }; + listenerRef.current = onKey; + window.addEventListener('keydown', onKey, true); + }, [listening, setter]); + + return { listening, startListening }; +} + +const keyLabel = (code: string) => + code === 'Space' ? 'Space' : code.replace('Key', '').replace('Digit', ''); + function Calls() { const [cameraOnJoin, setCameraOnJoin] = useSetting(settingsAtom, 'cameraOnJoin'); const [callNoiseSuppression, setCallNoiseSuppression] = useSetting( @@ -908,37 +939,10 @@ function Calls() { ); const [pttMode, setPttMode] = useSetting(settingsAtom, 'pttMode'); const [pttKey, setPttKey] = useSetting(settingsAtom, 'pttKey'); - const [listeningForKey, setListeningForKey] = useState(false); - const keyListenerRef = useRef<((e: KeyboardEvent) => void) | null>(null); + const [deafenKey, setDeafenKey] = useSetting(settingsAtom, 'deafenKey'); - useEffect( - () => () => { - if (keyListenerRef.current) - window.removeEventListener('keydown', keyListenerRef.current, true); - }, - [], - ); - - const handleKeyBind = useCallback(() => { - if (listeningForKey) return; - setListeningForKey(true); - const onKey = (e: KeyboardEvent) => { - e.preventDefault(); - if (e.code === 'Escape') { - setListeningForKey(false); - } else { - setPttKey(e.code); - setListeningForKey(false); - } - window.removeEventListener('keydown', onKey, true); - keyListenerRef.current = null; - }; - keyListenerRef.current = onKey; - window.addEventListener('keydown', onKey, true); - }, [listeningForKey, setPttKey]); - - const keyLabel = (code: string) => - code === 'Space' ? 'Space' : code.replace('Key', '').replace('Digit', ''); + const pttBind = useKeyBind(setPttKey); + const deafenBind = useKeyBind(setDeafenKey); return ( @@ -976,23 +980,40 @@ function Calls() { /> {pttMode && ( - {listeningForKey ? 'Press a key…' : keyLabel(pttKey)} + {pttBind.listening ? 'Press a key…' : keyLabel(pttKey)} } /> )} + + {deafenBind.listening ? 'Press a key…' : keyLabel(deafenKey)} + + } + /> ); @@ -1295,6 +1316,43 @@ function Messages() { ); } +function KeyboardShortcuts() { + const [quickSwitcherKey, setQuickSwitcherKey] = useSetting(settingsAtom, 'quickSwitcherKey'); + const qsBind = useKeyBind(setQuickSwitcherKey); + + return ( + + Keyboard Shortcuts + + + + {qsBind.listening ? 'Press a key…' : `Ctrl+${keyLabel(quickSwitcherKey)}`} + + + } + /> + + + ); +} + type GeneralProps = { requestClose: () => void; }; @@ -1325,6 +1383,7 @@ export function General({ requestClose }: GeneralProps) { + diff --git a/src/app/pages/client/ClientNonUIFeatures.tsx b/src/app/pages/client/ClientNonUIFeatures.tsx index b398ce406..c8757ac1d 100644 --- a/src/app/pages/client/ClientNonUIFeatures.tsx +++ b/src/app/pages/client/ClientNonUIFeatures.tsx @@ -264,19 +264,20 @@ function MessageNotifications() { function QuickSwitcherFeature() { const [open, setOpen] = useState(false); + const [quickSwitcherKey] = useSetting(settingsAtom, 'quickSwitcherKey'); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if ((e.ctrlKey || e.metaKey) && e.key === 'p') { + if ((e.ctrlKey || e.metaKey) && e.code === quickSwitcherKey) { e.preventDefault(); - setOpen(true); + setOpen((prev) => !prev); } }; window.addEventListener('keydown', handleKeyDown); return () => { window.removeEventListener('keydown', handleKeyDown); }; - }, []); + }, [quickSwitcherKey]); if (!open) return null; return setOpen(false)} />; diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index 1f09a1a8c..f40a73b0a 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -80,6 +80,9 @@ export interface Settings { nightLightEnabled: boolean; nightLightOpacity: number; + + deafenKey: string; + quickSwitcherKey: string; } const defaultSettings: Settings = { @@ -129,6 +132,9 @@ const defaultSettings: Settings = { nightLightEnabled: false, nightLightOpacity: 30, + + deafenKey: 'KeyM', + quickSwitcherKey: 'KeyP', }; export const getSettings = (): Settings => {