- 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
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.
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.
- 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
- 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
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.
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.
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.
- 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
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.
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.
- 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
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.
- 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)
- 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
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
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
- 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
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>
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>
- 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>
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>
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>
- 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>
- 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>
- 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>
- 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>
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>
- 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>
- 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>
- 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>
- 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>
- 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>