Commit Graph

4447 Commits

Author SHA1 Message Date
shakespeare.diy 229ee787d8 Fix infinite scroll to trigger on page boundary instead of pixel-based detection
- 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>
2026-02-18 15:46:15 -06:00
shakespeare.diy 03d5310ddd Revert to cb819e8: Use app relays for loser's race instead of separate profile relays
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>
2026-02-18 15:44:15 -06:00
shakespeare.diy e03527ba78 Fix infinite scroll race condition and add relay logging
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>
2026-02-18 15:43:23 -06:00
shakespeare.diy 06357794ef Move relay fetching inside queryFn and add debug logging
- 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>
2026-02-18 15:41:29 -06:00
shakespeare.diy cb819e811f Use app relays for loser's race instead of separate profile relays
- 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>
2026-02-18 15:39:11 -06:00
shakespeare.diy 00e973cf3c Add more profile relays to loser's race
- 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>
2026-02-18 15:32:23 -06:00
shakespeare.diy 7e56b749f5 Batch prefetch "replying to" profiles in Feed
- 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>
2026-02-18 15:29:26 -06:00
shakespeare.diy 9d92ec20ec Implement loser's race for author profiles
- 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>
2026-02-18 15:26:59 -06:00
shakespeare.diy 2d563af655 Fix scroll boundaries: trigger at page top, not bottom
- 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>
2026-02-18 15:22:43 -06:00
shakespeare.diy b638123def Fix infinite scroll: auto-load page 2 and add per-page boundaries
- 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>
2026-02-18 15:19:21 -06:00
shakespeare.diy bde81e198f Fix infinite scroll: replace useInView with raw IntersectionObserver + ref pattern
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>
2026-02-18 14:52:23 -06:00
shakespeare.diy 12c16b5b90 Fix infinite scroll: always render sentinel, increase rootMargin to 800px
- 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>
2026-02-18 14:41:48 -06:00
shakespeare.diy f9f2ba72f4 Revert to 9752099: fix: early sentinel trigger + short author prefetch timeout
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>
2026-02-18 10:47:44 -06:00
shakespeare.diy 01d9df86d3 fix: follow-up grouped query for orphaned authors after first prefetch
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>
2026-02-18 10:46:28 -06:00
shakespeare.diy 399397117a fix: move prefetch out of queryFn into useEffect — zero pagination blocking
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>
2026-02-18 10:43:04 -06:00
shakespeare.diy 97520999c0 fix: early sentinel trigger + short author prefetch timeout
- 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>
2026-02-18 10:38:50 -06:00
shakespeare.diy e1e84d7d71 fix: await prefetch in queryFn so cache is populated before NoteCards mount
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>
2026-02-18 10:32:45 -06:00
shakespeare.diy 974490f323 revert: delete useAuthor batcher, restore simple individual queries
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>
2026-02-18 10:29:24 -06:00
shakespeare.diy fba9a33c5c fix: stop retry storm from blocking pagination; fix scroll sentinel guard
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>
2026-02-18 10:27:28 -06:00
shakespeare.diy 2246e9c652 fix: reject missing profiles in batcher so TanStack retries; drop relay limit on kind-0 queries
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>
2026-02-18 10:23:20 -06:00
shakespeare.diy 8c7f116506 fix: stop cache poisoning that caused good author data to flash to fallback
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>
2026-02-18 10:19:48 -06:00
shakespeare.diy a44c6e85eb fix: batch useAuthor queries per tick; show stats skeletons while loading
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>
2026-02-18 10:15:14 -06:00
shakespeare.diy d4dbe65256 fix: fire prefetch in background and include p-tag pubkeys for mentions
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>
2026-02-18 10:07:57 -06:00
shakespeare.diy 3e3d7e3dcf fix: parallelize author/stats prefetch and stop seeding empty author entries
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>
2026-02-18 10:03:03 -06:00
shakespeare.diy 1dfe209690 fix: move author/stats prefetch inside useFeed queryFn to eliminate race
Previously the feed had two separate systems (batch hooks in Feed.tsx +
individual hooks in NoteCard) that raced each other. The batch ran as a
useQuery at the component level, but NoteCards mounted in the same render
cycle and fired their own per-card queries before the batch resolved.
This got worse with each page as both systems scaled up.

The fix: prefetch authors and stats *inside* useFeed's queryFn, after the
feed events are fetched but before the queryFn returns. By the time React
re-renders with the new page data, the ['author', pubkey] and
['event-stats', id] caches are already populated. NoteCard's individual
useAuthor and useEventStats hooks then resolve instantly from cache
without firing any extra network requests.

New page loads only fetch authors/stats for IDs not already in cache, so
page 1's data is never re-requested when page 2 loads.

