SkateSpots/src/components/TrustBadge.tsx
SkateSpots 2d1144bb7e feat(trust): batch provider reads, neutral direct follows, flagged notice
Second pass of TRUST_SCORE.md.

- useTrustAssertions now batches: every TrustBadge registers its subject into
  a shared per-viewer union and ONE query fetches the union (no per-card
  query), with a debounced refetch when a list grows. Same hook API.
- A direct follow (or self) is no longer shown as Verified - only a
  friend-of-friend or a provider assertion counts; Flagged is never
  suppressed.
- useUserReports reads public NIP-56 reports for an account, and
  TrustFlaggedNotice shows a flagged counterparty with the reporters grouped
  by the viewer's network plus Report / Dismiss. Wired into the listing
  seller card and the coach detail page.
- New walletUi.trust.* keys (en/es).
2026-10-09 17:17:11 +07:00

101 lines
3.3 KiB
TypeScript

import { Badge } from '@/components/ui/badge';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { CheckCircle2, HelpCircle, Flag } from 'lucide-react';
import { useTrustBucket } from '@/hooks/useTrustBucket';
import { bucketDescKey, bucketLabelKey, type TrustBucket } from '@/lib/trustLadder';
import { cn } from '@/lib/utils';
import { useTranslation } from '@/contexts/language';
interface TrustBadgeProps {
pubkey: string | undefined;
className?: string;
showLabel?: boolean;
size?: 'sm' | 'md' | 'lg';
/**
* Also render the neutral "Unknown" bucket. Off by default so a card row is
* not flooded with a badge on every author; surfaces with room can turn it on.
*/
showUnknown?: boolean;
}
const BUCKET_STYLE: Record<
TrustBucket,
{ className: string; Icon: typeof CheckCircle2 }
> = {
verified: {
className: 'border-green-500/50 text-green-400 bg-green-500/10',
Icon: CheckCircle2,
},
unknown: {
className: 'border-gray-500/50 text-gray-400',
Icon: HelpCircle,
},
flagged: {
className: 'border-red-500/60 text-red-400 bg-red-500/10',
Icon: Flag,
},
};
/**
* TrustBadge shows one of three buckets for an author:
* verified (someone in your network vouches), unknown (nothing said yet), or
* flagged (trusted people reported them). Prefers a NIP-85 assertion, falls
* back to the local web-of-trust score. Flagged always shows.
*/
export function TrustBadge({
pubkey,
className,
showLabel = true,
size = 'sm',
showUnknown = false,
}: TrustBadgeProps) {
const { t } = useTranslation();
const { bucket, assertion, directFollow, isLoading } = useTrustBucket(pubkey);
if (isLoading) return null;
// A direct follow (or yourself) is already known to the viewer — a "Verified"
// mark there is redundant. Flagged is a safety signal and is never suppressed.
if (bucket === 'verified' && directFollow) return null;
if (bucket === 'unknown' && !showUnknown) return null;
const { className: bucketClass, Icon } = BUCKET_STYLE[bucket];
const sizeClasses = {
sm: 'text-[9px] px-1.5 py-0 h-4 gap-0.5',
md: 'text-xs px-2 py-0.5 h-5 gap-1',
lg: 'text-sm px-3 py-1 h-6 gap-1.5',
};
const iconSizes = {
sm: 'h-2.5 w-2.5',
md: 'h-3 w-3',
lg: 'h-4 w-4',
};
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Badge
variant="outline"
className={cn(sizeClasses[size], bucketClass, className, 'cursor-help select-none')}
>
<Icon className={iconSizes[size]} />
{showLabel && <span className="font-bold uppercase">{t(bucketLabelKey(bucket))}</span>}
</Badge>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
<div className="space-y-1">
<p className="font-bold">{t(bucketLabelKey(bucket))}</p>
<p className="text-xs text-muted-foreground">{t(bucketDescKey(bucket))}</p>
{bucket === 'flagged' && assertion?.reporters !== undefined && assertion.reporters > 0 && (
<p className="text-xs text-muted-foreground">
{t('walletUi.trust.reportedByTrusted', { count: assertion.reporters })}
</p>
)}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}