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).
This commit is contained in:
@@ -27,6 +27,7 @@ import { availableActions, recallRemainingSeconds, type MessageAction } from '@/
|
|||||||
import { isRecalled } from '@/lib/messages/dispatch';
|
import { isRecalled } from '@/lib/messages/dispatch';
|
||||||
import { renderDrafty } from '@/lib/messages/drafty';
|
import { renderDrafty } from '@/lib/messages/drafty';
|
||||||
import { buildMentionedContent, detectMentionTrigger, type PendingMention } from '@/lib/messages/mentions';
|
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 { sendFile, sendImage, sendVoice } from '@/lib/tinode/attachments';
|
||||||
import { isTopicGone } from '@/lib/tinode/errors';
|
import { isTopicGone } from '@/lib/tinode/errors';
|
||||||
import { useChatList, useTinode, useTopicMessages } from '@/lib/tinode/hooks';
|
import { useChatList, useTinode, useTopicMessages } from '@/lib/tinode/hooks';
|
||||||
@@ -568,7 +569,7 @@ export function ConversationView() {
|
|||||||
// second, divergent resolution path.
|
// second, divergent resolution path.
|
||||||
const quotedSenderLabel = (fromUserId: string | undefined) => {
|
const quotedSenderLabel = (fromUserId: string | undefined) => {
|
||||||
if (myUserId && fromUserId === myUserId) return '我';
|
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;
|
return peerName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { checkUrlSafety } from '@/lib/api/security';
|
|||||||
import { avatarBgClass, avatarLetter } from '@/lib/avatar';
|
import { avatarBgClass, avatarLetter } from '@/lib/avatar';
|
||||||
import { useAuthedBlobUrl, useDebouncedValue, useEscapeKey } from '@/lib/hooks';
|
import { useAuthedBlobUrl, useDebouncedValue, useEscapeKey } from '@/lib/hooks';
|
||||||
import { renderDrafty } from '@/lib/messages/drafty';
|
import { renderDrafty } from '@/lib/messages/drafty';
|
||||||
|
import { groupSenderLabel } from '@/lib/messages/sender-name';
|
||||||
import { formatEventTimestamp } from '@/lib/messages/time';
|
import { formatEventTimestamp } from '@/lib/messages/time';
|
||||||
import { requestConfirm } from '@/stores/confirmDialog';
|
import { requestConfirm } from '@/stores/confirmDialog';
|
||||||
import { notify } from '@/stores/globalToast';
|
import { notify } from '@/stores/globalToast';
|
||||||
@@ -163,7 +164,7 @@ export function HistorySearchModal({ topicId, initialTab = 'text', onClose, isGr
|
|||||||
const senderLabel = (from: string): string => {
|
const senderLabel = (from: string): string => {
|
||||||
const full = normalizeSender(from);
|
const full = normalizeSender(from);
|
||||||
if (myUserId && full === myUserId) return '我';
|
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;
|
return peerName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { callResultDisplay, contactCardContentOf, isRecalled, isServerDeleted, s
|
|||||||
import { renderDrafty } from '@/lib/messages/drafty';
|
import { renderDrafty } from '@/lib/messages/drafty';
|
||||||
import { formatBubbleTime, formatEventTimestamp } from '@/lib/messages/time';
|
import { formatBubbleTime, formatEventTimestamp } from '@/lib/messages/time';
|
||||||
import { isFailedStatus, isSendingStatus, outgoingStatusLabel } from '@/lib/messages/status';
|
import { isFailedStatus, isSendingStatus, outgoingStatusLabel } from '@/lib/messages/status';
|
||||||
|
import { groupSenderLabel } from '@/lib/messages/sender-name';
|
||||||
import { retryMessage } from '@/lib/tinode/send';
|
import { retryMessage } from '@/lib/tinode/send';
|
||||||
import { ensureTopicAttached } from '@/lib/tinode/topics';
|
import { ensureTopicAttached } from '@/lib/tinode/topics';
|
||||||
|
|
||||||
@@ -154,12 +155,12 @@ export function MessageRow({
|
|||||||
not navigate away and lose whatever's already been picked. */}
|
not navigate away and lose whatever's already been picked. */}
|
||||||
{isGroup && !mine && !selectMode && msg.from ? (
|
{isGroup && !mine && !selectMode && msg.from ? (
|
||||||
<Link to={`/contacts/${msg.from}`} className="flex-none no-underline">
|
<Link to={`/contacts/${msg.from}`} className="flex-none no-underline">
|
||||||
<Avatar id={msg.from} name={senderName || msg.from} avatarFileId={avatarFileId} className="h-9 w-9 rounded-lg text-sm" />
|
<Avatar id={msg.from} name={groupSenderLabel(senderName)} avatarFileId={avatarFileId} className="h-9 w-9 rounded-lg text-sm" />
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<Avatar
|
<Avatar
|
||||||
id={msg.from || (isGroup ? '' : peerName)}
|
id={msg.from || (isGroup ? '' : peerName)}
|
||||||
name={mine ? '我' : isGroup ? senderName || msg.from : peerName}
|
name={mine ? '我' : isGroup ? groupSenderLabel(senderName) : peerName}
|
||||||
avatarFileId={avatarFileId}
|
avatarFileId={avatarFileId}
|
||||||
fallbackBgClass={mine ? 'bg-accent' : undefined}
|
fallbackBgClass={mine ? 'bg-accent' : undefined}
|
||||||
className="h-9 w-9 flex-none rounded-lg text-sm"
|
className="h-9 w-9 flex-none rounded-lg text-sm"
|
||||||
@@ -170,10 +171,10 @@ export function MessageRow({
|
|||||||
<div className={`flex items-center gap-1 px-0.5 text-xs text-text-tertiary-4 ${mine ? 'flex-row-reverse' : ''}`}>
|
<div className={`flex items-center gap-1 px-0.5 text-xs text-text-tertiary-4 ${mine ? 'flex-row-reverse' : ''}`}>
|
||||||
{!mine && !selectMode && msg.from ? (
|
{!mine && !selectMode && msg.from ? (
|
||||||
<Link to={`/contacts/${msg.from}`} className="text-text-tertiary-4 no-underline hover:underline">
|
<Link to={`/contacts/${msg.from}`} className="text-text-tertiary-4 no-underline hover:underline">
|
||||||
{senderName || msg.from}
|
{groupSenderLabel(senderName)}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span>{mine ? '我' : senderName || msg.from}</span>
|
<span>{mine ? '我' : groupSenderLabel(senderName)}</span>
|
||||||
)}
|
)}
|
||||||
<MessageRoleTag role={senderRole === 'owner' || senderRole === 'admin' ? senderRole : undefined} />
|
<MessageRoleTag role={senderRole === 'owner' || senderRole === 'admin' ? senderRole : undefined} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user