Audit of every rendered time found five families of ad-hoc formatting:
the shared Time component + copies of its today/yesterday branch
(forwarded header, thread summary, read receipts, device tile, moderation
alerts, edit history), locale-default toLocale*String calls that ignored
the user's 12/24 h and date-format settings (scheduled tray, reminders,
schedule preview, notification snooze, bookmarks, threads list, search
cache line, room insights, media gallery), a hard-coded en-US date in the
activity log, and three relative-age variants.
utils/formatTimestamp.ts now holds the rules — today → time; yesterday /
tomorrow → day word + time; last 6 days → weekday + time; older → date +
time in dateFormatString — plus autoDate / time / date / dateTime styles,
formatDayDivider (full weekday), formatShortAge (room list) and
formatRelativeAge (list rows). useTimestampFormatter binds them to the
settings. 11 unit tests with an injected 'now'.
Visible changes are limited to consistency: 12 h times keep the existing
zero-padded hh:mm A; the a11y label and Created-by line use the user's
date format instead of a fixed long month.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
Address findings from 2 review agents on the edit-history diff:
- Perf: diffWords is O(n*m); cap at 2000 tokens/side and fall back to a
coarse whole-block replaced diff above that, so a very large multi-edit
message can't freeze the main thread. Memoize the per-row diff in
DiffText. (Added a unit test for the coarse fallback.)
- Perceivability (a11y/design): the added-word <ins> highlight was
color-fill only, which is faint against the modal surface in the lotus
themes. Add a Success.ContainerLine border + horizontal padding (so the
rounded corners read as a chip) + box-decoration-break: clone for clean
wrapping, so the "added" cue survives low fill contrast.
- Consistency: a media/no-body edit now renders "(no text)" in diff mode
too (matched the toggle-off view; was blank).
- Softened the code comment's screen-reader claim (bare <ins>/<del> aren't
announced by default).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Edit History modal listed each version's full text with no indication
of what changed. Add a word-level diff: each edit highlights the words
added (green) and removed (struck-through red) relative to the previous
version, so a one-word fix is obvious at a glance.
- New pure, dependency-free diffWords (LCS over word/whitespace tokens) in
utils/textDiff.ts, with 8 unit tests (insert/delete/replace, whitespace
preserved, empty, no-mutation, word-not-char granularity).
- EditHistoryModal renders each edit via a DiffText component using
semantic <ins>/<del> (screen-reader-meaningful) styled with folds
Success/Critical tokens. A "Highlight changes" header toggle (default
on) switches back to full text, which keeps the rich formatted render;
the Original row is always the plain baseline.
- Diff is plain-text (body) only by design; formatted markup isn't diffed
(the toggle restores the rich view), and media/no-body edits diff as
empty strings gracefully.
Co-Authored-By: Claude Opus 4.8 <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>
- 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>
- 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>
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>
- 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>