API fix: delay was embedded in the path string causing 404 — moved to
proper query param object; prefix changed from '' to the full MSC4140
unstable prefix so authedRequest builds the correct URL. Cancel and
restart endpoints fixed the same way.
Date/time picker: replaced single datetime-local (hard to use time
portion) with separate date + time inputs side by side; colorScheme:'dark'
hints the browser to render calendar/clock popups in dark mode to match
the app. Preview row now shows as a styled Primary.Container chip with
clock icon. Relative time ("in 2h 15m") shown below the label.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced full-width sharp-cornered flat block with a compact rounded
notice: R300 border-radius, 3px amber left border accent, slight side
margins (S300) and bottom gap (S100) so it sits naturally above the
composer without looking jarring.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Schedule message: modal now always opens (even with empty composer);
includes its own message textarea pre-filled from editor content;
removed null-content early-return guard from handleScheduleClick;
fixed error text to use color.Critical.Main not CSS var
Image compression: removed 200KB size threshold — checkbox now shows
for all JPEG/PNG uploads (not just large ones); 'no significant saving'
message handles already-small files gracefully
Activity log: auto-paginate on mount — state events are absent from
initial sync window, so the log was always empty until Load More was
clicked manually
Insights summary: Text size H4→H5 with toLocaleString() formatting and
overflow:ellipsis so large numbers don't push tiles off screen
Bookmarks panel: replaced var(--bg-*) CSS vars (undefined in folds
themes) with color.Surface/SurfaceVariant/Primary folds tokens; added
left accent border on message preview block, count badge, clear button
in search, improved empty state, cleaner button hierarchy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P3-1: Message Bookmarks — right-click any message to bookmark; saved to
io.lotus.bookmarks account data (max 500, syncs across devices); star
icon in sidebar opens BookmarksPanel with filter, Jump-to-message, and
remove buttons; reactive to AccountData events
P3-2: Message Scheduling (MSC4140) — clock button next to send opens
ScheduleMessageModal with datetime-local picker; validates ≥1 min future;
calls PUT org.matrix.msc4140 delayed event API; collapsible
ScheduledMessagesTray above composer lists pending messages with cancel;
local Jotai atom tracks scheduled messages per room
P3-3: File Upload Compression — opt-in checkbox per JPEG/PNG file ≥200KB
in upload preview; canvas API compresses at 0.82 quality; shows before/
after size estimate; compressed blob used in upload when checked
P3-7: Room Insights — new Insights tab in room settings; top 5 active
members (bar chart), top 5 reactions (chips), media breakdown (4 tiles),
24-hour activity heatmap (CSS bar chart); all from local cache only with
disclaimer banner; never the first tab shown
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>
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>
- 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>
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>
- 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>
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>
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>
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>
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>
- 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 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>
- 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>
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>
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>
- 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>
- 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>