1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-30 04:23:07 +03:00

Rewrite receipt-handling code (#3901)

* Rewrite receipt-handling code

* Add tests around dangling receipts

* Fix mark as read for some rooms

* Add missing word

---------

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
Co-authored-by: Florian Duros <florianduros@element.io>
This commit is contained in:
Andy Balaam
2023-11-28 14:43:48 +00:00
committed by GitHub
parent a7496627fc
commit c49a527e5e
12 changed files with 1385 additions and 80 deletions

View File

@ -0,0 +1,541 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { FeatureSupport, MatrixClient, MatrixEvent, ReceiptContent, THREAD_RELATION_TYPE, Thread } from "../../../src";
import { Room } from "../../../src/models/room";
/**
* Note, these tests check the functionality of the RoomReceipts class, but most
* of them access that functionality via the surrounding Room class, because a
* room is required for RoomReceipts to function, and this matches the pattern
* of how this code is used in the wild.
*/
describe("RoomReceipts", () => {
beforeAll(() => {
jest.replaceProperty(Thread, "hasServerSideSupport", FeatureSupport.Stable);
});
afterAll(() => {
jest.restoreAllMocks();
});
it("reports events unread if there are no receipts", () => {
// Given there are no receipts in the room
const room = createRoom();
const [event] = createEvent();
room.addLiveEvents([event]);
// When I ask about any event, then it is unread
expect(room.hasUserReadEvent(readerId, event.getId()!)).toBe(false);
});
it("reports events we sent as read even if there are no (real) receipts", () => {
// Given there are no receipts in the room
const room = createRoom();
const [event] = createEventSentBy(readerId);
room.addLiveEvents([event]);
// When I ask about an event I sent, it is read (because a synthetic
// receipt was created and stored in RoomReceipts)
expect(room.hasUserReadEvent(readerId, event.getId()!)).toBe(true);
});
it("reports read if we receive an unthreaded receipt for this event", () => {
// Given my event exists and is unread
const room = createRoom();
const [event, eventId] = createEvent();
room.addLiveEvents([event]);
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for this event+user
room.addReceipt(createReceipt(readerId, event));
// Then that event is read
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
});
it("reports read if we receive an unthreaded receipt for a later event", () => {
// Given we have 2 events
const room = createRoom();
const [event1, event1Id] = createEvent();
const [event2] = createEvent();
room.addLiveEvents([event1, event2]);
// When we receive a receipt for the later event
room.addReceipt(createReceipt(readerId, event2));
// Then the earlier one is read
expect(room.hasUserReadEvent(readerId, event1Id)).toBe(true);
});
it("reports read for a non-live event if we receive an unthreaded receipt for a live one", () => {
// Given we have 2 events: one live and one old
const room = createRoom();
const [oldEvent, oldEventId] = createEvent();
const [liveEvent] = createEvent();
room.addLiveEvents([liveEvent]);
createOldTimeline(room, [oldEvent]);
// When we receive a receipt for the live event
room.addReceipt(createReceipt(readerId, liveEvent));
// Then the earlier one is read
expect(room.hasUserReadEvent(readerId, oldEventId)).toBe(true);
});
it("compares by timestamp if two events are in separate old timelines", () => {
// Given we have 2 events, both in old timelines, with event2 after
// event1 in terms of timestamps
const room = createRoom();
const [event1, event1Id] = createEvent();
const [event2, event2Id] = createEvent();
event1.event.origin_server_ts = 1;
event2.event.origin_server_ts = 2;
createOldTimeline(room, [event1]);
createOldTimeline(room, [event2]);
// When we receive a receipt for the older event
room.addReceipt(createReceipt(readerId, event1));
// Then the earlier one is read and the later one is not
expect(room.hasUserReadEvent(readerId, event1Id)).toBe(true);
expect(room.hasUserReadEvent(readerId, event2Id)).toBe(false);
});
it("reports unread if we receive an unthreaded receipt for an earlier event", () => {
// Given we have 2 events
const room = createRoom();
const [event1] = createEvent();
const [event2, event2Id] = createEvent();
room.addLiveEvents([event1, event2]);
// When we receive a receipt for the earlier event
room.addReceipt(createReceipt(readerId, event1));
// Then the later one is unread
expect(room.hasUserReadEvent(readerId, event2Id)).toBe(false);
});
it("reports unread if we receive an unthreaded receipt for a different user", () => {
// Given my event exists and is unread
const room = createRoom();
const [event, eventId] = createEvent();
room.addLiveEvents([event]);
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for another user
room.addReceipt(createReceipt(otherUserId, event));
// Then the event is still unread since the receipt was not for us
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// But it's read for the other person
expect(room.hasUserReadEvent(otherUserId, eventId)).toBe(true);
});
it("reports events we sent as read even if an earlier receipt arrives", () => {
// Given we sent an event after some other event
const room = createRoom();
const [previousEvent] = createEvent();
const [myEvent] = createEventSentBy(readerId);
room.addLiveEvents([previousEvent, myEvent]);
// And I just received a receipt for the previous event
room.addReceipt(createReceipt(readerId, previousEvent));
// When I ask about the event I sent, it is read (because of synthetic receipts)
expect(room.hasUserReadEvent(readerId, myEvent.getId()!)).toBe(true);
});
it("considers events after ones we sent to be unread", () => {
// Given we sent an event, then another event came in
const room = createRoom();
const [myEvent] = createEventSentBy(readerId);
const [laterEvent] = createEvent();
room.addLiveEvents([myEvent, laterEvent]);
// When I ask about the later event, it is unread (because it's after the synthetic receipt)
expect(room.hasUserReadEvent(readerId, laterEvent.getId()!)).toBe(false);
});
it("correctly reports readness even when receipts arrive out of order", () => {
// Given we have 3 events
const room = createRoom();
const [event1] = createEvent();
const [event2, event2Id] = createEvent();
const [event3, event3Id] = createEvent();
room.addLiveEvents([event1, event2, event3]);
// When we receive receipts for the older events out of order
room.addReceipt(createReceipt(readerId, event2));
room.addReceipt(createReceipt(readerId, event1));
// Then we correctly ignore the older receipt
expect(room.hasUserReadEvent(readerId, event2Id)).toBe(true);
expect(room.hasUserReadEvent(readerId, event3Id)).toBe(false);
});
it("reports read if we receive a threaded receipt for this event (main)", () => {
// Given my event exists and is unread
const room = createRoom();
const [event, eventId] = createEvent();
room.addLiveEvents([event]);
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for this event+user
room.addReceipt(createThreadedReceipt(readerId, event, "main"));
// Then that event is read
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
});
it("reports read if we receive a threaded receipt for this event (non-main)", () => {
// Given my event exists and is unread
const room = createRoom();
const [root, rootId] = createEvent();
const [event, eventId] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event]);
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for this event on this thread
room.addReceipt(createThreadedReceipt(readerId, event, rootId));
// Then that event is read
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
});
it("reports read if we receive an threaded receipt for a later event", () => {
// Given we have 2 events in a thread
const room = createRoom();
const [root, rootId] = createEvent();
const [event1, event1Id] = createThreadedEvent(root);
const [event2] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event1, event2]);
// When we receive a receipt for the later event
room.addReceipt(createThreadedReceipt(readerId, event2, rootId));
// Then the earlier one is read
expect(room.hasUserReadEvent(readerId, event1Id)).toBe(true);
});
it("reports unread if we receive an threaded receipt for an earlier event", () => {
// Given we have 2 events in a thread
const room = createRoom();
const [root, rootId] = createEvent();
const [event1] = createThreadedEvent(root);
const [event2, event2Id] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event1, event2]);
// When we receive a receipt for the earlier event
room.addReceipt(createThreadedReceipt(readerId, event1, rootId));
// Then the later one is unread
expect(room.hasUserReadEvent(readerId, event2Id)).toBe(false);
});
it("reports unread if we receive an threaded receipt for a different user", () => {
// Given my event exists and is unread
const room = createRoom();
const [root, rootId] = createEvent();
const [event, eventId] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event]);
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for another user
room.addReceipt(createThreadedReceipt(otherUserId, event, rootId));
// Then the event is still unread since the receipt was not for us
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// But it's read for the other person
expect(room.hasUserReadEvent(otherUserId, eventId)).toBe(true);
});
it("reports unread if we receive a receipt for a later event in a different thread", () => {
// Given 2 events exist in different threads
const room = createRoom();
const [root1] = createEvent();
const [root2] = createEvent();
const [thread1, thread1Id] = createThreadedEvent(root1);
const [thread2] = createThreadedEvent(root2);
setupThread(room, root1);
setupThread(room, root2);
room.addLiveEvents([root1, root2, thread1, thread2]);
// When we receive a receipt for the later event
room.addReceipt(createThreadedReceipt(readerId, thread2, root2.getId()!));
// Then the old one is still unread since the receipt was not for this thread
expect(room.hasUserReadEvent(readerId, thread1Id)).toBe(false);
});
it("correctly reports readness even when threaded receipts arrive out of order", () => {
// Given we have 3 events
const room = createRoom();
const [root, rootId] = createEvent();
const [event1] = createThreadedEvent(root);
const [event2, event2Id] = createThreadedEvent(root);
const [event3, event3Id] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event1, event2, event3]);
// When we receive receipts for the older events out of order
room.addReceipt(createThreadedReceipt(readerId, event2, rootId));
room.addReceipt(createThreadedReceipt(readerId, event1, rootId));
// Then we correctly ignore the older receipt
expect(room.hasUserReadEvent(readerId, event2Id)).toBe(true);
expect(room.hasUserReadEvent(readerId, event3Id)).toBe(false);
});
it("correctly reports readness when mixing threaded and unthreaded receipts", () => {
// Given we have a setup from this presentation:
// https://docs.google.com/presentation/d/1H1gxRmRFAm8d71hCILWmpOYezsvdlb7cB6ANl-20Gns/edit?usp=sharing
//
// Main1----\
// | ---Thread1a <- threaded receipt
// | |
// | Thread1b
// threaded receipt -> Main2--\
// | ----------------Thread2a <- unthreaded receipt
// Main3 |
// Thread2b <- threaded receipt
//
const room = createRoom();
const [main1, main1Id] = createEvent();
const [main2, main2Id] = createEvent();
const [main3, main3Id] = createEvent();
const [thread1a, thread1aId] = createThreadedEvent(main1);
const [thread1b, thread1bId] = createThreadedEvent(main1);
const [thread2a, thread2aId] = createThreadedEvent(main2);
const [thread2b, thread2bId] = createThreadedEvent(main2);
setupThread(room, main1);
setupThread(room, main2);
room.addLiveEvents([main1, thread1a, thread1b, main2, thread2a, main3, thread2b]);
// And the timestamps on the events are consistent with the order above
main1.event.origin_server_ts = 1;
thread1a.event.origin_server_ts = 2;
thread1b.event.origin_server_ts = 3;
main2.event.origin_server_ts = 4;
thread2a.event.origin_server_ts = 5;
main3.event.origin_server_ts = 6;
thread2b.event.origin_server_ts = 7;
// (Note: in principle, we have the information needed to order these
// events without using their timestamps, since they all came in via
// addLiveEvents. In reality, some of them would have come in via the
// /relations API, making it impossible to get the correct ordering
// without MSC4033, which is why we fall back to timestamps. I.e. we
// definitely could fix the code to make the above
// timestamp-manipulation unnecessary, but it would only make this test
// neater, not actually help in the real world.)
// When the receipts arrive
room.addReceipt(createThreadedReceipt(readerId, main2, "main"));
room.addReceipt(createThreadedReceipt(readerId, thread1a, main1Id));
room.addReceipt(createReceipt(readerId, thread2a));
room.addReceipt(createThreadedReceipt(readerId, thread2b, main2Id));
// Then we correctly identify that only main3 is unread
expect(room.hasUserReadEvent(readerId, main1Id)).toBe(true);
expect(room.hasUserReadEvent(readerId, main2Id)).toBe(true);
expect(room.hasUserReadEvent(readerId, main3Id)).toBe(false);
expect(room.hasUserReadEvent(readerId, thread1aId)).toBe(true);
expect(room.hasUserReadEvent(readerId, thread1bId)).toBe(true);
expect(room.hasUserReadEvent(readerId, thread2aId)).toBe(true);
expect(room.hasUserReadEvent(readerId, thread2bId)).toBe(true);
});
describe("dangling receipts", () => {
it("reports unread if the unthreaded receipt is in a dangling state", () => {
const room = createRoom();
const [event, eventId] = createEvent();
// When we receive a receipt for this event+user
room.addReceipt(createReceipt(readerId, event));
// The event is not added in the room
// So the receipt is in a dangling state
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// Add the event to the room
// The receipt is removed from the dangling state
room.addLiveEvents([event]);
// Then the event is read
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
});
it("reports unread if the threaded receipt is in a dangling state", () => {
const room = createRoom();
const [root, rootId] = createEvent();
const [event, eventId] = createThreadedEvent(root);
setupThread(room, root);
// When we receive a receipt for this event+user
room.addReceipt(createThreadedReceipt(readerId, event, rootId));
// The event is not added in the room
// So the receipt is in a dangling state
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// Add the events to the room
// The receipt is removed from the dangling state
room.addLiveEvents([root, event]);
// Then the event is read
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
});
it("should handle multiple dangling receipts for the same event", () => {
const room = createRoom();
const [event, eventId] = createEvent();
// When we receive a receipt for this event+user
room.addReceipt(createReceipt(readerId, event));
// We receive another receipt in the same event for another user
room.addReceipt(createReceipt(otherUserId, event));
// The event is not added in the room
// So the receipt is in a dangling state
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// Add the event to the room
// The two receipts should be processed
room.addLiveEvents([event]);
// Then the event is read
// We expect that the receipt of `otherUserId` didn't replace/erase the receipt of `readerId`
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
});
});
});
function createFakeClient(): MatrixClient {
return {
getUserId: jest.fn(),
getEventMapper: jest.fn().mockReturnValue(jest.fn()),
isInitialSyncComplete: jest.fn().mockReturnValue(true),
supportsThreads: jest.fn().mockReturnValue(true),
fetchRoomEvent: jest.fn().mockResolvedValue({}),
paginateEventTimeline: jest.fn(),
canSupport: { get: jest.fn() },
} as unknown as MatrixClient;
}
const senderId = "sender:s.ss";
const readerId = "reader:r.rr";
const otherUserId = "other:o.oo";
function createRoom(): Room {
return new Room("!rid", createFakeClient(), "@u:s.nz", { timelineSupport: true });
}
let idCounter = 0;
function nextId(): string {
return "$" + (idCounter++).toString(10);
}
/**
* Create an event and return it and its ID.
*/
function createEvent(): [MatrixEvent, string] {
return createEventSentBy(senderId);
}
/**
* Create an event with the supplied sender and return it and its ID.
*/
function createEventSentBy(customSenderId: string): [MatrixEvent, string] {
const event = new MatrixEvent({ sender: customSenderId, event_id: nextId() });
return [event, event.getId()!];
}
/**
* Create an event in the thread of the supplied root and return it and its ID.
*/
function createThreadedEvent(root: MatrixEvent): [MatrixEvent, string] {
const rootEventId = root.getId()!;
const event = new MatrixEvent({
sender: senderId,
event_id: nextId(),
content: {
"m.relates_to": {
event_id: rootEventId,
rel_type: THREAD_RELATION_TYPE.name,
["m.in_reply_to"]: {
event_id: rootEventId,
},
},
},
});
return [event, event.getId()!];
}
function createReceipt(userId: string, referencedEvent: MatrixEvent): MatrixEvent {
const content: ReceiptContent = {
[referencedEvent.getId()!]: {
"m.read": {
[userId]: {
ts: 123,
},
},
},
};
return new MatrixEvent({
type: "m.receipt",
content,
});
}
function createThreadedReceipt(userId: string, referencedEvent: MatrixEvent, threadId: string): MatrixEvent {
const content: ReceiptContent = {
[referencedEvent.getId()!]: {
"m.read": {
[userId]: {
ts: 123,
thread_id: threadId,
},
},
},
};
return new MatrixEvent({
type: "m.receipt",
content,
});
}
/**
* Create a timeline in the timeline set that is not the live timeline.
*/
function createOldTimeline(room: Room, events: MatrixEvent[]) {
const oldTimeline = room.getUnfilteredTimelineSet().addTimeline();
room.getUnfilteredTimelineSet().addEventsToTimeline(events, true, oldTimeline);
}
/**
* Perform the hacks required for this room to create a thread based on the root
* event supplied.
*/
function setupThread(room: Room, root: MatrixEvent) {
const thread = room.createThread(root.getId()!, root, [root], false);
thread.initialEventsFetched = true;
}

