diff --git a/src/hooks/useCampaignLists.ts b/src/hooks/useCampaignLists.ts index eefeb44b..8b9c289e 100644 --- a/src/hooks/useCampaignLists.ts +++ b/src/hooks/useCampaignLists.ts @@ -10,7 +10,6 @@ import { foldCampaignLists, } from '@/lib/campaignLists'; import { LIST_CURATOR_PUBKEY } from '@/lib/agoraDefaults'; -import { DITTO_RELAY } from '@/lib/appRelays'; import type { NostrEvent } from '@nostrify/nostrify'; @@ -34,7 +33,20 @@ interface UseCampaignListsResult { * follow-pack member is trusted to sign approve / hide labels. * * Because the curator is a hardcoded constant, this query depends on no - * other query — it fires on first paint with no waterfall. + * other query — it fires on first paint. + * + * **Relay fan-out.** This used to query `relay.ditto.pub` directly (a + * single-relay `nostr.relay(...)` call) to avoid a fast empty EOSE from a + * less-populated relay racing the surface to "no lists." But this query + * sits at the *head* of the home-page waterfall — every hero campaign is + * gated on its result (see `CampaignsPage`/`useCampaigns`) — so a slow + * `relay.ditto.pub` stalled the entire first paint. We now fan out to the + * whole read pool via `nostr.query`. The `authors: [LIST_CURATOR_PUBKEY]` + * filter is what enforces the trust model; correctness no longer depends + * on hitting one specific relay, and the curated relay is still in the + * fan-out so its events are found. The pool accumulates events across + * relays until first EOSE (+ the pool's eoseTimeout), so a late event from + * the curated relay still folds in on the next tick. * * Lists *and* the index are pulled in a single filter via * `'#t': [LIST_HASHTAG, LIST_INDEX_HASHTAG]` so there's only one @@ -46,12 +58,7 @@ export function useCampaignLists() { const query = useQuery({ queryKey: ['campaign-lists', LIST_CURATOR_PUBKEY], queryFn: async ({ signal }) => { - // Query the canonical app relay directly. The same reasoning as - // `useCampaignModerators` applies: a fast empty EOSE from a - // less-populated relay should not race the moderation surface to - // "no lists" while the curated relay still holds them. - const relay = nostr.relay(DITTO_RELAY); - const events = await relay.query( + const events = await nostr.query( [ { kinds: [CAMPAIGN_LIST_KIND], @@ -60,7 +67,7 @@ export function useCampaignLists() { limit: 500, }, ], - { signal: AbortSignal.any([signal, AbortSignal.timeout(8000)]) }, + { signal }, ); return foldCampaignLists(events); }, diff --git a/src/hooks/useCampaignModeration.ts b/src/hooks/useCampaignModeration.ts index 712d3c59..cc2a36b4 100644 --- a/src/hooks/useCampaignModeration.ts +++ b/src/hooks/useCampaignModeration.ts @@ -3,7 +3,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useNostrPublish } from './useNostrPublish'; import { useCampaignModerators } from './useCampaignModerators'; -import { DITTO_RELAY } from '@/lib/appRelays'; import { CAMPAIGN_KIND } from '@/lib/campaign'; import { AGORA_MODERATION_NAMESPACE, @@ -62,8 +61,12 @@ export function useCampaignModeration() { if (!moderators || moderators.length === 0) { return { ...EMPTY_MODERATION_DATA, moderators: [] }; } - const relay = nostr.relay(DITTO_RELAY); - const events = await relay.query( + // Fan out to the whole read pool rather than pinning a single relay. + // The `authors: moderators` filter enforces the trust model, so + // querying more relays only improves coverage — and it keeps this + // moderation surface off the single-relay critical path that was + // serializing the home page behind relay.ditto.pub. + const events = await nostr.query( [ { kinds: [LABEL_KIND], @@ -76,7 +79,7 @@ export function useCampaignModeration() { limit: 2000, }, ], - { signal: AbortSignal.any([signal, AbortSignal.timeout(8000)]) }, + { signal }, ); return foldModerationLabels(events, moderators, CAMPAIGN_KIND); }, diff --git a/src/hooks/useDiscoverCommunities.ts b/src/hooks/useDiscoverCommunities.ts index ed1c24c7..02c7ad46 100644 --- a/src/hooks/useDiscoverCommunities.ts +++ b/src/hooks/useDiscoverCommunities.ts @@ -2,7 +2,6 @@ import { useNostr } from '@nostrify/react'; import { useQuery } from '@tanstack/react-query'; import type { NostrEvent } from '@nostrify/nostrify'; -import { DITTO_RELAY } from '@/lib/appRelays'; import { COMMUNITY_DEFINITION_KIND, parseCommunityEvent, @@ -38,13 +37,15 @@ interface UseDiscoverCommunitiesOptions { export function useDiscoverCommunities(options: UseDiscoverCommunitiesOptions = {}) { const { limit = 24, enabled = true } = options; const { nostr } = useNostr(); - const relay = nostr.relay(DITTO_RELAY); return useQuery({ queryKey: ['discover-communities', limit], enabled, queryFn: async ({ signal }) => { - const events = await relay.query( + // Global discovery (no `authors:` filter), so fan out to the whole + // read pool: more relays means broader community coverage, and it + // keeps Discover off the single-relay critical path. + const events = await nostr.query( [{ kinds: [COMMUNITY_DEFINITION_KIND], limit }], { signal }, ); diff --git a/src/hooks/useFeaturedOrganizations.ts b/src/hooks/useFeaturedOrganizations.ts index cdef02e4..1096489d 100644 --- a/src/hooks/useFeaturedOrganizations.ts +++ b/src/hooks/useFeaturedOrganizations.ts @@ -3,7 +3,6 @@ import { useQuery } from '@tanstack/react-query'; import type { NostrEvent, NostrFilter } from '@nostrify/nostrify'; import { useOrganizationModeration } from '@/hooks/useOrganizationModeration'; -import { DITTO_RELAY } from '@/lib/appRelays'; import { COMMUNITY_DEFINITION_KIND, parseCommunityEvent, @@ -50,7 +49,6 @@ function parseCoord(coord: string): { pubkey: string; dTag: string } | null { */ export function useFeaturedOrganizations() { const { nostr } = useNostr(); - const relay = nostr.relay(DITTO_RELAY); const { data: moderation, isReady: moderationReady } = useOrganizationModeration(); // Derive the curated coord set: featured minus hidden, sorted by the @@ -102,8 +100,11 @@ export function useFeaturedOrganizations() { }), ); - const combinedSignal = AbortSignal.any([signal, AbortSignal.timeout(8000)]); - const events = await relay.query(filters, { signal: combinedSignal }); + // Fan out to the whole read pool. Each filter pins `authors:`, so the + // curation is still enforced by the moderation labels (the `featured` + // labels are themselves moderator-authored) — querying more relays only + // improves coverage and keeps this off the single-relay critical path. + const events = await nostr.query(filters, { signal }); // Latest-wins dedupe of addressable revisions, then index by coord so // we can return them in the moderator-controlled `featuredOrder`.