|
26 | 26 | import PencilSimpleLineIcon from 'phosphor-svelte/lib/PencilSimpleLine'; |
27 | 27 | import FolderIcon from 'phosphor-svelte/lib/Folder'; |
28 | 28 | import { loadFollowListProfiles, getFollowedPubkeys, followListReady } from '$lib/followListCache'; |
29 | | - import { addArticles as addToSharedStore, refreshCover as refreshSharedCover } from '$lib/articleStore'; |
| 29 | + import { articleStore, addArticles as addToSharedStore, refreshCover as refreshSharedCover } from '$lib/articleStore'; |
| 30 | + import { get } from 'svelte/store'; |
30 | 31 |
|
31 | 32 | $: isSignedIn = $userPublickey !== ''; |
32 | 33 | $: draftCount = $drafts.length; |
|
51 | 52 | let pullToRefreshEl: PullToRefresh; |
52 | 53 | |
53 | 54 | // Sync local articles to shared store (for explore page reuse) |
54 | | - $: if (articles.length > 0) { |
55 | | - addToSharedStore(articles); |
56 | | - refreshSharedCover(); |
| 55 | + // Only sync newly added articles to avoid rebuilding on every update |
| 56 | + let lastSyncedArticleCount = 0; |
| 57 | + $: { |
| 58 | + if (articles.length > lastSyncedArticleCount) { |
| 59 | + const newArticles = articles.slice(lastSyncedArticleCount); |
| 60 | + if (newArticles.length > 0) { |
| 61 | + addToSharedStore(newArticles); |
| 62 | + refreshSharedCover(); |
| 63 | + } |
| 64 | + lastSyncedArticleCount = articles.length; |
| 65 | + } else if (articles.length === 0) { |
| 66 | + lastSyncedArticleCount = 0; |
| 67 | + } |
57 | 68 | } |
58 | 69 |
|
59 | 70 | // Pagination tracking |
|
155 | 166 | // Always fetch all articles - feed shows all, cover always food-editorial |
156 | 167 | const cacheFilter = { kinds: [30023], hashtags: getFoodHashtags(), limit: 500 }; |
157 | 168 |
|
158 | | - // Try to load from cache first for instant paint |
| 169 | + // Try to hydrate from shared store first (e.g. if user visited /explore first) |
159 | 170 | let cacheWasUsed = false; |
160 | 171 | let cacheSufficient = false; |
161 | | - |
162 | | - if (!forceRefresh && browser) { |
| 172 | +
|
| 173 | + if (!forceRefresh) { |
| 174 | + const shared = get(articleStore); |
| 175 | + if (shared.length > 0 && articles.length === 0) { |
| 176 | + for (const article of shared) { |
| 177 | + if (!seenEventIds.has(article.id)) { |
| 178 | + seenEventIds.add(article.id); |
| 179 | + } |
| 180 | + } |
| 181 | + articles = [...shared].sort((a, b) => b.publishedAt - a.publishedAt); |
| 182 | + lastSyncedArticleCount = articles.length; |
| 183 | +
|
| 184 | + const coverArticles = articles.filter(a => isValidLongformArticle(a.event)); |
| 185 | + if (coverArticles.length >= 1) { |
| 186 | + cover = curateCover(coverArticles, false); |
| 187 | + } |
| 188 | + loading = false; |
| 189 | + cacheWasUsed = true; |
| 190 | + cacheSufficient = true; |
| 191 | +
|
| 192 | + if (articles.length > 0) { |
| 193 | + oldestTimestamp = Math.min(...articles.map(a => a.publishedAt)); |
| 194 | + } |
| 195 | + } |
| 196 | + } |
| 197 | +
|
| 198 | + // Try to load from IndexedDB cache for instant paint |
| 199 | + if (!forceRefresh && !cacheSufficient && browser) { |
163 | 200 | try { |
164 | 201 | const cachedEvents = await loadCachedFeedEvents(cacheFilter); |
165 | 202 | if (cachedEvents.length > 0) { |
|
190 | 227 | } |
191 | 228 | } |
192 | 229 |
|
193 | | - // If cache is fresh and sufficient, skip network fetch entirely |
194 | | - // Schedule a background refresh for later instead |
| 230 | + // If cache is fresh and sufficient, skip the primary food fetch |
| 231 | + // but still run the general fetch + background refresh so non-food categories populate |
195 | 232 | if (cacheSufficient && isCacheFresh() && !forceRefresh) { |
196 | 233 | if (import.meta.env.DEV) { |
197 | | - console.log('[Reads] Cache is fresh, skipping network fetch'); |
| 234 | + console.log('[Reads] Cache is fresh, skipping primary food fetch'); |
198 | 235 | } |
199 | | - |
200 | | - // Schedule background refresh after delay (won't block UI) |
| 236 | +
|
| 237 | + const startGen = getCurrentRelayGeneration(); |
| 238 | +
|
| 239 | + // General (all-topic) fetch so non-food categories aren't empty |
| 240 | + setTimeout(() => { |
| 241 | + if (getCurrentRelayGeneration() === startGen) { |
| 242 | + fetchGeneralArticles(startGen); |
| 243 | + } |
| 244 | + }, 1000); |
| 245 | +
|
| 246 | + // Schedule background refresh after delay |
201 | 247 | setTimeout(() => { |
202 | 248 | if (browser) { |
203 | 249 | backgroundRefresh(); |
204 | 250 | } |
205 | 251 | }, BACKGROUND_REFRESH_DELAY_MS); |
206 | | - |
| 252 | +
|
207 | 253 | return; |
208 | 254 | } |
209 | 255 |
|
|
534 | 580 | 'wss://relay.primal.net', |
535 | 581 | 'wss://nos.lol', |
536 | 582 | 'wss://relay.damus.io', |
537 | | - 'wss://nostr.wine' |
| 583 | + 'wss://nostr.wine', |
| 584 | + 'wss://antiprimal.net' |
538 | 585 | ]; |
539 | 586 | |
540 | 587 | console.log('[Reads] Direct fallback: querying', articleRelays.join(', ')); |
|
0 commit comments