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];