feat(call): send io.lotus.set_deafen to the fork (P6-2 phase 1)

CallControl now sends the new io.lotus.set_deafen action (join-gated via
forceState) on every deafen / screenshare-audio-mute toggle + on join, ALONGSIDE
the retained iframe-DOM .muted hack (transitional). Against the current pinned
bundle the action is immediately error-replied + swallowed by .catch — inert, no
timeout. Reordered toggleSound() to commit state before setSound() so the sent
deafen value isn't inverted.

Phase 2 (after the fork is published): bump the pin lotus.1 -> lotus.2 and delete
the DOM hack. Docs: HANDOFF §12.4, LOTUS_TODO P6-2, LOTUS_BUGS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 14:12:08 -04:00
parent 804caa5130
commit 4ff07ea2bd
4 changed files with 102 additions and 42 deletions
+34 -4
View File
@@ -31,6 +31,12 @@ export class CallControl extends EventEmitter implements CallControlState {
private _pipMode = false;
// P6-2: mirrors CallEmbed.joined. Set true from forceState(), which CallEmbed
// invokes only from onCallJoined(). Gates io.lotus.set_deafen so we never send
// before the fork's widget handler mounts (pre-join sends pend to a 10s
// timeout — HANDOFF_ELEMENT_CALL_FORK.md §12.1 F1).
private joined = false;
private get document(): Document | undefined {
return this.iframe.contentDocument ?? this.iframe.contentWindow?.document;
}
@@ -141,6 +147,12 @@ export class CallControl extends EventEmitter implements CallControlState {
this.spotlight,
);
await this.applyState();
// P6-2: CallEmbed calls forceState() only from onCallJoined(), so this is
// the join transition. Flip the gate open, then push the current deafen
// state to the fork's freshly-mounted handler. (setSound() above ran while
// this.joined was still false, so it was gated — this is the first send.)
this.joined = true;
this.sendDeafenState();
}
public startObserving() {
@@ -209,6 +221,7 @@ export class CallControl extends EventEmitter implements CallControlState {
el.muted = !sound || (isScreenshareAudio && this.screenshareAudioMuted);
});
}
this.sendDeafenState();
}
private applyScreenshareAudioMuted(): void {
@@ -221,6 +234,20 @@ export class CallControl extends EventEmitter implements CallControlState {
el.muted = this.screenshareAudioMuted;
});
}
this.sendDeafenState();
}
// P6-2: send deafen state to the fork (io.lotus.set_deafen). The DOM .muted
// code above is a transitional fallback — remove once the fork ships & the
// pin is bumped.
private sendDeafenState(): void {
if (!this.joined) return;
this.call.transport
.send('io.lotus.set_deafen', {
deafened: !this.sound,
screenshareAudioMuted: this.screenshareAudioMuted,
})
.catch(() => undefined);
}
public onMediaState(evt: CustomEvent<ElementMediaStateDetail>) {
@@ -286,10 +313,8 @@ export class CallControl extends EventEmitter implements CallControlState {
public toggleSound() {
const sound = !this.sound;
this.setSound(sound);
// After un-deafening, re-apply screenshare audio mute if active
if (sound) this.applyScreenshareAudioMuted();
// P6-2: commit state before setSound()/applyScreenshareAudioMuted() so
// sendDeafenState() (which reads this.sound) reports the new value.
const state = new CallControlState(
this.microphone,
this.video,
@@ -299,6 +324,11 @@ export class CallControl extends EventEmitter implements CallControlState {
this.screenshareAudioMuted,
);
this.state = state;
this.setSound(sound);
// After un-deafening, re-apply screenshare audio mute if active
if (sound) this.applyScreenshareAudioMuted();
this.emitStateUpdate();
if (!this.sound && this.microphone) {