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

Commit 6739443

Browse files
committed
Fall back to receipt timestamp if we have no event (react-sdk part)
1 parent 810b8ff commit 6739443

File tree

2 files changed

+139
-14
lines changed

2 files changed

+139
-14
lines changed

src/Unread.ts

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,18 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean {
7272
}
7373

7474
export function doesRoomOrThreadHaveUnreadMessages(roomOrThread: Room | Thread): boolean {
75+
// NOTE: this shares logic with hasUserReadEvent in
76+
// matrix-js-sdk/src/models/read-receipt.ts. They are not combined (yet)
77+
// because hasUserReadEvent is focussed on a single event, and this is
78+
// focussed on the whole room/thread.
79+
7580
// If there are no messages yet in the timeline then it isn't fully initialised
7681
// and cannot be unread.
7782
if (!roomOrThread || roomOrThread.timeline.length === 0) {
7883
return false;
7984
}
8085

81-
const myUserId = MatrixClientPeg.get().getUserId();
86+
const myUserId = MatrixClientPeg.get().getUserId()!;
8287

8388
// as we don't send RRs for our own messages, make sure we special case that
8489
// if *we* sent the last message into the room, we consider it not unread!
@@ -90,34 +95,26 @@ export function doesRoomOrThreadHaveUnreadMessages(roomOrThread: Room | Thread):
9095
return false;
9196
}
9297

93-
// get the most recent read receipt sent by our account.
94-
// N.B. this is NOT a read marker (RM, aka "read up to marker"),
95-
// despite the name of the method :((
96-
const readUpToId = roomOrThread.getEventReadUpTo(myUserId!);
97-
98-
// this just looks at whatever history we have, which if we've only just started
99-
// up probably won't be very much, so if the last couple of events are ones that
100-
// don't count, we don't know if there are any events that do count between where
101-
// we have and the read receipt. We could fetch more history to try & find out,
102-
// but currently we just guess.
98+
const readUpToId = roomOrThread.getEventReadUpTo(myUserId);
99+
const hasReceipt = makeHasReceipt(roomOrThread, readUpToId, myUserId);
103100

104101
// Loop through messages, starting with the most recent...
105102
for (let i = roomOrThread.timeline.length - 1; i >= 0; --i) {
106103
const ev = roomOrThread.timeline[i];
107104

108-
if (ev.getId() == readUpToId) {
105+
if (hasReceipt(ev)) {
109106
// If we've read up to this event, there's nothing more recent
110107
// that counts and we can stop looking because the user's read
111108
// this and everything before.
112109
return false;
113-
} else if (!shouldHideEvent(ev) && eventTriggersUnreadCount(ev)) {
110+
} else if (isImportantEvent(ev)) {
114111
// We've found a message that counts before we hit
115112
// the user's read receipt, so this room is definitely unread.
116113
return true;
117114
}
118115
}
119116

120-
// If we got here, we didn't find a message that counted but didn't find
117+
// If we got here, we didn't find a message was important but didn't find
121118
// the user's read receipt either, so we guess and say that the room is
122119
// unread on the theory that false positives are better than false
123120
// negatives here.
@@ -127,3 +124,49 @@ export function doesRoomOrThreadHaveUnreadMessages(roomOrThread: Room | Thread):
127124
});
128125
return true;
129126
}
127+
128+
/**
129+
* Given this event does not have a receipt, is it important enough to make
130+
* this room unread?
131+
*/
132+
function isImportantEvent(event: MatrixEvent): boolean {
133+
return !shouldHideEvent(event) && eventTriggersUnreadCount(event);
134+
}
135+
136+
/**
137+
* @returns a function that tells us whether a given event matches our read
138+
* receipt.
139+
*
140+
* We have the ID of an event based on a read receipt. If we can find the
141+
* corresponding event, then it's easy - our returned function just decides
142+
* whether the receipt refers to the event we are asking about.
143+
*
144+
* If we can't find the event, we guess by saying of the receipt's timestamp is
145+
* after this event's timestamp, then it's probably saying this event is read.
146+
*/
147+
function makeHasReceipt(
148+
roomOrThread: Room | Thread,
149+
readUpToId: string,
150+
myUserId: string,
151+
): (event: MatrixEvent) => boolean {
152+
// get the most recent read receipt sent by our account.
153+
// N.B. this is NOT a read marker (RM, aka "read up to marker"),
154+
// despite the name of the method :((
155+
const readEvent = roomOrThread.findEventById(readUpToId);
156+
157+
if (readEvent) {
158+
// If we found an event matching our receipt, then it's easy: this event
159+
// has a receipt if its ID is the same as the one in the receipt.
160+
return (ev) => ev.getId() == readUpToId;
161+
} else {
162+
// If we didn't, we have to guess by saying if this event is before the
163+
// receipt's ts, then it we pretend it has a receipt.
164+
const receipt = roomOrThread.getReadReceiptForUserId(myUserId);
165+
if (receipt) {
166+
const receiptTimestamp = receipt.data.ts;
167+
return (ev) => ev.getTs() < receiptTimestamp;
168+
} else {
169+
return (_ev) => false;
170+
}
171+
}
172+
}

