Skip to content
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

MSC3771: Read receipts for threads #3771

Merged
merged 29 commits into from
Sep 25, 2022
Merged
Changes from 15 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d330ba9
Add initial MSC for read receipts for threads.
clokep Apr 15, 2022
42d7daf
Fix events in diagram.
clokep Apr 20, 2022
4c5382d
Add sync response.
clokep Apr 20, 2022
b5dac99
Link to the spec.
clokep Apr 26, 2022
9445049
Clarify sentence.
clokep Apr 26, 2022
0e42ec3
Some clarifications.
clokep May 24, 2022
e74c989
Simplification.
clokep Jul 6, 2022
65fa009
Fix JSON key format.
clokep Jul 22, 2022
1f0d6dd
Add information on clearing notifications.
clokep Jul 22, 2022
03ac0e0
Fix example.
clokep Aug 3, 2022
e3e677a
Update with current understanding.
clokep Aug 8, 2022
5540252
Clarify introduction.
clokep Aug 16, 2022
bbdb23e
MSC3773 is not yet accepted.
clokep Aug 16, 2022
2678f3c
Updates from feedback.
clokep Aug 16, 2022
ea77632
Update from learnings from the proof of concept.
clokep Aug 31, 2022
1c60533
Add link to the current spec.
clokep Sep 6, 2022
347d332
Clarify that false positives are deliberate in the design.
clokep Sep 6, 2022
5d73531
Receipts must move forward.
clokep Sep 6, 2022
ef736e4
More info on unthreaded receipts.
clokep Sep 6, 2022
de7ba49
Reflow.
clokep Sep 8, 2022
e1ab8e5
Clarify the proposal to explain why both threaded and unthreaded rece…
clokep Sep 8, 2022
6e71034
Add information about validating that an event is part of a thread.
clokep Sep 12, 2022
2b857b7
Remove section on second-order relations.
clokep Sep 12, 2022
d423542
Use proper syntax highlighting.
clokep Sep 13, 2022
ecbd6c4
Clarify unthreaded vs. main timeline receipts.
clokep Sep 20, 2022
1a5b5ae
Fix typos.
clokep Sep 20, 2022
7471923
Clarify wording.
clokep Sep 20, 2022
b171a31
Clarify example.
clokep Sep 20, 2022
668c9de
Fix alternatives section.
clokep Sep 20, 2022
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
254 changes: 254 additions & 0 deletions proposals/3771-read-receipts-for-threads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
# MSC3771: Read receipts for threads
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
clokep marked this conversation as resolved.
Show resolved Hide resolved

Currently, each room can only have a single receipt of each type per user. The
read receipt (`m.read` or `m.read.private`) is used to sync the read status of a
clokep marked this conversation as resolved.
Show resolved Hide resolved
room across clients, to share with other users which events have been read and
is used by the homeserver to calculate the number of unread messages.

Unfortunately a client displaying threads may show a subset of a room's messages
at a time, causing a user's read receipt to be misleading.

