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.
This commit is contained in:
Laan Tungir
2026-07-01 09:22:33 -04:00
parent ee0077063d
commit 04405da933
3 changed files with 54 additions and 26 deletions

View File

@@ -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(() => {});

View File

@@ -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"
}

View File

@@ -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.