Files
eranos/src/components/ProfileHoverCard.tsx
T
Alex Gleason 64cf10f381 Fix useProfileUrl to call useNip05Verify directly instead of peeking cache
The cache-peek approach always returned npub because the cache was empty
at render time. Replace it with a proper hook that calls useNip05Verify,
relying on TanStack Query's deduplication to avoid redundant requests.

Also fix the dot separator in NoteCard to hide when NIP-05 is unverified,
and refactor ProfileSearchDropdown to pass the verified URL through
onClick rather than recomputing it in an event handler.
2026-02-23 19:39:13 -06:00

136 lines
4.7 KiB
TypeScript

import { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { HoverCard, HoverCardTrigger, HoverCardContent } from '@/components/ui/hover-card';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { ExternalFavicon } from '@/components/ExternalFavicon';
import { EmojifiedText } from '@/components/CustomEmoji';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
import { formatNip05Display, getNip05Domain } from '@/lib/nip05';
import { useNip05Verify } from '@/hooks/useNip05Verify';
import { useProfileUrl } from '@/hooks/useProfileUrl';
import { FollowButton } from '@/components/FollowButton';
import { cn } from '@/lib/utils';
interface ProfileHoverCardProps {
pubkey: string;
children: React.ReactNode;
/** If true, the trigger element won't be wrapped in anything extra */
asChild?: boolean;
}
/**
* Inner content component — mounts only when the hover card is open.
* Triggers a background refetch of the author's profile on mount.
*/
function ProfileHoverCardBody({ pubkey }: { pubkey: string }) {
const queryClient = useQueryClient();
const author = useAuthor(pubkey);
const metadata = author.data?.metadata;
const displayName = metadata?.name ?? genUserName(pubkey);
const profileUrl = useProfileUrl(pubkey, metadata);
const nip05 = metadata?.nip05;
const nip05Domain = getNip05Domain(nip05);
const { data: nip05Verified } = useNip05Verify(nip05, pubkey);
const nip05Display = nip05Verified && nip05 ? formatNip05Display(nip05) : undefined;
useEffect(() => {
queryClient.refetchQueries({ queryKey: ['author', pubkey] });
}, [pubkey, queryClient]);
return (
<>
{/* Mini banner */}
<div className="h-16 bg-secondary relative">
{metadata?.banner && (
<img
src={metadata.banner}
alt=""
className="w-full h-full object-cover"
loading="lazy"
/>
)}
{/* Follow button over the banner */}
<div className="absolute top-2 right-2">
<FollowButton pubkey={pubkey} size="sm" />
</div>
</div>
{/* Profile info */}
<div className="px-4 pb-4">
{/* Avatar overlapping the banner */}
<div className="-mt-8 mb-2">
<Link to={profileUrl} onClick={(e) => e.stopPropagation()}>
<Avatar className="size-16 border-3 border-background">
<AvatarImage src={metadata?.picture} alt={displayName} />
<AvatarFallback className="bg-primary/20 text-primary text-lg">
{displayName[0]?.toUpperCase()}
</AvatarFallback>
</Avatar>
</Link>
</div>
{/* Name + NIP-05 */}
<Link
to={profileUrl}
className="font-bold text-[15px] hover:underline block truncate"
onClick={(e) => e.stopPropagation()}
>
{author.data?.event ? (
<EmojifiedText tags={author.data.event.tags}>{displayName}</EmojifiedText>
) : displayName}
</Link>
{nip05Display && (
<div className="flex items-center gap-1 text-sm text-muted-foreground mt-0.5">
<span className="truncate">@{nip05Display}</span>
{nip05Domain && (
<ExternalFavicon url={`https://${nip05Domain}`} size={14} className="shrink-0" />
)}
</div>
)}
{metadata?.bot && (
<span className="text-xs text-primary mt-1 inline-block" title="Bot account">Bot</span>
)}
{/* Bio */}
{metadata?.about && (
<p className={cn(
'text-sm text-muted-foreground mt-2 whitespace-pre-wrap break-words',
'line-clamp-3',
)}>
{author.data?.event ? (
<EmojifiedText tags={author.data.event.tags}>{metadata.about}</EmojifiedText>
) : metadata.about}
</p>
)}
</div>
</>
);
}
/**
* Wraps any element with a hover card that shows a profile preview.
* Shows avatar, display name, NIP-05, and bio on hover.
*/
export function ProfileHoverCard({ pubkey, children, asChild }: ProfileHoverCardProps) {
return (
<HoverCard openDelay={300} closeDelay={150}>
<HoverCardTrigger asChild={asChild}>
{children}
</HoverCardTrigger>
<HoverCardContent
side="bottom"
align="start"
sideOffset={8}
className="w-72 p-0 rounded-2xl overflow-hidden border border-border shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<ProfileHoverCardBody pubkey={pubkey} />
</HoverCardContent>
</HoverCard>
);
}