From ef375e3578dd14ed9c7e8a75adcb85e8d331a29d Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Thu, 12 Mar 2026 16:40:48 +0800 Subject: [PATCH] fix(mobile): hide empty mobile reply backlink wrapper (#1060) getRenderableMobileBacklinks() now filters mobile backlinks down to replies that can actually render, so ReplyBacklinks only mounts mobileReplyBacklinks when there is visible content. This adds a regression test for the gap where a newly published reply exists before it receives its post number. --- src/components/post-mobile/post-mobile.tsx | 55 ++++++------------- .../__tests__/reply-backlink-utils.test.ts | 44 +++++++++++++++ src/lib/utils/reply-backlink-utils.ts | 44 +++++++++++++++ 3 files changed, 105 insertions(+), 38 deletions(-) create mode 100644 src/lib/utils/__tests__/reply-backlink-utils.test.ts create mode 100644 src/lib/utils/reply-backlink-utils.ts diff --git a/src/components/post-mobile/post-mobile.tsx b/src/components/post-mobile/post-mobile.tsx index 2bfcb417..d2ceef0f 100644 --- a/src/components/post-mobile/post-mobile.tsx +++ b/src/components/post-mobile/post-mobile.tsx @@ -43,6 +43,7 @@ import useProgressiveRender from '../../hooks/use-progressive-render'; import useFreshReplies from '../../hooks/use-fresh-replies'; import { BOARD_REPLIES_PREVIEW_FETCH_SIZE, BOARD_REPLIES_PREVIEW_VISIBLE_COUNT, REPLIES_PER_PAGE } from '../../lib/constants'; import { filterRepliesForDisplay, getPreviewDisplayReplies } from '../../lib/utils/replies-preview-utils'; +import { getRenderableMobileBacklinks } from '../../lib/utils/reply-backlink-utils'; import { getThreadTopNavigationState, scrollThreadContainerToTop } from '../../lib/utils/thread-scroll-utils'; const { addChallenge } = useChallengesStore.getState(); @@ -476,46 +477,24 @@ interface ReplyBacklinksProps extends PostProps { const ReplyBacklinks = ({ post, quotedByMap, directRepliesByParentCid }: ReplyBacklinksProps) => { const { cid, parentCid } = post || {}; - const directReplies = directRepliesByParentCid?.get(cid) || []; + const { opBacklinks, directReplyBacklinks, quotedReplyBacklinks } = getRenderableMobileBacklinks({ + cid, + parentCid, + quotedByMap, + directRepliesByParentCid, + }); - const opBacklinks = - cid && - !parentCid && - quotedByMap - ?.get(cid) - ?.map( - (reply: Comment) => - reply?.cid && - reply?.number && - !(reply?.deleted || reply?.removed) && , - ) - .filter(Boolean); - - const replyBacklinks = cid && parentCid && (directReplies.length > 0 || quotedByMap?.get(cid)?.length) && ( - <> - {directReplies.map( - (reply: Comment) => - reply?.parentCid === cid && - reply?.cid && - reply?.number && - !(reply?.deleted || reply?.removed) && , - )} - {quotedByMap - ?.get(cid) - ?.map( - (reply: Comment) => - reply?.parentCid !== cid && - reply?.cid && - reply?.number && - !(reply?.deleted || reply?.removed) && , - )} - - ); - - return opBacklinks?.length > 0 || replyBacklinks ? ( + return opBacklinks.length > 0 || directReplyBacklinks.length > 0 || quotedReplyBacklinks.length > 0 ? (
- {opBacklinks} - {replyBacklinks} + {opBacklinks.map((reply: Comment) => ( + + ))} + {directReplyBacklinks.map((reply: Comment) => ( + + ))} + {quotedReplyBacklinks.map((reply: Comment) => ( + + ))}
) : null; }; diff --git a/src/lib/utils/__tests__/reply-backlink-utils.test.ts b/src/lib/utils/__tests__/reply-backlink-utils.test.ts new file mode 100644 index 00000000..0c84b6a3 --- /dev/null +++ b/src/lib/utils/__tests__/reply-backlink-utils.test.ts @@ -0,0 +1,44 @@ +import type { Comment } from '@bitsocialnet/bitsocial-react-hooks'; +import { describe, expect, it } from 'vitest'; +import { getRenderableMobileBacklinks } from '../reply-backlink-utils'; + +const createReply = (overrides: Partial = {}) => + ({ + cid: 'reply-cid', + parentCid: 'target-cid', + subplebbitAddress: 'music.eth', + ...overrides, + }) as Comment; + +describe('getRenderableMobileBacklinks', () => { + it('does not report mobile reply backlinks until a renderable backlink exists', () => { + const unpublishedReply = createReply({ + number: undefined, + }); + + const beforePublish = getRenderableMobileBacklinks({ + cid: 'target-cid', + parentCid: 'op-cid', + quotedByMap: new Map([['target-cid', [unpublishedReply]]]), + directRepliesByParentCid: new Map([['target-cid', [unpublishedReply]]]), + }); + + expect(beforePublish.directReplyBacklinks).toHaveLength(0); + expect(beforePublish.quotedReplyBacklinks).toHaveLength(0); + + const publishedReply = createReply({ + cid: 'published-reply-cid', + number: 42, + }); + + const afterPublish = getRenderableMobileBacklinks({ + cid: 'target-cid', + parentCid: 'op-cid', + quotedByMap: new Map([['target-cid', [publishedReply]]]), + directRepliesByParentCid: new Map([['target-cid', [publishedReply]]]), + }); + + expect(afterPublish.directReplyBacklinks).toEqual([publishedReply]); + expect(afterPublish.quotedReplyBacklinks).toHaveLength(0); + }); +}); diff --git a/src/lib/utils/reply-backlink-utils.ts b/src/lib/utils/reply-backlink-utils.ts new file mode 100644 index 00000000..b02979a6 --- /dev/null +++ b/src/lib/utils/reply-backlink-utils.ts @@ -0,0 +1,44 @@ +import type { Comment } from '@bitsocialnet/bitsocial-react-hooks'; + +interface GetRenderableMobileBacklinksArgs { + cid?: string; + parentCid?: string; + quotedByMap?: Map; + directRepliesByParentCid?: Map; +} + +interface RenderableMobileBacklinks { + opBacklinks: Comment[]; + directReplyBacklinks: Comment[]; + quotedReplyBacklinks: Comment[]; +} + +const isRenderableBacklinkReply = (reply?: Comment) => Boolean(reply?.cid && typeof reply.number === 'number' && !(reply.deleted || reply.removed)); + +export const getRenderableMobileBacklinks = ({ cid, parentCid, quotedByMap, directRepliesByParentCid }: GetRenderableMobileBacklinksArgs): RenderableMobileBacklinks => { + if (!cid) { + return { + opBacklinks: [], + directReplyBacklinks: [], + quotedReplyBacklinks: [], + }; + } + + const quotedReplies = quotedByMap?.get(cid) ?? []; + + if (!parentCid) { + return { + opBacklinks: quotedReplies.filter(isRenderableBacklinkReply), + directReplyBacklinks: [], + quotedReplyBacklinks: [], + }; + } + + const directReplies = directRepliesByParentCid?.get(cid) ?? []; + + return { + opBacklinks: [], + directReplyBacklinks: directReplies.filter((reply) => reply?.parentCid === cid && isRenderableBacklinkReply(reply)), + quotedReplyBacklinks: quotedReplies.filter((reply) => reply?.parentCid !== cid && isRenderableBacklinkReply(reply)), + }; +};