Commit Graph

47 Commits

Author SHA1 Message Date
Chad Curtis 6b9aeddb06 Clean up review findings from video/vine/pagination commits
- Remove orphaned JSDoc and dead isShort code path in VideoContent
- Unify vine mute state into shared vineGlobalMute module so mute
  preference carries between NoteCard feeds and VinesFeedPage
- Fix fetchNextPage race cascade by using a ref guard instead of
  putting isFetchingNextPage in the useCallback dep array
- Batch event ingestion in useStreamPosts so the event map is sorted
  and flushed to state once per page instead of per-event
- Don't permanently kill pagination on transient network errors
- Move sort modifiers into searchParts before the join for consistency
- Wrap full VideoGridCard/ShortThumb content in ContentWarningGuard
  so metadata doesn't leak when policy is blur
2026-04-24 10:11:32 -05:00
Chad Curtis 5a80df05f5 Add infinite scroll pagination to search page
useStreamPosts previously fetched a single batch of 40 events with no
way to load more. Refactored to track the oldest event timestamp and
expose fetchNextPage/hasNextPage/isFetchingNextPage for cursor-based
pagination using the same NIP-50 search filter with an `until` param.

SearchPage now renders an IntersectionObserver sentinel below the post
list that triggers loading the next page when scrolled into view.
2026-04-24 09:51:50 -05:00
Chad Curtis e8941e8ef6 Fix search page 'N new posts' pill showing unfiltered count
The stream buffer count was reported raw without applying client-side
filters (search query, media type, replies, protocol, mute list, etc.),
so the pill would show e.g. '10 new posts' when only 3 matched the
active search criteria. Extract the filtering predicate into a shared
matchesFilters callback and derive the pill count from filtered buffer
contents instead of the raw streamBufferCount.
2026-03-29 08:00:29 -05:00
Chad Curtis 9056b43696 Highlight flushed posts, fix new-posts pill positioning, fix desktop tab gap
- New posts flushed from the stream buffer now briefly highlight with a
  primary-tinted fade animation so users can see what appeared
- New-posts pill uses responsive CSS (new-posts-pill utility) so it sits
  correctly below the SubHeaderBar on both mobile and desktop
- SubHeaderBar desktop padding moved inside the inner wrapper so the arc
  background extends to the viewport edge, eliminating the gap above tabs
2026-03-28 10:12:11 -05:00
Chad Curtis ab2145ffe9 Fix mobile UX: sticky safe area, page padding, stream buffering, media CW
- SubHeaderBar pinned mode adds safe-area-top padding when nav is hidden
- Increase signer nudge delay to 10s for decrypt ops (reduces false triggers on Amber)
- Add arc overhang spacer to Search, Notifications, and Profile pages
- Add PageHeader to Search page for consistent top-level layout
- Buffer streamed posts when user is scrolled down to prevent scroll jumps
- Show 'N new posts' pill that tracks SubHeaderBar position and nav state
- MediaCollage respects NIP-36 content warnings (blur/hide/show policy)
- Normalize main element classes across Profile and Notifications pages
2026-03-28 09:16:00 -05:00
Alex Gleason b25be7e8e9 Add relay.dreamith.to as a second Ditto relay for search, trending, and streaming queries 2026-03-19 22:25:26 -05:00
Chad Curtis 4684491b9f fix: apply search query filter to all streamed event kinds
Non-kind-1 events (articles, photos, videos, etc.) bypassed the client-side
search check via an early return, so streamed events of any extra kind passed
through regardless of the search query. Remove the early return and apply the
query filter to all kinds, also checking title/summary/subject/alt tags for
non-text events. Reply filtering still only applies to kind 1/1111.
2026-03-14 11:29:39 -05:00
Chad Curtis 79ea07098d fix: use all picker kinds when kind filter is set to 'all'
parseKindFilter returned undefined for 'all', causing useStreamPosts to fall
back to the user's enabled feed-settings kinds instead of every kind in the
kind picker list. Now kindsOverride always contains explicit kind numbers:
all picker kinds when filter is 'all', or the specific selection otherwise.
2026-03-14 11:15:59 -05:00
Chad Curtis cf1edc3230 fix: always include kind 1 in default search kinds
getEnabledFeedKinds only returns user-toggled extra kinds, never kind 1.
The fallback paths for videos/images/all were missing kind 1 entirely,
so searches returned no results unless the user had explicitly enabled
extra feed kinds.
2026-03-14 11:13:11 -05:00
Chad Curtis ba62b42ce0 Search filters: multi-person authors, sort toggle, icon polish
- Author scope 'person' renamed to 'people' supporting multiple authors;
  stored as repeated ?author= URL params; chips show avatar/name with
  individual remove buttons resolved via useAuthor
