useZaps was fetching all kind 9735 events for every post to count zaps
and sum sats. NIP-85 already provides these stats via useEventStats.
Stripped useZaps down to payment flow only. Deleted unused ZapButton
component.
useEventStats now consumes useNip85EventStats instead of having its
own inline NIP-85 query. This eliminates duplicate kind 30383 queries
for every post on the feed — both hooks now share the same TanStack
Query cache entry.
useEventStats was always firing a second query for reactions, zaps,
and quotes even when NIP-85 stats were found. With nip85OnlyMode
enabled this fetched hundreds of events across all relays for no
reason. Now returns NIP-85 data directly without a second query.
useFollowedProfiles was fetching kind 0 events for all 435 followed
users via useAuthors on every page load, just to enable client-side
search matching. This sent a massive query to all relays unnecessarily.
Instead, useSearchProfiles now uses the already-cached follow list
pubkeys (from useFollowList) to sort relay search results, putting
followed users first without any extra network requests.
NostrBatcher is a transparent proxy that wraps NPool and intercepts
.query() calls. When it detects a batchable single-item filter pattern,
the request is held for a microtask. If more queries with the same
pattern arrive in the same frame, they're combined into one REQ.
Client code doesn't need to know batching exists -- it calls
nostr.query() as usual. The proxy is provided as the 'nostr' object
in NostrContext, so all existing hooks get automatic batching.
Batchable patterns:
- { kinds: [0], authors: [pubkey] } -> profiles
- { ids: [id] } -> events by ID
- { kinds: [7], authors: [user], '#e': [id] } -> reactions
- { kinds: [k], authors: [a], '#d': [d] } -> NIP-85 stats, etc.
- { kinds: [...], '#e': [id] } -> interaction events (zaps, etc.)
- Multi-filter arrays where all filters reference the same #e/#q ID
Also removes manual pre-caching from useFeed (fetchAndCacheAuthors,
fetchAndCacheEventStats) which is no longer needed, and fixes
useComments queryKey instability.
Portal the dropdown to document.body using createPortal so it escapes
the modal's overflow-hidden and stacking context. Positions are now
calculated in viewport coordinates with position:fixed.
Mentions now trigger the Edit/Preview toggle. Compose preview uses
NoteContent directly for link previews and embeds instead of rendering
them separately, matching how real notes display. LinkPreview falls back
to a domain+favicon card when OEmbed data is unavailable. Added
disableEmbeds prop to NoteContent for future use.
The effect-based useBatchEventStats hook was unreliable due to race
conditions with Feed re-renders and abort cycles. Instead, fetch kind
30383 stats directly in useFeed's queryFn alongside author prefetching,
seeding the 'event-stats' cache so useEventStats resolves instantly.
Search autocomplete now prioritizes followed profiles via client-side
matching merged ahead of relay results. A following badge (UserRoundCheck)
appears on avatars in both the sidebar dropdown and search page accounts
tab. The compose box supports @mentions with an inline autocomplete
dropdown that inserts nostr:npub1 URIs and adds p tags on publish.
Link previews now fetch from a dedicated link preview service (default:
https://fetch.ditto.pub/link/{url}) that returns OEmbed JSON, instead of
fetching raw HTML through a CORS proxy and parsing OG tags client-side.
- Add linkPreviewUrl config with RFC 6570 URI template support
- Rewrite useLinkPreview to consume OEmbed responses
- Update LinkPreview to render OEmbed fields (thumbnail_url, provider_name, etc.)
- Use ExternalFavicon component for link preview favicons (consistent with rest of app)
- Add linkPreviewUrl setting to Advanced Settings UI
- Generalize faviconUrl.ts into templateUrl() with {url} variable support
The old DomainFavicon component had a multi-step fallback chain (HTML scraping,
direct URL guessing, then the configured provider) which meant the configurable
setting was almost never reached. The new ExternalFavicon component uses the
configured favicon URL template directly via RFC 6570 URI templates.
- Rename config field faviconProvider -> faviconUrl
- Add faviconUrl() utility with uri-templates for RFC 6570 support
- Remove local Favicon component from ProfileRightSidebar
- Update all consumers to use ExternalFavicon
The mew:authorCache localStorage layer and the useAuthors-based batch
prefetch in Feed.tsx are no longer needed — useFeed now pre-caches all
author profiles (kind 0) directly into the query cache during feed
fetching. This removes ~200 lines of redundant caching infrastructure.
Adds a fetchAndCacheAuthors helper inside useFeed's queryFn that batch-fetches
kind 0 metadata for all authors in a feed page and seeds the ['author', pubkey]
query cache. This eliminates redundant per-author relay queries when useAuthor
is called to render post cards. All three feed branches (communities, follows,
global) now pre-cache authors. The communities branch reuses its existing NIP-05
metadata fetch to avoid a duplicate relay round-trip.
Moved the Suspense boundary from App.tsx (which wrapped the entire app)
into MainLayout around just the Outlet and right sidebar. The left
sidebar, mobile nav, and mobile top bar now persist while lazy page
chunks load. A skeleton fallback fills the content area during loading.
MainLayout was rendered inside each page component, causing the entire
layout (sidebar, mobile nav, drawer) to be destroyed and recreated on
every route change. This made navigations feel like full page refreshes.
Restructured to use a React Router layout route with <Outlet />, so
MainLayout persists across navigations and only the page content swaps.
Pages that need custom layout options (showFAB, rightSidebar, etc.) use
the new useLayoutOptions hook to configure the persistent layout.
Use getProfileUrl() for all profile navigation instead of the static
/profile path. Compose box avatar, sidebar Profile link, sidebar account
popover, and mobile drawer all now link directly to the user's profile.
The /profile route becomes a redirect for backward compatibility.
- Add webxdcMeta.ts utility to extract name from manifest.toml and
icon (icon.png/icon.jpg) from the .xdc ZIP using fflate
- Upload extracted icon to Blossom during file attach
- Add webxdc_name and webxdc_icon properties to imeta tags
- Parse webxdc_name/webxdc_icon in NoteCard and PostDetailPage
- Show app icon and name in WebxdcEmbed launch card, remove filename
and 'Stateful · shared session' text
Shows '<cards icon> <name> shared a deck' above magic deck cards in the
feed, matching the pattern of treasure/stream action headers. Suppressed
when the card is already showing a repost header to avoid double headers.
Replace Image icon with Palette for the list/visual toggle button.
Add MagicDeckContent to PostDetailPage kind dispatch so deck events
show their full content when viewed as a post detail (naddr route).
Replace ScrollArea with overflow-y-auto for reliable scrolling in both
list and visual views. Clicking any card (in either view) opens a
full-screen lightbox showing the card image from Scryfall at large size,
with prev/next navigation via arrows, keyboard, and swipe gestures.
Shows card name and foil indicator in the lightbox header.
Decklist box now has a List/Visual toggle in the header. Visual mode
renders a 4-column grid of card images loaded from Scryfall's API using
set/collector_number for exact printings or card name as fallback.
Includes quantity badges, foil shimmer overlay, and graceful fallback
to card name text when images fail to load.
Merge format badges, card count, and sideboard count onto one line.
Replace expand/collapse with a ScrollArea box (max 240px) containing
the full decklist with sideboard inline below a separator header.
Replace Layers icon with custom CardsIcon (stacked cards SVG) across all
navigation and settings UIs. Filter out events tagged t:unlisted for kind
37381 (magic decks) and t:hidden for kind 37516 (geocaches) in NoteCard.
Support kind 37381 addressable events for MTG deck sharing per NIP-MG spec.
Includes MagicDeckContent component with card list rendering, format/archetype
badges, commander/companion display, sideboard, foil indicators, and
expand/collapse for large decks. Adds /decks route and sidebar navigation.
- Create useWebxdc hook bridging the Webxdc API with Nostr kind 4079 state updates
- Create WebxdcEmbed component with launch-on-click UX for sandboxed iframe rendering
- Update ComposeBox to accept .xdc files and generate imeta tags with webxdc UUID
- Update NoteCard and PostDetailPage to detect and render webxdc attachments
- Skip .xdc URLs in NoteContent to avoid rendering them as raw links
Stream events (kind 30311) now render properly in mixed feeds via NoteCard
with inline LiveStreamPlayer for live streams, thumbnail for ended/planned,
title + summary clickable to open stream details, and 'is streaming' /
'streamed' action headers. LiveStreamPlayer pauses when scrolled out of view.
Show a dismissible 'Muted Content' overlay instead of rendering muted
posts directly when navigating to their detail page. Follows the same
pattern as ContentWarningGuard — children are not mounted until the
user clicks 'Show Anyway', so media and nested queries are deferred.
Cache parsed mute items in localStorage (like the follow list pattern) so
muted content is filtered immediately on page refresh without waiting for
the relay fetch + decrypt cycle. The cache is written on initial sync,
after each successful decrypt, and eagerly on add/remove mutations.