Threading

[Added in v1.4]

Threads allow users to visually branch their conversations in a room. Typically mostly used when a room is discussing multiple topics, threads provide more organisation of communication that traditional rich replies can’t always offer.

Clients SHOULD render threads differently to regular messages or replies in the timeline, such as by providing some context to what is going on in the thread but keeping the full conversation history behind a disclosure.

Threads are established using a rel_type of m.thread and reference the thread root (the first event in a thread). It is not possible to create a thread from an event with a rel_type, which includes not being able to nest threads. All conversation in a thread reference the thread root instead of the most recent message, unlike rich reply chains.

As a worked example, the following represents a thread and how it’d be formed:

{
  // irrelevant fields excluded
  "type": "m.room.message",
  "event_id": "$alice_hello",
  "sender": "@alice:example.org",
  "content": {
    "msgtype": "m.text",
    "body": "Hello world! How are you?"
  }
}
{
  // irrelevant fields excluded
  "type": "m.room.message",
  "event_id": "$bob_hello",
  "sender": "@bob:example.org",
  "content": {
    "m.relates_to": {
      "rel_type": "m.thread",
      "event_id": "$alice_hello"
    },
    "msgtype": "m.text",
    "body": "I'm doing okay, thank you! How about yourself?"
  }
}
{
  // irrelevant fields excluded
  "type": "m.room.message",
  "event_id": "$alice_reply",
  "sender": "@alice:example.org",
  "content": {
    "m.relates_to": {
      "rel_type": "m.thread",
      "event_id": "$alice_hello" // note: always references the *thread root*
    },
    "msgtype": "m.text",
    "body": "I'm doing great! Thanks for asking."
  }
}

As shown, any event without a rel_type can become a thread root by simply referencing it using an m.thread relationship.

Fallback for unthreaded clients

Clients which understand how to work with threads should simply do so, however clients which might not be aware of threads (due to age or scope) might not be able to helpfully represent the conversation history to its users.

To work around this, events sent by clients which understand threads include rich reply metadata to attempt to form a reply chain representation of the conversation. This representation is not ideal for heavily threaded rooms, but allows for users to have context as to what is being discussed with respect to other messages in the room.

This representation is achieved by merging the two relationships and setting a new is_falling_back flag to true.

// within an event's content...
"m.relates_to": {
  // The m.thread relationship structure
  "rel_type": "m.thread",
  "event_id": "$root",

  // The rich reply structure
  "m.in_reply_to": {
    // The most recent message known to the client in the thread.
    // This should be something with a high chance of being rendered by the other client,
    // such as an `m.room.message` event.
    "event_id": "$target"
  },

  // A flag to denote that this is a thread with reply fallback
  "is_falling_back": true
}

For m.room.message events represented this way, no reply fallback is specified. This allows thread-aware clients to discard the m.in_reply_to object entirely when is_falling_back is true.

Replies within threads

In the fallback for unthreaded clients section, a new is_falling_back flag is added to m.relates_to. This flag defaults to false when not provided, which also allows a threaded message to contain a reply itself.

Aside from is_falling_back being false (or not specified), the fallback for unthreaded clients is used to create a reply within a thread: clients should render the event accordingly.

Server behaviour

Validation of m.thread relationships

Servers SHOULD reject client requests which attempt to start a thread off an event with a rel_type. If the client attempts to target an event which already has an m.thread, m.reference, or any other rel_type then it should receive a HTTP 400 error response with appropriate error message, as per the standard error response structure.

Server-side aggregation of m.thread relationships

Given threads always reference the thread root, an event can have multiple “child” events which then form the thread itself. These events should be aggregated by the server.

The aggregation for threads includes some information about the user’s participation in the thread, the approximate number of events in the thread (as known to the server), and the most recent event in the thread (topologically). This is then bundled into the event as m.thread:

{
  "event_id": "$root_event",
  // irrelevant fields not shown
  "unsigned": {
    "m.relations": {
      "m.thread": {
        "latest_event": {
          // A serialized copy of the latest event in the thread.
          // Some fields are not shown here for brevity.
          "event_id": "$message",
          "sender": "@alice:example.org",
          "room_id": "!room:example.org",
          "type": "m.room.message",
          "content": {
            "msgtype": "m.text",
            "body": "Woo! Threads!"
          }
        },
        "count": 7,
        "current_user_participated": true
      }
    }
  }
}

latest_event is the most recent event (topologically to the server) in the thread sent by an un-ignored user.

Note that any bundled aggregations on latest_event should also be present. The server should be careful to avoid loops, though loops are not currently possible due to m.thread not being possible to target an event with a rel_type already.

count is simply the number of events using m.thread as a rel_type pointing to the target event. It does not include events sent by ignored users.

