Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

[WIP] Rewrite receipt handling #11864

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default defineConfig({
HOMESERVER: "synapse",
},
retries: {
runMode: 4,
runMode: 0,
openMode: 0,
},

Expand Down
40 changes: 19 additions & 21 deletions cypress/e2e/read-receipts/editing-messages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ describe("Read receipts", () => {
goTo(room1);
assertStillRead(room2);
});
it("Editing a message after marking as read makes the room unread", () => {
it("Editing a message after marking as read leaves the room read", () => {
// Given the room is marked as read
goTo(room1);
receiveMessages(room2, ["Msg1"]);
Expand All @@ -145,7 +145,7 @@ describe("Read receipts", () => {
// When a message is edited
receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]);

// Then the room remains unread
// Then the room remains read
assertStillRead(room2);
});
it("Editing a reply after reading it makes the room unread", () => {
Expand Down Expand Up @@ -264,8 +264,7 @@ describe("Read receipts", () => {
assertStillRead(room2);
assertReadThread("Msg1");
});
// XXX: fails because the unread dot remains after marking as read
it.skip("Marking a room as read after an edit in a thread makes it read", () => {
it("Marking a room as read after an edit in a thread makes it read", () => {
// Given an edit in a thread is making the room unread
goTo(room1);
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), editOf("Resp1", "Edit1")]);
Expand All @@ -277,8 +276,7 @@ describe("Read receipts", () => {
// Then it is read
assertRead(room2);
});
// XXX: fails because the unread dot remains after marking as read
it.skip("Editing a thread message after marking as read leaves the room read", () => {
it("Editing a thread message after marking as read leaves the room read", () => {
// Given a room is marked as read
goTo(room1);
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]);
Expand All @@ -292,9 +290,9 @@ describe("Read receipts", () => {
// Then the room becomes unread
assertStillRead(room2);
});
// XXX: fails because we see a dot instead of an unread number - probably the server and client disagree
// XXX: fails because the room becomes unread after restart
it.skip("A room with an edited threaded message is still read after restart", () => {
// Given an edit in a thread is making a room unread
// Given an edit in a thread is leaving a room read
goTo(room1);
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]);
markAsRead(room2);
Expand All @@ -304,7 +302,7 @@ describe("Read receipts", () => {
// When I restart
saveAndReload();

// Then is it still unread
// Then is it still read
assertRead(room2);
});
it("A room where all threaded edits are read is still read after restart", () => {
Expand All @@ -319,11 +317,11 @@ describe("Read receipts", () => {
saveAndReload();
assertRead(room2);
});
// XXX: fails because we see a dot instead of an unread number - probably the server and client disagree
// XXX: fails because the room becomes unread after restart
it.skip("A room where all threaded edits are marked as read is still read after restart", () => {
goTo(room1);
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), editOf("Resp1", "Edit1")]);
assertUnread(room2, 3);
assertUnread(room2, 2);
markAsRead(room2);
assertRead(room2);

Expand Down Expand Up @@ -358,7 +356,8 @@ describe("Read receipts", () => {
assertStillRead(room2);
assertReadThread("Edit1");
});
it("Reading an edit of a thread root leaves the room read", () => {
// XXX: fails because the room+thread become unread when I look at the room
it.skip("Reading an edit of a thread root leaves the room read", () => {
// Given a fully-read thread exists
goTo(room2);
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]);
Expand Down Expand Up @@ -392,8 +391,7 @@ describe("Read receipts", () => {
// Then the room stays read
assertStillRead(room2);
});
// XXX: fails because the room has an unread dot after I marked it as read
it.skip("Marking a room as read after an edit of a thread root keeps it read", () => {
it("Marking a room as read after an edit of a thread root keeps it read", () => {
// Given a fully-read thread exists
goTo(room2);
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]);
Expand All @@ -402,19 +400,19 @@ describe("Read receipts", () => {
goTo(room1);
assertRead(room2);

// When the thread root is edited
receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]);
// When the thread root is edited (and I receive another message
// to allow Mark as read)
receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1"), "Msg2"]);

// And I mark the room as read
// And when I mark the room as read
markAsRead(room2);

// Then the room becomes read and stays read
assertStillRead(room2);
goTo(room1);
assertStillRead(room2);
});
// XXX: fails because the room has an unread dot after I marked it as read
it.skip("Editing a thread root that is a reply after marking as read leaves the room read", () => {
it("Editing a thread root that is a reply after marking as read leaves the room read", () => {
// Given a thread based on a reply exists and is read because it is marked as read
goTo(room1);
receiveMessages(room2, ["Msg", replyTo("Msg", "Reply"), threadedOff("Reply", "InThread")]);
Expand All @@ -423,14 +421,14 @@ describe("Read receipts", () => {
assertRead(room2);

// When I edit the thread root
receiveMessages(room1, [editOf("Reply", "Edited Reply")]);
receiveMessages(room2, [editOf("Reply", "Edited Reply")]);

// Then the room is read
assertStillRead(room2);

// And the thread is read
goTo(room2);
assertReadThread("EditedReply");
assertReadThread("Edited Reply");
});
it("Marking a room as read after an edit of a thread root that is a reply leaves it read", () => {
// Given a thread based on a reply exists and the reply has been edited
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/read-receipts/high-level.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ describe("Read receipts", () => {
});

describe("Paging up", () => {
// Flaky test https://github.com/vector-im/element-web/issues/26437
// XXX: Fails because flaky test https://github.com/vector-im/element-web/issues/26437
it.skip("Paging up through old messages after a room is read leaves the room read", () => {
// Given lots of messages are in the room, but we have read them
goTo(room1);
Expand Down
12 changes: 4 additions & 8 deletions cypress/e2e/read-receipts/new-messages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,7 @@
// Then all messages are still read
assertRead(room2);
});
// XXX: fails because the room remains unread even though I sent a message
it.skip("Me sending a message from a different client marks room as read", () => {
it.only("Me sending a message from a different client marks room as read", () => {

Check failure on line 223 in cypress/e2e/read-receipts/new-messages.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected focused test
// Given I have unread messages
goTo(room1);
assertRead(room2);
Expand Down Expand Up @@ -345,8 +344,7 @@
// Then thread does appear unread
assertUnreadThread("Msg1");
});
// XXX: fails because the room is still "bold" even though the notification counts all disappear
it.skip("Marking a room with unread threads as read makes it read", () => {
it("Marking a room with unread threads as read makes it read", () => {
// Given I have an unread thread
goTo(room1);
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]);
Expand All @@ -358,8 +356,7 @@
// Then the room is read
assertRead(room2);
});
// XXX: fails for the same reason as "Marking a room with unread threads as read makes it read"
it.skip("Sending a new thread message after marking as read makes it unread", () => {
it("Sending a new thread message after marking as read makes it unread", () => {
// Given a thread exists
goTo(room1);
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]);
Expand All @@ -374,8 +371,7 @@
// Then the room becomes unread
assertUnread(room2, 1);
});
// XXX: fails for the same reason as "Marking a room with unread threads as read makes it read"
it.skip("Sending a new different-thread message after marking as read makes it unread", () => {
it("Sending a new different-thread message after marking as read makes it unread", () => {
// Given 2 threads exist, and Thread2 has the latest message in it
goTo(room1);
receiveMessages(room2, ["Thread1", "Thread2", threadedOff("Thread1", "t1a")]);
Expand Down
61 changes: 56 additions & 5 deletions src/Unread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,51 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean {
return false;
}

for (const timeline of [room, ...room.getThreads()]) {
// If the current timeline has unread messages, we're done.
if (doesRoomOrThreadHaveUnreadMessages(timeline)) {
for (const withTimeline of [room, ...room.getThreads()]) {
if (doesTimelineHaveUnreadMessages(room, withTimeline.timeline)) {
// We found an unread, so the room is unread
return true;
}
}

// If we got here then no timelines were found with unread messages.
return false;
}

function doesTimelineHaveUnreadMessages(room: Room, timeline: Array<MatrixEvent>): boolean {
const myUserId = room.client.getSafeUserId();
const latestImportantEventId = findLatestImportantEvent(room.client, timeline)?.getId();
if (latestImportantEventId) {
return !room.hasUserReadEvent(myUserId, latestImportantEventId);
} else {
// We couldn't find an important event to check - check the unimportant ones.
const earliestUnimportantEventId = timeline.at(0)?.getId();
if (!earliestUnimportantEventId) {
// There are no events in this timeline - it is uninitialised, so we
// consider it read
return false;
} else if (room.hasUserReadEvent(myUserId, earliestUnimportantEventId)) {
// Some of the unimportant events are read, and there are no
// important ones after them, so we've read everything.
return false;
} else {
// We have events. and none of them are read. We must guess that
// the timeline is unread, because there could be older unread
// important events that we don't have loaded.
logger.warn("Falling back to unread room because of no read receipt or counting message found", {
roomId: room.roomId,
earliestUnimportantEventId: earliestUnimportantEventId,
});
return true;
}
}
}

export function doesRoomOrThreadHaveUnreadMessages(roomOrThread: Room | Thread): boolean {
const room = roomOrThread instanceof Thread ? roomOrThread.room : roomOrThread;
return doesTimelineHaveUnreadMessages(room, roomOrThread.timeline);

/*
// NOTE: this shares logic with hasUserReadEvent in
// matrix-js-sdk/src/models/read-receipt.ts. They are not combined (yet)
// because hasUserReadEvent is focussed on a single event, and this is
Expand Down Expand Up @@ -119,6 +153,23 @@ export function doesRoomOrThreadHaveUnreadMessages(roomOrThread: Room | Thread):
readUpToId,
});
return true;
*/
}

/**
* Look backwards through the timeline and find the last event that is
* "important" in the sense of isImportantEvent.
*
* @returns the latest important event, or null if none were found
*/
function findLatestImportantEvent(client: MatrixClient, timeline: Array<MatrixEvent>): MatrixEvent | null {
for (let index = timeline.length - 1; index >= 0; index--) {
const event = timeline[index];
if (isImportantEvent(client, event)) {
return event;
}
}
return null;
}

/**
Expand All @@ -140,7 +191,7 @@ function isImportantEvent(client: MatrixClient, event: MatrixEvent): boolean {
* If we can't find the event, we guess by saying of the receipt's timestamp is
* after this event's timestamp, then it's probably saying this event is read.
*/
function makeHasReceipt(
/*function makeHasReceipt(
roomOrThread: Room | Thread,
readUpToId: string | null,
myUserId: string,
Expand All @@ -163,4 +214,4 @@ function makeHasReceipt(
// We pick the more recent of the two receipts as the latest
const receiptTimestamp = Math.max(receiptTs, unthreadedReceiptTs);
return (ev) => ev.getTs() < receiptTimestamp;
}
}*/
Loading
Loading