From 2ee8bac6a012026f01d4c83bcfaed9ada1a0b1fb Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 6 Sep 2026 10:16:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B6=88=E6=81=AF=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E4=BC=9A=E8=AF=9D=E9=A2=84=E8=A7=88=E6=81=92=E4=B8=BA?= =?UTF-8?q?"=E6=9A=82=E6=97=A0=E6=B6=88=E6=81=AF"=EF=BC=8C=E5=8D=B3?= =?UTF-8?q?=E4=BD=BF=E4=BC=9A=E8=AF=9D=E6=9C=89=E7=9C=9F=E5=AE=9E=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit message-center-view.jsx 的预览文字读 topic.latestMessage(),这个方法 只会从话题自己的本地消息缓存里取值——而这个缓存只有在话题真正被 subscribe 过一次数据拉取后才会填充。me.contacts()(resetContactList) 给出的是每个联系人真实的、已缓存的Topic实例,包括用户这次会话里 从未打开过的——这些话题的latestMessage()恒为undefined,即使同一个 对象自己的touched/seq字段能证明服务端真实存在历史消息。 新增prefetchLatestMessagePreviews():对chatList里seq>0但本地缓存为空 的话题,一次性拉取最新1条消息填充预览。用一个Set跟踪已尝试过的话题, 避免resetContactList()(几乎每个{meta}/{pres}事件都会触发一次)反复 重新拉取同一个话题。 调试过程中发现并修正了自己犯的两个错误:(1) resetContactList()在 websocket握手完成前就会被本地持久化缓存恢复触发一次,此时的subscribe 必然503失败,加了isConnected()判断跳过,避免过早失败把话题永久标记 为"已尝试";(2) Topic.subscribe(getParams, setParams)的参数顺序搞反, 把查询对象传成了第二个参数(setParams),导致subscribe成功但发出的 {sub}包完全没有get子句,从未真正请求过消息数据。 已用真实浏览器验证:好友kcg051307会话(真实71条历史消息)刷新页面 后预览从"暂无消息"变为真实的最新消息内容,未读角标数字不受影响 (确认没有误将未读消息标记为已读)。 Co-Authored-By: Claude Sonnet 5 --- src/views/tinode-web.jsx | 48 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/views/tinode-web.jsx b/src/views/tinode-web.jsx index 3f2d917b..83ab86b5 100644 --- a/src/views/tinode-web.jsx +++ b/src/views/tinode-web.jsx @@ -216,6 +216,17 @@ class TinodeWeb extends React.Component { this.selfRef = React.createRef(); + // message-center-view.jsx's conversation-list preview reads topic.latestMessage(), which the + // vendored SDK only ever populates from a topic's own local message cache -- itself only filled + // in once that specific topic has actually been subscribed with a data fetch. me.contacts() + // (resetContactList below) yields the real, already-cached Topic instances for every contact -- + // including ones the user has never opened this session -- so most rows fall straight through to + // the '暂无消息' fallback despite `touched`/`seq` on the same object proving real history exists + // server-side. This tracks which topics we've already kicked off a one-shot "just the last + // message" fetch for, so repeated resetContactList() calls (it fires on nearly every {meta}/{pres} + // event, not just once at boot) don't re-subscribe the same topic over and over. + this.previewPrefetchAttempted = new Set(); + this.state = this.getBlankState(); this.handleResize = this.handleResize.bind(this); @@ -1284,6 +1295,8 @@ class TinodeWeb extends React.Component { } }); + this.prefetchLatestMessagePreviews(newState.chatList); + const past = new Date(0); newState.chatList.sort((a, b) => { // Pinned topics take precedence. @@ -1297,6 +1310,41 @@ class TinodeWeb extends React.Component { this.setState(newState); } + // See the comment on this.previewPrefetchAttempted in the constructor for why this exists. + // seq > 0 is the same 'does this topic actually have any messages at all' signal + // message-center-view.jsx would otherwise have no way to distinguish from 'genuinely empty' -- + // skip topics with no history instead of firing a subscribe that would just come back empty. + // + // !this.tinode.isConnected() live repro: resetContactList() fires as soon as the persisted local + // cache restores 'me's last-known contact list (log line "Persistent cache initialized"), which is + // *before* the websocket has even sent {hi} -- every subscribe() call made at that point instantly + // rejects with a 503 "Websocket is not connected". Skip attempting entirely while disconnected + // (rather than catching the failure) so these topics *aren't* added to previewPrefetchAttempted yet + // -- resetContactList() runs again moments later once the real {meta topic:'me'} response arrives + // over the now-live connection, and this gets a genuine second try then. + prefetchLatestMessagePreviews(chatList) { + if (!this.tinode.isConnected()) { + return; + } + for (const c of chatList) { + if (!c.topic || c.seq <= 0 || c.latestMessage() || this.previewPrefetchAttempted.has(c.topic)) { + continue; + } + this.previewPrefetchAttempted.add(c.topic); + // Topic.subscribe(getParams, setParams) -- the query is the *first* argument. Passing it + // second (as setParams) was silently swallowed: the resulting {sub} packet had no `get` + // clause at all (confirmed live: `out: {"sub":{"id":...,"topic":"..."}}`, no `get` key), so + // subscribe succeeded but never actually requested -- let alone received -- any message data. + c.subscribe(c.startMetaQuery().withLaterData(1).build()).catch(err => { + // Leave it in previewPrefetchAttempted either way -- a topic that fails once we're + // definitely connected (e.g. a stale contact the user no longer has access to) isn't going + // to succeed on the next resetContactList() a moment later either, and would otherwise retry + // on every single {meta}/{pres} event for the rest of the session. + console.warn(`Failed to prefetch latest message for '${c.topic}'`, err); + }); + } + } + static checkIfPinsUpdated(me, chatList) { for (let i = 0; i < chatList.length; i++) { const c = chatList[i];