/** * AddToListDialog * * A dialog for adding a user (by pubkey) to one of the current user's * NIP-51 Follow Sets (kind 30000) or Follow Packs (kind 39089), * or creating a new Follow Set on the fly. */ import { useState } from 'react'; import { Plus, Loader2, List, Users, X, PartyPopper, Check } from 'lucide-react'; import { useQueryClient } from '@tanstack/react-query'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; import { Skeleton } from '@/components/ui/skeleton'; import { useUserLists } from '@/hooks/useUserLists'; import { useFollowPacks } from '@/hooks/useFollowPacks'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { toast } from '@/hooks/useToast'; import type { FollowPack } from '@/hooks/useFollowPacks'; interface AddToListDialogProps { /** Hex pubkey of the user to add */ pubkey: string; /** Display name for the user being added */ displayName?: string; open: boolean; onOpenChange: (open: boolean) => void; } export function AddToListDialog({ pubkey, displayName, open, onOpenChange }: AddToListDialogProps) { const { user } = useCurrentUser(); const { lists, isLoading: listsLoading, addToList, createList, isInList } = useUserLists(); const { data: followPacks = [], isLoading: packsLoading } = useFollowPacks(); const { mutateAsync: publishEvent } = useNostrPublish(); const queryClient = useQueryClient(); const [newListName, setNewListName] = useState(''); const [pendingId, setPendingId] = useState(null); const [creatingNew, setCreatingNew] = useState(false); // Optimistic tracking for packs added this session const [addedPackIds, setAddedPackIds] = useState>(new Set()); const handleOpenChange = (o: boolean) => { if (!o) setAddedPackIds(new Set()); onOpenChange(o); }; const close = () => handleOpenChange(false); const isLoading = listsLoading || packsLoading; /** Add pubkey to a Follow Set (kind 30000). */ const handleAddToList = async (listId: string) => { if (pendingId) return; setPendingId(listId); try { await addToList.mutateAsync({ listId, pubkey }); toast({ title: 'Added to list' }); } catch { toast({ title: 'Failed to add to list', variant: 'destructive' }); } finally { setPendingId(null); } }; /** Add pubkey to a Follow Pack (kind 39089). */ const handleAddToPack = async (pack: FollowPack) => { if (pendingId) return; setPendingId(pack.id); try { const newTags = [...pack.event.tags, ['p', pubkey]]; await publishEvent({ kind: 39089, content: pack.event.content ?? '', tags: newTags, }); setAddedPackIds((prev) => new Set(prev).add(pack.id)); queryClient.invalidateQueries({ queryKey: ['own-follow-packs', user?.pubkey] }); toast({ title: `Added to "${pack.title}"` }); } catch { toast({ title: 'Failed to add to pack', variant: 'destructive' }); } finally { setPendingId(null); } }; const handleCreateAndAdd = async () => { if (!newListName.trim() || creatingNew) return; setCreatingNew(true); try { const result = await createList.mutateAsync({ title: newListName.trim(), pubkeys: [pubkey], }); toast({ title: `Created "${result.title}" and added` }); setNewListName(''); } catch { toast({ title: 'Failed to create list', variant: 'destructive' }); } finally { setCreatingNew(false); } }; const hasAny = lists.length > 0 || followPacks.length > 0; if (!user) return null; return ( {/* Header */}
{displayName ? `Add ${displayName} to list` : 'Add to list'}
{isLoading ? (
{Array.from({ length: 3 }).map((_, i) => (
))}
) : !hasAny ? (
No lists or packs yet. Create one below.
) : (
{/* Follow Sets */} {lists.length > 0 && ( <> {followPacks.length > 0 && (

Lists

)} {lists.map((list) => { const inList = isInList(list.id, pubkey); const isPending = pendingId === list.id; return ( } label={list.title} count={list.pubkeys.length} inList={inList} isPending={isPending} disabled={!!pendingId} onAdd={() => handleAddToList(list.id)} /> ); })} )} {/* Follow Packs */} {followPacks.length > 0 && ( <> {lists.length > 0 && }

Follow Packs

{followPacks.map((pack) => { const inPack = pack.pubkeys.includes(pubkey) || addedPackIds.has(pack.id); const isPending = pendingId === pack.id; return ( } label={pack.title} count={pack.pubkeys.length} inList={inPack} isPending={isPending} disabled={!!pendingId} onAdd={() => handleAddToPack(pack)} /> ); })} )}
)}
{/* Create new list */}

New list

setNewListName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleCreateAndAdd(); }} className="flex-1 h-8 text-base md:text-sm" disabled={creatingNew} />
); } // ─── ListRow ────────────────────────────────────────────────────────────────── interface ListRowProps { icon: React.ReactNode; label: string; count: number; inList: boolean; isPending: boolean; disabled: boolean; onAdd: () => void; } function ListRow({ icon, label, count, inList, isPending, disabled, onAdd }: ListRowProps) { return (
{icon}
{label}
{count} {count === 1 ? 'person' : 'people'}
); }