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
This commit is contained in:
parent
25dda1eadb
commit
227757d499
12 changed files with 218 additions and 10 deletions
1
changelogs/client_server/newsfragments/1255.feature
Normal file
1
changelogs/client_server/newsfragments/1255.feature
Normal file
|
@ -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).
|
1
changelogs/server_server/newsfragments/1255.feature
Normal file
1
changelogs/server_server/newsfragments/1255.feature
Normal file
|
@ -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).
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||

|
||||
|
||||
{{% 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 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:
|
|||
{
|
||||
<room_id>: {
|
||||
<receipt_type>: {
|
||||
<user_id>: { <content> }
|
||||
<user_id>: { <content (ts & thread_id, currently)> }
|
||||
},
|
||||
...
|
||||
},
|
||||
|
|
|
@ -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: |-
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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
|
||||
|
|
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…
Add table
Add a link
Reference in a new issue