- Repositioned "Replying to @user" to appear between the header and content
- Matches Ditto's UI pattern for better visual hierarchy
- Hide reply context when post is a repost (repost takes priority)
- Added click event stop propagation to reply context link
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The error occurred because the AccountSwitcher component was returning null while the DropdownMenu was still mounted and open. This caused React to throw a minified error #300 about invalid elements.
The fix ensures the dropdown menu is closed before removing the login, preventing the component from being unmounted while still rendering its dropdown content.
Changes:
- Added controlled state for dropdown menu (isOpen/setIsOpen)
- Created handleLogout function that closes dropdown before removing login
- Used setTimeout to ensure dropdown closes before state update
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Remove e tag with mention marker from quotes (was causing reply behavior)
- Only use q tag for NIP-18 quote reposts
- Automatically append nostr:nevent URI to quote content
- Quote posts now publish as standalone kind 1 events, not replies
- Reorganized imports for better readability
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Created shared feedUtils.ts with FeedItem type and parseRepostContent function
- Updated useFeed to import from shared utilities (removed duplication)
- Updated useProfileFeed to unwrap kind 6 reposts into FeedItems
- ProfilePage now properly displays reposts with "reposted by" header
- Fixed raw JSON issue by parsing repost content and showing original event
- Maintains same repost unwrapping logic across all feeds
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Added kind 6 to profile feed query kinds
- Updated filterByTab to always show reposts in 'posts' tab
- Reposts now appear in user profiles alongside their posts
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- When quotedEvent is present, don't pass event as replyTo in ComposeBox
- This ensures quote posts are published as kind 1 with q tags, not replies
- Repost (kind 6) was already working correctly
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Quoted events from the quote button cannot be removed
- X button still available for detected embeds typed in content
- Provides clearer UX for intentional quote posts vs detected links
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Added mb-3 to embed containers for spacing before toolbar
- Ensures proper visual separation between embedded content and action buttons
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Changed embed margin from mt-2 to mt-4 for better visual separation
- Applies to both quoted events and detected embeds (nevent, naddr, links)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Added quotedEvent prop to ComposeBox for quote post support
- Auto-detect and display embeds: nevent, note, naddr, and link previews
- Add remove button (X) to clear any embed from compose
- Quote posts open ReplyComposeModal with quoted event embedded
- Updated RepostMenu to open quote dialog instead of toast
- Added NIP-18 quote tags (q tag) when publishing quotes
- Embeds shown at bottom of compose with EmbeddedNote/EmbeddedNaddr/LinkPreview
- Support for manually typed nostr: URIs in compose content
- Invalidate event stats after quote to update counts
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Created RepostMenu component with two options: Repost and Quote post
- Shows as popover on desktop (above button, aligned left)
- Shows as drawer on mobile with Close button
- Integrated into NoteCard, PostDetailPage, and NotificationsPage
- Implements kind 6 repost functionality
- Quote post shows placeholder toast (to be implemented)
- Includes proper event invalidation to update stats after repost
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Changed PostDetailPage action button numbers from text-xs to text-sm for consistency with ReactionButton
- Added tabular-nums class to all number spans for proper alignment
- Adjusted gap between action buttons from gap-4 to gap-5 for better spacing
- All action button numbers now consistently use text-sm tabular-nums
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Changed all action button icons from size-[18px] (18px) to size-5 (20px)
- Updated icons in NoteCard, PostDetailPage, NotificationsPage, and ReactionButton
- Reduced gap between action buttons from gap-6 to gap-4 for tighter spacing
- Icons affected: MessageCircle, Repeat2, Heart, Zap, MoreHorizontal
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Created canZap() utility function to check if a user has lud16 or lud06
- Updated ZapButton component to use the new helper
- Conditionally render zap button in NoteCard when user can be zapped
- Conditionally render zap button in PostDetailPage when user can be zapped
- Conditionally render zap button in NotificationsPage ActionButtons when user can be zapped
- Updated ProfilePage to use canZap() helper for consistency
- Zap icon now only shows for users who can actually receive zaps
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Add stopPropagation to dialog and drawer overlay/content click handlers to prevent clicks on modals from triggering click events on underlying elements like NoteCard.
This fixes the issue where clicking outside the zap modal would still register as a note click and navigate to the post detail page.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Prevent clicks from closing modals (image gallery, zap dialog, etc.) from bubbling up to NoteCard and triggering unwanted navigation to post details.
Changes:
- Added handleCardClick in NoteCard to check if click originated from modal/dialog elements
- Updated ImageGallery to stop propagation when closing lightbox via backdrop or close button
- Enhanced event handling for download button in lightbox
- Prevents navigation when clicking on dialog overlays, drawer overlays, or modal content
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
When a note is a reply (has e tags) but all p tags are marked as mentions, we now fall back to using the first p tag for the reply context. This handles clients that strictly follow NIP-10 and mark all p tags with appropriate markers.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
When a note has e tags with root/reply markers but the p tags use mention markers, we now fall back to using the pubkey from the e tag (5th element) to determine who is being replied to. This properly handles replies that follow NIP-10 strictly and mark mentions explicitly.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Added rootMargin: '400px' to the intersection observer so it triggers 400px before reaching the bottom. This prevents missing the trigger when scrolling fast and ensures a smoother experience by pre-loading the next page.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Added back the auto-fetch effect that loads page 2 immediately after page 1 finishes loading. This ensures users always have 2 pages of content loaded, and then the infinite scroll ref at the bottom handles loading page 3 and beyond.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Replaced the complex PageBoundary system with the simpler nostr-infinite-scroll skill pattern:
- Single intersection observer ref at the bottom of the feed
- Simplified deduplication logic
- Removed unnecessary pageItems mapping
- Cleaner, more maintainable code that matches proven patterns
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Added rootMargin: '400px' to the useInView hook in PageBoundary to trigger loading the next page 400px before the boundary comes into view. This matches Agora's implementation and ensures smoother infinite scroll behavior.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Re-added the useInView import from react-intersection-observer that was accidentally removed in the previous commit.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The infinite scroll was broken because the PageBoundary component was placed at the top of each page but required pageIndex > 0 and pageIndex === totalPages - 1, which would never trigger correctly.
Changed:
- Moved PageBoundary to the bottom of each page instead of the top
- Removed the pageIndex > 0 condition (no longer needed)
- Updated comments to reflect the new bottom placement
Now when you scroll to the bottom of page 0, it triggers loading page 1. When you reach the bottom of page 1, it triggers page 2, and so on.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The toggle was still reverting because the NostrSync component was
re-syncing old encrypted settings whenever the query refetched.
Root cause:
- When toggle is clicked, it updates config and saves to Nostr
- The encrypted settings query refetches the data
- NostrSync effect runs again with the refetched (old) data
- This overwrites the just-updated local config
Solution:
- Added lastSyncedTimestamp ref to track which settings we've synced
- Only sync if the remote timestamp is newer than what we've synced
- Changed updateSettings call from mutateAsync to mutate (non-blocking)
- This prevents re-syncing the same settings multiple times
The toggle now works correctly without reverting.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The app relays toggle was reverting on first click due to a race condition
between local state and the config context. The issue was caused by:
1. Local state (useAppRelays) was being synced via useEffect
2. When toggle was clicked, both local state and config were updated
3. The useEffect re-ran and reset the local state from config
4. This created a race condition causing the toggle to appear to undo itself
Solution:
- Removed local useAppRelays state entirely
- Use config.useAppRelays directly from context
- Removed the useEffect dependency on config.useAppRelays
- Now only config context manages this state, eliminating the race condition
The toggle now works correctly on first click.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Remove `rootMargin: '800px'` from Feed.tsx PageBoundary component
- Add explicit `threshold: 0` to both Feed.tsx and ProfilePage.tsx
- Infinite scroll now triggers as soon as the page boundary enters viewport
- Fixes issue where short windows would break infinite scroll detection
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This reverts the codebase back to the state at commit cb819e8.
Reverted 2 commit(s):
- e03527b: Fix infinite scroll race condition and add relay logging
- 0635779: Move relay fetching inside queryFn and add debug logging
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Infinite scroll fixes:
- Allow boundaries within 2 pages of end to trigger (not just last page)
- Prevents race where scrolling fast skips page boundaries
- Reduced rootMargin from 800px to 400px for more controlled loading
- Pages can trigger next load even if not technically "last" yet
Profile loading debug:
- Log which relays are being queried in loser's race
- Helps debug why profiles aren't loading (missing relays like nos.lol)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Get effective relays inside queryFn for latest config
- Add console logging to debug loser's race execution
- Log when loser's race starts, which relays respond, and if any still missing
- This will help identify if relays are failing or timing out
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Removed hardcoded PROFILE_RELAYS list
- Loser's race now queries the same relays as the pool
- Gets effective relays from app config (user relays + app relays)
- Queries each relay individually with 5000ms timeout (vs 500ms EOSE in pool)
- Solves issue where profiles exist on app relays (like nos.lol) but not on hardcoded profile relays
- Now covers all relays user has configured instead of arbitrary subset
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Added wss://ditto.pub/relay (matches Agora)
- Added wss://purplepag.es (profile-specific relay)
- Now queries 5 relays in loser's race instead of 3
- Better coverage for profiles that don't exist on main relays
- Each relay gets 5000ms timeout (vs 500ms in pool EOSE)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Extract reply-to pubkeys from p tags in kind 1 events
- Add them to feedPubkeys for batch prefetching
- Prevents placeholder names in "Replying to @..." context
- Only affects kind 1 (text notes) with reply markers
- Profiles now load immediately from cache in ReplyContext component
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Added PROFILE_RELAYS fallback list (primal, damus, ditto)
- Fast path: Query pool with EOSE timeout for quick results
- Slow path: For missing profiles, query each fallback relay individually
- Each fallback relay is queried directly (not through pool) to avoid the relay that won first race
- Added parseAuthorEvent helper for consistent metadata parsing
- Increased timeout to 5000ms for better profile discovery
- Both useAuthor and useAuthors now use the same pattern
- Results from slow path are seeded into cache for instant access
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Moved PageBoundary to the TOP of each page instead of bottom
- When you scroll to page 2, it triggers page 3 to load
- Increased rootMargin to 800px for earlier prefetching
- Separated loading spinner to bottom (only shows during fetch)
- Now you should rarely see the spinner unless network is slow
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Auto-fetch page 2 immediately when page 1 loads
- Add PageBoundary component for each page with intersection observer
- Each page's boundary triggers the next page load when reached
- Added 400px rootMargin for smoother prefetching
- Improved loading indicator placement (only shows on last page)
- Maintains deduplication logic per page before flattening
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The previous useInView approach had two failure modes:
1. rootMargin is ignored by some browsers when inside an iframe
2. React effects only fire on dep *changes* — if inView stayed true the
whole time (sentinel already visible), subsequent pages never loaded
New approach:
- Raw IntersectionObserver with refs that stay fresh without stale closures
- Secondary useEffect triggers fetchNextPage when fetching completes and
the sentinel is still within 800px of the viewport, covering the case
where fetching was in progress when the sentinel first intersected
- Applied to both Feed.tsx and ProfilePage.tsx
- Removed react-intersection-observer dependency from ProfilePage
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Remove `hasNextPage &&` conditional wrapping the sentinel div so it stays
in the DOM at all times — this fixes the case where a tall post sits at
the bottom and the sentinel is already in view when it first mounts,
causing IntersectionObserver to miss the trigger
- Increase rootMargin from 400px → 800px so the next page loads well
before the user reaches the bottom
- Apply both fixes to Feed.tsx and ProfilePage.tsx
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This reverts the codebase back to the state at commit 9752099.
Reverted 2 commit(s):
- 01d9df8: fix: follow-up grouped query for orphaned authors after first prefetch
- 3993971: fix: move prefetch out of queryFn into useEffect — zero pagination blocking
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
First query fires with 1500ms timeout — fast path for most profiles.
After it resolves, check which pubkeys still have no cache entry and
fire one more grouped query for those orphans with 4000ms timeout.
Also handles the case where the first query fails entirely by retrying
the full batch. All grouped — never one subscription per pubkey.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The queryFn now only fetches feed events and returns them immediately.
Author and stats prefetching moves to a useEffect in Feed.tsx that fires
after each render where feedItems changes.
The 400px early sentinel means the next page starts fetching while the
user is still reading. The useEffect prefetch runs concurrently — by the
time the user scrolls to new cards, authors are likely already cached.
Cards that miss the window fall back to individual useAuthor queries.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Feed sentinel now fires 400px before the bottom (rootMargin: '400px')
so the next page fetch starts while the user is still reading, hiding
the latency entirely in most cases
- Author prefetch timeout cut from 5000ms to 1500ms — relay responds
well within that window for cached/known profiles; cards whose authors
miss the deadline fall back to individual useAuthor queries rather than
blocking the whole page render
- Stats prefetch reverted to fire-and-forget — not worth blocking on,
useEventStats per-card handles misses with its own skeleton state
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The fire-and-forget prefetch had a fundamental race: React renders all
NoteCards immediately when the feed query resolves, before the prefetch
has a chance to populate the cache. Each card's useAuthor call finds an
empty cache and opens its own relay subscription — 20+ concurrent REQ
messages per relay, which hits subscription limits and most fail silently.
The only reliable fix is to await the prefetch inside the queryFn before
returning items. This ensures ['author', pubkey] is in cache for every
pubkey on the page when React renders the cards. useAuthor finds fresh
data and never opens a relay subscription.
Authors and stats are fetched in parallel (Promise.all), so the extra
latency is max(author_time, stats_time) not their sum. With eoseTimeout
at 500ms this adds ~500ms per page — a worthwhile tradeoff for reliable
author resolution vs the broken fire-and-forget approach.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The batcher was architecturally unsound. A queryFn that returns a Promise
resolved by external module-level state breaks TanStack Query's assumptions
about query lifecycle — refetches create new pending entries, old entries
pile up, cache seeding races with promise resolution, and the whole thing
becomes unpredictable.
useAuthor is now back to a simple, clean individual query. The prefetch
in useFeed already fetches all needed pubkeys (authors + reposters +
p-tag mentions) before cards mount and seeds the cache with updatedAt.
When the prefetch wins the race, useAuthor finds fresh data and never
calls queryFn. When it doesn't (rare), one individual query fires per
card — which is fine and was always fine at small scale.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Two bugs:
1. Pagination stalling at page 2: batcher was rejecting promises for
pubkeys not found by the relay (retry: 2, delay: 1s). With 15+ posts
each having p-tag mentions, a single page could generate 20-40 retry
queries firing over 2 seconds. These flooded the relay's concurrent
subscription slots, causing the page 3 feed query to time out. The
8-second timeout would expire, queryFn would throw, and getNextPageParam
would return undefined — stopping pagination permanently.
Fix: resolve with {} for missing profiles (show generated name, no retry).
Only seed real profile data into cache; missing profiles stay uncached
so they can be fetched if encountered again.
2. Scroll sentinel used isFetchingNextPage as the guard, which is false
during background refetchInterval refetches. If the sentinel was in
view during a 60s background refetch, fetchNextPage() fired while the
refetch was running, potentially causing a double-fetch or being silently
dropped. Now guards on isFetching (true for any in-flight query).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause of flash-to-fallback: relay was returning fewer kind-0 events
than requested because we were sending limit: pubkeys.length (e.g. 45).
Most relays cap kind-0 responses at 20-25 regardless. Missing pubkeys
resolved with {} which got cached permanently for 5 minutes.
- Remove limit from all kind-0 (author profile) queries — relays return
one per pubkey so there is no meaningful limit to set; omitting it lets
the relay return all matches
- Batcher now rejects (not resolves) for pubkeys not returned by the relay,
so TanStack Query retries them (retry: 2, delay: 1s) rather than caching {}
- Batcher checks if prefetch already seeded the cache before entering the
batch queue — avoids duplicate relay work when both systems overlap
- Relay timeout rejects all pending entries so they retry individually
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The flash-to-fallback bug was caused by setQueryData() seeding cache without
updatedAt, making TanStack Query treat the data as immediately stale and
schedule a background refetch. That refetch's queryFn would race with other
setQueryData calls and could write {} over good profile data.
Three related fixes:
1. useAuthor: remove the getQueryData cache-check inside queryFn — wrong
pattern that broke TanStack Query's stale/fresh contract. TanStack Query
already skips calling queryFn when data is fresh; checking inside queryFn
is redundant and caused stale data to be returned as if it were fresh.
queryFn now just queues into the batcher unconditionally. The batcher only
seeds cache for pubkeys it actually got data for (no {} writes).
2. seedAuthorCache: new helper that calls setQueryData with updatedAt: Date.now()
so the seeded data is treated as fresh for the full staleTime (5 min).
Without updatedAt, seeded data is immediately stale and triggers refetches
that race and potentially overwrite good data.
3. useAuthors: use seedAuthorCache instead of bare setQueryData, and remove
the empty {} seeding for missing profiles.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
useAuthor batcher:
- All useAuthor() calls in the same JS tick now coalesce into a single
relay query instead of each opening their own subscription. With 20
cards mounting simultaneously (post authors, repost headers, reply-to
lines, @mentions), this reduces N round trips to 1. The batcher uses
a Promise.resolve() microtask flush so it captures all calls from the
same render cycle before firing.
- TanStack Query cache is seeded per-pubkey so subsequent renders hit
cache as normal. retry: false since the batcher handles retries.
Stats skeletons:
- While useEventStats is loading, show small skeleton placeholders next
to each action button (reply, repost, reaction, zap) instead of blank
space. Gives the user feedback that counts are incoming.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Two fixes:
1. Pagination speed: stop blocking queryFn on author/stats prefetch.
Items now return immediately and render right away. The prefetch runs
fire-and-forget in the background — if it resolves before the user
scrolls to a card the cache is warm; if not, useAuthor/useEventStats
per-card handle it as before. No more waiting for 2 relay round trips
before new cards appear.
2. Mentions dropping 50%: the prefetch was only collecting post authors
and repost authors, missing the pubkeys in p-tags (reply-to targets
and @mentions). With 15 cards mounting at once, each firing its own
useAuthor for these missed pubkeys, the relay rate-limited ~half of
them. Now p-tag pubkeys are included in the single batch author fetch,
so all mention/reply-to names resolve in one round trip.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Two bugs in the useFeed prefetch approach:
1. Empty author seeding was blocking mentions from resolving.
We were calling setQueryData(['author', pk], {}) for any feed post
author with no profile on our relay. If that same pubkey appeared
as a @mention inside another post, useAuthor found the empty {} in
cache, considered it fresh, and never queried — so the mention showed
a generated name forever. Fix: only seed cache when we actually got a
profile event back. Let useAuthor handle the no-profile case itself.
2. Author and stats prefetches were sequential (await author, then await
stats) when they're completely independent. Now run in parallel with
Promise.all — page load time roughly halved for the prefetch step.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>