Commit Graph

2471 Commits

Author SHA1 Message Date
Chad Curtis d2ec2a0f91 Trim hero map viewBox to land-bearing latitudes
The full equirectangular viewBox (-90..+90 lat) leaves a tall empty
ocean band at the bottom of the hero on wide viewports. Antarctica's
coastline only reaches ~lat -75 around its perimeter, so the bottom
~5° of the viewBox is always empty. Trim to -85..+85 so 'slice' fills
the hero with land texture instead of empty space. No distortion —
preserveAspectRatio still scales uniformly.
2026-05-24 20:57:17 -05:00
Chad Curtis ceb1995b6a Lock campaign card body height and lift meta chips onto the banner
Cards in a multi-column grid now have a deterministic, predictable
silhouette. The body region is a fixed stack — title (1 line, truncates)
+ summary (1 line, truncates with a non-breaking-space placeholder when
absent) + progress (invisible-bar placeholder absorbs no-goal and
silent-payment cases) + creator footer — so no card has dead space or
ragged bottoms regardless of which optional fields are populated.

Country, deadline, hidden badge, and moderation menu all moved onto the
banner image as glass chips overlaid on a bottom gradient (chips
appear only when there's data to show, so a chip-free banner stays
clean). The donor-count chip became an inline aside on the creator
line. The variable meta row is gone entirely from the body.

CampaignPrivateNotice was rebuilt to mirror CampaignProgress's
vertical footprint (invisible bar + one text row) so silent-payment
cards line up with public-progress cards beside them.

Progress bars on cards and the detail-page DonateColumn use
`bg-foreground/15` for the track instead of the primitive's default
`bg-secondary`, which in dark mode shares its color with the card
surface and made the empty portion of the bar invisible. The new
foreground-tinted track reads cleanly on the card in both themes.
2026-05-24 20:48:01 -05:00
Chad Curtis 34c9ab7492 Frame composer, sidebar, and feed/comments as panels with brand-edged surfaces
- Wrap composer + comments / composer + feed in a rounded muted panel
  with continuous border-primary rails.
- Composer and campaign sidebar use a near-white surface with a
  brand-orange border; dark mode swaps to a near-black warm surface.
- Per-article borders and backgrounds retinted via scoped selectors
  so NoteCard stays untouched globally.
- Add hideCommentContext, authorBadge to NoteCard and
  renderAuthorBadge, leafCardClassName to ThreadedReplyList; move the
  Campaigner badge into the author row.
- Light-mode tokens: --card to 0 0% 98%, --muted to 30 12% 94% so
  cards and muted regions read as real surface steps.
- Drop the unused campaignsDetail.commentCount locale key everywhere.
2026-05-24 20:29:57 -05:00
Alex Gleason 4f59006fa4 ui/dialog: set min-width: 0 on DialogContent grid children
DialogContent is a CSS grid container, and grid items default to
`min-width: auto` — i.e. the intrinsic content width. Long
unbreakable strings (e.g. a combined BIP-21 `bitcoin:<bc1>?sp=<sp1>`
URI in the wallet receive dialog) refuse to shrink, blow past
`max-w-lg` / `w-[calc(100%-2rem)]`, and the `flex-1 min-w-0 truncate`
pattern on the inner span never engages because the parent already
accepted the full intrinsic width.

Adding `[&>*]:min-w-0` to the grid container fixes truncation for
every dialog without touching individual call sites.
2026-05-24 18:38:54 -05:00
Alex Gleason abd6cd95b1 wallet: simplify the seed-phrase backup dialog
Strip the redundant in-dialog header, the BIP-86/Taproot explainer, and
both the Reveal and Copy buttons from the seed-phrase backup UI. The
seed-phrase box itself is now the reveal affordance — tap to expose the
words, tap again to hide them. Copy-to-clipboard is gone entirely (users
write the words down rather than paste them).

- WalletBackupMnemonic: drop the inline "Wallet seed phrase" h2 + KeyRound
  icon (the DialogTitle already renders the same heading), drop the
  Nostr-key-derivation explainer paragraph, drop the two-button row at the
  bottom, drop the copied-state and useToast wiring. Wrap the seed-phrase
  box in a <button> with aria-pressed and aria-label so screen readers know
  it's a toggle.
- en.json:
  - Rewrite walletBackup.dialogDescription to the new short copy: "You can
    access your Agora wallet in any Bitcoin wallet by importing this 24-word
    backup."
  - Rewrite walletBackup.hidden to "Tap to display your 24-word seed
    phrase." (dropping the "Tap Reveal" wording, since there is no Reveal
    button anymore).
  - Add walletBackup.revealAria / hideAria for the toggle button's
    aria-label.
  - Remove walletBackup.explainer, .reveal, .hide, .copy, .copied,
    .copyFailedTitle, .copyFailedDescription.
- Mirror the same restructuring across all 14 other locales. Four locales
  (hi, id, tr, zh-Hant) didn't previously have a walletBackup block at all
  and were falling back to English — they now ship full translations.
2026-05-24 18:09:02 -05:00
Alex Gleason 1e3e985622 Merge branch 'main' of gitlab.com:soapbox-pub/agora 2026-05-24 16:40:07 -05:00
Chad Curtis e00a135eb0 Merge branch 'main' of gitlab.com:soapbox-pub/agora into feat/my-square
# Conflicts:
#	src/components/TopNav.tsx
#	src/components/auth/AccountSwitcher.tsx
2026-05-24 16:27:43 -05:00
Alex Gleason c7473f824b wallet: derive BIP-39 mnemonic from nsec (v2) so funds import into any BIP-39 wallet
The HD wallet seed is now BIP-39-compatible. Pipeline:

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

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

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

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

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

