From 04405da933e6cc003c128bec1cfb25bafa24ff35 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Wed, 1 Jul 2026 09:22:33 -0400 Subject: [PATCH] Fix feed page dropped posts: out-of-order events and race condition prependPostCard() now inserts posts in the correct sorted position within the existing DOM instead of falling back to a full renderFeed() rebuild when a post is older than the first visible post. This handles relays that deliver events out of order without wiping the feed. Also fixed a race condition in fetchFeedWindow: the fire-and-forget ndkFetchEvents callback was calling renderFeed() after bootstrap completed, which wiped prepended live posts and caused concurrent rebuild races. Now it only calls renderFeed() if the feed hasn't been rendered yet (renderedPostIds.size === 0). Same prependPostCard sorted-insert fix applied to post.html. --- www/feed.html | 40 ++++++++++++++++++++++++++++------------ www/js/version.json | 6 +++--- www/post.html | 34 +++++++++++++++++++++++----------- 3 files changed, 54 insertions(+), 26 deletions(-) diff --git a/www/feed.html b/www/feed.html index 45397c8..658cb16 100644 --- a/www/feed.html +++ b/www/feed.html @@ -488,21 +488,33 @@ import { initPostCards } from './js/post-interactions2.mjs'; return false; } - // Only prepend if the post is newer than the first currently visible post. - const firstChild = divFeed.firstElementChild; - const firstPostId = firstChild ? firstChild.getAttribute('data-post-id') : null; - const firstPost = firstPostId ? posts.find((p) => p.id === firstPostId) : null; + const postCreatedAt = post.created_at || 0; - if (!firstPost || (post.created_at || 0) < (firstPost.created_at || 0)) { - // Post is older than the first visible post (or unknown) — full rebuild for safety. - renderFeed(); - return false; + // Find the correct insertion point by scanning visible children's created_at. + // Posts are displayed newest-first, so we insert before the first child + // that is older than (or equal to) the new post. + const postEl = cards.createCard(post, { currentPubkey, autoWire: false, autoplayVideo: autoplayVideosEnabled }); + let inserted = false; + + for (const child of divFeed.children) { + const childId = child.getAttribute('data-post-id'); + if (!childId) continue; + const childPost = posts.find((p) => p.id === childId); + if (!childPost) continue; + const childCreatedAt = childPost.created_at || 0; + if (postCreatedAt >= childCreatedAt) { + divFeed.insertBefore(postEl, child); + inserted = true; + break; + } } - const postEl = cards.createCard(post, { currentPubkey, autoWire: false, autoplayVideo: autoplayVideosEnabled }); - divFeed.insertBefore(postEl, divFeed.firstChild); - renderedPostIds.add(post.id); + if (!inserted) { + // Post is older than all visible posts — append at the bottom. + divFeed.appendChild(postEl); + } + renderedPostIds.add(post.id); cards.wireInteractions([post.id], { currentPubkey }); // Trim DOM to displayCount. @@ -543,7 +555,11 @@ import { initPostCards } from './js/post-interactions2.mjs'; (Array.isArray(fresh) ? fresh : []).forEach((evt1) => { upsertFeedPost(evt1); }); - if (posts.length > 0) { + // Only do a full re-render if the feed hasn't been rendered yet. + // If the feed is already rendered, the posts are in the array and + // will appear when the user clicks "See More" or on next full render. + // This prevents the relay fetch callback from wiping prepended live posts. + if (posts.length > 0 && renderedPostIds.size === 0) { renderFeed(); } }).catch(() => {}); diff --git a/www/js/version.json b/www/js/version.json index 1b6226b..7f8aa53 100644 --- a/www/js/version.json +++ b/www/js/version.json @@ -1,5 +1,5 @@ { - "VERSION": "v0.7.85", - "VERSION_NUMBER": "0.7.85", - "BUILD_DATE": "2026-07-01T11:59:39.440Z" + "VERSION": "v0.7.86", + "VERSION_NUMBER": "0.7.86", + "BUILD_DATE": "2026-07-01T13:22:33.456Z" } diff --git a/www/post.html b/www/post.html index f90e67c..b2939e4 100644 --- a/www/post.html +++ b/www/post.html @@ -667,21 +667,33 @@ import { initPostCards } from './js/post-interactions2.mjs'; return false; } - // Only prepend if the post is newer than the first currently visible post. - const firstChild = divFeed.firstElementChild; - const firstPostId = firstChild ? firstChild.getAttribute('data-post-id') : null; - const firstPost = firstPostId ? posts.find((p) => p.id === firstPostId) : null; + const postCreatedAt = post.created_at || 0; - if (!firstPost || (post.created_at || 0) < (firstPost.created_at || 0)) { - // Post is older than the first visible post (or unknown) — full rebuild for safety. - renderFeed(); - return false; + // Find the correct insertion point by scanning visible children's created_at. + // Posts are displayed newest-first, so we insert before the first child + // that is older than (or equal to) the new post. + const postEl = cards.createCard(post, { currentPubkey, autoWire: false, autoplayVideo: false }); + let inserted = false; + + for (const child of divFeed.children) { + const childId = child.getAttribute('data-post-id'); + if (!childId) continue; + const childPost = posts.find((p) => p.id === childId); + if (!childPost) continue; + const childCreatedAt = childPost.created_at || 0; + if (postCreatedAt >= childCreatedAt) { + divFeed.insertBefore(postEl, child); + inserted = true; + break; + } } - const postEl = cards.createCard(post, { currentPubkey, autoWire: false, autoplayVideo: false }); - divFeed.insertBefore(postEl, divFeed.firstChild); - renderedPostIds.add(post.id); + if (!inserted) { + // Post is older than all visible posts — append at the bottom. + divFeed.appendChild(postEl); + } + renderedPostIds.add(post.id); cards.wireInteractions([post.id], { currentPubkey }); // Trim DOM to displayCount.