From a964d51ba5b15ccf1440cb6245472f3f5edff687 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 6 Sep 2026 13:32:59 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9B=9E=E5=A4=8D/=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E6=B6=88=E6=81=AF=E8=A7=A6=E5=8F=91=E9=A3=8E=E6=8E=A7?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E6=97=B6=EF=BC=8C"=E6=AD=A3=E5=9C=A8?= =?UTF-8?q?=E5=9B=9E=E5=A4=8D"=E9=A2=84=E8=A7=88=E6=9D=A1=E5=9C=A8?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E7=BB=93=E6=9E=9C=E5=87=BA=E6=9D=A5=E5=89=8D?= =?UTF-8?q?=E5=B0=B1=E8=A2=AB=E6=B8=85=E7=A9=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象:对一条历史消息点"回复",输入含链接的文本点发送,风控弹窗(安全提醒/警示级) 刚弹出的同时,composer 里"正在回复 XX"的预览条就已经消失了;如果这时候点"取消发送", 这条回复就被永久放弃——原来引用的消息内容和回复关系不再存在,也没有任何提示,唯一的 恢复办法是回到那条历史消息重新点一次"回复"。编辑消息(修改一条已发消息的内容后再次 包含链接)是同一根因的同款问题,只是编辑可以靠重新点"编辑"找回原文,没有回复那么彻底 不可逆。 根因:sendMessage() 原来在根据 this.state.reply / this.props.forwardMessage 组装好 msg/head 之后,同步、无条件地立刻调用了 this.handleCancelReply()(清空 reply/编辑 状态),这一步发生在下面的 URL 风控检查(POST /security/urls/check)之前——但风控 弹窗要等用户点"继续发送"或"取消发送"才会真正决定要不要走 doSend(),这中间的等待期里 UI 状态已经被提前清空了。 修法:把 handleCancelReply() 从三处提前调用点移到 doSend() 内部、真正调用 this.props.sendMessage(...) 之前。这样它只在真的要发送时才执行——不含链接的普通消息 里 doSend() 本来就是同步立即执行,时机与原来完全一样;命中风控的消息里,"继续发送" 时才会触发(此时清空回复状态是正确的,消息确实发出去了),"取消发送"时 doSend() 根本不会被调用,回复/编辑的预览条和引用内容都会完整保留,可以直接修改文本重试或 自己手动取消。转发场景不受影响:转发消息不会触发这条风控检查(originalText 恒为 null),移动调用时机对它是纯粹的时序等价替换。 验证:真实浏览器操作,对一条消息点"回复",输入含 https://example.com/a?token=abc123 的文本发送——警示弹窗弹出的同时预览条仍然可见;点"取消发送"后弹窗消失、预览条依旧 完整保留、composer 文本不变;再次点发送、这次点"我已知晓,继续发送",消息正确以 带引用卡片的回复形式发出,预览条在发送成功后才清空。 Co-Authored-By: Claude Sonnet 5 --- src/views/messages-view.jsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/views/messages-view.jsx b/src/views/messages-view.jsx index e8c229d6..1e91548b 100644 --- a/src/views/messages-view.jsx +++ b/src/views/messages-view.jsx @@ -1525,7 +1525,6 @@ class MessagesView extends React.Component { // We are forwarding a message. msg = this.props.forwardMessage.msg; head = this.props.forwardMessage.head; - this.handleCancelReply(); } else if (this.state.reply) { if (this.state.reply.editing) { if (msg == this.state.contentToEdit) { @@ -1545,7 +1544,6 @@ class MessagesView extends React.Component { } msg = Drafty.append(Drafty.sanitizeEntities(this.state.reply.content), msg); } - this.handleCancelReply(); } else if (typeof msg == 'string' && this.mentionedUsers && this.mentionedUsers.length) { // 4.2-4.3 @提醒:Drafty.parse 本来就会把纯文本里的"@word"自动识别成 MN 实体,但 // data.val 存的是匹配到的原始文字(tinode.dev.js 的 ENTITY_TYPES 表,pack: val => @@ -1584,6 +1582,19 @@ class MessagesView extends React.Component { // the same backend version=0-skips-the-lock issue explained in handleComposerChange above. const draftVersionAtSend = this.state.draftVersion || 0; const doSend = () => { + // Was called eagerly above (right after building `msg`/`head` from `this.state.reply`/ + // `this.props.forwardMessage`), synchronously and unconditionally -- including on a message + // that turns out to need the URL risk dialog below, which only resolves *after* this function + // already returned. That meant clicking send on a reply/edit containing a link made the + // "回复"/编辑 preview bar vanish immediately, before the user had even seen the risk prompt; + // clicking "取消发送" then left the reply/edit permanently abandoned -- the quoted message was + // gone from the composer with no way back except manually replying again, even though the user + // only meant to cancel sending the flagged text, not the reply itself. Moving the call here + // means it only fires at the point a send is actually going to happen: immediately for the + // common non-URL case (this function runs synchronously right away, same timing as before), + // or deferred until "继续发送" for a flagged one -- and never at all if the dialog is cancelled, + // so the reply/edit state and its preview bar correctly survive a cancelled risk prompt. + this.handleCancelReply(); const result = this.props.sendMessage(msg, uploadCompletionPromise, uploader, head); // Only a plain-text compose (not a reply/edit/forward, all of which pass a Drafty object or // reuse contentToEdit rather than the persisted draft) has a server-side draft worth clearing.