Commit Graph

4447 Commits

Author SHA1 Message Date
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 15b4549714 i18n: translate CSAEPolicyPage via markdown loader
Extracts the ~285-line JSX prose body into per-language markdown files
served through the existing `usePolicyMarkdown` hook + `PolicyMarkdown`
component shipped with the privacy page.

Ships en/es/zh/ar/fa/ps. Khmer and Shona are left unregistered for now
and fall back to English at runtime via the loader's en fallback path —
the policy text is dense and translators will want a careful pass.

No new infrastructure; reuses the loader + renderer end-to-end.
2026-05-23 20:55:15 -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
Alex Gleason 407cc72a6e Remove deprecated createGeoIdentifier function
Annotated for removal after migration period with zero in-tree callers.
Consumers should use createCountryIdentifier() instead.
2026-05-23 20:29:08 -05:00
Alex Gleason 64546f5a7f Remove unused npm dependencies
Drop 13 packages that have zero imports across src/, configs, and
native projects:

- @radix-ui/react-{menubar,navigation-menu,aspect-ratio,context-menu}
  (no shadcn primitive consumes them)
- react-leaflet, leaflet, @types/leaflet (no map usage)
- smol-toml, fflate, html-to-image, input-otp
- react-resizable-panels, react-day-picker

Also drops the orphaned .leaflet-* CSS overrides in src/index.css.
2026-05-23 20:28:52 -05:00
Alex Gleason 7483c7e302 Remove orphan challenge-covers assets
The 11 cover images in /public/challenge-covers/ were superseded by
Blossom-hosted URLs in DEFAULT_ACTION_COVERS. No code referenced the
local files; the comment claiming otherwise was stale.

Reclaims ~4.4 MB from the bundle.
2026-05-23 20:28:16 -05:00
mkfain 8e64f6a8ac i18n: translate AboutPage
Adds the about.* namespace covering the entire /about landing page:

- hero — eyebrow ('About {appName}'), the 3-part headline with the
  highlighter span (split into headlinePart1 / appName / headlinePart2
  so each language can put the verb where it naturally belongs), body
  paragraph, the three trust chips (Decentralized, Open source,
  Censorship resistant), the two CTA buttons, and every string in the
  tilted sample-campaign card (Venezuelan vigil alt text, org name,
  campaign title, two-line description, 'raised / of $10,000',
  'N donors · N countries' line, Donate Bitcoin button).
- howItWorks — section header + lede, plus three step cards (image alt
  text, title, body) covering signup / send / spend.
- twoWays — section header, the two RailCards (kicker, tagline, title,
  description, three bullets, tradeoff title + intro + bullets each),
  the trade-off intro for Public Payments goes through <Trans /> with
  inline links to the Donor and Activist guides, and the No-custody
  banner including title, body, and the three GoFundMe / GiveSendGo /
  'other Bitcoin' comparison items.
- faq — section eyebrow + title, and the three FAQ chapter labels +
  descriptions (Getting started / Bitcoin donations / About Nostr).
  FAQ_CHAPTERS stays a module-level constant; its label/description
  fields just became labelKey/descriptionKey suffixes that AboutPage
  hands to t() at render time.
- guides — section header + lede, both Donor and Activist guide cards
  (image alt, role chip, title, description, three bullets each, cta),
  and the closing 'Still stuck? Follow Team Soapbox' line.

The FAQ accordion bodies themselves come from helpContent.ts via
HelpFAQSection — that file (701 lines of prose) is the Stage E2
candidate and stays English here.

Added rtl:rotate-180 to the two hero CTA arrow icons and the guide-
card arrow icons so they flip in RTL.

ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn,
ps, fa still pending native-speaker review.
2026-05-23 20:15:15 -05:00
mkfain 5ccf7ae8e9 i18n: translate page chrome for Privacy, CSAE, Donor, Activist, Changelog
Adds the policyPages.* namespace with five sub-namespaces, one per
page, all just translating the PageHeader title, SEO title/description,
and any chrome strings the page wraps around its (untranslated) body:

- privacy.* — PrivacyPolicyPage header. Body prose stays English for
  E2 markdown extraction.
- csae.* — CSAEPolicyPage header. Same — body is policy prose.
- donorGuide.* — DonorGuidePage title + subtitle for the GuideHero.
  Prose blocks come from helpContent.ts (still English; that file is
  the E2 candidate).
