Skip to content

Commit 3b43d87

Browse files
clokeprichvdhtulirturt2liveanoadragon453
authored
MSC3771: Read receipts for threads (#3771)
* Add initial MSC for read receipts for threads. * Fix events in diagram. * Add sync response. * Link to the spec. Co-authored-by: Richard van der Hoff <[email protected]> * Clarify sentence. * Some clarifications. * Simplification. * Fix JSON key format. Co-authored-by: Tulir Asokan <[email protected]> * Add information on clearing notifications. * Fix example. * Update with current understanding. * Clarify introduction. * MSC3773 is not yet accepted. * Updates from feedback. * Update from learnings from the proof of concept. * Add link to the current spec. Co-authored-by: Travis Ralston <[email protected]> * Clarify that false positives are deliberate in the design. * Receipts must move forward. * More info on unthreaded receipts. * Reflow. * Clarify the proposal to explain why both threaded and unthreaded receipts need to exist and what the main timeline is. * Add information about validating that an event is part of a thread. * Remove section on second-order relations. * Use proper syntax highlighting. Co-authored-by: Andrew Morgan <[email protected]> * Clarify unthreaded vs. main timeline receipts. * Fix typos. Co-authored-by: Hubert Chathi <[email protected]> * Clarify wording. Co-authored-by: Hubert Chathi <[email protected]> * Clarify example. Co-authored-by: Hubert Chathi <[email protected]> * Fix alternatives section. Co-authored-by: Richard van der Hoff <[email protected]> Co-authored-by: Tulir Asokan <[email protected]> Co-authored-by: Travis Ralston <[email protected]> Co-authored-by: Andrew Morgan <[email protected]> Co-authored-by: Hubert Chathi <[email protected]>
1 parent 39f8040 commit 3b43d87

File tree

1 file changed

+373
-0
lines changed

1 file changed

+373
-0
lines changed
+373
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
# MSC3771: Read receipts for threads
2+
3+
## Background
4+
5+
Currently, each room can only have a single receipt of each type per user. The
6+
read receipt ([`m.read`](https://spec.matrix.org/v1.3/client-server-api/#receipts)
7+
or [`m.read.private`](https://github.com/matrix-org/matrix-spec-proposals/pull/2285))
8+
is used to sync the read status of a room across clients, to share with other
9+
users which events have been read, and is used by the homeserver to calculate the
10+
number of unread messages.
11+
12+
Now that [MSC3440](https://github.com/matrix-org/matrix-doc/pull/3440) has merged
13+
to add support for threads, there are two ways to display messages:
14+
15+
* *Unthreaded*: The traditional way of displaying messages before threads existed.
16+
All messages are just shown in the order they’re provided by the server as a
17+
single timeline[^1].
18+
* *Threaded*: Taking into account the `m.thread` and other relations to separate
19+
a room DAG into multiple sub-timelines:
20+
* One timeline for each root message (I.e. the target of a thread relation)
21+
* One for messages which are not part of a thread: the main timeline.
22+
23+
For an example room DAG (solid lines are show topological ordering, dotted lines
24+
show event relations):
25+
26+
```mermaid
27+
flowchart RL
28+
I-->H
29+
H-->G
30+
G-->F
31+
F-->E
32+
E-->D
33+
D-->C
34+
C-->B
35+
B-->A
36+
C-.->|m.thread|A
37+
D-.->|m.thread|B
38+
E-.->|m.thread|A
39+
F-.->|m.thread|B
40+
G-.->|m.reaction|C
41+
H-.->|m.edit|E
42+
```
43+
44+
This can be separated into three threaded timelines:
45+
46+
```mermaid
47+
flowchart RL
48+
subgraph "Main" timeline
49+
B-->A
50+
I-->B
51+
end
52+
subgraph Thread A timeline
53+
C-->A
54+
E-->C
55+
G-.->|m.reaction|C
56+
H-.->|m.edit|E
57+
end
58+
subgraph Thread B timeline
59+
D-->B
60+
F-->D
61+
end
62+
```
63+
64+
Due to this separation of messages into separate timelines a single read receipt
65+
per room causes missed (or flaky) notification counts and does not give an accurate
66+
representation of what messages have been read by people.
67+
68+
Note that it is expected that some clients will continue to show only an unthreaded
69+
view of the room, either until they are able to support a threaded view or because
70+
they do not wish to incorporate threads.
71+
72+
## Proposal
73+
74+
This MSC proposes allowing a receipt per thread, as well as an unthreaded receipt.
75+
Thus, receipts are split into two categories, which this document calls "unthreaded"
76+
and "threaded". Threaded receipts are identified by the root message of the thread;
77+
additionally there is a special pseudo-thread for the main timeline. This allows marking
78+
the main timeline (a pseudo-thread) as read, without marking any actual threads (split
79+
off from the main timeline) as read.
80+
81+
The most significant difference between threaded and unthreaded receipts is how
82+
they clear notifications:
83+
84+
* Unthreaded receipts clear notifications just as they do today (i.e.
85+
"notifications prior to and including that event MUST be marked as read").
86+
* Threaded receipts clear notifications in a similar way, but taking into account
87+
the thread the receipt is part of (i.e. "notifications generated from events
88+
with a thread relation matching the receipt’s thread ID prior to and including
89+
that event which are MUST be marked as read")
90+
91+
Using the above diagrams with threaded read receipts on `E` and `I`; and an
92+
unthreaded read receipt on `D` would give:
93+
94+
```mermaid
95+
flowchart RL
96+
subgraph "Main" timeline
97+
B-->A
98+
I-->B
99+
end
100+
subgraph Thread A timeline
101+
C-->A
102+
E-->C
103+
G-.->|m.reaction|C
104+
H-.->|m.edit|E
105+
end
106+
subgraph Thread B timeline
107+
D-->B
108+
F-->D
109+
end
110+
111+
classDef unthreaded fill:yellow,stroke:#333,stroke-width:2px
112+
classDef threaded fill:crimson,stroke:#333,stroke-width:2px
113+
classDef both fill:orange,stroke:#333,stroke-width:2px
114+
115+
%% An unthreaded read receipt on D marks A, B, C, D as read.
116+
class A,B,C both;
117+
class D unthreaded;
118+
%% Threaded read receipts on E and I mark C, E and A, B, I as
119+
%% read, respectively.
120+
class E,I threaded;
121+
```
122+
123+
As denoted by the colors:
124+
125+
* The unthreaded read receipt on `D` would mark `A`, `B`, `C`, and `D` as read.
126+
* The threaded read receipt on `E` would mark `C` and `E` as read.
127+
* The threaded read receipt on `I` would mark `A`, `B`, and `I` as read.
128+
129+
### Threaded receipts
130+
131+
This MSC proposes allowing the same receipt type to exist multiple times in a room
132+
per user:
133+
134+
* Once for the unthreaded timeline.
135+
* Once for the main timeline in the room.
136+
* Once per threaded timeline.
137+
138+
No other changes to receipts are proposed, i.e. this still does not allow a caller
139+
to move their receipts backwards in a room. The relationship between `m.read` and
140+
`m.read.private` is not changed.
141+
142+
The request body to the [`/receipt` endpoint](https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidreceiptreceipttypeeventid)
143+
gains the following optional fields:
144+
145+
* `thread_id` (`string`): The thread that the receipt belongs to (i.e. the
146+
`event_id` contained within the `m.relates_to` of the event represented by
147+
`eventId`).
148+
149+
A special value of `"main"` corresponds to the receipt being for the main
150+
timeline (i.e. events which are not part of a thread).
151+
152+
If this field is not provided then the receipt applies to the unthreaded
153+
version of the room.[^2]
154+
155+
The following conditions are errors and should be rejected with a `400` error
156+
with `errcode` of `M_INVALID_PARAM`:
157+
158+
* A non-string `thread_id` (or empty) `thread_id` field.
159+
* Providing the `thread_id` properties for a receipt of type `m.fully_read`.
160+
* If the given `event_id` is not related to the `thread_id`. There may be multiple
161+
relations between events ((e.g. a `m.annotation` to `m.thread`), homeservers
162+
should apply a reasonable maximum number of relations to traverse when attempting
163+
to identify if an event is part of a thread.
164+
165+
It is recommended that at least 3 relations are traversed when attempting to find
166+
a thread, implementations should be careful to not infinitely recurse.[^3]
167+
168+
Given a threaded message:
169+
170+
```json
171+
{
172+
"event_id": "$thread_reply",
173+
"room_id": "!room:example.org",
174+
"content": {
175+
"m.relates_to": {
176+
"rel_type": "m.thread",
177+
"event_id": "$thread_root"
178+
}
179+
}
180+
}
181+
```
182+
183+
A client could mark this as read by sending a request:
184+
185+
```
186+
POST /_matrix/client/r0/rooms/!room:example.org/receipt/m.read/$thread_reply
187+
188+
{
189+
"thread_id": "$thread_root"
190+
}
191+
```
192+
193+
And to send a receipt on the main timeline (e.g. on the root event):
194+
195+
```
196+
POST /_matrix/client/r0/rooms/!room:example.org/receipt/m.read/$thread_root
197+
198+
{
199+
"thread_id": "main"
200+
}
201+
```
202+
203+
As it is today, not providing the `thread_id` field sends an unthreaded receipt:
204+
205+
```
206+
POST /_matrix/client/r0/rooms/!room:example.org/receipt/m.read/$thread_reply
207+
208+
{}
209+
```
210+
211+
### Receiving threaded receipts via `/sync`.
212+
213+
The client would receive this as part of `/sync` response similar to other receipts:
214+
215+
```json5
216+
{
217+
"content": {
218+
"$thread_reply": {
219+
"m.read": {
220+
"@rikj:jki.re": {
221+
"ts": 1436451550453,
222+
"thread_id": "$thread_root" // or "main" or absent
223+
}
224+
}
225+
}
226+
},
227+
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
228+
"type": "m.receipt"
229+
}
230+
```
231+
232+
If there is no `thread_id` field then the receipt applies to the unthreaded
233+
timeline. Clients may interpret this as applying only to the main timeline or
234+
as applying across the main timeline and all threaded timelines.
235+
236+
### Sending threaded receipts over federation
237+
238+
Homeservers should include a `thread_id` field for threaded receipts in the
239+
[Receipt Metadata](https://spec.matrix.org/v1.3/server-server-api/#receipts) when
240+
sending the `m.receipt` EDU over federation. Unthreaded receipts lack this field,
241+
as they do today.
242+
243+
### Notifications
244+
245+
[MSC3773](https://github.com/matrix-org/matrix-spec-proposals/pull/3773) discusses
246+
how notifications for threads are created and returned to the client, but does
247+
not provide a way to clear threaded notifications.
248+
249+
A threaded read receipt (i.e. a `m.read` or `m.read.private` receipt with a `thread_id`
250+
property) should clear notifications for the matching thread following the
251+
[current rules](https://spec.matrix.org/v1.3/client-server-api/#receiving-notifications),
252+
but only clear notifications with a matching `thread_id` (as discussed in MSC3773).
253+
See the examples of the read receipts on `E` and `I` [above](#proposal).
254+
255+
An unthreaded read receipt (i.e. a `m.read` or `m.read.private` receipt *without*
256+
a `thread_id`) should apply the [current rules](https://spec.matrix.org/v1.3/client-server-api/#receiving-notifications)
257+
and disregard thread information when clearing notifications. To re-iterate, this
258+
means it would clear any earlier notifications across *all* threads. This is
259+
illustrated by the read receipt on event `D` [above](#proposal).
260+
261+
## Potential issues
262+
263+
### Long-lived rooms
264+
265+
For long-lived rooms or rooms with many threads there could be a significant number
266+
of receipts. This has a few downsides:
267+
268+
* The size of the `/sync` response would increase without bound.
269+
* The effort to generate and process the receipts for each room would increase
270+
without bound.
271+
272+
### Compatibility with unthreaded clients
273+
274+
When a user has both a client which is "unthreaded" and "threaded" then there
275+
is a possibility for read receipts to be misrepresented when switching between
276+
clients. Using the example room DAG from the preamble of this MSC:
277+
278+
* A user which has an unthreaded receipt on event `D` and a threaded receipt on
279+
event `E` would likely see event `E` as unread on an "unthreaded" client.
280+
281+
The proposed solution may result in events being incorrectly marked as unread
282+
(when they have been read). The false positive for unread notifications is
283+
deliberate to avoid losing message / missing notifications.
284+
285+
Solutions to this problem are deemed out of scope of this MSC. A solution that
286+
was briefly explored was [ranged read receipts](https://hackmd.io/Gxm8zuuSROeencoJ6gjgSg).
287+
288+
### Federation compatibility
289+
290+
A homeserver which does not understand threaded receipts will be unable to properly
291+
understand that multiple receipts exist in a room. They will generally be processed
292+
as unthreaded receipts with the latest receipt winning, regardless of thread.
293+
294+
This could make read receipts of remote users jump between threads, but this should
295+
not be any worse than it is today. Additionally, since it only affects remote
296+
users, it will not impact notifications.
297+
298+
## Alternatives
299+
300+
### Thread ID location
301+
302+
Instead of adding the thread ID in the body, it could be provided as part of the
303+
URL path or as a query parameter. Adding it to the URL (as part of the path or a
304+
query parameter) would make it difficult to differentiate the receipt's event ID
305+
field from the thread ID.
306+
307+
Another idea was to encode information for all threads in the single receipt, e.g.
308+
by adding them to the body of the single read receipt. This could cause data
309+
integrity issues if multiple clients attempt to update the receipt without first
310+
reading it.
311+
312+
### Receipt type
313+
314+
To potentially improve compatibility it could make sense to use a separate receipt
315+
type (e.g. `m.read.thread`) as the read receipt for threads. Without some syncing
316+
mechanism between unthreaded and threaded receipts this seems likely to cause
317+
users to re-read the same notifications on threaded and unthreaded clients.
318+
319+
While it is possible to map from an unthreaded read receipt to multiple threaded
320+
read receipts, the opposite is not possible (to the author's knowledge). In short,
321+
it seems the [compatibility issues discussed above](#compatibility-with-unthreaded-clients)
322+
would not be solved by adding more receipt types.
323+
324+
This also gets more complicated with the addition of the `m.read.private` receipt --
325+
would there additionally be an `m.read.private.thread`? How do you map between
326+
all of these?
327+
328+
## Security considerations
329+
330+
There is potential for abuse by allowing clients to specify a unique `threadId`.
331+
A mitigation could be to ensure that the receipt is related to an event of the
332+
thread, ensuring that each thread only has a single receipt.
333+
334+
## Future extensions
335+
336+
### Threaded fully read markers
337+
338+
The `m.fully_read` marker is not supported in threads, a future MSC could expand
339+
support to this pseudo-receipt.
340+
341+
### Setting threaded receipts using the `/read_markers` endpoint
342+
343+
This MSC does not propose expanding the `/read_markers` endpoint to support threaded
344+
receipts. A future MSC might expand this to support an object per receipt with
345+
an event ID and thread ID or some other way of setting multiple receipts at once.
346+
347+
## Unstable prefix
348+
349+
To detect server support, clients can either rely on the spec version (when stable)
350+
or the presence of a `org.matrix.msc3771` flag in `unstable_features` on `/versions`.
351+
352+
## Dependencies
353+
354+
This MSC depends on the following MSCs, which have not yet been accepted into
355+
the spec:
356+
357+
* [MSC3773](https://github.com/matrix-org/matrix-spec-proposals/pull/3773): Notifications for threads
358+
359+
[^1]: Throughout this document "timeline" is used to mean what the user sees in
360+
the user interface of their Matrix client.
361+
362+
[^2]: Generally it would be surprising if the same client sent both threaded and
363+
unthreaded receipts, but it is allowed. The only known use-case for this is that
364+
a threaded client can use this to clear *all* notifications in a room by sending
365+
an unthreaded read receipt on the latest event in the room (regardless of which
366+
thread it appears in).
367+
368+
[^3]: Three relations is relatively arbitrary, but is meant to cover an edit or
369+
reaction to a thread (to an event with no relations, i.e. the root of a thread):
370+
`A<--[m.thread]--B<--[m.annotation]--C`.
371+
With an additional leftover for future improvements. This is considered reasonable
372+
since threads cannot fork, edits cannot modify relation information, and generally
373+
annotations to annotations are ignored by user interfaces.

0 commit comments

Comments
 (0)