- {#if shouldAutoRenderMedia && (post.content && post.content.trim())} + {#if shouldAutoRenderMedia} {/if}

diff --git a/src/routes/relay/+page.svelte b/src/routes/relay/+page.svelte index 99ab93b..fd3b3b6 100644 --- a/src/routes/relay/+page.svelte +++ b/src/routes/relay/+page.svelte @@ -7,6 +7,7 @@ import { goto } from '$app/navigation'; import { sessionManager } from '../../lib/services/auth/session-manager.js'; import { KIND } from '../../lib/types/kind-lookup.js'; + import type { NostrEvent } from '../../lib/types/nostr.js'; interface RelayInfo { url: string; @@ -82,12 +83,54 @@ relays = relayList; - // Load favorite relays if user is logged in - await loadFavoriteRelays(); + // Load favorite relays if user is logged in (non-blocking - show page immediately) + loadFavoriteRelays().catch(err => { + console.debug('Error loading favorite relays in background:', err); + }); loading = false; } + function processFavoriteRelayEvent(event: NostrEvent): void { + // Extract relay URLs from 'relay' tags (kind 10012 uses 'relay' tags) + const favoriteRelayUrls = new Set(); + for (const tag of event.tags) { + if (tag[0] === 'relay' && tag[1]) { + // Normalize URL: trim, remove trailing slash, ensure protocol + let url = tag[1].trim(); + // Remove trailing slash + url = url.replace(/\/$/, ''); + // If no protocol, assume wss:// + if (!url.startsWith('ws://') && !url.startsWith('wss://')) { + url = `wss://${url}`; + } + favoriteRelayUrls.add(url); + } + } + + // Get connection status - normalize URLs for comparison + const connectedRelays = nostrClient.getConnectedRelays(); + const normalizeUrlForComparison = (url: string): string => { + return url.replace(/\/$/, '').toLowerCase(); + }; + const favoriteRelayList: RelayInfo[] = Array.from(favoriteRelayUrls).map(url => { + const normalizedUrl = normalizeUrlForComparison(url); + const isConnected = connectedRelays.some(connected => + normalizeUrlForComparison(connected) === normalizedUrl + ); + return { + url, + categories: ['Favorite'], + connected: isConnected + }; + }); + + // Sort by URL + favoriteRelayList.sort((a, b) => a.url.localeCompare(b.url)); + + favoriteRelays = favoriteRelayList; + } + async function loadFavoriteRelays() { const currentPubkey = sessionManager.getCurrentPubkey(); if (!currentPubkey) { @@ -95,63 +138,51 @@ return; } + // Load from cache first (fast - instant display) + try { + const { getRecentCachedEvents } = await import('../../lib/services/cache/event-cache.js'); + const cachedEvents = await getRecentCachedEvents([KIND.FAVORITE_RELAYS], 60 * 60 * 1000, 10); // 1 hour cache + const cachedFavoriteEvent = cachedEvents.find(e => e.pubkey === currentPubkey); + + if (cachedFavoriteEvent) { + // Process cached event immediately + processFavoriteRelayEvent(cachedFavoriteEvent); + } + } catch (error) { + console.debug('Error loading cached favorite relays:', error); + } + try { // Fetch kind 10012 (FAVORITE_RELAYS) event for current user // Fetch multiple in case there are multiple replaceable events const favoriteEvents = await nostrClient.fetchEvents( [{ kinds: [KIND.FAVORITE_RELAYS], authors: [currentPubkey], limit: 10 }], relayManager.getProfileReadRelays(), - { useCache: true, cacheResults: true, timeout: 5000 } + { + useCache: 'cache-first', // Already shown cache above, now stream updates + cacheResults: true, + timeout: config.standardTimeout + } ); if (favoriteEvents.length === 0) { - favoriteRelays = []; + if (favoriteRelays.length === 0) { + favoriteRelays = []; + } return; } // Get the latest event (replaceable event, so should be only one, but take latest just in case) const latestEvent = favoriteEvents.sort((a, b) => b.created_at - a.created_at)[0]; - // Extract relay URLs from 'relay' tags (kind 10012 uses 'relay' tags) - const favoriteRelayUrls = new Set(); - for (const tag of latestEvent.tags) { - if (tag[0] === 'relay' && tag[1]) { - // Normalize URL: trim, remove trailing slash, ensure protocol - let url = tag[1].trim(); - // Remove trailing slash - url = url.replace(/\/$/, ''); - // If no protocol, assume wss:// - if (!url.startsWith('ws://') && !url.startsWith('wss://')) { - url = `wss://${url}`; - } - favoriteRelayUrls.add(url); - } - } - - // Get connection status - normalize URLs for comparison - const connectedRelays = nostrClient.getConnectedRelays(); - const normalizeUrlForComparison = (url: string): string => { - return url.replace(/\/$/, '').toLowerCase(); - }; - const favoriteRelayList: RelayInfo[] = Array.from(favoriteRelayUrls).map(url => { - const normalizedUrl = normalizeUrlForComparison(url); - const isConnected = connectedRelays.some(connected => - normalizeUrlForComparison(connected) === normalizedUrl - ); - return { - url, - categories: ['Favorite'], - connected: isConnected - }; - }); - - // Sort by URL - favoriteRelayList.sort((a, b) => a.url.localeCompare(b.url)); - - favoriteRelays = favoriteRelayList; + // Process the event (update favorite relays with fresh data) + processFavoriteRelayEvent(latestEvent); } catch (err) { console.error('Error loading favorite relays:', err); - favoriteRelays = []; + // Don't clear favoriteRelays if we have cached data + if (favoriteRelays.length === 0) { + favoriteRelays = []; + } } } diff --git a/src/routes/rss/+page.svelte b/src/routes/rss/+page.svelte index b9824c1..ac92057 100644 --- a/src/routes/rss/+page.svelte +++ b/src/routes/rss/+page.svelte @@ -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 @@ let rssItems = $state([]); let feedErrors = $state>(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 @@ // Track the last feeds we loaded to detect changes let lastLoadedFeeds = $state([]); + // 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 @@ 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 @@ // 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 @@ 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((_, 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 @@ 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 @@ {:else if rssItems.length === 0}

No RSS items found.

{:else} +
+

+ Showing {paginatedItems.length} of {rssItems.length} items + {#if totalPages > 1} + (Page {currentPage} of {totalPages}) + {/if} +

+
+ + {#if totalPages > 1} + + {/if} +
- {#each rssItems as item (item.link + item.pubDate.getTime())} + {#each paginatedItems as item (item.link + item.pubDate.getTime())}
@@ -435,6 +534,38 @@
{/each}
+ + {#if totalPages > 1} + + {/if} {/if}