- activistGuide.* — ActivistGuidePage hero same shape as donor.
- changelog.* — ChangelogPage chrome: title, error/empty states, Past
  releases divider, Show less / Read more, pre-release banner ('Pre-
  release build', body line, View unreleased changes link), and a
  category-tooltip map (Added / Changed / Deprecated / Removed / Fixed
  / Security). The markdown body itself stays raw English — Read more
  truncates whatever the parser produced.

ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn,
ps, fa still pending native-speaker review.
2026-05-23 19:37:38 -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
Chad Curtis fe4162bd00 Close gap before 'unstoppable' in home hero highlight
Two issues left over from the previous restore:

- `ps-1` adds 0.25rem of padding-inline-start. In LTR that pushes
  the first letter rightward off the box's left edge, creating a
  visible gap. Drop to `ps-0` so the letter sits flush with the
  start edge.

- Bebas Neue's italic skew shifts the visible left edge of "U"
  rightward of its geometric box, leaving an apparent gap even with
  zero padding. Apply `text-indent: -0.06em` to pull the letter back
  into the box. The shift is small enough that other scripts
  (Arabic, Khmer, Chinese) tolerate it.
2026-05-23 19:06:30 -05:00
mkfain 924bfc3486 Merge remote-tracking branch 'origin/main' 2026-05-23 19:05:33 -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
Chad Curtis 27d88c34a8 Restore orange highlight on 'unstoppable' in home hero
Commit ab8457a8 migrated the home-page hero tagline to <Trans /> and
passed the wrapper elements via the OBJECT form of the `components`
prop:

    <Trans components={{ 0: <span className="bg-primary ..." /> }} />

In our react-i18next version (17.0.4), the object form silently drops
the indexed tags — the translated text renders, but stripped of the
wrapping element, so the orange highlighter span never makes it into
the DOM.

Switching to the ARRAY form makes i18next pick up the indexed tags:

    <Trans components={[ <span className="bg-primary ..." />, <br /> ]} />

While here:

- Add an index-1 <br /> so English keeps its original two-line layout
  (Connecting activists to / unstoppable funding.). Translations that
  prefer inline flow simply omit <1></1> from their string.
- Switch the highlighter's padding from `pl-1 pr-3` to logical
  `ps-1 pe-3` so the asymmetric flourish extending past the word's
  trailing edge flips correctly for RTL languages (ar, fa, ps).

Regression-of: ab8457a8
2026-05-23 19:03:34 -05:00
mkfain cca2732fdb i18n: translate ProfileSettings
Adds the profileSettings.* namespace covering ProfileSettings:
PageHeader (title, subtitle, Save button), intro section, profile
fields (Website, Lightning, label/value/ticker/address placeholders),
the seven field-preset pills (Music, Photo, Video, Email, Wallet, Link,
Weather, Custom) with labels, descriptions, and value placeholders,
media mismatch warnings (6 keys covering audio / image / video × wrong
type / unknown extension), the upload tooltip, crop dialog titles,
mobile fields preview button, Advanced section + Bot Account row, save
toasts, and the BackupKeySection (Your Key heading, extension / bunker
body, secret-key explainer + warning, copy / reveal / hide aria labels,
Back Up Key button, all backup-related toasts).

Module-level structures pulled the same way as elsewhere this session:
FIELD_PRESETS / CUSTOM_PRESET became key-only skeletons
(FIELD_PRESET_SKELETONS / CUSTOM_PRESET_SKELETON), and a useFieldPresets
hook materializes them inside the component over t(). The locale-
independent fields (icon, type, accept, formatHint, defaultLabel
emojis) stay at module scope. getMediaMismatchWarning was renamed to
getMediaMismatchWarningKey and now returns a translation key suffix
instead of an English string — the SortableFieldRow translates the key
at render time.

ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn,
ps, fa still pending native-speaker review.
2026-05-23 19:00:24 -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 4672c8a35c i18n: translate SearchPage
Adds a top-level search.* namespace covering the global search page:

- SEO + PageHeader title, three tab labels (Agora / Nostr / Accounts).
- The whole filters popover: title + Reset button, 'From' author-scope
  segmented control (Anyone / Follows / People), 'Sort' segmented
  control (Recent / Hot / Trending), the four selects (Media,
  Protocol, Language, Kind), the custom-kind input placeholder, and
  the 'Include replies' switch label.
- The active-filter chip strip (built in a useMemo) — 'No replies',
  'Images', 'Videos', 'Shorts & Divines', 'No media', 'Mastodon',
  'Bluesky', 'Hot', 'Trending', 'All kinds', 'Kind: {{kind}}',
  'Kind {{kind}}', kindsCount_one / _other, 'My follows', and
  authorsCount_one / _other.
- The Clear shortcut button, the 'search:' debug-string label, and
  the 'New posts' pill with newPosts_one / _other plural.
- Five empty-state messages (Posts results / Posts prompt / Accounts
  results / Follows prompt / Agora results / Agora prompt), plus the
  EmptyState 'Active filters:' header and 'Clear all filters' button.
- The 'Following' badge title and 🤖 'Bot account' tooltip on
  AccountItem + FollowItem (each needs its own useTranslation()).
- The SearchInput placeholder.

All 7 non-English locales drafted inline. Native-speaker review
still pending for km, sn, ps, fa.
2026-05-23 18:16:58 -05:00
mkfain 9e6f02887c i18n: translate NotificationSettings page
Adds a top-level notifSettings.* namespace covering the
/settings/notifications page: SEO meta, page header (title +
subtitle), Push Notifications section (heading, enable-push row,
unsupported/denied banner copy), Android-only Delivery Method
section (Push / Persistent radio with descriptions), 'Notify Me
About' section with the Filter / Types sub-headings, 'Only from
people I follow' row, and the eight notification type rows
(reactions, reposts, zaps, mentions, comments, badges, letters,
highlights) — each with its own label + description under
notifSettings.types.{key}.

NOTIFICATION_TYPES rows now carry labelKey / descriptionKey
strings instead of literal English, looked up via t() inside the
component render. The enable-failure toast (title + description)
also runs through t().

All 7 non-English locales drafted inline. Native-speaker review
still pending for km, sn, ps, fa.
2026-05-23 18:09:58 -05:00
mkfain a897d31a44 i18n: translate NotificationsPage
Adds a top-level notifications.* namespace covering the entire
notifications feed. Highlights:

- The 48-entry NOTIFICATION_KIND_NOUNS map (kind → bare noun used
  in 'reacted to your <noun>' / 'reposted your <noun>' etc.) was
  moved into notifications.kinds.* in the locale files and looked
  up via a new useNotificationKindNoun() hook. The component-side
  table now only maps kind → i18n key suffix so it stays small and
  diffable when new kinds are added.

- Action-verb strings for every notification type are interpolated:
  reactedToYour / repostedYour use {{noun}}; zappedYou has a
  zappedYouWithAmount variant taking {{sats}}; commentedOnYour
  takes {{noun}}; highlightedYour takes {{noun}}; mentionedYou,
  repliedToYourNote, repliedToYourComment, sentYouLetter,
  awardedBadge, and awardedBadges are flat.

- The condensed group subject uses <Trans i18nKey='subject.twoActors'
  components={{0: ActorLink, 1: ActorLink}}> so component order can
  flip in RTL languages; the 'and N others' branch uses
  andOthers_one / _other and {{count}}.

- '+N more' actor-overflow counter uses interpolation.

- Letter notification's 'View all letters' / 'Reply' buttons,
  SEO meta, tab labels (All / Mentions), the empty state and
  logged-out CTA all routed through t().

All 7 non-English locales drafted inline. Native-speaker review
still pending for km, sn, ps, fa.
2026-05-23 18:04:30 -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 dfd5463511 i18n: translate WalletRecoveryPage
Adds a top-level walletRecovery.* namespace covering the legacy
Breez/Spark Lightning sweep flow: SEO meta, page chrome, logged-out
empty state, destination address card, recovery-phrase input
(including the NIP-78 backup detection alert, the amber 'only paste
phrases you trust' warning, and the sweep button), the in-progress
loader with all five progress strings (loading SDK, connecting,
checking balance, preparing transfer with sats interpolation,
broadcasting), success card (sats interpolation + view-transaction),
error card, toast on successful relay-backup decrypt, and the six
walletRecovery.errors.* strings thrown into setError().

Back-to-wallet arrow flipped with rtl:rotate-180. All 7 non-English
locales drafted inline. Native-speaker review still pending for km,
sn, ps, fa.
2026-05-23 17:39:59 -05:00
mkfain 15c6634512 i18n: translate WalletPage
Adds a top-level wallet.* namespace covering the Bitcoin wallet
page: SEO meta, logged-out / unsupported-signer empty states, balance
display (refresh + pending), Send / Receive buttons, the Receive
dialog (on-chain + silent-payment tabs incl. wsrv-style <0>...</0>
Trans interpolations for the bip352IndexerUrl mono span and silent
balance highlight), and the transactions list (relative date helper
now takes t + i18n.language).

formatTxDate was lifted from a closure constant to a parameterised
helper so 'Today' / 'Yesterday' / '{count}d ago' / 'Pending' route
through t() and toLocaleDateString uses the active i18n language
(with an en-US fallback if the locale is unknown to Intl).

All 7 non-English locales drafted inline. Native-speaker review still
pending for km, sn, ps, fa.
2026-05-23 17:31:15 -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
mkfain 86a084f30d i18n: translate CampaignDetailPage
Extract user-facing strings on the campaign detail page into the
campaignsDetail.* namespace. Covers the hero (back/edit/delete chips,
author attribution, deadline pill, comment action label), the
engagement counter row above the comments (repost/quote/like counts
with pluralized labels and a bold count wrapper), the comments +
donations section header and empty state, the delete-confirm
AlertDialog, the donate sidebar (raised/of-goal labels, donation
count, recent-donations list, share button, ended state), the story
component, and pin/unpin and deletion toasts.

Chevron + arrow icons flip with rtl:rotate-180. Uses i18next plural
suffixes for the four count-driven labels (reposts/quotes/likes,
comments, donations, days-left).

This completes Priority 1 of the i18n rollout (Pledges +
Communities/Groups + Campaigns verticals — list pages, create forms,
detail pages).

Native-speaker review still pending for: km, sn, ps, fa.
2026-05-23 17:14:52 -05:00
mkfain c111ebc93e i18n: translate CreateCampaignPage
Extract user-facing strings on the create-campaign form (and its edit
variant) into the campaignsCreate.* namespace. Covers the four gate
states (login, invalid edit link, loading, not-author), the wallet
picker (source dropdown, accept-types dropdown, custom address +
silent-payment inputs with inline validation), the field labels and
placeholders, the country selector, success/error toasts, and the
mutation's twenty user-facing error messages.

Back-arrow flips with rtl:rotate-180.

Native-speaker review still pending for: km, sn, ps, fa.
2026-05-23 17:06:31 -05:00
mkfain 314654ca78 i18n: translate CreateCommunityPage
Extract user-facing strings on the create-group form (and its edit-mode
variant) into the groups.create.* namespace. Covers all four
non-success states (login gate, invalid edit link, loading-group
spinner, not-the-founder gate), the field labels and placeholders, the
URL preview footer with mono-spaced slug, moderator chip rows and
remove-button aria, the cover-image and country selectors, the submit
button's create/edit/uploading variants, and the mutation's eight
user-facing error messages.

Back-arrow flips with rtl:rotate-180.

Native-speaker review still pending for: km, sn, ps, fa.
2026-05-23 16:53:31 -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 48c2a6de50 i18n: translate CommunitiesPage (Groups list)
Extract user-facing strings on the groups index into the groups.list.*
namespace: hero copy, the moderator review rails (Needs review / Hidden),
the My-groups and Featured-groups shelves, the empty states for both
logged-out viewers and logged-in users with no groups, the ticker stat
labels, and the show-more/show-less collapsible toggle.

Uses i18next plural suffixes for the three ticker stats (campaigns
raised, featured groups, countries posting today).

Native-speaker review still pending for: km, sn, ps, fa.
2026-05-23 16:45:45 -05:00
mkfain ad0e2f4b18 i18n: translate ActionDetailPage
Translate the pledge detail page chrome into the pledges.detail.*
namespace, including the hero (back button, deadline pill, author
attribution, share/submit actions), the funding sidebar (funded/of
amount/trust note/share button), the submissions section
(skeleton/empty state/composer placeholder), pin/unpin toasts, and
the loading variant rendered by NIP19Page while the addressable
coordinate decodes.

Uses i18next plural suffixes for submission counts and days-left
labels. Chevron + arrow icons flip with rtl:rotate-180.

Native-speaker review still pending for: km, sn, ps, fa.
2026-05-23 16:39:26 -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 0b3db3eb8f i18n: translate ActionsPage (Pledges list)
Extract all user-facing strings on the Pledges index page into the
pledges.* namespace and supply translations for the 7 non-English
locales. Includes the hero copy, the card chrome (Pledged label,
ended badge, share menu, attribution), the sort/filter controls, the
section dividers, the empty state, and the SEO title/description.

Uses i18next plural suffixes (_one/_other) for the open-count pill and
the show-more button uses {{count}} interpolation.

Native-speaker review still pending for: km, sn, ps, fa.
2026-05-23 16:24:39 -05:00
mkfain ab8457a856 i18n: translate CampaignsPage landing
Add campaigns.home.* namespace covering the hero (tagline, body,
three CTAs), the Featured and Community Campaigns section headings and
descriptions, the Browse-all link, the moderator-only Pending and
Hidden section labels (title/description/emptyText), the non-mod
"Your campaigns" section, and the empty-state card.

The hero tagline uses <Trans i18nKey="campaigns.home.heroTagline"
components={{0: <span/>}}> so each locale controls where the orange
highlighter falls. The English-only inner-span optical alignment
(negative margin to counter Bebas Neue italic skew on a leading 'u')
is dropped — it was overfit to one word and would have produced ugly
results in any non-English layout.

The ArrowRight icon in the "How it works" CTA picks up rtl:rotate-180
so it points the right way in RTL languages.
2026-05-23 15:59:35 -05:00
mkfain 1d8899b0ab i18n: translate Index, NotFound, AllCampaignsPage
Add feed.indexTagline, notFound.*, and campaigns.all.* namespaces. Wire
useTranslation into:

- Index.tsx: SEO description tagline ("Your content. Your vibe. Your
  rules.")
- NotFound.tsx: 404 heading + "Go home" button + SEO meta
- AllCampaignsPage.tsx: page title, SEO meta, search aria/placeholder,
  clear-search aria, sort radio labels (Top/New) + sort group aria,
  show-hidden switch, start-campaign CTA, all three empty states (no
  match, all hidden, empty) + their hints

CampaignsPage landing page still needs translation; deferred to next
commit because it has hero sections and featured-slot copy that's worth
a dedicated pass.
2026-05-23 15:53:14 -05:00
mkfain 5c86b64bb1 i18n: translate Appearance/Network/Advanced/WalletSettings sub-pages
Add settings.appearance.*, settings.network.*, settings.advanced.*, and
settings.wallet.* namespaces. Wire useTranslation into the four small
settings sub-pages so the page header, intro text, theme picker labels,
relay/Blossom section headings, image upload quality toggle, and
collapsible Wallet trigger all switch language live.

Leaves the Notification, Profile, and Organizers settings sub-pages
plus the embedded WalletSettings / RelayListManager / BlossomSettings /
AdvancedSettings component bodies for a follow-up commit.
2026-05-23 15:48:29 -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
mkfain ae3cdacff3 i18n: scaffold 8 languages, add Language settings page
Phase 1 multi-language foundation:
- Register 8 locales in i18next (en, es, ar, fa, ps, km, sn, zh) with
  static-bundled JSON. Wire languageChanged listener to set
  document.documentElement.lang and dir for RTL support.
- Expose SUPPORTED_LANGUAGES (code + nativeName) so the switcher and
  validator pick up new entries automatically.
- Restructure en.json: drop ~280 dead wallet-port keys, introduce
  settings.sections.*, language.*, and a tightened common.* namespace.
  Keep organizers.* and the keys actually consumed by t() calls today.
- Translate common, settings, language, and organizers namespaces in
  all 7 non-English locales (machine drafts; km/sn/ps/fa flagged for
  native-speaker QA before announcement).
- Add LanguageSettingsPage at /settings/language with a radio-style
  picker rendering each language's native name in its own script/dir.
- Add Language entry to settingsSections, between Appearance and Network.
- Translate SettingsPage strings (page title, section labels, delete
  button) using the new keys. Flip the chevron with rtl:rotate-180.
- Add src/test/locales.test.ts validator that fails CI on any locale
  introducing keys absent from en.json (typo / stale-translation guard).

No visible UX change in English. Switching language now translates the
Settings hub, the Language sub-page, and the existing Organizers admin
end to end.
2026-05-23 15:38:09 -05:00
Alex Gleason 4ef932bad1 Merge branch 'main' of gitlab.com:soapbox-pub/agora 2026-05-23 15:06:12 -05:00
filemon 7d5ec66ec2 MyDashboardPage: inline HorizontalScroll helper
The shared HorizontalScroll component was removed from main during
orphaned component cleanup. Instead of restoring the shared file,
inline the small helper directly in MyDashboardPage since it is the
only consumer.
2026-05-23 17:02:52 -03: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