ea15db430c
- LOTUS_FEATURES.md: full reference of every custom Lotus feature with implementation details, file paths, and API notes - LOTUS_BUGS.md: audit of confirmed bugs with root causes and fixes - LOTUS_TODO_REFERENCE.md: technical implementation notes for backlog items - LOTUS_TODO.md: trim completed audit results section, link to FEATURES doc - README.md: rewrite to marketing-friendly feature list format; fix incorrect claim that screenshare auto-reverts view to grid (removed) Fix: auto-revert spotlight on screenshare was removed (600ms grid-click caused fullscreen to show avatars); corrected in LOTUS_FEATURES.md and removed from README.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
777 lines
32 KiB
Markdown
777 lines
32 KiB
Markdown
# 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. [Glassmorphism Sidebar (P5-3)](#glassmorphism-sidebar-p5-3)
|
||
5. [Night Light / Blue Light Filter (P5-5)](#night-light--blue-light-filter-p5-5)
|
||
6. [Voice / Video Call Improvements](#voice--video-call-improvements)
|
||
7. [Per-Message Read Receipts](#per-message-read-receipts)
|
||
8. [Delivery Status Indicators](#delivery-status-indicators)
|
||
9. [Messaging Enhancements](#messaging-enhancements)
|
||
10. [Presence](#presence)
|
||
11. [UX & Composer](#ux--composer)
|
||
12. [Room Customization](#room-customization)
|
||
13. [Moderation](#moderation)
|
||
14. [Notifications](#notifications)
|
||
15. [Server Integration](#server-integration)
|
||
16. [Infrastructure](#infrastructure)
|
||
17. [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 `<html>` 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 `<canvas>` 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.
|
||
|
||
### Files
|
||
|
||
- `src/app/styles/Animations.css.ts` — vanilla-extract keyframe definitions
|
||
- `src/app/features/lotus/chatBackground.ts` — `getChatBg()` implementation and pattern registry
|
||
|
||
---
|
||
|
||
## 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
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
### Noise Suppression Toggle
|
||
|
||
A `noiseSuppression` URL parameter is passed to the Element Call widget URL, allowing the noise suppression feature to be toggled from within Lotus settings.
|
||
|
||
### 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<eventId, userId[]>
|
||
```
|
||
|
||
- 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 `<img loading="lazy">` 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
|
||
|
||
### 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`
|
||
|
||
---
|
||
|
||
## 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
|
||
|
||
### 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.
|
||
|
||
---
|
||
|
||
## 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
|
||
|
||
---
|
||
|
||
## 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<eventId, userId[]>` 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 |
|