import { useState, useEffect, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { useNostr } from '@nostrify/react'; import { useQuery } from '@tanstack/react-query'; import { Check, Copy, QrCode, ExternalLink, Bitcoin } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Skeleton } from '@/components/ui/skeleton'; import { useToast } from '@/hooks/useToast'; import { nip19 } from 'nostr-tools'; import type { NostrEvent } from '@nostrify/nostrify'; import QRCode from 'qrcode'; interface ProfileField { label: string; value: string; } interface ProfileRightSidebarProps { pubkey: string; fields?: ProfileField[]; } interface MediaItem { url: string; eventId: string; authorPubkey: string; } /** Extracts image URLs from content. */ function extractImageUrls(content: string): string[] { const regex = /https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg)(\?[^\s]*)?/gi; return content.match(regex) || []; } /** Extracts video URLs from content. */ function extractVideoUrls(content: string): string[] { const regex = /https?:\/\/[^\s]+\.(mp4|webm|mov)(\?[^\s]*)?/gi; return content.match(regex) || []; } /** Hook to query media from a user's posts, returning URL + event ID pairs. */ function useProfileMedia(pubkey: string) { const { nostr } = useNostr(); return useQuery({ queryKey: ['profile-media', pubkey], queryFn: async ({ signal }) => { const events = await nostr.query( [{ kinds: [1], authors: [pubkey], limit: 50 }], { signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]) }, ); const items: MediaItem[] = []; const seen = new Set(); for (const event of events) { const images = extractImageUrls(event.content); const videos = extractVideoUrls(event.content); for (const url of [...images, ...videos]) { if (!seen.has(url)) { seen.add(url); items.push({ url, eventId: event.id, authorPubkey: event.pubkey }); } } } return items.slice(0, 9); }, enabled: !!pubkey, staleTime: 5 * 60 * 1000, }); } /** Build a nevent link for navigating to an event. */ function eventLink(item: MediaItem): string { return `/${nip19.neventEncode({ id: item.eventId, author: item.authorPubkey })}`; } /** Try multiple favicon paths: /favicon.ico, then /favicon.svg. */ function Favicon({ url }: { url: string }) { const candidates = useMemo(() => { try { const origin = new URL(url).origin; return [`${origin}/favicon.ico`, `${origin}/favicon.svg`]; } catch { return []; } }, [url]); const [index, setIndex] = useState(0); const [failed, setFailed] = useState(false); if (candidates.length === 0 || failed) return null; const src = candidates[index]; return ( { if (index < candidates.length - 1) { setIndex(index + 1); } else { setFailed(true); } }} /> ); } /** Bitcoin QR code modal */ function BitcoinQRModal({ address }: { address: string }) { const [qrUrl, setQrUrl] = useState(''); const [copied, setCopied] = useState(false); const { toast } = useToast(); useEffect(() => { QRCode.toDataURL(`bitcoin:${address}`, { width: 280, margin: 2, color: { dark: '#000000', light: '#FFFFFF' }, }).then(setQrUrl).catch(console.error); }, [address]); const handleCopy = async () => { await navigator.clipboard.writeText(address); setCopied(true); toast({ title: 'Copied', description: 'Bitcoin address copied to clipboard' }); setTimeout(() => setCopied(false), 2000); }; return (
Bitcoin
{/* QR Code */}
{qrUrl ? ( Bitcoin QR ) : (
)}
{/* Address + Copy */}
); } /** A single profile field row. Handles $BTC specially. */ function ProfileFieldRow({ field }: { field: ProfileField }) { const [copied, setCopied] = useState(false); const { toast } = useToast(); const isBtc = field.label === '$BTC'; const handleCopy = async () => { await navigator.clipboard.writeText(field.value); setCopied(true); toast({ title: 'Copied', description: 'Bitcoin address copied to clipboard' }); setTimeout(() => setCopied(false), 2000); }; if (isBtc) { return (
Bitcoin
{field.value}
); } // Regular field: label + linked value with favicon const isUrl = field.value.startsWith('http://') || field.value.startsWith('https://'); return (
{field.label}
{isUrl ? ( {field.value.replace(/^https?:\/\//, '')} ) : (

{field.value}

)}
); } export function ProfileRightSidebar({ pubkey, fields }: ProfileRightSidebarProps) { const { data: media, isLoading: mediaLoading } = useProfileMedia(pubkey); return ( ); }