import { useState, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { Repeat2, Quote, Heart, Zap, X } from 'lucide-react'; import { nip19 } from 'nostr-tools'; import { Dialog, DialogContent, DialogTitle, } from '@/components/ui/dialog'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Skeleton } from '@/components/ui/skeleton'; import { useEventInteractions, type RepostEntry, type QuoteEntry, type ReactionEntry, type ZapEntry } from '@/hooks/useEventInteractions'; import { useAuthor } from '@/hooks/useAuthor'; import { genUserName } from '@/lib/genUserName'; import { timeAgo } from '@/lib/timeAgo'; import { cn } from '@/lib/utils'; export type InteractionTab = 'reposts' | 'quotes' | 'reactions' | 'zaps'; interface InteractionsModalProps { eventId: string; open: boolean; onOpenChange: (open: boolean) => void; /** Which tab to show initially. */ initialTab?: InteractionTab; } /** Formats a sats amount into a compact human-readable string. */ function formatSats(sats: number): string { if (sats >= 1_000_000) return `${(sats / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`; if (sats >= 1_000) return `${(sats / 1_000).toFixed(1).replace(/\.0$/, '')}K`; return sats.toString(); } export function InteractionsModal({ eventId, open, onOpenChange, initialTab = 'reposts' }: InteractionsModalProps) { const [activeTab, setActiveTab] = useState(initialTab); const { data, isLoading } = useEventInteractions(open ? eventId : undefined); // Reset tab to initialTab when modal opens with a different tab const handleOpenChange = (value: boolean) => { if (value) setActiveTab(initialTab); onOpenChange(value); }; const repostCount = data?.reposts.length ?? 0; const quoteCount = data?.quotes.length ?? 0; const reactionCount = data?.reactions.length ?? 0; const zapCount = data?.zaps.length ?? 0; const tabConfig: { key: InteractionTab; label: string; count: number; icon: React.ReactNode }[] = [ { key: 'reposts', label: 'Reposts', count: repostCount, icon: }, { key: 'quotes', label: 'Quotes', count: quoteCount, icon: }, { key: 'reactions', label: 'Reactions', count: reactionCount, icon: }, { key: 'zaps', label: 'Zaps', count: zapCount, icon: }, ]; return ( {/* Header */}
Post interactions
{/* Tab bar */}
{tabConfig.map(({ key, label, count, icon }) => ( ))}
{/* Content */} {isLoading ? (
{Array.from({ length: 4 }).map((_, i) => ( ))}
) : activeTab === 'reposts' ? ( ) : activeTab === 'quotes' ? ( ) : activeTab === 'reactions' ? ( ) : ( )}
); } /* ──── Reposts Tab ──── */ function RepostsTab({ reposts }: { reposts: RepostEntry[] }) { if (reposts.length === 0) { return ; } return (
{reposts.map((repost, i) => ( ))}
); } /* ──── Quotes Tab ──── */ function QuotesTab({ quotes }: { quotes: QuoteEntry[] }) { if (quotes.length === 0) { return ; } return (
{quotes.map((quote, i) => ( ))}
); } /* ──── Reactions Tab ──── */ function ReactionsTab({ reactions }: { reactions: ReactionEntry[] }) { // Group reactions by emoji const grouped = useMemo(() => { const groups = new Map(); for (const r of reactions) { const key = r.emoji; if (!groups.has(key)) groups.set(key, []); groups.get(key)!.push(r); } // Sort groups by count (most popular first) return Array.from(groups.entries()).sort((a, b) => b[1].length - a[1].length); }, [reactions]); if (reactions.length === 0) { return ; } return (
{grouped.map(([emoji, entries]) => (
{/* Emoji group header */}
{emoji} {entries.length}
{/* Users who reacted with this emoji */}
{entries.map((entry, i) => ( ))}
))}
); } /* ──── Zaps Tab ──── */ function ZapsTab({ zaps }: { zaps: ZapEntry[] }) { if (zaps.length === 0) { return ; } const totalSats = zaps.reduce((sum, z) => sum + z.amountSats, 0); return (
{/* Total */}
{formatSats(totalSats)} sats from {zaps.length} zap{zaps.length !== 1 ? 's' : ''}
{zaps.map((zap, i) => ( ))}
); } /* ──── Shared Row Components ──── */ function UserRow({ pubkey, subtitle }: { pubkey: string; subtitle?: string }) { const author = useAuthor(pubkey); const metadata = author.data?.metadata; const displayName = metadata?.name || genUserName(pubkey); const npub = useMemo(() => nip19.npubEncode(pubkey), [pubkey]); return ( {displayName[0].toUpperCase()}
{displayName} {metadata?.nip05 && ( @{metadata.nip05} )}
{subtitle && ( {subtitle} )}
); } function ZapRow({ zap }: { zap: ZapEntry }) { const author = useAuthor(zap.senderPubkey); const metadata = author.data?.metadata; const displayName = metadata?.name || genUserName(zap.senderPubkey); const npub = useMemo(() => nip19.npubEncode(zap.senderPubkey), [zap.senderPubkey]); return ( {displayName[0].toUpperCase()}
{displayName} {metadata?.nip05 && ( @{metadata.nip05} )}
{zap.message && (

{zap.message}

)}
{/* Zap amount badge */}
{formatSats(zap.amountSats)}
); } function QuoteRow({ quote }: { quote: QuoteEntry }) { const author = useAuthor(quote.pubkey); const metadata = author.data?.metadata; const displayName = metadata?.name || genUserName(quote.pubkey); const nevent = useMemo(() => nip19.neventEncode({ id: quote.eventId, author: quote.pubkey }), [quote.eventId, quote.pubkey]); return ( {displayName[0].toUpperCase()}
{displayName} {metadata?.nip05 && ( @{metadata.nip05} )}
{quote.content && (

{quote.content}

)} {timeAgo(quote.createdAt)}
); } function EmptyState({ message }: { message: string }) { return (
{message}
); } function InteractionRowSkeleton() { return (
); }