修复已建立的P2P会话详情栏/头像404:话题名不是对方user_id

用户报告:控制台反复出现 GET /v1/im/users/p2pp77kUAkgqpVbZoiTpjPWtg/profile
404。根因:这个应用的多处代码(DetailColumn的好友资料/拉黑/备注/删除好友,以及
本会话早前刚加的头像peerProfileQuery)一直假设"P2P会话的topicId就是对方的
user_id"——这个假设只在"第一次用对方usr...地址打开、还从未真正subscribe过"
这个短暂窗口内成立。读tinode-sdk源码确认:一旦subscribe成功,SDK会把这个话题
从usr...地址重命名为服务端分配的真实p2p...哈希名,而Topic.topicType()自己的
前缀表里根本没有p2p这一项(只有usr映射到P2P类型),导致topic.isP2PType()/
topic.p2pPeerDesc()对已建立的会话恒定返回false/undefined——SDK自己内建的"取
对方身份"方法在这个场景下也是坏的,不只是这个项目自己的代码有问题。

只要一个P2P会话有过真实的消息历史(几乎所有现存对话都是这样),它的topicId
就是这个不能反解的p2p...哈希,而不是对方user_id,直接拿去查/v1/im/users/{id}/
profile、匹配好友列表的user_id、传给拉黑/删除好友接口,全部会用错id。

修复:topics.ts新增getP2PPeerUserId(topic, myUserId)——topicId本身是usr...
前缀时直接返回(还没subscribe过,本来就对),否则读topic.subscribers()真实
的订阅者列表,找出不是自己的那个人的真实user_id;订阅者列表还没加载出来时返回
undefined而不是猜一个不可能对的值,调用方原本就有的enabled门控会正确地等待
而不是提前拿错误id发请求。tinode-sdk.d.ts补充subscribers()方法和Subscription
类型声明(原来完全没声明这个方法)。DetailColumn.tsx/ConversationView.tsx改用
这个真实user_id去查资料/匹配好友/头像取色,不再直接假设topicId能用。

实机验证:真实会话grpMc5uN...不对,是p2pp77kUAkgqpVbZoiTpjPWtg(systest04↔
systest02,有真实历史消息的老会话)——修复前详情栏"ID:"整行空白、404貫穿控制
台;修复后详情栏正确显示"ID:usrp77kUAkgqpU",好友资料/备注/拉黑状态全部正确
关联到真实好友记录。
This commit is contained in:
Your Name
2026-09-17 12:52:44 +08:00
parent 425b06aa71
commit 50767f701f
4 changed files with 97 additions and 12 deletions
+26
View File
@@ -33,6 +33,18 @@ declare module 'tinode-sdk' {
params?: Record<string, unknown>;
}
/** One entry from `Topic#subscribers()` -- fields confirmed live against a real P2P topic (only
* `user` is actually consumed anywhere in this app so far; the rest are declared for completeness). */
export interface Subscription {
user: string;
online?: boolean;
updated?: Date;
read?: number;
recv?: number;
acs?: { want?: string; given?: string; mode?: string };
public?: TopicDescPublic | null;
}
export interface CredentialParam {
meth: string;
val?: string;
@@ -191,6 +203,20 @@ declare module 'tinode-sdk' {
isSelfType(): boolean;
getAccessMode(): AccessMode | undefined;
/**
* Iterates this topic's cached subscriber list, one `Subscription` per call -- for a P2P topic
* this is always exactly the two participants (self included), confirmed live. This is the only
* reliable way to recover the OTHER party's real `usr...` id once a P2P topic has been subscribed
* at least once: the SDK renames a freshly-addressed `usr...` topic to a real, opaque `p2p...`
* name the moment `subscribe()` succeeds (see topics.ts's `openConversation`), and -- confirmed by
* reading tinode.dev.js -- `Topic.topicType()`'s own prefix table has no `p2p` entry at all (only
* `usr` maps to the P2P type), so `isP2PType()`/`p2pPeerDesc()` silently stop working the instant
* a topic has this real name. `topic.name` itself is NOT a usable substitute for the peer's id
* past that point -- anything needing "the other user" (profile lookup, friends-list matching,
* block/remark) must go through this method instead.
*/
subscribers(callback: (sub: Subscription) => void): void;
/**
* Iterates locally-cached messages in `[sinceId, beforeId]` (both omitted = everything cached).
* `callback(msg, prevMsg, nextMsg, bufferIndex)` fires once per message, already resolved to the
+31
View File
@@ -38,6 +38,37 @@ export function getActiveConversationTopic(): string | null {
return activeConversationTopic;
}
/**
* The other party's real `usr...` id for a P2P conversation -- NOT the same as `topic.name` once a
* conversation has any real history. Real, live-confirmed bug this fixes: a P2P topic addressed by
* the peer's own `usr...` id (the only address that exists before the very first subscribe -- e.g.
* straight from a friend's profile) gets renamed by the SDK to a real, opaque `p2p...` name the
* instant `subscribe()` succeeds (see openConversation's own doc comment on this same rename). Every
* P2P conversation with actual history is addressed by this `p2p...` form from then on -- but reading
* tinode.dev.js confirms `Topic.topicType()`'s prefix table has no `p2p` entry at all (only `usr` maps
* to the P2P type), so `topic.isP2PType()`/`topic.p2pPeerDesc()` silently return false/undefined for
* every one of these topics, and code that assumed "the P2P topic name IS the peer's user id" (true
* only for the brief pre-first-subscribe window) breaks the moment that assumption stops holding --
* confirmed live: `GET /users/{topicName}/profile` 404s, `friends.find(f => f.user_id === topicName)`
* never matches, block/remark/avatar all silently target a user id that doesn't exist.
*
* Returns `undefined` (never `topic.name` as a guess) when the subscriber list isn't populated yet --
* live-confirmed this matters: on the render right after navigating straight to a `p2p...`-named
* conversation, `subscribers()` can still be empty for the one or two renders before the subscribe
* response fills it in, and returning `topic.name` as a placeholder there just fires the exact same
* doomed `GET /users/{p2p...}/profile` request this function exists to prevent, a render later. Every
* caller already guards its query with `enabled: !!peerUserId`, so `undefined` correctly makes it wait
* instead of firing once with a value it knows is wrong.
*/
export function getP2PPeerUserId(topic: Topic, myUserId: string | null | undefined): string | undefined {
if (topic.name.startsWith('usr')) return topic.name;
let peer: string | undefined;
topic.subscribers((sub) => {
if (!peer && sub.user !== myUserId) peer = sub.user;
});
return peer;
}
const confirmedReadSeq = new Map<string, number>();
/**