This reverts the codebase back to the state at commit 7419757.
Reverted 1 commit(s):
- 7460586: Reduce feed page size from 15 to 10
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
When the author pubkey was available and their NIP-65 write relays
were also tried, the error message now says so instead of only
mentioning "your connected relays".
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
When an event can't be found on the user's relays or relay hints, and
an author pubkey is available (nevent author hint or naddr pubkey),
the hooks now:
1. Fetch the author's kind 10002 (NIP-65) relay list from connected relays
2. Extract their write relays (where they publish content)
3. Query those relays for the target event
This runs automatically as step 3 in both useEvent and useAddrEvent,
before the user ever sees the "Event not found" screen. For naddr
events, the author pubkey is always available; for nevent, it uses the
embedded author hint when present.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Removed suggested relay quick-pick buttons entirely
- Wrapped "Try another relay" in a Collapsible (collapsed by default)
- Event IDs and pubkeys now show full value with CSS truncation at
the end, each with a click-to-copy button (checkmark feedback)
- When author info is embedded (nevent author hint or naddr pubkey),
fetches and displays their profile inline — avatar, display name,
and NIP-05, linked to their profile page
- Passed nevent author field through from NIP19Page to PostDetailPage
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Added wss://cache2.primal.net/v1 as a read-only app relay
- Replaced generic 404 on PostDetailPage/AddrPostDetailPage with a
contextual "Event not found" view that shows:
- Event details (ID for regular events; kind/author/d-tag for naddr)
- Relay hints that were already tried
- A text input to try fetching from a custom relay URL
- Quick-pick buttons for well-known public relays (filtered to
exclude relays already tried)
- On successful relay retry, the event renders in place without
navigation
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This reverts the codebase back to the state at commit 7d2c788.
Reverted 1 commit(s):
- 409d443: Fix event detail 404 by retrying instead of caching empty results
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The NPool's 500ms eoseTimeout causes event-by-ID queries to resolve
before slower relays respond. Previously, useEvent returned null on
the first miss and the page immediately showed a 404.
Now:
- useEvent/useAddrEvent throw on miss so TanStack Query retries
(3 retries with exponential backoff: 2s, 4s, 8s)
- PostDetailPage shows the skeleton while retrying (checks isFetching)
- Only shows 404 after all retries are exhausted
- Feed cache seeding from previous commit still provides instant
navigation for events already seen in the feed
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
When clicking a nevent or naddr link, the NIP19Page was discarding the
relay hints encoded in the identifier. The useEvent/useAddrEvent hooks
would only query the user's configured relays, which may not have the
event — resulting in a 404 even though the event appeared in the feed.
Now relay hints from nevent/naddr identifiers are passed through to the
hooks, which first try the user's relays, then fall back to querying
the hint relays directly via nostr.group().
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This reverts the codebase back to the state at commit 9ca82c3.
Reverted 4 commit(s):
- 94b43ac: Skip already-cached pubkeys in useAuthors batch query
- ee69816: Adopt Agora's faster feed loading pattern
- 0f630fc: Use Agora's useAuthor pattern: pool race + losers bracket fallback
- 7eafd14: Batch useAuthor and useEventStats to reduce concurrent REQs
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Only send uncached pubkeys to the relay. As feed pages load, profiles
fetched by earlier pages are already in the ['author', pubkey] cache,
so subsequent pages only request the new authors they introduce.
Also seeds empty entries for pubkeys with no kind 0 result to avoid
re-fetching them on the next page load.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add useInfiniteScroll hook using native IntersectionObserver (400px rootMargin)
instead of react-intersection-observer library
- Rewrite useFeed: 5s timeout (was 8s), AbortSignal.any pattern, placeholderData
to prevent flicker, cleaner filter construction with spread
- Rewrite Feed.tsx to use useInfiniteScroll ref instead of useInView
- Update ProfilePage.tsx to use useInfiniteScroll for both feed and likes tabs
- Update useProfileFeed: 5s timeout, placeholderData, cleaner filter construction
- Remove react-intersection-observer dependency (replaced by native hook)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Replace the custom batching layer with Agora's proven approach:
1. Fast path: pool.query() races all relays via EOSE timeout
2. Losers bracket: if pool returns empty, query each relay
individually and resolve on the first hit
The useAuthors batch hook in Feed.tsx still prefetches profiles
in bulk, seeding the cache. Individual useAuthor calls hit cache
first (5min staleTime) and only miss for uncached pubkeys.
Keep the stats batching (useEventStats) since that genuinely
reduces 2N REQs to 2 total per flush window.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Both hooks now collect requests within a 50ms window and flush them
as a single relay query, matching the pattern used in Agora.
useAuthor: Instead of N individual kind:0 queries (one per post author),
all pubkeys within a 50ms window are batched into a single query with
authors: [pk1, pk2, ...]. Results are seeded into the individual
['author', pubkey] cache entries.
useEventStats: Instead of 2 REQs per post (e-tag + q-tag), all event
IDs within a 50ms window are batched into 2 total queries. Results are
grouped by referenced event ID and seeded into individual caches.
Also restored the correct reqRouter pattern (all read relays with
eoseTimeout race) matching Agora's approach.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The deduplication was comparing raw URL strings, so
'wss://relay.ditto.pub' and 'wss://relay.ditto.pub/' were treated
as different relays, each getting their own WebSocket connection.
- Normalize URLs (lowercase, strip trailing slashes) before comparing
- Strip trailing slashes from NIP-65 relay URLs when syncing
- Deduplicate user-only relay lists too, not just merged lists
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The real problem: nostr.relay() and nostr.group() each open brand new
WebSocket connections outside the pool. Every hook that called these
was creating duplicate connections to relays the pool already had open.
Fixed by routing all queries and subscriptions through the pool (nostr)
directly, which reuses its internal connections:
- useStreamPosts: nostr.relay('wss://relay.ditto.pub') → nostr
- useStreamKind: nostr.relay('wss://relay.ditto.pub') → nostr
- useSearchProfiles: nostr.relay('wss://relay.ditto.pub') → nostr
- useFollowActions: nostr.group([...]) → nostr
- FollowPackDetailContent: nostr.group([...]) → nostr
Also reverted the hacky relay cleanup code in NostrProvider since
it was trying to solve the wrong problem.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Detect when relays are removed from the effective relay list
- Proactively close connections to removed relays to prevent duplicates
- Fixes issue where app relays and user relays would both stay connected
- Maintains pool as a ref but intelligently manages relay connections
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Recreate NPool when relay list changes instead of only on initial mount
- Close old connections before creating new pool with updated relays
- Properly deduplicate app relays and user relays from NIP-65
- Add console logging for debugging relay connection changes
- Fixes issue where NostrSync would update relay list but NPool wouldn't reconnect
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add useEffect cleanup to close NPool connections on unmount
- Prevents connection leaks during hot reloads and component remounts
- Calls pool.close() to properly cleanup all active relay WebSockets
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Simplified architecture - removed the fake "local relay" approach entirely:
- Deleted LocalRelay.ts (didn't work with NPool)
- Hooks now check IndexedDB directly before network queries
- DB version bumped to v3 to trigger clean migration
How it works now:
1. useAuthor checks eventStore.getManyProfiles() first
2. useFollowList checks eventStore.query() first
3. useFeed checks eventStore.query() first
4. If cached: return instantly + background refresh
5. If not cached: fetch from network
6. Events stream to IndexedDB as they arrive
This is the standard offline-first pattern used by production apps.
NOTE: Users may need to hard refresh (Ctrl+Shift+R) to clear old build cache.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Complete rewrite of caching approach - check IndexedDB BEFORE network queries for instant loads.
Key changes:
- Removed fake "local relay" approach (doesn't work with NPool)
- useAuthor: Check eventStore.getManyProfiles() first, return cached instantly
- useFollowList: Check eventStore.query() first, return cached instantly
- useFeed: Check eventStore.query() first, return cached instantly
- All hooks: Fire background network refresh after returning cached data
Fixed bugs:
- getKey() now returns correct key type for each store
- Removed debug logging spam
- Proper error handling (silent failures for cache writes)
Result: When data is cached, queries return in <10ms instead of waiting for network.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Complete rewrite of eventStore using Jumble's proven approach. This should eliminate the 2-3 second feed delays.
Key changes inspired by Jumble:
1. **Separate stores per event type**: profiles, contacts, relayLists, events
2. **Direct key lookups**: Profiles keyed by pubkey (not event.id), instant retrieval
3. **Cursor-based queries**: Use openCursor() instead of getAll() for feed events
4. **Batch profile fetching**: getManyProfiles() fetches multiple profiles in one transaction
5. **Replaceable event handling**: Auto-dedupe by pubkey, only keep newest
Performance improvements:
- Profile queries: O(1) direct lookup by pubkey vs O(n) scan
- Multiple profiles: Single transaction vs N separate queries
- Feed queries: Cursor iteration vs loading all events into memory
- Cache hits: <5ms for profiles, <20ms for batch of 20 profiles
New hook:
- useBatchAuthors(): Batch-fetch profiles for feed authors (cache-first, then network)
This architecture is proven in production by Jumble and should provide instant loads when data is cached.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Added comprehensive debug logging to track query performance:
- EventStore: Log filter details and duration for each query
- useAuthor: Log query duration and whether profile was found
- NostrProvider: Log event count from each relay
This will help identify where delays are occurring:
- Are queries fast but waiting for network?
- Is IndexedDB slow?
- Are profiles missing from cache?
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Fixed broken query implementation that was causing timeouts/delays:
- Simplified query logic with proper async operation tracking
- Added debug logging to track query performance
- Added relay event count logging
The previous implementation had race conditions in completion tracking when processing multiple authors/kinds, causing queries to never resolve or timeout.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Major performance improvement for local relay queries - uses IndexedDB indexes instead of loading all events into memory.
Changes:
- EventStore.query(): Use indexes (id, pubkey, kind, kind_pubkey composite) for fast lookups
- Reduce eoseTimeout from 500ms to 10ms for near-instant cached queries
- Extract matchesFilter() and finalizeQueryResults() helper methods
Query optimization strategies:
1. IDs query: Direct objectStore.get() for each ID (fastest)
2. Kind+Authors: Use kind_pubkey composite index
3. Authors only: Use pubkey index
4. Kinds only: Use kind index
5. Complex queries: Fallback to getAll() + filter
Performance impact:
- Before: getAll() loads thousands of events, filters in JS (~100-500ms)
- After: Index lookups return only matching events (~5-20ms)
- Total query time: <30ms for cached data (10ms eoseTimeout + ~15ms IndexedDB)
This makes page refreshes feel instant when data is cached.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Major improvement to the local relay caching strategy - events now stream directly into the cache as they arrive from remote relays, eliminating the need for background syncing.
Changes:
- NostrProvider: Intercept relay.req() to cache events as they stream in
- Remove useEventSync hook (no longer needed)
- useAuthor: Don't throw/retry when profile not found
- Documentation: Updated to reflect streaming cache strategy
How it works:
- Every remote relay's req() method is wrapped to intercept events
- Each event is cached to IndexedDB as it arrives (fire and forget)
- Cache builds organically as you browse the app
- Posts cached when viewing feed
- Profiles cached when viewing author info
- Replies cached when opening threads
Benefits:
- Profiles load instantly on refresh (already cached from previous views)
- No background polling/syncing overhead
- Cache always reflects what you've actually seen
- Zero configuration required
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Implements a local relay system that automatically caches Nostr events in IndexedDB for instant loading and offline access. Inspired by the mi project's event storage architecture.
Features:
- EventStore: IndexedDB wrapper with query support and composite indexes
- LocalRelay: Relay interface implementation backed by IndexedDB
- Automatic integration: Local relay is transparently included in all queries via NPool
- Event syncing: useEventSync hook for background event synchronization
- Settings UI: Display cache stats with export/import/clear functionality
The local relay provides zero-latency queries by responding instantly with cached events while simultaneously fetching fresh data from remote relays. All published events are automatically cached for future queries.
Cache displayed in Settings > Relays as "local://indexeddb" with event count and management controls.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This reverts the codebase back to the state at commit 9a2b3a2.
Reverted 1 commit(s):
- 629f163: Implement two-phase profile loading from agora
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add PROFILE_RELAYS constant with known reliable profile relays
- Add parseAuthorEvent helper for consistent parsing
- Fast path: pool races all relays with 500ms EOSE timeout
- Slow path: race individual profile relays, resolve on first hit
- useAuthors also gets fallback for missing pubkeys
- Cache seeding continues to make individual useAuthor calls instant
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Redesigned RelayListManager with cleaner, simpler layout
- Added nos.lol to the default app relays list
- App relays now show with simple toggle switch
- User relays section with empty state when no relays configured
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add `useAppRelays` boolean option to AppConfig
- Define APP_RELAYS constant with default app relays:
- wss://relay.ditto.pub
- wss://relay.primal.net
- wss://relay.damus.io
- Update NostrProvider to use getEffectiveRelays() which merges
app relays with user relays when useAppRelays is true
- Redesign RelayListManager with two sections:
- App Relays: Read-only list with enable/disable toggle
- Your Relays: User's personal NIP-65 relay list (editable)
- User relays now stored separately from app relays in config
- Default config starts with empty user relays and useAppRelays=true
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This reverts the codebase back to the state at commit dff9ec4.
Reverted 1 commit(s):
- 21d9541: Increase timeout for parent event fetching in reply detail view
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add optional timeout parameter to useEvent hook (defaults to 5000ms)
- Set 30-second timeout for parent event fetch in ParentNote component
- Ensures parent events load reliably even on slow relays
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Parse the `a` tag (e.g. `37516:pubkey:d-tag`) into AddrCoords
- Fetch the referenced geocache event via useAddrEvent
- Display the geocache's `name` tag as a clickable link (with chest icon)
that navigates to the geocache's naddr detail page
- Fall back to showing the d-tag identifier if the event can't be fetched
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add TreasureHeader component that renders "<chest> <name> hid a treasure"
for geocache listings (kind 37516) and "<chest> <name> found a treasure"
for found logs (kind 7516), styled like the existing RepostHeader
- Create FoundLogContent component for rendering kind 7516 found log events
with geocache reference badge, verified badge, log text, and images
- Add kind 7516 detection to NoteCard and PostDetailPage content dispatch
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Create ChestIcon component using the chest SVG from the treasures project
- Replace all MapPin usages for Treasures in sidebar, feed settings, treasures page, and router
- Remove unused @lucide/lab dependency (chest SVG is inlined)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Split Treasures into two sub-kinds: Geocaches (kind 37516) and Found Logs (kind 7516)
- Add `showTreasureGeocaches`, `showTreasureFoundLogs`, `feedIncludeTreasureGeocaches`, `feedIncludeTreasureFoundLogs` to FeedSettings
- Extend ExtraKindDef with optional `subKinds` array for nested settings
- Update FeedSettingsForm to render nested sub-kind toggles under parent entries
- Update useStreamKind to accept single kind or array of kinds
- Create TreasuresPage that filters kinds based on sub-kind settings
- Add getPageKinds() utility for resolving sub-kind-aware page kinds
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Replace the show-on-hover scrollbar styles with scrollbar-width: none
and ::-webkit-scrollbar { display: none } so no visual scrollbar
ever appears on the feed or anywhere else in the app.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Add e.stopPropagation() to hashtag, nostr-link, and mention Links
inside NoteContent so clicks don't bubble up to the parent card's
onClick handler that navigates to the post detail page.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Updated sticky headers on NotificationsPage, SettingsPage, BookmarksPage,
HashtagPage, KindFeedPage, WalletPage, PostDetailPage, and PlaceholderPage
to use h-20 for consistent height with the compose form, and removed the
bottom border-b from all of them.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add compact prop to NoteCard to hide action buttons for embeds
- Pass compact to NoteCard in EmbeddedNaddr for follow pack embeds
- Remove "Starter Pack" pill from FollowPackContent
- Move member count pill to the left of the avatar stack on bottom row
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
When an naddr for a follow pack (kind 39089/30000) appears embedded in
a note, render it using the same NoteCard component used in feeds
instead of a separate EmbeddedNaddrCard. This ensures consistent
presentation: same FollowPackContent with title, badges, description,
cover image, and avatar stack in both contexts.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
When viewing a follow pack directly (naddr), show the full detail view
with member list, individual follow buttons, Follow All, and copy link.
The compact FollowPackContent is still used in NoteCard feeds.
Uses the shared PostDetailShell for consistent back button/header.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add kind-based content dispatch to PostDetailContent (same as NoteCard):
polls, geocaches, colors, follow packs, and text notes
- Add AddrPostDetailPage that fetches by addr coords and reuses the
same PostDetailShell/PostDetailContent
- Route all naddr identifiers through AddrPostDetailPage in NIP19Page
- Delete AddrDetailPage and FollowPackDetailPage (parallel renderers)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add kind 39089 (starter packs) to EXTRA_KINDS with sidebar/feed toggles
- Create FollowPackContent component for NoteCard rendering with title,
description, member count badge, cover image, and avatar stack preview
- Add kind dispatch in NoteCard for 39089 and 30000
- Add /packs route using KindFeedPage
- Add PartyPopper icon in sidebar and feed settings
- Default showPacks to true so it appears in sidebar
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>