This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

cinny-threads: clear thread notifications on room Mark-as-Read

A room's unread in cinny is the main-timeline notification count plus the
sum of every thread's count (Room.getUnreadNotificationCount sums
threadNotifications), and roomHaveNotification keys off that. The
room-level Mark-as-Read sent a receipt only for the latest non-thread
event, clearing the main timeline but leaving every thread count intact -
so a room with any unread thread stayed bold in the sidebar even after
being marked read, with no per-thread indicator to explain why.

Send an unthreaded read receipt against the room's latest event instead;
the SDK/server treat it as clearing the main timeline and every thread's
unread bucket at once.

Assisted-by: Claude Code (claude-opus-4-8[1m])

+33 -17
+33 -17
2configs/services/matrix/cinny-threads-ui.patch
··· 2558 2558 + ); 2559 2559 +}; 2560 2560 diff --git a/src/app/utils/notifications.ts b/src/app/utils/notifications.ts 2561 - index a23bd1a..3f5bc7e 100644 2561 + index a23bd1a..4ba2d2b 100644 2562 2562 --- a/src/app/utils/notifications.ts 2563 2563 +++ b/src/app/utils/notifications.ts 2564 - @@ -1,17 +1,21 @@ 2565 - -import { MatrixClient, ReceiptType } from 'matrix-js-sdk'; 2566 - +import { MatrixClient, ReceiptType, RelationType } from 'matrix-js-sdk'; 2567 - 2568 - export async function markAsRead(mx: MatrixClient, roomId: string, privateReceipt: boolean) { 2569 - const room = mx.getRoom(roomId); 2564 + @@ -5,22 +5,28 @@ export async function markAsRead(mx: MatrixClient, roomId: string, privateReceip 2570 2565 if (!room) return; 2571 2566 2572 2567 const timeline = room.getLiveTimeline().getEvents(); 2573 2568 - const readEventId = room.getEventReadUpTo(mx.getUserId()!); 2574 2569 + const userId = mx.getUserId(); 2575 2570 + if (!userId) return; 2576 - + const readEventId = room.getEventReadUpTo(userId); 2577 2571 2578 - const getLatestValidEvent = () => { 2579 - for (let i = timeline.length - 1; i >= 0; i -= 1) { 2580 - const latestEvent = timeline[i]; 2581 - if (latestEvent.getId() === readEventId) return null; 2572 + - const getLatestValidEvent = () => { 2573 + - for (let i = timeline.length - 1; i >= 0; i -= 1) { 2574 + - const latestEvent = timeline[i]; 2575 + - if (latestEvent.getId() === readEventId) return null; 2582 2576 - if (!latestEvent.isSending()) return latestEvent; 2583 - + if (!latestEvent.isRelation(RelationType.Thread) && !latestEvent.isSending()) { 2584 - + return latestEvent; 2585 - + } 2577 + + // Latest non-pending event in the room, thread replies included. 2578 + + let latestEvent = null; 2579 + + for (let i = timeline.length - 1; i >= 0; i -= 1) { 2580 + + if (!timeline[i].isSending()) { 2581 + + latestEvent = timeline[i]; 2582 + + break; 2586 2583 } 2587 - return null; 2588 - }; 2584 + - return null; 2585 + - }; 2586 + - if (timeline.length === 0) return; 2587 + - const latestEvent = getLatestValidEvent(); 2588 + - if (latestEvent === null) return; 2589 + + } 2590 + + if (!latestEvent) return; 2591 + 2592 + + // Send an UNTHREADED receipt. A room's unread in cinny is the main-timeline 2593 + + // count plus the sum of every thread's count (Room.getUnreadNotificationCount), 2594 + + // so a plain main/threaded receipt leaves the room marked unread whenever any 2595 + + // thread still has unread notifications - the room stays bold even after 2596 + + // "Mark as Read". An unthreaded receipt clears the main timeline and every 2597 + + // thread's unread bucket in one shot. 2598 + await mx.sendReadReceipt( 2599 + latestEvent, 2600 + - privateReceipt ? ReceiptType.ReadPrivate : ReceiptType.Read 2601 + + privateReceipt ? ReceiptType.ReadPrivate : ReceiptType.Read, 2602 + + true 2603 + ); 2604 + } 2589 2605 diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts 2590 2606 index 05dfeb2..a29d52b 100644 2591 2607 --- a/src/app/utils/room.ts