### 线程 {{% added-in v="1.4" %}} 线程允许用户在一个房间中以可视方式分支他们的对话。通常在线上讨论多个主题时使用,线程相较于传统的 [富回复](#rich-replies) 能够提供更有组织的交流方式,而富回复未必能够兼顾所有场景。 客户端应当在时间线上以区别于普通消息或回复的方式渲染线程,例如为线程提供一些上下文信息,但将完整的对话历史隐藏于可展开内容之后。 线程通过 `rel_type` 设为 `m.thread` 来建立,并引用 *线程根*(即该线程事件所指向的主时间线事件)。无法从本身已是事件关系子事件(即带有 `m.relates_to` 和 `rel_type` 属性的事件,参考 [关系类型](#relationship-types))创建线程。因此,线程也无法嵌套。 与富回复链不同,线程中的所有事件都引用线程根,而不是最新的消息。 下面通过示例展示线程及其形成方式: ```json { // 已省略无关字段 "type": "m.room.message", "event_id": "$alice_hello", "sender": "@alice:example.org", "content": { "msgtype": "m.text", "body": "Hello world! How are you?" } } ``` ```json { // 已省略无关字段 "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?" } } ``` ```json { // 已省略无关字段 "type": "m.room.message", "event_id": "$alice_reply", "sender": "@alice:example.org", "content": { "m.relates_to": { "rel_type": "m.thread", "event_id": "$alice_hello" // 注意:始终指向 *线程根* }, "msgtype": "m.text", "body": "I'm doing great! Thanks for asking." } } ``` 如上所示,任何没有 `rel_type` 的事件都可以仅通过 `m.thread` 关系被引用来成为线程根。 #### 非线程化客户端的回退机制 能够理解线程的客户端应直接以线程方式处理,但某些客户端(出于历史原因或功能范围限制)可能无法向用户有效展现对话历史。 为此,支持线程的客户端发送事件时应包含 [富回复](#rich-replies) 元数据,以尝试形成对话的回复链。这种方式在高线程活跃房间中并不理想,但可以为用户提供与房间内其他消息相关的上下文信息。 该兼容方式通过合并两种关系并为 `is_falling_back` 标记设为 `true` 实现。 ```json // 在事件内容中…… "m.relates_to": { // m.thread 关系结构 "rel_type": "m.thread", "event_id": "$root", // 富回复结构 "m.in_reply_to": { // 线程中客户端已知的最新消息,应选取其他客户端有较大渲染概率的事件, // 如 `m.room.message` 事件。 "event_id": "$target" }, // 标记此事件为带回复回退的线程 "is_falling_back": true } ``` {{% boxes/note %}} 对于对线程有一定感知(即不直接渲染线程、但知道规范中有该功能)的客户端,可以将对带有 `rel_type` 为 `m.thread` 事件的富回复视作线程内部的回复,以实现线程客户端侧的对话连续性。 实现方法为:从被回复事件中复制出 `event_id`(线程根),添加 `m.in_reply_to` 元数据,并在 `m.relates_to` 中加入 `is_falling_back: true`。 {{% /boxes/note %}} #### 线程内的回复 在 [非线程化客户端的回退机制](#fallback-for-unthreaded-clients) 部分,为 `m.relates_to` 新增了 `is_falling_back` 字段。当未提供该字段时,默认为 `false`,这同样允许线程消息本身作为回复。 除了 `is_falling_back` 为 `false`(或未指定)以外,客户端应利用非线程化客户端的回退机制在线程内创建回复,并据此渲染事件。 #### 服务器行为 ##### `m.thread` 关系的验证 服务器应拒绝客户端针对带有 `m.relates_to` 属性的事件尝试发起线程的请求。如果客户端试图对带有 `m.relates_to` 属性的事件作为目标事件,则应返回 HTTP 400 错误及相应错误信息,按照 [标准错误响应](#standard-error-response) 结构处理。 {{% boxes/note %}} 此种情况目前没有单独的错误码:服务器应与 HTTP 400 一同返回 `M_UNKNOWN`。 {{% /boxes/note %}} ##### 服务器侧对 `m.thread` 关系的聚合 由于线程总是引用线程根,一个事件将拥有多个“子事件”,共同组成该线程。服务器应对这些事件进行 [聚合](#aggregations-of-child-events)。 线程聚合的数据包括用户在该线程中的参与情况、线程(服务器已知范围内)大致的事件数量,以及线程内最新(按服务器视角的拓扑顺序)的一条消息。 与任何其他子事件聚合一样,`m.thread` 聚合结果通过 `unsigned` 下的 `m.relations` 属性返回给线程根。例如: ```json { "event_id": "$root_event", // 未显示无关字段 "unsigned": { "m.relations": { "m.thread": { "latest_event": { // 线程中最新事件的序列化副本。 // 部分字段为简化未示出。 "event_id": "$message", "sender": "@alice:example.org", "room_id": "!room:example.org", "type": "m.room.message", "content": { "msgtype": "m.text", "body": "Woo! Threads!" }, "unsigned": { "m.relations": { // ... } } }, "count": 7, "current_user_participated": true } } } } ``` `latest_event` 为线程中由未被[忽略的用户](#ignoring-users)发送,服务器视拓扑顺序最新的一条事件。 注意,正如上例,`latest_event` 的子事件本身也需被聚合并包含在该事件下的 `m.relations` 下。服务器需注意避免形成循环,尽管由于 `m.thread` 不允许指向带有 `m.relates_to` 属性的事件,目前不可能产生循环。 `count` 仅指向目标事件的 `rel_type` 为 `m.thread` 的事件数量,未包含[被忽略用户](#ignoring-users)发送的事件。 `current_user_participated` 为 `true` 时,表明认证用户满足以下任一条件: 1. 是线程根事件的 `sender`; 2. 是某个引用线程根且 `rel_type` 为 `m.thread` 的事件的 `sender`。 #### 查询房间内线程 客户端如需获取某线程内的所有事件,可通过 [`GET /relations/{threadRootId}/m.thread`](#get_matrixclientv1roomsroomidrelationseventidreltype); 如需获取某房间内所有线程,则需专用 API: {{% http-api spec="client-server" api="threads_list" %}}