fix(threads): harden threads list after review
Address findings from 3 review agents on the Threads list panel:
- Last-activity accuracy (SDK): sort key and the "last reply <time>"
label now use thread.replyToEvent.getTs() (server bundle latest_event)
instead of lastReply(), which returns the ROOT time until each thread's
replies lazily paginate (or permanently on fetch error). Applied to the
hook signature too.
- Live-refresh completeness (correctness): the useRoomThreads signature
now includes thread.length and the root event's replacingEventId, so a
mid-thread redaction (reply count) and a root-message edit (row snippet)
refresh the row live instead of going stale.
- a11y: the row's aria-label was the button's whole accessible name,
hiding the snippet/count/unread from screen readers. It now describes
the thread ("Open thread by <name>, unread, N replies, last reply ..").
- Unread badge: replaced the bare green dot (Success = the mention color)
with the app-wide UnreadBadge, using the Highlight count so mentions
render red and ordinary unread renders secondary, matching room-nav.
- Hover/focus affordance: the clickable row moved its inline styles to a
css class with token-based :hover / :active backgrounds.
- Participant pile now also includes the last replier from the bundle.
- Stabilized the panel's onClose/onOpenThread with useCallback so its
Escape listener isn't re-subscribed every Room render. Added
filter->sort pipeline + all/participating immutability tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,23 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { NotificationCountType, Room, RoomEvent, Thread, ThreadEvent } from 'matrix-js-sdk';
|
||||
|
||||
// Cheap signature over the fields the Threads list actually depends on (id,
|
||||
// last-activity ts, unread count, participation). We only push a new snapshot
|
||||
// when this changes, so noisy repeated ThreadEvent.Update / UnreadNotifications
|
||||
// re-emits that don't alter the list don't cause a re-render.
|
||||
// Cheap signature over the fields the Threads list actually renders on (id,
|
||||
// last-activity ts, reply count, unread, participation, and a root-edit token).
|
||||
// We only push a new snapshot when this changes, so noisy repeated
|
||||
// ThreadEvent.Update / UnreadNotifications re-emits that don't alter the list
|
||||
// don't cause a re-render.
|
||||
// `replyToEvent` reads the server bundle's latest_event, so last-activity is
|
||||
// accurate immediately (before/without lazily paginating each thread's replies);
|
||||
// `length` covers reply-count changes (e.g. a mid-thread redaction) and
|
||||
// `replacingEventId` covers a root-message edit changing the row snippet.
|
||||
const signatureOf = (room: Room, threads: Thread[]): string =>
|
||||
threads
|
||||
.map((t) => {
|
||||
const ts = t.lastReply()?.getTs() ?? t.rootEvent?.getTs() ?? 0;
|
||||
const ts = t.replyToEvent?.getTs() ?? t.rootEvent?.getTs() ?? 0;
|
||||
const unread = room.getThreadUnreadNotificationCount(t.id, NotificationCountType.Total) ?? 0;
|
||||
return `${t.id}:${ts}:${unread}:${t.hasCurrentUserParticipated ? 1 : 0}`;
|
||||
const rootEdit = t.rootEvent?.replacingEventId() ?? '';
|
||||
const participated = t.hasCurrentUserParticipated ? 1 : 0;
|
||||
return `${t.id}:${ts}:${t.length}:${unread}:${participated}:${rootEdit}`;
|
||||
})
|
||||
.sort()
|
||||
.join('|');
|
||||
|
||||
Reference in New Issue
Block a user