import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useSkateRelays } from '@/hooks/useSkateRelays'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { ADMIN_PUBKEYS } from '@/lib/admins'; import { MODERATION_LABEL_NAMESPACE, MODERATION_LABEL_CLAIM_APPROVED, SPOT_CLAIM_NAMESPACE, SPOT_CLAIM_LABEL, } from '@/lib/types'; /** * Private-spot ownership claims. * * A private spot (a backyard ramp, a gated park) can't be verified against a * domain the way a shop can. Instead the owner FILES A CLAIM — a signed NIP-32 * kind 1985 label in `org.skatespots.spotclaim` — and a SkateSpots moderator * confirms it by publishing a `claim-approved` label in the moderation * namespace. Only then does the claimant finish (re-publish the spot under their * own key). Nothing is ever auto-transferred, so a stranger can't grief a pin. */ export interface SpotClaim { /** Claimant hex pubkey (the label's author). */ claimant: string; createdAt: number; /** The NIP-05 handle they claimed with (label content). */ nip05: string; } /** Ownership claims filed against a spot coordinate (public, signed). */ export function useSpotClaims(coord: string | undefined) { const { querySkateContent, getQueryKeySuffix } = useSkateRelays(); return useQuery({ queryKey: ['spot-claims', coord, getQueryKeySuffix()], enabled: !!coord, staleTime: 60_000, queryFn: async () => { if (!coord) return []; const signal = AbortSignal.timeout(8000); const events = await querySkateContent( [{ kinds: [1985], '#a': [coord], limit: 200 }], { signal }, ); const byClaimant = new Map(); for (const ev of events) { const inNamespace = ev.tags.some(([n, v]) => n === 'L' && v === SPOT_CLAIM_NAMESPACE); const isClaim = ev.tags.some(([n, v]) => n === 'l' && v === SPOT_CLAIM_LABEL); if (!inNamespace || !isClaim) continue; const current = byClaimant.get(ev.pubkey); if (!current || ev.created_at > current.createdAt) { byClaimant.set(ev.pubkey, { claimant: ev.pubkey, createdAt: ev.created_at, nip05: ev.content }); } } return [...byClaimant.values()].sort((a, b) => b.createdAt - a.createdAt); }, }); } /** `coord` → set of claimant pubkeys a moderator has approved. */ export function useApprovedSpotClaims() { const { querySkateContent, getQueryKeySuffix } = useSkateRelays(); return useQuery>>({ queryKey: ['spot-claim-approvals', getQueryKeySuffix()], staleTime: 60_000, queryFn: async () => { const signal = AbortSignal.timeout(8000); const events = await querySkateContent( [{ kinds: [1985], authors: ADMIN_PUBKEYS, '#l': [MODERATION_LABEL_CLAIM_APPROVED], limit: 500 }], { signal }, ); const map = new Map>(); for (const ev of events) { const inNamespace = ev.tags.some(([n, v]) => n === 'L' && v === MODERATION_LABEL_NAMESPACE); if (!inNamespace) continue; const coord = ev.tags.find(([n]) => n === 'a')?.[1]; const claimant = ev.tags.find(([n]) => n === 'p')?.[1]; if (!coord || !claimant) continue; if (!map.has(coord)) map.set(coord, new Set()); map.get(coord)!.add(claimant.toLowerCase()); } return map; }, }); } export interface AllSpotClaim { coord: string; ownerPubkey: string; identifier: string; claimant: string; nip05: string; createdAt: number; } /** * EVERY private-spot ownership claim on the relays (admins use this to find * work). Small volume — claims are rare and manual. */ export function useAllSpotClaims() { const { querySkateContent, getQueryKeySuffix } = useSkateRelays(); return useQuery({ queryKey: ['spot-claims-all', getQueryKeySuffix()], staleTime: 60_000, queryFn: async () => { const signal = AbortSignal.timeout(8000); const events = await querySkateContent( [{ kinds: [1985], '#L': [SPOT_CLAIM_NAMESPACE], limit: 500 }], { signal }, ); const seen = new Map(); for (const ev of events) { const isClaim = ev.tags.some(([n, v]) => n === 'l' && v === SPOT_CLAIM_LABEL); if (!isClaim) continue; const coord = ev.tags.find(([n]) => n === 'a')?.[1]; if (!coord) continue; const [, ownerPubkey, identifier] = coord.split(':'); const key = `${coord}:${ev.pubkey}`; const current = seen.get(key); if (!current || ev.created_at > current.createdAt) { seen.set(key, { coord, ownerPubkey, identifier, claimant: ev.pubkey, nip05: ev.content, createdAt: ev.created_at }); } } return [...seen.values()].sort((a, b) => b.createdAt - a.createdAt); }, }); } /** File (or refresh) an ownership claim for a private spot. */export function useSubmitSpotClaim() { const { user } = useCurrentUser(); const { mutateAsync: publish, isPending } = useNostrPublish(); const queryClient = useQueryClient(); return { isPending, submit: async ({ coord, ownerPubkey, nip05 }: { coord: string; ownerPubkey: string; nip05: string }) => { if (!user) throw new Error('Log in first.'); await publish({ kind: 1985, content: nip05, tags: [ ['L', SPOT_CLAIM_NAMESPACE], ['l', SPOT_CLAIM_LABEL, SPOT_CLAIM_NAMESPACE], ['a', coord], ['p', ownerPubkey], ['alt', 'SkateSpots private-spot ownership claim'], ], }); queryClient.invalidateQueries({ queryKey: ['spot-claims', coord] }); }, }; }