PageHeader now exposes aria-label="{room name} room header" so screen
reader users know which room's header they are navigating.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Screen readers now announce new messages politely via role='log' +
aria-live='polite' on the message container in RoomTimeline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two icon-adjacent buttons were missing descriptive labels: the
"Exit formatting" key-symbol button in Toolbar.tsx and the "Pinned
messages" pin icon in RoomViewHeader.tsx.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds objectPosition:'center top' to all cover-fit thumbnail surfaces so
portrait images show faces/subjects instead of the center-slice when
the 600px AttachmentBox height cap forces cropping.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MediaGallery: fixed panel now goes full-width (100%) on mobile instead
of the inaccessible 320px right sidebar. Added 'Media Gallery' MenuItem
to RoomMenu (visible only on mobile) so users can open it from the
More Options (···) button.
MembersDrawer: removed ScreenSize.Desktop gate in Room.tsx so it now
renders on mobile too. CSS media query (≤750px) makes it position:fixed
inset:0 width:100% on mobile instead of the 266px desktop sidebar.
Added 'Members' MenuItem to RoomMenu for mobile access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds enabled=true param to useDecryptedMediaUrl in MediaGallery.tsx.
GalleryTile now uses useNearViewport(300px) — decryption is deferred
until the tile approaches the viewport, preventing burst of 100
concurrent decrypt/fetch calls when a pagination batch loads.
Blob revocation was already correct (no actual leak); this fixes the
load-burst performance issue. Full windowing virtualization deferred.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On mobile, SidebarNav (which contains BookmarksTab) is hidden while
inside a room. Added a Saved Messages toggle to the RoomMenu (···More
Options) so users can open/close the bookmarks panel without leaving
the room. Works on all screen sizes; UNTESTED on device.
Also marks Remind Me Later as done in LOTUS_TODO — it was already
fully wired (RemindMeDialog + ReminderMonitor + Message.tsx trigger).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ScheduleMessageModal, PollCreator, JoinAddressPrompt, JumpToTime,
EditHistoryModal, ForwardMessageDialog, RemindMeDialog — all now
render fullscreen on mobile (≤750px) and as a capped-width box on
desktop, consistent with the existing useModalStyle pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bug #10: use `100dvh` on <html> so layout shrinks when mobile virtual keyboard
appears (prevents composer from being pushed off-screen)
- Bug #7: add `minWidth/minHeight: 44px` to all 8 composer toolbar IconButtons on
mobile via mobileOrTablet() check (WCAG 2.1 AA touch target requirement)
- Bug #8: add `@media (max-width: 750px) { width: 100% }` to PageNav recipe
variants so the nav panel fills full width on mobile instead of overflowing
with its fixed desktop width
- Bug #9: introduce `useModalStyle(maxWidth)` hook — returns fullscreen styles on
mobile (no border-radius, no max-width cap, height 100%) and desktop box styles
otherwise; applied to LeaveRoomPrompt, LeaveSpacePrompt, ReportRoomModal,
ReportUserModal
- Bug #11: mark as FALSE POSITIVE in LOTUS_BUGS.md — `useState(() => atom(...))`
is the correct Jotai pattern for stable local atom references
- Scheduled Messages persistence: mark as FIXED — already uses atomWithStorage +
createJSONStorage with error-safe JSON parsing
- UrlPreviewCard TDS colors: mark as BRAND EXCEPTION — SVG logo fills and site
badge backgrounds are official third-party brand colors; cannot convert without
inventing new CSS variables (violates TDS rule 3)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ReportUserModal.tsx — category dropdown + reason input, calls
POST /_matrix/client/v3/users/{userId}/report via mx.http.authedRequest,
inline success/error feedback, auto-closes 1500ms after success
- Wire Report User button into UserRoomProfile.tsx between UserModeration
and UserDeviceSessions (hidden for own profile)
- Bug #6: enforce mutual exclusion between chat backgrounds and seasonal
themes — ChatBgGrid clears seasonal→'off' on non-'none' pick;
SeasonalBgGrid clears chatBackground→'none' on real theme pick;
SeasonalEffect guards against legacy persisted state at render time
- TDS: strip all hardcoded hex/rgba fallbacks from LotusToastContainer.tsx
(var(--lt-bg-card), --lt-accent-orange, --lt-text-primary/secondary,
--lt-accent-orange-dim/border, --lt-box-glow-orange)
- Mark Bug #6 FIXED, MSC4260 DONE, toast TDS FIXED in LOTUS_BUGS.md and
LOTUS_TODO.md; note EventReaders + CallControls already compliant
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace raw CSS variables and Button presets with folds color/config
tokens and MenuItem components, matching the ForwardMessageDialog and
ScheduleMessageModal patterns exactly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous fix (7f329e3b) made Page transparent so the body background
would show through. But PageRoot sits between Page and body with an opaque
Background.Container color, so the body background was blocked — it only
showed through the glassmorphism sidebar (which is a sibling of PageRoot,
not a child).
Revert to applying getChatBg() directly to Page via inline style, which
overrides PageRoot's class-level background-color by CSS specificity rules.
SidebarNav continues to mirror the same background to document.body so the
glassmorphism sidebar can blur through it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the background was applied directly to <Page> (room view
only) when glassmorphism was off, and to document.body only when
glassmorphism was on. This caused two bugs:
- Without glassmorphism: background only visible in the chat panel,
not behind the sidebar
- With glassmorphism: Page reverted to its opaque theme surface color,
so the body background only showed through the glass sidebar
Fix: SidebarNav now always applies the active background to
document.body (regardless of glassmorphism). RoomView's <Page> is
made transparent whenever a background is active so the body
background shows through both the sidebar and the chat area.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
256×256 APNG overlays from avatardecoration.com (open CORS CDN).
Stored in the user's Matrix profile as io.lotus.avatar_decoration via
MSC4133 so all Lotus Chat users see each other's decorations.
- avatarDecorations.ts: curated catalog of 110 original-IP decorations
across 9 categories (Gaming, Cyber, Space, Fantasy, Elements,
Japanese, Nature, Spooky, Cozy)
- useAvatarDecoration: per-user profile fetch with module-level cache
and in-flight deduplication so concurrent renders for the same userId
share one HTTP request
- AvatarDecoration: position:relative wrapper that overlays the APNG
8px beyond the avatar on all sides; renders nothing when no decoration
is set (zero cost for undecorated users)
- ProfileDecoration: scrollable grid picker in Settings → Profile,
grouped by category with live preview; Save button appears only when
the selection differs from what's saved
- Applied at all five avatar display sites: message timeline, members
drawer, knock list, @mention autocomplete, notifications inbox
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P4-3 — Knock-to-join Notifications for Admins:
- usePendingKnocks() hook reactively tracks knocking members via
RoomMemberEvent.Membership; returns empty array if user lacks invite power
- Members icon in RoomViewHeader shows a Warning badge with the knock count
when there are pending requests; badge updates in real time without
needing to open the drawer; aria-label updated to describe pending count
P5-11 — AFK Auto-Mute in Voice:
- useAfkAutoMute() hook opens a monitoring-only getUserMedia stream,
connects it to an AnalyserNode, and polls RMS every 500ms
- If mic is on and RMS stays below threshold for the configured timeout,
calls callEmbed.control.setMicrophone(false) and shows an in-app toast
- Hook is called inside CallControls so monitoring is only active during calls
- Settings: afkAutoMute toggle + afkTimeoutMinutes select (1/5/10/20/30 min,
default 10) added to Settings → Calls
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- EditHistoryModal: decrypt fetched edit events in E2EE rooms via
mx.decryptEventIfNeeded() before rendering; previously events not
found in the room cache showed ciphertext or "(no text)"
- CallEmbedProvider: add touch support for PiP resize corners;
extracted shared applyResize() helper; onTouchStart wired to all
four corners alongside existing onMouseDown
- RoomView: skip chatBgStyle when glassmorphism is active; document.body
already carries the background for the blur effect, rendering it twice
doubled CSS animation work unnecessarily
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each button (Format, Emoji, Sticker, GIF, Location, Poll, Voice,
Schedule) can be individually hidden in Settings → Editor.
All default to on, stored in composerToolbarButtons object.
getSettings deep-merges the nested object so new buttons default
to true for existing users with saved settings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- useFileDrop: reset drag overlay when mouse leaves browser window
(relatedTarget === null signals viewport exit, counter was getting stuck)
- useDeviceVerificationStatus: add member count to useMemo deps so new
room members' devices get checked, not just initial joined members
- index.css: define --bg-surface-variant used by VoiceMessageRecorder,
MessageSearch, SearchFilters, UserRoomProfile (was falling back to transparent)
- syntaxHighlight: fix Python inline comments — # after space/tab was
treated as plain text; only start-of-line was recognised
- usePresenceUpdater: replace internal baseUrl cast with mx.getHomeserverUrl()
- useLocalMessageSearch: scan all linked timelines via getUnfilteredTimelineSet()
not just the live window, so scrolled-back history is included in E2EE search
- RoomViewHeader: show search button in encrypted rooms — local search is
implemented and handles them; the guard was a holdover from before it existed
- recent-emoji: return emojis in recency order (array is already unshifted on
use) instead of sorting by total usage count
Skipped: media gallery memory leak (needs virtualization refactor),
bookmark race condition (needs queue/lock), Night Light portal coverage
(position:fixed already covers full viewport — not a real bug).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The checkbox was only shown for image/jpeg and image/png. Users
uploading WebP, GIF, AVIF, BMP, TIFF, HEIC (iPhone photos) or any
other raster format never saw the checkbox at all.
Fix: isCompressible now checks file.type.startsWith('image/') and
excludes only image/svg+xml (vector — would rasterise) and empty type
strings. compressImage signature widened to File | Blob so it matches
the TUploadContent type without unsafe casts.
The send-path guard in handleSendUpload was also widened from the
hardcoded jpeg/png check to use isCompressible(), keeping the two gates
in sync. The Blob-safe id attribute uses the .name fallback so it
doesn't break when originalFile is a Blob without a name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 new CSS-only animated backgrounds in the chat background picker:
- Digital Rain: two-layer vertical stripe scroll with parallax (wide
stripes at 8s, narrow at 4s via single keyframe with split positions)
- Star Drift: three-layer radial-gradient star field drifting diagonally
- Grid Pulse: neon grid lines that expand/contract (backgroundSize keyframe)
- Aurora Flow: large radial gradient bands sweeping across 200% canvas
- Fireflies: three layers of glowing dots drifting across the viewport
All use vanilla-extract keyframes (GPU-composited transforms/positions,
no canvas, no JS timers). prefers-reduced-motion is respected in
getChatBg() by stripping the animation property at call time. A "Pause
Background Animations" toggle in Settings → Appearance provides an
in-app override for the same purpose.
BG labels de-duplicated ("Digital Rain", "Star Drift", "Aurora Flow")
to avoid the duplicate "Stars" and "Aurora" entries that had appeared.
LIGHT anim-fireflies background corrected from near-black #0a0a10 to
warm white #fffdf0. Four unused keyframe exports removed from
Animations.css.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Glassmorphism: the sidebar is a flex sibling of the room view, so
backdrop-filter had nothing behind it to blur. Fix: apply the active
chat background to document.body when glassmorphismSidebar is on
(cleaned up when it's turned off or the component unmounts). Now the
sidebar blurs through the same background pattern as the room view,
making the frosted-glass effect obvious.
Image upload cleanup: delete the pre-uploaded original MXC from the
homeserver after the compressed version is successfully uploaded
(Synapse 1.97+ DELETE /_matrix/client/v1/media/{server}/{mediaId}).
Also delete on cancel when a successful upload is removed by the user.
Both are best-effort — failures are swallowed so UX is unaffected.
Added tryDeleteMxcContent() utility to src/app/utils/matrix.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PresenceRingAvatar: replace circular wrapper div (borderRadius 50%) with
React.cloneElement injecting outline+outlineOffset directly onto the child
Avatar element — outline follows the child's actual border-radius so the
ring matches the avatar shape in every context
- EditHistoryModal: use getClearContent() for the Original entry instead of
evt.event.content, which is still the ciphertext for E2EE messages and has
no body field. getClearContent() returns decrypted content bypassing the
_replacingEvent chain, fixing the "(no text)" shown for encrypted originals
- MessageQuickReactions: 5 → 3 emoji (toolbar too wide with 5)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P5-18: PresenceRingAvatar wrapper component applies a 2px box-shadow
ring to user avatars — green (online), yellow (idle/unavailable), red
(DND via status_msg='dnd'), no ring (offline). Applied to: message
timeline sender avatars, members drawer (members + knock requests),
@mention autocomplete, and inbox notification senders.
P5-6: Leading emoji in room names renders at 1.15× in the sidebar via
Unicode emoji regex detection in RoomNavItem. Emoji picker (EmojiBoard
in PopOut) added to all three room-name inputs: Create Room dialog
(converted to controlled input), Room Settings name field (shown only
when canEditName), and the "Rename for me" local rename dialog.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P5-17: MessageQuickReactions moved from 3-dots menu to hover toolbar;
shows 5 recent emoji directly on hover. Clicking a quick-reaction also
closes any open emoji picker (setEmojiBoardAnchor). Line separator
removed from component.
P5-7: LotusToastContainer slides in from bottom-right when window is
focused — replaces OS notification for in-focus events. Correct room
path (DM vs home) derived from mDirectAtom. Invite toast routes to
inbox. 4s auto-dismiss. Full TDS styling via CSS custom properties.
P5-8: Confirmed already implemented upstream (MentionHighlightPulse,
0.6s scale+glow, one-shot, prefers-reduced-motion). Marked complete.
Code-review fixes: toast navigation used nonexistent /room/ route;
emoji picker stayed open after toolbar quick-reaction.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>