From f8fcc0be57fa7f471ffd22d4b9559cb6d0ff20bf Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Fri, 13 Feb 2026 11:45:02 -0800 Subject: Implement infinite scroll for feed items view (NK-5ocxgm) --- frontend/src/components/FeedItems.tsx | 46 ++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 4 deletions(-) (limited to 'frontend/src/components/FeedItems.tsx') diff --git a/frontend/src/components/FeedItems.tsx b/frontend/src/components/FeedItems.tsx index aaec2f4..179d5cd 100644 --- a/frontend/src/components/FeedItems.tsx +++ b/frontend/src/components/FeedItems.tsx @@ -11,10 +11,17 @@ export default function FeedItems() { const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [hasMore, setHasMore] = useState(true); const [error, setError] = useState(''); - useEffect(() => { - setLoading(true); + const fetchItems = (maxId?: string) => { + if (maxId) { + setLoadingMore(true); + } else { + setLoading(true); + setItems([]); + } setError(''); let url = '/api/stream'; @@ -26,6 +33,10 @@ export default function FeedItems() { params.append('tag', tagName); } + if (maxId) { + params.append('max_id', maxId); + } + // Apply filters if (filterFn === 'all') { params.append('read_filter', 'all'); @@ -50,13 +61,24 @@ export default function FeedItems() { return res.json(); }) .then((data) => { - setItems(data); + if (maxId) { + setItems((prev) => [...prev, ...data]); + } else { + setItems(data); + } + setHasMore(data.length > 0); setLoading(false); + setLoadingMore(false); }) .catch((err) => { setError(err.message); setLoading(false); + setLoadingMore(false); }); + }; + + useEffect(() => { + fetchItems(); }, [feedId, tagName, filterFn]); const [selectedIndex, setSelectedIndex] = useState(-1); @@ -134,6 +156,14 @@ export default function FeedItems() { const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { + // Infinity scroll sentinel + if (entry.target.id === 'load-more-sentinel') { + if (entry.isIntersecting && !loadingMore && hasMore && items.length > 0) { + fetchItems(String(items[items.length - 1]._id)); + } + return; + } + // If item is not intersecting and is above the viewport, it's been scrolled past if (!entry.isIntersecting && entry.boundingClientRect.top < 0) { const index = Number(entry.target.getAttribute('data-index')); @@ -154,8 +184,11 @@ export default function FeedItems() { if (el) observer.observe(el); }); + const sentinel = document.getElementById('load-more-sentinel'); + if (sentinel) observer.observe(sentinel); + return () => observer.disconnect(); - }, [items]); + }, [items, loadingMore, hasMore]); if (loading) return
Loading items...
; if (error) return
Error: {error}
; @@ -178,6 +211,11 @@ export default function FeedItems() { ))} + {hasMore && ( +
+ {loadingMore ? 'Loading more...' : ''} +
+ )} )} -- cgit v1.2.3