test/Unread-test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,88 @@ describe("Unread", () => {
321321

322322
expect(doesRoomHaveUnreadMessages(room)).toBe(true);
323323
});
324+
325+
it("returns false when the event for a thread receipt can't be found, but the receipt ts is late", () => {
326+
// Given a room that is read
327+
let receipt = new MatrixEvent({
328+
type: "m.receipt",
329+
room_id: "!foo:bar",
330+
content: {
331+
[event.getId()!]: {
332+
[ReceiptType.Read]: {
333+
[myId]: { ts: 1 },
334+
},
335+
},
336+
},
337+
});
338+
room.addReceipt(receipt);
339+
340+
// And a thread
341+
const { rootEvent, events } = mkThread({ room, client, authorId: myId, participantUserIds: [aliceId] });
342+
343+
// When we provide a receipt that points at an unknown event,
344+
// but its timestamp is after all events in the thread
345+
//
346+
// (This could happen if we mis-filed a reaction into the main
347+
// thread when it should actually have gone into this thread, or
348+
// maybe the event is just not loaded for some reason.)
349+
const receiptTs = Math.max(...events.map((e) => e.getTs())) + 100;
350+
receipt = new MatrixEvent({
351+
type: "m.receipt",
352+
room_id: "!foo:bar",
353+
content: {
354+
["UNKNOWN_EVENT_ID"]: {
355+
[ReceiptType.Read]: {
356+
[myId]: { ts: receiptTs, threadId: rootEvent.getId()! },
357+
},
358+
},
359+
},
360+
});
361+
room.addReceipt(receipt);
362+
363+
expect(doesRoomHaveUnreadMessages(room)).toBe(false);
364+
});
365+
366+
it("returns true when the event for a thread receipt can't be found, and the receipt ts is early", () => {
367+
// Given a room that is read
368+
let receipt = new MatrixEvent({
369+
type: "m.receipt",
370+
room_id: "!foo:bar",
371+
content: {
372+
[event.getId()!]: {
373+
[ReceiptType.Read]: {
374+
[myId]: { ts: 1 },
375+
},
376+
},
377+
},
378+
});
379+
room.addReceipt(receipt);
380+
381+
// And a thread
382+
const { rootEvent, events } = mkThread({ room, client, authorId: myId, participantUserIds: [aliceId] });
383+
384+
// When we provide a receipt that points at an unknown event,
385+
// but its timestamp is before some of the events in the thread
386+
//
387+
// (This could happen if we mis-filed a reaction into the main
388+
// thread when it should actually have gone into this thread, or
389+
// maybe the event is just not loaded for some reason.)
390+
const receiptTs = events.at(-1).getTs() - 100;
391+
receipt = new MatrixEvent({
392+
type: "m.receipt",
393+
room_id: "!foo:bar",
394+
content: {
395+
["UNKNOWN_EVENT_ID"]: {
396+
[ReceiptType.Read]: {
397+
[myId]: { ts: receiptTs, threadId: rootEvent.getId()! },
398+
},
399+
},
400+
},
401+
});
402+
room.addReceipt(receipt);
403+
404+
expect(doesRoomHaveUnreadMessages(room)).toBe(true);
405+
});
324406
});
325407

326408
it("returns true for a room that only contains a hidden event", () => {

0 commit comments

Comments
 (0)