聊天输入框的 @提及实现
商业 IM 软件里,输入 @ 选中成员以后,会在输入框里插入名字并高亮。显示这几个字并不难,麻烦的是保存以后还得知道提及了谁。
群里可能有人重名,昵称也可能修改,所以不能只传一段 @张三。这里记一下 mention 的数据结构和文本位置计算。
mention 的数据结构
当前协议模型为每次提及保留四个主要字段:
| 字段 | 用途 |
|---|---|
jid | 被提及的用户身份 |
displayName | 这次提及使用的显示名称 |
startIndex | 展示文本中的开始位置 |
endIndex | 展示文本中的结束边界 |
其中身份和名字需要分开。两位成员都叫张三,显示相同不代表指向同一个人。
至于旧消息中的名字应该保留发送时的文字,还是跟随用户资料更新,是产品选择。当前结构允许保存消息携带的显示名称,但不能据此推断所有 UI 都会采取相同更新策略。
原始文本和展示文本各有用途
项目有一种文本表示形式:
请 @[张三](user-a@example.com) 看一下
这是匿名化示例。解析后用于展示的内容是:
请 @张三 看一下
原始表示里有身份信息,展示文本则更接近日常阅读。MentionMessage 同时保存 originalText、displayText 和 mentions,把这几层关系保留下来。
当前解析使用下面的模式寻找标记:
final mentionRegex = RegExp(r'@\[([^\]]+)\]\(([^)]+)\)');
它对应的是项目约定的格式,不是一个通用 Markdown 解析器。显示名中的右方括号、身份字段中的右括号以及转义规则,都不在这条模式里自动解决。
因此,输入端允许哪些字符,序列化时怎样表达特殊字符,都需要与解析规则一致。
替换以后,后面的索引会移动
假设一条消息里有两次提及:
请 @[张三](a@example.com) 和 @[李四](b@example.com) 确认
第一段原始标记替换成 @张三 后,文本变短了。第二次提及在展示文本中的位置,也会比它在原始文本中的位置更靠前。
当前代码维护了一个累计偏移量:
final startIndex = match.start - offset;
final endIndex = startIndex + displayName.length + 1;
每次替换以后,把减少的长度累计进去:
offset += match.group(0)!.length - displayName.length - 1;
末尾的 1 对应展示文本中的 @。
按这套计算,上面的展示结果是“请 @张三 和 @李四 确认”,两个提及的区间分别为 [2, 5) 和 [8, 11)。这里使用左闭右开的表示,便于直接表达子字符串范围。
关键前提是:匹配位置来自原始文本,区间却属于替换后的展示文本。两个文本版本不能混用。
一个看得见的字符,不一定占一个位置
如果在句首加一个 emoji,后面的区间会变多少?
以 😀 为例,它在 Dart 字符串里占两个 UTF-16 code unit。Dart 的 String.length 统计的就是这个单位,而不是用户看到的字符数量。参见 String.length 官方文档。
所以解析下面这段输入时:
😀 @[张三](a@example.com)
展示结果中的提及从索引 3 开始,结束边界为 6。不能按“一个表情加一个空格”算成索引 2。
这也是跨端协议需要明确索引单位的原因。即使两端都用 UTF-16 索引,也仍然要保证它们针对的是同一份展示文本,没有在一端额外做换行替换、字符规范化或昵称修改。
对编辑器来说,光标位置、用户感知字符和协议区间还可能承担不同职责。不能只因为它们最终都用整数表示,就认为可以直接交换。
接收端的区间校验
接收端会从 XML 提取 mention 的身份、显示名和位置。当前解析在位置转换失败时回退为零,并在身份非空时收集记录。
这些步骤把数据读进模型,但还不能证明区间满足完整约束。
例如,进一步校验时需要考虑开始位置是否小于结束位置、边界是否超过展示文本长度、多次提及是否重叠,以及区间里的文字是否与显示名称对应。目前这层还没有把这些检查做全。
App 的编辑辅助方法还使用了 position > startIndex && position <= endIndex 判断位置。这与取子字符串时的左闭右开区间不同,可能是在表达光标落点的语义。调用方必须明确用途,不能把两种判断直接互换。
模型转换也会影响跨端结果
协议侧模型使用 displayName,App 的 AtMention.toJson() 则把显示名输出到 nickname。App 也有转换到协议模型的专门方法。
两个模型的 JSON 字段不完全一样。如果绕过转换,身份和文本可能还在,显示名字段却对不上。
我会拿带 emoji、连续提及和重名的文本走一次转换,再按区间截出名字核对。只用一条 @张三 的例子,很多位置问题看不出来。