From 58f0e44f9b32bbb707a43b726d92225634775509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 5 Mar 2022 07:53:41 +0100 Subject: [PATCH 1/9] Add `ReceiptType` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/@types/read_receipts.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/@types/read_receipts.ts diff --git a/src/@types/read_receipts.ts b/src/@types/read_receipts.ts new file mode 100644 index 00000000000..7a3ba268446 --- /dev/null +++ b/src/@types/read_receipts.ts @@ -0,0 +1,21 @@ +/* +Copyright 2022 Šimon Brandner + +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. +*/ + +export enum ReceiptType { + Read = "m.read", + FullyRead = "m.fully_read", + ReadPrivate = "org.matrix.msc2285.read.private" +} From 89c5867e9395f7dc2de609c543f82bb2853413e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 5 Mar 2022 08:43:39 +0100 Subject: [PATCH 2/9] Implement changes to MSC2285 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/client.ts | 71 ++++++++++++++++++++++------------------- src/models/room.ts | 17 +++++----- src/sync-accumulator.ts | 28 ++++++++++++---- 3 files changed, 68 insertions(+), 48 deletions(-) diff --git a/src/client.ts b/src/client.ts index b56c2052bb9..6f018040f09 100644 --- a/src/client.ts +++ b/src/client.ts @@ -178,6 +178,7 @@ import { CryptoStore } from "./crypto/store/base"; import { MediaHandler } from "./webrtc/mediaHandler"; import { IRefreshTokenResponse } from "./@types/auth"; import { TypedEventEmitter } from "./models/typed-event-emitter"; +import { ReceiptType } from "./@types/read_receipts"; export type Store = IStore; export type SessionStore = WebStorageSessionStore; @@ -1079,7 +1080,13 @@ export class MatrixClient extends TypedEventEmitter { - return Object.keys(content[eid]['m.read']).includes(this.getUserId()); + const read = content[eid][ReceiptType.Read]; + if (read && Object.keys(read).includes(this.getUserId())) return true; + + const readPrivate = content[eid][ReceiptType.ReadPrivate]; + if (readPrivate && Object.keys(readPrivate).includes(this.getUserId())) return true; + + return false; }).length > 0; if (!isSelf) return; @@ -4466,13 +4473,14 @@ export class MatrixClient extends TypedEventEmitter { + public sendReceipt(event: MatrixEvent, receiptType: ReceiptType, body: any, callback?: Callback): Promise<{}> { if (typeof (body) === 'function') { callback = body as any as Callback; // legacy body = {}; @@ -4499,32 +4507,19 @@ export class MatrixClient extends TypedEventEmitterThis - * property is unstable and may change in the future. + * @param {ReceiptType} receiptType other than ReceiptType.Read are experimental! Optional. * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: to an empty object * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public async sendReadReceipt(event: MatrixEvent, opts?: { hidden?: boolean }, callback?: Callback): Promise<{}> { - if (typeof (opts) === 'function') { - callback = opts as any as Callback; // legacy - opts = {}; - } - if (!opts) opts = {}; - + public async sendReadReceipt(event: MatrixEvent, receiptType = ReceiptType.Read, callback?: Callback): Promise<{}> { const eventId = event.getId(); const room = this.getRoom(event.getRoomId()); if (room && room.hasPendingEvent(eventId)) { throw new Error(`Cannot set read receipt to a pending event (${eventId})`); } - const addlContent = { - "org.matrix.msc2285.hidden": Boolean(opts.hidden), - }; - - return this.sendReceipt(event, "m.read", addlContent, callback); + return this.sendReceipt(event, receiptType, {}, callback); } /** @@ -4537,16 +4532,15 @@ export class MatrixClient extends TypedEventEmitterThis property is unstable and may change in the future. + * @param {MatrixEvent} rpEvent the m.read.private read receipt event for when we don't + * want other users to see the read receipts. This is experimental. Optional. * @return {Promise} Resolves: the empty object, {}. */ public async setRoomReadMarkers( roomId: string, rmEventId: string, - rrEvent: MatrixEvent, - opts: { hidden?: boolean }, + rrEvent?: MatrixEvent, + rpEvent?: MatrixEvent, ): Promise<{}> { const room = this.getRoom(roomId); if (room && room.hasPendingEvent(rmEventId)) { @@ -4561,11 +4555,23 @@ export class MatrixClient extends TypedEventEmitterThis - * property is currently unstable and may change in the future. + * @param {string} rpEventId rpEvent the m.read.private read receipt event for when we + * don't want other users to see the read receipts. This is experimental. Optional. * @return {Promise} Resolves: the empty object, {}. */ public setRoomReadMarkersHttpRequest( roomId: string, rmEventId: string, rrEventId: string, - opts: { hidden?: boolean }, + rpEventId: string, ): Promise<{}> { const path = utils.encodeUri("/rooms/$roomId/read_markers", { $roomId: roomId, }); const content = { - "m.fully_read": rmEventId, - "m.read": rrEventId, - "org.matrix.msc2285.hidden": Boolean(opts ? opts.hidden : false), + [ReceiptType.FullyRead]: rmEventId, + [ReceiptType.Read]: rrEventId, + [ReceiptType.ReadPrivate]: rpEventId, }; return this.http.authedRequest(undefined, Method.Post, path, undefined, content); diff --git a/src/models/room.ts b/src/models/room.ts index 7b019190cdf..8cd39355fbf 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -40,6 +40,7 @@ import { RoomState } from "./room-state"; import { Thread, ThreadEvent, EventHandlerMap as ThreadHandlerMap } from "./thread"; import { Method } from "../http-api"; import { TypedEventEmitter } from "./typed-event-emitter"; +import { ReceiptType } from "../@types/read_receipts"; // These constants are used as sane defaults when the homeserver doesn't support // the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be @@ -50,7 +51,7 @@ import { TypedEventEmitter } from "./typed-event-emitter"; const KNOWN_SAFE_ROOM_VERSION = '6'; const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6']; -function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: string): MatrixEvent { +function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent { // console.log("synthesizing receipt for "+event.getId()); return new MatrixEvent({ content: { @@ -91,7 +92,7 @@ interface IWrappedReceipt { } interface ICachedReceipt { - type: string; + type: ReceiptType; userId: string; data: IReceipt; } @@ -100,7 +101,7 @@ type ReceiptCache = {[eventId: string]: ICachedReceipt[]}; interface IReceiptContent { [eventId: string]: { - [type: string]: { + [key in ReceiptType]: { [userId: string]: IReceipt; }; }; @@ -1555,7 +1556,7 @@ export class Room extends TypedEventEmitter // Don't synthesize RR for m.room.redaction as this causes the RR to go missing. if (event.sender && event.getType() !== EventType.RoomRedaction) { this.addReceipt(synthesizeReceipt( - event.sender.userId, event, "m.read", + event.sender.userId, event, ReceiptType.Read, ), true); // Any live events from a user could be taken as implicit @@ -2017,7 +2018,7 @@ export class Room extends TypedEventEmitter */ public getUsersReadUpTo(event: MatrixEvent): string[] { return this.getReceiptsForEvent(event).filter(function(receipt) { - return receipt.type === "m.read"; + return [ReceiptType.Read, ReceiptType.ReadPrivate].includes(receipt.type); }).map(function(receipt) { return receipt.userId; }); @@ -2196,7 +2197,7 @@ export class Room extends TypedEventEmitter } this.receiptCacheByEventId[eventId].push({ userId: userId, - type: receiptType, + type: receiptType as ReceiptType, data: receipt, }); }); @@ -2209,9 +2210,9 @@ export class Room extends TypedEventEmitter * client the fact that we've sent one. * @param {string} userId The user ID if the receipt sender * @param {MatrixEvent} e The event that is to be acknowledged - * @param {string} receiptType The type of receipt + * @param {ReceiptType} receiptType The type of receipt */ - public addLocalEchoReceipt(userId: string, e: MatrixEvent, receiptType: string): void { + public addLocalEchoReceipt(userId: string, e: MatrixEvent, receiptType: ReceiptType): void { this.addReceipt(synthesizeReceipt(userId, e, receiptType), true); } diff --git a/src/sync-accumulator.ts b/src/sync-accumulator.ts index 65b5cf00ad5..345e917d48c 100644 --- a/src/sync-accumulator.ts +++ b/src/sync-accumulator.ts @@ -24,6 +24,7 @@ import { deepCopy } from "./utils"; import { IContent, IUnsigned } from "./models/event"; import { IRoomSummary } from "./models/room-summary"; import { EventType } from "./@types/event"; +import { ReceiptType } from "./@types/read_receipts"; interface IOpts { maxTimelineEntries?: number; @@ -165,6 +166,7 @@ interface IRoom { _readReceipts: { [userId: string]: { data: IMinimalEvent; + type: ReceiptType; eventId: string; }; }; @@ -433,13 +435,24 @@ export class SyncAccumulator { // of a hassle to work with. We'll inflate this back out when // getJSON() is called. Object.keys(e.content).forEach((eventId) => { - if (!e.content[eventId]["m.read"]) { + if (!e.content[eventId][ReceiptType.Read] && !e.content[eventId][ReceiptType.ReadPrivate]) { return; } - Object.keys(e.content[eventId]["m.read"]).forEach((userId) => { + const read = e.content[eventId][ReceiptType.Read]; + read && Object.keys(read).forEach((userId) => { // clobber on user ID currentData._readReceipts[userId] = { - data: e.content[eventId]["m.read"][userId], + data: e.content[eventId][ReceiptType.Read][userId], + type: ReceiptType.Read, + eventId: eventId, + }; + }); + const readPrivate = e.content[eventId][ReceiptType.ReadPrivate]; + readPrivate && Object.keys(readPrivate).forEach((userId) => { + // clobber on user ID + currentData._readReceipts[userId] = { + data: e.content[eventId][ReceiptType.ReadPrivate][userId], + type: ReceiptType.ReadPrivate, eventId: eventId, }; }); @@ -601,11 +614,12 @@ export class SyncAccumulator { Object.keys(roomData._readReceipts).forEach((userId) => { const receiptData = roomData._readReceipts[userId]; if (!receiptEvent.content[receiptData.eventId]) { - receiptEvent.content[receiptData.eventId] = { - "m.read": {}, - }; + receiptEvent.content[receiptData.eventId] = {}; + } + if (!receiptEvent.content[receiptData.eventId][receiptData.type]) { + receiptEvent.content[receiptData.eventId][receiptData.type] = {}; } - receiptEvent.content[receiptData.eventId]["m.read"][userId] = ( + receiptEvent.content[receiptData.eventId][receiptData.type][userId] = ( receiptData.data ); }); From 2845dcc72f7840960a72bea0f51d55b3fe498868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 5 Mar 2022 18:20:11 +0100 Subject: [PATCH 3/9] Improve tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- spec/unit/matrix-client.spec.ts | 41 ++++++++++++++++++++++++++++++ spec/unit/sync-accumulator.spec.js | 15 ++++++++--- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 760526e80a2..cf1d7c26ee6 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -14,6 +14,7 @@ import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib"; import { EventStatus, MatrixEvent } from "../../src/models/event"; import { Preset } from "../../src/@types/partials"; import * as testUtils from "../test-utils"; +import { ReceiptType } from "../../src/@types/read_receipts"; jest.useFakeTimers(); @@ -980,4 +981,44 @@ describe("MatrixClient", function() { client.supportsExperimentalThreads = supportsExperimentalThreads; }); }); + + describe("read-markers and read-receipts", () => { + it("setRoomReadMarkers", () => { + client.setRoomReadMarkersHttpRequest = jest.fn(); + const room = { + hasPendingEvent: jest.fn().mockReturnValue(false), + addLocalEchoReceipt: jest.fn(), + }; + const rrEvent = new MatrixEvent({ event_id: "read_event_id" }); + const rpEvent = new MatrixEvent({ event_id: "read_private_event_id" }); + client.getRoom = () => room; + + client.setRoomReadMarkers( + "room_id", + "read_marker_event_id", + rrEvent, + rpEvent, + ); + + expect(client.setRoomReadMarkersHttpRequest).toHaveBeenCalledWith( + "room_id", + "read_marker_event_id", + "read_event_id", + "read_private_event_id", + ); + expect(room.addLocalEchoReceipt).toHaveBeenCalledTimes(2); + expect(room.addLocalEchoReceipt).toHaveBeenNthCalledWith( + 1, + client.credentials.userId, + rrEvent, + ReceiptType.Read, + ); + expect(room.addLocalEchoReceipt).toHaveBeenNthCalledWith( + 2, + client.credentials.userId, + rpEvent, + ReceiptType.ReadPrivate, + ); + }); + }); }); diff --git a/spec/unit/sync-accumulator.spec.js b/spec/unit/sync-accumulator.spec.js index b089e0cebc1..5fe9a3611b0 100644 --- a/spec/unit/sync-accumulator.spec.js +++ b/spec/unit/sync-accumulator.spec.js @@ -15,6 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { ReceiptType } from "../../src/@types/read_receipts"; import { SyncAccumulator } from "../../src/sync-accumulator"; // The event body & unsigned object get frozen to assert that they don't get altered @@ -294,10 +295,13 @@ describe("SyncAccumulator", function() { room_id: "!foo:bar", content: { "$event1:localhost": { - "m.read": { + [ReceiptType.Read]: { "@alice:localhost": { ts: 1 }, "@bob:localhost": { ts: 2 }, }, + [ReceiptType.ReadPrivate]: { + "@dan:localhost": { ts: 4 }, + }, "some.other.receipt.type": { "@should_be_ignored:localhost": { key: "val" }, }, @@ -309,7 +313,7 @@ describe("SyncAccumulator", function() { room_id: "!foo:bar", content: { "$event2:localhost": { - "m.read": { + [ReceiptType.Read]: { "@bob:localhost": { ts: 2 }, // clobbers event1 receipt "@charlie:localhost": { ts: 3 }, }, @@ -337,12 +341,15 @@ describe("SyncAccumulator", function() { room_id: "!foo:bar", content: { "$event1:localhost": { - "m.read": { + [ReceiptType.Read]: { "@alice:localhost": { ts: 1 }, }, + [ReceiptType.ReadPrivate]: { + "@dan:localhost": { ts: 4 }, + }, }, "$event2:localhost": { - "m.read": { + [ReceiptType.Read]: { "@bob:localhost": { ts: 2 }, "@charlie:localhost": { ts: 3 }, }, From cd91fbf632c627654d3d3464083f14fd50c537c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 9 Mar 2022 15:11:52 +0100 Subject: [PATCH 4/9] Apply suggestions from review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/client.ts | 16 ++++++---------- src/sync-accumulator.ts | 36 ++++++++++++++++++++---------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/client.ts b/src/client.ts index 6f018040f09..0327b33f7ca 100644 --- a/src/client.ts +++ b/src/client.ts @@ -4548,27 +4548,23 @@ export class MatrixClient extends TypedEventEmitter { - // clobber on user ID - currentData._readReceipts[userId] = { - data: e.content[eventId][ReceiptType.Read][userId], - type: ReceiptType.Read, - eventId: eventId, - }; - }); + if (read) { + Object.keys(read).forEach((userId) => { + // clobber on user ID + currentData._readReceipts[userId] = { + data: e.content[eventId][ReceiptType.Read][userId], + type: ReceiptType.Read, + eventId: eventId, + }; + }); + } const readPrivate = e.content[eventId][ReceiptType.ReadPrivate]; - readPrivate && Object.keys(readPrivate).forEach((userId) => { - // clobber on user ID - currentData._readReceipts[userId] = { - data: e.content[eventId][ReceiptType.ReadPrivate][userId], - type: ReceiptType.ReadPrivate, - eventId: eventId, - }; - }); + if (readPrivate) { + Object.keys(readPrivate).forEach((userId) => { + // clobber on user ID + currentData._readReceipts[userId] = { + data: e.content[eventId][ReceiptType.ReadPrivate][userId], + type: ReceiptType.ReadPrivate, + eventId: eventId, + }; + }); + } }); }); } From 68111feb34c18cc3095433c9d71ea4f9b4869acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 8 Apr 2022 21:56:22 +0200 Subject: [PATCH 5/9] Update `getEventReadUpTo()` to handle private read receipts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/models/room.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/models/room.ts b/src/models/room.ts index 6acf4c82afc..69b90e2f314 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -2386,8 +2386,10 @@ export class Room extends TypedEventEmitter }); } - public getReadReceiptForUserId(userId: string, ignoreSynthesized = false): IWrappedReceipt | null { - const [realReceipt, syntheticReceipt] = this.receipts["m.read"]?.[userId] ?? []; + public getReadReceiptForUserId( + userId: string, ignoreSynthesized = false, receiptType = ReceiptType.Read, + ): IWrappedReceipt | null { + const [realReceipt, syntheticReceipt] = this.receipts[receiptType]?.[userId] ?? []; if (ignoreSynthesized) { return realReceipt; } @@ -2405,8 +2407,22 @@ export class Room extends TypedEventEmitter * @return {String} ID of the latest event that the given user has read, or null. */ public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null { - const readReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized); - return readReceipt?.eventId ?? null; + const timelineSet = this.getUnfilteredTimelineSet(); + const publicReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.Read); + const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.ReadPrivate); + + // If we have both, compare them + let comparison; + if (publicReadReceipt?.eventId && privateReadReceipt?.eventId) { + comparison = timelineSet.compareEventOrdering(publicReadReceipt?.eventId, privateReadReceipt?.eventId); + } + + // The public receipt is more likely to drift out of date so the private + // one has precedence + if (!comparison) return privateReadReceipt?.eventId ?? publicReadReceipt?.eventId ?? null; + + // If public read receipt is older, return the private one + return (comparison < 0) ? privateReadReceipt?.eventId : publicReadReceipt?.eventId; } /** From 94b30b5b7b834fce48c1a8850efe392f885f82aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 8 Apr 2022 21:56:32 +0200 Subject: [PATCH 6/9] Write tests for `getEventReadUpTo()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- spec/unit/room.spec.ts | 29 ++++++++++++++++++++++++++++- src/models/room.ts | 2 +- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 8cf41b5f5c4..f05c4da091c 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -23,6 +23,7 @@ import * as utils from "../test-utils/test-utils"; import { DuplicateStrategy, EventStatus, + EventTimelineSet, EventType, JoinRule, MatrixEvent, @@ -31,12 +32,13 @@ import { RoomEvent, } from "../../src"; import { EventTimeline } from "../../src/models/event-timeline"; -import { Room } from "../../src/models/room"; +import { IWrappedReceipt, Room } from "../../src/models/room"; import { RoomState } from "../../src/models/room-state"; import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event"; import { TestClient } from "../TestClient"; import { emitPromise } from "../test-utils/test-utils"; import { ThreadEvent } from "../../src/models/thread"; +import { ReceiptType } from "../../src/@types/read_receipts"; describe("Room", function() { const roomId = "!foo:bar"; @@ -2094,4 +2096,29 @@ describe("Room", function() { expect(room.eventShouldLiveIn(reply2, events, roots).shouldLiveInThread).toBeFalsy(); }); }); + + describe("getEventReadUpTo()", () => { + const client = new TestClient(userA).client; + const room = new Room(roomId, client, userA); + + it("handles missing receipt type", () => { + room.getReadReceiptForUserId = (userId, ignore, receiptType) => { + return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as IWrappedReceipt : null; + }; + + expect(room.getEventReadUpTo(userA)).toEqual("eventId"); + }); + + it("prefers older receipt", () => { + room.getReadReceiptForUserId = (userId, ignore, receiptType) => { + return (receiptType === ReceiptType.Read + ? { eventId: "eventId1" } + : { eventId: "eventId2" } + ) as IWrappedReceipt; + }; + room.getUnfilteredTimelineSet = () => ({ compareEventOrdering: (event1, event2) => 1 } as EventTimelineSet); + + expect(room.getEventReadUpTo(userA)).toEqual("eventId1"); + }); + }); }); diff --git a/src/models/room.ts b/src/models/room.ts index 69b90e2f314..9d6ba6b7593 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -94,7 +94,7 @@ interface IReceipt { ts: number; } -interface IWrappedReceipt { +export interface IWrappedReceipt { eventId: string; data: IReceipt; } From 6a25677ea13d622d8cd0c955e8523d2f79ffa6cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 6 May 2022 20:42:13 +0200 Subject: [PATCH 7/9] Give `getReadReceiptForUserId()` a JSDOC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/models/room.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/models/room.ts b/src/models/room.ts index 50cb9523631..1b791b964d1 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -2321,6 +2321,13 @@ export class Room extends TypedEventEmitter }); } + /** + * Gets the latest receipt for a given user in the room + * @param userId The id of the user for which we want the receipt + * @param ignoreSynthesized Whether to ignore synthesized receipts or not + * @param receiptType Optional. The type of the receipt we want to get + * @returns the latest receipts of the chosen type for the chosen user + */ public getReadReceiptForUserId( userId: string, ignoreSynthesized = false, receiptType = ReceiptType.Read, ): IWrappedReceipt | null { From 794599d837174297d5b81a3c636dd23a10af8f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 6 May 2022 21:08:02 +0200 Subject: [PATCH 8/9] Types! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/models/room.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room.ts b/src/models/room.ts index 1b791b964d1..e1acec2f896 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -2354,7 +2354,7 @@ export class Room extends TypedEventEmitter const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.ReadPrivate); // If we have both, compare them - let comparison; + let comparison: number | undefined; if (publicReadReceipt?.eventId && privateReadReceipt?.eventId) { comparison = timelineSet.compareEventOrdering(publicReadReceipt?.eventId, privateReadReceipt?.eventId); } From f62f94da78f111b4498c4cd3e13215df7322f062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 6 May 2022 21:26:38 +0200 Subject: [PATCH 9/9] Try to use receipt `ts`s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/models/room.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/models/room.ts b/src/models/room.ts index e1acec2f896..fc538a62aab 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -2359,6 +2359,9 @@ export class Room extends TypedEventEmitter comparison = timelineSet.compareEventOrdering(publicReadReceipt?.eventId, privateReadReceipt?.eventId); } + // If we didn't get a comparison try to compare the ts of the receipts + if (!comparison) comparison = publicReadReceipt?.data?.ts - privateReadReceipt?.data?.ts; + // The public receipt is more likely to drift out of date so the private // one has precedence if (!comparison) return privateReadReceipt?.eventId ?? publicReadReceipt?.eventId ?? null;