修复"查找聊天记录"跳转定位到原消息在全部入口100%失效

componentDidUpdate里的jumpToSeq检查只在这个值发生变化时触发,但
tinode-web.jsx从不会同时渲染MessagesView和第7章侧边栏(!chapterSevenActive
门禁),所以从History的搜索结果/图片宫格/视频宫格跳转,每次都是卸载
第7章视图、挂载一个"首次渲染就已经带着jumpToSeq"的全新MessagesView
实例——componentDidUpdate根本不会跑(它不处理首次挂载)。7.12/7.15/
7.16全部"定位到消息来源"入口100%失效:jumpseq参数永久留在URL里,
会话固定停在最底部。

修复过程中还发现一个真实的竞态:首次挂载意味着componentSetup()刚刚
才发起topic订阅(异步、fire-and-forget),jumpToMessage自己的
topic.getMeta()调用要求订阅已建立,立即调用会抢在订阅完成前——实测
这个抢跑的getMeta()调用真实收到了服务端 {"code":403,"text":"permission
denied"} 拒绝(不是本地兜底,是真实的服务端往返),而真正的订阅请求
当时还在路上。改为轮询已有的isSubscribed()标志(而不是猜一个固定延迟
或改造subscribe()暴露其内部promise),封顶5秒,真正卡住的订阅仍会
落到jumpToMessage自己已有的错误处理上。

第十二轮最小粒度测试 Ch7 发现,已用真实浏览器验证:从工作台等其它
页面跳转过来时,消息正确加载、jumpseq参数正确从URL清除。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-06 05:30:53 +08:00
co-authored by Claude Sonnet 5
parent 6882103461
commit 68044fa1bb
+39
View File
@@ -288,6 +288,45 @@ class MessagesView extends React.Component {
this.componentSetup({}, {});
// Ch.7: jump to a message found via chat-history search/media grid (content-library-view.jsx's
// History) -- the componentDidUpdate check a few lines below only fires on a *change* to
// jumpToSeq while this component stays mounted, but tinode-web.jsx never renders MessagesView
// and a Ch.7 side panel at the same time (`!chapterSevenActive` gates it) -- every jump from
// History's search results/image grid/video grid therefore unmounts the Ch.7 view and mounts a
// *brand new* MessagesView instance with jumpToSeq already set on its very first render, which
// componentDidUpdate never sees (it doesn't run for the initial mount). Confirmed live: the
// seq stayed in the URL indefinitely (jumpToMessage's own `removeUrlParam` call never fired)
// and the conversation opened at the bottom instead of scrolling to the found message --
// 100% reproducible for every Ch.7 "定位到消息来源" entry point, not an intermittent timing issue.
if (this.props.jumpToSeq) {
const seq = parseInt(this.props.jumpToSeq, 10);
const topicObj = this.props.tinode && this.props.topic ? this.props.tinode.getTopic(this.props.topic) : null;
const attemptJump = () => this.jumpToMessage(seq);
// A first-ever mount also means `componentSetup()` just above only *started* subscribing
// this topic (subscribe() -> topic.subscribe() is async, fire-and-forget from here) --
// jumpToMessage's own topic.getMeta() call requires an already-established subscription,
// and firing immediately races ahead of it: confirmed live, this raced getMeta() call came
// back as a real `{"code":403,"text":"permission denied"}` control message (not a local
// guard, an actual rejected server round trip) while the real subscribe's own request was
// still in flight one id apart. Poll the already-exposed isSubscribed() flag rather than
// guessing a fixed delay or reworking subscribe()'s fire-and-forget call sites to expose
// their promise; capped at 5s so a genuinely stuck subscribe still falls through to
// jumpToMessage's own existing error handling instead of waiting forever.
if (topicObj && !topicObj.isSubscribed()) {
let tries = 0;
const waitForSubscription = () => {
if (topicObj.isSubscribed() || ++tries > 25) {
attemptJump();
} else {
window.setTimeout(waitForSubscription, 200);
}
};
waitForSubscription();
} else {
attemptJump();
}
}
getBootstrap().then(data => {
this.setState({messagePolicy: data.message_policy || null});
}).catch(_ => {});