Other changes:
- Re-key SP storage NIP-78 d-tag to /v2 so v1 and v2 UTXOs do not mix
- Re-key the persisted receive-address cursor to :v2: namespace
- Relax SP spend-key helper to 16-64 byte seeds (BIP-32 range) so the
  migration sweep can sign with the legacy 32-byte v1 seed too
- Remove stale NIP-SP references from derivation comments (the draft
  was not relevant to our use case)
- Document the wallet derivation scheme in NIP.md
- Translate every new string to all 10 non-English locales
2026-05-24 15:39:22 -05:00
lemon 157e04fed1 fix: localize shared creation form copy 2026-05-24 00:40:27 -07:00
lemon 64ac109e6c fix: translate event detail page 2026-05-24 00:40:27 -07:00
lemon 81a765aef1 fix: translate detail page labels 2026-05-24 00:40:27 -07:00
lemon 78d680fa37 fix: localize feed mode switcher 2026-05-24 00:37:04 -07:00
lemon 2df55a731a fix: translate compose destination strings 2026-05-24 00:34:04 -07:00
lemon 00eb2128c3 fix: place card translate controls in footers 2026-05-24 00:30:29 -07:00
lemon 4d40099426 feat: translate preview cards 2026-05-24 00:30:29 -07:00
lemon 9392cc5061 feat: translate event detail cards 2026-05-24 00:30:29 -07:00
lemon 24ba779da6 fix: map Persian and Pashto translation targets 2026-05-24 00:30:29 -07:00
lemon 0efd72268e feat: translate campaign cards 2026-05-24 00:30:29 -07:00
lemon 3e7b7e824f fix: move note translation action 2026-05-24 00:30:29 -07:00
lemon 8d7d81dd3f feat: add note translation button 2026-05-24 00:30:29 -07:00
mkfain f66e6e80c1 i18n: route Donor / Activist guide strings through locales
Splits the guide content same way the FAQ was split:

  * Structure (block order, kinds, audience, callout variant, option
    grid chips/hrefs) stays in helpContent.ts as a typed array of
    GuideBlockStructure descriptors with stable IDs.
  * Every user-visible string moves to en.json under guides.donor.*,
    guides.activist.*, and guides.shared.* (badge labels, tldr eyebrow,
    payment comparison table headers + rows).

PaymentComparisonTable, InlinePaymentBadge, and GuideTLDR call
useTranslation() so a language switch triggers re-render. DonorGuidePage
and ActivistGuidePage already do, so getDonorGuideBlocks /
getActivistGuideBlocks re-run on every render and pick up fresh i18n
values automatically.

This commit lands the en strings only; non-en locales follow in the
next commit.
2026-05-23 23:07:49 -05:00
Alex Gleason ca74e87ea3 Merge branch 'main' of gitlab.com:soapbox-pub/agora
# Conflicts:
#	src/lib/helpContent.ts
2026-05-23 21:11:19 -05:00
Alex Gleason 6f187d580f Merge branch 'main' of gitlab.com:soapbox-pub/agora
# Conflicts:
#	src/components/music/MusicTrackRow.tsx
#	src/pages/ActionDetailPage.tsx
2026-05-23 21:09:16 -05:00
mkfain e63a08c2e2 i18n: route FAQ strings through the locale namespace
Splits the FAQ definition in `helpContent.ts` into two layers:

  * a structural template (category order, item IDs, hidden flag) that
    stays in TS, and
  * a flat `faq.*` namespace in `locales/*.json` that holds every
    user-visible string (category labels, questions, answer paragraph
    arrays).

`getFAQCategories` / `getFAQItems` / `getFAQItem` keep the same
`(appName)` signature and `FAQCategory` / `FAQItem` shape — internally
they resolve strings through `i18n.t()`, with `{appName}` literals
rewritten to `{{appName}}` for i18next interpolation. Answer arrays use
`returnObjects: true` so a missing locale entry falls back to English
without breaking the renderer.

`HelpFAQSection` reads `i18n.language` to re-resolve on language switch;
`HelpTip` calls `useTranslation()` for the same reason. The Donor /
Activist guide blocks below are still keyed off the original
single-brace `{appName}` literal — that's a separate i18n pass.
2026-05-23 21:05:46 -05:00
Alex Gleason 29fd0c9a0f Remove unused exports and dead code
Aggressive cleanup of 359 exports across 153 files identified as
having zero importers outside their declaring module:

- 105 symbols deleted entirely (no internal uses either)
- 254 symbols un-exported (still referenced file-locally; dropped the
  `export` keyword to shrink the public surface)
- ~70 cascade cleanups of locals that became dead once their sole
  consumer was removed

Notable shrinkage:
- src/hooks/useShakespeare.ts: 626 \u2192 22 lines (unwired AI chat surface;
  only the ChatMessage type is consumed)
- src/hooks/useTrending.ts: only useEventStats survives; trending feed
  hooks were never wired up
- src/hooks/useTrustedCountryStats.ts: dead type re-exports removed
- src/lib/bitcoin.ts: PSBT helpers \u2014 unused wallet feature scaffolding
- src/lib/communityUtils.ts: unused NIP-72 moderation helpers
- src/lib/extraKinds.ts, src/lib/colorUtils.ts: unused helpers
- src/lib/logger.ts: bare debug/info/warn/error exports dropped;
  consumers use the `logger` object
- src/lib/aiChatSystemPrompt.ts: trimmed to the
  DEFAULT_SYSTEM_PROMPT_TEMPLATE constant
- src/components/music/MusicTrackRow.tsx: dead row component removed;
  only the skeleton is consumed

