Wrap the hero banner and tab strip in a shared image+gradient backdrop
so the banner image continues underneath the tabs and fades into the
page background, removing the hard seam between them. The gradient
holds heavy darkness through the tab strip (kept legible with light
tab text + drop-shadowed underline) and drops to the page bg only at
the very bottom edge.
Reduce banner cognitive load: move the description behind an Info
button next to the title (drop the inline line-clamp and its
ResizeObserver-based clipping detection), promote the avatar stack
above the title row, and shorten the banner aspect ratio (2:1 mobile,
21:9 desktop).
The persistent vaul bottom sheet ate half the map on phones and tablets
and made the docked discovery panel crush the map between 900px and
1280px. Swap it for a centered Dialog modal opened by a single button
anchored top-right next to Leaflet's zoom controls.
The docked WorldDiscoveryPanel now hides below xl (1280px) instead of
the sidebar breakpoint (900px) so the map stays usable when the panel
would otherwise crowd it. CommunityStatsPanel drops its rounded-2xl
card border in compact mode so it doesn't render box-in-a-box inside
the modal or docked panel.
Initial map zoom bumps from 2 to 3 below xl so phones and tablets
don't see ocean bands above and below the world tiles. Default
viewport center moves to Venezuela. Leaflet zoom controls picked up
themed background / foreground / border / primary-on-hover styling to
match the rest of the UI, and the country search header is opaque so
it no longer renders blurry inside the modal.
The community detail page previously fanned out the FAB into a stack of
chips positioned in the page's bottom-right corner, which drifted away
from the actual FAB on desktop (where the FAB is sticky inside the
center column). Move the menu into FloatingComposeButton itself: when a
page declares a `fabMenu` via useLayoutOptions, the FAB renders as a
Radix Popover trigger and the menu opens anchored to it on both mobile
and desktop. Hover state inverts to primary surface + foreground so
icons stop sitting on a same-color background (`--accent` mirrors
`--primary` in this theme system).
Initiatives now renders goals + events as one chronological list. The
sub-toggle is gone; active events sort ascending by start date, then
active goals by newest, then a single Past section in descending order
by closing/end timestamp.
The community detail page now mirrors the adventure-detail / follow-pack
banner pattern: the hero image fills the top area with a gradient
overlay, and the title, description, member avatar stack, follow toggle,
members-only filter, edit, and share controls all sit inside it. The
former Members tab is gone; tapping the avatar stack opens a dedicated
members dialog that hosts the badge panel, leadership and rank-and-file
sections, ban controls, and (for founders/mods) an inline AddMemberPanel
so search-and-add happens in the same surface instead of a second
dialog hop.
To support that embed, AddMemberDialog's form body is extracted into a
reusable AddMemberPanel export; the thin Dialog wrapper is kept for any
existing callers and now delegates submit/reset/close to an onComplete
callback.
Replace the smooth arc shapes shared by the mobile top bar, sub-header
tabs, and bottom nav with angled V polylines centralized in
ArcBackground. The top bar and sub-header now use flat rectangles, and
the bottom nav has a sharp V apex that cradles a centered Agora-bolt
Feed button. The bottom nav row layout changes from
[Home, Search, Notifications, Profile] to
[Search, Communities, _apex_, Notifications, World] with smaller outer
items, and the apex links to the configured home/feed page with
scroll-to-top + invalidation on re-tap.
Also drop the redundant 'Feed' page header on the home feed and the
border under the compact ComposeBox so it blends with the tabs strip
below it.
- useFollowingFeed now also queries posts for the user's followed
hashtag interests (NIP-51 kind 10015 t tags) and merges them into
the combined Following feed, subject to the same recency floor.
- Drop the per-hashtag and per-geotag tabs from the home feed
subheader. Legacy 'hashtag:'/'geotag:' session-storage values fall
back to the Following tab.
- Invalidate the new following-feed query keys when interests change
so the Following feed refreshes immediately on follow/unfollow.
- Remove the now-dead HashtagFeedContent and GeotagFeedContent
components and their unused imports.
- Split the home feed's old Follows tab into 'Following' (combined) and
'Network' (people-only, original behavior preserved).
- Add country follows via NIP-51 kind 10015 i tags (iso3166:XX), with
a Follow/Unfollow button on country pages reusing FollowToggleButton.
- New useFollowingFeed merges network + community activity + followed
country events, sorted strictly by recency. A recency floor (oldest
loaded network item, or now-14d when network is empty) prevents
sparse sources from surfacing old events too early.
- Empty state on Following is country-centric and routes to the World
tab to encourage country discovery.
- Invalidate the new feed query keys on follow/unfollow and
community-bookmark mutations.
Two post-merge regressions from the ditto/main merge:
1. Left sidebar collapsed to icon-only column at desktop widths.
Ditto's responsive aside (`hidden sidebar:flex ... lg:w-1/4
lg:max-w-[300px]`) sizes itself off the flex parent. Agora wraps
`<LeftSidebar />` in an extra `<div className="hidden
sidebar:block">`, which had no width — so `w-1/4` computed
against zero and the sidebar collapsed.
Removed the wrapper div in MainLayout — the `<aside>` now handles
its own hiding and width directly inside the flex parent, matching
Ditto's structure.
2. Kind 3 contact lists rendered as empty/broken cards.
Ditto unified kinds 3/30000/39089 under `PeopleListContent` (uses
`parsePeopleList` which synthesizes a "{Name}'s follows" title
for kind 3 since the event carries no title/description/image of
its own). Agora's NoteCard still used the old `FollowPackContent`
path that only matched 30000/39089, so kind 3 fell through to the
default note render.
Swapped `FollowPackContent` for `PeopleListContent` and added
kind 3 to the `isFollowPack` check.
Regression-of: 740fc1c6
Two new Feed-section toggles in Content Settings, both disabled by
default (existing users don't suddenly get a noisy feed of every like
and zap their follows hand out):
- Reactions (kind 7)
- Zaps (kind 9735 Lightning + kind 8333 on-chain — one combined
toggle since users don't think in terms of payment rails)
When enabled, reactions and zaps from followed users surface in the
Follows feed as a header above the target post — same shape as the
existing kind 6 / 16 repost overlay ("X reacted to" / "X zapped
1,234 sats" / "X reposted"). The reaction overlay renders the kind 7
event's actual emoji via ReactionEmoji (handling unicode, "+"/"-"
likes, and NIP-30 custom emojis) rather than a generic smiley. The
target event is unwrapped by useFeed and useProfileFeed in a single
batched ids query, then deduped so a direct post always wins over any
overlay for the same event.
The verb in each overlay header is a Link to the underlying reaction
/ repost / zap event's /:nip19 page, matching the new behavior in
Notifications. Reposts now carry the wrapper event (`repostEvent`)
through FeedItem so this works for them too without a separate fetch.
Global feed continues to exclude reposts, and now also excludes
reactions and zaps for the same reason — they need an author filter
to be useful and would otherwise drown out direct posts.
Some LNURL providers omit the `amount` tag entirely and only encode
the value inside the bolt11 invoice. NotificationsPage's local
`getZapAmountSats` didn't parse bolt11, so those zaps showed up as
"X zapped you" with no number. Move the helper to a shared module
and route the 9735 branch through `extractZapAmount`, which already
falls back amount tag → description JSON → bolt11.
While in this code, wrap the "reacted to", "reposted", and "zapped"
verbs in a Link pointing at the underlying event's nevent so readers
can jump straight to the reaction / repost / zap detail page.
Splits the existing Follow All button on people-list, follow-pack, follow-set, badge, and Team Soapbox detail views into a primary Follow All + a caret dropdown whose lone option is Mute All. Mute All opens an AlertDialog and, on confirm, merges every pubkey in the list into the viewer's NIP-51 kind 10000 mute list. Lets you treat any people list as a mute source as well as a follow source.
To make mute meaningful for users who already followed someone before muting, follow-scoped feed queries now subtract muted pubkeys from the authors filter at query time, via a shared useMutedAuthorFilter hook that builds the muted-pubkey Set once per mute-list change and exposes a single excludeMuted helper. The hook replaces ten inline copies of the same filter — including two that allocated a new Set per follow-list element inside a .filter() callback. Render-layer mute filtering stays in place as a second line of defense.
Also adds reusable hooks for the bulk operations (useFollowActions.followMany, useMuteList.muteManyPubkeys) and replaces four duplicated inline Follow All implementations with a single FollowAllSplitButton component.
Hardcoded MEMPOOL_API constant in src/lib/bitcoin.ts becomes a baseUrl
parameter on every fetch helper, sourced from a new `esploraBaseUrl`
field on AppConfig (default `https://mempool.space/api`). The wallet,
zap dialogs, on-chain zap verification, and NIP-73 Bitcoin tx/address
pages now read the URL from useAppContext and pass it through, so
self-hosted Esplora deployments (or Blockstream's) work without code
changes. The mempool.space-specific `/v1/prices` extension is still
appended by fetchBtcPrice.
Use relay-side COUNT as a stable floor for totalPosts and state-level
leaderboard/distribution counts. Falls back gracefully to event-based
counts if the relay does not support NIP-45. Surfaces legacy
content-scan municipality matches as a separate hint on the KPI tile.
- totalPosts: globalCount ?? viewPosts.length
- Leaderboard/distribution: Math.max(eventCount, stateCountFromRelay)
- Participants: stays fully event-based (preserves live/activity semantics)
- Municipalities: stays fully event-based (no per-muni COUNT queries)
- legacyDetected: deduplicated count of posts attributed via content scan
The release-summary paragraph (max 500 chars by convention) skipped the
truncation that the legacy first-bullet fallback applied, so toasts could
render an entire paragraph. Truncate both branches uniformly on a word
boundary with an ellipsis, matching the prior 60-character cap.
Regression-of: d044218c
Consolidates Bitcoin-related HTTP onto a single host — the rest of the
wallet already uses mempool.space for addresses, txs, UTXOs, fees, and
broadcast — so dropping the CoinGecko dependency removes one external
service from the connect-src surface and simplifies CSP / privacy review.
mempool.space's /api/v1/prices returns USD (and several other fiat
currencies) at the same shape we need, so fetchBtcPrice keeps its
`Promise<number>` signature and every caller continues to read from
the same React Query cache key.
The featured-article card was showing the raw `title` field
(e.g. "Japan_Cup") and opening Wikipedia in a new tab. Use the
API's `normalizedtitle` for display and route the click through
/i/ so users land on the in-app article view.
Each CHANGELOG.md release section now begins with a single plaintext
paragraph (max ~500 chars) before any `### Category` heading. That
paragraph drives the release blurb in three storefronts and the
in-app version-update toast, so we no longer ship a marketing-grade
description in one place and a raw bullet list in another.
scripts/extract-release-notes.mjs is the single source of truth for
extraction. It emits the full section (summary + lists) by default
and only the summary paragraph with --summary, with a
`Ditto vX.Y.Z` fallback for legacy entries that have no summary.
CI changes:
- New `release-notes` job (build stage, default node:22 image)
produces `artifacts/release-notes.md` and
`artifacts/release-notes-summary.txt` once per pipeline.
- `release` job pulls release-notes.md as the GitLab Release
description (replaces the old inline awk extraction). It now uses
`needs:` with `artifacts: false` for build-apk/build-ipa to
avoid re-downloading the .apk/.aab/.ipa it doesn't open.
- `publish-app-store` copies release-notes-summary.txt to
`ios/fastlane/metadata/en-US/release_notes.txt` (replaces its
own awk extraction).
- `publish-google-play` drops `--skip_upload_changelogs`, writes
the summary to
`android/fastlane/metadata/android/en-US/changelogs/<versionCode>.txt`
and points fastlane supply at `--metadata_path`. This is the
first time we upload a What's New text to the Play Store from CI.
App-side changes:
- `src/lib/changelog.ts` parser captures the leading non-blank
paragraph (before any bullet or category heading) into
`entry.summary`.
- `VersionCheck.tsx` toast uses `entry.summary` when present,
falling back to the legacy 60-char first-bullet excerpt for
backward compatibility.
- `ChangelogPage` renders the summary as a lede paragraph above
the bullet list in both LatestRelease and ChangelogEntryCard.
Changelog content:
- Added summary paragraphs to v2.14.3, v2.14.2, v2.14.1.
Skill + AGENTS.md updates:
- `release` skill documents the summary paragraph format, the
500-char convention, and the seven-job pipeline.
- `ci-cd-publishing` skill gains a 'Release notes pipeline' section
mapping each storefront to its source artifact.
- AGENTS.md pipeline summary mentions release-notes and the summary
flow into both store "What's new" fields.