From 68044fa1bbc5b7254ee826ddc9bb8b44f3564ac0 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 6 Sep 2026 05:30:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D"=E6=9F=A5=E6=89=BE=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E8=AE=B0=E5=BD=95"=E8=B7=B3=E8=BD=AC=E5=AE=9A?= =?UTF-8?q?=E4=BD=8D=E5=88=B0=E5=8E=9F=E6=B6=88=E6=81=AF=E5=9C=A8=E5=85=A8?= =?UTF-8?q?=E9=83=A8=E5=85=A5=E5=8F=A3100%=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/views/messages-view.jsx | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/views/messages-view.jsx b/src/views/messages-view.jsx index fe446595..eb48a1ab 100644 --- a/src/views/messages-view.jsx +++ b/src/views/messages-view.jsx @@ -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(_ => {});