The scheduled-messages tray was cancel-only. Add an inline edit button
that re-opens ScheduleMessageModal seeded with the existing body and
send-time, letting the user change the text and/or when it sends.
MSC4140 has no in-place edit, so an edit is schedule-new + cancel-old.
Order matters: the modal schedules the new delayed event first, then we
cancel the old one and only prune it from local state once the server
confirms. A failed cancel therefore leaves a visible, retriable copy in
the tray instead of silently letting the stale message fire or losing
the edit. Edits go through the plain-text composer, so rich content
collapses to m.text (acceptable for v1).
ScheduleMessageModal gains optional initialSendAt (seed the pickers) and
title props so it is reusable for both scheduling and editing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Voice messages carry an MSC1767 waveform (org.matrix.msc1767.audio.waveform) and the
recorder draws a live one, but AudioContent playback only showed a plain seek bar.
Now the player renders the waveform as bars that fill with the accent (TDS green under
Lotus Terminal) as the clip plays, and the waveform itself is the seek control —
click, drag, or keyboard (arrows +/-5s, Home/End) with role=slider + ARIA value text.
- AudioContent: new optional "waveform" prop + a WaveformSeek sub-component
(downsamples to 48 bars, mirrors the recorder's bar styling); falls back to the
plain Range seek bar when there's no waveform.
- Threaded through MAudio (RenderAudioContentProps) so timeline voice messages get it
automatically; the Media Gallery Audio tab passes it directly.
Improves both the timeline and the new gallery Audio tab at once.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review of 28cb004e (3 agents) surfaced two real issues:
- Downloads used a raw mxc→http anchor, so encrypted media (voice messages are
almost always in E2EE DMs) downloaded ciphertext. Reuse FileDownloadButton for
both the audio AND file rows — it decrypts before saving, adds loading/success
state + a toast, and derives a filename extension.
- Audio playback bypassed MAudio's mimetype sanitization; pass the value through
getBlobSafeMimeType (e.g. application/ogg → audio/ogg) so encrypted/odd-mimetype
audio actually plays. Give voice messages a real filename+extension.
Also corrected the docs: AudioContent renders a seek bar, not an MSC3245 waveform.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New Audio tab (m.audio) listing voice messages + audio files with an inline player
(reuses AudioContent + MediaControl: play/seek/speed, MSC3245 voice waveform,
decrypt-on-play), sender/date, and download. Added to the tab counts.
- Jump-to-message: a 'Go to message' action on file rows, audio rows, and in the
lightbox navigates the timeline to the source event (useRoomNavigate) and closes
the drawer; threaded eventId into LightboxItem.
- Update the (stale) LOTUS_FEATURES.md Media Gallery entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Upgrade the MSC3381 poll feature from a leaky half-implementation to a complete,
cross-client-correct one:
- End/close a poll (creator or redact-PL mod) via inline confirm → m.poll.end;
locks voting, reveals results, marks winner(s); only pre-end responses count.
- Honor poll kind: undisclosed polls hide counts/percent/bars/total until ended
(creator gets a Show-live-results vs Hidden-until-ended toggle; default live).
Previously every poll was created undisclosed yet the UI leaked live results.
- Enforce max_selections for multi-choice; radiogroup/checkbox a11y with arrow-key
roving and an AT-announced winner.
- Robust, dual-namespace wire handling: parse BOTH stable (m.poll/m.id/m.selections)
and unstable (org.matrix.msc3381.poll.*) by hand — matrix-js-sdk 41.7.0's
PollStart/Response parsers only understand the unstable bodies, so delegating to
them broke every stable poll (caught in agent review). Use the SDK Poll model only
for end validation + before-end filtering.
- Pure tally/visibility/winner/parse logic extracted to utils/poll.ts with 14 tests
incl. a stable/unstable wire-format round-trip.
Reviewed by 3 agents (spec/cross-client, logic, a11y/UI); findings applied.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- index.tsx: request navigator.storage.persist() for logged-in sessions so the
browser can't evict the IndexedDB rust-crypto store (eviction while the
localStorage session survives resurrects the device with a blank store → the
KE-1 "one time key already exists" upload storm). Guarded, checks persisted()
first, best-effort.
- Docs: remove HANDOFF_ELEMENT_CALL_FORK.md, LOTUS_E2EE_INVESTIGATION.md, and
LOTUS_BUGS.md. Port their live content into the three kept docs — verification
backlog → LOTUS_TESTING; open bugs + E2EE (KE-1..4) + an Element Call fork
operational reference (publish steps + io.lotus action catalog) → LOTUS_TODO.
Fix all dangling references (README, code comments, cross-doc links). Full
history of the removed docs remains in git.
Gates: tsc/eslint/prettier clean, build OK, 665 tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Forward: checkbox multi-select room picker + "Send to N rooms" batch send
(Promise.allSettled). Full success auto-closes; partial failure keeps the dialog
open with a "Forwarded to X/N — failed: …" summary and prunes the selection to
only the failures (retry won't duplicate to already-sent rooms). Content builder
extracted to a unit-tested forwardContent.ts (edit-forwarding, reply-strip,
undecryptable-refused; 4 tests).
Bookmarks: BookmarksPanel resolves each saved message's live event (useRoomEvent)
so previews reflect edits and show a deleted indicator for redactions; the stored
snapshot stays as the fallback while loading, on fetch failure, or after leaving
the room. Stored bookmark shape unchanged.
Gates: tsc/eslint/prettier clean, build OK, 665 tests. Reviewed (dup-resend on
retry + Checkbox readOnly fixed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- P5-42 → [~] IMPLEMENTED (pragmatic WebView2 keep-alive) + LOTUS_FEATURES entry.
- P5-51 → [DEFERRED] with a concrete future-work spec (single-session storage map:
sessions.ts localStorage keys + initMatrix IndexedDB stores; the 6 things true
per-context isolation needs; multi-account as the smaller intermediate step).
- P5-52 → [DROPPED] (matrix-js-sdk can't do true per-room sync filtering; only
cosmetic client-side hiding).
- P5-53 → [DEFERRED] with the lighter automation-rules alternative recorded.
Every desktop P5 item is now dispositioned: implemented, won't-fix, or
deferred-with-spec/dropped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CallEmbed sets `autoGainControl=false` for the ML noise-suppression tier so
the browser's auto gain control doesn't fight the in-source ML model; the
browser/off tiers keep AGC on.
- Docs: refresh the LOTUS_FEATURES noise-suppression section (browser-native
default, quality-ordered dropdown, DFN3 ML default, attenuation floor,
gate-after-ML, DFN level 60, AGC-off, the reliability fixes) and LOTUS_TODO
P5-30 (mark tuning/reliability/AGC done; record GTCRN as researched-and-deferred).
Co-Authored-By: Claude Opus 4.8 <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>
Captures the plan to fork element-hq/element-call and build it from source for
true ownership of the in-call experience (decorations, focus/screenshare,
reconnect mic, native theming, call-audio injection) — none of which are fixable
against the prebuilt @element-hq/element-call-embedded bundle we ship today.
- New HANDOFF_ELEMENT_CALL_FORK.md: self-contained plan for a fresh session
(current architecture, full file inventory, phases, new-repo decision, the
denoise-shim interaction, doc corrections).
- Tagged every related note with [EC-FORK] + links: README (For Developers),
LOTUS_BUGS (EC limitations), LOTUS_TODO (soundboard, denoise, soundboard
cross-origin correction), LOTUS_FEATURES (call section).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit/repair of the multi-model denoise work so it actually builds and only
exposes working, self-hosted models.
- Complete the DTLN/DFN3 revert: uninstall @workadventure/noise-suppression
and deepfilternet3-noise-filter (package.json + lockfile), drop the unused
DTLN asset-copy block from vite.config.js (was shipping ~2MB of unused
tflite/wasm), and narrow DenoiseModelId to the bundled models (rnnoise,
speex). Coerce any retired persisted model value back to the default.
- Fix General.tsx CI typecheck failures introduced by the denoise UI: restore
three imports the rewrite deleted (useDateFormatItems, SequenceCardStyle,
useTauriUpdater), add the missing denoise/sound imports, and correct
hallucinated Folds props (Text has no variant/bold; Box uses
alignItems/justifyContent). tsc now passes with 0 errors.
- Harden the vite denoise plugin: required RNNoise/Speex/gate assets and the
shim now fail the build loudly if missing (instead of a silent warn that
shipped a broken ML feature), and the index.html shim injection is verified.
- CI: move the cinny-desktop submodule bump into ci.yml as a `trigger-desktop`
job gated on `needs: build`, and delete the standalone trigger-desktop.yml.
A failing push no longer kicks off the slow Tauri builds in parallel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement a flexible, multi-model noise suppression pipeline for Element Call/LiveKit integration:
- ML Engines: Added support for RNNoise, Speex, DTLN, and DeepFilterNet 3 models.
- Pipeline Architecture: Implemented modular audio processing in lotus-denoise.js, supporting 'Series Suppression' (running browser-native NSNet2 before ML) and a hardware-style Noise Gate.
- UI & UX Enhancements:
- Settings UI: Added model comparison chart with CPU/Quality metadata.
- Tuning: Added Live Microphone Meter for calibrating Noise Gate thresholds.
- Reporting: Added LotusToast system to alert users when ML suppression fails or falls back to raw input.
- Robustness & Quality:
- Capture Fidelity: Removed forced 48kHz capture constraints to allow native-rate capture (solving static issues with high-end audio interfaces).
- Performance: Added WASM SIMD detection with transparent fallback.
- Capability Detection: Added browser feature detection to disable unsupported ML modes.
- Build Integration: Updated Vite config to self-host all model WASM/tflite assets in /denoise/ directory.
ML noise suppression produced loud static on real calls. RNNoise requires
mono 48kHz float input; feeding it stereo or wrong-rate data is the classic
cause of that static. Harden the shim:
- request mono (channelCount:1) + 48kHz capture
- run a 48kHz AudioContext and BAIL to the raw mic if the browser won't
give a true 48kHz context (wrong-rate data -> static)
- force the worklet node to explicit mono in/out
- use the non-SIMD rnnoise.wasm (SIMD build artifacts on some GPUs)
- share one AudioContext across captures
Also fix the two CI-blocking eslint errors (unused vars in UrlPreviewCard
and useLocalMessageSearch) and apply repo-wide prettier formatting so
check:eslint and check:prettier pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the boolean call noise-suppression setting with a 3-way control
(Off / Browser-native / ML beta) in Settings -> General -> Calls.
- Off: noiseSuppression=false to Element Call
- Browser-native: EC's built-in WebRTC suppressor (prior default)
- ML (beta): on-device RNNoise (@sapphi-red/web-noise-suppressor)
Element Call captures the mic inside its iframe and publishes to LiveKit,
so the host can't reach that track; LiveKit's Krisp filter is Cloud-only
(we self-host the SFU) and EC's own RNNoise PR #3892 is unmerged. The ML
tier instead injects a same-origin pre-init shim into the vendored EC
index.html (build/lotus-denoise.js, wired by the lotusDenoise vite plugin)
that patches getUserMedia and routes the captured mic through an RNNoise
AudioWorklet before LiveKit sees it -- the same post-capture pipeline as
#3892, with no EC fork/AGPL/rebase burden. Falls back to the raw mic if
setup fails; keeps echoCancellation/AGC on the raw capture.
- settings.ts: callNoiseSuppression -> 'off'|'browser'|'ml' + legacy
boolean migration (true->browser, false->off)
- CallEmbed/useCallEmbed: tier maps to noiseSuppression param and appends
lotusDenoise=ml (native suppressor off in ML mode)
- vite.config.js: copy RNNoise worklet/wasm + shim into the EC bundle and
inject the shim <script> before EC's module entry
- docs: LOTUS_FEATURES.md, LOTUS_TODO.md (P5-30 done)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Point DECORATION_CDN at Lotus Nextcloud WebDAV share instead of external
avatardecoration.com; all 99 APNG files are self-hosted and served via
direct DAV URL (no CORS issue for <img> elements)
- Add onError handler to AvatarDecoration.tsx to silently hide the overlay
if a file is missing or the CDN is unreachable
- Rewrite scripts/syncDecorations.mjs: now sends HTTP HEAD requests to the
live Nextcloud CDN (batches of 16 in parallel) and removes catalog entries
for files that return non-2xx; empty categories are pruned automatically.
Workflow: delete files from Nextcloud → run `npm run sync:decorations` →
commit the updated avatarDecorations.ts. No local files needed.
- Add public/decorations/ to .gitignore; delete the 85 MB local APNG cache
that was downloaded during development (files live on Nextcloud now)
- Add sync:decorations script to package.json
- Update LOTUS_FEATURES.md, LOTUS_TODO.md (P5-13 + P5-14 ✓), README.md
with avatar decoration documentation and catalog sync workflow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- usePresenceUpdater: replace stale closure with readStatus() called at
invocation time so changing custom status in Profile Settings is never
silently overwritten by subsequent activity events
- CallEmbedProvider: fix m.space.parent state-key lookup by switching
getStateEvent → getStateEvents (plural); space channel voice rooms no
longer trigger the incoming-call ring/animation
- Add useUserNotes hook (io.lotus.user_notes account data, reactive via
useAccountDataCallback, 500-char limit, cross-device sync)
- UserRoomProfile: add UserPrivateNotes textarea with 800ms debounced
auto-save, saving indicator, char counter when <100 chars remain;
shown only when viewing another user's profile
- LOTUS_FEATURES.md: add Private Notes section, Status Revert fix note,
animation improvements subsection, Seasonal Themes section
- LOTUS_BUGS.md: mark presence revert + voice ringing bugs as resolved
- README.md + landing/index.html: document all new June 2026 features
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sounds (P5-16): browsers block the Web Audio context until a user gesture
starts it, so join/leave sounds — which fire later with no gesture — were
silent. unlockCallSounds() now primes/resumes the shared AudioContext inside
the Join click (centralized in useCallStart so every join path is covered),
making the per-client sounds reliably audible to everyone in the call.
Voice limit (P5-10): the limit is now a hard, cross-client server-side cap
enforced by the voice-limit-guard sidecar (matrix repo) that fronts
lk-jwt-service and refuses LiveKit tokens when a room is full. Updated
LOTUS_FEATURES.md / README.md / LOTUS_TODO.md to reflect that the client
'Channel Full' check is UX only and the server is authoritative.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
P5-10 Voice Channel User Limit:
- New StateEvent.LotusVoiceLimit (io.lotus.voice_limit) with { max_users }
- RoomVoiceLimit admin control in Room Settings > General > Voice
(power-level gated via permissions.stateEvent)
- CallPrescreen reads the limit reactively and disables Join with a
'Channel Full (N/N)' message at capacity; existing members can rejoin
P5-16 Custom Join/Leave Sound Effects:
- useCallJoinLeaveSounds hook wired into CallUtils; detects participant
join/leave via MatrixRTCSession membership changes (sender|deviceId),
filters out self, only fires while joined
- Sounds synthesized in-browser with Web Audio (callSounds.ts) - no
assets bundled; styles Off/Chime/Soft/Retro
- 'Join & Leave Sounds' selector in Settings > Calls (previews on change)
Docs: LOTUS_FEATURES.md, README.md, LOTUS_TODO.md (P5-10/P5-16 marked done)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- LOTUS_FEATURES.md: added sections for Knock-to-Join Notifications for
Admins and AFK Auto-Mute in Voice under their parent headings
- LOTUS_TODO.md: marked P4-3 and P5-11 as [x] completed
- README.md: updated Calls & Voice bullet list with AFK auto-mute entry;
expanded knock admin badge entry with badge-count detail
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- P5-21: Custom @mention highlight color picker in Settings → Appearance.
CSS vars with luminance-computed text color; resets cleanly to theme default.
- P5-22: Font selector (System, Inter, JetBrains Mono, Fira Code) in
Settings → Appearance. Fira Code added to Google Fonts preload.
- P5-27: Gaming/Work/Sleep preset buttons at top of Settings → Notifications.
Each atomically applies a group of notification settings.
- AppearanceEffects component in App.tsx applies CSS vars on settings change.
- LOTUS_BUGS.md: mark presence + manifest icon bugs as resolved.
- LOTUS_TODO.md: mark P3-6, P5-21, P5-22, P5-27 as [x].
- LOTUS_FEATURES.md: document all four completed features.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- LOTUS_FEATURES.md: full reference of every custom Lotus feature with
implementation details, file paths, and API notes
- LOTUS_BUGS.md: audit of confirmed bugs with root causes and fixes
- LOTUS_TODO_REFERENCE.md: technical implementation notes for backlog items
- LOTUS_TODO.md: trim completed audit results section, link to FEATURES doc
- README.md: rewrite to marketing-friendly feature list format; fix
incorrect claim that screenshare auto-reverts view to grid (removed)
Fix: auto-revert spotlight on screenshare was removed (600ms grid-click
caused fullscreen to show avatars); corrected in LOTUS_FEATURES.md and
removed from README.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>