fix: PTT blur/unmute, EC button hiding robustness, PTT status indicator
This commit is contained in:
@@ -5,6 +5,8 @@ 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;
|
||||||
@@ -157,6 +159,9 @@ 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])
|
||||||
@@ -175,10 +180,22 @@ 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="Critical"
|
||||||
|
fill="Soft"
|
||||||
|
radii="300"
|
||||||
|
size="300"
|
||||||
|
outlined
|
||||||
|
style={{ pointerEvents: 'none', fontWeight: 700, letterSpacing: '0.06em' }}
|
||||||
|
>
|
||||||
|
<Text size="T200">PTT {pttKeyLabel}</Text>
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
<MicrophoneButton
|
<MicrophoneButton
|
||||||
enabled={microphone}
|
enabled={microphone}
|
||||||
onToggle={() => callEmbed.control.toggleMicrophone()}
|
onToggle={() => callEmbed.control.toggleMicrophone()}
|
||||||
disabled={!callJoined}
|
disabled={!callJoined || pttMode}
|
||||||
/>
|
/>
|
||||||
<SoundButton
|
<SoundButton
|
||||||
enabled={sound}
|
enabled={sound}
|
||||||
|
|||||||
@@ -58,14 +58,18 @@ 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);
|
||||||
|
|
||||||
// Mute mic immediately when PTT is enabled mid-call so the key handler can activate
|
// Handle PTT mode toggle mid-call
|
||||||
const pttModeRef = useRef(pttMode);
|
const pttModeRef = useRef(pttMode);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pttMode && !pttModeRef.current && microphone) {
|
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) {
|
||||||
|
// PTT just disabled — restore mic to on
|
||||||
|
callEmbed.control.setMicrophone(true);
|
||||||
}
|
}
|
||||||
pttModeRef.current = pttMode;
|
pttModeRef.current = pttMode;
|
||||||
}, [pttMode, callEmbed, microphone]);
|
}, [pttMode, callEmbed]);
|
||||||
|
|
||||||
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||||
setCords(evt.currentTarget.getBoundingClientRect());
|
setCords(evt.currentTarget.getBoundingClientRect());
|
||||||
@@ -101,11 +105,18 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
setPttActive(false);
|
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('keydown', onKeyDown);
|
||||||
window.addEventListener('keyup', onKeyUp);
|
window.addEventListener('keyup', onKeyUp);
|
||||||
|
window.addEventListener('blur', onBlur);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', onKeyDown);
|
window.removeEventListener('keydown', onKeyDown);
|
||||||
window.removeEventListener('keyup', onKeyUp);
|
window.removeEventListener('keyup', onKeyUp);
|
||||||
|
window.removeEventListener('blur', onBlur);
|
||||||
};
|
};
|
||||||
}, [pttMode, pttKey, callEmbed, microphone]);
|
}, [pttMode, pttKey, callEmbed, microphone]);
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const CallMemberCard = style({
|
|||||||
|
|
||||||
export const CallControlContainer = style({
|
export const CallControlContainer = style({
|
||||||
padding: config.space.S400,
|
padding: config.space.S400,
|
||||||
|
position: 'relative',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PrescreenMessage = style({
|
export const PrescreenMessage = style({
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ export class CallEmbed {
|
|||||||
|
|
||||||
private readonly initialState: CallControlState;
|
private readonly initialState: CallControlState;
|
||||||
|
|
||||||
|
private styleRetryObserver?: MutationObserver;
|
||||||
|
|
||||||
static getIntent(dm: boolean, ongoing: boolean): ElementCallIntent {
|
static getIntent(dm: boolean, ongoing: boolean): ElementCallIntent {
|
||||||
if (ongoing) {
|
if (ongoing) {
|
||||||
return dm ? ElementCallIntent.JoinExistingDM : ElementCallIntent.JoinExisting;
|
return dm ? ElementCallIntent.JoinExistingDM : ElementCallIntent.JoinExisting;
|
||||||
@@ -236,6 +238,7 @@ export class CallEmbed {
|
|||||||
this.disposables.forEach((disposable) => {
|
this.disposables.forEach((disposable) => {
|
||||||
disposable();
|
disposable();
|
||||||
});
|
});
|
||||||
|
this.styleRetryObserver?.disconnect();
|
||||||
this.call.stop();
|
this.call.stop();
|
||||||
this.container.removeChild(this.iframe);
|
this.container.removeChild(this.iframe);
|
||||||
this.control.dispose();
|
this.control.dispose();
|
||||||
@@ -263,11 +266,33 @@ export class CallEmbed {
|
|||||||
if (!doc) return;
|
if (!doc) return;
|
||||||
|
|
||||||
doc.body.style.setProperty('background', 'none', 'important');
|
doc.body.style.setProperty('background', 'none', 'important');
|
||||||
const controls = doc.body.querySelector('[data-testid="incall_leave"]')?.parentElement
|
|
||||||
?.parentElement;
|
// Inject CSS for things that can't be reliably caught by DOM timing
|
||||||
if (controls) {
|
if (!doc.getElementById('lotus-ec-styles')) {
|
||||||
controls.style.setProperty('position', 'absolute');
|
const style = doc.createElement('style');
|
||||||
controls.style.setProperty('visibility', 'hidden');
|
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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user