From 351360843575c63a4493dae6e85fff23d5200d59 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 14 May 2026 19:29:45 -0400 Subject: [PATCH] fix: PTT blur/unmute, EC button hiding robustness, PTT status indicator --- src/app/features/call-status/CallControl.tsx | 19 ++++++++++- src/app/features/call/CallControls.tsx | 17 ++++++++-- src/app/features/call/styles.css.ts | 1 + src/app/plugins/call/CallEmbed.ts | 35 +++++++++++++++++--- 4 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/app/features/call-status/CallControl.tsx b/src/app/features/call-status/CallControl.tsx index 6416fda52..8d35be213 100644 --- a/src/app/features/call-status/CallControl.tsx +++ b/src/app/features/call-status/CallControl.tsx @@ -5,6 +5,8 @@ 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; @@ -157,6 +159,9 @@ 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]) @@ -175,10 +180,22 @@ export function CallControl({ return ( + {pttMode && ( + + PTT {pttKeyLabel} + + )} callEmbed.control.toggleMicrophone()} - disabled={!callJoined} + disabled={!callJoined || pttMode} /> { - if (pttMode && !pttModeRef.current && microphone) { + 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; - }, [pttMode, callEmbed, microphone]); + }, [pttMode, callEmbed]); const handleOpenMenu: MouseEventHandler = (evt) => { setCords(evt.currentTarget.getBoundingClientRect()); @@ -101,11 +105,18 @@ export function CallControls({ callEmbed }: CallControlsProps) { setPttActive(false); } }; + // Release PTT if the tab loses focus — prevents mic getting stuck on after tab switching + const onBlur = () => { + callEmbed.control.setMicrophone(false); + setPttActive(false); + }; window.addEventListener('keydown', onKeyDown); window.addEventListener('keyup', onKeyUp); + window.addEventListener('blur', onBlur); return () => { window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp); + window.removeEventListener('blur', onBlur); }; }, [pttMode, pttKey, callEmbed, microphone]); diff --git a/src/app/features/call/styles.css.ts b/src/app/features/call/styles.css.ts index 2b9f28ad6..e867cf4d3 100644 --- a/src/app/features/call/styles.css.ts +++ b/src/app/features/call/styles.css.ts @@ -21,6 +21,7 @@ export const CallMemberCard = style({ export const CallControlContainer = style({ padding: config.space.S400, + position: 'relative', }); export const PrescreenMessage = style({ diff --git a/src/app/plugins/call/CallEmbed.ts b/src/app/plugins/call/CallEmbed.ts index efc833e62..98167f224 100644 --- a/src/app/plugins/call/CallEmbed.ts +++ b/src/app/plugins/call/CallEmbed.ts @@ -49,6 +49,8 @@ export class CallEmbed { private readonly initialState: CallControlState; + private styleRetryObserver?: MutationObserver; + static getIntent(dm: boolean, ongoing: boolean): ElementCallIntent { if (ongoing) { return dm ? ElementCallIntent.JoinExistingDM : ElementCallIntent.JoinExisting; @@ -236,6 +238,7 @@ export class CallEmbed { this.disposables.forEach((disposable) => { disposable(); }); + this.styleRetryObserver?.disconnect(); this.call.stop(); this.container.removeChild(this.iframe); this.control.dispose(); @@ -263,11 +266,33 @@ export class CallEmbed { if (!doc) return; doc.body.style.setProperty('background', 'none', 'important'); - const controls = doc.body.querySelector('[data-testid="incall_leave"]')?.parentElement - ?.parentElement; - if (controls) { - controls.style.setProperty('position', 'absolute'); - controls.style.setProperty('visibility', 'hidden'); + + // Inject CSS for things that can't be reliably caught by DOM timing + if (!doc.getElementById('lotus-ec-styles')) { + const style = doc.createElement('style'); + style.id = 'lotus-ec-styles'; + style.textContent = [ + 'body { background: none !important; }', + // Hide "using to Device key transport" status line + '[style*="height: 0"][style*="z-index: 1"][style*="align-self: center"] { display: none !important; }', + ].join('\n'); + (doc.head ?? doc.body).appendChild(style); + } + + // Hide EC built-in controls (we provide our own) + const leaveBtn = doc.body.querySelector('[data-testid="incall_leave"]'); + if (leaveBtn) { + this.styleRetryObserver?.disconnect(); + this.styleRetryObserver = undefined; + const controls = leaveBtn.parentElement?.parentElement; + if (controls) { + controls.style.setProperty('position', 'absolute'); + controls.style.setProperty('visibility', 'hidden'); + } + } else if (!this.styleRetryObserver) { + // Controls not in DOM yet — observe and retry when they appear + this.styleRetryObserver = new MutationObserver(() => this.applyStyles()); + this.styleRetryObserver.observe(doc.body, { childList: true, subtree: true }); } }