From dd29eaca427ac98e10597e47ba2ffe1204fc7c5f Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Sat, 17 Oct 2026 10:12:20 +0800 Subject: [PATCH] fix(web): sync group member nicknames --- web/src/app/AppShell.tsx | 7 ++++++- web/src/features/groups/GroupAdminModal.tsx | 14 ++++++++------ web/src/features/groups/GroupDetailColumn.tsx | 10 ++++++---- web/src/features/groups/GroupMembersModal.tsx | 9 +++++---- web/src/features/messages/ConversationView.tsx | 6 ++++-- web/src/features/messages/HistorySearchModal.tsx | 6 +++++- web/src/features/messages/MentionPicker.tsx | 2 +- web/src/lib/api/groups.ts | 3 ++- web/src/lib/messages/group-member-name.ts | 9 +++++++++ web/src/lib/tinode/topics.ts | 2 +- web/tests/group-member-name.test.ts | 13 +++++++++++++ 11 files changed, 60 insertions(+), 21 deletions(-) create mode 100644 web/src/lib/messages/group-member-name.ts create mode 100644 web/tests/group-member-name.test.ts diff --git a/web/src/app/AppShell.tsx b/web/src/app/AppShell.tsx index 5ffc15f0..879d616a 100644 --- a/web/src/app/AppShell.tsx +++ b/web/src/app/AppShell.tsx @@ -48,10 +48,15 @@ export function AppShell() { // and NewFriendsView's list (['friend-requests','all']) both refetch on invalidation. Listening // here rather than in those views keeps the badge live no matter which page the user is on. useEffect(() => { - return subscribeTopicInfo((_topicName, info) => { + return subscribeTopicInfo((topicName, info) => { if (info?.event?.startsWith('friend.request.')) { queryClient.invalidateQueries({ queryKey: ['friend-requests'] }); } + if (info?.event === 'group.member.profile.updated' && topicName.startsWith('grp')) { + queryClient.invalidateQueries({ queryKey: ['group-members', topicName] }); + queryClient.invalidateQueries({ queryKey: ['group-members-preview', topicName] }); + queryClient.invalidateQueries({ queryKey: ['group-my-profile', topicName] }); + } }); }, [queryClient]); // "sys" has its own authoritative unread source in entry_badges (bootstrap.ts) -- excluded here diff --git a/web/src/features/groups/GroupAdminModal.tsx b/web/src/features/groups/GroupAdminModal.tsx index 077160d6..34ba24ee 100644 --- a/web/src/features/groups/GroupAdminModal.tsx +++ b/web/src/features/groups/GroupAdminModal.tsx @@ -24,6 +24,7 @@ import { } from '@/lib/api/groups'; import { avatarBgClass, avatarLetter } from '@/lib/avatar'; import { useEscapeKey } from '@/lib/hooks'; +import { groupMemberDisplayName } from '@/lib/messages/group-member-name'; import { requestConfirm } from '@/stores/confirmDialog'; import { requestActionToken } from '@/stores/securityChallenge'; import { useSessionStore } from '@/stores/session'; @@ -288,9 +289,9 @@ function MemberPickerList({ className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left hover:bg-surface-rail disabled:opacity-50" > - {avatarLetter(m.display_name)} + {avatarLetter(groupMemberDisplayName(m))} - {m.display_name || m.user_id} + {groupMemberDisplayName(m)} ))} @@ -370,9 +371,9 @@ function AdminsSubView({ {admins.map((a) => (
- {avatarLetter(a.display_name)} + {avatarLetter(groupMemberDisplayName(a))} - {a.display_name || a.user_id} + {groupMemberDisplayName(a)} ))} {canInvite ? ( diff --git a/web/src/features/groups/GroupMembersModal.tsx b/web/src/features/groups/GroupMembersModal.tsx index a552e52d..208225e3 100644 --- a/web/src/features/groups/GroupMembersModal.tsx +++ b/web/src/features/groups/GroupMembersModal.tsx @@ -9,6 +9,7 @@ import { getFriends } from '@/lib/api/friends'; import { canRemoveMember, getAllGroupMembers, inviteGroupMembers, removeGroupMember, type GroupInvitationResult, type GroupRole } from '@/lib/api/groups'; import { avatarBgClass, avatarLetter } from '@/lib/avatar'; import { useEscapeKey } from '@/lib/hooks'; +import { groupMemberDisplayName } from '@/lib/messages/group-member-name'; import { requestConfirm } from '@/stores/confirmDialog'; import { openUserCard } from '@/stores/userCard'; @@ -81,7 +82,7 @@ export function GroupMembersModal({ topicId, canInvite, myUserId, myRole, initia const membersQuery = useQuery({ queryKey: ['group-members', topicId], queryFn: () => getAllGroupMembers(topicId) }); const members = membersQuery.data || []; - const filtered = members.filter((m) => !query.trim() || (m.display_name || m.user_id).toLowerCase().includes(query.trim().toLowerCase())); + const filtered = members.filter((m) => !query.trim() || groupMemberDisplayName(m).toLowerCase().includes(query.trim().toLowerCase())); const friendsQuery = useQuery({ queryKey: ['friends'], @@ -198,9 +199,9 @@ export function GroupMembersModal({ topicId, canInvite, myUserId, myRole, initia
{canRemoveMember(myUserId, myRole, m) ? ( @@ -208,7 +209,7 @@ export function GroupMembersModal({ topicId, canInvite, myUserId, myRole, initia type="button" disabled={removingIds.has(m.user_id)} onClick={async () => { - const label = m.display_name || m.user_id; + const label = groupMemberDisplayName(m); if (await requestConfirm({ message: `确定将"${label}"移出群聊吗?`, danger: true })) { setRemovingIds((prev) => new Set(prev).add(m.user_id)); removeMutation.mutate(m.user_id, { diff --git a/web/src/features/messages/ConversationView.tsx b/web/src/features/messages/ConversationView.tsx index 08261b4f..87312a2d 100644 --- a/web/src/features/messages/ConversationView.tsx +++ b/web/src/features/messages/ConversationView.tsx @@ -28,6 +28,7 @@ import { flatTextContentOf, isRecalled } from '@/lib/messages/dispatch'; import { buildMentionedContent, detectMentionTrigger, type PendingMention } from '@/lib/messages/mentions'; import { buildReplyInfo, replySnapshotText } from '@/lib/messages/reply'; import { groupSenderLabel } from '@/lib/messages/sender-name'; +import { groupMemberDisplayName } from '@/lib/messages/group-member-name'; import { sendFile, sendImage, sendVoice } from '@/lib/tinode/attachments'; import { isTopicGone } from '@/lib/tinode/errors'; import { useChatList, useTinode, useTopicMessages } from '@/lib/tinode/hooks'; @@ -602,7 +603,8 @@ export function ConversationView() { const quotedSenderLabel = (fromUserId: string | undefined) => { if (myUserId && fromUserId === myUserId) return '我'; if (isGroup && fromUserId) { - return groupSenderLabel(memberByUserId.get(fromUserId)?.display_name, staleSenderById.get(fromUserId)?.display_name); + const member = memberByUserId.get(fromUserId); + return groupSenderLabel(member && groupMemberDisplayName(member), staleSenderById.get(fromUserId)?.display_name); } return peerName; }; @@ -1075,7 +1077,7 @@ export function ConversationView() { mine={mine} isGroup={isGroup} peerName={peerName} - senderName={sender?.display_name || staleSender?.display_name} + senderName={sender ? groupMemberDisplayName(sender) : staleSender?.display_name} senderRole={sender?.role} avatarFileId={rowAvatarFileId} selectMode={selectMode} diff --git a/web/src/features/messages/HistorySearchModal.tsx b/web/src/features/messages/HistorySearchModal.tsx index 9bb375e4..eb63a5eb 100644 --- a/web/src/features/messages/HistorySearchModal.tsx +++ b/web/src/features/messages/HistorySearchModal.tsx @@ -11,6 +11,7 @@ import { useAuthedBlobUrl, useDebouncedValue, useEscapeKey } from '@/lib/hooks'; import { contactCardContentOf, mobileAttachmentContentOf } from '@/lib/messages/dispatch'; import { renderDrafty } from '@/lib/messages/drafty'; import { groupSenderLabel } from '@/lib/messages/sender-name'; +import { groupMemberDisplayName } from '@/lib/messages/group-member-name'; import { formatEventTimestamp } from '@/lib/messages/time'; import { requestConfirm } from '@/stores/confirmDialog'; import { notify } from '@/stores/globalToast'; @@ -199,7 +200,10 @@ export function HistorySearchModal({ topicId, initialTab = 'text', onClose, isGr const senderLabel = (from: string): string => { const full = normalizeSender(from); if (myUserId && full === myUserId) return '我'; - if (isGroup) return groupSenderLabel(membersQuery.data?.find((m) => m.user_id === full)?.display_name); + if (isGroup) { + const member = membersQuery.data?.find((m) => m.user_id === full); + return groupSenderLabel(member && groupMemberDisplayName(member)); + } return peerName; }; diff --git a/web/src/features/messages/MentionPicker.tsx b/web/src/features/messages/MentionPicker.tsx index c1a2983e..ac180af1 100644 --- a/web/src/features/messages/MentionPicker.tsx +++ b/web/src/features/messages/MentionPicker.tsx @@ -58,7 +58,7 @@ export function MentionPicker({ topicId, query, onPick }: MentionPickerProps) { // who have no idea what the sender's personal remark for this person means. `nickname` // is that member's own real public name, unaffected by anyone's remark, so it stays // correct for whoever else ends up reading the mention. - const label = m.nickname || m.display_name || m.user_id; + const label = m.group_nickname || m.nickname || m.display_name || m.user_id; return (