# 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 (Advanced Multi-Tier) (P5-30) A comprehensive mic noise-suppression system in **Settings → General → Calls** designed for high-end hardware and detailed performance testing. | Tier | Description | | ------------------ | ----------------------------------------------------------------------------- | | **Off** | No suppression applied. | | **Browser-native** | Google NSNet2 (WebRTC built-in). Best general performance/CPU balance. | | **ML (Advanced)** | Custom ML pipeline supporting multiple models, series suppression, and gates. | **Advanced Features & Test Options:** - **Multiple ML Models:** Toggle between **RNNoise** (standard hybrid) and **Speex** (legacy DSP-based) to compare artifact levels and suppression strength. - **Series Suppression (Combination):** Optional toggle to run the browser's native stationary noise filter _before_ the ML model. This allows testing the individual performance of the ML model vs the combined effectiveness at removing fan hum. - **Noise Gate:** Configurable hardware-style gate with a dB threshold. Hard-cuts all audio when input is below the threshold, ensuring absolute silence between sentences. - **Live Microphone Meter:** A real-time volume visualizer in the settings panel to help users accurately tune their Noise Gate threshold. - **High-Fidelity Capture:** Captures at hardware native rates (supporting high-end gear like **Scarlett Solo + PodMic**) and handles high-quality resampling via Web Audio to prevent the "static" artifacts caused by low-quality browser pre-resamplers. - **Performance:** Automatic WASM SIMD detection with transparent fallback to standard binaries. - **Support Detection:** UI now detects `AudioWorklet` / `AudioContext` support and disables ML options in unsupported environments. - **Status Reporting:** The ML shim notifies the host app via `postMessage`. If initialization fails, a system toast alerts the user of the fallback to the raw microphone. **Open-Source Model Roadmap:** | Model | Transients (Clicks) | Voice Quality | CPU Usage (WASM) | | :--- | :--- | :--- | :--- | | **RNNoise** | Poor | Moderate | < 5% | | **DTLN** | Good | High | 10-20% | | **DeepFilterNet 3** | **Excellent** | **Very High** | 25-50%+ | > **Note:** DeepFilterNet 3 is planned for future inclusion in the desktop build where larger binaries and higher CPU overhead are more acceptable. ### Files - `build/lotus-denoise.js` — multi-model getUserMedia shim - `vite.config.js` — `lotusDenoise()` plugin (copies assets for RNNoise, Speex, and NoiseGate) - `src/app/plugins/call/CallEmbed.ts` — advanced tier → widget URL params - `src/app/utils/lotusDenoiseUtils.ts` — support detection and model comparison metadata - `src/app/features/settings/general/General.tsx` — advanced settings UI + mic meter ### Call Button Scoping The call button is shown only in DMs and invite-only rooms that do not have an `m.space.parent` event. It is hidden in public rooms and space channels to avoid accidental broadcast calls. ### Picture-in-Picture (PiP) - 280×158px floating window that stays on screen when navigating away from the call room - Draggable via pointer events with a 5px movement threshold to distinguish drags from clicks; touch events are supported - Clicking the PiP window navigates back to the call room - Implemented with an imperative `useEffect` style for position overrides because `useCallEmbedPlacementSync` writes geometry directly onto the DOM element, making declarative approaches unreliable ### Call Embed Positioning - Uses `getBoundingClientRect()` (viewport-relative) instead of the previous `offsetTop`/`offsetLeft` (parent-relative) calculations, fixing misalignment in scrolled layouts - Position is synced on mount via `useEffect` with a `ResizeObserver` to handle dynamic layout changes ### Dark Mode in Element Call `applyStyles()` injects `:root { color-scheme: dark | light }` into the Element Call iframe after the user joins. The theme is also updated when `setTheme()` is called, keeping the call UI in sync with the Lotus theme selection. ### Call Embed Wallpaper The active `chatBackground` pattern is applied to the `div[data-call-embed-container]` wrapper. The iframe's `html, body` are forced to `background: none !important` so the host pattern shows through. ### TDS Typing Indicator The animated typing indicator dots use `var(--lt-accent-orange)` as their color when the TDS theme is active, matching the terminal aesthetic. --- ## Per-Message Read Receipts A full per-message read receipt system showing exactly who has seen each message. ### Core Hook — `useRoomReadPositions` ```ts useRoomReadPositions(room: Room): Map ``` - Listens to `Room.localEchoUpdated` and `RoomMember.typing` events to stay reactive - Debounced 150ms to avoid excessive re-renders during rapid receipt updates - Located at `src/app/hooks/useRoomReadPositions.ts` ### `nearestRenderableId()` A utility that walks backward through the event timeline from a given event ID to find the nearest event that is actually rendered (skipping reactions, edits, and other non-display events). Used to map a user's read position to a visible message. ### `ReadPositionsContext` React context at `src/app/features/room/ReadPositionsContext.ts` that provides the positions map to all timeline components without prop drilling. ### `ReadReceiptAvatars` A pill of overlapping 24px user avatars displayed at the bottom-right of each message. Shows a maximum of 5 avatars; additional readers are shown as an overflow count (e.g., `+3`). ### "Seen by" Modal — `EventReaders` Clicking the avatar pill opens a modal (`src/app/components/event-readers/EventReaders.tsx`) listing: - User avatar - Display name - Formatted timestamp of when the receipt was recorded - Respects the `hour24Clock` setting for timestamp formatting --- ## Delivery Status Indicators Visual feedback on message delivery state, shown on the sender's own messages: | State | Indicator | | -------------------------- | ---------------------------- | | Sending (local echo) | ⟳ rotating clock icon | | Sent (server ACK received) | ✓ checkmark | | Failed | ✕ in red; orange glow in TDS | The indicator is hidden once the server confirms the event (when the internal status transitions to `null`), keeping the timeline clean for settled messages. --- ## Messaging Enhancements ### Rich Room Topics - Topic `formatted_body` is rendered via `sanitizeCustomHtml` + `html-react-parser`, supporting bold, italic, links, and other inline HTML - The room header shows a plain-text preview of the topic - Clicking the topic preview opens a full modal with the formatted body - The room settings topic editor includes a formatting toolbar with **B**, _I_, ~~S~~, and `code` buttons ### Edit History Viewer `EditHistoryModal.tsx` fetches and displays the full edit history of a message. - API: `GET /_matrix/client/v1/rooms/{roomId}/relations/{eventId}/m.replace` - E2EE fix: the "Original" entry uses `getClearContent()` to retrieve the decrypted content rather than the encrypted payload - Accessible from the message context menu ### Inline GIF Preview - Detects Giphy and Tenor share URLs via pattern matching on the message body - Renders matched URLs as `` inline in the message - URL is proxied through the Matrix `/_matrix/media/v3/preview_url` endpoint to avoid mixed-content issues ### GIF Picker - Giphy-powered picker accessible from the composer toolbar - The button is only shown when `gifApiKey` is set in `config.json` - Selected GIFs are sent as `m.image` events - Picker UI is styled with TDS variables when the TDS theme is active - Located at `src/app/components/GifPicker.tsx` ### Message Forwarding Context menu → **Forward** allows forwarding a message to any room the user is a member of. ### Draft Persistence - Composer drafts are stored in `localStorage` keyed by `roomId` - Draft is cleared on successful send - The Jotai atom is the primary source of truth; `localStorage` is only read on room mount ### Message Search Date Range - The search panel accepts `from_ts` and `to_ts` values (epoch milliseconds) passed to the search API - A chip shows the active date range with an **×** button to clear it ### Image / Video Captions Images and videos can be sent with a caption. The caption and media are sent as a single event. ### Location Sharing `m.location` events render an inline map tile using the coordinates from the event content. ### Deleted Message Placeholders Redacted events display "This message has been deleted" along with the redaction reason if one was provided, rather than leaving a blank gap in the timeline. This is a one-line change in the `eventRenderer` filter. ### Message Bookmarks - Bookmarks are stored in `io.lotus.bookmarks` account data, syncing across all devices - Maximum of 500 bookmarked entries - `BookmarksPanel.tsx` is a sidebar panel accessible from the navigation rail - Hook: `src/app/hooks/useBookmarks.ts` ### Message Scheduling - Implements MSC4140 delayed events for scheduling messages to be sent at a future time - `ScheduleMessageModal.tsx` provides the date/time picker UI - A collapsible "Scheduled" tray in the room shows all pending scheduled messages with individual cancel buttons - Utilities in `src/app/utils/scheduledMessages.ts` ### File Upload Compression (opt-in) - Implemented in `UploadCardRenderer.tsx` - Uses the Canvas API: `canvas.toBlob(callback, 'image/jpeg', 0.82)` for compression - A `Switch` toggle in the upload preview UI lets the user opt in per upload - Shows before and after file sizes so the user can evaluate the tradeoff - Works on all image types except SVG (which cannot be drawn to canvas) - On send, the original uncompressed MXC URL is deleted from the media store - On cancel, any orphaned MXC from a prior upload is cleaned up via `tryDeleteMxcContent()` ### Richer URL Preview Cards `UrlPreviewCard.tsx` implements 13 domain-specific card layouts: | Domain | Layout | | -------------- | ---------------------------------------------------- | | YouTube | Thumbnail, title, channel, duration | | Vimeo | Thumbnail, title, author | | GitHub | Repo name, description, stars/forks/language | | Twitter / X | Avatar, display name, handle, tweet body | | Reddit | Subreddit, post title, score, comment count | | Spotify | Album art, track/album/playlist name, artist | | Twitch | Stream thumbnail, streamer, game, viewer count | | Steam | Header image, game name, price, rating | | Wikipedia | Article title, extract excerpt | | Discord | Server name, invite metadata | | npm | Package name, version, description, weekly downloads | | Stack Overflow | Question title, vote/answer count, tags | | IMDb | Poster, title, year, rating | Generic (non-domain-specific) cards display a Google S2 favicon. Empty or unparseable preview responses are suppressed entirely rather than showing a blank card. ### Poll Creation - `PollCreator.tsx` creates stable `m.poll.start` events - Supports 2 to 10 answer options - Supports both single-choice and multiple-choice modes - Accessible via the `Icons.OrderList` button in the composer toolbar ### Poll Display `PollContent.tsx` renders polls in read-only mode. Handles both the stable `m.poll` format and the legacy MSC3381 unstable `org.matrix.msc3381.poll.start` format. Displays current vote counts and a note directing users to Element to cast votes. ### Voice Message Playback Speed `AudioContent.tsx` adds a playback speed cycle button to voice message players. Available speeds: `[0.75, 1, 1.5, 2]×`. A `useEffect` sets `audioElement.playbackRate` whenever the speed selection changes. --- ## Presence ### Discord-Style Presence Selector A presence status selector in the user panel offering five modes: | Mode | Matrix broadcast | | -------------- | ----------------------------------- | | Online | `online` | | Idle | `unavailable` | | Do Not Disturb | `unavailable` + `status_msg: 'dnd'` | | Invisible | `offline` | | Auto | Standard Matrix presence lifecycle | - Selection persists via the `presenceStatus` setting - `usePresenceUpdater` short-circuits its automatic presence updates when a manual mode (anything other than Auto) is selected ### Custom Status Message - Up to 64 characters of free text plus an emoji - Optional auto-clear timer with presets: 30 minutes, 1 hour, 4 hours, 1 day, 3 days, 7 days - Status is broadcast via `mx.setPresence({ status_msg: ... })` - Character counter appears at 56/64 characters remaining to warn of the limit ### Presence Badges `PresenceBadge` component displays a colored dot indicating presence state. Used in: - Members drawer - User settings panel ### Presence Avatar Ring `PresenceRingAvatar` wraps any avatar component using `React.cloneElement` to inject an `outline: 2px solid` ring whose color maps to the user's presence state. `outlineOffset: 2px` ensures the ring sits cleanly outside the avatar regardless of the avatar's `border-radius`. Applied in: - Message timeline - Members drawer - `@mention` autocomplete dropdown - Inbox / notifications panel ### Status Revert Bug Fix (June 2026) `usePresenceUpdater` previously captured the user's custom status message once via `localStorage.getItem` at effect initialization. When the user changed their status message in Profile Settings, subsequent automatic transitions back to `online` (e.g., returning from idle) would silently broadcast the old status message, reverting the custom status. Fixed by replacing the single read with a `readStatus()` function called inside every `setOnline` and `setUnavailable` invocation, so the current localStorage value is always used. ### Document Title Unread Count The browser tab title updates to reflect unread state: - `(N) Lotus Chat` — N unread messages - `· Lotus Chat` — unread activity without a specific count - `Lotus Chat` — no unread items ### Extended Profile Fields Supports MSC4133 custom profile fields via `PUT /_matrix/client/unstable/uk.tcpip.msc4133/{userId}/{field}`: - `m.pronouns` — displayed in profile panels - `m.tz` — IANA timezone string (e.g., `America/New_York`) Hook: `src/app/hooks/useExtendedProfile.ts` ### User Local Time When a user has `m.tz` set in their profile: - Their profile panel shows a clock icon, their current local time, and the timezone abbreviation - The displayed time updates every 60 seconds - Respects the global `hour24Clock` setting for 12h/24h formatting Hook: `src/app/hooks/useLocalTime.ts` ### User-to-User Private Notes (P5-34) A private text note on any user's profile, visible only to the logged-in user and synced across all their devices. - Textarea in the user profile popout (below device sessions), shown only when viewing another user — never on your own profile - Auto-saves 800 ms after the last keystroke with a "Saving…" indicator - Character counter appears when fewer than 100 characters remain (max 500) - Stored in `io.lotus.user_notes` account data as `{ [userId]: string }` — deletes the key when the note is cleared - Reactive: updates immediately if account data arrives from another device mid-session Hook: `src/app/hooks/useUserNotes.ts` --- ## UX & Composer ### Message Length Counter A character count indicator is shown in the composer when `charCount > 0`. The counter resets to zero when switching rooms. ### Quick Emoji Reactions on Hover A hover toolbar appears over messages, showing the 3 most recently used emojis (sourced from `ElementRecentEmoji` account data) as one-click reaction buttons. The buttons are positioned between the emoji-board button and the Reply button. Clicking a quick reaction closes any open emoji picker. ### In-App Notification Toasts `LotusToastContainer.tsx` displays rich in-app notification toasts with: - 24px sender avatar - Sender display name - Message body preview - Room name - × dismiss button - 4-second auto-dismiss timeout - Slide-in animation via `@keyframes lotusToastIn` OS-level notifications are unchanged and still fire when the window is not focused. ### Collapsible Long Messages Messages exceeding a configurable line threshold are truncated with a "Show more" toggle. - Default threshold: 20 lines - Threshold is configurable in **Settings → Appearance** - Uses CSS `max-height` + `overflow: hidden` with a smooth transition - Transition is disabled when `prefers-reduced-motion: reduce` is active ### Message Send Animation A subtle animation plays on the sender's own messages as they appear in the timeline: - `transform: scale(0.97) → scale(1)` combined with `opacity: 0.4 → 1` - Duration: 0.15s, ease-out - Only applied to the current user's outgoing messages - Disabled when `prefers-reduced-motion: reduce` is active ### Right-Click Room Context Menu Right-clicking a room in the sidebar opens a context menu with: - **Mute** with a duration submenu: 15 minutes, 1 hour, 8 hours, 24 hours, Indefinite - **Copy Room Link** — copies the `matrix.to` URI to clipboard - **Mark as Read** — marks all events in the room as read - **Leave Room** — with a confirmation step - **Room Settings** — opens the room settings panel ### Unverified Device Warning - Controlled by the `warnOnUnverifiedDevices` setting (off by default) - When enabled, a `Warning.Container` banner is displayed above the composer in encrypted rooms that have unverified device sessions - The warning is informational only and never blocks sending ### Sidebar Room Filter A text input at the top of the room list filters rooms by display name in real time. The filter is cleared automatically when switching sidebar tabs. ### DM Last Message Preview Direct message entries in the sidebar show: - A 48-character truncated preview of the last message body - A relative timestamp (e.g., "2m ago") - Reactivity via `useRoomLatestRenderedEvent` - Encrypted messages show "Encrypted message" if decryption fails; successfully decrypted messages show their plaintext preview ### Room Sort Order The room list sort order can be configured in **Settings → Appearance**: - Recent Activity (default) - A → Z (alphabetical) - Unread First Persists via the `homeRoomSort` setting. ### Favorite Rooms - Rooms can be favorited via the `m.favourite` tag (standard Matrix tag) - A "Favorites" section appears above the main room list when any rooms are favorited - Favorited rooms display a star indicator in the sidebar - Favorites sync across all devices via account data ### Invite Link + QR Code `RoomShareInvite.tsx` provides a shareable invite UI: - 160×160px QR code generated via `api.qrserver.com` - "Copy Link" button to copy the `matrix.to` URI - Also accessible via a toggle button (⊞) in the Invite modal ### Private Read Receipts A toggle in **Settings → Privacy** switches between sending `m.read` (public receipts) and `m.read.private` (private receipts visible only to the sender and the server). ### Media Gallery `MediaGallery.tsx` — a right-side drawer for browsing room media. - Three tabs: **Images**, **Videos**, **Files** - Reads already-decrypted events from the room timeline - Encrypted images show a lock placeholder rather than an error - "Load More" button triggers `mx.paginateEventTimeline()` to fetch older media ### Knock-to-Join - `RoomIntro.tsx` shows a "Request to Join" button for knock-restricted rooms - Clicking sends `mx.knockRoom(roomId)` with an optional reason - The members drawer shows a "Pending Requests" section for room admins, listing users who have knocked ### Knock-to-Join Notifications for Admins (P4-3) Room and space admins are notified in real time when users knock on a restricted room. - `usePendingKnocks(room)` hook listens to `RoomMemberEvent.Membership` events and returns all members currently in the `knock` state - Power level check: only shown to users with sufficient invite-level permissions (`usePowerLevelsContext()`) - **Members button badge:** when knocks are pending, a `Warning`-variant solid `Badge` overlays the Members button in the room header showing the pending count - Badge is `aria-hidden`; the Members button `aria-label` is updated to announce the count for screen readers Hook: `src/app/hooks/usePendingKnocks.ts` ### Code Syntax Highlighting (TDS) `syntaxHighlight.ts` provides TDS-aware syntax highlighting using inline styles derived from `--lt-accent-*` CSS variables. Supported languages: JavaScript, TypeScript, JSX, TSX, Python, Rust. Falls back to ReactPrism for unsupported languages. ### Room Emoji Prefix A leading emoji in a room name is rendered at 1.15× size in the sidebar for visual hierarchy. An emoji picker button (😊) is added to all room name input fields, prepending the selected emoji to the room name. ### Configurable Composer Toolbar (P3-6) Users can individually show or hide each composer toolbar button in **Settings → Editor → Composer Toolbar Buttons**: - Format Toggle, Emoji, Sticker, GIF, Location, Poll, Voice Message, Schedule Message - All default to **on** — no visible change for existing users - New buttons added in future will also default to on (deep-merge in `getSettings`) - Send and Attach File buttons are not hideable - Sticker still respects the existing `width < 500px` auto-hide on top of the setting --- ## Room Customization ### Personal Room Name Overrides - Users can set a personal display name for any room, stored in `io.lotus.room_names` account data - A pencil indicator in the sidebar shows rooms with active overrides - Overrides sync across all devices - Applied consistently in: room header, sidebar, room intro, and call overlay ### Export Room History `ExportRoomHistory.tsx` exports a room's message history in three formats: - **Plain Text** — human-readable transcript - **JSON** — raw event data - **HTML** — styled, self-contained page Features: - Optional date range filter - Progress indicator during export - Uses `mx.paginateEventTimeline()` to retrieve history in chunks - E2EE-aware: exports decrypted content for rooms the user has keys for ### Room Activity / Mod Log `RoomActivityLog.tsx` provides a searchable log of administrative events in the room: - Member join/leave/kick/ban events - Power level changes - Room name, topic, avatar, and ACL changes - Human-readable descriptions for each event type - Type filter to narrow results - "Load More" button for pagination ### Server ACL Editor `RoomServerACL.tsx` — UI for editing `m.room.server_acl` state events. - Separate allow and deny server lists - Wildcard validation (e.g., `*.example.com`) - "Allow IP literal addresses" toggle - Read-only for users without admin power level ### Room Stats / Insights `RoomInsights.tsx` (not shown by default; accessible as a non-default tab in room settings). - Top 5 most active members (bar chart by message count) - Top 5 most-used reactions - Media breakdown by type (images, videos, files) - 24-hour activity heatmap showing message volume by hour of day --- ## Moderation ### Report Room `ReportRoomModal.tsx` provides a UI for reporting a room to the homeserver. - API: `POST /_matrix/client/v3/rooms/{roomId}/report` - Category dropdown for classifying the report - Modal auto-closes 1.5 seconds after a successful submission - Hidden for rooms the user owns and for server notice rooms ### Policy List / Ban List Viewer Accessible via **Room/Space Settings → Policy Lists** (admin only). - Displays the room's subscribed policy lists in read-only format - Subscribe (join) and unsubscribe (leave) controls for each list - Enforcement is delegated to Draupnir or equivalent tooling; Lotus only manages list membership --- ## Notifications ### Custom Notification Sounds - `messageSoundId` and `inviteSoundId` settings select from a predefined map - Sound options defined in `notificationSounds.ts` as `NOTIFICATION_SOUND_MAP` - Preview (▶) buttons in the settings panel let users audition sounds before selecting - Separate sound selection for: `notification`, `invite`, `call`, `none` ### Notification Quiet Hours - Controlled by `quietHoursEnabled`, `quietHoursStart`, and `quietHoursEnd` settings - Correctly handles overnight spans (e.g., 22:00 → 07:00) - Gates both `notify()` (visual/OS notifications) and `playSound()` (audio alerts) - When active, notifications are silently dropped rather than queued ### Full Push Rule Editor A complete UI for managing Matrix push notification rules: - Displays rules by kind: `override`, `room`, `sender`, `underride` - Enable/disable toggle per rule - Delete button for removable rules - Add-rule form for creating new `room` and `sender` rules ### Notification Profile Presets (P5-27) Three one-tap presets at the top of **Settings → Notifications** that apply a group of notification settings atomically: - **Gaming 🎮** — notifications on, all sounds off (`messageSoundId: none`, `inviteSoundId: none`) - **Work 💼** — all notifications and sounds on (restores defaults) - **Sleep 🌙** — all notifications off (`showNotifications: false`, sounds off) --- ## Server Integration ### Server Support Contact - Fetches `/.well-known/matrix/support` on the user's homeserver - Contact information is displayed in **Settings → Help & About** - Styled with TDS cyan when the TDS theme is active - Degrades gracefully with no error shown on 404 responses ### Server Notices `m.server_notice` rooms receive special treatment: - A `Chip variant="Warning"` badge reading "Server Notice" is shown in the room header - The composer is read-only (no message input) - Invite, Report Room, and Room Settings menu items are hidden --- ## Infrastructure ### Authenticated Media `mxcUrlToHttp()` calls now use the correct argument order for MSC3916 authenticated media: ```ts mxcUrlToHttp(mx, mxcUrl, useAuthentication, width, height, 'crop'); ``` The `useAuthentication` parameter was previously mispositioned, causing unauthenticated requests to be sent for media in rooms that required authentication. ### Upstream Tracking - An `upstream` git remote pointing to `github.com/cinnyapp/cinny` is maintained for merge tracking - A daily divergence check runs via `cinny-upstream-check.sh` on LXC 106 - This enables prompt review of upstream security patches and feature releases ### Rolldown CJS Interop — millify `src/app/plugins/millify.ts` re-exports `millify` as a named import to bypass the `__toESM` interop bug in the Rolldown bundler, which caused the default export of CJS modules to be undefined at runtime. ### Sentry Noise Filter `ignoreErrors: ['Request timed out']` is added to `Sentry.init()` to suppress a high-volume, low-signal error caused by transient network conditions. This keeps the Sentry issue queue focused on actionable errors. ### URL Preview Default in Encrypted Rooms The `encUrlPreview` setting defaults to `true` rather than `false`. A security advisory chip in **Settings → Privacy** explains the tradeoff (the homeserver can see which URLs are being previewed) so users can make an informed choice. --- ## Key Custom Files | File | Purpose | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `src/lotus-terminal.css.ts` | TDS CSS variable definitions, scanline/vignette effects, dark + light palette tokens | | `src/lotus-boot.ts` | Matrix-style boot sequence animation on the welcome page | | `src/app/hooks/useRoomReadPositions.ts` | Reactive hook returning `Map` for per-message read receipts | | `src/app/features/room/ReadPositionsContext.ts` | React context providing read positions map to timeline components | | `src/app/components/read-receipt-avatars/` | `ReadReceiptAvatars` component — overlapping avatar pill with overflow count | | `src/app/components/event-readers/EventReaders.tsx` | "Seen by" modal listing readers with display name and timestamp | | `src/app/components/GifPicker.tsx` | Giphy-powered GIF picker, TDS-styled, gated on `gifApiKey` in config | | `src/app/features/call/CallControls.tsx` | Push to Deafen (M key), PTT visual indicator, TDS typing dots | | `src/app/plugins/call/CallControl.ts` | `onControlMutation()` state tracking, screenshare audio mute logic, call button scoping | | `src/app/components/CallEmbedProvider.tsx` | PiP window, draggable overlay, navigate-on-click, imperative geometry sync | | `src/app/plugins/call/CallEmbed.ts` | `getBoundingClientRect()`-based embed positioning, ResizeObserver sync, dark mode injection | | `src/app/plugins/millify.ts` | Named re-export of `millify` to fix Rolldown `__toESM` CJS interop bug | | `src/app/features/room/MediaGallery.tsx` | Images/Videos/Files gallery drawer with pagination and E2EE awareness | | `src/app/features/room/PollCreator.tsx` | Poll creation UI for stable `m.poll.start`, single/multiple choice, 2–10 options | | `src/app/features/common-settings/general/RoomShareInvite.tsx` | QR code + copy link invite sharing modal | | `src/app/utils/syntaxHighlight.ts` | TDS-aware syntax highlighter using `--lt-accent-*` inline styles | | `src/app/features/room-settings/ExportRoomHistory.tsx` | Plain Text / JSON / HTML room history export with date range and E2EE support | | `src/app/features/room-settings/RoomActivityLog.tsx` | Human-readable mod log for member and state change events | | `src/app/features/room-settings/RoomServerACL.tsx` | `m.room.server_acl` editor with allow/deny lists and wildcard validation | | `src/app/features/room-settings/RoomInsights.tsx` | Room stats: top members, top reactions, media breakdown, activity heatmap | | `src/app/features/bookmarks/BookmarksPanel.tsx` | Bookmarks sidebar panel backed by `io.lotus.bookmarks` account data | | `src/app/hooks/useBookmarks.ts` | Hook for reading and mutating the bookmarks account data entry | | `src/app/features/room/ScheduleMessageModal.tsx` | MSC4140 delayed event scheduling UI with date/time picker | | `src/app/utils/scheduledMessages.ts` | Helpers for creating, listing, and cancelling MSC4140 delayed events | | `src/app/hooks/useExtendedProfile.ts` | MSC4133 extended profile fields (`m.pronouns`, `m.tz`) read/write | | `src/app/hooks/useLocalTime.ts` | Derives current local time from `m.tz` profile field, updates every 60s | | `src/app/components/url-preview/UrlPreviewCard.tsx` | 13 domain-specific URL preview layouts plus generic fallback with favicon | | `src/app/features/lotus/avatarDecorations.ts` | Avatar decoration catalog, CDN URL, `decorationUrl()` helper | | `src/app/hooks/useAvatarDecoration.ts` | Profile field fetch with module-level cache and in-flight deduplication | | `src/app/components/avatar-decoration/AvatarDecoration.tsx` | APNG overlay wrapper rendered around avatars in timeline, members drawer, autocomplete | | `src/app/features/settings/account/ProfileDecoration.tsx` | Settings decoration picker — scrollable grid, category headers, save button | | `scripts/syncDecorations.mjs` | CDN HEAD-check sync script: removes catalog entries for deleted Nextcloud files |