diff --git a/src/views/messages-view.jsx b/src/views/messages-view.jsx index c7fc153b..fe446595 100644 --- a/src/views/messages-view.jsx +++ b/src/views/messages-view.jsx @@ -3,7 +3,7 @@ import React, { Suspense } from 'react'; 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')); 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 // (消息页面—长按弹出.png) lists "群撤回" as its own, always-shown menu item alongside "撤回" -- // 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 canRecallAsAdmin = topic.isGroupType() && topic.getAccessMode() && topic.getAccessMode().isAdmin(); - if ((params.userFrom == this.props.myUserId || canRecallAsAdmin) && params.seq > 0 && recallWindow > 0 && - params.timestamp && params.timestamp.getTime() > (new Date().getTime() - recallWindow * 1000)) { + if ((params.userFrom == this.props.myUserId || canRecallAsAdmin) && params.seq > 0 && params.seq < LOCAL_SEQID && + recallWindow > 0 && params.timestamp && params.timestamp.getTime() > (new Date().getTime() - recallWindow * 1000)) { menuItems.push('menu_item_recall'); } // Self -> (1) always hard-delete with [Delete] title.