修复消息发送失败/排队中时长按菜单仍提供"撤回"

判断只查 params.seq > 0,未排除 tinode-sdk 本地占位 seq(LOCAL_SEQID,
268435455,同样是正数)——发送失败/仍在排队的消息会被误判成"已持久化
的真实消息",点击撤回后端正确拒绝(无此消息),却给用户一个答非所问
的"请求内容有误"提示。

第十一轮最小粒度测试 Ch3-4 发现。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-06 02:13:34 +08:00
co-authored by Claude Sonnet 5
parent 89fdd31cd4
commit fc757cc043
+15 -3
View File
@@ -3,7 +3,7 @@
import React, { Suspense } from 'react'; import React, { Suspense } from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { Drafty, Tinode, TheCard } from 'tinode-sdk'; import { Drafty, Tinode, TheCard, LOCAL_SEQID } from 'tinode-sdk';
const CallPanel = React.lazy(_ => import('../widgets/call-panel.jsx')); const CallPanel = React.lazy(_ => import('../widgets/call-panel.jsx'));
import ChatMessage from '../widgets/chat-message.jsx'; import ChatMessage from '../widgets/chat-message.jsx';
@@ -1088,10 +1088,22 @@ class MessagesView extends React.Component {
// allowAdmin=true, so the backend has always accepted this; the reference screenshot // allowAdmin=true, so the backend has always accepted this; the reference screenshot
// (消息页面—长按弹出.png) lists "群撤回" as its own, always-shown menu item alongside "撤回" -- // (消息页面—长按弹出.png) lists "群撤回" as its own, always-shown menu item alongside "撤回" --
// this was previously never offered at all since the only condition checked was authorship. // this was previously never offered at all since the only condition checked was authorship.
//
// `params.seq > 0` alone is not enough to mean "a real, server-persisted message": Tinode
// SDK gives every message that hasn't been confirmed by the server yet (still sending, or
// failed outright) a local placeholder seq of LOCAL_SEQID (0xFFFFFFF = 268435455) -- a large
// *positive* number, so it passed this check. Round 11 live testing reproduced this: sending
// a message into a since-blocked P2P topic left a bubble stuck in "发送失败" with seq
// 268435455, and this menu still offered "撤回" for it -- clicking it fired
// POST /topics/.../messages/268435455/recall, which the server correctly rejects (no such
// message), surfacing a confusing "请求内容有误,请稍后重试" toast for an action that should
// never have been offered. `seq < LOCAL_SEQID` excludes exactly that placeholder range,
// matching the same bound the SDK itself uses internally to distinguish a confirmed seq from
// a queued one.
const recallWindow = this.state.messagePolicy ? this.state.messagePolicy.recall_window_seconds : 0; const recallWindow = this.state.messagePolicy ? this.state.messagePolicy.recall_window_seconds : 0;
const canRecallAsAdmin = topic.isGroupType() && topic.getAccessMode() && topic.getAccessMode().isAdmin(); const canRecallAsAdmin = topic.isGroupType() && topic.getAccessMode() && topic.getAccessMode().isAdmin();
if ((params.userFrom == this.props.myUserId || canRecallAsAdmin) && params.seq > 0 && recallWindow > 0 && if ((params.userFrom == this.props.myUserId || canRecallAsAdmin) && params.seq > 0 && params.seq < LOCAL_SEQID &&
params.timestamp && params.timestamp.getTime() > (new Date().getTime() - recallWindow * 1000)) { recallWindow > 0 && params.timestamp && params.timestamp.getTime() > (new Date().getTime() - recallWindow * 1000)) {
menuItems.push('menu_item_recall'); menuItems.push('menu_item_recall');
} }
// Self -> (1) always hard-delete with [Delete] title. // Self -> (1) always hard-delete with [Delete] title.