# Relations and threads An event can point at another through `content.m.relates_to`: a thread reply (`m.thread`), an edit (`m.replace`), a reaction (`m.annotation`), a reference (`m.reference`). None of that is checked by the authorisation rules. A relation is a client convention, not part of the room's DAG, and a server can accept an event without understanding what it relates to. What the server adds is three things: 1. **The index.** Every event's `m.relates_to` is written out with it, so that "the children of this event" is an index lookup. 2. **The listings.** `/relations` lists an event's children and `/threads` lists a room's threads. 3. **The summaries.** Wherever an event is served, its children are summarised into `unsigned.m.relations`, so a client drawing a timeline knows a message has a thread under it, or has been edited, without asking about every event. The code is in `internal/roomserver/relations.go` (the reads and the summaries) and `internal/syncapi/relations.go` (the tokens), over the relation reads of the room store in `internal/storage/relations.go`. ## The index `roomserver_events` has two more columns, `relates_to_id` and `rel_type`, taken from `content.m.relates_to.event_id` and `.rel_type` when both are strings. Migration 0012 fills them in for the events already stored. An `m.in_reply_to` on its own is not a relation in this sense: a reply points at an event without being one of its children. A redaction clears both columns. The redaction algorithm strips the content they came from, so a redacted thread reply leaves its thread, and a redacted edit stops being one. ## `GET /_matrix/client/v1/rooms/{roomID}/relations/{eventID}[/{relType}[/{eventType}]]` The children of one event, newest first by default. | Parameter | | |---|---| | `relType`, `eventType` | Narrow by relation type, then by event type. | | `dir` | `b` (default) or `f`. | | `from`, `to` | Pagination tokens. They are `/messages` tokens: a sync's `prev_batch` works as a `from`. | | `limit` | 50 by default, 1000 at most. | | `recurse` | Include the children's children, down to three levels. The response then carries `recursion_depth`: how deep it found anything. The filters apply at every level. | The parent has to be visible to the user, and the children are bounded to what they may see, by the same visible range as a timeline read ([rooms.md](rooms.md)). A parent the user may not see is `404`, not `403`: whether a particular event exists in a room is itself not theirs to know. ## `GET /_matrix/client/v1/rooms/{roomID}/threads` The room's threads, the most recently active first. A thread is a root that has at least one `m.thread` child; its activity is its newest child. `include=participated` keeps the threads the user started or replied in. Each root carries its `m.thread` summary. `from` takes the `next_batch` of the previous page, a token of its own (`t`), because threads are ordered by activity rather than by any one stream. A thread whose root this server does not hold — sent before this server joined the room and never backfilled — is not listed: there is no event to show it under. ## Unread counts and receipts Each notification is filed under its thread, a threaded read receipt reads only that thread (`thread_id: "main"` only the main timeline), and a sync whose filter sets `unread_thread_notifications` gets the counts per thread. How, and what stays a room total, is in [push.md](push.md). ## Bundled aggregations Every event a timeline read returns — `/sync`, sliding sync, `/messages`, `/context`, `GET /event`, `/relations` and `/threads` — gets `unsigned.m.relations` when it has children: | Key | What | |---|---| | `m.thread` | `latest_event` (the newest reply, in full), `count`, and `current_user_participated` — true for a user who sent the root or any reply. | | `m.replace` | The newest edit, in full. Only an edit from the original's sender counts, and only one of the same event type; a state event cannot be edited, and a redacted event's edits are not shown. | | `m.reference` | `chunk`: the IDs of the events referencing it, oldest first, at most 50. | Annotations (reactions) are not summarised. The spec dropped their aggregation, and every client counts reactions from the events themselves. The summaries are computed when the event is read, by one grouped query over the batch of events being returned, bounded to what the user may see: a user who has left the room sees a thread's count and latest reply as of when they left. ## What is deliberately missing | Thing | Why | |---|---| | Rejecting a thread whose root is itself a relation | The spec says a server *should*; Warp accepts it, and clients render it as an ordinary reply. | | The relation filters of a room event filter (`related_by_rel_types`, `related_by_senders`) | Ignored, like every filter field Warp does not act on; the client gets a superset. |