Drop donor count from campaign cards to skip the per-receipt tx fan-out

The card footer showed "x donors", sourced from useCampaignDonations'
donorCount. Populating that count required fetching every kind 8333
receipt for the campaign and then verifying each one with a per-receipt
Esplora /tx call — a fan-out that, across a ~200-card grid, hammered
every configured Esplora backend.

Cards only ever render the raised total (the progress bar), which needs
just the single /address balance lookup. Add a receipts option to
useCampaignDonations that skips the receipt fetch and verification
fan-out, and have CampaignCard pass receipts: false. Remove the now-dead
donor-count UI and its unused t()/useTranslation import.
This commit is contained in:
Chad Curtis
2026-06-25 06:27:16 -05:00
parent 9362ebf8d6
commit 88bb768658
2 changed files with 23 additions and 12 deletions
+6 -7
View File
@@ -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 {
* `<Link>` 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<HTMLAnchorElement>(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
<div className="flex items-center justify-between gap-3 border-t border-border/60 pt-3 text-xs text-muted-foreground">
<div className="flex min-w-0 items-center gap-2">
<AuthorByline pubkey={campaign.pubkey} insideLink />
{!isSilentPayment && stats && stats.donorCount > 0 && (
<span className="shrink-0 text-muted-foreground/80">
· {t('common.donors', { count: stats.donorCount })}
</span>
)}
</div>
{(footerBadge || translateAction) && (
<div className="flex shrink-0 items-center gap-1.5">
+17 -5
View File
@@ -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: {