Skip to content

Commit

Permalink
Implement changes to MSC2285
Browse files Browse the repository at this point in the history
Signed-off-by: Šimon Brandner <[email protected]>
  • Loading branch information
SimonBrandner committed Mar 5, 2022
1 parent 58f0e44 commit c3a3d38
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 45 deletions.
66 changes: 36 additions & 30 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;
if (rpEvent) {
rpEventId = rpEvent.getId();
if (room && room.hasPendingEvent(rpEventId)) {
throw new Error(`Cannot set read receipt to a pending event (${rpEventId})`);
}
if (room) {
room.addLocalEchoReceipt(this.credentials.userId, rpEvent, ReceiptType.Read);
}
}

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

/**
Expand Down Expand Up @@ -7354,16 +7360,16 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
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
28 changes: 21 additions & 7 deletions src/sync-accumulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -165,6 +166,7 @@ interface IRoom {
_readReceipts: {
[userId: string]: {
data: IMinimalEvent;
type: ReceiptType;
eventId: string;
};
};
Expand Down Expand Up @@ -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,
};
});
Expand Down Expand Up @@ -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
);
});
Expand Down

0 comments on commit c3a3d38

Please sign in to comment.