Rebrand Agora to Eranos and strip the non-Grin rails. Add Grin donations:
a GoblinPay client + GrinPayDialog, on-chain payment-proof verification
(receiver-sig + kernel-on-chain + dedupe), and a proof-verified campaign
tally (kind 3414). Shift the brand from orange to gold. 118 tests green.
Replace the single baked-in terremoto-venezuela campaign with a live,
agnostic showcase: every Venezuela-located campaign tagged
`humanitarian-aid` or `emergency-relief` and created since the
earthquake is pulled in and rendered as a card carousel across the home
hero and the dedicated relief page.
- New useVenezuelaReliefCampaigns hook queries kind 33863 filtered by
`#i: [iso3166:VE]` + `#t: [humanitarian-aid, emergency-relief]` +
`since` (quake timestamp), and aggregates on-chain raised totals the
same way useProfileCampaignStats does (one /address lookup per
campaign, summed; no per-receipt /tx fan-out). The flagship
terremoto-venezuela campaign predates the geo-tag convention and
carries no `iso3166:VE` tag, so it's pinned by coordinate and merged
ahead of the filtered results, deduped by aTag.
- Extend useCampaigns with `categories` (`#t`) and `since` options.
- New VenezuelaReliefShowcase: an auto-panning marquee of CampaignCards
(seamless -50% loop, hover/focus pause, click-drag + momentum, edge
fades, prefers-reduced-motion fallback to a native scroll rail),
ported from the surveil deck shelf.
- Hero/page CTAs and the popup donate button now point at the relief
page (no more single-campaign naddr deep-link); the page CTA scrolls
to the showcase. Drop the embedded CampaignDetailPage, the
.relief-donate-flash CSS, and the now-unused scroll-to-campaign logic.
- VenezuelaReliefGoal now reads the aggregate hook and shows the
combined raised total + matching-campaign count only (no goal bar —
an aggregate goal across independent campaigns is meaningless).
- Tighten the hero height so the carousel sits near the fold.
- Locale updates across all 16 languages: drop `startCampaign`/`goalOf`,
add `showcaseTitle` + `campaignCount`.
- Patch react-router open redirect (GHSA-2j2x-hqr9-3h42) within 6.x
- Bump vitest to 4.x to fix UI server file read/exec (GHSA-5xrq-8626-4rwp)
npm audit now reports 0 vulnerabilities; full test suite passes.
The 720x880 logo.svg was force-rendered into a 512x512 square, stretching
the bolt horizontally. Render height-bounded (aspect-preserving) instead
and let the centered composite handle fit. Regenerate all Android, iOS,
and web/PWA/Zapstore icons.
src/lib/countries.ts imported the full iso-3166 package solely to build
a Set of valid ISO 3166-2 subdivision codes for validation. That dataset
(~5000 objects with names, parents, and tree structure) landed in the
eagerly-preloaded countries chunk because NoteContent, ComposeBox, and
campaign.ts all import from countries.ts on the critical path.
Ship only the subdivision code strings instead, generated at build time
into src/lib/subdivisionCodes.ts via scripts/gen-subdivision-codes.mjs.
iso-3166 moves to devDependencies since only the generator script needs
it now. The strict-validation contract (rejecting US-ZZ etc.) is
preserved.
The recipient input on /wallet's Send dialog now has a camera button
that opens a QR scanner. Bitcoin BIP-21 URIs are parsed and the
silent-payment fallback (?sp=) is preferred when present, falling
back to the on-chain address otherwise. Plain addresses, sp1… codes,
npub, and nprofile values are dropped into the input verbatim and
resolved by the existing recipient logic.
QrScannerDialog is a standalone component (ported from Ditto) that
owns the camera lifecycle via getUserMedia and the qr-scanner npm
package. It surfaces failure modes (insecure context, denied
permission, no camera, busy camera, overconstrained, ready timeout)
instead of a silent black screen, and offers a flash toggle when the
device supports it.
Android needed an explicit CAMERA permission in the manifest; iOS's
existing NSCameraUsageDescription string was extended to mention QR
scanning. No Capacitor camera plugin is required — the standard web
APIs work inside WKWebView and Android's WebView.
Bebas Neue (the .font-display family) ships only Latin glyphs, so
Chinese hero headlines fall back to system fonts and lose their
industrial display character. Add @fontsource/noto-sans-tc weight 900
and a :lang() rule that swaps it in for any .font-display element
while the page language is a Chinese variant (zh, zh-Hant, zh-TW,
zh-HK).
The fontsource CSS uses unicode-range descriptors, so non-Chinese
users do not download the Han glyph slices (effectively zero cost
for Latin-only locales).
The rule reverses Tailwind's italic, uppercase, tracking, and the
hero's 0.022em -webkit-text-stroke fatten trick — none of those are
meaningful for CJK text and the stroke trick muddies strokes at
weight 900.
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.
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).
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.
Replace the bitcoinjs-lib + ecpair + @bitcoinerlab/secp256k1 + Buffer-polyfill
stack with @scure/btc-signer (plus @noble/curves for BIP-352 point math)
across every consumer:
- src/lib/bitcoin.ts: P2TR payment + PSBT build/sign/finalize via btc.p2tr
and btc.Transaction. signPsbtLocal hands the raw 32-byte private key to
signIdx, which detects tapInternalKey and applies the BIP-341 TapTweak
internally — the ECPair + manual taggedHash('TapTweak', ...) song-and-dance
is gone. The empty-string-on-invalid-pubkey contract is preserved via an
explicit on-curve check using schnorr.utils.lift_x.
- src/lib/hdwallet/derivation.ts: deriveLeafTaprootSigner is removed in
favour of deriveLeafPrivateKey, which is now sufficient because
signIdx tweaks internally. The lazy ensureEcc / ECPairFactory plumbing
is gone.
- src/lib/hdwallet/transaction.ts: PSBT pipeline ported to btc.Transaction;
signHdPsbt now wipes the materialised leaf privkey after signIdx().
- src/lib/hdwallet/sp/{crypto,scanner}.ts: replace ecc.pointFromScalar /
pointAdd / pointMultiply with secp256k1.Point methods from @noble/curves.
pointMultiplyCompressed is exported for the scanner. Noble multiply is
strict (throws on scalar 0 or >= n) so the wrappers preserve the previous
"Failed to compute …" semantics.
- src/lib/campaign.ts: parseCampaignWallet uses the shared
validateBitcoinAddress helper instead of bitcoin.address.toOutputScript.
- src/lib/bitcoin-signers.ts: NSecSignerBtc no longer touches Buffer.
- src/lib/polyfills.ts + src/main.tsx: drop the global Buffer polyfill and
the bitcoin.initEccLib(ecc) bootstrap — neither is needed anymore.
package.json: removes bitcoinjs-lib, ecpair, @bitcoinerlab/secp256k1, and
the buffer polyfill package; adds @scure/btc-signer ^2.2.0. @noble/curves
and @scure/{base,bip32,bip39} were already in tree.
bitcoin.test.ts gains a PSBT round-trip regression block. The unsigned-PSBT
hex fixtures in those tests were captured from the bitcoinjs-lib pipeline
before the migration, so the new build path is asserted to produce
byte-for-byte identical PSBT envelopes (input layout, output ordering,
fee-vs-change decision, PSBT v0 serialisation). Signing uses random aux so
witness bytes differ run-to-run; the tests verify the resulting raw tx hex
has the right Schnorr-key-path witness shape (0x01 stack + 0x40-byte sig)
for every input, plus that signPsbtLocal still throws when no input belongs
to the signer.
All 25 bitcoin.test.ts tests pass; full \`npm run test\` (72 tests + tsc +
eslint + vite build) is green.
- Switch the hook headline to Bebas Neue (font-display family) at heavier
size with a synthetic webkit-text-stroke fatten, italic, uppercase, and
tight leading so 'Connecting activists to / unstoppable funding.' reads
as a single editorial statement.
- Force the orange highlight onto its own line via <br>; tune left/right
padding and inner text offset so the U sits flush with 'Connecting'
above while the box extends past the word as a flourish.
- Fix two horizontal slashes across the world map caused by
antimeridian-crossing rings (Russia, Antarctica): detect any longitude
step > 180° and close+restart the SVG subpath instead of drawing the
connecting line.
- Drop the 2008-era left-edge darkening gradient and the bottom
vignette behind the map.
- Dim the central radial brand-orange glow (~half alpha).
- Tighten the arc-flow dash period and pixel size.
Derive a static sp1q… identifier from the user's nsec via BIP-352's
spend/scan key paths (m/352'/0'/0'/0'/0 and m/352'/0'/0'/1'/0), then
bech32m-encode scan_pubkey || spend_pubkey with HRP "sp" and version 0.
The Receive dialog now has two tabs: the existing BIP86 fresh-address
flow and a Silent payment tab that shows the static address with QR
and copy. The SP tab is labelled receive-only — the wallet doesn't yet
scan for incoming silent payments, so funds sent there won't appear in
the balance until scan + spend support is wired in.
A production-grade BIP86 Taproot HD wallet, separate from the single-address
wallet at /wallet. The seed is derived deterministically from the user's nsec
via HKDF-SHA-256 with an app-specific info string, so there is no new secret
for the user to back up \u2014 if they have their nsec they have the wallet.
Architecture:
- src/lib/hdwallet/derivation.ts \u2014 HKDF seed, BIP86 (m/86'/0'/0'),
receive (0) and change (1) chains, per-leaf P2TR addresses, TapTweaked
signing keys.
- src/lib/hdwallet/scan.ts \u2014 Standard gap-limit (20) scan across both
chains via Esplora. Aggregated UTXO set, balance, and tx history
(merged by txid so send-with-change shows as one row).
- src/lib/hdwallet/transaction.ts \u2014 Largest-first coin selection
(confirmed first), multi-input P2TR PSBT build with per-input
tapInternalKey from re-derived child keys, fresh change addresses on
the internal chain (no address reuse).
- useHdWalletAccess \u2014 Gates on login type === 'nsec'. Extension and
bunker logins keep the key isolated, so the page shows an explanatory
card with a link back to /wallet.
- useHdWallet \u2014 Scan + tx history queries (60 s refresh), persisted
receive-cursor in secure storage (Keychain on native, localStorage on
web), auto-advance when chain catches up so old addresses are never
re-shown.
- HDWalletPage \u2014 Mirrors /wallet's clean UX: big USD balance, send
button, QR + truncated address, 'New address' rotator, collapsible tx
history.
- HDSendBitcoinDialog \u2014 Mirrors SendBitcoinDialog (USD presets, fee
speed picker, two-tap arm for large amounts, raw-address privacy
disclaimer, success screen) but uses the HD UTXO set across many
addresses and signs with per-input HD-derived keys.
The @samthomson/nostr-messaging library opens fresh NRelay1 sockets per
participant per relay outside the shared NPool, fanning out to every
conversation partner's NIP-65 + NIP-17 inbox relays plus all
discoveryRelays in hybrid mode. In practice this drives connection counts
to several hundred relays per session.
Rather than band-aid the fan-out, drop the feature entirely and point
users to White Noise for end-to-end encrypted Nostr chat.
- Replace /messages with a 'Install White Noise' CTA card (route kept)
- Delete MessagingSettingsPage, DMProviderWrapper, messaging-intro.png
- Remove DMProvider wrapper and PROTOCOL_MODE config from App.tsx
- Drop messaging config from AppConfig, AppConfigSchema,
EncryptedSettingsSchema, EncryptedSettings, and the NostrSync /
useInitialSync sync paths
- Remove messages sidebar entry, default sidebarOrder slot, and
SettingsPage messaging card
- Uninstall @samthomson/nostr-messaging and drop its tailwind content
glob and vitest deps.inline entry
- Update copy in PrivacyPolicy, AdvancedSettings delete-account warning,
ProfileSettings nsec warning, RequestToVanishDialog deletion checklist,
MainLayout comment, and NIP.md
- Leave kind 4 rendering (EncryptedMessageContent) intact so DM events
authored elsewhere still display in feeds and quote embeds
Left over from the ditto/main merge — version was bumped back to
2.8.0 (Agora's version) in package.json post-merge but the lockfile
wasn't regenerated until npm install ran.
Hardcoded MEMPOOL_API constant in src/lib/bitcoin.ts becomes a baseUrl
parameter on every fetch helper, sourced from a new `esploraBaseUrl`
field on AppConfig (default `https://mempool.space/api`). The wallet,
zap dialogs, on-chain zap verification, and NIP-73 Bitcoin tx/address
pages now read the URL from useAppContext and pass it through, so
self-hosted Esplora deployments (or Blockstream's) work without code
changes. The mempool.space-specific `/v1/prices` extension is still
appended by fetchBtcPrice.
Mirror the existing Android publishing flow for iOS. The pipeline
gains two jobs: build-ipa runs on a self-hosted Mac runner and
produces a signed App Store IPA; publish-app-store runs on a shared
Linux runner and submits the prebuilt IPA to App Store Connect.
Build pipeline (.gitlab-ci.yml):
- build-ipa (Mac, stage build, parallel with build-apk): decodes the
ASC API key, runs match (with api_key, so cert validity is verified
against Apple before xcodebuild starts), builds web assets, syncs
Capacitor, stamps MARKETING_VERSION. Uploads Ditto-${CI_COMMIT_TAG}
.ipa to GitLab's Generic Packages registry.
- publish-app-store (Linux ruby:3.3, needs: [build-ipa]): gem
install fastlane, decode the ASC API key, extract the changelog
section into release_notes.txt, fastlane submit_release with
IPA_PATH pointing at the inherited artifact. No Xcode, no signing,
no keychain \u2014 pure Apple API call.
- release job now needs both build-apk and build-ipa, and links three
assets (APK / AAB / IPA).
fastlane (ios/fastlane/Fastfile, Matchfile, Appfile, metadata/):
- Four lanes: build_ipa (CI build), submit_release (CI publish, reads
IPA_PATH from env), release (single-step convenience for local
dev), submit_only (debug lane to re-submit an already-uploaded
build).
- Match config points at the private gitlab.com/soapbox-pub
/certificates repo. App Store Connect API key is built inline in
the Fastfile to avoid a collision with match's APP_STORE_CONNECT
_API_KEY_PATH env var (match wants a JSON descriptor, the action
writes a raw .p8). CI overrides CODE_SIGN_STYLE=Manual via xcargs
so the Xcode project can stay on Automatic for local development.
Vite config (vite.config.ts):
- Renames the build-time config override env var from CONFIG_FILE to
DITTO_CONFIG_FILE. GitLab Runner sets CONFIG_FILE to its own TOML
config in job env, which broke vite's loader.
App-side changes:
- ios/App/App.xcodeproj/project.pbxproj: team GZLTTH5DLM stamped in;
MARKETING_VERSION gets stamped from the tag at build time.
- public/CHANGELOG.md, package.json: v2.14.3.
Skills + AGENTS.md updated to reflect the six-job pipeline (test /
deploy unchanged, build now has two jobs, release / publish updated)
and to document Mac-runner operations, fastlane match cert rotation,
and local debugging workflows.
Stacked avatars in PeopleAvatarStack are now clickable, navigating to
the user's npub profile so readers can jump straight to a member from a
follow list, follow set, or follow pack — not only from the surrounding
post. Each avatar is wrapped in a Link with a stopPropagation handler so
the click doesn't bubble up to the card-level navigation, and the focus
ring is now visible on keyboard focus.
Kind 3 follow-list events are legacy replaceable kinds (NIP-01) but
fell outside the 10000–19999 range that NoteCard, EmbeddedPeopleListCard,
PostDetailPage, and NoteMoreMenu all special-cased — so clicking a
follow list in a feed went to a per-event nevent that pinned to a stale
revision instead of the stable naddr. The four call sites are now
unified behind a new lib/encodeEvent.ts helper that treats kinds 0, 3,
and 41 as replaceable, alongside 10000–19999 and 30000–39999. The same
helper exposes encodeEventNevent for callers that intentionally want to
reference a historical version (e.g. the profile-recovery dialog).
Redesign the on-chain flow around a sleek 'Send $X' experience: click
the big number to edit, presets sit underneath, comment collapses behind
a chevron, recipient address and OR divider are gone, the send button
just says 'Send $5.20' with the fee included, and fee speeds are
deduplicated so duplicate sat/vB tiers don't repeat.
The fee speed now auto-adjusts when the amount changes to keep the fee
below 40% of the send amount — once the user manually picks a speed,
auto-adjustment is disabled for the session.
Dialog framing switches from 'Send a Zap' to 'Send Bitcoin', the
redundant description line is dropped, and the (?) popover routes to
one of two new tab-specific FAQ entries (send-bitcoin-onchain or
send-bitcoin-lightning).
When a user tapped "Open Signer App", the dialog previously stayed
frozen on the same screen — same button, same copy-URI fallback, no
feedback — until the login either succeeded (and the dialog dismissed)
or timed out after two minutes. With slow or flaky signers (Amber's
current listening-REQ bug being the immediate trigger, but any NIP-46
signer that takes more than a second or two to respond hits the same
hole) this looked indistinguishable from a hang. Users retapped the
button, closed the dialog, gave up.
Now the dialog swaps the QR / Open Signer App area for a centered
spinner with a live status line as the handshake advances:
- "Waiting for signer connection…" while the signer app has the user
and we're listening on kind 24133 for the connect-ack.
- "Getting public key…" once the connect-ack arrives and we're
issuing the NIP-46 get_public_key RPC.
On mobile the swap happens synchronously when the user taps "Open
Signer App" so they see the progress state the moment they return
from the signer — this is the most important window, since that's
exactly when the original UI left them staring at a button they
were worried they needed to re-tap. On desktop the QR stays visible
through the awaiting-connect phase (they may still be scanning with
a different device) and only swaps in once the signer has
acknowledged.
The progress view includes a Cancel link (primary color, matches the
"Create account" affordance) that aborts the in-flight subscription
and regenerates fresh connect params — equivalent to the existing
Retry path, but reachable while the handshake is live instead of
only after a failure.
The handshake phases are surfaced via the new `onStatus` callback
on `NLogin.fromNostrConnect` in @nostrify/react 0.6.0. Bumps
@nostrify/react to ^0.6.0 and @nostrify/nostrify / @nostrify/types
to their matching versions (^0.52.0 / ^0.37.0) to avoid duplicate
nested package copies that would otherwise split type identity.
Incidental cleanup while editing the dialog: the Copy URI button and
the "Tap to open your signer app" / "Scan with your signer app"
status lines are removed. The primary Open Signer App button is
self-explanatory on mobile, and the QR on desktop doesn't need a
caption.
User relays are no longer used until the user explicitly opts in via
Settings > Network. Adds a useUserRelays toggle alongside the existing
useAppRelays toggle in RelayListManager, defaulting to false. Fresh
installs and new accounts will only query app-default relays until the
user enables their personal NIP-65 list.
The user's relay list (kind 10002) is still synced from Nostr and
managed in the UI when logged in — the toggle only controls whether it
is included in the effective relay set used by NostrProvider's pool
and useNativeNotifications. The setting is persisted to AppConfig and
synced cross-device via NIP-78 encrypted settings.
getEffectiveRelays now takes both flags and short-circuits accordingly,
producing an empty list when both are off (instead of the previous
behavior of always returning user relays).
The right sidebar previously required xl (1280px) to appear, so horizontal
iPad (1024px) viewports saw only the left sidebar + main column. Use the
existing lg breakpoint (1024px) to control right-sidebar visibility, and
let the sidebars scale fluidly with the viewport by setting them to w-1/4
max-w-[300px] instead of fixed pixel widths. The center column (flex-1)
absorbs whatever space remains, so the layout fills the viewport smoothly
from 1024px up through the 1200px wrapper cap instead of leaving dead
space at intermediate widths. Below the lg breakpoint, the left sidebar
keeps its fixed 300px width.
The alt-tag fallback shipped in 9813a226 let several display paths fall
through to other tags (title, name, summary, description, d) or to the
raw content when alt was absent. For an unknown kind like attestr.xyz
(31871), that surfaced the opaque d-tag identifier
'e5272de9:289bce03a0b7:1777206698' as the preview and leaked raw content
into the hover-card reply indicator — both worse than the 'This event
kind is not supported' tombstone the feature was meant to produce.
Tighten the fallback everywhere an unknown kind might render:
- getEventFallbackText: only the NIP-31 alt tag; no title/name/d.
- CommentContext.getEventDisplayName: known kinds keep title/name/d,
unknown kinds consider only alt. getKindLabel returns 'an unsupported
event' instead of 'a post', so 'Commenting on ...' never implies the
root is a text note.
- EmbeddedNote tagMeta: alt only, no title/name/description fallback.
- ExternalContentHeader.AddressableEventPreview: drop d-tag fallback.
- EmbeddedNaddr: gate rich title/description/content rendering behind
a known-kind check; unknown kinds render UnknownKindContent instead
of extractMetadata, which was leaking plaintext content as the
description when the body wasn't JSON.
Regression-of: 9813a226
Previously, any kind not explicitly handled by NoteCard or
PostDetailContent fell through to the kind-1 text-note renderer, which
ran the URL/hashtag/nostr: tokenizer over arbitrary content — broken
for events whose content is JSON or empty.
Add an UnknownKindContent component that displays the NIP-31 'alt' tag
(falling back to title/name/summary/d) in a rounded card, or a dashed
'This event kind is not supported' tombstone when the event carries no
fallback text. Route to it from both dispatchers when the kind isn't
1, 11, or 1111.
Extend the same handling to embedded quote previews (EmbeddedNote,
EmbeddedNaddr, AddressableEventPreview) so reply-context hover cards,
compose previews, more-menu previews, notification references, and
inline nostr: mentions all display unknown kinds consistently instead
of feeding JSON or arbitrary content to the kind-1 tokenizer.
Adds feed support for kind 2473 (bird-by-ear detections) and kind 30621
(user-drawn star figures) from Birdstar. Detections render as species
cards using the existing Wikidata + Wikipedia summary hooks; constellations
render as gnomonically-projected SVG star-maps backed by the Hipparcos
catalog from d3-celestial. The 1.1 MB catalog is code-split via lazy() so
it only loads when a constellation event is actually viewed.
Replace the plain launch card with a Game Boy-style cartridge (using
public/cartridge.png) whose label region centers the app icon. The whole
cartridge scales as one image and reacts to the pointer with the
existing useCardTilt 3D effect.
The cartridge is tinted by the icon's dominant color: a new
useDominantColor hook samples the icon in an off-screen canvas, picks
the heaviest hue bucket, and exposes it as HSL. A mask-image layer
masked to the cartridge silhouette blends the color over the grayscale
PNG with mix-blend-mode: color, preserving the shading. Grayscale or
CORS-blocked icons fall back to the original gray cartridge.
The app name moves out from under the cartridge and into the description
card in FileMetadataContent — rendered larger and bolder above the
note's content — while WebxdcEmbed still renders its own name card when
a parent isn't providing one (e.g. the kind 1 imeta path).
Replace the invisible click shield with a dramatic visual sequence when
Blobbi reaches max overstimulation: a radial shockwave expands from
Blobbi, a dark backdrop covers the UI (Blobbi stands alone in the void),
and canvas debris particles rain down like rubble. On recovery the
backdrop fades and debris converges back.
Also fix the debug bypass in useShakeReaction (true || threshold) that
shipped nausea on every shake regardless of hunger stat, and remove the
toast notification replaced by the visual overlay.
The Poll/Spoiler popover opened side="bottom" from the toolbar, which
is near the bottom edge of the dialog. Because the dialog container
uses overflow-hidden (needed for flex layout containment) and the
PortalContainerProvider portals content inside the dialog DOM, the
popover was clipped.
Switch to side="top" so the menu opens upward into the visible area
of the modal.