fix(threads): review-wave fixes for per-thread notifications

- useRoomsListener now PREPENDS the emitting Room (was appended): the SDK emits
  RoomEvent.UnreadNotifications with VARIABLE arity (0/1/2 args), so a trailing
  extra arg landed in the wrong positional slot on the most common room-count
  sync path — room.isSpaceRoom() threw inside the SDK emit loop and the badge
  PUT never ran. Both consumers updated (CONFIRMED HIGH review finding).
- roomToUnread: SpaceChild RESET now passes the thread prefs so muted-thread
  subtraction survives space-child state changes.

Reviewer also verified: badge subtraction math exact (no double-subtraction),
encrypted thread replies caught by the timeline guard (m.relates_to is
cleartext), fresh prefs flow to handlers, single-owner wiring load-bearing.
Documented-acceptable: hasCurrentUserParticipated can lag until the server
bundle refreshes after your first reply; dedupe maps grow per-session only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 22:53:32 -04:00
parent 501d493ca4
commit 0adce52d37
3 changed files with 20 additions and 20 deletions
+6 -10
View File
@@ -4,7 +4,6 @@ import {
IRoomTimelineData,
MatrixClient,
MatrixEvent,
NotificationCount,
Room,
RoomEvent,
SyncState,
@@ -282,18 +281,15 @@ export const useBindRoomToUnreadAtom = (mx: MatrixClient, unreadAtom: typeof roo
// RoomEvent.UnreadNotifications is emitted room-level only (never re-emitted
// client-side), so the main Timeline pathway misses thread-count changes and
// room badges lag. useRoomsListener appends the emitting Room as the final
// arg, making this a surgical per-room PUT (not a full RESET per emit) with
// muted-thread subtraction re-applied. Room-mute keeps its DELETE semantics.
// room badges lag. useRoomsListener PREPENDS the emitting Room (the SDK emits
// this event with variable arity — 0/1/2 args — so only a leading slot is
// positionally stable), making this a surgical per-room PUT with muted-thread
// subtraction re-applied. Room-mute keeps its DELETE semantics.
useRoomsListener(
mx,
RoomEvent.UnreadNotifications,
useCallback(
(
_unreadNotifications: NotificationCount | undefined,
_threadId: string | undefined,
room: Room,
) => {
(room: Room) => {
if (room.isSpaceRoom()) return;
if (getNotificationType(mx, room.roomId) === NotificationType.Mute) {
setUnreadAtom({ type: 'DELETE', roomId: room.roomId });
@@ -333,7 +329,7 @@ export const useBindRoomToUnreadAtom = (mx: MatrixClient, unreadAtom: typeof roo
if (mEvent.getType() === StateEvent.SpaceChild) {
setUnreadAtom({
type: 'RESET',
unreadInfos: getUnreadInfos(mx),
unreadInfos: getUnreadInfos(mx, threadNotificationsRef.current),
});
}
},