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>
9.0 KiB
Lotus Chat — Technical Implementation Field Guide
Date: June 2026
This document provides exhaustive, low-level implementation details for the remaining items in LOTUS_TODO.md. Follow these patterns to ensure code is "Lotus-perfect" (idiomatic, performant, and TDS-compliant).
🧵 Priority 3 — Higher Complexity
P3-8 · Thread Panel (Full Side Drawer)
Architecture: Mirror the MembersDrawer pattern but with a specialized timeline.
- 1. State (src/app/state/room/thread.ts):
export const activeThreadIdAtom = atom<string | null>(null); - 2. Layout (src/app/features/room/Room.tsx):
Insert the
ThreadPanelconditionally alongside theRoomTimeline.{ activeThreadId && ( <> <Line variant="Background" direction="Vertical" size="300" /> <ThreadPanel roomId={roomId} threadId={activeThreadId} /> </> ); } - 3. Component (src/app/features/room/thread/ThreadPanel.tsx):
- Use
room.getThread(threadId)from the SDK. - Render a
Headerwith a "Close" button that setsactiveThreadIdAtomtonull. - Reuse
RoomTimelinebut pass a filteredEventTimelineSet. - Pro Tip: Use
thread.timelineSetdirectly for the most accurate thread view.
- Use
🛠️ Priority 4 — Specialized Features
P4-4 · Math / LaTeX Rendering
Mechanism: KaTeX injection into the HTML parser.
- 1. Sanitizer (src/app/utils/sanitize.ts):
You must allow KaTeX-specific tags and classes (e.g.,
span,annotation,math). Use a specialized allowed list for math blocks. - 2. 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; }); } - 3. CSS (src/app/styles/CustomHtml.css.ts):
Import
katex/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 for
m.authenticationin/.well-known/matrix/client. - Redirect to the MAS authorization endpoint.
- Handle the callback in a new
OidcCallbackroute and store the OIDCrefresh_token.
- Use
🎨 Priority 5 — Gamer / Aesthetic / Customization
P5-1 · Custom Accent Color Picker (Non-TDS only)
Mechanism: Dynamic CSS variable injection.
- 1. Setting (src/app/state/settings.ts):
Add
customAccentColor: string(hex). - 2. Manager (src/app/pages/ThemeManager.tsx):
Inside the
useEffectthat monitors theme changes:if (!lotusTerminal && customAccentColor) { document.documentElement.style.setProperty('--lt-accent-orange', customAccentColor); // Also derive a 'glow' version (e.g. 50% opacity) document.documentElement.style.setProperty('--lt-accent-orange-glow', `${customAccentColor}80`); } - 3. UI (src/app/features/settings/general/General.tsx):
Use a
<Input type="color">component. Hide this section iflotusTerminalistrue.
P5-40 · Desktop — Proactive Update Notifications (Tauri)
Mechanism: Global Background Check via useTauriUpdater.
- Objective: Alert users to app updates without requiring a manual check in settings.
- Key Files:
src/app/hooks/useTauriUpdater.ts: Logic source.src/app/pages/client/ClientNonUIFeatures.tsx: Background mounting point.src/app/features/toast/LotusToastContainer.tsx: UI for notification.
- Implementation:
- Create a
TauriUpdateFeaturecomponent. - Use
useTauriUpdater()to get thecheckfunction andstatus. - In a
useEffect, callcheck()on mount and then on asetInterval(e.g., every 12 hours). - Watch the
status. When it transitions to{ state: 'available', version: '...' }, trigger an in-app Lotus Toast. - The toast should say "Lotus Chat v[version] is available!" with an "Update" button that calls the
install()function from the hook. - Persistence: Store the
lastChecktimestamp inlocalStorageto ensure the background check doesn't fire redundant commands every time the user refreshes or re-opens the app.
- Create a
🔊 Audio & Communications
P5-15 · In-Call Soundboard
Mechanism: Local-to-Global Audio Bridge.
- Architecture: Use the
Web Audio APIto mix sounds into theMediaStreambefore it enters the Element Call widget. - Implementation:
- Create an
AudioContext. - Create a
MediaStreamDestinationNode. - Create an
AudioBufferSourceNodefor the clip. - Route the mic
MediaStreamand the clip source to the destination. - Pass the destination's
.streamto the call bridge.
- Create an
P5-20 · Quick Reply from Browser Notification
Mechanism: Service Worker notificationclick Action.
- 1. Registration (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); // Uses existing session mapping // Send via direct fetch to bypass SDK loading 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, }), }); } });
🔬 Extreme Complexity Projects
P5-30 · Advanced ML Noise Suppression (Krisp-style)
Mechanism: RNNoise WASM + Web Audio Worklet Pipeline.
- Objective: Filter non-vocal noise from the microphone stream in real-time.
- Architecture:
- Engine: Use
RNNoise(Recurrent Neural Network for noise suppression). It is lightweight and highly effective for speech. - Pipeline:
Mic Stream->AudioWorkletNode(Processing) ->MediaStreamDestination->Element Call.
- Engine: Use
- Implementation Steps:
- WASM Wrapper: Compile the
RNNoiseC library to WebAssembly. Use a library likernnoise-wasmornoise-suppression-js. - Audio Worklet: Create
src/app/utils/audio/RnnoiseWorklet.ts. This must handle 480-sample chunks (10ms of audio at 48kHz), which is the standard frame size for RNNoise. - Client Integration:
- In
CallControl.ts, intercept thelocalStream. - Pass the stream through the Worklet.
- Crucially, you must ensure that the processed stream is used by the
RTCPeerConnectionwithin the Element Call iframe.
- In
- WASM Wrapper: Compile the
P5-31 · Granular Voice & Screenshare Quality Controls
Mechanism: WebRTC Encoding Parameters + Backend Quality Guard.
-
Objective: Per-room and per-user control over audio fidelity and screenshare smoothness.
-
Architecture:
-
State Event:
io.lotus.room_quality(state key"") containing:{ "audio_bitrate": 128000, "screen_max_res": "1080p", "screen_max_fps": 60 } -
Client-Side (RoomInput / CallControl):
- Screenshare: In
src/app/plugins/call/CallControl.ts, when initiating screenshare, map the "Quality" setting togetDisplayMediaconstraints:
const constraints = { video: { width: { ideal: 1920 }, // 1080p frameRate: { ideal: 60 }, }, };- Audio Bitrate: After the call joins, find the
RTCRtpSenderfor the audio track and update parameters:
const sender = peerConnection.getSenders().find((s) => s.track?.kind === 'audio'); const params = sender.getParameters(); params.encodings[0].maxBitrate = roomBitrate || 128000; await sender.setParameters(params); - Screenshare: In
-
Backend Sidecar (The "Quality Guard"):
- Pattern: Extend the
voice-limit-guard.py(on LXC 151) to handle quality metadata. - Mechanism: When a user requests a LiveKit JWT to join a room, the Guard fetches the
io.lotus.room_qualityevent for that room via the Synapse Admin API. - Enforcement: The Guard injects these limits into the LiveKit token claims (if supported) or simply returns them to the client as an authorized "config" packet that the client must respect.
- Pattern: Extend the
-
-
Challenges:
- LiveKit Compatibility: Ensuring the SFU doesn't over-compress a high-bitrate stream from a "Pro" user.
- Network Stability: High bitrates (512kbps audio + 60fps 1080p video) require significant upstream bandwidth. Implement a "Network Warning" UI if packets are dropped.