import { useQuery } from '@tanstack/react-query'; import { useSkateRelays } from '@/hooks/useSkateRelays'; import { ADMIN_PUBKEYS } from '@/lib/admins'; import { NEWSLETTER_KIND, NEWSLETTER_TAG, parseNewsletterIssue, type SkateNewsletterIssue, } from '@/lib/types'; /** * Live SkateSpots newsletter issues, published by app admins as NIP-23 * long-form articles (kind 30023) tagged `skatespotsnews`. * * Trust: the query is scoped to `authors: ADMIN_PUBKEYS` at the relay level AND * re-validated client-side via `parseNewsletterIssue` (which re-checks the * admin allowlist + the newsletter tag) so a rogue relay can't inject issues. * * Sorted newest-first by `published_at`. */ export function useNewsletterIssues(limit = 30) { const { querySkateContent, getQueryKeySuffix } = useSkateRelays(); return useQuery({ queryKey: ['newsletter-issues', getQueryKeySuffix()], staleTime: 5 * 60 * 1000, gcTime: 30 * 60 * 1000, queryFn: async (c): Promise => { const signal = AbortSignal.any([c.signal, AbortSignal.timeout(6000)]); const events = await querySkateContent( [ { kinds: [NEWSLETTER_KIND], authors: ADMIN_PUBKEYS, '#t': [NEWSLETTER_TAG], limit, }, ], { signal }, ); const issues: SkateNewsletterIssue[] = []; for (const ev of events) { const parsed = parseNewsletterIssue(ev); if (parsed) issues.push(parsed); } // Dedupe by addressable coordinate (pubkey:d) keeping the newest. const byCoord = new Map(); for (const issue of issues) { const key = `${issue.pubkey}:${issue.identifier}`; const existing = byCoord.get(key); if (!existing || issue.createdAt > existing.createdAt) byCoord.set(key, issue); } return Array.from(byCoord.values()).sort((a, b) => b.publishedAt - a.publishedAt); }, }); }