From 92892e3e7b1b9d51410002d7411f9da57b0dc028 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 16 Sep 2026 16:23:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=A4=E4=B8=AA=E4=B8=A5?= =?UTF-8?q?=E9=87=8D=E5=B4=A9=E6=BA=83=EF=BC=9A=E7=BE=A4=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E6=9F=A5=E8=AF=A2=E9=94=AE=E5=86=B2=E7=AA=81?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E6=95=B4=E4=B8=AA=E4=BC=9A=E8=AF=9D=E5=B4=A9?= =?UTF-8?q?=E6=BA=83=E3=80=81=E7=A6=81=E8=A8=80=E5=88=97=E8=A1=A8=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=20=E4=B8=8D=E5=AD=98=E5=9C=A8=E7=9A=84=E5=B5=8C?= =?UTF-8?q?=E5=A5=97=E5=AD=97=E6=AE=B5=E5=B4=A9=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ConversationView.tsx和GroupAdminModal.tsx对群禁言列表用了完全相同的 React Query缓存键['group-mutes', topicId],但分别调用getGroupMutes (单页,{items:[]}包装对象)和getAllGroupMutes(全部页,展开后的纯数组) 两个返回形状不同的函数。两个组件同时挂载时后写入的一方会覆盖缓存, 另一方读到错误形状后对undefined取.length直接崩溃,导致只要打开某个 群的"群管理"面板整个会话界面就白屏。改成两处统一用getAllGroupMutes, 真正共享同一份、同一种形状的数据 - GroupMute类型声明的是嵌套的user:{user_id,display_name},但真实响应 是扁平的{user_id,muted_by,muted_at,reason,status,version},且完全没有 display_name字段。这个字段错配从来没被暴露过,因为此前从未有真实账号 真正禁言过人后去看这个列表——本轮实测"禁言→查看列表"时才第一次真正 用真实数据渲染,立刻崩溃。改成扁平字段+交叉查群成员列表解析真实姓名 (同一模式ConversationView.tsx的memberByUserId、GroupFilesModal的 uploader_id都已经在用) 用全新账号在全新群里完整实测:禁言成员1人正确显示头像+姓名"systester2" (不再是原始uid或崩溃),解除禁言后列表正确清零,两处此前必崩的入口 (群管理面板本身、禁言列表渲染)全部验证通过。 --- web/src/features/groups/GroupAdminModal.tsx | 42 ++++++++++++------- .../features/messages/ConversationView.tsx | 17 ++++++-- web/src/lib/api/groups.ts | 11 ++++- 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/web/src/features/groups/GroupAdminModal.tsx b/web/src/features/groups/GroupAdminModal.tsx index 950f368c..53aab35a 100644 --- a/web/src/features/groups/GroupAdminModal.tsx +++ b/web/src/features/groups/GroupAdminModal.tsx @@ -410,6 +410,13 @@ function MutesSubView({ topicId, muted, onClose }: { topicId: string; muted: Gro const queryClient = useQueryClient(); const [adding, setAdding] = useState(false); const [error, setError] = useState(''); + // `GroupMute` itself carries only a bare `user_id` (see its own type comment) -- resolve a real + // name/avatar the same way ConversationView.tsx's `memberByUserId` does, rather than rendering the + // raw id (this list previously crashed outright reading a `.user.display_name` that never existed + // on the real response, confirmed live: `Cannot read properties of undefined (reading 'user_id')` + // the moment a real muted member was ever rendered here). + const membersQuery = useQuery({ queryKey: ['group-members', topicId], queryFn: () => getAllGroupMembers(topicId) }); + const memberByUserId = new Map((membersQuery.data || []).map((m) => [m.user_id, m])); const invalidate = () => queryClient.invalidateQueries({ queryKey: ['group-mutes', topicId] }); const muteMutation = useMutation({ mutationFn: (userId: string) => muteGroupMembers(topicId, [{ user_id: userId, expires_at: null }]), @@ -439,22 +446,25 @@ function MutesSubView({ topicId, muted, onClose }: { topicId: string; muted: Gro /> ) : ( <> - {muted.map((m) => ( -
- - {avatarLetter(m.user.display_name)} - - {m.user.display_name || m.user.user_id} - -
- ))} + {muted.map((m) => { + const name = memberByUserId.get(m.user_id)?.display_name || m.user_id; + return ( +
+ + {avatarLetter(name)} + + {name} + +
+ ); + })}