- Remove useAuthors + useBatchEventStats calls from Feed.tsx entirely
- Export computePageStats from useTrending for use in useFeed
- useBatchEventStats / useAuthors still exist for non-feed use cases

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 09:58:16 -06:00
shakespeare.diy 0d8176834a fix: proper mobile action bar layout and pagination stats/author stability
Action bar (NoteCard):
- Left-aligned buttons with consistent ml-2 spacing between them
- More button pushed to the far right with ml-auto
- Stat counts use text-xs so they never blow out the row width
- No artificial max-width cap — works naturally on any screen size

Pagination stats/authors (useAuthors + useBatchEventStats):
- Both batch hooks now filter out already-cached IDs before building
  the query key and firing the request
- When page 2 loads, only genuinely new pubkeys/event IDs are fetched;
  page 1's data is untouched in cache
- The query key stabilises to an empty string once all items are cached,
  so no more cascading re-fetches as the feed grows
- useAuthor: return {} instead of throwing when no kind-0 event is found,
  eliminating the retry storm when many cards mount simultaneously

Also removes the usePageBatch hack introduced in the previous attempt.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 09:49:34 -06:00
shakespeare.diy f1778babb6 fix: mobile action bar layout + infinite scroll stats/author drop
- NoteCard: replace fixed gap-6 action bar with justify-between + max-w-xs
  on mobile so stats never overflow or wrap on narrow screens

- usePageBatch: new hook that batches author/stats prefetches per page
  instead of across the whole accumulated feed. The old approach used a
  single query key built from ALL loaded event IDs; adding page 2 changed
  the key, abandoned the old cache entry, and fired a fresh request for
  every item — causing stats and author data to visually disappear on
  pages 2 and 3. Now each page gets its own stable cache entry that is
  never invalidated by subsequent page loads.