View File

@ -19,7 +19,7 @@ import { mocked } from "jest-mock";
import { MatrixClient, PendingEventOrdering } from "../../../src/client";
import { Room, RoomEvent } from "../../../src/models/room";
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread";
import { makeThreadEvent, mkThread } from "../../test-utils/thread";
import { makeThreadEvent, mkThread, populateThread } from "../../test-utils/thread";
import { TestClient } from "../../TestClient";
import { emitPromise, mkEdit, mkMessage, mkReaction, mock } from "../../test-utils/test-utils";
import { Direction, EventStatus, EventType, MatrixEvent } from "../../../src";
@ -149,20 +149,38 @@ describe("Thread", () => {
});
it("considers other events with no RR as unread", () => {
const { thread, events } = mkThread({
// Given a long thread exists
const { thread, events } = populateThread({
room,
client,
authorId: myUserId,
participantUserIds: [myUserId],
authorId: "@other:foo.com",
participantUserIds: ["@other:foo.com"],
length: 25,
ts: 190,
});
// Before alice's last unthreaded receipt
expect(thread.hasUserReadEvent("@alice:example.org", events.at(1)!.getId() ?? "")).toBeTruthy();
const event1 = events.at(1)!;
const event2 = events.at(2)!;
const event24 = events.at(24)!;
// After alice's last unthreaded receipt
expect(thread.hasUserReadEvent("@alice:example.org", events.at(-1)!.getId() ?? "")).toBeFalsy();
// And we have read the second message in it with an unthreaded receipt
const receipt = new MatrixEvent({
type: "m.receipt",
room_id: room.roomId,
content: {
// unthreaded receipt for the second message in the thread
[event2.getId()!]: {
[ReceiptType.Read]: {
[myUserId]: { ts: 200 },
},
},
},
});
room.addReceipt(receipt);
// Then we have read the first message in the thread, and not the last
expect(thread.hasUserReadEvent(myUserId, event1.getId()!)).toBe(true);
expect(thread.hasUserReadEvent(myUserId, event24.getId()!)).toBe(false);
});
it("considers event as read if there's a more recent unthreaded receipt", () => {
@ -481,13 +499,13 @@ describe("Thread", () => {
// And a thread with an added event (with later timestamp)
const userId = "user1";
const { thread, message } = await createThreadAndEvent(client, 1, 100, userId);
const { thread, message2 } = await createThreadAnd2Events(client, 1, 100, 200, userId);
// Then a receipt was added to the thread
const receipt = thread.getReadReceiptForUserId(userId);
expect(receipt).toBeTruthy();
expect(receipt?.eventId).toEqual(message.getId());
expect(receipt?.data.ts).toEqual(100);
expect(receipt?.eventId).toEqual(message2.getId());
expect(receipt?.data.ts).toEqual(200);
expect(receipt?.data.thread_id).toEqual(thread.id);
// (And the receipt was synthetic)
@ -505,14 +523,14 @@ describe("Thread", () => {
// And a thread with an added event with a lower timestamp than its other events
const userId = "user1";
const { thread } = await createThreadAndEvent(client, 200, 100, userId);
const { thread, message1 } = await createThreadAnd2Events(client, 300, 200, 100, userId);
// Then no receipt was added to the thread (the receipt is still
// for the thread root). This happens because since we have no
// Then the receipt is for the first message, because its
// timestamp is later. This happens because since we have no
// recursive relations support, we know that sometimes events
// appear out of order, so we have to check their timestamps as
// a guess of the correct order.
expect(thread.getReadReceiptForUserId(userId)?.eventId).toEqual(thread.rootEvent?.getId());
expect(thread.getReadReceiptForUserId(userId)?.eventId).toEqual(message1.getId());
});
});
@ -530,11 +548,11 @@ describe("Thread", () => {
// And a thread with an added event (with later timestamp)
const userId = "user1";
const { thread, message } = await createThreadAndEvent(client, 1, 100, userId);
const { thread, message2 } = await createThreadAnd2Events(client, 1, 100, 200, userId);
// Then a receipt was added to the thread
const receipt = thread.getReadReceiptForUserId(userId);
expect(receipt?.eventId).toEqual(message.getId());
expect(receipt?.eventId).toEqual(message2.getId());
});
it("Creates a local echo receipt even for events BEFORE an existing receipt", async () => {
@ -550,22 +568,24 @@ describe("Thread", () => {
// And a thread with an added event with a lower timestamp than its other events
const userId = "user1";
const { thread, message } = await createThreadAndEvent(client, 200, 100, userId);
const { thread, message2 } = await createThreadAnd2Events(client, 300, 200, 100, userId);
// Then a receipt was added to the thread, because relations
// recursion is available, so we trust the server to have
// provided us with events in the right order.
// Then a receipt was added for the last message, even though it
// has lower ts, because relations recursion is available, so we
// trust the server to have provided us with events in the right
// order.
const receipt = thread.getReadReceiptForUserId(userId);
expect(receipt?.eventId).toEqual(message.getId());
expect(receipt?.eventId).toEqual(message2.getId());
});
});
async function createThreadAndEvent(
async function createThreadAnd2Events(
client: MatrixClient,
rootTs: number,
eventTs: number,
message1Ts: number,
message2Ts: number,
userId: string,
): Promise<{ thread: Thread; message: MatrixEvent }> {
): Promise<{ thread: Thread; message1: MatrixEvent; message2: MatrixEvent }> {
const room = new Room("room1", client, userId);
// Given a thread
@ -576,24 +596,41 @@ describe("Thread", () => {
participantUserIds: [],
ts: rootTs,
});
// Sanity: the current receipt is for the thread root
expect(thread.getReadReceiptForUserId(userId)?.eventId).toEqual(thread.rootEvent?.getId());
// Sanity: there is no read receipt on the thread yet because the
// thread events don't get properly added to the room by mkThread.
expect(thread.getReadReceiptForUserId(userId)).toBeNull();
const awaitTimelineEvent = new Promise<void>((res) => thread.on(RoomEvent.Timeline, () => res()));
// When we add a message that is before the latest receipt
const message = makeThreadEvent({
// Add a message with ts message1Ts
const message1 = makeThreadEvent({
event: true,
rootEventId: thread.id,
replyToEventId: thread.id,
user: userId,
room: room.roomId,
ts: eventTs,
ts: message1Ts,
});
await thread.addEvent(message, false, true);
await thread.addEvent(message1, false, true);
await awaitTimelineEvent;
return { thread, message };
// Sanity: the thread now has a properly-added event, so this event
// has a synthetic receipt.
expect(thread.getReadReceiptForUserId(userId)?.eventId).toEqual(message1.getId());
// Add a message with ts message2Ts
const message2 = makeThreadEvent({
event: true,
rootEventId: thread.id,
replyToEventId: thread.id,
user: userId,
room: room.roomId,
ts: message2Ts,
});
await thread.addEvent(message2, false, true);
await awaitTimelineEvent;
return { thread, message1, message2 };
}
function createClientWithEventMapper(canSupport: Map<Feature, ServerSupport> = new Map()): MatrixClient {