From c0f1a77958a344b2d5b83a1fd8ebb3a271cf8b39 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Tue, 9 Jun 2026 19:08:02 +0700 Subject: [PATCH] fix(post ids): fall back to comment cid for missing ids (#1167) * fix(post ids): fall back to comment cid for missing ids * fix(post ids): count cid-resolved account replies --- .../post-community-address-compat.test.tsx | 85 ++++++++++++++++--- src/components/post-desktop/post-desktop.tsx | 37 ++++---- src/components/post-mobile/post-mobile.tsx | 40 ++++----- .../__tests__/use-fresh-replies.test.tsx | 51 ++++++++--- src/hooks/use-fresh-replies.ts | 44 ++++++++-- .../__tests__/author-post-counts.test.ts | 3 +- src/lib/utils/author-post-counts.ts | 9 +- src/lib/utils/comment-user-id-utils.ts | 7 ++ 8 files changed, 208 insertions(+), 68 deletions(-) create mode 100644 src/lib/utils/comment-user-id-utils.ts diff --git a/src/components/__tests__/post-community-address-compat.test.tsx b/src/components/__tests__/post-community-address-compat.test.tsx index 6f3cefcf..cb601fa7 100644 --- a/src/components/__tests__/post-community-address-compat.test.tsx +++ b/src/components/__tests__/post-community-address-compat.test.tsx @@ -47,8 +47,10 @@ type TestComment = { const testState = vi.hoisted(() => ({ addChallengeMock: vi.fn(), + accountCommentsByCid: {} as Record, hasMoreReplies: false, openReplyModalMock: vi.fn(), + pseudonymityMode: 'none', replyComments: [] as Array, setResetFunctionMock: vi.fn(), virtuosoProps: [] as Array<{ defaultItemHeight?: number; heightEstimates?: number[]; itemSize?: unknown }>, @@ -82,8 +84,8 @@ vi.mock('react-i18next', () => ({ })); vi.mock('@bitsocial/bitsocial-react-hooks', () => ({ - useAccount: () => ({ author: { address: '0xviewer' } }), - useAccountComment: () => undefined, + useAccount: () => ({ id: 'viewer-account', author: { address: '0xviewer' } }), + useAccountComment: (options?: { commentCid?: string }) => (options?.commentCid ? testState.accountCommentsByCid[options.commentCid] : undefined), useEditedComment: () => ({ editedComment: undefined }), usePublishCommentModeration: () => ({ error: undefined, @@ -137,7 +139,12 @@ vi.mock('react-virtuoso', () => ({ })); vi.mock('../../lib/get-short-address', () => ({ - default: (value?: string) => (value ? value.slice(0, 4) : ''), + default: (value?: string) => { + if (!value) return ''; + if (value.includes('.')) return value; + if (value.length < 20) return ''; + return value.slice(8, 20); + }, })); vi.mock('../../views/post/post.module.css', () => ({ @@ -247,14 +254,14 @@ vi.mock('../../hooks/use-current-time', () => ({ })); vi.mock('../../hooks/use-board-pseudonymity-mode', () => ({ - useBoardPseudonymityMode: () => 'none', + useBoardPseudonymityMode: () => testState.pseudonymityMode, })); -vi.mock('../comment-content', () => ({ +vi.mock('../comment-content/comment-content', () => ({ default: ({ comment }: { comment?: TestComment }) => createElement('div', { 'data-testid': 'comment-content' }, comment?.cid ?? 'missing'), })); -vi.mock('../comment-media', () => ({ +vi.mock('../comment-media/comment-media', () => ({ default: () => createElement('div', { 'data-testid': 'comment-media' }, 'media'), })); @@ -266,24 +273,24 @@ vi.mock('../failed-publish-notice', () => ({ default: () => createElement('div', { 'data-testid': 'failed-publish-notice' }, 'failed-publish-notice'), })); -vi.mock('../embed', () => ({ +vi.mock('../embed/embed-utils', () => ({ canEmbed: () => false, })); -vi.mock('../loading-ellipsis', () => ({ +vi.mock('../loading-ellipsis/loading-ellipsis', () => ({ default: ({ string }: { string: string }) => createElement('div', { 'data-testid': 'loading-ellipsis' }, string), })); -vi.mock('../post-desktop/post-menu-desktop', () => ({ +vi.mock('../post-desktop/post-menu-desktop/post-menu-desktop', () => ({ default: ({ postMenu }: { postMenu: { communityAddress?: string } }) => createElement('div', { 'data-testid': 'post-menu-desktop' }, postMenu.communityAddress ?? 'missing'), })); -vi.mock('../reply-quote-preview', () => ({ +vi.mock('../reply-quote-preview/reply-quote-preview', () => ({ default: ({ backlinkReply }: { backlinkReply?: TestComment }) => createElement('div', { 'data-testid': 'reply-quote-preview' }, backlinkReply?.cid ?? 'missing'), })); -vi.mock('../tooltip', () => ({ +vi.mock('../tooltip/tooltip', () => ({ default: ({ children }: { children?: React.ReactNode }) => createElement(React.Fragment, {}, children), })); @@ -371,7 +378,7 @@ vi.mock('../../hooks/use-delete-failed-post', () => ({ }), })); -vi.mock('../post-mobile/post-menu-mobile', () => ({ +vi.mock('../post-mobile/post-menu-mobile/post-menu-mobile', () => ({ default: ({ postMenu }: { postMenu: { communityAddress?: string } }) => createElement('div', { 'data-testid': 'post-menu-mobile' }, postMenu.communityAddress ?? 'missing'), })); @@ -449,7 +456,9 @@ const makeLegacyThreadWithoutReplies = (): TestComment => ({ describe('post community address compatibility', () => { beforeEach(() => { vi.clearAllMocks(); + testState.accountCommentsByCid = {}; testState.hasMoreReplies = false; + testState.pseudonymityMode = 'none'; testState.replyComments = []; testState.virtuosoProps = []; @@ -505,6 +514,58 @@ describe('post community address compatibility', () => { expect(container.querySelector('.capcodeAdminIcon')).toBeTruthy(); }); + it('falls back to author shortAddress when the full author address cannot be shortened', async () => { + testState.pseudonymityMode = 'per-post'; + const post = { + ...makeLegacyThreadWithoutReplies(), + author: { address: 'short-address', shortAddress: 'B2mAZojE' }, + }; + + await renderWithRoute(createElement(PostDesktop, { post } as any), '/mu/thread/post-1'); + expect(container.textContent).toContain('ID: B2mAZojE'); + + await renderWithRoute(createElement(PostMobile, { post } as any), '/mu/thread/post-1'); + expect(container.textContent).toContain('ID: B2mAZojE'); + }); + + it('uses safe account reply data by cid when a reply has no index', async () => { + testState.pseudonymityMode = 'per-post'; + const post = makeLegacyThread(); + const reply = post.replies?.pages?.new?.comments?.[0]; + if (!reply?.cid) { + throw new Error('missing fixture reply'); + } + reply.index = undefined; + reply.author = {}; + testState.accountCommentsByCid[reply.cid] = { + ...reply, + author: { shortAddress: 'ReplyKid9' }, + }; + + await renderWithRoute(createElement(PostDesktop, { post, showAllReplies: true }), '/mu/thread/post-1'); + expect(container.textContent).toContain('ID: ReplyKid'); + + await renderWithRoute(createElement(PostMobile, { post, showAllReplies: true }), '/mu/thread/post-1'); + expect(container.textContent).toContain('ID: ReplyKid'); + }); + + it('falls back to the reply cid when published reply author metadata is missing', async () => { + testState.pseudonymityMode = 'per-post'; + const post = makeLegacyThread(); + const reply = post.replies?.pages?.new?.comments?.[0]; + if (!reply) { + throw new Error('missing fixture reply'); + } + reply.cid = 'Qmb4NxbRDVVJF7w9QtwXuY94jqGAAx7Thx3JPuofDVPKY1'; + reply.author = {}; + + await renderWithRoute(createElement(PostDesktop, { post, showAllReplies: true }), '/mu/thread/post-1'); + expect(container.textContent).toContain('ID: Qmb4NxbR'); + + await renderWithRoute(createElement(PostMobile, { post, showAllReplies: true }), '/mu/thread/post-1'); + expect(container.textContent).toContain('ID: Qmb4NxbR'); + }); + it('forwards Pretext-backed reply estimates into Virtuoso for desktop and mobile thread views', async () => { testState.hasMoreReplies = true; diff --git a/src/components/post-desktop/post-desktop.tsx b/src/components/post-desktop/post-desktop.tsx index c7de1164..75eeb0f5 100644 --- a/src/components/post-desktop/post-desktop.tsx +++ b/src/components/post-desktop/post-desktop.tsx @@ -25,17 +25,17 @@ import useScrollToReply from '../../hooks/use-scroll-to-reply'; import useSafeAccountComment from '../../hooks/use-safe-account-comment'; import { useCurrentTime } from '../../hooks/use-current-time'; import { useBoardPseudonymityMode } from '../../hooks/use-board-pseudonymity-mode'; -import CommentContent from '../comment-content'; -import CommentMedia from '../comment-media'; +import CommentContent from '../comment-content/comment-content'; +import CommentMedia from '../comment-media/comment-media'; import EditMenu from '../edit-menu/edit-menu'; import FailedPublishNotice from '../failed-publish-notice'; -import { canEmbed } from '../embed'; -import LoadingEllipsis from '../loading-ellipsis'; +import { canEmbed } from '../embed/embed-utils'; +import LoadingEllipsis from '../loading-ellipsis/loading-ellipsis'; import PostAuthorFlags from '../post-author-flags'; import PostFlashTag from '../post-flash-tag'; -import PostMenuDesktop from './post-menu-desktop'; -import ReplyQuotePreview from '../reply-quote-preview'; -import Tooltip from '../tooltip'; +import PostMenuDesktop from './post-menu-desktop/post-menu-desktop'; +import ReplyQuotePreview from '../reply-quote-preview/reply-quote-preview'; +import Tooltip from '../tooltip/tooltip'; import TimeAgoTooltip from '../time-ago-tooltip'; import { PostProps } from '../../views/post/post'; import { create } from 'zustand'; @@ -69,6 +69,7 @@ import { getThreadTopNavigationState, scrollThreadContainerToTop } from '../../l import useDeleteFailedPost from '../../hooks/use-delete-failed-post'; import { getThreadPostCountsByAuthor } from '../../lib/utils/author-post-counts'; import { withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils'; +import { getCommentUserID } from '../../lib/utils/comment-user-id-utils'; import { getFeedPostHeightEstimate, getReplyHeightEstimates, reportReplyHeightAuditSample } from '../../lib/utils/pretext-height-estimates'; import { getAuthorBadge } from '../../lib/utils/author-display-utils'; import { hasCommentFlagsForDirectory } from '../../lib/comment-flag-selection'; @@ -233,7 +234,7 @@ const PostInfo = ({ const archived = isCommentArchived(post); const purged = post?.commentModeration?.purged; const title = post?.title?.trim(); - const { address, shortAddress } = author || {}; + const { address } = author || {}; const displayName = author?.displayName?.trim(); const authorBadge = getAuthorBadge({ address, role: roles?.[address]?.role }); const hasFailedState = state === 'failed'; @@ -268,7 +269,7 @@ const PostInfo = ({ const alertThresholdSeconds = getAlertThresholdSeconds(); const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds; - const userID = address ? getShortAddress(address) : shortAddress; + const userID = getCommentUserID(post); const userIDBackgroundColor = hashStringToColor(userID); const userIDTextColor = getTextColorForBackground(userIDBackgroundColor); @@ -277,11 +278,11 @@ const PostInfo = ({ const handleUserAddressClick = useAuthorAddressClick(); const numberOfPostsByAuthor = (() => { - if (!showUserID || deleted || removed || purged || !shortAddress || !postCid) { + if (!showUserID || deleted || removed || purged || !userID || !postCid) { return 0; } - return Math.max(postsByAuthorInThread?.get(shortAddress) ?? 0, 1); + return Math.max(postsByAuthorInThread?.get(userID) ?? 0, 1); })(); const { hidden } = useHide({ cid: cid || '' }); @@ -756,10 +757,12 @@ const Reply = ({ disableDeferredLayout, }: PostProps & { directRepliesByParentCid?: Map; postsByAuthorInThread?: Map; disableDeferredLayout?: boolean }) => { const accountReply = useSafeAccountComment({ + commentCid: reply?.cid, commentIndex: typeof reply?.index === 'number' ? reply.index : undefined, }); const hasReplyIndex = typeof reply?.index === 'number'; - let post = hasReplyIndex && accountReply?.index === reply.index ? accountReply : reply; + const isAccountReply = (hasReplyIndex && accountReply?.index === reply.index) || (!!reply?.cid && accountReply?.cid === reply.cid); + let post = isAccountReply ? accountReply : reply; // handle pending mod or author edit const { editedComment } = useEditedComment({ comment: post }); if (editedComment) { @@ -767,7 +770,8 @@ const Reply = ({ } post = withResolvedCommentCommunityAddress(post); - const { author, cid, deleted, link, linkHeight, linkWidth, postCid, reason, removed, spoiler, communityAddress, thumbnailUrl, parentCid } = post || {}; + const { cid, deleted, link, linkHeight, linkWidth, postCid, reason, removed, spoiler, communityAddress, thumbnailUrl, parentCid } = post || {}; + const userID = getCommentUserID(post); const purged = post?.commentModeration?.purged; const directories = useDirectories(); const boardPath = communityAddress ? getBoardPath(communityAddress, directories) : undefined; @@ -797,7 +801,7 @@ const Reply = ({ return (
{'>>'}
-
+
{ const { t } = useTranslation(); const resolvedPost = withResolvedCommentCommunityAddress(post); - const { author, cid, content, deleted, link, linkHeight, linkWidth, postCid, removed, spoiler, state, communityAddress, thumbnailUrl, parentCid } = resolvedPost || {}; + const { cid, content, deleted, link, linkHeight, linkWidth, postCid, removed, spoiler, state, communityAddress, thumbnailUrl, parentCid } = resolvedPost || {}; + const userID = getCommentUserID(resolvedPost); const purged = resolvedPost?.commentModeration?.purged; const params = useParams(); const location = useLocation(); @@ -1159,7 +1164,7 @@ const PostDesktop = ({
diff --git a/src/components/post-mobile/post-mobile.tsx b/src/components/post-mobile/post-mobile.tsx index 0e9a2890..f36de83a 100644 --- a/src/components/post-mobile/post-mobile.tsx +++ b/src/components/post-mobile/post-mobile.tsx @@ -24,15 +24,15 @@ import useScrollToReply from '../../hooks/use-scroll-to-reply'; import useSafeAccountComment from '../../hooks/use-safe-account-comment'; import { useCurrentTime } from '../../hooks/use-current-time'; import { useBoardPseudonymityMode } from '../../hooks/use-board-pseudonymity-mode'; -import CommentContent from '../comment-content'; -import CommentMedia, { MediaLoadFailureInfo } from '../comment-media'; +import CommentContent from '../comment-content/comment-content'; +import CommentMedia, { MediaLoadFailureInfo } from '../comment-media/comment-media'; import FailedPublishNotice from '../failed-publish-notice'; -import LoadingEllipsis from '../loading-ellipsis'; +import LoadingEllipsis from '../loading-ellipsis/loading-ellipsis'; import PostAuthorFlags from '../post-author-flags'; import PostFlashTag from '../post-flash-tag'; -import PostMenuMobile from './post-menu-mobile'; -import ReplyQuotePreview from '../reply-quote-preview'; -import Tooltip from '../tooltip'; +import PostMenuMobile from './post-menu-mobile/post-menu-mobile'; +import ReplyQuotePreview from '../reply-quote-preview/reply-quote-preview'; +import Tooltip from '../tooltip/tooltip'; import TimeAgoTooltip from '../time-ago-tooltip'; import { PostProps } from '../../views/post/post'; import capitalize from 'lodash/capitalize'; @@ -58,6 +58,7 @@ import { getThreadTopNavigationState, scrollThreadContainerToTop } from '../../l import useDeleteFailedPost from '../../hooks/use-delete-failed-post'; import { getThreadPostCountsByAuthor } from '../../lib/utils/author-post-counts'; import { withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils'; +import { getCommentUserID } from '../../lib/utils/comment-user-id-utils'; import { getFeedPostHeightEstimate, getReplyHeightEstimates, reportReplyHeightAuditSample } from '../../lib/utils/pretext-height-estimates'; import { getAuthorBadge } from '../../lib/utils/author-display-utils'; import { hasCommentFlagsForDirectory } from '../../lib/comment-flag-selection'; @@ -100,7 +101,7 @@ const PostInfoAndMedia = ({ : undefined; const isReply = parentCid; const title = post?.title?.trim(); - const { address, shortAddress } = author || {}; + const { address } = author || {}; const displayName = author?.displayName?.trim(); const authorBadge = getAuthorBadge({ address, role: roles?.[address]?.role }); @@ -220,17 +221,17 @@ const PostInfoAndMedia = ({ const pseudonymityMode = useBoardPseudonymityMode(communityAddress); const showUserID = pseudonymityMode === 'per-post'; + const userID = getCommentUserID(resolvedPost); const handleUserAddressClick = useAuthorAddressClick(); const numberOfPostsByAuthor = (() => { - if (!showUserID || deleted || removed || purged || !shortAddress || !postCid) { + if (!showUserID || deleted || removed || purged || !userID || !postCid) { return 0; } - return Math.max(postsByAuthorInThread?.get(shortAddress) ?? 0, 1); + return Math.max(postsByAuthorInThread?.get(userID) ?? 0, 1); })(); - const userID = address ? getShortAddress(address) : shortAddress; const userIDBackgroundColor = hashStringToColor(userID); const userIDTextColor = getTextColorForBackground(userIDBackgroundColor); @@ -532,17 +533,20 @@ const Reply = ({ disableDeferredLayout, }: PostProps & { directRepliesByParentCid?: Map; postsByAuthorInThread?: Map; disableDeferredLayout?: boolean }) => { const accountReply = useSafeAccountComment({ + commentCid: reply?.cid, commentIndex: typeof reply?.index === 'number' ? reply.index : undefined, }); const hasReplyIndex = typeof reply?.index === 'number'; - let post = hasReplyIndex && accountReply?.index === reply.index ? accountReply : reply; + const isAccountReply = (hasReplyIndex && accountReply?.index === reply.index) || (!!reply?.cid && accountReply?.cid === reply.cid); + let post = isAccountReply ? accountReply : reply; // handle pending mod or author edit const { editedComment } = useEditedComment({ comment: post }); if (editedComment) { post = editedComment; } post = withResolvedCommentCommunityAddress(post); - const { author, cid, deleted, postCid, reason, removed, communityAddress } = post || {}; + const { cid, deleted, postCid, reason, removed, communityAddress } = post || {}; + const userID = getCommentUserID(post); const purged = post?.commentModeration?.purged; const directories = useDirectories(); const boardPath = communityAddress ? getBoardPath(communityAddress, directories) : undefined; @@ -565,12 +569,7 @@ const Reply = ({ return (
-
+
{ const { t } = useTranslation(); const resolvedPost = withResolvedCommentCommunityAddress(post); - const { author, cid, parentCid, postCid, replyCount, state, communityAddress } = resolvedPost || {}; + const { cid, parentCid, postCid, replyCount, state, communityAddress } = resolvedPost || {}; + const userID = getCommentUserID(resolvedPost); const params = useParams(); const location = useLocation(); const navigationType = useNavigationType(); @@ -866,7 +866,7 @@ const PostMobile = ({ className={`${styles.postOp} ${shouldShowSnow() ? styles.xmasHatWrapper : ''}`} data-thread-container-cid={cid} data-cid={cid} - data-author-address={author?.shortAddress} + data-author-address={userID} data-post-cid={postCid} > {shouldShowSnow() && } diff --git a/src/hooks/__tests__/use-fresh-replies.test.tsx b/src/hooks/__tests__/use-fresh-replies.test.tsx index ac7941f5..1d981c3a 100644 --- a/src/hooks/__tests__/use-fresh-replies.test.tsx +++ b/src/hooks/__tests__/use-fresh-replies.test.tsx @@ -18,27 +18,26 @@ type TestComment = { const testState = vi.hoisted(() => ({ accountComments: [] as TestComment[], - accountCommentsCalls: [] as Array<{ commentIndices?: number[] } | undefined>, + accountCommentsCalls: [] as Array<{ commentIndices?: number[]; filter?: (comment: TestComment) => boolean } | undefined>, replies: [] as TestComment[], })); vi.mock('@bitsocial/bitsocial-react-hooks', () => ({ - useAccountComments: (options?: { commentIndices?: number[] }) => { + useAccountComments: (options?: { commentIndices?: number[]; filter?: (comment: TestComment) => boolean }) => { testState.accountCommentsCalls.push(options); if (!options?.commentIndices?.length) { - return { - accountComments: testState.accountComments, - }; + const accountComments = options?.filter ? testState.accountComments.filter(options.filter) : testState.accountComments; + return { accountComments }; } const normalizedCommentIndices = options.commentIndices.filter((commentIndex) => Number.isInteger(commentIndex) && commentIndex >= 0); - return { - accountComments: normalizedCommentIndices - .map((commentIndex) => testState.accountComments.find((accountComment) => accountComment.index === commentIndex)) - .filter(Boolean), - }; + const accountComments = normalizedCommentIndices + .map((commentIndex) => testState.accountComments.find((accountComment) => accountComment.index === commentIndex)) + .filter(Boolean); + + return { accountComments }; }, })); @@ -146,6 +145,38 @@ describe('useFreshReplies', () => { expect(testState.accountCommentsCalls).toContainEqual({ commentIndices: [0] }); }); + it('replaces unindexed replies with account comments matched by cid', () => { + testState.replies = [ + { + cid: 'reply-cid', + content: 'stale reply', + communityAddress: 'music.eth', + }, + { + cid: 'network-reply-cid', + content: 'network reply', + communityAddress: 'music.eth', + }, + ]; + testState.accountComments = [ + { + cid: 'reply-cid', + content: 'fresh reply', + index: 5, + number: 27, + communityAddress: 'music.eth', + }, + ]; + + renderHook(); + + expect(latestValue[0]).toBe(testState.accountComments[0] as never); + expect(latestValue[0]?.number).toBe(27); + expect(latestValue[1]).toBe(testState.replies[1] as never); + expect(testState.accountCommentsCalls).toContainEqual({ commentIndices: [-1] }); + expect(testState.accountCommentsCalls).toContainEqual({ filter: expect.any(Function) }); + }); + it('orders replies by final post number after a pending reply is approved', () => { testState.replies = [ { diff --git a/src/hooks/use-fresh-replies.ts b/src/hooks/use-fresh-replies.ts index e467a426..234541f3 100644 --- a/src/hooks/use-fresh-replies.ts +++ b/src/hooks/use-fresh-replies.ts @@ -10,29 +10,63 @@ const useFreshReplies = (replies: Comment[] = []) => { () => Array.from(new Set(replies.map((reply) => reply?.index).filter((replyIndex): replyIndex is number => typeof replyIndex === 'number'))), [replies], ); + const replyCidsWithoutIndices = useMemo( + () => + Array.from( + new Set( + replies + .map((reply) => (typeof reply?.index === 'number' ? undefined : reply?.cid)) + .filter((replyCid): replyCid is string => typeof replyCid === 'string' && replyCid.length > 0), + ), + ), + [replies], + ); const accountCommentLookupOptions = useMemo(() => (replyIndices.length > 0 ? { commentIndices: replyIndices } : EMPTY_ACCOUNT_COMMENT_LOOKUP), [replyIndices]); - const { accountComments } = useAccountComments(accountCommentLookupOptions); + const accountCommentCidLookupOptions = useMemo(() => { + if (replyCidsWithoutIndices.length === 0) { + return EMPTY_ACCOUNT_COMMENT_LOOKUP; + } + + const replyCidSet = new Set(replyCidsWithoutIndices); + return { + filter: (accountComment: Comment) => typeof accountComment?.cid === 'string' && replyCidSet.has(accountComment.cid), + }; + }, [replyCidsWithoutIndices]); + const { accountComments: accountCommentsByIndexList } = useAccountComments(accountCommentLookupOptions); + const { accountComments: accountCommentsByCidList } = useAccountComments(accountCommentCidLookupOptions); return useMemo(() => { if (!replies.length) { return replies; } - if (!accountComments?.length) { + if (!accountCommentsByIndexList?.length && !accountCommentsByCidList?.length) { return sortRepliesForDisplay(replies); } const accountCommentsByIndex = new Map(); - for (const accountComment of accountComments) { + for (const accountComment of accountCommentsByIndexList) { if (typeof accountComment?.index === 'number') { accountCommentsByIndex.set(accountComment.index, accountComment); } } + const accountCommentsByCidMap = new Map(); + for (const accountComment of accountCommentsByCidList) { + if (typeof accountComment?.cid === 'string') { + accountCommentsByCidMap.set(accountComment.cid, accountComment); + } + } let hasFreshReplies = false; const nextReplies = replies.map((reply) => { if (typeof reply?.index !== 'number') { - return reply; + const freshReply = typeof reply?.cid === 'string' ? accountCommentsByCidMap.get(reply.cid) : undefined; + if (!freshReply) { + return reply; + } + + hasFreshReplies = true; + return freshReply; } const freshReply = accountCommentsByIndex.get(reply.index); @@ -65,7 +99,7 @@ const useFreshReplies = (replies: Comment[] = []) => { }); return sortRepliesForDisplay(hasDuplicateReplyIndices ? dedupedReplies : nextReplies); - }, [accountComments, replies]); + }, [accountCommentsByCidList, accountCommentsByIndexList, replies]); }; export default useFreshReplies; diff --git a/src/lib/utils/__tests__/author-post-counts.test.ts b/src/lib/utils/__tests__/author-post-counts.test.ts index c4d2bce4..d8343cb1 100644 --- a/src/lib/utils/__tests__/author-post-counts.test.ts +++ b/src/lib/utils/__tests__/author-post-counts.test.ts @@ -27,13 +27,14 @@ describe('getThreadPostCountsByAuthor', () => { expect(counts.get('author-b')).toBe(2); }); - it('skips comments missing a cid or short address', () => { + it('uses cid fallback for comments without author metadata', () => { const counts = getThreadPostCountsByAuthor( { cid: 'post-1', author: { shortAddress: 'author-a' } } as any, [{ cid: 'reply-1' }, { author: { shortAddress: 'author-b' } }] as any[], ); expect(counts.get('author-a')).toBe(1); + expect(counts.get('reply-1')).toBe(1); expect(counts.has('author-b')).toBe(false); }); }); diff --git a/src/lib/utils/author-post-counts.ts b/src/lib/utils/author-post-counts.ts index f23f5044..f1629632 100644 --- a/src/lib/utils/author-post-counts.ts +++ b/src/lib/utils/author-post-counts.ts @@ -1,4 +1,5 @@ -import { Comment } from '@bitsocial/bitsocial-react-hooks'; +import type { Comment } from '@bitsocial/bitsocial-react-hooks'; +import { getCommentUserID } from './comment-user-id-utils'; export function getThreadPostCountsByAuthor(post: Comment | undefined, replies: Comment[] = []): Map { const counts = new Map(); @@ -6,11 +7,11 @@ export function getThreadPostCountsByAuthor(post: Comment | undefined, replies: for (const comment of [post, ...replies]) { const cid = comment?.cid; - const shortAddress = comment?.author?.shortAddress; - if (!cid || !shortAddress || seenCids.has(cid)) continue; + const userID = getCommentUserID(comment); + if (!cid || !userID || seenCids.has(cid)) continue; seenCids.add(cid); - counts.set(shortAddress, (counts.get(shortAddress) ?? 0) + 1); + counts.set(userID, (counts.get(userID) ?? 0) + 1); } return counts; diff --git a/src/lib/utils/comment-user-id-utils.ts b/src/lib/utils/comment-user-id-utils.ts new file mode 100644 index 00000000..97db017e --- /dev/null +++ b/src/lib/utils/comment-user-id-utils.ts @@ -0,0 +1,7 @@ +import type { Comment } from '@bitsocial/bitsocial-react-hooks'; +import getShortAddress from '../get-short-address'; + +export function getCommentUserID(comment: Comment | undefined): string { + const { address, shortAddress } = comment?.author || {}; + return (address ? getShortAddress(address) : '') || shortAddress || comment?.cid || ''; +}