- P5-36/43/44/46/47/49/55/57 → [~] IMPLEMENTED (web verified; native CI-compile-pending, runtime-verify on Windows). - P5-40 → [x] DONE (TauriUpdateFeature already ships the update toast). - P5-50 → [WON'T FIX] (can't inject Media Foundation into WebView2's WebRTC pipeline; Chromium already HW-decodes). - P5-35 → note the "can't compile-test without Windows" premise is outdated (CI compiles Windows now); remains Tier B (rides with P5-41). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
56 KiB
Lotus Chat — Work Backlog
Repo: lotus branch at https://code.lotusguild.org/LotusGuild/cinny
Deploy: push to lotus → CI → auto-deploy to chat.lotusguild.org (~11 min)
⚠️ TDS DESIGN LAW — READ BEFORE TOUCHING ANY UI
ALL Lotus Terminal Design System (TDS) styling — colors, animations, glows, borders, fonts, spacing — MUST come exclusively from
/root/code/web_template/base.cssCSS variables. Do NOT hardcode hex values. Do NOT invent new variable names. Do NOT deviate from the design tokens defined in that file. The canonical variable reference:--lt-accent-orange,--lt-accent-cyan,--lt-accent-green,--lt-glow-orange,--lt-box-glow-*,--lt-border-color, etc. Reference implementation for code patterns:/root/code/tinker_tickets/(markdown.js, base.js, ticket.css) This rule applies to EVERY task in this file without exception.
🧩 NATIVE-CINNY LAW — EVERY FEATURE MUST FEEL LIKE STOCK CINNY
Every feature we implement must feel native to the upstream Cinny app — indistinguishable from something the Cinny team would have shipped. Reference: https://github.com/cinnyapp/cinny.
Concretely this means:
- Use the
foldsdesign system, not bespoke UI. Build with folds primitives (Button,Chip,IconButton,Menu,MenuItem,Dialog,Modal,Input,Switch,Badge,SettingTile,SequenceCard, etc.) and folds tokens (color.*,config.space.*,config.radii.*,config.borderWidth.*). No hardcoded hex/rgba()for UI chrome, no invented/undefined CSS variables.- Match Cinny's existing patterns. Before adding UI, find the closest existing Cinny component/flow and mirror it (e.g. a new dropdown uses
Button+PopOut+Menu+MenuItemlike the rest; a new modal has aHeaderwith a closeIconButton; a new setting is aSettingTileinside aSequenceCard). Consistency with stock Cinny beats personal style.- Lotus-custom additions should be unobtrusive and fit Cinny's visual language, spacing, and interaction conventions — a stranger using Cinny should not be able to tell which features are ours.
The ONE exception: explicit Lotus Terminal Design System (TDS) features, which intentionally have their own distinct look and follow the TDS Design Law above. TDS styling is opt-in (only active in Lotus Terminal mode); everything else must look and feel like native Cinny.
Completed features are documented in LOTUS_FEATURES.md.
✅ Done — Awaiting Verification
Built and gate-green; verify per LOTUS_TESTING.md, then they graduate to LOTUS_FEATURES.md. (Bug-side fixes awaiting verification live in LOTUS_BUGS.md.)
| Feature | Test guide |
|---|---|
| Full-Screen Camera Broadcasts (per-participant focus) | A5 / G2 |
| Advanced search filters (sender/date/has-link/has:image·file·video/pinned/recent) | K2 / M1 / M2 / M4 |
| Custom Accent Color Picker (non-TDS themes) | M3 |
| 5 Color Theme Presets (Cyberpunk/Ocean/Blood Red/Matrix/Midnight) | M5 |
| Intersection-based lazy media loading | H1 |
| Context-aware thumbnail previews | H2 |
| Desktop — proactive update notifications (Tauri) | J1 |
| Remind Me Later | K1 |
| Mobile Bookmarks access | E5 |
| In-Call Soundboard (P5-15, uploadable clips → real call inject) | D2-7 |
| Call Quality Controls (P5-31, user + room-admin caps) | D2-8 |
| Call Permissions (P5-31, hard server-side screenshare/camera policy) | D2-9 |
Legend:
[AUDIT REQUIRED]— at least one assumption needs code/server verification before implementing[SERVER CHECK]— depends on a Synapse feature or MSC; verify onmatrix.lotusguild.org[LOW PRIORITY]— implement after all higher-priority items[EXTREME COMPLEXITY]— multi-sprint, plan separately before touching[BLOCKED]— cannot build until a server upgrade, upstream MSC, or dependency resolves[IMPROVE]— feature exists in upstream Cinny; this task enhances it for Lotus Chat
Status: [ ] pending · [~] in progress · [x] completed
Server Capabilities (as of June 2026)
- Homeserver:
matrix.lotusguild.org - Synapse version:
1.155.0(2026-06-18) — fully up to date; last version for Debian 12 (LXC 151 already on Debian 13 Trixie) - Matrix spec: up to
v1.12formally; newer MSC features viaunstable_features
Confirmed facts
| Finding | Impact |
|---|---|
MSC flags ON: msc4140 · msc3771 · msc3440.stable · msc4133.stable · simplified_msc3575 · msc4222 · msc3266 · msc3401_matrix_rtc |
All safe to use now |
MSC flags OFF: msc4306 (thread subscriptions) · msc3882 · msc3912 · msc4155 |
These features are BLOCKED |
MSC3266 room summary: flag msc3266_enabled: true set but GET /v1/rooms/{id}/summary still returns 404 (M_UNRECOGNIZED) |
Room Preview BLOCKED — endpoint not implemented in Synapse 1.155 |
| MSC3892 relation redaction: not in flags | Reaction Redaction feature BLOCKED |
MSC4260 report user: POST /_matrix/client/v3/users/{userId}/report returns 200 ✅ |
Report User UNBLOCKED — endpoint live since Synapse 1.133; ready to build |
| MSC4151 report room: HTTP 405 on GET = endpoint exists (POST only) | Report Room live ✅ |
folds AvatarImage does NOT accept children |
Add frame/overlay inside UserAvatar.tsx itself — optional frameName prop |
| No in-app toast system exists (was) | Built ToastProvider + Jotai queue; at App.tsx:65 |
useUnverifiedDeviceCount() hook exists |
src/app/hooks/useDeviceVerificationStatus.ts:65-106 |
Voice player: AudioContent.tsx:44-223 |
Playback rate on hidden <audio> at line 217 |
CallControl.setMicrophone(bool) at CallControl.ts:206-212 |
For AFK auto-mute |
CallControl.toggleSound() at CallControl.ts:230-251 |
Push-to-deafen — just wire a hotkey to this |
| matrix-js-sdk has NO arbitrary profile field methods | Use mx.http.authedRequest() for MSC4133 |
Sanitizer (sanitize.ts) allows table, div, span, a, code, hr |
LFG HTML card is safe locally; test on Element/FluffyChat |
Sanitizer STRIPS <math>/MathML tags |
Math/LaTeX task must also modify sanitizer |
Service worker EXISTS at src/sw.ts |
Quick-reply task: add notificationclick handler |
knockSupported() utility exists at matrix.ts:376-391 |
Knock UX: only need "Request to Join" in RoomIntro.tsx |
KeywordMessages.tsx already has custom keyword push rules |
Full push rule editor: only non-keyword rule types need new UI |
getMatrixToRoom() in matrix-to.ts generates invite URLs |
Invite link: just add QR code to room settings |
io.lotus.inject_audio widget action publishes a clip as a real call track |
In-call soundboard CAN now mix into the call (no longer local-only); needs cinny UI to drive the action |
| Folds uses vanilla-extract in non-TDS, NOT CSS custom properties | Custom accent color: must create new vanilla-extract theme variant dynamically |
| Theme presets need ~50 CSS custom properties each | Significant design work before coding |
useCallSpeakers.ts CSS MutationObserver polling |
Visual speaking indicator: TDS ring animation on top of existing data |
| MSC3489/3672 live location: BOTH false on server | Live Location BLOCKED |
Key File Reference
| What you need | File | Lines |
|---|---|---|
| Global keydown hook | src/app/hooks/useKeyDown.ts |
whole file |
| Room navigation | src/app/hooks/useRoomNavigate.ts |
19-72 |
| All room IDs atom | src/app/state/room-list/roomList.ts |
allRoomsAtom |
| Room unread counts | src/app/state/room/roomToUnread.ts |
roomToUnreadAtom |
| Overlay portal provider | src/app/pages/App.tsx |
65 |
| Portal container div | index.html |
101 |
| Room settings tabs | src/app/features/room-settings/RoomSettings.tsx |
27-56 |
| State event read/write pattern | src/app/features/common-settings/general/RoomEncryption.tsx |
42-52 |
| Power level checker | src/app/hooks/usePowerLevels.ts |
whole file |
| Slash command registration | src/app/hooks/useCommands.ts |
140-537 |
| Chat background picker | src/app/features/settings/general/General.tsx |
945-981 |
| Chat backgrounds definition | src/app/features/lotus/chatBackground.ts |
whole file |
| Matrix.to URL builder | src/app/plugins/matrix-to.ts |
getMatrixToRoom() |
| Media event content types | src/app/types/matrix/common.ts |
46-91 |
| Media URL conversion | src/app/utils/matrix.ts |
mxcUrlToHttp() |
| Message pagination (search) | src/app/features/message-search/useMessageSearch.ts |
74-121 |
| Infinite pagination pattern | src/app/features/message-search/MessageSearch.tsx |
234-365 |
| Poll event format | src/app/components/message/content/PollContent.tsx |
1-320 |
| Theme class application | src/app/hooks/useTheme.ts |
25-60 |
| Animations file | src/app/styles/Animations.css.ts |
whole file |
| Message status (EventStatus) | src/app/features/room/message/Message.tsx |
84-142 |
| Call member change events | src/app/hooks/useCall.ts |
37-52 |
| Mic control in calls | src/app/plugins/call/CallControl.ts |
206-212 |
| Device verification hook | src/app/hooks/useDeviceVerificationStatus.ts |
65-106 |
| Knock room support check | src/app/utils/matrix.ts |
376-391 |
| Room join button location | src/app/components/room-intro/RoomIntro.tsx |
25-119 |
| Notification mute via push rules | src/app/hooks/useRoomsNotificationPreferences.ts |
110-150 |
| Message text body CSS | src/app/components/message/layout/layout.css.ts |
182-205 |
Priority 3 — Higher complexity / lower daily frequency
[ ] P3-4 · Accessibility Improvements (WCAG 2.1 AA)
What: Comprehensive audit and fix pass targeting the critical user paths:
- Room list navigation (keyboard-only)
- Reading messages in the timeline (screen reader announces new messages)
- Composing and sending a reply
- Opening and closing modals (focus trap, return focus)
- ARIA labels on all icon-only buttons
Scope: Do NOT attempt to make every corner of the app AA-compliant in one pass — focus on the golden path (open app → find room → read → reply → send).
[AUDIT REQUIRED] — Run an automated audit first: npx axe-core or browser DevTools accessibility tree. Document every violation before writing a single line of code. Prioritize by severity (critical > serious > moderate).
Investigation Findings:
- Root Cause: Inconsistent focus management, missing
aria-liveregions for dynamic timeline updates, and sparse global keyboard shortcuts. - Approach: Standardize
focus-trap-reactusage (referenceRoomNavItem.tsx). Addaria-liveregions to the timeline. ExpanduseKeyDown.tsfor section navigation shortcuts. - Complexity: Medium-High (audit is the main work).
[ ] P3-8 · Thread Panel (full side drawer)
⚠️ LARGEST FEATURE — requires its own planning session before implementation.
What: A right-side drawer for threaded conversations. Currently "Reply in Thread" exists but there is no panel to read or write thread replies.
Features:
- Click "Reply in Thread" → opens thread drawer on the right
- Thread root event shown at the top of the panel
- Full message rendering for all in-thread replies (reuse timeline components)
- Reply input at the bottom (full composer with formatting, emoji, etc.)
- Unread count badge on the thread button in the main timeline
- Keyboard shortcut to close thread panel
Architecture:
- New Jotai atom:
activeThreadEventId: string | null - New component:
src/app/features/room/thread/ThreadPanel.tsx - Rendered alongside
RoomViewas a conditional right panel (mirror the members drawer pattern) - Filter events in timeline to
m.threadrelation for the active root event ID - Shares the same
mxclient and room reference as the main timeline
[AUDIT REQUIRED] — Deeply audit how m.thread relation events are currently stored and retrieved in the matrix-js-sdk. Understand the thread aggregation API: GET /rooms/{roomId}/relations/{eventId}/m.thread. Check if RoomTimeline.tsx currently filters out thread replies from the main timeline (it should — confirm).
Investigation Findings:
- Root Cause: Current
m.threadevents are treated as standardm.room.messageevents and rendered in the main timeline. - Approach: Introduce new Jotai atom
activeThreadEventId. CreateThreadPanel.tsx. UpdateRoomTimeline.tsxto filter out thread relations (m.relates_to). Implement aggregation fetch usingGET /rooms/{roomId}/relations/{eventId}/m.thread. Usethread.timelineSetdirectly for the most accurate thread view. - Complexity: High.
Priority 4 — Specialized, high complexity, or low priority
[ ] P4-7 · Virtualized Infinite Scroll for Search Results
What: Replace the manual "load more" button with an automated, virtualized infinite scroll for search results.
Approach: Utilize @tanstack/react-virtual in MessageSearch.tsx to handle the nextToken automatically as the user scrolls.
[ ] P4-8 · Encrypted Message Search Indexing & Caching
What: Implement a persistent local cache for search results, optimized for encrypted rooms.
Approach: Use IndexedDB to store search metadata (event IDs, timestamps) to prevent redundant server-side decryption/fetching.
[ ] P4-1 · Thread Notification Mode Per-Thread (MSC3771)
Spec: MSC3771 (stable). Depends on Thread Panel (#P3-8).
What: Per-thread notification toggle: "All messages" vs "Mentions only". Accessible from the thread panel header. Tracks unread counts separately per thread.
[AUDIT REQUIRED] — Implement after Thread Panel. Requires understanding how the SDK tracks per-thread unread counts.
Complexity: Medium (after thread panel exists).
[ ] P4-2 · Thread Subscriptions (MSC4306) [BLOCKED]
Spec: MSC4306 (Synapse experimental). Depends on Thread Panel (#P3-8).
What: "Follow thread" button to receive notifications for a thread you haven't posted in. Uses MSC4306 subscription endpoint.
[SERVER CHECK] — org.matrix.msc4306 = false on matrix.lotusguild.org — BLOCKED until server enables it.
Complexity: Medium (after thread panel exists).
[ ] P4-4 · Math / LaTeX Rendering in Messages (LOW PRIORITY)
Spec: CS-API §11.5 (stable) — formatted_body can contain LaTeX.
What: Render $...$ or $$...$$ LaTeX expressions in message bodies. Use KaTeX (lightweight, ~100KB, renders server-side-compatible CSS). Must gracefully fall back to raw LaTeX text if KaTeX fails.
Note: This is LOW PRIORITY — only useful for academic/technical communities. Implement last.
[AUDIT REQUIRED] — Confirm KaTeX bundle size impact on the Vite bundle. Check if matrix-js-sdk's HTML sanitizer strips LaTeX before it reaches the renderer. The formatted_body sanitization pipeline is the main risk here. (Confirmed: sanitizer STRIPS <math> tags — must be patched alongside the renderer.)
Complexity: Low-Medium.
[ ] P4-5 · Live Location Sharing (MSC3489 + MSC3672) (LOW PRIORITY, HIGH COMPLEXITY) [BLOCKED]
Spec: MSC3489 + MSC3672. Implemented in Element Web.
Note: Static location sharing is already implemented. This adds live/real-time GPS beacons. Very low priority per user preference.
What: Start sharing live location → creates m.beacon_info state event → client posts m.beacon events on a timer → other users see your position update live on a map.
[SERVER CHECK] — org.matrix.msc3489 = false AND org.matrix.msc3672 = false on matrix.lotusguild.org — BLOCKED.
Complexity: High. Requires background geolocation API + live map rendering.
[~] P4-6 · OIDC / SSO Next-Gen Auth (MSC3861) — CLIENT-SIDE BUILT, awaiting live verification
Spec: MSC3861 / MSC2965, Matrix spec v1.15. OAuth2-native auth via a Matrix Authentication Service (MAS).
Scope decision (2026-06): CLIENT-ONLY. We implemented OIDC login in the Lotus client so it can sign into next-gen homeservers (mozilla.org, eventually matrix.org). We deliberately did not convert lotusguild's own Synapse to MAS (no account migration; lotusguild keeps password + legacy Authelia SSO).
Built (matrix-js-sdk already ships the OIDC API; this was wiring):
- Discovery:
cs-api.tsgetOidcIssuer()(stablem.authentication+ msc2965). Flow hint:useParsedLoginFlowsgetOidcCompatibilityFlag()(MSC3824). - Login:
pages/auth/oidc/{oidcConfig,oidcLoginUtil,oidcState}.ts(dynamic registration + cache, PKCE authorize),login/OidcLogin.tsx, issuer-gatedLogin.tsx. - Callback:
oidc/OidcCallback.tsx+App.tsxshort-circuit (non-hash redirect path). - Session/refresh:
state/sessions.tsOIDC fields,client/{oidcTokenRefresher,oidcLogout}.ts,initMatrix.tswiring. - Account mgmt:
settings/account/OidcManageAccount.tsx. - 13 unit tests (discovery/flow/session/cache/callback parsing). All gates green.
Awaiting verification (needs a real MSC3861 server — lotusguild is NOT one): deploy + log into mozilla.org (requires adding mozilla to the deployed
config.jsonhomeserverList + its domains to the CSPconnect-src/img-src— see below), OR run a localmatrix-authentication-service+ Synapsemsc3861dev loop.
To enable the mozilla.org test: add tomatrix/cinny/config.jsonhomeserverList"mozilla.org", and to the nginx CSPconnect-src/img-src:https://mozilla.org https://mozilla.modular.im https://chat.mozilla.org https://vector.im.
Priority 5 — Gamer / Aesthetic / Customization
[MOVED] P5-9 · LFG (Looking for Group) Command → LotusBot
Decision: Implemented as !lfg in LotusBot rather than a client slash command. Bot-side rendering works consistently across all Matrix clients; client-side enhanced cards would only be visible to Lotus Chat users and require sanitizer auditing. The bot can also support richer flows (list active LFGs, DM interested players, auto-expire posts).
[~] P5-15 · In-Call Soundboard — IMPLEMENTED (⚠️ awaiting live verification, D2-7)
What: Soundboard button in the call controls bar → popout grid of the user's clips; clicking one plays it into the call as a real published track (peers hear it) and locally (presser hears it). Clips are user-uploadable, just like custom emojis/stickers.
🔱 [EC-FORK] Fork side + cinny side DONE. The fork ships io.lotus.inject_audio (LotusWidgetActions.InjectAudio, allow-listed in widget.ts), armed via the lotusAudioInject=1 flag; it publishes a clip as a separate LiveKit track — a real in-call soundboard mixed into the call, not local-only. cinny now drives it.
Shipped (cinny):
- Clips stored in
io.lotus.soundboardaccount data → synced across devices like emoji/sticker packs (useSoundboardhook;AccountDataEvent.LotusSoundboard). - Upload audio (≤1 MB, ≤40 clips) →
mx.uploadContent→ mxc; play resolves mxc → authed download →blob:object URL (the widget can't fetch authenticated media itself) →control.injectAudio(url, volume)+ local playback. CallSoundboard.tsxpopout in the call bar (upload / play / delete), gated on thesoundboardEnabledsetting (Settings → General → Calls, + volume slider).
Remaining: a dedicated Settings management page (optional — upload/delete already live in the popout); a small default clip set; live verification (D2-7). Files:utils/soundboardClips.ts,hooks/useSoundboard.ts,features/call/CallSoundboard.tsx,plugins/call/CallControl.ts#injectAudio.
Complexity: Medium — done.
[~] P5-20 · Quick Reply from Browser Notification
What: Inline reply field in browser notification toasts via Notification Actions API. Reply sends as threaded reply to the triggering message.
[AUDIT REQUIRED] (1) Verify browser Notification Actions API support in target browsers. (2) Confirmed: service worker EXISTS at src/sw.ts — add notificationclick handler there.
Complexity: Medium-High.
Partial Fix Applied ⚠️ UNTESTED: Notifications now (a) show the real message body (username: message instead of "New inbox notification from..."), (b) click navigates directly to the room at the specific event (not the inbox), (c) window.focus() called on click so the tab comes to front, (d) reminder toasts also link to the specific event. Full inline-reply via Notification Actions API still needs the SW push+notificationclick pipeline (requires switching from new Notification() to showNotification() through the SW).
[x] P5-30 · Advanced ML Noise Suppression (Krisp-style)
What: High-end background noise cancellation using a pre-trained ML model (RNNoise) running in the browser. Removes dogs, fans, and keyboard clicks from the mic stream.
Shipped: 3-tier setting (Off / Browser-native / ML) in Settings → General → Calls.
🔱 [EC-FORK] DONE — moved in-source (2026-06). ML denoise is now a first-class audio stage inside the forked Element Call: a LiveKit TrackProcessor<Audio> activated by lotusDenoiseSource=1 (cinny sets it when ML is selected). The old build-time getUserMedia/index.html monkeypatch is removed. Because EC re-runs the processor on every (re)publish, denoise now survives reconnects and mic-device switches — this is the A7 fix (see LOTUS_BUGS.md A7, LOTUS_TESTING.md §D2-1). The processor degrades to the raw mic rather than going silent.
Key decision: LiveKit's Krisp filter is LiveKit-Cloud-only (we self-host the SFU); EC's own RNNoise PR #3892 is unmerged. Owning the fork let us implement the in-source stage directly.
Models — all in-source in the fork:
- DeepFilterNet 3 (48 kHz, ML default) · DTLN (16 kHz) · RNNoise (48 kHz) · Speex (48 kHz) — all four wired and selectable; dropdown ordered best-quality first. Tier default is Browser-native.
- Quality tuning (2026-07): dry/wet attenuation floor (~-16 dB, RNNoise/Speex only — the "robotic" fix; DTLN/DFN would comb-filter), gate-after-ML, DFN level 80→60. Floor tunable via
lotusDenoiseFloor. - AEC/AGC (2026-07): echo-cancellation ON; AGC OFF for the ML tier (
autoGainControl=false, threaded through ECUrlParams→ConnectionFactory) so browser AGC doesn't fight the model; playback confirmed no AEC-defeat. - Reliability (2026-07): never-silent watchdog, resume-timeout, WASM-cache reject-eviction, activate-off-local-participant, init/build leak fixes.
- Open verification: real-call by-ear A/B — model choice, floor value, AGC on/off (RNNoise known-weak historically).
LOTUS_TESTING.md§D2-1 / J2. - GTCRN (RESEARCHED — DEFERRED): tiny MIT 16 kHz model that beats RNNoise, but no drop-in browser package — needs a ~1-week from-scratch build:
onnxruntime-web(WASM, 1 thread) in a Web Worker (ORT can't run in an AudioWorklet — issue #13072) behind a custom AudioWorklet ring-buffer node presenting as anAudioNode; modelgtcrn_simple.onnx(~300 KB, stateful — threadconv/tra/intercaches per frame); we write STFT/iSTFT (n_fft 512/hop 256). Assets ~3–4 MB via thelotusDenoise()vite plugin. Registration checklist known (both repos, incl. the 2nddenoisePipeline.tsused by the DenoiseTester). Revisit only if low-power quality is insufficient after validating the current tuning. - Desktop-only / HW-gated (future): FRCRN or NVIDIA Maxine (RTX/Tensor only) — impossible in-browser; would run in the Tauri Rust backend + bridge a virtual mic into the webview. Detect capability; web falls back to RNNoise.
- Excluded: Krisp (LiveKit Cloud only); FRCRN/Maxine on web (GPU/server-bound).
[~] P5-31 · Granular Voice & Screenshare Quality Controls — IMPLEMENTED (⚠️ awaiting live verification, D2-8)
What: Let users (and room admins) adjust audio bitrate and screenshare bitrate/framerate.
🔱 [EC-FORK] Fork side + client side DONE. The fork ships io.lotus.set_quality (LotusWidgetActions.SetQuality) that applies audio/screenshare encoding params (RTCRtpSender.setParameters, all simulcast encodings, re-applied on TrackUnmuted/republish) inside EC. cinny now drives it.
Shipped (cinny):
- User settings (Settings → General → Calls): Microphone Bitrate, Screenshare Bitrate, Screenshare Framerate (
callAudioBitrate/screenshareBitrate/screenshareFramerate). - Room-admin caps:
io.lotus.room_qualitystate event (StateEvent.LotusRoomQuality) +RoomQuality.tsxin Room Settings → General → Voice (mirrorsRoomVoiceLimit). - Apply logic:
useCallQuality(wired inCallEmbedProvider'sCallUtils) buildsmin(user setting, room cap)and sendsio.lotus.set_qualityon join / when settings change (utils/callQuality.ts, unit-tested).
Server-side enforcement (DONE — matrix repo): extended voice-limit-guard.py (LXC 151) to also read io.lotus.room_quality and hard-enforce a publish-source policy for ALL clients.
- Reality (researched, primary-source, LiveKit 1.9.11): numeric bitrate/fps caps cannot be hard-enforced server-side — LiveKit is a pure SFU (forwards, never transcodes); there is NO bitrate/fps field in the JWT grant,
RoomConfiguration, serverlimit:config, or any admin RPC, and stock Element Call ignores room metadata / custom claims for publish quality. So numeric caps stay cooperative (our fork honors them viamin()→set_quality, already shipped). - What IS hard-enforced cross-client:
VideoGrant.canPublishSources. The guard holds the LiveKit secret, so whenio.lotus.room_qualitysetsallow_screenshare:false/allow_camera:falseit re-signs the issued JWT with a narrowed source list → the SFU refuses those tracks for every client (Element, FluffyChat, our fork). Mic always kept. Fail-open; unit-tested (livekit/test_voice_limit_guard.py). Admin UI: Room Settings → Voice → Call Permissions switches. cinny also hides the blocked buttons. - Live (mid-call) enforcement — DONE: the JWT re-sign covers new joins; for participants already in the call, a background reconcile loop in the guard calls LiveKit
UpdateParticipantevery ~3 s to narrowcanPublishSources, which unpublishes an in-progress screenshare/camera server-side for all clients and blocks re-publish (verified LiveKit 1.9.11 auto-unpublishes on permission narrowing). Only removes forbidden sources (never grants), preserves other permission flags, no-ops once compliant. So flipping a room audio-only kills live cameras/screenshares within ~one interval. - Not enforceable / deferred: numeric server enforcement (impossible — see above); screenshare resolution control (
set_qualitycovers bitrate + framerate; resolution needs agetDisplayMediahook inside the fork).
Complexity: DONE — client (cooperative numeric caps) + server (hard publish-source policy). Only the physically-impossible numeric server enforcement is out of scope.
[ ] P5-35 · Desktop — Notification Click Opens Room (DEFERRED)
What: Clicking a system tray notification navigates to the relevant room. Quick-reply from the notification toast would send the reply without opening the window.
Status: Deferred (Tier B). Note: the "can't compile-test without a Windows build environment" premise is outdated — CI now compiles Windows (Gitea self-hosted windows runner + GitHub windows-latest), and windows-crate/COM code already ships (e.g. set_badge_count, and the Tier A jump list). This still depends on P5-41 (the WinRT toast + custom activator), so it rides with that; it was not part of the Tier A wave.
Note: Tray icon and matrix: deep links already bring the window forward on most interactions. Revisit when tauri-plugin-notification gains click handler support upstream.
Complexity: High (platform-specific native code required).
[~] P5-36 · Desktop — Windows Jump List — IMPLEMENTED (Tier A); native CI-compile-pending, runtime-verify on Windows
What: Right-clicking the taskbar icon shows a jump list with recent/favorite rooms for quick navigation.
Status: Deferred — implementing the Windows COM jump list API in Tauri requires iterating on C++/COM code that can only be compile-checked on Windows, making blind CI iteration impractical.
Action when unblocked: Revisit when a Tauri plugin abstracts the Windows Shell ICustomDestinationList interface, or when a Windows build environment is available for local iteration.
Complexity: High (Windows-only native COM).
[ ] P5-41 · Desktop — Native WinRT Toast Notifications
What: Replace emulated notifications with native WinRT Toast notifications.
Approach: Implement native WinRT Toast integration using windows-rs to enable full Action Center integration, including native Quick Reply functionality.
[ ] P5-42 · Desktop — Persistent Background Sync
What: Maintain light connection to homeserver when WebView2 is suspended.
Approach: Implement a headless Rust sidecar to fetch unread counts/notifications while the webview is suspended to ensure instant notification delivery.
[~] P5-43 · Desktop — System Media Transport Controls (SMTC) — IMPLEMENTED (Tier A); CI-compile-pending; SMTC may need an active media/audio session to surface — verify on Windows
What: Integrate with Windows SMTC for volume flyout call/media control.
Approach: Use Windows SMTC API to expose call status, mic mute/unmute, and media controls to the Windows volume flyout/media overlay.
[~] P5-44 · Desktop — Taskbar Thumbnail Toolbar — IMPLEMENTED (Tier A); native CI-compile-pending, runtime-verify on Windows
What: Add persistent call controls to the taskbar preview.
Approach: Implement a COM thumbnail toolbar in the application preview window, featuring Mute/Deafen/End Call buttons.
[~] P5-46 · Desktop — System Power Management (Call Continuity) — IMPLEMENTED (Tier A reference); web verified; native CI-compile-pending (Windows only; macOS/Linux no-op TODO)
What: Prevent system sleep/hibernate during active calls.
Approach: Use Tauri/Rust power-manager or platform-specific APIs to block system power saving states while a voice/video session is active.
[~] P5-47 · Desktop — TDS-Styled Native Window Chrome — IMPLEMENTED (Tier A, OPT-IN/default-off, runtime-reversible); web verified; native CI-compile-pending
What: Replace system titlebar with custom Lotus TDS chrome.
Approach: Configure Tauri window (decorations: false) and implement custom, TDS-token compliant titlebar controls (Close/Max/Min) for a cohesive UI.
[ ] P5-48 · Desktop — Native File System Drag-and-Drop Improvements
What: Enhance drag-and-drop support for Windows.
Approach: Improve handling for Windows file shortcuts, recursive folder uploads, and shell-integrated "Send To" context menu actions.
[~] P5-49 · Desktop — Network Awareness (NCSI Integration) — IMPLEMENTED (Tier A; INetworkListManager poll → mx.retryImmediately); native CI-compile-pending, runtime-verify on Windows
What: Proactively detect Windows network connectivity changes.
Approach: Integrate with the Windows Network Connectivity Status Indicator (NCSI) API to improve offline mode transition latency and network recovery.
[WON'T FIX] P5-50 · Desktop — Windows Hardware-Accelerated Media Pipeline
What: Replace standard browser decoding with native Windows Media Foundation.
Why won't-fix (researched): WebRTC media (the call pipeline) lives entirely inside WebView2/Chromium — you cannot inject Media Foundation/DirectShow into WebRTC's decode path from the Tauri host. Chromium already uses the platform's hardware decoders (D3D11VA/MF) where the GPU supports the codec, so there is no separate CPU pipeline to offload. Not actionable as described.
[ ] P5-51 · Desktop — Federated "Identity Contexts" (Isolation Manager)
What: Compartmentalize sessions, local databases, and caches into isolated "Contexts."
Approach: Implement a zero-leak boundary for personas (e.g., Work vs. Personal) by isolating IndexedDB, filesystem caches, and session persistence per context.
Priority: Extreme Low (Multi-sprint/Architectural).
[~] P5-52 · Desktop — Room-Level Sync Governor (Performance Control) [STILL_CONSIDERING]
What: Granular sync tuning for individual rooms.
Approach: Allow per-room overrides for sync frequency and event type filtering (e.g., disable read receipts/typing in heavy rooms) to optimize performance. Implementation requires careful UX to prevent complexity fatigue.
[ ] P5-53 · Desktop — Local-Only "Scripting" Plugin System (Tampermonkey-like)
What: A sandboxed environment for local execution of user scripts on Matrix events.
Approach: Implement a WASM-based execution engine that allows users to write local-only, client-side scripts to interact with incoming Matrix events, trigger sounds/notifications, or inject custom UI elements based on event payload rules. Designed for privacy — all logic runs exclusively on the local machine.
[~] P5-55 · Desktop — Composer Toolbar Drag-and-Drop Reordering — IMPLEMENTED (Tier A); web-verified (tsc/build/tests). Awaiting live UX check
What: Allow users to reorder toolbar icons via drag-and-drop.
Approach: Extend the current settings-based toolbar toggle system to include a drag-and-drop UI mode in the composer settings, allowing users to personalize their icon order.
[ ] P5-56 · Desktop — Windows "Focus Assist" (DND) Sync
What: Automatically toggle notification state based on Windows Focus Assist.
Approach: Integrate with the Windows NotificationCenter / Focus state via Tauri/Rust to automatically enable/disable Lotus Chat's internal notification suppression mode when Windows Focus Assist is toggled.
[~] P5-57 · Desktop — Visual Draft Persistence Indicator — IMPLEMENTED (Tier A); web-verified. Awaiting live UX check
🚀 Features to Add
- Mobile Audit: Comprehensive audit of all features in LOTUS_FEATURES.md for mobile PWA usability and layout responsiveness.
Blocked Features
These features are confirmed desirable but cannot be built until the listed dependency is resolved.
Check back after each Synapse upgrade — re-run /matrix/client/versions and unstable_features to see if they've become available.
[BLOCKED] · Live Location Sharing (MSC3489 + MSC3672)
Blocked by: org.matrix.msc3489 = false AND org.matrix.msc3672 = false on matrix.lotusguild.org (confirmed from unstable_features).
What it would do: Real-time GPS beacon streaming upgrading the existing static location share.
Action when unblocked: Both MSCs must be enabled on the homeserver before any client work.
[BLOCKED] · Reaction / Relation Redaction (MSC3892)
Blocked by: org.matrix.msc3892 = false on matrix.lotusguild.org
What it would do: Cleanly remove a reaction without redacting the parent message.
Current behavior: Full event redaction — acceptable fallback, no user-facing issue.
Action when unblocked: Find onReactionToggle redaction call site; swap in MSC3892 endpoint with fallback.
[BLOCKED] · Room Preview Before Joining (MSC3266)
Blocked by: GET /_matrix/client/v1/rooms/{roomId}/summary returns M_UNRECOGNIZED 404 — endpoint not implemented in Synapse 1.155. Config flag msc3266_enabled: true is set but has no effect; Synapse appears not to have shipped a stable implementation at the v1 path. Verified 2026-06-18.
What it would do: Show room name, topic, avatar, member count before joining.
Action when unblocked: Re-test after each future Synapse upgrade.
[BLOCKED] · Thread Subscriptions (MSC4306)
Blocked by: org.matrix.msc4306 = false on matrix.lotusguild.org
What it would do: Follow a thread without posting; get notifications for replies.
Action when unblocked: Add "Follow thread" button in the thread panel header (depends on #P3-8 Thread Panel).
[DONE] · Report User (MSC4260) ✅
Previously blocked by: Server spec v1.12, but POST /_matrix/client/v3/users/{userId}/report was confirmed 200 on 2026-06-18 (live since Synapse 1.133.0).
What it does: Reports a specific user to homeserver admins (separate from reporting a message).
Note: Report Message already exists in upstream Cinny. This adds Report User to the profile panel.
Implemented 2026-06-18: ReportUserModal.tsx added at src/app/features/room/ReportUserModal.tsx. Button wired into UserRoomProfile.tsx between UserModeration and UserDeviceSessions (hidden for own profile). Category dropdown + reason text, inline success/error feedback, auto-close 1500ms after success.
Pending Audits
[ ] Audit-3 · Profile banner image — Matrix protocol support
Research whether Matrix spec or MSC4133 (v1.16) defines a standard profile banner field. uk.tcpip.msc4133.stable = true on our server — check if a banner_url or similar field is defined. If no cross-client standard exists, do not implement.
📚 Implementation Reference
Exhaustive, low-level implementation details for backlog items. Follow these patterns to ensure code is "Lotus-perfect" (idiomatic, performant, and TDS-compliant).
P3-8 · Thread Panel (Full Side Drawer)
Architecture: Mirror the MembersDrawer pattern but with a specialized timeline.
- State (
src/app/state/room/thread.ts):export const activeThreadIdAtom = atom<string | null>(null); - Layout (
src/app/features/room/Room.tsx): InsertThreadPanelconditionally alongsideRoomTimeline:{ activeThreadId && ( <> <Line variant="Background" direction="Vertical" size="300" /> <ThreadPanel roomId={roomId} threadId={activeThreadId} /> </> ); } - Component (
src/app/features/room/thread/ThreadPanel.tsx): Useroom.getThread(threadId)from the SDK. Render aHeaderwith a "Close" button that setsactiveThreadIdAtomtonull. ReuseRoomTimelinebut pass a filteredEventTimelineSet. Usethread.timelineSetdirectly for the most accurate thread view.
P4-4 · Math / LaTeX Rendering
Mechanism: KaTeX injection into the HTML parser.
- Sanitizer (
src/app/utils/sanitize.ts): Allow KaTeX-specific tags and classes (e.g.,span,annotation,math). Use a specialized allowed list for math blocks.[Gemini_Found]
sanitize.tsusessanitize-html(not DOMPurify) with an explicit allowlist (allowedTags) anddisallowedTagsMode: 'discard'. All MathML tags are currently absent from the allowlist and are silently stripped. UpdatepermittedHtmlTagsto include:<math>,<mi>,<mo>,<mn>,<ms>,<mtext>,<mspace>,<mrow>,<mfrac>,<msqrt>,<mroot>,<mstyle>,<merror>,<mpadded>,<mphantom>,<mfenced>,<menclose>,<msub>,<msup>,<msubsup>,<munder>,<mover>,<munderover>,<mmultiscripts>,<mtable>,<mtr>,<mtd>,<maligngroup>,<malignmark>, andannotation. Also add the required MathML attributes (e.g.xmlns,display,mathvariant) topermittedTagToAttributes. - Parser (
src/app/plugins/react-custom-html-parser.tsx): Detect$ ... $and$$ ... $$patterns in text nodes:if (node.type === 'text') { const parts = node.data.split(/(\$\$.*?\$\$|\$.*?\$)/g); return parts.map((p) => { if (p.startsWith('$')) return <KaTeX math={p.replace(/\$/g, '')} />; return p; }); } - CSS (
src/app/styles/CustomHtml.css.ts): Importkatex/dist/katex.min.cssonly when a math block is rendered to save initial bundle size.
P4-6 · OIDC / SSO Next-Gen Auth (MSC3861)
Mechanism: Matrix Authentication Service (MAS) Integration.
- Architecture: Shift from password-based
/loginto OAuth2authorization_codeflow. - Key Files:
src/app/pages/auth/Login.tsxandsrc/app/hooks/useAuth.ts. - Implementation: Use
oidc-client-tsor a similar lightweight OIDC library. Check form.authenticationin/.well-known/matrix/client. Redirect to the MAS authorization endpoint. Handle the callback in a newOidcCallbackroute and store the OIDCrefresh_token.
P5-1 · Custom Accent Color Picker (Non-TDS only)
Mechanism: Dynamic CSS variable injection.
- Setting (
src/app/state/settings.ts): AddcustomAccentColor: string(hex). - Manager (
src/app/pages/ThemeManager.tsx): Inside theuseEffectthat monitors theme changes:if (!lotusTerminal && customAccentColor) { document.documentElement.style.setProperty('--lt-accent-orange', customAccentColor); document.documentElement.style.setProperty('--lt-accent-orange-glow', `${customAccentColor}80`); } - UI (
src/app/features/settings/general/General.tsx): Use<Input type="color">. Hide this section iflotusTerminalistrue.
P5-15 · In-Call Soundboard
Mechanism: Local-to-Global Audio Bridge via Web Audio API.
- Create an
AudioContextand aMediaStreamDestinationNode. - Create an
AudioBufferSourceNodefor each clip. - Route the mic
MediaStreamand the clip source to the destination node. - Pass the destination's
.streamto the call bridge.
⚠️ [Gemini_Found — CORRECTED] Gemini originally suggested using LiveKit's
LocalAudioTrack.replaceTrack()to mix audio into the call stream. This is not possible from Lotus Chat's realm: Element Call runs in a cross-origin iframe controlled viamatrix-widget-api(postMessage). LiveKit's JS SDK and itsLocalAudioTracklive inside EC's sandboxed context — inaccessible from our code. This directly contradicts the confirmed constraint already listed in the Server Capabilities table: "Cindy CANNOT inject audio into EC call stream — In-call soundboard must be redesigned as local-only." The soundboard must be a local-playback-only feature (output through the user's speakers, not mixed into the call audio stream).🔱 [EC-FORK — RESOLVED] Both the original claim and the earlier "practical blocker still holds" correction are now outdated. EC is same-origin and we own the source, so we no longer reach into EC's module scope from cinny — instead the fork exposes the inject point itself: the
io.lotus.inject_audiowidget action (LotusWidgetActions.InjectAudio) publishes a clip as a separate LiveKit track from inside EC. A real in-call soundboard (mixed into the call, not local-only) is therefore unblocked, and the cinny-side soundboard UI is now built (P5-15 above): uploadable clips played into the call via this action, stored inio.lotus.soundboardaccount data.
P5-20 · Quick Reply from Browser Notification
Mechanism: Service Worker notificationclick Action.
[Gemini_Found] Implementation detail:
serviceWorkerRegistration.showNotification()should be used instead ofnew Notification()so that the service worker can listen to thenotificationclickevent.new Notification()creates notifications that are bound to the client page, not the SW.
// src/sw.ts
self.addEventListener('notificationclick', (event) => {
if (event.action === 'reply' && event.reply) {
const { roomId, threadId } = event.notification.data;
const session = sessions.get(event.clientId);
fetch(`${session.baseUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message`, {
method: 'POST',
headers: { Authorization: `Bearer ${session.accessToken}` },
body: JSON.stringify({
msgtype: 'm.text',
body: event.reply,
'm.relates_to': threadId ? { rel_type: 'm.thread', event_id: threadId } : undefined,
}),
});
}
});
P5-30 · Advanced ML Noise Suppression — Model Roadmap
See shipped implementation in LOTUS_FEATURES.md → "Noise Suppression (Advanced Multi-Tier)".
Models status:
- RNNoise (sapphi, 48 kHz) — ✅ working, default fallback. Keep — runs on any hardware.
- Speex (sapphi, 48 kHz) — ✅ working, low value; candidate to drop.
- DTLN (@workadventure, 16 kHz) — 🟡 wired; sample-rate fix applied (was robotic at 48 kHz). TODO: verify in a real call. Narrowband (16 kHz) = slightly telephone-y even when correct.
Constraints: client-side AudioWorklet, fully self-hosted, no GPU, self-hosted SFU (no LiveKit Cloud).
Roadmap:
- Verify DTLN 16 kHz fix in a real call.
- DeepFilterNet 3 — best self-hostable upgrade: Rust→WASM, CPU real-time, 48 kHz fullband. Self-host
df_bg.wasm+ DFN3 ONNX model; wire a 48 kHz worklet. Audio quality unverifiable without a real-call test. - Desktop-only / HW-gated: FRCRN (Alibaba) or NVIDIA Maxine (RTX/Tensor only). Runs in Tauri Rust backend + bridges a virtual mic into the webview. Must detect capability; web + weak HW falls back to RNNoise/DTLN.
P5-31 · Granular Voice & Screenshare Quality Controls
Mechanism: WebRTC Encoding Parameters + Backend Quality Guard.
- State Event:
io.lotus.room_quality(state key"") containing:{ "audio_bitrate": 128000, "screen_max_res": "1080p", "screen_max_fps": 60 } - Screenshare: In
src/app/plugins/call/CallControl.ts, map the "Quality" setting togetDisplayMediaconstraints. - Audio Bitrate: After the call joins, find the
RTCRtpSenderfor the audio track:const sender = peerConnection.getSenders().find((s) => s.track?.kind === 'audio'); const params = sender.getParameters(); params.encodings[0].maxBitrate = roomBitrate || 128000; await sender.setParameters(params); - Backend Sidecar: Extend
voice-limit-guard.py(LXC 151) to fetchio.lotus.room_qualityand inject limits into the LiveKit JWT or return them as an authorized config packet.
[x] P5-40 · Desktop — Proactive Update Notifications (Tauri) — DONE (already shipped: TauriUpdateFeature in ClientNonUIFeatures.tsx polls every 12h + fires the sticky update toast)
Key Files: src/app/hooks/useTauriUpdater.ts, src/app/pages/client/ClientNonUIFeatures.tsx, src/app/features/toast/LotusToastContainer.tsx.
- Create a
TauriUpdateFeaturecomponent. UseuseTauriUpdater()to get thecheckfunction andstatus. - In a
useEffect, callcheck()on mount and then on asetInterval(every 12 hours). - When status transitions to
{ state: 'available', version: '...' }, fire a Lotus Toast: "Lotus Chat v[version] is available!" with an "Update" button that callsinstall(). - Store
lastChecktimestamp inlocalStorageto prevent redundant checks on refresh.
Mobile Bookmarks Visibility Fix
Issue: ClientLayout.tsx explicitly restricts BookmarksPanel to ScreenSize.Desktop (lines 51-56).
// ClientLayout.tsx
{
bookmarksOpen && (
<BookmarksPanel
onClose={() => setBookmarksOpen(false)}
isMobile={screenSize !== ScreenSize.Desktop}
/>
);
}
BookmarksPanel.tsx already supports the isMobile prop (line 127) to enable full-screen absolute positioning. No other changes required.
Remind Me Later (Slack-style)
Mechanism: Account Data + Timer/Service Worker.
- Storage (
src/app/hooks/useReminders.ts): Store in account dataio.lotus.remindersasArray<{ id: string, roomId: string, eventId: string, timestamp: number }>. - Context Menu (
src/app/features/room/message/MessageContextMenu.tsx): Add "Remind me" option → opens date/time picker modal (reuseJumpToTime.tsxlogic). - Trigger (foreground):
setTimeoutin a hook insideReminderMonitorinClientNonUIFeatures.tsx→ pushes totoastQueueAtominstate/toast.tswhen due. - Trigger (background): Use Service Worker —
setTimeoutin the main thread will not fire when the PWA is suspended.
Mobile Usability Audit — Methodology
- Viewport & Touch: All interactive elements must have at least
44px × 44pxtouch targets. Audit for horizontal overflow (horizontal scrolling must be disabled). - Modal Responsiveness: All modals (Settings, Profile, etc.) MUST cover the full screen on mobile, not float as overlays.
- Sidebar / Panels: On mobile, sidebar panels (Members, Bookmarks, Media) must become full-screen overlays (using a
DrawerorModalpattern) rather than side-by-side flexbox panels. - Input & Composer: Ensure the composer doesn't get obscured by the mobile keyboard. Test focus trap and blur behaviors.
Implementation Notes
⚠️ TDS DESIGN LAW (repeated here for emphasis)
Every TDS color, animation, glow, border, shadow, and font value MUST come from
/root/code/web_template/base.css.
Never hardcode hex values. Never invent CSS variable names.
Key variables:--lt-accent-orange·--lt-accent-cyan·--lt-accent-green·--lt-glow-*·--lt-box-glow-*·--lt-border-color·--lt-font-mono
Reference implementation:/root/code/tinker_tickets/(markdown.js, base.js, ticket.css)
This applies without exception to every task marked[IMPROVE],[Build], or any UI change.
Design Rules
- All new components must respect both TDS dark (
LotusTerminalTheme) and TDS light (LotusTerminalLightTheme) modes - Non-TDS theme work (custom accent color, theme presets) uses vanilla-extract theme files — match the pattern in
src/lotus-terminal.css.ts - Code syntax highlighting token classes:
.tok-kw .tok-str .tok-num .tok-cmt .tok-fn(defined inweb_template/base.css) folds AvatarImagedoes NOT accept children — wrap Avatar components externally for overlays/frames/borders
CI/CD Pipeline
edit → commit → git push origin lotus
→ Gitea Actions: tsc --noEmit, eslint, prettier (~3 min)
→ Webhook: lotus_deploy.sh on LXC 106 polls CI, then npm ci && npm run build → rsync
→ Live at chat.lotusguild.org (~11 min total)
Per-Feature Checklist (before marking complete)
npx tsc --noEmit— zero TypeScript errorsnpx eslint src/— zero new errors (warnings OK if pre-existing)npx prettier --check src/— formatting passesREADME.mdupdated (Lotus-custom features only — not upstream Cinny features)landing/index.htmlupdated if the feature appears in the comparison table- Visually tested at
chat.lotusguild.orgafter CI deploys
Homeserver Access (for server audits)
- Synapse (Matrix): LXC 151 on
compute-storage-01—pct exec 151 -- bash - Config:
/etc/matrix-synapse/homeserver.yaml - Version check:
curl -s https://matrix.lotusguild.org/_matrix/client/versions