2026-07-02 16:31:10 -04:00
|
|
|
import { MatrixClient, NotificationCountType, ReceiptType } from 'matrix-js-sdk';
|
2026-06-02 19:31:30 -04:00
|
|
|
import { getSettings } from '../state/settings';
|
2022-03-18 09:09:14 +05:30
|
|
|
|
2026-02-16 06:03:37 +11:00
|
|
|
export async function markAsRead(mx: MatrixClient, roomId: string, privateReceipt: boolean) {
|
2026-06-02 19:31:30 -04:00
|
|
|
const { privateReadReceipts } = getSettings();
|
2022-03-18 09:09:14 +05:30
|
|
|
const room = mx.getRoom(roomId);
|
|
|
|
|
if (!room) return;
|
|
|
|
|
|
2026-07-02 16:31:10 -04:00
|
|
|
const receiptType =
|
|
|
|
|
privateReceipt || privateReadReceipts ? ReceiptType.ReadPrivate : ReceiptType.Read;
|
|
|
|
|
|
2022-03-18 09:09:14 +05:30
|
|
|
const timeline = room.getLiveTimeline().getEvents();
|
2024-07-22 16:17:19 +05:30
|
|
|
const readEventId = room.getEventReadUpTo(mx.getUserId()!);
|
2022-03-18 09:09:14 +05:30
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-02 16:31:10 -04:00
|
|
|
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
|
2026-07-02 17:09:28 -04:00
|
|
|
// clears the main timeline + every event up to this one.
|
2026-07-02 16:31:10 -04:00
|
|
|
await mx.sendReadReceipt(latestEvent, receiptType, true);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-02 17:09:28 -04:00
|
|
|
// 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.
|
2026-07-02 16:31:10 -04:00
|
|
|
const threads = room.getThreads();
|
|
|
|
|
await Promise.all(
|
|
|
|
|
threads.map((thread) => {
|
|
|
|
|
const unread =
|
|
|
|
|
room.getThreadUnreadNotificationCount(thread.id, NotificationCountType.Total) ?? 0;
|
|
|
|
|
if (unread <= 0) return undefined;
|
2026-07-02 17:09:28 -04:00
|
|
|
const lastReply = thread.lastReply();
|
2026-07-02 16:31:10 -04:00
|
|
|
if (!lastReply || lastReply.isSending()) return undefined;
|
2026-07-02 17:09:28 -04:00
|
|
|
// Threaded receipt (unthreaded = false → the SDK scopes it to this thread
|
|
|
|
|
// via the reply's real threadRootId; it never touches the main marker).
|
2026-07-02 16:31:10 -04:00
|
|
|
return mx.sendReadReceipt(lastReply, receiptType, false).catch(() => undefined);
|
|
|
|
|
}),
|
2025-02-26 21:44:53 +11:00
|
|
|
);
|
2022-03-18 09:09:14 +05:30
|
|
|
}
|