- 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
- CallSoundboard: the 30s safety timeout (which unsticks the one-at-a-time
playingKey guard if audio never signals end) was never cleared, so it fired
~30s after every clip. It's now stored in a per-play token that done() clears
by identity — a natural 'ended' cancels it, and a stale done() from a prior
clip can't disarm a newer clip's timer (which matters because a rejected
audio.play() fires neither ended nor error, leaving the timer as the only
guard-reset). The unmount effect also clears any pending timer, and the timer
is armed only when there's an audio element.
- PrescreenControls: useMediaPermissions set PermissionStatus.onchange but never
removed it → a permission change after unmount setState'd a dead component and
retained the callback. Now guards all setState with a cancelled flag and
detaches onchange in the effect cleanup.
Bug-hunt findings from LOTUS_TODO. Three review passes (the last prescribed the
per-play token to close a shared-ref cross-play edge). Gate-green (tsc, eslint,
prettier, 922 tests, build).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From the 6-agent deep per-feature audit. Mobile-gated / consistency fixes;
desktop unchanged except two intentional dialog-width normalizations noted below.
- In-call control bar: wrap="Wrap" on the SequenceCard so the compact two-group
row wraps on the narrowest phones (<=390px) instead of pushing End off-screen
(M1 fixed the 500-750px band; this covers narrower). Desktop stays one row.
- In-call soundboard popout: clamp maxWidth to the viewport (like M5's screenshare
popover) so it can't overflow a narrow phone.
- Report-Message dialog + "Seen by" (EventReaders) modals (Message.tsx x2 +
RoomViewFollowing): add useModalStyle so they go full-screen on mobile like
their sibling report/receipt modals (they floated as fixed cards before).
- In-app toast container: full-width toasts inset from both edges on mobile
(ScreenSize.Mobile); a fixed 280-340px card previously overflowed a narrow
phone. Desktop byte-identical (bottom-right floating card).
- Policy-list tabs + audio-controls rows: wrap="Wrap" (inert on desktop).
Intentional desktop deltas (normalizing to existing sibling modals, verified by
two review passes as consistent, not regressions): Report dialog max-width
380->480px; EventReaders modals 460->360px.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Editor (SoundboardPackEditor): show each clip's length in seconds (stored on
upload via getAudioDurationMs, and captured on preview for existing clips); the
preview button now toggles play/stop with a 'now playing' equalizer indicator;
reworked the volume control into a fixed cell with a % readout so the slider's
max no longer collides with the delete button.
Call soundboard: clip names wrap (up to 3 lines, word-break) instead of being
truncated with an ellipsis; cards grow to fit.
TODO: logged the basic audio-editor / video->audio-extractor as a large project.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- C-H1: forceState only on FIRST join; on EC reconnect re-arm the fork handlers
(resendForkState — deafen+quality only) instead of clobbering live mic/video/
deafen back to the join-time snapshot.
- C-H2: AFK auto-mute reads the fork's io.lotus.call_state VAD of the LOCAL
published track instead of getUserMedia on the browser DEFAULT mic (which could
measure silence while the user spoke on another device → auto-mute an active
speaker). Fails safe (never mutes) when call_state is null OR empty.
- C-H3: control observer re-binds after EC re-renders (body subtree:true + 100ms
debounce) with an early-return so unchanged state doesn't re-render.
- C-M3 setQuality join-gated; C-M4 hangup 4s fallback dispose (idempotent);
C-M5 PTT no longer silently un-deafens; C-M6 screenshare-audio mute resets on
stop; C-L4 deafen key works in the iframe; C-L6 setState-after-unmount guards.
Reviewed (C-H2 [] fail-safe + C-H3 re-render guard applied). tsc/eslint/prettier
clean, build OK, 677 tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Soundboard v2 — a near-parallel of the custom-emoji image-pack system for
in-call audio clips.
- Data model: 3-tier packs mirroring MSC2545 — room/space pack (state event
io.lotus.soundboard, inherited by child rooms via parent-space aggregation),
global refs (io.lotus.soundboard_rooms), and the personal pack
(io.lotus.soundboard account data; the v1 flat-list content is migrated to the
pack shape on read). New plugins/soundboard/ (readers, SoundboardPack, utils) +
hooks/useSoundboardPacks (useRelevantSoundboardPacks = user U global U room,
deduped). Unit-tested (migration + slug).
- Management: reusable SoundboardPackEditor (name + emoji + per-clip volume +
delete + upload + batched save), power-level-gated for room packs like emoji
packs; a Soundboard page wired into Room + Space settings.
- In-call: CallSoundboard rewritten as a Discord-style grid grouped by pack
(emoji + name tiles), sourcing room+parent-space U personal clips; a Manage
toggle embeds the editors; per-clip volume x master volume on playback.
- Spam guard: host gates on a playing key (fork enforces one clip at a time).
- Control bar: Mute-Screenshare moved next to the Screenshare button.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Element Call is now consumed as our self-built fork
(@lotusguild/element-call-embedded); wire up its previously-dormant
capabilities and document the fork as live.
Soundboard (P5-15): a call-bar button plays user-uploaded audio clips into the
call as a real published track (io.lotus.inject_audio) plus local playback.
Clips are uploadable like emoji/sticker packs, stored in io.lotus.soundboard
account data (synced across devices). Gated by a Settings toggle + volume.
Quality controls (P5-31): per-user mic/screenshare bitrate + screenshare
framerate (Settings -> Calls), applied via io.lotus.set_quality clamped to any
room cap. Room admins set caps and hard call-permissions (allow_screenshare /
allow_camera) in Room Settings -> Voice; the call bar hides blocked buttons.
- New: CallSoundboard, useSoundboard, soundboardClips; RoomQuality,
useCallQuality, callQuality (+ unit tests).
- Optimistic-write RoomQuality admin UI (no stale-state clobber).
- Docs: mark EC fork live across README/FEATURES/TODO/BUGS/TESTING; add D2
manual-test steps.
Numeric quality caps are client-cooperative; screenshare/camera permissions are
hard-enforced server-side (see LotusGuild/matrix voice-limit-guard).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>