From 1952f0ebcdfcd36321432ef3fb9d8e8317ecf079 Mon Sep 17 00:00:00 2001 From: Austin Date: Sun, 20 Sep 2026 16:42:35 +0800 Subject: [PATCH] fix(messages): never render raw uid as group sender name (#32) Senders missing from the member list (left/removed) fell back to msg.from verbatim, and nameless members arrive from the REST API with the uid itself as display_name. Resolve both through a shared groupSenderLabel helper that substitutes a fixed placeholder for uid-shaped names (message bubbles, quoted replies, history search). --- .../features/messages/ConversationView.tsx | 3 ++- .../features/messages/HistorySearchModal.tsx | 3 ++- web/src/features/messages/MessageBubble.tsx | 9 ++++---- web/src/lib/messages/sender-name.ts | 22 +++++++++++++++++++ 4 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 web/src/lib/messages/sender-name.ts diff --git a/web/src/features/messages/ConversationView.tsx b/web/src/features/messages/ConversationView.tsx index 580e9f1f..75df60f5 100644 --- a/web/src/features/messages/ConversationView.tsx +++ b/web/src/features/messages/ConversationView.tsx @@ -27,6 +27,7 @@ import { availableActions, recallRemainingSeconds, type MessageAction } from '@/ import { isRecalled } from '@/lib/messages/dispatch'; import { renderDrafty } from '@/lib/messages/drafty'; import { buildMentionedContent, detectMentionTrigger, type PendingMention } from '@/lib/messages/mentions'; +import { groupSenderLabel } from '@/lib/messages/sender-name'; import { sendFile, sendImage, sendVoice } from '@/lib/tinode/attachments'; import { isTopicGone } from '@/lib/tinode/errors'; import { useChatList, useTinode, useTopicMessages } from '@/lib/tinode/hooks'; @@ -568,7 +569,7 @@ export function ConversationView() { // second, divergent resolution path. const quotedSenderLabel = (fromUserId: string | undefined) => { if (myUserId && fromUserId === myUserId) return '我'; - if (isGroup && fromUserId) return memberByUserId.get(fromUserId)?.display_name || fromUserId; + if (isGroup && fromUserId) return groupSenderLabel(memberByUserId.get(fromUserId)?.display_name); return peerName; }; diff --git a/web/src/features/messages/HistorySearchModal.tsx b/web/src/features/messages/HistorySearchModal.tsx index 035928c9..4b54f579 100644 --- a/web/src/features/messages/HistorySearchModal.tsx +++ b/web/src/features/messages/HistorySearchModal.tsx @@ -9,6 +9,7 @@ import { checkUrlSafety } from '@/lib/api/security'; import { avatarBgClass, avatarLetter } from '@/lib/avatar'; import { useAuthedBlobUrl, useDebouncedValue, useEscapeKey } from '@/lib/hooks'; import { renderDrafty } from '@/lib/messages/drafty'; +import { groupSenderLabel } from '@/lib/messages/sender-name'; import { formatEventTimestamp } from '@/lib/messages/time'; import { requestConfirm } from '@/stores/confirmDialog'; import { notify } from '@/stores/globalToast'; @@ -163,7 +164,7 @@ 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 membersQuery.data?.find((m) => m.user_id === full)?.display_name || from; + if (isGroup) return groupSenderLabel(membersQuery.data?.find((m) => m.user_id === full)?.display_name); return peerName; }; diff --git a/web/src/features/messages/MessageBubble.tsx b/web/src/features/messages/MessageBubble.tsx index cd1d2e07..b11e4724 100644 --- a/web/src/features/messages/MessageBubble.tsx +++ b/web/src/features/messages/MessageBubble.tsx @@ -9,6 +9,7 @@ import { callResultDisplay, contactCardContentOf, isRecalled, isServerDeleted, s import { renderDrafty } from '@/lib/messages/drafty'; import { formatBubbleTime, formatEventTimestamp } from '@/lib/messages/time'; import { isFailedStatus, isSendingStatus, outgoingStatusLabel } from '@/lib/messages/status'; +import { groupSenderLabel } from '@/lib/messages/sender-name'; import { retryMessage } from '@/lib/tinode/send'; import { ensureTopicAttached } from '@/lib/tinode/topics'; @@ -154,12 +155,12 @@ export function MessageRow({ not navigate away and lose whatever's already been picked. */} {isGroup && !mine && !selectMode && msg.from ? ( - + ) : ( {!mine && !selectMode && msg.from ? ( - {senderName || msg.from} + {groupSenderLabel(senderName)} ) : ( - {mine ? '我' : senderName || msg.from} + {mine ? '我' : groupSenderLabel(senderName)} )} diff --git a/web/src/lib/messages/sender-name.ts b/web/src/lib/messages/sender-name.ts new file mode 100644 index 00000000..7551da22 --- /dev/null +++ b/web/src/lib/messages/sender-name.ts @@ -0,0 +1,22 @@ +/** + * Sender display label for group chat surfaces (bubbles, quoted replies, history search). + * + * Two distinct paths used to leak the raw `usr...` id into the message list (Zentao #32): + * - the sender is no longer in the member list (left/was removed) so there is no resolved + * name at all, and the row fell back to `msg.from` verbatim; + * - the member IS in the list but has no nickname/remark anywhere, so the server's own + * display-name fallback in userSummaryPayload is the user id itself. + * Both must render a fixed placeholder instead -- a uid is an internal identifier, not a + * user-facing name. Membership of a uid is decided by shape (`usr…` followed by id charset), + * because the server's fallback means the raw id can arrive *inside* `senderName`. + */ +const UID_SHAPE = /^usr[A-Za-z0-9_-]+$/; + +export const UNKNOWN_SENDER_LABEL = '未知用户'; + +export function groupSenderLabel(senderName: string | undefined): string { + const name = senderName?.trim(); + if (name && !UID_SHAPE.test(name)) return name; + // No usable name -- never show the raw uid even when it IS available; it's not a user-facing name. + return UNKNOWN_SENDER_LABEL; +}