修复群聊解散通知覆盖不全:主动退出/解散群聊会误报"该群聊已解散"且留下幽灵会话

前情:上一版修复只覆盖了"被动看着群被别人解散"这一种场景(后端走自定义
{info what:"group" event:"group.dissolved"}事件而非原生{pres what:"gone"},
需要单独识别)。这次用户实测发现同一个"话题彻底消失后清理不干净"的问题在另外
两条路径上都复现了:自己点"退出群聊"、自己(群主)点"解散群聊"。

根因:这两个操作各自的mutation.onSuccess只是navigate('/messages'),没有做任何
本地缓存清理;而服务端在REST调用成功后,通常还会异步地给这条已经不再属于你的
订阅补发一个原生/自定义的"gone"信号——这个信号触发的是topics.ts里那条本该只用
于"被动围观"场景的goneListeners通知链,于是ConversationView的通用"该群聊已
解散"提示被复用到了并不贴切的"我自己退出/解散"场景上,且同一个SDK缓存stub没
被清理的老毛病(getTopic()被再次调用就会重建一个空壳)也照样在这两条路径上
重演,表现为消息列表里多出一个显示原始群ID、"1970/1/1"的幽灵会话。

修复:
- topics.ts的markTopicGoneAndNotify新增goneTopicNames去重判断——只要
  forgetTopic已经先标记过这个话题,稍后到达的原生/自定义gone信号就直接跳过
  (包括不再触发goneListeners),避免重复通知。
- GroupDetailColumn.tsx的leaveMutation/disbandMutation改成onSuccess里先调用
  forgetTopic(topicId)(清缓存+加入排除名单+让上面的守卫生效)再navigate,
  不再空等异步信号来做清理。

实机验证:systest04建群拉systest02入群 -> systest02主动退出,返回消息列表后
侧栏不再残留幽灵会话;systest04(群主)解散同一个群,同样返回消息列表且侧栏
干净。顺带在GroupDetailColumn.tsx里把群头像/成员格子头像接上了真实上传头像
(见下一条commit的头像修复,这个文件因为改动集中放在了一起)。
This commit is contained in:
Your Name
2026-09-16 23:36:35 +08:00
parent df22f26628
commit 804618ad63
2 changed files with 100 additions and 31 deletions
+31 -11
View File
@@ -3,6 +3,7 @@ import { useLayoutEffect, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { Avatar } from '@/app/Avatar';
import { Icon } from '@/app/Icon';
import { Toast } from '@/app/Toast';
import { QRCodeModal } from '@/features/contacts/QRCodeModal';
@@ -24,8 +25,8 @@ import {
type GroupSettings,
} from '@/lib/api/groups';
import type { MediaType } from '@/lib/api/messages';
import { avatarBgClass, avatarLetter } from '@/lib/avatar';
import { useTinode } from '@/lib/tinode/hooks';
import { forgetTopic } from '@/lib/tinode/topics';
import { requestConfirm } from '@/stores/confirmDialog';
import { notify } from '@/stores/globalToast';
import { requestActionToken } from '@/stores/securityChallenge';
@@ -151,15 +152,30 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }:
onError: (err) => showError(err, '昵称保存失败'),
});
// Both mutations call forgetTopic() the instant their own REST call succeeds, rather than waiting
// on the async native/`group.dissolved` gone-signal that normally follows a group actually going
// away -- real, user-reported bugs fixed by this: (1) the chat list kept showing a broken stale
// stub for this group after leaving/disbanding it (same lingering-stub symptom `forgetTopic` was
// originally built to fix for the refresh/404 case, just reached via a third path); (2) that same
// late signal, left unguarded, used to re-fire ConversationView's generic "该群聊已解散" toast on
// top of this action's own navigate -- wrong wording specifically for 退出群聊 (leaving yourself
// isn't the group being dissolved). `markTopicGoneAndNotify`'s own guard (topics.ts) now skips that
// late signal entirely once `forgetTopic` has already marked this name gone here.
const leaveMutation = useMutation({
mutationFn: () => leaveGroup(topicId),
onSuccess: () => navigate('/messages'),
onSuccess: () => {
forgetTopic(topicId);
navigate('/messages');
},
onError: (err) => showError(err, '退出群聊失败'),
});
const disbandMutation = useMutation({
mutationFn: (actionToken: string) => disbandGroup(topicId, actionToken),
onSuccess: () => navigate('/messages'),
onSuccess: () => {
forgetTopic(topicId);
navigate('/messages');
},
onError: (err) => showError(err, '解散群聊失败'),
});
@@ -237,9 +253,12 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }:
<div className="bg-white md:contents">
<div className="flex flex-col gap-2 px-4 py-4 md:border-b md:border-border-list-item md:px-5">
<div className="flex items-center gap-3">
<span className={`grid h-13 w-13 flex-none place-items-center rounded-xl text-xl font-medium text-white ${avatarBgClass(topicId)}`}>
{avatarLetter(groupQuery.data?.name || peerName)}
</span>
<Avatar
id={topicId}
name={groupQuery.data?.name || peerName}
avatarFileId={groupQuery.data?.avatar_file_id}
className="h-13 w-13 flex-none rounded-xl text-xl"
/>
<div className="flex min-w-0 flex-col">
<strong className="truncate text-base font-semibold text-text-heading">{groupQuery.data?.name || peerName}</strong>
<span className="text-xs text-text-tertiary-2">
@@ -274,11 +293,12 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }:
<div key={m.user_id} className="relative flex flex-col items-center gap-1">
{/* README §4.10/#1e: mobile member-grid avatars are 44px, one size up from
desktop's 40px (unconditional here before this fix). */}
<span
className={`grid h-11 w-11 place-items-center rounded-lg text-sm font-medium text-white md:h-10 md:w-10 ${avatarBgClass(m.user_id)}`}
>
{avatarLetter(m.display_name)}
</span>
<Avatar
id={m.user_id}
name={m.display_name}
avatarFileId={m.avatar_file_id}
className="h-11 w-11 rounded-lg text-sm md:h-10 md:w-10"
/>
<RoleCorner role={m.role} />
<span className="w-full truncate text-center text-[11px] text-text-secondary">{m.display_name || m.user_id}</span>
</div>