Commit Graph

4305 Commits

Author SHA1 Message Date
mkfain fd3446a2f5 Hide badges in edit profile, drop 'Add to sidebar' from profile menu
Edit profile (ProfileSettings) renders an interactive ProfileCard for
the avatar/banner/bio editor. The card was showing the user's NIP-58
badge showcase grid, which doesn't belong on the edit form. Add a
showBadges prop to ProfileCard (default true to preserve all other
consumers — NoteCard, PostDetailPage, MusicArtistsTab, MusicDiscoverTab)
and pass showBadges={false} from ProfileSettings.

Profile 3-dots menu: drop the 'Add to sidebar / Remove from sidebar'
row. Sidebar pinning is a feed-management feature that doesn't belong
on a profile-action menu — add-to-list already covers the
list-management use case. Cleans out the supporting machinery
(addToSidebar / removeFromSidebar / orderedItems / sidebarId /
isInSidebar / handleToggleSidebar) and the now-unused Trash2 and
PanelLeft lucide icons.
2026-05-22 00:11:36 -05:00
mkfain ea3a1ff5bd Drop LinkFooter from the profile rail
The rail rendered <LinkFooter /> at the bottom of its content. On
desktop that worked — the rail is sticky and scrolls independently, so
the footer landed at the bottom of the rail's scroll container.

On mobile the rail is just the first stacked element above the right
column (tabs + feed). That meant the footer sat in the middle of the
page, between the rail's profile-fields section and the tab bar — a
weird mid-page footer band.

The global SiteFooter rendered by FundraiserLayout already handles
About / Privacy / Safety / Source / Changelog at the page level, so
the rail's LinkFooter was redundant on every breakpoint anyway. Drop
it.
2026-05-22 00:07:51 -05:00
mkfain 93e9f7ca97 Make the profile rail scroll independently of the feed
Symptom: scrolling on top of the rail moved the page (and feed) instead
of the rail. Once the page reached the end of the feed, the rail would
finally scroll until pagination loaded more, then the feed took over
again. Frustrating for users on tall rails.

Cause: the rail aside was 'lg:sticky lg:top-4' with no height cap and no
internal scroll, so it scrolled with the page until its bottom reached
the viewport bottom — at which point sticky positioning kicked in. There
was nowhere for the rail to scroll independently because it had no
overflow container of its own.

Earlier I removed the rail's 'lg:overflow-y-auto' because it clipped
the avatar's '-mt-16' overhang above the rail's top edge. The fix is to
split the rail into two layers:

  - Outer flex column (the aside contents) — owns the avatar, has no
    overflow constraint, so the avatar's negative-margin overhang above
    the aside's top is never clipped.
  - Inner scroll container — wraps everything below the avatar with
    'lg:flex-1 lg:min-h-0 lg:overflow-y-auto'. On lg+ this fills the
    remaining height of a now-bounded sticky aside
    ('lg:h-[calc(100vh-2rem)]') and scrolls internally. Mouse wheel
    over the rail scrolls the rail; mouse wheel over the right column
    scrolls the page.

Below lg the inner container's lg-prefixed classes don't apply, so the
rail still flows naturally above the tab content as before.
2026-05-22 00:05:16 -05:00
mkfain 6b7bdb9322 Expand profile Activity feed to all enabled kinds, not just notes/reposts
The unified profile feed was only fetching kind 1/6 in the
'includeAuthorNotes' branch, which dropped articles (30023), photos (20),
videos (21/22), polls, and any other kind the user has enabled in their
feed settings. The legacy Posts tab pulled the full getEnabledFeedKinds
set, so the merged feed lost content compared to before.

Pipe getEnabledFeedKinds(feedSettings) through useAgoraFeed when
includeAuthorNotes is set. Always force kind 1 + 6 in if the user has
disabled them in settings — a profile feed without notes would be
useless. The post-filter now accepts any author-scoped event whose
kind is in that set, mirroring the relay request shape.
2026-05-22 00:02:41 -05:00
mkfain 4312a7c6f6 Merge profile Activity + Posts into a unified feed
Profile pages had two tabs that both showed 'this person's stuff' — one
strict-Agora (Activity) and one general kind-1/6 (Posts). For a profile,
that distinction is noise: visitors want to see everything someone has
done on the network in one timeline.

