Reapply "fix(federation): normalize federated username display across UI"

This reverts commit 5c3185b69f.
This commit is contained in:
Jannis Braun
2026-03-27 02:35:55 +01:00
parent d11923f6a5
commit 1607a8569c
6 changed files with 60 additions and 31 deletions
@@ -886,13 +886,15 @@ function TabButton({ children, active, onClick }: { children: React.ReactNode, a
function FriendItem({ friend, onRemove, onDm }: { friend: TaggedFriend, onRemove: () => void, onDm: () => void }) { function FriendItem({ friend, onRemove, onDm }: { friend: TaggedFriend, onRemove: () => void, onDm: () => void }) {
const instanceLabel = friend._instanceOrigin ? (() => { try { return new URL(friend._instanceOrigin).host; } catch { return friend._instanceOrigin; } })() : ''; const instanceLabel = friend._instanceOrigin ? (() => { try { return new URL(friend._instanceOrigin).host; } catch { return friend._instanceOrigin; } })() : '';
const { baseName: friendBaseName } = parseFederatedUsername(friend.username);
const friendDisplayName = friend.displayName ?? friendBaseName;
return ( return (
<div className="flex items-center justify-between px-3 h-[62px] rounded-[8px] hover:bg-interactive-hover group transition-colors border-t border-interactive-muted mx-2"> <div className="flex items-center justify-between px-3 h-[62px] rounded-[8px] hover:bg-interactive-hover group transition-colors border-t border-interactive-muted mx-2">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Avatar src={friend.avatar} name={friend.displayName ?? friend.username} size={32} status={friend.status} userId={friend.homeUserId ?? friend.id} avatarColor={friend.avatarColor} /> <Avatar src={friend.avatar} name={friendDisplayName} size={32} status={friend.status} userId={friend.homeUserId ?? friend.id} avatarColor={friend.avatarColor} />
<div className="flex flex-col leading-tight"> <div className="flex flex-col leading-tight">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="text-txt-primary font-semibold text-[15px]">{friend.displayName ?? friend.username}</span> <span className="text-txt-primary font-semibold text-[15px]">{friendDisplayName}</span>
<span className="text-txt-tertiary text-[13px] opacity-0 group-hover:opacity-100 transition-opacity font-medium">@{friend.username}</span> <span className="text-txt-tertiary text-[13px] opacity-0 group-hover:opacity-100 transition-opacity font-medium">@{friend.username}</span>
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
@@ -937,14 +939,16 @@ function RequestItem({ request, type, onAccept, onDecline, onCancel }: {
const user = request.user; const user = request.user;
if (!user) return null; if (!user) return null;
const instanceLabel = request._instanceOrigin ? (() => { try { return new URL(request._instanceOrigin).host; } catch { return request._instanceOrigin; } })() : ''; const instanceLabel = request._instanceOrigin ? (() => { try { return new URL(request._instanceOrigin).host; } catch { return request._instanceOrigin; } })() : '';
const { baseName: reqBaseName } = parseFederatedUsername(user.username);
const reqDisplayName = user.displayName ?? reqBaseName;
return ( return (
<div className="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-interactive-hover group transition-colors border-t border-interactive-muted mx-2"> <div className="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-interactive-hover group transition-colors border-t border-interactive-muted mx-2">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Avatar src={user.avatar} name={user.displayName ?? user.username} size={32} status={user.status as any} userId={user.homeUserId ?? user.id} avatarColor={user.avatarColor} /> <Avatar src={user.avatar} name={reqDisplayName} size={32} status={user.status as any} userId={user.homeUserId ?? user.id} avatarColor={user.avatarColor} />
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="text-txt-primary font-bold text-sm">{user.displayName ?? user.username}</span> <span className="text-txt-primary font-bold text-sm">{reqDisplayName}</span>
<span className="text-txt-tertiary text-xs">@{user.username}</span> <span className="text-txt-tertiary text-xs">@{user.username}</span>
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
@@ -6,7 +6,7 @@ import { useAuthStore } from '../../stores/authStore';
import { useSocialStore } from '../../stores/socialStore'; import { useSocialStore } from '../../stores/socialStore';
import { Avatar } from '../ui/Avatar'; import { Avatar } from '../ui/Avatar';
import { hasPermissionBit, PermissionBits } from '../../utils/permissions'; import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
import { isSelf } from '../../utils/identity'; import { isSelf, parseFederatedUsername } from '../../utils/identity';
import { useDelayedLoading } from '../../hooks/useDelayedLoading'; import { useDelayedLoading } from '../../hooks/useDelayedLoading';
import type { MessageWithUser } from '@backspace/shared'; import type { MessageWithUser } from '@backspace/shared';
@@ -374,8 +374,9 @@ function WelcomeHeader({ channelId }: { channelId: string }) {
if (isDm) { if (isDm) {
const dm = dmChannels.find(d => d.id === channelId); const dm = dmChannels.find(d => d.id === channelId);
const otherUser = dm?.members.find(m => !isSelf(m, authUser)); const otherUser = dm?.members.find(m => !isSelf(m, authUser));
const displayName = otherUser?.displayName ?? otherUser?.username ?? 'Unknown'; const { baseName } = parseFederatedUsername(otherUser?.username ?? 'unknown');
const username = otherUser?.username ?? 'unknown'; const displayName = otherUser?.displayName ?? baseName;
const username = baseName;
const isFriend = otherUser ? friends.some(f => f.id === otherUser.id) : false; const isFriend = otherUser ? friends.some(f => f.id === otherUser.id) : false;
return ( return (
@@ -12,6 +12,7 @@ import { useVoiceStore } from '../../stores/voiceStore';
import { Avatar } from '../ui/Avatar'; import { Avatar } from '../ui/Avatar';
import { Mascot } from '../ui/Mascot'; import { Mascot } from '../ui/Mascot';
import { Username } from '../ui/Username'; import { Username } from '../ui/Username';
import { Tooltip } from '../ui/Tooltip';
import { wsSend } from '../../hooks/useWebSocket'; import { wsSend } from '../../hooks/useWebSocket';
import { AudioManager } from '../../audio/AudioManager'; import { AudioManager } from '../../audio/AudioManager';
import { hasPermissionBit, PermissionBits } from '../../utils/permissions'; import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
@@ -494,9 +495,11 @@ export function ChannelSidebar() {
const isGroup = dm.members.length > 2; const isGroup = dm.members.length > 2;
const isDmUnread = unreadChannels.has(dm.id) && currentChannelId !== dm.id; const isDmUnread = unreadChannels.has(dm.id) && currentChannelId !== dm.id;
const firstOtherDm = isGroup ? null : otherMembers[0];
const { baseName: dmBaseName, domain: dmDomain } = parseFederatedUsername(firstOtherDm?.username ?? '');
const dmDisplayName = isGroup const dmDisplayName = isGroup
? otherMembers.map(m => m.displayName ?? parseFederatedUsername(m.username).baseName).join(', ') ? otherMembers.map(m => m.displayName ?? parseFederatedUsername(m.username).baseName).join(', ')
: otherMembers[0]?.displayName ?? otherMembers[0]?.username; : firstOtherDm?.displayName ?? dmBaseName;
const dmItem = ( const dmItem = (
<div <div
@@ -534,14 +537,24 @@ export function ChannelSidebar() {
<Avatar src={otherMembers[0]?.avatar} name={otherMembers[0]?.displayName ?? parseFederatedUsername(otherMembers[0]?.username ?? '').baseName} size={32} status={otherMembers[0]?.status as any} userId={otherMembers[0]?.homeUserId ?? otherMembers[0]?.id} user={otherMembers[0]} /> <Avatar src={otherMembers[0]?.avatar} name={otherMembers[0]?.displayName ?? parseFederatedUsername(otherMembers[0]?.username ?? '').baseName} size={32} status={otherMembers[0]?.status as any} userId={otherMembers[0]?.homeUserId ?? otherMembers[0]?.id} user={otherMembers[0]} />
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<Username <div className="flex items-center gap-1 min-w-0">
username={dmDisplayName ?? ''} <span
className={`text-[15px] truncate leading-tight block ${ className={`text-[15px] truncate leading-tight ${
currentChannelId === dm.id ? 'text-white font-medium' currentChannelId === dm.id ? 'text-white font-medium'
: isDmUnread ? 'text-white font-bold' : isDmUnread ? 'text-white font-bold'
: 'text-txt-tertiary group-hover:text-txt-secondary font-medium' : 'text-txt-tertiary group-hover:text-txt-secondary font-medium'
}`} }`}
/> >
{dmDisplayName}
</span>
{!isGroup && dmDomain && (
<Tooltip content={firstOtherDm?.username ?? ''} position="top">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" className="text-txt-tertiary/60 flex-shrink-0">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z" />
</svg>
</Tooltip>
)}
</div>
{isGroup ? ( {isGroup ? (
<div className="text-[12px] text-txt-tertiary truncate leading-tight mt-0.5"> <div className="text-[12px] text-txt-tertiary truncate leading-tight mt-0.5">
{dm.members.length} Members {dm.members.length} Members
@@ -15,7 +15,8 @@ import { Avatar } from '../ui/Avatar';
import { useVoiceStore } from '../../stores/voiceStore'; import { useVoiceStore } from '../../stores/voiceStore';
import { wsSend } from '../../hooks/useWebSocket'; import { wsSend } from '../../hooks/useWebSocket';
import { MemberListToggleButton } from './MemberListToggleButton'; import { MemberListToggleButton } from './MemberListToggleButton';
import { isSelf } from '../../utils/identity'; import { isSelf, parseFederatedUsername } from '../../utils/identity';
import { Tooltip } from '../ui/Tooltip';
import { joinVoiceChannel } from '../../utils/voice'; import { joinVoiceChannel } from '../../utils/voice';
import { SearchPopover } from '../chat/SearchPopover'; import { SearchPopover } from '../chat/SearchPopover';
import { isDmChannel, getChannelOrigin } from '../../stores/spaceStore'; import { isDmChannel, getChannelOrigin } from '../../stores/spaceStore';
@@ -83,9 +84,11 @@ export function MainContent() {
const dmChannel = dmChannels.find(dm => dm.id === currentChannelId); const dmChannel = dmChannels.find(dm => dm.id === currentChannelId);
const otherMembers = dmChannel?.members.filter(m => !isSelf(m, authUser)) ?? []; const otherMembers = dmChannel?.members.filter(m => !isSelf(m, authUser)) ?? [];
const isGroupDm = (dmChannel?.members.length ?? 0) > 2; const isGroupDm = (dmChannel?.members.length ?? 0) > 2;
const firstOther = otherMembers[0];
const { baseName: firstBaseName, domain: firstDomain } = parseFederatedUsername(firstOther?.username ?? '');
const dmName = isGroupDm const dmName = isGroupDm
? otherMembers.map(m => m.displayName ?? m.username).join(', ') ? otherMembers.map(m => m.displayName ?? parseFederatedUsername(m.username).baseName).join(', ')
: otherMembers[0]?.displayName ?? otherMembers[0]?.username ?? 'Direct Message'; : firstOther?.displayName ?? (firstBaseName || 'Direct Message');
const isInDmCall = activeDmCall?.dmChannelId === currentChannelId; const isInDmCall = activeDmCall?.dmChannelId === currentChannelId;
const isCallingThisDm = outgoingCall?.dmChannelId === currentChannelId; const isCallingThisDm = outgoingCall?.dmChannelId === currentChannelId;
@@ -166,6 +169,13 @@ export function MainContent() {
<path d="M12.5 2A6.5 6.5 0 0 0 6 8.5c0 1.82.75 3.47 1.95 4.65A10.02 10.02 0 0 0 2 22h2c0-4.42 3.58-8 8-8 .35 0 .69.03 1.03.07A6.49 6.49 0 0 0 19 8.5 6.5 6.5 0 0 0 12.5 2Zm0 11A4.5 4.5 0 1 1 17 8.5a4.5 4.5 0 0 1-4.5 4.5Z" /> <path d="M12.5 2A6.5 6.5 0 0 0 6 8.5c0 1.82.75 3.47 1.95 4.65A10.02 10.02 0 0 0 2 22h2c0-4.42 3.58-8 8-8 .35 0 .69.03 1.03.07A6.49 6.49 0 0 0 19 8.5 6.5 6.5 0 0 0 12.5 2Zm0 11A4.5 4.5 0 1 1 17 8.5a4.5 4.5 0 0 1-4.5 4.5Z" />
</svg> </svg>
<span className="font-bold text-[15px] tracking-[-0.02em] text-txt-primary truncate">{dmName}</span> <span className="font-bold text-[15px] tracking-[-0.02em] text-txt-primary truncate">{dmName}</span>
{!isGroupDm && firstDomain && (
<Tooltip content={firstOther?.username ?? ''} position="bottom">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" className="text-txt-tertiary/80 flex-shrink-0">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z" />
</svg>
</Tooltip>
)}
{isGroupDm && ( {isGroupDm && (
<span className="text-xs text-txt-tertiary flex-shrink-0">({dmChannel?.members.length} Members)</span> <span className="text-xs text-txt-tertiary flex-shrink-0">({dmChannel?.members.length} Members)</span>
)} )}
@@ -118,7 +118,7 @@ export function MobileDmsScreen() {
<div className="relative"> <div className="relative">
<Avatar <Avatar
src={avatarUrl} src={avatarUrl}
name={friend.displayName ?? friend.username} name={friend.displayName ?? parseFederatedUsername(friend.username).baseName}
avatarColor={friend.avatarColor} avatarColor={friend.avatarColor}
size={40} size={40}
/> />
@@ -129,7 +129,7 @@ export function MobileDmsScreen() {
}`} /> }`} />
</div> </div>
<span className="text-[10px] text-txt-secondary truncate w-full text-center"> <span className="text-[10px] text-txt-secondary truncate w-full text-center">
{friend.displayName ?? friend.username} {friend.displayName ?? parseFederatedUsername(friend.username).baseName}
</span> </span>
</button> </button>
); );
@@ -144,8 +144,8 @@ export function MobileDmsScreen() {
const otherMembers = dm.members.filter(m => m.id !== authUser?.id); const otherMembers = dm.members.filter(m => m.id !== authUser?.id);
const isGroup = dm.members.length > 2; const isGroup = dm.members.length > 2;
const name = isGroup const name = isGroup
? otherMembers.map(m => m.displayName ?? m.username).join(', ') ? otherMembers.map(m => m.displayName ?? parseFederatedUsername(m.username).baseName).join(', ')
: otherMembers[0]?.displayName ?? otherMembers[0]?.username ?? 'Unknown'; : otherMembers[0]?.displayName ?? (parseFederatedUsername(otherMembers[0]?.username ?? '').baseName || 'Unknown');
const lastMsgId = dm.lastMessage?.id; const lastMsgId = dm.lastMessage?.id;
const readState = readStates.get(dm.id); const readState = readStates.get(dm.id);
@@ -207,7 +207,7 @@ export function MobileDmsScreen() {
{preview && ( {preview && (
<p className={`text-xs truncate mt-0.5 ${isUnread ? 'text-txt-secondary font-medium' : 'text-txt-tertiary'}`}> <p className={`text-xs truncate mt-0.5 ${isUnread ? 'text-txt-secondary font-medium' : 'text-txt-tertiary'}`}>
{previewSender && previewSender.id !== authUser?.id {previewSender && previewSender.id !== authUser?.id
? `${previewSender.displayName ?? previewSender.username}: ${preview}` ? `${previewSender.displayName ?? parseFederatedUsername(previewSender.username).baseName}: ${preview}`
: preview} : preview}
</p> </p>
)} )}
+8 -7
View File
@@ -15,13 +15,14 @@ export function Username({ username, showAt, className, style }: UsernameProps)
return <span className={className} style={style}>{prefix}{username}</span>; return <span className={className} style={style}>{prefix}{username}</span>;
} }
const name = username.slice(0, atIndex); const name = username.slice(0, atIndex);
const domain = username.slice(atIndex + 1);
return ( return (
<Tooltip content={`${prefix}${username}`} position="top"> <span className={`inline-flex items-center gap-0.5 ${className ?? ''}`} style={style}>
<span className={className} style={style}> {prefix}{name}
{prefix}{name} <Tooltip content={`${prefix}${username}`} position="top">
<span className="text-txt-tertiary text-[0.8em] ml-0.5 font-normal">@{domain}</span> <svg width="0.85em" height="0.85em" viewBox="0 0 24 24" fill="currentColor" className="text-txt-tertiary/70 flex-shrink-0 inline-block align-[-0.05em]">
</span> <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z" />
</Tooltip> </svg>
</Tooltip>
</span>
); );
} }