Lotus Chat
A Matrix client for Lotus Guild — forked from Cinny v4.12.1.
Deployed at chat.lotusguild.org.
Changes from upstream Cinny
Branding & Identity
- Package renamed to
lotus-chat, description updated to "Lotus Chat — Matrix client for Lotus Guild" - App title changed from "Cinny" to "Lotus Chat" throughout
- Favicon, PWA icons, and all icon sizes (57×57 → 180×180 Apple touch icons) replaced with Lotus.png variants
- Logo in About dialog and Auth page replaced with official Lotus.png
- Auth footer rewritten: shows dynamic version from
package.json, links to lotusguild.org, chat.lotusguild.org, and matrix.lotusguild.org - Welcome page tagline changed from "Yet another matrix client" to "A Matrix client for Lotus Guild"
- Encryption key export filename changed from
cinny-keys.txttolotus-keys.txt manifest.jsonupdated with Lotus name, description, and branding colors
LotusGuild Terminal Design System (TDS) v1.2
A full custom theme engine layered on top of Cinny's vanilla-extract theming:
Dark mode (LotusTerminalTheme):
- CRT terminal aesthetic: scanline overlay, vignette, phosphor glow
- Palette: bg
#030508, orange#FF6B00, cyan#00D4FF, green#00FF88, text#c4d9ee - Monospace font stack, terminal-style scrollbars
- Custom hex-grid and circuit-board CSS background patterns
- Matrix-style boot messages on the welcome page (press Escape to skip)
- CSS variables:
--lt-*family covering colors, glow effects, borders, animations
Light mode (LotusTerminalLightTheme):
- Full light palette: bg
#edf0f5, orange#c44e00, cyan#0062b8, green#006d35, text#111827 - No CRT effects (scanlines, vignette disabled)
- Light-mode scrollbars, adjusted code block colors, semantic color overrides
- Scoped to
html[data-theme="light"] body.lotusTerminalBodyClass ThemeManager.tsxsetsdata-themeattribute based on active theme kind
Chat Backgrounds (20+ custom patterns, all TDS-aware):
- Blueprint grid, carbon fiber, starfield, topographic contours, herringbone, crosshatch
- Chevron, polka dots, triangles, plaid
- All patterns use CSS custom properties — adapt to both TDS dark and light themes
- Settings toggle for showing per-message sender profiles
Voice / Video Call Improvements
- Element Call 0.19.4: Upgraded from 0.16.3. Dist copied to
public/element-call/by vite at build time. - Camera default OFF: Camera no longer persists across sessions via localStorage. Always starts disabled. Optional
cameraOnJoinsetting for explicit opt-in. - Deafen button: Tooltip corrected to "Deafen" / "Undeafen" (was "Turn Off Sound" / "Turn On Sound")
- Screenshare confirmation: A confirm dialog appears before screenshare is broadcast to call participants
- Auto-revert spotlight on screenshare: When someone starts screensharing, EC normally forces all participants into spotlight view. Patched in
CallControl.tsonControlMutation()— detects the screenshare button goingprimaryand clicksgridButtonafter 600ms to revert to grid layout. Participants choose to watch screenshare manually. - Push to Talk (PTT):
- Configurable keybind (default: Space) via Settings > General > Calls
- Mic activates on keydown, deactivates on keyup; mic muted on tab blur/focus to prevent stuck-on mic
- Visual indicator: plain folds
Chipby default; when LotusGuild TDS is active: orangePTT — Hold SPACE/ green● LIVEin JetBrains Mono - Listens on both main window and EC iframe
contentWindowfor reliable key capture - Implemented via
CallControl.setMicrophone()public method on the widget bridge - Mic state preservation: when enabling PTT mode mid-call, the user's previous mic state is saved and restored when PTT is disabled — prevents unwanted unmute if the user had manually muted before switching to PTT.
- Noise suppression toggle: Settings > General > Calls — passes
noiseSuppressionURL parameter to the embedded Element Call widget - Call button scoping: The upstream Cinny 4.12.1 call button (voice + video dropdown) is restricted to DMs and private group chats only. Specifically: direct messages, or invite-only rooms that have no
m.space.parentstate event (i.e. not a space/guild text channel). Public rooms and space channels are excluded to prevent accidental mass-notifications.Room.tsxswitches to CallView layout when a call embed is active in the current room. - Poll display:
m.poll.startevents (both stable Matrix 1.7m.pollcontent key and MSC3381 unstableorg.matrix.msc3381.poll.start) render as read-only poll cards inside the standard message bubble — question and answer options shown. Registered as top-level event renderers AND inside theEncryptedContentcallback so encrypted polls also display after decryption. "Open in Element to vote" note displayed. Implemented inPollContent.tsx. - Deleted message placeholder: Redacted
m.room.message,m.room.encrypted, andm.stickerevents no longer disappear from the timeline. Instead they reach the existingRedactedContentcomponent (trash icon + italic "This message has been deleted" with reason if provided), matching Element, FluffyChat, Commet, and Nheko behaviour. One-line change in theeventRendererfilter inRoomTimeline.tsx. - Picture-in-picture (PiP): When navigating away from a call room while in an active call, the call embed shrinks to a 280x158px floating window in the bottom-right corner. The PiP window is draggable — drag it anywhere on screen to move it out of the way. Clicking (without dragging) navigates back to the call room. Drag vs click distinguished by a 5px movement threshold; touch drag supported. Imperative style overrides on
callEmbedRef.currentviauseEffect— a wrapper div cannot be used becauseuseCallEmbedPlacementSyncwritestop/left/width/heightdirectly onto that element. - Call embed positioning:
useCallEmbedPlacementSyncusesgetBoundingClientRect()(notoffsetTop/Left) for accurate viewport-relative coordinates on theposition:fixedcontainer. Position is synced immediately on mount viauseEffectin addition to the ResizeObserver, so the embed is placed correctly the instant the call view renders. The[pipMode, callVisible]effect inCallEmbedProvideronly clears pip-specific styles when actually exiting pip mode — no longer clobbers the position set bysyncCallEmbedPlacementon everycallVisibletoggle. - Dark mode in element-call: After joining,
CallEmbed.applyStyles()injects:root { color-scheme: dark|light }into the iframe document so@media (prefers-color-scheme)rules inside element-call resolve to the correct Cinny theme regardless of the OS system preference.themeKindis stored on theCallEmbedinstance and updated on everysetTheme()call, so live theme switching also re-injects the CSS. Without this, users with OS light mode would see a white background even when Cinny is in dark mode. - Call embed wallpaper: The user's
chatBackgroundpattern (Blueprint, Carbon, Stars…) is applied as thebackgroundImage/backgroundColorofdiv[data-call-embed-container]when the call is in full view (not PiP). The iframehtml, bodyis forced tobackground: none !importantso the pattern shows through. WhenchatBackgroundisnone, behaviour is unchanged.
Moderation
- Report Room: A "Report Room" option in the room header menu (⋮) allows users to report a room to homeserver admins with a reason and abuse category (Spam / Harassment / Inappropriate Content / Other). Calls
POST /_matrix/client/v3/rooms/{roomId}/report(MSC4151, confirmed supported on matrix.lotusguild.org). Implemented inReportRoomModal.tsxwith loading/success/error states. - Policy List / Ban List Viewer (MSC2313): A "Policy Lists" tab in Room / Space Settings (admin-only, power-level gated) shows all subscribed
m.policy.rule.*rooms and their contents — banned users, banned rooms, and banned servers — each with entity, reason, and recommendation fields. Subscribe (join the policy room) and Unsubscribe (leave) actions are provided. Enforcement remains solely with the Draupnir bot; this UI is a read-only complement.
Messaging Enhancements
- Rich room topics: Room topics that contain formatted text (bold, links, italic) are now rendered with full HTML formatting. Falls back to plain text if no
formatted_bodyis present. Activates when any room admin sets a formatted topic. - Edit history viewer: Clicking the "edited" label on any edited message opens a modal showing every prior version with timestamps. Fetches all
m.replacerelations for the event and displays them oldest-to-newest. Previously the "edited" label was visible but unclickable. E2EE fix: the "Original" entry now usesgetClearContent()(bypasses the replacing-event chain, returns the decrypted pre-edit body) instead ofevent.contentwhich is still raw ciphertext for encrypted messages — fixes "(no text)" shown for almost all E2EE message originals. - Inline GIF preview: Giphy and Tenor share links sent as plain text auto-embed as animated GIFs inline in the timeline. URL patterns are detected client-side; the image is fetched via the homeserver's
/_matrix/media/v3/preview_urlproxy (no direct contact with Giphy/Tenor from the client). Rendered as<img loading="lazy">— respects the existing URL preview enabled/disabled setting. - GIF picker: Giphy-powered GIF search and send. Button appears in the message composer only when
gifApiKeyis set inconfig.json. Sends GIF asm.image— fetches blob, uploads viamx.uploadContent, sends withmx.sendMessage.FocusTraphandles click-outside / Escape to close. When TDS is active: dark navy background (#060c14), orange dim border,// GIF_SEARCHheader, CSS overrides for Giphy SDK search bar (dark bg, orange border/focus ring, JetBrains Mono), custom orange scrollbar. All TDS styles live inlotus-terminal.css.ts— no runtime<style>injection, eliminating flash of unstyled content. - Message forwarding: Forward any message to any room from the message context menu.
- Draft persistence: Unsent message drafts survive page reload via
localStorage(draft-msg-<roomId>). Jotai in-memory atom is primary; localStorage is used as fallback on reload and cleared on send. - Message search date range: From/To date pickers in the search filter bar. Sends
from_ts/to_tsepoch ms to the Matrix/searchendpoint. Chip shows active range with X to clear. - Image/video captions: Caption text field on image and video upload — sent as a single event with the media.
- Location sharing: Map embed view for incoming location events + static share button. Renders
m.locationevents inline with a map tile. - Deleted message placeholders: Redacted
m.room.message,m.room.encrypted, andm.stickerevents render as "This message has been deleted" with reason (if provided) rather than disappearing. One-line change in theeventRendererfilter inRoomTimeline.tsx. - Message bookmarks / saved messages: Right-click any message → "Bookmark" / "Remove Bookmark". A star icon in the sidebar nav opens
BookmarksPanel.tsx— a right-side panel listing all saved messages with room name, preview text, relative timestamp, filter input, "Jump to message" deep-link, and individual remove buttons. Stored inio.lotus.bookmarksaccount data (max 500 entries); syncs across devices. Implemented insrc/app/features/bookmarks/BookmarksPanel.tsx+src/app/hooks/useBookmarks.ts. - Message scheduling (MSC4140): Clock button next to send opens
ScheduleMessageModal.tsxwith a message textarea and datetime picker. Messages sent via the MSC4140 delayed events API (org.matrix.msc4140), confirmed supported onmatrix.lotusguild.org. A collapsible tray above the composer lists pending scheduled messages with Cancel buttons. Utilities insrc/app/utils/scheduledMessages.ts. - Richer link preview cards:
UrlPreviewCard.tsxrenders domain-specific cards for 13 sites: YouTube (thumbnail + ▶ play overlay), Vimeo, GitHub (repo parse), Twitter/X (tweet text + media parse), Reddit (subreddit + upvotes + comments), Spotify (artwork), Twitch (LIVE badge + game), Steam, Wikipedia, Discord (server invite), npm, Stack Overflow, and IMDb (poster). Generic cards gain a favicon from Google's S2 service. Cards that produce no renderable content are suppressed. - File upload compression (opt-in): JPEG and PNG files in the upload preview show a "Compress" checkbox. When checked, a Canvas API call (
toBlob(..., 'image/jpeg', 0.82)) compresses the image client-side. Original and compressed sizes are shown side-by-side. Compression is strictly opt-in — unchecked by default, skipped for GIF/SVG/WebP.
Room Customization
- Personal room name overrides: Right-click any room in the sidebar → "Rename for me…" to set a local display name visible only to you. Other members see the original name unchanged. A small pencil icon marks rooms with a custom local name. Stored in Matrix account data (
io.lotus.room_names). Usesio.lotus.room_namesaccount data key (based on MSC4431). - Export room history: Room Settings → Export tab. Supports Plain Text, JSON, and HTML formats with optional start/end date range filters. Paginates backwards via
mx.paginateEventTimeline()with a live progress counter. E2EE-aware — events that failed decryption are skipped rather than exported as garbled ciphertext. Downloads viaBlob+<a download>. Implemented insrc/app/features/room-settings/ExportRoomHistory.tsx. - Room activity / mod log: Room Settings → Activity tab. Filterable log of
m.room.member(join/leave/kick/ban/unban/invite),m.room.power_levels,m.room.name,m.room.topic,m.room.avatar, andm.room.server_aclevents. Human-readable descriptions, relative timestamps, type-filter dropdown, Load More pagination, and auto-paginate on mount. Implemented insrc/app/features/room-settings/RoomActivityLog.tsx. - Server ACL editor: Room Settings → Server ACL tab. Reads and writes
m.room.server_aclstate events. Editable allow and deny server lists with wildcard pattern validation (*.example.com). "Allow IP literal addresses" toggle. Read-only view shown to users without the required power level. Implemented insrc/app/features/room-settings/RoomServerACL.tsx. - Room stats / insights: Room Settings → Insights tab (not the default tab). Derives all statistics from the local timeline cache only; a disclaimer banner clarifies this. Shows top 5 active members (bar chart), top 5 reactions (emoji chips), media breakdown (images/videos/audio/files tiles), and a 24-hour activity heatmap (CSS bar chart). Implemented in
src/app/features/room-settings/RoomInsights.tsx.
Per-Message Read Receipts
Full per-message read receipt system — shows who has read each message directly in the timeline.
Architecture:
useRoomReadPositions(room)hook — computes aMap<eventId, userId[]>from all joined members'room.getEventReadUpTo()positions. Subscribes toRoomEvent.Receiptfor live updates (debounced at 150ms to batch burst updates from mass-read events).nearestRenderableId(liveEvents, evtId)— receipts can land on reaction/edit events thatRoomTimelineskips (rendersnull). This walks backwards from the receipt event through the live timeline until it finds a non-reaction/non-edit event to attach to.ReadPositionsContext— React context providing the positions map fromRoomTimelinedown to allMessageinstances without prop drilling.ReadReceiptAvatarscomponent — renders a pill-shaped row of overlappingStackedAvatarcircles (24px,SurfaceVariantoutline) below messages with readers. Pill usescolor.SurfaceVariant.Containerbackground for visibility on any wallpaper. Max 5 avatars shown ++Noverflow count. Avatar fallback usescolorMXID(userId)for distinctive per-user color.- Clicking the pill opens the "Seen by" modal (
EventReaders) listing all readers with their avatar, display name, and a formatted read timestamp ("Today at 3:42 PM", "Yesterday at 10:15 AM", "May 14 at 9:00 AM"). Timestamps useroom.getReadReceiptForUserId(userId)?.data.tsand respect the user's 24-hour clock setting. - Authenticated media (
mxcUrlToHttputility) used for all avatar loads, matching the correct Lotus utility signature.
Delivery Status Indicators
Own messages display a small status marker below the message content (when no read receipts are visible yet):
⟳— message is being sent / encrypting✓— message confirmed sent (local echo)✕— message failed to send (shown in red; orange glow in TDS mode)- Status hidden once the server confirms receipt (
status === null) — read receipts take over at that point
URL Preview Cards (TDS)
URL preview cards (UrlPreviewCard) styled for terminal mode:
- Dark transparent background with cyan border-left accent (Anduril Orange)
- Link text in cyan, hover switches to orange with glow
- Light TDS variant: off-white background with blue accent
Reaction Chips (TDS)
Emoji reaction buttons styled for terminal mode via button[data-reaction-key] selector:
- Unselected:
rgba(0,212,255,0.06)background, cyan border - Hover: brighter background + box-shadow glow
- Own reaction (aria-pressed=true): orange tint
rgba(255,107,0,0.12), orange border - Light TDS: equivalent blue/orange variants
DM Call Improvements
- Incoming call ring: DM calls trigger a ring tone with Answer/Decline UI. 30-second auto-dismiss if unanswered. Implemented in
Room.tsxandRoomViewHeader.tsx.
Room Customization
- Room emoji prefix: A leading emoji in a room name (e.g. 🎮 general) renders at 1.15× size in the sidebar for visual impact. Matrix room names already support Unicode — this is purely a rendering enhancement in
RoomNavItem.tsx. All three room-name inputs (Create Room, Room Settings, "Rename for me…" dialog) now include a 😊 emoji picker button that prepends the selected emoji to the name field.
Presence
- Discord-style presence selector: Clicking your avatar in the bottom-left sidebar opens a popout with five status options — Online (green), Idle (yellow), Do Not Disturb (red, broadcasts
unavailablewithstatus_msg: 'dnd'), Invisible (grey outline, broadcastsoffline), and Auto (activity-tracking, the original behaviour). The selected status persists across reloads via the settings atom. A colored badge on the avatar reflects the current status at a glance.usePresenceUpdatershort-circuits immediately for manual modes; full idle-timer and visibility-change logic only runs in Auto mode. Settings also exposed viasrc/app/state/settings.ts(presenceStatusfield). - Custom status message: Set a short status text (up to 64 characters) with an emoji picker, shown below your display name in member lists and presence displays. Accessible via Settings → Account → Profile. Includes an auto-clear timer (options: 30 minutes, 1 hour, 4 hours, 1 day, 3 days, 7 days) — after the timer expires, the status is automatically cleared by setting
status_msg: ''viamx.setPresence. A character counter (shown when ≥ 56/64 chars) prevents overflow. Implemented insrc/app/features/settings/account/Profile.tsx. - Presence badges on members: Online/busy/away dots shown next to users in the room members drawer and settings members panel (
PresenceBadgecomponent fromsrc/app/components/presence/Presence.tsx). - Presence avatar border ring: A 2px colored
outlinering on user avatars throughout the app shows presence at a glance — green (online), yellow (idle), red (DND), no ring (offline). Implemented asPresenceRingAvatarcomponent (src/app/components/presence/PresenceRingAvatar.tsx) usingReact.cloneElementto injectoutline+outlineOffsetdirectly onto the childAvatarelement — the ring follows the avatar's actualborder-radiusregardless of shape. Applied to: message timeline sender avatars, members drawer, @mention autocomplete, and inbox notification senders. - Document title unread count: Tab title updates to
(N) Lotus Chatfor mentions,· Lotus Chatfor unreads,Lotus Chatwhen clear. - Extended profile fields (MSC4133): Settings → Account → Profile includes Pronouns (
m.pronouns) and Timezone (m.tz) fields, saved via MSC4133PUT /_matrix/client/unstable/uk.tcpip.msc4133/{userId}/{field}. Both fields are displayed in user profile panels. Implemented viasrc/app/hooks/useExtendedProfile.ts. - User local time in profile: When a user has
m.tzset, their profile panel shows a clock icon, their current local time, and the timezone abbreviation (e.g. EST, JST). Updates every 60 seconds. Respects the viewer'shour24Clocksetting. Implemented viasrc/app/hooks/useLocalTime.ts.
UX & Composer
- Message length counter: A muted character counter appears just left of the send button while typing, disappearing when the composer is empty. Resets on room switch.
- Quick emoji reactions on hover: The 3 most-recently-used emoji reactions appear directly in the message hover toolbar (between the emoji-board button and Reply), so reacting requires a single click rather than opening the 3-dots menu first. Clicking a quick-reaction also closes any open emoji picker. Powered by
useRecentEmojisourced from Matrix account data. - In-app notification toasts: When a message or invite notification fires and the browser window is focused, a slim TDS-styled toast card slides in from the bottom-right instead of triggering an OS notification. Card shows: 24px avatar (initials fallback), sender name in orange, truncated message body, room name, × dismiss, 4 s auto-dismiss. Clicking navigates directly to the correct room (DM or home path) or the invites inbox. OS notifications are unchanged when the window is not focused. Implemented in
src/app/features/toast/LotusToastContainer.tsx+src/app/state/toast.ts. - Collapsible long messages: Messages exceeding ~20 lines are auto-collapsed with a "Read more ↓" button. Click to expand inline; a "Collapse ↑" button re-folds. Threshold (in lines) configurable in Settings → Appearance. Uses CSS
max-height+overflow: hidden— works correctly with code blocks and embedded media. Respectsprefers-reduced-motion. - Message send animation: Own sent messages fade and scale into the timeline (0.15 s ease-out:
scale(0.97)→scale(1),opacity 0.4→1). Incoming messages are unaffected. Respectsprefers-reduced-motion. - Right-click room context menu: Expanded sidebar room context menu — Mute now opens a duration submenu (15 min / 1 hr / 8 hr / 24 hr / Indefinite) with auto-restore after the selected window; Copy Room Link copies the
matrix.toURL with a "Copied!" flash; Mark as Read marks the room read to the latest event; plus Leave Room and Room Settings shortcuts. - Unverified device warning:
warnOnUnverifiedDevicessetting (default off). When enabled via Settings → General → Privacy, a warning banner appears above the composer in encrypted rooms that contain unverified devices, showing the count. Sending is never blocked — the banner is informational only. Uses the existinguseUnverifiedDeviceCount()hook. - Sidebar room filter: A search-icon input at the top of the Home and DMs sidebar tabs filters rooms by display name in real time. Clears on tab switch. Styled to match the members-drawer search bar (
size="400", search prefix icon). - DM last message preview: Each DM row in the sidebar shows a truncated message body (48 chars) and relative timestamp (
Xm,Xhr,Yesterday,D MMM) below the room name, sourced reactively fromuseRoomLatestRenderedEvent. Encrypted rooms show "Encrypted message" only on actual decryption failure. - Room sort order: Sort icon in the Rooms sidebar header lets users sort non-space rooms by Recent Activity (default), A→Z, or Unread First. Persists via
homeRoomSortsetting. - Favorite rooms: Right-click any room → "Add to Favorites" / "Remove from Favorites". Favorited rooms (using the standard Matrix
m.favouritetag) appear in a collapsible "Favorites" section above the main room list on the Home tab. Syncs across devices via account data. - Poll creation: Polls can be created directly from the composer —
Icons.OrderListbutton opens a modal with question field, 2–10 answer options (add/remove), and Single/Multiple choice toggle. Sends a stablem.poll.startevent. (Poll display & voting were already supported.) - Voice message playback speed:
0.75×→1×→1.5×→2×speed toggle pill on voice message player — cycles on click viaplaybackRateon the<audio>element. - Invite link + QR code: Room settings → General shows a "Share Room" tile with the
matrix.toinvite URL and a QR code. The Invite modal also has a⊞toggle button showing a QR panel when clicked. Both useapi.qrserver.com(added to CSP on LXC 106). - Private read receipts: Settings → General → Privacy — "Private Read Receipts" toggle. When on, sends
m.read.privateinstead ofm.readso other room members can't see when you've read messages. - Media gallery: A right-side drawer (photo icon in room header, Desktop only) showing Images | Videos | Files tabs. Reads already-decrypted timeline events — works in E2EE rooms. Encrypted-blob images show a lock-icon placeholder. Load More paginates backwards via
mx.paginateEventTimeline(). - Knock-to-join: When a room's join rule is
knock, RoomIntro shows "Request to Join" (callsmx.knockRoom()) with "Request sent" pending state. Room admins see a "Pending Requests" section in the members drawer with Approve / Deny buttons. - Code syntax highlighting (TDS mode): Fenced code blocks in messages highlight keywords (cyan), strings (green), numbers (orange), comments (italic dim), function names (purple) using inline
--lt-accent-*CSS variables. Custom tokenizer insyntaxHighlight.ts— supports JS/TS/JSX/TSX, Python, Rust. Falls back to ReactPrism for other languages.
Settings (Appearance)
-
Animated Chat Backgrounds: Five CSS-only animated wallpapers added to the background picker — Digital Rain (two-layer vertical stripe scroll with parallax), Star Drift (three-layer radial-gradient star field drifting diagonally), Grid Pulse (neon grid lines expanding/contracting via
backgroundSizekeyframe), Aurora Flow (four radial gradient ellipses sweeping across a 200% canvas), Fireflies (three layers of glowing dots drifting). All use vanilla-extractkeyframes()— no canvas, GPU-composited. Respectsprefers-reduced-motion: reduce(animation stripped at call time). "Pause Background Animations" toggle in Settings → Appearance provides an in-app override. Implemented insrc/app/styles/Animations.css.ts+src/app/features/lotus/chatBackground.ts. -
Glassmorphism Sidebar: Settings → Appearance toggle (off by default). When enabled, the left sidebar becomes semi-transparent (
background: rgba(3,5,8,0.55)) withbackdrop-filter: blur(12px)so chat background patterns show through as a frosted glass effect. Fix: the active chat background is mirrored ontodocument.bodyviauseEffectinSidebarNav.tsxso the blur has content to work through (previously the sidebar was a flex sibling with nothing physically behind it). Implemented as a vanilla-extractSidebarGlassclass applied to the<Sidebar>container inSidebarNav.tsx. -
Night Light / Blue Light Filter: Warm orange overlay (
rgba(255,140,0,N%)) across the entire UI. Toggle + intensity slider (5–80%) in Settings → Appearance.position:fixed; pointer-events:none; z-index:9998. Persists across sessions.
Notification Enhancements
- Custom notification sounds:
messageSoundId/inviteSoundIdsettings select per-category notification sound (notification.ogg,invite.ogg,call.ogg, or None). Settings → Notifications expands the sound toggle with Message Sound + Invite Sound selects and ▶ preview buttons. SharednotificationSounds.tsmodule. - Notification quiet hours:
quietHoursEnabled/quietHoursStart/quietHoursEndsettings suppress all desktop notifications and sounds during a configured time window. Handles overnight spans (e.g. 23:00–08:00). Settings → Notifications: Quiet Hours card with toggle + start/end time pickers. - Full push rule editor: Settings → Notifications → Advanced Push Rules section. Covers override, room, sender, and underride rule kinds. Each row has a human-readable label for built-in rules, an enable/disable toggle, and a delete button for custom rules. An add-rule form at the bottom of the room and sender sections lets users create new per-room or per-sender push rules by entering the room/user ID.
Calls (Extended)
- Push-to-Deafen: Press
Mduring a call to toggle speaker mute (deafen). Configurable in Settings → General → Calls alongside the PTT key. Skips editable elements; guardse.repeat; usesel.ownerDocument.bodyfor iframe safety. - TDS typing indicator dots: When Lotus Terminal mode is active, the animated typing indicator dots turn TDS orange (
var(--lt-accent-orange)) viacolor: currentColorinheritance.
Server Integration
- Server support contact (MSC1929): Settings → Help & About displays the homeserver admin contact fetched from
/.well-known/matrix/support. Shows the admin's Matrix ID and a link to the support page when the homeserver has configured this endpoint. Degrades gracefully when not configured (section is hidden on 404 or network error). In TDS mode the contact text and link render in--lt-accent-cyan. Implemented insrc/app/features/settings/about/About.tsx. - Server notices: Rooms of type
m.server_notice(system messages from the homeserver) now render with a distinct "Server Notice"<Chip variant="Warning">badge in the room header and a disabled composer showing "This is a server notice room — you cannot send messages here." Previously indistinguishable from regular DMs. Badge insrc/app/features/room/RoomViewHeader.tsx; composer guard insrc/app/features/room/RoomInput.tsx.
Infrastructure
- Authenticated media: All avatar/media loads use
mxcUrlToHttp(mx, mxcUrl, useAuthentication, w, h, 'crop')from../../utils/matrix— the Lotus utility that handles MSC3916 authenticated media. (Upstream Cinny uses the SDK method with incorrect argument order for authenticated endpoints.) - Upstream tracking:
git remote add upstream https://github.com/cinnyapp/cinny.git. Merge strategy:git fetch upstream && git merge upstream/main. Daily check viacinny-upstream-check.shon LXC 106 — notifies Matrix on new upstream commits. - Rolldown CJS interop — millify:
src/app/plugins/millify.tsuses a named import (import { millify as millifyPlugin } from 'millify') instead of a default import. Rolldown's__toESMhelper withmode=1setsa.default = module_object(not the function itself) whenhasOwnPropertyprevents the copy — callingmillifyPlugin()would throw(0, zc.default) is not a function. Named import bypasses the interop entirely. - Sentry noise filter:
ignoreErrors: ['Request timed out']added toSentry.initinsrc/index.tsxto suppress unhandled rejections from the matrixRTC delayed-event heartbeat (matrix-sdk) and the widget PostmessageTransport initial-load race (matrix-widget-api). Neither is actionable from client code. - URL preview default in encrypted rooms:
encUrlPreviewdefault changed fromfalsetotrueinsrc/app/state/settings.ts. A security note is shown next to the toggle in Settings → General explaining that the homeserver fetches the URL (and sees it) but not the message content.
Build
npm ci
npm run build # outputs to dist/
Vite's render-chunks phase requires ~6 GB Node heap. If OOM killed, set:
NODE_OPTIONS=--max_old_space_size=6144 npm run build
Development workflow
All code changes should be made in the local clone at /root/code/cinny on the dev box, then committed and pushed to origin/lotus. The CI/CD pipeline handles everything from there — no manual build or deploy steps needed.
edit → commit → git push # ~11 minutes → auto-deployed to chat.lotusguild.org
Pipeline (.gitea/workflows/ci.yml + lotus_deploy.sh on LXC 106):
- Push triggers a Gitea Actions build — TypeScript check, ESLint, Prettier, bundle size report
- Build must pass as the CI gate; quality checks are informational (
continue-on-error) - A Gitea webhook fires
lotus_deploy.shon LXC 106, which polls the API until CI passes (up to 15 min), then pullsorigin/lotus, runsnpm ci && npm run build, and rsyncs to/var/www/html/
LXC 106's stored Gitea credential is read-only — it can only pull. Pushes must be done from the dev box with your personal credentials (entered manually, never cached).
Deployment
Built files are served from /var/www/html/ on LXC 106 (nginx). Config lives at /opt/lotus-cinny/config.json (vite copies it to dist/):
{
"defaultHomeserver": 0,
"homeserverList": ["matrix.lotusguild.org"],
"allowCustomHomeservers": false,
"gifApiKey": "<giphy_key>"
}
Key Custom Files
| File | Purpose |
|---|---|
src/lotus-terminal.css.ts |
All TDS CSS tokens, global styles, light/dark variants |
src/lotus-boot.ts |
Boot sequence animation (runs once per session) |
src/app/hooks/useRoomReadPositions.ts |
Per-message read receipt position map |
src/app/features/room/ReadPositionsContext.ts |
React context for read positions |
src/app/components/read-receipt-avatars/ |
Read receipt avatar pill component |
src/app/components/event-readers/EventReaders.tsx |
"Seen by" modal with timestamps |
src/app/components/GifPicker.tsx |
GIF search + send |
src/app/features/call/CallControls.tsx |
PTT badge + keybind logic |
src/app/plugins/call/CallControl.ts |
EC widget bridge (screenshare revert, PTT mic) |
src/app/components/CallEmbedProvider.tsx |
PiP + draggable call embed, call wallpaper carry-over |
src/app/plugins/call/CallEmbed.ts |
EC widget bridge: iframe setup, color-scheme dark/light injection, built-in control hiding, theme sync |
src/app/plugins/millify.ts |
Named import fix for Rolldown CJS interop (prevents zc.default is not a function crash) |
src/app/features/room/MediaGallery.tsx |
Right-side media gallery drawer (images/videos/files) |
src/app/features/room/PollCreator.tsx |
Poll creation modal (single/multiple choice, 2–10 options) |
src/app/features/common-settings/general/RoomShareInvite.tsx |
Invite link + QR code tile for room settings |
src/app/utils/syntaxHighlight.ts |
TDS code syntax tokenizer (JS/TS/Python/Rust → inline CSS vars) |
src/app/features/room-settings/ExportRoomHistory.tsx |
Export room messages to plain text / JSON / HTML with date range filter and E2EE awareness |
src/app/features/room-settings/RoomActivityLog.tsx |
Filterable mod log of room state events (joins, kicks, bans, power level changes, etc.) |
src/app/features/room-settings/RoomServerACL.tsx |
Server ACL viewer/editor (allow/deny lists, IP literal toggle, power-level gated) |
src/app/features/room-settings/RoomInsights.tsx |
Room stats panel: top members bar chart, top reactions, media breakdown, 24h heatmap |
src/app/features/bookmarks/BookmarksPanel.tsx |
Saved messages sidebar panel with filter, jump-to-message, and remove |
src/app/hooks/useBookmarks.ts |
Read/write io.lotus.bookmarks account data for message bookmarks |
src/app/features/room/ScheduleMessageModal.tsx |
Schedule-message modal with datetime picker; sends via MSC4140 delayed events API |
src/app/utils/scheduledMessages.ts |
Utilities for MSC4140 scheduled message state and cancel endpoint |
src/app/hooks/useExtendedProfile.ts |
Read/write MSC4133 extended profile fields (m.pronouns, m.tz) |
src/app/hooks/useLocalTime.ts |
Formats user local time from m.tz IANA zone; updates every 60 s |
src/app/components/url-preview/UrlPreviewCard.tsx |
Domain-specific URL preview cards for 13 sites + generic favicon fallback |