From 41ae0f4fa0c7a5f52e9d65daf4537dbd2129a0b9 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 14 May 2026 20:14:06 -0400 Subject: [PATCH] fix: PTT input guard, listener stability, focus restore mute, single badge --- src/app/features/call-status/CallControl.tsx | 20 +-------- src/app/features/call/CallControls.tsx | 46 +++++++++++++------- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/app/features/call-status/CallControl.tsx b/src/app/features/call-status/CallControl.tsx index bd781fab6..7d0fb1e4a 100644 --- a/src/app/features/call-status/CallControl.tsx +++ b/src/app/features/call-status/CallControl.tsx @@ -5,8 +5,6 @@ import { StatusDivider } from './components'; import { CallEmbed, useCallControlState } from '../../plugins/call'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { callEmbedAtom } from '../../state/callEmbed'; -import { useSetting } from '../../state/hooks/settings'; -import { settingsAtom } from '../../state/settings'; type MicrophoneButtonProps = { enabled: boolean; @@ -159,10 +157,6 @@ export function CallControl({ }) { const { microphone, video, sound, screenshare } = useCallControlState(callEmbed.control); 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( useCallback(() => callEmbed.hangup(), [callEmbed]) ); @@ -180,22 +174,10 @@ export function CallControl({ return ( - {pttMode && ( - - {microphone ? '● Live' : `PTT ${pttKeyLabel}`} - - )} callEmbed.control.toggleMicrophone()} - disabled={!callJoined || pttMode} + disabled={!callJoined} /> { microphoneRef.current = microphone; }, [microphone]); + // Handle PTT mode toggle mid-call const pttModeRef = useRef(pttMode); useEffect(() => { if (pttMode && !pttModeRef.current) { - // PTT just enabled — mute mic so key handler can activate callEmbed.control.setMicrophone(false); } else if (!pttMode && pttModeRef.current) { - // PTT just disabled — restore mic to on callEmbed.control.setMicrophone(true); } pttModeRef.current = pttMode; @@ -93,39 +95,53 @@ export function CallControls({ callEmbed }: CallControlsProps) { useEffect(() => { if (!pttMode) return; + const iframeWindow = callEmbed.iframe.contentWindow; + const onKeyDown = (e: KeyboardEvent) => { - if (e.code === pttKey && !e.repeat && !microphone) { - e.preventDefault(); - callEmbed.control.setMicrophone(true); - setPttActive(true); - } + if (e.code !== pttKey || e.repeat) return; + // Don't intercept keys typed into a text input or editable element + const target = e.target as HTMLElement; + 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) => { - if (e.code === pttKey) { - callEmbed.control.setMicrophone(false); - setPttActive(false); - } + if (e.code !== pttKey) return; + callEmbed.control.setMicrophone(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 = () => { callEmbed.control.setMicrophone(false); setPttActive(false); }; - // The EC iframe captures keyboard events when it has focus, so listen on it too - const iframeWindow = callEmbed.iframe.contentWindow; + // Re-mute on focus restore: EC can re-assert audio_enabled:true on audio-context resume + const onFocus = () => { + callEmbed.control.setMicrophone(false); + setPttActive(false); + }; window.addEventListener('keydown', onKeyDown); window.addEventListener('keyup', onKeyUp); window.addEventListener('blur', onBlur); + window.addEventListener('focus', onFocus); iframeWindow?.addEventListener('keydown', onKeyDown); iframeWindow?.addEventListener('keyup', onKeyUp); return () => { window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp); window.removeEventListener('blur', onBlur); + window.removeEventListener('focus', onFocus); iframeWindow?.removeEventListener('keydown', onKeyDown); 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( useCallback(() => callEmbed.hangup(), [callEmbed])