diff options
| author | Adam Mathes <adam@adammathes.com> | 2026-02-13 11:45:02 -0800 |
|---|---|---|
| committer | Adam Mathes <adam@adammathes.com> | 2026-02-13 11:45:02 -0800 |
| commit | f8fcc0be57fa7f471ffd22d4b9559cb6d0ff20bf (patch) | |
| tree | ca2d51b24b0908e1c857618f114ce6fa5593041f /frontend/src/components/FeedItems.tsx | |
| parent | fc2bc854f4e3bae3503d5000f1fbc414bfa7e0cc (diff) | |
| download | neko-f8fcc0be57fa7f471ffd22d4b9559cb6d0ff20bf.tar.gz neko-f8fcc0be57fa7f471ffd22d4b9559cb6d0ff20bf.tar.bz2 neko-f8fcc0be57fa7f471ffd22d4b9559cb6d0ff20bf.zip | |
Implement infinite scroll for feed items view (NK-5ocxgm)
Diffstat (limited to 'frontend/src/components/FeedItems.tsx')
| -rw-r--r-- | frontend/src/components/FeedItems.tsx | 46 |
1 files changed, 42 insertions, 4 deletions
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<Item[]>([]); 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 <div className="feed-items-loading">Loading items...</div>; if (error) return <div className="feed-items-error">Error: {error}</div>; @@ -178,6 +211,11 @@ export default function FeedItems() { <FeedItem item={item} /> </div> ))} + {hasMore && ( + <div id="load-more-sentinel" className="loading-more"> + {loadingMore ? 'Loading more...' : ''} + </div> + )} </ul> )} </div> |
