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:
@@ -67,10 +67,13 @@ test('sortThreads breaks ties deterministically by id', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('filterThreads / sortThreads do not mutate input', () => {
|
||||
const input = [t('a', 100), t('b', 300)];
|
||||
test('filterThreads / sortThreads do not mutate input (all/participating branches)', () => {
|
||||
const input = [t('a', 100, 1, true), t('b', 300, 0, false)];
|
||||
const before = input.map((x) => x.id);
|
||||
filterThreads(input, 'all');
|
||||
filterThreads(input, 'participating');
|
||||
filterThreads(input, 'unread');
|
||||
sortThreads(input, 'recent');
|
||||
sortThreads(input, 'oldest');
|
||||
assert.deepEqual(
|
||||
input.map((x) => x.id),
|
||||
@@ -78,6 +81,15 @@ test('filterThreads / sortThreads do not mutate input', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('filter → sort pipeline composes (unread + recent)', () => {
|
||||
const input = [t('a', 100, 2), t('b', 300, 0), t('c', 200, 1)];
|
||||
const out = sortThreads(filterThreads(input, 'unread'), 'recent');
|
||||
assert.deepEqual(
|
||||
out.map((x) => x.id),
|
||||
['c', 'a'],
|
||||
);
|
||||
});
|
||||
|
||||
test('isThreadFilter / isThreadSort accept valid and reject junk', () => {
|
||||
assert.equal(isThreadFilter('all'), true);
|
||||
assert.equal(isThreadFilter('unread'), true);
|
||||
|
||||
Reference in New Issue
Block a user