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