The previous version wrapped UserAvatar in a div inside SidebarAvatar,
which broke the folds Avatar CSS (expects AvatarImage/AvatarFallback as
direct child) — causing the white circle instead of the avatar.
New approach:
- SidebarAvatar has only UserAvatar as its direct child (restored)
- Clicking the avatar opens Settings directly (original behavior)
- PresencePicker renders a small absolutely-positioned button in the
bottom-right corner of SidebarItem (which already has position:relative)
- Clicking the presence dot opens the status picker menu
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- useMessageSearch: add fromTs/toTs to useCallback dep array (exhaustive-deps error)
- useMessageSearch: restore eslint-disable on the correct line for the `as any` cast
- VoiceMessageRecorder: remove two eslint-disable directives for rules that are
globally off (jsx-a11y/media-has-caption) or not enabled (react/no-array-index-key)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a manual presence picker to the sidebar user avatar. Clicking the
avatar opens a popout menu with Online, Idle, Do Not Disturb, Invisible,
and Auto (activity-based) options. The selected status is shown as a
colored badge on the avatar and stored in settings (survives reloads).
usePresenceUpdater now short-circuits for manual states and only runs
the full activity-tracking logic in Auto mode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
E1 - Document title unread count: FaviconUpdater now also sets
document.title to '(N) Lotus Chat' for mentions, '· Lotus Chat'
for plain unreads, and 'Lotus Chat' when clear. Reuses the
existing roomToUnread forEach loop.
E2 - Draft persistence across reloads: on room unmount, unsent message
is written to localStorage as 'draft-msg-<roomId>'. On mount, if
the Jotai atom is empty (page reload), the localStorage draft is
restored. Cleared on send. Uses the existing Slate node JSON format.
E5 - Search date range filter: new DateRangeButton in SearchFilters
with From/To date inputs in a PopOut. Dates stored as epoch ms in
?fromTs=&toTs= URL params. Passed to Matrix /search as from_ts /
to_ts filter fields (valid spec fields, cast via 'as any' since
SDK types don't include them yet).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When lotusTerminal is enabled, the recording dot turns orange (#FF6B00),
the duration timer uses JetBrains Mono in green (#00FF88), and the
waveform bars match green — consistent with the PTT badge and GIF
picker terminal aesthetics.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The rect variable in the onUp and onTouchEnd closures was shadowing the
outer rect declaration in handlePipMouseDown and handlePipTouchStart.
Renamed inner declarations to savedRect. Also renamed rect → elRect in
handlePipDoubleClick for the same reason.
Removed unused eslint-disable-next-line comment in MessageSearch.tsx.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
B1 - GIF upload progress: spinner on GIF button + disabled state while
fetch+upload is in flight; clears on success or error
B2 - PiP position persistence: drag end saves left/top to localStorage;
entering PiP restores saved position (clamped to current viewport)
B3 - PiP snap-to-corner: double-click the PiP overlay snaps to nearest
corner with a 180ms CSS transition; saves new position
B4 - Device sessions loading state: useOtherUserDevices now returns
{status:'loading'|'error'|'success', devices} instead of bare
array; UserDeviceSessions shows spinner while loading
B5 - Device sessions error state: catch in hook sets status:'error';
panel shows warning icon + 'Could not load sessions' message
B6 - Screenshare fullscreen Safari guard: hide button when
document.fullscreenEnabled is false (iOS Safari, some mobile)
B7 - Status save error: show critical-coloured error text below Save
button when saveState.status === AsyncStatus.Error
B9 - Encrypted search coverage counter: 'X / Y cached' badge next to
'Encrypted Rooms' heading using existing localResult fields
D2 - PiP screenshare spotlight: track auto-spotlight in a ref; release
spotlight when screenshare ends while in PiP mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users on the same homeserver (matrix.lotusguild.org) get +1000 to their
score, everyone else starts at +1 per shared room. Sorting by score
descending means @jared:matrix.lotusguild.org always appears before
@jared:matrix.org when both match the query.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two root causes identified:
1. Layout: PopOut renders a Fragment, but the Fragment's children are not
true flex items of the parent Box in practice — the Input lost its
full-width stretch. Replaced PopOut with a relative-positioned Box
wrapper + absolute-positioned dropdown div (top:100%, left:0, right:0).
Input is now a direct flex child of the wrapper and stretches normally.
2. Autocomplete empty: mx.getUsers() returns almost nothing with lazy
member loading enabled — only users seen in presence events, not room
members. Switched to iterating mx.getRooms() + room.getMembers() and
deduplicating by userId, which covers everyone in every room.
Also: removed PopOut/FocusTrap/RectCords imports no longer needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs introduced in 6957e890:
1. Layout: extra Box wrapper around Input wasn't stretching to full
width. Removed the wrapper — Input is now a direct PopOut child,
restoring its original full-width flex behaviour.
2. FocusTrap: the autocomplete dropdown had a FocusTrap that immediately
deactivated because the search input (outside the trap) was focused.
Removed the FocusTrap entirely; onMouseDown+preventDefault on each
suggestion item already prevents input blur on click, and onBlur
with a 120ms delay handles dismissal when clicking truly outside.
3. from: regex: @ was required (from:@user) but users naturally type
from:user without it. Updated FROM_REGEX and FROM_TYPING_REGEX to
make @ optional; userId construction already prepends @ if missing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The banner saying 'E2EE · Shield = verified identity' was cluttering
the top of the member list. Hovering the lock icon in the room header
already communicates encryption status, and tooltips on the shield
badges already explain what verified means. Removed the entire block.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Type 'from:@name' in the search box to filter by sender — a dropdown
of matching users (avatar + display name + full ID) appears as you type
and selecting one converts it into a removable sender chip in the filter
bar. Multiple senders supported. Also works via manual entry on submit.
- SearchInput: detects trailing 'from:@...' pattern on every keystroke,
shows PopOut autocomplete from mx.getUsers(), onMouseDown prevents
input blur when selecting, cleans up fragment after selection
- SearchFilters: selectedSenders/onSelectedSendersChange props, sender
chips rendered with user icon and X to remove
- useLocalMessageSearch: filters cached events by sender set when senders
param is provided (encrypted room search respects the filter too)
- MessageSearch: handleSenderAdd deduplicates and writes to ?senders= URL
param; localResult now passes senders to the local search
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each encrypted room in scope shows:
- Message count and oldest cached date
- 'Load messages' if no cache, 'Load more' if more history available
- 'Fully cached' label when all history is loaded
Clicking load/load-more paginates backwards 100 messages at a time.
localResult re-computes via cacheVersion after each load so search
results update automatically without re-typing the query.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
No Matrix web client supports E2EE message search server-side — the
homeserver only sees ciphertext. This is the same approach FluffyChat
takes: scan locally decrypted events already in the live timeline.
Changes:
- useLocalMessageSearch: searches getLiveTimeline().getEvents() in
encrypted rooms using decrypted content (getContent(), not event.content)
- MessageSearch: runs client-side search in parallel with server search,
shows results in a dedicated 'Encrypted Rooms' section with clear notice
about scope (only cached/recently viewed messages)
- Encryption notice shown when encrypted rooms are in scope — explains
why results may be missing and what to do
- Server result limit raised from 20 → 50
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
useAsync re-throws after setting error state, so callers that don't
await or catch the returned promise get an unhandled rejection. Fixes
JAVASCRIPT-REACT-E (429 on presence endpoint).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hide Typing & Read Receipts and Hide Online Status were buried in
the Editor section. Extracted into a new Privacy section that sits
between Messages and Calls, where users would naturally look.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Announce online immediately on app startup
- Idle detection: unavailable after 10 min of no input, online on return
- Tab visibility: unavailable when hidden, online when focused again
- Page close: offline via fetch+keepalive (survives unload without bfcache penalty)
- hidePresence setting: broadcasts offline and stops all tracking
- Added 'Hide Online Status' toggle in General settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows X/64 below the input. Fades in at 56 chars (warning colour) and
turns critical red at the limit so users always know where they stand.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add maxLength=128 to the status input to prevent absurdly long statuses.
In UserHeroName (profile panel), tighten the status display to LineClamp2,
add overflow:hidden on the container, and use overflowWrap:anywhere so
unbroken strings (emoji chains, URLs) wrap instead of overflowing.
Member list already truncates via the truncate prop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
React.lazy + Suspense interacted badly with the nested FocusTraps (the
settings Modal500 outer trap and EmojiBoard's inner trap). During the
suspend/resolve cycle targetFromEvent returned undefined, causing
handleGroupItemClick to bail before calling onEmojiSelect.
Switched to a direct import matching MessageEditor and PowersEditor
which both use EmojiBoard inside settings panels without lazy loading.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Emoji bug root cause: EmojiBoard wraps itself in a FocusTrap with
clickOutsideDeactivates:true. When the picker was rendered inside
Input's 'after' prop, the FocusTrap treated clicks on the emoji items
as outside-clicks and deactivated (calling requestClose) before the
onEmojiSelect callback fired. Fixed by moving the emoji PopOut to be
a direct sibling of Input in the form row instead of nesting it inside
Input.after — matching the established pattern used in MessageEditor.
429 rate-limit: mx.setPresence() calls in handleClear and the
auto-clear timer effect had no rejection handling, causing unhandled
promise rejections logged to Sentry when Synapse rate-limits presence
updates. Added .catch(() => undefined) to both call sites. Sentry
issue JAVASCRIPT-REACT-E resolved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When focus moves to the emoji picker the input loses focus, making
selectionStart/selectionEnd unreliable. Replace the cursor-tracking
insertion with a simple functional state updater that always appends
the emoji to the end — reliable and appropriate for a short status field.
Also removes the now-unused inputRef and useRef import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an 'Auto-clear after' dropdown to the Status Message settings tile
with options: Never / 30 min / 1 hr / 4 hr / 8 hr / Until midnight /
1 day / 7 days.
How it works:
- On save, stores the expiry timestamp in localStorage keyed by userId
(lotus-status-expiry-<userId>) and sets expiryTs state
- A single useEffect on expiryTs drives the timer — re-saving cancels
the previous timer cleanly via useEffect cleanup
- On mount, reads stored expiry from localStorage so auto-clear
survives page reloads (fires immediately if already expired)
- Manual Clear Status also removes the stored expiry and cancels any
active timer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
React nullifies synthetic event's currentTarget before async state
updater callbacks run. Capture getBoundingClientRect() synchronously
in the onClick handler, then pass the already-computed rect into
setEmojiAnchor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MembersDrawer: show presence.status as small muted text below
username in every member row (live via useUserPresence)
- UserHero/UserHeroName: accept optional status prop; render below
the @username handle in user profile popouts
- UserRoomProfile: pass presence?.status down to UserHeroName
- Profile settings: new ProfileStatus tile below Display Name
* Input with inline emoji picker (lazy-loaded EmojiBoard)
* Cursor-aware emoji insertion (preserves caret position)
* Save via mx.setPresence({ status_msg }) / Clear button
* Pre-fills from current presence; syncs on remote update
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add screenshareAudioMuted state to CallControlState and CallControl
- setSound() now preserves screenshare audio mute when un-deafening
- Add toggleScreenshareAudio() targeting audio[data-lk-source="screen_share_audio"]
- Add ScreenshareAudioButton (volume icon, warns when muted) to controls bar
- Fix unused prevScreenshare variable (ESLint error from prior commit)
- Run Prettier on Controls.tsx and CallControl.ts (CI formatting failures)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove revert-to-grid logic that was overriding EC's natural screenshare
spotlight, causing fullscreen to show user avatars instead of the screen
- Add fullscreen button to call controls (visible when screensharing) that
requests fullscreen on the call embed container
- Add FullscreenButton component with enter/exit SVG icons to Controls.tsx
- PIP mode: sync setPipMode to CallControl; auto-enable spotlight when
screenshare is active in pip so the screenshare fills the window
- Make useCallControlState accept undefined control for safe use in
CallEmbedProvider
- Add package-lock.json to .gitignore (generated by local npm install)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FocusTrap monitors focusin events and can redirect focus into the menu
container (blurring the editor) before individual MenuItem onMouseDown
handlers fire. Adding preventDefault at the container level ensures no
click anywhere inside the menu can steal focus from the editor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add onMouseDown preventDefault to all autocomplete suggestion MenuItems
so clicking a suggestion keeps the editor focused. Without this, the
mousedown event blurs the editor before onClick fires, causing Slate's
ReactEditor.toDOMNode to fail with "Cannot resolve a DOM node from Slate
node: {"text":""}" when Transforms.collapse tries to sync the selection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When selecting an autocomplete suggestion (user/room mention, emoticon,
command), Slate's replaceWithElement inserts a void inline node into the
model but React hasn't flushed the DOM update yet. Calling
Transforms.move + insertText immediately after causes ReactEditor.toDOMNode
to fail with "Cannot resolve a DOM node from slate node: {\"text\":\" \"}".
Fix: wrap moveCursor body in setTimeout(fn, 0) so React can flush the void
element's DOM node before Slate attempts to resolve the cursor position.
Also call ReactEditor.focus to restore editor focus after the autocomplete
menu item click blurs the editor.
Fixes all callers: UserMentionAutocomplete, RoomMentionAutocomplete,
EmoticonAutocomplete, CommandAutocomplete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- IncomingCall: Reject uses variant=Critical (red), Ignore uses variant=Secondary
— previously both were variant=Success (green), making them visually identical
to the Answer button
- EventReaders: TDS timestamp glow reduced from double-layer to single-layer
(was 0 0 6px + 0 0 14px, now just 0 0 5px at 0.45 opacity)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- VoiceMessageRecorder recording dot now pulses (reuses pttLivePulse keyframe)
- Added data-voice-recorder / data-voice-rec-dot / data-voice-waveform attributes
for TDS targeting: green pulsing dot, cyan waveform bars, subtle border in TDS dark
- Wire VoiceMessageRecorder onError to the same input-bar error display used by
location errors (mic denied, media error surfaces to user instead of silent fail)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reply: distinguish loading (placeholder) from not-found (null) — show
"Original message not available" instead of a stuck loading bar
- RoomInput: geolocation errors now surface inline (denied / timed out /
unsupported); location button shows Spinner during fetch and is disabled
- Message menu: Retry Send + Cancel Message items appear when a message
is in NOT_SENT or CANCELLED state, calling mx.resendEvent / cancelPendingEvent
- ReactionViewer: sidebar gains role=listbox / role=option and ArrowUp/Down
keyboard navigation between reactions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ForwardMessageDialog:
- Room list now shows small avatars (48px crop) + DM label beneath room name
- Forward is now async: spinner overlay while in-flight, '✓ Forwarded' only
shown after sendEvent resolves; error clears sending state so user can retry
- Search bar hidden in success state for cleaner confirmation view
DeliveryStatus:
- QUEUED state used ⏳ emoji breaking the ASCII/terminal aesthetic; changed
to ⟳ matching the SENDING/ENCRYPTING icon
PollContent:
- Added data-poll-content + data-poll-answer + data-selected attributes so
TDS CSS can override inline styles without JS branching
- Added data-poll-content-label on the ◉ Poll header
- TDS dark: answers get cyan dim bg/border, selected gets orange highlight
with subtle box-shadow; hover brightens border; label uses cyan glow
- TDS light: equivalent blue/orange variants
Caption input:
- Marked with data-caption-input; focus-visible ring added in index.css
(blue for default, dark-theme dark blue) and lotus-terminal.css.ts
(orange glow for TDS dark, orange for TDS light)
Boot sequence:
- Added '[ ESC ] skip' hint at bottom-right of overlay so users know
they can dismiss it without waiting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CallControls: screenshare confirm now closes on Escape or click-outside
(transparent fixed backdrop + window keydown listener); cleaned indentation
- GifPicker: TDS header rendered a JSX comment ({/* GIF_SEARCH */}) so the
// GIF_SEARCH label was invisible; changed to {'// GIF_SEARCH'}
- CallEmbedProvider: PiP resize clamping now works at initial bottom/right
position by normalising to top/left before parsing el.style.left
- CallEmbedProvider: incoming call subtitle now reads 'Incoming Video Call'
or 'Incoming Voice Call' based on m.call.intent
- PollContent: progress bar background now uses --bg-surface-active /
--bg-surface-low instead of hardcoded white (invisible in light mode)
- index.css + lotus-terminal.css.ts: define --bg-surface, --bg-surface-low,
--bg-surface-active, --bg-surface-border, --text-primary as global CSS vars
with vanilla fallbacks and TDS dark/light overrides; these were used by
poll, location map, upload card and GIF picker but never defined
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Synapse's thumbnail endpoint returns 400 Bad Request when the
allow_redirect=true query parameter is present (added by matrix-js-sdk
41.x for authenticated media). Default allowRedirects to false in our
mxcUrlToHttp wrapper so the parameter is never appended.
Also extend the downloadMedia legacy-URL fallback to cover 400 in
addition to 401, catching any encrypted-media fetches that still carry
the old URL shape after a cache refresh.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
downloadMedia: on 401 (SW session race or allow_redirect hop stripping
auth), retry via /_matrix/media/v3/ which is public on this homeserver
(allow_public_access_to_media_repo: true). Fixes images not loading
after sending, and avatar 401s in call prescreen tiles.
CallEmbed: inject flex-centering CSS for EC 0.19.4 participant avatar
container so the initial letter is correctly centered in its circle.
CSS class names are scoped to _avatarContainer_1mrho_40 in EC 0.19.4.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each RoomNavItem subscribes to session_started/session_ended on the
MatrixRTCSessionManager, one per visible room. The default limit of 10
fires a spurious warning when 11+ rooms are in the sidebar. Listeners
are properly cleaned up — this is not a real leak.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the server returns a 4xx/5xx, downloadMedia was silently returning
the error response body as a blob. decryptAttachment would then fail with
a misleading 'Mismatched SHA-256 digest' instead of surfacing the real
HTTP error. Now throws immediately so callers (useAsyncCallback) can
show the correct error state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove [CallEmbed] state, container styles, and syncCallEmbedPlacement
debug logs that were flooding the console during call sessions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add online/offline/idle presence dots next to verification shields in
both the room members drawer and the common-settings members list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a collapsible "Sessions" section to the user profile card that
appears when cross-signing is active and the profile belongs to another
user. Each session shows a colour-coded shield (green = verified, yellow
= unverified) and a "Verify" button for unverified devices that
initiates the SAS emoji flow via crypto.requestDeviceVerification.
New hook useOtherUserDevices fetches the target user's device list via
crypto.getUserDeviceInfo and reacts to CryptoEvent.DevicesUpdated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spaces and unencrypted rooms have hasEncryptionStateEvent() = false,
causing all badges to be hidden. Cross-signing verification is a user
identity property, not room-specific — show badges whenever
crossSigningActive, keep the E2EE banner gated on isEncrypted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract MemberVerificationBadge into a shared component and render it in:
- UserRoomProfile: shield badge beside the display name on the profile card
- common-settings Members: badge next to each member in the room/space
settings members page (accessible from the lobby)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add VoiceMessageRecorder component: mic button in composer toolbar,
live waveform + timer, preview before send, MSC3245-compliant content
(org.matrix.msc3245.voice, org.matrix.msc1767.audio with waveform),
E2EE support via encryptFile before upload
- Add useUserVerifiedStatus hook: uses crypto.getUserVerificationStatus,
reacts live to CryptoEvent.UserTrustStatusChanged
- MembersDrawer: show green/yellow shield badge per member in encrypted
rooms (cross-signing verified/unverified), E2EE status banner in header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- RenderMessageContent: add case for m.key.verification.request msgtype
so it renders an informational card instead of "Unsupported message"
- MsgTypeRenderers/FallbackContent: add VerificationRequestContent and
MessageVerificationRequestContent components (lock icon + instructional text)
- DeviceVerification: remove isSelfVerification guard from
ReceiveSelfDeviceVerification so cross-user verification requests also
trigger the SAS emoji dialog (was silently dropped before)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Auto-fixed by prettier --write. Patch scripts used in the previous session
wrote code without running the formatter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Document the CI/CD pipeline: edit locally in /root/code/cinny, commit and
push to origin/lotus, Gitea Actions builds (~11 min), webhook triggers
lotus_deploy.sh which gates on CI pass before deploying to /var/www/html/.
Note that LXC credential is read-only; pushes require manual auth from dev box.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CallEmbed: inject :root { color-scheme } into iframe so EC respects Cinny
theme regardless of OS preference (fixes white background in dark mode)
- CallEmbed: store themeKind, update color-scheme CSS on live setTheme() calls
- CallEmbed: catch transport.send() rejection in setTheme() to prevent
unhandled promise rejection when widget not ready yet (fixes REACT-8)
- CallEmbed: html + body both set to background:none so wallpaper shows through
- CallEmbedProvider: apply chatBackground wallpaper style to call embed
container in full-view mode (not PiP) -- wallpapers carry over to calls
- useCallEmbed: pass themeKind through to CallEmbed constructor
- index.tsx: ignoreErrors: [Request timed out] to suppress matrixRTC
heartbeat timeouts (REACT-9) from Sentry noise
- README: document 0.19.4, positioning fix, dark mode fix, wallpaper,
millify Rolldown interop fix, Sentry noise filter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- millify.ts: use named import { millify as millifyPlugin } instead of
default import to fix Rolldown CJS interop bug where zc.default gets
set to the whole module object instead of the function (mode=1 forces
default=n instead of default=n.default, breaking MembersDrawer)
- useCallEmbed.ts: use getBoundingClientRect() for accurate fixed
positioning; add useEffect to trigger syncCallEmbedPlacement on mount
so embed is positioned before the first resize event
- CallEmbedProvider.tsx: fix [pipMode, callVisible] effect to NOT clear
top/left/width/height when callVisible changes (previously cleared
position set by syncCallEmbedPlacement every time joined changed);
only clear pip-specific styles when actually exiting pip; add debug
console logging for positioning state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the listenAction wrapper only called preventDefault() to stop
the switch default from firing an error, but it never sent a reply.
The widget transport would then wait for a response until it timed out.
Now the wrapper also calls transport.reply(ev.detail, {}) to return an
immediate success, fixing io.element.join, io.element.device_mute, and
set_always_on_screen.
The base WidgetDriver throws Failed to override function for these
methods. ClientWidgetApi routes update_delayed_event widget actions to
cancelScheduledDelayedEvent, restartScheduledDelayedEvent, or
sendScheduledDelayedEvent. Without these overrides every delayed-event
refresh from element-call fails, causing MembershipManager to drop the
call after retries.
Also make listenAction auto-call preventDefault so io.element.join and
other custom widget actions return success. Add set_always_on_screen
handler so element-call PiP requests are acknowledged.
The glob pattern dist/* preserved the full node_modules/... path
when copying to public/element-call/, resulting in only a nested
node_modules directory being deployed (causing 404 on index.html).
Switching to a directory src with rename lets the plugin copy the
dist folder wholesale as public/element-call/.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- RoomTimeline.tsx: add eslint-disable comment for intentional eventsLength
dep on timelineSegments useMemo (needed to detect in-place timeline mutations)
- Remove ~47 stale eslint-disable-next-line comments across 28 files for rules
that are now off in the flat config (no-param-reassign, jsx-a11y/media-has-caption,
react/no-array-index-key, etc); run prettier to reformat
- vite.config.js: move manualChunks from rollupOptions.output to
rolldownOptions.output so Rolldown (Vite 8) actually applies it; main bundle
drops from 3.5 MB to 814 kB gzip-248 kB, matrix-sdk gets its own 1.16 MB
cacheable chunk
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Lazy-import CreateRoomForm/CreateSpaceForm in CreateRoom.tsx and Create.tsx
so create-room and create-space get their own chunks; eliminates
INEFFECTIVE_DYNAMIC_IMPORT warnings
- Add RouteError component wired to root route errorElement so crashes show
a reload button instead of React Router dev screen
- ci.yml: use secrets.SENTRY_AUTH_TOKEN so source maps upload on CI builds
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix timelineSegments useMemo stale cache: the Perf-5 optimization used
timeline.linkedTimelines as its only dep, but that reference never changes
when events are added in-place; adding eventsLength as a dep makes it
recompute on every new live event so the binary search always finds the
new item
- Add LobbySkeleton: shimmer placeholder for space lobby (header + hero +
room list rows) shown while the Lobby chunk lazy-loads
- Add AuthSkeleton: shimmer placeholder for auth pages (logo + server
picker + form fields) shown while AuthLayout chunk lazy-loads
- Wire both into Router.tsx fallback props
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Automated cleanup removed const mx = useMatrixClient() from 3 more components
that use it (MessagePinItem, Message, Event) in addition to the 2 fixed in
the previous hotfix. Root cause: the cleanup script used substring matching
on indentation which removed declarations at any indent level, not just the
one targeted unused variable.
All 5 components that call mx.* now have their declarations restored.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The automated unused-var cleanup incorrectly removed const mx = useMatrixClient()
from MessageDeleteItem and ReportMessage components in Message.tsx. Both components
use mx inside their useCallback closures (mx.redactEvent, mx.reportEvent). This
caused a ReferenceError crash on the messages view in production.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Synapse does not yet ship MSC3786/MSC3914 as server-default push rules.
matrix-js-sdk patches them client-side every login and warns. Filter these
at console.warn level -- functionality is unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ServerConfigsLoader: skip validateAuthMetadata when getAuthMetadata()
rejects (404 on /auth_issuer means server uses traditional SSO, not
native Matrix OIDC/MAS - this is expected and should not log errors)
- Router: use HydrateFallback={() => null} instead of hydrateFallbackElement={null}
so react-router v7 counts it as truthy and suppresses the spurious warning
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove @esbuild-plugins/node-globals-polyfill (redundant since Vite 8
rolldownOptions.define handles globalThis). Add rolldownOptions.checks
to suppress PREFER_BUILTIN_FEATURE until Vite exposes output in rolldownOptions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GHSA-qjx8-664m-686j: prototype hijack in js-cookie <= 3.0.5 used
transitively via react-use in @giphy/react-components.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- react 18.2.0 to 19.2.6
- react-dom 18.2.0 to 19.2.6
- @types/react 18.2.39 to 19.2.15
- @types/react-dom 18.2.17 to 19.2.3
React 19 breaking changes fixed:
- useRef<T>(null) now returns RefObject<T | null>; cast to
RefObject<T> at 16 component call sites (safe, runtime unchanged)
- useRef<T>() without arg no longer valid; add | undefined>(undefined)
in useDebounce, useFileDrop, useThrottle, useVirtualPaginator hooks,
RoomInput, RoomTimeline, and ClientNonUIFeatures
- useReducer<typeof reducer> 1-arg form removed; drop explicit type arg
in useForceUpdate (inferred from reducer function)
- global JSX namespace removed; import type { JSX } from react in
react-custom-html-parser.tsx
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- eslint 8.57.1 to 9.39.4
- @typescript-eslint/eslint-plugin 7.18.0 to 8.59.4
- @typescript-eslint/parser 7.18.0 to 8.59.4
- globals 11.12.0 to 17.6.0
- @eslint/eslintrc and @eslint/js added for FlatCompat
- Replace .eslintrc.cjs + .eslintignore with eslint.config.mjs
- Use flat configs for react, react-hooks, typescript-eslint directly
- FlatCompat only for airbnb-base (no flat config support yet)
- Fix no-unused-vars override from airbnb and react/display-name: off
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- vite 6.4.2 to 8.0.14
- @vitejs/plugin-react 5.2.0 to 6.0.2
- Migrate optimizeDeps.esbuildOptions to rolldownOptions (Vite 8 uses rolldown)
- Remove @esbuild-plugins/node-globals-polyfill (no longer needed)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- typescript 5.9.3 to 6.0.3
- moduleResolution Node to bundler (correct for Vite projects)
- target/lib ES2016 to ES2020 (enables flatMap, Promise.allSettled)
- Fix global to globalThis in initMatrix.ts (browser env)
- Fix EventEmitter default to named import in CallControl.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@giphy/react-components@10.1.2 imports noUUIDRandom from @giphy/js-util,
which was only added in 5.x. Previously the uuid override forced uuid@14
into js-util@4.4.2 breaking the noUUIDRandom export. Pin js-util@5.2.0
directly and drop the uuid override (moderate severity, not high).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolves all TS2345/TS2347/TS7006 type errors introduced by stricter TypeScript 5.x.
Fix Icons.Settings to Icons.Setting, cast account data returns, fix implicit any.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a new deploy lands while a tab is open, lazy-loaded chunks (like
GifPicker) disappear because their content-hash filename changes. Vite
dispatches a vite:preloadError event in this case. We reload once and
clear the flag on successful load so future deploys can trigger again.
Icons.Settings is undefined in folds v2.6.2; only Icons.Setting exists.
This caused TypeError: i is not a function when rendering m.room.join_rules
or m.room.guest_access state events in the room timeline, crashing DMs with
those events visible in the initial view.
react-router v7's generatePath() now calls encodeURIComponent() on all
path params. pathUtils.ts was also calling encodeURIComponent() before
passing to generatePath, resulting in double-encoding (e.g. '#' became
'%2523' instead of '%23').
This caused spaces/rooms with alias paths to receive double-encoded
room IDs from useParams(), which were then re-encoded by matrix-sdk
when making HTTP requests (400 Bad Request from Synapse).
Remove the manual encodeURIComponent() calls -- generatePath handles it.
- Fix prettier formatting in useCall.ts and initMatrix.ts (unblocks CI)
- Fix viteStaticCopy stripBase so manifest.json and public/locales/ land
at correct output paths (was getting extra 'public/' prefix from v4 path
preservation behavior)
- Silence react-router v7 HydrateFallback warning on root route (SPA has
no SSR hydration, null is intentional)
1.6.0 did not export SearchContextManager/SearchContext/SearchBar,
causing React error #130 (element type undefined) when opening GifPicker.
5.9.4 uses @emotion (not styled-components), supports React 16-18, and
exports all required components. Downgrade @giphy/js-fetch-api to 4.2.2
to match the peer dep range.
When matrix-sdk is briefly upgraded then reverted, the local IndexedDB
schema version is higher than the SDK expects. Detect the VersionError
DOMException and show a clear 'Clear local data and reload' button
instead of a cryptic error message.
@giphy/react-components@10.x calls styled-components internals
(mergeAttributes) that do not exist in styled-components v6 — crashes
on open. Reverted to 1.6.0 until giphy publishes a v6-compatible release.
WelcomePage: remove Sentry test button (verified working), rename
Support -> Lotus Matrix Guide.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All newly flagged high-severity packages (lodash, js-cookie) are either
in dev-only tools (commitizen) or tree-shaken out of the deployed bundle
(react-use/js-cookie is unused). Zero deployed-bundle impact confirmed.
Being 9 major versions behind accumulates migration debt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
lodash >= 4.18.0 patches prototype-pollution (GHSA-f23m-r3pf-42rh) and
code-injection (GHSA-r5fr-rjxr-66jc) used by slate-dom/slate-react in
the deployed bundle.
Attempted @giphy/react-components@10.1.2 upgrade but it pulled in new
high-severity lodash and js-cookie vulns — net regression, reverted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- dompurify updated to 3.4.5 to fix 7 XSS/prototype-pollution CVEs
- emojibase-data added to manualChunks: splits 856 kB out of the main
bundle, reducing it from 1.8 MB to 932 kB
- husky prepare script updated from deprecated "husky install" to "husky"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace broken vite-plugin-static-copy target for pdf.worker with a
custom closeBundle plugin that copies the file directly to dist root.
Also uninstall vite-plugin-top-level-await which was removed from
vite.config.js in the previous commit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prettier: auto-formatted 103 files to fix baseline. Prettier check in CI
is now a hard gate (removed continue-on-error).
Brotli: installed libnginx-mod-http-brotli-filter/static. Enabled in nginx
with brotli_static on for pre-compressed assets and comp_level 6.
Sentry releases: deploy script now exports VITE_APP_VERSION=<git-short-sha>
before building so each Sentry release maps to an exact commit.
CI also passes github.sha as VITE_APP_VERSION.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Build is the only hard gate. TS/ESLint/Prettier/audit run as informational
checks (continue-on-error) since the codebase has pre-existing issues from
matrix-js-sdk type incompatibilities and upstream formatting.
Bundle size table is written to the job summary after every build so regressions
are visible without digging into logs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Runs npm ci + npm run build on every push to lotus and on PRs.
Marks commit as failed if the build breaks — gives early feedback
before the webhook deploy script also catches it.
Source map upload skipped in CI (deploy script handles that).
npm audit runs informational-only (continue-on-error) since known
vulns require upstream fixes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RoomSkeleton: shimmer skeleton matching Room header/timeline/input layout,
used as Suspense fallback for all three Room routes (home/direct/space)
Sentry source maps: @sentry/vite-plugin uploads 72 hidden source map files
to Sentry on each build then deletes them from dist — stack traces now show
real file/line numbers instead of minified bundle positions.
Auth token loaded from /etc/lotus-deploy.env (not in git).
Auto-deploy: webhook receiver on port 9001, nginx proxies
/hooks/lotus-deploy, HMAC-SHA256 verified, triggers on lotus branch push.
Deploy script: git reset --hard + npm ci + npm run build + rsync to webroot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
browserTracingIntegration injects sentry-trace and baggage headers into all
outgoing fetch calls. Synapse does not list these in Access-Control-Allow-Headers,
so every Matrix API call was blocked by the browser CORS preflight check.
Removed browserTracingIntegration, set tracePropagationTargets:[] and
tracesSampleRate:0. Error capture (the useful part) is unaffected.
CSP fix (Sentry ingest domain) is applied via nginx — no code change needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Initialize Sentry SDK in index.tsx when VITE_SENTRY_DSN env var is set
- Wrap entire App with Sentry.ErrorBoundary (replaces the hard crash with a retry UI)
- 5% trace sample rate, sendDefaultPii disabled, strip events containing accessToken
- Add .env.production template with VITE_SENTRY_DSN placeholder
- Get your DSN from sentry.io -> Project Settings -> Client Keys
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add defensive check in folds Icon component so that if src is ever
undefined or non-function (root cause unknown, possibly data-dependent),
the SVG renders empty rather than throwing and crashing the whole app.
Also adds postinstall script to re-apply the patch after npm install.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes GHSA-q89c-q3h5-w34g: path traversal & URL injection via unsanitised
lng/ns parameters. Remaining open issues are all in devDependencies
(commitizen/lodash/tmp) or dev-server-only tools (esbuild/vite), with no
runtime impact on the production build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lobby, Explore/FeaturedRooms/PublicRooms, Inbox/Notifications/Invites are
now lazy-loaded via React.lazy so they only enter the bundle when navigated
to. Main bundle: 2547 kB → 2472 kB (gzip 637 → 618 kB).
Spoiler aria-pressed was initialised to false (revealed); changed to true
so the spoiler starts hidden, matching CSS logic (aria-pressed=true →
color:transparent).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously every visible Message subscribed to settingsAtom via useSetting,
creating O(80) active atom subscriptions. Now RoomTimeline reads it once
and passes it down as a prop, reducing subscriptions to 1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
C-1 complete sweep across all components and features:
- Call controls: mic mute/unmute, deafen/undeafen, video, screenshare, chat
- RoomInput: dismiss reply, attach file, sticker, emoji, GIF, location, toolbar
- Media viewers: close in image/pdf/text viewers and editors
- Settings dialogs: close buttons in all room/space/common settings panels
- Lobby: back, toggle member list, scroll to top, pack add/remove
- Auth: server picker, UIA flow cancel
- Upload cards: cancel uploads
- URL preview: prev/next buttons
- Members drawer: close + scroll to top
- RoomViewHeader: back, start call, toggle member list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Perf-3: Replace raw roomToUnreadAtom subscription in Home, Direct, Space with
selectAtom-derived Set<string> — components now only re-render when rooms
gain/lose unread presence, not on every notification count update
Perf-5: RoomTimeline eventRenderer now uses binary search on precomputed
timelineSegments instead of O(N×T) linear scan per visible message
A11y L-1: Add as=h2 semantic heading to Home, Direct, Inbox, Space page nav
titles so screen readers announce page sections correctly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BUG-18: clearTimeout cleanup in focusItem useLayoutEffect prevents leaked timers
BUG-24: Room timeline listener catches first poll vote before Relations object exists
BUG-25: Use timelineRef.current in handleOpenEvent to prevent stale index on rapid navigation
Perf-6: React.lazy + Suspense for GifPicker and EmojiBoard (initial bundle -114 kB)
Perf-7: React.memo on RoomNavItem to prevent re-renders on unrelated state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.
- Message.tsx: show delivery status (sending/sent/failed) on own messages when
no read receipts yet; hidden once server confirms (status null); TDS-styled
- GifPicker.tsx: move terminal CSS from runtime <style> tag into lotus-terminal.css.ts
eliminating flash of unstyled content (M-6)
- lotus-terminal.css.ts: add [data-gif-terminal] selector rules for GIF picker
PollContent now:
- Reads existing m.poll.response / org.matrix.msc3381.poll.response events
from the room timeline on mount to restore vote state across refreshes
- Counts votes per answer (per-sender latest-wins deduplication)
- Shows percentage bars and vote totals in real time
- Subscribes to RelationsEvent.Add/Remove/Redaction so counts update live
when other users vote without requiring a page reload
- Optimistic local update keeps the UI snappy while the send request flies
- CallEmbed: fix memory leak — mx event listeners were never removed
because dispose() called .bind(this) again, creating new function
objects. Now uses arrow class fields so start()/dispose() share the
exact same reference.
- callPreferences: toggleVideo is a no-op when cameraOnJoin=false,
preventing internal state drift from the returned value.
- CallControls: PTT key guard now blocks on SELECT elements and walks
the DOM for inherited contentEditable to prevent key interception
inside dropdowns and custom editors.
- RoomInput: GIF fetch validates Giphy CDN domain allow-list,
HTTP Content-Type header, and enforces 20 MB size cap.
These are superseded by IncomingCallListener in CallEmbedProvider (merged from v4.12.1). IncomingCallNotification was already removed from Router.tsx in a previous commit.
- ForwardMessageDialog: use sendEvent instead of sendMessage to preserve
the original event type (stickers, polls, etc.)
- PollContent: use m.selections for stable m.poll.response (per spec),
was incorrectly using m.responses
- Router.tsx: remove IncomingCallNotification (CallEmbedProvider.IncomingCallListener now handles all calls)
- CallEmbedProvider: restore touch drag (handlePipTouchStart), grip dots on resize handles, fix normaliseToTopLeft width/height
- FallbackContent/MsgTypeRenderers: add originalBody prop to show struck-through original text on deleted messages
- RoomTimeline: cache text message bodies so they can be shown after redaction
- Poll voting: PollContent sends m.poll.response on answer click
- Location: MLocation shows OSM map embed + share-location button in toolbar
- Image captions: caption field on media uploads sets message body
- Message forwarding: ForwardMessageDialog with searchable room picker
- Also includes ring timeout fix and earlier session patches
Bug fixes:
- IncomingCallNotification: track ring setTimeout ID and clear it on
cleanup/dismiss — prevents orphaned callbacks after unmount
- RoomTimeline: allow redacted m.room.message, m.room.encrypted and
m.sticker events past the early-return filter so they hit the
existing RedactedContent renderer showing the trash-icon placeholder
New features:
- PollContent component: read-only display of m.poll.start and
org.matrix.msc3381.poll.start events (both stable Matrix 1.7 and
MSC3381 unstable content keys); renders poll question + answer
options inside the standard Message bubble; registered both as
top-level event renderers and inside EncryptedContent callback
so encrypted polls also render after decryption
- Deleted message placeholder: m.room.message and m.room.encrypted
redacted events now show the existing MessageDeletedContent
component (trash icon + italic notice) instead of disappearing
entirely — matches Element, FluffyChat, Commet, Nheko behaviour
When another user starts a call in a DM room, show a fixed-position
notification with caller avatar, name, and Answer/Decline buttons.
A Web Audio API double-pulse ring tone plays until answered, declined,
or the 30-second auto-dismiss fires.
- useIncomingDmCall hook: listens to MatrixRTC SessionStarted/SessionEnded,
filters DM rooms (encrypted, ≤2 members), auto-dismisses after 30s,
stops if caller leaves or user joins another call
- IncomingCallNotification component: ring tone, caller info, themed UI
for LotusGuild Terminal TDS (navy bg, orange border, neon-green Answer)
and standard Cinny dark/light (CSS vars, folds Button Success/Critical)
- Router.tsx: mount IncomingCallNotification inside CallEmbedProvider
Add 4 corner resize handles to the PiP window (SE/SW/NE/NW).
Each handle shows a 3-dot grip indicator and sets the appropriate
resize cursor. Resize handles sit above the drag overlay (zIndex 2)
and stop propagation so they do not trigger drag-to-move.
On resize start the element is normalised to top/left positioning
so math is consistent regardless of whether bottom/right was active.
Minimum size 200x112px. Viewport clamped.
EC 0.19.3 changed the toolbar layout. The old previousElementSibling
traversal from the leave button pointed at wrong elements:
- settingsButton was finding the raise-hand button
- reactionsButton was finding the screenshare button
Fix: use stable selectors instead:
- settingsButton: data-testid=settings-bottom-center (new in EC 0.19.3)
- reactionsButton: [class*=raiseHand] (CSS module class, consistent in 0.19.x)
Drag the PiP window anywhere on screen to move it out of the way.
Click (without dragging) still navigates back to the call room.
5px movement threshold distinguishes drag from click.
Mouse and touch both supported. Cursor shows grab/grabbing cues.
- Camera no longer starts enabled when user disables it in prescreen
- When PTT mode is enabled, call starts muted so PTT works immediately
without requiring a manual mute first
- CallControlState also updated to match the forced-off audio for PTT
EC 0.16.x ignores io.element.device_mute for initial state at startup,
so audio= and video= URL params are the only reliable way to set the
initial device state before the call begins.
Check navigator.permissions for microphone state before the call starts.
If the user has blocked microphone access, disable the Join button and
show an inline message explaining how to fix it in browser settings.
Subscribes to permission change events so the UI updates if they grant
access without refreshing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove upstream Cinny homeserver list and set Lotus Guild homeserver
as the only default. Prevents deploying with wrong homeserver on fresh builds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Push to Talk: keydown/keyup binds mic to configurable key (default Space)
with visual PTT indicator and key-binding UI in Settings > General > Calls
- Camera always defaults OFF on join; cameraOnJoin setting for explicit opt-in
- Deafen button tooltip corrected to Deafen/Undeafen instead of Turn Off/On Sound
- Screenshare confirmation dialog before broadcasting to call participants
- Noise suppression toggle wired from settings through CallEmbed URL params
- CallControl.setMicrophone() public method for programmatic mic control
- Calls settings section added to General settings page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: Update featured communities in Explore
* Add new spaces and rooms to config.json
* Remove #pcapdroid room from configuration
* Update rooms list in config.json
Replaced '#archlinux:archlinux.org' with '#tuwunel:grin.hu' in rooms list.
* Update channel list in config.json
* add option to start video all in DM
* show speaker icon for dm's in call status name
* show call view if call is active in room
* add Atria call ringtone
* update element call and widget api
* add option to start voice/video call in dms
* only show call button if user have permission
* allow call widget to send call notification event
* show incoming call dialog and play sound
* fix call permission checks
* allow option to start call in all rooms
* send notification when starting call in non-voice rooms
* hide header call button from voice rooms
* prevent call join if call not supported and started by other party
* update call menu style
* show call not supported message on incoming call notification
* improve the incoming call layout
* video call with right click without opening menu
* allow call widget to fetch media url
* add webRTC missing error
* improve call permission label
---------
Co-authored-by: Krishan <33421343+kfiven@users.noreply.github.com>
- WelcomePage: use official Lotus.png instead of generated SVG
- Hex Grid background: proper pointy-top hexagons via SVG data URI (was
just triangles from linear-gradient trick)
- Boot sequence: Matrix-specific messages (TLS cert, E2EE Olm/Megolm,
cross-signing, media proxy, /help hint)
- Terminal mode CSS: nav right border, header bottom glow, kbd TDS key
style, abbr cyan underline, time amber color, img hover cyan outline,
explicit body color anchor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: do not attempt to join call on doubleclick if missing permissions
* update comment
* export getPowersLevelFromMatrixEvent for usage elsewhere
* only read vc permissions when actually needed instead of reactively
* chore: add matrixrooms.info to directory list
matrixrooms.info is a directory of all public Matrix rooms it can find,
regardless of homeserver. It is much larger than the morg directory,
so is more useful as a general search
* chore: install deps related to semantic release
* chore: add husky config
* ci: add a script to update version number on new release
* ci: update ci/cd to include semantic release changes
* chore: merge dev to semantic-release
Allow using filenames in codeblocks
- If there is a dot in the language name, we instead treat the first line after ``` as the filename and everything after the last dot as the language
- we use a custom "data-label" attribute on the code block to specify the name of the file (so only compatible with cinny from this point onwards)
* Show large image overlay when clicking url preview thumbnail
* Move image overlay into its own component
* Move ImageOverlay props into extended type
* Remove export for internal type
> Please read through [the Discussion rules](https://github.com/cinnyapp/cinny/discussions/2653) and check for both existing [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc) prior to opening a new Discussion.
- type:markdown
attributes:
value:'# Issue Details'
- type:textarea
attributes:
label:Issue Description
description:|
Provide a detailed description of the issue. Include relevant information, such as:
- The feature or configuration option you encounter the issue with.
- Screenshots, screen recordings, or other supporting media (as needed).
- If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR, commit) in your description.
placeholder:|
When I try to send a message in a room, the message doesn't appear in the timeline.
OR
The application crashes when I click on the settings button.
validations:
required:true
- type:textarea
attributes:
label:Expected Behavior
description:|
Describe how you expect Cinny to behave in this situation.
placeholder:|
I expected the message to appear in the room timeline immediately after sending.
OR
The settings panel should open smoothly without any crashes.
validations:
required:true
- type:textarea
attributes:
label:Actual Behavior
description:|
Describe how Cinny actually behaves in this situation. If it is not immediately obvious how the actual behavior differs from the expected behavior described above, please be sure to mention the deviation specifically.
placeholder:|
The application freezes for 3 seconds and then shows a white screen.
validations:
required:true
- type:textarea
attributes:
label:Reproduction Steps
description:|
Provide a detailed set of step-by-step instructions for reproducing this issue.
placeholder:|
1. Open Cinny and log in to my account
2. Navigate to the #general room
3. Type a message in the message box
4. Press Enter to send
5. Notice that the message doesn't appear in the timeline
validations:
required:true
- type:textarea
attributes:
label:Environement
description:|
Please provide information about your environment. Include the following:
- OS:
- Browser:
- Cinny Web Version: (app.cinny.in or self hosted)
- Cinny desktop Version: (appimage or deb or flatpak)
- Matrix Homeserver:
placeholder:|
- OS: Windows 11
- Browser: Chrome 120.0.6099.109
- Cinny Web Version: 3.2.0 (app.cinny.in or self hosted)
- Cinny desktop Version: 3.2.0 (appimage or deb or flatpak)
- Matrix Homeserver: matrix.org (Synapse 1.97.0)
render:text
validations:
required:true
- type:textarea
id:logs
attributes:
label:Relevant Logs
description:|
If applicable, add browser console logs to help explain your problem.
**To get browser console logs:**
- Chrome/Edge: Press F12 → Console tab
- Firefox: Press F12 → Console tab
- Safari: Develop → Show Web Inspector → Console
Please wrap large log outputs in code blocks with triple backticks (```).
placeholder:|
```
Error: Failed to send message
at MessageComposer.sendMessage (composer.js:245)
at HTMLButtonElement.onClick (composer.js:189)
TypeError: Cannot read property 'content' of undefined
at RoomTimeline.render (timeline.js:567)
```
render:shell
validations:
required:false
- type:textarea
attributes:
label:Additional context
description:|
Add any other context about the problem here (e.g., when did this start happening, does it happen on different homeservers, etc.)
placeholder:|
- This started happening after I updated to version 3.2.0
- It only happens in encrypted rooms, not in public rooms
- I've tried on both Firefox and Chrome with the same result
- It works fine on my phone using the same account
- This happens on all homeservers I've tested (matrix.org, mozilla.org)
validations:
required:false
- type:markdown
attributes:
value:|
# User Acknowledgements
> [!TIP]
> Use these links to review the existing Cinny [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc).
- type:checkboxes#add faqs in future
attributes:
label:'I acknowledge that:'
options:
- label:I have searched the Cinny repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion.
required:true
- label:I have checked the "Preview" tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (` ``` `) on separate lines.
<!-- Please read https://github.com/ajbura/cinny/blob/dev/CONTRIBUTING.md before submitting your pull request -->
### Description
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
Fixes #
#### Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
### Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
@@ -5,6 +5,7 @@ First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
>
> - Star the project
> - Tweet about it (tag @cinnyapp)
> - Refer this project in your project's readme
@@ -18,7 +19,8 @@ Bug reports and feature suggestions must use descriptive and concise titles and
## Pull requests
> ### Legal Notice
>When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
>
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. You will also be asked to [sign the CLA](https://github.com/cinnyapp/cla) upon submiting your pull request.
**NOTE: If you want to add new features, please discuss with maintainers before coding or opening a pull request.** This is to ensure that we are on same track and following our roadmap.
@@ -26,9 +28,9 @@ Bug reports and feature suggestions must use descriptive and concise titles and
Example:
|Not ideal|Better|
|---|----|
|Fixed markAllAsRead in RoomTimeline|Fix read marker when paginating room timeline|
<img alt="Follow on Mastodon" src="https://img.shields.io/mastodon/follow/106845779685925461?domain=https%3A%2F%2Ffosstodon.org&logo=mastodon&style=social"></a>
A Matrix client focusing primarily on simple, elegant and secure interface. The main goal is to have an instant messaging application that is easy on people and has a modern touch.
Deployed at [chat.lotusguild.org](https://chat.lotusguild.org).
## Getting started
The web app is available at [app.cinny.in](https://app.cinny.in/) and gets updated on each new release. The `dev` branch is continuously deployed at [dev.cinny.in](https://dev.cinny.in) but keep in mind that it could have things broken.
---
You can also download our desktop app from the [cinny-desktop repository](https://github.com/cinnyapp/cinny-desktop).
## Changes from upstream Cinny
## Self-hosting
To host Cinny on your own, simply download the tarball from [GitHub releases](https://github.com/cinnyapp/cinny/releases/latest), and serve the files from `dist/` using your preferred webserver. Alternatively, you can just pull the docker image from [DockerHub](https://hub.docker.com/r/ajbura/cinny) or [GitHub Container Registry](https://github.com/cinnyapp/cinny/pkgs/container/cinny).
### Branding & Identity
* The default homeservers and explore pages are defined in [`config.json`](config.json).
- 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
* You need to set up redirects to serve the assests. Example configurations; [netlify](netlify.toml), [nginx](contrib/nginx/cinny.domain.tld.conf), [caddy](contrib/caddy/caddyfile).
* If you have trouble configuring redirects you can [enable hash routing](config.json#L35) — the url in the browser will have a `/#/` between the domain and open channel (ie. `app.cinny.in/#/home/` instead of `app.cinny.in/home/`) but you won't have to configure your webserver.
### LotusGuild Terminal Design System (TDS) v1.2
* To deploy on subdirectory, you need to rebuild the app youself after updating the `base` path in [`build.config.ts`](build.config.ts).
* For example, if you want to deploy on `https://cinny.in/app`, then set `base: '/app'`.
A full custom theme engine layered on top of Cinny's vanilla-extract theming:
<details><summary><b>PGP Public Key to verify tarball</b></summary>
- All patterns use CSS custom properties — adapt to both TDS dark and light themes
- Settings toggle for showing per-message sender profiles
## Local development
> [!TIP]
> We recommend using a version manager as versions change very quickly. You will likely need to switch between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Recommended nodejs version is Krypton LTS (v24.13.1).
### Voice / Video Call Improvements
Execute the following commands to start a development server:
```sh
npm ci # Installs all dependencies
npm start # Serve a development version
- **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.
### 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, 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`.
### 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:
- 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).
- **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.
### 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.
---
## Build
```bash
npm ci
npm run build # outputs to dist/
```
To build theapp:
```sh
npm run build # Compiles the app into the dist/ directory
NODE_OPTIONS=--max_old_space_size=6144 npm run build
```
### Running with Docker
This repository includes a Dockerfile, which builds the application from source and serves it with Nginx on port 80. To
use this locally, you can build the container like so:
## 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.
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/`):
```json
{
"defaultHomeserver":0,
"homeserverList":["matrix.lotusguild.org"],
"allowCustomHomeservers":false,
"gifApiKey":"<giphy_key>"
}
```
This will forward your `localhost` port 8080 to the container's port 80. You can visit the app in your browser by navigating to `http://localhost:8080`.
## 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 |
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
content="Lotus Chat — the Lotus Guild Matrix client. Secure, fast, and built for our community."
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.