import React, { useRef, useEffect, useContext, useState } from "react"; import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { readTweet, readFollowingFeed, buildCSRFHeaders } from "../ash_rpc"; import { AuthCtx } from "../context"; import { FEED_PAGE_SIZE } from "../constants"; import { Spinner, ErrorBanner } from "./ui"; import { TweetCard } from "./tweet-card"; import type { Tweet } from "../types"; export function Feed() { const sentinelRef = useRef(null); const { data, isLoading, isError, error, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteQuery({ queryKey: ["tweets"], queryFn: async ({ pageParam }: { pageParam: number }) => { const res = await readTweet({ fields: ["id", "content", "likes", "likedByMe", "commentCount", "userId", "state", "userEmail", "userUsername", "userDisplayName", "userAvatarUrl", "insertedAt", { media: ["id", "s3Key"] }], sort: "-insertedAt", page: { limit: FEED_PAGE_SIZE, offset: pageParam }, filter: { parentTweetId: { isNil: true } }, headers: buildCSRFHeaders(), }); if (!res.success) throw new Error("Failed to load tweets"); const pageData = res.data as any; const tweets: Tweet[] = Array.isArray(pageData) ? pageData : (pageData?.results ?? []); const hasMore: boolean = Array.isArray(pageData) ? false : (pageData?.hasMore ?? false); return { tweets, hasMore, nextOffset: pageParam + FEED_PAGE_SIZE }; }, initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.hasMore ? lastPage.nextOffset : undefined, }); useEffect(() => { const el = sentinelRef.current; if (!el) return; const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { fetchNextPage(); } }, { threshold: 0.1 }, ); observer.observe(el); return () => observer.disconnect(); }, [hasNextPage, isFetchingNextPage, fetchNextPage]); if (isLoading) return ; if (isError) return ; const tweets = data?.pages.flatMap((p) => p.tweets) ?? []; if (tweets.length === 0) { return (

Nothing posted yet

Be the first to mix something in.

); } return (
{tweets.map((t) => ( ))}
{isFetchingNextPage && }
); } export function FollowingFeed() { const { userId } = useContext(AuthCtx); const sentinelRef = useRef(null); const { data, isLoading, isError, error, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteQuery({ queryKey: ["following_tweets"], queryFn: async ({ pageParam }: { pageParam: number }) => { const res = await readFollowingFeed({ fields: ["id", "content", "likes", "likedByMe", "commentCount", "userId", "state", "userEmail", "userUsername", "userDisplayName", "userAvatarUrl", "insertedAt", { media: ["id", "s3Key"] }], sort: "-insertedAt", page: { limit: FEED_PAGE_SIZE, offset: pageParam }, filter: { parentTweetId: { isNil: true } }, headers: buildCSRFHeaders(), }); if (!res.success) throw new Error("Failed to load following feed"); const pageData = res.data as any; const tweets: Tweet[] = Array.isArray(pageData) ? pageData : (pageData?.results ?? []); const hasMore: boolean = Array.isArray(pageData) ? false : (pageData?.hasMore ?? false); return { tweets, hasMore, nextOffset: pageParam + FEED_PAGE_SIZE }; }, initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.hasMore ? lastPage.nextOffset : undefined, enabled: !!userId, }); useEffect(() => { const el = sentinelRef.current; if (!el) return; const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { fetchNextPage(); } }, { threshold: 0.1 }, ); observer.observe(el); return () => observer.disconnect(); }, [hasNextPage, isFetchingNextPage, fetchNextPage]); if (!userId) { return (

Your personalised feed

Sign in {" "}to see posts from people you follow.

); } if (isLoading) return ; if (isError) return ; const tweets = data?.pages.flatMap((p) => p.tweets) ?? []; if (tweets.length === 0) { return (

Nothing here yet

Follow some people from the{" "} Users {" "}page to fill this feed.

); } return (
{tweets.map((t) => ( ))}
{isFetchingNextPage && }
); } export function RefreshButton({ queryKey = ["tweets"] }: { queryKey?: string[] }) { const qc = useQueryClient(); const [spinning, setSpinning] = useState(false); async function refresh() { setSpinning(true); await qc.invalidateQueries({ queryKey }); setTimeout(() => setSpinning(false), 600); } return ( ); }