修复P2P会话完全不出现在聊天列表、标题栏显示原始ID+离线的根因

根因:tinode-sdk自带的Topic.topicType()前缀表只认'usr'为P2P类型前缀,没有
'p2p'这个条目,而本后端真实分配的P2P话题名是`p2p<hash>`形式——导致
isCommType()对每一个P2P会话都恒定返回false,MeTopic.contacts()因此把所有
单聊会话过滤掉。用真实账号实测确认这不是新会话/顶号账号特有的问题:任何
账号的任何P2P好友关系都受影响,只是群聊和"slf"自聊天不受影响所以此前一直
没被发现。

聊天列表改用Tinode#mapTopics()(不做类型过滤的原始遍历)+ 本项目自己正确
的前缀判断(p2p/grp/slf),不再依赖SDK自身的isCommType()。

实测(e2etester1/e2etester2真实新建好友关系,此前从未聊过天):修复前
标题栏显示原始topic hash+"离线"(对方实际在线)、侧边栏"暂无会话"(消息
其实已正确送达,只是不可见);修复后标题栏正确显示对方昵称+"在线",侧边栏
正确出现该会话及消息预览。
This commit is contained in:
Your Name
2026-09-16 13:41:26 +08:00
parent 29a1f1c2fd
commit ff69e4548c
2 changed files with 39 additions and 7 deletions
+4
View File
@@ -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<unknown>;
createAccountBasic(
+35 -7
View File
@@ -253,23 +253,51 @@ export function ensureTopicAttached(topicName: string): Promise<Topic> {
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<peerId>`, used before a P2P topic has ever been subscribed), but this backend's
* REAL, server-assigned P2P topic name is `p2p<hash>` (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<string, Contact>();
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;