import { MatrixClient, NotificationCountType, ReceiptType } from 'matrix-js-sdk'; import { getSettings } from '../state/settings'; export async function markAsRead(mx: MatrixClient, roomId: string, privateReceipt: boolean) { const { privateReadReceipts } = getSettings(); const room = mx.getRoom(roomId); if (!room) return; const receiptType = privateReceipt || privateReadReceipts ? ReceiptType.ReadPrivate : ReceiptType.Read; const timeline = room.getLiveTimeline().getEvents(); const readEventId = room.getEventReadUpTo(mx.getUserId()!); const getLatestValidEvent = () => { for (let i = timeline.length - 1; i >= 0; i -= 1) { const latestEvent = timeline[i]; if (latestEvent.getId() === readEventId) return null; if (!latestEvent.isSending()) return latestEvent; } return null; }; const latestEvent = timeline.length > 0 ? getLatestValidEvent() : null; if (latestEvent) { // Unthreaded receipt: with client threadSupport enabled the SDK would // otherwise scope this to the main timeline (thread_id: "main"). Unthreaded // clears the main timeline + every event up to this one. await mx.sendReadReceipt(latestEvent, receiptType, true); } // Clear per-thread notification counts too — the room's unread dot sums them, // so an unread thread reply keeps the dot lit even after the main timeline is // read (threadSupport moves thread replies out of the main timeline, so the // unthreaded receipt above doesn't necessarily cover them). // // CRITICAL: only send for a GENUINE loaded thread reply, via thread.lastReply(). // NEVER fall back to the thread root: a root event is "in the main timeline", // so sendReadReceipt(root, false) resolves (via threadIdForReceipt) to a MAIN // receipt at that old root event. If the root isn't in the loaded timeline it // moves the main read receipt onto an event we don't have -> getEventReadUpTo() // returns null -> the room is reported unread on every mark-read call (this was // the P6 regression, amplified by the bulk mark-all-orphan-rooms-read callers). // If a thread's replies aren't loaded (lastReply() null), just skip it. const threads = room.getThreads(); await Promise.all( threads.map((thread) => { const unread = room.getThreadUnreadNotificationCount(thread.id, NotificationCountType.Total) ?? 0; if (unread <= 0) return undefined; const lastReply = thread.lastReply(); if (!lastReply || lastReply.isSending()) return undefined; // Threaded receipt (unthreaded = false → the SDK scopes it to this thread // via the reply's real threadRootId; it never touches the main marker). return mx.sendReadReceipt(lastReply, receiptType, false).catch(() => undefined); }), ); }