Add an 'includeAuthorNotes' option to useAgoraFeed. When set alongside
'authors', the relay query gains a fifth filter '{ kinds: [1, 6],
authors }' and the post-filter relaxes the strict t:agora gate for
events authored by the requested set. The strong author scope is the
trust anchor — we know it's by this person, so we surface it.

ProfileActivityTab consumes the new option, becoming the single feed
for the profile. Drops the Posts tab, the Posts & replies overflow tab,
useProfileFeed integration on ProfilePage, the feedItems / currentItems
machinery, the MIN_VISIBLE_ITEMS auto-load effect, the pinned-posts
surface (was tied to the Posts tab; togglePin still exists elsewhere),
the PinnedLabel helper, the profile-pinned-events query, and a pile of
now-unused imports (NoteCard, usePinnedNotes, isEventMuted,
useProfileFeed, filterByTab, FeedItem, useQuery, useNostr from the
outer scope, useMuteList outside ProfileMoreMenu, Pin lucide icon).

Profile tabs are now: Activity / Campaigns / Pledges. The home /
mixed feed is unaffected because includeAuthorNotes defaults off.

Net: -150 LoC in ProfilePage.tsx; +28 LoC in useAgoraFeed; -8 LoC in
ProfileActivityTab.
2026-05-21 23:59:34 -05:00
mkfain 0c5eae3ceb Fix profile tab bar bleeding past the column on mobile
ProfileTabs had '-mx-4 sm:-mx-6 lg:mx-0' to bleed the bar to the page
edges, paired with matching positive padding on the inner scroll track.
On lg+ that worked (the bleed was suppressed) but below lg the negative
margin pushed the tab bar wider than the rest of the profile content
column, making the page width look broken once the rail stacked above
the right column.

Drop the bleed entirely. The tab bar now sits exactly at the column's
width at every breakpoint. The translucent backdrop and bottom border
still read as a visual separator without escaping the content area.
2026-05-21 23:53:42 -05:00
mkfain 3ec8d1b9f9 Merge branch 'main' of gitlab.com:soapbox-pub/agora 2026-05-21 23:51:00 -05:00
mkfain 121991f3e5 Drop Media/Badges/Likes tabs, redesign profile tab bar
Tabs:
  - Activity, Campaigns, Pledges, Posts. That's it.
  - Media / Badges / Likes removed along with their renderers,
    ProfileBadgesTab function (152 LoC), useProfileMedia + useProfileLikes
    hooks, mediaEvents / likedItems / mediaFeedItems / likedFeedItems
    memos, the sidebarMediaUrl state and its callbacks, and the
    profile-media / profile-likes-infinite cache invalidations.
  - The 'isCoreProfileTab' fallthrough now only handles posts/replies.

Visuals:
  - New ProfileTabs component replaces the global SubHeaderBar +
    TabButton pair on this page. It's a clean column-local sticky bar
    with backdrop-blurred translucent background, hairline bottom
    border, and an animated underline that slides between active tabs.
    No arc decoration, no hover-slice tracking — just tabs.
  - Sticks to top-mobile-bar on small screens (clears mobile chrome)
    and top-0 from sidebar breakpoint up. Active tab auto-scrolls into
    view when it overflows the column.

Net: -289 LoC in ProfilePage.tsx; +118 LoC for the new ProfileTabs.
2026-05-21 23:50:35 -05:00
mkfain 6e7fcb8732 Trim profile rail and tabs to the essentials
Rail:
  - Drop the badge preview row.
  - Pull Followers + Following onto a single inline horizontal row at
    the top of the stat block. The campaigns-count row is gone (it was
    redundant with the Campaigns section that sits right below it).
  - Pledges and Raised remain as full rows in the secondary stat list.

Tabs:
  - Remove the Overview tab. Activity is now the default, leftmost tab.
  - Remove the Wall tab and all its supporting code: useWallComments
    hook, wallReplyTarget memo, wallComments / orderedWallReplies
    flatten, wallComposeOpen state, openWallCompose callback,
    profileFollowsMe gate, FAB wall-compose wiring, the wall-comments
    cache invalidation, and the Wall tab renderer.
  - Remove the overflow '⋯' dropdown that exposed non-default core
    tabs. Every core tab is now visible in the strip.

