Commit Graph

86 Commits

Author SHA1 Message Date
Alex Gleason 59c0d25fa6 Change verifier statement kind to 14672 2026-06-12 12:55:00 -05:00
Alex Gleason b271c4e889 Add verifier statements (kind 15063)
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
2026-06-12 12:42:50 -05:00
Alex Gleason f95ab1b422 Treat campaign verification as a moderator action
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.
2026-06-06 19:11:48 -05:00
Alex Gleason b8c1bc7409 Add agora.verified campaign verification labels
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.
2026-06-06 18:06:36 -05:00
Chad Curtis c79699ca71 campaigns: remove deadline from events and form
Drop the optional `deadline` tag from kind 33863 campaigns. Removes the
date input and validation from the create/edit form, the deadline chips
on the card, detail, and inline-preview surfaces, and the derived
"ended" state that disabled donations after the deadline. Cleans up the
associated locale keys and NIP.md documentation.
2026-06-01 17:03:30 -05:00
mkfain b0759402cf Retire the approval axis; Featured becomes the sole positive-curation mechanism
Now that moderators can directly order the Featured row, the second
"Community Campaigns" bucket (approved + not-featured + not-hidden)
is redundant. This commit removes the approval axis end-to-end and
collapses the home page to a single curated section.

Protocol (NIP.md):

- `ModerationLabel` shrinks from six values to four — `hidden`,
  `unhidden`, `featured`, `unfeatured`. The legacy `approved` /
  `unapproved` labels are now ignored on read and MUST NOT be
  published.
- `ModerationAxis` shrinks from three to two: `hide` and `featured`,
  both supported by all three surfaces (campaigns, organizations,
  pledges).
- The rank tag now only applies to `featured` labels.
- A migration note in NIP.md explains the retirement and tells
  clients to ignore lingering approval-axis labels in relay
  archives.

UI:

- CampaignsPage drops the Community Campaigns and Pending sections.
  Home is now Featured (with the empty state in place when nothing
  is featured) → Browse-all link → moderator-only Hidden section.
  The labeled-coord targeted fetch shrinks to hidden coords only.
- ModerationMenu loses the Approve / Unapprove rows and the
  `hasApproval` / `isApproved` plumbing.
- `CampaignCard`'s `axes` prop drops `'approval'`.
- `ReorderAxis` collapses to a single axis — the type and the
  parameter are removed from the reorder hook, provider, context,
  and grid component since every reorder targets the featured axis.
- Pledge and organization moderation hooks lose their defensive
  `'approved' | 'unapproved'` rejection branches now that those
  values are off the `ModerationLabel` union.

i18n (16 locales):

- Five moderation.menu keys removed: `approve`, `unapprove`,
  `approvedState`, `toastApproved`, `toastUnapproved`.
- Five campaigns.home keys removed: `community`, `communityDesc`,
  `pending`, `pendingDesc`, `pendingEmpty`.
- `campaigns.home.yourCampaignsDesc` rewritten across every locale
  to drop the "appears on the homepage once a moderator approves"
  copy; new copy points authors at /campaigns for discovery and
  notes that the team curates a featured selection on the home page.

Test suite green: tsc, eslint, vitest, vite build all pass.
2026-05-30 22:48:21 +02:00
mkfain ef9c2eff89 Fix campaign reorders silently reverting when moving downward
The first reorder implementation encoded list position directly in
the moderation label's `created_at` and republished the same axis
label with a chosen timestamp. That fights the fold's
newest-event-per-(coord,axis) rule the moment a moderator tries to
lower a campaign's position: the new label has an older
`created_at` than the existing one and the fold rejects it. The
relay accepts the publish, but every subsequent read folds back to
the higher-`created_at` predecessor and the move appears to revert.

Move-up worked (its new `created_at` was strictly newer); move-down,
drag-down, and any drag-to-midpoint that landed below an existing
neighbor silently no-op'd. Anything dragged into the middle of an
already-old list also picked a past timestamp that some relays
reject for being too far behind "now".

The fix decouples sort key from event recency:

- Reorder publishes always use `created_at = now`, so the fold's
  newest-wins rule always picks them up.
- The chosen position is encoded as a `["rank", "<integer>"]`
  tag on the label.
