-
Notifications
You must be signed in to change notification settings - Fork 399
MSC3440: Threading via m.thread
relation
#3440
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
Changes from 7 commits
beda89e
2fa27ac
d8a0a94
7d39887
6e37911
c142b17
c578f75
33acdf4
7102165
f84f949
f02dc8d
3e46728
44e967f
65d0d55
2b76a6e
99c5b2e
4ee42b1
fc81bbd
91e6ec7
eaeef00
26fb5f2
a23c795
6b1a368
f227592
b493f21
46e1e9b
e40efa0
23928e7
0880a86
1bbb021
3c977f7
0140454
700464c
0035202
e3cb699
847f468
c8ffa62
a7cbf8d
5896d69
ee5df80
00daf64
a5d8aab
d7ed3c4
5c04906
d667a0b
b157dfd
3162bea
fa232f4
68d9c42
5bbb015
707af2b
b28a365
8f82dfa
8f8be64
b6d8076
cd671ef
a61c01e
362e661
b831fb3
e2dde8e
e640f6b
bda3a1e
89c4b5e
61bb518
9159a5a
a97307a
f541dab
82b4c62
75f4cb2
54ce185
6d6baa2
893cf1f
641e326
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,283 @@ | ||
# MSC3440 Threading via `m.thread` relation | ||
|
||
## Problem | ||
|
||
Threading is a great way to create alternative timelines to group messages related | ||
to each other. This is particularly useful in high traffic rooms where multiple | ||
conversations can happen in parallel or when a single discussion might stretch | ||
over a very long period of time. | ||
|
||
The main goal when implementing threads is to create conversations that are easier | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
to follow and smoother to read. | ||
|
||
There have been several experiments in threading for Matrix... | ||
|
||
- [MSC2326](https://github.com/matrix-org/matrix-doc/pull/2326): | ||
Label based filtering | ||
- [MSC2836](https://github.com/matrix-org/matrix-doc/pull/2836): | ||
Threading by serverside traversal of relationships | ||
- "Threads as rooms" | ||
|
||
...but none have yet been widely adopted due to the upheaval of implementing an | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
entirely new Client-Server API construct in the core message sending/display path. | ||
|
||
Meanwhile, threading is very clearly a core requirement for any modern messaging | ||
solution, and Matrix uptake is suffering due to the lack of progress. | ||
|
||
## Proposal | ||
|
||
### Event format | ||
|
||
A new relation would be used to express that an event belongs to a thread. | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```json | ||
"m.relates_to": { | ||
"rel_type": "m.thread", | ||
"event_id": "$thread_root" | ||
} | ||
``` | ||
Where $thread_root is the event ID of the root message in the thread. | ||
|
||
A big advantage of relations over quote replies is that they can be server-side | ||
aggregated. It means that a client is not bound to download the entire history of | ||
a room to have a comprehensive list of events being part of a thread. | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
When a thread head is aggregated (as in MSC2675), returns a summary of the thread: | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
the latest message, a list of participants and the total count of messages. | ||
I.e. in places which include bundled relations (per | ||
[MSC2675](https://github.com/matrix-org/matrix-doc/pull/2675)), the thread root | ||
would include additional information in the `unsigned` field: | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```json | ||
{ | ||
"latest_event": { | ||
"content": { ... }, | ||
... | ||
}, | ||
"senders": ["@john:example.com", ...], | ||
"count": 7 | ||
} | ||
``` | ||
|
||
#### Quote replies in a thread | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
No recommendation to modifying quote replies is made, this would still be handled | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
via the `m.in_reply_to` field of `m.relates_to`. Thus you could quote a reply in a thread: | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```json | ||
"m.relates_to": { | ||
"rel_type": "m.thread", | ||
"event_id": "$thread_root", | ||
"m.in_reply_to": { | ||
"event_id": "$event_target" | ||
} | ||
} | ||
``` | ||
|
||
It is possible that an `m.in_reply_to` event targets an event that is outside the | ||
related thread. Clients should always do their upmost to display the quote-reply | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
and upon clicking it the event should be displayed and highlighted in its original context. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is an uncovered case: replying to a message in a thread from outside a thread, i.e. Replying to a threaded message from a currently unaware client like element android will on element web show the reply in the thread as if it had the Allowing replies to threaded events without being part of the thread may be regarded as either:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for bringing this up! I reviewed the behaviour an I agree with the conclusion that you came to. An event that has
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
### Fetch all replies to a thread | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
To fetch an entire thread, the `/relations` API can be used as defined in | ||
[MSC2675](https://github.com/matrix-org/matrix-doc/pull/2675) | ||
|
||
``` | ||
GET /_matrix/client/unstable/rooms/!room_id:domain/relations/$thread_root/m.thread | ||
``` | ||
novocaine marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Where `$thread_root` is the event ID of the root message in the thread. | ||
|
||
In order to properly display a thread it is necessary to retrieve the relations | ||
to threaded events, e.g. the reactions to the threaded events. This proposes | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
clarifying [MSC2675](https://github.com/matrix-org/matrix-doc/pull/2675) that | ||
the `/relations` API includes bundled relations. This follows what MSC2675 already describes: | ||
|
||
> Any API which receives events should bundle relations (apart from non-gappy | ||
incremental syncs), for instance: initial sync, gappy incremental sync, | ||
/messages and /context. | ||
|
||
### Fetch all threads in a room | ||
|
||
To fetch all threads in a room it is proposed to use the | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[`/messages`](https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-rooms-roomid-messages) | ||
API and expand the room event filtering to include relations. The `RoomEventFilter` | ||
clokep marked this conversation as resolved.
Show resolved
Hide resolved
|
||
will take additional parameters: | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
* `relation_types`: A list of relation types which must be bundled with the event | ||
to include it. If this list is absent then no filtering is done on relation types. | ||
* `relation_senders`: A list of senders of relations... | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
This can also be combined with the `sender` field to search for threads which a | ||
user has participated in (or not participated in). | ||
|
||
``` | ||
GET /_matrix/client/unstable/rooms/!room_id:domain/messages/filter=... | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
|
||
Where filter would be JSON and URL-encoded string include the above new fields: | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```json | ||
{ | ||
"types": ["m.room.message"], | ||
"relation_senders": [...], | ||
"relation_types": ["m.thread"] | ||
} | ||
``` | ||
|
||
### Limitations | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
#### Read receipts | ||
|
||
Read receipts and read markers assume a single chronological timeline. Threading | ||
changes that assumption making the current API not very practical. | ||
|
||
Clients can synthetize read receipts but it is possible that some notification get | ||
dbkr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
lost upon a fresh start where the clients have to start off the `m.read` | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
information received from the homeserver. | ||
|
||
Synchronising the synthesized notification count across devices will present its | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
own challenges and is probably undesirable at this stage. The preferred route | ||
would be to create another MSC to make read receipts support multiple timelines | ||
in a single room. | ||
|
||
#### Single-layer event aggration | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Bundling only includes relations a single-layer deep. Given the following list of | ||
events, `ev4` would not be returned as it isn't a direct reference to `ev1`. | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
``` | ||
[ | ||
{ | ||
"event_id": "ev1", | ||
... | ||
}, | ||
{ | ||
"event_id": "ev2", | ||
... | ||
"m.relates_to": { | ||
"rel_type": "m.thread", | ||
"event_id": "ev1", | ||
"m.in_reply_to": { | ||
"event_id": "ev1" | ||
} | ||
} | ||
}, | ||
{ | ||
"event_id": "ev3", | ||
... | ||
"m.relates_to": { | ||
"rel_type": "m.annotation", | ||
"event_id": "ev1", | ||
"key": "✅" | ||
} | ||
}, | ||
{ | ||
"event_id": "ev4", | ||
... | ||
"m.relates_to": { | ||
"rel_type": "m.annotation", | ||
"event_id": "ev1", | ||
"key": "❎" | ||
} | ||
} | ||
] | ||
``` | ||
|
||
### Client considerations | ||
|
||
#### Display "m.thread" as "m.in_reply_to" | ||
|
||
It is possible for clients to provide a backwards compatible experience for users | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
by treating the new relation `m.thread` the same way they would treat a `m.in_reply_to` event. | ||
|
||
Failing to do the above should still render the event in the room's timeline. | ||
It might create a disjointed experience as events might lack the original context | ||
for correct understanding. | ||
|
||
#### Sending `m.thread` before fully implementing threads | ||
richvdh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Clients that do not support threads yet should include a `m.thread` relation to the | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
event body if a user is replying to an event that has an `m.thread` relation type | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
This is done so that clients that support threads can render the event in the most | ||
relevant context. | ||
|
||
If a client does not include that relation type to the outgoing event, it will be | ||
rendered in the room timeline with a quote reply that should open and highlight the | ||
event in the thread context when clicked. | ||
|
||
When replying to the following event, a client that does not support thread should | ||
copy in `rel_type` and `event_id` properties in their reply mixin. | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
``` | ||
{ | ||
... | ||
"m.relates_to": { | ||
"rel_type": "m.thread", | ||
"event_id": "ev1" | ||
} | ||
} | ||
``` | ||
|
||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
## Alternatives | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
[MSC2836](https://github.com/matrix-org/matrix-doc/pull/2836), "Threading as rooms", | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
building on `m.in_reply_to` are the main alternatives here. The first two are | ||
non-overlapping with this MSC. | ||
|
||
It is also worth noting that relations in this MSC could be expressed using the | ||
scalable relation format described in [MSC3051](https://github.com/matrix-org/matrix-doc/pull/3051). | ||
|
||
### Threads as rooms | ||
|
||
The provides full server-side APIs for navigating trees of events, and could be | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
considered an extension of this MSC for scenarios which require that capability | ||
(e.g. Twitter-style microblogging as per [Cerulean](https://matrix.org/blog/2020/12/18/introducing-cerulean), | ||
or building an NNTP or IMAP or Reddit style threaded UI) | ||
|
||
"Threads as rooms" is the idea that each thread could just get its own Matrix room | ||
in parallel with the one which spawned the thread. | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Advantages to "Threads as rooms" include: | ||
* May be simpler for client implementations. | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* Also requires minimal Client-Server API changes | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Disadvantages include: | ||
* Access control, membership, history visibility, room versions etc needs to be | ||
synced between the thread-room and the parent room | ||
* Harder to control lifetime of threads in the context of the parent room if | ||
they're completely split off | ||
* Clients which aren't aware of them are going to fill up with a lot of rooms. | ||
* Bridging to non-threaded chat systems is trickier as you may have to splice | ||
together rooms | ||
* The sheer number of rooms involved probably makes it dependent on `/sync` v3 | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
landing (the to-be-specced next generation of `/sync` which is constant-time | ||
complexity with your room count). | ||
|
||
### Threads via m.in_reply_to | ||
|
||
The rationale for using a new relation type instead of building on `m.in_reply_to` | ||
is to re-use the event relationship APIs provided by | ||
[MSC2675](https://github.com/matrix-org/matrix-doc/pull/2675). The MSC3267 definition | ||
of `m.reference` relationships could be updated to mention threads (perhaps by | ||
using the key field from [MSC2677](https://github.com/matrix-org/matrix-doc/pull/2677) | ||
as the thread ID), but it is clearer to define a new relation type. It is unclear | ||
what impact this would have on [MSC3267](https://github.com/matrix-org/matrix-doc/pull/3267), | ||
but that is unimplemented by clients. | ||
|
||
## Security considerations | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
None | ||
|
||
## Unstable prefix | ||
|
||
Clients and servers should use list of unstable prefixes listed below while this | ||
MSC has not been included in a spec release. | ||
|
||
* `io.element.thread` should be used in place of `m.thread` as relation type | ||
* `io.element.relation_senders` should be used in place of `relation_senders` | ||
in the `RoomEventFilter` | ||
* `io.element.relation_types` should be used in place of `relation_types` | ||
in the `RoomEventFilter` |
Uh oh!
There was an error while loading. Please reload this page.