feat: configurable keybindings for push-to-deafen and quick switcher
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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]),
|
||||
|
||||
@@ -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 (
|
||||
<Box direction="Column" gap="100">
|
||||
@@ -976,23 +980,40 @@ function Calls() {
|
||||
/>
|
||||
{pttMode && (
|
||||
<SettingTile
|
||||
title="PTT Key Binding"
|
||||
title="PTT Key"
|
||||
description="Press a key to bind it as your push-to-talk key."
|
||||
after={
|
||||
<Button
|
||||
size="300"
|
||||
variant={listeningForKey ? 'Warning' : 'Secondary'}
|
||||
fill={listeningForKey ? 'Solid' : 'Soft'}
|
||||
variant={pttBind.listening ? 'Warning' : 'Secondary'}
|
||||
fill={pttBind.listening ? 'Solid' : 'Soft'}
|
||||
radii="300"
|
||||
outlined
|
||||
onClick={handleKeyBind}
|
||||
onClick={pttBind.startListening}
|
||||
style={{ minWidth: '90px' }}
|
||||
>
|
||||
<Text size="B300">{listeningForKey ? 'Press a key…' : keyLabel(pttKey)}</Text>
|
||||
<Text size="B300">{pttBind.listening ? 'Press a key…' : keyLabel(pttKey)}</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<SettingTile
|
||||
title="Push to Deafen"
|
||||
description="Toggle speaker mute during a call. Press Escape to cancel rebind."
|
||||
after={
|
||||
<Button
|
||||
size="300"
|
||||
variant={deafenBind.listening ? 'Warning' : 'Secondary'}
|
||||
fill={deafenBind.listening ? 'Solid' : 'Soft'}
|
||||
radii="300"
|
||||
outlined
|
||||
onClick={deafenBind.startListening}
|
||||
style={{ minWidth: '90px' }}
|
||||
>
|
||||
<Text size="B300">{deafenBind.listening ? 'Press a key…' : keyLabel(deafenKey)}</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
@@ -1295,6 +1316,43 @@ function Messages() {
|
||||
);
|
||||
}
|
||||
|
||||
function KeyboardShortcuts() {
|
||||
const [quickSwitcherKey, setQuickSwitcherKey] = useSetting(settingsAtom, 'quickSwitcherKey');
|
||||
const qsBind = useKeyBind(setQuickSwitcherKey);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Keyboard Shortcuts</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title="Quick Room Switcher"
|
||||
description="Open the quick room switcher overlay. Ctrl/Cmd + this key."
|
||||
after={
|
||||
<Button
|
||||
size="300"
|
||||
variant={qsBind.listening ? 'Warning' : 'Secondary'}
|
||||
fill={qsBind.listening ? 'Solid' : 'Soft'}
|
||||
radii="300"
|
||||
outlined
|
||||
onClick={qsBind.startListening}
|
||||
style={{ minWidth: '90px' }}
|
||||
>
|
||||
<Text size="B300">
|
||||
{qsBind.listening ? 'Press a key…' : `Ctrl+${keyLabel(quickSwitcherKey)}`}
|
||||
</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type GeneralProps = {
|
||||
requestClose: () => void;
|
||||
};
|
||||
@@ -1325,6 +1383,7 @@ export function General({ requestClose }: GeneralProps) {
|
||||
<Messages />
|
||||
<Privacy />
|
||||
<Calls />
|
||||
<KeyboardShortcuts />
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
|
||||
@@ -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 <QuickSwitcher onClose={() => setOpen(false)} />;
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user