import { useMemo, useState } from 'react'; import { useSeoMeta } from '@unhead/react'; import { Link } from 'react-router-dom'; import { nip19 } from 'nostr-tools'; import { PageHeader } from '@/components/PageHeader'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { LoginArea } from '@/components/auth/LoginArea'; import { ZapButton } from '@/components/ZapButton'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useNostrLogin } from '@nostrify/react/login'; import { hasNip44 as checkNip44 } from '@/lib/signerCapabilities'; import { useAuthor } from '@/hooks/useAuthor'; import { useFollowing } from '@/hooks/useFollowing'; import { useConversations } from '@/hooks/useDirectMessages'; import { useCrewmates, type Crewmate } from '@/hooks/useCrewmates'; import { useTranslation } from '@/contexts/language'; import { usePrivateContacts, usePrivateContactActions } from '@/hooks/usePrivateContacts'; import { useToast } from '@/hooks/useToast'; import { genUserName } from '@/lib/genUserName'; import { Users, MessageCircle, UserCheck, Lock, Star, StarOff, Search, X, Loader2, } from 'lucide-react'; import { cn } from '@/lib/utils'; function ContactRow({ pubkey, subtitle, isSaved, onToggleSave, saving, }: { pubkey: string; subtitle?: React.ReactNode; isSaved: boolean; onToggleSave: () => void; saving: boolean; }) { const { t } = useTranslation(); const author = useAuthor(pubkey); const meta = author.data?.metadata; const name = meta?.name ?? genUserName(pubkey); const npub = nip19.npubEncode(pubkey); // Build a minimal event-like object for ZapButton (needs pubkey). const zapTarget = author.data?.event; return (
{name.slice(0, 2).toUpperCase()}
{name} {subtitle &&
{subtitle}
}
{zapTarget && }
); } function EmptyTab({ icon: Icon, text }: { icon: typeof Users; text: string }) { return (

{text}

); } export default function ContactsPage() { const { t } = useTranslation(); useSeoMeta({ title: t('socialUi.contacts.seoTitle'), description: t('socialUi.contacts.seoDescription'), }); const { user } = useCurrentUser(); const { logins } = useNostrLogin(); const { toast } = useToast(); const [query, setQuery] = useState(''); const { data: following, isLoading: followingLoading } = useFollowing(); const { crewmates, isLoading: crewmatesLoading } = useCrewmates(); const { data: inbox, isLoading: convosLoading } = useConversations(); const { data: savedContacts } = usePrivateContacts(); const { addContact, removeContact, isAdding, isRemoving } = usePrivateContactActions(); const savedSet = useMemo(() => new Set(savedContacts ?? []), [savedContacts]); const [pendingPubkey, setPendingPubkey] = useState(null); const toggleSave = async (pubkey: string) => { if (!checkNip44(user, logins)) { toast({ title: t('socialUi.chat.encryptionUnavailable'), description: t('socialUi.contacts.encryptionUnavailableDesc'), variant: 'destructive', }); return; } setPendingPubkey(pubkey); try { if (savedSet.has(pubkey)) { await removeContact(pubkey); toast({ title: t('socialUi.contacts.removed') }); } else { await addContact(pubkey); toast({ title: t('socialUi.contacts.saved'), description: t('socialUi.contacts.savedDesc') }); } } catch (err) { toast({ title: t('socialUi.contacts.updateFailed'), description: err instanceof Error ? err.message : t('common.tryAgain'), variant: 'destructive', }); } finally { setPendingPubkey(null); } }; const q = query.trim().toLowerCase(); const matchNpub = (pk: string) => !q || nip19.npubEncode(pk).toLowerCase().includes(q); if (!user) { return ( <>

{t('socialUi.contacts.loginTitle')}

{t('socialUi.contacts.loginDesc')}

); } const crewmatesFiltered = crewmates.filter((c) => matchNpub(c.pubkey)); const followingFiltered = (following ?? []).filter(matchNpub); const convoPubkeys = (inbox?.conversations ?? []).map((c) => c.pubkey).filter(matchNpub); const savedFiltered = (savedContacts ?? []).filter(matchNpub); const renderList = (pubkeys: string[], subtitleFor?: (pk: string) => React.ReactNode) => (
{pubkeys.map((pk) => ( toggleSave(pk)} saving={(isAdding || isRemoving) && pendingPubkey === pk} /> ))}
); const crewmateSubtitle = (c: Crewmate) => c.sharedCrews.length > 0 ? ( {c.sharedCrews.slice(0, 2).join(', ')} {c.sharedCrews.length > 2 && ` +${c.sharedCrews.length - 2}`} ) : undefined; return ( <>
{/* Header */} {/* Search */}
setQuery(e.target.value)} placeholder={t('socialUi.contacts.filterPlaceholder')} className="h-10 w-full rounded-xl border border-white/10 bg-white/5 pl-9 pr-9 text-sm text-white placeholder:text-gray-600 focus:border-teal-500/50 focus:outline-none" /> {query && ( )}
{t('socialUi.contacts.tabCrew')} {t('socialUi.contacts.tabFollows')} {t('socialUi.contacts.tabDms')} {t('socialUi.contacts.tabSaved')} {/* Crew-mates */} {crewmatesLoading ? ( ) : crewmatesFiltered.length === 0 ? ( ) : ( renderList( crewmatesFiltered.map((c) => c.pubkey), (pk) => { const cm = crewmatesFiltered.find((c) => c.pubkey === pk); return cm ? crewmateSubtitle(cm) : undefined; }, ) )} {/* Following */} {followingLoading ? ( ) : followingFiltered.length === 0 ? ( ) : ( renderList(followingFiltered) )} {/* Recent DMs */} {convosLoading ? ( ) : convoPubkeys.length === 0 ? ( ) : ( renderList(convoPubkeys) )} {/* Saved private contacts */}

{t('socialUi.contacts.savedHint')}

{savedFiltered.length === 0 ? ( ) : ( renderList(savedFiltered) )}
); } function ListSkeleton() { return (
{Array.from({ length: 5 }).map((_, i) => (
))}
); }