fix(notifications): clear thread receipts on mark-read; cap avatar-decoration refetch
Two federated-room bugs surfaced by the desktop build: 1. markAsRead only sent one unthreaded receipt at the main-timeline tail. With threadSupport enabled, thread replies leave the main timeline, so a reply newer than that tail was never covered — its per-thread notification count (which the room dot sums) lingered, so the unread dot never cleared even after reading. It also early-returned when the main timeline was already read. Now also send a threaded receipt at each unread thread's latest reply. 2. useAvatarDecoration never cached non-404 failures, so every avatar mount re-requested io.lotus.avatar_decoration for federated users whose homeserver 403s/502s the field — a refetch storm that spammed the console and hammered our homeserver's federation. Now cache definitive rejections (400/403/404) and give up after ~2 transient (429/5xx) attempts per session. Gates: tsc/eslint/prettier clean, build OK, 665 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { MatrixClient, ReceiptType } from 'matrix-js-sdk';
|
||||
import { MatrixClient, NotificationCountType, ReceiptType } from 'matrix-js-sdk';
|
||||
import { getSettings } from '../state/settings';
|
||||
|
||||
export async function markAsRead(mx: MatrixClient, roomId: string, privateReceipt: boolean) {
|
||||
@@ -6,6 +6,9 @@ export async function markAsRead(mx: MatrixClient, roomId: string, privateReceip
|
||||
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()!);
|
||||
|
||||
@@ -17,17 +20,31 @@ export async function markAsRead(mx: MatrixClient, roomId: string, privateReceip
|
||||
}
|
||||
return null;
|
||||
};
|
||||
if (timeline.length === 0) return;
|
||||
const latestEvent = getLatestValidEvent();
|
||||
if (latestEvent === null) return;
|
||||
|
||||
// Unthreaded receipt: with client threadSupport enabled the SDK would
|
||||
// otherwise scope this to the main timeline (thread_id: "main"), leaving
|
||||
// per-thread notification counts permanently unread. Unthreaded preserves
|
||||
// the pre-threads wire behavior — one receipt clears everything.
|
||||
await mx.sendReadReceipt(
|
||||
latestEvent,
|
||||
privateReceipt || privateReadReceipts ? ReceiptType.ReadPrivate : ReceiptType.Read,
|
||||
true,
|
||||
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, in any thread.
|
||||
await mx.sendReadReceipt(latestEvent, receiptType, true);
|
||||
}
|
||||
|
||||
// ...but a thread reply NEWER than the main-timeline tail is not covered by
|
||||
// the receipt above (threadSupport moves thread replies out of the main
|
||||
// timeline), so its per-thread notification count — and the room's unread dot,
|
||||
// which sums thread counts — would linger even after "reading" the room. Send
|
||||
// a threaded receipt at the latest reply of every thread that still has unread
|
||||
// counts. Also runs when the main timeline is already read (latestEvent null).
|
||||
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() ?? thread.rootEvent;
|
||||
if (!lastReply || lastReply.isSending()) return undefined;
|
||||
// Threaded receipt (unthreaded = false → the SDK scopes it to this thread).
|
||||
return mx.sendReadReceipt(lastReply, receiptType, false).catch(() => undefined);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user