feat: Improve avatar loading experience by displaying placeholders until images are fully loaded across various components.
This commit is contained in:
@@ -33,6 +33,10 @@ export function ChatHeader({
|
||||
}: ChatHeaderProps) {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
const [avatarLoaded, setAvatarLoaded] = useState(false);
|
||||
|
||||
const placeholder = '/assets/icons/user-placeholder-icon.svg';
|
||||
const isPlaceholderAvatar = !avatar || avatar === placeholder;
|
||||
|
||||
const menuItems: DropdownMenuItem[] = [
|
||||
{
|
||||
@@ -118,15 +122,24 @@ export function ChatHeader({
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Avatar */}
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className="w-[50px] h-[50px] rounded-full overflow-hidden border border-[#00293D]/20">
|
||||
<Image
|
||||
src={avatar}
|
||||
alt={name}
|
||||
width={50}
|
||||
height={50}
|
||||
className="object-cover rounded-full"
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
/>
|
||||
<div className="w-[50px] h-[50px] rounded-full overflow-hidden border border-[#00293D]/20 relative">
|
||||
{(!avatar || !avatarLoaded || isPlaceholderAvatar) && (
|
||||
<Image
|
||||
src={placeholder}
|
||||
alt={name}
|
||||
width={50}
|
||||
height={50}
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
{avatar && !isPlaceholderAvatar && (
|
||||
<img
|
||||
src={avatar}
|
||||
alt={name}
|
||||
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-200 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||
onLoad={() => setAvatarLoaded(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isOnline && (
|
||||
<div className="absolute bottom-0 right-0 w-3 h-3 bg-green-500 rounded-full border-2 border-white" />
|
||||
|
||||
@@ -80,6 +80,44 @@ function formatLastSeen(lastSeenAt: string | null | undefined, isOnline: boolean
|
||||
return `Last seen ${diffDays}d ago`;
|
||||
}
|
||||
|
||||
// Avatar component with placeholder-until-loaded pattern
|
||||
function AvatarWithPlaceholder({ src, alt, size }: { src: string; alt: string; size: number }) {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const placeholder = '/assets/icons/user-placeholder-icon.svg';
|
||||
const isPlaceholder = !src || src === placeholder;
|
||||
|
||||
return (
|
||||
<>
|
||||
{(!src || !loaded) && (
|
||||
<Image
|
||||
src={placeholder}
|
||||
alt={alt}
|
||||
width={size}
|
||||
height={size}
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
{src && !isPlaceholder && (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-200 ${loaded ? 'opacity-100' : 'opacity-0'}`}
|
||||
onLoad={() => setLoaded(true)}
|
||||
/>
|
||||
)}
|
||||
{src && isPlaceholder && (
|
||||
<Image
|
||||
src={placeholder}
|
||||
alt={alt}
|
||||
width={size}
|
||||
height={size}
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Connected agent with avatar URL
|
||||
interface ConnectedAgent {
|
||||
agentProfileId: string;
|
||||
@@ -589,14 +627,11 @@ export function MessagingPage() {
|
||||
>
|
||||
{/* Avatar */}
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className="w-[50px] h-[50px] rounded-full overflow-hidden border border-[#00293D]/20">
|
||||
<Image
|
||||
<div className="w-[50px] h-[50px] rounded-full overflow-hidden border border-[#00293D]/20 relative">
|
||||
<AvatarWithPlaceholder
|
||||
src={getValidAvatarUrl(agent.avatarUrl)}
|
||||
alt={agent.name}
|
||||
width={50}
|
||||
height={50}
|
||||
className="object-cover rounded-full"
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
size={50}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -659,14 +694,11 @@ export function MessagingPage() {
|
||||
|
||||
{/* Avatar with online indicator */}
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className="w-[70px] h-[70px] rounded-full overflow-hidden border border-[#00293D]/20">
|
||||
<Image
|
||||
<div className="w-[70px] h-[70px] rounded-full overflow-hidden border border-[#00293D]/20 relative">
|
||||
<AvatarWithPlaceholder
|
||||
src={getValidAvatarUrl(conversationAvatarUrls[conversation.id])}
|
||||
alt={conversation.otherParty?.name || 'User'}
|
||||
width={70}
|
||||
height={70}
|
||||
className="object-cover rounded-full"
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
size={70}
|
||||
/>
|
||||
</div>
|
||||
{conversation.otherParty?.isOnline && (
|
||||
|
||||
Reference in New Issue
Block a user