From 2ff7b8bc8803100fdaf7d2d644f54cdc955eaa80 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Thu, 25 Jun 2026 23:49:21 +0700 Subject: [PATCH] fix(board): preserve feed order during hydration --- src/views/board/__tests__/board.test.tsx | 23 +++++++------ src/views/board/board.tsx | 44 ++++++++---------------- 2 files changed, 27 insertions(+), 40 deletions(-) diff --git a/src/views/board/__tests__/board.test.tsx b/src/views/board/__tests__/board.test.tsx index e9462d50..b7438e65 100644 --- a/src/views/board/__tests__/board.test.tsx +++ b/src/views/board/__tests__/board.test.tsx @@ -867,7 +867,7 @@ describe('Board', () => { ); }); - it('keeps propagated board posts in active order when hook updates are appended', async () => { + it('preserves hook order when multiboard updates are appended during hydration', async () => { const currentTimestamp = Math.floor(Date.now() / 1000); testState.pageSizes = { guiPostsPerPage: 6, @@ -876,19 +876,22 @@ describe('Board', () => { paginationFeedPostsPerPage: 6, }; testState.feed = [ - { cid: 'pinned-post', pinned: true, communityAddress: 'music-posting.eth', timestamp: currentTimestamp - 300 }, - { cid: 'older-post', communityAddress: 'music-posting.eth', lastReplyTimestamp: currentTimestamp - 200, timestamp: currentTimestamp - 200 }, - { cid: 'newly-propagated-post', communityAddress: 'music-posting.eth', postCid: 'newly-propagated-post', timestamp: currentTimestamp }, - { cid: 'middle-post', communityAddress: 'music-posting.eth', lastReplyTimestamp: currentTimestamp - 100, timestamp: currentTimestamp - 100 }, + { cid: 'first-visible-post', communityAddress: 'music-posting.eth', lastReplyTimestamp: currentTimestamp - 200, timestamp: currentTimestamp - 200 }, + { cid: 'second-visible-post', communityAddress: 'music-posting.eth', lastReplyTimestamp: currentTimestamp - 100, timestamp: currentTimestamp - 100 }, + { cid: 'later-appended-newer-post', communityAddress: 'sports-posting.bso', postCid: 'later-appended-newer-post', timestamp: currentTimestamp }, ]; + testState.filteredDirectoryAddresses = ['music-posting.eth', 'sports-posting.bso']; - await renderBoard({ initialEntry: '/mu', routePath: '/:boardIdentifier/*' }); + await renderBoard({ + boardProps: { viewType: 'all' }, + initialEntry: '/all', + routePath: '/all/*', + }); expect(Array.from(container.querySelectorAll('[data-testid="post"]')).map((element) => element.textContent)).toEqual([ - 'pinned-post', - 'newly-propagated-post', - 'middle-post', - 'older-post', + 'first-visible-post', + 'second-visible-post', + 'later-appended-newer-post', ]); }); diff --git a/src/views/board/board.tsx b/src/views/board/board.tsx index d6c50f77..801b35fe 100644 --- a/src/views/board/board.tsx +++ b/src/views/board/board.tsx @@ -53,32 +53,17 @@ const EMPTY_COMMUNITIES_PAGES = {}; /** Board feed always uses 'active' sort; catalog dropdown does not affect board ordering. */ const BOARD_SORT_TYPE = 'active' as const; -const toFiniteNumber = (value: unknown) => (typeof value === 'number' && Number.isFinite(value) ? value : 0); - -const compareBoardActivePosts = (firstPost: Comment, secondPost: Comment) => { - const activeDifference = - toFiniteNumber(secondPost?.lastReplyTimestamp ?? secondPost?.timestamp) - toFiniteNumber(firstPost?.lastReplyTimestamp ?? firstPost?.timestamp); - if (activeDifference !== 0) return activeDifference; - - const upvoteDifference = toFiniteNumber(secondPost?.upvoteCount) - toFiniteNumber(firstPost?.upvoteCount); - if (upvoteDifference !== 0) return upvoteDifference; - - return toFiniteNumber(secondPost?.timestamp) - toFiniteNumber(firstPost?.timestamp); -}; - -const sortBoardActiveFeed = (posts: Comment[]) => { - const pinnedPosts: Comment[] = []; - const regularPosts: Comment[] = []; - - for (const post of posts) { - if (post?.pinned) { - pinnedPosts.push(post); - } else { - regularPosts.push(post); - } +const mergeVisibleLocalAccountComments = (feed: Comment[], visibleLocalAccountComments: Comment[]) => { + if (visibleLocalAccountComments.length === 0) { + return feed; } - return [...pinnedPosts, ...regularPosts.toSorted(compareBoardActivePosts)]; + let firstRegularPostIndex = 0; + while (firstRegularPostIndex < feed.length && feed[firstRegularPostIndex]?.pinned) { + firstRegularPostIndex += 1; + } + + return [...feed.slice(0, firstRegularPostIndex), ...visibleLocalAccountComments, ...feed.slice(firstRegularPostIndex)]; }; type RefreshHoldState = { @@ -487,21 +472,20 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t return [nonokoPendingAccountComment, ...filteredComments.filter((comment) => comment.cid !== nonokoPendingAccountComment.cid)]; }, [nonokoPendingAccountComment, filteredComments]); - const sortedFeed = useMemo(() => sortBoardActiveFeed(feed), [feed]); - const canShowRecentLocalAccountComments = !isSingleCommunityBoard || sortedFeed.length > 0 || isRawBoardThreadStateFullyLoaded; + const canShowRecentLocalAccountComments = !isSingleCommunityBoard || feed.length > 0 || isRawBoardThreadStateFullyLoaded; const feedWithLocalAccountComments = useMemo(() => { if (isBoardRefreshPending) { - return sortedFeed; + return feed; } const visibleLocalAccountComments = canShowRecentLocalAccountComments ? localAccountComments : nonokoPendingAccountComment ? localAccountComments.slice(0, 1) : []; if (visibleLocalAccountComments.length === 0) { - return sortedFeed; + return feed; } - return sortBoardActiveFeed([...feed, ...visibleLocalAccountComments]); - }, [canShowRecentLocalAccountComments, feed, isBoardRefreshPending, localAccountComments, nonokoPendingAccountComment, sortedFeed]); + return mergeVisibleLocalAccountComments(feed, visibleLocalAccountComments); + }, [canShowRecentLocalAccountComments, feed, isBoardRefreshPending, localAccountComments, nonokoPendingAccountComment]); const combinedFeed = feedWithLocalAccountComments; const cappedFeed = useMemo(