Skip to content

Commit 227757d

Browse files
authored
Threads: Read receipts & notifications (#1255)
* Spec MSC3771: Threaded read receipts Note: this builds on a (as of writing) non-existent "threading" section, which is part of a different commit. * Spec MSC3773: Threaded notifications * changelog * Various clarifications per review
1 parent 25dda1e commit 227757d

File tree

12 files changed

+218
-10
lines changed

12 files changed

+218
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add per-thread notifications and read receipts, as per [MSC3771](https://github.com/matrix-org/matrix-spec-proposals/pull/3771) and [MSC3773](https://github.com/matrix-org/matrix-spec-proposals/pull/3773).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add per-thread notifications and read receipts, as per [MSC3771](https://github.com/matrix-org/matrix-spec-proposals/pull/3771) and [MSC3773](https://github.com/matrix-org/matrix-spec-proposals/pull/3773).

content/client-server-api/modules/push.md

+15-2
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,10 @@ determined by the push rules which apply to an event.
107107

108108
When the user updates their read receipt (either by using the API or by
109109
sending an event), notifications prior to and including that event MUST
110-
be marked as read. Note that users can send both an `m.read` and
111-
`m.read.private` receipt, both of which are capable of clearing notifications.
110+
be marked as read. Which specific events are affected can vary depending
111+
on whether a [threaded read receipt](#threaded-read-receipts) was used.
112+
Note that users can send both an `m.read` and `m.read.private` receipt,
113+
both of which are capable of clearing notifications.
112114

113115
If the user has both `m.read` and `m.read.private` set in the room then
114116
the receipt which is more recent/ahead must be used to determine where
@@ -121,6 +123,17 @@ ahead), however if the `m.read.private` receipt were to be updated to
121123
event D then the user has read up to D (the `m.read` receipt is now
122124
behind the `m.read.private` receipt).
123125

126+
{{< added-in v="1.4" >}} When handling threaded read receipts, the server
127+
is to partition the notification count to each thread (with the main timeline
128+
being its own thread). To determine if an event is part of a thread the
129+
server follows the [event relationship](#forming-relationships-between-events)
130+
until it finds a thread root (as specified by the [threading module](#threading)),
131+
however it is not recommended that the server traverse infinitely. Instead,
132+
implementations are encouraged to do a maximum of 3 hops to find a thread
133+
before deciding that the event does not belong to a thread. This is primarily
134+
to ensure that future events, like `m.reaction`, are correctly considered
135+
"part of" a given thread.
136+
124137
##### Push Rules
125138

126139
A push rule is a single rule that states under what *conditions* an

content/client-server-api/modules/receipts.md

+130-6
Original file line numberDiff line numberDiff line change
@@ -22,33 +22,68 @@ that the user had read all events *up to* the referenced event. See the
2222
[Receiving notifications](#receiving-notifications) section for more
2323
information on how read receipts affect notification counts.
2424

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+
2535
#### Events
2636

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`.
2940

3041
{{% event event="m.receipt" %}}
3142

3243
#### Client behaviour
3344

45+
{{< changed-in v="1.4" >}} Altered to support threaded read receipts.
46+
3447
In `/sync`, receipts are listed under the `ephemeral` array of events
3548
for a given room. New receipts that come down the event streams are
3649
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).
3852
For example:
3953

4054
Client receives m.receipt:
4155
user = @alice:example.com
4256
receipt_type = m.read
4357
event_id = $aaa:example.com
58+
thread_id = undefined
4459

4560
Client receives another m.receipt:
4661
user = @alice:example.com
4762
receipt_type = m.read
4863
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.
4977

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.
5287

5388
Clients should send read receipts when there is some certainty that the
5489
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
5893
for the event to count as "read". Clients SHOULD NOT send read receipts
5994
for events sent by their own user.
6095

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+
61102
A client can update the markers for its user by interacting with the
62103
following HTTP APIs.
63104

@@ -87,6 +128,89 @@ not have their notification counts rewound to that point in time. While
87128
uncommon, it is considered valid to have an `m.read` (public) receipt lag
88129
several messages behind the `m.read.private` receipt, for example.
89130

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+
90214
#### Server behaviour
91215

92216
For efficiency, receipts SHOULD be batched into one event per room
@@ -99,7 +223,7 @@ format of the EDUs are:
99223
{
100224
<room_id>: {
101225
<receipt_type>: {
102-
<user_id>: { <content> }
226+
<user_id>: { <content (ts & thread_id, currently)> }
103227
},
104228
...
105229
},

data/api/client-server/definitions/room_event_filter.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Copyright 2016 OpenMarket Ltd
2+
# Copyright 2022 The Matrix.org Foundation C.I.C.
23
#
34
# Licensed under the Apache License, Version 2.0 (the "License");
45
# you may not use this file except in compliance with the License.
@@ -16,6 +17,12 @@ allOf:
1617
- type: object
1718
title: RoomEventFilter
1819
properties:
20+
unread_thread_notifications:
21+
type: boolean
22+
description: |-
23+
If `true`, enables per-[thread](/client-server-api/#threading) notification
24+
counts. Only applies to the `/sync` endpoint. Defaults to `false`.
25+
x-addedInMatrixVersion: "1.4"
1926
lazy_load_members:
2027
type: boolean
2128
description: |-

data/api/client-server/sync.yaml

+45-2
Original file line numberDiff line numberDiff line change
@@ -239,17 +239,50 @@ paths:
239239
Counts of unread notifications for this room. See the
240240
[Receiving notifications](/client-server-api/#receiving-notifications) section
241241
for more information on how these are calculated.
242+
243+
If `unread_thread_notifications` was specified as `true` on the `RoomEventFilter`,
244+
these counts will only be for the main timeline rather than all events in the room.
245+
See the [threading module](#threading) for more information.
246+
x-changedInMatrixVersion:
247+
1.4: |
248+
Updated to reflect behaviour of having `unread_thread_notifications` as `true` in
249+
the `RoomEventFilter` for `/sync`.
242250
properties:
243251
highlight_count:
244252
title: Highlighted notification count
245253
type: integer
246254
description: The number of unread notifications
247-
for this room with the highlight flag set
255+
for this room with the highlight flag set.
248256
notification_count:
249257
title: Total notification count
250258
type: integer
251259
description: The total number of unread notifications
252-
for this room
260+
for this room.
261+
unread_thread_notifications:
262+
title: Unread Thread Notification Counts
263+
type: object
264+
description: |-
265+
If `unread_thread_notifications` was specified as `true` on the `RoomEventFilter`,
266+
the notification counts for each [thread](#threading) in this room. The object is
267+
keyed by thread root ID, with values matching `unread_notifications`.
268+
269+
If a thread does not have any notifications it can be omitted from this object. If
270+
no threads have notification counts, this whole object can be omitted.
271+
x-addedInMatrixVersion: "1.4"
272+
additionalProperties:
273+
title: ThreadNotificationCounts
274+
type: object
275+
properties:
276+
highlight_count:
277+
title: ThreadedHighlightNotificationCount
278+
type: integer
279+
description: |-
280+
The number of unread notifications for this *thread* with the highlight flag set.
281+
notification_count:
282+
title: ThreadedTotalNotificationCount
283+
type: integer
284+
description: |-
285+
The total number of unread notifications for this *thread*.
253286
invite:
254287
title: Invited Rooms
255288
type: object
@@ -424,6 +457,16 @@ paths:
424457
}
425458
}
426459
]
460+
},
461+
"unread_notifications": {
462+
"highlight_count": 1,
463+
"notification_count": 5
464+
},
465+
"unread_thread_notifications": {
466+
"$threadroot": {
467+
"highlight_count": 3,
468+
"notification_count": 6
469+
}
427470
}
428471
}
429472
},

data/api/server-server/definitions/event-schemas/m.receipt.yaml

+9
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ allOf:
6565
A POSIX timestamp in milliseconds for when the user read
6666
the event specified in the read receipt.
6767
example: 1533358089009
68+
thread_id:
69+
type: string
70+
x-addedInMatrixVersion: "1.4"
71+
description: |-
72+
The root thread event's ID (or `main`) for which
73+
thread this receipt is intended to be under. If
74+
not specified, the read receipt is *unthreaded*
75+
(default).
76+
example: "$threadroot"
6877
required: ['ts']
6978
required: ['event_ids', 'data']
7079
required: ['m.read']

data/event-schemas/schema/m.receipt.yaml

+8
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ properties:
3838
type: integer
3939
format: int64
4040
description: The timestamp the receipt was sent at.
41+
thread_id:
42+
type: string
43+
x-addedInMatrixVersion: "1.4"
44+
description: |-
45+
The root thread event's ID (or `main`) for which
46+
thread this receipt is intended to be under. If
47+
not specified, the read receipt is *unthreaded*
48+
(default).
4149
"m.read.private":
4250
type: object
4351
title: Own User
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<mxfile host="app.diagrams.net" modified="2022-09-27T03:26:23.216Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" etag="YZcXq9Sm_7Lqw5o2RvSU" version="14.6.7" type="device"><diagram id="_rQ0dgHO1UnHExDn0l7E" name="Page-1">7ZpdU+IwFIZ/DZc6bdNWuJQCujPquOvOrl452TbQaNowIXztr9+EJv0goLCirYwyo81JGpL3PQ8nU2mBIFlcMDiOr2mESMuxokUL9FqO41i+I/7IyDKL2JbvZZERw5GKFYE7/BfpgSo6xRGaVAZySgnH42owpGmKQl6JQcbovDpsSEn1XcdwhIzAXQiJGf2NIx5n0bZzVsQvER7F+p1tv5P1JFAPVjuZxDCi81II9FsgYJTy7CpZBIhI9bQu2X2DLb35whhK+S43/PIf6YMfPSfXNzfx1WP3ez/2ToDaxwySqdrxz5ghGInYudQaJ4jgFKkt8KXWhdFpGiE5td0C3XmMObobw1D2zkUqiFjME6K6hzTlA5hgIrPgEpEZ4jiEsgMTElBC2WpS0O/Jl4jPEJMjyDnBo1T0cTpW09ypJajty4FosVURO9dZZCiiCeJsKYbo9NTWqOT0dHteOO2c+acqY+Oyz201FKr8GuWzFxaIC+XCPo742x3pHrsjAFQdcS3TEdu3Nvhhv5sfruHHNcTpsTuRq7zUHJhO5G59jBO2YcS5If0rYsPJOCsSQ7yQBq2LHAR9byDW2D2Egp2qgsDeoOAGAcG76dc25EKRKHaqSRmP6YimkPSLaLeay8WYKyrTbhV8QpwvVeWGU06rkqMF5vfi2lLXD/JafKRmrd6i1NVb6kYq9nuvJ5CN0l2yWdy2aun7Kiz9FHhOxHZv0Fz8/kETmObGyn2/bKuQiU5ZiF7QUx1tOGQjxF/LWzNNGCKQ41l1HQc33TGg6TYZGsdvGDQueDM0T9NkrMenNEU1cFRC56GM1RaOCPyDSBeGz6PVTkpeDwL5eqlsHRAw8BkAAwZgwZsBOwRIbn5u1afbjlczSs4XSnWh5O6IklMnSuZJu9cElMBaTXLdumuSewwg7Xm2awhI3o4ggTpB8gyQ+o0AyW5eTbINYb5Q+iCU/B1RcutEyXwaN2gCSu76g7K6axLoGEIlpwzBkGOaHgNje577IjiJ8+XLxi3kHLF0FXEstzkYnr2toqmEPLFObfFTSUpbfbjvTKqa/ZZisYliCB0OJ2Jp6ymaL+L/s9b878dFE+g2C6Xv1lwoQXsD3yjC/BjY3rN+fh62Ozuy7dVZYs3KcdkECF3QvNOqZQhz/LQ1BCX9ILrZj1D0KkswfWsETO6HPdcXzeJLFdlBofhuCuj/Aw==</diagram></mxfile>
18.1 KB
Loading

static/diagrams/threaded-dag.drawio

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<mxfile host="app.diagrams.net" modified="2022-09-27T03:11:43.523Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" etag="L_ujIRop4Jndk67DcTE9" version="14.6.7" type="device"><diagram id="_rQ0dgHO1UnHExDn0l7E" name="Page-1">7VpbU+IwFP41PMqkTS/wKDfdGXXcdWdXn5wsDTTaNkwIAvvrN6UJbYlCF1gbXcYZTU5P0ub0u6S1DdiNFxcMTcJrGuCoYYNg0YC9hm3bwLPFnzSyzCIW8NwsMmYkkLE8cEd+Y5UoozMS4GkpkVMacTIpB4c0SfCQl2KIMTovp41oVD7rBI2xFrgbokiP/iQBD7Noy/bz+CUm41Cd2fLa2ZEYqWS5kmmIAjovhGC/AbuMUp614kUXR2n1VF2ycYM3jq4vjOGEVxnww3ukD17wHF/f3IRXj52v/dA9k7O8oGgmF3wur5YvVQnmIeH4boKGaX8u7nMDdkIeR6JniSaaTrLCj8gCi3N15JSYcbx481qtdQUEdjCNMWdLkaIGQFk0BRtH9uf5PVApYaH8KobkXR+vZ84LIxqyNn9Tp5ZWFhwIoMguZTykY5qgqJ9HO4zOkiAtyapOec4VpRMZfMKcLyXq0YzTcmnxgvB70Qay/ZC2m67s9RaFQ72l6iRivfdqgrRTGJV282Grnho3ogkfoJhEaeA7iQXpbHCD5+L3NxqjZH1j03Vvv62iTHTGhnhLPaUscMTGmO/Cpw4ThiPEyUv5Oo5+022NHB0TyGED08jhn8hxTHLAiuSwayWHr7EjbvKQYRQcjIanWTxR+QlN8IEAadpuASNWNYSApu8WQWLtgEiApuF6AWnnFnGOWbKK2MAR0Qj9wlEHDZ/Hq+V2aUTZqkBw0E1/NoF2iaMXzMkQ1QOvWrUXaujqGqG9vmna2z5p7zHJ4VQkB6xVe52Po71F6QXVALIpvWAHQj6O9FZFV63OroOrZ4L0Qsc06XVP0ntMcrgVyeHUKr3ux5He/ba99ifd9laF13YVOgNNB/pSIitDbjXdOWNoWUiYUJLwaeFst2kgP5WjZpSCp577B2/kW7C1LV80sivYGK0uh45GU1GYTYKsS7A/Z3TK9E0wFMeCTdcwS3FOlnJMznsVOe/Wain6e0ZjLWWv3bz9SXfzVdF16G5+L/dwwYa27XAPG7S25Zfd41jO4GnIHxjhDG3THjXgyReOyVy/InO9Wn1BZ0fcFK4w5IQmRjvDf/+KvSq+Dn2LuJ8ztMrOoH04sOkMbbgt/984g/7fpQsTnME17v2/fZASgJMzlOvZrshcv1ZnAK84Aw4If7ddwrvq/puKXcEQpkIeSDL+vloMyAOi54LXbbCA8wp2UgP2an1abWvQuzRBmD3XNGG2tLKchPkAcqgPQneyo10nOyxdmb+YQA///T6oFN38o9ZsO5h/Gwz7fwA=</diagram></mxfile>

static/diagrams/threaded-dag.png

11.3 KB
Loading

0 commit comments

Comments
 (0)