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.
Add a 'Delete organization' item to the banner-overlay dropdown on the
community detail page, gated by the existing isFounder check. Clicking
it opens an AlertDialog that publishes a NIP-09 (kind 5) deletion
request referencing the community definition by both 'e' and 'a' tags
via the shared useDeleteEvent hook.
On success we invalidate every org-related query key the hook doesn't
touch — addr-event for this community, community-definition,
manageable-organizations, featured-organizations, and the
followed-organizations sub-queries — then navigate back to
/communities so the user lands on a list that already reflects the
deletion. Errors surface as a destructive toast.
The confirmation copy is explicit about NIP-09's advisory nature
(well-behaved relays will honor it; campaigns and pledges published
under the organization stay on-chain) and points users toward 'Edit
organization' as the non-destructive alternative.
The /communities page previously rendered both 'My organizations' and
'Featured organizations' as horizontally-scrolling shelves of 256px
cards, which hid most content off-screen and made it tedious to browse.
Now both shelves use a new responsive CommunityGrid (1/2/3/4 columns by
breakpoint) inside the existing max-w-5xl content column. Card visuals
stay identical at the desktop breakpoint (~232-256px wide); the only
difference is that cards now wrap onto subsequent rows.
'My organizations' starts collapsed at the first 4 entries with a
'Show N more' / 'Show less' toggle, replacing the prior 18-card cap so
power users can see everything in one place when they want to.
Featured stays a full grid (the curated list is intentionally short)
and its loading skeleton count is bumped from 4 to 8 to fill two rows.
Campaign-launch events (kind 33863) previously fell through to the
generic text-note path in NoteCard, rendering as just the author row
plus the campaign's raw markdown story. No banner, no title, no
progress, no donor count, no goal, no deadline, no link to the
campaign page.
Wire campaigns through the same polished CampaignCard component the
campaign directory already uses:
- New CampaignNoteCardContent component — thin wrapper that parses the
event and renders a CampaignCard. Malformed events silently drop.
- NoteCard: add isCampaign flag, exclude from isTextNote, add the
dispatch branch, and import HandHeart for the action header.
- KIND_HEADER_MAP: 33863 entry gives 'Alice launched a campaign'
header above the rich card (uses publishedAtAction so edits read
'updated a' instead of 'launched a'). nounRoute points at
/campaigns/all so the bold 'campaign' word is clickable.
- CommentContext: register 33863 in KIND_LABELS ('a campaign'),
KIND_SUFFIXES ('campaign'), and KIND_ICONS (HandHeart) so comments
on campaigns render with proper context labels instead of falling
back to 'an unsupported event'.
The whole feed card now links to the campaign's naddr-based detail
route via CampaignCard's existing <Link> wrapper.
Embedded quote-preview rendering for kind 33863 and notification-stack
integration are deferred — out of scope for the activity-feed card.
The post-to-country picker previously only listed countries the user
followed, so a user following VE couldn't post about Iran without
following Iran first. Two changes fix this:
- New "Choose another country…" item at the bottom of the destination
dropdown opens a searchable CommandDialog over the full COUNTRY_LIST.
Search matches both name and ISO code ("iran" and "IR" both work).
- The dropdown's quick-pick list now also includes any ad-hoc country
the user has selected via the picker, even if not in their follow
list, so they have a one-tap way back to it.
- canChooseDestination no longer requires followedCountries.length > 0;
any logged-in user composing a top-level kind 1 can now pick a country.
- Snap-back guard now only fires when the selected code is invalid
(deleted from the country directory), not just because it isn't in
the follow list.
Also: the orange Post! / Publish poll button text is now forced white
via a className override. Previously it relied on the theme's
--primary-foreground token which was producing low-contrast text on
the orange background.
Five small UX improvements bundled together.
Default post-to-country with localStorage persistence
- New `useDefaultPostCountry` hook (localStorage-backed) hydrates the
composer's destination from a saved preference on every fresh compose.
- ComposeBox's country-destination picker (formerly a shadcn Select)
becomes a DropdownMenu so it can mix country options with an action
item.
- New "Set as default" item appears at the bottom of the dropdown
when the current selection is not already the saved default; clicking
it persists the choice and shows a toast.
- A passive "Country X is your default" label replaces the action
item when the current selection already is the default.
- resetComposeState now resets to the saved default instead of
hardcoded 'world', so the next compose lands where the user expects.
- The existing snap-back-to-world guard now also clears the saved
default if it points at a country the user has just unfollowed.
Remove weather from country feed pages
- Drop `WeatherVitalsRow`'s weather panel — the row keeps the
population / languages / currency vitals but no longer renders
temperature, sky description, or icon.
- Remove the live day/night sky-overlay flip on the country hero;
default to the warm daytime gradient.
- Remove the `PrecipitationEffect` overlay (animated rain/snow) from
country pages.
- Delete the now-orphan `useWeather` hook and
`PrecipitationEffect` component.
Remove organization activity feed from /communities
- Drop the `OrganizationActivityFeed` section and its helpers.
- /communities is now a directory page: hero + My organizations shelf
+ Featured organizations shelf.
- Delete the now-orphan `useOrganizationHomeActivityFeed` and
`useOrganizationMembersOnlyFilter` hooks.
Compact FeedModeSwitcher
- Strip the per-item descriptions ("Campaigns, pledges, donations,
and Agora posts", etc.) from the home-feed mode dropdown.
- Each menu item is now a single line: icon + label + optional check.
- Shrink menu width from w-72 to w-56 to match the new content density.
- Keep the disabled-Following tooltip — that's a state explanation,
not help text.
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.
The CountryFlagBackdrop rendered a faded full-width Wikipedia flag image
across the top of every country-rooted (kind 1111, iso3166-rooted) post.
It cluttered the feed and competed with post content. Drop it.
The CountryCommentPill in the upper-right of the card header is retained
— it remains the sole country chrome for world posts.
Removed:
- CountryFlagBackdrop component from CommentContext.tsx
- Both NoteCard render sites (threaded + normal layouts)
- The CountryFlagBackdrop import in NoteCard
- Dead imports in CommentContext: useState, getWikipediaTitle,
customFlagAsset, useFlagPalette, useWikipediaSummary
Updated jsdoc on useIsCountryRooted to reflect that country chrome
is now just the pill, not pill+backdrop.
The home /feed page now offers a top-left dropdown to switch between three
chronological streams:
- Agora: campaigns, pledges, donations, communities, comments on Agora
entities, and #Agora-tagged kind 1 notes (the existing useAgoraFeed mix,
widened for NIP-72 communities).
- All Nostr: the global kind 1 stream interleaved with the full Agora mix.
- Following: same content scoped to authors in the logged-in user's
follow list (gracefully gated when the follow list is loading or empty).
Implementation:
- useMixedFeed orchestrates the three modes, paginating the Agora and
kind 1 layers in lockstep and merging chronologically.
- useAgoraFeed now accepts an optional authors filter (server-side) so
Following mode doesn't fetch and discard the global Agora mix. It also
includes new community definitions (34550) and community-scoped
comments (1111 with A=34550:...).
- FeedModeSwitcher is the new top-left picker: large text2xl trigger,
shadcn DropdownMenu with iconified options and active checkmark.
Following is disabled (with tooltip) for logged-out users.
- AGORA_DEFAULT_NOTE_TAGS moved to src/lib/agoraNoteTags.ts; ComposeBox
now auto-attaches t:agora to every top-level kind 1 from anywhere in
the app (replies, quotes-as-replies, polls, and comments are unaffected).
- Feed mode persistence upgraded from sessionStorage to localStorage so
preference sticks across sessions.
- Feed entry added to TopNav as the first item.
The globe backdrop, hue rotation, and translucent card treatment are
removed for a cleaner solid-background presentation. Specialized
feed pages (kind-specific, tag-filtered) keep the original Follows /
Global tab pair unchanged.
The parenthetical '(<campaign title>)' after 'organizer' was awkward
and redundant -- the donor already knows which campaign they're on.
Strip it and remove the now-unused `campaignTitle` prop from
CampaignWalletDonatePanel.
Replaces every kind 30223 surface with kind 33863 -- the self-authored
fundraising campaign with a single `w` Bitcoin wallet endpoint. Hard
cutoff: no migration, no dual-read, no legacy support.
Schema (src/lib/campaign.ts):
- `CAMPAIGN_KIND` constant bumped 30223 -> 33863.
- New `CampaignWallet` type with `onchain` (`bc1q`/`bc1p`) and `sp`
(`sp1`) modes, prefix-disambiguated. Bitcoin-mainnet only;
testnet/regtest/lightning prefixes are rejected at parse time.
- New `parseCampaignWallet()` validates bech32 via bitcoinjs-lib for
on-chain addresses and shape-checks silent-payment codes.
- `ParsedCampaign` drops `recipients`, `category`, `tags`,
`location`, `archived`, `image` (-> `banner`), `goalSats` (->
`goalUsd`). Adds `wallet` and `bannerImeta` (parsed NIP-92).
- `CAMPAIGN_CATEGORIES`, `CampaignCategory`,
`LEGACY_CAMPAIGN_CATEGORY_ALIASES`, `getCampaignPrimaryTagLabel`,
`splitDonation`, `minDonationForSplit`, `DonationSplit`,
`CampaignRecipient` removed.
Publishing (useDonateCampaign):
- Single-output PSBT paying `campaign.wallet.value`.
- Kind 8333 receipt has NO `p` tags -- campaigns are not Nostr-identity
recipients. `i`, `amount`, `a`, `K`, `alt` only.
- SP campaigns are refused with a clear error directing donors to
external BIP-352-capable wallets via the on-page QR/copy panel.
Verification (useOnchainZaps.verifyOnchainZap):
- Two modes: identity-recipient (existing `p`-tag derivation) and
campaign-wallet (match outputs against `campaign.w`). The branch is
selected by whether the receipt has an `a` tag pointing at a kind
33863 campaign. SP-targeted receipts are rejected.
Querying (useCampaigns, useAllCampaigns, useCampaignDonations):
- Drop `category`, `recipientPubkeys`, `includeArchived` options.
- `useCampaignDonations` now takes a `ParsedCampaign` and verifies
every receipt on-chain against the campaign's `w` address before
counting it. SP campaigns short-circuit to zeros.
- Search drops `location` and `t`-tag branches; title/summary/story
only.
UI:
- `CampaignCard`, `HeroCampaignSpotlight`, `CampaignsPage`: drop
recipient counts, archived/category badges, `location`; use
`banner`. Silent-payment campaigns render a "Private -- totals not
public" notice instead of progress.
- `CampaignDetailPage`: archive flow replaced with NIP-09 kind 5
deletion. Drops the multi-beneficiary recipient column. Donate column
shows the in-app PSBT donate button (on-chain) plus the always-
available external-wallet QR/copy panel. SP campaigns show the panel
only -- no in-app donate.
- `CreateCampaignPage`: drops Beneficiaries section, tag input, and
USD-to-sats conversion. Adds a Wallet field with mode-aware
validation hint. Goal is integer USD. Banner upload captures NIP-94
tags and converts to NIP-92 `imeta` at publish.
- `DonateDialog`: collapses ~1200 LOC of split logic into a single-
output flow. Form -> Confirm -> Success. Logged-out and signer-
unsupported users are pointed at the external-wallet panel.
- New `CampaignWalletDonatePanel` component (replaces the
pubkey-derived `BeneficiaryDonateDialog`). QR + copy + open-in-wallet
for any `bc1`/`sp1` endpoint, with mode-appropriate privacy notice.
Removed:
- `useArchiveCampaign` hook (closure via NIP-09 deletion only).
- `ClaimPage` and its `/claim` route (claim-for-someone-else flow no
longer applies -- campaigns are self-authored).
- `BeneficiaryDonateDialog.tsx` (replaced by
`CampaignWalletDonatePanel.tsx`).
- Community-donate synthesis hack in `CommunityDetailPage` (no more
fabricating a `ParsedCampaign` from community moderators).
NIP.md was updated separately to specify Kind 33863.
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.
Reduce cognitive load on the Content settings page by collapsing the
two-section toggle layout, group sub-headers, sub-kind rows, kind
badges, and column headers into a single flat list of 14 toggles
ordered by importance: Posts, Replies, Reposts, Articles, Highlights,
Photos, Videos, Voice Messages, Events, Polls, Organizations, Badges,
Reactions, Zaps.
Each row is now a plain label + one-line description + switch. No
content-kind icons, no [1234] kind-number badges, no Media / Social /
Whimsy sub-headers, no Normal/Short video or Badge Definitions /
Profile Badges / Badge Awards sub-rows (the parent toggle now governs
all sub-kinds together).
Combine kind 6 (Reposted Notes) and kind 16 (Reposted Other Content)
into a single "Reposts" toggle via extraFeedKinds: [16]. The old
feedIncludeGenericReposts flag stays in the schema for backwards
compat but no longer surfaces in UI.
Rename "Comments" -> "Replies" — Nostr's NIP-22 threading is most
naturally called replies.
Strip NIP / kind-number references from all curated descriptions
(NIP-22, NIP-52, NIP-58, NIP-68, NIP-71, NIP-72, NIP-84, NIP-A0,
"kind 30009", etc.). Plain English only.
Merge the standalone /settings/content page (mutes + sensitive
content) into /settings/feed as inline sections under the toggle
list, since both are about "what you see in the feed." Delete
ContentPage.tsx and its route; remove the Content entry from the
settings index. Drop the giant ShieldAlert icon from the sensitive
content intro.
Rename "Home Feed Tabs" -> "Saved Feeds" in the page section heading.
The settings UI iterates EXTRA_KINDS and renders a toggle row per kind,
which exposed every Nostr content type the app understands (vines,
treasures, colors, decks, webxdc, birdstar, emoji packs, music,
podcasts, development, etc.) regardless of whether they fit Agora's
activist-utopian framing. The result was a wall of toggles with no
meaningful default.
Add an `agora` boolean to ExtraKindDef and mark only the curated set:
posts, comments, reposts, generic-reposts, reactions, zaps, articles,
highlights, photos, videos (with sub-toggles), voice messages, events,
polls, organizations (NIP-72 communities), and badges. Filter the
"Basic Home Feed Options" and "Show More Content Types" sections to
`def.agora === true`. Move badges from the "Whimsy" section into
"Social" so the Whimsy and Development groups vanish entirely after
filtering.
Enable zaps in the home feed by default (they're core engagement,
not noise) and drop "Disabled by default" from the zaps description.
Other pages (KindFeedPage deep-links, ExternalContentHeader quoted
events, etc.) still see the full EXTRA_KINDS registry, so external
content from non-curated kinds still renders correctly when linked.
Remove the spellbook-themed settings index: drop the "Codex of
Configuration" heading, the gradient ornaments with ✦/◆ dividers, the
sigil that appeared after two minutes of inactivity, and the IntroImage
illustration tiles on every section row and sub-page intro block. The
index is now a flat divider-separated list of labels and one-line
descriptions, with breathing room on both sides.
Delete the Magic settings page, its CursorFireEffect overlay, the
animate-sigil-glow / animate-pulse-slow keyframes, the magicMouse
AppConfig flag (schema, default, test fixture), and the /settings/magic
route. Delete the now-unreferenced IntroImage component and the ten
*-intro.png assets it masked.
Disable content types that don't fit an activist tool by default: vines,
treasures (geocaches + found logs), colors, decks, webxdc, birdstar
(detections / birdex / constellations), emoji packs, custom emojis, user
statuses, music, podcasts, and development. They remain available in
settings — just off out of the box. Highlights is bumped on by default
to pair with Articles. Posts, comments, reposts, articles, highlights,
events, polls, communities, people lists, badges, photos, videos, and
voice messages stay on.