From 489f8d854409c3f1db23f997ed6569618d280eeb Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 9 Mar 2022 16:52:51 +0000 Subject: [PATCH] MSC3440: Threading via `m.thread` relation (#3440) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Threading via relation * Add explainer on how to handle m.in_reply_to * Clarify wording on threading MSC * Mention MSC3051 in the alternative section of MSC3440 * Clarify updates to MSC2675 for MSC3440 Co-authored-by: Patrick Cloke * Line wrap the MSC * More line wrapping for MSC3440 * Clarify single-layer event aggregation section * Update thread-as-rooms advantages * Clarify backwards compatibility and incremental support * Clarify wording and correct typos * Splitting Cerulean and MSC2836 in alternatives section * Add dependencies for threads MSC * Clarify intro to threads as rooms * Add currentUserParticipated flag * snake_case over camelCase * Adding dependency to MSC3567 * Add threads capability * Fix typo * Update syntax highlighting to use jsonc * Add limitations when fetching thread content by relation type * Add reply chain fallback via m.in_reply_to * Clarity in wording and fix typo Co-authored-by: James Salter Co-authored-by: Matthew Hodgson * Cosmetic changes based on pull request feedback * Add note to allow clients to omit fallback for rich replies * fix typo * Clarify wording to not confuse thread answers with quote-replies * move relations justification to alternatives section * Clarify handling of m.in_reply_to missing rel_type:m.thread * Fix typo * Fix typo * Declare MSC2781 as a dependency * Use rich reply over quote reply * Depend on MSC3676 rather than MSC2781 Rather than relying on fallbacks being removed outright (and all the notification complications that causes in #2781), instead depend on #3676 which makes fallbacks best effort. * Remove full stop typo Co-authored-by: Erik Johnston * Clarify new filtering parameters. * Fix typo. * Update wording for client side considerations Co-authored-by: Hubert Chathi * Add m.in_reply_to mixin to thread fallback * Add guidance for clients and servers for thread invalid relations * update thread root wording * Add better definition to reply target event Co-authored-by: Travis Ralston * Add note regarding forward compatibility * link to MSC2674 * Update proposals/3440-threading-via-relations.md Co-authored-by: Hubert Chathi * Clarification on responsibilities for the reply fallback Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Update `/messages` API endpoint version on example Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Apply wording suggestions from code review Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Add notes on server-side invalid relation filtering * Fix typo * reword paragraph about forwarding m.thread relation * Add unstable prefix for capability endpoint * Re-order alternatives to match intro paragraph * rework relation_senders and relation_types definition * Apply wording suggestions from code review Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Co-authored-by: Kim Brose * Clarify fallback mechanism * Rename filter property names * Change m.render_in to m.display_reply_fallback * Clarify what endpoints support the new filter * Switch from /capabilities to /versions * remove references to Cerulean * Update latest_event description * Clarity in wording and fix typo Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * rename m.display_reply_fallback to hide_reply * remove redundant paragraph about forward compat * Improve bundled relationship example * Explain context on why a thread-unaware client might want to send m.thread * Clarify `hide_reply` Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Rename hide_reply to show_reply * rename show_reply to is_falling_back * Add note about stable support. * Update proposals/3440-threading-via-relations.md Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Co-authored-by: Patrick Cloke Co-authored-by: James Salter Co-authored-by: Matthew Hodgson Co-authored-by: Erik Johnston Co-authored-by: Patrick Cloke Co-authored-by: Hubert Chathi Co-authored-by: Travis Ralston Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Co-authored-by: Kim Brose --- proposals/3440-threading-via-relations.md | 385 ++++++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 proposals/3440-threading-via-relations.md diff --git a/proposals/3440-threading-via-relations.md b/proposals/3440-threading-via-relations.md new file mode 100644 index 00000000..4a6975d2 --- /dev/null +++ b/proposals/3440-threading-via-relations.md @@ -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 + +#### 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 +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, + "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 +``` + +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 + +#### 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. + +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 + +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. + +```jsonc +{ + // ... + "m.relates_to": { + "rel_type": "m.thread", + "event_id": "ev1", + "is_falling_back": false, + "m.in_reply_to": { + "event_id": "$event_target" + } + } +} +``` + +## Alternatives + +"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 + +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. + +## 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).