From 0d2637861d634dca0f13ee03cb64ff7ee601b55a Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 6 Sep 2026 16:38:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=A5=BD=E5=8F=8B=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E4=B8=BB=E9=A1=B5"=E5=8F=91=E6=B6=88=E6=81=AF/?= =?UTF-8?q?=E9=9F=B3=E8=A7=86=E9=A2=91=E9=80=9A=E8=AF=9D"=E5=AF=B9?= =?UTF-8?q?=E9=9B=B6=E6=B6=88=E6=81=AF=E5=8E=86=E5=8F=B2=E7=9A=84=E6=96=B0?= =?UTF-8?q?=E5=A5=BD=E5=8F=8B=E5=BF=85=E7=8E=B0"=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=B7=B2=E7=A6=81=E7=94=A8"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: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)、输入框正常出现,并成功发送/收到多条真实文本消息。 --- src/views/contact-management-view.jsx | 18 ++++++++++++++---- src/views/sidepanel-view.jsx | 3 ++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/views/contact-management-view.jsx b/src/views/contact-management-view.jsx index 6285f29b..e82fb1fa 100644 --- a/src/views/contact-management-view.jsx +++ b/src/views/contact-management-view.jsx @@ -406,7 +406,7 @@ function Verification({uid, onToast}) { ; } -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
navigate('friend-settings', uid)} />
@@ -659,7 +669,7 @@ function ConfirmSheet({type, uid, tinode, onToast}) {
; } -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 = ; else if (view === 'friend-qr') content = ; else if (view === 'friend-verification') content = ; - else if (view === 'friend-profile' || view === 'group-member-profile') content = ; + else if (view === 'friend-profile' || view === 'group-member-profile') content = ; else if (view === 'friend-settings' || view === 'friend-settings-blocked') content = ; else if (view === 'block-list') content = ; else if (view === 'friend-delete-confirm') content = ; diff --git a/src/views/sidepanel-view.jsx b/src/views/sidepanel-view.jsx index 8e61ad05..12383e2c 100644 --- a/src/views/sidepanel-view.jsx +++ b/src/views/sidepanel-view.jsx @@ -196,7 +196,8 @@ class SidepanelView extends React.PureComponent { if (CONTACT_MANAGEMENT_VIEWS.includes(view)) { return
+ tinode={this.props.tinode} onTopicSelected={this.props.onTopicSelected} onCreateTopic={this.props.onCreateTopic} + onOpenScanner={this.props.onOpenScanner} key={view} />
; }