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>