Commit Graph

3760 Commits

Author SHA1 Message Date
lemon ac3cdf34b2 Rename user-facing 'Community' → 'Organization'
User-visible copy now matches the Organization rebrand. Internal
symbols, file names, query keys, routes, and storage keys are
intentionally left alone for this pass — they're still pinned to
"community" / "communities" until a dedicated rename commit.

Touched strings:

- `MobileBottomNav` and `sidebarItems` labels: "Communities" →
  "Organize", matching the existing TopNav copy.
- `CommunityDetailPage` hero fallback ("Unnamed Community" →
  "Unnamed Organization") and "About this organization" aria-label.
- `CommunityContent` thumbnail fallback name.
- `ExternalContentHeader.CommunityPreview` row label and fallback name.
- `NoteCard` kind-34550 noun "community" → "organization" (used in
  feed-card action lines like "created an organization") and the
  article switches from "a" to "an".
- `NoteMoreMenu` overflow-menu labels: "Report post to community" →
  "Report post to organization", "Remove from community" → "Remove
  from organization".
- `BanConfirmDialog` title, description, and success/failure toasts.
- `CommunityContentWarning` reporter pluralization and the
  fallback report-type label ("community guidelines" →
  "organization guidelines"); reporters are now scoped to founder /
  moderators per the commit 4 cleanup, so the wording reflects that.
