# Lotus Chat — Feature Reference Everything added to Lotus Chat beyond upstream Cinny v4.12.1. Last updated: June 2026. --- ## Table of Contents 1. [Branding & Identity](#branding--identity) 2. [LotusGuild Terminal Design System (TDS) v1.2](#lotusguild-terminal-design-system-tds-v12) 3. [Animated Chat Backgrounds (P5-4)](#animated-chat-backgrounds-p5-4) 4. [Seasonal Theme Overlays (P5-12)](#seasonal-theme-overlays-p5-12) 5. [Avatar Decorations (P5-13/P5-14)](#avatar-decorations-p5-13p5-14) 6. [Glassmorphism Sidebar (P5-3)](#glassmorphism-sidebar-p5-3) 7. [Night Light / Blue Light Filter (P5-5)](#night-light--blue-light-filter-p5-5) 8. [Voice / Video Call Improvements](#voice--video-call-improvements) 9. [Per-Message Read Receipts](#per-message-read-receipts) 10. [Delivery Status Indicators](#delivery-status-indicators) 11. [Messaging Enhancements](#messaging-enhancements) 12. [Presence](#presence) 13. [UX & Composer](#ux--composer) 14. [Room Customization](#room-customization) 15. [Moderation](#moderation) 16. [Notifications](#notifications) 17. [Server Integration](#server-integration) 18. [Infrastructure](#infrastructure) 19. [Key Custom Files](#key-custom-files) --- ## Branding & Identity - Package renamed to `lotus-chat`; description updated in `package.json` - App title set to "Lotus Chat" throughout (window title, meta tags, manifest) - Favicon, PWA icons (all sizes), and Apple touch icons replaced with `Lotus.png` - Lotus logo displayed in the About dialog and Auth page - Auth footer: dynamic version pulled from `package.json`; links to `lotusguild.org`, `chat.lotusguild.org`, and `matrix.lotusguild.org` - Welcome page tagline: "A Matrix client for Lotus Guild" - Encryption key export filename changed to `lotus-keys.txt` - `manifest.json` updated with Lotus name, description, and branding colors --- ## LotusGuild Terminal Design System (TDS) v1.2 ### Dark Mode — `LotusTerminalTheme` A CRT terminal aesthetic applied globally when the TDS theme is active. **Visual effects:** - Scanline overlay via repeating `linear-gradient` pseudo-element - Vignette via radial-gradient overlay - Phosphor glow on text and accents via `text-shadow` / `box-shadow` **Color palette:** | Token | Value | Role | |---|---|---| | `--lt-bg` | `#030508` | Page/panel background | | `--lt-accent-orange` | `#FF6B00` | Primary accent | | `--lt-accent-cyan` | `#00D4FF` | Secondary accent | | `--lt-accent-green` | `#00FF88` | Success / active states | | `--lt-text` | `#c4d9ee` | Body text | **Typography & chrome:** - Monospace font stack applied to all UI elements - Terminal-style scrollbars (thin, accent-colored track) **Decorative patterns:** - Custom hex-grid CSS background pattern - Circuit-board CSS background pattern (switchable) **Boot sequence:** - Matrix-style boot messages on the welcome page; press Escape to skip - Implemented in `src/lotus-boot.ts` **CSS variable family:** all custom tokens use the `--lt-*` prefix, defined in `src/lotus-terminal.css.ts`. ### Light Mode — `LotusTerminalLightTheme` A full light-palette counterpart to the dark TDS theme. **Color palette:** | Token | Light Value | Role | |---|---|---| | `--lt-bg` | `#edf0f5` | Page/panel background | | `--lt-accent-orange` | `#c44e00` | Primary accent | | `--lt-accent-cyan` | `#0062b8` | Secondary accent | | `--lt-accent-green` | `#006d35` | Success / active states | | `--lt-text` | `#111827` | Body text | **Differences from dark mode:** - CRT effects (scanlines, vignette, phosphor glow) are disabled - Scoped to `html[data-theme="light"] body.lotusTerminalBodyClass` to avoid bleed into non-TDS themes - `ThemeManager.tsx` is responsible for setting the `data-theme` attribute on the `` element when theme changes ### Chat Background Patterns (20+ static) A library of CSS-only background patterns for the chat area, all using CSS custom properties so they adapt automatically to both TDS dark and light palettes: - Blueprint grid - Carbon fiber - Starfield - Topographic contours - Herringbone - Crosshatch - Chevron - Polka dots - Triangles - Plaid - (and additional variants) --- ## Animated Chat Backgrounds (P5-4) Five CSS-only animated wallpapers implemented with vanilla-extract keyframes. No `` element is used — all animation is pure CSS. ### Available Animations **Digital Rain** Two-layer vertical stripe scroll with parallax effect. Wide stripes animate at 8s, narrow stripes at 4s, creating depth. **Star Drift** Three-layer radial-gradient dot field drifting diagonally across the viewport. Each layer moves at a distinct speed and angle. **Grid Pulse** Neon grid lines that expand and contract via a `backgroundSize` keyframe. Grid color follows `--lt-accent-cyan` in dark mode. **Aurora Flow** Four radial-gradient ellipses sweeping across a 200% canvas. Colors use the TDS green/cyan/orange palette and blend softly. **Fireflies** Three layers of warm glowing dots that drift slowly. Dot color follows `--lt-accent-orange` for a warm bioluminescent feel. ### API ```ts getChatBg(bg: ChatBg, isDark: boolean, pauseAnimations?: boolean): CSSProperties ``` Strips all `animation` properties from the returned style object when either `pauseAnimations` is `true` or the `prefers-reduced-motion: reduce` media query is active. ### Settings Integration A "Pause Background Animations" toggle is exposed in **Settings → Appearance**. The preference is persisted and read by `getChatBg()` at render time. ### Animation Improvements (June 2026) All five animated backgrounds were rewritten for smoother, more organic motion: - **Digital Rain** — added a phosphor glow flicker (`animRainGlowKeyframe`, 2.1 s) layered on top of the column scroll; stripe opacity increased for better visibility - **Star Drift** — each of the three dot layers now moves by exactly its own tile width/height per cycle (`−130 px`, `−190 px`, `−260 px`), eliminating the visible seam on loop - **Grid Pulse** — independent brightness oscillation (`animGridBrightnessKeyframe`, 3.3 s) runs alongside the size breathe (4 s) at a prime period ratio so they never synchronise - **Aurora Flow** — four gradient layers now have individual `backgroundSize` values (`200%`, `250%`, `300%`, `220%`); the keyframe drives each layer through a distinct 5-stop path, replacing the robotic single back-and-forth - **Fireflies** — glow pulse (`animFirefliesGlowKeyframe`, 2.3 s `filter: brightness`) and opacity blink (`animFirefliesBlinkKeyframe`, 1.7 s) added on top of the position drift; prime periods create unsynchronised bioluminescence ### Files - `src/app/styles/Animations.css.ts` — vanilla-extract keyframe definitions - `src/app/features/lotus/chatBackground.ts` — `getChatBg()` implementation and pattern registry --- ## Seasonal Theme Overlays (P5-12) Decorative CSS-only overlays that activate automatically on holidays and events. Manually overrideable in **Settings → Appearance → Seasonal Theme**. ### Themes | Theme | Window | Effect | |---|---|---| | 🎆 New Year | Dec 31–Jan 2 | Radial firework bursts in gold, red, cyan, purple; gold shimmer sweep | | 🏮 Lunar New Year | Jan 22–Feb 5 | Floating paper lanterns bobbing; silk texture; gold shimmer accent | | 💖 Valentine's Day | Feb 10–15 | ♥ hearts floating upward; soft pink ambient glow | | 🍀 St. Patrick's Day | Mar 15–18 | ☘ clovers drifting down; gold metallic shimmer top border | | 🃏 April Fool's | Apr 1 | Glitch overlay: RGB channel separation, hue-rotate spikes, scanline sweep, "SIGNAL LOST" watermark | | 🌱 Earth Day | Apr 20–23 | 🌿🍃 leaf emoji drift; sage green ambient tint; vine accent on left edge | | 🍂 Autumn | Sep 21–Oct 31 | Warm orange/amber leaf shapes rotating and falling | | 👾 Arcade Day | Sep 12 | CRT scanlines; blinking pixel corner decorations; "INSERT COIN" prompt | | 🚀 Deep Space Week | Oct 4–10 | Warp-speed star streaks radiating from screen centre; nebula purple/blue ambient | | 🎃 Halloween | Oct 15–Nov 1 | Purple and orange glowing particles; SVG spider web in top-left corner; dark purple tint | | ❄️ Christmas | Dec 10–Jan 2 | White dot snowfall in multiple layers at varied speeds | ### Implementation - `SeasonalEffect` component mounted in `App.tsx` at `z-index: 9997` (below night light, above content) - Auto-detection via `getActiveSeason(now: Date)` — themes checked in priority order (New Year > Valentine's > … > Autumn) - `seasonalThemeOverride` setting: `'auto' | 'off' | ` — persisted in `settingsAtom` - All particle animations gated on `prefers-reduced-motion: reduce` — ambient overlays (tints, textures, shimmer) remain active ### Files - `src/app/components/seasonal/SeasonalEffect.tsx` — theme detection, date ranges, all overlay components - `src/app/components/seasonal/Seasonal.css.ts` — vanilla-extract keyframes (fall, leaf, float-up, bob, glitch, burst, warp, scanline, shimmer, etc.) --- ## Avatar Decorations (P5-13/P5-14) Animated APNG overlay frames that float around user avatars, inspired by Discord's Avatar Decorations feature. Each decoration extends 8px beyond the avatar border on all sides, with a transparent center hole that reveals the avatar beneath. Other Lotus Chat users see your selected decoration in real time — stored in the Matrix profile via MSC4133. ### Decoration Library 99 hand-curated, original-IP decorations (no licensed character artwork) organized into 9 categories: | Category | Count | Highlights | |---|---|---| | Gaming | 13 | Slither 'n Snack, Joystick, Space Invaders, Gaming Headsets | | Cyber | 9 | Cybernetic, Glitch, Digital Sunrise, Futuristic UI (3 colors) | | Space | 8 | Black Hole, Constellations, Solar Orbit, Aurora | | Fantasy | 22 | Kitsune, Phoenix, Glowing Runes, D&D dice, Crystal Balls | | Elements | 7 | Fire, Water, Air, Earth, Lightning, Ki Energy | | Japanese | 6 | Kabuto, Oni Mask, Sakura Warrior, Straw Hat | | Nature | 12 | **Lotus Flower**, Koi Pond, Sakura, Fall Leaves, Fireflies | | Spooky | 13 | Candlelight, Witch Hat, Ghosts, Jack-o'-Lantern | | Cozy | 11 | Cozy Cat, Fox Hat (3 colors), Cat Ears, Frog Hat | All decoration files are 256×256 APNGs. They animate natively in all modern browsers via `` elements. ### Architecture **Profile storage — MSC4133:** Decoration preference is stored in the public Matrix profile field `io.lotus.avatar_decoration` (a slug string, e.g. `lotus_flower`). Any Lotus Chat user viewing your profile sees your current decoration. **CDN:** Files are self-hosted on the Lotus Nextcloud instance. Direct access: `https://drive.lotusguild.org/public.php/dav/files/{token}/cinny-decorations/{slug}.png`. `` elements load cross-origin freely — no CORS headers needed. **Module-level cache with in-flight deduplication:** `useAvatarDecoration(userId)` fetches the profile field once per user per session. A `Map` cache prevents redundant requests; a second `pending` waiters map ensures multiple components requesting the same userId simultaneously share one HTTP request rather than firing duplicates. **Wrapping pattern:** `AvatarDecoration` renders a `position: relative; display: inline-flex` wrapper div. The decoration `` is `position: absolute` with `top/left/right/bottom: -8px`, extending equally on all sides while the `z-index: 10` keeps it above the avatar. `onError` hides the image if the CDN file is absent. This wrapper sits outside `PresenceRingAvatar` so the presence ring and decoration layer are fully independent. ### Placement — Where Decorations Render | Location | File | |---|---| | Message timeline | `src/app/features/room/message/Message.tsx` | | Members drawer | `src/app/features/room/MembersDrawer.tsx` | | `@mention` autocomplete | `src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx` | | Inbox / notifications | `src/app/pages/client/inbox/Notifications.tsx` | ### Settings — Decoration Picker **Settings → Account → Avatar Decoration** shows a scrollable grid of all decorations, grouped by category. Each cell is a 52×52px button with a live preview of the APNG. The currently selected decoration gets a 2px cyan border. "No Decoration" clears the field. Changes are saved only when the "Save" button is clicked (visible only when a change is pending). After save, `invalidateDecorationCache(userId)` forces other components to re-fetch. ### Catalog Sync Script After deleting decoration files from the Nextcloud share, run: ```bash npm run sync:decorations ``` The script (`scripts/syncDecorations.mjs`) sends HTTP HEAD requests to the CDN URL for every slug in `avatarDecorations.ts` and automatically removes entries for files that returned 404. Empty categories are pruned automatically. Review with `git diff`. ### Files - `src/app/features/lotus/avatarDecorations.ts` — full catalog (`DECORATION_CATEGORIES`, `ALL_DECORATIONS`, `decorationUrl()`, `DECORATION_CDN`) - `src/app/hooks/useAvatarDecoration.ts` — profile fetch, module-level cache, `invalidateDecorationCache()` - `src/app/components/avatar-decoration/AvatarDecoration.tsx` — wrapper component with APNG overlay - `src/app/features/settings/account/ProfileDecoration.tsx` — settings UI (picker grid, save button) - `scripts/syncDecorations.mjs` — CDN sync script to prune deleted decorations from the catalog --- ## Glassmorphism Sidebar (P5-3) An optional frosted-glass sidebar style toggled in **Settings → Appearance**. **Implementation:** - `SidebarGlass` vanilla-extract class applies `background: rgba(3, 5, 8, 0.55)` and `backdropFilter: blur(12px)` to the sidebar element - `SidebarNav.tsx` uses a `useEffect` to mirror the active chat background onto `document.body` when the glassmorphism setting is enabled, so the blur filter has meaningful content to work through - Degrades gracefully on browsers without `backdrop-filter` support (falls back to the semi-transparent background) --- ## Night Light / Blue Light Filter (P5-5) A warm orange overlay rendered over the entire UI to reduce blue light emission. **Implementation:** - `NightLightOverlay` component mounted directly in `App.tsx` - CSS: `position: fixed; inset: 0; pointer-events: none; z-index: 9998` - Orange tint color with configurable opacity - **Controls:** Toggle to enable/disable + intensity slider ranging from 5% to 80% opacity - Settings persisted via the standard Lotus settings store --- ## Font Selector (P5-22) Users can choose the UI font in **Settings → Appearance**: - **System Default** — `system-ui, -apple-system, sans-serif` - **Inter** — `'InterVariable', sans-serif` (current default) - **JetBrains Mono** — `'JetBrains Mono', monospace` (already loaded from Google Fonts) - **Fira Code** — `'Fira Code', monospace` (added to Google Fonts preload in `index.html`) Applied by overriding `--font-secondary` on `document.body` via `AppearanceEffects` in `App.tsx`. The TDS terminal mode font stack is unaffected. --- ## Custom @Mention Highlight Color (P5-21) Users can set a custom background color for `@mention` chips that highlight their own name, in **Settings → Appearance**. - Color picker (native ``) with a **Reset** button to revert to the theme default - Text color (black/white) auto-computed from the chosen background's luminance for readability - Applied via CSS custom properties `--mention-highlight-bg`, `--mention-highlight-text`, `--mention-highlight-border` set on `document.body` - `CustomHtml.css.ts` uses these as CSS `var()` fallbacks over the original folds `Success` token colors --- ## Voice / Video Call Improvements ### Element Call Upgrade Upgraded embedded Element Call widget from **0.16.3** to **0.19.4**. ### Camera Default Off Camera starts disabled on join. The `cameraOnJoin` setting is explicitly opt-in and is not persisted to `localStorage` between sessions, preventing the previous behavior where a prior-session preference could unexpectedly enable the camera. ### UI Fixes - **Deafen button** — corrected tooltip text that was previously swapped with the mute tooltip - **Screenshare confirmation** — a confirmation dialog is shown before initiating a screenshare broadcast to the room - **Auto-revert spotlight on screenshare** — removed; the 600ms grid-revert click was causing fullscreen screenshare to show avatar tiles instead of the screen. EC now handles layout natively. ### Push to Talk (PTT) - Configurable keybind; defaults to `Space` - Visual indicator shown in both TDS and non-TDS themes while PTT is active - Event listener attached to both the main window and the Element Call iframe's `contentWindow` to ensure reliable capture regardless of focus - Mic state is preserved correctly when switching into or out of PTT mode ### Push to Deafen `M` key triggers `toggleSound()` in `CallControls.tsx`, toggling the deafen state without requiring a mouse click. ### AFK Auto-Mute in Voice (P5-11) Automatically mutes the microphone after a configurable period of microphone-on silence. **Implementation:** - `useAfkAutoMute(callEmbed)` hook opens a separate monitoring-only `getUserMedia` stream (independent of Element Call's stream) and analyzes it via `AudioContext` + `AnalyserNode` - RMS level is sampled every 500ms; if it stays below threshold while the mic is on, the silence timer starts - After the configured timeout (`afkTimeoutMinutes` setting), `callEmbed.control.setMicrophone(false)` mutes the mic and an in-app toast is shown - Monitoring stream and `AudioContext` are fully cleaned up on unmount (no resource leak) - Activated inside `CallControls` via `useAfkAutoMute(callEmbed)` — no changes required to `CallEmbed` or Element Call **Settings (Settings → Calls):** - **AFK Auto-Mute** toggle (default: off) - **Idle Timeout** dropdown — 1 / 5 / 10 / 20 / 30 minutes (shown only when enabled; default: 10 minutes) Hook: `src/app/hooks/useAfkAutoMute.ts` ### Voice Channel User Limit (P5-10) Room admins can cap the number of participants allowed in a room's voice call. The cap is a **hard, server-side limit enforced for every Matrix client** (Element, FluffyChat, …), backed by a client-side UX layer in Lotus Chat. **Client (this repo):** - Limit is stored in the `io.lotus.voice_limit` room state event with content `{ max_users: N }` (0 / absent = no limit) - `RoomVoiceLimit` component in Room Settings → General → **Voice** lets admins set the cap with a number input. Editing is gated by `permissions.stateEvent(StateEvent.LotusVoiceLimit, …)`, so only users with `state_default` power (or above) can change it - `CallPrescreen` (`CallView.tsx`) reads the limit reactively via `useStateEvent` and compares it against the live `useCallMembers` count; at capacity the **Join** button is disabled and a "Channel Full (N/N)" message is shown - A user already in the session (rejoining) is never blocked — only new joiners are gated Files: `src/app/features/common-settings/general/RoomVoiceLimit.tsx`, `src/app/features/call/CallView.tsx`, `StateEvent.LotusVoiceLimit` in `src/types/matrix/room.ts` **Server (the hard backstop — `matrix` repo `livekit/voice-limit-guard.py`):** - Every client must fetch a LiveKit JWT from `lk-jwt-service` before joining a call. A fail-open guard sidecar sits in front of it (guard on `:8070`, lk-jwt-service moved to `:8071`) - On each token request the guard reads the room's `io.lotus.voice_limit` (Synapse admin API), and if the room is at capacity it returns `403` so the client cannot obtain a token and therefore cannot join — regardless of which client they use - Distinct Matrix users are counted via LiveKit `ListParticipants`; rejoins / extra devices are allowed. Any failure fails open so calls never break > The client-side "Channel Full" check is UX/early-feedback; the server guard is the actual enforcement. ### Custom Join / Leave Sound Effects (P5-16) A local sound plays when another participant joins or leaves a call you're in. **Implementation:** - `useCallJoinLeaveSounds(embed)` hook (wired in `CallUtils` inside `CallEmbedProvider`) listens to `MatrixRTCSession` membership changes via `useCallMembersChange` - Membership identity is tracked by `sender|deviceId`; a snapshot is taken when the session (re)starts so participants already present never trigger a sound - Your own membership is filtered out (`mx.getSafeUserId()` prefix), and sounds fire only while you are actually joined (`useCallJoined`) - Sounds are synthesized in-browser with the Web Audio API (`OscillatorNode` + envelope) — no audio assets to bundle. Join uses a rising motif, leave a falling one - Three styles: **Chime** (sine), **Soft** (triangle), **Retro** (square arpeggio), plus **Off** **Settings (Settings → Calls):** - **Join & Leave Sounds** dropdown — Off / Chime / Soft / Retro (default: Chime). Selecting a style previews the join sound immediately Files: `src/app/utils/callSounds.ts`, `src/app/hooks/useCallJoinLeaveSounds.ts` ### Noise Suppression (3-Tier, incl. on-device ML) (P5-30) A three-way mic noise-suppression control in **Settings → General → Calls**: | Tier | What it does | |---|---| | **Off** | No suppression (`noiseSuppression=false` to Element Call). | | **Browser-native** | Element Call's built-in WebRTC suppressor (`noiseSuppression=true`). Default. | | **ML (beta)** | On-device RNNoise — Krisp-style removal of fans, keyboards, dogs, etc. | **Why a shim, not a fork:** Element Call captures the mic *inside* its iframe and publishes to LiveKit; the host can't reach that track. LiveKit's Krisp filter is LiveKit-Cloud-only (we self-host the SFU), and EC's own RNNoise work (PR #3892) is unmerged. So the **ML tier** is delivered by injecting a same-origin pre-init script into the vendored EC `index.html` that monkeypatches `getUserMedia` and routes the captured mic through an RNNoise `AudioWorklet` (`@sapphi-red/web-noise-suppressor`) before LiveKit ever sees it — the same post-capture pipeline #3892 uses, executed from the realm we already control. Works on the self-hosted LiveKit SFU, survives EC version bumps, no EC fork/AGPL/rebase burden. **How it's wired:** - `callNoiseSuppression` setting is `'off' | 'browser' | 'ml'` (legacy boolean migrates: `true`→`browser`, `false`→`off`) - `CallEmbed.getWidget()` maps the tier to the `noiseSuppression` URL param and appends `lotusDenoise=ml` for the ML tier (browser-native suppressor is disabled in ML mode so RNNoise owns suppression) - The `lotusDenoise` vite plugin copies the RNNoise worklet + wasm into `public/element-call/denoise/`, copies the shim, and injects `