Integrate Bitcoin content into the /i/* external content system using NIP-73
identifiers (bitcoin:tx:{txid} and bitcoin:address:{address}).
- Add bitcoin-tx and bitcoin-address types to ExternalContent parser
- Create BitcoinTxHeader with mempool.space-style inputs/outputs flow view
- Create BitcoinAddressHeader with balance, stats, and recent transactions
- Add useBitcoinTx and useBitcoinAddress hooks (mempool.space Esplora API)
- Switch all Bitcoin API calls from blockstream.info to mempool.space
- Update WalletPage to link transactions to /i/bitcoin:tx:{txid} pages
- Remove unused blockExplorerAddress/blockExplorerTx config fields
- Add compact Bitcoin previews for embedded note contexts
Add blockExplorerAddress and blockExplorerTx fields as RFC 6570 URI
templates with {address} and {txid} variables respectively. Default to
mempool.space instead of blockstream.info. Wallet page uses UriTemplate
to fill the configured templates.
Transactions button with chevron replaces the 'View on explorer' link.
Clicking toggles the tx list open/closed with a smooth accordion slide
using CSS grid-template-rows animation. Chevron rotates on open.
Fetch transactions from Blockstream Esplora API, compute net amount per
tx relative to the user's address, and display as a list below the QR
code. Each row shows receive/send direction, relative date, USD amount
(with BTC underneath), and links to the block explorer. Includes loading
skeletons and empty state.
Fetch BTC/USD price from CoinGecko (refreshes every 60s). Display USD
as the hero balance, BTC amount as the secondary line. Remove sats
display entirely. Pending amounts also shown in USD.
Remove outer Balance card wrapper, stats grid, and How It Works section.
Balance is now the hero element, centered with QR code below and a
compact pill-shaped address with inline copy. Clean, minimal layout.
Derive a bc1p... Taproot address directly from the user's Nostr public key
(both use secp256k1 x-only keys) and display balance via Blockstream API.
Includes QR code, copy-to-clipboard, balance with pending detection, and
a WALLET.md documenting the derivation algorithm. Sending is not yet
implemented.
openDatabase() now catches errors from idb's openDB() (which throws
synchronously when indexedDB is undefined) and returns null. All
consumers — profileCache, nip05Cache, dmMessageStore — check for null
and silently degrade to in-memory only.
The DM message store also stops re-throwing errors, which previously
could produce unhandled rejections in DMProvider.
Replace the local-shakespeare.dev preview domain with iframe.diy, which
provides a service-worker based sandbox. This brings the nsite preview
implementation in line with Shakespeare's approach.
Key changes:
- iframe.diy handshake: listen for 'ready', respond with 'init'
- Derive private HMAC-SHA256 subdomains via deriveIframeSubdomain('nsite', ...)
- Inject preview script into HTML responses for console forwarding,
SPA navigation tracking, and /index.html path normalization
- Remove sandbox attribute (iframe.diy manages its own sandboxing)
- Serve injected script from virtual /__injected__/preview.js path
The i-tag UUID used for webxdc coordination is attacker-controlled.
Using it directly as the iframe.diy subdomain lets a malicious event
author pick a subdomain that collides with another app's origin,
gaining access to its localStorage/IndexedDB.
Introduce a persistent random seed in localStorage (ditto:seed) and
derive the subdomain as base36(HMAC-SHA256(seed, prefix|identifier)).
The prefix (e.g. "webxdc") domain-separates different use-cases.
The subdomain is stable per device+app but unpredictable to event
authors.
Apply a strict CSP header to every response served from the .xdc archive
to enforce the webxdc offline sandbox. Permits same-origin, inline, eval,
wasm, data: and blob: but blocks all external network access.
Migrate the webxdc iframe runtime from webxdc.app to iframe.diy. Instead of
sending ZIP bytes to the iframe and having the SW unzip them, the parent now
unzips the .xdc archive and serves files via iframe.diy's fetch-proxy RPC.
A webxdc bridge script is served as a virtual /webxdc.js file, and a
<script> tag is injected into HTML responses via DOMParser to load it.
- Rewrite Webxdc.tsx to use iframe.diy's ready/init/fetch protocol
- Unzip .xdc archives on the parent side and serve via fetch RPC responses
- Serve webxdc bridge as virtual /webxdc.js via the fetch handler
- Inject <script src="/webxdc.js"> into HTML using DOMParser
Portal ProfileImageLightbox to document.body, matching the fix
already applied to the shared Lightbox component. Without the
portal, the lightbox was trapped inside the center column's z-0
stacking context from MainLayout, causing the right sidebar
(a sibling outside that context) to paint on top.
Replace outdated references to 'inventory items', 'consume',
'quantity', and 'storage decrement' across 14 files. Comments
now consistently describe items as reusable abilities sourced
from the shop catalog, not consumable inventory.
Items are now single-use abilities — tap item, press Use, effect
happens immediately. No confirmation dialogs or quantity selectors.
Changes:
- Remove BlobbiUseItemConfirmDialog and InventoryUseConfirmDialog
- Remove quantity state, selectors, and multi-use loops from modals
- Simplify mutation hooks to always apply item effects once
- Drop quantity parameter from UseItemFunction type signature
- Update all call sites through the full stack (BlobbiPage, context,
companion layer, companion item use hook)
Items are now treated as abilities/tools unlocked by stage, not
consumable inventory that must be purchased. All catalog items are
shown in the companion action menu regardless of inventory quantity.
Changes:
- Source items from shop catalog instead of user inventory storage
- Remove quantity validation and storage decrement on item use
- Remove quantity badges and 'in inventory' text from all modals
- Keep stage-based filtering (egg vs baby/adult restrictions)
- Cap quantity selector at 99 instead of inventory count
Replaceable and addressable event headers now distinguish between
first publish and subsequent updates using the published_at tag:
- published_at == created_at → 'created' verb (e.g. 'created an emoji pack')
- published_at != created_at → 'updated' verb (e.g. 'updated an emoji pack')
- no published_at → 'shared' fallback for backward compatibility
Extend useNostrPublish with an optional `prev` property on the event
template. For replaceable and addressable kinds, the hook automatically
manages published_at:
- First publish (no prev): set published_at equal to created_at
- Update (prev provided): preserve published_at from the old event
- Old event lacks published_at: don't fabricate one
- Caller already set published_at in tags: leave it alone
Callers pass `prev` when they have the old event from fetchFreshEvent,
giving the hook everything it needs without extra network requests.
Updated all 11 call sites that publish replaceable or addressable events.
Documents the prev convention in AGENTS.md.
Two issues caused custom tab feeds (e.g. Magic Decks) to loop:
1. ProfileSavedFeedContent flattened pages without deduplication, so
events returned by multiple pages rendered as visible duplicates.
2. useTabFeed only stopped paginating when rawCount === 0. For
addressable events the relay keeps returning the same latest
versions, so rawCount never hit zero. Changed to rawCount < limit
(relay returned fewer than requested = exhausted).
Replace grouped-by-emoji layout with a flat list where each reaction
row shows an inline emoji badge (similar to the zap amount badge).
Add an emoji summary bar at the top when multiple emoji types are
present. This makes it immediately obvious who reacted with what.
The scroll-aware active indicator reporting and scroll listener logic was
duplicated between TabButton and SortableTabChip. Extract into a shared
useActiveTabIndicator hook in SubHeaderBar.
- Add pencil icon to SortableTabChip for editing existing custom tabs
- Wire onEdit to open ProfileTabEditModal with the existing tab data
- Clear the active arc underline when an active tab is removed (cleanup in useLayoutEffect)
- Round dnd-kit transform values to avoid sub-pixel rendering issues
SubHeaderBar: add left/right chevron scroll arrows on desktop when tabs
overflow, with gradient fade. Auto-scroll active tab into view and keep
arc hover/active indicators aligned during horizontal scroll.
ContentSettings: add Interest Tabs section with inline add/remove for
hashtags and geotags. Remove buttons always visible (mobile-friendly),
X icons use strokeWidth 4.
On desktop, overflowing feed tabs were completely inaccessible since the
scrollbar was hidden and there was no swipe gesture. Add left/right
chevron scroll buttons that appear only on desktop when tabs overflow,
with gradient fade indicators. Also auto-scrolls the active tab into
view when switching tabs, and keeps the arc hover/active indicators
aligned during horizontal scroll.
When a depth-collapsed 'Show X more replies' button was the last item
in a reply sequence, it lacked a bottom border separator. Added an
isLast prop to ExpandThreadButton that adds border-b when the button
terminates the visual sequence.
DashboardShell uses fixed positioning on mobile, placing it directly
over the body background image. Without the bg-background/85 class
that MainLayout's center column provides, the raw background image
showed through unthemed. Add the same 85% opacity background overlay
used consistently across the rest of the app.
All blobbi mutations now follow the read-modify-write pattern: fetch fresh
state from relays before mutating, then optimistically update the cache.
This prevents two classes of bugs:
1. Stale cache reads: mutations were reading from TanStack Query cache
(30s staleTime) instead of relays, causing newer events to be silently
overwritten with old stats when actions happened within the cache window.
2. Invalidation races: every mutation called invalidateCompanion() after
the optimistic update, which triggered a refetch from relays before the
just-published event had propagated, overwriting the optimistic data
with the pre-mutation state.
Changes:
- ensureCanonicalBlobbiBeforeAction now fetches fresh companion + profile
from relays (the read step) instead of using cached closure values
- useBlobbiCareActivity fetches fresh companion before streak updates
- Removed all invalidateCompanion()/invalidateProfile() calls after
optimistic updates across every action hook
- updateCompanionEvent now updates ALL blobbi-collection query caches
for the user, not just the specific d-tag list it was instantiated with,
keeping BlobbiPage and companion layer caches in sync
Settings (theme, sidebar, etc.) changed on one device were not applied
on other devices. Three root causes:
1. NostrSync seeded lastSyncedTimestamp to remoteSync on first load,
then the guard (remoteSync <= lastSyncedTimestamp) blocked the same
data from being applied. Settings were never applied on page reload.
2. The encrypted settings query had staleTime: Infinity and
refetchOnWindowFocus: false, so remote changes were never fetched.
3. useInitialSync was missing customTheme, corsProxy, faviconUrl, and
linkPreviewUrl fields.
To avoid gating every F5 behind a spinner, a lastSync timestamp is
now persisted to localStorage whenever settings are applied. On reload,
InitialSyncGate checks this: if present, render immediately from
localStorage and let NostrSync hot-swap remote changes in background.
If absent (new browser, cleared storage), show the spinner until
settings load.
Initial sync applied the theme mode (e.g. 'custom') from encrypted
settings but not the customTheme config (colors, fonts, background),
so the theme appeared broken on first login requiring manual setup
which also triggered an unwanted kind 16767 publish.
Set hasSubHeader on LetterComposePage so the MobileTopBar uses a flat
rect instead of the down-arc variant, preventing the 20px arc overhang
from painting over the LetterEditor picker panel.
Introduce a /follow/:npub deep link that auto-follows a user when
visited by a logged-in user, or presents an immersive business card
with a 'Follow on Ditto' CTA for logged-out visitors. The page applies
the target user's profile theme, renders their feed with infinite
scroll, and uses the same banner/avatar/arc styling as the main profile.
Add a FollowQRDialog that generates a themed QR code for the follow
URL. The QR colors are derived from the active theme: primary color
for modules (with contrast-safe darkening/lightening), and background
color for the QR background. Foreground text color is used when it is
colorful and offers significantly better contrast.
Surface the QR dialog from: own profile page (top-level button),
profile more menu, desktop sidebar account popover, and mobile drawer.
Custom emoji images with natural dimensions <= 16x16 now render with
image-rendering: pixelated to preserve crisp pixels instead of blurring.
Also consolidates 6 direct <img> sites to use the shared CustomEmojiImg
component so all custom emoji rendering benefits from this behavior.
The onboardingDone flag can be true on inconsistent accounts where the
user never actually hatched an egg. Now the ceremony check always waits
for companions to load and inspects their real stages:
- Any baby/adult exists: skip ceremony, auto-fix flag if needed
- Only eggs exist: ceremony with existing egg (regardless of flag)
- No companions resolved: ceremony creates a new egg
A ceremonyCheckDone flag prevents the effect from re-firing as
companion data updates during normal use.
The ceremony was triggered whenever onboardingDone was false, without
waiting for companion data to load. This caused a new egg to be
published on every page visit/refresh for users mid-onboarding.
Now the decision tree waits for companions to load before deciding:
- No profile / no pets: ceremony creates a new egg (brand new user)
- Has baby/adult: skip ceremony, auto-fix onboardingDone flag
- Has only eggs: reuse an existing egg via existingCompanion prop
- Stale pet references: treat as new user
The chosen egg is locked in a ref so mid-ceremony refreshes don't
switch eggs or create duplicates.
Portal the first-time hatching ceremony to document.body with z-[100],
matching the subsequent hatch ceremony implementation. The overlay was
previously rendered inline inside the center column's stacking context
(relative z-0), which prevented its fixed z-50 from painting over the
sibling RightSidebar.