From ff69e4548c1ad6569017106b781610d370333d3b Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 16 Sep 2026 13:41:26 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DP2P=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E5=AE=8C=E5=85=A8=E4=B8=8D=E5=87=BA=E7=8E=B0=E5=9C=A8=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E5=88=97=E8=A1=A8=E3=80=81=E6=A0=87=E9=A2=98=E6=A0=8F?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=8E=9F=E5=A7=8BID+=E7=A6=BB=E7=BA=BF?= =?UTF-8?q?=E7=9A=84=E6=A0=B9=E5=9B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:tinode-sdk自带的Topic.topicType()前缀表只认'usr'为P2P类型前缀,没有 'p2p'这个条目,而本后端真实分配的P2P话题名是`p2p`形式——导致 isCommType()对每一个P2P会话都恒定返回false,MeTopic.contacts()因此把所有 单聊会话过滤掉。用真实账号实测确认这不是新会话/顶号账号特有的问题:任何 账号的任何P2P好友关系都受影响,只是群聊和"slf"自聊天不受影响所以此前一直 没被发现。 聊天列表改用Tinode#mapTopics()(不做类型过滤的原始遍历)+ 本项目自己正确 的前缀判断(p2p/grp/slf),不再依赖SDK自身的isCommType()。 实测(e2etester1/e2etester2真实新建好友关系,此前从未聊过天):修复前 标题栏显示原始topic hash+"离线"(对方实际在线)、侧边栏"暂无会话"(消息 其实已正确送达,只是不可见);修复后标题栏正确显示对方昵称+"在线",侧边栏 正确出现该会话及消息预览。 --- web/src/lib/tinode/tinode-sdk.d.ts | 4 +++ web/src/lib/tinode/topics.ts | 42 +++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 7 deletions(-) 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;