import { useNostr } from '@nostrify/react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useCurrentUser } from './useCurrentUser'; import { useToast } from './useToast'; import { nip19 } from 'nostr-tools'; /** * Hook to fetch the list of pubkeys that a user follows */ export function useFollowing(pubkey?: string) { const { nostr } = useNostr(); const { user } = useCurrentUser(); const targetPubkey = pubkey || user?.pubkey; return useQuery({ queryKey: ['following', targetPubkey], queryFn: async (c) => { if (!targetPubkey) return []; const signal = AbortSignal.any([c.signal, AbortSignal.timeout(5000)]); const events = await nostr.query( [{ kinds: [3], authors: [targetPubkey], limit: 1 }], { signal } ); if (!events[0]) return []; // Extract followed pubkeys from p tags const following = events[0].tags .filter(([tag]) => tag === 'p') .map(([, pubkey]) => pubkey); return following; }, enabled: !!targetPubkey, staleTime: 60000, // 1 minute }); } /** * Hook to fetch the list of pubkeys that follow a user */ export function useFollowers(pubkey?: string) { const { nostr } = useNostr(); const { user } = useCurrentUser(); const targetPubkey = pubkey || user?.pubkey; return useQuery({ queryKey: ['followers', targetPubkey], queryFn: async (c) => { if (!targetPubkey) return []; const signal = AbortSignal.any([c.signal, AbortSignal.timeout(10000)]); // Query all kind 3 events that have a p tag with our pubkey const events = await nostr.query( [{ kinds: [3], '#p': [targetPubkey], limit: 100 }], { signal } ); // Extract the authors (people who follow us) const followers = events.map((event) => event.pubkey); // Deduplicate (same person might have multiple follow lists) return Array.from(new Set(followers)); }, enabled: !!targetPubkey, staleTime: 60000, // 1 minute }); } /** * Hook to follow/unfollow users */ export function useFollowActions() { const { nostr } = useNostr(); const { user } = useCurrentUser(); const { toast } = useToast(); const queryClient = useQueryClient(); const followMutation = useMutation({ mutationFn: async (targetPubkey: string) => { if (!user) throw new Error('Must be logged in to follow users'); // Get current follow list const events = await nostr.query( [{ kinds: [3], authors: [user.pubkey], limit: 1 }], { signal: AbortSignal.timeout(5000) } ); const currentFollowList = events[0]; const currentTags = currentFollowList?.tags || []; // Check if already following const alreadyFollowing = currentTags.some( ([tag, pubkey]) => tag === 'p' && pubkey === targetPubkey ); if (alreadyFollowing) { throw new Error('Already following this user'); } // Add new p tag const newTags = [...currentTags, ['p', targetPubkey]]; // Publish new follow list await user.signer.signEvent({ kind: 3, content: currentFollowList?.content || '', tags: newTags, created_at: Math.floor(Date.now() / 1000), }).then((signedEvent) => nostr.event(signedEvent)); return targetPubkey; }, onSuccess: (targetPubkey) => { // Invalidate queries to refetch queryClient.invalidateQueries({ queryKey: ['following', user?.pubkey] }); queryClient.invalidateQueries({ queryKey: ['trust-score'] }); toast({ title: 'Followed successfully', description: 'This user is now in your network', }); }, onError: (error) => { toast({ title: 'Failed to follow', description: error instanceof Error ? error.message : 'Unknown error', variant: 'destructive', }); }, }); const unfollowMutation = useMutation({ mutationFn: async (targetPubkey: string) => { if (!user) throw new Error('Must be logged in to unfollow users'); // Get current follow list const events = await nostr.query( [{ kinds: [3], authors: [user.pubkey], limit: 1 }], { signal: AbortSignal.timeout(5000) } ); const currentFollowList = events[0]; if (!currentFollowList) { throw new Error('No follow list found'); } // Remove the p tag for this pubkey const newTags = currentFollowList.tags.filter( ([tag, pubkey]) => !(tag === 'p' && pubkey === targetPubkey) ); // Publish updated follow list await user.signer.signEvent({ kind: 3, content: currentFollowList.content || '', tags: newTags, created_at: Math.floor(Date.now() / 1000), }).then((signedEvent) => nostr.event(signedEvent)); return targetPubkey; }, onSuccess: () => { // Invalidate queries queryClient.invalidateQueries({ queryKey: ['following', user?.pubkey] }); queryClient.invalidateQueries({ queryKey: ['trust-score'] }); toast({ title: 'Unfollowed successfully', description: 'Removed from your network', }); }, onError: (error) => { toast({ title: 'Failed to unfollow', description: error instanceof Error ? error.message : 'Unknown error', variant: 'destructive', }); }, }); /** * Parse npub/nip05 and follow the user */ const followByIdentifier = async (identifier: string) => { try { // Try to decode as npub first if (identifier.startsWith('npub1')) { const decoded = nip19.decode(identifier); if (decoded.type === 'npub') { await followMutation.mutateAsync(decoded.data); return; } } // Try to resolve as NIP-05 if (identifier.includes('@')) { const [name, domain] = identifier.split('@'); const response = await fetch( `https://${domain}/.well-known/nostr.json?name=${name}` ); const data = await response.json(); const pubkey = data.names?.[name]; if (pubkey) { await followMutation.mutateAsync(pubkey); return; } } // Try as raw hex pubkey if (identifier.length === 64 && /^[0-9a-f]+$/.test(identifier)) { await followMutation.mutateAsync(identifier); return; } throw new Error('Invalid identifier. Use npub1..., user@domain.com, or hex pubkey'); } catch (error) { toast({ title: 'Failed to follow', description: error instanceof Error ? error.message : 'Invalid identifier', variant: 'destructive', }); } }; return { follow: followMutation.mutateAsync, unfollow: unfollowMutation.mutateAsync, followByIdentifier, isFollowing: followMutation.isPending, isUnfollowing: unfollowMutation.isPending, }; } /** * Check if current user is following a specific pubkey */ export function useIsFollowing(targetPubkey: string | undefined) { const { data: following = [] } = useFollowing(); if (!targetPubkey) return false; return following.includes(targetPubkey); }