@@ -18,8 +18,21 @@ import "fake-indexeddb/auto";
18
18
19
19
import HttpBackend from "matrix-mock-request" ;
20
20
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" ;
22
32
import { TestClient } from "../TestClient" ;
33
+ import { ReceiptType } from "../../src/@types/read_receipts" ;
34
+ import { mkThread } from "../test-utils/thread" ;
35
+ import { SyncState } from "../../src/sync" ;
23
36
24
37
describe ( "MatrixClient syncing" , ( ) => {
25
38
const userA = "@alice:localhost" ;
@@ -51,6 +64,86 @@ describe("MatrixClient syncing", () => {
51
64
return httpBackend ! . stop ( ) ;
52
65
} ) ;
53
66
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
+
54
147
describe ( "Stuck unread notifications integration tests" , ( ) => {
55
148
const ROOM_ID = "!room:localhost" ;
56
149
0 commit comments