- Add sort filter: Recent / Hot / Trending — maps to NIP-50 sort:hot /
  sort:trending search terms, persisted to URL and SavedFeedFilters
- Both segment toggles (From / Sort) now show size-3.5 Lucide icons:
  Globe / Users / UserSearch for author scope; Clock / Flame / TrendingUp
  for sort
- SavedFeedFilters: authorPubkey→authorPubkeys[], add sort field; schema
  and all consumers (Feed.tsx, ProfilePage.tsx) updated accordingly
- useStreamPosts: resolves array of npub/hex pubkeys, applies sort: NIP-50
  term to initial search query
2026-03-05 21:14:30 -06:00
Chad Curtis 699b4bc32a Fix search page filter UX: persist to URL, add author scope, debounce, streaming fixes
- Persist all filters (replies, media, language, platform, kind, author) to URL params so they survive refresh and can be shared
- Add author scope filter: accepts npub or hex pubkey, filters both initial batch and live streaming
- Add reset all filters button in popover header + chip strip with clear link
- Show NIP-50 search string block below the input so users can see exactly what's sent to the relay
- Show active filter chips; empty state now lists active filters with a 'Clear all' shortcut
- Add kind + media type conflict warning when both are set simultaneously
- Fix Vines streaming: hardcoded 'return false' for kind 1 changed to only reject kind 1/1111, letting 22/34236 through
- Fix streaming language/protocol gap: protocol filter is now applied client-side to live streaming events via proxy tag detection
- Wire up useContentFilters: shouldFilterEvent() now applied in useStreamPosts useMemo so user-defined content rules affect search
- Add useDebounce hook (300ms) and apply to useSearchProfiles to stop hammering relays on every keystroke
2026-03-05 21:14:30 -06:00
Chad Curtis 51cd385284 Add kind filter dropdown with icons to search filters popover 2026-03-03 05:15:02 -06:00
Alex Gleason d147a25590 Fix all ESLint errors and warnings across 15 files 2026-03-01 21:59:19 -06:00
Chad Curtis 01932ff9cd include kinds 20/21/22 in search media filters and rename Vines to Shorts & Vines 2026-03-01 20:40:37 -06:00
Chad Curtis e81c7c183c Fix mention e-tags incorrectly treated as reply markers per NIP-10 2026-02-28 20:17:17 -06:00
Chad Curtis a602a47759 Replace search filter panel with filter icon popover, add Bluesky filter, fix protocol query logic 2026-02-28 17:11:35 -06:00
Alex Gleason f07ebab004 Include kind 1111 comments in feeds and profile 'Posts & replies' tab
Add kind 1111 to EXTRA_KINDS tied to feedIncludePosts so comments
appear in all feeds (follows, global, communities) and profile feeds.
Explicitly exclude kind 1111 from profile 'Posts' tab since they are
always replies. Treat kind 1111 like kind 1 in streaming filter so
reply/search/media filtering applies consistently.
2026-02-27 21:25:29 -06:00
Chad Curtis dd628c62e9 Filter trends, search, and global feed to protocol:nostr only 2026-02-26 19:26:53 -06:00
Alex Gleason 386a2fec65 Use kind 16 (generic repost) for reposting non-kind-1 events
Per NIP-18, kind 6 is specifically for reposting kind 1 text notes,
while kind 16 should be used for all other event types. Added
isRepostKind() and getRepostKind() helpers to centralize the logic,
and updated all repost-related code paths: publishing, deletion,
status checks, interactions, feed processing, notifications, and
stream/search filtering.
2026-02-24 15:22:19 -06:00
Chad Curtis 848c5132e3 Centralize DITTO_RELAY const and fix URL consistency
- Export DITTO_RELAY from appRelays.ts, replacing hardcoded relay URLs
- Normalize all relay URLs to use trailing slashes, preventing NPool
  from opening duplicate connections to the same relay
- Add cache-based fallback to mention search: when relay search returns
  nothing, scan already-fetched author profiles in the query cache
