feat(call): host half of the 0.25.0-lotus.2 fork changes

- io.lotus.request_state: when the fork's lotus handlers (re)register
  (an EC-side remount that doesn't unmount us) we re-send deafen, quality
  and the focus pin, and the decoration pusher re-pushes its roster —
  decorations and the pin no longer vanish for the rest of the call
  (element-call#17).
- focus_participant carries the per-device media id from call_state
  (speaking device preferred) so a multi-device user pins the right
  device (element-call#30).
- injectAudio returns the fork's reply; when it refuses with
  reason:"muted" the soundboard shows "Unmute your microphone…" instead
  of playing the clip locally as if it went out (element-call#13).

All backwards compatible with the 0.25.0-lotus.1 bundle (unknown action
is acked; missing reply fields default to "played").

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-13 01:26:34 -04:00
co-authored by Claude Opus 5
parent 039b74c2b9
commit f50f50be72
5 changed files with 72 additions and 6 deletions
@@ -71,7 +71,12 @@ function ParticipantMenu({
if (isFocused) {
callEmbed?.control.clearFocusParticipant();
} else {
callEmbed?.control.focusCameraParticipant(userId);
// [EC#30] Pass the fork's per-device media id when we have one (from
// io.lotus.call_state) so a multi-device user pins the active device.
const parts = callEmbed?.getLotusParticipants() ?? [];
const mine = parts.filter((p) => p.userId === userId);
const pick = mine.find((p) => p.speaking) ?? mine.find((p) => p.audioEnabled) ?? mine[0];
callEmbed?.control.focusCameraParticipant(userId, pick?.id ?? null);
}
};