import { useMemo } from 'react'; import { useNostr } from '@nostrify/react'; import { useQuery } from '@tanstack/react-query'; import type { NostrEvent } from '@nostrify/nostrify'; import { SKATE_SPOT_KIND } from '@/lib/types'; // Kind 1063 = File Metadata (NIP-94) for community photos export const PHOTO_CONTRIBUTION_KIND = 1063; export interface PhotoContribution { id: string; pubkey: string; url: string; mimeType?: string; caption: string; createdAt: number; event: NostrEvent; } export interface TagContribution { id: string; pubkey: string; tags: string[]; createdAt: number; event: NostrEvent; } /** * Parse a kind 1063 file metadata event into a PhotoContribution */ function parsePhotoContribution(event: NostrEvent): PhotoContribution | null { if (event.kind !== PHOTO_CONTRIBUTION_KIND) return null; const url = event.tags.find(([name]) => name === 'url')?.[1]; if (!url) return null; const mimeType = event.tags.find(([name]) => name === 'm')?.[1]; return { id: event.id, pubkey: event.pubkey, url, mimeType, caption: event.content, createdAt: event.created_at, event, }; } /** * Parse a kind 1111 comment event that contains tag suggestions */ function parseTagContribution(event: NostrEvent): TagContribution | null { if (event.kind !== 1111) return null; // Check if this is a tag contribution (has #skatespot-tags marker) const isTagContribution = event.tags.some( ([name, value]) => name === 't' && value === 'skatespot-tags' ); if (!isTagContribution) return null; // Extract suggested tags (all t tags except the marker) const tags = event.tags .filter(([name, value]) => name === 't' && value !== 'skatespot-tags') .map(([, value]) => value) .filter(Boolean) as string[]; if (tags.length === 0) return null; return { id: event.id, pubkey: event.pubkey, tags, createdAt: event.created_at, event, }; } /** * Hook to fetch community photo contributions for a spot */ export function useSpotPhotos(spotPubkey: string, spotIdentifier: string) { const { nostr } = useNostr(); const spotRef = `${SKATE_SPOT_KIND}:${spotPubkey}:${spotIdentifier}`; return useQuery({ queryKey: ['spot-photos', spotPubkey, spotIdentifier], queryFn: async (c) => { const signal = AbortSignal.any([c.signal, AbortSignal.timeout(5000)]); // Query kind 1063 events that reference this spot via 'a' tag const events = await nostr.query([ { kinds: [PHOTO_CONTRIBUTION_KIND], '#a': [spotRef], limit: 50, }, ], { signal }); return events .map(parsePhotoContribution) .filter((p): p is PhotoContribution => p !== null) .sort((a, b) => b.createdAt - a.createdAt); }, enabled: !!spotPubkey && !!spotIdentifier, }); } /** * Hook to fetch community tag suggestions for a spot */ export function useSpotTagSuggestions(spotPubkey: string, spotIdentifier: string) { const { nostr } = useNostr(); const spotRef = `${SKATE_SPOT_KIND}:${spotPubkey}:${spotIdentifier}`; return useQuery({ queryKey: ['spot-tag-suggestions', spotPubkey, spotIdentifier], queryFn: async (c) => { const signal = AbortSignal.any([c.signal, AbortSignal.timeout(5000)]); // Query kind 1111 comments that are tag contributions const events = await nostr.query([ { kinds: [1111], '#a': [spotRef], '#t': ['skatespot-tags'], limit: 100, }, ], { signal }); return events .map(parseTagContribution) .filter((t): t is TagContribution => t !== null) .sort((a, b) => b.createdAt - a.createdAt); }, enabled: !!spotPubkey && !!spotIdentifier, }); } /** * Hook to get aggregated community tags with vote counts */ export function useAggregatedSpotTags(spotPubkey: string, spotIdentifier: string) { const { data: tagContributions } = useSpotTagSuggestions(spotPubkey, spotIdentifier); return useMemo(() => { if (!tagContributions) return []; // Count how many times each tag was suggested const tagCounts = new Map }>(); for (const contribution of tagContributions) { for (const tag of contribution.tags) { const existing = tagCounts.get(tag) || { count: 0, contributors: new Set() }; // Only count each contributor once per tag if (!existing.contributors.has(contribution.pubkey)) { existing.count++; existing.contributors.add(contribution.pubkey); } tagCounts.set(tag, existing); } } // Convert to array and sort by count return Array.from(tagCounts.entries()) .map(([tag, { count, contributors }]) => ({ tag, count, contributorCount: contributors.size, })) .sort((a, b) => b.count - a.count); }, [tagContributions]); }