Skip to content

Commit 21dfbea

Browse files
spe1020claude
authored andcommitted
fix: address Copilot review feedback on reads article improvements
- Run general (no-hashtag) article fetch even when cache is fresh so non-food categories populate - Hydrate reads page from shared articleStore on load for cross-page caching between /reads and /explore - Optimize shared store sync to only add newly appended articles - Make selectedSort reactive to login state transitions - Separate follow pubkey set from profile cache to prevent non-followed pubkeys from skewing "For You" ranking - Scan all image regex matches in extractImage (not just first) - Fix misleading IndexedDB comment in articleStore - Remove unused imports in LongformFoodFeed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cc2a607 commit 21dfbea

File tree

7 files changed

+89
-26
lines changed

7 files changed

+89
-26
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "zap.cooking",
33
"license": "MIT",
4-
"version": "4.2.118",
4+
"version": "4.2.119",
55
"private": true,
66
"scripts": {
77
"dev": "vite dev",

src/components/LongformFoodFeed.svelte

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@
77
import {
88
TOP_RELAY_FOOD_HASHTAGS,
99
isValidLongformArticle,
10-
isValidLongformArticleNoFoodFilter,
1110
eventToArticleData,
1211
type ArticleData
1312
} from '$lib/articleUtils';
14-
import { articleStore, foodArticles, addArticles } from '$lib/articleStore';
13+
import { foodArticles, addArticles } from '$lib/articleStore';
1514
1615
let localArticles: ArticleData[] = [];
1716
let loading = true;

src/components/table/FeedSection.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
let selectedCategory = 'All';
1717
let selectedSort: SortOption = isSignedIn ? 'foryou' : 'newest';
1818
19+
// When user signs in, switch to "For You" if still on unsigned default
20+
$: if (isSignedIn && selectedSort === 'newest') {
21+
selectedSort = 'foryou';
22+
}
23+
1924
// Filter out cover articles and articles without real images (no placeholders in reads)
2025
$: feedArticles = articles.filter((a) => !coverArticleIds.includes(a.id) && a.imageUrl);
2126
$: dedupedArticles = deduplicatePreviewArticles(feedArticles);

src/lib/articleStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* Shared article store — single source of truth for articles across reads and explore pages.
3-
* Articles are fetched once and cached in this store + IndexedDB.
3+
* Articles are fetched once and kept in these Svelte stores (in-memory cache).
44
*/
55
import { writable, derived, get } from 'svelte/store';
66
import type { ArticleData, CuratedCover } from './articleUtils';

src/lib/articleUtils.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -301,10 +301,12 @@ export function extractImage(event: NDKEvent): string | null {
301301
return imageTag[1];
302302
}
303303

304-
// Extract first image URL from content
304+
// Extract first valid image URL from content
305305
const imageMatches = content.match(IMAGE_URL_REGEX);
306-
if (imageMatches && imageMatches.length > 0 && isValidImageUrl(imageMatches[0])) {
307-
return imageMatches[0];
306+
if (imageMatches) {
307+
for (const url of imageMatches) {
308+
if (isValidImageUrl(url)) return url;
309+
}
308310
}
309311

310312
return null;
@@ -479,7 +481,7 @@ const MAX_ARTICLES_PER_AUTHOR = 3; // Limit articles per author in feed
479481

480482
// Blacklisted pubkeys — known spammers filtered at quality-check level
481483
const BLACKLISTED_PUBKEYS = new Set<string>([
482-
// Add known spammer hex pubkeys here
484+
'73d9e19ef07e0d098fc0fc5fb75db0f854824e8b4e43905acce638ddf6469960', // npub1w0v7r8hs0cxsnr7ql30mwhdslp2gyn5tfepeqkkvucudmajxn9sqgz5svp
483485
]);
484486

485487
// Spam title patterns (case-insensitive)

src/lib/followListCache.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export interface CachedProfile {
1717

1818
// Singleton cache
1919
const profileCache = new Map<string, CachedProfile>();
20+
/** Pubkeys derived exclusively from the kind:3 contact list (not from search/addToCache) */
21+
const followPubkeySet = new Set<string>();
2022
let isLoading = false;
2123
let isLoaded = false;
2224
let loadPromise: Promise<void> | null = null;
@@ -76,11 +78,17 @@ async function doLoadFollowList(): Promise<void> {
7678
return;
7779
}
7880

79-
// Extract all follow pubkeys
81+
// Extract all follow pubkeys from the kind:3 contact list
8082
const followPubkeys = contactEvent.tags
8183
.filter(t => t[0] === 'p' && t[1])
8284
.map(t => t[1]);
83-
85+
86+
// Populate the authoritative follow set (independent of profile cache)
87+
followPubkeySet.clear();
88+
for (const pk of followPubkeys) {
89+
followPubkeySet.add(pk);
90+
}
91+
8492
if (followPubkeys.length === 0) {
8593
isLoaded = true;
8694
followListReady.set(true);
@@ -198,11 +206,12 @@ export function searchCachedProfiles(query: string, limit: number = 10): CachedP
198206
}
199207

200208
/**
201-
* Get the set of followed pubkeys (without profile data).
209+
* Get the set of followed pubkeys (derived from kind:3 contact list).
202210
* Returns empty set if not loaded yet.
211+
* This is independent of the profile cache — addToCache() does not affect it.
203212
*/
204213
export function getFollowedPubkeys(): Set<string> {
205-
return new Set(profileCache.keys());
214+
return new Set(followPubkeySet);
206215
}
207216

208217
/**
@@ -217,6 +226,7 @@ export function isCacheLoaded(): boolean {
217226
*/
218227
export function resetCache(): void {
219228
profileCache.clear();
229+
followPubkeySet.clear();
220230
isLoaded = false;
221231
isLoading = false;
222232
loadPromise = null;

src/routes/reads/+page.svelte

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
import PencilSimpleLineIcon from 'phosphor-svelte/lib/PencilSimpleLine';
2727
import FolderIcon from 'phosphor-svelte/lib/Folder';
2828
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';
3031
3132
$: isSignedIn = $userPublickey !== '';
3233
$: draftCount = $drafts.length;
@@ -51,9 +52,19 @@
5152
let pullToRefreshEl: PullToRefresh;
5253
5354
// 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+
}
5768
}
5869
5970
// Pagination tracking
@@ -155,11 +166,37 @@
155166
// Always fetch all articles - feed shows all, cover always food-editorial
156167
const cacheFilter = { kinds: [30023], hashtags: getFoodHashtags(), limit: 500 };
157168
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)
159170
let cacheWasUsed = false;
160171
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) {
163200
try {
164201
const cachedEvents = await loadCachedFeedEvents(cacheFilter);
165202
if (cachedEvents.length > 0) {
@@ -190,20 +227,29 @@
190227
}
191228
}
192229
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
195232
if (cacheSufficient && isCacheFresh() && !forceRefresh) {
196233
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');
198235
}
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
201247
setTimeout(() => {
202248
if (browser) {
203249
backgroundRefresh();
204250
}
205251
}, BACKGROUND_REFRESH_DELAY_MS);
206-
252+
207253
return;
208254
}
209255
@@ -534,7 +580,8 @@
534580
'wss://relay.primal.net',
535581
'wss://nos.lol',
536582
'wss://relay.damus.io',
537-
'wss://nostr.wine'
583+
'wss://nostr.wine',
584+
'wss://antiprimal.net'
538585
];
539586
540587
console.log('[Reads] Direct fallback: querying', articleRelays.join(', '));

0 commit comments

Comments
 (0)