|
|
|
|
@ -35,6 +35,15 @@
@@ -35,6 +35,15 @@
|
|
|
|
|
// Batch-loaded reactions: eventId -> reactions[] |
|
|
|
|
let reactionsMap = $state<Map<string, NostrEvent[]>>(new Map()); |
|
|
|
|
|
|
|
|
|
// Batch-loaded parent events: eventId -> parentEvent |
|
|
|
|
let parentEventsMap = $state<Map<string, NostrEvent>>(new Map()); |
|
|
|
|
|
|
|
|
|
// Batch-loaded quoted events: eventId -> quotedEvent |
|
|
|
|
let quotedEventsMap = $state<Map<string, NostrEvent>>(new Map()); |
|
|
|
|
|
|
|
|
|
// Batch-loaded zap counts: eventId -> count |
|
|
|
|
let zapCountsMap = $state<Map<string, number>>(new Map()); |
|
|
|
|
|
|
|
|
|
// Drawer state for viewing parent/quoted events |
|
|
|
|
let drawerOpen = $state(false); |
|
|
|
|
let drawerEvent = $state<NostrEvent | null>(null); |
|
|
|
|
@ -58,11 +67,17 @@
@@ -58,11 +67,17 @@
|
|
|
|
|
let subscriptionId: string | null = $state(null); |
|
|
|
|
let refreshInterval: ReturnType<typeof setInterval> | null = null; |
|
|
|
|
let subscriptionSetup = $state(false); // Track if subscription is already set up |
|
|
|
|
let isMounted = $state(true); // Track if component is still mounted |
|
|
|
|
let activeFetchPromises = $state<Set<Promise<any>>>(new Set()); // Track active fetch promises |
|
|
|
|
|
|
|
|
|
onMount(async () => { |
|
|
|
|
isMounted = true; |
|
|
|
|
await nostrClient.initialize(); |
|
|
|
|
if (!isMounted) return; // Check if unmounted during init |
|
|
|
|
await loadUserLists(); |
|
|
|
|
if (!isMounted) return; |
|
|
|
|
await loadFeed(); |
|
|
|
|
if (!isMounted) return; |
|
|
|
|
// Set up persistent subscription for new events (only once) |
|
|
|
|
if (!subscriptionSetup) { |
|
|
|
|
setupSubscription(); |
|
|
|
|
@ -214,14 +229,38 @@
@@ -214,14 +229,38 @@
|
|
|
|
|
// Cleanup subscription on unmount |
|
|
|
|
$effect(() => { |
|
|
|
|
return () => { |
|
|
|
|
isMounted = false; // Mark as unmounted to halt all operations |
|
|
|
|
|
|
|
|
|
// Cancel all active fetch promises |
|
|
|
|
activeFetchPromises.forEach(promise => { |
|
|
|
|
// Promises can't be cancelled directly, but we'll ignore their results |
|
|
|
|
}); |
|
|
|
|
activeFetchPromises.clear(); |
|
|
|
|
|
|
|
|
|
// Unsubscribe from real-time updates |
|
|
|
|
if (subscriptionId) { |
|
|
|
|
nostrClient.unsubscribe(subscriptionId); |
|
|
|
|
subscriptionId = null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Clear refresh interval |
|
|
|
|
if (refreshInterval) { |
|
|
|
|
clearInterval(refreshInterval); |
|
|
|
|
refreshInterval = null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Clear update timeout |
|
|
|
|
if (updateTimeout) { |
|
|
|
|
clearTimeout(updateTimeout); |
|
|
|
|
updateTimeout = null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Disconnect intersection observer |
|
|
|
|
if (observer) { |
|
|
|
|
observer.disconnect(); |
|
|
|
|
observer = null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
subscriptionSetup = false; |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
@ -241,25 +280,7 @@
@@ -241,25 +280,7 @@
|
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Cleanup on unmount |
|
|
|
|
$effect(() => { |
|
|
|
|
return () => { |
|
|
|
|
if (observer) { |
|
|
|
|
observer.disconnect(); |
|
|
|
|
} |
|
|
|
|
if (updateTimeout) { |
|
|
|
|
clearTimeout(updateTimeout); |
|
|
|
|
} |
|
|
|
|
if (subscriptionId) { |
|
|
|
|
nostrClient.unsubscribe(subscriptionId); |
|
|
|
|
subscriptionId = null; |
|
|
|
|
} |
|
|
|
|
if (refreshInterval) { |
|
|
|
|
clearInterval(refreshInterval); |
|
|
|
|
refreshInterval = null; |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
// This cleanup is now handled in the main cleanup effect above |
|
|
|
|
|
|
|
|
|
// Set up persistent subscription for real-time updates |
|
|
|
|
function setupSubscription() { |
|
|
|
|
@ -314,6 +335,13 @@
@@ -314,6 +335,13 @@
|
|
|
|
|
|
|
|
|
|
// Refresh every 30 seconds |
|
|
|
|
refreshInterval = setInterval(async () => { |
|
|
|
|
if (!isMounted) { |
|
|
|
|
if (refreshInterval) { |
|
|
|
|
clearInterval(refreshInterval); |
|
|
|
|
refreshInterval = null; |
|
|
|
|
} |
|
|
|
|
return; // Don't refresh if component is unmounted |
|
|
|
|
} |
|
|
|
|
try { |
|
|
|
|
// Use single relay if provided, otherwise use normal relay list |
|
|
|
|
const relays = singleRelay ? [singleRelay] : relayManager.getFeedReadRelays(); |
|
|
|
|
@ -386,6 +414,7 @@
@@ -386,6 +414,7 @@
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
async function loadFeed() { |
|
|
|
|
if (!isMounted) return; // Don't load if component is unmounted |
|
|
|
|
loading = true; |
|
|
|
|
relayError = null; // Clear any previous errors |
|
|
|
|
try { |
|
|
|
|
@ -430,6 +459,7 @@
@@ -430,6 +459,7 @@
|
|
|
|
|
useCache: true, // Fill from cache if relay query returns nothing |
|
|
|
|
cacheResults: true, // Cache the results |
|
|
|
|
timeout: 3000, // 3-second timeout |
|
|
|
|
priority: 'high' as const, // High priority for posts/highlights - display first |
|
|
|
|
onUpdate: (updatedEvents: NostrEvent[]) => { |
|
|
|
|
// Update incrementally as events arrive |
|
|
|
|
handleUpdate(updatedEvents); |
|
|
|
|
@ -440,7 +470,12 @@
@@ -440,7 +470,12 @@
|
|
|
|
|
console.log(`[FeedPage] Single-relay mode: fetching from ${singleRelay} with useCache=false, cacheResults=false`); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const events = await nostrClient.fetchEvents(filters, relays, fetchOptions); |
|
|
|
|
const fetchPromise = nostrClient.fetchEvents(filters, relays, fetchOptions); |
|
|
|
|
activeFetchPromises.add(fetchPromise); |
|
|
|
|
const events = await fetchPromise; |
|
|
|
|
activeFetchPromises.delete(fetchPromise); |
|
|
|
|
|
|
|
|
|
if (!isMounted) return; // Don't process if component is unmounted |
|
|
|
|
|
|
|
|
|
console.log(`[FeedPage] Loaded ${events.length} events from ${singleRelay ? `single relay ${singleRelay}` : 'relays'} (relay-first mode)`); |
|
|
|
|
|
|
|
|
|
@ -506,8 +541,33 @@
@@ -506,8 +541,33 @@
|
|
|
|
|
if (sortedPosts.length > 0 || sortedHighlights.length > 0) { |
|
|
|
|
const allTimestamps = [...sortedPosts.map(e => e.created_at), ...sortedHighlights.map(e => e.created_at)]; |
|
|
|
|
oldestTimestamp = Math.min(...allTimestamps); |
|
|
|
|
// Batch load reactions for all posts |
|
|
|
|
await loadReactionsForPosts(sortedPosts); |
|
|
|
|
|
|
|
|
|
// Load secondary data (reactions, profiles, etc.) with low priority after posts are displayed |
|
|
|
|
// Use setTimeout to defer loading so posts/highlights render first |
|
|
|
|
const secondaryDataPromise = new Promise<void>((resolve) => { |
|
|
|
|
setTimeout(() => { |
|
|
|
|
if (!isMounted) { |
|
|
|
|
resolve(); |
|
|
|
|
return; // Don't load if component is unmounted |
|
|
|
|
} |
|
|
|
|
// Load in parallel but with low priority - don't await, let it load in background |
|
|
|
|
const promise = Promise.all([ |
|
|
|
|
loadReactionsForPosts(sortedPosts), |
|
|
|
|
loadParentAndQuotedEvents(sortedPosts), |
|
|
|
|
loadZapCountsForPosts(sortedPosts), |
|
|
|
|
loadProfilesForPosts(sortedPosts) |
|
|
|
|
]).catch(error => { |
|
|
|
|
if (isMounted) { // Only log if still mounted |
|
|
|
|
console.error('[FeedPage] Error loading secondary data:', error); |
|
|
|
|
} |
|
|
|
|
}).finally(() => { |
|
|
|
|
activeFetchPromises.delete(promise); |
|
|
|
|
resolve(); |
|
|
|
|
}); |
|
|
|
|
activeFetchPromises.add(promise); |
|
|
|
|
}, 100); // Small delay to ensure posts render first |
|
|
|
|
}); |
|
|
|
|
activeFetchPromises.add(secondaryDataPromise); |
|
|
|
|
} else { |
|
|
|
|
console.log('[FeedPage] No events found. Relays:', relays); |
|
|
|
|
// In single-relay mode, if we got 0 events, it might mean the relay doesn't have any |
|
|
|
|
@ -525,7 +585,7 @@
@@ -525,7 +585,7 @@
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function loadMore() { |
|
|
|
|
if (loadingMore || !hasMore) return; |
|
|
|
|
if (!isMounted || loadingMore || !hasMore) return; // Don't load if unmounted |
|
|
|
|
|
|
|
|
|
loadingMore = true; |
|
|
|
|
try { |
|
|
|
|
@ -543,7 +603,7 @@
@@ -543,7 +603,7 @@
|
|
|
|
|
|
|
|
|
|
// In single-relay mode: never use cache, only fetch directly from relay |
|
|
|
|
// In normal mode: use relay-first with cache fallback |
|
|
|
|
const events = await nostrClient.fetchEvents( |
|
|
|
|
const fetchPromise = nostrClient.fetchEvents( |
|
|
|
|
filters, |
|
|
|
|
relays, |
|
|
|
|
singleRelay ? { |
|
|
|
|
@ -558,6 +618,11 @@
@@ -558,6 +618,11 @@
|
|
|
|
|
timeout: 3000 // 3-second timeout |
|
|
|
|
} |
|
|
|
|
); |
|
|
|
|
activeFetchPromises.add(fetchPromise); |
|
|
|
|
const events = await fetchPromise; |
|
|
|
|
activeFetchPromises.delete(fetchPromise); |
|
|
|
|
|
|
|
|
|
if (!isMounted) return; // Don't process if component is unmounted |
|
|
|
|
|
|
|
|
|
if (events.length === 0) { |
|
|
|
|
hasMore = false; |
|
|
|
|
@ -585,8 +650,30 @@
@@ -585,8 +650,30 @@
|
|
|
|
|
if (uniqueNewPosts.length > 0) { |
|
|
|
|
const sorted = uniqueNewPosts.sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
allPosts = [...allPosts, ...sorted]; |
|
|
|
|
// Batch load reactions for new posts |
|
|
|
|
await loadReactionsForPosts(sorted); |
|
|
|
|
// Load secondary data with low priority after posts are displayed |
|
|
|
|
const secondaryDataPromise = new Promise<void>((resolve) => { |
|
|
|
|
setTimeout(() => { |
|
|
|
|
if (!isMounted) { |
|
|
|
|
resolve(); |
|
|
|
|
return; // Don't load if component is unmounted |
|
|
|
|
} |
|
|
|
|
const promise = Promise.all([ |
|
|
|
|
loadReactionsForPosts(sorted), |
|
|
|
|
loadParentAndQuotedEvents(sorted), |
|
|
|
|
loadZapCountsForPosts(sorted), |
|
|
|
|
loadProfilesForPosts(sorted) |
|
|
|
|
]).catch(error => { |
|
|
|
|
if (isMounted) { // Only log if still mounted |
|
|
|
|
console.error('[FeedPage] Error loading secondary data for new posts:', error); |
|
|
|
|
} |
|
|
|
|
}).finally(() => { |
|
|
|
|
activeFetchPromises.delete(promise); |
|
|
|
|
resolve(); |
|
|
|
|
}); |
|
|
|
|
activeFetchPromises.add(promise); |
|
|
|
|
}, 100); |
|
|
|
|
}); |
|
|
|
|
activeFetchPromises.add(secondaryDataPromise); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (uniqueNewHighlights.length > 0) { |
|
|
|
|
@ -638,6 +725,7 @@
@@ -638,6 +725,7 @@
|
|
|
|
|
|
|
|
|
|
// Debounced update handler to prevent rapid re-renders and loops |
|
|
|
|
function handleUpdate(updated: NostrEvent[]) { |
|
|
|
|
if (!isMounted) return; // Don't update if component is unmounted |
|
|
|
|
if (!updated || updated.length === 0) return; |
|
|
|
|
|
|
|
|
|
// Deduplicate incoming updates before adding to pending |
|
|
|
|
@ -669,6 +757,7 @@
@@ -669,6 +757,7 @@
|
|
|
|
|
|
|
|
|
|
// Batch updates every 500ms to prevent rapid re-renders |
|
|
|
|
updateTimeout = setTimeout(() => { |
|
|
|
|
if (!isMounted) return; // Don't update if component is unmounted |
|
|
|
|
if (pendingUpdates.length === 0) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
@ -768,7 +857,7 @@
@@ -768,7 +857,7 @@
|
|
|
|
|
|
|
|
|
|
// Batch load reactions for multiple posts at once |
|
|
|
|
async function loadReactionsForPosts(postsToLoad: NostrEvent[]) { |
|
|
|
|
if (postsToLoad.length === 0) return; |
|
|
|
|
if (!isMounted || postsToLoad.length === 0) return; // Don't load if unmounted |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
const reactionRelays = relayManager.getProfileReadRelays(); |
|
|
|
|
@ -779,24 +868,31 @@
@@ -779,24 +868,31 @@
|
|
|
|
|
|
|
|
|
|
// Batch fetch all reactions for all posts in one query |
|
|
|
|
// In single-relay mode: never use cache |
|
|
|
|
const allReactions = await nostrClient.fetchEvents( |
|
|
|
|
const fetchPromise = nostrClient.fetchEvents( |
|
|
|
|
[ |
|
|
|
|
{ kinds: [KIND.REACTION], '#e': eventIds, limit: 1000 }, |
|
|
|
|
{ kinds: [KIND.REACTION], '#E': eventIds, limit: 1000 } |
|
|
|
|
{ kinds: [KIND.REACTION], '#e': eventIds, limit: 100 }, |
|
|
|
|
{ kinds: [KIND.REACTION], '#E': eventIds, limit: 100 } |
|
|
|
|
], |
|
|
|
|
relaysForReactions, |
|
|
|
|
singleRelay ? { |
|
|
|
|
relayFirst: true, |
|
|
|
|
useCache: false, // Never use cache in single-relay mode |
|
|
|
|
cacheResults: false, // Don't cache in single-relay mode |
|
|
|
|
timeout: 3000 |
|
|
|
|
timeout: 3000, |
|
|
|
|
priority: 'low' // Low priority - secondary data |
|
|
|
|
} : { |
|
|
|
|
relayFirst: true, |
|
|
|
|
useCache: true, |
|
|
|
|
cacheResults: true, |
|
|
|
|
timeout: 3000 |
|
|
|
|
timeout: 3000, |
|
|
|
|
priority: 'low' // Low priority - secondary data |
|
|
|
|
} |
|
|
|
|
); |
|
|
|
|
activeFetchPromises.add(fetchPromise); |
|
|
|
|
const allReactions = await fetchPromise; |
|
|
|
|
activeFetchPromises.delete(fetchPromise); |
|
|
|
|
|
|
|
|
|
if (!isMounted) return; // Don't process if component is unmounted |
|
|
|
|
|
|
|
|
|
// Group reactions by event ID |
|
|
|
|
const newReactionsMap = new Map<string, NostrEvent[]>(); |
|
|
|
|
@ -826,6 +922,196 @@
@@ -826,6 +922,196 @@
|
|
|
|
|
console.error('[FeedPage] Error batch loading reactions:', error); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Batch load parent and quoted events for all posts |
|
|
|
|
async function loadParentAndQuotedEvents(postsToLoad: NostrEvent[]) { |
|
|
|
|
if (!isMounted || postsToLoad.length === 0) return; // Don't load if unmounted |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
const relays = singleRelay ? [singleRelay] : relayManager.getFeedReadRelays(); |
|
|
|
|
|
|
|
|
|
// Collect all parent and quoted event IDs |
|
|
|
|
const parentEventIds = new Set<string>(); |
|
|
|
|
const quotedEventIds = new Set<string>(); |
|
|
|
|
|
|
|
|
|
for (const post of postsToLoad) { |
|
|
|
|
// Check for parent event (reply) |
|
|
|
|
const replyTag = post.tags.find((t) => t[0] === 'e' && t[3] === 'reply'); |
|
|
|
|
if (replyTag && replyTag[1]) { |
|
|
|
|
parentEventIds.add(replyTag[1]); |
|
|
|
|
} else { |
|
|
|
|
// Fallback: find any 'e' tag that's not the root |
|
|
|
|
const rootId = post.tags.find((t) => t[0] === 'root')?.[1]; |
|
|
|
|
const eTag = post.tags.find((t) => t[0] === 'e' && t[1] !== rootId && t[1] !== post.id); |
|
|
|
|
if (eTag && eTag[1]) { |
|
|
|
|
parentEventIds.add(eTag[1]); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Check for quoted event |
|
|
|
|
const quotedTag = post.tags.find((t) => t[0] === 'q'); |
|
|
|
|
if (quotedTag && quotedTag[1]) { |
|
|
|
|
quotedEventIds.add(quotedTag[1]); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Batch fetch all parent and quoted events in one query |
|
|
|
|
const allEventIds = [...parentEventIds, ...quotedEventIds]; |
|
|
|
|
if (allEventIds.length === 0) return; |
|
|
|
|
|
|
|
|
|
const fetchPromise = nostrClient.fetchEvents( |
|
|
|
|
[{ kinds: [KIND.SHORT_TEXT_NOTE], ids: allEventIds }], |
|
|
|
|
relays, |
|
|
|
|
singleRelay ? { |
|
|
|
|
relayFirst: true, |
|
|
|
|
useCache: false, |
|
|
|
|
cacheResults: false, |
|
|
|
|
timeout: 3000, |
|
|
|
|
priority: 'low' // Low priority - secondary data |
|
|
|
|
} : { |
|
|
|
|
relayFirst: true, |
|
|
|
|
useCache: true, |
|
|
|
|
cacheResults: true, |
|
|
|
|
timeout: 3000, |
|
|
|
|
priority: 'low' // Low priority - secondary data |
|
|
|
|
} |
|
|
|
|
); |
|
|
|
|
activeFetchPromises.add(fetchPromise); |
|
|
|
|
const events = await fetchPromise; |
|
|
|
|
activeFetchPromises.delete(fetchPromise); |
|
|
|
|
|
|
|
|
|
if (!isMounted) return; // Don't process if component is unmounted |
|
|
|
|
|
|
|
|
|
// Map events by ID |
|
|
|
|
const eventsById = new Map<string, NostrEvent>(); |
|
|
|
|
for (const event of events) { |
|
|
|
|
eventsById.set(event.id, event); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Store parent events |
|
|
|
|
for (const post of postsToLoad) { |
|
|
|
|
const replyTag = post.tags.find((t) => t[0] === 'e' && t[3] === 'reply'); |
|
|
|
|
let parentId: string | undefined; |
|
|
|
|
if (replyTag && replyTag[1]) { |
|
|
|
|
parentId = replyTag[1]; |
|
|
|
|
} else { |
|
|
|
|
const rootId = post.tags.find((t) => t[0] === 'root')?.[1]; |
|
|
|
|
const eTag = post.tags.find((t) => t[0] === 'e' && t[1] !== rootId && t[1] !== post.id); |
|
|
|
|
parentId = eTag?.[1]; |
|
|
|
|
} |
|
|
|
|
if (parentId && eventsById.has(parentId)) { |
|
|
|
|
parentEventsMap.set(post.id, eventsById.get(parentId)!); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Store quoted events |
|
|
|
|
for (const post of postsToLoad) { |
|
|
|
|
const quotedTag = post.tags.find((t) => t[0] === 'q'); |
|
|
|
|
if (quotedTag && quotedTag[1] && eventsById.has(quotedTag[1])) { |
|
|
|
|
quotedEventsMap.set(post.id, eventsById.get(quotedTag[1])!); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('[FeedPage] Error batch loading parent/quoted events:', error); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Batch load zap counts for all posts |
|
|
|
|
async function loadZapCountsForPosts(postsToLoad: NostrEvent[]) { |
|
|
|
|
if (!isMounted || postsToLoad.length === 0) return; // Don't load if unmounted |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
const config = nostrClient.getConfig(); |
|
|
|
|
const threshold = config.zapThreshold; |
|
|
|
|
const zapRelays = singleRelay ? [singleRelay] : relayManager.getZapReceiptReadRelays(); |
|
|
|
|
const eventIds = postsToLoad.map(p => p.id); |
|
|
|
|
|
|
|
|
|
// Batch fetch all zap receipts in one query |
|
|
|
|
const fetchPromise = nostrClient.fetchEvents( |
|
|
|
|
[{ kinds: [KIND.ZAP_RECEIPT], '#e': eventIds, limit: 100 }], |
|
|
|
|
zapRelays, |
|
|
|
|
singleRelay ? { |
|
|
|
|
relayFirst: true, |
|
|
|
|
useCache: false, |
|
|
|
|
cacheResults: false, |
|
|
|
|
timeout: 3000, |
|
|
|
|
priority: 'low' // Low priority - secondary data |
|
|
|
|
} : { |
|
|
|
|
relayFirst: true, |
|
|
|
|
useCache: true, |
|
|
|
|
cacheResults: true, |
|
|
|
|
timeout: 3000, |
|
|
|
|
priority: 'low' // Low priority - secondary data |
|
|
|
|
} |
|
|
|
|
); |
|
|
|
|
activeFetchPromises.add(fetchPromise); |
|
|
|
|
const receipts = await fetchPromise; |
|
|
|
|
activeFetchPromises.delete(fetchPromise); |
|
|
|
|
|
|
|
|
|
if (!isMounted) return; // Don't process if component is unmounted |
|
|
|
|
|
|
|
|
|
// Count zaps per event (filtered by threshold) |
|
|
|
|
const countsByEventId = new Map<string, number>(); |
|
|
|
|
for (const receipt of receipts) { |
|
|
|
|
const eTags = receipt.tags.filter(t => t[0] === 'e' && t[1]); |
|
|
|
|
for (const tag of eTags) { |
|
|
|
|
const eventId = tag[1]; |
|
|
|
|
if (eventIds.includes(eventId)) { |
|
|
|
|
const amountTag = receipt.tags.find((t) => t[0] === 'amount'); |
|
|
|
|
if (amountTag && amountTag[1]) { |
|
|
|
|
const amount = parseInt(amountTag[1], 10); |
|
|
|
|
if (!isNaN(amount) && amount >= threshold) { |
|
|
|
|
countsByEventId.set(eventId, (countsByEventId.get(eventId) || 0) + 1); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Store zap counts |
|
|
|
|
for (const [eventId, count] of countsByEventId.entries()) { |
|
|
|
|
zapCountsMap.set(eventId, count); |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('[FeedPage] Error batch loading zap counts:', error); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Batch load profiles for all posts |
|
|
|
|
async function loadProfilesForPosts(postsToLoad: NostrEvent[]) { |
|
|
|
|
if (!isMounted || postsToLoad.length === 0) return; // Don't load if unmounted |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
// Collect all unique pubkeys |
|
|
|
|
const pubkeys = new Set<string>(); |
|
|
|
|
for (const post of postsToLoad) { |
|
|
|
|
pubkeys.add(post.pubkey); |
|
|
|
|
// Also collect pubkeys from parent/quoted events if we have them |
|
|
|
|
const parentEvent = parentEventsMap.get(post.id); |
|
|
|
|
if (parentEvent) { |
|
|
|
|
pubkeys.add(parentEvent.pubkey); |
|
|
|
|
} |
|
|
|
|
const quotedEvent = quotedEventsMap.get(post.id); |
|
|
|
|
if (quotedEvent) { |
|
|
|
|
pubkeys.add(quotedEvent.pubkey); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Batch fetch all profiles using fetchProfiles (which handles caching) |
|
|
|
|
const { fetchProfiles } = await import('../../services/user-data.js'); |
|
|
|
|
const relayList = singleRelay ? [singleRelay] : undefined; |
|
|
|
|
const fetchPromise = fetchProfiles(Array.from(pubkeys), relayList); |
|
|
|
|
activeFetchPromises.add(fetchPromise); |
|
|
|
|
await fetchPromise; |
|
|
|
|
activeFetchPromises.delete(fetchPromise); |
|
|
|
|
|
|
|
|
|
if (!isMounted) return; // Don't process if component is unmounted |
|
|
|
|
|
|
|
|
|
// Profiles are now cached and ProfileBadge will use the cache |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('[FeedPage] Error batch loading profiles:', error); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
<div class="feed-page"> |
|
|
|
|
@ -881,7 +1167,14 @@
@@ -881,7 +1167,14 @@
|
|
|
|
|
{#if event.kind === KIND.HIGHLIGHTED_ARTICLE} |
|
|
|
|
<HighlightCard highlight={event} onOpenEvent={openDrawer} /> |
|
|
|
|
{:else} |
|
|
|
|
<FeedPost post={event} onOpenEvent={openDrawer} reactions={reactionsMap.get(event.id)} /> |
|
|
|
|
<FeedPost |
|
|
|
|
post={event} |
|
|
|
|
onOpenEvent={openDrawer} |
|
|
|
|
reactions={reactionsMap.get(event.id)} |
|
|
|
|
parentEvent={parentEventsMap.get(event.id)} |
|
|
|
|
quotedEvent={quotedEventsMap.get(event.id)} |
|
|
|
|
zapCount={zapCountsMap.get(event.id) || 0} |
|
|
|
|
/> |
|
|
|
|
{/if} |
|
|
|
|
{/each} |
|
|
|
|
</div> |
|
|
|
|
|