Icons.BlockCode is the {} curly-braces icon, which is unintuitive for a
QR code toggle. No QR-specific icon exists in folds, so replace the
IconButton with a labeled Button ("QR Code") that clearly communicates
its purpose. Also fix var(--bg-surface-border) → color.Surface.ContainerLine
in the QR panel border (the CSS variable doesn't exist in Cinny's theme).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
The modal was built with raw <div>/<input> and CSS custom properties
(--bg-surface, --bg-surface-low, --tc-surface-high, etc.) that don't
exist in Cinny's vanilla-extract theme, causing invisible/unstyled
inputs and a transparent background.
Rewrite to match the ReportRoomModal pattern:
- Overlay + OverlayBackdrop + OverlayCenter for the backdrop
- FocusTrap (clickOutsideDeactivates, escapeDeactivates via stopPropagation)
- Box as="form" with color.Surface.Container background and color.Other.Shadow
- Header variant="Surface" for the title bar
- folds Input variant="Background" for all text fields (replaces raw <input>)
- color.Critical.Main for error text
- Spinner before prop on submit button while submitting
- All spacing/radii from config.* tokens
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
var(--bg-surface-variant) and var(--border-surface-variant) are not
defined in Cinny's vanilla-extract theme, so the select element renders
with a transparent/white background. Also native <option> elements ignore
most inherited CSS.
Fix: use folds color tokens (color.SurfaceVariant.*) for background,
border, and text color; add colorScheme:'dark' so the browser renders
the OS popup with dark styling; apply background+color to <option>
elements as a belt-and-suspenders fallback for browsers that honour it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New features:
- Video playback: lightbox renders <video controls autoPlay> for MsgType.Video;
thumbnail tiles show a play-button badge overlay; LightboxImage renamed to
LightboxMedia to handle both types.
- Auto-load on scroll: IntersectionObserver on a sentinel div replaces the
manual "Load More" button. Detaches while loading or when history is exhausted.
- Month separators: image/video grid grouped by month ("June 2026", etc.) with
a hairline divider; separator only shown when more than one month is present.
Bugs fixed by code review:
- flatIdx++: index was incremented before the !thumbMxc null-guard, causing
tiles rendered after a skipped event to open the wrong lightbox item. Guard
is now checked first; flatIdx only increments when a tile actually renders.
- lightboxIndex never reset on tab switch: stale index kept the lightbox open
(or opened the wrong item) after switching tabs. handleTabChange() now calls
setLightboxIndex(null) alongside setTab().
- Silent catch retry storm: pagination errors left canLoadMore=true, causing
the IntersectionObserver to re-fire handleLoadMore on every render cycle
when the sentinel was still visible. Error state now sets loadError=true,
removes the sentinel, and shows a manual Retry button instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause of padlock: all images in E2EE rooms have content.file so the
gallery skipped thumbnails for all of them. Fix:
- useDecryptedMediaUrl hook: downloads + decrypts encrypted media using
downloadEncryptedMedia/decryptFile, creates a blob URL, revokes on
unmount. For unencrypted media returns the HTTP URL directly.
- GalleryTile: prefers content.info.thumbnail_file (smaller encrypted
thumb) over content.file; falls back gracefully. Shows spinner while
decrypting, broken-image icon on error. Hover overlay shows sender
name + relative date with a gradient.
- Lightbox: full-screen overlay with ← → keyboard/button navigation,
filename/sender/date header, image counter. Full-res decryption done
in LightboxImage (separate component per item so keys reset the hook).
- File list: shows sender name + file size (formatted KB/MB).
- Empty states: distinct messages for "nothing in recent events" vs
"nothing found after loading more". "Beginning of history" shown when
pagination exhausts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Suppress Ctrl+P browser print dialog via SuppressPrintShortcut in
ClientNonUIFeatures (no UI opened, just preventDefault)
- mxcUrlToHttp: build URL manually instead of delegating to SDK.
The SDK forces allow_redirect=true when useAuthentication=true;
Synapse's /_matrix/client/v1/media/thumbnail rejects that with 400.
Manual construction omits allow_redirect entirely.
- Gallery: redesign using folds color tokens (color.Surface.*) instead
of non-existent CSS custom properties; add ThumbState so broken
images show an icon placeholder; use useAuthentication for thumbnails
now that the URL builder is fixed; "Load More" always visible.
- PollCreator: replace raw <button> with folds Button components so the
Single/Multiple choice toggle renders with actual visual difference.
- PollContent: support multiple-choice polls end-to-end —
myVote:string → myVotes:Set<string>; computeVotes collects all
m.selections (not just [0]); toggle-select for multi, radio for
single; checkbox/radio indicator icons next to each option;
"◉ Poll · Multiple choice" / "Single choice" label in header;
sends full selections array on every vote event.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
InviteUserPrompt: add QR code toggle button (Icons.BlockCode) in header.
When toggled, shows a 180x180 QR code image (api.qrserver.com) and the
raw invite URL below it, between the header and the search form.
inviteUrl computed once and shared between Copy Link and QR display.
Server: added https://api.qrserver.com to img-src in CSP header on
LXC 106 (/etc/nginx/sites-available/cinny) — nginx reloaded.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use useAuthentication=false for thumbnail requests: the v1 authenticated
URL adds allow_redirect=true which Synapse rejects with 400
- Encrypted events (content.file set) show a lock+filename placeholder
since server can't thumbnail encrypted blobs
- Unencrypted thumbnails add onError handler to hide broken images gracefully
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The existing SearchModalRenderer (Ctrl+K) is already a polished room/DM
switcher with avatars, unread badges, fuzzy search, and keyboard nav.
Our QuickSwitcher was an inferior duplicate. Removing it entirely:
- Delete QuickSwitcher.tsx
- Remove QuickSwitcherFeature from ClientNonUIFeatures
- Remove quickSwitcherKey from settingsAtom
- Remove Keyboard Shortcuts section from General settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PollCreator: replace maxSelections/options.length stale-closure pattern
with isMultiple: boolean state. max_selections computed from filledOptions
at submit time. Radio inputs replaced with styled toggle buttons that
visually highlight the active selection.
PollContent: catch getPendingEvents error (Sentry JAVASCRIPT-REACT-N).
SDK throws Cannot call getPendingEvents with pendingEventOrdering ==
chronological when sending poll vote events with m.reference relation.
Silently catch so optimistic UI update stands — vote will retry on next
sync if needed.
Fixes JAVASCRIPT-REACT-N
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- RoomNavItem: change isEncrypted() to isDecryptionFailure() so DM
previews show actual message body for successfully decrypted E2EE
events instead of always showing 'Encrypted message'
- Home.tsx / Direct.tsx: upgrade filter inputs to size 400 / radii 400
with search icon prefix to match the members list search bar style
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MediaGallery: switch from createMessagesRequest (returns raw encrypted
events) to room.getLiveTimeline().getEvents() which gives already-
decrypted MatrixEvent objects. Load More uses paginateEventTimeline().
- QuickSwitcher: change hotkey from Ctrl+K to Ctrl+P to avoid conflict
with the existing SearchModalRenderer mod+k handler
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P1-5: Voice message playback speed toggle (0.75×/1×/1.5×/2×) in AudioContent.tsx
P1-10: Private read receipts toggle in Privacy settings; wired to notifications.ts
P1-3: Room filter input on Home tab and DMs tab (client-side, clears on tab switch)
P1-8: Favorite rooms via m.favourite tag — Favorites section in Home sidebar, star/unstar in right-click menu
P1-9: Room invite link + QR code in room settings (Share Room tile, api.qrserver.com QR)
P1-6: Poll creation modal in composer (PollCreator.tsx, sends m.poll.start)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Convert TypingIndicator named function expression to arrow function to
satisfy prefer-arrow-callback lint rule. Auto-format RoomInput.tsx to
resolve Prettier check failures in CI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Server notice rooms were falling through to the default hash/lock icon
in the room list. Now return Icons.Warning before any other type checks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mEvent.getContent() returns post-edit content because matrix-js-sdk applies
the latest replace event in-memory. Reading mEvent.event.content gives the
raw server content (the true original before any edits). Edit entries from
the relations API correctly use m.new_content per Matrix spec.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- nginx (LXC 106, live): added https://*.giphy.com to connect-src CSP —
browser was blocking fetch() to media2.giphy.com CDN with CSP violation
- EditHistoryModal: render formatted_body as sanitized HTML (via
html-react-parser + sanitizeCustomHtml) with linkification for plain
text, matching how messages render in the timeline
- useAsyncCallback + ThumbnailContent + ImageContent + VideoContent +
ClientConfigLoader: use .catch(() => undefined) instead of void to
silence unhandled promise rejections from fire-and-forget useEffect
calls — errors already captured in AsyncState.Error for UI display
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
useAsync re-throws errors after storing them in state — correct for awaited
callers but causes unhandled rejections when load() is called without .catch()
in useEffects. The error is already captured in AsyncState.Error so the
re-throw provides no additional value in these fire-and-forget patterns.
Fixes JAVASCRIPT-REACT-M (Sentry: Media download failed: 401 Unauthorized)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- RoomInput: GIF domain check uses endsWith('.giphy.com') — handles all
Giphy CDN shards; all silent failure paths now show user-facing error
- RoomProfile: topic plain-text field is now HTML-stripped (no markdown
syntax visible in header); formatting toolbar (B/I/S/code) above the
textarea wraps selected text with correct markdown syntax
- InviteUserPrompt: Copy Link button added to dialog header with
'Copied!' confirmation; removed Copy Link from both three-dot menus
- RoomViewHeader/RoomNavItem: unused copy-link imports removed
- nginx (live): support_page URL updated from lotusguild.org →
matrix.lotusguild.org
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- EditHistoryModal: use raw fetch with /_matrix/client/v1/ path for
relations API — Synapse only supports relations at v1, not v3 (fixes
Sentry JAVASCRIPT-REACT-K / 404 error)
- RoomViewHeader: remove useReportRoomSupported spec-version gate —
Synapse 1.114+ has the endpoint but only advertises spec v1.12;
button now always shows for non-creator, non-server-notice rooms
- ReportRoomModal: handle M_UNRECOGNIZED/404 with "not supported by
your homeserver" message
- RoomNavItem: add isServerNotice guard to Room Settings in sidebar
context menu (was only guarded in header three-dots menu)
- initMatrix: bump setMaxListeners from 50 → 150 to prevent
MaxListenersExceededWarning with large room lists
- RoomProfile: save topic with formatted_body + format when markdown
syntax is detected; add markdown hint below topic textarea
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users can right-click any room and 'Rename for me...' to set a local
display name visible only to them. Stored in account data under
io.lotus.room_names. Shows a pencil indicator on renamed rooms.
useLocalRoomName() hook overrides useRoomName() when a local name exists.
Also includes:
- Rich room topic rendering via RoomTopicContent object (formatted_body
support in RoomTopicViewer with HTML sanitization via sanitizeCustomHtml)
- Edit history viewer: clicking '(edited)' on a message opens a modal
showing all prior versions with timestamps (EditHistoryModal.tsx)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Settings Help/About fetches /.well-known/matrix/support and displays
admin contact + support page link (graceful 404 degradation)
- Server notice rooms (m.server_notice) now show a Warning badge in the
room header and hide the message composer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Report Room: new ReportRoomModal with reason + category, POST /rooms/{id}/report
- URL preview: encUrlPreview default changed to true; security note added to settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass status_msg: '' explicitly on setOnline/setOffline/setUnavailable(idle)
so the Matrix server overwrites the 'dnd' status_msg left from DND mode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 af3155e1:
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>