The entire skate-spots codebase was accidentally copied into src/src/ and public/public/ instead of src/ and public/ directly. This left the real src/ directory as blank MKStack template stubs, explaining why the landing page showed the default template instead of SkateSpots. Changes made: - Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx, all pages, components, hooks, contexts, lib, i18n, test) - Copied all files from public/public/ up to public/ (images, manifest, etc.) - Deleted src/src/ and public/public/ nested directories - Removed HelloWorld.tsx template stub from pages/ - Retained the previously fixed index.css (Tailwind v3 directives) Full skate-spots feature set is now active: - All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.) - NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler - Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.) - Outfit Variable font, Amber pre-check in main.tsx - All hooks (useSkateSpots, useCoaches, useZaps, etc.) - i18n support (en/es) Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
247 lines
7.1 KiB
TypeScript
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);
|
|
}
|