Fallouts:
  - DEFAULT_TAB_LABELS = CORE_TAB_LABELS (every core tab is shown), so
    CORE_TAB_LABELS is dropped.
  - Delete src/components/profile/ProfileOverviewTab.tsx.
  - Drop a pile of now-unused imports from ProfilePage.tsx
    (MoreHorizontal, MessageSquare, DropdownMenu*, FeedCard,
    ComposeBox, ReplyComposeModal, useWallComments,
    FlatThreadedReplyList, ProfileOverviewTab) and the badge-related
    imports from ProfileIdentityRail (BadgeThumbnail,
    useBadgeDefinitions, useProfileBadges, nip19).

Net: -418 LoC across the page and rail.
2026-05-21 23:38:13 -05:00
mkfain d5a54f6844 Restore avatar position, add z-index instead
The previous commit raised the avatar too far up the page by matching
the negative margin to the avatar's full height. The user's actual
complaint was z-stacking, not vertical position.

Restore -mt-16 md:-mt-20 so half the avatar overlaps the banner (the
classic profile look) and add 'relative z-10' to the avatar button so
it explicitly layers above the banner's stacking context regardless of
which parent creates a new context (e.g. the lg:sticky aside).
2026-05-21 23:29:11 -05:00
mkfain 69f7ec9176 Lift profile avatar fully above the banner
The avatar's negative margin was -mt-16/-mt-20 (-64/-80px) but the
avatar itself is 112/128px tall, so roughly half of it sat under the
rail boundary and got cut off behind the banner edge.

Match the negative margin to the avatar's full size — -mt-28 md:-mt-32 —
so the bottom edge of the avatar sits at the rail's top edge (which is
the banner's bottom edge). The whole avatar floats over the banner now.

