import { useQuery } from '@tanstack/react-query'; import { useNostr } from '@nostrify/react'; import type { NostrEvent } from '@nostrify/nostrify'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useRelays } from '@/lib/relays'; /** * Trusted-trader recommendations — built on the Attestations NIP's * **Attestor Recommendation** (kind `31873`). A skater recommends another as a * trusted marketplace trader, for discovery: profiles show who vouches for them. * * d = `:reco:` · p = target · k = `30402` (listings) */ export const RECOMMENDATION_KIND = 31873; export interface TraderRecommendation { id: string; recommender: string; target: string; kinds: string[]; note: string; createdAt: number; event: NostrEvent; } function parse(event: NostrEvent): TraderRecommendation | null { if (event.kind !== RECOMMENDATION_KIND) return null; const d = event.tags.find(([t]) => t === 'd')?.[1]; const target = event.tags.find(([t]) => t === 'p')?.[1]; if (!d || !target) return null; return { id: event.id, recommender: event.pubkey, target, kinds: event.tags.filter(([t]) => t === 'k').map(([, v]) => v), note: event.content, createdAt: event.created_at, event, }; } /** Recommendations received by `pubkey` (newest per recommender). */ export function useTraderRecommendations(pubkey?: string) { const { nostr } = useNostr(); const { read } = useRelays(); const relayKey = read.slice().sort().join(','); return useQuery({ queryKey: ['trader-recommendations', pubkey, relayKey], enabled: !!pubkey, staleTime: 5 * 60 * 1000, queryFn: async (c) => { if (!pubkey) return []; const signal = AbortSignal.any([c.signal, AbortSignal.timeout(10000)]); const events = await nostr.query( [{ kinds: [RECOMMENDATION_KIND], '#p': [pubkey], limit: 200 }], { signal }, ); const byRecommender = new Map(); for (const rec of events .map(parse) .filter((r): r is TraderRecommendation => !!r) .sort((a, b) => b.createdAt - a.createdAt)) { if (!byRecommender.has(rec.recommender)) byRecommender.set(rec.recommender, rec); } return [...byRecommender.values()]; }, }); } /** The current user's own recommendation for `target`, if any. */ export function useMyRecommendation(target?: string) { const { nostr } = useNostr(); const { user } = useCurrentUser(); return useQuery({ queryKey: ['my-recommendation', user?.pubkey, target], enabled: !!user?.pubkey && !!target, staleTime: 60 * 1000, queryFn: async (c) => { if (!user?.pubkey || !target) return null; const signal = AbortSignal.any([c.signal, AbortSignal.timeout(8000)]); const events = await nostr.query( [{ kinds: [RECOMMENDATION_KIND], authors: [user.pubkey], '#p': [target], limit: 10 }], { signal }, ); return ( events .map(parse) .filter((r): r is TraderRecommendation => !!r) .sort((a, b) => b.createdAt - a.createdAt)[0] ?? null ); }, }); } export function useRecommendationActions() { const { user } = useCurrentUser(); const { mutateAsync: publish } = useNostrPublish(); /** Recommend `target` as a trusted trader (re-publishing replaces yours). */ const recommend = async (target: string, note?: string) => { if (!user) throw new Error('Log in to recommend a trader.'); if (target === user.pubkey) throw new Error('You cannot recommend yourself.'); await publish({ kind: RECOMMENDATION_KIND, content: note?.trim() || '', tags: [ ['d', `${user.pubkey}:reco:${target}`], ['p', target], ['k', '30402'], ['alt', 'SkateSpots: trusted-trader recommendation'], ], }); }; return { recommend }; }