mirror of
https://github.com/matrix-org/matrix-spec
synced 2026-03-26 13:04:10 +01:00
Spec MSC3771: Threaded read receipts
Note: this builds on a (as of writing) non-existent "threading" section, which is part of a different commit.
This commit is contained in:
parent
6c6c602845
commit
343237e87c
|
|
@ -22,33 +22,68 @@ that the user had read all events *up to* the referenced event. See the
|
||||||
[Receiving notifications](#receiving-notifications) section for more
|
[Receiving notifications](#receiving-notifications) section for more
|
||||||
information on how read receipts affect notification counts.
|
information on how read receipts affect notification counts.
|
||||||
|
|
||||||
|
{{< added-in v="1.4" >}} Read receipts exist in three major forms:
|
||||||
|
* Unthreaded: Denotes a read-up-to receipt regardless of threads. This is how
|
||||||
|
pre-threading read receipts worked.
|
||||||
|
* Threaded, main timeline: Denotes a read-up-to receipt for events not in a
|
||||||
|
particular thread. Identified by the thread ID `main`.
|
||||||
|
* Threaded, in a thread: Denotes a read-up-to receipt within a particular
|
||||||
|
thread. Identified by the event ID of the thread root.
|
||||||
|
|
||||||
|
Threaded read receipts are discussed in further detail [below](#threaded-read-receipts).
|
||||||
|
|
||||||
#### Events
|
#### Events
|
||||||
|
|
||||||
Each `user_id`, `receipt_type` pair must be associated with only a
|
{{< changed-in v="1.4" >}} Each `user_id`, `receipt_type`, and categorisation
|
||||||
single `event_id`.
|
(unthreaded, or `thread_id`) tuple must be associated with only a single
|
||||||
|
`event_id`.
|
||||||
|
|
||||||
{{% event event="m.receipt" %}}
|
{{% event event="m.receipt" %}}
|
||||||
|
|
||||||
#### Client behaviour
|
#### Client behaviour
|
||||||
|
|
||||||
|
{{< changed-in v="1.4" >}} Altered to support threaded read receipts.
|
||||||
|
|
||||||
In `/sync`, receipts are listed under the `ephemeral` array of events
|
In `/sync`, receipts are listed under the `ephemeral` array of events
|
||||||
for a given room. New receipts that come down the event streams are
|
for a given room. New receipts that come down the event streams are
|
||||||
deltas which update existing mappings. Clients should replace older
|
deltas which update existing mappings. Clients should replace older
|
||||||
receipt acknowledgements based on `user_id` and `receipt_type` pairs.
|
receipt acknowledgements based on `user_id`, `receipt_type`, and the
|
||||||
|
`thread_id` (if present).
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
Client receives m.receipt:
|
Client receives m.receipt:
|
||||||
user = @alice:example.com
|
user = @alice:example.com
|
||||||
receipt_type = m.read
|
receipt_type = m.read
|
||||||
event_id = $aaa:example.com
|
event_id = $aaa:example.com
|
||||||
|
thread_id = undefined
|
||||||
|
|
||||||
Client receives another m.receipt:
|
Client receives another m.receipt:
|
||||||
user = @alice:example.com
|
user = @alice:example.com
|
||||||
receipt_type = m.read
|
receipt_type = m.read
|
||||||
event_id = $bbb:example.com
|
event_id = $bbb:example.com
|
||||||
|
thread_id = main
|
||||||
|
|
||||||
The client should replace the older acknowledgement for $aaa:example.com with
|
The client does not replace any acknowledgements, yet.
|
||||||
this one for $bbb:example.com
|
|
||||||
|
Client receives yet another m.receipt:
|
||||||
|
user = @alice:example.com
|
||||||
|
receipt_type = m.read
|
||||||
|
event_id = $ccc:example.com
|
||||||
|
thread_id = undefined
|
||||||
|
|
||||||
|
The client replaces the older acknowledgement for $aaa:example.com
|
||||||
|
with this new one for $ccc:example.com, but does not replace the
|
||||||
|
acknowledgement for $bbb:example.com because it belongs to a thread.
|
||||||
|
|
||||||
|
Client receives yet another m.receipt:
|
||||||
|
user = @alice:example.com
|
||||||
|
receipt_type = m.read
|
||||||
|
event_id = $ddd:example.com
|
||||||
|
thread_id = main
|
||||||
|
|
||||||
|
Now the client replaces the older $bbb:example.com acknowledgement with
|
||||||
|
this new $ddd:example.com acknowledgement. The client does NOT replace the
|
||||||
|
older acknowledgement for $ccc:example.com as it is unthreaded.
|
||||||
|
|
||||||
Clients should send read receipts when there is some certainty that the
|
Clients should send read receipts when there is some certainty that the
|
||||||
event in question has been **displayed** to the user. Simply receiving
|
event in question has been **displayed** to the user. Simply receiving
|
||||||
|
|
@ -87,6 +122,84 @@ not have their notification counts rewound to that point in time. While
|
||||||
uncommon, it is considered valid to have an `m.read` (public) receipt lag
|
uncommon, it is considered valid to have an `m.read` (public) receipt lag
|
||||||
several messages behind the `m.read.private` receipt, for example.
|
several messages behind the `m.read.private` receipt, for example.
|
||||||
|
|
||||||
|
##### Threaded read receipts
|
||||||
|
|
||||||
|
{{% added-in v="1.4" %}}
|
||||||
|
|
||||||
|
If a client does not use [threading](#threading), then they will simply only
|
||||||
|
send "unthreaded" read receipts which affect the whole room regardless of threads.
|
||||||
|
|
||||||
|
Threading introduces a concept of multiple conversations being held in the same
|
||||||
|
room and thus deserve their own read receipts and notification counts. An event
|
||||||
|
is considered to be "in a thread" if it has either a `rel_type` of `m.thread` or
|
||||||
|
has child events with a `rel_type` of `m.thread` (in which case it'd be the
|
||||||
|
thread root). Events not in a thread but still in the room are considered to be
|
||||||
|
part of the "main timeline", or a special thread with an ID of `main`.
|
||||||
|
|
||||||
|
A threaded read receipt is simply one which has a `thread_id` on it, targeting
|
||||||
|
either a thread root's event ID or `main` for the main timeline.
|
||||||
|
|
||||||
|
The following is an example DAG for a room, with dotted lines showing event
|
||||||
|
relationships and solid lines showing topological ordering.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
{{% boxes/note %}}
|
||||||
|
`m.reaction` relationships are not currently specified, but are shown here for
|
||||||
|
their conceptual place in a threaded DAG. They are currently proposed as
|
||||||
|
[MSC2677](https://github.com/matrix-org/matrix-spec-proposals/pull/2677).
|
||||||
|
{{% /boxes/note %}}
|
||||||
|
|
||||||
|
This DAG can be represented as 3 threaded timelines, with `A` and `B` being thread
|
||||||
|
roots:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
With this, we can demonstrate that:
|
||||||
|
* A threaded read receipt on `I` would mark `A`, `B`, and `I` as read.
|
||||||
|
* A threaded read receipt on `E` would mark `C` and `E` as read.
|
||||||
|
* An unthreaded read receipt on `D` would mark `A`, `B`, `C`, and `D` as read.
|
||||||
|
|
||||||
|
Note that marking `A` as read with a threaded read receipt would not mean
|
||||||
|
that `C`, `E`, `G`, or `H` get marked as read: Thread A's timeline would need
|
||||||
|
its own threaded read receipt at `H` to accomplish that.
|
||||||
|
|
||||||
|
The read receipts for the above 3 examples would be:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$I": {
|
||||||
|
"m.read": {
|
||||||
|
"@user:example.org": {
|
||||||
|
"ts": 1661384801651,
|
||||||
|
"thread_id": "main" // because `I` is not in a thread, but is a threaded receipt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$E": {
|
||||||
|
"m.read": {
|
||||||
|
"@user:example.org": {
|
||||||
|
"ts": 1661384801651,
|
||||||
|
"thread_id": "$A" // because `E` is in Thread `A`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$D": {
|
||||||
|
"m.read": {
|
||||||
|
"@user:example.org": {
|
||||||
|
"ts": 1661384801651
|
||||||
|
// no `thread_id` because the receipt is *unthreaded*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Conditions on sending read receipts, such as using private read receipts, only
|
||||||
|
sending once an event is display, not being able to move backwards in time, etc
|
||||||
|
still apply to threaded and unthreaded read receipts. For example, a client might
|
||||||
|
send a private read receipt for a threaded event when the user expands that thread.
|
||||||
|
|
||||||
#### Server behaviour
|
#### Server behaviour
|
||||||
|
|
||||||
For efficiency, receipts SHOULD be batched into one event per room
|
For efficiency, receipts SHOULD be batched into one event per room
|
||||||
|
|
@ -99,7 +212,7 @@ format of the EDUs are:
|
||||||
{
|
{
|
||||||
<room_id>: {
|
<room_id>: {
|
||||||
<receipt_type>: {
|
<receipt_type>: {
|
||||||
<user_id>: { <content> }
|
<user_id>: { <content (ts & thread_id, currently)> }
|
||||||
},
|
},
|
||||||
...
|
...
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,13 @@ properties:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
description: The timestamp the receipt was sent at.
|
description: The timestamp the receipt was sent at.
|
||||||
|
thread_id:
|
||||||
|
type: string
|
||||||
|
description: |-
|
||||||
|
The root thread event's ID (or `main`) for which
|
||||||
|
thread this receipt is intended to be under. If
|
||||||
|
not specified, the read receipt is *unthreaded*
|
||||||
|
(default).
|
||||||
"m.read.private":
|
"m.read.private":
|
||||||
type: object
|
type: object
|
||||||
title: Own User
|
title: Own User
|
||||||
|
|
|
||||||
1
static/diagrams/threaded-dag-threads.drawio
Normal file
1
static/diagrams/threaded-dag-threads.drawio
Normal file
|
|
@ -0,0 +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>
|
||||||
BIN
static/diagrams/threaded-dag-threads.png
Normal file
BIN
static/diagrams/threaded-dag-threads.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
1
static/diagrams/threaded-dag.drawio
Normal file
1
static/diagrams/threaded-dag.drawio
Normal file
|
|
@ -0,0 +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>
|
||||||
BIN
static/diagrams/threaded-dag.png
Normal file
BIN
static/diagrams/threaded-dag.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Loading…
Reference in a new issue