Merge upstream v4.12.3 (Element Call 0.20.1) into lotus
CI / Build & Quality Checks (push) Successful in 10m42s
CI / Trigger Desktop Build (push) Successful in 10s

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 04:11:41 -04:00
13 changed files with 136 additions and 53 deletions
+63 -10
View File
@@ -14,10 +14,14 @@ export class CallControl extends EventEmitter implements CallControlState {
private iframe: HTMLIFrameElement;
private bodyMutationObserver: MutationObserver;
private controlMutationObserver: MutationObserver;
private _pipMode = false;
private mediaStatePromiseResolver: undefined | (() => void);
private get document(): Document | undefined {
return this.iframe.contentDocument ?? this.iframe.contentWindow?.document;
}
@@ -30,17 +34,29 @@ export class CallControl extends EventEmitter implements CallControlState {
return screenshareBtn ?? undefined;
}
private get leaveButton(): Element | undefined {
const leaveBtn = this.document?.querySelector('[data-testid="incall_leave"]');
return leaveBtn ?? undefined;
}
private get settingsButton(): HTMLElement | undefined {
// EC 0.19.3: settings button has data-testid="settings-bottom-center"
return (
(this.document?.querySelector('[data-testid="settings-bottom-center"]') as HTMLElement) ??
undefined
);
// EC 0.20.1: settings button moved to bottom-left; fall back to bottom-center.
const settingsButtonLeft = this.document?.querySelector(
'[data-testid="settings-bottom-left"]',
) as HTMLButtonElement | undefined;
const settingsButtonCenter = this.document?.querySelector(
'[data-testid="settings-bottom-center"]',
) as HTMLButtonElement | undefined;
return settingsButtonLeft ?? settingsButtonCenter ?? undefined;
}
private get reactionsButton(): HTMLElement | undefined {
// EC 0.19.3: reactions/raise-hand button has a CSS module class containing "raiseHand"
return (this.document?.querySelector('[class*="raiseHand"]') as HTMLElement) ?? undefined;
// EC 0.20.1: reactions/raise-hand button sits just before the leave button.
const reactionsButton = this.leaveButton?.previousElementSibling as HTMLElement | null;
return reactionsButton ?? undefined;
}
private get spotlightButton(): HTMLInputElement | undefined {
@@ -66,6 +82,7 @@ export class CallControl extends EventEmitter implements CallControlState {
this.call = call;
this.iframe = iframe;
this.bodyMutationObserver = new MutationObserver(this.onBodyMutation.bind(this));
this.controlMutationObserver = new MutationObserver(this.onControlMutation.bind(this));
}
@@ -118,6 +135,30 @@ export class CallControl extends EventEmitter implements CallControlState {
}
public startObserving() {
if (!this.document) return;
this.bodyMutationObserver.observe(this.document.body, {
childList: true,
subtree: false, // only direct children of body
});
this.onBodyMutation();
}
private onBodyMutation() {
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');
}
this.observeControls();
}
private observeControls() {
this.controlMutationObserver.disconnect();
const screenshareBtn = this.screenshareButton;
@@ -141,8 +182,14 @@ export class CallControl extends EventEmitter implements CallControlState {
this.setSound(this.sound);
}
private setMediaState(state: ElementMediaStatePayload) {
return this.call.transport.send(ElementWidgetActions.DeviceMute, state);
private async setMediaState(state: ElementMediaStatePayload) {
const data = await this.call.transport.send(ElementWidgetActions.DeviceMute, state);
return new Promise<typeof data>((resolve) => {
if (this.mediaStatePromiseResolver) {
this.mediaStatePromiseResolver();
}
this.mediaStatePromiseResolver = () => resolve(data);
});
}
private setSound(sound: boolean): void {
@@ -186,9 +233,14 @@ export class CallControl extends EventEmitter implements CallControlState {
if (this.microphone && !this.sound) {
this.toggleSound();
}
if (this.mediaStatePromiseResolver) {
this.mediaStatePromiseResolver();
this.mediaStatePromiseResolver = undefined;
}
}
public onControlMutation() {
private onControlMutation() {
const screenshare: boolean = this.screenshareButton?.getAttribute('data-kind') === 'primary';
const spotlight: boolean = this.spotlightButton?.checked ?? false;
@@ -321,6 +373,7 @@ export class CallControl extends EventEmitter implements CallControlState {
}
public dispose() {
this.bodyMutationObserver.disconnect();
this.controlMutationObserver.disconnect();
}