Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions spec/unit/matrix-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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,
);
});
});
});
15 changes: 11 additions & 4 deletions spec/unit/sync-accumulator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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" },
},
Expand All @@ -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 },
},
Expand Down Expand Up @@ -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 },
},
Expand Down
21 changes: 21 additions & 0 deletions src/@types/read_receipts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
Copyright 2022 Šimon Brandner <simon.bra.ag@gmail.com>

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"
}
71 changes: 38 additions & 33 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1079,7 +1080,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// Figure out if we've read something or if it's just informational
const content = event.getContent();
const isSelf = Object.keys(content).filter(eid => {
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;
Expand Down Expand Up @@ -4466,13 +4473,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Send a receipt.
* @param {Event} event The event being acknowledged
* @param {string} receiptType The kind of receipt e.g. "m.read"
* @param {ReceiptType} receiptType The kind of receipt e.g. "m.read". Other than
* ReceiptType.Read are experimental!
* @param {object} body Additional content to send alongside the receipt.
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public sendReceipt(event: MatrixEvent, receiptType: string, body: any, callback?: Callback): Promise<{}> {
public sendReceipt(event: MatrixEvent, receiptType: ReceiptType, body: any, callback?: Callback): Promise<{}> {
if (typeof (body) === 'function') {
callback = body as any as Callback; // legacy
body = {};
Expand All @@ -4499,32 +4507,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Send a read receipt.
* @param {Event} event The event that has been read.
* @param {object} opts The options for the read receipt.
* @param {boolean} opts.hidden True to prevent the receipt from being sent to
* other users and homeservers. Default false (send to everyone). <b>This
* property is unstable and may change in the future.</b>
* @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);
}

/**
Expand All @@ -4537,16 +4532,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {MatrixEvent} rrEvent the event tracked by the read receipt. This is here for
* convenience because the RR and the RM are commonly updated at the same time as each
* other. The local echo of this receipt will be done if set. Optional.
* @param {object} opts Options for the read markers
* @param {object} opts.hidden True to hide the receipt from other users and homeservers.
* <b>This property is unstable and may change in the future.</b>
* @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)) {
Expand All @@ -4561,11 +4555,23 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
throw new Error(`Cannot set read receipt to a pending event (${rrEventId})`);
}
if (room) {
room.addLocalEchoReceipt(this.credentials.userId, rrEvent, "m.read");
room.addLocalEchoReceipt(this.credentials.userId, rrEvent, ReceiptType.Read);
}
}

// Add the optional private RR update, do local echo like `sendReceipt`
let rpEventId;
Comment thread
SimonBrandner marked this conversation as resolved.
Outdated
if (rpEvent) {
rpEventId = rpEvent.getId();
if (room && room.hasPendingEvent(rpEventId)) {
Comment thread
SimonBrandner marked this conversation as resolved.
Outdated
throw new Error(`Cannot set read receipt to a pending event (${rpEventId})`);
}
if (room) {
room.addLocalEchoReceipt(this.credentials.userId, rpEvent, ReceiptType.ReadPrivate);
}
Comment thread
SimonBrandner marked this conversation as resolved.
Outdated
}

return this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId, opts);
return this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId, rpEventId);
}

/**
Expand Down Expand Up @@ -7345,25 +7351,24 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {string} rrEventId ID of the event tracked by the read receipt. This is here
* for convenience because the RR and the RM are commonly updated at the same time as
* each other. Optional.
* @param {object} opts Options for the read markers.
* @param {object} opts.hidden True to hide the read receipt from other users. <b>This
* property is currently unstable and may change in the future.</b>
* @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);
Expand Down
17 changes: 9 additions & 8 deletions src/models/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: {
Expand Down Expand Up @@ -91,7 +92,7 @@ interface IWrappedReceipt {
}

interface ICachedReceipt {
type: string;
type: ReceiptType;
userId: string;
data: IReceipt;
}
Expand All @@ -100,7 +101,7 @@ type ReceiptCache = {[eventId: string]: ICachedReceipt[]};

interface IReceiptContent {
[eventId: string]: {
[type: string]: {
[key in ReceiptType]: {
[userId: string]: IReceipt;
};
};
Expand Down Expand Up @@ -1555,7 +1556,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
// 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
Expand Down Expand Up @@ -2017,7 +2018,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
*/
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;
});
Expand Down Expand Up @@ -2196,7 +2197,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
this.receiptCacheByEventId[eventId].push({
userId: userId,
type: receiptType,
type: receiptType as ReceiptType,
data: receipt,
});
});
Expand All @@ -2209,9 +2210,9 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
* 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);
}

Expand Down
Loading