@@ -16,7 +16,7 @@ limitations under the License.
1616
1717import React , { createRef , ReactNode } from "react" ;
1818import 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" ;
2020import { MatrixEvent , MatrixEventEvent } from "matrix-js-sdk/src/models/event" ;
2121import { EventTimelineSet , IRoomTimelineData } from "matrix-js-sdk/src/models/event-timeline-set" ;
2222import { Direction , EventTimeline } from "matrix-js-sdk/src/models/event-timeline" ;
@@ -26,7 +26,7 @@ import { SyncState } from "matrix-js-sdk/src/sync";
2626import { RoomMember , RoomMemberEvent } from "matrix-js-sdk/src/models/room-member" ;
2727import { debounce , throttle } from "lodash" ;
2828import { 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" ;
3030import { Thread , ThreadEvent } from "matrix-js-sdk/src/models/thread" ;
3131import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts" ;
3232import { 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