|
|
|
|
@ -3,6 +3,7 @@
@@ -3,6 +3,7 @@
|
|
|
|
|
import { sessionManager } from '../../lib/services/auth/session-manager.js'; |
|
|
|
|
import { nostrClient } from '../../lib/services/nostr/nostr-client.js'; |
|
|
|
|
import { relayManager } from '../../lib/services/nostr/relay-manager.js'; |
|
|
|
|
import { config } from '../../lib/services/nostr/config.js'; |
|
|
|
|
import { cacheRSSFeed, getCachedRSSFeed, type RSSItem } from '../../lib/services/cache/rss-cache.js'; |
|
|
|
|
import { onMount } from 'svelte'; |
|
|
|
|
import { goto } from '$app/navigation'; |
|
|
|
|
@ -22,6 +23,8 @@
@@ -22,6 +23,8 @@
|
|
|
|
|
let rssItems = $state<RSSItem[]>([]); |
|
|
|
|
let feedErrors = $state<Map<string, string>>(new Map()); |
|
|
|
|
let feedsLoaded = $state(false); // Track if feeds have been loaded to prevent re-triggering |
|
|
|
|
let currentPage = $state(1); |
|
|
|
|
const itemsPerPage = 50; |
|
|
|
|
|
|
|
|
|
let subscribedFeeds = $derived.by(() => { |
|
|
|
|
if (!rssEvent) return []; |
|
|
|
|
@ -33,6 +36,16 @@
@@ -33,6 +36,16 @@
|
|
|
|
|
// Track the last feeds we loaded to detect changes |
|
|
|
|
let lastLoadedFeeds = $state<string[]>([]); |
|
|
|
|
|
|
|
|
|
// Computed: get items for current page |
|
|
|
|
let paginatedItems = $derived.by(() => { |
|
|
|
|
const start = (currentPage - 1) * itemsPerPage; |
|
|
|
|
const end = start + itemsPerPage; |
|
|
|
|
return rssItems.slice(start, end); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Computed: total pages |
|
|
|
|
let totalPages = $derived.by(() => Math.ceil(rssItems.length / itemsPerPage)); |
|
|
|
|
|
|
|
|
|
onMount(async () => { |
|
|
|
|
await nostrClient.initialize(); |
|
|
|
|
const session = sessionManager.getSession(); |
|
|
|
|
@ -61,19 +74,53 @@
@@ -61,19 +74,53 @@
|
|
|
|
|
rssItems = []; |
|
|
|
|
feedErrors.clear(); |
|
|
|
|
lastLoadedFeeds = []; |
|
|
|
|
currentPage = 1; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Reset to page 1 when items change significantly |
|
|
|
|
$effect(() => { |
|
|
|
|
if (rssItems.length > 0 && currentPage > totalPages) { |
|
|
|
|
currentPage = 1; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
async function checkRssEvent() { |
|
|
|
|
if (!currentPubkey) return; |
|
|
|
|
|
|
|
|
|
loading = true; |
|
|
|
|
// Load from cache first (fast - instant display) |
|
|
|
|
try { |
|
|
|
|
const { getRecentCachedEvents } = await import('../../lib/services/cache/event-cache.js'); |
|
|
|
|
const cachedEvents = await getRecentCachedEvents([RSS_FEED_KIND], 60 * 60 * 1000, 1); // 1 hour cache |
|
|
|
|
const cachedRssEvent = cachedEvents.find(e => e.pubkey === currentPubkey); |
|
|
|
|
|
|
|
|
|
if (cachedRssEvent) { |
|
|
|
|
// Only update if it's actually different (prevents unnecessary re-renders) |
|
|
|
|
if (!rssEvent || rssEvent.id !== cachedRssEvent.id) { |
|
|
|
|
rssEvent = cachedRssEvent; |
|
|
|
|
// Reset feeds loaded state when event changes |
|
|
|
|
feedsLoaded = false; |
|
|
|
|
lastLoadedFeeds = []; |
|
|
|
|
} |
|
|
|
|
loading = false; // Show cached content immediately |
|
|
|
|
} else { |
|
|
|
|
loading = true; // Only show loading if no cache |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.debug('Error loading cached RSS event:', error); |
|
|
|
|
loading = true; // Show loading if cache check fails |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
const relays = relayManager.getProfileReadRelays(); |
|
|
|
|
const events = await nostrClient.fetchEvents( |
|
|
|
|
[{ kinds: [RSS_FEED_KIND], authors: [currentPubkey], limit: 1 }], |
|
|
|
|
relays, |
|
|
|
|
{ useCache: true, cacheResults: true } |
|
|
|
|
{ |
|
|
|
|
useCache: 'cache-first', // Already shown cache above, now stream updates |
|
|
|
|
cacheResults: true, |
|
|
|
|
timeout: config.standardTimeout |
|
|
|
|
} |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
if (events.length > 0) { |
|
|
|
|
@ -84,6 +131,7 @@
@@ -84,6 +131,7 @@
|
|
|
|
|
// Reset feeds loaded state when event changes |
|
|
|
|
feedsLoaded = false; |
|
|
|
|
lastLoadedFeeds = []; |
|
|
|
|
currentPage = 1; // Reset to first page when event changes |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
@ -133,15 +181,24 @@
@@ -133,15 +181,24 @@
|
|
|
|
|
|
|
|
|
|
if (feedsToFetch.length > 0) { |
|
|
|
|
const fetchPromises = feedsToFetch.map(async (feedUrl) => { |
|
|
|
|
try { |
|
|
|
|
const items = await fetchRssFeed(feedUrl); |
|
|
|
|
try { |
|
|
|
|
// Add timeout to prevent hanging requests |
|
|
|
|
const timeoutPromise = new Promise<never>((_, reject) => { |
|
|
|
|
setTimeout(() => reject(new Error('Request timeout')), 10000); // 10 second timeout |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const items = await Promise.race([ |
|
|
|
|
fetchRssFeed(feedUrl), |
|
|
|
|
timeoutPromise |
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
// Cache the fetched items |
|
|
|
|
await cacheRSSFeed(feedUrl, items); |
|
|
|
|
return { feedUrl, items }; |
|
|
|
|
} catch (error) { |
|
|
|
|
} catch (error) { |
|
|
|
|
// Only log non-CORS errors to avoid console spam |
|
|
|
|
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch feed'; |
|
|
|
|
if (!errorMessage.includes('CORS') && !errorMessage.includes('Cross-Origin')) { |
|
|
|
|
if (!errorMessage.includes('CORS') && !errorMessage.includes('Cross-Origin') && !errorMessage.includes('timeout')) { |
|
|
|
|
console.error(`Error fetching RSS feed ${feedUrl}:`, error); |
|
|
|
|
} |
|
|
|
|
feedErrors.set(feedUrl, errorMessage); |
|
|
|
|
@ -166,11 +223,12 @@
@@ -166,11 +223,12 @@
|
|
|
|
|
itemMap.set(item.link, item); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Sort by date (newest first) |
|
|
|
|
// Sort by date (newest first) |
|
|
|
|
const combinedItems = Array.from(itemMap.values()); |
|
|
|
|
combinedItems.sort((a, b) => b.pubDate.getTime() - a.pubDate.getTime()); |
|
|
|
|
|
|
|
|
|
rssItems = combinedItems; |
|
|
|
|
currentPage = 1; // Reset to first page when items update |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
feedsLoaded = true; |
|
|
|
|
@ -383,8 +441,49 @@
@@ -383,8 +441,49 @@
|
|
|
|
|
{:else if rssItems.length === 0} |
|
|
|
|
<p class="text-fog-text-light dark:text-fog-dark-text-light">No RSS items found.</p> |
|
|
|
|
{:else} |
|
|
|
|
<div class="rss-items-info mb-4"> |
|
|
|
|
<p class="text-fog-text dark:text-fog-dark-text text-sm"> |
|
|
|
|
Showing {paginatedItems.length} of {rssItems.length} items |
|
|
|
|
{#if totalPages > 1} |
|
|
|
|
(Page {currentPage} of {totalPages}) |
|
|
|
|
{/if} |
|
|
|
|
</p> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
{#if totalPages > 1} |
|
|
|
|
<div class="pagination pagination-top mb-4"> |
|
|
|
|
<button |
|
|
|
|
class="pagination-button" |
|
|
|
|
disabled={currentPage === 1} |
|
|
|
|
onclick={() => { |
|
|
|
|
if (currentPage > 1) currentPage--; |
|
|
|
|
}} |
|
|
|
|
aria-label="Previous page" |
|
|
|
|
> |
|
|
|
|
Previous |
|
|
|
|
</button> |
|
|
|
|
|
|
|
|
|
<div class="pagination-info"> |
|
|
|
|
<span class="text-fog-text dark:text-fog-dark-text"> |
|
|
|
|
Page {currentPage} of {totalPages} |
|
|
|
|
</span> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<button |
|
|
|
|
class="pagination-button" |
|
|
|
|
disabled={currentPage === totalPages} |
|
|
|
|
onclick={() => { |
|
|
|
|
if (currentPage < totalPages) currentPage++; |
|
|
|
|
}} |
|
|
|
|
aria-label="Next page" |
|
|
|
|
> |
|
|
|
|
Next |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
|
|
|
|
|
<div class="rss-items"> |
|
|
|
|
{#each rssItems as item (item.link + item.pubDate.getTime())} |
|
|
|
|
{#each paginatedItems as item (item.link + item.pubDate.getTime())} |
|
|
|
|
<article class="rss-item"> |
|
|
|
|
<div class="rss-item-header"> |
|
|
|
|
<div class="rss-item-meta"> |
|
|
|
|
@ -435,6 +534,38 @@
@@ -435,6 +534,38 @@
|
|
|
|
|
</article> |
|
|
|
|
{/each} |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
{#if totalPages > 1} |
|
|
|
|
<div class="pagination pagination-bottom mt-4"> |
|
|
|
|
<button |
|
|
|
|
class="pagination-button" |
|
|
|
|
disabled={currentPage === 1} |
|
|
|
|
onclick={() => { |
|
|
|
|
if (currentPage > 1) currentPage--; |
|
|
|
|
}} |
|
|
|
|
aria-label="Previous page" |
|
|
|
|
> |
|
|
|
|
Previous |
|
|
|
|
</button> |
|
|
|
|
|
|
|
|
|
<div class="pagination-info"> |
|
|
|
|
<span class="text-fog-text dark:text-fog-dark-text"> |
|
|
|
|
Page {currentPage} of {totalPages} |
|
|
|
|
</span> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<button |
|
|
|
|
class="pagination-button" |
|
|
|
|
disabled={currentPage === totalPages} |
|
|
|
|
onclick={() => { |
|
|
|
|
if (currentPage < totalPages) currentPage++; |
|
|
|
|
}} |
|
|
|
|
aria-label="Next page" |
|
|
|
|
> |
|
|
|
|
Next |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
@ -623,4 +754,62 @@
@@ -623,4 +754,62 @@
|
|
|
|
|
color: var(--fog-dark-text, #cbd5e1); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.rss-items-info { |
|
|
|
|
padding: 0.5rem 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.pagination { |
|
|
|
|
display: flex; |
|
|
|
|
justify-content: center; |
|
|
|
|
align-items: center; |
|
|
|
|
gap: 1rem; |
|
|
|
|
margin-top: 2rem; |
|
|
|
|
margin-bottom: 2rem; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.pagination-top { |
|
|
|
|
margin-top: 0; |
|
|
|
|
margin-bottom: 1rem; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.pagination-bottom { |
|
|
|
|
margin-top: 2rem; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.pagination-button { |
|
|
|
|
padding: 0.5rem 1rem; |
|
|
|
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
|
|
|
border-radius: 0.25rem; |
|
|
|
|
background: var(--fog-post, #ffffff); |
|
|
|
|
color: var(--fog-text, #1e293b); |
|
|
|
|
cursor: pointer; |
|
|
|
|
transition: all 0.2s; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.pagination-button:hover:not(:disabled) { |
|
|
|
|
background: var(--fog-highlight, #f1f5f9); |
|
|
|
|
border-color: var(--fog-accent, #94a3b8); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.pagination-button:disabled { |
|
|
|
|
opacity: 0.5; |
|
|
|
|
cursor: not-allowed; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .pagination-button { |
|
|
|
|
background: var(--fog-dark-post, #1f2937); |
|
|
|
|
border-color: var(--fog-dark-border, #374151); |
|
|
|
|
color: var(--fog-dark-text, #f9fafb); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .pagination-button:hover:not(:disabled) { |
|
|
|
|
background: var(--fog-dark-highlight, #475569); |
|
|
|
|
border-color: var(--fog-dark-accent, #64748b); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.pagination-info { |
|
|
|
|
min-width: 120px; |
|
|
|
|
text-align: center; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
</style> |
|
|
|
|
|