Skip to content

Fix thread staying unread after a non-counting event follows your reply - #34905

Open
nathanael-h wants to merge 1 commit into
element-hq:developfrom
nathanael-h:fix/unread-thread-after-own-reply
Open

Fix thread staying unread after a non-counting event follows your reply#34905
nathanael-h wants to merge 1 commit into
element-hq:developfrom
nathanael-h:fix/unread-thread-after-own-reply

Conversation

@nathanael-h

Copy link
Copy Markdown

Fixes #34904

Problem

doesTimelineHaveUnreadMessages decides whether a timeline is unread by finding the latest important event and asking whether we have read it:

const latestImportantEventId = findLatestImportantEvent(room.client, timeline)?.getId();
if (latestImportantEventId) {
    return !room.hasUserReadEvent(myUserId, latestImportantEventId);
}

"Important" filters through eventTriggersUnreadCount, which returns false for our own events — so the latest important event is always the newest event from someone else, never our reply. Whether that event counts as read then comes down to the js-sdk shortcut in RoomReceipts.hasUserReadEvent, which only fires when we sent the literal last event in the timeline:

// TODO: what if they sent the second-last event in the thread?
if (this.userSentLatestEventInThread(threadId, userId)) { ... }
return !!(timeline && timeline.length > 0 && timeline[timeline.length - 1].getSender() === userId);

So: you reply in a thread, then anything that doesn't trigger an unread count arrives after your reply — a reaction, an edit, a redaction, a membership change — and the thread goes back to unread, judged against a message you had already read before replying.

The main timeline is shielded from this, because Room.addLiveEvent synthesizes a read receipt for every event's sender, including ours. Threads have no equivalent, which is where the bug actually surfaces (and why the new test lives in the doesRoomHaveUnreadThreads() block — an equivalent main-timeline test passes with or without this change).

Change

Ask the right question — "is our latest event newer than the latest important event?" instead of "is our event the very last one?":

if (userSentEventAfterLatestImportantEvent(room.client, timeline)) return false;

The helper walks the timeline backwards and returns on whichever comes first, one of our events or an important event. Since our own events are never important, there's no ordering ambiguity. Pending/failed local echoes are skipped (!event.status), so a send that hasn't landed doesn't mark anything read.

Tests

Two tests in doesRoomHaveUnreadThreads():

  • thread root(alice) -> reply(me) -> redacted(alice) is read — fails on develop with expected true to be false, passes with this change;
  • thread root(alice) -> reply(me) -> message(alice) is still unread, so a genuinely newer incoming message isn't swallowed.

Plus a main-timeline test that another user's message arriving after ours still reads as unread.

Unread.test.ts                                    32 passed
+ RoomNotifs, utils/notifications, useRoomThreadNotifications,
  room-list-v3, notification-badge viewmodels     278 passed (13 files)

oxlint, oxfmt --check and tsc --noEmit are clean for the changed files.

Context

Found while working on #32851 (Threads Activity Centre cross-room threads panel), which currently carries a local guard in useUnreadThreadRooms.ts to suppress the false positive. @florianduros asked in review that the helper be fixed rather than worked around — this is that fix; the TAC guard will be dropped once this lands.

Checklist

  • Tests written for new code (and old code if feasible).
  • New or updated public/exported symbols have accurate TSDoc documentation.
  • Linter and other CI checks pass.
  • I have licensed the changes to Element by completing the Contributor License Agreement (CLA)

`doesTimelineHaveUnreadMessages` judges the timeline against the latest
*important* event, which is the newest event from someone else, since our
own events never trigger an unread count. Whether that event counts as read
then rests on the js-sdk's shortcut, which only fires when we sent the
literal last event in the timeline (`userSentLatestEventInThread`).

So replying in a thread and then receiving anything that doesn't trigger an
unread count — a reaction, an edit, a redaction, a membership change — leaves
the thread unread even though we have demonstrably read it. The main timeline
is shielded from this by the synthetic read receipt the js-sdk records for our
own events; threads have no equivalent, so this is where it surfaces.

Treat the timeline as read when our own latest event is newer than the latest
important event, rather than requiring it to be the very last one.

Fixes element-hq#34904
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Z-Community-PR Issue is solved by a community member's PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

doesTimelineHaveUnreadMessages: thread stays unread when a non-counting event lands after your own reply

1 participant