chore: added refresh and update feeds
e9c64afd
2 file(s) · +130 −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 |
| 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> |
|