diff --git a/src/components/CampaignCard.tsx b/src/components/CampaignCard.tsx index e5851513..19146074 100644 --- a/src/components/CampaignCard.tsx +++ b/src/components/CampaignCard.tsx @@ -1,6 +1,5 @@ import { useMemo, useRef } from 'react'; import type { ReactNode } from 'react'; -import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { HandHeart, ShieldCheck } from 'lucide-react'; @@ -164,7 +163,6 @@ interface CampaignCardProps { * `` to the campaign's naddr-based detail route. */ export function CampaignCard({ campaign, variant = 'compact', className, footerBadge, showModerationMenu = true }: CampaignCardProps) { - const { t } = useTranslation(); const { translatedEvent, translateAction } = useEventTranslation(campaign.event, { iconOnly: true, buttonClassName: 'size-8 rounded-full p-0 text-muted-foreground hover:text-primary hover:bg-primary/10', @@ -180,8 +178,14 @@ export function CampaignCard({ campaign, variant = 'compact', className, footerB // already there by the time the user sees it. const cardRef = useRef(null); const inView = useInView(cardRef); + // Cards only show the raised total (the progress bar), never the donor + // list — so we skip the kind 8333 receipt fetch and the per-receipt + // `/tx` verification fan-out. Only the single Esplora `/address` balance + // lookup runs, which keeps a ~200-card grid from firing an N-receipt + // `/tx` storm per card. const { data: stats, isLoading: donationsLoading } = useCampaignDonations(campaign, { enabled: inView, + receipts: false, }); const { data: btcPrice } = useBtcPrice(); @@ -328,11 +332,6 @@ export function CampaignCard({ campaign, variant = 'compact', className, footerB
- {!isSilentPayment && stats && stats.donorCount > 0 && ( - - · {t('common.donors', { count: stats.donorCount })} - - )}
{(footerBadge || translateAction) && (
diff --git a/src/hooks/useCampaignDonations.ts b/src/hooks/useCampaignDonations.ts index e0f924a0..78d344af 100644 --- a/src/hooks/useCampaignDonations.ts +++ b/src/hooks/useCampaignDonations.ts @@ -97,12 +97,24 @@ export function useCampaignDonations( * option's default was introduced to stop. */ refetchInterval?: number | false; + /** + * Skip the kind 8333 receipt fetch and the per-receipt `/tx` + * verification fan-out — i.e. everything that powers the donor + * list, donor count, and per-tx breakdown. Only the single Esplora + * `/address` balance lookup that drives the headline `totalSats` + * (the progress bar) runs. + * + * Card grids only render the raised total, never the donor list, so + * they pass `receipts: false` to avoid an N-receipt `/tx` storm per + * card. The detail page leaves this `true` to populate its donor UI. + */ + receipts?: boolean; } = {}, ): { data: CampaignDonationStats; isLoading: boolean; } { - const { enabled = true, refetchInterval = false } = options; + const { enabled = true, refetchInterval = false, receipts: fetchReceipts = true } = options; const { nostr } = useNostr(); const { config } = useAppContext(); const { esploraApis } = config; @@ -146,7 +158,7 @@ export function useCampaignDonations( ); return events; }, - enabled: enabled && !!aTag && hasOnchain, + enabled: enabled && fetchReceipts && !!aTag && hasOnchain, staleTime: 15_000, }); @@ -174,7 +186,7 @@ export function useCampaignDonations( queryFn: ({ signal }: { signal: AbortSignal }) => verifyOnchainZap(event, esploraApis, walletValue, signal), staleTime: 60_000, - enabled: enabled && !!walletValue && hasOnchain, + enabled: enabled && fetchReceipts && !!walletValue && hasOnchain, })), }); @@ -205,8 +217,8 @@ export function useCampaignDonations( hasOnchain && (!enabled || addressQuery.isLoading || - receiptsQuery.isLoading || - verifications.some((v) => v.isLoading)); + (fetchReceipts && + (receiptsQuery.isLoading || verifications.some((v) => v.isLoading)))); return { data: {