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

MSC3440: Threading via m.thread relation #3440

Merged
merged 73 commits into from
Mar 9, 2022
Merged
Changes from 22 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
beda89e
Threading via relation
germain-gg Oct 13, 2021
2fa27ac
Add explainer on how to handle m.in_reply_to
germain-gg Oct 18, 2021
d8a0a94
Clarify wording on threading MSC
germain-gg Oct 18, 2021
7d39887
Mention MSC3051 in the alternative section of MSC3440
germain-gg Oct 18, 2021
6e37911
Clarify updates to MSC2675 for MSC3440
germain-gg Oct 18, 2021
c142b17
Line wrap the MSC
germain-gg Oct 18, 2021
c578f75
More line wrapping for MSC3440
germain-gg Oct 18, 2021
33acdf4
Clarify single-layer event aggregation section
germain-gg Oct 25, 2021
7102165
Update thread-as-rooms advantages
germain-gg Oct 25, 2021
f84f949
Clarify backwards compatibility and incremental support
germain-gg Nov 17, 2021
f02dc8d
Clarify wording and correct typos
germain-gg Nov 17, 2021
3e46728
Splitting Cerulean and MSC2836 in alternatives section
germain-gg Nov 17, 2021
44e967f
Add dependencies for threads MSC
germain-gg Nov 19, 2021
65d0d55
Clarify intro to threads as rooms
germain-gg Nov 19, 2021
2b76a6e
Add currentUserParticipated flag
germain-gg Nov 19, 2021
99c5b2e
snake_case over camelCase
germain-gg Nov 19, 2021
4ee42b1
Adding dependency to MSC3567
Dec 14, 2021
fc81bbd
Add threads capability
Jan 5, 2022
91e6ec7
Merge branch 'main' into gsouquet/threading-via-relations
germain-gg Jan 5, 2022
eaeef00
Fix typo
germain-gg Jan 7, 2022
26fb5f2
Update syntax highlighting to use jsonc
germain-gg Jan 11, 2022
a23c795
Add limitations when fetching thread content by relation type
germain-gg Jan 11, 2022
6b1a368
Add reply chain fallback via m.in_reply_to
germain-gg Jan 13, 2022
f227592
Clarity in wording and fix typo
Jan 18, 2022
b493f21
Cosmetic changes based on pull request feedback
germain-gg Jan 18, 2022
46e1e9b
Add note to allow clients to omit fallback for rich replies
germain-gg Jan 18, 2022
e40efa0
fix typo
germain-gg Jan 18, 2022
23928e7
Clarify wording to not confuse thread answers with quote-replies
germain-gg Jan 20, 2022
0880a86
move relations justification to alternatives section
germain-gg Jan 20, 2022
1bbb021
Clarify handling of m.in_reply_to missing rel_type:m.thread
germain-gg Jan 20, 2022
3c977f7
Fix typo
germain-gg Jan 20, 2022
0140454
Fix typo
germain-gg Jan 20, 2022
700464c
Declare MSC2781 as a dependency
germain-gg Jan 21, 2022
0035202
Use rich reply over quote reply
germain-gg Jan 24, 2022
e3cb699
Depend on MSC3676 rather than MSC2781
ara4n Jan 26, 2022
847f468
Remove full stop typo
germain-gg Feb 10, 2022
c8ffa62
Clarify new filtering parameters.
clokep Feb 14, 2022
a7cbf8d
Fix typo.
clokep Feb 15, 2022
5896d69
Update wording for client side considerations
germain-gg Feb 16, 2022
ee5df80
Add m.in_reply_to mixin to thread fallback
germain-gg Feb 16, 2022
00daf64
Add guidance for clients and servers for thread invalid relations
germain-gg Feb 16, 2022
a5d8aab
update thread root wording
germain-gg Feb 17, 2022
d7ed3c4
Add better definition to reply target event
germain-gg Feb 22, 2022
5c04906
Add note regarding forward compatibility
germain-gg Feb 22, 2022
d667a0b
link to MSC2674
richvdh Feb 22, 2022
b157dfd
Update proposals/3440-threading-via-relations.md
germain-gg Feb 22, 2022
3162bea
Clarification on responsibilities for the reply fallback
germain-gg Feb 22, 2022
fa232f4
Update `/messages` API endpoint version on example
germain-gg Feb 22, 2022
68d9c42
Apply wording suggestions from code review
germain-gg Feb 23, 2022
5bbb015
Add notes on server-side invalid relation filtering
germain-gg Feb 22, 2022
707af2b
Fix typo
germain-gg Feb 22, 2022
b28a365
reword paragraph about forwarding m.thread relation
germain-gg Feb 23, 2022
8f82dfa
Add unstable prefix for capability endpoint
germain-gg Feb 23, 2022
8f8be64
Re-order alternatives to match intro paragraph
germain-gg Feb 23, 2022
b6d8076
rework relation_senders and relation_types definition
germain-gg Feb 23, 2022
cd671ef
Apply wording suggestions from code review
germain-gg Feb 24, 2022
a61c01e
Clarify fallback mechanism
germain-gg Feb 24, 2022
362e661
Rename filter property names
germain-gg Feb 24, 2022
b831fb3
Change m.render_in to m.display_reply_fallback
germain-gg Feb 24, 2022
e2dde8e
Clarify what endpoints support the new filter
germain-gg Feb 25, 2022
e640f6b
Switch from /capabilities to /versions
germain-gg Feb 25, 2022
bda3a1e
remove references to Cerulean
germain-gg Feb 25, 2022
89c4b5e
Update latest_event description
germain-gg Feb 25, 2022
61bb518
Clarity in wording and fix typo
germain-gg Mar 1, 2022
9159a5a
rename m.display_reply_fallback to hide_reply
germain-gg Mar 1, 2022
a97307a
remove redundant paragraph about forward compat
germain-gg Mar 1, 2022
f541dab
Improve bundled relationship example
germain-gg Mar 1, 2022
82b4c62
Explain context on why a thread-unaware client might want to send m.t…
germain-gg Mar 1, 2022
75f4cb2
Clarify `hide_reply`
germain-gg Mar 4, 2022
54ce185
Rename hide_reply to show_reply
germain-gg Mar 8, 2022
6d6baa2
rename show_reply to is_falling_back
germain-gg Mar 8, 2022
893cf1f
Add note about stable support.
clokep Mar 8, 2022
641e326
Update proposals/3440-threading-via-relations.md
germain-gg Mar 9, 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
335 changes: 335 additions & 0 deletions proposals/3440-threading-via-relations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
# MSC3440 Threading via `m.thread` relation

