Each beneficiary listed on a campaign page now has a Donate button
next to it. Clicking it opens a dialog that shows the beneficiary's
Bitcoin (Taproot) address — derived from their Nostr pubkey — as a
scannable BIP-21 QR code and a copyable address.
This is distinct from the existing campaign DonateDialog, which
splits across all recipients. The new dialog targets a single
beneficiary directly, so it intentionally skips amount entry and
the campaign-tally flow.
- Push Leaflet zoom controls below the sticky header on /world so they
remain clickable instead of sitting behind the 64px top bar.
- Replace plain <a> tags with React Router <Link> in the site footer so
navigation to /help, /privacy, /safety, and /changelog is client-side
instead of triggering a full page reload.
- Use plain-text preview with line-clamp-3 in the post more-menu instead
of NoteContent + a conflicting max-h hard clip, so long content
truncates with an ellipsis rather than cutting off abruptly.
- Switch the campaign detail Donate/Share row from a rigid 4-column grid
to flex so the Share button gets its natural width instead of being
cramped into 25% of the row on mobile.
- Make the campaign URL preview in /campaigns/new truncate with an
ellipsis for long slugs instead of overflowing or clipping silently.
A single Bitcoin transaction with N outputs now produces a single kind
8333 onchain-zap event listing every recipient under its own `p` tag,
instead of one event per recipient. The `amount` tag carries the total
sats paid to the listed recipients (the full donation, excluding the
donor's change).
This is straight-forward forward-compatibility: legacy single-recipient
events are just the degenerate case (one `p` tag, amount equal to the
one recipient's slice). Aggregators (`useCampaignDonations`,
`useGlobalDonations`) simplify to summing the `amount` tag across every
matching event — under both schemas an event's `amount` is the total
paid to the recipients listed in that event, so the sum across all
events for a campaign is the campaign total either way.
The verifier (`verifyOnchainZap`) now sums tx outputs paying any listed
recipient's derived Taproot address and strips the sender from the
recipient set so a tx that includes the sender plus legitimate
recipients still verifies. The notifications surface uses a new
`getZapAmountSatsForRecipient` helper to attribute only the viewer's
estimated slice (amount / p_count) rather than crediting them with the
full multi-recipient donation. `CampaignDetailPage` keeps its
group-by-(txid, donor) reply rendering so legacy multi-event donations
still collapse to a single donation card.
Apply getStableCount to the participants/full-state-list section so
per-state numbers are consistent with the leaderboard and distribution
chart. Previously the participants list used raw event-based counts
while the other sections used NIP-45 COUNT floors, causing visible
mismatches (e.g. Miranda showing 847 in the list but 859 in the donut).
In municipalities view this is a no-op — getStableCount returns the raw
feed.count unchanged since there are no per-municipality COUNT queries.
Live/activity indicators still derive from loaded events.
Pass max-w-5xl mx-auto sm:px-6 to PageHeader via its className prop so
the title and action buttons sit inside the same centered column as the
dashboard body. This is a local override — the shared PageHeader
component is not modified.
Add pb-8 to the content container for comfortable breathing room between
the last dashboard section and the footer.
Normalize the dashboard layout and card styling for visual consistency
with the rest of the app. No data fetching or behavioral changes.
Page container:
- Widen content from max-w-4xl to max-w-5xl (justifies noMaxWidth opt-out)
- Add responsive padding (px-4 sm:px-6)
- Replace non-standard pb-24 with pb-16 sidebar:pb-0
- Add min-h-screen to prevent short-page footer ride-up
- Error state now uses the same max-w-5xl container (no layout jump)
Header actions:
- Replace Trash2 icon with PanelLeftClose for sidebar toggle (less alarming)
- Convert sidebar toggle from outline button with text to ghost icon button
- Add aria-label attributes for accessibility
- Tighten gap from gap-2 to gap-1.5 for compact icon-button row
- Move statusBadge/headerActions above the error early-return so both
code paths share the same header
Chart cards (ActivityChart, TopRegionsChart, DistributionDonut):
- Replace raw rounded-2xl border divs with shadcn Card/CardHeader/CardContent
- Picks up consistent rounded-lg, bg-card, shadow-sm, and standard padding
List cards (ParticipantsList, RecentActivityList):
- Normalize from rounded-2xl to rounded-lg with bg-card and shadow-sm
- Preserve overflow-hidden and custom internal grid layouts
Skeleton:
- Add tabs placeholder skeleton
- Use Card/CardHeader/CardContent for chart skeletons
- Normalize table skeleton wrapper to match new card styles
Tabs:
- Add bg-muted/50 to TabsList for subtle visual grounding
Extends the prior Tibet/CountryFlag work so the Snow Lion flag wins
everywhere a CN-XZ post or page surfaces, not just in the Discover
country pulse strip.
- Comment context (NoteCard + PostDetailPage)
* Country pill on the card header swaps in the SVG via
CountryFlag (was bare emoji span).
* Pill hover card uses the subdivision's own name, drops the
parent-country sub-line, and labels it as 'Country' rather
than 'Region' for codes with a custom flag.
* CountryFlagBackdrop (the faded full-bleed flag behind a
country-rooted note) prefers the bundled SVG over the
Wikipedia lead image, which for Tibet returns an
administrative map.
- PostDetailPage 'country above the post' chip
* CountryPreview now routes through CountryFlag and prefers
info.subdivisionName when a custom flag is registered, so
the chip reads as 'Tibet' instead of 'China'.
- Country page (/i/iso3166:CN-XZ)
* Hero banner driven by customFlagAsset(code) when present,
sharing the same <img>+skyOverlay pipeline as Wikipedia
photos so the day/night tint and bottom fade still apply.
* Subline beneath the title no longer falls into the
'subdivision = show parent country' branch for custom-flag
codes; it now reads the Wikipedia description / official
name like other countries do.
* Big flag slot uses CountryFlag too, bypassing the Wikipedia
subdivision thumbnail.
- Helpers split out of CountryFlag.tsx into src/lib/customFlags.ts
(hasCustomFlag, customFlagAsset) so the component file only
exports a component — fixes the react-refresh warning that came
out of the first pass.
- Action cards (feed + detail) now render their country chip
through CountryFlag, picking up the SVG for any future Tibet-
tagged action.
The older Pathos/Agora codebase treated CN-XZ as country-level Tibet
with a bundled Wikimedia Snow Lion SVG (commits f03d2400, 351b3be4,
6e04b80d). That fell out somewhere in the port — restore it.
- public/flag-tibet.svg recovered verbatim from f03d2400.
- New CountryFlag component centralises the country-flag rendering
decision: emoji for everyone Unicode covers, bundled SVG for the
short list of recognised flags that don't have an emoji
codepoint (Tibet today, room for more later).
- CountryPulseStrip special-cases CN-XZ as country-level: renders
'Tibet' (not 'Tibet Autonomous Region, China') and drops the
XZ subdivision-token badge.
Also adds the subdivisionFlag() helper for RGI tag-sequence
subdivisions (England, Scotland, Wales) — Unicode actually does
ship those, and the strip now picks them up automatically.
Other Unicode-missing subdivisions (US states, Canadian provinces)
still render as parent country flag plus a typographic ISO 3166-2
badge. They have no emoji codepoint and bundling a flag pack for
every state is out of scope for this change.
Cap the Discover page at max-w-5xl (down from max-w-7xl) so the
hero, country pulse, and shelves stop sprawling on widescreen
displays. Tighten the mixed feed below to max-w-2xl so each row
reads at a comfortable line length, the same reading column width
as the rest of Agora's NoteCard feeds — while the horizontal
shelves keep their wider canvas above.