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
+9 -53
View File
@@ -105,8 +105,6 @@ export class CallEmbed {
private readonly initialState: CallControlState;
private styleRetryObserver?: MutationObserver;
private themeKind: ElementCallThemeKind = 'dark';
// Watchdog: detects an iframe that never reaches a usable state.
@@ -217,6 +215,9 @@ export class CallEmbed {
// - transparent background so the room wallpaper shows through natively
lotusCallState: 'true',
lotusTransparent: 'true',
// [Gitea #43] The fork hides its own footer (we draw the call bar) and
// sets its root color-scheme from the theme, instead of us injecting CSS.
lotusHostControls: 'true',
// [lotus #3 / P5-15] Arm the fork's audio-inject handler so the in-call
// soundboard can publish clips into the call. Dormant until the host
// sends io.lotus.inject_audio (only on an explicit user click), so
@@ -353,11 +354,6 @@ export class CallEmbed {
public setTheme(theme: ElementCallThemeKind) {
this.themeKind = theme;
const doc = this.document;
if (doc && this.joined) {
const styleEl = doc.getElementById('lotus-ec-styles');
if (styleEl) styleEl.textContent = this.buildStyleContent();
}
return this.call.transport
.send(WidgetApiToWidgetAction.ThemeChange, { name: theme })
.catch(() => {
@@ -438,6 +434,12 @@ export class CallEmbed {
this.control.onControlsState((evt.detail as { data?: unknown } | undefined)?.data);
}),
);
// [Gitea #43] PTT / deafen keys pressed while focus is inside the frame.
this.disposables.push(
this.listenAction('io.lotus.hotkey', (evt) => {
this.control.onHotkeyReport((evt.detail as { data?: unknown } | undefined)?.data);
}),
);
this.disposables.push(
this.listenAction('io.lotus.call_state', (evt) => {
const data = (evt.detail as { data?: { participants?: unknown } } | undefined)?.data;
@@ -479,7 +481,6 @@ export class CallEmbed {
});
this.clearLoadWatchdog();
this.loadErrorListeners.clear();
this.styleRetryObserver?.disconnect();
this.call.stop();
this.container.removeChild(this.iframe);
this.control.dispose();
@@ -572,7 +573,6 @@ export class CallEmbed {
private onCallJoined(): void {
this.settleLoad();
this.applyStyles();
this.control.startObserving();
// C-H1: EC fires JoinCall again on an EC reconnect (this action has no
@@ -590,50 +590,6 @@ export class CallEmbed {
this.control.forceState(this.initialState);
}
private buildStyleContent(): string {
return [
'html, body { background: none !important; }',
`:root { color-scheme: ${this.themeKind}; }`,
'[style*="height: 0"][style*="z-index: 1"][style*="align-self: center"] { display: none !important; }',
// EC 0.19.4: avatar uses line-height centering which breaks in some tile sizes;
// override with flexbox for reliable centering of the initial letter.
'._avatarContainer_1mrho_40 ._avatar_va14e_8 { display: flex !important; align-items: center !important; justify-content: center !important; line-height: 1 !important; }',
].join('\n');
}
private applyStyles(): void {
const doc = this.document;
if (!doc) return;
doc.body.style.setProperty('background', 'none', 'important');
if (!doc.getElementById('lotus-ec-styles')) {
const style = doc.createElement('style');
style.id = 'lotus-ec-styles';
style.textContent = this.buildStyleContent();
(doc.head ?? doc.body).appendChild(style);
} else {
const styleEl = doc.getElementById('lotus-ec-styles');
if (styleEl) styleEl.textContent = this.buildStyleContent();
}
// 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 });
}
}
private onEvent(ev: MatrixEvent): void {
this.mx.decryptEventIfNeeded(ev);
this.feedEvent(ev);