diff --git a/web/src/lib/tinode/tinode-sdk.d.ts b/web/src/lib/tinode/tinode-sdk.d.ts index c519d8e8..3a37942b 100644 --- a/web/src/lib/tinode/tinode-sdk.d.ts +++ b/web/src/lib/tinode/tinode-sdk.d.ts @@ -302,6 +302,10 @@ declare module 'tinode-sdk' { getMeTopic(): MeTopic; getTopic(topicName: string): Topic; + /** Iterates EVERY locally-cached topic (any type, including `me`/`fnd`), unlike `MeTopic.contacts()` + * which additionally filters by `isCommType()` -- see topics.ts's `getChatListSnapshot` for why + * that filter can't be trusted for this backend's P2P topics. */ + mapTopics(callback: (topic: Topic, key: string) => void): void; account(userId: string, scheme: string, secret: string, login: boolean, desc?: unknown): Promise; createAccountBasic( diff --git a/web/src/lib/tinode/topics.ts b/web/src/lib/tinode/topics.ts index b958211b..4cf37d03 100644 --- a/web/src/lib/tinode/topics.ts +++ b/web/src/lib/tinode/topics.ts @@ -253,23 +253,51 @@ export function ensureTopicAttached(topicName: string): Promise { let chatListSnapshot: Contact[] | null = null; +/** + * Real, addressable chat-list topic types this backend actually issues -- deliberately NOT using + * `Topic#isCommType()`/`isP2PType()` (confirmed live by reading tinode-sdk's own `Topic.topicType()` + * table: it maps prefix `'usr'` to P2P, with NO entry for `'p2p'` at all). That table is right for a + * peer ALIAS (`usr`, used before a P2P topic has ever been subscribed), but this backend's + * REAL, server-assigned P2P topic name is `p2p` (confirmed via raw `{meta topic:"me"}` capture: + * every subscription the server reports uses this form, never the alias) -- so `isCommType()` returns + * false for every P2P topic once its real name is known, silently filtering ALL of them out of + * `MeTopic.contacts()`. This has nothing to do with account age or how a conversation was opened -- + * verified by direct fresh-login inspection that even a heavily-used account with many real, working + * P2P friendships had ZERO P2P entries in `contacts()`; every P2P row the chat list has ever shown + * this session was actually the special-cased `slf` self-chat entry. + */ +function isChatListTopicName(name: string): boolean { + return name.startsWith('p2p') || name.startsWith('grp') || name === 'slf'; +} + /** * Cached the same way getMessagesSnapshot is, but invalidated by ANY topic's change (a new message * on a contact that isn't currently open still needs to move that row and refresh its preview). * Triggers each contact's one-shot preview prefetch as a side effect of being read -- cheap and * idempotent (prefetchPreview no-ops after the first call per topic), so it's fine to do this on * every rebuild rather than needing a separate "wire up previews" pass. + * + * Uses `Tinode#mapTopics` (every locally-cached topic, unfiltered) + `isChatListTopicName` instead of + * `MeTopic#contacts()` -- see that function's own comment for why `contacts()`'s built-in filter can't + * be trusted here. `mapTopics` still only sees topics the SDK has locally cached, which `me`'s own + * subscribe (`subscribeMeTopic`'s `withLaterSub()`) and any direct `openConversation()` both populate. */ export function getChatListSnapshot(): Contact[] { if (chatListSnapshot) return chatListSnapshot; const byTopic = new Map(); - getTinode() - .getMeTopic() - .contacts((contact) => { - if (!contact.topic) return; - prefetchPreview(contact.topic); - byTopic.set(contact.topic, contact); - }); + getTinode().mapTopics((topic) => { + if (!topic.name || !isChatListTopicName(topic.name)) return; + prefetchPreview(topic.name); + // `.topic` (what every consumer of this list actually reads) normally arrives via + // `_processMetaSubs` merging the server's own `sub.topic` field onto this same cached instance -- + // but a brand-new P2P topic reached through `openConversation()` is cached here the moment its + // OWN subscribe resolves, potentially before `me`'s subscription list has ever reported it. Set + // explicitly rather than assuming that merge already happened; kept as the same live instance + // (not a clone) so prototype methods like `latestMessage()` survive. + const contact = topic as unknown as Contact; + contact.topic = topic.name; + byTopic.set(topic.name, contact); + }); const rows = [...byTopic.values()]; chatListSnapshot = rows; return rows;