- Feed: swap useAuthors + useBatchEventStats calls for usePageBatch

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 09:40:18 -06:00
shakespeare.diy e3b8a65fce Fix abort signal handling for nostrconnect to prevent immediate timeout
- Add optional abortSignal parameter to nostrconnect method
- Pass abort controller signal from LoginDialog to nostrconnect
- Properly handle AbortError when dialog is closed
- Prevent "signal has been aborted" error on dialog close
- Use custom abort controller instead of only relying on timeout signal

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 04:35:13 -06:00
shakespeare.diy 606f75417b Move remote signer option under More Options alongside secret key
- Change tab order: Secret Key first, Remote Signer second
- Remove "Use remote signer" standalone button
- Add clickable "More Options" button that expands collapsible
- Remote signer is now at the same level as secret key in the tabs
- Don't auto-generate nostrconnect session on dialog open

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 04:33:51 -06:00
shakespeare.diy f0728d1737 Implement remote signer login option with nostrconnect
- Add nostrconnect method to useLoginActions for NIP-46 client-initiated connections
- Add helper functions generateNostrConnectParams and generateNostrConnectURI
- Create QRCodeCanvas component wrapper for displaying QR codes
- Update LoginDialog to support remote signer login with QR code scanning
- Add "Use remote signer" button when extension is available
- Display QR code on desktop, "Open Signer App" button on mobile
- Support manual bunker URI input as fallback option
- Add connection retry on error
- Use existing useIsMobile hook for responsive behavior

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 04:31:51 -06:00
shakespeare.diy 3c4c84d255 Fix URL parsing with trailing punctuation and preserve whitespace formatting
- Strip trailing punctuation from URLs (.,;:!?)])  that are likely not part of the URL
- Fixes issue where (https://example.com) was parsed incorrectly
- Preserve newlines and formatting after embedded content
- Only collapse excessive whitespace (3+ newlines) instead of removing all whitespace
- Maintains proper text flow after link previews and embedded events

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 04:27:33 -06:00
shakespeare.diy 973064af99 Fix whitespace handling for naddr URLs with embedded events
- Preserve one newline after naddr-embed tokens when present
- Prevents text from flowing directly after URL without spacing
- Embedded event now properly displays with preserved formatting

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 04:25:38 -06:00
shakespeare.diy 724663f7dd Support mentions in all contexts including inside quotes and parentheses
- Removed lookahead that was preventing matches in certain contexts
- NIP-19 identifiers now render as mentions regardless of surrounding characters
- Maintains single @ display by consuming optional @ prefix

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 04:23:08 -06:00
shakespeare.diy d3d98a00bf Fix double @@ rendering for mentions and support mentions inside quotes
- Updated regex to optionally match @ before NIP-19 identifiers
- Removed word boundary requirement to allow mentions in quotes
- Mentions now render correctly with single @ symbol in all contexts

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 04:22:04 -06:00
shakespeare.diy 625dd91016 Adjust ComposeBox styling: increase avatar size and add text opacity
- Change avatar from size-11 to size-12 for better visual balance
- Increase textarea top padding from pt-2 to pt-2.5
- Add opacity-85 to textarea text for softer appearance

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 04:20:42 -06:00
shakespeare.diy bac9609b0d Right sidebars pt-5 → pt-4
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 04:12:04 -06:00
shakespeare.diy 26508210f7 Right sidebars pt-6 → pt-5
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 04:11:30 -06:00
shakespeare.diy e7821e0bc1 Remove Notifications title bar entirely, increase tab padding to py-5
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 04:09:16 -06:00
shakespeare.diy 6ebec34eae Make Follows/Global and All/Mentions tabs sticky on mobile (top-12)
Tab bars should stick below the mobile top bar (h-12 = top-12) on mobile
and at top-0 on desktop. These are navigation tabs, not page title headers.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 04:07:49 -06:00
shakespeare.diy fd1e6fb64a Hide Notifications title header on mobile, keep All/Mentions tabs
The "Notifications" header with bell icon is redundant with the bottom nav
on mobile, so it's now hidden below the sidebar breakpoint. The All/Mentions
tab bar remains visible as the top element on mobile.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 04:06:52 -06:00
shakespeare.diy 8532d5a45e Make mobile top bar slightly taller (h-10→h-12) and unstick page headers on mobile
- MobileTopBar: bump height from h-10 (40px) to h-12 (48px)
- Page headers (STICKY_HEADER_CLASS): only sticky on desktop (sidebar:sticky sidebar:top-0), scroll naturally on mobile
- Updated inline sticky classes in Feed.tsx and PostDetailPage.tsx to match

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 04:03:04 -06:00
shakespeare.diy 6d7375bb19 Scope overscroll-behavior to PullToRefresh component only
Remove global overscroll-behavior-y: contain from CSS. Instead, set it
on document.documentElement via JS when the PullToRefresh component
mounts and restore the previous value on unmount. This way other pages
keep the browser's native pull-to-refresh behavior.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 04:00:44 -06:00
shakespeare.diy e87ae03805 Fix pull-to-refresh hijacked by browser native gesture
- Add overscroll-behavior-y: contain on html/body to disable browser's
  native pull-to-refresh
- Switch from React synthetic touch events to native addEventListener
  with { passive: false } so e.preventDefault() actually blocks the
  browser gesture during pull-down
- Use a mirrored ref (currentPull) for synchronous reads inside native
  event handlers

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 03:57:53 -06:00
shakespeare.diy 69a05f690a Add pull-to-refresh on mobile feed
- Create PullToRefresh component with touch drag detection
- Rubber-band resistance effect for natural feel
- Spinner indicator appears below tabs when pulling down
- Triggers feed data refetch on release past threshold
- Only active on mobile (touch events + indicator hidden on sidebar breakpoint)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 03:55:23 -06:00
shakespeare.diy 950b11f39e fix: skeleton load sparklines while activity data is fetching
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 03:52:14 -06:00
shakespeare.diy cef2693cdf feat: replace fake sparklines with real hashtag activity data
- Add useTagSparklines hook that queries kind-1 posts for each trending
  tag over the last 24 hours, buckets them into 10 time intervals, and
  returns actual post counts per bucket.
- All tags are fetched in a single relay query for efficiency.
- TrendSparkline component now accepts a `data: number[]` prop and
  renders the real activity curve instead of random noise.
- Remove all seeded PRNG code that was generating fake trend lines.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 03:51:16 -06:00
shakespeare.diy 9213da9d27 fix: make trend sparklines deterministic per tag name
Replace Math.random() with a seeded PRNG (mulberry32) keyed on the
hashtag name so the sparkline shape is always the same for a given tag
across page loads.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 03:48:08 -06:00
shakespeare.diy 883bf893cf Fix settings spasm by skipping NostrSync after local writes
- Add write timestamp guard to prevent NostrSync from overwriting
  settings we just changed locally
- 10 second cooldown window after any local settings write
- Eliminates back-and-forth between local state and stale encrypted data

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 03:44:06 -06:00
shakespeare.diy abf6be233f Fix settings glitch by updating cache in-place instead of refetching
- Replace invalidateQueries with setQueryData on mutation success
- Prevents refetch -> NostrSync -> updateConfig -> re-render loop
- Local settings change is now the only render, no secondary flash

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 03:42:32 -06:00
shakespeare.diy dd5021788f Publish encrypted settings in background without triggering re-renders
- Use nostr.event() directly instead of useNostrPublish mutation
- Sign and publish happens without React Query mutation lifecycle
- Prevents signer prompt from causing component re-renders and glitches
- Settings still save properly but UI stays stable during theme changes

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 03:40:01 -06:00