-
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 all 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,385 @@ | ||
# MSC3440 Threading via `m.thread` relation | ||
|
||
## Problem | ||
|
||
Threading allows users to branch out a new conversation from the main timeline of a room | ||
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 of implementing threads is to facilitate conversations that are easier | ||
to follow and smoother to read. | ||
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 type (see [MSC2674](https://github.com/matrix-org/matrix-doc/pull/2674)) | ||
`m.thread` expresses that an event belongs to a thread. | ||
|
||
```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. | ||
|
||
When a thread root is aggregated (as in MSC2675), it returns a summary of the thread: | ||
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: | ||
|
||
```jsonc | ||
{ | ||
"event_id": "$root_event", | ||
"unsigned": { | ||
"m.relations": { | ||
"m.thread": { | ||
"latest_event": { | ||
"event_id": "$thread_event", | ||
// ... | ||
}, | ||
"count": 7, | ||
"current_user_participated": true | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
|
||
* `latest_event`: The most recent event which relates to this event, with | ||
`rel_type` of `m.thread`. | ||
* `count`: An integer counting the number of `m.thread` events | ||
* `current_user_participated`: A flag set to `true` if the current logged in user | ||
has participated in the thread | ||
clokep marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
#### Rich replies in a thread | ||
|
||
Rich replies are still handled via the `m.in_reply_to` field of `m.relates_to`. | ||
However clients should specify that this is not a thread fallback by setting | ||
the `is_falling_back` property to `false`. | ||
|
||
```json | ||
"m.relates_to": { | ||
"rel_type": "m.thread", | ||
"event_id": "$thread_root", | ||
"is_falling_back": false, | ||
"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 utmost to display the rich reply | ||
and when clicked, the event should be displayed and highlighted in its original context. | ||
|
||
A rich reply without `rel_type: m.thread` targeting a thread relation must be | ||
rendered in the main timeline. This will allow users to advertise threaded messages | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
in the room. | ||
|
||
### Backwards compatibility | ||
|
||
A thread will be displayed as a chain of replies on clients unaware of threads. | ||
|
||
Thread-ready clients should always include an `m.in_reply_to` property when sending | ||
a threaded event. Unless the user is explicitly replying to another event (see "Rich replies in a thread", above), | ||
the `m.in_reply_to` property should reference the latest message-like event in the | ||
thread, and clients should also specify that `m.in_reply_to` | ||
is a fallback mechanism (rather than a genuine reply) by setting the `is_falling_back` property to `true`. | ||
|
||
(If omitted, `is_falling_back` defaults to `false`, and receiving clients will treat the | ||
`m.in_reply_to` part of the event as a genuine reply.) | ||
|
||
```jsonc | ||
"m.relates_to": { | ||
"rel_type": "m.thread", | ||
"event_id": "ev1", | ||
"is_falling_back": true, | ||
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. So one thing I noticed when looking at the push rules again: I expected this key to be in the "m.in_reply_to" relation, not on the thread relation. Is that intentional or not? Could this possibly be moved to the relation, that is actually falling back, so in this case the reply? Otherwise this seems to be a bit ambiguous. 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. Moving it at this point would mean having to handle it in both cases indefinitely to not break the semantics of existing history. 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. Well, the only consequence of that change would be that some old events would render with a reply. Considering the short timeframe that the use of m.thread has been legal and that this is not in any spec release yet and won't be for 3 months, I think fixing that now would be the much, much smaller pain point. Clients could accept both for a few months in the transition period and after that old threads might have some extra replies in them, but since they are still marked as beta, that will be justifyable. If we don't fix it, we will have to introduce a new field to mark other relations as fallbacks in the future, which will be then something we will actually have to have compatibility with for years. So no, I don't agree we would need to handle that indefinitely. Implementing a feature before it is in a spec release has some risk and the problems with that will be minimal. I can write an MSC for that though, if it is something we need to argue about. 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. I think @deepbluev7 has a point, but as I just wrote on #3664: please can you open a new issue for it. 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. I wrote an MSC instead: #3825 |
||
"m.in_reply_to": { | ||
"event_id": "last_event_id_in_thread", | ||
} | ||
} | ||
``` | ||
|
||
Historically replies have been limited to text messages due to the legacy fallback | ||
prepended to `formatted_body`. This MSC is dependant on | ||
[MSC3676](https://github.com/matrix-org/matrix-doc/pull/3676) which strips that | ||
requirement to unlock use of any event type in this context. | ||
|
||
### Fetch all relations to a thread root | ||
|
||
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. | ||
|
||
> 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 | ||
|
||
[Event filters](https://spec.matrix.org/v1.2/client-server-api/#filtering) (as | ||
used by endpoints including `/messages`, `/sync` and `/context`) are extended | ||
with new options to allow filtering events by their relating events: | ||
|
||
* `related_by_rel_types`: A list of relation types to include. An event `A` is included | ||
in the filter only if there exists another event `B` which relates to `A` with a | ||
`rel_type` which is defined in the list | ||
* `related_by_senders`: A list of senders to include. An event `A` is included in | ||
the filter only if there exists another event `B` which relates to `A`, and | ||
which has a `sender` which is in the list. | ||
|
||
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/v3/rooms/!room_id:domain/messages?filter=... | ||
``` | ||
|
||
The filter string includes the new fields, above. In this example, the URL | ||
encoded JSON is presented unencoded and formatted for legibility: | ||
|
||
```jsonc | ||
{ | ||
"types": ["m.room.message"], | ||
"related_by_senders": [ | ||
// ... | ||
], | ||
"related_by_rel_types": ["m.thread"] | ||
} | ||
``` | ||
|
||
Note that the newly added filtering parameters return events based on information | ||
in related events. Consider the following events in a room: | ||
|
||
* `A`: a `m.room.message` event sent by `alice` | ||
* `B`: a `m.room.message` event sent by `bob` which relates to `A` with type `m.thread` | ||
|
||
Using a filter of `"related_by_rel_types": ["m.thread"]` would return event `A` as it | ||
has another event which relates to it via `m.thread`. | ||
|
||
Similarly, using a filter of `"related_by_senders": ["bob"]` would return event `A` | ||
as it has another event which relates to it sent by `bob`. | ||
|
||
### Server capabilities | ||
|
||
Threads might have sporadic support across servers, to simplify feature | ||
detections for clients, a homeserver must advertise unstable support for threads | ||
as part of the `/versions` API: | ||
|
||
```jsonc | ||
{ | ||
"unstable_features": { | ||
"org.matrix.msc3440": true, | ||
// ... | ||
} | ||
} | ||
``` | ||
|
||
### 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 synthesize read receipts but it is possible that some notifications get | ||
lost on a fresh start where the clients have to start off the `m.read` | ||
information received from the homeserver. | ||
|
||
Synchronising the synthesized notification count across devices is out of scope and deferred to a later MSC. | ||
|
||
#### Single-layer event aggregation | ||
|
||
This MSC does not include support for nested threads. | ||
|
||
Nested threading is out of scope for this proposal and would be the subject of | ||
a different MSC. | ||
A `m.thread` event can only reference events that do not have a `rel_type` | ||
|
||
```jsonc | ||
[ | ||
{ | ||
"event_id": "ev1", | ||
// ... | ||
}, | ||
{ | ||
"event_id": "ev2", | ||
// ... | ||
"m.relates_to": { | ||
"rel_type": "m.thread", | ||
"event_id": "ev1", | ||
"is_falling_back": true, | ||
"m.in_reply_to": { | ||
"event_id": "ev1" | ||
} | ||
} | ||
}, | ||
{ | ||
"event_id": "ev3", | ||
// ... | ||
"m.relates_to": { | ||
"rel_type": "m.annotation", | ||
"event_id": "ev1", | ||
"key": "✅" | ||
} | ||
} | ||
] | ||
``` | ||
|
||
Given the above list of events, only `ev1` would be a valid target for an `m.thread` | ||
relation event. | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Servers should reject attempts to send events with invalid thread relations via the | ||
Client-Server API with an HTTP `400` status code and a | ||
`M_UNKNOWN` error code. | ||
Events received over federation should always be accepted without checking | ||
the validity of the relations as it would break the extensibility of this proposal | ||
in a future MSC. | ||
|
||
This means that events with invalid thread relations can make their way into the | ||
network, either due by malicious activity or buggy implementation. If a client | ||
receives such events, it should hide them as soon as it can determine for certain | ||
that the associated event is not a valid target. | ||
|
||
Servers are expected to not filter out invalid `m.thread` relations from the results when | ||
serving endpoints that deal with message relations. Clients that call those | ||
endpoints should be aware that they may return events with invalid relations, | ||
and deal with them appropriately. | ||
|
||
### Client considerations | ||
|
||
#### Sending `m.thread` before fully implementing threads | ||
richvdh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
There will be clients that will not or can't support threads. Whether this is a | ||
deliberate choice or because the system bridges to a platform that does not support | ||
threads, there are a number of steps developer of those systems can take to ensure | ||
continuity of conversation in the ecosystem. | ||
|
||
Clients that do not offer a threading UI should behave as follows when replying, for | ||
best interaction with those that do. | ||
They should set the `m.in_reply_to` part as usual, and then add on | ||
`"rel_type": "m.thread"` and `"event_id": "$thread_root"`, copying `$thread_root` | ||
from the replied-to event. | ||
|
||
If the `m.thread` relation type is not present in an incoming event, it should | ||
be treated as not being part of the thread. For example, if a client has a | ||
separate area for displaying threads, clients can render the event in the main | ||
room timeline as a rich reply that will open and highlight the event in the | ||
thread context when clicked. | ||
|
||
When replying to the following event, a client that does not support threads should | ||
copy in `rel_type` and `event_id` properties in their reply mixin. | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```jsonc | ||
{ | ||
// ... | ||
"m.relates_to": { | ||
"rel_type": "m.thread", | ||
"event_id": "ev1", | ||
"is_falling_back": false, | ||
"m.in_reply_to": { | ||
"event_id": "$event_target" | ||
} | ||
} | ||
} | ||
``` | ||
|
||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
## Alternatives | ||
germain-gg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
"Threading as rooms", building on `m.in_reply_to`, and [MSC2836](https://github.com/matrix-org/matrix-doc/pull/2836) are the main alternatives here. | ||
|
||
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 | ||
|
||
Threads as rooms could provide full server-side APIs for navigating trees of events, | ||
and could be considered an extension of this MSC for scenarios which require that | ||
capability | ||
|
||
"Threads as rooms" is the idea that each thread could just get its own Matrix room. | ||
|
||
Advantages to "Threads as rooms" include: | ||
* May be simpler for client implementations | ||
* Restricting events visibility as the room creator | ||
* Ability to create read-only threads | ||
|
||
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 | ||
|
||
### 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. | ||
|
||
A big advantage of relations over rich 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. | ||
|
||
### Threads via serverside traversal of relationships MSC2836 | ||
|
||
Advantages include: | ||
* Fits other use cases than instant messaging | ||
* Simple possible API shape to implement threading in a useful way | ||
|
||
Disadvantages include: | ||
* Relationships are queried using `/event_relationships` which is outside the | ||
bounds of the `/sync` API so lacks the nice things /sync gives you (live updates). | ||
That being said, the event will come down `/sync`, you just may not have the | ||
context required to see parents/siblings/children. | ||
* Threads can be of arbitrary width (unlimited direct replies to a single message) | ||
and depth (unlimited chain of replies) which complicates UI design when you just | ||
want "simple" threading. | ||
* Does not consider use cases like editing or reactions | ||
|
||
## 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.thread` should be used in place of `m.thread` as a capability entry | ||
* `io.element.relation_senders` should be used in place of `related_by_senders` | ||
in the `RoomEventFilter` | ||
* `io.element.relation_types` should be used in place of `related_by_rel_types` | ||
in the `RoomEventFilter` | ||
* `io.element.show_reply` should be used in place of `is_falling_back` | ||
|
||
While this MSC is considered stable, but not in v1.3 of the specification, clients | ||
should look for `org.matrix.msc3440.stable` as an unstable feature flag as part of | ||
the `/versions` API response to determine server support for the stable identifiers. | ||
Comment on lines
+375
to
+377
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. just to ack this here too: we (the SCT) are actively committing to v1.3 having threads, so are okay with this. |
||
|
||
## Dependencies | ||
|
||
This MSC builds on [MSC2674](https://github.com/matrix-org/matrix-doc/pull/2674), | ||
[MSC2675](https://github.com/matrix-org/matrix-doc/pull/2675), | ||
[MSC3567](https://github.com/matrix-org/matrix-doc/pull/3567) and, | ||
[MSC3676](https://github.com/matrix-org/matrix-doc/pull/3676) (which at the | ||
time of writing have not yet been accepted into the spec). |
Uh oh!
There was an error while loading. Please reload this page.