7.9 KiB
回执
{{% changed-in v="1.4" %}} 新增了私有已读回执。
本模块增加了对回执的支持。回执是一种对事件的确认方式。该模块定义了用于表示用户已读至某一事件的 m.read
回执,以及用于相同目的但不会被其他用户察觉的 m.read.private
回执。主要来说,m.read.private
旨在清除通知,而不向他人公开已读状态。
为每个事件发送回执可能导致向主服务器发送大量流量。为了防止这一问题,回执采用“已读至”标记的方式实现。此标记表示确认适用于“至(含)”指定事件的所有事件。例如,将某事件标记为“已读”就意味着用户已经阅读了至此的所有事件。关于已读回执如何影响通知计数,请参见接收通知章节。
{{% added-in v="1.4" %}} 已读回执主要有三种形式:
- 非线程化:表示已读至某事件的回执,与线程无关。这等同于线程功能引入前的已读回执。
- 线程化,主时间线:表示对非特定线程事件的已读回执。用线程 ID
main
标识。 - 线程化,特定线程:表示在指定线程内的已读回执。用线程根事件的事件 ID 标识。
有关线程化回执的更多详细信息,请参阅下文。
事件
{{% changed-in v="1.4" %}} 每个 user_id
、receipt_type
和类别(非线程化,或 thread_id
)三元组必须只关联一个 event_id
。
{{% event event="m.receipt" %}}
客户端行为
{{% changed-in v="1.4" %}} 修订以支持线程化已读回执。
在 /sync
接口中,回执列在指定房间的 ephemeral
事件数组下。新收到的回执是增量信息,用于更新已有的映射。客户端应依据 user_id
、receipt_type
和(如有)thread_id
替换旧的已读回执。例如:
客户端收到 m.receipt:
user = @alice:example.com
receipt_type = m.read
event_id = $aaa:example.com
thread_id = undefined
客户端收到另一个 m.receipt:
user = @alice:example.com
receipt_type = m.read
event_id = $bbb:example.com
thread_id = main
此时客户端尚未替换任何确认。
客户端再次收到 m.receipt:
user = @alice:example.com
receipt_type = m.read
event_id = $ccc:example.com
thread_id = undefined
客户端用新回执 $ccc:example.com 替换之前 $aaa:example.com 的确认,但不会替换 $bbb:example.com,因为它属于线程。
客户端再次收到 m.receipt:
user = @alice:example.com
receipt_type = m.read
event_id = $ddd:example.com
thread_id = main
现在客户端用新 $ddd:example.com 的确认替换旧的 $bbb:example.com。客户端不会替换 $ccc:example.com 的旧回执,因为它是未线程化的。
客户端应在确定事件已显示给用户时才发送已读回执。仅仅收到事件并不能确保用户已经看到。用户应当执行某些操作,如查看事件所处房间或关闭通知,事件方可计为“已读”。客户端不应为自己的用户发送已读回执。
与发送回执的规则类似,线程化回执应出现在线程上下文中。如果线程被折叠,客户端尚未向用户展示该事件(或任何相关已读回执)。一旦用户展开线程,就应发送线程化已读回执,并显示来自其他用户的每线程回执。
客户端可通过以下 HTTP API 与其用户相关的回执状态进行更新。
{{% http-api spec="client-server" api="receipts" %}}
私有已读回执
{{% added-in v="1.4" %}}
部分用户希望标记房间为已读,以清除其通知计数,但又不希望暴露自己已经阅读了特定消息。为此,客户端可以发送 m.read.private
回执,作用同 m.read
,但不会向其他用户广播回执,从而实现只清除通知不公开已读状态。
服务器不得将 m.read.private
回执发送给除最初发送用户外的任何其他用户。
在 m.read
和 m.read.private
两者间,决定最高已读至标记时会采用“更靠前”或“更近期”的回执。关于这对通知计数影响的更多信息,请参见通知章节。
如果客户端发送的 m.read
回执“落后”于 m.read.private
回执,其他用户会看到该变化,但发送用户的通知计数不会回退到那一时间点。尽管不常见,出现 m.read
(公开)回执比 m.read.private
回执滞后几条消息的情况也是合法的。
线程化已读回执
{{% added-in v="1.4" %}}
如果客户端未使用线程功能,则只会发送“非线程化”已读回执,无论线程如何都影响整个房间。
线程化回执指的是带有 thread_id
的回执,其目标为线程根事件的事件 ID 或主时间线用 main
。
线程化引入了在同一房间中进行多次独立会话的概念,因此也对应有独立的已读回执和通知计数。某事件被认为“属于线程”,需满足以下任一条件:
- 其
rel_type
为m.thread
,或 - 在事件关系链上,其父事件通过
rel_type
为m.thread
的方式被关联到线程根。实现时不应无限级递归,建议最多递归 3 级以覆盖间接关系。
房间内未归属于某线程的事件视为主时间线中的事件。当用作线程引用(如回执和通知计数中),主时间线采用特殊线程 ID main
。
线程根本身被视作主时间线事件,通过非线程关系与线程根相关的事件也被视为主时间线事件。
以下是一个房间的 DAG 示例,虚线表示事件间关系,实线表示拓扑排序。
{{% diagram name="threaded-dag" alt="呈现包含线程关系的单一时间线的DAG图" %}}
该 DAG 可分解为 3 条线程化时间线,其中 A
和 B
为线程根:
{{% diagram name="threaded-dag-threads" alt="呈现包含3条相关线程化时间线的 DAG 图" %}}
据此可说明:
- 在
I
上的线程化已读回执会标记A
、B
和I
为已读。 - 在
E
上的线程化已读回执会标记C
和E
为已读。 - 在
D
上的非线程化已读回执会标记A
、B
、C
和D
为已读。
注意,仅用线程化回执将 A
标记为已读,并不会让 C
、E
、G
或 H
也被标记为已读。线程 A 的时间线需在 H
上设置属于该线程的线程化回执才能做到。
上述 3 个例子的回执示例如下:
{
"$I": {
"m.read": {
"@user:example.org": {
"ts": 1661384801651,
"thread_id": "main" // 因为 `I` 不在任何线程中,但回执为线程化回执
}
}
},
"$E": {
"m.read": {
"@user:example.org": {
"ts": 1661384801651,
"thread_id": "$A" // 因为 `E` 属于线程 `A`
}
}
},
"$D": {
"m.read": {
"@user:example.org": {
"ts": 1661384801651
// 无 `thread_id`,因为这是*非线程化*回执
}
}
}
}
发送已读回执的条件在线程化与非线程化场景下适用方式一致。例如,当用户展开某线程时,客户端可能会为该线程事件发送私有已读回执。
服务器行为
出于高效性考虑,应将回执合并打包为按房间和线程分组的事件后再发送给客户端。
部分回执会作为类型为 m.receipt
的 EDU 跨联邦发送。该 EDU 格式为:
{
<room_id>: {
<receipt_type>: {
<user_id>: { <内容(ts & thread_id, 当前支持)> }
},
...
},
...
}
这些均以增量方式相较此前已发送的回执推送。目前仅应使用一个 <receipt_type>
:m.read
。m.read.private
不得出现在联邦 m.receipt
EDU 内。
安全性注意事项
回执是在事件图之外发送的,因此 m.receipt
事件内容不会进行完整性校验。