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.
This commit is contained in:
Tommaso Casaburi
2026-03-12 16:40:48 +08:00
committed by GitHub
parent 48fccede8d
commit ef375e3578
3 changed files with 105 additions and 38 deletions
+17 -38
View File
@@ -43,6 +43,7 @@ import useProgressiveRender from '../../hooks/use-progressive-render';
import useFreshReplies from '../../hooks/use-fresh-replies'; 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 { 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 { 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'; import { getThreadTopNavigationState, scrollThreadContainerToTop } from '../../lib/utils/thread-scroll-utils';
const { addChallenge } = useChallengesStore.getState(); const { addChallenge } = useChallengesStore.getState();
@@ -476,46 +477,24 @@ interface ReplyBacklinksProps extends PostProps {
const ReplyBacklinks = ({ post, quotedByMap, directRepliesByParentCid }: ReplyBacklinksProps) => { const ReplyBacklinks = ({ post, quotedByMap, directRepliesByParentCid }: ReplyBacklinksProps) => {
const { cid, parentCid } = post || {}; const { cid, parentCid } = post || {};
const directReplies = directRepliesByParentCid?.get(cid) || []; const { opBacklinks, directReplyBacklinks, quotedReplyBacklinks } = getRenderableMobileBacklinks({
cid,
parentCid,
quotedByMap,
directRepliesByParentCid,
});
const opBacklinks = return opBacklinks.length > 0 || directReplyBacklinks.length > 0 || quotedReplyBacklinks.length > 0 ? (
cid &&
!parentCid &&
quotedByMap
?.get(cid)
?.map(
(reply: Comment) =>
reply?.cid &&
reply?.number &&
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`op-bl-${reply.cid}`} isBacklinkReply={true} backlinkReply={reply} />,
)
.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) && <ReplyQuotePreview key={reply.cid} isBacklinkReply={true} backlinkReply={reply} />,
)}
{quotedByMap
?.get(cid)
?.map(
(reply: Comment) =>
reply?.parentCid !== cid &&
reply?.cid &&
reply?.number &&
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${reply.cid}`} isBacklinkReply={true} backlinkReply={reply} />,
)}
</>
);
return opBacklinks?.length > 0 || replyBacklinks ? (
<div className={styles.mobileReplyBacklinks}> <div className={styles.mobileReplyBacklinks}>
{opBacklinks} {opBacklinks.map((reply: Comment) => (
{replyBacklinks} <ReplyQuotePreview key={`op-bl-${reply.cid}`} isBacklinkReply={true} backlinkReply={reply} />
))}
{directReplyBacklinks.map((reply: Comment) => (
<ReplyQuotePreview key={reply.cid} isBacklinkReply={true} backlinkReply={reply} />
))}
{quotedReplyBacklinks.map((reply: Comment) => (
<ReplyQuotePreview key={`qb-${reply.cid}`} isBacklinkReply={true} backlinkReply={reply} />
))}
</div> </div>
) : null; ) : null;
}; };
@@ -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<Comment> = {}) =>
({
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);
});
});
+44
View File
@@ -0,0 +1,44 @@
import type { Comment } from '@bitsocialnet/bitsocial-react-hooks';
interface GetRenderableMobileBacklinksArgs {
cid?: string;
parentCid?: string;
quotedByMap?: Map<string, Comment[]>;
directRepliesByParentCid?: Map<string, Comment[]>;
}
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)),
};
};