From 8cb96e925486c1aab86f5cb4e5b544abcf3a743d Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 6 Sep 2026 16:12:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=92=A4=E5=9B=9E=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=9C=A8=E5=AF=B9=E6=96=B9=E7=95=8C=E9=9D=A2=E6=98=BE?= =?UTF-8?q?=E7=A4=BA"=E6=97=A0=E6=95=88=E5=86=85=E5=AE=B9"=E8=80=8C?= =?UTF-8?q?=E9=9D=9E"=E6=B6=88=E6=81=AF=E5=B7=B2=E5=88=A0=E9=99=A4"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象:真实调用撤回接口成功后(recall_window_seconds=120秒窗口内,长按菜单 "撤回"),撤回发起方自己的消息列表里这条消息直接消失,没有问题;但对方 (消息接收方)的聊天界面上,这条消息位置渲染成一个灰色警告图标+"无效内容" (invalid content),不是预期的"此消息已被撤回/删除"占位提示。 根因:Tinode原生协议里,一条消息被撤回后,服务端把这条消息的content原地 置为null(seq位置保留,其余内容清空)。chat-message.jsx渲染消息内容时按 Drafty富文本、纯字符串、其余情况三段分支处理,content为null时两段判断 (Drafty.isValid和typeof=='string')都不成立,直接落到兜底的"invalid content"分支——这段代码从未加过null-content(撤回)的专门判断。这跟 messages-view.jsx里另一套依赖msg.hi标记位的"删除"消息渲染(走独立的 组件)是两码事:撤回消息还是走的普通 ChatMessage组件,从未接入deletedText这个机制。 修复:chat-message.jsx新增一个content==null的分支,复用 messages-view.jsx里messages.message_deleted已经注册好的同一个i18n id/文案("消息已删除",非新增翻译串),渲染为灰色图标+"消息已删除", 不再落到"无效内容"兜底分支。 验证:真实浏览器操作,两个全新注册账号互为好友,A发一条文本消息后 立即长按撤回(120秒窗口内),B的聊天界面上这条消息正确显示"消息已删除" (此前是"无效内容")。 第14+轮最小粒度测试 Ch4 发现(撤回消息渲染场景)。 Co-Authored-By: Claude Sonnet 5 --- src/widgets/chat-message.jsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/widgets/chat-message.jsx b/src/widgets/chat-message.jsx index 586061f3..cca076a5 100644 --- a/src/widgets/chat-message.jsx +++ b/src/widgets/chat-message.jsx @@ -374,6 +374,23 @@ class BaseChatMessage extends React.PureComponent { // Content consists of 1-5 emoji characters. Count how many and use it to increase the font size. textSizeClass += ' emoji-' + (content || '').match(/\p{RGI_Emoji}/vg).length; } + } else if (content == null) { + // Tinode's wire protocol marks a recalled message by nulling its `content` in place (the seq + // slot stays, everything else is gone) -- see server's message.recalled handling and + // IM-前后端对接文档.md 1256行. That's a *different* deletion shape from the msg.hi-flagged kind + // messages-view.jsx already handles by rendering a dedicated + // instead of a ChatMessage at all (deletedText is that component's own prop, not this one's -- + // there's no equivalent plumbed in here). Recalled messages still go through ordinary ChatMessage, + // so without this branch a null content fell all the way through to the generic "invalid content" + // case below -- confirmed live: recalling a message left the *other* party's chat showing a bare + // "⚠ invalid content" (无效内容) row where "消息已删除" belongs (the recalling side just drops the + // row from its own list locally, unaffected by this). Reuses the exact same i18n id/defaultMessage + // messages-view.jsx's `messages.message_deleted` already registers (translated zh string: + // "消息已删除"), not a new string. `content == null` (not `===`) intentionally also catches + // `undefined`, same as every other falsy-content branch here. + content = <>block + ; } else { content = <>warning_amber