4a401cf816
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>
215 lines
9.0 KiB
Markdown
215 lines
9.0 KiB
Markdown
# 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):**
|
|
```typescript
|
|
export const activeThreadIdAtom = atom<string | null>(null);
|
|
```
|
|
- **2. Layout (src/app/features/room/Room.tsx):**
|
|
Insert the `ThreadPanel` conditionally alongside the `RoomTimeline`.
|
|
```tsx
|
|
{
|
|
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 `Header` with a "Close" button that sets `activeThreadIdAtom` to `null`.
|
|
- Reuse `RoomTimeline` but pass a filtered `EventTimelineSet`.
|
|
- **Pro Tip:** Use `thread.timelineSet` directly for the most accurate thread view.
|
|
|
|
---
|
|
|
|
## 🛠️ 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.
|
|
```tsx
|
|
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.css` only 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 `/login` to OAuth2 `authorization_code` flow.
|
|
- **Key Files:** `src/app/pages/auth/Login.tsx` and `src/app/hooks/useAuth.ts`.
|
|
- **Implementation:**
|
|
1. Use `oidc-client-ts` or a similar lightweight OIDC library.
|
|
2. Check for `m.authentication` in `/.well-known/matrix/client`.
|
|
3. Redirect to the MAS authorization endpoint.
|
|
4. Handle the callback in a new `OidcCallback` route and store the OIDC `refresh_token`.
|
|
|
|
---
|
|
|
|
## 🎨 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 `useEffect` that monitors theme changes:
|
|
```typescript
|
|
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 if `lotusTerminal` is `true`.
|
|
|
|
### 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:**
|
|
1. Create a `TauriUpdateFeature` component.
|
|
2. Use `useTauriUpdater()` to get the `check` function and `status`.
|
|
3. In a `useEffect`, call `check()` on mount and then on a `setInterval` (e.g., every 12 hours).
|
|
4. Watch the `status`. When it transitions to `{ state: 'available', version: '...' }`, trigger an in-app **Lotus Toast**.
|
|
5. The toast should say "Lotus Chat v[version] is available!" with an "Update" button that calls the `install()` function from the hook.
|
|
6. **Persistence:** Store the `lastCheck` timestamp in `localStorage` to ensure the background check doesn't fire redundant commands every time the user refreshes or re-opens the app.
|
|
|
|
---
|
|
|
|
## 🔊 Audio & Communications
|
|
|
|
### P5-15 · In-Call Soundboard
|
|
|
|
**Mechanism:** Local-to-Global Audio Bridge.
|
|
|
|
- **Architecture:** Use the `Web Audio API` to mix sounds into the `MediaStream` before it enters the Element Call widget.
|
|
- **Implementation:**
|
|
1. Create an `AudioContext`.
|
|
2. Create a `MediaStreamDestinationNode`.
|
|
3. Create an `AudioBufferSourceNode` for the clip.
|
|
4. Route the mic `MediaStream` and the clip source to the destination.
|
|
5. Pass the destination's `.stream` to the call bridge.
|
|
|
|
### P5-20 · Quick Reply from Browser Notification
|
|
|
|
**Mechanism:** Service Worker `notificationclick` Action.
|
|
|
|
- **1. Registration (src/sw.ts):**
|
|
```typescript
|
|
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:**
|
|
1. **Engine:** Use `RNNoise` (Recurrent Neural Network for noise suppression). It is lightweight and highly effective for speech.
|
|
2. **Pipeline:** `Mic Stream` -> `AudioWorkletNode` (Processing) -> `MediaStreamDestination` -> `Element Call`.
|
|
- **Implementation Steps:**
|
|
1. **WASM Wrapper:** Compile the `RNNoise` C library to WebAssembly. Use a library like `rnnoise-wasm` or `noise-suppression-js`.
|
|
2. **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.
|
|
3. **Client Integration:**
|
|
- In `CallControl.ts`, intercept the `localStream`.
|
|
- Pass the stream through the Worklet.
|
|
- Crucially, you must ensure that the processed stream is used by the `RTCPeerConnection` within the Element Call iframe.
|
|
|
|
### 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:**
|
|
1. **State Event:** `io.lotus.room_quality` (state key `""`) containing:
|
|
```json
|
|
{
|
|
"audio_bitrate": 128000,
|
|
"screen_max_res": "1080p",
|
|
"screen_max_fps": 60
|
|
}
|
|
```
|
|
2. **Client-Side (RoomInput / CallControl):**
|
|
- **Screenshare:** In `src/app/plugins/call/CallControl.ts`, when initiating screenshare, map the "Quality" setting to `getDisplayMedia` constraints:
|
|
|
|
```typescript
|
|
const constraints = {
|
|
video: {
|
|
width: { ideal: 1920 }, // 1080p
|
|
frameRate: { ideal: 60 },
|
|
},
|
|
};
|
|
```
|
|
|
|
- **Audio Bitrate:** After the call joins, find the `RTCRtpSender` for the audio track and update parameters:
|
|
|
|
```typescript
|
|
const sender = peerConnection.getSenders().find((s) => s.track?.kind === 'audio');
|
|
const params = sender.getParameters();
|
|
params.encodings[0].maxBitrate = roomBitrate || 128000;
|
|
await sender.setParameters(params);
|
|
```
|
|
|
|
3. **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_quality` event 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.
|
|
|
|
- **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.
|