- T1 (🔴): markThreadAsRead no longer receipts the thread ROOT (a 2nd instance of the read-marker-corruption regression — opening a thread whose root is old re-lit the whole room). Extracted to a pure threadReceipt.ts + 5 regression tests. - N1 (🔴): favicon/tab-title unread count now sums only leaf rooms (was double- counting every ancestor-space aggregate in roomToUnread). - N2 (🔴): notifications/sounds dedupe on the event id, not the unread count — fixes "read a DM, next message never notifies again". - T4 (🟠): the thread notification path no longer re-gates on the room count, so an explicit per-thread "All replies" override in a Mentions-only room fires. - N3 (🟠): getUnreadInfos skips phantom {0,0} entries (muted-thread-only rooms no longer light the nav row / pollute unread filters). - N4 (🟠): the Receipt handler recomputes unread instead of blanket-DELETE, so a threaded receipt can't wipe a room's valid main-timeline badge. - T2 (🟠): thread "Jump to Latest" re-anchors the virtual window (was landing on a stale mid/old event). Gates: tsc/eslint/prettier clean, build OK, 678 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
29 lines
1.3 KiB
TypeScript
29 lines
1.3 KiB
TypeScript
import { MatrixClient, ReceiptType, Thread } from 'matrix-js-sdk';
|
|
|
|
/**
|
|
* Send a threaded read receipt for a thread, clearing its per-thread unread
|
|
* count.
|
|
*
|
|
* CRITICAL: never receipt the thread ROOT. A thread's liveTimeline is
|
|
* `[root, reply1, …]`, so the latest event IS the root when replies aren't
|
|
* loaded yet (common — the thread panel fires this on mount before replies
|
|
* fetch). The root is "in the main timeline", so a receipt for it is written by
|
|
* the SDK with `thread_id:"main"` at the old root, dragging the room's MAIN read
|
|
* marker backwards (`getEventReadUpTo` → an old/unloaded event) and re-lighting
|
|
* the whole room. We only receipt a genuine loaded reply (`thread.lastReply()`);
|
|
* if none is loaded we bail (the per-thread count clears when the reply loads
|
|
* and this runs again). Mirrors the root guard in `utils/notifications.ts`.
|
|
*
|
|
* Pure (no React/CSS) so it can be unit-tested — see `threadReceipt.test.ts`.
|
|
*/
|
|
export const markThreadAsRead = async (
|
|
mx: MatrixClient,
|
|
thread: Thread,
|
|
privateReceipt: boolean,
|
|
): Promise<void> => {
|
|
const lastReply = thread.lastReply();
|
|
if (!lastReply || lastReply.isSending() || lastReply.getId() === thread.id) return;
|
|
|
|
await mx.sendReadReceipt(lastReply, privateReceipt ? ReceiptType.ReadPrivate : ReceiptType.Read);
|
|
};
|