current_user_participated is true when the authenticated user is either:

  1. The sender of the event receiving the bundle (they sent the thread root).
  2. The sender of an event which references the thread root with a rel_type of m.thread.

Querying threads in a room

Clients looking to get all the events in a thread can use GET /relations/{threadRootId}/m.thread, however getting all threads in a room is done through a dedicated API:

GET /_matrix/client/v1/rooms/{roomId}/threads


Added in v1.4

Paginates over the thread roots in a room, ordered by the latest_event of each thread root in its bundle.

Rate-limited: Yes
Requires authentication: Yes

Request

Request parameters

path parameters
Name Type Description
roomId string Required: The room ID where the thread roots are located.
query parameters
Name Type Description
from string A pagination token from a previous result. When not provided, the server starts paginating from the most recent event visible to the user (as per history visibility rules; topologically).
include enum Optional (default all) flag to denote which thread roots are of interest to the caller. When all, all thread roots found in the room are returned. When participated, only thread roots for threads the user has participated in will be returned.

One of: [all, participated].

limit integer

Optional limit for the maximum number of thread roots to include per response. Must be an integer greater than zero.

Servers should apply a default value, and impose a maximum value to avoid resource exhaustion.


Responses

Status Description
200 A portion of the available thread roots in the room, based on the filter criteria.
400

The request was invalid in some way. A meaningful errcode and description error text will be returned. Example reasons for rejection are:

  • The from token is unknown to the server.
403

The user cannot view or peek on the room. A meaningful errcode and description error text will be returned. Example reasons for rejection are:

  • The room is not set up for peeking.
  • The user has been banned from the room.
  • The room does not exist.
429 This request was rate-limited.

200 response

Name Type Description
chunk [ClientEvent] Required:

The thread roots, ordered by the latest_event in each event’s aggregation bundle. All events returned include bundled aggregations.

If the thread root event was sent by an ignored user, the event is returned redacted to the caller. This is to simulate the same behaviour of a client doing aggregation locally on the thread.

next_batch string A token to supply to from to keep paginating the responses. Not present when there are no further results.
ClientEvent
Name Type Description
content object Required: The body of this event, as created by the client which sent it.
event_id string Required: The globally unique identifier for this event.
origin_server_ts integer Required: Timestamp (in milliseconds since the unix epoch) on originating homeserver when this event was sent.
room_id string Required: The ID of the room associated with this event.
sender string Required: Contains the fully-qualified ID of the user who sent this event.
state_key string

Present if, and only if, this event is a state event. The key making this piece of state unique in the room. Note that it is often an empty string.

State keys starting with an @ are reserved for referencing user IDs, such as room members. With the exception of a few events, state events set with a given user’s ID as the state key MUST only be set by that user.

type string Required: The type of the event.
unsigned UnsignedData Contains optional extra information about the event.
UnsignedData
Name Type Description
age integer The time in milliseconds that has elapsed since the event was sent. This field is generated by the local homeserver, and may be incorrect if the local time on at least one of the two servers is out of sync, which can cause the age to either be negative or greater than it actually is.
prev_content EventContent The previous content for this event. This field is generated by the local homeserver, and is only returned if the event is a state event, and the client has permission to see the previous content.

Changed in v1.2: Previously, this field was specified at the top level of returned events rather than in unsigned (with the exception of the GET .../notifications endpoint), though in practice no known server implementations honoured this.
redacted_because ClientEvent The event that redacted this event, if any.
transaction_id string The client-supplied transaction ID, for example, provided via PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}, if the client being given the event is the same one which sent it.
{
  "chunk": [
    {
      "content": {
        "body": "This is an example text message",
        "format": "org.matrix.custom.html",
        "formatted_body": "<b>This is an example text message</b>",
        "msgtype": "m.text"
      },
      "event_id": "$143273582443PhrSn:example.org",
      "origin_server_ts": 1432735824653,
      "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
      "sender": "@example:example.org",
      "type": "m.room.message",
      "unsigned": {
        "age": 1234
      }
    }
  ],
  "next_batch": "next_batch_token"
}

400 response

Error
Name Type Description
errcode string Required: An error code.
error string A human-readable error message.
{
  "errcode": "M_INVALID_PARAM",
  "error": "Unknown pagination token"
}

403 response

Error
Name Type Description
errcode string Required: An error code.
error string A human-readable error message.
{
  "errcode": "M_FORBIDDEN",
  "error": "You are not allowed to view this room."
}

429 response

RateLimitError
Name Type Description
errcode string Required: The M_LIMIT_EXCEEDED error code
error string A human-readable error message.
retry_after_ms integer The amount of time in milliseconds the client should wait before trying the request again.
{
  "errcode": "M_LIMIT_EXCEEDED",
  "error": "Too many requests",
  "retry_after_ms": 2000
}