diff --git a/content/client-server-api/modules/receipts.md b/content/client-server-api/modules/receipts.md index 9f80ff60..a13e2f9f 100644 --- a/content/client-server-api/modules/receipts.md +++ b/content/client-server-api/modules/receipts.md @@ -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: +* 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 should replace the older acknowledgement for $aaa:example.com with - this one for $bbb:example.com + The client does not replace any acknowledgements, yet. + + 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 @@ -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 @@ -99,7 +212,7 @@ format of the EDUs are: { : { : { - : { } + : { } }, ... }, diff --git a/data/event-schemas/schema/m.receipt.yaml b/data/event-schemas/schema/m.receipt.yaml index b9ec7e4e..bde3d40c 100644 --- a/data/event-schemas/schema/m.receipt.yaml +++ b/data/event-schemas/schema/m.receipt.yaml @@ -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 diff --git a/static/diagrams/threaded-dag-threads.drawio b/static/diagrams/threaded-dag-threads.drawio new file mode 100644 index 00000000..2f3121d7 --- /dev/null +++ b/static/diagrams/threaded-dag-threads.drawio @@ -0,0 +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== \ No newline at end of file diff --git a/static/diagrams/threaded-dag-threads.png b/static/diagrams/threaded-dag-threads.png new file mode 100644 index 00000000..4cf865ed Binary files /dev/null and b/static/diagrams/threaded-dag-threads.png differ diff --git a/static/diagrams/threaded-dag.drawio b/static/diagrams/threaded-dag.drawio new file mode 100644 index 00000000..e481b875 --- /dev/null +++ b/static/diagrams/threaded-dag.drawio @@ -0,0 +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= \ No newline at end of file diff --git a/static/diagrams/threaded-dag.png b/static/diagrams/threaded-dag.png new file mode 100644 index 00000000..085d0f09 Binary files /dev/null and b/static/diagrams/threaded-dag.png differ