|
|
|
|
@ -114,6 +114,7 @@
@@ -114,6 +114,7 @@
|
|
|
|
|
|
|
|
|
|
// Don't update the feed automatically - only store new posts for manual update |
|
|
|
|
// This prevents feed jumping and allows user to control when to refresh |
|
|
|
|
// Works the same whether drawer is open or closed - events go to pending arrays |
|
|
|
|
|
|
|
|
|
const updatedRegularPosts = updated.filter((e: NostrEvent) => e.kind === 1); |
|
|
|
|
const updatedReplaceable = updated.filter((e: NostrEvent) => |
|
|
|
|
@ -128,7 +129,7 @@
@@ -128,7 +129,7 @@
|
|
|
|
|
|
|
|
|
|
// NEVER update the feed automatically from onUpdate callback |
|
|
|
|
// This prevents feed jumping - user must click button to see updates |
|
|
|
|
// Only store new posts in pending arrays |
|
|
|
|
// Only store new posts in pending arrays (works for both drawer open and closed) |
|
|
|
|
if (!isReset) { |
|
|
|
|
// Store new posts in pending arrays instead of updating feed automatically |
|
|
|
|
const existingIds = new Set([...posts, ...pendingNewPosts].map(p => p.id)); |
|
|
|
|
@ -142,15 +143,17 @@
@@ -142,15 +143,17 @@
|
|
|
|
|
if (trulyNewPosts.length > 0) { |
|
|
|
|
pendingNewPosts = [...pendingNewPosts, ...trulyNewPosts]; |
|
|
|
|
|
|
|
|
|
// Update counter for new posts |
|
|
|
|
if (lastPostId) { |
|
|
|
|
const newCount = trulyNewPosts.filter(e => e.id !== lastPostId).length; |
|
|
|
|
if (newCount > 0) { |
|
|
|
|
newPostsCount += newCount; |
|
|
|
|
// Update counter for new posts (only if drawer is closed, to avoid confusion) |
|
|
|
|
if (!drawerOpen) { |
|
|
|
|
if (lastPostId) { |
|
|
|
|
const newCount = trulyNewPosts.filter(e => e.id !== lastPostId).length; |
|
|
|
|
if (newCount > 0) { |
|
|
|
|
newPostsCount += newCount; |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
// If no lastPostId, count all new posts |
|
|
|
|
newPostsCount += trulyNewPosts.length; |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
// If no lastPostId, count all new posts |
|
|
|
|
newPostsCount += trulyNewPosts.length; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -212,36 +215,31 @@
@@ -212,36 +215,31 @@
|
|
|
|
|
// Only sort if we have posts to prevent unnecessary re-renders |
|
|
|
|
if (regularPosts.length > 0 || otherFeedEvents.length > 0) { |
|
|
|
|
posts = sortPosts([...regularPosts, ...otherFeedEvents]); |
|
|
|
|
// Invalidate cache |
|
|
|
|
cachedFeedItems = null; |
|
|
|
|
} |
|
|
|
|
replaceableEvents = replaceable.sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
lastPostId = regularPosts.length > 0 ? regularPosts[0].id : null; |
|
|
|
|
// Invalidate cache |
|
|
|
|
cachedFeedItems = null; |
|
|
|
|
} else { |
|
|
|
|
// For infinite scroll (loading more), add new posts directly to feed |
|
|
|
|
// This is a user-initiated action, so update immediately |
|
|
|
|
// Don't re-sort existing posts - just append new ones to prevent jumping |
|
|
|
|
const existingIds = new Set(posts.map(p => p.id)); |
|
|
|
|
const allNewEvents = [...regularPosts, ...otherFeedEvents]; |
|
|
|
|
const newPosts = allNewEvents.filter(e => !existingIds.has(e.id)); |
|
|
|
|
|
|
|
|
|
if (newPosts.length > 0) { |
|
|
|
|
// Sort only the new posts, then append to existing (preserve existing order) |
|
|
|
|
const sortedNewPosts = sortPosts(newPosts); |
|
|
|
|
posts = [...posts, ...sortedNewPosts]; |
|
|
|
|
// Invalidate cache |
|
|
|
|
cachedFeedItems = null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const existingReplaceableIds = new Set(replaceableEvents.map(e => e.id)); |
|
|
|
|
const newReplaceable = replaceable.filter(e => !existingReplaceableIds.has(e.id)); |
|
|
|
|
if (newReplaceable.length > 0) { |
|
|
|
|
// Append new replaceable events without re-sorting existing ones |
|
|
|
|
replaceableEvents = [...replaceableEvents, ...newReplaceable.sort((a, b) => b.created_at - a.created_at)]; |
|
|
|
|
// Invalidate cache |
|
|
|
|
cachedFeedItems = null; |
|
|
|
|
// BUT: Don't update if drawer is open - freeze the feed |
|
|
|
|
if (!drawerOpen) { |
|
|
|
|
// Don't re-sort existing posts - just append new ones to prevent jumping |
|
|
|
|
const existingIds = new Set(posts.map(p => p.id)); |
|
|
|
|
const allNewEvents = [...regularPosts, ...otherFeedEvents]; |
|
|
|
|
const newPosts = allNewEvents.filter(e => !existingIds.has(e.id)); |
|
|
|
|
|
|
|
|
|
if (newPosts.length > 0) { |
|
|
|
|
// Sort only the new posts, then append to existing (preserve existing order) |
|
|
|
|
const sortedNewPosts = sortPosts(newPosts); |
|
|
|
|
posts = [...posts, ...sortedNewPosts]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const existingReplaceableIds = new Set(replaceableEvents.map(e => e.id)); |
|
|
|
|
const newReplaceable = replaceable.filter(e => !existingReplaceableIds.has(e.id)); |
|
|
|
|
if (newReplaceable.length > 0) { |
|
|
|
|
// Append new replaceable events without re-sorting existing ones |
|
|
|
|
replaceableEvents = [...replaceableEvents, ...newReplaceable.sort((a, b) => b.created_at - a.created_at)]; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -286,33 +284,61 @@
@@ -286,33 +284,61 @@
|
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Phase 3: Fetch kind 0 profiles for npubs in feed |
|
|
|
|
// One request per relay with all filters, sent in parallel, update cache in background (10s timeout) |
|
|
|
|
const uniquePubkeys = new Set<string>(); |
|
|
|
|
for (const event of [...posts, ...replaceableEvents]) { |
|
|
|
|
uniquePubkeys.add(event.pubkey); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (uniquePubkeys.size > 0) { |
|
|
|
|
const profileFilter = [{ |
|
|
|
|
kinds: [0], |
|
|
|
|
authors: Array.from(uniquePubkeys).slice(0, 100) // Limit to avoid huge requests |
|
|
|
|
}]; |
|
|
|
|
// Phase 3: Fetch kind 0 profiles, statuses, and activity data for npubs in feed |
|
|
|
|
// Batch fetch all at once to prevent individual requests from ProfileBadge components |
|
|
|
|
// This runs on both initial load and background refresh |
|
|
|
|
const uniquePubkeys = new Set<string>(); |
|
|
|
|
for (const event of [...posts, ...replaceableEvents]) { |
|
|
|
|
uniquePubkeys.add(event.pubkey); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Fetch in background, update cache, view will update automatically via cache |
|
|
|
|
// Don't use onUpdate here to prevent triggering UI updates that cause jumping |
|
|
|
|
nostrClient.fetchEvents( |
|
|
|
|
profileFilter, |
|
|
|
|
relays, |
|
|
|
|
{ |
|
|
|
|
useCache: true, |
|
|
|
|
cacheResults: true, |
|
|
|
|
timeout: 10000 |
|
|
|
|
} |
|
|
|
|
).catch(err => { |
|
|
|
|
console.error('Error fetching profiles:', err); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
if (uniquePubkeys.size > 0) { |
|
|
|
|
const pubkeysArray = Array.from(uniquePubkeys).slice(0, 100); // Limit to avoid huge requests |
|
|
|
|
|
|
|
|
|
// Batch fetch profiles (kind 0) |
|
|
|
|
const profileFilter = [{ |
|
|
|
|
kinds: [0], |
|
|
|
|
authors: pubkeysArray |
|
|
|
|
}]; |
|
|
|
|
|
|
|
|
|
// Batch fetch user statuses (kind 30315) |
|
|
|
|
const statusFilter = [{ |
|
|
|
|
kinds: [30315], |
|
|
|
|
authors: pubkeysArray, |
|
|
|
|
'#d': ['general'], |
|
|
|
|
limit: 1 |
|
|
|
|
} as any]; |
|
|
|
|
|
|
|
|
|
// Batch fetch activity data (recent events from each pubkey) |
|
|
|
|
const activityFilter = [{ |
|
|
|
|
authors: pubkeysArray, |
|
|
|
|
kinds: [0, 1, 7, 11, 1111], |
|
|
|
|
limit: 1 |
|
|
|
|
}]; |
|
|
|
|
|
|
|
|
|
// Fetch all in parallel, update cache, view will update automatically via cache |
|
|
|
|
// Don't use onUpdate here to prevent triggering UI updates that cause jumping |
|
|
|
|
// Run in background to not block initial render |
|
|
|
|
Promise.all([ |
|
|
|
|
nostrClient.fetchEvents(profileFilter, relays, { |
|
|
|
|
useCache: true, |
|
|
|
|
cacheResults: true, |
|
|
|
|
timeout: 10000 |
|
|
|
|
}), |
|
|
|
|
nostrClient.fetchEvents(statusFilter, relays, { |
|
|
|
|
useCache: true, |
|
|
|
|
cacheResults: true, |
|
|
|
|
timeout: 10000 |
|
|
|
|
}), |
|
|
|
|
nostrClient.fetchEvents(activityFilter, relays, { |
|
|
|
|
useCache: true, |
|
|
|
|
cacheResults: true, |
|
|
|
|
timeout: 10000 |
|
|
|
|
}) |
|
|
|
|
]).catch(err => { |
|
|
|
|
console.error('Error fetching profiles/statuses/activity:', err); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Fetch missing parent events (batch, one request per relay) |
|
|
|
|
const allEventIds = new Set(posts.map(e => e.id)); |
|
|
|
|
@ -428,24 +454,21 @@
@@ -428,24 +454,21 @@
|
|
|
|
|
return result; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleShowNewPosts() { |
|
|
|
|
// Don't update feed while drawer is open - freeze in place |
|
|
|
|
if (drawerOpen) return; |
|
|
|
|
|
|
|
|
|
// Merge pending new posts into the feed |
|
|
|
|
if (pendingNewPosts.length > 0) { |
|
|
|
|
// Sort the merged array to maintain proper order |
|
|
|
|
const merged = [...pendingNewPosts, ...posts]; |
|
|
|
|
posts = sortPosts(merged); |
|
|
|
|
pendingNewPosts = []; |
|
|
|
|
// Invalidate cache |
|
|
|
|
cachedFeedItems = null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (pendingNewReplaceable.length > 0) { |
|
|
|
|
replaceableEvents = [...pendingNewReplaceable, ...replaceableEvents].sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
pendingNewReplaceable = []; |
|
|
|
|
// Invalidate cache |
|
|
|
|
cachedFeedItems = null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Scroll to top and reset new posts count |
|
|
|
|
@ -507,10 +530,6 @@
@@ -507,10 +530,6 @@
|
|
|
|
|
return filtered; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Cache the sorted items to prevent re-sorting on every render |
|
|
|
|
let cachedFeedItems: Array<{ id: string; event: NostrEvent; type: 'post' | 'replaceable'; created_at: number }> | null = null; |
|
|
|
|
let cachedFeedItemsKey = ''; |
|
|
|
|
|
|
|
|
|
function openThreadDrawer(event: NostrEvent, e?: MouseEvent) { |
|
|
|
|
// Don't open drawer if clicking on interactive elements (but allow the wrapper itself) |
|
|
|
|
if (e) { |
|
|
|
|
@ -537,17 +556,21 @@
@@ -537,17 +556,21 @@
|
|
|
|
|
function closeThreadDrawer() { |
|
|
|
|
drawerOpen = false; |
|
|
|
|
selectedEvent = null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getAllFeedItems(): Array<{ id: string; event: NostrEvent; type: 'post' | 'replaceable'; created_at: number }> { |
|
|
|
|
// Create a key based on posts/replaceableEvents length and filter state |
|
|
|
|
const currentKey = `${posts.length}_${replaceableEvents.length}_${showOPsOnly}_${showResponsesToMe}`; |
|
|
|
|
|
|
|
|
|
// Return cached result if nothing changed |
|
|
|
|
if (cachedFeedItems && cachedFeedItemsKey === currentKey) { |
|
|
|
|
return cachedFeedItems; |
|
|
|
|
// Events that arrived while drawer was open are already in pendingNewPosts/pendingNewReplaceable |
|
|
|
|
// Update the counter now that drawer is closed so user sees the notification |
|
|
|
|
if (pendingNewPosts.length > 0 && lastPostId) { |
|
|
|
|
const newCount = pendingNewPosts.filter(e => e.id !== lastPostId).length; |
|
|
|
|
if (newCount > 0) { |
|
|
|
|
newPostsCount = newCount; |
|
|
|
|
} |
|
|
|
|
} else if (pendingNewPosts.length > 0) { |
|
|
|
|
newPostsCount = pendingNewPosts.length; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Use $derived to make this reactive and prevent infinite loops |
|
|
|
|
let allFeedItems = $derived.by(() => { |
|
|
|
|
const items: Array<{ id: string; event: NostrEvent; type: 'post' | 'replaceable'; created_at: number }> = []; |
|
|
|
|
|
|
|
|
|
// Add filtered posts |
|
|
|
|
@ -571,15 +594,9 @@
@@ -571,15 +594,9 @@
|
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Sort by created_at, newest first (only when cache is invalid) |
|
|
|
|
const sorted = items.sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
|
|
|
|
|
// Cache the result |
|
|
|
|
cachedFeedItems = sorted; |
|
|
|
|
cachedFeedItemsKey = currentKey; |
|
|
|
|
|
|
|
|
|
return sorted; |
|
|
|
|
} |
|
|
|
|
// Sort by created_at, newest first |
|
|
|
|
return items.sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
}); |
|
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
<div class="Feed-feed"> |
|
|
|
|
@ -623,7 +640,7 @@
@@ -623,7 +640,7 @@
|
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
<div class="posts-list"> |
|
|
|
|
{#each getAllFeedItems() as item (item.id)} |
|
|
|
|
{#each allFeedItems as item (item.id)} |
|
|
|
|
{#if item.type === 'post'} |
|
|
|
|
{@const parentId = item.event.tags.find((t) => t[0] === 'e' && t[3] === 'reply')?.[1]} |
|
|
|
|
{@const parentEvent = parentId ? (posts.find(p => p.id === parentId) || loadedParentQuotedEvents.get(parentId)) : undefined} |
|
|
|
|
@ -673,7 +690,7 @@
@@ -673,7 +690,7 @@
|
|
|
|
|
{#if loadingMore} |
|
|
|
|
<p class="text-center text-fog-text-light dark:text-fog-dark-text-light mt-4">Loading more...</p> |
|
|
|
|
{/if} |
|
|
|
|
{#if !hasMore && getAllFeedItems().length > 0} |
|
|
|
|
{#if !hasMore && allFeedItems.length > 0} |
|
|
|
|
<p class="text-center text-fog-text-light dark:text-fog-dark-text-light mt-4">No more posts</p> |
|
|
|
|
{/if} |
|
|
|
|
{#if (showOPsOnly || showResponsesToMe) && getFilteredPosts().length === 0 && posts.length > 0 && replaceableEvents.length === 0} |
|
|
|
|
|