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>
This commit is contained in:
shakespeare.diy
2026-02-17 04:16:55 -06:00
parent 2c7884bd61
commit 4ef4177ec5
+26 -18
View File
@@ -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<NostrEvent[]>([]);
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]);