feat(call): stop injecting CSS and key listeners into the call frame (#43)
CI / Build & Quality Checks (push) Successful in 3m5s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 9s
CI / Trigger Desktop Build (push) Successful in 8s
CI / Playwright smoke (e2e) (push) Successful in 12m42s

Pins @lotusguild/element-call-embedded 0.25.0-lotus.19.

- Styles: the fork now hides its own footer (`lotusHostControls=1`) and
  sets its root color-scheme from the theme, so the host no longer
  injects `#lotus-ec-styles` or sets inline styles on EC's DOM. The two
  other injected rules matched nothing in EC 0.25 (dead). The
  transparent background was already the fork's (`lotusTransparent`).
- Hotkeys: PTT / deafen keys pressed with focus inside the call frame now
  arrive as `io.lotus.hotkey` (the host sends the codes via
  `io.lotus.set_hotkeys`), instead of listeners on the frame's window.
  The window binding stays only for a fork that doesn't report `hotkeys`.
- Fixes (with lotus.19): pressing the deafen key M with focus in the call
  also hit EC's own "M = toggle mic" shortcut, so the first press turned
  the mic ON instead of deafening — even in push-to-talk mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
Lotus CI
2026-09-26 14:35:44 -04:00
co-authored by Claude Opus 5.5
parent 024edbf546
commit 502b9dfd83
6 changed files with 200 additions and 82 deletions
+52 -14
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
import { atom, useSetAtom } from 'jotai';
import { CallEmbed, useCallControlState } from '../plugins/call';
import { useSetting } from '../state/hooks/settings';
@@ -150,6 +150,20 @@ export function useCallHotkeys(callEmbed: CallEmbed | undefined, joined: boolean
const { microphone } = useCallControlState(embed?.control);
const setPttActive = useSetAtom(pttActiveAtom);
// [Gitea #43] Once the fork reports it handles hotkeys, keys pressed inside
// the call frame arrive as io.lotus.hotkey and we stop adding listeners to
// the frame's window (which needs same-origin access).
const [forkHotkeys, setForkHotkeys] = useState(false);
useEffect(() => {
if (!embed) {
setForkHotkeys(false);
return undefined;
}
const sync = () => setForkHotkeys(embed.control.forkHandlesHotkeys);
sync();
return embed.control.onForkHotkeysChange(sync);
}, [embed]);
// Track microphone via ref so the PTT effect doesn't need it as a dep (avoids listener churn)
const microphoneRef = useRef(microphone);
useEffect(() => {
@@ -210,11 +224,12 @@ export function useCallHotkeys(callEmbed: CallEmbed | undefined, joined: boolean
pttActiveRef.current = false;
setPttActive(false);
};
const onKeyDown = (e: KeyboardEvent) => {
// `prevent` is absent for keys reported by the fork, which already
// cancelled the default action inside its frame.
const pttDown = (e: KeyLike, editable: boolean, interactive: boolean, prevent?: () => void) => {
if (!isPttKeyDown(e, pttKey)) return;
const target = e.target as HTMLElement;
if (isEditable(target)) return;
if (!isInteractive(target)) e.preventDefault();
if (editable) return;
if (!interactive) prevent?.();
// Key auto-repeat re-fires keydown while held; don't restart the clock.
if (pttActiveRef.current) return;
// C-M5: mark PTT active BEFORE unmuting so the mic echo (onMediaState)
@@ -225,6 +240,10 @@ export function useCallHotkeys(callEmbed: CallEmbed | undefined, joined: boolean
setPttActive(true);
holdWatchdog.current.arm();
};
const onKeyDown = (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
pttDown(e, isEditable(target), isInteractive(target), () => e.preventDefault());
};
const onKeyUp = (e: KeyboardEvent) => {
if (e.code !== pttKey) return;
release();
@@ -249,16 +268,25 @@ export function useCallHotkeys(callEmbed: CallEmbed | undefined, joined: boolean
};
};
const unbindHost = bind(window);
// BUG-9: also wire iframe key/blur/focus so PTT works with focus in the iframe
const unbindIframe = bindIframeWindow(embed.iframe, bind);
// BUG-9: PTT also works with focus in the call frame — reported by the
// fork, or (older fork) via listeners on the frame's window.
embed.control.setHotkeyCodes('ptt', [pttKey]);
const unbindFork = embed.control.onHotkey((r) => {
if (r.type === 'focus') onFocusChange();
else if (r.type === 'keydown') pttDown(r, r.editable, r.interactive);
else if (r.code === pttKey) release();
});
const unbindIframe = forkHotkeys ? () => undefined : bindIframeWindow(embed.iframe, bind);
return () => {
unbindHost();
unbindFork();
unbindIframe();
embed.control.setHotkeyCodes('ptt', []);
// BUG-8: if the embed changes while PTT is active, release mic on cleanup
if (pttActiveRef.current) release();
};
// microphone intentionally read via microphoneRef — excluded from deps to avoid listener churn
}, [pttMode, pttKey, embed, setPttActive]);
}, [pttMode, pttKey, embed, setPttActive, forkHotkeys]);
// [cinny-desktop #2] System-wide PTT/deafen while a game has focus. The
// desktop polls the configured keys without consuming them and emits one
@@ -330,15 +358,25 @@ export function useCallHotkeys(callEmbed: CallEmbed | undefined, joined: boolean
embed.control.toggleSound();
};
window.addEventListener('keydown', onKeyDown);
// C-L4: also bind the EC iframe window so the deafen key works when focus is
// inside the iframe (mirrors the PTT binding above).
const unbindIframe = bindIframeWindow(embed.iframe, (win) => {
win.addEventListener('keydown', onKeyDown);
return () => win.removeEventListener('keydown', onKeyDown);
// C-L4: the deafen key also works with focus inside the call frame (no
// composer there, so only the editable-field check applies).
embed.control.setHotkeyCodes('deafen', [deafenKey]);
const unbindFork = embed.control.onHotkey((r) => {
if (r.type === 'keydown' && isDeafenKeyDown(r, deafenKey) && !r.editable) {
embed.control.toggleSound();
}
});
const unbindIframe = forkHotkeys
? () => undefined
: bindIframeWindow(embed.iframe, (win) => {
win.addEventListener('keydown', onKeyDown);
return () => win.removeEventListener('keydown', onKeyDown);
});
return () => {
window.removeEventListener('keydown', onKeyDown);
unbindFork();
unbindIframe();
embed.control.setHotkeyCodes('deafen', []);
};
}, [embed, deafenKey, deafenHotkey]);
}, [embed, deafenKey, deafenHotkey, forkHotkeys]);
}