diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index dbb891449cc..3e6423d7a45 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -33,8 +33,9 @@ import { IJoinedRoom, IStateEvent, IMinimalEvent, - NotificationCountType, IEphemeral, + NotificationCountType, IEphemeral, Room, } from "../../src"; +import { ReceiptType } from '../../src/@types/read_receipts'; import { UNREAD_THREAD_NOTIFICATIONS } from '../../src/@types/sync'; import * as utils from "../test-utils/test-utils"; import { TestClient } from "../TestClient"; @@ -1312,7 +1313,8 @@ describe("MatrixClient syncing", () => { join: { [roomOne]: { ephemeral: { - events: [], + events: [ + ], } as IEphemeral, timeline: { events: [ @@ -1397,6 +1399,9 @@ describe("MatrixClient syncing", () => { rooms: { join: { [roomOne]: { + ephemeral: { + events: [], + }, timeline: { events: [ utils.mkMessage({ @@ -1455,6 +1460,50 @@ describe("MatrixClient syncing", () => { expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(2); }); }); + + it("caches unknown threads receipts and replay them when the thread is created", async () => { + const THREAD_ID = "$unknownthread:localhost"; + + const receipt = { + type: "m.receipt", + room_id: "!foo:bar", + content: { + "$event1:localhost": { + [ReceiptType.Read]: { + "@alice:localhost": { ts: 666, thread_id: THREAD_ID }, + }, + }, + }, + }; + syncData.rooms.join[roomOne].ephemeral.events = [receipt]; + + httpBackend!.when("GET", "/sync").respond(200, syncData); + client!.startClient(); + + return Promise.all([ + httpBackend!.flushAllExpected(), + awaitSyncEvent(), + ]).then(() => { + const room = client?.getRoom(roomOne); + expect(room).toBeInstanceOf(Room); + + expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(true); + + const thread = room!.createThread(THREAD_ID, undefined, [], true); + + expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(false); + + const receipt = thread.getReadReceiptForUserId("@alice:localhost"); + + expect(receipt).toStrictEqual({ + "data": { + "thread_id": "$unknownthread:localhost", + "ts": 666, + }, + "eventId": "$event1:localhost", + }); + }); + }); }); describe("of a room", () => { diff --git a/src/models/room.ts b/src/models/room.ts index 4c3ebafbe82..bd12b9fc796 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -201,6 +201,7 @@ export class Room extends ReadReceipt { private txnToEvent: Record = {}; // Pending in-flight requests { string: MatrixEvent } private notificationCounts: NotificationCount = {}; private readonly threadNotifications = new Map(); + public readonly cachedThreadReadReceipts = new Map(); private readonly timelineSets: EventTimelineSet[]; public readonly threadsTimelineSets: EventTimelineSet[] = []; // any filtered timeline sets we're maintaining for this room @@ -2053,6 +2054,16 @@ export class Room extends ReadReceipt { this.updateThreadRootEvents(thread, toStartOfTimeline, false); } + // Pulling all the cached thread read receipts we've discovered when we + // did an initial sync, and applying them to the thread now that it exists + // on the client side + if (this.cachedThreadReadReceipts.has(threadId)) { + for (const { event, synthetic } of this.cachedThreadReadReceipts.get(threadId)!) { + this.addReceipt(event, synthetic); + } + this.cachedThreadReadReceipts.delete(threadId); + } + this.emit(ThreadEvent.New, thread, toStartOfTimeline); return thread; @@ -2628,13 +2639,24 @@ export class Room extends ReadReceipt { const receiptDestination: Thread | this | undefined = receiptForMainTimeline ? this : this.threads.get(receipt.thread_id ?? ""); - receiptDestination?.addReceiptToStructure( - eventId, - receiptType as ReceiptType, - userId, - receipt, - synthetic, - ); + + if (receiptDestination) { + receiptDestination.addReceiptToStructure( + eventId, + receiptType as ReceiptType, + userId, + receipt, + synthetic, + ); + } else { + // The thread does not exist locally, keep the read receipt + // in a cache locally, and re-apply the `addReceipt` logic + // when the thread is created + this.cachedThreadReadReceipts.set(receipt.thread_id!, [ + ...(this.cachedThreadReadReceipts.get(receipt.thread_id!) ?? []), + { event, synthetic }, + ]); + } }); }); });