Two follow-ups to the Nostr-native newsletter (skipped auto-DM by design — too spammy/fragile for now; the bell mailbox already covers the inbox feel). Subscriber count (social proof): - useNewsletterSubscriberCount tallies public NIP-78 subscription markers (deduped per author, counting current "true" state). - Shown under the subscribe CTA as "Join N+ skaters getting the drops", hidden until it's meaningful (>=3) and rounded once it grows. Archive backfill (consistency without losing the rich page): - lib/newsletterArticle.ts converts a static NEWSLETTER_ISSUES entry into a NIP-23 (kind 30023) article — structured sections rendered to Markdown, stable slug `issue-N`, published_at from the issue date. - NewsletterBackfillPanel (admin-only) diffs the static archive against what's already on-relay and publishes only the missing issues; safe to re-run, reports partial failures. The rich static page rendering is untouched. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
46 lines
1.8 KiB
TypeScript
46 lines
1.8 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
|
import { APP_DATA_KIND, NEWSLETTER_SUB_D } from '@/lib/types';
|
|
|
|
/**
|
|
* A best-effort community tally of newsletter subscribers.
|
|
*
|
|
* Each subscriber owns a public NIP-78 marker (kind 30078, d=NEWSLETTER_SUB_D)
|
|
* with a `subscribed` boolean tag. We query those markers and count the ones
|
|
* currently set to "true", deduped by author (one vote per pubkey).
|
|
*
|
|
* This is intentionally approximate — relays only return what they hold — so we
|
|
* present it as social proof ("N+ skaters"), not an exact figure.
|
|
*/
|
|
export function useNewsletterSubscriberCount() {
|
|
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
|
|
|
return useQuery({
|
|
queryKey: ['newsletter-subscriber-count', getQueryKeySuffix()],
|
|
staleTime: 5 * 60 * 1000,
|
|
gcTime: 30 * 60 * 1000,
|
|
queryFn: async (c): Promise<number> => {
|
|
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(6000)]);
|
|
const events = await querySkateContent(
|
|
[{ kinds: [APP_DATA_KIND], '#d': [NEWSLETTER_SUB_D], limit: 1000 }],
|
|
{ signal },
|
|
);
|
|
|
|
// Keep the newest marker per author, then count the "true" ones.
|
|
const latestByAuthor = new Map<string, { subscribed: boolean; createdAt: number }>();
|
|
for (const ev of events) {
|
|
const subscribed = ev.tags.find(([n]) => n === 'subscribed')?.[1] === 'true';
|
|
const existing = latestByAuthor.get(ev.pubkey);
|
|
if (!existing || ev.created_at > existing.createdAt) {
|
|
latestByAuthor.set(ev.pubkey, { subscribed, createdAt: ev.created_at });
|
|
}
|
|
}
|
|
|
|
let count = 0;
|
|
for (const { subscribed } of latestByAuthor.values()) {
|
|
if (subscribed) count++;
|
|
}
|
|
return count;
|
|
},
|
|
});
|
|
}
|