root e836634cff fix: save/restore mic state on PTT mode toggle (I-4)
When enabling PTT mode, save current mic state before muting.
When disabling PTT, restore saved state instead of unconditionally unmuting.
Prevents surprising unmute if user had manually muted before switching to PTT.
2026-05-16 01:59:42 -04:00
2022-12-20 20:47:51 +05:30
2022-01-30 20:58:38 +05:30
2022-12-20 20:47:51 +05:30
2024-05-22 21:56:44 +10:00
2024-09-07 19:15:55 +05:30
2023-02-24 17:28:04 +05:30

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
  • 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 (18+ 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.3: 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
  • 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.

Messaging Enhancements

  • 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, injected <style> overrides Giphy SDK search bar (dark bg, orange border/focus ring, JetBrains Mono), custom orange scrollbar.
  • Message forwarding: Forward any message to any room from the message context menu.
  • 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.

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.
  • 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.

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.

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.

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

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
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%