60 lines
2 KiB
TypeScript
60 lines
2 KiB
TypeScript
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', limit, getQueryKeySuffix()],
|
|
staleTime: 5 * 60 * 1000,
|
|
gcTime: 30 * 60 * 1000,
|
|
queryFn: async (c): Promise<SkateNewsletterIssue[]> => {
|
|
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<string, SkateNewsletterIssue>();
|
|
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);
|
|
},
|
|
});
|
|
}
|