修复好友个人主页"发消息/音视频通话"对零消息历史的新好友必现"消息已禁用"

根因:Profile.openChat() 一直只调用 onTopicSelected(tinode-web.jsx 的 handleTopicSelected),
这个函数只负责"切换到某个已知 topic",从不发送原生 Tinode {sub} 请求。对一个从未被任何一方
订阅过的全新 P2P 会话(刚通过好友验证、双方都还没发过消息),本地没有缓存的 topic 订阅信息,
messages-view.jsx 的 isWriter 因此永远停在初始值 false,输入框被替换成"消息已禁用"提示且没有
任何入口能发出第一条消息——对每一个"刚加好友、还没聊过天"的真实场景都 100% 必现,不是边缘情况。

tinode-web.jsx 里其实已经有正确处理这个情况的 handleStartTopicRequest(通过 onCreateTopic prop
暴露):判断 topic 是否已缓存,没有缓存就先设置 sub.mode 为完整权限再切换,已缓存则直接退化成
普通的 handleTopicSelected,两种情况都能正确处理。问题是 sidepanel-view.jsx 渲染
ContactManagementView(好友个人主页/群成员资料页所在文件)时,只透传了 onTopicSelected,
从未把 onCreateTopic 一起传下去,导致 Profile 组件即使想调用正确的入口也拿不到。

修复:sidepanel-view.jsx 补上 onCreateTopic 的透传;contact-management-view.jsx 的
ContactManagementView/Profile 都新增接收这个 prop,openChat 优先调用 onCreateTopic
(不可用时兜底回退到原来的 onTopicSelected,避免类型不匹配报错)。

验证:用两个全新注册、互加好友但零消息历史的真实账号(ch7test1/ch7test2)在浏览器里复现——
修复前点"发消息"进入会话后输入框区域固定显示"消息已禁号"且没有任何输入控件,WS 日志确认
自始至终没有对该 p2p topic 发过 {sub};修复后点"发消息"能看到真实的 {sub} 请求(带
acs.mode: JRWPA)、输入框正常出现,并成功发送/收到多条真实文本消息。
This commit is contained in:
Your Name
2026-09-06 16:38:46 +08:00
parent 4773ee5f64
commit 0d2637861d
2 changed files with 16 additions and 5 deletions
+14 -4
View File
@@ -406,7 +406,7 @@ function Verification({uid, onToast}) {
</main></div>;
}
function Profile({uid, friend, onTopicSelected, onToast}) {
function Profile({uid, friend, onTopicSelected, onCreateTopic, onToast}) {
const [profile, setProfile] = React.useState(() => (uid ? readCachedData('profile-seed:' + uid) : null));
// A failed fetch with no seed to fall back on previously left this stuck on "加载中…" forever --
// tracked separately from `profile` itself (which legitimately stays null while genuinely still
@@ -478,7 +478,17 @@ function Profile({uid, friend, onTopicSelected, onToast}) {
// Real native Tinode p2p topic name (from my side) is just the peer's own user_id ("usrXXXX") --
// NOT `profile.topic` (a separate "p2pXXXX" identifier the custom REST layer uses internally,
// which Tinode.topicType() doesn't recognize, so selecting it left the message pane blank).
const openChat = () => onTopicSelected(uid);
//
// Must go through onCreateTopic (tinode-web.jsx's handleStartTopicRequest), not the plain
// onTopicSelected -- confirmed live: a freshly-accepted friend with zero prior message history has
// no locally cached p2p topic yet, and onTopicSelected (handleTopicSelected) only *selects* a topic,
// it never sends the native {sub} request that actually creates/joins it. messages-view.jsx's own
// isWriter stays stuck at its default `false` (no ACS ever arrives because nothing subscribed),
// permanently showing "消息已禁用" with no compose box and no way to send the first message --
// 100% reproducible for every real "just became friends, say hi" case, not an edge case.
// handleStartTopicRequest already guards the case this topic *does* have history (isTopicCached),
// delegating straight to handleTopicSelected then, so this is safe to call unconditionally.
const openChat = () => (onCreateTopic || onTopicSelected)(uid);
return <div className="people-page"><Header title="个人资料" back={friend ? 'address-book' : 'chat-standard'}
right={isFriend ? '设置' : null} onRight={() => navigate('friend-settings', uid)} />
<main className="people-profile">
@@ -659,7 +669,7 @@ function ConfirmSheet({type, uid, tinode, onToast}) {
</div>;
}
export default function ContactManagementView({view, hashParams, myUserId, tinode, onTopicSelected, onOpenScanner}) {
export default function ContactManagementView({view, hashParams, myUserId, tinode, onTopicSelected, onCreateTopic, onOpenScanner}) {
const [toast, setToast] = React.useState('');
const uid = hashParams && hashParams.uid;
let content;
@@ -669,7 +679,7 @@ export default function ContactManagementView({view, hashParams, myUserId, tinod
else if (view === 'my-friend-qr') content = <QrPage onToast={setToast} />;
else if (view === 'friend-qr') content = <FriendQr uid={uid} onToast={setToast} />;
else if (view === 'friend-verification') content = <Verification uid={uid} onToast={setToast} />;
else if (view === 'friend-profile' || view === 'group-member-profile') content = <Profile uid={uid} friend={view === 'friend-profile'} onTopicSelected={onTopicSelected} onToast={setToast} />;
else if (view === 'friend-profile' || view === 'group-member-profile') content = <Profile uid={uid} friend={view === 'friend-profile'} onTopicSelected={onTopicSelected} onCreateTopic={onCreateTopic} onToast={setToast} />;
else if (view === 'friend-settings' || view === 'friend-settings-blocked') content = <Settings uid={uid} initialBlocked={view === 'friend-settings-blocked'} onToast={setToast} />;
else if (view === 'block-list') content = <BlockList onToast={setToast} />;
else if (view === 'friend-delete-confirm') content = <ConfirmSheet type="delete" uid={uid} tinode={tinode} onToast={setToast} />;
+2 -1
View File
@@ -196,7 +196,8 @@ class SidepanelView extends React.PureComponent {
if (CONTACT_MANAGEMENT_VIEWS.includes(view)) {
return <div id="sidepanel" className="contact-management-panel">
<ContactManagementView view={view} hashParams={this.props.hashParams} myUserId={this.props.myUserId}
tinode={this.props.tinode} onTopicSelected={this.props.onTopicSelected} onOpenScanner={this.props.onOpenScanner} key={view} />
tinode={this.props.tinode} onTopicSelected={this.props.onTopicSelected} onCreateTopic={this.props.onCreateTopic}
onOpenScanner={this.props.onOpenScanner} key={view} />
</div>;
}