import { MatrixClient, MatrixEvent, ReceiptType, Thread } from 'matrix-js-sdk'; // Walks a timeline backwards and returns the latest event that is // fully sent to the homeserver (not local-echo / pending). Shared by // the main-room and thread receipt senders so the «which event is // the read marker pointed at» semantic stays consistent. // // Note: deliberately does NOT skip on `evt.getId() === readEventId`. // SDK's `addLocalEchoReceipt` updates the local read-up-to marker // synchronously on every `sendReadReceipt` call (read-receipt.js:329-332), // so a readEventId-match short-circuit would bail on the second // invocation even when the server failed to process the first one // (network failure, 5xx, MSC3773-less server) — leaving the unread // counters stuck. The server is idempotent on duplicate receipts, so // re-sending costs at most one round-trip; callers gate on // `document.hasFocus()` to avoid spam from backgrounded tabs. const pickLatestNonPending = (events: MatrixEvent[]): MatrixEvent | null => { for (let i = events.length - 1; i >= 0; i -= 1) { const evt = events[i]; if (evt && !evt.isSending()) { return evt; } } return null; }; const receiptType = (privateReceipt: boolean): ReceiptType => privateReceipt ? ReceiptType.ReadPrivate : ReceiptType.Read; export async function markAsRead(mx: MatrixClient, roomId: string, privateReceipt: boolean) { const room = mx.getRoom(roomId); if (!room) return; const latestEvent = pickLatestNonPending(room.getLiveTimeline().getEvents()); if (!latestEvent) return; await mx.sendReadReceipt(latestEvent, receiptType(privateReceipt)); } // Thread-scoped variant. SDK auto-attaches `thread_id: ` via // `threadIdForReceipt(event)` (matrix-js-sdk client.js:2655). export async function markAsThreadRead( mx: MatrixClient, thread: Thread, privateReceipt: boolean ) { if (!mx.getUserId()) return; const latestEvent = pickLatestNonPending(thread.liveTimeline.getEvents()); if (!latestEvent) return; await mx.sendReadReceipt(latestEvent, receiptType(privateReceipt)); }