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
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:
co-authored by
Claude Opus 5.5
parent
024edbf546
commit
502b9dfd83
@@ -19,6 +19,41 @@ export type LotusQualityPayload = {
|
||||
screenshareMaxFramerate?: number | null;
|
||||
};
|
||||
|
||||
/** fromWidget `io.lotus.hotkey` (Gitea #43): a watched key in the call frame. */
|
||||
export type ForkHotkeyReport =
|
||||
| {
|
||||
type: 'keydown' | 'keyup';
|
||||
code: string;
|
||||
repeat: boolean;
|
||||
ctrlKey: boolean;
|
||||
altKey: boolean;
|
||||
metaKey: boolean;
|
||||
shiftKey: boolean;
|
||||
/** Went to a text field in the frame. */
|
||||
editable: boolean;
|
||||
/** Went to a button/link in the frame. */
|
||||
interactive: boolean;
|
||||
}
|
||||
| { type: 'focus' };
|
||||
|
||||
export function parseForkHotkeyReport(data: unknown): ForkHotkeyReport | null {
|
||||
if (typeof data !== 'object' || data === null) return null;
|
||||
const d = data as Record<string, unknown>;
|
||||
if (d.type === 'focus') return { type: 'focus' };
|
||||
if ((d.type !== 'keydown' && d.type !== 'keyup') || typeof d.code !== 'string') return null;
|
||||
return {
|
||||
type: d.type,
|
||||
code: d.code,
|
||||
repeat: d.repeat === true,
|
||||
ctrlKey: d.ctrlKey === true,
|
||||
altKey: d.altKey === true,
|
||||
metaKey: d.metaKey === true,
|
||||
shiftKey: d.shiftKey === true,
|
||||
editable: d.editable === true,
|
||||
interactive: d.interactive === true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Capability Delegation (`postMessage(msg, { delegate })`) ships only in
|
||||
* Chromium; other engines silently ignore the option, so the frame would get
|
||||
@@ -204,6 +239,7 @@ export class CallControl extends EventEmitter implements CallControlState {
|
||||
if (this._audioOutputId !== undefined) this.sendAudioOutput(this._audioOutputId);
|
||||
// [Gitea #17] The pin lives fork-side and is dropped on a handler remount.
|
||||
if (this._focusedUserId !== null) this.sendFocus(this._focusedUserId, this._focusedMediaId);
|
||||
this.sendHotkeyCodes();
|
||||
}
|
||||
|
||||
public startObserving() {
|
||||
@@ -230,15 +266,8 @@ export class CallControl extends EventEmitter implements CallControlState {
|
||||
|
||||
private applyBodyMutation() {
|
||||
if (!this.document) return;
|
||||
|
||||
this.document.body.style.setProperty('background', 'none', 'important');
|
||||
|
||||
const controls = this.leaveButton?.parentElement?.parentElement;
|
||||
if (controls) {
|
||||
controls.style.setProperty('position', 'absolute');
|
||||
controls.style.setProperty('visibility', 'hidden');
|
||||
}
|
||||
|
||||
// Hiding EC's footer and the transparent background are the fork's job now
|
||||
// (lotusHostControls / lotusTransparent URL flags, Gitea #43).
|
||||
this.observeControls();
|
||||
}
|
||||
|
||||
@@ -350,16 +379,71 @@ export class CallControl extends EventEmitter implements CallControlState {
|
||||
// controls_state). Used only where Capability Delegation exists.
|
||||
private forkScreenshare = false;
|
||||
|
||||
// [Gitea #43] The fork reports call hotkeys pressed inside its frame
|
||||
// (io.lotus.set_hotkeys → io.lotus.hotkey), so the host stops adding key
|
||||
// listeners to the frame's window.
|
||||
private forkHotkeys = false;
|
||||
|
||||
private hotkeyCodes = new Map<string, string[]>();
|
||||
|
||||
private hotkeyListeners = new Set<(report: ForkHotkeyReport) => void>();
|
||||
|
||||
private forkHotkeysListeners = new Set<() => void>();
|
||||
|
||||
public get forkHandlesHotkeys(): boolean {
|
||||
return this.forkHotkeys;
|
||||
}
|
||||
|
||||
/** Subscribe to `forkHandlesHotkeys` turning on. Returns an unsubscribe. */
|
||||
public onForkHotkeysChange(cb: () => void): () => void {
|
||||
this.forkHotkeysListeners.add(cb);
|
||||
return () => {
|
||||
this.forkHotkeysListeners.delete(cb);
|
||||
};
|
||||
}
|
||||
|
||||
/** Key codes `source` (e.g. 'ptt', 'deafen') wants reported from the frame. */
|
||||
public setHotkeyCodes(source: string, codes: string[]): void {
|
||||
this.hotkeyCodes.set(source, codes);
|
||||
this.sendHotkeyCodes();
|
||||
}
|
||||
|
||||
private sendHotkeyCodes(): void {
|
||||
if (!this.joined || !this.forkHotkeys) return;
|
||||
const codes = [...new Set([...this.hotkeyCodes.values()].flat())];
|
||||
this.call.transport.send('io.lotus.set_hotkeys', { codes }).catch(() => undefined);
|
||||
}
|
||||
|
||||
/** Subscribe to the fork's `io.lotus.hotkey` reports. Returns an unsubscribe. */
|
||||
public onHotkey(cb: (report: ForkHotkeyReport) => void): () => void {
|
||||
this.hotkeyListeners.add(cb);
|
||||
return () => {
|
||||
this.hotkeyListeners.delete(cb);
|
||||
};
|
||||
}
|
||||
|
||||
/** [Gitea #43] The fork's `io.lotus.hotkey` report. */
|
||||
public onHotkeyReport(data: unknown): void {
|
||||
const report = parseForkHotkeyReport(data);
|
||||
if (report) this.hotkeyListeners.forEach((l) => l(report));
|
||||
}
|
||||
|
||||
/** [Gitea #43] The fork's `io.lotus.controls_state` report. */
|
||||
public onControlsState(data: unknown) {
|
||||
if (typeof data !== 'object' || data === null) return;
|
||||
const { screensharing, layout, screenshareAction } = data as {
|
||||
const { screensharing, layout, screenshareAction, hotkeys } = data as {
|
||||
screensharing?: unknown;
|
||||
layout?: unknown;
|
||||
screenshareAction?: unknown;
|
||||
hotkeys?: unknown;
|
||||
};
|
||||
this.forkControls = true;
|
||||
this.forkScreenshare = screenshareAction === true;
|
||||
if (hotkeys === true && !this.forkHotkeys) {
|
||||
this.forkHotkeys = true;
|
||||
this.sendHotkeyCodes();
|
||||
this.forkHotkeysListeners.forEach((l) => l());
|
||||
}
|
||||
this.applyControls(
|
||||
typeof screensharing === 'boolean' ? screensharing : this.screenshare,
|
||||
layout === 'spotlight' || layout === 'grid' ? layout === 'spotlight' : this.spotlight,
|
||||
|
||||
Reference in New Issue
Block a user