jared c798625a79 fix: DM filter input full width — add direction="Column" to wrapper Box
Without direction="Column" the flex container defaults to row, so the
Input only takes its intrinsic width instead of stretching. Matches the
identical Box in Home.tsx which already had direction="Column".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 16:38:42 -04:00
2026-05-23 11:26:45 -04:00
2022-12-20 20:47:51 +05:30
2022-01-30 20:58:38 +05:30
2024-09-07 19:15:55 +05:30
2023-02-24 17:28:04 +05:30
2026-05-23 11:26:45 -04:00

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.txt to lotus-keys.txt
  • manifest.json updated 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.tsx sets data-theme attribute 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 cameraOnJoin setting 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.ts onControlMutation() — detects the screenshare button going primary and clicks gridButton after 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 Chip by default; when LotusGuild TDS is active: orange PTT — Hold SPACE / green ● LIVE in JetBrains Mono
    • Listens on both main window and EC iframe contentWindow for 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 noiseSuppression URL 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.parent state event (i.e. not a space/guild text channel). Public rooms and space channels are excluded to prevent accidental mass-notifications. Room.tsx switches to CallView layout when a call embed is active in the current room.
  • Poll display: m.poll.start events (both stable Matrix 1.7 m.poll content key and MSC3381 unstable org.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 the EncryptedContent callback so encrypted polls also display after decryption. "Open in Element to vote" note displayed. Implemented in PollContent.tsx.
  • Deleted message placeholder: Redacted m.room.message, m.room.encrypted, and m.sticker events no longer disappear from the timeline. Instead they reach the existing RedactedContent component (trash icon + italic "This message has been deleted" with reason if provided), matching Element, FluffyChat, Commet, and Nheko behaviour. One-line change in the eventRenderer filter in RoomTimeline.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.current via useEffect — a wrapper div cannot be used because useCallEmbedPlacementSync writes top/left/width/height directly onto that element.
  • Call embed positioning: useCallEmbedPlacementSync uses getBoundingClientRect() (not offsetTop/Left) for accurate viewport-relative coordinates on the position:fixed container. Position is synced immediately on mount via useEffect in addition to the ResizeObserver, so the embed is placed correctly the instant the call view renders. The [pipMode, callVisible] effect in CallEmbedProvider only clears pip-specific styles when actually exiting pip mode — no longer clobbers the position set by syncCallEmbedPlacement on every callVisible toggle.
  • 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. themeKind is stored on the CallEmbed instance and updated on every setTheme() 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 chatBackground pattern (Blueprint, Carbon, Stars…) is applied as the backgroundImage/backgroundColor of div[data-call-embed-container] when the call is in full view (not PiP). The iframe html, body is forced to background: none !important so the pattern shows through. When chatBackground is none, 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 in ReportRoomModal.tsx with loading/success/error states.

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_body is 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.replace relations for the event and displays them oldest-to-newest. Previously the "edited" label was visible but unclickable.
  • GIF picker: Giphy-powered GIF search and send. Button appears in the message composer only when gifApiKey is set in config.json. Sends GIF as m.image — fetches blob, uploads via mx.uploadContent, sends with mx.sendMessage. FocusTrap handles click-outside / Escape to close. When TDS is active: dark navy background (#060c14), orange dim border, // GIF_SEARCH header, CSS overrides for Giphy SDK search bar (dark bg, orange border/focus ring, JetBrains Mono), custom orange scrollbar. All TDS styles live in lotus-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_ts epoch ms to the Matrix /search endpoint. 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.location events inline with a map tile.
  • Deleted message placeholders: Redacted m.room.message, m.room.encrypted, and m.sticker events render as "This message has been deleted" with reason (if provided) rather than disappearing. One-line change in the eventRenderer filter in RoomTimeline.tsx.

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). Uses io.lotus.room_names account data key (based on MSC4431).

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 a Map<eventId, userId[]> from all joined members' room.getEventReadUpTo() positions. Subscribes to RoomEvent.Receipt for live updates (debounced at 150ms to batch burst updates from mass-read events).
  • nearestRenderableId(liveEvents, evtId) — receipts can land on reaction/edit events that RoomTimeline skips (renders null). 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 from RoomTimeline down to all Message instances without prop drilling.
  • ReadReceiptAvatars component — renders a pill-shaped row of overlapping StackedAvatar circles (24px, SurfaceVariant outline) below messages with readers. Pill uses color.SurfaceVariant.Container background for visibility on any wallpaper. Max 5 avatars shown + +N overflow count. Avatar fallback uses colorMXID(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 use room.getReadReceiptForUserId(userId)?.data.ts and respect the user's 24-hour clock setting.
  • Authenticated media (mxcUrlToHttp utility) 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.tsx and RoomViewHeader.tsx.

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 unavailable with status_msg: 'dnd'), Invisible (grey outline, broadcasts offline), 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. usePresenceUpdater short-circuits immediately for manual modes; full idle-timer and visibility-change logic only runs in Auto mode. Settings also exposed via src/app/state/settings.ts (presenceStatus field).
  • 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: '' via mx.setPresence. A character counter (shown when ≥ 56/64 chars) prevents overflow. Implemented in src/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 (PresenceBadge component from src/app/components/presence/Presence.tsx).
  • Document title unread count: Tab title updates to (N) Lotus Chat for mentions, · Lotus Chat for unreads, Lotus Chat when clear.

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.
  • 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 from useRoomLatestRenderedEvent. Encrypted rooms show "Encrypted message" only on actual decryption failure.
  • Favorite rooms: Right-click any room → "Add to Favorites" / "Remove from Favorites". Favorited rooms (using the standard Matrix m.favourite tag) 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.OrderList button opens a modal with question field, 210 answer options (add/remove), and Single/Multiple choice toggle. Sends a stable m.poll.start event. (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 via playbackRate on the <audio> element.
  • Invite link + QR code: Room settings → General shows a "Share Room" tile with the matrix.to invite URL and a QR code. The Invite modal also has a toggle button showing a QR panel when clicked. Both use api.qrserver.com (added to CSP on LXC 106).
  • Private read receipts: Settings → General → Privacy — "Private Read Receipts" toggle. When on, sends m.read.private instead of m.read so 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" (calls mx.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 in syntaxHighlight.ts — supports JS/TS/JSX/TSX, Python, Rust. Falls back to ReactPrism for other languages.

Settings (Appearance)

  • Night Light / Blue Light Filter: Warm orange overlay (rgba(255,140,0,N%)) across the entire UI. Toggle + intensity slider (580%) in Settings → Appearance. position:fixed; pointer-events:none; z-index:9998. Persists across sessions.

Calls (Extended)

  • Push-to-Deafen: Press M during a call to toggle speaker mute (deafen). Configurable in Settings → General → Calls alongside the PTT key. Skips editable elements; guards e.repeat; uses el.ownerDocument.body for iframe safety.
  • TDS typing indicator dots: When Lotus Terminal mode is active, the animated typing indicator dots turn TDS orange (var(--lt-accent-orange)) via color: currentColor inheritance.

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 in src/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 in src/app/features/room/RoomViewHeader.tsx; composer guard in src/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 via cinny-upstream-check.sh on LXC 106 — notifies Matrix on new upstream commits.
  • Rolldown CJS interop — millify: src/app/plugins/millify.ts uses a named import (import { millify as millifyPlugin } from 'millify') instead of a default import. Rolldown's __toESM helper with mode=1 sets a.default = module_object (not the function itself) when hasOwnProperty prevents the copy — calling millifyPlugin() would throw (0, zc.default) is not a function. Named import bypasses the interop entirely.
  • Sentry noise filter: ignoreErrors: ['Request timed out'] added to Sentry.init in src/index.tsx to 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: encUrlPreview default changed from false to true in src/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):

  1. Push triggers a Gitea Actions build — TypeScript check, ESLint, Prettier, bundle size report
  2. Build must pass as the CI gate; quality checks are informational (continue-on-error)
  3. A Gitea webhook fires lotus_deploy.sh on LXC 106, which polls the API until CI passes (up to 15 min), then pulls origin/lotus, runs npm 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, 210 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)
S
Description
Lotus Guild fork of Cinny — custom Matrix web client
Readme AGPL-3.0 90 MiB
Languages
TypeScript 98.2%
JavaScript 1.4%
CSS 0.3%
HTML 0.1%