This might better be described by an example. Given a room with the following
DAG of events (note that the dotted lines are a thread relation, as specified by
[MSC3440](https://github.com/matrix-org/matrix-doc/pull/3440)):

```mermaid
flowchart RL
F-->E
E-->D
D-->C
C-->B
B-->A
C-.->A
E-.->A
```

A client might interpret this as:

```mermaid
flowchart RL
subgraph Main timeline
D-->B
B-->A
F-->D
end
subgraph Thread timeline
C-->A
E-->C
end

style A fill:cyan,stroke:#333,stroke-width:2px
style F fill:orange,stroke:#333,stroke-width:2px
```

While viewing the "main" timeline of the room, a client might move the read
receipt from event `A` to event `F` without ever showing events `C` and `E`. The
user then reads the thread, the client has no way to mark `E` as read.

## Proposal

### Threaded receipts
clokep marked this conversation as resolved.
Show resolved Hide resolved

This MSC proposes allowing the same receipt type to exist multiple times in a room,
once per thread (and once for the "main" timeline of the room).

The body of request to the [`/receipt` endpoint](https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidreceiptreceipttypeeventid)
clokep marked this conversation as resolved.
Show resolved Hide resolved
gains the following fields:


* `thread_id` (`string`): The thread that the receipt belongs to (i.e. the
`event_id` contained within the `m.relates_to` of the event represented by
`eventId`).
clokep marked this conversation as resolved.
Show resolved Hide resolved

A special value of `"main"` corresponds to the receipt being for the "main"
timeline (i.e. events which are not part of a thread).
clokep marked this conversation as resolved.
Show resolved Hide resolved

The following conditions are errors and should be rejected with a `400` error
with `errcode` of `M_INVALID_PARAM`:

* A non-string `thread_id` (or empty) `thread_id` field.
* Providing the `thread_id` properties for a receipt of type `m.fully_read`.

This updates the unique tuple for receipts from
`(room ID, user ID, receipt type)` to `(room ID, user ID, receipt type, thread ID)`.

Given a threaded message:

```json
{
"event_id": "$thread_reply",
"room_id": "!room:example.org",
"content": {
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$thread_root"
}
}
}
```

A client could mark this as read by sending a request:

```
POST /_matrix/client/r0/rooms/!room:example.org/receipt/m.read/$thread_reply

{
"thread_id": "$thread_root"
}
```

And to send a receipt on the "main" timeline (e.g. on the root event):

```
POST /_matrix/client/r0/rooms/!room:example.org/receipt/m.read/$thread_root

{
"thread_id": "main"
}
```

### Receiving threaded receipts via `/sync`.

The client would receive this as part of `/sync` response similar to other receipts:

```json5
{
"content": {
"$thread_reply": {
"m.read": {
"@rikj:jki.re": {
"ts": 1436451550453,
"thread_id": "$thread_root" // or "main"
}
}
}
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.receipt"
}
```

If there is no `thread_id` field than no thread information was supplied with
the receipt, clients may interpret this as only applying to the main timeline or
as applying across all threads, if possible.
germain-gg marked this conversation as resolved.
Show resolved Hide resolved

### Sending threaded receipts over federation

Homeservers should provide the `thread_id` (as provided by the client) in the
[Receipt Metadata](https://spec.matrix.org/v1.3/server-server-api/#receipts) when
sending the `m.receipt` EDU over federation.

### Notifications

[MSC3773](https://github.com/matrix-org/matrix-spec-proposals/pull/3773) discusses
how notifications for threads are created and returned to the client, but does
not provide a way to clear threaded notifications.

A threaded read receipt (i.e. a `m.read` or `m.read.private` receipt with a `thread_id`
property) should clear notifications for the matching thread following the
[current rules](https://spec.matrix.org/v1.3/client-server-api/#receiving-notifications),
but only clear notifications with a matching `thread_id`.

An unthreaded read receipt (i.e. a `m.read` or `m.read.private` receipt *without*
a `thread_id`) should apply follow the [current rules](https://spec.matrix.org/v1.3/client-server-api/#receiving-notifications)
as today and disregard thread information when clearing notifications.

Using the example room DAG from the preamble of this MSC, consider the following
scenarios (all of which start with 5 unread messages: 3 on the main timeline and
2 in thread `A`).

* A threaded read receipt sent for event `C` on thread `A`, results in: 3 unread
on the main timeline and 1 on thread `A`.
* A threaded read receipt sent for event `D` on the main timeline: 1 unread on
the main timeline and 2 on thread `A`.
* An unthreaded read receipt send for event `D`: 1 unread on the main timeline
and 1 on thread `A`.

## Potential issues

### Long-lived rooms

For long-lived rooms or rooms with many threads there could be a significant number
of receipts. This has a few downsides:

* The size of the `/sync` response would increase without bound.
* The effort to generate and process the receipts for each room would increase
without bound.
dbkr marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

Do we have a plan for this? I don't think this is a viable situation long-term, though happy enough to punt it for now. (might be worth looping in people thinking about sliding sync?)

Copy link
Member Author

Choose a reason for hiding this comment

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

Do we have a plan for this? I don't think this is a viable situation long-term, though happy enough to punt it for now. (might be worth looping in people thinking about sliding sync?)

I don't have a long-term plan for this, no. I'm unsure how much of a concern this really is -- it would depend how often folks sending threaded read receipts, although "without bound" might have been a bit of hyperbole.

I think from this we'll end up processing more receipts, but they'll be over smaller chunks of events, so I hope the actual effort isn't too much worse.


### Compatibility with unthreaded clients

When a user has both a client which is "unthreaded" and "threaded" then there
is a possibility for read receipts to be misrepresented when switching between
clients. Using the example room DAG from the preamble of this MSC:

* A user which has an unthreaded receipt on event `D` and a threaded receipt on
event `E` would likely see event `E` as unread on an "unthreaded" client.

Solutions to this problem are deemed out of scope of this MSC.
clokep marked this conversation as resolved.
Show resolved Hide resolved

### Second-order relations

For simplicity, clients may wish to send receipts for events which are not directly
related to a root thread event (e.g. a reaction to thread event). This should
generally be treated as acceptable by the server (i.e. the server should only care
about the ordering of events, not about whether the events are in the same thread).
clokep marked this conversation as resolved.
Show resolved Hide resolved

## Alternatives

### Thread ID location

Instead of adding the thread ID as a new path part, it could be added to the body
clokep marked this conversation as resolved.
Show resolved Hide resolved
of the receipt. There may be a small backwards compatibility benefit to this, but
it seems clearer to put it as part of the URL.

Instead of encoding the thread ID as an integral part of the receipt, all of the
read threads could be added to the body of the single receipt. This could cause
data integrity issues if multiple clients attempt to update the receipt without
first reading it.

### Receipt type

To potentially improve compatibility it could make sense to use a separate receipt
type (e.g. `m.read.thread`) as the read receipt for threads. Without some syncing
mechanism between unthreaded and threaded receipts this seems likely to cause
users to re-read the same notifications on threaded and unthreaded clients.

While it is possible to map from an unthreaded read receipt to multiple threaded
read receipts, the opposite is not possible (to the author's knowledge). In short,
it seems the [compatibility issues discussed above](#compatibility-with-unthreaded-clients)
would not be solved by adding more receipt types.

This also gets more complcated with the addition of the `m.read.private` receipt --
clokep marked this conversation as resolved.
Show resolved Hide resolved
would there additionally be an `m.read.private.thread`? How do you map between
all of these?

## Security considerations

There is potential for abuse by allowing clients to specify a unique `threadId`.
A mitigation could be to ensure that the receipt is related to an event of the
thread, ensuring that each thread only has a single receipt.

## Future extensions

### Threaded fully read markers

The `m.fully_read` marker is not supported in threads, a future MSC could expand
support to this pseudo-receipt.

### Setting threaded receipts using the `/read_markers` endpoint

This MSC does not propose expanding the `/read_markers` endpoint to support threaded
receipts. A future MSC might expand this to support an object per receipt with
an event ID and thread ID or some other way of setting multiple receipts at once.

## Unstable prefix

To detect server support, clients can either rely on the spec version (when stable)
or the presence of a `org.matrix.msc3771` flag in `unstable_features` on `/versions`.

## Dependencies

This MSC depends on the following MSCs, which have not yet been accepted into
the spec:

* [MSC3773](https://github.com/matrix-org/matrix-spec-proposals/pull/3773): Notifications for threads