Extend OnboardingRole and StartSignupOptions to include 'verifier', add a
third RoleCard to the role picker, and wire the pick handler to branch on
the new role. For now the verifier pick routes to the public /organizations
onboarding tool; a later commit replaces this with a captive sub-flow.
Adds an opt-in Tor mode that routes all app traffic through a local
SOCKS5 proxy backed by arti (Tor in Rust), bundled via the
org.torproject:arti-mobile:1.7.0.1 AAR.
- TorController starts/stops arti and installs a fail-closed WebView
proxy override (no direct fallback) so traffic can't leak while Tor is
connecting or down. Connectivity is verified against
check.torproject.org (IsTor) and re-checked continuously; the exit IP
is surfaced for verification, and the status isn't latched so a dropped
circuit downgrades honestly.
- TorPlugin bridges enable/disable/status to the Capacitor/JS layer.
Toggling applies live, in place, with no app restart.
- UI: a slim fail-closed status banner (replacing the old full-screen
gate), the Tor toggle in Advanced settings reachable while logged out,
and Settings/Search/About added to the logged-out menu.
- R8 keep rules for org.torproject.arti.** so the JNI native-method
classes aren't stripped/renamed; androidx.webkit on the compile
classpath for the WebView proxy APIs.
The meta description still framed Agora as a generic Nostr social
client. Update it to describe the actual product: peer-to-peer
crowdfunding with an integrated non-custodial on-chain Bitcoin wallet.
Once an organization publishes its verifier statement on /organizations,
show an interactive walkthrough demonstrating how to verify a campaign:
a looping three-step animation on a mock campaign card where a cursor opens
the three-dots menu and selects 'Verify this campaign'. Step list is
clickable to scrub; motion is gated behind prefers-reduced-motion.
The campaign card wraps its contents in a react-router <Link>. The
moderation menu's VerificationDialog and CampaignListMembershipDialog
portal their DOM out of the card, but React synthetic events still
bubble through the component tree to the Link — so clicking anywhere
in either dialog navigated to the campaign page. Wrap both dialogs in
a span that stops click/pointerdown propagation, mirroring the guards
already on the dropdown trigger and content.
The 'Verify this campaign' control was gated on the hardcoded moderator
pack. Extend eligibility to self-declared verifiers (accounts with a
kind 14672 verifier statement): useCampaignVerifications now exposes
canVerify (moderator OR verifier) and its verify mutation accepts
verifiers.
The campaign moderation kebab (ModerationMenu / ModerationOverlay) now
renders for verifiers too, but shows only the verify row — the
moderator-only items (Hide, Add to list) and the Hidden badge stay
gated on moderator status. Pledge and group surfaces are unchanged.
Verifier profiles (accounts with a kind 14672 statement) now get a
'Verified' tab listing the campaigns they have vouched for, resolved
from the account's own agora.verified (kind 1985) labels via the new
useVerifiedCampaigns hook. The tab is surfaced first and becomes the
default on initial load (without overriding a manual tab selection),
on both desktop and mobile. Non-verifiers never see the tab.
Adds ProfileVerifiedTab, the useVerifiedCampaigns hook, and profile.tabs
/ profile.verified i18n keys across all locales.
Restore ProfileVerifierSection to its original spot as the first item in
ProfileOverviewSections (left sidebar on desktop, overview tab on mobile),
reverting the full-width-above-tabs placement. Keeps the 'How We Verify'
heading inside the box and no shield icon.
Surface a verifier's kind 14672 statement as a full-width band above the
profile tabs (below the header) on both desktop and mobile, instead of
tucked into the left identity rail / overview tab.
Drop the shield-check icon and rename the heading to 'How We Verify' —
the statement is a self-authored claim, not a platform endorsement, so
the heading stays neutral with no trust-implying badge. Rename the
verifier.profileSectionTitle key to verifier.howWeVerifyTitle across all
locales.
Drop the benefit-cards section between the steps and the editor, along
with the now-unused BenefitCard component, Users/Eye/cn imports, and the
organizations.why.* i18n keys across all locales.
Restore the deleted Milkdown WYSIWYG editor (its deps and CSS were still
installed) as a reusable component under src/components/markdown/:
MilkdownEditor, a formatting toolbar (bold/italic/headings/lists/links/
quote/code/hr + markdown help popover), and an insert-link dialog. All
strings are now i18n-driven via a new mdEditor locale block, translated
across every locale.
Wire it into the /organizations verifier-statement editor, replacing the
raw textarea + separate live preview with rich-text editing that emits
markdown. Fix the editor placeholder so it only shows while empty by
toggling a has-content class on the ProseMirror DOM.
Rework the /organizations page to match the /about landing style so it
doubles as marketing and a functional tool: a dark world-map hero with a
scroll-to CTA, a three-step onboarding section, benefit cards, and a
'get started' section housing the verifier-statement editor. Logged out
shows the org-profile login gate; logged in shows the full publish /
update / withdraw editor.
Move the route to the wide FundraiserLayout so section backgrounds span
the viewport, and expand the organizations i18n block (hero, steps, why,
getStarted) across all locales.
Replace the /settings/verifier route with a public /organizations help
page that onboards organizations onto Agora. Logged-out visitors are
prompted to log in with — or create — their organization's Nostr
profile; once signed in they get the full verifier-statement editor
that previously lived under settings.
Remove the Verifier entry from the settings list, delete
VerifierSettingsPage, and link /organizations from the site footer
instead. The old /settings/verifier path now redirects to
/organizations.
The Verifier page was the last settings page still rendering an in-header
subtitle. Drop it from both the logged-in and login-gate headers (the
verifier.subtitle string is still used as the SEO meta description).
Move the header's horizontal padding onto the inner content row so the
constrained column matches the body's max-w column exactly (previously the
header column was 32px wider than the body content under the narrow
max-w-3xl layout). Reduce the back-arrow-to-title gap from gap-4 to gap-1.5.
Migrate EventDashboard's header layout classes from className to
contentClassName to match the new model.
The bot value stays in the form schema and is preserved on save; only the
toggle UI is removed. Drops the now-unused Switch and Form sub-component
imports.
Delete the Website, Lightning address, sortable custom-fields list, and
add-field preset pills (the entire Profile Fields block) from the profile
settings form. Existing values are still loaded into form state, so they
are preserved on save and continue to render in the profile card and the
live sidebar preview — only the editing controls are gone.
Also removes the now-dead helpers, handlers, presets, SortableFieldRow
component, and unused imports.
- Add an opt-in contentClassName to PageHeader so a page can constrain the
header row to the same centered column as its body. Settings sub-pages now
pass max-w-2xl/max-w-xl so the title and back arrow line up with the cards
instead of floating against the viewport edge.
- Drop the entire header from the Settings hub (no title/back arrow); the
grouped card list now reads the same on mobile and desktop.
- Remove redundant subtitles and intro blurbs from Appearance, Language,
Network, Advanced, Profile, and Notifications.
- Delete the now-orphaned i18n keys across all locales.
Rework the Settings hub and sub-pages into iOS-style grouped inset cards:
- Hub groups rows into Account / App / System sections, each row gets a
colored gradient icon tile, with a rounded bg-card container and hairline
dividers instead of the flat full-width list.
- Appearance and Language pickers use the same inset-card list rather than
bordered selectable buttons.
- Network and Advanced replace the heavy accent-bar section headers with
uppercase group labels above clean bg-card panels.
- Add settings.groups.{account,app,system} strings across all locales.
Anyone can become a verifier by publishing a kind 15063 replaceable
event whose Markdown content describes how they vet campaigns. The
statement is surfaced prominently in the profile overview so donors
can judge whether to trust the account's judgement.
- Document kind 15063 in NIP.md
- useVerifierStatement / useSetVerifierStatement hooks (read-modify-write)
- /settings/verifier form page with live preview, publish/withdraw
- ProfileVerifierSection rendered first in the profile overview
- Localize all strings across every locale
Agora is a P2P crowdfunding client using on-chain Bitcoin with a
non-custodial HD wallet (BIP-86 Taproot, BIP-352 silent payments),
not a Lightning project. Update the overview, document the on-chain
wallet and crowdfunding hooks, and demote Lightning to secondary
tipping. Drop the stale useShakespeare hook reference.
A forward-jumping scan range (e.g. "Scan recent" on a wallet whose
cursor was far behind) recorded the skipped gap as scanned: runScan set
scanHeight = max(scanHeight, highestContiguousScanned) where contiguity
was measured only from the range's own fromHeight. The auto-scanner then
resumed from the new cursor, so payments received in the gap were
silently never discovered.
The cursor now only advances when the scan range is contiguous with the
effective persisted cursor (relay scanHeight or local checkpoint).
Non-contiguous scans still merge any UTXOs they find — only the cursor
is held back, so a later auto-scan pass walks the gap. Never-scanned
wallets (cursor 0) keep the documented bootstrap jump to a recent
window.
The people receiving donations are not necessarily activists — they are
often simply people in need (e.g. in Venezuela and Gaza). Rename the
user-facing term from "activist" to "recipient" across all copy and the
underlying code.
- Rename ActivistGuidePage -> RecipientGuidePage and the /about/activists
route -> /about/recipients (legacy URL kept as a redirect).
- Rename guide structure/helpers, the paymentComparison audience literal,
and the related i18n key namespaces (activistGuide -> recipientGuide,
about.guides.activist -> .recipient, guides.activist -> guides.recipient,
activistRows/activistHeader -> recipientRows/recipientHeader,
ctaActivist -> ctaRecipient).
- Translate the term in all 16 locales, plus fix stale "activists" hero
taglines left over from 0233a75d in id/hi/sw/tr/zh-Hant.
The Private wallet tab only showed a spinner plus 'Scanning… block X / Y'
text while the silent-payment scanner ran, which gave no sense of how far
along the scan was. Render a Progress bar (matching the Scan options
dialog) above the block-count text so completeness is visible at a glance.
Profile and detail pages that render music tracks, podcast episodes, or
audio kinds call useAudioPlayer(), but the AudioPlayerProvider and its
companion floating-bar / navigation-guard components were deleted as
"orphaned" in 256560cf — they were never wired into App.tsx, so the hook
always threw "useAudioPlayer must be used within AudioPlayerProvider".
Restore the provider, MinimizedAudioBar, and AudioNavigationGuard, re-export
AudioPlayerContext from the context def, wrap AppRouter in the provider, and
render the two router-dependent components inside BrowserRouter.
Regression-of: 256560cf
Three changes target the scan pipeline's two bottlenecks (HTTP latency
and main-thread ECDH):
- Cache per-block tweak + UTXO data in IndexedDB (blockCache.ts) and
route fetches through it. Mined blocks below the indexer tip are
immutable, so a repeat or overlapping scan now costs zero round-trips
for blocks already seen. Keyed by (indexerUrl, height); degrades
gracefully when IndexedDB is unavailable.
- Move the secp256k1 ECDH + per-output Pk derivation into a dedicated
Web Worker (scan.worker.ts + scanWorkerClient.ts). The scan loop no
longer has to setTimeout(0) every 64 entries to keep the UI
responsive, and fetching now overlaps with ECDH. The client falls back
to a main-thread scan if the worker can't be constructed or errors on
a block. bscan stays on-device — a same-origin Worker is not the
remote scan-helper pattern BIP-352 contemplates.
- Make fetch concurrency tunable via AppConfig.bip352ScanConcurrency
(clamped to [1, 32], default 8) so a fast self-hosted indexer can run
hotter than the polite public-host default.
Moderators had no way to hide/verify/add-to-list a campaign from its
detail page — the hero toolbar only showed creator (edit/delete) controls.
Follow the same pattern as ActionDetailPage (pledge detail): place a
ModerationMenu in both the mobile and desktop toolbars of CampaignHero.
It returns null for non-moderators so no conditional wrapping is needed.
- Add coord and entityTitle props to CampaignHeroProps; thread them from
CampaignDetailContent (campaign.aTag, campaign.title)
- Mobile: ModerationMenu sits in the right-side controls group, before
the creator edit/delete buttons
- Desktop: ModerationMenu appears at the far right of the top overlay bar
(after creator controls when present), styled with the same
bg-black/30 backdrop-blur chip as ActionDetailPage
On /campaigns/lists/:slug, ListMemberCard rendered its own reorder/remove
kebab at top-3 right-3 z-20, directly on top of CampaignCard's ModerationOverlay
(also top-3 right-3 z-10). The ListMemberCard kebab always won stacking order,
blocking moderators from accessing moderation actions on list-page cards.
- Add showModerationMenu prop to CampaignCard (default true); when false,
ModerationOverlay renders only the Hidden badge (no menu trigger), keeping
hide-state visibility without adding a second kebab in the same corner.
- ListMemberCard now passes showModerationMenu={false} to CampaignCard and
embeds ModerationMenuItems directly into its own dropdown (below a separator
after the reorder/remove items), giving moderators a single combined menu
with both list-management and moderation actions.
- Dialog state (CampaignListMembershipDialog, VerificationDialog) is hoisted
to ListMemberCard so the dropdown-unmount lifecycle doesn't tear them down.
Scan progress was only persisted to the relay on a match or at end-of-scan,
so refreshing mid-scan re-scanned every block since the last completed scan.
Add a free, synchronous localStorage checkpoint of the advancing scanHeight
(throttled to 5s, plus a final write on finish) scoped per pubkey+indexer.
The auto-scanner now resumes from max(relay scanHeight, local checkpoint),
bounding mid-scan-refresh re-scan to ~5s of blocks without adding any
encrypt/sign/broadcast traffic. Relay publishing stays gated to matches and
end-of-scan for cross-device sync; manual scans still honor the exact
fromHeight the user picks.
Lift the silent-payment orchestrator into an HdWalletSpProvider mounted at
the app root so a single shared scan instance keeps running across page
navigation. Add a background auto-scanner that resumes from the last
persisted scanHeight and keeps up with the chain tip without any user
action, bounding the first pass on a never-scanned wallet to ~7 days of
blocks. Surface live scan state on the Private wallet tab and add an
auto-scan toggle to the scan dialog.
Add an inline variant of CampaignVerificationBadge that reads on a light
page surface (bordered chip with a 'Verified' label) and render it next to
the author byline in the campaign detail heading. The overlay variant (dark
pill) is unchanged for cards over banners.
New i18n key campaignVerification.verifiedLabel across all locales.
Clicking 'Verify this campaign' now opens a VerificationDialog instead of
publishing immediately. The dialog previews the moderator's own avatar
with the verification checkmark (as it appears on verified campaign cards)
and shows the attestation the moderator is making before confirming.
- New VerificationDialog component (avatar + checkmark preview + attestation
text + Cancel / Verify actions).
- ModerationMenu owns the dialog state and the verify mutation, hoisted out
of the dropdown content (which unmounts on select) like the existing
add-to-list dialog; passes onRequestVerify down to CampaignVerifyItem.
- Removing a verification stays inline (no confirmation).
- New i18n keys dialogTitle + attestation across all locales.
Verification is gated by the existing campaign moderator pack
(useCampaignModerators / CAMPAIGN_MODERATORS), not a separate allowlist.
- Remove the config.labelers field (AppContext interface, Zod schema,
App.tsx and TestApp defaults) and delete useCampaignLabelers.
- useCampaignVerifications now reads/writes agora.verified labels gated
on the moderator pack (isModerator), same authors filter as the other
label streams.
- Move the verify / remove-verification row INSIDE the moderation kebab's
'Moderator actions' section (leadingExtra slot of ModerationItemsShell),
no longer a top-level item above the section label.
- Revert the isMod || isLabeler widening in ModerationMenu/Overlay back to
plain isMod.
- Remove the trailing 'Verified' checkmark text from 'Remove my
verification'.
- Rename labeler->moderator in agoraVerification, the badge component, and
all locale strings; drop now-unused notVerified / verifiedState keys.
- Update NIP.md to document verification as a moderator action.
The verify / remove-verification row now renders above the moderator
rows. The moderator section's own 'Moderator actions' label provides the
visual separator, so no extra divider is needed between them.
The verify / remove-verification controls now live as rows in the
campaign 'Moderator actions' 3-dots menu instead of inside the
verification badge popover. The badge over the card is now display-only
(stacked verifier avatars + a popover listing them, linking to profiles).
Because labelers are a distinct allowlist from the moderator pack, the
campaign moderation menu/overlay now mounts for a user who is a mod OR a
labeler; the verify row itself is gated on labeler membership, while the
hide/add-to-list rows stay moderator-only.
Trusted labelers (AppConfig.labelers) can vouch for campaigns via NIP-32
kind 1985 labels in the new agora.verified namespace. Verifier avatars
render as a stacked badge on campaign cards; hovering/clicking opens a
popover listing verifiers (linking to their profiles). Logged-in labelers
get verify / remove-verification controls — verify publishes a label,
unverify issues a kind 5 deletion of their own label.
The read query filters by authors: labelers so verifications from outside
the allowlist are never honored. Kind 1985 label reqs are routed to the
search relays (relay.ditto.pub, relay.dreamith.to) in NostrProvider.
Expose the Esplora API endpoints, Blockbook URL, and BIP-352 silent
payment indexer (already wired through AppConfig) in a new Bitcoin
section on the Advanced settings page. Each field validates input,
normalizes trailing slashes, and offers a reset-to-default button;
clearing the indexer URL disables silent-payment scanning.
The home hero row sliced the Featured Campaigns list to the first 6
members (FEATURED_HERO_CAP). Drop the cap so the row renders every
campaign the curator has added — the existing hero layout (two
column-spanning cards + 4-up tail) already scales to any count. The
skeleton placeholder count stays bounded by FEATURED_SKELETON_CAP for
the loading state only.
Switch the home-page hero row from the World Liberty Congress curated
list (d=world-liberty-congress) to the Featured Campaigns list
(d=featured-campaigns), both curator-published kind-30003 NIP-51
bookmark sets.
Replace the WLC org-branded heading (avatar, name, npub profile link
pulled via useAuthor) with a plain 'Featured Campaigns' heading that
links to /campaigns/lists/featured-campaigns. Drop the now-unused
Avatar / useAuthor / genUserName imports and the WLC pubkey/npub
constants.
Rename the WLC_* constants to FEATURED_* and replace the wlcDesc
locale key with featuredTitle + featuredDesc across all 16 locales.
The campaigns grid renders up to 200 CampaignCards at once with no
virtualization, and each card eagerly ran useCampaignDonations: an
Esplora /address lookup per card (polling every 30s via refetchInterval)
plus a /tx verification call per kind-8333 donation receipt. On first
paint that's hundreds of /address calls and potentially thousands of
/tx calls in one burst, then 200 more /address calls every 30s forever.
Once mempool.space starts returning 429, the failover client retries
each call across every configured endpoint, cascading the rate-limit to
all of them.
Gate the donation lookup behind an IntersectionObserver so only on-screen
cards talk to Esplora, and drop the per-card 30s polling. The detail page
(a single instance, not a grid) opts back into the 30s live refresh via a
new refetchInterval option.
- Add useInView hook (once-only, rootMargin pre-arm, IO-less fallback).
- useCampaignDonations gains enabled + refetchInterval options; default
no polling, longer staleTime; treat 'not enabled yet' as loading so
off-screen cards show a skeleton instead of flashing '0 raised'.
- Gate CampaignCard and CampaignInlinePreview on visibility.
- CampaignDetailPage opts into refetchInterval: 30_000.