Both EmojiShortcodeAutocomplete and MentionAutocomplete had identical
logic for fixed viewport positioning with viewport-flip, scroll/resize
dismissal, and portal rendering. Extract into a shared hook to reduce
duplication and centralize the positioning behavior.
- Skip appending protocol:nostr if the resolved filter already contains it
- Add comment explaining why the 2-element prefix key correctly invalidates
the full 5-element useTabFeed query key via TanStack prefix matching
8. Extract TrendSparkline to its own file so TrendingWidget doesn't
depend on the old RightSidebar (re-export kept for compat)
9. Widget definition lookup uses a pre-built Map instead of linear scan
10. SortableWidget wrapped in React.memo to skip re-renders when only
sibling state changes (picker open, other widget collapse)
11. handleDragEnd computes indices from the updater's current array
instead of closing over sortableIds (eliminates stale closure risk
if a query refetch re-renders mid-drag)
5. FeedWidget now scopes queries to followed authors when logged in,
falls back to global when logged out, and requests exact limit
6. BlueskyWidget uses its own useQuery instead of sharing the infinite
query with BlueskyPage (separate query key, single page, no memory leak)
7. WikipediaWidget uses openUrl() instead of <a target=_blank> which
silently fails inside Capacitor WKWebView on iOS
1. Wrap each widget in ErrorBoundary so one crash doesn't kill the sidebar
2. Resize uses local state during drag, commits to config only on pointerup
(was hammering localStorage at 60fps)
3. AI chat messages persist in module-level cache across collapse/expand
(collapsing previously destroyed the conversation)
4. StatusWidget catches rejected promises from mutateAsync and shows
destructive toast instead of silently failing
Replace the empty right sidebar placeholder with a user-configurable widget
system. Users can add, remove, reorder, collapse, and resize widgets via
drag-and-drop and a picker dialog. Config persists in localStorage (same
pattern as sidebarOrder) and syncs via encrypted settings.
v1 widgets: Trending Tags, Blobbi (mini pet), Status (NIP-38), AI Chat,
Wikipedia (featured article), Bluesky (trending posts), and feed widgets
for Photos, Music, Articles, Events, and Books.
Defaults: Trending + Blobbi for fresh installs. Desktop-only (hidden below
xl breakpoint). Profile pages retain their dedicated ProfileRightSidebar.
Detect lnbc/lntb/lnbcrt/lntbs invoices (with optional lightning: prefix)
in note text and render them as interactive cards with a theme-aware QR
code, decoded amount, copy button, and Open in Wallet action.
- Add lightning-invoice token type to NoteContent tokenizer
- Create LightningInvoiceCard with tap-to-expand square QR, cqw-scaled
amount text, and responsive layout
- Extract shared theme-aware QR color logic into src/lib/qrColors.ts
(deduplicate from FollowQRDialog)
Use capacitor-secure-storage-plugin to persist login credentials
(nsec keys) in iOS Keychain / Android KeyStore instead of plaintext
localStorage. Web behavior is unchanged. Existing native users are
auto-migrated on first launch: if secure storage is empty but
localStorage has data, it is moved over and the plaintext copy is
removed.
Also ignore ios/ directory in ESLint (Capacitor-generated files).
Upgrade to the new version that includes the NLoginStorage interface
and storage/fallback props on NostrLoginProvider for pluggable async
storage backends (e.g. Capacitor Secure Storage).
- Add resolve.dedupe for react/react-dom to prevent dual-React issues
- Update NoteContent tests to use async findBy* queries since the
provider now always awaits storage initialization
SandboxFrame's virtual script serving intercepted /webxdc.js and served
the empty placeholder content before resolveFile was ever called. The
dynamically generated bridge script (which embeds selfAddr etc.) was
never reaching the iframe.
Move bridge serving and HTML injection into resolveFileWithBridge so
the content is served from bridgeScriptRef after onReady populates it.
The sandbox frame was sending init immediately and calling onReady
concurrently, so fetch requests arrived before the archive was
downloaded and unzipped. Now onReady is awaited before init is sent,
matching the original Webxdc behavior.
Extract duplicated sandbox protocol logic from NsitePreviewDialog and
Webxdc into a single SandboxFrame component. Shared utilities (MIME
types, base64, HTML injection, JSON-RPC types) move to src/lib/sandbox/.
Add configurable sandboxDomain to AppConfig so the iframe.diy domain
can be overridden via ditto.json, preparing for native Capacitor
implementations.
Strip unused console/navigation/error RPC from previewInjectedScript,
leaving only the /index.html path normalization.
The Blobbi collection was previously discovered via the profile's has[] tag
list, meaning any blobbi whose d-tag was missing from that secondary index
would be invisible to the user despite existing on the relay.
Now useBlobbisCollection() without args queries all kind 31124 events by
author + ecosystem namespace tag — the user authored these events, so that
is the source of truth. The profile.has[] list is still used for selection
ordering preference, but no longer gates discovery.
The dList parameter remains available for targeted fetches (e.g. the
companion layer only needs one specific blobbi).
Adds a Mail-icon menu item in the profile more menu for other users'
profiles. Navigates to /letters/compose?to={npub} so the recipient is
pre-filled, matching the same flow used by the notification reply button.
Only show the delivery method radio group when push notifications are
enabled. Update the persistent option description to explain it is for
devices that don't support push notifications (e.g. GrapheneOS).
Default to push mode (no foreground service). Persistent mode with
the always-on background polling service is opt-in via the new
Delivery Method section in notification settings.
- Add notificationStyle ('push' | 'persistent') to EncryptedSettings
- Show radio group in NotificationSettings on native platforms
- Pass notificationStyle through Capacitor plugin to SharedPreferences
- DittoNotificationPlugin starts/stops foreground service on style change
- MainActivity only starts service on launch when style is persistent
- Re-enable unread polling on native when push mode is active
FollowPackDetailContent, TeamSoapboxCard, and InitialSyncGate all had
handleFollowAll implementations that queried kind 3 directly (bypassing
fetchFreshEvent) and rebuilt the tag array with only p-tags, silently
dropping all non-p-tags (relay hints, petnames, etc.). They also did
not pass prev for published_at preservation.
Align all three with the safe pattern already used in FollowPage and
useFollowActions.
The /follow route now accepts naddr1 identifiers for follow packs
(kind 39089) and follow sets (kind 30000) in addition to npub/nprofile.
Renders an immersive fullscreen layout with pack info hero, avatar
stack, big Follow All CTA with status indicator, and Feed/Members
tabs using the standard SubHeaderBar arc.
Follow All uses the safe fetch-fresh -> modify -> publish pattern
with prev for published_at preservation.
Shared components (PackFeedTab, MemberCard, MemberCardSkeleton) and
parsePackEvent are reused from FollowPackDetailContent and packUtils.
Also fixes SubHeaderBar tab indicator positioning when innerClassName
centers the tab container (adds containerOffset + ResizeObserver for
layout-dependent recalculation).
- Remove unused 'authors' parameter from useInfiniteHotFeed
- Extract inline query from Feed.tsx into useCuratedDittoFeed hook
- Use content-based fingerprint for query key instead of list length
- Add error state handling so curator fetch failure shows empty state
instead of infinite skeletons for first-time visitors
- Move hardcoded curator pubkey to AppConfig (curatorPubkey) so it
can be overridden via ditto.json without a code change
- Remove LANDING_KINDS/LANDING_WEBXDC_FILTER from Feed.tsx (now in hook)
Split createBitcoinTransaction into buildUnsignedPsbt, signPsbtLocal,
and finalizePsbt so the signing step can be delegated to any signer.
Introduce BtcSigner interface and three extended signer classes:
- NSecSignerBtc: local Taproot signing with the raw private key
- NBrowserSignerBtc: delegates to window.nostr.signPsbt() (NIP-07)
- NConnectSignerBtc: sends sign_psbt RPC over NIP-46 relay channel
useCurrentUser now constructs Btc-extended signers instead of base
ones. signerWithNudge forwards signPsbt when present on the wrapped
signer. SendBitcoinDialog uses the new useBitcoinSigner hook instead
of useNsecAccess, enabling sending from all login types.
Implement sending Bitcoin transactions from the wallet page. The send
flow uses a 3-step dialog: form entry, confirmation review, and success
result. Only available for nsec logins since extension/bunker signers
don't expose the raw private key needed for Taproot signing.
Fixes over the reference implementation:
- Send Max correctly subtracts estimated fees
- Address validation via bitcoinjs-lib (checksum + format)
- Fee estimation accounts for actual output count (1 vs 2)
- Confirmation step before broadcast (irreversible action)
- All API calls use mempool.space (consistent with existing code)
- Success links to in-app NIP-73 tx detail page
New files:
- src/hooks/useNsecAccess.ts: extract private key from nsec login
- src/components/SendBitcoinDialog.tsx: 3-step send dialog
New functions in src/lib/bitcoin.ts:
- fetchUTXOs, getFeeRates, broadcastTransaction
- validateBitcoinAddress, estimateFee, maxSendable
- createBitcoinTransaction (PSBT construction + Taproot signing)
- npubToBitcoinAddress, btcToSats
The link was using inline-flex items-center with child spans at different
font sizes (text-sm 'transaction' + text-xs monospace hash). Flexbox
center-aligns by box center, not text baseline, causing the smaller text
to appear shifted up. Changed to plain inline text flow so the browser's
natural baseline alignment handles mixed font sizes correctly.
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.