import { ShieldCheck, Loader2, HeartHandshake, Users } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; import { Link } from 'react-router-dom'; import { nip19 } from 'nostr-tools'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAuthor } from '@/hooks/useAuthor'; import { useVouches, useVouchFor } from '@/hooks/useVouches'; import { useToast } from '@/hooks/useToast'; import { useTranslation } from '@/contexts/language'; import { genUserName } from '@/lib/genUserName'; import { cn } from '@/lib/utils'; interface VouchCardProps { /** Provider coordinate `kind:pubkey:d`. */ coord: string; /** Provider pubkey (to hide the button for the owner). */ providerPubkey: string; /** Provider display name. */ providerName: string; accentText?: string; accentBg?: string; } function VoucherAvatar({ pubkey }: { pubkey: string }) { const author = useAuthor(pubkey); const meta = author.data?.metadata; const name = meta?.name ?? genUserName(pubkey); const npub = nip19.npubEncode(pubkey); return ( {name.slice(0, 2).toUpperCase()} {name} ); } /** * The community anti-fraud primitive. Skaters vouch for real providers; the * verdict is trust-weighted so fake profiles with only cold/fake vouches read as * "new / unvouched" while genuinely-known people surface as scene-verified. */ export function VouchCard({ coord, providerPubkey, providerName, accentText = 'text-teal-400', accentBg = 'bg-teal-500 hover:bg-teal-600', }: VouchCardProps) { const { t } = useTranslation(); const { user } = useCurrentUser(); const { data, isLoading } = useVouches(coord); const { mutateAsync: vouch, isPending } = useVouchFor(); const { toast } = useToast(); const isOwner = user?.pubkey.toLowerCase() === providerPubkey.toLowerCase(); // Trust tier from the trust-weighted score. const tier = data.trustedVouchers >= 3 || data.weightedScore >= 150 ? 'verified' : data.count > 0 ? 'vouched' : 'new'; const handleVouch = async () => { try { await vouch({ coord, providerName, retract: data.viewerVouched }); toast({ title: data.viewerVouched ? t('socialUi.vouch.removedTitle') : t('socialUi.vouch.vouchedTitle'), description: data.viewerVouched ? t('socialUi.vouch.removedDesc', { name: providerName }) : t('socialUi.vouch.vouchedDesc', { name: providerName }), }); } catch (error) { toast({ title: t('socialUi.vouch.couldNotVouch'), description: error instanceof Error ? error.message : t('common.tryAgain'), variant: 'destructive', }); } }; return ( {t('socialUi.vouch.title')} {tier === 'verified' && ( {t('socialUi.vouch.sceneVerified')} )} {tier === 'vouched' && ( {t('socialUi.vouch.vouched')} )} {tier === 'new' && ( {t('socialUi.vouch.newUnvouched')} )} {isLoading ? ( {t('socialUi.vouch.checking')} ) : data.count === 0 ? ( {t('socialUi.vouch.empty')} ) : ( {data.vouchers.slice(0, 6).map((pk) => ( ))} {data.count === 1 ? t('socialUi.vouch.countOne', { count: data.count }) : t('socialUi.vouch.countMany', { count: data.count })} {data.trustedVouchers > 0 && ( 🛡 {t('socialUi.vouch.trusted', { count: data.trustedVouchers })} )} )} {user && !isOwner && ( {isPending ? ( ) : ( )} {data.viewerVouched ? t('socialUi.vouch.remove') : t('socialUi.vouch.vouchFor', { name: providerName.split(' ')[0] })} )} {isOwner && ( {t('socialUi.vouch.ownerNote')} )} ); }
{t('socialUi.vouch.title')}
{t('socialUi.vouch.checking')}
{t('socialUi.vouch.empty')}
🛡 {t('socialUi.vouch.trusted', { count: data.trustedVouchers })}
{t('socialUi.vouch.ownerNote')}