Skip to content

Threads: Read receipts & notifications #1255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 119 additions & 6 deletions content/client-server-api/modules/receipts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
information on how read receipts affect notification counts.

{{< added-in v="1.4" >}} Read receipts exist in three major forms:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MSC3771 doesn't specify that unthreaded/threaded applies only to read receipts, but there isn't currently any other type of receipts...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, we're very slowly moving this module to talk about only read receipts at the moment (at least until another receipt shows up)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, just wanted to ensure this was on purpose.

* 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

Each `user_id`, `receipt_type` pair must be associated with only a
single `event_id`.
{{< changed-in v="1.4" >}} Each `user_id`, `receipt_type`, and categorisation
(unthreaded, or `thread_id`) tuple must be associated with only a single
`event_id`.

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

#### Client behaviour

{{< changed-in v="1.4" >}} Altered to support threaded read receipts.

In `/sync`, receipts are listed under the `ephemeral` array of events
for a given room. New receipts that come down the event streams are
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:

Client receives m.receipt:
user = @alice:example.com
receipt_type = m.read
event_id = $aaa:example.com
thread_id = undefined

Client receives another m.receipt:
user = @alice:example.com
receipt_type = m.read
event_id = $bbb:example.com
thread_id = main

The client does not replace any acknowledgements, yet.

The client should replace the older acknowledgement for $aaa:example.com with
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
event in question has been **displayed** to the user. Simply receiving
Expand Down Expand Up @@ -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
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.

![threaded-dag](/diagrams/threaded-dag.png)

{{% 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:

![threaded-dag-threads](/diagrams/threaded-dag-threads.png)

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

For efficiency, receipts SHOULD be batched into one event per room
Expand All @@ -99,7 +212,7 @@ format of the EDUs are:
{
<room_id>: {
<receipt_type>: {
<user_id>: { <content> }
<user_id>: { <content (ts & thread_id, currently)> }
},
...
},
Expand Down
7 changes: 7 additions & 0 deletions data/event-schemas/schema/m.receipt.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ properties:
type: integer
format: int64
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":
type: object
title: Own User
Expand Down
1 change: 1 addition & 0 deletions static/diagrams/threaded-dag-threads.drawio
Original file line number Diff line number Diff line change
@@ -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>
Binary file added static/diagrams/threaded-dag-threads.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions static/diagrams/threaded-dag.drawio
Original file line number Diff line number Diff line change
@@ -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>
Binary file added static/diagrams/threaded-dag.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.