fix: consistent avatar gradient colors across all 17 call sites
Avatar fallback gradients were hashed by display name alone when no user prop was passed, causing the same person to appear in different colors across messages, replies, member lists, voice panels, etc. Added userId prop to Avatar and wired it through all 17 call sites so the gradient always hashes by user ID.
This commit is contained in:
@@ -231,7 +231,7 @@ function FriendItem({ friend, onRemove, onDm }: { friend: Friend, onRemove: () =
|
|||||||
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} />
|
<Avatar src={friend.avatar} name={friend.displayName ?? friend.username} size={32} status={friend.status} userId={friend.id} />
|
||||||
<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]">{friend.displayName ?? friend.username}</span>
|
||||||
@@ -277,7 +277,7 @@ function RequestItem({ request, type, onAccept, onDecline, onCancel }: {
|
|||||||
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} />
|
<Avatar src={user.avatar} name={user.displayName ?? user.username} size={32} status={user.status as any} userId={user.id} />
|
||||||
<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">{user.displayName ?? user.username}</span>
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export function MentionPopover({ query, selectedIndex, onSelect }: MentionPopove
|
|||||||
name={displayName}
|
name={displayName}
|
||||||
size={24}
|
size={24}
|
||||||
status={member.user.status}
|
status={member.user.status}
|
||||||
|
userId={member.user.id}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="text-[14px] font-medium truncate"
|
className="text-[14px] font-medium truncate"
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export function Message({ message, isCompact, isFirstInGroup }: MessageProps) {
|
|||||||
<div className="flex-1 min-w-0 pr-4">
|
<div className="flex-1 min-w-0 pr-4">
|
||||||
{message.replyTo && (
|
{message.replyTo && (
|
||||||
<div className="flex items-center gap-1 mb-1 ml-[-4px] opacity-80 hover:opacity-100 cursor-pointer group/reply">
|
<div className="flex items-center gap-1 mb-1 ml-[-4px] opacity-80 hover:opacity-100 cursor-pointer group/reply">
|
||||||
<Avatar src={message.replyTo.user.avatar} name={message.replyTo.user.username} size={16} />
|
<Avatar src={message.replyTo.user.avatar} name={message.replyTo.user.username} size={16} user={message.replyTo.user} />
|
||||||
<span
|
<span
|
||||||
className="text-[14px] font-bold text-txt-primary hover:underline"
|
className="text-[14px] font-bold text-txt-primary hover:underline"
|
||||||
style={message.replyTo ? replyRoleColor(message.replyTo) : undefined}
|
style={message.replyTo ? replyRoleColor(message.replyTo) : undefined}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ function WelcomeHeader({ channelId }: { channelId: string }) {
|
|||||||
return (
|
return (
|
||||||
<div className="px-4 pt-8 pb-4">
|
<div className="px-4 pt-8 pb-4">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<Avatar src={otherUser?.avatar} name={displayName} size={80} />
|
<Avatar src={otherUser?.avatar} name={displayName} size={80} userId={otherUser?.id} />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-[32px] leading-10 font-bold text-txt-primary">{displayName}</h3>
|
<h3 className="text-[32px] leading-10 font-bold text-txt-primary">{displayName}</h3>
|
||||||
<p className="text-txt-secondary text-[14px] mt-1">
|
<p className="text-txt-secondary text-[14px] mt-1">
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export function ActivityPanel() {
|
|||||||
size={32}
|
size={32}
|
||||||
status={isOffline ? 'offline' : friend.status}
|
status={isOffline ? 'offline' : friend.status}
|
||||||
className={isOffline ? 'opacity-60' : undefined}
|
className={isOffline ? 'opacity-60' : undefined}
|
||||||
|
userId={friend.id}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className={`text-[13.5px] leading-[1.2] font-medium truncate ${isOffline ? 'text-txt-tertiary' : 'text-txt-primary'}`}>
|
<div className={`text-[13.5px] leading-[1.2] font-medium truncate ${isOffline ? 'text-txt-tertiary' : 'text-txt-primary'}`}>
|
||||||
|
|||||||
@@ -215,12 +215,12 @@ export function ChannelSidebar() {
|
|||||||
zIndex: 2 - i,
|
zIndex: 2 - i,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar src={m.avatar} name={m.displayName ?? m.username} size={22} />
|
<Avatar src={m.avatar} name={m.displayName ?? m.username} size={22} userId={m.id} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Avatar src={otherMembers[0]?.avatar} name={otherMembers[0]?.displayName ?? otherMembers[0]?.username ?? ''} size={32} status={otherMembers[0]?.status as any} />
|
<Avatar src={otherMembers[0]?.avatar} name={otherMembers[0]?.displayName ?? otherMembers[0]?.username ?? ''} size={32} status={otherMembers[0]?.status as any} userId={otherMembers[0]?.id} />
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className={`text-[15px] truncate leading-tight ${
|
<div className={`text-[15px] truncate leading-tight ${
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ export function MemberSidebar() {
|
|||||||
size={32}
|
size={32}
|
||||||
status={isOffline ? 'offline' : member.user.status}
|
status={isOffline ? 'offline' : member.user.status}
|
||||||
className={isOffline ? 'opacity-60' : undefined}
|
className={isOffline ? 'opacity-60' : undefined}
|
||||||
|
user={member.user}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export function AddDmMemberModal() {
|
|||||||
disabled={isAdding}
|
disabled={isAdding}
|
||||||
className="w-full flex items-center gap-3 px-3 py-2 rounded-[4px] hover:bg-interactive-hover transition-colors text-left disabled:opacity-50"
|
className="w-full flex items-center gap-3 px-3 py-2 rounded-[4px] hover:bg-interactive-hover transition-colors text-left disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<Avatar src={user.avatar} name={user.displayName ?? user.username} size={36} status={user.status as any} />
|
<Avatar src={user.avatar} name={user.displayName ?? user.username} size={36} status={user.status as any} userId={user.id} />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-[14px] font-medium text-txt-primary truncate">
|
<div className="text-[14px] font-medium text-txt-primary truncate">
|
||||||
{user.displayName ?? user.username}
|
{user.displayName ?? user.username}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export function NewDmModal() {
|
|||||||
onClick={() => handleSelectUser(user)}
|
onClick={() => handleSelectUser(user)}
|
||||||
className="w-full flex items-center gap-3 px-3 py-2 rounded-[4px] hover:bg-interactive-hover transition-colors text-left"
|
className="w-full flex items-center gap-3 px-3 py-2 rounded-[4px] hover:bg-interactive-hover transition-colors text-left"
|
||||||
>
|
>
|
||||||
<Avatar src={user.avatar} name={user.displayName ?? user.username} size={36} status={user.status as any} />
|
<Avatar src={user.avatar} name={user.displayName ?? user.username} size={36} status={user.status as any} userId={user.id} />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-[14px] font-medium text-txt-primary truncate">
|
<div className="text-[14px] font-medium text-txt-primary truncate">
|
||||||
{user.displayName ?? user.username}
|
{user.displayName ?? user.username}
|
||||||
|
|||||||
@@ -428,6 +428,7 @@ export function ServerSettingsModal() {
|
|||||||
name={displayName}
|
name={displayName}
|
||||||
size={32}
|
size={32}
|
||||||
status={member.user.status}
|
status={member.user.status}
|
||||||
|
user={member.user}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium">{displayName}</div>
|
<div className="text-sm font-medium">{displayName}</div>
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export function UserSettingsModal() {
|
|||||||
name={user.displayName ?? user.username}
|
name={user.displayName ?? user.username}
|
||||||
size={64}
|
size={64}
|
||||||
status={user.status}
|
status={user.status}
|
||||||
|
userId={user.id}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-bold text-lg">{user.displayName ?? user.username}</div>
|
<div className="font-bold text-lg">{user.displayName ?? user.username}</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface AvatarProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
onClick?: (e: React.MouseEvent) => void;
|
onClick?: (e: React.MouseEvent) => void;
|
||||||
user?: User;
|
user?: User;
|
||||||
|
userId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusColors: Record<string, string> = {
|
const statusColors: Record<string, string> = {
|
||||||
@@ -20,11 +21,11 @@ const statusColors: Record<string, string> = {
|
|||||||
offline: 'bg-status-offline',
|
offline: 'bg-status-offline',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Avatar({ src, name, size = 40, status, className = '', onClick, user }: AvatarProps) {
|
export function Avatar({ src, name, size = 40, status, className = '', onClick, user, userId }: AvatarProps) {
|
||||||
const openUserProfile = useUIStore((s) => s.openUserProfile);
|
const openUserProfile = useUIStore((s) => s.openUserProfile);
|
||||||
const initials = name.charAt(0).toUpperCase();
|
const initials = name.charAt(0).toUpperCase();
|
||||||
const fontSize = size < 32 ? 'text-xs' : size < 48 ? 'text-sm' : 'text-lg';
|
const fontSize = size < 32 ? 'text-xs' : size < 48 ? 'text-sm' : 'text-lg';
|
||||||
const gradient = getAvatarGradient(user?.id, name);
|
const gradient = getAvatarGradient(userId ?? user?.id, name);
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent) => {
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
|
|||||||
@@ -269,6 +269,7 @@ export function PictureInPicture() {
|
|||||||
<Avatar
|
<Avatar
|
||||||
name={displayParticipant.username}
|
name={displayParticipant.username}
|
||||||
size={64}
|
size={64}
|
||||||
|
userId={displayParticipant.userId}
|
||||||
/>
|
/>
|
||||||
{speakingParticipantIds.has(displayParticipant.identity) && (
|
{speakingParticipantIds.has(displayParticipant.identity) && (
|
||||||
<div className="absolute -inset-1 rounded-full ring-2 ring-status-online animate-pulse" />
|
<div className="absolute -inset-1 rounded-full ring-2 ring-status-online animate-pulse" />
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex flex-col items-center justify-center gap-3 bg-surface-channel">
|
<div className="w-full h-full flex flex-col items-center justify-center gap-3 bg-surface-channel">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar src={null} name={participant.username} size={large ? 80 : 48} />
|
<Avatar src={null} name={participant.username} size={large ? 80 : 48} userId={participant.userId} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center px-4">
|
<div className="text-center px-4">
|
||||||
<p className="text-txt-primary text-sm font-semibold">
|
<p className="text-txt-primary text-sm font-semibold">
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export function VoiceChannel({ channelId, channelName, onClick }: VoiceChannelPr
|
|||||||
name={displayName}
|
name={displayName}
|
||||||
size={20}
|
size={20}
|
||||||
status={status}
|
status={status}
|
||||||
|
userId={userId}
|
||||||
/>
|
/>
|
||||||
<span className="text-[13px] text-txt-secondary truncate flex-1 min-w-0">{displayName}</span>
|
<span className="text-[13px] text-txt-secondary truncate flex-1 min-w-0">{displayName}</span>
|
||||||
{/* Status badges */}
|
{/* Status badges */}
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
|
|||||||
src={null}
|
src={null}
|
||||||
name={participant.username}
|
name={participant.username}
|
||||||
size={large ? 100 : 64}
|
size={large ? 100 : 64}
|
||||||
|
userId={participant.userId}
|
||||||
/>
|
/>
|
||||||
{isSpeaking && (
|
{isSpeaking && (
|
||||||
<div className="absolute -inset-1.5 rounded-full ring-[3px] ring-status-online animate-pulse" />
|
<div className="absolute -inset-1.5 rounded-full ring-[3px] ring-status-online animate-pulse" />
|
||||||
|
|||||||
Reference in New Issue
Block a user