## Problem

Threading is a great way to create alternative timelines to group messages related
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
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"
- Building threads off `m.in_reply_to`
germain-gg marked this conversation as resolved.
Show resolved Hide resolved

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

```jsonc
{
"latest_event": {
"content": {
// ...
},
// ...
},
"count": 7,
"current_user_participated": true
}
```

* `latest_event`: A reference to the last `m.thread` relation part of the thread
richvdh marked this conversation as resolved.
Show resolved Hide resolved
* `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

#### 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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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. m.in_reply_to exists, but m.thread does not. This happens particularly when replying from a client that isn't yet thread-aware, e.g. currently conversations between element web with labs threads and element android. I come here from element-hq/element-web#19910 to discuss this very case, because - in my understanding - currently element web handles it contrary to the way of what @gsouquet suggests in his earlier comment which is now marked as resolved even though the MSC hasn't been amended to cover it:

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 m.thread relation. Repeating this (reply to reply to threaded message, etc.) currently breaks and the 2nd level of reply is shown on the main timeline again.

Allowing replies to threaded events without being part of the thread may be regarded as either:

  • unwanted behaviour: thread-aware clients should "reverse-fallback", i.e. a reply chain on a threaded message always should be regarded as threaded messages. At this point, we're back at threads-as-reply-chains so imo this option should be disregarded if this MSC's way to implement threads is to be accepted.
  • feature: it is now possible to pull a threaded discussion back to the main timeline, e.g. to get everybody's attention about an important point made in a thread which everybody should be aware of. However I find questionable how often this would actually be useful. Further, this would regularly happen unintentionally when a non-thread aware client is involved, e.g. doesn't fall back well either. This option doesn't sound very viable to me as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 m.in_reply_to but no rel_type:m.thread should be display in the room, and Element should not try to be smart and display that message in a thread.
This will only be done if clients that do not fully support thread decide to attach the new relation. This will open the door to have a reply in the main timeline targetting a threaded event in case one wants to advertise a piece of the conversation to a wider audience.

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 exist pointing to the event
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
being filtered. If this list is absent then no filtering is done on relation types.
* `relation_senders`: A list of senders of relations which must exist pointing to
the event being filtered. If this list is absent then no filtering is done on relation types.
richvdh 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
```

The filter string would include the new fields, above. In this example, the URL
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
encoded JSON is presented unencoded and formatted for legibility:

```jsonc
{
"types": ["m.room.message"],
"relation_senders": [
// ...
],
"relation_types": ["m.thread"]
}
```

### Server capabilities

Threads might have sporadic support across servers, to simplify feature
detections for clients, a homeserver must return a capability entry for threads.
germain-gg marked this conversation as resolved.
Show resolved Hide resolved

```jsonc
{
"capabilities": {
// ...
"m.thread": {
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
"enabled": 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 synthetize read receipts but it is possible that some notifications get
germain-gg 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. This MSC is not looking to
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
solve nested threading but is rather focusing on bringing mechanisms to allow
threading in chat applications

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",
"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



####
### Client considerations

#### Display "m.thread" as "m.in_reply_to"

Clients that don't support threads will render threaded messages in the room's
timeline at the point at which they were sent. This does risk a confusing experience
for those on such clients, but options to mitigate this are limited.

Having older clients treat threaded messages as replies would give a better
experience, but adding reply metadata in addition to thread metadata would mean
replies could not then be used in threads and would be significant extra metadata.

Clients that wish to offer basic thread support can display threads as replies to
the thread root message. See matrix-org/matrix-react-sdk#7109 for an example.
germain-gg marked this conversation as resolved.
Show resolved Hide resolved

#### 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

```jsonc
{
// ...
"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", and
building on `m.in_reply_to` are the main alternatives here. The first two are
non-overlapping with this MSC.
germain-gg marked this conversation as resolved.
Show resolved Hide resolved

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 (e.g. Twitter-style microblogging as per
[Cerulean](https://matrix.org/blog/2020/12/18/introducing-cerulean), or building
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
an NNTP or IMAP or Reddit style threaded UI)
germain-gg marked this conversation as resolved.
Show resolved Hide resolved

"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.
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
* 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 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

### 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`
germain-gg marked this conversation as resolved.
Show resolved Hide resolved

## 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) and,
[MSC3567](https://github.com/matrix-org/matrix-doc/pull/3567/files) (which at the
time of writing have not yet been accepted into the spec).