2026-02-22 16:42:08 -06:00
Chad Curtis fe6e2065bc Implement mute user/conversation actions and apply mute filtering across all feeds
Wire up the previously stubbed 'Mute @user' and 'Mute Conversation' menu
actions in NoteMoreMenu and ProfilePage to call useMuteList().addMute,
with toggle support for unmuting. Apply mute filtering (via isEventMuted
from muteHelpers) across all feed surfaces: main feed, profile feed,
notifications, post detail replies, search/explore stream, hashtag page,
domain feed, trending posts, and right sidebar.
2026-02-21 03:57:59 -06:00
Chad Curtis 1e9bd782aa Add Notes section for feed content organization (posts, reposts, articles)
- Add 'feed' section to EXTRA_KINDS for Posts (kind 1), Reposts (kind 6),
  and Articles (kind 30023) as feed-only toggles
- Add feedIncludePosts, feedIncludeReposts, feedIncludeArticles to FeedSettings
  (posts and reposts default to enabled, articles disabled)
- Add 'Notes' collapsible section in ContentSettings above 'Other Stuff'
- Feed kinds are now fully dynamic from settings instead of hardcoded
- Repost unwrapping accepts any event kind, not just kind 1
- Add article (kind 30023) card rendering with title, image, and summary
- Update all feed hooks for consistency (useFeed, useProfileFeed,
  useStreamPosts, DomainFeedPage, HashtagPage)
2026-02-21 00:36:58 -06:00
Chad Curtis c82fc125c3 Remove debug logging bloat from useStreamPosts 2026-02-19 05:27:26 -06:00
Chad Curtis eb93d5dd09 Re-enable search filters and add logging for initial query
- Re-enabled language and media search filters for initial query
- Added console logging to track initial batch fetch
- Initial query fetches 40 events with NIP-50 filters applied
- Streaming subscription then continues from relay.ditto.pub for new events
2026-02-19 05:26:03 -06:00
Chad Curtis c6a2f7c960 Fix streaming by bypassing pool's eoseTimeout
The root cause: NPool.req() aborts subscriptions 500ms after the first EOSE
due to eoseTimeout configuration. This is intended for queries but breaks
streaming subscriptions.

Solution: Use relay() directly for streaming instead of the pool. This
bypasses the pool's eoseTimeout logic and allows subscriptions to stay
open indefinitely after EOSE.

- Use nostr.relay('wss://relay.ditto.pub') for streaming subscription
- Keep using the pool for initial query (benefits from multi-relay speed)
- Added detailed logging to track subscription lifecycle
- Re-enabled search filters for initial query only
2026-02-19 05:23:07 -06:00
Chad Curtis 3593fddc87 Fix streaming on search page
The search page was broken because it tried to use NIP-50 search filters
in the streaming subscription. Relays don't support streaming search queries
because search requires full-text indexing.

