fix: PTT input guard, listener stability, focus restore mute, single badge
This commit is contained in:
@@ -5,8 +5,6 @@ import { StatusDivider } from './components';
|
|||||||
import { CallEmbed, useCallControlState } from '../../plugins/call';
|
import { CallEmbed, useCallControlState } from '../../plugins/call';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
import { callEmbedAtom } from '../../state/callEmbed';
|
import { callEmbedAtom } from '../../state/callEmbed';
|
||||||
import { useSetting } from '../../state/hooks/settings';
|
|
||||||
import { settingsAtom } from '../../state/settings';
|
|
||||||
|
|
||||||
type MicrophoneButtonProps = {
|
type MicrophoneButtonProps = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -159,10 +157,6 @@ export function CallControl({
|
|||||||
}) {
|
}) {
|
||||||
const { microphone, video, sound, screenshare } = useCallControlState(callEmbed.control);
|
const { microphone, video, sound, screenshare } = useCallControlState(callEmbed.control);
|
||||||
const setCallEmbed = useSetAtom(callEmbedAtom);
|
const setCallEmbed = useSetAtom(callEmbedAtom);
|
||||||
const [pttMode] = useSetting(settingsAtom, 'pttMode');
|
|
||||||
const [pttKey] = useSetting(settingsAtom, 'pttKey');
|
|
||||||
const pttKeyLabel = pttMode ? (pttKey === 'Space' ? 'SPACE' : pttKey.replace('Key', '').replace('Digit', '')) : '';
|
|
||||||
|
|
||||||
const [hangupState, hangup] = useAsyncCallback(
|
const [hangupState, hangup] = useAsyncCallback(
|
||||||
useCallback(() => callEmbed.hangup(), [callEmbed])
|
useCallback(() => callEmbed.hangup(), [callEmbed])
|
||||||
);
|
);
|
||||||
@@ -180,22 +174,10 @@ export function CallControl({
|
|||||||
return (
|
return (
|
||||||
<Box shrink="No" alignItems="Center" gap="300">
|
<Box shrink="No" alignItems="Center" gap="300">
|
||||||
<Box alignItems="Inherit" gap="200">
|
<Box alignItems="Inherit" gap="200">
|
||||||
{pttMode && (
|
|
||||||
<Chip
|
|
||||||
variant={microphone ? 'Success' : 'Warning'}
|
|
||||||
fill="Soft"
|
|
||||||
radii="400"
|
|
||||||
size="300"
|
|
||||||
outlined
|
|
||||||
style={{ pointerEvents: 'none', fontWeight: 700 }}
|
|
||||||
>
|
|
||||||
<Text size="T200">{microphone ? '● Live' : `PTT ${pttKeyLabel}`}</Text>
|
|
||||||
</Chip>
|
|
||||||
)}
|
|
||||||
<MicrophoneButton
|
<MicrophoneButton
|
||||||
enabled={microphone}
|
enabled={microphone}
|
||||||
onToggle={() => callEmbed.control.toggleMicrophone()}
|
onToggle={() => callEmbed.control.toggleMicrophone()}
|
||||||
disabled={!callJoined || pttMode}
|
disabled={!callJoined}
|
||||||
/>
|
/>
|
||||||
<SoundButton
|
<SoundButton
|
||||||
enabled={sound}
|
enabled={sound}
|
||||||
|
|||||||
@@ -59,14 +59,16 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
const [pttKey] = useSetting(settingsAtom, 'pttKey');
|
const [pttKey] = useSetting(settingsAtom, 'pttKey');
|
||||||
const [pttActive, setPttActive] = useState(false);
|
const [pttActive, setPttActive] = useState(false);
|
||||||
|
|
||||||
|
// Track microphone via ref so the PTT effect doesn't need it as a dep (avoids listener churn)
|
||||||
|
const microphoneRef = useRef(microphone);
|
||||||
|
useEffect(() => { microphoneRef.current = microphone; }, [microphone]);
|
||||||
|
|
||||||
// Handle PTT mode toggle mid-call
|
// Handle PTT mode toggle mid-call
|
||||||
const pttModeRef = useRef(pttMode);
|
const pttModeRef = useRef(pttMode);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pttMode && !pttModeRef.current) {
|
if (pttMode && !pttModeRef.current) {
|
||||||
// PTT just enabled — mute mic so key handler can activate
|
|
||||||
callEmbed.control.setMicrophone(false);
|
callEmbed.control.setMicrophone(false);
|
||||||
} else if (!pttMode && pttModeRef.current) {
|
} else if (!pttMode && pttModeRef.current) {
|
||||||
// PTT just disabled — restore mic to on
|
|
||||||
callEmbed.control.setMicrophone(true);
|
callEmbed.control.setMicrophone(true);
|
||||||
}
|
}
|
||||||
pttModeRef.current = pttMode;
|
pttModeRef.current = pttMode;
|
||||||
@@ -93,39 +95,53 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pttMode) return;
|
if (!pttMode) return;
|
||||||
|
const iframeWindow = callEmbed.iframe.contentWindow;
|
||||||
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.code === pttKey && !e.repeat && !microphone) {
|
if (e.code !== pttKey || e.repeat) return;
|
||||||
e.preventDefault();
|
// Don't intercept keys typed into a text input or editable element
|
||||||
callEmbed.control.setMicrophone(true);
|
const target = e.target as HTMLElement;
|
||||||
setPttActive(true);
|
if (
|
||||||
}
|
target.tagName === 'INPUT' ||
|
||||||
|
target.tagName === 'TEXTAREA' ||
|
||||||
|
target.contentEditable === 'true'
|
||||||
|
) return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (!microphoneRef.current) callEmbed.control.setMicrophone(true);
|
||||||
|
setPttActive(true);
|
||||||
};
|
};
|
||||||
const onKeyUp = (e: KeyboardEvent) => {
|
const onKeyUp = (e: KeyboardEvent) => {
|
||||||
if (e.code === pttKey) {
|
if (e.code !== pttKey) return;
|
||||||
callEmbed.control.setMicrophone(false);
|
callEmbed.control.setMicrophone(false);
|
||||||
setPttActive(false);
|
setPttActive(false);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
// Release PTT if the tab loses focus — prevents mic getting stuck on after tab switching
|
// Release PTT when the tab loses focus to prevent stuck-on mic
|
||||||
const onBlur = () => {
|
const onBlur = () => {
|
||||||
callEmbed.control.setMicrophone(false);
|
callEmbed.control.setMicrophone(false);
|
||||||
setPttActive(false);
|
setPttActive(false);
|
||||||
};
|
};
|
||||||
// The EC iframe captures keyboard events when it has focus, so listen on it too
|
// Re-mute on focus restore: EC can re-assert audio_enabled:true on audio-context resume
|
||||||
const iframeWindow = callEmbed.iframe.contentWindow;
|
const onFocus = () => {
|
||||||
|
callEmbed.control.setMicrophone(false);
|
||||||
|
setPttActive(false);
|
||||||
|
};
|
||||||
window.addEventListener('keydown', onKeyDown);
|
window.addEventListener('keydown', onKeyDown);
|
||||||
window.addEventListener('keyup', onKeyUp);
|
window.addEventListener('keyup', onKeyUp);
|
||||||
window.addEventListener('blur', onBlur);
|
window.addEventListener('blur', onBlur);
|
||||||
|
window.addEventListener('focus', onFocus);
|
||||||
iframeWindow?.addEventListener('keydown', onKeyDown);
|
iframeWindow?.addEventListener('keydown', onKeyDown);
|
||||||
iframeWindow?.addEventListener('keyup', onKeyUp);
|
iframeWindow?.addEventListener('keyup', onKeyUp);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', onKeyDown);
|
window.removeEventListener('keydown', onKeyDown);
|
||||||
window.removeEventListener('keyup', onKeyUp);
|
window.removeEventListener('keyup', onKeyUp);
|
||||||
window.removeEventListener('blur', onBlur);
|
window.removeEventListener('blur', onBlur);
|
||||||
|
window.removeEventListener('focus', onFocus);
|
||||||
iframeWindow?.removeEventListener('keydown', onKeyDown);
|
iframeWindow?.removeEventListener('keydown', onKeyDown);
|
||||||
iframeWindow?.removeEventListener('keyup', onKeyUp);
|
iframeWindow?.removeEventListener('keyup', onKeyUp);
|
||||||
};
|
};
|
||||||
}, [pttMode, pttKey, callEmbed, microphone]);
|
// microphone intentionally read via microphoneRef — excluded from deps to avoid listener churn
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [pttMode, pttKey, callEmbed]);
|
||||||
|
|
||||||
const [hangupState, hangup] = useAsyncCallback(
|
const [hangupState, hangup] = useAsyncCallback(
|
||||||
useCallback(() => callEmbed.hangup(), [callEmbed])
|
useCallback(() => callEmbed.hangup(), [callEmbed])
|
||||||
|
|||||||
Reference in New Issue
Block a user