From 3f66182484f368b63a288dcbbad865d26d7f2e15 Mon Sep 17 00:00:00 2001 From: highperfocused Date: Thu, 27 Nov 2025 21:30:42 +0100 Subject: [PATCH] Add useFollowerPictureFeed hook and integrate it into Index page for user-specific picture feed --- src/hooks/useFollowerPictureFeed.ts | 60 +++++++++++++++++++++++++++++ src/pages/Index.tsx | 10 ++++- 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 src/hooks/useFollowerPictureFeed.ts diff --git a/src/hooks/useFollowerPictureFeed.ts b/src/hooks/useFollowerPictureFeed.ts new file mode 100644 index 0000000..6b6d0bd --- /dev/null +++ b/src/hooks/useFollowerPictureFeed.ts @@ -0,0 +1,60 @@ +import { useNostr } from '@nostrify/react'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import type { NostrEvent } from '@nostrify/nostrify'; +import { useCurrentUser } from './useCurrentUser'; + +/** + * Hook for fetching kind 20 (NIP-68) picture events from followed users with infinite scroll + * Uses NIP-02 follow list to filter events + */ +export function useFollowerPictureFeed() { + const { nostr } = useNostr(); + const { user } = useCurrentUser(); + + return useInfiniteQuery({ + queryKey: ['follower-picture-feed', user?.pubkey], + queryFn: async ({ pageParam, signal }) => { + if (!user?.pubkey) { + return []; + } + + // First, fetch the user's follow list (kind 3, NIP-02) + const followListEvents = await nostr.query( + [{ kinds: [3], authors: [user.pubkey], limit: 1 }], + { signal: AbortSignal.any([signal, AbortSignal.timeout(1500)]) } + ); + + if (followListEvents.length === 0) { + return []; + } + + // Extract followed pubkeys from p tags + const followedPubkeys = followListEvents[0].tags + .filter(([tag]) => tag === 'p') + .map(([, pubkey]) => pubkey) + .filter(Boolean); + + if (followedPubkeys.length === 0) { + return []; + } + + // Fetch pictures from followed users + const filter = pageParam + ? { kinds: [20], authors: followedPubkeys, limit: 20, until: pageParam as number } + : { kinds: [20], authors: followedPubkeys, limit: 20 }; + + const events = await nostr.query([filter], { + signal: AbortSignal.any([signal, AbortSignal.timeout(1500)]) + }); + + return events as NostrEvent[]; + }, + getNextPageParam: (lastPage) => { + if (lastPage.length === 0) return undefined; + // Subtract 1 since 'until' is inclusive + return lastPage[lastPage.length - 1].created_at - 1; + }, + initialPageParam: undefined as number | undefined, + enabled: !!user?.pubkey, + }); +} diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 8f504f8..7128dbf 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -1,6 +1,7 @@ import { useSeoMeta } from '@unhead/react'; import { Layout } from '@/components/Layout'; import { usePictureFeed } from '@/hooks/usePictureFeed'; +import { useFollowerPictureFeed } from '@/hooks/useFollowerPictureFeed'; import { PictureCard } from '@/components/feed/PictureCard'; import { MinimalPictureCard } from '@/components/feed/MinimalPictureCard'; import { useInView } from 'react-intersection-observer'; @@ -8,6 +9,7 @@ import { useEffect, useMemo } from 'react'; import { Skeleton } from '@/components/ui/skeleton'; import { Card, CardContent } from '@/components/ui/card'; import { useAppContext } from '@/hooks/useAppContext'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; const Index = () => { useSeoMeta({ @@ -16,7 +18,13 @@ const Index = () => { }); const { config } = useAppContext(); - const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = usePictureFeed(); + const { user } = useCurrentUser(); + + // Use follower feed for logged-in users, global feed for logged-out users + const globalFeed = usePictureFeed(); + const followerFeed = useFollowerPictureFeed(); + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = user ? followerFeed : globalFeed; + const { ref, inView } = useInView(); useEffect(() => {