- `CommunityReportDialog` description copy.
- `CreateGoalDialog` placeholder example.
- `CreateActionDialog` org-scoped description string.
- `CreateCommunityEventDialog` NIP-31 `alt` tag prefix.
- `CommentContext` kind-34550 entries in the action-noun and
  rendered-noun maps ("a community" / "community" → "an
  organization" / "organization").
- `extraKinds` kind-34550 entry: label, description, and blurb.
- `kindLabels` kinds 4550, 10004, 34550.
- `DiscoverHero` ticker stat copy.
- `GetFeedTool` error message drops "communities" since the
  Following feed no longer includes organization activity (removed
  in the badge-runtime commit).
2026-05-20 12:32:57 -07:00
lemon a68cad44c3 Drop badge-award member runtime
Removes NIP-58 badge-award membership validation from Agora's
organization model. Authorization collapses to two roles:

- Founder = author of the kind 34550 event (only the founder can edit
  organization metadata, enforced by replaceable-event semantics).
- Moderators = `p` tags with role "moderator" on that event (can hide
  content via kind 1984 content-bans; cannot edit the org).

There is no "member" tier any more. The kind 8 / kind 30009 chain
that previously gated discussion access, the members-only feed
filter, the avatar-stack member count, and member-ban moderation
actions are all gone.

What changes:

- `useCommunityMembers` returns `{ founderPubkey, moderatorPubkeys }`
  read directly from the parsed community event. The kind 8 badge
  query, `resolveMembership`, `isAuthorizedAward`, and the rank-1
  member tier are removed. The hook still queries kind 1984 events
  scoped to the org so content-bans and soft reports keep working.

- `applyCommunityModerationToEvents` keeps content-ban filtering for
  founder/mods. `resolveCommunityModeration` is simplified to a
  single pass — only founder/moderators can publish moderation
  actions, and the parsed report classification drops the
  `member-ban` action since wholesale member bans no longer exist.

- `BanConfirmDialog` drops `mode="member"`. Only event-level content
  bans remain. `NoteMoreMenu` drops the "Ban from community"
  affordance accordingly.

- `ParsedCommunity` loses `memberBadgeATag` and
  `memberBadgeRelayHint`. `parseCommunityEvent` no longer reads the
  `['a', '30009:…', '', 'member']` tag. `CreateCommunityPage` no
  longer mints a "Member of <org>" badge or attaches the member-badge
  tag, and on edit it strips any pre-existing member-badge tag so
  legacy wiring doesn't linger.

- The "Voices from everywhere" cross-organization activity feed is
  deleted from the Organize page. It duplicated per-org activity (now
  served by the official-activity shelves on each org detail page)
  and was the only consumer of `useCommunityActivityFeed`.
  `useFollowingFeed` drops its community-feed leg for the same
  reason. `useMyCommunities`, `useMembersOnlyFilter`, and
  `MembersOnlyToggle` go with it.

- `CommunityDetailPage` loses the MembersOnlyToggle in the hero, the
  "Add members" and "Edit badge" overflow-menu items, and the
  per-member ban affordance in the members dialog. The avatar stack
  now renders founder + moderators with a "Founder + N moderators"
  label and the dialog title becomes "Leadership".

- Deleted: `AddMemberDialog`, `CommunityBadgePanel` (which exported
  `CommunityBadgeEditorDialog`), `useCommunityActivityFeed`,
  `useMyCommunities`, `useMembersOnlyFilter`, `MembersOnlyToggle`,
  plus three already-unused legacy components (`CommunityChatPanel`,
  `CommunityPulsePanel`, `CreateCommunityDialog`) and
  `useCommunityChatMessages`. `PersonSearch` is extracted from the
  deleted `AddMemberDialog` into its own file so the create-campaign
  and create-community forms keep their member-picker UI.

The standalone NIP-58 badge tooling (`BadgesPage`, `GiveBadgeDialog`,
`useBadgeFeed`, etc.) is untouched — it's a separate feature surface
from organization membership and continues to work.

Out of scope: file/symbol renames (Community* → Organization*).
That's the next commit.
2026-05-20 12:32:57 -07:00
lemon 743390edf7 Rebrand Organize page: My organizations + Featured organizations
Updates the Organize page (/communities) for the organization-first
direction:

- All user-visible "Community" copy becomes "Organization" — page
  title, hero subtitle, section headers, empty states, logged-out
  prompts, members-only filter messaging, and the create-organization
  CTA. The route, file name, and internal symbols are unchanged for
  this commit; the codebase-wide rename is the next pass.

- "My organizations" replaces "My communities". Wired to
  `useManageableOrganizations` instead of `useMyCommunities`, so the
  shelf reflects the same trust model used for implicit org tagging in
  the create flows — organizations the user either founded (author of
  the kind 34550 event) or is listed as a moderator on (`p` tag with
  role "moderator"). The badge-award member-validation path is no
  longer surfaced here.

- "Featured organizations" replaces "Discover communities". Backed by
  a new `useFeaturedOrganizations` hook with a hardcoded list of
  curator-approved kind 34550 event IDs in
  `FEATURED_ORGANIZATION_EVENT_IDS`. Pinning by event ID locks the
  card to a specific revision; bumping a featured org means swapping
  in a newer ID. This is intentionally a stopgap until a configurable
  featuring mechanism (AppConfig field, NIP-51 list, etc.) lands.

- The hero stat ticker still rotates donations / orgs / countries, but
  the "orgs" entry now counts featured organizations instead of all
  discoverable communities.

`useMyCommunities` and `useDiscoverCommunities` stay in place for now
because `useCommunityActivityFeed` and `DiscoverPage` still depend on
them. The badge-award removal pass will follow.
2026-05-20 12:32:57 -07:00
lemon 558e5affea Align organization detail page layout with campaign and pledge pages
Restructures the org detail page body to match the rhythm already
established on /campaigns/:naddr and /pledges/:naddr:

- Container widens from `max-w-3xl` to `max-w-6xl` and the hero gets
  the same `sm:aspect-[21/9]` treatment so it doesn't feel cramped at
  the wider viewport width.
- The tab strip is removed entirely. Activity was the only visible tab
  and the Pulse / Chat triggers were already hidden. With one section
  left there's no reason to keep the `<Tabs>` wrapper or its plumbing
  (`activeTab`, `setActiveTab`, `fabAvailable`).
- Below the hero: the existing Donate / Share action row, then the
  official-activity shelves, then a new pledge-style engagement card
  (stats counters + `<PostActionBar>`), then a NIP-22 comments section.
- No funding progress bar — an organization isn't a fundraising target
  itself. Campaigns continue to provide that for fundraising.
- Comments render in the same `rounded-2xl bg-card border` frame the
  pledge page uses, with the same "No comments yet" dashed empty
  card. The legacy interleaved "initiatives + discussion" feed is
  gone — official campaigns / pledges / events now live in the
  shelves, and the comments section is purely NIP-22 replies.
- Adds `useEventStats`, `InteractionsModal`, `NoteMoreMenu`, and a
  separate `replyOpen` slot for the engagement bar's Reply button
  (the FAB's "New post" item keeps its own `composeOpen` slot so the
  two entry points don't interleave state).
- Removes the now-unused `<CommunityChatPanel>`, `<CommunityPulsePanel>`,
  `<ComposeBox>`, `<NoteCard>`, and `<FeedCard>` imports, along with
  the `useCommunityGoals` / `useCommunityActions` / `useCommunityEvents`
  hooks and the chain of derived state (`moderatedGoals`, `activeGoals`,
  `pastGoals`, `eventItems`, `actionEvents`, `activeInitiatives`,
  `pastInitiatives`, `activityItems`) that fed the old feed. Those
  components still exist in the codebase for now; commit 4 will prune
  the badge-award member runtime separately.

The FAB is now always available (single-column page) and routes
`New campaign` / `New pledge` to their dedicated create pages with
`?org=<naddr>`. `New event` still opens the in-page calendar dialog
since there's no dedicated create page for that yet.
2026-05-20 12:32:57 -07:00
lemon ec7d7f4326 Add official-activity shelves to organization detail page
The org detail page now surfaces three horizontally-scrolling shelves
above the activity feed:

- Campaigns (kind 30223)
- Pledges (kind 36639)
- Upcoming events (NIP-52 kinds 31922 / 31923)

All three shelves are powered by the useOrganization* hooks added in
5b5e8fe8, which author-filter to founder + moderators before querying
by the org's uppercase `A` root-scope tag. Anyone can publish an
event with an org's `A` tag, so the author filter is the actual
trust boundary that decides what counts as "official" activity for
that organization.

Each shelf is suppressed entirely when it has nothing to show, so an
org with no campaigns/pledges/events keeps the discussion feed at the
top of the viewport.

The activity-tab FAB now routes campaign and pledge creation to the
dedicated create pages with `?org=<naddr>` in the query string,
closing the loop with the implicit-tagging change from 8bef15a3. The
in-page `CreateGoalDialog` and `CreateActionDialog` are removed —
zap goals are being deprecated in favor of campaigns, and pledges now
go through the dedicated create page. The calendar-event dialog stays
in-page since there's no dedicated create page yet; it already emits
the uppercase `A` tag.

Pulse and Chat tab triggers are hidden from the tab strip while the
organization-first redesign is in progress. The corresponding
`<TabsContent>` panels are left intact so the code paths stay
verified — only the trigger affordances are suppressed.
2026-05-20 12:32:57 -07:00
lemon 3520c0ad6b Fix orphan spacing above pledge details action bar
The stats row had a `pb-2` and the PostActionBar received a `pt-3`
className when stats were present — leftover spacing from when a
horizontal divider sat between them. With the divider removed the
padding became an unjustified gap above the action bar.

Drop both so the action bar sits flush against the stats row with
even top/bottom spacing inside the engagement card, matching the
no-divider look the campaign details page already has when stats
are absent.
2026-05-20 12:32:57 -07:00
lemon c4f52b8aa7 Match pledge action bar to campaign details
Restore the `pt-3 border-t border-border/60` divider/padding
that separates engagement counts from the action bar, identical
to the campaign details page. The earlier `pt-3` without the
border left dead space; with the border back, the two pages
share the same visual rhythm.
2026-05-20 12:32:57 -07:00
lemon df44b1b2c6 Drop primary-tag badge from pledge card cover 2026-05-20 12:32:57 -07:00
lemon 8c6721a3fc Match pledge hero cover handling to the card
Pledge cards render their cover image with a built-in fallback:
when `action.image` is unset or fails to load, they fall back to
`DEFAULT_COVER_IMAGE`. The detail-page hero only handled the
unset case (with a gradient + icon) and never recovered from a
broken URL.

Mirror the card pattern in the hero: render an `<img>` with an
`onError` handler that swaps in `DEFAULT_COVER_IMAGE` on load
failure, so pledges without a cover (or with a dead one) still
look intentional instead of blank.
2026-05-20 12:32:57 -07:00
lemon fb7676b760 Simplify pledge detail hero and funding card
Remove visual clutter from the pledge detail page:

- Drop the "Pledge" badge and description preview from the hero
  overlay so the cover image and title carry the framing.
- Remove the divider between engagement counts and the action bar.
- Drop the "Remaining" and "Stored as sats" tiles from the
  funding card — donors only need funded vs. pledged totals.

The hero cover image is unchanged; it sources `action.image`
sanitized by `sanitizeUrl`, which falls back to the gradient
placeholder only when the event has no `image` tag.
2026-05-20 12:32:57 -07:00
lemon f97792b81e Drop deadline helper subtext from pledge form 2026-05-20 12:32:57 -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 248b07f45c Drop org selector from create forms; derive org from ?org= query param
Reverts the user-controlled OrganizationSelector added in b3997ec5.
Selecting an organization from inside a create form was the wrong
affordance: if a user starts a pledge or campaign from outside any
organization, the publication is implicitly under their own identity,
and if they start it from inside an organization, the org tag is
already implied by where the create flow was launched.

CreateCampaignPage and CreateActionPage now read `?org=<naddr|aTag>`
from the URL instead of rendering a selector field. When the param is
present, useManageableOrganizations checks whether the current user is
the founder or a moderator of that org; if so, the form emits the
`A` / `K` / `P` tags on publish. If not, the param is silently
dropped (so a stale or copy-pasted link can't forge an org-tagged
event), and a small note explains the publication is going under the
user's account.

OrganizationContextChip is a tiny shared component that surfaces the
resolved org as a non-interactive chip under the form title. The
organization detail page CTAs (next commit) will populate `?org=`
when launching the create flow from inside an org.

OrganizationSelector.tsx is removed — no other call sites.
2026-05-20 12:32:57 -07:00
lemon a759e653de Let campaigns and pledges publish under an organization
Adds an optional Organization selector to the create-campaign and
create-pledge forms. Only NIP-72 communities where the current user
is the founder or a listed moderator are offered, so the resulting
event's uppercase `A` root-scope tag lines up with the trust filter
applied by useOrganizationCampaigns / useOrganizationPledges.

Technically anyone could publish a kind 30223 or 36639 with another
org's `A` tag outside the Agora client. Restricting the selector
inside the client is the first line of defense; the author-filter in
the org-detail queries (next commits) is the actual security boundary.

Changes:

- useManageableOrganizations queries kind 34550 events the user
  founded (`authors`) or is moderator-tagged on (`#p`), then keeps
  only the ones where they're founder OR moderator.
- OrganizationSelector renders those orgs in a popover combobox with
  Founder / Moderator badges, plus a 'No organization' option.
- CreateCampaignPage and CreateActionPage gain a new optional 'Publish
  under organization' field. On publish, the form emits
  `A: 34550:<pubkey>:<d>` plus the `K` and `P` companion tags.
- CreateCampaignPage hydrates the selector from an existing campaign's
  `A` tag when editing.

Existing campaigns and pledges without an `A` tag continue to work
unchanged.
2026-05-20 12:32:57 -07:00
lemon 14939ff534 Add organization role helpers and official-activity hooks
Introduces the Agora Organization model on top of NIP-72 communities:
only the founder (kind 34550 event author) can edit metadata, and the
founder plus listed moderators (p tags with role "moderator") can
moderate the feed. Membership/badge-award validation will be dropped
from the runtime path in follow-up commits.

src/lib/communityUtils.ts grows four small helpers:

  - isOrganizationFounder
  - isOrganizationModerator
  - canEditOrganization        (founder only)
  - canModerateOrganization    (founder OR moderator)
  - getOrganizationOfficialAuthors

src/hooks/useOrganizationActivity.ts adds three trust-filtered queries:

  - useOrganizationCampaigns
  - useOrganizationPledges
  - useOrganizationEvents

Each query passes the founder-plus-moderators list as the `authors`
filter so forged events tagged with the organization's uppercase `A`
root-scope tag from non-moderator pubkeys never surface as official
activity. This matches the nostr-security skill's guidance on
author-filtering trust-sensitive queries.

The hooks do not yet ship in the Organization page; that wiring is in
the next commit.
2026-05-20 12:32:57 -07:00
lemon c9b48aeaae Align pledges with campaign patterns 2026-05-20 12:32:57 -07:00
lemon 24aa3a32d9 Polish pledge navigation and amount input 2026-05-20 12:32:57 -07:00
lemon 3dd229edfb Reframe actions as pledges 2026-05-20 12:32:57 -07:00
Chad Curtis 75a3453daa Merge branch 'dashboard-ui-polish' into 'main'
Polish dashboard page to match app design system

Closes #12

See merge request soapbox-pub/agora!24
2026-05-20 19:18:46 +00:00
Chad Curtis 7d0f565101 Merge branch 'ui-polish' into 'main'
Fix five small UI polish issues

Closes #13

See merge request soapbox-pub/agora!25
2026-05-20 19:15:13 +00:00
Chad Curtis 3d744518b2 Merge branch 'update-campaign-goal' into 'main'
Clarify that campaign goal is saved as sats, prevent edit drift

Closes #17

See merge request soapbox-pub/agora!26
2026-05-20 19:13:19 +00:00
Alex Gleason d4590a9340 Brighten the account switcher chevron on hover 2026-05-20 14:00:21 -05:00
Alex Gleason 3c330efaa9 Compact the top-nav account switcher trigger 2026-05-20 13:58:54 -05:00
Alex Gleason 89c392fa63 Remove Start Campaign button from top nav; brighten Join contrast 2026-05-20 13:49:36 -05:00
Alex Gleason a76a971321 Remove World link from the top nav 2026-05-20 13:22:42 -05:00
mkfain d2eca1811d Reword Community Campaigns subtitle
"Community-submitted fundraisers approved by moderators" was
procedural; replace with "Help fund the changes worth making" so the
section reads aspirationally rather than as a moderation explainer.
2026-05-19 18:39:36 -05:00
mkfain fdb849aa1d Default All Campaigns to Top; rename Newest pill to New
Top (most sats raised) is now the default sort; visiting /campaigns/all
shows the ranked list immediately. The chronological pill is renamed
from Newest to New for brevity.

URL state inverts accordingly: ?sort=none is the explicit value, Top
omits the param to keep the canonical URL clean.
2026-05-19 18:35:55 -05:00
mkfain d3e0d177a5 Rank All Campaigns by donation volume; drop NIP-50 sort/search
The previous NIP-50 sort:top/sort:hot path against Ditto was a dead end
for kind 30223: Ditto's engagement scoring is built for kind 1 notes
(likes / reposts / replies), none of which apply to fundraising
campaigns. All three sort modes returned indistinguishable output, and
the relay-side search: field returned nothing for campaign content.

Switch to client-side ranking and filtering using campaign-native
signals:

- Sort is now "Newest" (chronological, default) and "Top" (most sats
  raised across kind 8333 donation receipts).
- Newest is the default since it gives full relay coverage with no extra
  data dependency.
- Top batch-fetches every kind 8333 receipt tagging any visible campaign
  in a single round-trip, sums the amount tag, and ranks by total sats
  with donor count + created_at as tiebreakers. While scores load the
  list stays chronological so the page renders something useful
  immediately.
- Search filters client-side across title, summary, story, location,
  and t-tags. Substring, case-insensitive.

Drop the Hot mode entirely \u2014 7-day donation activity is too noisy with
current campaign volume to justify a separate UI affordance, and a
single Top vs. Newest choice is clearer.

Remove the page subtitle ("Every campaign published on Agora, including
ones awaiting moderation\u2026") at the user's request.

The hook no longer routes any queries to Ditto's relay group; the
default user-configured pool is used throughout, so campaigns published
only to non-Ditto relays are now discoverable too.
2026-05-19 18:31:59 -05:00
mkfain b7c88ecca8 Add sort + search to the All Campaigns page
Top/Hot/None sort and a free-text search bar, capped at 2 cards per row.
Top is the default.

Top and Hot use NIP-50 extensions (`sort:top`, `sort:hot`) that Ditto
implements; the page routes those queries (and any free-text search) to
the Ditto relay group via `nostr.group(DITTO_RELAYS)`. None with no
search uses the default user-configured pool so campaigns published only
to non-Ditto relays are still discoverable.

If Top/Hot returns nothing — cold cache, or Ditto doesn't yet weight
engagement for kind 30223 — we silently retry against Ditto without the
`sort:` field rather than show an empty page. Mirrors useMusicFeed.

The search input debounces at 300ms via the existing useDebounce hook.
Sort and search are independent: `search: "<query> sort:top"` works.

URL state (`?sort=top|hot|none&q=<query>`) makes results shareable.
Default values are omitted from the URL so the canonical path stays
clean.

Refactored useCampaigns to expose parseCampaignEvents — a pure helper
that handles the (pubkey, d) dedupe, archive filter, and parse step.
useAllCampaigns reuses it and passes `sortByCreatedAt: false` for
top/hot/search so we don't undo the relay's score order. No behavior
change for existing useCampaigns callers.
2026-05-19 18:16:22 -05: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 ed923bcde6 Drop the Team Soapbox attribution from the All campaigns subheader 2026-05-19 16:19:04 -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
filemon cb32405e55 Clarify that campaign goal is saved as sats, prevent edit drift
The goal input accepts USD but the published event stores sats. The UI
gave no indication of this, so the displayed USD goal silently drifted
as BTC price changed — confusing creators.

Create mode: the preview now reads "Saved as X sats · about $Y today.
The USD estimate may change with BTC price."

Edit mode: a goalTouched flag tracks whether the user actually changed
the goal field. If untouched, the submit handler preserves the original
goalSats exactly instead of round-tripping through USD at the current
price. A helper note shows the current saved sats so the creator knows
the field is pre-filled from a reverse conversion.
2026-05-19 17:05:12 -03:00
filemon b9fee19510 Merge branch 'main' into ui-polish 2026-05-19 12:25:15 -03:00
filemon f6b209949a Fix post menu preview: keep NoteContent with working line-clamp
The previous attempt replaced NoteContent with plain text, losing rich
rendering of hashtags, mentions, custom emoji, and nostr identifiers.
Reverting to NoteContent while keeping the overflow fix needed two
changes to make line-clamp-3 work:

- Render NoteContent as a <span> (as="span") so it participates in
  the parent's -webkit-box line counting. The default <div> wrapper
  with overflow-hidden created a separate block formatting context
  that defeated line-clamp entirely.
- Add disableNoteEmbeds to prevent block-level EmbeddedNote/
  EmbeddedNaddr cards from appearing inside the compact preview.

The outer container keeps overflow-wrap-anywhere so long URLs break
safely within the clamped area.

Regression-of: fc950865
2026-05-19 12:17:27 -03:00
Alex Gleason 949bd5fde4 Harden cover image forms and self-zap feedback 2026-05-19 00:33:16 -07:00
lemon 27d65bc389 Reuse the Create Community page for editing, drop founder row
Two follow-ups to the new /communities/new page:

1. Stop rendering the founder as an 'anonymous' chip at the top of the
   Moderators section. The row was synthesized from just a pubkey
   (genUserName fallback, no avatar), which looked broken even though
   the founder is always implicitly the first moderator on the
   published kind 34550. Drop the row entirely; the founder remains
   pubkey #0 in the moderator list when we publish, just isn't
   visible as a chip.

2. Wire the page to handle ?edit=<naddr> the same way
   CreateCampaignPage handles its edit param:

   - Decode the naddr, reject anything that isn't a kind 34550 with
     an 'Invalid edit link' guard card.
   - Fetch the existing community (inline useQuery against
     ['community', pubkey, dTag] since there's no useCommunity hook
     yet). Show a 'Loading community…' card while it resolves.
   - Prefill name, description, image, and moderators when the data
     lands. Resolve each moderator's kind-0 profile via the same
     two-step cache-then-network pattern campaigns use for recipients,
     so chips render with proper avatars and names instead of fallback
     stubs.
   - Show a 'Community cannot be edited' guard if the viewer isn't
     the founder, mirroring the campaign edit author check.
   - On submit, fetchFreshEvent + publish kind 34550 with prev. Strip
     d/name/description/image/alt and any p-with-role-moderator tags
     from the previous tag set; rebuild them from form state, then
     re-append the preserved tags (badge a-tag, relay hints, …) and a
     fresh alt. The implicit member badge is left alone in edit mode
     (matches CreateCommunityDialog's edit branch).

CommunityDetailPage's 'Edit community' dropdown item now navigates to
/communities/new?edit=<naddr> instead of opening CreateCommunityDialog.
The dialog mount and its editCommunityOpen state are removed.

The dialog file is left in the tree even though nothing imports it
anymore — keeping it makes a revert cleaner if the user changes their
mind.
2026-05-19 00:19:38 -07:00
lemon f2805ed9d8 Add a dedicated Create Community page
CommunitiesPage used to open CreateCommunityDialog when the user hit
'Create community' in the hero, the My Communities shelf, or the empty
state. Replace those entry points with navigate('/communities/new'),
mirroring how CreateCampaignPage and CreateActionPage already work.

The new page covers the same three NIP-72 fields the dialog handled
(name + description + cover image) plus a Moderators section that
the dialog never exposed:

- Name, with a live URL preview of the derived slug for transparency.
- Description, in the same Textarea shape as the dialog.
- Cover image, via the shared <CoverImageField> so drag-and-drop +
  paste-URL behavior matches the campaign and action create pages.
- Moderators, via the same <PersonSearch> CreateCampaignPage uses for
  recipients. The founder is pinned at the top of the list as a
  non-removable row labeled 'Founder'; PersonSearch is told to exclude
  the founder so they can't be added a second time. Extra moderators
  go into the kind 34550 event as additional ['p', pk, '', 'moderator']
  tags alongside the founder's.

Submit logic is the kind 30009 badge mint + kind 34550 community
publish from CreateCommunityDialog's create branch, including the
d-tag and badge-d-tag collision checks. Cache keys touched on success
match the dialog: ['addr-event', 34550, pubkey, dTag] is seeded,
['my-communities'] and ['community-activity-feed'] are invalidated.

CreateCommunityDialog itself stays in the tree because
CommunityDetailPage still opens it in edit mode. A unified edit page
that folds in 'View members', 'Add members', and 'Edit badge' is
explicitly out of scope for this change.
2026-05-19 00:19:38 -07:00
lemon 0745d99e85 Share the cover-image picker between Action and Campaign forms
Both forms had their own CoverPicker / dropzone implementation, and the
two had diverged: the action page learned to accept drag-and-drop while
the campaign page was still click-only. Extract the entire affordance
(dashed dropzone + sanitized preview + remove button + template strip +
URL input) into <CoverImageField> in src/components/, used by both
pages.

The new component takes a controlled value/onChange pair and an optional
templates array, so the campaign page (no templates) and the action page
(six Blossom-hosted defaults) reuse the same dropzone and the same
drag-and-drop, MIME-checked upload path. Clicking a template fills both
the dropzone preview and the URL input from a single source of truth.

While here, dedup three more copy-pastes between the two pages:

- Lift FormSection (the titled section wrapper with the Required /
  Recommended / Optional badge) into src/components/FormSection.tsx.
  Both pages now import the same component instead of redeclaring it.
- Move getTodayDateInput() into src/lib/dateInput.ts. Both pages need
  the same YYYY-MM-DD-in-local-tz string for the deadline picker's min
  attribute; keeping it in one place means future timezone tweaks land
  in one file.
- Run the action page's coverImage through sanitizeUrl() at submit
  time the same way the campaign page does, so a paste-in cover URL
  that isn't well-formed https:// drops out of the published 'image'
  tag instead of getting written verbatim.

No user-visible behavior change on the action page; the campaign page
gains drag-and-drop and the 'Click or drag an image here' prompt copy.
Net diff: -318 / +13 in the page files.
2026-05-19 00:19:38 -07:00
lemon 4bba4159f1 Serve default action covers from Blossom
The template gallery used to point at relative paths under
/challenge-covers/, which meant any kind-36639 event whose author
picked a template published an 'image' tag like
'/challenge-covers/cover9.png'. That string only resolves on Agora's
own origin, so the cover broke as soon as another Nostr client
rendered the event.

Replace each DEFAULT_ACTION_COVERS entry with the public Blossom URL
of the same image so the tag we publish is portable across clients.
DEFAULT_COVER_IMAGE (the fallback for action cards whose author never
set one) now points at the Blossom-hosted Justice image too.

The original /public/challenge-covers/ files are kept in place because
the Actions hero banner still reads them as static assets through Vite.
2026-05-19 00:19:38 -07:00
lemon 23977a64ca Let the cover-image dropzone accept drag-and-drop
The dropzone label previously only opened the file picker on click. Wire
up onDragOver / onDragEnter / onDragLeave / onDrop so dragging an image
from the OS file manager (or another browser window) onto the box
uploads it through the same useUploadFile path the click flow uses.

- Highlight the dropzone (border-primary + light primary fill) while a
  file is being dragged over it so users get a clear hit target.
- Validate the dropped file's MIME type against the same image/png,
  image/jpeg, image/webp set the file input's accept attribute enforces,
  so a stray PDF dragged in doesn't get posted to Blossom.
- Update the placeholder copy to 'Click or drag an image here' so
  the affordance is discoverable.
- Reject the dropped file silently while an upload is already in
  flight (matches the pointer-events-none on the click path).
2026-05-19 00:19:38 -07:00
lemon c73c15de22 Polish the Create Action page form
- Reorder the fields to match how authors think about an action: Title,
  Type + Bounty on the same row, Description, Country, Cover image,
  Start date + Deadline on the same row, then the Timezone block.
- Default the Type select to "Action" so the most general bucket is
  picked unless the author opts into a more specific kind.
- Block past dates in the Deadline picker (min={today}) and reject them
  at submit time with a clear error, mirroring CreateCampaignPage.
- Promote Country and Cover image from Optional to Recommended now that
  the actions index leans heavily on both for filtering and visual scan.
- Replace the cover field with the campaign-style picker: a clickable
  dashed dropzone (image preview + remove button + ImagePlus prompt),
  a thumbnail strip in between, and a URL input below. Clicking a
  thumbnail just fills the URL input, no permanent default selection
  is forced on the user, and the URL stays editable.
- Trim the default cover gallery to seven curated images by dropping
  the four overlapping/redundant entries (Protest March, Unity,
  Resistance, Change, Demonstration).
2026-05-19 00:19:38 -07:00
lemon a5159e040b Convert Create Action modal into a dedicated /actions/new page
The actions index used to open CreateActionDialog from three places (hero
CTA, FAB, empty state). Now all three navigate to /actions/new, which
mirrors CreateCampaignPage's layout — back arrow + page title, a single
rounded form panel of FormSection blocks, and a full-width submit
button — while preserving every field, validation rule, and tag-emitting
behavior of the old modal.

CreateActionDialog itself stays in the tree because CommunityDetailPage
still opens it inline to attach an action to a NIP-72 community.
2026-05-19 00:19:34 -07:00
Alex Gleason c2bf0bd88e Restrict the campaigns hero rotation to featured campaigns
The rotating banner, spotlight card, and globe markers now all draw
from the hand-picked featured pool only. Previously the spotlight
loop also pulled in every campaign returned by useCampaigns, so the
banner image, spotlight card, and globe pins cycled through community
submissions alongside the two featured slots.

Featured campaigns still flow through the existing country/coords
gate, so anything without a resolvable country is dropped from the
globe — and from the banner — to keep the three in sync.
2026-05-19 00:37:56 -05:00
Alex Gleason 9fec863f18 Put the entire country page inside one FeedCard
User flagged that the /i/iso3166:VE page still looked disjointed:
the cinematic hero bled edge-to-edge, the ExternalActionBar sat as
a bare Twitter-style border row below it, the ComposeBox sat raw,
and only the Pinned + Recent feeds were wrapped in their own
FeedCards underneath. Five separate stripes stacked vertically with
no shared container.

Restructure the country branch so the whole surface lives inside
one rounded FeedCard:

- CountryContentHeader (cinematic hero) becomes the top of the card
  with its rounded corners clipped by the FeedCard's overflow-hidden.
- Hero gradient's bottom stop changes from hsl(var(--background)) to
  hsl(var(--card)) so the fade meets the card surface, not the page
  background, eliminating the dark-mode color seam.
- Drop the section's mb-2 since the action bar now sits flush.
- ExternalActionBar sits flush inside the card; its existing
  border-b reads as an internal section separator.
- ComposeBox renders with hideBorder + bg-transparent so it inherits
  the card surface. Added an optional className prop to ComposeBox
  so callers can override the default bg-background/85.
- Pinned section gets a border-t border-border heading band; its
  loading state inlines NoteCard-shaped skeletons rather than the
  FeedCard-wrapped CommentsSkeleton (which would have card chrome
  inside card chrome).
- Recent section's heading becomes a border-t band between Pinned
  and Recent when both are present; its loading state also inlines
  skeletons; FlatThreadedReplyList sits flush in the card.
- The infinite-scroll sentinel + empty state move outside the card.

URL / unknown content types keep the previous edge-to-edge action
bar treatment (their content headers already render their own
chrome and don't need the unified card).
2026-05-19 00:14:27 -05:00
Alex Gleason 6aeed26642 Wrap focused post, community feeds, and pinned posts in FeedCard
The previous sweep wrapped vertical NoteCard feeds in FeedCard but
missed three surfaces that share the same Twitter-style edge-to-edge
problem:

- PostDetailPage's focused post (`/nevent…`, `/note…`): the
  ancestor previews + AncestorThread + focused <article> all sat
  bare on the page background while the replies below were wrapped
  in a FeedCard. The main post read like background noise and the
  replies read like the actual content. Wrap the entire focused
  group (previews + ancestors + focused article — all seven kind
  variants funnel through this) in one FeedCard so the page reads
  "thread context → this post → replies" as two cohesive cards.
  Also wrap the ProfileBadgesDetailView's bare top NoteCard.

- CommunityDetailPage's Activity tab (`/naddr…` community kind
  34550): bare `divide-y divide-border` for the activity stream
  AND the past-initiatives stream. Replace with FeedCard. The
  community page already supplies its own `max-w-3xl px-4 sm:px-6`
  wrapper, so the FeedCard opts out of its own margins via `mx-0
  sm:mx-0` to avoid doubling up the side padding. The Past-Initiatives
  heading gains a `border-t border-border` so it reads as a section
  divider inside the card rather than a floating header.

- CommunityPulsePanel: same divide-y → FeedCard fix.

- ExternalContentPage's country pages (`/i/iso3166:VE`): Pinned
  posts list was bare while Recent posts below it sat in a FeedCard
  — same Pinned-bare / Recent-card mismatch on the same screen. Wrap
  Pinned in FeedCard. Add a "Recent" eyebrow heading above the
  recent posts feed when both sections are visible, so the two
  sections are clearly delineated. Move the Pinned heading's
  horizontal padding from `px-4` to `px-4 sm:px-6` so it lines
  up with the card margins on tablet+.
2026-05-19 00:02:58 -05:00
Alex Gleason 3530754518 Wrap NoteCard feeds across the app in a soft FeedCard surface
After fixing /discover's 'Voices from everywhere' feed, the same
Twitter-style edge-to-edge divided-list pattern was hiding in 24
other feed sections across the app: notifications, profile wall,
search results (posts + accounts + community posts + the empty-state
FollowsList), bookmarks, badges, trends, list members + their posts,
domain feeds, relay feeds, events feed, communities feed, the
Bluesky page, external-content comments + book reviews, post detail
replies (in three render paths), and the FollowPage members tab. On
the GoFundMe-shaped layout they all read as 'random separators
floating in space and posts with no sides.'

Introduce a shared FeedCard component that bakes in the standard
canvas — mx-4 sm:mx-6 rounded-2xl bg-card border border-border/60
shadow-sm overflow-hidden — and replace the bare divide-y wrappers
at all 25 feed-list sites with it. NoteCard rows already self-apply
px-4 py-3 border-b border-border, so live feeds don't need a
divide-y; pure skeleton lists (rows with no own borders) keep
divide-y on the FeedCard className.

Sites intentionally left alone:
- The 8 popover/autocomplete dropdown lists inside max-h-* scrollers
  on BlueskyPage, WikipediaPage, BooksPage, and ArchivePage (already
  inside bg-popover rounded-lg chrome — double-wrapping clashes).
- CampaignDetailPage's beneficiaries list (already inside a Card,
  the divide-y is a section separator).
- CampaignDetailPage's comments card (uses a custom -mx-2 sm:-mx-4
  inside the article column; FeedCard's mx-4 sm:mx-6 is wrong for
  that nesting).

Also fix TrendsPage's hashtag skeleton: it used a vertical divide-y
list shape but the loaded UI is a flex-wrap of pill badges. Replace
TrendSkeleton with a small h-7 w-24 rounded-full so the skeleton
matches the loaded shape and doesn't pop on transition.
2026-05-18 23:47:08 -05:00
Alex Gleason 5049116a6f Wrap the Voices feed in a soft card
DiscoverPage's 'Voices from everywhere' feed was the page's odd one
out: every section above (country pulse strip, Help raise hope
shelf, Find your people shelf) renders inside its own banded or
rounded canvas, but the Voices feed used the legacy Twitter timeline
treatment — edge-to-edge rows separated by floating divide-y
hairlines with no container chrome. On a GoFundMe-shaped page next
to rounded shelves above, the rows read as 'separators floating in
space and posts with no sides'.

Wrap the feed (loading skeleton, populated, and empty states) in
the same rounded-2xl bg-card border border-border/60 shadow-sm
overflow-hidden surface I introduced for the campaign-page comments
card. Drop the redundant divide-y on the populated state — NoteCard
already self-applies px-4 py-3 border-b border-border, so the
container only needed dividers in the skeleton state where rows have
no border of their own. Add the same border-b to the CampaignCard
branch of DiscoverFeedRow so campaign rows separate correctly
between NoteCard neighbours inside the card.
2026-05-18 23:14:46 -05:00
Alex Gleason 3b641a8d7c Sweep remaining Twitter-style action rows into PostActionBar / chip style
The first chip-row pass touched the campaign page (PostActionBar +
NoteCard). The post-detail page, book feed, and external/Bluesky
content rows still rendered the old spread-out pill toolbar — visible
when clicking any note from /discover, the book index, or a Bluesky
syndication. Bring them in line:

- PostDetailPage.tsx: replace the five remaining inline action rows
  (standard post, repost card, zap card, profile detail, vanish event)
  with <PostActionBar event={event} onReply onMore />. Drops ~270
  lines of duplicated reply/repost/react/share/more JSX and removes
  the now-dead handleShare, repostTotal, encodedEventId locals and
  the MessageCircle / MoreHorizontal / Share2 / ReactionButton /
  RepostMenu / toast / shareOrCopy imports that fed them. Update the
  loading skeleton to use chip-shaped placeholders.

- BookFeedItem.tsx: same migration. The component already had a
  bespoke action row that mirrored PostActionBar minus the share
  button — using PostActionBar restores parity and removes the
  hand-rolled zap branch, the canZapAuthor / isZapped / useUserZap
  glue, and the MessageCircle / RepostIcon / Zap / formatNumber
  imports.

- ExternalContentHeader.tsx + BlueskyPage.tsx: these can't switch to
  PostActionBar because the comment/repost handlers publish to
  Bluesky's external semantics, not Nostr. Hand-restyle the rows to
  the chip aesthetic instead (h-9 px-3 rounded-full, label fallback
  on sm+, share/more pushed right with a flex spacer).

- ExternalReactionButton.tsx: mirror the variant: 'pill' | 'chip'
  prop added to ReactionButton in the previous commit, so the
  external-content rows can opt into the chip look without affecting
  ExternalContentPage's sidebar toolbar which still uses the pill.

PodcastDetailContent, MusicDetailContent, and PhotoBottomBar still
use their own deliberate aesthetics (large circular play button with
matching side buttons; immersive lightbox bar). Left alone — they
don't read as Twitter rows.
2026-05-18 23:04:39 -05:00
Alex Gleason 3e31d26660 Replace Twitter-style action bar with GoFundMe-style chip row
The campaign page (and every post feed) inherited Ditto's spread-out
pill action bar — icons across the full width, framed by a heavy
top+bottom border band, with cascading dots on the 'Show more replies'
thread connector. It read as a Twitter toolbar, which is wrong for a
fundraising client.

Restyle the shared action row across PostActionBar and NoteCard:
- Chip-style buttons (h-9 px-3 rounded-full) with inline counts.
- Engagement actions cluster left, share/more pushed right with a
  flex spacer instead of justify-between.
- Drop the unconditional top+bottom border band; pages add their own
  separator via className when needed.
- Show 'React'/'Reply'/'Repost' word labels on sm+ when the count is
  zero, so the bar reads as labelled affordances rather than icon
  pills floating in space.
- Add a 'chip' variant to ReactionButton so it can switch between the
  legacy pill (still used by PhotoBottomBar, PodcastDetailContent,
  MusicDetailContent, BookFeedItem, PostDetailPage, etc.) and the new
  chip look.

ThreadedReplyList's 'Show N more replies' button drops the four
cascading dots in favour of a single soft connector that brightens on
hover.

On the campaign page itself, wrap the engagement stats + action bar
in a soft card, add a 'Comments & donations' section heading with a
count, and replace the bare 'No comments yet' line with a dashed
empty-state CTA that opens the reply composer.
2026-05-18 22:47:18 -05:00