Changes:
- Split filters into initialFilter (with search) and streamFilter (kinds only)
- Initial query uses relay-level NIP-50 search extensions (language, media, video)
- Streaming subscription only filters by kinds
- Re-added client-side filtering for streaming events (search text, media type)
- Language filtering remains relay-level only (can't detect language client-side)

Now the search page correctly:
1. Fetches initial results using relay search filters
2. Streams new events by kind
3. Filters streaming events client-side as they arrive
2026-02-19 05:15:56 -06:00
Chad Curtis 0f1bdd40fb Implement Ditto relay language and media search filters
- Add language filtering using NIP-50 language: extension
- Add media filtering using NIP-50 media: and video: extensions
- Move media filtering from client-side to relay-level for better performance
- Support filtering by images (media:true video:false), videos (video:true), or no media (media:false)
- Language filter supports all existing language options (en, es, fr, de, ja, zh, etc.)
- Filters are now applied at the relay level via search query extensions rather than client-side filtering
2026-02-19 05:11:59 -06:00
shakespeare.diy 0fa3e58a83 Drastically reduce concurrent relay queries to prevent rate limiting
The app was firing an excessive number of concurrent REQs, causing relays
to return "too many concurrent REQs" errors. Root causes and fixes:

1. **Batch event stats** (biggest win): Created `useBatchEventStats` hook
   that fetches interaction stats for ALL visible feed items in a single
   relay query instead of 2 queries per NoteCard. For a 15-item feed this
   reduces ~30 concurrent REQs to just 1. Results are seeded into the
   individual `['event-stats', id]` cache for instant resolution.

2. **Merged dual queries**: Both `useEventStats` and `useEventInteractions`
   were firing 2 parallel queries (e-tag + q-tag). Merged each into a
   single query using multiple filter objects (relay handles as OR).

3. **Reduced limits across the board**:
   - useEventStats: limit 200+50 → 50+20 (single query)
   - useEventInteractions: limit 500+100 → 50+20 (single query)
   - useTrendingTags: limit 200 → 50
   - useReplies: limit 100 → 50
   - useProfileFeed PAGE_SIZE: 30 → 20
   - useStreamPosts/useStreamKind subscription limit: 100 → 0

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 01:48:27 -06:00
shakespeare.diy 4fdd596cbc Stop opening duplicate WebSocket connections
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>
2026-02-18 00:11:28 -06:00
shakespeare.diy ff38bb14bc Render polls, geocaches, and color moments per their NIP specs
- PollContent (kind 1068 / NIP-88): Shows question, options with
  selectable vote UI, tallied results with percentage bars, poll type
  badge (single/multi), expiry status. Fetches kind 1018 vote events,
  deduplicates per pubkey, and allows voting when logged in.

- GeocacheContent (kind 37516 / NIP-GC): Shows cache name, type badge,
  size badge, D/T difficulty pips (1-5), description, image, and
  ROT13-decoded hint toggle. Supports verified cache indicator.

- ColorMomentContent (kind 3367 / Espy NIP): Renders color palettes
  with all 6 layout modes (horizontal, vertical, grid, star,
  checkerboard, diagonalStripes). Emoji overlay with adaptive
  light/dark text based on palette luminance. Click-to-copy hex
  swatches. Name display above palette.

- NoteCard now dispatches to the correct content component based on
  event.kind (1, 1068, 3367, 34236, 37516).

- Replaced VinesPage/useStreamVines with generic KindFeedPage/useStreamKind.
  All kind-specific routes (/vines, /polls, /treasures, /colors) use
  the same generic streaming page parameterized by kind number.

- Centralized extra-kind definitions in lib/extraKinds.ts. All feeds
  (home, search, profile, hashtag) respect feed settings for which
  extra kinds to include.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 05:24:01 -06:00
shakespeare.diy c48ce58adc Revert to 2c7884b: Switch media type filters to imeta tag-based detection, rename labels
This reverts the codebase back to the state at commit 2c7884b.

Reverted 1 commit(s):
- 4ef4177: Query media types uniquely via relay filters (#m tag)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 04:17:33 -06:00
shakespeare.diy 4ef4177ec5 Query media types uniquely via relay filters (#m tag)
Each media type now produces its own distinct relay query that restarts
the stream, just like vines does with kind 34236:

- Images: kind 1 + #m filter for image/* MIME types
- Videos: kind 1 + #m filter for video/* MIME types
- Vines: kind 34236 (unchanged)
- All / No media: kind 1 (no #m filter)

The mediaType is now a dependency of the useEffect, so switching
between any filter restarts the stream with a fresh relay query.
Client-side imeta verification remains as a secondary check.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 04:16:55 -06:00
shakespeare.diy 2c7884bd61 Switch media type filters to imeta tag-based detection, rename labels
- Replace URL regex matching (extractImages/extractVideos) with imeta tag
  MIME type checking (hasImageImeta/hasVideoImeta) for media filtering
- Images filter now checks for imeta tags with `m image/*` MIME types
- Videos filter now checks for imeta tags with `m video/*` MIME types
- Rename "Regular videos" → "Videos"
- Rename "Short videos (Vines)" → "Vines"

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 04:14:57 -06:00
shakespeare.diy 4f117403b0 Add Vines support: kind 34236 feed page, VineCard, search filter
- Created VinesPage with streaming feed of kind 34236 events
- Created VineCard component: renders video with thumbnail, play overlay,
  author header, title, and hashtag links
- Updated useStreamPosts: 'vines' filter switches to kind 34236 query
  (restarts stream since it's a different kind)
- Search page renders VineCard for kind 34236 events, NoteCard for kind 1
- Updated AppRouter: /vines route now uses VinesPage instead of placeholder
- Created useStreamVines hook for dedicated Vines page streaming
- Validates vine events: requires d tag, imeta with url

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 01:16:56 -06:00
shakespeare.diy 5b08d2d305 Fix filters: apply client-side without restarting the stream
- Stream effect only depends on [nostr, query] — filter toggles don't
  restart the stream or re-fetch from the relay
- Store all raw events in allEvents state (unfiltered)
- Apply includeReplies/mediaType filters via useMemo on the raw events
- Toggling a filter instantly shows/hides posts from the existing set
  instead of fetching a new batch and getting "no posts"

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 01:09:31 -06:00
shakespeare.diy 0b1abc2ca0 Make filter changes restart the stream with new results
- Added options.includeReplies and options.mediaType to effect deps
- Changing a radio button or toggle now re-fetches and re-streams
- Removed refs that were preventing filter changes from taking effect

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 01:07:25 -06:00
shakespeare.diy 63461b84a7 Use wss://relay.ditto.pub for all streaming (both default and search)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 01:05:17 -06:00
shakespeare.diy 97a5c9669e Use nostr.relay() directly for streaming instead of pool with relays option
- Call nostr.relay('wss://relay.damus.io').req() directly on a single relay
  instead of passing relays option to pool.req()
- Use refs for filter options to avoid restarting the stream on filter changes
- Minimal effect deps: only [nostr, query] trigger stream restart
- Filter changes apply via optionsRef without killing the subscription

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 01:04:06 -06:00
shakespeare.diy 9d95b10a48 Clean up debug logs from stream hook
The debug logs confirmed the stream IS working correctly:
- req() subscription opens successfully
- EOSE is received and subscription stays open
- New events arrive after EOSE
- Cleanup only happens when the preview environment reloads (replaceDocument)

Removed eoseTimeout from NPool in previous commit which was the
main fix for subscription lifecycle issues.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 01:02:21 -06:00
shakespeare.diy 67d523b260 Match bitmap/clawchat streaming pattern exactly, remove eoseTimeout
- Rewrote useStreamPosts to exactly mirror bitmap's useChatSession pattern
- Removed eoseTimeout: 200 from NPool config (not a valid NPoolOpts field,
  was potentially causing issues with subscription lifecycle)
- Uses AbortController directly (not useRef)
- fetchInitialMessages + subscribeToMessages run in parallel
- 30s timeout on initial fetch (matching bitmap)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 01:00:01 -06:00
shakespeare.diy d9d6e19ca0 Fix streaming: pass relays option directly to req() like clawchat does
- Pass `relays` array directly to nostr.req() and nostr.query()
  to bypass pool's reqRouter/eoseTimeout that was killing streams
- This is the pattern clawchat uses successfully for live subscriptions
- Default stream uses multiple relays, NIP-50 search uses relay.ditto.pub only
- Filter out future-dated events (created_at > now)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 00:57:41 -06:00
shakespeare.diy 99f922ea15 Fix streaming: use single relay instead of pool, filter future events
- Always use relay.ditto.pub directly for req() streaming
  (NPool's eoseTimeout: 200 was killing subscriptions prematurely)
- Filter out events with created_at in the future
- Matches clawchat pattern: fetchInitial + streamNew in parallel
- Both query() and req() go to same single relay

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 00:55:48 -06:00
shakespeare.diy 738b29f075 Fix streaming: run fetch + stream in parallel, use limit: 100 for req
- Match clawchat's proven streaming pattern
- fetchInitial() and streamNew() run in parallel, not sequentially
- req() uses limit: 100 (not limit: 0 which was wrong)
- Added isSubscribed guard for safe state updates
- Proper AbortError handling on both fetch and stream paths

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 00:53:27 -06:00
shakespeare.diy 87e1580c5a Fix streaming: query initial batch then req for live updates
- Step 1: query() with limit: 40 to fetch initial posts immediately
- Step 2: req() with limit: 0 + since: now to stream new posts live
- Fixes blank page issue (limit: 0 alone returns no stored events)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 00:52:09 -06:00
shakespeare.diy dcf0650eeb Fix streaming: use AsyncIterable API with AbortController, stream by default
- Rewrote useStreamPosts to use Nostrify's correct req() API which
  returns AsyncIterable, not a subscription object with .close()
- Uses AbortController to cancel streams on cleanup (fixes sub.close error)
- Streams posts by default with no search query (global kind:1 feed)
- When search query is provided, uses NIP-50 search on relay.ditto.pub
- Properly handles EOSE, CLOSED, and EVENT messages from the stream

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 00:51:12 -06:00
shakespeare.diy 9d3d62f7e6 Set stream subscription limit to 0 for proper real-time streaming
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 00:49:27 -06:00
shakespeare.diy 151746e7b2 Implement Ditto-style search page with live streaming posts
- Added Posts, Trends, and Accounts tabs
- Implemented sidebar search filters for Posts tab (replies, media type, language)
- Created useStreamPosts hook for real-time streaming subscriptions
- Search uses relay.ditto.pub for NIP-50 search functionality
- Posts stream in real-time with REQ subscription instead of static queries
- Layout matches Ditto design with filters in left sidebar on desktop
- All tabs functional with proper empty states

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 00:44:37 -06:00