Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit e3e129f

Browse files
committed
Send correct receipts when viewing a room
1 parent f4f2cb1 commit e3e129f

File tree

3 files changed

+335
-153
lines changed

3 files changed

+335
-153
lines changed

src/components/structures/TimelinePanel.tsx

Lines changed: 156 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ limitations under the License.
1616

1717
import React, { createRef, ReactNode } from "react";
1818
import ReactDOM from "react-dom";
19-
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
19+
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
2020
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
2121
import { EventTimelineSet, IRoomTimelineData } from "matrix-js-sdk/src/models/event-timeline-set";
2222
import { Direction, EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
@@ -26,7 +26,7 @@ import { SyncState } from "matrix-js-sdk/src/sync";
2626
import { RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/models/room-member";
2727
import { debounce, throttle } from "lodash";
2828
import { logger } from "matrix-js-sdk/src/logger";
29-
import { ClientEvent } from "matrix-js-sdk/src/client";
29+
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
3030
import { Thread, ThreadEvent } from "matrix-js-sdk/src/models/thread";
3131
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
3232
import { MatrixError } from "matrix-js-sdk/src/http-api";
@@ -256,6 +256,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
256256

257257
// A map of <callId, LegacyCallEventGrouper>
258258
private callEventGroupers = new Map<string, LegacyCallEventGrouper>();
259+
private initialReadMarkerId: string | null = null;
259260

260261
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
261262
super(props, context);
@@ -265,13 +266,12 @@ class TimelinePanel extends React.Component<IProps, IState> {
265266

266267
// XXX: we could track RM per TimelineSet rather than per Room.
267268
// but for now we just do it per room for simplicity.
268-
let initialReadMarker: string | null = null;
269269
if (this.props.manageReadMarkers) {
270270
const readmarker = this.props.timelineSet.room?.getAccountData("m.fully_read");
271271
if (readmarker) {
272-
initialReadMarker = readmarker.getContent().event_id;
272+
this.initialReadMarkerId = readmarker.getContent().event_id;
273273
} else {
274-
initialReadMarker = this.getCurrentReadReceipt();
274+
this.initialReadMarkerId = this.getCurrentReadReceipt();
275275
}
276276
}
277277

@@ -283,7 +283,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
283283
canBackPaginate: false,
284284
canForwardPaginate: false,
285285
readMarkerVisible: true,
286-
readMarkerEventId: initialReadMarker,
286+
readMarkerEventId: this.initialReadMarkerId,
287287
backPaginating: false,
288288
forwardPaginating: false,
289289
clientSyncState: MatrixClientPeg.get().getSyncState(),
@@ -971,26 +971,64 @@ class TimelinePanel extends React.Component<IProps, IState> {
971971
continue; /* aborted */
972972
}
973973
// outside of try/catch to not swallow errors
974-
this.sendReadReceipt();
974+
this.sendReadReceipts();
975975
}
976976
}
977977

978-
private sendReadReceipt = (): void => {
979-
if (SettingsStore.getValue("lowBandwidth")) return;
978+
/**
979+
* Whether to send public or private receipts.
980+
*/
981+
private async determineReceiptType(client: MatrixClient): Promise<ReceiptType> {
982+
const roomId = this.props.timelineSet.room.roomId;
983+
const shouldSendPublicReadReceipts = SettingsStore.getValue("sendReadReceipts", roomId);
980984

981-
if (!this.messagePanel.current) return;
982-
if (!this.props.manageReadReceipts) return;
983-
// This happens on user_activity_end which is delayed, and it's
984-
// very possible have logged out within that timeframe, so check
985-
// we still have a client.
986-
const cli = MatrixClientPeg.get();
987-
// if no client or client is guest don't send RR or RM
988-
if (!cli || cli.isGuest()) return;
985+
if (shouldSendPublicReadReceipts) {
986+
return ReceiptType.Read;
987+
}
989988

990-
let shouldSendRR = true;
989+
if (
990+
!(await client.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) ||
991+
!(await client.isVersionSupported("v1.4"))
992+
) {
993+
// The server does not support private read receipt. Fall back to public ones.
994+
return ReceiptType.Read;
995+
}
991996

992-
const currentRREventId = this.getCurrentReadReceipt(true);
993-
const currentRREventIndex = this.indexForEventId(currentRREventId);
997+
return ReceiptType.ReadPrivate;
998+
}
999+
1000+
/**
1001+
* Whether a fully_read marker should be send.
1002+
*/
1003+
private shouldSendRM(): boolean {
1004+
if (this.lastRRSentEventId && this.lastRMSentEventId === this.state.readMarkerEventId) {
1005+
// Prevent sending the same receipt twice.
1006+
return false;
1007+
}
1008+
1009+
if (this.state.readMarkerEventId && this.state.readMarkerEventId === this.initialReadMarkerId) {
1010+
// The initial read marker is a historical one.
1011+
// Prevent sending it again.
1012+
return false;
1013+
}
1014+
1015+
if (this.props.timelineSet.thread) {
1016+
// Read marker for threads are not supported per spec.
1017+
return false;
1018+
}
1019+
1020+
return true;
1021+
}
1022+
1023+
/**
1024+
* Whether a read receipt should be send.
1025+
*/
1026+
private shouldSendRR(
1027+
currentRREventId: string | null,
1028+
currentRREventIndex: number | null,
1029+
lastReadEvent: MatrixEvent | null,
1030+
lastReadEventIndex: number | null,
1031+
): boolean {
9941032
// We want to avoid sending out read receipts when we are looking at
9951033
// events in the past which are before the latest RR.
9961034
//
@@ -1003,107 +1041,117 @@ class TimelinePanel extends React.Component<IProps, IState> {
10031041
// timeline which is *after* the latest RR (so we should actually send
10041042
// RRs) - but that is a bit of a niche case. It will sort itself out when
10051043
// the user eventually hits the live timeline.
1006-
//
1044+
10071045
if (
10081046
currentRREventId &&
10091047
currentRREventIndex === null &&
10101048
this.timelineWindow?.canPaginate(EventTimeline.FORWARDS)
10111049
) {
1012-
shouldSendRR = false;
1050+
return false;
10131051
}
1052+
// Only send a RR if the last read event is ahead in the timeline relative to the current RR event.
1053+
// Only send a RR if the last RR set != the one we would send
1054+
return (
1055+
(lastReadEventIndex === null || currentRREventIndex === null || lastReadEventIndex > currentRREventIndex) &&
1056+
(!this.lastRRSentEventId || this.lastRRSentEventId !== lastReadEvent?.getId())
1057+
);
1058+
}
1059+
1060+
private sendReadReceipts = async (): Promise<void> => {
1061+
if (SettingsStore.getValue("lowBandwidth")) return;
1062+
if (!this.messagePanel.current) return;
1063+
if (!this.props.manageReadReceipts) return;
10141064

1065+
// This happens on user_activity_end which is delayed, and it's
1066+
// very possible have logged out within that timeframe, so check
1067+
// we still have a client.
1068+
const client = MatrixClientPeg.get();
1069+
// if no client or client is guest don't send RR or RM
1070+
if (!client || client.isGuest()) return;
1071+
1072+
const currentRREventId = this.getCurrentReadReceipt(true);
1073+
const currentRREventIndex = this.indexForEventId(currentRREventId);
10151074
const lastReadEventIndex = this.getLastDisplayedEventIndex({
10161075
ignoreOwn: true,
10171076
});
1018-
if (lastReadEventIndex === null) {
1019-
shouldSendRR = false;
1020-
}
1021-
let lastReadEvent: MatrixEvent | null = this.state.events[lastReadEventIndex ?? 0];
1022-
shouldSendRR =
1023-
shouldSendRR &&
1024-
// Only send a RR if the last read event is ahead in the timeline relative to
1025-
// the current RR event.
1026-
lastReadEventIndex > currentRREventIndex &&
1027-
// Only send a RR if the last RR set != the one we would send
1028-
this.lastRRSentEventId !== lastReadEvent?.getId();
1029-
1030-
// Only send a RM if the last RM sent != the one we would send
1031-
const shouldSendRM = this.lastRMSentEventId != this.state.readMarkerEventId;
1032-
1033-
// we also remember the last read receipt we sent to avoid spamming the
1034-
// same one at the server repeatedly
1035-
if (shouldSendRR || shouldSendRM) {
1036-
if (shouldSendRR) {
1037-
this.lastRRSentEventId = lastReadEvent?.getId();
1038-
} else {
1039-
lastReadEvent = null;
1040-
}
1041-
this.lastRMSentEventId = this.state.readMarkerEventId;
1077+
const lastReadEvent: MatrixEvent | null = this.state.events[lastReadEventIndex ?? 0];
10421078

1043-
const roomId = this.props.timelineSet.room.roomId;
1044-
const sendRRs = SettingsStore.getValue("sendReadReceipts", roomId);
1079+
const shouldSendRR = this.shouldSendRR(
1080+
currentRREventId,
1081+
currentRREventIndex,
1082+
lastReadEvent,
1083+
lastReadEventIndex,
1084+
);
1085+
const shouldSendRM = this.shouldSendRM();
1086+
1087+
debuglog(`Sending Read Markers for ${this.props.timelineSet.room.roomId}: `, {
1088+
shouldSendRR,
1089+
shouldSendRM,
1090+
currentRREventId,
1091+
currentRREventIndex,
1092+
lastReadEventId: lastReadEvent?.getId(),
1093+
lastReadEventIndex,
1094+
readMarkerEventId: this.state.readMarkerEventId,
1095+
});
10451096

1046-
debuglog(
1047-
`Sending Read Markers for ${roomId}: `,
1048-
`rm=${this.state.readMarkerEventId} `,
1049-
`rr=${sendRRs ? lastReadEvent?.getId() : null} `,
1050-
`prr=${lastReadEvent?.getId()}`,
1051-
);
1097+
const proms = [];
10521098

1053-
if (this.props.timelineSet.thread && sendRRs && lastReadEvent) {
1054-
// There's no support for fully read markers on threads
1055-
// as defined by MSC3771
1056-
cli.sendReadReceipt(lastReadEvent, sendRRs ? ReceiptType.Read : ReceiptType.ReadPrivate);
1057-
} else {
1058-
cli.setRoomReadMarkers(
1059-
roomId,
1060-
this.state.readMarkerEventId ?? "",
1061-
sendRRs ? lastReadEvent ?? undefined : undefined, // Public read receipt (could be null)
1062-
lastReadEvent ?? undefined, // Private read receipt (could be null)
1063-
).catch(async (e): Promise<void> => {
1064-
// /read_markers API is not implemented on this HS, fallback to just RR
1065-
if (e.errcode === "M_UNRECOGNIZED" && lastReadEvent) {
1066-
if (
1067-
!sendRRs &&
1068-
!(await cli.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) &&
1069-
!(await cli.isVersionSupported("v1.4"))
1070-
)
1071-
return;
1072-
try {
1073-
await cli.sendReadReceipt(
1074-
lastReadEvent,
1075-
sendRRs ? ReceiptType.Read : ReceiptType.ReadPrivate,
1076-
);
1077-
return;
1078-
} catch (error) {
1079-
logger.error(e);
1080-
this.lastRRSentEventId = undefined;
1081-
}
1082-
} else {
1083-
logger.error(e);
1084-
}
1085-
// it failed, so allow retries next time the user is active
1086-
this.lastRRSentEventId = undefined;
1087-
this.lastRMSentEventId = undefined;
1088-
});
1099+
if (shouldSendRR && lastReadEvent) {
1100+
proms.push(this.sendReadReceipt(client, lastReadEvent));
1101+
}
10891102

1090-
// do a quick-reset of our unreadNotificationCount to avoid having
1091-
// to wait from the remote echo from the homeserver.
1092-
// we only do this if we're right at the end, because we're just assuming
1093-
// that sending an RR for the latest message will set our notif counter
1094-
// to zero: it may not do this if we send an RR for somewhere before the end.
1095-
if (this.isAtEndOfLiveTimeline() && this.props.timelineSet.room) {
1096-
this.props.timelineSet.room.setUnreadNotificationCount(NotificationCountType.Total, 0);
1097-
this.props.timelineSet.room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
1098-
dis.dispatch({
1099-
action: "on_room_read",
1100-
roomId: this.props.timelineSet.room.roomId,
1101-
});
1102-
}
1103+
if (shouldSendRM) {
1104+
const readMarkerEvent = this.props.timelineSet.findEventById(this.state.readMarkerEventId);
1105+
1106+
if (readMarkerEvent) {
1107+
proms.push(await this.sendReadMarker(client, readMarkerEvent));
11031108
}
11041109
}
1110+
1111+
await Promise.all(proms);
11051112
};
11061113

1114+
/**
1115+
* Sends a read receipt for event.
1116+
* Resets the last sent event Id in case of an error, so that it will be retried next time.
1117+
*/
1118+
private async sendReadReceipt(client: MatrixClient, event: MatrixEvent): Promise<void> {
1119+
this.lastRRSentEventId = event.getId();
1120+
const receiptType = await this.determineReceiptType(client);
1121+
1122+
try {
1123+
await client.sendReadReceipt(event, receiptType);
1124+
} catch (err) {
1125+
// it failed, so allow retries next time the user is active
1126+
this.lastRRSentEventId = undefined;
1127+
1128+
logger.error("Error sending receipt", {
1129+
room: this.props.timelineSet.room.roomId,
1130+
error: err,
1131+
});
1132+
}
1133+
}
1134+
1135+
/**
1136+
* Sends a fully_read marker for readMarkerEvent.
1137+
* Resets the last sent event Id in case of an error, so that it will be retried next time.
1138+
*/
1139+
private async sendReadMarker(client: MatrixClient, readMarkerEvent: MatrixEvent): Promise<void> {
1140+
this.lastRMSentEventId = this.state.readMarkerEventId;
1141+
1142+
try {
1143+
await client.sendReadReceipt(readMarkerEvent, ReceiptType.FullyRead, true);
1144+
} catch (err) {
1145+
// it failed, so allow retries next time the user is active
1146+
this.lastRMSentEventId = undefined;
1147+
1148+
logger.error("Error sending fully_read", {
1149+
room: this.props.timelineSet.room.roomId,
1150+
error: err,
1151+
});
1152+
}
1153+
}
1154+
11071155
// if the read marker is on the screen, we can now assume we've caught up to the end
11081156
// of the screen, so move the marker down to the bottom of the screen.
11091157
private updateReadMarker = (): void => {
@@ -1135,7 +1183,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
11351183
}
11361184

11371185
// Send the updated read marker (along with read receipt) to the server
1138-
this.sendReadReceipt();
1186+
this.sendReadReceipts();
11391187
};
11401188

11411189
// advance the read marker past any events we sent ourselves.
@@ -1220,7 +1268,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
12201268
this.loadTimeline(this.state.readMarkerEventId, 0, 1 / 3);
12211269
};
12221270

1223-
/* update the read-up-to marker to match the read receipt
1271+
/**
1272+
* update the read-up-to marker to match the read receipt
12241273
*/
12251274
public forgetReadMarker = (): void => {
12261275
if (!this.props.manageReadMarkers) return;
@@ -1244,7 +1293,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
12441293
this.setReadMarker(rmId, rmTs);
12451294

12461295
// Send the receipts to the server immediately (don't wait for activity)
1247-
this.sendReadReceipt();
1296+
this.sendReadReceipts();
12481297
};
12491298

12501299
/* return true if the content is fully scrolled down and we are
@@ -1437,7 +1486,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
14371486
}
14381487

14391488
if (this.props.sendReadReceiptOnLoad) {
1440-
this.sendReadReceipt();
1489+
this.sendReadReceipts();
14411490
}
14421491
},
14431492
);

0 commit comments

Comments
 (0)