- `foldModerationLabels` extracts the rank with a `created_at`
  fallback, so labels published before this change (and any normal
  approve / hide / feature actions that don't carry a rank) still
  sort by `created_at` exactly as they used to.

Ranks are sourced from `Date.now() * 1000` (microseconds since
epoch), so:

- Fresh "feature" / "approve" publishes always sit above legacy
  labels whose effective rank is a seconds-since-epoch value.
- Midpoint inserts have ~1000x headroom per second of inter-rank
  gap, comfortably enough for thousands of reorders before any
  renumbering would matter.
- Headroom against `Number.MAX_SAFE_INTEGER` is ~150 years.

Callers downstream (CampaignsPage, CampaignsDiscoverySection,
PledgesDiscoverySection, useFeaturedOrganizations) still consume
`featuredOrder` / `approvedOrder` as `Map<coord, number>` sorted
descending — the map names and shapes are unchanged, only the
value computation is now "rank ?? created_at" instead of
"created_at".

NIP.md updated to document the rank tag, the fallback semantics,
and the reorder operations in terms of ranks.
2026-05-30 22:22:09 +02:00
mkfain 9e26bb8209 Let moderators reorder Featured and Community campaign lists
The Featured row already sorted by the moderator's `featured` label
`created_at`, but reordering required clicking Unfeature then Feature
again — clumsy, and the Community grid sorted only by campaign
`created_at` with no moderator input at all.

This commit promotes the existing axis-label `created_at` into a
first-class sort key on both lists and adds drag-and-drop + kebab-row
UI for moderators.

Protocol (no schema change):

- The Featured row sorts by the `featured` label's `created_at`,
  newest first (existing behavior).
- The Community grid now sorts by the `approved` label's
  `created_at`, newest first (mirroring the Featured row).
- Reordering = republishing the same axis label for the moved
  campaign with a chosen `created_at`. Move-to-top stamps `now`;
  move-up stamps `neighborAbove.t + 1`; move-down stamps
  `neighborBelow.t - 1`. Drag-to-position picks a value between the
  two new neighbors.
- No new tags, no new kinds, no new authority — readers that already
  understand the moderation namespace pick up the order for free.
- Conflict model unchanged: newest label per (coord, axis) wins.

Implementation:

- `foldModerationLabels` now populates `approvedOrder` alongside
  `featuredOrder`.
- `useCampaignModeration().moderate` accepts an optional explicit
  `created_at` for the label event (omitted for normal
  approve/hide/feature; passed by the reorder hook).
- New `useReorderCampaign` hook with `moveToTop`, `moveUp`,
  `moveDown`, and a general `moveTo(toIndex)` used by drag-and-drop.
- New `ReorderableCampaignGrid` wraps a list of `CampaignCard`s:
    - non-mods get a plain grid, zero overhead;
    - mods on desktop get HTML5 drag-and-drop with a six-dot handle
      on hover (the handle is the only `draggable` element so card
      clicks still navigate the underlying `<Link>`);
    - mods on mobile get Move up / Move down / Move to top rows
      injected into the existing moderator kebab via a context
      provider (`ReorderProvider` / `useReorderControlsFor`).
- An optimistic local order smooths the gap between publish and
  refetch so the card snaps into the new position immediately; it
  rolls back automatically on publish failure.
- Translations added in all 15 non-English locales.
- NIP.md documents the ordering convention in a new
  "Moderator-driven Ordering" section under the campaign-moderation
  surfacing rules.
2026-05-30 22:07:53 +02:00
mkfain c53e476dee Move the all-campaigns directory from /campaigns/all to /campaigns
/campaigns was a redirect to / (the curated home), and the actual
all-campaigns directory lived at /campaigns/all. Flip the routing
so /campaigns IS the directory, the home page stays at /, and
/campaigns/all becomes a redirect to /campaigns for any external
links and bookmarks that still point there.

Rewrite every internal link/navigate target accordingly (TopNav,
the Browse-all CTA on the home page, the OnboardingGate donor
redirect, NoteCard's kind-33863 nounRoute) and refresh the
doc/comment references in NIP.md and the discovery hooks.
2026-05-30 13:19:29 +02:00
Alex Gleason 0a643de87f Merge branch 'main' of gitlab.com:soapbox-pub/agora 2026-05-25 15:19:01 -05:00
Chad Curtis e33f306a64 Land the missing usePledgeModeration hook and document pledge labels
The moderation menu / overlay / review-queue refactor wired pledges
into the same shared moderation components as campaigns and groups,
and three call sites (ActionsPage, ModerationMenu, ModerationOverlay)
imported usePledgeModeration from @/hooks/usePledgeModeration. The
hook file itself was never staged, so the tree didn't build and any
fresh clone would have failed tsc at those imports.

Add the hook (two-axis model — hide + featured, no approval gate, same
agora.moderation namespace and Team Soapbox moderator pack as
campaigns/organizations) and document the kind 36639 surface in
NIP.md alongside the existing 33863 / 34550 entries so the spec
matches the implementation.

Regression-of: c61e9a06
2026-05-25 02:20:16 -05:00
Alex Gleason 7e3de000a2 NIP.md: source campaign 'raised' total from on-chain address, not 8333 receipts
The kind 33863 Querying section previously told clients to aggregate
verified kind 8333 receipts for the campaign total. That undercounts
the campaign whenever a donor pays the BIP-21 QR with a native wallet
(no Nostr receipt published) — which the spec itself documents as a
supported donation path.

Document the actual implementation: the headline 'raised' amount is
cumulative chain_stats.funded_txo_sum on the on-chain `w` address,
fetched from an Esplora endpoint (default mempool.space). Kind 8333
receipts remain the source of the recent-donation list (donor pubkey,
comment, timestamp) but no longer drive the headline total.

Update the Wallet Modes UI matrix to match.
2026-05-24 23:53:34 -05:00
Alex Gleason c7473f824b wallet: derive BIP-39 mnemonic from nsec (v2) so funds import into any BIP-39 wallet
The HD wallet seed is now BIP-39-compatible. Pipeline:

  entropy  = HKDF-SHA256(nsec, info="agora/v1", length=32)
  mnemonic = BIP-39 encoding of (entropy || checksum)  // 24 words
  seed     = PBKDF2-HMAC-SHA512(mnemonic, salt="mnemonic", iters=2048)

The 24 words import cleanly into Sparrow, Electrum, Trezor, Ledger,
BlueWallet, Phoenix, etc., at the BIP-86 / BIP-352 paths. HKDF domain
separation means a leaked mnemonic compromises only the wallet, not
the Nostr identity (unlike the raw nsec).

v1 derivation (nsec used directly as BIP-32 master seed) is retained
as migration-only code. A new /wallet/migrate-v1 page detects funds
at the legacy addresses and builds a single sweep PSBT to consolidate
them into the v2 wallet. A persistent banner on /wallet surfaces the
flow when v1 funds exist.

The mnemonic shows up in two places: a "Back up wallet" dialog on
/wallet, and a section in Profile -> Advanced next to the nsec
backup. nsec backup copy updated to explain the relationship.

Locked test vectors pin the entire derivation pipeline (nsec -> 24
words -> first BIP-86 address -> sp1q...) so any future drift fails
loudly. Regenerate via scripts/derive_vectors.mjs.

Other changes:
- Re-key SP storage NIP-78 d-tag to /v2 so v1 and v2 UTXOs do not mix
- Re-key the persisted receive-address cursor to :v2: namespace
- Relax SP spend-key helper to 16-64 byte seeds (BIP-32 range) so the
  migration sweep can sign with the legacy 32-byte v1 seed too
- Remove stale NIP-SP references from derivation comments (the draft
  was not relevant to our use case)
- Document the wallet derivation scheme in NIP.md
- Translate every new string to all 10 non-English locales
2026-05-24 15:39:22 -05:00
lemon 67a31a918a Add group country and topic fields 2026-05-22 23:37:32 -07:00
Alex Gleason 708ebd9bef Campaigns: support both on-chain and silent-payment wallets per campaign
A campaign may now declare up to two `w` tags — at most one mainnet
on-chain address (bc1q…/bc1p…) and at most one silent-payment code
(sp1…) — and the QR/payment panel combines them into a single BIP-21
URI (`bitcoin:<bc1>?sp=<sp1>`) when both are present. BIP-352-aware
wallets pick the SP parameter automatically; legacy wallets fall back
to the on-chain address.

The campaign form is reorganized around the dual-endpoint model. Users
with nsec access see two avatar chips — "My wallet" and "My private
wallet" — both selected by default and an "Add another address"
disclosure that reveals separate bc1 and sp1 inputs. A typed value
wins over the corresponding chip's HD-derived value, so a cold-storage
address can be substituted without giving up the SP code. Users
without nsec access (extension / bunker logins) see the two custom
inputs unconditionally. At least one of the four sources must resolve.

The on-chain receive-index cursor is still advanced only at publish
time, and now only when "My wallet" is selected AND no custom
on-chain value was provided — so the cursor never burns on a no-op
edit or on a publish where the user overrode the chip with their own
address.

`ParsedCampaign.wallet` is replaced by `ParsedCampaign.wallets`, a
`{ onchain?, sp? }` struct. Consumers (`useCampaignDonations`,
`useDonateCampaign`, `useProfileCampaignStats`, `useOnchainZaps`,
`CampaignCard`, `CampaignDetailPage`, profile rails) keep their
existing on-chain semantics by reading `wallets.onchain`. The
"Private campaign" badge and hidden-aggregates UI now trigger on
SP-only campaigns (no on-chain endpoint), matching the spec.
2026-05-23 00:00:39 -05:00
mkfain af483d9989 Cut Approved from organization moderation, fix Featured load latency
Organizations now use a two-axis moderation model — featured and hidden.
The approval axis is campaign-only. Every Agora-tagged organization is
publicly visible by default; moderators curate by lifting orgs into the
Featured shelf or suppressing them with a Hidden label, nothing in
between.

Concretely:

- CommunityModerationMenu drops the Approve / Unapprove items. The org
  side never publishes those labels; useOrganizationModeration's
  mutate() rejects them defensively so a stray UI bug can't poison the
  label stream with axis-mixed events. The shared ModerationData type
  still tracks approvedCoords for symmetry with the campaign side, but
  the org UI never reads or writes it.

- /communities loses the in-flight 'Approved organizations' grid that
  was never finished and was conceptually wrong here. The moderator-only
  rail formerly called 'Pending review' is renamed 'Needs review' and
  re-derived as t:agora AND not featured AND not hidden.

Two perf fixes that should make Featured paint noticeably faster:

- The 200-event discovery query and the 2000-event label fold that
  power the moderator rails now live INSIDE ModeratorReviewSections.
  Non-moderator viewers (the overwhelming majority on /communities)
  never trigger either query — they used to fire at the page level
  whether they were used or not.

- Every CommunityMiniCard used to subscribe to useOrganizationModeration
  individually so it could overlay the Hidden badge and kebab. With
  18+ cards per page, that meant 18 component subscriptions to a
  query no non-mod cared about. The badge + kebab now live inside a
  single CommunityModerationOverlay that's gated on isMod up front
  and returns null for everyone else; non-mods never subscribe.

- staleTime on useOrganizationModeration and useFeaturedOrganizations
  bumped from 30s to 5m, with a 1-hour gcTime. Moderators feature and
  hide on human timescales, not seconds — repeat visits to /communities
  shouldn't pay a fresh relay round-trip every half-minute. Local
  changes still invalidate the keys explicitly so moderator actions
  show up immediately.

Also reverts two pieces of unrelated scope creep I built by mistake:
the 'Approved campaigns' section on the org detail page and the
moderator kebab on the campaign detail page. Those weren't asked for —
they were chasing a misread of an earlier instruction.

NIP.md updated to reflect the campaign-vs-org axis split and the new
'Needs review' surface name.
2026-05-21 21:43:48 -05:00
mkfain 9fd585ebdd Moderator-curated featured organizations
Replace the hardcoded FEATURED_ORGANIZATION_AUTHORS allowlist with the
same NIP-32 label flow that curates featured campaigns: Team Soapbox
pack members publish kind 1985 labels in the agora.moderation namespace
tagging an organization's 34550:<pubkey>:<d> coordinate as featured,
hidden, or approved, and useFeaturedOrganizations folds those labels
into the /communities Featured shelf.

The campaign and organization label streams share a single namespace
and a single moderator pack — they're separated purely by which kind
prefix the 'a' tag carries. To keep that contract enforced in one
place, the constants, types, and folding logic are now in
src/lib/agoraModeration.ts; useCampaignModeration and the new
useOrganizationModeration both call foldModerationLabels with their
respective kind. The campaign hook's external surface
(AGORA_MODERATION_NAMESPACE, ModerationLabel, CampaignModerationData)
is preserved via re-exports so existing call sites don't move.

Moderators see a CommunityModerationMenu kebab overlaid on every
CommunityMiniCard exposing approve/unapprove, hide/unhide, and
feature/unfeature. Mounting reads moderation state once per page from
the shared TanStack cache, mirroring CampaignCard. Non-moderators see
no overlay (the menu returns null) and no card affordances change.

The 'My organizations' shelf intentionally ignores moderation — a
user's own founded, moderated, or followed organizations always render
regardless of label state. Only the Featured shelf consumes the
curation rollup.

The Featured grid is uncapped: moderators control how many orgs
surface by labeling, and ordering follows the recency of each
'featured' label so re-publishing bumps an org to the top.

NIP.md's 'Campaign Moderation Labels' section is renamed to 'Agora
Moderation Labels' and documents the kind-34550 coord form and the
'My organizations ignores moderation' rule.

Note: existing surfaced organizations will disappear from the shelf
until a moderator publishes featured labels for them.
2026-05-21 21:12:33 -05:00
mkfain 7ee35644e3 Strict t:agora tagging for all Agora-created content
The Agora activity feed now filters strictly to Agora-created content via
the relay-indexed single-letter `t:agora` tag. Multi-letter tags like
NIP-89 `client` are not indexed by relays and cannot serve this purpose.

Every event Agora publishes that represents first-class Agora content
now carries `["t", "agora"]`, added via a new `withAgoraTag` helper
in `src/lib/agoraNoteTags.ts` that dedupes against any user-supplied
`t:agora` tag.

Tagged at publish time:
- Communities (kind 34550) — CreateCommunityPage
- Campaigns (kind 30223) — CreateCampaignPage, useArchiveCampaign
- Pledges (kind 36639) — CreateActionPage (alongside agora-action)
- Calendar events (kinds 31922 / 31923) — CreateEventPage and
  CreateCommunityEventDialog
- Onchain zaps (kind 8333) — useOnchainZap, useDonateCampaign,
  SendBitcoinDialog
- Zap goals (kind 9041) — CreateGoalDialog
- NIP-22 comments (kind 1111) — usePostComment, covering every comment
  authored from within the app regardless of root kind
- Kind 1 notes — already covered by ComposeBox default tags

Intentionally not tagged: reactions, reposts, follows, profile metadata,
lists, settings, badges, vanish requests, encrypted DMs, live chat.

useAgoraFeed tightened:
- Entity kinds and Agora-comment kinds now require `#t=agora` at the
  relay layer (server-side filter).
- World layer (kind 1111 / 1068 with `#k=iso3166|geo`) remains
  unfiltered — intentionally cross-client.
- `#Agora`-tagged kind 1 notes still surface from any author (preserves
  viral / opt-in discovery via user-typed hashtags).
- Donation enrichment now requires the Agora marker on zap receipts.
- `isRelevantAgoraEvent` rewritten as a strict checker that demands
  the marker for everything outside the world layer.

Legacy content without the marker disappears from the feed. It remains
reachable by direct link and via kind-specific directories (e.g.
`/campaigns/all`). Authors who edit a legacy event through the Agora UI
will automatically add the marker via the helper.

NIP.md updated with a new "Agora Content Marker" section under "Agora
Protocols" — documents the tagged-kind table, the untagged-kind list,
the canonical query shape, and the backward-compatibility behavior.
2026-05-21 20:25:01 -05:00
Chad Curtis d66eaf6aa4 Redefine Campaign kind: 30223 -> 33863 with wallet endpoint
Replaces the kind 30223 Campaign spec with a clean kind 33863 design.
Hard cutoff: no migration, no dual-read, no legacy support.

Key changes:
- Kind number 33863 (FUND on T9 keypad).
- Single `w` tag carries one bech32(m) wallet endpoint. Prefix
  selects the mode: `bc1q`/`bc1p` for public on-chain, `sp1` for
  silent payments (BIP-352). Other prefixes are rejected.
- Recipient `p` tags removed. Campaigns are self-authored; the
  event author is the beneficiary and owns the wallet in `w`.
- No split weights, no dust calculations, no BIP-340/341 Taproot
  derivation from Nostr pubkeys.
- `t` topic tags and `agora.category` labels removed. Discovery is
  via search, country (`#i`), and moderator curation.
- `image` -> `banner`, with required NIP-92 `imeta` for dim,
  blurhash, MIME, and SHA-256.
- `goal` is a single integer USD value, no unit, no currency code.
- `status` tag removed. To close a campaign, publish a NIP-09 kind
  5 deletion referencing the campaign's `a` coordinate.
- `location` legacy tag removed.
- Kind 8333 receipts for campaigns carry no `p` tags; verification
  matches tx outputs against the campaign's `w` address. SP
  campaigns publish no receipts and hide all aggregate UI by design.
2026-05-21 19:03:55 -05:00
Alex Gleason b53cb20d61 Replace Discover with Support nav link
Remove the Discover page and route entirely. In the top nav, swap the
Discover entry for a Support link pointing at /campaigns/all (the all
campaigns directory).

Also drops the now-orphaned DiscoverHero component and useDiscoverFeed
hook, and updates the NIP.md campaign moderation note that referenced
the deleted /discover route.
2026-05-21 17:39:15 -05:00
lemon ffb9c93ee6 Add pins across detail comments 2026-05-21 14:08:11 -07:00
lemon 48794fa3b4 Pin campaign activity updates 2026-05-21 14:08:11 -07:00
lemon f3c9a74b0f Simplify pledge creation form
Pledges are now open-ended by default — drop the optional start
date input and stop publishing the legacy `start` tag. The kind
36639 `created_at` already marks when the pledge becomes active.

Hide the built-in cover image templates so the upload area is the
only path for cover art, encouraging pledgers to supply their own
visuals. The thumbnail strip remains available via the
`templates` prop on `CoverImageField` for other surfaces.

Rename the description heading from "Story" to "Description"
to avoid implying a narrative expectation, and label the optional
tag input as "Recommended" to nudge categorization.

Mirror the same changes in `CreateActionDialog` (the community
scoped variant) and mark `start` as legacy in `NIP.md`.
2026-05-20 12:32:57 -07:00
lemon c9b48aeaae Align pledges with campaign patterns 2026-05-20 12:32:57 -07:00
lemon 3dd229edfb Reframe actions as pledges 2026-05-20 12:32:57 -07:00
mkfain 09bd4096e2 Make Featured a moderation axis and add an All Campaigns page
Featured was a hardcoded array of naddrs in src/lib/featuredCampaigns.ts.
Promote it to a third moderation axis (`featured` / `unfeatured`)
alongside `approved` and `hidden`, managed by Team Soapbox pack members
through the same kebab menu on each campaign card.

The Featured row on the homepage now:
- Reads from `moderation.featuredCoords`, ordered newest-featured-label first.
- Caps at 4 campaigns.
- Adapts its grid to 1/2/3/4 desktop columns based on count (mobile stays
  one column), collapses when nothing is featured, and surfaces the hero
  `variant="featured"` card only when exactly one campaign is featured.
- Hide still wins: a featured-then-hidden campaign disappears from the row.

Rename the homepage's second section from "All campaigns" to "Community
Campaigns", which more accurately reflects that it's the approved-not-
hidden set with featured campaigns deduplicated out.

Add a new /campaigns/all page that lists every campaign found on relays
(approved, pending, and unmoderated alike), with a "Show hidden" toggle
that adds hidden campaigns back in. The Discover page's "All campaigns"
link now points here instead of /, since the homepage is no longer a
truly-all view post-moderation. Also surface a small "Browse all
campaigns" link beneath the Community Campaigns grid so the new page is
discoverable from home.

Update NIP.md to document the third axis and the home-page surfacing
rules (Featured row, Community Campaigns grid, Discover shelf).

Delete src/lib/featuredCampaigns.ts entirely — there's no longer a build-
time list to maintain.
2026-05-19 18:04:28 -05:00
Chad Curtis d065580e47 Curate the campaigns homepage with Team Soapbox labels
Move campaign curation from a hardcoded HIDDEN_CAMPAIGN_COORDS set to a
real moderation system. Team Soapbox (kind 39089 follow pack
k4p5w0n22suf) is the moderator roster; each member signs NIP-32 kind
1985 labels in the agora.moderation namespace to approve or hide a
campaign. The home page and Discore shelf render the approved-and-
not-hidden set; moderators additionally see Pending + Hidden sections
and a per-card kebab menu. Non-moderator authors get a Your Campaigns
section explaining their campaign is live on Nostr but awaiting a
homepage approval.
2026-05-19 16:14:45 -05:00
Alex Gleason d71d6de05f Publish a single kind 8333 receipt per donation tx
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.
2026-05-18 16:36:32 -05:00
lemon 634e161085 Remove ephemeral geo chat 2026-05-18 09:49:31 -07:00
lemon f665ffa0c0 Flatten campaign form details 2026-05-17 23:12:13 -07:00
lemon ba2c541c31 Add country tags to campaigns 2026-05-17 23:12:13 -07:00
mkfain 323c613222 Add archive flow for campaigns
Authors can soft-close a campaign by republishing it with a
`["status", "archived"]` tag. Archived campaigns are hidden from the
main fundraisers feed and the donate button is disabled, but the detail
page still loads by direct link so existing donors can find it and
past donations remain attached. The author sees Archive / Reopen
buttons on the detail page and an Archived badge on cards.

useCampaigns gains an `includeArchived` option (default false) so a
future profile view can opt in. NIP.md documents the new status tag.
2026-05-17 20:20:44 -05:00
Alex Gleason 1db8b4d5b0 Add fundraising campaigns as the new home surface
Pivot the homepage from a Twitter-style social feed to a GoFundMe-style
fundraising hub. Introduces a new addressable kind 30223 "Campaign" that
carries the marketing-style metadata (title, summary, cover image, story,
category, goal, deadline, location) plus a list of recipient pubkeys with
optional split weights. Documented in NIP.md alongside the kind 8333
onchain-zap spec it builds on.

Donations are sent as a single multi-output Bitcoin transaction (one
output per recipient, derived Taproot addresses) using the existing
buildUnsignedMultiOutputPsbt + useBitcoinSigner infrastructure that
backs community on-chain zaps. After broadcast, the client publishes
one kind 8333 receipt per recipient with the campaign's `a` coordinate
so the donation aggregates into the campaign's totals.

UI surfaces:

- /campaigns is now the default homePage. Hero, two featured slots
  (placeholders in src/lib/featuredCampaigns.ts), then a grid of
  user-submitted campaigns.
- /campaigns/new is a full create form with cover upload, slug
  collision check, recipient builder with per-row weights, and
  preset/custom donation-amount UX.
- naddr1 identifiers for kind 30223 route to CampaignDetailPage via
  NIP19Page (full story rendered through the existing ArticleContent
  markdown component, plus a sticky donate rail with progress).
- DonateDialog presets are tuned for on-chain amounts (10K-1M sats)
  with a dust-aware minimum guard derived from the split math.
- Fundraisers sidebar item with a HandHeart icon.

Kept the existing social-feed pages addressable from the sidebar; the
overhaul is scoped to the home/landing experience rather than removing
the underlying Nostr features.
2026-05-17 16:48:06 -05:00
Alex Gleason c5b929187a Remove Nostr direct messaging feature
The @samthomson/nostr-messaging library opens fresh NRelay1 sockets per
participant per relay outside the shared NPool, fanning out to every
conversation partner's NIP-65 + NIP-17 inbox relays plus all
discoveryRelays in hybrid mode. In practice this drives connection counts
to several hundred relays per session.

Rather than band-aid the fan-out, drop the feature entirely and point
users to White Noise for end-to-end encrypted Nostr chat.

- Replace /messages with a 'Install White Noise' CTA card (route kept)
- Delete MessagingSettingsPage, DMProviderWrapper, messaging-intro.png
- Remove DMProvider wrapper and PROTOCOL_MODE config from App.tsx
- Drop messaging config from AppConfig, AppConfigSchema,
  EncryptedSettingsSchema, EncryptedSettings, and the NostrSync /
  useInitialSync sync paths
- Remove messages sidebar entry, default sidebarOrder slot, and
  SettingsPage messaging card
- Uninstall @samthomson/nostr-messaging and drop its tailwind content
  glob and vitest deps.inline entry
- Update copy in PrivacyPolicy, AdvancedSettings delete-account warning,
  ProfileSettings nsec warning, RequestToVanishDialog deletion checklist,
  MainLayout comment, and NIP.md
- Leave kind 4 rendering (EncryptedMessageContent) intact so DM events
  authored elsewhere still display in feeds and quote embeds
2026-05-17 14:37:49 -05:00
lemon ac48231e82 Document batch on-chain zaps 2026-05-17 01:12:16 -07:00
lemon 6d4d8ee9fb Document community actions 2026-05-16 09:03:45 -07:00
Alex Gleason 740fc1c63c Merge ditto/main into agora
Pulls in 387 commits from ditto/main while preserving Agora-specific
features. Where the two codebases diverged on the same concept, kept
the Agora side per project direction.

Kept Agora-specific:
- SparkWallet stack (over Ditto's nostr-derived Bitcoin wallet)
- Communities (NIP-72 + chat + members), Messages, Organizers,
  Actions, Verified, Appearance settings
- DMProviderWrapper, country/organizer moderation in NoteMoreMenu
- 'Agora' branding, pub.agora.app bundle ID, version 2.8.0
- Built-in theme system (src/themes.ts) only

Rejected from Ditto:
- All Blobbi virtual pet code (80+ files, route, provider, sidebar,
  kind labels, feed setting, NIP.md entries, CSS animations)
- Custom theme events (kinds 36767/16767) — ThemesPage, ThemeContent,
  active profile themes, theme snapshot recovery
- On-chain zaps (kind 8333) and the entire Bitcoin wallet implementation
  (useBitcoinWallet, bitcoin-signers, BitcoinContentHeader,
  bitcoinjs-lib / @bitcoinerlab/secp256k1 / ecpair / tiny-secp256k1)
- ZapSuccessScreen (depended on dropped bitcoin lib)

Pulled in from Ditto:
- .agents/skills/* (12 new specialized skills, slim AGENTS.md)
- @nostrify bumps to 0.52 / 0.6 / 0.37
- New routes/pages: Music, Podcasts, Videos, Vines, Wikipedia, Books,
  Bluesky, Archive, AIChat, Trends, Webxdc, Highlights, Decks, Emojis,
  Development, Treasures, Colors, Packs
- Birdstar feed integration (kinds 2473, 12473, 30621)
- Wikipedia/Wikidata/Scryfall lookup in ExternalContentPage
- release-notes CI job + extract-release-notes.mjs script
- nsite:// URI handling in feed/sidebar
- iOS fastlane setup
- src/lib/avatarShape.ts + Avatar shape prop (kept for new Music/People
  components that depend on it)

Preserved Agora's ABSOLUTE 'NEVER COMMIT' rule at the top of AGENTS.md
and dropped Ditto's contradicting 'Commit at the end of every task'
section.

Validation: npm run test passes (tsc, eslint, 40/40 vitest, vite build).
2026-05-13 18:35:03 -05:00
lemon 53e7122302 Add community chat tab 2026-05-11 11:12:13 -07:00
lemon ea4295cb89 Tighten flat community primitives
Extract isAuthorizedAward helper as the single source of truth for
membership award validation, used by both resolveMembership and
useMyCommunities. Simplify resolveCommunityModeration by dropping
the dead banned-reporter guard from pass 1 (impossible under strict
rank ordering). Flip useMembersOnlyFilter default to opt-in to match
the spec's MAY wording, and reword the NIP to match.
2026-05-11 11:12:13 -07:00
lemon 5e99ac817b Document flat community membership 2026-05-11 11:12:13 -07:00
lemon a9ea21e3d4 Show bookmarked communities in My Communities via NIP-51 kind 10004
Bookmarking a kind 34550 community now writes to the NIP-51 Communities
list (kind 10004) keyed by the addressable coordinate, so the reference
stays valid across community updates. My Communities merges bookmarked
communities as a third discovery source alongside founded and member-of,
with Founder/Member/Bookmarked badges on each card.

Bookmark toasts live on the mutation itself so they survive the more-menu
dialog unmounting between .mutate() and publish resolution.
2026-05-11 11:12:13 -07:00
filemon 478f53177e Bound social care history and simplify Blobbi social consolidation
- Gate social actions by projected stat thresholds (< 70) so visitors
      can only help stats in visual distress
    - Add energy category with Energy Drink and Power Nap Pillow items
    - Apply 6-hour recency window to interaction queries (limit 30)
    - Fix BlobbiActionsProvider tree placement so BlobbiPage shares context
      with the companion layer
    - Preserve event content in dev editor (don't overwrite checkpoint JSON)
    - Show Needs Now summary in activity tab with priority badges
    - Remove unused need-driven consolidation infrastructure

   Regression-of: 9aecefff
2026-05-06 22:27:50 -03:00
filemon 20d7aa199d Merge branch 'main' into feat/blobbi-1124-interactions 2026-05-04 13:29:31 -03:00
lemon d1017697a4 Add NIP-75 community fundraising goals
Implement zap goals (kind 9041) linked to communities via a-tag.
Includes goal creation dialog, progress tracking from zap receipts,
recipient profile/lightning address display, community link, and
members-only filtering. Goals appear in community detail Fundraising
tab, activity feed, and main feed via NoteCard.
2026-05-02 23:39:07 -07:00
Alex Gleason b2634d2fcb Render Birdstar Birdex events (kind 12473) as tiled life lists
A Birdex is a replaceable per-author index of every bird species the
author has ever confirmed via kind 2473, stored as positional i/n tag
pairs in chronological first-seen order. In feeds, show a compact
tile strip of the most recently-added species with a "+N" capstone
when the list overflows — mirroring how kind 3 follow lists preview
members as an avatar stack plus "+N more". On the post-detail page,
render every species as a responsive grid so visitors can browse the
author's whole life list.

Each tile resolves the species' Wikidata entity through English
Wikipedia to pull a thumbnail and common-name label, reusing the
same fetch path as kind 2473 detection cards. The Wikidata URL is
sanitized before being routed, and the paired n tag provides a
scientific-name fallback while the remote lookup is in flight.
2026-04-30 01:44:15 -05:00
sam b46703eaed remove blobbis 2026-04-30 13:19:22 +07:00
filemon 9483fbc99a Merge branch 'main' into feat/blobbi-1124-interactions 2026-04-30 00:56:01 -03:00
Alex Gleason c5d5165f84 Merge remote-tracking branch 'origin/main' into wallet
# Conflicts:
#	package-lock.json
#	src/components/CommentContext.tsx
#	src/components/ExternalContentHeader.tsx
#	src/components/NoteCard.tsx
#	src/pages/VinesFeedPage.tsx
2026-04-29 21:44:07 -05:00
filemon 222f641123 Merge branch 'main' into feat/blobbi-1124-interactions 2026-04-29 14:58:01 -03:00
sam 3f28bf571a remove all theme stuff 2026-04-29 22:04:39 +07:00