@@ -500,7 +502,7 @@ function TransferOwnerSubView({ topicId, groupVersion, onClose }: { topicId: str
cacheKey="eligible-owner"
opts={{ eligibleRole: 'owner' }}
onPick={async (m) => {
- if (!(await requestConfirm({ message: `确定将群主转让给 ${m.display_name || m.user_id} 吗?此操作不可撤销。`, danger: true }))) return;
+ if (!(await requestConfirm({ message: `确定将群主转让给 ${groupMemberDisplayName(m)} 吗?此操作不可撤销。`, danger: true }))) return;
const actionToken = await requestActionToken({ scene: 'group_owner_transfer', actionDescription: '转让群主' });
if (!actionToken) return;
transferMutation.mutate({ userId: m.user_id, actionToken });
diff --git a/web/src/features/groups/GroupDetailColumn.tsx b/web/src/features/groups/GroupDetailColumn.tsx
index 2dd72349..30e1f35f 100644
--- a/web/src/features/groups/GroupDetailColumn.tsx
+++ b/web/src/features/groups/GroupDetailColumn.tsx
@@ -31,6 +31,7 @@ import {
import type { MediaType } from '@/lib/api/messages';
import { uploadFile } from '@/lib/api/upload';
import { MAX_TITLE_LENGTH } from '@/lib/config';
+import { groupMemberDisplayName } from '@/lib/messages/group-member-name';
import { useTinode } from '@/lib/tinode/hooks';
import { forgetTopic } from '@/lib/tinode/topics';
import { requestConfirm } from '@/stores/confirmDialog';
@@ -119,8 +120,7 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }:
// GroupAdminModal's join-request badge, so this is a cache hit more often than not).
const summaryQuery = useQuery({ queryKey: ['group-summary', topicId], queryFn: () => getGroupSummary(topicId) });
const membersQuery = useQuery({ queryKey: ['group-members-preview', topicId], queryFn: () => getGroupMembers(topicId, {}) });
- // `GET .../members` never returns `group_nickname` on any item (confirmed live by the old app) --
- // only this per-user profile endpoint does, so the nickname row needs its own fetch. Uses the
+ // The nickname row needs the per-user profile endpoint. Uses the
// dedicated `members/me/profile` route, not getGroupMemberProfile(topic, myUserId) -- see that
// function's own warning for why the generic by-id path 400s on self-lookups.
const myProfileQuery = useQuery({
@@ -168,6 +168,8 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }:
mutationFn: (nickname: string) => updateMyGroupNickname(topicId, nickname, myProfileQuery.data?.version ?? 1),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['group-my-profile', topicId, myUserId] });
+ queryClient.invalidateQueries({ queryKey: ['group-members-preview', topicId] });
+ queryClient.invalidateQueries({ queryKey: ['group-members', topicId] });
setNicknameEditing(false);
},
onError: (err) => showError(err, '昵称保存失败'),
@@ -430,12 +432,12 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }:
desktop's 40px (unconditional here before this fix). */}
- {m.display_name || m.user_id}
+ {groupMemberDisplayName(m)}
))}
{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 (