diff --git a/src/client.ts b/src/client.ts index b56c2052bb9..d95330903c9 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 TypedEventEmitter { 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 ); });