meet/lib/KeyboardShortcuts.tsx
2025-05-23 10:18:53 -06:00

110 lines
4.0 KiB
TypeScript

import React from 'react';
import { Track } from 'livekit-client';
import { useLocalParticipant, useTrackToggle } from '@livekit/components-react';
import { useSettingsState } from './SettingsContext';
import { KeyCommand } from './types';
export function KeyboardShortcuts() {
const { state } = useSettingsState();
const { localParticipant, isMicrophoneEnabled } = useLocalParticipant();
const { toggle: toggleMic, pending: pendingMicChange } = useTrackToggle({ source: Track.Source.Microphone });
const { toggle: toggleCamera, pending: pendingCameraChange } = useTrackToggle({ source: Track.Source.Camera });
const pttHeldRef = React.useRef(false);
React.useEffect(() => {
const handlers = Object.entries(state.keybindings)
.flatMap(([command, binding]) => {
switch (command) {
case KeyCommand.PTT:
if (!state.enablePTT || !Array.isArray(binding)) return [];
const [enable, disable] = binding;
const t = getEventTarget(enable.target);
if (!t) return null;
const on = async (event: KeyboardEvent) => {
if (enable.guard(event)) {
event.preventDefault();
if (!isMicrophoneEnabled) {
pttHeldRef.current = true;
localParticipant?.setMicrophoneEnabled(true);
}
}
};
const off = async (event: KeyboardEvent) => {
if (disable.guard(event)) {
event.preventDefault();
if (pttHeldRef.current && isMicrophoneEnabled) {
pttHeldRef.current = false;
localParticipant?.setMicrophoneEnabled(false);
}
}
};
t.addEventListener(enable.eventName, on as any);
t.addEventListener(disable.eventName, off as any);
return [
{ eventName: enable.eventName, target: t, handler: on },
{ eventName: disable.eventName, target: t, handler: off },
];
case KeyCommand.ToggleMic:
if (!Array.isArray(binding)) {
const t = getEventTarget(binding.target);
if (!t) return null;
const handler = async (event: KeyboardEvent) => {
if (binding.guard(event) && !pendingMicChange) {
event.preventDefault();
toggleMic?.().catch(console.error);
}
};
t.addEventListener(binding.eventName, handler as any);
return { eventName: binding.eventName, target: t, handler };
}
case KeyCommand.ToggleCamera:
if (!Array.isArray(binding)) {
const t = getEventTarget(binding.target);
if (!t) return null;
const handler = async (event: KeyboardEvent) => {
if (binding.guard(event) && !pendingCameraChange) {
event.preventDefault();
toggleCamera?.().catch(console.error);
}
};
t.addEventListener(binding.eventName, handler as any);
return { eventName: binding.eventName, target: t, handler };
}
default:
return [];
}
})
.filter(Boolean) as Array<{
target: EventTarget;
eventName: string;
handler: (event: KeyboardEvent) => void;
}>;
return () => {
handlers.forEach(({ target, eventName, handler }) => {
target.removeEventListener(eventName, handler as any);
});
};
}, [state, toggleCamera, pendingCameraChange, toggleMic, pendingMicChange, localParticipant, isMicrophoneEnabled]);
return null;
}
function getEventTarget(
target: Window | Document | HTMLElement | string = window,
): EventTarget | null {
const targetElement = typeof target === 'string' ? document.querySelector(target) : target;
if (!targetElement) {
console.warn(`Target element not found for ${target}`);
return null;
}
return targetElement;
}