src/hooks/useNostr.ts (intentional decoy) and src/i18n.ts
(side-effect import) were preserved per their respective contracts.
2026-05-23 20:56:43 -05:00
mkfain 06432d2155 i18n: translate PrivacyPolicyPage via markdown loader
Extracts the ~110-line JSX prose body into per-language markdown files
and renders it through a new `usePolicyMarkdown` hook + `PolicyMarkdown`
component. Eight locales ship beside `en.md`; missing locales fall back
to English at runtime.

The loader uses dynamic `import()` of `*.md?raw` (Vite + Bun friendly,
no `import.meta.glob`) keyed on a static `{ slug: { lng: loader } }`
table so bundlers can code-split per locale and we keep one shared chunk
for the markdown ecosystem. `{{appName}}` placeholders are interpolated
at render time with backslash-escape of markdown specials, and the
rendered output goes through rehype-sanitize.

This is the reusable loader infrastructure for the long-form policy
pages — CSAEPolicyPage is the next consumer.
2026-05-23 20:41:50 -05:00
Alex Gleason 5500ccc188 Delete orphan source files
Four files with zero importers across the repo:

- src/components/TeamSoapboxCard.tsx
- src/components/discovery/ProfileCard.tsx (superseded by
  src/components/ProfileCard.tsx)
- src/components/letter/StickerPicker.tsx (superseded by
  src/components/StickerPicker.tsx)
- src/hooks/useDominantColor.ts

src/hooks/useNostr.ts is also orphan but intentional \u2014 it exists as
a re-export decoy and is kept.
2026-05-23 20:29:26 -05:00
mkfain 2a89242d25 i18n: translate NoteCard + NoteMoreMenu
Adds two large namespaces:

noteCard.* — the action labels surfaced under reaction / repost / poll
vote actor rows (reacted / reposted / voted), the Read more / Show less
truncation toggle, the live-stream status badges (LIVE / ENDED /
PLANNED / UNKNOWN), the Bot account avatar title, the donation prefix
on synthetic zap cards (Donated X to / Donated to), and the entire
KIND_HEADER_MAP — every 'X did a Y' header for the 24 kinds Agora
renders an action header for (photos, encrypted messages, letters,
treasures + finds, decks, emoji packs, groups, campaigns, badges,
streams, Zapstore app/release/asset, generic apps, git repos / patches
/ PRs, NIPs, nsites, zaps, pledges, follow packs, follow sets).

The KIND_HEADER_MAP refactor pulls the same trick as profileSettings
presets: each entry stores i18n keys instead of English strings, and
EventActionHeader runs them through t() at render time. publishedAtKey
is just publishedAtAction returning keys instead of phrases. The map
itself stays a module-level constant.

noteMoreMenu.* — the entire 'more' overflow menu hanging off every
NoteCard: every menu item (View post details, View Event JSON,
Bookmark, Add to list, Add/Remove from sidebar, Pin/Unpin to profile,
Pin/Unpin to country feed, Mute Conversation / mute @user / Report /
Remove from group / Delete post), the Encrypted content fallback in
the post preview, every toast (success + failure for each mutation),
the Delete confirmation alert dialog, and the Event JSON dialog (title,
Event ID label, Raw JSON label, Broadcast Event button + Broadcasting
state, plus the {{label}} copied toast).

ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn,
ps, fa still pending native-speaker review.
2026-05-23 19:30:37 -05:00
mkfain 850f35c29d i18n: translate ComposeBox + ReplyComposeModal
Adds the compose.* namespace covering ComposeBox: placeholders
(default, poll, country-scoped, content warning), preview / edit toggle,
poll mode (Add option, Option N, Single/Multiple choice, Back to post,
Publish poll, success/failure toasts), voice recording (Cancel, Send,
Sending, mic-denied toast, voice-message tooltip), toolbar
(Attach file, Emoji/GIF, More, Poll, Spoiler, Emoji, Stickers tabs),
submit (Posting / Posted toasts, upload-failed / publish-failed),
and destination dropdown (Post to label, Global / community
explainer popover, Choose another country picker, search placeholder,
empty state, Set as default, default-updated toasts).

Also adds replyModal.* for ReplyComposeModal — the six title fallbacks
(New poll, New comment, Comment on profile, Reply to post, Quote post,
New post), three placeholder fallbacks, and the Bluesky disclaimer.

ComposeBox's hardcoded default placeholder ('What's on your mind?') and
submitLabel ('Post!') props moved from prop defaults to t() fallbacks
inside the component so existing call sites can keep passing translated
strings unchanged. Renamed an inner .map((t) => …) variable in the poll
type/duration row to .map((pt) => …) since t shadowed the i18next t.
Added rtl:rotate-180 to the 'Back to post' chevron since it visually
points left/back.

ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn,
ps, fa still pending native-speaker review.
2026-05-23 19:16:53 -05:00
mkfain c74dbb6a3b i18n: translate Feed, FeedModeSwitcher, FeedEmptyState, ReplyContext, PostActionBar, ReactionButton
Extends the feed.* namespace with five new sub-namespaces:
compose.placeholder, tabs (Follows / Following / Global), empty (the
six per-tab + per-mode empty-state messages plus the three CTA labels
that show under them), modeSwitcher (the home feed's mode-picker dropdown
with Agora / All Nostr / Following options, the dropdown trigger's aria
label, and the disabled-Following tooltip), replyContext (Replying to,
the and-joiner, plus a pluralized andOthers_one/andOthers_other), and
actions (Reply / Repost / Undo repost / React / Zap / Share / More + the
Link-copied toast).

