From 227757d49933b67a1f328ba30e0760c345cdbdf1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 28 Sep 2022 14:49:25 -0600 Subject: [PATCH] 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 --- .../client_server/newsfragments/1255.feature | 1 + .../server_server/newsfragments/1255.feature | 1 + content/client-server-api/modules/push.md | 17 ++- content/client-server-api/modules/receipts.md | 136 +++++++++++++++++- .../definitions/room_event_filter.yaml | 7 + data/api/client-server/sync.yaml | 47 +++++- .../definitions/event-schemas/m.receipt.yaml | 9 ++ data/event-schemas/schema/m.receipt.yaml | 8 ++ static/diagrams/threaded-dag-threads.drawio | 1 + static/diagrams/threaded-dag-threads.png | Bin 0 -> 18578 bytes static/diagrams/threaded-dag.drawio | 1 + static/diagrams/threaded-dag.png | Bin 0 -> 11621 bytes 12 files changed, 218 insertions(+), 10 deletions(-) create mode 100644 changelogs/client_server/newsfragments/1255.feature create mode 100644 changelogs/server_server/newsfragments/1255.feature create mode 100644 static/diagrams/threaded-dag-threads.drawio create mode 100644 static/diagrams/threaded-dag-threads.png create mode 100644 static/diagrams/threaded-dag.drawio create mode 100644 static/diagrams/threaded-dag.png diff --git a/changelogs/client_server/newsfragments/1255.feature b/changelogs/client_server/newsfragments/1255.feature new file mode 100644 index 00000000..a1d15d98 --- /dev/null +++ b/changelogs/client_server/newsfragments/1255.feature @@ -0,0 +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). \ No newline at end of file diff --git a/changelogs/server_server/newsfragments/1255.feature b/changelogs/server_server/newsfragments/1255.feature new file mode 100644 index 00000000..a1d15d98 --- /dev/null +++ b/changelogs/server_server/newsfragments/1255.feature @@ -0,0 +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). \ No newline at end of file diff --git a/content/client-server-api/modules/push.md b/content/client-server-api/modules/push.md index 5cce9449..9f9b30ac 100644 --- a/content/client-server-api/modules/push.md +++ b/content/client-server-api/modules/push.md @@ -107,8 +107,10 @@ determined by the push rules which apply to an event. When the user updates their read receipt (either by using the API or by sending an event), notifications prior to and including that event MUST -be marked as read. Note that users can send both an `m.read` and -`m.read.private` receipt, both of which are capable of clearing notifications. +be marked as read. Which specific events are affected can vary depending +on whether a [threaded read receipt](#threaded-read-receipts) was used. +Note that users can send both an `m.read` and `m.read.private` receipt, +both of which are capable of clearing notifications. If the user has both `m.read` and `m.read.private` set in the room then 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 event D then the user has read up to D (the `m.read` receipt is now behind the `m.read.private` receipt). +{{< added-in v="1.4" >}} When handling threaded read receipts, the server +is to partition the notification count to each thread (with the main timeline +being its own thread). To determine if an event is part of a thread the +server follows the [event relationship](#forming-relationships-between-events) +until it finds a thread root (as specified by the [threading module](#threading)), +however it is not recommended that the server traverse infinitely. Instead, +implementations are encouraged to do a maximum of 3 hops to find a thread +before deciding that the event does not belong to a thread. This is primarily +to ensure that future events, like `m.reaction`, are correctly considered +"part of" a given thread. + ##### Push Rules A push rule is a single rule that states under what *conditions* an diff --git a/content/client-server-api/modules/receipts.md b/content/client-server-api/modules/receipts.md index 9f80ff60..7fdde0d0 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 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. - 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 = $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 @@ -58,6 +93,12 @@ room that the event was sent to or dismissing a notification in order for the event to count as "read". Clients SHOULD NOT send read receipts for events sent by their own user. +Similar to the rules for sending receipts, threaded receipts should appear +in the context of the thread. If a thread is rendered behind a disclosure, +the client hasn't yet shown the event (or any applicable read receipts) +to the user. Once they expand the thread though, a threaded read receipt +would be sent and per-thread receipts from other users shown. + A client can update the markers for its user by interacting with the following HTTP APIs. @@ -87,6 +128,89 @@ 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. + +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. + +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 meets any of the following criteria: +* It has a `rel_type` of `m.thread`. +* It has child events with a `rel_type` of `m.thread` (in which case it'd be the + thread root). +* Following the event relationships, it has a parent event which qualifies for + one of the above. Implementations should not recurse infinitely, though: a + maximum of 3 hops is recommended to cover indirect relationships. + +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`. + +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 apply similarly 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 +223,7 @@ format of the EDUs are: { : { : { - : { } + : { } }, ... }, diff --git a/data/api/client-server/definitions/room_event_filter.yaml b/data/api/client-server/definitions/room_event_filter.yaml index 8d96a165..ab8ef79e 100644 --- a/data/api/client-server/definitions/room_event_filter.yaml +++ b/data/api/client-server/definitions/room_event_filter.yaml @@ -1,4 +1,5 @@ # Copyright 2016 OpenMarket Ltd +# Copyright 2022 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,6 +17,12 @@ allOf: - type: object title: RoomEventFilter properties: + unread_thread_notifications: + type: boolean + description: |- + If `true`, enables per-[thread](/client-server-api/#threading) notification + counts. Only applies to the `/sync` endpoint. Defaults to `false`. + x-addedInMatrixVersion: "1.4" lazy_load_members: type: boolean description: |- diff --git a/data/api/client-server/sync.yaml b/data/api/client-server/sync.yaml index f8127bf5..93265cae 100644 --- a/data/api/client-server/sync.yaml +++ b/data/api/client-server/sync.yaml @@ -239,17 +239,50 @@ paths: Counts of unread notifications for this room. See the [Receiving notifications](/client-server-api/#receiving-notifications) section for more information on how these are calculated. + + If `unread_thread_notifications` was specified as `true` on the `RoomEventFilter`, + these counts will only be for the main timeline rather than all events in the room. + See the [threading module](#threading) for more information. + x-changedInMatrixVersion: + 1.4: | + Updated to reflect behaviour of having `unread_thread_notifications` as `true` in + the `RoomEventFilter` for `/sync`. properties: highlight_count: title: Highlighted notification count type: integer description: The number of unread notifications - for this room with the highlight flag set + for this room with the highlight flag set. notification_count: title: Total notification count type: integer description: The total number of unread notifications - for this room + for this room. + unread_thread_notifications: + title: Unread Thread Notification Counts + type: object + description: |- + If `unread_thread_notifications` was specified as `true` on the `RoomEventFilter`, + the notification counts for each [thread](#threading) in this room. The object is + keyed by thread root ID, with values matching `unread_notifications`. + + If a thread does not have any notifications it can be omitted from this object. If + no threads have notification counts, this whole object can be omitted. + x-addedInMatrixVersion: "1.4" + additionalProperties: + title: ThreadNotificationCounts + type: object + properties: + highlight_count: + title: ThreadedHighlightNotificationCount + type: integer + description: |- + The number of unread notifications for this *thread* with the highlight flag set. + notification_count: + title: ThreadedTotalNotificationCount + type: integer + description: |- + The total number of unread notifications for this *thread*. invite: title: Invited Rooms type: object @@ -424,6 +457,16 @@ paths: } } ] + }, + "unread_notifications": { + "highlight_count": 1, + "notification_count": 5 + }, + "unread_thread_notifications": { + "$threadroot": { + "highlight_count": 3, + "notification_count": 6 + } } } }, diff --git a/data/api/server-server/definitions/event-schemas/m.receipt.yaml b/data/api/server-server/definitions/event-schemas/m.receipt.yaml index 335e7c27..bbc3ac67 100644 --- a/data/api/server-server/definitions/event-schemas/m.receipt.yaml +++ b/data/api/server-server/definitions/event-schemas/m.receipt.yaml @@ -65,6 +65,15 @@ allOf: A POSIX timestamp in milliseconds for when the user read the event specified in the read receipt. example: 1533358089009 + thread_id: + type: string + x-addedInMatrixVersion: "1.4" + 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). + example: "$threadroot" required: ['ts'] required: ['event_ids', 'data'] required: ['m.read'] diff --git a/data/event-schemas/schema/m.receipt.yaml b/data/event-schemas/schema/m.receipt.yaml index b9ec7e4e..c501d022 100644 --- a/data/event-schemas/schema/m.receipt.yaml +++ b/data/event-schemas/schema/m.receipt.yaml @@ -38,6 +38,14 @@ properties: type: integer format: int64 description: The timestamp the receipt was sent at. + thread_id: + type: string + x-addedInMatrixVersion: "1.4" + 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 0000000000000000000000000000000000000000..4cf865edd5d3eeccf8a5cb3850a739f3847e84e8 GIT binary patch literal 18578 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..085d0f0915a41eb5279332cb6d7f83852d22e8b0 GIT binary patch literal 11621 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 R!F$t82D+v?xmxEh{tuJ*8)^Um literal 0 HcmV?d00001