@@ -22,33 +22,68 @@ that the user had read all events *up to* the referenced event. See the
22
22
[ Receiving notifications] ( #receiving-notifications ) section for more
23
23
information on how read receipts affect notification counts.
24
24
25
+ {{< added-in v="1.4" >}} Read receipts exist in three major forms:
26
+ * Unthreaded: Denotes a read-up-to receipt regardless of threads. This is how
27
+ pre-threading read receipts worked.
28
+ * Threaded, main timeline: Denotes a read-up-to receipt for events not in a
29
+ particular thread. Identified by the thread ID ` main ` .
30
+ * Threaded, in a thread: Denotes a read-up-to receipt within a particular
31
+ thread. Identified by the event ID of the thread root.
32
+
33
+ Threaded read receipts are discussed in further detail [ below] ( #threaded-read-receipts ) .
34
+
25
35
#### Events
26
36
27
- Each ` user_id ` , ` receipt_type ` pair must be associated with only a
28
- single ` event_id ` .
37
+ {{< changed-in v="1.4" >}} Each ` user_id ` , ` receipt_type ` , and categorisation
38
+ (unthreaded, or ` thread_id ` ) tuple must be associated with only a single
39
+ ` event_id ` .
29
40
30
41
{{% event event="m.receipt" %}}
31
42
32
43
#### Client behaviour
33
44
45
+ {{< changed-in v="1.4" >}} Altered to support threaded read receipts.
46
+
34
47
In ` /sync ` , receipts are listed under the ` ephemeral ` array of events
35
48
for a given room. New receipts that come down the event streams are
36
49
deltas which update existing mappings. Clients should replace older
37
- receipt acknowledgements based on ` user_id ` and ` receipt_type ` pairs.
50
+ receipt acknowledgements based on ` user_id ` , ` receipt_type ` , and the
51
+ ` thread_id ` (if present).
38
52
For example:
39
53
40
54
Client receives m.receipt:
41
55
user = @alice:example.com
42
56
receipt_type = m.read
43
57
event_id = $aaa:example.com
58
+ thread_id = undefined
44
59
45
60
Client receives another m.receipt:
46
61
user = @alice:example.com
47
62
receipt_type = m.read
48
63
event_id = $bbb:example.com
64
+ thread_id = main
65
+
66
+ The client does not replace any acknowledgements, yet.
67
+
68
+ Client receives yet another m.receipt:
69
+ user = @alice:example.com
70
+ receipt_type = m.read
71
+ event_id = $ccc:example.com
72
+ thread_id = undefined
73
+
74
+ The client replaces the older acknowledgement for $aaa:example.com
75
+ with this new one for $ccc:example.com, but does not replace the
76
+ acknowledgement for $bbb:example.com because it belongs to a thread.
49
77
50
- The client should replace the older acknowledgement for $aaa:example.com with
51
- this one for $bbb:example.com
78
+ Client receives yet another m.receipt:
79
+ user = @alice:example.com
80
+ receipt_type = m.read
81
+ event_id = $ddd:example.com
82
+ thread_id = main
83
+
84
+ Now the client replaces the older $bbb:example.com acknowledgement with
85
+ this new $ddd:example.com acknowledgement. The client does NOT replace the
86
+ older acknowledgement for $ccc:example.com as it is unthreaded.
52
87
53
88
Clients should send read receipts when there is some certainty that the
54
89
event in question has been ** displayed** to the user. Simply receiving
@@ -58,6 +93,12 @@ room that the event was sent to or dismissing a notification in order
58
93
for the event to count as "read". Clients SHOULD NOT send read receipts
59
94
for events sent by their own user.
60
95
96
+ Similar to the rules for sending receipts, threaded receipts should appear
97
+ in the context of the thread. If a thread is rendered behind a disclosure,
98
+ the client hasn't yet shown the event (or any applicable read receipts)
99
+ to the user. Once they expand the thread though, a threaded read receipt
100
+ would be sent and per-thread receipts from other users shown.
101
+
61
102
A client can update the markers for its user by interacting with the
62
103
following HTTP APIs.
63
104
@@ -87,6 +128,89 @@ not have their notification counts rewound to that point in time. While
87
128
uncommon, it is considered valid to have an ` m.read ` (public) receipt lag
88
129
several messages behind the ` m.read.private ` receipt, for example.
89
130
131
+ ##### Threaded read receipts
132
+
133
+ {{% added-in v="1.4" %}}
134
+
135
+ If a client does not use [ threading] ( #threading ) , then they will simply only
136
+ send "unthreaded" read receipts which affect the whole room regardless of threads.
137
+
138
+ A threaded read receipt is simply one which has a ` thread_id ` on it, targeting
139
+ either a thread root's event ID or ` main ` for the main timeline.
140
+
141
+ Threading introduces a concept of multiple conversations being held in the same
142
+ room and thus deserve their own read receipts and notification counts. An event is
143
+ considered to be "in a thread" if it meets any of the following criteria:
144
+ * It has a ` rel_type ` of ` m.thread ` .
145
+ * It has child events with a ` rel_type ` of ` m.thread ` (in which case it'd be the
146
+ thread root).
147
+ * Following the event relationships, it has a parent event which qualifies for
148
+ one of the above. Implementations should not recurse infinitely, though: a
149
+ maximum of 3 hops is recommended to cover indirect relationships.
150
+
151
+ Events not in a thread but still in the room are considered to be part of the
152
+ "main timeline", or a special thread with an ID of ` main ` .
153
+
154
+ The following is an example DAG for a room, with dotted lines showing event
155
+ relationships and solid lines showing topological ordering.
156
+
157
+ ![ threaded-dag] ( /diagrams/threaded-dag.png )
158
+
159
+ {{% boxes/note %}}
160
+ ` m.reaction ` relationships are not currently specified, but are shown here for
161
+ their conceptual place in a threaded DAG. They are currently proposed as
162
+ [ MSC2677] ( https://github.com/matrix-org/matrix-spec-proposals/pull/2677 ) .
163
+ {{% /boxes/note %}}
164
+
165
+ This DAG can be represented as 3 threaded timelines, with ` A ` and ` B ` being thread
166
+ roots:
167
+
168
+ ![ threaded-dag-threads] ( /diagrams/threaded-dag-threads.png )
169
+
170
+ With this, we can demonstrate that:
171
+ * A threaded read receipt on ` I ` would mark ` A ` , ` B ` , and ` I ` as read.
172
+ * A threaded read receipt on ` E ` would mark ` C ` and ` E ` as read.
173
+ * An unthreaded read receipt on ` D ` would mark ` A ` , ` B ` , ` C ` , and ` D ` as read.
174
+
175
+ Note that marking ` A ` as read with a threaded read receipt would not mean
176
+ that ` C ` , ` E ` , ` G ` , or ` H ` get marked as read: Thread A's timeline would need
177
+ its own threaded read receipt at ` H ` to accomplish that.
178
+
179
+ The read receipts for the above 3 examples would be:
180
+
181
+ ``` json
182
+ {
183
+ "$I" : {
184
+ "m.read" : {
185
+ "@user:example.org" : {
186
+ "ts" : 1661384801651 ,
187
+ "thread_id" : " main" // because `I` is not in a thread, but is a threaded receipt
188
+ }
189
+ }
190
+ },
191
+ "$E" : {
192
+ "m.read" : {
193
+ "@user:example.org" : {
194
+ "ts" : 1661384801651 ,
195
+ "thread_id" : " $A" // because `E` is in Thread `A`
196
+ }
197
+ }
198
+ },
199
+ "$D" : {
200
+ "m.read" : {
201
+ "@user:example.org" : {
202
+ "ts" : 1661384801651
203
+ // no `thread_id` because the receipt is *unthreaded*
204
+ }
205
+ }
206
+ }
207
+ }
208
+ ```
209
+
210
+ Conditions on sending read receipts apply similarly to threaded and unthreaded read
211
+ receipts. For example, a client might send a private read receipt for a threaded
212
+ event when the user expands that thread.
213
+
90
214
#### Server behaviour
91
215
92
216
For efficiency, receipts SHOULD be batched into one event per room
@@ -99,7 +223,7 @@ format of the EDUs are:
99
223
{
100
224
<room_id>: {
101
225
<receipt_type>: {
102
- <user_id>: { <content> }
226
+ <user_id>: { <content (ts & thread_id, currently) > }
103
227
},
104
228
...
105
229
},
0 commit comments