import React, { useState, useEffect, useRef } from 'react'; import { Link } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import localForage from 'localforage'; import { sourceLink, infoLine, logos } from './utils.js'; function Feed({ updateCache, filterSmallweb, feedSources }) { const [stories, setStories] = useState(() => JSON.parse(localStorage.getItem('stories')) || false); const [error, setError] = useState(''); const [loadingStatus, setLoadingStatus] = useState(null); const isInitialMount = useRef(true); useEffect(() => { if (isInitialMount.current) { isInitialMount.current = false; } else { setStories(false); } }, [filterSmallweb, feedSources]); useEffect(() => { const controller = new AbortController(); if ('serviceWorker' in navigator) { navigator.serviceWorker.getRegistration().then(reg => { if (reg) { console.log('Checking for client update...'); reg.update(); } }); } const params = new URLSearchParams(); if (filterSmallweb) { params.append('smallweb', 'true'); } const allSources = Object.keys(feedSources); const enabledSources = allSources.filter(key => feedSources[key]); if (enabledSources.length > 0 && enabledSources.length < allSources.length) { enabledSources.forEach(source => params.append('source', source)); } const apiUrl = `/api?${params.toString()}`; fetch(apiUrl, { signal: controller.signal }) .then(res => { if (!res.ok) { throw new Error(`Server responded with ${res.status} ${res.statusText}`); } return res.json(); }) .then( async (result) => { const newApiStories = result.stories; const updated = !stories || stories.map(s => s.id).join() !== newApiStories.map(s => s.id).join(); console.log('New stories available:', updated); if (!updated) return; setLoadingStatus({ current: 0, total: newApiStories.length }); let currentStories = Array.isArray(stories) ? [...stories] : []; let preloadedCount = 0; for (const [index, newStory] of newApiStories.entries()) { if (controller.signal.aborted) { break; } try { const storyFetchController = new AbortController(); const timeoutId = setTimeout(() => storyFetchController.abort(), 10000); // 10-second timeout const storyRes = await fetch('/api/' + newStory.id, { signal: storyFetchController.signal }); clearTimeout(timeoutId); if (!storyRes.ok) { throw new Error(`Server responded with ${storyRes.status} ${storyRes.statusText}`); } const storyResult = await storyRes.json(); const fullStory = storyResult.story; await localForage.setItem(fullStory.id, fullStory); console.log('Preloaded story:', fullStory.id, fullStory.title); updateCache(fullStory.id, fullStory); preloadedCount++; setLoadingStatus({ current: preloadedCount, total: newApiStories.length }); const existingStoryIndex = currentStories.findIndex(s => s.id === newStory.id); if (existingStoryIndex > -1) { currentStories.splice(existingStoryIndex, 1); } currentStories.splice(index, 0, newStory); localStorage.setItem('stories', JSON.stringify(currentStories)); setStories(currentStories); } catch (error) { let errorMessage; if (error.name === 'AbortError') { errorMessage = `The request to fetch story '${newStory.title}' (${newStory.id}) timed out after 10 seconds. Your connection may be unstable. (${preloadedCount} / ${newApiStories.length} stories preloaded)`; console.log('Fetch timed out for story:', newStory.id); } else { errorMessage = `An error occurred while fetching story '${newStory.title}' (ID: ${newStory.id}): ${error.toString()}. (${preloadedCount} / ${newApiStories.length} stories preloaded)`; console.log('Fetch failed for story:', newStory.id, error); } setError(errorMessage); break; } } const finalStories = currentStories.slice(0, newApiStories.length); const removedStories = currentStories.slice(newApiStories.length); for (const story of removedStories) { console.log('Removed story:', story.id, story.title); localForage.removeItem(story.id); } localStorage.setItem('stories', JSON.stringify(finalStories)); setStories(finalStories); setLoadingStatus(null); }, (error) => { if (error.name === 'AbortError') { console.log('Feed fetch aborted.'); return; } const errorMessage = `Failed to fetch the main story list from the API. Your connection may be down or the server might be experiencing issues. ${error.toString()}.`; setError(errorMessage); } ); return () => controller.abort(); }, [updateCache, filterSmallweb, feedSources]); return (
{error}
{stories &&Loaded feed from cache.
}Loading...
} {loadingStatus &&Preloading stories {loadingStatus.current} / {loadingStatus.total}...
}