PostActionBar's replyLabel prop default went from 'Reply' to undefined;
the component falls back to t('feed.actions.reply') at render time so
existing call sites that pass an already-translated string keep working
unchanged. FeedModeSwitcher's OPTIONS array swapped its hard-coded
label string for an i18nKey suffix, materialized through t() inside the
component.

ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn,
ps, fa still pending native-speaker review.
2026-05-23 19:05:18 -05:00
mkfain b1e6c89ac5 i18n: translate ProfilePage and profile/* components
Adds the profile.* namespace covering ProfilePage (the more menu, follow
toast, image lightbox, follow/follower modals, Bitcoin QR modal, inline
profile fields, NIP-05 not-found states) and every sub-component the page
mounts: ProfileIdentityRail (action bar, stats, campaigns/pledges/groups
sections, RailPledgeCard, RailOrgCell), ProfileActivityTab,
ProfileCampaignsTab, ProfilePledgesTab, and OrganizationsAllDialog.

The desktop and mobile tab arrays (DESKTOP_TAB_LABELS / MOBILE_TAB_LABELS)
moved from English label strings to label keys so the tab maps are
translated through t('profile.tabs.*') at render time. CORE_TAB_IDS now
keys off the same identifier slugs to keep the click-through routing
intact.

Inner sub-components (RailCampaignsSection, RailPledgeCard, RailOrgCell,
StatList, ActionBar, ProfilePledgeCard, ProfileFieldInline,
BitcoinQRModal, FollowingListModal, FollowersListModal,
ProfileImageLightbox, ProfileMoreMenu) each pick up their own
useTranslation() per the established pattern — no t prop drilling.

ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn, ps,
fa still pending native-speaker review.
2026-05-23 18:48:31 -05:00
mkfain e8cbdd2031 i18n: translate HDSilentPaymentScanDialog
Adds a top-level spScan.* namespace covering the BIP-352 silent-
payment scan dialog: header (title + description), the from/to block
inputs (label + 'tip' placeholder), the indexer-tip / last-scanned
footer line (with 'never' fallback), the include-already-spent
toggle + its long explanation, the in-progress block-counter and
matches_one/_other plural, the post-scan summary
(scannedRange + foundOutputs_one/_other / noNewPayments), and the
'Reconcile spent UTXOs' subsection (title, long description,
checking interpolation, checked_one/_other plural with prune count,
Reconciling… / Reconcile now button). Action buttons reuse
common.cancel + common.close.

All 7 non-English locales drafted inline. Native-speaker review
still pending for km, sn, ps, fa.
2026-05-23 17:56:51 -05:00
mkfain bd01b9273e i18n: translate HDSendBitcoinDialog + BitcoinPublicDisclaimer
Two top-level namespaces:

walletSend.* — the Send Bitcoin dialog at /wallet: dialog title,
amount approx-sats interpolation, recipient label/placeholder, the
three recipient descriptions (silent payment / Nostr / raw address),
fee speed labels (10 min / 30 min / 1 hour / 1 day) and the
{{rate}} sat/vB display, network fee row, available-balance footer,
the four progress strings (building / signing / broadcasting /
sending), the 10-error error catalogue thrown into setError() +
the mutation onError toast title, two-tap arming ('Tap again to
confirm'), and the SuccessScreen (title, sats fallback, view
transaction, done).

bitcoinPublic.* — the BitcoinPublicDisclaimer shared component
used by both the wallet's Send dialog and the campaign DonateDialog.
Lead sentence, 'Learn more' link, the two long body variants
(with/without cash-out advice), and the 'I understand this
transaction is public.' acknowledgement label. The leadText prop
override is preserved; only the default is now translated.

The FEE_SPEED_LABELS module constant was replaced by an in-component
useMemo so the labels stay reactive to language switches.

All 7 non-English locales drafted inline. Native-speaker review
still pending for km, sn, ps, fa.
2026-05-23 17:52:46 -05:00
mkfain 45ecc3cc8c i18n: translate WalletSettings (NWC) component
Adds a top-level walletConnect.* namespace covering the NWC settings
component used by the Advanced settings page: status section (WebLN
+ NWC cards with Ready / Not Found / None badges), the Add button,
empty state, per-connection rows (active badge, defaultWalletName
fallback, set-active and remove icon-button titles), the help text
shown when no signing method is available, and the Connect-NWC
dialog (title, description, alias placeholder, Connect /
Connecting... button).

Toast titles+descriptions for URI-required validation and active-
wallet-changed feedback also go through t(). The connectedCount_one
/ _other plural lets each locale handle pluralisation natively (zh
collapses to one form, ar has separate one/other, etc.).

All 7 non-English locales drafted inline. Native-speaker review
still pending for km, sn, ps, fa.
2026-05-23 17:45:02 -05:00
mkfain 60dda02a15 Merge branch 'main' of gitlab.com:soapbox-pub/agora
Conflicts in NetworkSettingsPage.tsx and SettingsPage.tsx, both
introduced by upstream's Low-Bandwidth Mode + image proxy feature
landing on top of my i18n refactor of the same settings pages.

Resolution:
- Translated the new Low-Bandwidth Mode and Image Proxy block under
  settings.network.{lowBandwidthHeading,reduceDataUsage,
  reduceDataUsageDesc,useImageProxy,useImageProxyDesc,proxyUrl,
  proxyApiDesc,reset}, using <Trans> for the wsrv.nl link in
  proxyApiDesc (<0>...</0> component slot).
- Updated settings.network.subtitle and settings.sections.networkDesc
  in en.json to match the new upstream English ("Manage data usage,
  relays, and file upload servers." / "Data usage, relays, and file
  upload servers.").
- Mirrored the new keys + updated values into all 7 non-English
  locales. Native-speaker review still pending for km, sn, ps, fa.
2026-05-23 17:23:35 -05:00
Chad Curtis 6f9257621a Low-bandwidth mode: gate all images on tap, independent of image proxy
Previously the tap-to-load placeholder only kicked in when the proxy was
also disabled. The proxy still saves bandwidth, but users who flipped on
low-bandwidth mode generally want the explicit consent step on every
image — proxied or not. Make the two settings independent:

- ImageGallery / ProxiedImage: gate on lowBandwidthMode alone.
- LinkPreview: suppress thumbnail whenever lowBandwidthMode is on.
- Settings copy + AppConfig JSDoc updated to match.
2026-05-23 16:48:49 -05:00
mkfain e21a958fdb i18n: translate CreateActionPage + FormSection
Extract user-facing strings on the create-pledge form into the
pledges.create.* namespace and add per-form-section labels via a new
forms.* namespace shared with FormSection (Required/Recommended/
Optional badge).

Translates the login gate, field labels, placeholders, the
mutation's user-facing error messages, success / failure toasts, the
country search box, and the country-hint footnote (with a <0> wrapper
around the iso3166 monospace span). The back-arrow flips with
rtl:rotate-180 for Arabic/Farsi/Pashto.

Native-speaker review still pending for: km, sn, ps, fa.
2026-05-23 16:33:35 -05:00
Chad Curtis 0d3f44935c Add image proxy and low-bandwidth mode
Two new AppConfig fields cover the data-saving story:

- `imageProxy` (default `https://wsrv.nl`) — wsrv.nl/weserv-compatible
  image-resizing proxy. Empty string disables it. `proxyImageUrl(src, width)`
  and `useImageProxy()` rewrite URLs to WebP at quality 75; the `default=`
  param redirects to the origin if the proxy can't fetch upstream. The
  proxy base URL is parsed through `URL` and rejected unless it's
  `https:`, so user input from the settings field can't smuggle non-https
  schemes into <img src>.
- `lowBandwidthMode` (default `false`) — forces autoplay off everywhere
  (VideoPlayer, LiveStreamPlayer), skips `useVideoThumbnail`'s background
  frame-grab, and (when the proxy is also disabled) gates feed images and
  link-preview thumbnails behind a tap-to-load placeholder.

The two settings are independent. A privacy-conscious user can run with
the proxy off without being forced into tap-to-load, and a metered-data
user can keep the proxy on without ever seeing a placeholder. Tap-to-load
only kicks in when both "I'm low-bandwidth" and "the proxy is off"
are true (or the proxy errors in a gated context).

Three new shared pieces:
  - `src/lib/proxyImageUrl.ts` — pure URL rewriter
  - `src/hooks/useImageProxy.ts` — memoized `(src, width) => string`
  - `src/components/MediaPlaceholder.tsx` — tap-to-load pill with
    optional blurhash background; rendered as `<div role="button">`
    so it nests cleanly inside InlineImage's outer lightbox button
  - `src/components/ProxiedImage.tsx` — `<img>` with proxy + onError
    fallback + optional placeholder gating

Wired call sites with per-context widths:

  Inline post images       (InlineImage,        w=600, gated)
  Image gallery tiles      (GridImage,          w=600, gated)
  Lightbox                 (LightboxImage,      w=1200)
  Profile banner           (ProfileBannerImage, w=1200)
  Profile hover banner     (ProfileHoverCard,   w=400)
  Link preview thumb       (LinkPreview,        w=400, suppressed when
                                                low-bandwidth + no proxy)
  Sidebar media tile       (ProfileRightSidebar, w=300)
  Avatars (via shadcn)     (AvatarImage,        w=96 default,
                                                w=128 hover card,
                                                w=256 profile header)
  Custom emojis            (CustomEmojiImg,     w=48)
  Badge thumbnails         (BadgeThumbnail,     w=max(size*2, 128))
  Badge hero + glare mask  (BadgeContent,       w=256;
                            BadgeDetailContent, w=320;
                            EmbeddedNaddr,      w=192)
  Music / podcast art      (AudioKindContent,   w=600;
                            MusicTrackRow,      w=96;
                            MusicDetailContent, w=320 hero / 96 row;
                            PodcastDetailContent, w=320)

Settings UI lives in NetworkSettingsPage with the Low-Bandwidth Mode
toggle at the top (above relay / Blossom plumbing) and the Image Proxy
section between Blossom Servers and Image Uploads. The low-bandwidth
description text adapts to whether the proxy is configured, so the
proxy↔tap-to-load coupling is visible at the toggle.

Video Tier 1:
  - VideoPlayer overrides `autoPlay` to false in low-bandwidth
  - LiveStreamPlayer no longer hardcodes `autoPlay`, reads config
  - useVideoThumbnail skips its background frame-grab entirely

`lowBandwidthMode` is added to EncryptedSettingsSchema so it crosses
devices alongside `autoplayVideos`. `imageProxy` is local-only (privacy
/ network choice, not a UX preference).
2026-05-23 16:29:38 -05:00
mkfain def426b0e8 i18n: translate LoginArea + AccountSwitcher
Add auth.* namespace (join, login, signup, logout, addAccount). Wire
useTranslation into:

- LoginArea: "Join" button label
- AccountSwitcher: account menu items (Dashboard, Wallet, Notifications,
  Profile, Search, Settings, About reuse nav.* keys; logout and
  addAccount use the new auth.* keys)

AccountSwitcher header NOTE preserved.
2026-05-23 15:43:52 -05:00
mkfain 5244cc21b2 i18n: translate TopNav + SiteFooter
Wire useTranslation into TopNav (nav items, mobile drawer, profile menu,
search button aria-label, brand home aria-label, open/close menu labels,
mobile footer links) and SiteFooter (tagline + footer nav links).

Add nav.* namespace to en.json + all 7 locale files. The mobile drawer
and desktop nav now switch language live the moment a user picks a new
language in /settings/language.
2026-05-23 15:41:27 -05:00
Alex Gleason 4ef932bad1 Merge branch 'main' of gitlab.com:soapbox-pub/agora 2026-05-23 15:06:12 -05:00
filemon d0590a204b Merge branch 'main' into feat/my-square 2026-05-23 16:45:05 -03:00
Alex Gleason 7c50fa9a90 remove user-customizable theme system
Agora's colors and fonts are now hardcoded in the bundle. There is no
runtime CSS-variable injection, no remote-loaded theme, no font-family
override from event data, no background image, no recolored favicon.
Switching themes only toggles the .dark class on <html>.

Removed:

- src/themes.ts: ThemeConfig, ThemesConfig, CoreThemeColors, ThemeFont,
  ThemeBackground, themePresets (22 presets), buildThemeCssFromCore,
  deriveTokensFromCore, the 'custom' Theme variant. Now exports only
  resolveTheme(theme) -> 'light' | 'dark'.
- src/lib/fontLoader.ts: deleted. Mounted arbitrary @font-face rules with
  event-sourced URLs and font-family overrides — the primary CSS-injection
  vector. sanitizeCssString moved to src/lib/cssSanitize.ts (still used by
  the Letter feature).
- AppProvider hooks useApplyFonts / useApplyBackground / useApplyFavicon.
- useTheme.applyCustomTheme — the entrypoint that wrote external palettes
  to global theme state.
- ColorMomentEyeButton + its callers in NoteCard and PostDetailPage. Color
  Moments (kind 3367) still render as palette art, but the 'Set as theme'
  button is gone.
- LetterAttachment in LetterDetailSheet — the gift-box UI that applied an
  attached color moment as the recipient's theme.
- paletteToTheme() from colorMomentUtils — only getColors() remains.
- customTheme / themes fields from AppConfig, AppConfigSchema,
  EncryptedSettings, EncryptedSettingsSchema. Existing customTheme values
  in localStorage and encrypted NIP-78 settings are now ignored.
- 14 unused @fontsource packages. Only the 10 letter fonts and the 2 base
  UI fonts (Inter Variable, Bebas Neue) remain. noto-sans-nushu is kept
  for the encrypted-letter obfuscation indicator.

Added:

- Static :root {} and .dark {} blocks in src/index.css with the 19 shadcn
  tokens hardcoded. These were previously injected at runtime by
  AppProvider; without them the app would lose all colors when the runtime
  injector was removed.
- src/lib/cssSanitize.ts holding the sanitizeCssString helper for the
  Letter feature's font-family interpolation.

Simplified:

- public/theme.js: no longer reads customTheme or themes from localStorage,
  just resolves system/light/dark with hardcoded built-ins. Must stay in
  sync with src/index.css colors.
- src/lib/fonts.ts: trimmed to only the 10 letter fonts. findBundledFont
  and resolveCssFamily removed (they were only used by fontLoader).
2026-05-23 14:31:04 -05:00
filemon d192d5ac19 Rename My Square to My Dashboard and add navigation links
- Rename MySquarePage -> MyDashboardPage (file, component, default export)
- Change route from /my-square to /my-dashboard
- Update user-facing copy: SEO title, logged-out heading, JSDoc
- Add 'My Dashboard' link to AccountSwitcher dropdown
- Add 'My Dashboard' entry to TopNav mobile/profile menu
- Use LayoutDashboard icon to distinguish from the existing Dashboard
- Existing /dashboard route and EventDashboardPage are untouched
2026-05-23 16:25:15 -03:00
mkfain b05ded032a About: deeper, alternating dark-mode section backgrounds
Replaces the previous near-uniform dark surfaces (#0e1218 / #11151c)
with genuinely dark, alternating tones plus a subtle world-map
texture on two sections, so the page reads as an editorial document
in dark mode the same way it does in light mode.

Section background mapping (dark mode)

  Section                  Was         Now
  -----------------------  ----------  ------------
  Hero                     #0a0c14     #0a0c14      (unchanged)
  Three steps              #11151c     #0a0c14 + texture
  Two ways to get paid     #0e1218     #13181f + texture
  Frequently asked         #11151c     #0a0c14
  Pick the side            #0e1218     #13181f

The two alternating tones (#0a0c14 / #13181f) preserve the
cream/white rhythm from light mode. Sections 2 and 3 gain a
dark-only world-map background image at 5-6% opacity (the same
texture used in the hero) so they don't read as flat slabs. The
texture is gated behind 'hidden dark:block' so it has zero impact
on the light-mode rendering.

Card surface lifted

bg-[#1a1f29] → bg-[#1c2230] across StepCard, RailCard, GuideCard,
the FAQ accordion card-row (reference mode), the FAQ card variant,
and the FAQ tab pill. Hover-state for the tab pill follows from
#222937 to #252b3a. Net effect: cards now sit ~12 lightness above
the deepest section background and ~6 above the lifted one, keeping
clear elevation against both tones.
2026-05-23 14:02:27 -05:00
mkfain 76597ae774 About + HelpFAQSection: full dark-mode support
Every section of the About page now renders correctly in dark mode.
The hero stays dark in both modes (its identity is anchored on the
dark navy backdrop), and the four light sections (Three steps, Two
ways to get paid, Frequently asked, Pick the side) each gain a
dark-mode counterpart so the page reads consistently inside the
app's theme.

Section background mapping

  Section                  Light bg     Dark bg
  -----------------------  -----------  -----------
  Hero                     #0a0c14      #0a0c14   (unchanged)
  Three steps              #faf8f4      #11151c
  Two ways to get paid     white        #0e1218
  Frequently asked         #f5f1eb      #11151c
  Pick the side            white        #0e1218

Card surfaces (StepCard, RailCard, GuideCard, FAQ accordion item)
map bg-white -> dark:bg-[#1a1f29] and border-gray-200 ->
dark:border-white/10. Hover shadows pick up a darker variant
(dark:hover:shadow-[0_8px_24px_rgba(0,0,0,0.4)]) on cards that have
a hover-lift effect.

Text-color mapping (applied throughout)

  text-gray-900 -> dark:text-white               (headings)
  text-gray-800 -> dark:text-gray-100            (inline strongs)
  text-gray-700 -> dark:text-gray-300            (bullet body)
  text-gray-600 -> dark:text-gray-300            (body prose)
  text-amber-600 / 700 -> dark:text-amber-400    (trade-off label
                                                   + icon)
  border-gray-100 -> dark:border-white/10        (separators)

No-Custody comparison banner

The light gradient card (white -> primary/5) converts to a glass
tile in dark mode (white/0.04 -> primary/0.08) with primary/30
border. The inner Comparison grid's separator border picks up
dark:border-white/10. ComparisonItem's light theme path now also
sets dark:text-gray-300 on its body copy so it reads on the dark
glass tile.

HelpFAQSection

Both the 'reference' card-row accordion (used by the About page)
and the 'cards' masonry variant gain dark-mode card surfaces and
text-color mapping. The category tab pills get a dark-mode
inactive state. The active pill (bg-primary) is already correct in
both modes.
2026-05-23 13:53:23 -05:00
Alex Gleason a28a86d723 Merge branch 'main' of gitlab.com:soapbox-pub/agora 2026-05-23 13:45:54 -05:00
Alex Gleason 076e9d2c37 remove Twitter, Spotify, and Reddit iframe embeds
These three embed components rendered third-party content in
iframes (Twitter's platform.twitter.com widget, Spotify's
embed-iframe, Reddit's embed.reddit.com widget). Removed wholesale:

- SpotifyEmbed, TweetEmbed, RedditEmbed components.
- extractTweetId, extractSpotifyEmbed, extractRedditPost, and the
  SpotifyEmbedInfo type from lib/linkEmbed.ts.
- The Twitter/Spotify/Reddit branches in LinkEmbed and
  isEmbeddableUrl/embedLabel.

Tweet, Spotify, and Reddit URLs now fall through to the generic
LinkPreview card. useLinkPreview keeps its native Spotify/Reddit
oEmbed shortcuts so those previews still render with rich title +
thumbnail metadata instead of going through the link-preview proxy.
2026-05-23 13:45:23 -05:00
Alex Gleason 55d873548d remove iframe.diy sandbox, nsite preview, and webxdc runtime
The iframe.diy-based sandbox infrastructure powered two features:
the nsite preview dialog (Run button on NsiteCard and AppHandlerContent)
and the webxdc embed (cartridge launcher + sandboxed iframe runtime
with kind 4932/20932 sync events).

Both are removed wholesale:

- iframe sandbox plumbing: SandboxFrame, src/lib/sandbox/*,
  iframeSubdomain, previewInjectedScript.
- nsite preview: NsitePreviewDialog, NsitePermissionManager,
  NsitePermissionPrompt, useNsiteSignerRpc, nsitePermissions,
  nsiteNostrProvider, NsitePlayerContext.
- webxdc runtime: WebxdcEmbed, Webxdc, GameControls, useWebxdc,
  webxdcMeta, public/cartridge.png, NOSTR_WEBXDC.md,
  @webxdc/types dependency, kindLabels/signerWithNudge entries
  for kinds 4932/20932.
- AppConfig: drop sandboxDomain, showWebxdc, feedIncludeWebxdc.
- extraKinds: drop kind 1063 webxdc entry; sidebar drops 'webxdc'.
- ComposeBox: .xdc uploads now flow through the generic file path
  (no UUID injection, no manifest extraction, .xdc removed from
  the file picker accept list).
- NoteContent / FileMetadataContent: webxdc branches removed; .xdc
  attachments fall through to the generic file card.
- LayoutContext / CenterColumnContext: only consumed by the removed
  fullscreen preview panels — deleted along with its provider in
  AppRouter.

NsiteCard keeps its rich link-preview card but loses the Run button
and preview dialog. AppHandlerContent keeps a kind 35128 `a`-tag
reference but replaces 'Run' with an external 'Open Site' link to
`<pubkeyB36><dTag>.nsite.lol`. The standard HTML iframe `sandbox`
attribute used by SpotifyEmbed/TweetEmbed/RedditEmbed is unrelated
to iframe.diy and stays.
2026-05-23 13:38:28 -05:00
mkfain 4d13c79633 Rename Help to About, redesign as landing-style page, redesign Donor + Activist guides
Squashed re-application of 15 local commits onto the new route-level
layout system (refactor: replace useLayoutOptions store with route-level
layout choice). Drops the obsolete useLayoutOptions({ fullBleed: true })
calls; the About page and the two guide pages instead live under the
wide FundraiserLayout route group in AppRouter.

Routing

- /about, /about/donors, /about/activists are now the canonical paths,
  in the wide layout (no max-width cap so sections can span the
  viewport with their own backgrounds).
- /help, /help/donors, /help/activists become <Navigate> redirects so
  existing bookmarks and links keep working.

About page (new src/pages/AboutPage.tsx)

A landing-style document modeled on https://soapbox.pub/agora,
brought in-app to explain how the platform works. Five sections:

  1. Hero (dark navy + world-map texture + orange halos), Bebas Neue
     italic headline with an inline orange highlighter behind the
     brand name, three trust chips, and Donor / Activist Guide CTAs.
     Tilted Venezuelan sample-campaign card on lg+, hidden on mobile.
     On mobile the H1 fits on one line via text-4xl + a conditional
     <br className="hidden sm:inline" />.
  2. Three steps. No middleman. (cream) 3-up white cards with 4:3
     step images and corner 01 / 02 / 03 numerals.
  3. Two ways to get paid. (white) Bitcoin Public Payments vs.
     Bitcoin Silent Payments compare cards with gradient header
     strips. Public-Payments trade-off carries the above-ground-
     activism warning; Silent-Payments trade-off is five bold-headline
     bullets (few wallets, slow, no push notifications, buggy,
     no public counts). Below: a primary-tinted No-Custody banner
     plus a 3-column comparison grid (Unlike GoFundMe / Unlike
     GiveSendGo / Unlike other 'Bitcoin' platforms).
  4. Frequently asked. (cream) Three integrated FAQ chapters in
     page flow (Getting started / Bitcoin donations / About Nostr),
     each with a Bebas Neue numeral + Inter Bold heading + card-row
     accordion items with a left orange accent on hover/open.
  5. Pick the side you're on. (white) Two large image-led guide
     cards (Donor / Activist) using the soapbox.pub photography.
     Closes with a quiet 'Still stuck? Follow Team Soapbox' line
     linking in-app to the pack via the /:nip19 route.

Typography is Bebas Neue (font-display) italic font-normal with
WebkitTextStroke for the hero H1 and the step numerals only; every
other heading uses Inter Bold (font-sans font-bold tracking-tight).
Bebas Neue is never font-bold (synthetic bold renders as smear at
display sizes).

Em dashes have been removed throughout the page and the
HelpFAQSection component. Box-drawing chars (U+2500) in section
banner comments are not em dashes and stay.

Section backgrounds alternate dark → cream → white → cream → white.
The dark and cream sections keep their literal palette in both light
and dark mode (an editorial choice that gives the page its
landing-page identity rather than being just another themed surface).

Donor + Activist Guides (new block-based design)

Both pages now compose from a typed sequence of GuideBlock variants
defined in helpContent.ts. Each block kind is rendered by a dedicated
component under src/components/guide/:

- GuideTLDR              top-of-page summary card with lede + checklist
- GuideSteps             numbered vertical flow of short steps
- PaymentComparisonTable Public vs. Silent side-by-side. Three-column
                         grid on desktop, two stacked tinted cards on
                         mobile. Row content driven by audience flag.
- CalloutCard            tinted info / warning / danger / success blocks
- OptionGrid             two-column tile grid for privacy and cash-out
                         options
- GuideProse             plain prose escape hatch
- InlinePaymentBadge     small pill that distinguishes the two payment
                         options
- index.ts               barrel

Content is rewritten throughout to reflect current reality: campaigns
can accept Public only, Silent only, or both; the QR code carries
both endpoints when both are accepted; wallets without silent-
payments support fall back to a regular Bitcoin transaction; silent
payments are slow, scan-based, lack push notifications, are
bleeding-edge, and produce no public donation counts.

Activist Guide structure:

  TLDR
  How receiving works
  What everyone can see           (intentionally before the table)
  Public vs. Silent comparison
  A note on silent payments today (calm prose, not an alarm callout)
  Move donations promptly
  Cashing out privately           (silent-payments hop, Lightning
                                   swap, coinjoin, P2P with brokers,
                                   spend it directly)
  Avoid centralized tumblers

Donor Guide structure:

  TLDR
  How a donation flows
  Public vs. Silent comparison
  Public donations are visible on-chain forever callout
  Donating privately option grid
  Consumer apps can't make you anonymous callout
  A note on silent payments today

Other touchpoints

- Sidebar (sidebarItems.tsx): Help label → About, icon LifeBuoy → Info,
  path /help → /about.
- Top nav profile menu (TopNav.tsx): Help → About.
- Site footer (AppRouter.tsx inline): Help → About.
- AccountSwitcher dropdown: Help → About.
- LandingHero FAQ button → /about#faq.
- HelpTip popover footer link → /about#faq.
- GuideHero back link → /about, label 'Back to About', wider
  max-w-5xl on lg+ container so it sits well on the now-full-bleed
  hero. Inner overlay min-height bumped to 320px on lg+.
- CampaignsPage 'How it works' button → /about.

New assets in /public/about/ pulled from soapbox.pub:

- world-map-bg.png (hero + textures)
- venezuela-libertad-presos-politicos.png (hero sample-card image)
- donor-guide-freedom-libertad.jpeg
- activist-guide-unity.png

Step photos in /public/help/ (step-1-account.jpg, step-2-send.jpg,
step-3-spend.jpg) for the Three Steps section.

HelpFAQSection gains:

- variant: 'list' | 'cards' (default 'list')
- tabs: boolean (only meaningful with variant='cards')
- listTone: 'default' | 'reference' (quieter category labels and more
  breathable accordion items for the About page; existing inline
  callers keep the default pill style)

In 'reference' mode each accordion item gets a card-row treatment
(rounded white card, subtle border, hover lifts to primary/40 border,
left orange accent rule driven off data-state=open).

helpContent.ts FAQ content (FAQItem / FAQCategory and templates) is
left untouched. Only the donor/activist guide section was rewritten
into GuideBlock[] arrays.
2026-05-23 13:38:13 -05:00