From 4ef4177ec5ca6bdcf5147b00c5f68f75a67d5c27 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Tue, 17 Feb 2026 04:16:55 -0600 Subject: [PATCH] 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 --- src/hooks/useStreamPosts.ts | 44 ++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/src/hooks/useStreamPosts.ts b/src/hooks/useStreamPosts.ts index 54dd3a72..8c173ef0 100644 --- a/src/hooks/useStreamPosts.ts +++ b/src/hooks/useStreamPosts.ts @@ -32,32 +32,42 @@ function filterEvent(event: NostrEvent, options: StreamPostsOptions): boolean { if (event.tags.some(([name]) => name === 'e')) return false; } - if (options.mediaType !== 'all') { - const hasImages = hasImageImeta(event); - const hasVideos = hasVideoImeta(event); - switch (options.mediaType) { - case 'images': return hasImages; - case 'videos': return hasVideos; - case 'vines': return false; // kind 1 posts aren't vines - case 'none': return !hasImages && !hasVideos; - } + // For images/videos, verify the imeta tag matches (relay returns kind 1 with any imeta) + switch (options.mediaType) { + case 'images': return hasImageImeta(event); + case 'videos': return hasVideoImeta(event); + case 'none': return !hasImageImeta(event) && !hasVideoImeta(event); } return true; } +/** Build the relay filter for the given media type. */ +function buildFilter(mediaType: StreamPostsOptions['mediaType']): NostrFilter { + switch (mediaType) { + case 'vines': + return { kinds: [34236] }; + case 'images': + return { kinds: [1], '#m': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml', 'image/apng'] }; + case 'videos': + return { kinds: [1], '#m': ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime', 'video/mpeg'] }; + default: + return { kinds: [1] }; + } +} + /** * Stream posts using a direct relay connection. - * When mediaType is 'vines', streams kind 34236 events instead of kind 1. - * Other filters are applied client-side via useMemo. + * Each media type produces its own unique query so the relay returns + * the most relevant results for that filter. */ export function useStreamPosts(query: string, options: StreamPostsOptions) { const { nostr } = useNostr(); const [allEvents, setAllEvents] = useState([]); const [isLoading, setIsLoading] = useState(true); - // Vines filter changes the kind queried, so it must restart the stream - const isVines = options.mediaType === 'vines'; + // mediaType changes the relay filter, so it must restart the stream + const { mediaType } = options; useEffect(() => { const ac = new AbortController(); @@ -90,9 +100,7 @@ export function useStreamPosts(query: string, options: StreamPostsOptions) { const relay = nostr.relay('wss://relay.ditto.pub'); - const baseFilter: NostrFilter = isVines - ? { kinds: [34236] } - : { kinds: [1] }; + const baseFilter: NostrFilter = buildFilter(mediaType); if (query.trim()) { baseFilter.search = query.trim(); @@ -138,9 +146,9 @@ export function useStreamPosts(query: string, options: StreamPostsOptions) { alive = false; ac.abort(); }; - }, [nostr, query, isVines]); + }, [nostr, query, mediaType]); - // Apply client-side filters without restarting the stream + // Apply client-side filters (replies toggle + imeta verification) const posts = useMemo(() => { return allEvents.filter((event) => filterEvent(event, options)); }, [allEvents, options.includeReplies, options.mediaType]);