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