Add own control buttons for element-call (#2744)

* add mutation observer hok

* add hook to read speaking member by observing iframe content

* display speaking member name in call status bar and improve layout

* fix shrining

* add joined call control bar

* remove chat toggle from room header

* change member speaking icon to mic

* fix joined call control appear in other

* show spinner on end call button

* hide call statusbar for mobile view when room is selected

* make call statusbar more mobile friendly

* fix call status bar item align
This commit is contained in:
Ajay Bura 2026-03-09 14:04:48 +11:00 committed by GitHub
commit bc6caddcc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 521 additions and 100 deletions

View file

@ -14,12 +14,58 @@ export class CallControl extends EventEmitter implements CallControlState {
private iframe: HTMLIFrameElement;
private controlMutationObserver: MutationObserver;
private get document(): Document | undefined {
return this.iframe.contentDocument ?? this.iframe.contentWindow?.document;
}
private get screenshareButton(): HTMLElement | undefined {
const screenshareBtn = this.document?.querySelector(
'[data-testid="incall_screenshare"]'
) as HTMLElement | null;
return screenshareBtn ?? undefined;
}
private get settingsButton(): HTMLElement | undefined {
const leaveBtn = this.document?.querySelector('[data-testid="incall_leave"]');
const settingsButton = leaveBtn?.previousElementSibling as HTMLElement | null;
return settingsButton ?? undefined;
}
private get reactionsButton(): HTMLElement | undefined {
const reactionsButton = this.settingsButton?.previousElementSibling as HTMLElement | null;
return reactionsButton ?? undefined;
}
private get spotlightButton(): HTMLInputElement | undefined {
const spotlightButton = this.document?.querySelector(
'input[value="spotlight"]'
) as HTMLInputElement | null;
return spotlightButton ?? undefined;
}
private get gridButton(): HTMLInputElement | undefined {
const gridButton = this.document?.querySelector(
'input[value="grid"]'
) as HTMLInputElement | null;
return gridButton ?? undefined;
}
constructor(state: CallControlState, call: ClientWidgetApi, iframe: HTMLIFrameElement) {
super();
this.state = state;
this.call = call;
this.iframe = iframe;
this.controlMutationObserver = new MutationObserver(this.onControlMutation.bind(this));
}
public getState(): CallControlState {
@ -38,6 +84,14 @@ export class CallControl extends EventEmitter implements CallControlState {
return this.state.sound;
}
public get screenshare(): boolean {
return this.state.screenshare;
}
public get spotlight(): boolean {
return this.state.spotlight;
}
public async applyState() {
await this.setMediaState({
audio_enabled: this.microphone,
@ -47,6 +101,26 @@ export class CallControl extends EventEmitter implements CallControlState {
this.emitStateUpdate();
}
public startObserving() {
this.controlMutationObserver.disconnect();
const screenshareBtn = this.screenshareButton;
if (screenshareBtn) {
this.controlMutationObserver.observe(screenshareBtn, {
attributes: true,
attributeFilter: ['data-kind'],
});
}
const spotlightBtn = this.spotlightButton;
if (spotlightBtn) {
this.controlMutationObserver.observe(spotlightBtn, {
attributes: true,
});
}
this.onControlMutation();
}
public applySound() {
this.setSound(this.sound);
}
@ -72,7 +146,9 @@ export class CallControl extends EventEmitter implements CallControlState {
const state = new CallControlState(
data.audio_enabled ?? this.microphone,
data.video_enabled ?? this.video,
this.sound
this.sound,
this.screenshare,
this.spotlight
);
this.state = state;
@ -83,6 +159,20 @@ export class CallControl extends EventEmitter implements CallControlState {
}
}
public onControlMutation() {
const screenshare: boolean = this.screenshareButton?.getAttribute('data-kind') === 'primary';
const spotlight: boolean = this.spotlightButton?.checked ?? false;
this.state = new CallControlState(
this.microphone,
this.video,
this.sound,
screenshare,
spotlight
);
this.emitStateUpdate();
}
public toggleMicrophone() {
const payload: ElementMediaStatePayload = {
audio_enabled: !this.microphone,
@ -104,7 +194,13 @@ export class CallControl extends EventEmitter implements CallControlState {
this.setSound(sound);
const state = new CallControlState(this.microphone, this.video, sound);
const state = new CallControlState(
this.microphone,
this.video,
sound,
this.screenshare,
this.spotlight
);
this.state = state;
this.emitStateUpdate();
@ -113,6 +209,30 @@ export class CallControl extends EventEmitter implements CallControlState {
}
}
public toggleScreenshare() {
this.screenshareButton?.click();
}
public toggleSpotlight() {
if (this.spotlight) {
this.gridButton?.click();
return;
}
this.spotlightButton?.click();
}
public toggleReactions() {
this.reactionsButton?.click();
}
public toggleSettings() {
this.settingsButton?.click();
}
public dispose() {
this.controlMutationObserver.disconnect();
}
private emitStateUpdate() {
this.emit(CallControlEvent.StateUpdate);
}

View file

@ -5,9 +5,21 @@ export class CallControlState {
public readonly sound: boolean;
constructor(microphone: boolean, video: boolean, sound: boolean) {
public readonly screenshare: boolean;
public readonly spotlight: boolean;
constructor(
microphone: boolean,
video: boolean,
sound: boolean,
screenshare = false,
spotlight = false
) {
this.microphone = microphone;
this.video = video;
this.sound = sound;
this.screenshare = screenshare;
this.spotlight = spotlight;
}
}

View file

@ -220,6 +220,7 @@ export class CallEmbed {
});
this.call.stop();
this.container.removeChild(this.iframe);
this.control.dispose();
this.mx.off(ClientEvent.Event, this.onEvent.bind(this));
this.mx.off(MatrixEventEvent.Decrypted, this.onEventDecrypted.bind(this));
@ -233,6 +234,21 @@ export class CallEmbed {
private onCallJoined(): void {
this.joined = true;
this.applyStyles();
this.control.startObserving();
}
private applyStyles(): void {
const doc = this.document;
if (!doc) return;
doc.body.style.setProperty('background', 'none', 'important');
const controls = doc.body.querySelector('[data-testid="incall_leave"]')?.parentElement
?.parentElement;
if (controls) {
controls.style.setProperty('position', 'absolute');
controls.style.setProperty('visibility', 'hidden');
}
}
private onEvent(ev: MatrixEvent): void {