Drop lg:overflow-y-auto + lg:max-h-[calc(100vh-2rem)] from the rail
<aside> so the avatar's overhang above the rail's top edge isn't
clipped. The rail still sticks via lg:sticky lg:top-4; when content is
taller than the viewport, sticky positioning naturally scrolls with
the page (matching GitHub's profile rail behavior).
2026-05-21 23:27:33 -05:00
mkfain d92ec350e4 Rebuild profile as a real two-column layout
Profile page was structurally broken: a stack of full-width sections
(banner, header, campaigns strip, orgs strip, tabs, content) with a
sidebar that only existed beside the tab content. The tab bar split
the page in two unrelated halves and the sidebar floated in the
middle.

Replace it with a GitHub-style two-column shell:

  - LEFT: ProfileIdentityRail (new), sticky on lg+. Owns avatar
    overlapping the banner via -mt-16, identity (name/NIP-05/website/
    bio), badge preview, action bar with auto-flow wrap (Follow /
    Donate / ⋯, or Edit profile / QR / ⋯ for own profile), a vertical
    stat list (followers / following / campaigns / pledges / raised),
    a compact campaigns section (cap 2 + 'See all → Campaigns tab'),
    an organizations grid (cap 4 + 'See all' dialog), the freeform
    profile fields, and LinkFooter.

  - RIGHT: tab navigation and the active tab's content. Tabs stick to
    the top of this column only, so they're clearly nav for that
    column, not a page-wide divider.

Below lg the grid collapses to a single column and the rail stacks
above the tabs — the avatar still overlaps the banner because the rail
is the first child below it. Reads top-down like a document.

Drops ProfileCampaignsStrip and ProfileOrganizationsStrip as page-wide
strips; their content folds into the rail. Extracts the orgs overflow
modal into a small standalone component (OrganizationsAllDialog) so
the rail can use it without pulling in the strip files.

Trims ProfileOverviewTab to just Recent activity + Recent posts; the
'Featured campaign' section is redundant now that the rail carries
campaigns as standing facts.

Net: ProfilePage.tsx -930 LoC. The shape of the page no longer fights
with the way users read web pages.
2026-05-21 23:23:59 -05:00
Chad Curtis 671e3f14fe Rename Search tabs to Agora / Nostr / Accounts and pin Agora to client:Agora
The first Search tab is now 'Agora' (kindsOverride [33863 Campaigns,
36639 Pledges, 34550 Groups]) and additionally narrows results to
events whose NIP-89 'client' tag matches the running app name. Ditto's
relay (Agora's primary relay) indexes the multi-letter 'client' tag
server-side, so the constraint is fast and free; relays that don't
index it ignore the filter and return unfiltered results, which is a
graceful degradation rather than a correctness problem.

The second tab is renamed 'Nostr' to make its role explicit: it's the
unconstrained, cross-client firehose. The third tab (Accounts) is
unchanged.

Plumbing:

- useStreamPosts gains a clientName option that adds '#client': [name]
  to both the search and stream subscriptions.
- AgoraSearchTab reads config.clientName ?? config.appName from
  AppContext and passes it through.
- ?tab=communities and ?tab=activity continue to alias to ?tab=agora
  for back-compat with linked URLs.

Empty-state copy and tab/comments are updated.
2026-05-21 23:12:19 -05:00
mkfain 4dbf8b00ec Strip Ditto custom-tab system, cap orgs, move sidebar right
Three corrections from the previous profile-refocus pass:

1. ProfileOrganizationsStrip is now capped at 4 cards (the design always
   intended a single hero row on lg+). Overflow surfaces in a 'See all
   N organizations' modal triggered from the section header so a
   power-affiliated profile no longer pushes the rest of the page down.
   The grid switches to 1/2/4 cols at sm/lg to match the 4-card cap.

2. The supplementary rail moves from the left of the tab content to the
   right. The two-column grid template flips from
   '[300px_minmax(0,1fr)]' to '[minmax(0,1fr)_320px]', and the JSX
   reorders <section> before <aside>. The page canvas widens from
   max-w-6xl to max-w-7xl (matching CampaignsPage / AllCampaignsPage),
   so on ultrawides the sidebar sits closer to the viewport's right
   edge instead of floating in the middle of the content area.

3. Kind 16769 (Profile Tabs) is fully removed from ProfilePage. The
   pencil edit-mode, drag-to-reorder via dnd-kit, '+' Add custom tab
   menu, ProfileTabEditModal render, ProfileSavedFeedContent renderer,
   CORE_TAB_FILTERS map, NoTabsEmptyState, and all backing state
   (tabEditMode, localTabs, tabModalOpen, editingTab, dndSensors,
   profileTabsQuery, profileTabsData, profileSavedTabs, profileVars,
   publishProfileTabs) are deleted. The tab list is now the fixed
   DEFAULT_TAB_LABELS shown identically to owner and visitor, with the
   four legacy social tabs (Posts & replies, Media, Badges, Likes)
   reachable through a single overflow dropdown. This drops six
   imports (useProfileTabs, usePublishProfileTabs, ProfileTabEditModal,
   useResolveTabFilter, useTabFeed, useActiveTabIndicator), three
   dnd-kit module imports, the lucide icons Pencil, GripVertical, Plus,
   and the DropdownMenuSeparator unused after the deletion. The shared
   kind 16769 infrastructure stays in the codebase because SearchPage
   and the people-list features still depend on it.

The 'hasTabs' gate that protected against the legacy empty-tab-list
case is now structurally true and is inlined into the hook calls
(useProfileMedia, useProfileLikesInfinite, useWallComments) and tab
conditional renderers. Net diff: 154 added, 512 removed in
ProfilePage.tsx.
2026-05-21 23:08:57 -05:00
Chad Curtis 0622efc781 Replace Search Communities tab with Activity (campaigns + pledges)
The old Communities tab pinned kindsOverride to [34550] — communities
only. The new Activity tab pins it to [33863 Campaigns, 36639 Pledges]
so /search opens onto Agora's first-class non-kind-1 content. Kind 1
text posts (and their reposts) continue to live on the Posts tab; the
Accounts tab is unchanged.

URL contract:

- The new default is ?tab= absent → Activity.
- ?tab=activity is the explicit form.
- ?tab=communities still resolves (parseTab aliases it to activity) so
  existing links don't 404, but they now show the campaigns/pledges
  stream rather than a communities-only stream.

Empty-state copy and stale 'communities tab' comments are updated.
2026-05-21 23:06:57 -05:00
Chad Curtis edf9f77060 Default the Search Kind filter to Agora content only
Search previously defaulted to 'all kinds' — every kind in the picker
(50+), which means a user typing 'bitcoin' into the search bar got a
firehose of music tracks, podcasts, bird detections, geocaches, and
every other supported NIP. The Posts tab is meant to surface Agora
content; the long-tail kinds belong behind an explicit opt-in.

Introduce 'agora' as a new sentinel value for the Kind filter that
expands to AGORA_PRESET_KIND_VALUES (Campaigns, Pledges, Communities,
Posts, Articles, Events, Polls, Photos, Videos). Make it the default
in DEFAULT_FILTERS, teach parseKindFilter to resolve it, and render
both 'Agora content' and 'All kinds' as top-level selectable rows in
the KindPicker (with the existing Agora-curated quick-list still
appearing as individual selectable items below).

The active-filter chip summary suppresses a chip when the default
'agora' is in effect, surfaces 'All kinds' as a chip when the user
explicitly broadens the search, and continues to show individual kind
labels for everything else.
2026-05-21 23:02:57 -05:00
Chad Curtis f762a8b0d7 Add breathing room above SiteFooter and drop its top border
Removes the border-t border-border line above the global footer and
adds pt-12 so the footer separates from page content via whitespace
instead of a hairline divider.
2026-05-21 23:02:57 -05:00
Chad Curtis ee79b789a7 Drop the FeedCard wrapper from PostDetailPage
PostDetailPage wrapped the focused post (with ancestor previews,
ancestor thread chain, and the focused event itself) in one rounded
FeedCard surface and the replies list in another, with an extra
max-w-6xl mx-auto px-4 sm:px-6 outer container on top of the layout's
own column. That made the post detail page look boxy and inset
compared to the Activity feed and the Search results, which render
edge-to-edge in bare divs.

Match the Activity feed pattern exactly: drop every FeedCard wrapper
(focused post + replies + skeleton), drop the redundant outer max-w
+ px container, and let NoteCard's self-applied px-4 py-3 + bottom
border handle the row layout. Replies / reply-skeleton lists become
bare divs (with divide-y on the skeleton, since skeleton rows don't
self-border).
2026-05-21 23:02:57 -05:00
Chad Curtis 6a55092f2c Drop the FeedCard wrapper from Search results
Search results were rendered inside FeedCard — a rounded, bordered,
margin-inset card surface — while the main Activity feed renders posts
edge-to-edge in a bare div. The mismatch made search results look
'boxed in' compared to every other feed in the app.

Replace every FeedCard wrapper on the Search page (Posts, Communities,
Accounts, Follows lists, and their skeletons) with the bare-div pattern
used by Feed.tsx. NoteCard self-applies a bottom border, so the result
lists drop divide-y while skeleton lists keep it (skeleton rows don't
self-border).
2026-05-21 23:02:57 -05:00
Chad Curtis 59f1b07a03 Remove 'Add to feed' button from the Search page
The bookmark-style 'Add to feed' button next to the search input has
been removed. Saving a search as a home-feed tab or profile tab is
rarely used and clutters the search bar's filter affordances. The
underlying useSavedFeeds / usePublishProfileTabs hooks remain available
for callers that need them.

Also drops the now-unused SaveDestinationRow helper, the savePopover
state, and a handful of imports (BookmarkPlus, Check, Loader2, User,
TabFilter type, useSavedFeeds, useProfileTabs, usePublishProfileTabs,
useCurrentUser) that only existed to support the removed UI.
2026-05-21 23:02:57 -05:00
Chad Curtis 1dbac90108 Expose search in TopNav and add Agora kind presets to the Kind picker
Add a Search icon button to the TopNav right cluster (visible on all
breakpoints) that links to /search, so users no longer have to dig into
the mobile drawer or profile menu to reach search.

Surface Agora's main content kinds in the Search filter Kind picker:

- Add Campaigns (33863) and Pledges (36639) to buildKindOptions(), since
  they are first-class Agora content but live outside EXTRA_KINDS (which
  drives the sidebar / feed-settings UI and shouldn't be polluted with
  kinds that don't have their own toggleable feed/sidebar items).

- Group the curated Agora set — Campaigns, Pledges, Communities, Posts,
  Articles, Events, Polls, Photos, Videos — under an 'Agora content'
  section at the top of the KindPicker dropdown, with the long-tail list
  of every other supported kind under an 'All kinds' section below. When
  the user types in the search box the partition collapses to a flat
  result list.
2026-05-21 23:02:56 -05:00
mkfain c774405dc3 Restructure profile tabs around Agora activity
Re-order CORE_TAB_LABELS so the Agora-native tabs lead and the legacy
social tabs fall back to the overflow menu:

  Overview · Campaigns · Pledges · Activity · Posts · Wall (default)
  + Posts & replies · Media · Badges · Likes (overflow)

Overview becomes the default landing tab and replaces 'Posts' as the
first visible card on a fresh profile. CORE_TAB_FILTERS gets matching
NIP-01 filter entries for each new tab so kind 16769 events remain
interpretable by non-Agora clients (Campaigns → kinds:[33863] + author,
Pledges → kinds:[36639] + author, Activity → the Agora-feed kind set
plus #t:agora, Overview → the same Posts shape as a safe default).

The four new tab renderers live in src/components/profile/:

  - ProfileOverviewTab — composite: featured campaign + recent Agora
    activity preview + 3 most recent posts, with section-level 'see all'
    links into the deeper tabs. Empty-state shows own-profile CTAs to
    start a campaign / create a pledge.
  - ProfileCampaignsTab — full grid of the user's campaigns with
    New / Top sort and a Show-hidden toggle gated to the owner + Team
    Soapbox moderators. 'Top' sort fans out one receipts query +
    per-receipt verification across the visible set via useQueries (no
    rules-of-hooks violation), keying into the same caches as
    useCampaignDonations so verifier results are shared.
  - ProfilePledgesTab — pledges created by the user, split into Active
    and Ended groups, with a card visual that mirrors /pledges. 'Pledges
    backed' (zapped submissions on others' pledges) is intentionally
    deferred to v2 per the design plan.
  - ProfileActivityTab — useAgoraFeed scoped to a single author with
    infinite scroll; renders the mixed-kind timeline through NoteCard.

The 'I Have No Mouth, and I Must Scream' empty state on profiles whose
kind 16769 publishes an empty tab list is replaced with a neutral
one-liner — the Harlan Ellison quote rotation was an upstream Easter
egg that clashes with Agora's framing.
2026-05-21 22:51:40 -05:00
mkfain c738b60c7b Add Campaigns + Organizations hero strips and two-column profile body
Insert two responsive hero rows between the profile header and the tab
strip:

  - ProfileCampaignsStrip — renders the profile owner's campaigns as a
    full CampaignCard grid (1/2/3/4 columns at sm/lg/xl), capped at 6
    with a 'View all' link into the (forthcoming) Campaigns tab.
    Filters hidden campaigns out for visitors; own-profile sees them so
    creators understand their moderation state.

  - ProfileOrganizationsStrip — renders orgs the profile founded or
    moderates as CommunityMiniCards with a Founder / Moderator badge
    overlay. Backed by a new useProfileOrganizations hook that takes any
    pubkey and only surfaces public signals — kind 34550 author and
    kind 34550 #p moderator entries. The 'follows' axis (kind 10004
    bookmarks) is intentionally not shown because bookmarks are private
    state that another viewer can't see.

Both strips self-collapse when empty, so a Nostr-native profile with no
Agora activity remains compact.

The body below the tab strip becomes a two-column grid at lg+: a
300 px sticky left rail (the existing ProfileRightSidebar, now
inlined into a grid cell via a new variant='inline' prop) and a
min-w-0 right column for the tab content. Below lg the rail is hidden
and tab content flows full-width — the rail's profile-fields content
is already shown inline in the mobile header, so nothing is lost.

The legacy 'xl:hidden' inline profile-fields breakpoint shifts to
'lg:hidden' so the rail and the inline fields don't double up at lg+.
2026-05-21 22:44:41 -05:00
mkfain 70e78b7e5f Refocus profile header on Agora: campaigns, pledges, raised, donate
Replace the kind-1 posting streak chip with Agora-native stat chips
(Campaigns / Pledges / Raised), opt out of FundraiserLayout's
max-w-3xl cap so the profile can use a max-w-6xl canvas like
CommunityDetailPage, and surface a Donate button (single-campaign
direct, multi-campaign dropdown) next to Follow when the profile has
at least one on-chain campaign.

The stat chips wrap on narrow viewports and click through to the
corresponding tab id ("campaigns" / "pledges") — those tabs land
in commit 3.

Adds useProfileCampaignStats, which fans receipt-verification queries
out across the profile's campaigns in parallel using the same kind
8333 -> Esplora verification path as useCampaignDonations.

Drops two leftover Ditto comments (relay.ditto.pub media note,
"Ditto-style profiles" shape cleanup). The dead useLayoutOptions
rightSidebar registration is removed — FundraiserLayout silently
ignored it.
2026-05-21 22:39:52 -05:00
mkfain 4e9c6b37d3 Rename Support nav label to Campaigns, Organize to Groups
Touches user-facing labels only:

- TopNav: Support -> Campaigns, Organize -> Groups
- Sidebar: Organize -> Groups
- MobileBottomNav: Organize -> Groups
- /communities hero kicker: Organize -> Groups

Routes, hooks, and the country-organizers admin feature
(`OrganizersPage` / `useOrganizers` — a separate concept covering
appointed pinners for country feeds) are left alone. Code comments
referring to the "Organize hero" are kept as-is so future readers can
still find their way around by structural name.
2026-05-21 22:06:36 -05:00
mkfain c09775473a Widen the Support page so desktop fits more campaigns per row
Drops MainLayout's default 600px column cap on /campaigns/all (the same
`noMaxWidth: true` trick the Pledge index uses) and lifts the grid
from `sm:grid-cols-2` to `sm:grid-cols-2 lg:grid-cols-3
xl:grid-cols-4`. Mobile and small tablets stay 1/2 columns so the
cards keep their tappable size; the inner `max-w-7xl` wrapper keeps
the page from sprawling on ultrawide monitors. No hero added — the
existing toolbar still sits directly under the page title.

Skeleton placeholder count bumped 4 → 8 so the loading state fills the
wider grid instead of leaving most of the row empty.
2026-05-21 22:02:14 -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 7ea0f0977d Scope org moderation surfaces and add Approved campaigns to org page
Three related changes:

1. Pending review on /communities now filters to orgs that carry the
   t:agora marker. Without this gate every kind 34550 community on the
   network ends up in the moderator queue — badge-gated NIP-72 spaces,
   music scenes, anything else — none of which Agora moderators are
   expected to triage. ParsedCommunity now carries its source event's
   tags so the check can run without re-fetching, and a hasAgoraTag()
   helper joins withAgoraTag() in src/lib/agoraNoteTags.ts.

2. Campaign detail page gets the same moderator Feature/Approve/Hide
   kebab the campaign cards already have. CampaignModerationMenu drops
   into the hero's top-right row next to the creator's Edit/Delete
   buttons; it self-gates on Team Soapbox membership and renders null
   for non-moderators, so creators who aren't moderators see no change.
   Moderation state is read from the same useCampaignModeration cache
   the rest of the page consumes.

3. Org detail page gets a dedicated 'Approved campaigns' section that
   mirrors the home page's surfacing rule
   (approvedCoords ∩ !hiddenCoords). The org's campaigns are split
   client-side: approved-and-not-hidden go into the new grid section
   above the mixed activity rail, everything else stays in the
   existing OfficialActivityShelves to avoid double-rendering. The
   section only renders when at least one campaign is approved, so
   unmoderated orgs don't show an empty rail.

Per the user's constraint, no org moderation UI is exposed on the org
detail page itself — moderation actions for organizations stay on
CommunityMiniCard (grid cards) only. The detail-page banner dropdown
remains founder-only (Edit, Delete, View leadership).
2026-05-21 21:32:57 -05:00
mkfain b10335efc1 Add Pending review and Hidden sections to /communities for moderators
Mirror the home page's moderator review rails on the organizations page.
Below 'Featured organizations,' moderators (Team Soapbox pack members)
now see two collapsible sections:

  - Pending review — kind 34550 orgs with no approve or hide label yet.
  - Hidden — orgs whose latest hide-axis label is 'hidden.'

Both sections fetch a 200-event slice via useDiscoverCommunities and
fold it against useOrganizationModeration's approved/hidden coords. The
existing CommunityMiniCard kebab menu lets moderators feature, approve,
hide, or unhide directly from each card.

Sections are collapsed by default when long (>6 orgs), expanded
otherwise — same heuristic as the campaign-side ModeratorSection.
Non-moderators see no change to the page.
2026-05-21 21:21:25 -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 c2fee23582 Let founders delete their organization
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.
2026-05-21 20:57:14 -05:00
mkfain 0a7388ac2f Replace organizations horizontal scroll with responsive grid
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.
2026-05-21 20:52:12 -05:00
mkfain fed1bb9ce0 Render kind 33863 campaigns as rich cards in the activity feed
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.
2026-05-21 20:36:04 -05:00
mkfain b864a73573 Rename TopNav 'Feed' to 'Activity' 2026-05-21 20:26:01 -05:00
mkfain d52d9e25a5 Clip Agora layer to Nostr recency window in All Nostr / Following modes
After paginating the All Nostr feed past a certain depth the visible
items would degenerate back into Agora-only content. The root cause:

- The Nostr (kind 1) firehose is dense — one page of 30 events covers
  ~minutes of real time.
- The Agora layer is sparse — one page of 25 events covers ~days.
- With lockstep pagination both layers' `until` cursors advanced at
  roughly the same rate per page, so after several pages the Nostr
  cursor was at -30 minutes while Agora was at -30 days.
- When scrolling down through the chronologically-sorted merge, the
  user eventually reached timestamps below the Nostr cursor's reach,
  where the only remaining buffered items were old Agora ones.

Fix:

1. Compute the Nostr layer's "floor" (oldest loaded timestamp) and
   only render Agora items at or above that floor. Older Agora items
   are held back until Nostr catches up.

2. Smarter pagination — in mixed mode, always advance Nostr (it's the
   dense layer driving the scroll), but only advance Agora when its
   floor is at or above Nostr's. Otherwise we'd fetch Agora pages
   that can't be displayed because they sit below the visible window.

3. `hasNextPage` is now Nostr-driven in mixed modes — once Nostr is
   exhausted the feed is genuinely done, even if Agora still has more
   buffered older items (they were already shown earlier in scroll).

Pure Agora mode is unaffected (still paginates Agora directly).
2026-05-21 20:26:01 -05:00
mkfain 7506ed7dec Make All Nostr feed actually pull the firehose
The All Nostr layer was previously routing through `useFeed('global')`,
which has two problems for a 'show me everything' surface:

- It uses Ditto's `search: 'sort:hot protocol:nostr'` NIP-50 extension.
  Relays that don't honor this extension return nothing; relays that do
  return only curated 'hot' content, not the firehose.
- It filters by `getEnabledFeedKinds(feedSettings)`, so only kinds the
  user explicitly enabled in their feed-kind settings come through.

Replace the indirection with a dedicated `useNostrLayer` infinite query
inside `useMixedFeed` that pulls kinds [1, 6, 16] (notes + reposts) with
no `search:` filter and no kind-settings gate. The result is a straight
chronological pull of recent Nostr activity, which is what 'All Nostr'
should actually mean.

Following mode reuses the same layer with an `authors:` filter, replacing
the previous `useFeed('network')` path. The follow-list gate is preserved
so a logged-in user doesn't briefly see the global mix before their
follows arrive.
2026-05-21 20:26:01 -05:00
mkfain 4188e926a4 Post to any country, white Post button text
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.
2026-05-21 20:26:01 -05:00
mkfain f4688137bc Persistent default country, drop weather + organize feed, simplify mode switcher
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.
2026-05-21 20:26:01 -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
mkfain b83d35fc75 Remove flag backdrop behind world posts, keep corner flag pill
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.
2026-05-21 20:22:36 -05:00
mkfain f08e3d6226 Add three-mode home feed with Agora / All Nostr / Following
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.
2026-05-21 20:22:36 -05:00
Chad Curtis d1c53df4d4 Style campaign donate CTAs with white text and white QR logo 2026-05-21 19:27:57 -05:00
Chad Curtis 6693f2c153 Render wallet QR in donate dialog when signer can't sign PSBTs 2026-05-21 19:16:50 -05:00
Chad Curtis 8436c7b787 Drop campaign title from SP donation disclaimer
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.
2026-05-21 19:04:41 -05:00
Chad Curtis 710aa08818 Remove inaccurate URL preview from campaign form
The 'URL preview' line showed '/<slug>', but campaigns are routed via
NIP-19 naddr (`/:nip19`), not by slug. The actual URL is
'/naddr1...' which only exists after publish. Showing the slug
masquerading as a URL was confidently wrong, so drop it.
2026-05-21 19:04:41 -05:00
Chad Curtis 935c121bab Implement Campaign kind 33863
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.
2026-05-21 19:04:41 -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 b6f90a03c4 Merge branch 'main' of gitlab.com:soapbox-pub/agora 2026-05-21 17:46:47 -05:00
Chad Curtis 51d50e3b33 Remove "React" label from reaction button 2026-05-21 17:40:10 -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