SkateSpots/src/hooks/useFollowing.ts
SkateSpots 6def78fffe chore(lint): clear all no-unused-vars warnings (position-precise)
Removed/renamed the remaining 39 unused locals/dead code, file-by-file:
- unused destructured bindings/params (remove the binding, keep the call)
- unused single-line pure consts and dead functions/interfaces removed
- deleted the dead _FeedItem component and its now-unused imports
- amber _readChunks self-call fixed; SpotMap popup call site updated

Warnings: 182 -> 7 (only react-refresh/only-export-components remain).
no-explicit-any=0, exhaustive-deps=0, no-unused-vars=0.
Gate: 101 tests, 0 errors, boot smoke green. AGENTS.md baseline + guidance updated.
2026-09-26 22:20:21 +07:00

247 lines
7.1 KiB
TypeScript

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);
}