docs-matrix-spec/locales/zh-Hans/client-server-api/modules/threading.md
2025-04-20 16:13:37 +08:00

6.6 KiB
Raw Blame History

线程

{{% added-in v="1.4" %}}

线程允许用户在一个房间中以可视方式分支他们的对话。通常在线上讨论多个主题时使用,线程相较于传统的 富回复 能够提供更有组织的交流方式,而富回复未必能够兼顾所有场景。

客户端应当在时间线上以区别于普通消息或回复的方式渲染线程,例如为线程提供一些上下文信息,但将完整的对话历史隐藏于可展开内容之后。

线程通过 rel_type 设为 m.thread 来建立,并引用 线程根(即该线程事件所指向的主时间线事件)。无法从本身已是事件关系子事件(即带有 m.relates_torel_type 属性的事件,参考 关系类型)创建线程。因此,线程也无法嵌套。

与富回复链不同,线程中的所有事件都引用线程根,而不是最新的消息。

下面通过示例展示线程及其形成方式:

{
  // 已省略无关字段
  "type": "m.room.message",
  "event_id": "$alice_hello",
  "sender": "@alice:example.org",
  "content": {
    "msgtype": "m.text",
    "body": "Hello world! How are you?"
  }
}
{
  // 已省略无关字段
  "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?"
  }
}
{
  // 已省略无关字段
  "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 关系被引用来成为线程根。

非线程化客户端的回退机制

能够理解线程的客户端应直接以线程方式处理,但某些客户端(出于历史原因或功能范围限制)可能无法向用户有效展现对话历史。

为此,支持线程的客户端发送事件时应包含 富回复 元数据,以尝试形成对话的回复链。这种方式在高线程活跃房间中并不理想,但可以为用户提供与房间内其他消息相关的上下文信息。

该兼容方式通过合并两种关系并为 is_falling_back 标记设为 true 实现。

// 在事件内容中……
"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_typem.thread 事件的富回复视作线程内部的回复,以实现线程客户端侧的对话连续性。

实现方法为:从被回复事件中复制出 event_id(线程根),添加 m.in_reply_to 元数据,并在 m.relates_to 中加入 is_falling_back: true。 {{% /boxes/note %}}

线程内的回复

非线程化客户端的回退机制 部分,为 m.relates_to 新增了 is_falling_back 字段。当未提供该字段时,默认为 false,这同样允许线程消息本身作为回复。

除了 is_falling_backfalse(或未指定)以外,客户端应利用非线程化客户端的回退机制在线程内创建回复,并据此渲染事件。

服务器行为

m.thread 关系的验证

服务器应拒绝客户端针对带有 m.relates_to 属性的事件尝试发起线程的请求。如果客户端试图对带有 m.relates_to 属性的事件作为目标事件,则应返回 HTTP 400 错误及相应错误信息,按照 标准错误响应 结构处理。

{{% boxes/note %}} 此种情况目前没有单独的错误码:服务器应与 HTTP 400 一同返回 M_UNKNOWN。 {{% /boxes/note %}}

服务器侧对 m.thread 关系的聚合

由于线程总是引用线程根,一个事件将拥有多个“子事件”,共同组成该线程。服务器应对这些事件进行 聚合

线程聚合的数据包括用户在该线程中的参与情况、线程(服务器已知范围内)大致的事件数量,以及线程内最新(按服务器视角的拓扑顺序)的一条消息。

与任何其他子事件聚合一样,m.thread 聚合结果通过 unsigned 下的 m.relations 属性返回给线程根。例如:

{
  "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 为线程中由未被忽略的用户发送,服务器视拓扑顺序最新的一条事件。

注意,正如上例,latest_event 的子事件本身也需被聚合并包含在该事件下的 m.relations 下。服务器需注意避免形成循环,尽管由于 m.thread 不允许指向带有 m.relates_to 属性的事件,目前不可能产生循环。

count 仅指向目标事件的 rel_typem.thread 的事件数量,未包含被忽略用户发送的事件。

current_user_participatedtrue 时,表明认证用户满足以下任一条件:

  1. 是线程根事件的 sender
  2. 是某个引用线程根且 rel_typem.thread 的事件的 sender

查询房间内线程

客户端如需获取某线程内的所有事件,可通过 GET /relations/{threadRootId}/m.thread 如需获取某房间内所有线程,则需专用 API

{{% http-api spec="client-server" api="threads_list" %}}