mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
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
This commit is contained in:
@@ -47,8 +47,10 @@ type TestComment = {
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
addChallengeMock: vi.fn(),
|
||||
accountCommentsByCid: {} as Record<string, TestComment | undefined>,
|
||||
hasMoreReplies: false,
|
||||
openReplyModalMock: vi.fn(),
|
||||
pseudonymityMode: 'none',
|
||||
replyComments: [] as Array<TestComment | undefined>,
|
||||
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;
|
||||
|
||||
|
||||
@@ -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<string, Comment[]>; postsByAuthorInThread?: Map<string, number>; 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 (
|
||||
<div className={`${styles.replyDesktop} ${disableDeferredLayout ? styles.pretextVirtualizedReply : ''}`}>
|
||||
<div className={styles.sideArrows}>{'>>'}</div>
|
||||
<div className={`${styles.reply} ${isRouteLinkToReply && styles.highlight}`} data-cid={cid} data-author-address={author?.shortAddress} data-post-cid={postCid}>
|
||||
<div className={`${styles.reply} ${isRouteLinkToReply && styles.highlight}`} data-cid={cid} data-author-address={userID} data-post-cid={postCid}>
|
||||
<PostInfo
|
||||
post={post}
|
||||
postReplyCount={postReplyCount}
|
||||
@@ -852,7 +856,8 @@ const PostDesktop = ({
|
||||
}: PostProps) => {
|
||||
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 = ({
|
||||
<div
|
||||
data-thread-container-cid={cid}
|
||||
data-cid={cid}
|
||||
data-author-address={author?.shortAddress}
|
||||
data-author-address={userID}
|
||||
data-post-cid={postCid}
|
||||
className={`${styles.opContainer} ${shouldShowSnow() && hasThumbnail ? styles.xmasHatWrapper : ''}`}
|
||||
>
|
||||
|
||||
@@ -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<string, Comment[]>; postsByAuthorInThread?: Map<string, number>; 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 (
|
||||
<div className={`${styles.replyMobile} ${disableDeferredLayout ? styles.pretextVirtualizedReply : ''}`}>
|
||||
<div className={styles.reply}>
|
||||
<div
|
||||
className={`${styles.replyContainer} ${isRouteLinkToReply && styles.highlight}`}
|
||||
data-cid={cid}
|
||||
data-author-address={author?.shortAddress}
|
||||
data-post-cid={postCid}
|
||||
>
|
||||
<div className={`${styles.replyContainer} ${isRouteLinkToReply && styles.highlight}`} data-cid={cid} data-author-address={userID} data-post-cid={postCid}>
|
||||
<PostInfoAndMedia
|
||||
onMediaLoadFailureChange={setFailedMediaUrl}
|
||||
post={post}
|
||||
@@ -608,7 +607,8 @@ const PostMobile = ({
|
||||
}: PostProps) => {
|
||||
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() && <img src='assets/xmashat.gif' className={styles.xmasHat} alt='' />}
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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<number, Comment>();
|
||||
for (const accountComment of accountComments) {
|
||||
for (const accountComment of accountCommentsByIndexList) {
|
||||
if (typeof accountComment?.index === 'number') {
|
||||
accountCommentsByIndex.set(accountComment.index, accountComment);
|
||||
}
|
||||
}
|
||||
const accountCommentsByCidMap = new Map<string, Comment>();
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string, number> {
|
||||
const counts = new Map<string, number>();
|
||||
@@ -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;
|
||||
|
||||
@@ -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 || '';
|
||||
}
|
||||
Reference in New Issue
Block a user