@@ -18,8 +18,21 @@ import "fake-indexeddb/auto";
1818
1919import HttpBackend from "matrix-mock-request" ;
2020
21- import { Category , ISyncResponse , MatrixClient , NotificationCountType , Room } from "../../src" ;
21+ import {
22+ Category ,
23+ ClientEvent ,
24+ EventType ,
25+ ISyncResponse ,
26+ MatrixClient ,
27+ MatrixEvent ,
28+ NotificationCountType ,
29+ RelationType ,
30+ Room ,
31+ } from "../../src" ;
2232import { TestClient } from "../TestClient" ;
33+ import { ReceiptType } from "../../src/@types/read_receipts" ;
34+ import { mkThread } from "../test-utils/thread" ;
35+ import { SyncState } from "../../src/sync" ;
2336
2437describe ( "MatrixClient syncing" , ( ) => {
2538 const userA = "@alice:localhost" ;
@@ -51,6 +64,86 @@ describe("MatrixClient syncing", () => {
5164 return httpBackend ! . stop ( ) ;
5265 } ) ;
5366
67+ it ( "reactions in thread set the correct timeline to unread" , async ( ) => {
68+ const roomId = "!room:localhost" ;
69+
70+ // start the client, and wait for it to initialise
71+ httpBackend ! . when ( "GET" , "/sync" ) . respond ( 200 , {
72+ next_batch : "s_5_3" ,
73+ rooms : {
74+ [ Category . Join ] : { } ,
75+ [ Category . Leave ] : { } ,
76+ [ Category . Invite ] : { } ,
77+ } ,
78+ } ) ;
79+ client ! . startClient ( { threadSupport : true } ) ;
80+ await Promise . all ( [
81+ httpBackend ?. flushAllExpected ( ) ,
82+ new Promise < void > ( ( resolve ) => {
83+ client ! . on ( ClientEvent . Sync , ( state ) => state === SyncState . Syncing && resolve ( ) ) ;
84+ } ) ,
85+ ] ) ;
86+
87+ const room = new Room ( roomId , client ! , selfUserId ) ;
88+ jest . spyOn ( client ! , "getRoom" ) . mockImplementation ( ( id ) => ( id === roomId ? room : null ) ) ;
89+
90+ const thread = mkThread ( { room, client : client ! , authorId : selfUserId , participantUserIds : [ selfUserId ] } ) ;
91+ const threadReply = thread . events . at ( - 1 ) ! ;
92+ room . addLiveEvents ( [ thread . rootEvent ] ) ;
93+
94+ // Initialize read receipt datastructure before testing the reaction
95+ room . addReceiptToStructure ( thread . rootEvent . getId ( ) ! , ReceiptType . Read , selfUserId , { ts : 1 } , false ) ;
96+ thread . thread . addReceiptToStructure (
97+ threadReply . getId ( ) ! ,
98+ ReceiptType . Read ,
99+ selfUserId ,
100+ { thread_id : thread . thread . id , ts : 1 } ,
101+ false ,
102+ ) ;
103+ expect ( room . getReadReceiptForUserId ( selfUserId , false ) ?. eventId ) . toEqual ( thread . rootEvent . getId ( ) ) ;
104+ expect ( thread . thread . getReadReceiptForUserId ( selfUserId , false ) ?. eventId ) . toEqual ( threadReply . getId ( ) ) ;
105+
106+ const reactionEventId = `$9-${ Math . random ( ) } -${ Math . random ( ) } ` ;
107+ let lastEvent : MatrixEvent | null = null ;
108+ jest . spyOn ( client ! as any , "sendEventHttpRequest" ) . mockImplementation ( ( event ) => {
109+ lastEvent = event as MatrixEvent ;
110+ return { event_id : reactionEventId } ;
111+ } ) ;
112+
113+ await client ! . sendEvent ( roomId , EventType . Reaction , {
114+ "m.relates_to" : {
115+ rel_type : RelationType . Annotation ,
116+ event_id : threadReply . getId ( ) ,
117+ key : "" ,
118+ } ,
119+ } ) ;
120+
121+ expect ( lastEvent ! . getId ( ) ) . toEqual ( reactionEventId ) ;
122+ room . handleRemoteEcho ( new MatrixEvent ( lastEvent ! . event ) , lastEvent ! ) ;
123+
124+ // Our ideal state after this is the following:
125+ //
126+ // Room: [synthetic: threadroot, actual: threadroot]
127+ // Thread: [synthetic: threadreaction, actual: threadreply]
128+ //
129+ // The reaction and reply are both in the thread, and their receipts should be isolated to the thread.
130+ // The reaction has not been acknowledged in a dedicated read receipt message, so only the synthetic receipt
131+ // should be updated.
132+
133+ // Ensure the synthetic receipt for the room has not been updated
134+ expect ( room . getReadReceiptForUserId ( selfUserId , false ) ?. eventId ) . toEqual ( thread . rootEvent . getId ( ) ) ;
135+ expect ( room . getEventReadUpTo ( selfUserId , false ) ) . toEqual ( thread . rootEvent . getId ( ) ) ;
136+ // Ensure the actual receipt for the room has not been updated
137+ expect ( room . getReadReceiptForUserId ( selfUserId , true ) ?. eventId ) . toEqual ( thread . rootEvent . getId ( ) ) ;
138+ expect ( room . getEventReadUpTo ( selfUserId , true ) ) . toEqual ( thread . rootEvent . getId ( ) ) ;
139+ // Ensure the synthetic receipt for the thread has been updated
140+ expect ( thread . thread . getReadReceiptForUserId ( selfUserId , false ) ?. eventId ) . toEqual ( reactionEventId ) ;
141+ expect ( thread . thread . getEventReadUpTo ( selfUserId , false ) ) . toEqual ( reactionEventId ) ;
142+ // Ensure the actual receipt for the thread has not been updated
143+ expect ( thread . thread . getReadReceiptForUserId ( selfUserId , true ) ?. eventId ) . toEqual ( threadReply . getId ( ) ) ;
144+ expect ( thread . thread . getEventReadUpTo ( selfUserId , true ) ) . toEqual ( threadReply . getId ( ) ) ;
145+ } ) ;
146+
54147 describe ( "Stuck unread notifications integration tests" , ( ) => {
55148 const ROOM_ID = "!room:localhost" ;
56149
0 commit comments