chore: added refresh and update feeds e9c64afd
Steve · 2025-11-02 07:19 2 file(s) · +130 −2
README.md +2 −2
7 7
8 8
## Roadmap
9 9
10 -
- [ ] Mark posts as read/unread
10 +
- [x] Mark posts as read/unread
11 11
- [ ] Import/Export OPML
12 12
- [ ] Import/Export account through mnemonic
13 -
- [ ] Refresh/Update Feeds
13 +
- [x] Refresh/Update Feeds
14 14
- [ ] Tweakcn theme switching
src/components/app-sidebar.tsx +128 −0
93 93
	const [mobileView, setMobileView] = React.useState<"feeds" | "posts">(
94 94
		"feeds",
95 95
	);
96 +
	const hasRefreshedOnMount = React.useRef(false);
96 97
97 98
	const { hidden, isMobile, setOpenMobile } = useSidebar();
98 99
	const { insert, update } = useEvolu();
252 253
			`Marked ${unmarkedCount} post${unmarkedCount !== 1 ? "s" : ""} as unread`,
253 254
		);
254 255
	}, [filteredPosts, allReadStatusesWithUnread, insert, update]);
256 +
257 +
	const refreshFeeds = React.useCallback(async () => {
258 +
		if (allFeeds.length === 0) {
259 +
			toast.error("No feeds to refresh");
260 +
			return;
261 +
		}
262 +
263 +
		toast.info(
264 +
			`Refreshing ${allFeeds.length} feed${allFeeds.length !== 1 ? "s" : ""}...`,
265 +
		);
266 +
		let totalNewPosts = 0;
267 +
268 +
		try {
269 +
			for (const feed of allFeeds) {
270 +
				try {
271 +
					let xmlData: string;
272 +
273 +
					// Try to fetch directly first
274 +
					try {
275 +
						const xmlFetch = await fetch(feed.feedUrl);
276 +
						xmlData = await xmlFetch.text();
277 +
					} catch (corsError) {
278 +
						// Fall back to corsproxy.io if CORS error occurs
279 +
						const xmlFetch = await fetch(
280 +
							`https://corsproxy.io/?url=${encodeURIComponent(feed.feedUrl)}`,
281 +
						);
282 +
						xmlData = await xmlFetch.text();
283 +
					}
284 +
285 +
					const parsedXmlData = await parser.parse(xmlData);
286 +
287 +
					// Determine if it's RSS or Atom feed
288 +
					let feedData: any;
289 +
					let posts: any[];
290 +
					let isAtom = false;
291 +
292 +
					if (parsedXmlData.rss) {
293 +
						// RSS feed
294 +
						feedData = parsedXmlData.rss.channel;
295 +
						posts = feedData.item || [];
296 +
					} else if (parsedXmlData.feed) {
297 +
						// Atom feed
298 +
						feedData = parsedXmlData.feed;
299 +
						posts = feedData.entry || [];
300 +
						isAtom = true;
301 +
					} else {
302 +
						console.warn(`Unsupported feed format for ${feed.title}`);
303 +
						continue;
304 +
					}
305 +
306 +
					// Get existing posts for this feed to avoid duplicates
307 +
					// Use allPosts to ensure we check against all posts in the database
308 +
					const existingPosts = allPosts.filter((p) => p.feedId === feed.id);
309 +
					const existingLinks = new Set(existingPosts.map((p) => p.link));
310 +
311 +
					// Process new posts/entries
312 +
					let newPostsCount = 0;
313 +
					for (const post of posts) {
314 +
						const postLink = isAtom
315 +
							? typeof post.link === "string"
316 +
								? post.link || post.id
317 +
								: post.link?.[0] || post.id
318 +
							: post.link || post.id;
319 +
320 +
						// Skip if we already have this post
321 +
						if (existingLinks.has(postLink)) {
322 +
							continue;
323 +
						}
324 +
325 +
						insert("rssPost", {
326 +
							title: post.title,
327 +
							author: isAtom
328 +
								? post.author?.name || feedData.title
329 +
								: post.author || feedData.title,
330 +
							publishedDate: new Date(
331 +
								post.pubDate || post.updated,
332 +
							).toISOString(),
333 +
							link: postLink,
334 +
							feedId: feed.id,
335 +
							content:
336 +
								post["content:encoded"] ||
337 +
								post.content ||
338 +
								"Please open on the web",
339 +
						});
340 +
						newPostsCount++;
341 +
					}
342 +
343 +
					totalNewPosts += newPostsCount;
344 +
345 +
					// Update feed's dateUpdated
346 +
					update("rssFeed", {
347 +
						id: feed.id as any,
348 +
						dateUpdated: new Date().toISOString(),
349 +
					});
350 +
				} catch (error) {
351 +
					console.error(`Error refreshing feed "${feed.title}":`, error);
352 +
					// Continue with other feeds even if one fails
353 +
				}
354 +
			}
355 +
356 +
			if (totalNewPosts > 0) {
357 +
				toast.success(
358 +
					`Refreshed feeds and found ${totalNewPosts} new post${totalNewPosts !== 1 ? "s" : ""}`,
359 +
				);
360 +
			} else {
361 +
				toast.success("All feeds up to date");
362 +
			}
363 +
		} catch (error) {
364 +
			console.error("Error refreshing feeds:", error);
365 +
			toast.error("Failed to refresh feeds");
366 +
		}
367 +
	}, [allFeeds, allPosts, insert, update]);
368 +
369 +
	// Run refresh on component mount (only once, even in strict mode)
370 +
	React.useEffect(() => {
371 +
		if (!hasRefreshedOnMount.current) {
372 +
			hasRefreshedOnMount.current = true;
373 +
			refreshFeeds();
374 +
		}
375 +
		// eslint-disable-next-line react-hooks/exhaustive-deps
376 +
	}, []); // Only run once on mount
255 377
256 378
	async function addFeed() {
257 379
		if (!urlInput.trim()) {
547 669
												<SidebarMenuButton onClick={reset}>
548 670
													<RotateCw className="size-4" />
549 671
													<span>Reset</span>
672 +
												</SidebarMenuButton>
673 +
											</SidebarMenuItem>
674 +
											<SidebarMenuItem>
675 +
												<SidebarMenuButton onClick={refreshFeeds}>
676 +
													<RotateCw className="size-4" />
677 +
													<span>Refresh</span>
550 678
												</SidebarMenuButton>
551 679
											</SidebarMenuItem>
552 680
										</SidebarMenu>