mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(thread): count user id tooltips from thread data
Replace the DOM-based user ID tooltip count with a deduped per-thread author map in `getThreadPostCountsByAuthor()`. This keeps mounted preview copies from doubling ID totals on desktop and mobile thread views.
This commit is contained in:
@@ -53,6 +53,7 @@ import { computeOmittedCount, filterRepliesForDisplay, getPreviewDisplayReplies,
|
|||||||
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
|
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
|
||||||
import { getThreadTopNavigationState, scrollThreadContainerToTop } from '../../lib/utils/thread-scroll-utils';
|
import { getThreadTopNavigationState, scrollThreadContainerToTop } from '../../lib/utils/thread-scroll-utils';
|
||||||
import useDeleteFailedPost from '../../hooks/use-delete-failed-post';
|
import useDeleteFailedPost from '../../hooks/use-delete-failed-post';
|
||||||
|
import { getThreadPostCountsByAuthor } from '../../lib/utils/author-post-counts';
|
||||||
import { withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
import { withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
||||||
|
|
||||||
const { addChallenge } = useChallengesStore.getState();
|
const { addChallenge } = useChallengesStore.getState();
|
||||||
@@ -97,7 +98,8 @@ const PostInfo = ({
|
|||||||
onReject,
|
onReject,
|
||||||
quotedByMap,
|
quotedByMap,
|
||||||
directRepliesByParentCid,
|
directRepliesByParentCid,
|
||||||
}: PostProps & { directRepliesByParentCid?: Map<string, Comment[]> }) => {
|
postsByAuthorInThread,
|
||||||
|
}: PostProps & { directRepliesByParentCid?: Map<string, Comment[]>; postsByAuthorInThread?: Map<string, number> }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { author, cid, deleted, locked, pinned, parentCid, postCid, reason, removed, state, communityAddress, timestamp } = post || {};
|
const { author, cid, deleted, locked, pinned, parentCid, postCid, reason, removed, state, communityAddress, timestamp } = post || {};
|
||||||
const archived = isCommentArchived(post);
|
const archived = isCommentArchived(post);
|
||||||
@@ -228,12 +230,11 @@ const PostInfo = ({
|
|||||||
|
|
||||||
const handleUserAddressClick = useAuthorAddressClick();
|
const handleUserAddressClick = useAuthorAddressClick();
|
||||||
const numberOfPostsByAuthor = (() => {
|
const numberOfPostsByAuthor = (() => {
|
||||||
if (!showUserID || deleted || removed || purged || !shortAddress || !postCid || typeof document === 'undefined') {
|
if (!showUserID || deleted || removed || purged || !shortAddress || !postCid) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const domCount = document.querySelectorAll(`[data-author-address="${shortAddress}"][data-post-cid="${postCid}"]`).length;
|
return Math.max(postsByAuthorInThread?.get(shortAddress) ?? 0, 1);
|
||||||
return Math.max(domCount, 1);
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const { hidden } = useHide(post);
|
const { hidden } = useHide(post);
|
||||||
@@ -704,7 +705,8 @@ const Reply = ({
|
|||||||
threadNumber,
|
threadNumber,
|
||||||
quotedByMap,
|
quotedByMap,
|
||||||
directRepliesByParentCid,
|
directRepliesByParentCid,
|
||||||
}: PostProps & { directRepliesByParentCid?: Map<string, Comment[]> }) => {
|
postsByAuthorInThread,
|
||||||
|
}: PostProps & { directRepliesByParentCid?: Map<string, Comment[]>; postsByAuthorInThread?: Map<string, number> }) => {
|
||||||
const accountReply = useAccountComment({
|
const accountReply = useAccountComment({
|
||||||
commentIndex: typeof reply?.index === 'number' ? reply.index : undefined,
|
commentIndex: typeof reply?.index === 'number' ? reply.index : undefined,
|
||||||
});
|
});
|
||||||
@@ -744,6 +746,7 @@ const Reply = ({
|
|||||||
<PostInfo
|
<PostInfo
|
||||||
post={post}
|
post={post}
|
||||||
postReplyCount={postReplyCount}
|
postReplyCount={postReplyCount}
|
||||||
|
postsByAuthorInThread={postsByAuthorInThread}
|
||||||
roles={roles}
|
roles={roles}
|
||||||
isHidden={hidden}
|
isHidden={hidden}
|
||||||
threadNumber={threadNumber}
|
threadNumber={threadNumber}
|
||||||
@@ -890,6 +893,7 @@ const PostDesktop = ({
|
|||||||
|
|
||||||
// Author-deleted replies are hidden from thread replies; moderator removals still render their placeholder.
|
// Author-deleted replies are hidden from thread replies; moderator removals still render their placeholder.
|
||||||
const filteredReplies = filterRepliesForDisplay(freshRepliesForRender);
|
const filteredReplies = filterRepliesForDisplay(freshRepliesForRender);
|
||||||
|
const postsByAuthorInThread = getThreadPostCountsByAuthor(resolvedPost, filteredReplies);
|
||||||
const directRepliesByParentCid = (() => {
|
const directRepliesByParentCid = (() => {
|
||||||
const map = new Map<string, Comment[]>();
|
const map = new Map<string, Comment[]>();
|
||||||
for (const reply of filteredReplies) {
|
for (const reply of filteredReplies) {
|
||||||
@@ -1011,6 +1015,7 @@ const PostDesktop = ({
|
|||||||
isHidden={hidden}
|
isHidden={hidden}
|
||||||
post={resolvedPost}
|
post={resolvedPost}
|
||||||
postReplyCount={replyCount}
|
postReplyCount={replyCount}
|
||||||
|
postsByAuthorInThread={postsByAuthorInThread}
|
||||||
roles={roles}
|
roles={roles}
|
||||||
threadNumber={resolvedPost?.number}
|
threadNumber={resolvedPost?.number}
|
||||||
isModQueue={isModQueue}
|
isModQueue={isModQueue}
|
||||||
@@ -1070,6 +1075,7 @@ const PostDesktop = ({
|
|||||||
reply={reply}
|
reply={reply}
|
||||||
roles={roles}
|
roles={roles}
|
||||||
postReplyCount={replyCount}
|
postReplyCount={replyCount}
|
||||||
|
postsByAuthorInThread={postsByAuthorInThread}
|
||||||
threadNumber={resolvedPost?.number}
|
threadNumber={resolvedPost?.number}
|
||||||
quotedByMap={quotedByMap}
|
quotedByMap={quotedByMap}
|
||||||
directRepliesByParentCid={directRepliesByParentCid}
|
directRepliesByParentCid={directRepliesByParentCid}
|
||||||
@@ -1096,6 +1102,7 @@ const PostDesktop = ({
|
|||||||
reply={reply}
|
reply={reply}
|
||||||
roles={roles}
|
roles={roles}
|
||||||
postReplyCount={replyCount}
|
postReplyCount={replyCount}
|
||||||
|
postsByAuthorInThread={postsByAuthorInThread}
|
||||||
threadNumber={resolvedPost?.number}
|
threadNumber={resolvedPost?.number}
|
||||||
quotedByMap={quotedByMap}
|
quotedByMap={quotedByMap}
|
||||||
directRepliesByParentCid={directRepliesByParentCid}
|
directRepliesByParentCid={directRepliesByParentCid}
|
||||||
@@ -1114,6 +1121,7 @@ const PostDesktop = ({
|
|||||||
reply={reply}
|
reply={reply}
|
||||||
roles={roles}
|
roles={roles}
|
||||||
postReplyCount={replyCount}
|
postReplyCount={replyCount}
|
||||||
|
postsByAuthorInThread={postsByAuthorInThread}
|
||||||
threadNumber={resolvedPost?.number}
|
threadNumber={resolvedPost?.number}
|
||||||
quotedByMap={quotedByMap}
|
quotedByMap={quotedByMap}
|
||||||
directRepliesByParentCid={directRepliesByParentCid}
|
directRepliesByParentCid={directRepliesByParentCid}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import { filterRepliesForDisplay, getPreviewDisplayReplies } from '../../lib/uti
|
|||||||
import { getRenderableMobileBacklinks } from '../../lib/utils/reply-backlink-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';
|
||||||
import useDeleteFailedPost from '../../hooks/use-delete-failed-post';
|
import useDeleteFailedPost from '../../hooks/use-delete-failed-post';
|
||||||
|
import { getThreadPostCountsByAuthor } from '../../lib/utils/author-post-counts';
|
||||||
import { withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
import { withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
||||||
|
|
||||||
const { addChallenge } = useChallengesStore.getState();
|
const { addChallenge } = useChallengesStore.getState();
|
||||||
@@ -62,7 +63,7 @@ const RepliesFooter = ({ hasMore, loadingString }: { hasMore: boolean; loadingSt
|
|||||||
// Store scroll position for replies virtuoso across navigations
|
// Store scroll position for replies virtuoso across navigations
|
||||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||||
|
|
||||||
const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: PostProps) => {
|
const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber, postsByAuthorInThread }: PostProps & { postsByAuthorInThread?: Map<string, number> }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const directories = useDirectories();
|
const directories = useDirectories();
|
||||||
const resolvedPost = withResolvedCommentCommunityAddress(post);
|
const resolvedPost = withResolvedCommentCommunityAddress(post);
|
||||||
@@ -206,12 +207,11 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
|||||||
|
|
||||||
const handleUserAddressClick = useAuthorAddressClick();
|
const handleUserAddressClick = useAuthorAddressClick();
|
||||||
const numberOfPostsByAuthor = (() => {
|
const numberOfPostsByAuthor = (() => {
|
||||||
if (!showUserID || deleted || removed || purged || !shortAddress || !postCid || typeof document === 'undefined') {
|
if (!showUserID || deleted || removed || purged || !shortAddress || !postCid) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const domCount = document.querySelectorAll(`[data-author-address="${shortAddress}"][data-post-cid="${postCid}"]`).length;
|
return Math.max(postsByAuthorInThread?.get(shortAddress) ?? 0, 1);
|
||||||
return Math.max(domCount, 1);
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const userID = address ? getShortAddress(address) : shortAddress;
|
const userID = address ? getShortAddress(address) : shortAddress;
|
||||||
@@ -493,7 +493,8 @@ const Reply = ({
|
|||||||
threadNumber,
|
threadNumber,
|
||||||
quotedByMap,
|
quotedByMap,
|
||||||
directRepliesByParentCid,
|
directRepliesByParentCid,
|
||||||
}: PostProps & { directRepliesByParentCid?: Map<string, Comment[]> }) => {
|
postsByAuthorInThread,
|
||||||
|
}: PostProps & { directRepliesByParentCid?: Map<string, Comment[]>; postsByAuthorInThread?: Map<string, number> }) => {
|
||||||
const accountReply = useAccountComment({
|
const accountReply = useAccountComment({
|
||||||
commentIndex: typeof reply?.index === 'number' ? reply.index : undefined,
|
commentIndex: typeof reply?.index === 'number' ? reply.index : undefined,
|
||||||
});
|
});
|
||||||
@@ -525,7 +526,7 @@ const Reply = ({
|
|||||||
data-author-address={author?.shortAddress}
|
data-author-address={author?.shortAddress}
|
||||||
data-post-cid={postCid}
|
data-post-cid={postCid}
|
||||||
>
|
>
|
||||||
<PostInfoAndMedia post={post} postReplyCount={postReplyCount} roles={roles} threadNumber={threadNumber} />
|
<PostInfoAndMedia post={post} postReplyCount={postReplyCount} postsByAuthorInThread={postsByAuthorInThread} roles={roles} threadNumber={threadNumber} />
|
||||||
{!hidden && (!(removed || deleted || purged) || ((removed || deleted) && reason) || purged) && (
|
{!hidden && (!(removed || deleted || purged) || ((removed || deleted) && reason) || purged) && (
|
||||||
<CommentContent comment={post} prependContent={failedPublishNotice} />
|
<CommentContent comment={post} prependContent={failedPublishNotice} />
|
||||||
)}
|
)}
|
||||||
@@ -604,6 +605,7 @@ const PostMobile = ({
|
|||||||
|
|
||||||
// Author-deleted replies are hidden from thread replies; moderator removals still render their placeholder.
|
// Author-deleted replies are hidden from thread replies; moderator removals still render their placeholder.
|
||||||
const filteredReplies = filterRepliesForDisplay(freshRepliesForRender);
|
const filteredReplies = filterRepliesForDisplay(freshRepliesForRender);
|
||||||
|
const postsByAuthorInThread = getThreadPostCountsByAuthor(resolvedPost, filteredReplies);
|
||||||
const previewDisplayReplies = getPreviewDisplayReplies(filteredReplies, BOARD_REPLIES_PREVIEW_VISIBLE_COUNT);
|
const previewDisplayReplies = getPreviewDisplayReplies(filteredReplies, BOARD_REPLIES_PREVIEW_VISIBLE_COUNT);
|
||||||
|
|
||||||
const directRepliesByParentCid = (() => {
|
const directRepliesByParentCid = (() => {
|
||||||
@@ -702,7 +704,13 @@ const PostMobile = ({
|
|||||||
data-post-cid={postCid}
|
data-post-cid={postCid}
|
||||||
>
|
>
|
||||||
{shouldShowSnow() && <img src='assets/xmashat.gif' className={styles.xmasHat} alt='' />}
|
{shouldShowSnow() && <img src='assets/xmashat.gif' className={styles.xmasHat} alt='' />}
|
||||||
<PostInfoAndMedia post={resolvedPost} postReplyCount={replyCount} roles={roles} threadNumber={resolvedPost?.number} />
|
<PostInfoAndMedia
|
||||||
|
post={resolvedPost}
|
||||||
|
postReplyCount={replyCount}
|
||||||
|
postsByAuthorInThread={postsByAuthorInThread}
|
||||||
|
roles={roles}
|
||||||
|
threadNumber={resolvedPost?.number}
|
||||||
|
/>
|
||||||
<CommentContent comment={resolvedPost} prependContent={failedPublishNotice} />
|
<CommentContent comment={resolvedPost} prependContent={failedPublishNotice} />
|
||||||
<ReplyBacklinks post={resolvedPost} quotedByMap={quotedByMap} directRepliesByParentCid={directRepliesByParentCid} />
|
<ReplyBacklinks post={resolvedPost} quotedByMap={quotedByMap} directRepliesByParentCid={directRepliesByParentCid} />
|
||||||
</div>
|
</div>
|
||||||
@@ -765,6 +773,7 @@ const PostMobile = ({
|
|||||||
<Reply
|
<Reply
|
||||||
postReplyCount={replyCount}
|
postReplyCount={replyCount}
|
||||||
reply={reply}
|
reply={reply}
|
||||||
|
postsByAuthorInThread={postsByAuthorInThread}
|
||||||
roles={roles}
|
roles={roles}
|
||||||
threadNumber={resolvedPost?.number}
|
threadNumber={resolvedPost?.number}
|
||||||
quotedByMap={quotedByMap}
|
quotedByMap={quotedByMap}
|
||||||
@@ -790,6 +799,7 @@ const PostMobile = ({
|
|||||||
<Reply
|
<Reply
|
||||||
postReplyCount={replyCount}
|
postReplyCount={replyCount}
|
||||||
reply={reply}
|
reply={reply}
|
||||||
|
postsByAuthorInThread={postsByAuthorInThread}
|
||||||
roles={roles}
|
roles={roles}
|
||||||
threadNumber={resolvedPost?.number}
|
threadNumber={resolvedPost?.number}
|
||||||
quotedByMap={quotedByMap}
|
quotedByMap={quotedByMap}
|
||||||
@@ -807,6 +817,7 @@ const PostMobile = ({
|
|||||||
<Reply
|
<Reply
|
||||||
postReplyCount={replyCount}
|
postReplyCount={replyCount}
|
||||||
reply={reply}
|
reply={reply}
|
||||||
|
postsByAuthorInThread={postsByAuthorInThread}
|
||||||
roles={roles}
|
roles={roles}
|
||||||
threadNumber={resolvedPost?.number}
|
threadNumber={resolvedPost?.number}
|
||||||
quotedByMap={quotedByMap}
|
quotedByMap={quotedByMap}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { getThreadPostCountsByAuthor } from '../author-post-counts';
|
||||||
|
|
||||||
|
describe('getThreadPostCountsByAuthor', () => {
|
||||||
|
it('counts the OP and replies per author short address', () => {
|
||||||
|
const post = { cid: 'post-1', author: { shortAddress: 'author-a' } } as any;
|
||||||
|
const replies = [
|
||||||
|
{ cid: 'reply-1', author: { shortAddress: 'author-b' } },
|
||||||
|
{ cid: 'reply-2', author: { shortAddress: 'author-a' } },
|
||||||
|
{ cid: 'reply-3', author: { shortAddress: 'author-a' } },
|
||||||
|
] as any[];
|
||||||
|
|
||||||
|
const counts = getThreadPostCountsByAuthor(post, replies);
|
||||||
|
|
||||||
|
expect(counts.get('author-a')).toBe(3);
|
||||||
|
expect(counts.get('author-b')).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deduplicates repeated CIDs so preview copies do not inflate the count', () => {
|
||||||
|
const post = { cid: 'post-1', author: { shortAddress: 'author-a' } } as any;
|
||||||
|
const duplicateReply = { cid: 'reply-1', author: { shortAddress: 'author-b' } } as any;
|
||||||
|
const replies = [duplicateReply, duplicateReply, { cid: 'reply-2', author: { shortAddress: 'author-b' } }] as any[];
|
||||||
|
|
||||||
|
const counts = getThreadPostCountsByAuthor(post, replies);
|
||||||
|
|
||||||
|
expect(counts.get('author-a')).toBe(1);
|
||||||
|
expect(counts.get('author-b')).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips comments missing a cid or short address', () => {
|
||||||
|
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.has('author-b')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Comment } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
|
|
||||||
|
export function getThreadPostCountsByAuthor(post: Comment | undefined, replies: Comment[] = []): Map<string, number> {
|
||||||
|
const counts = new Map<string, number>();
|
||||||
|
const seenCids = new Set<string>();
|
||||||
|
|
||||||
|
for (const comment of [post, ...replies]) {
|
||||||
|
const cid = comment?.cid;
|
||||||
|
const shortAddress = comment?.author?.shortAddress;
|
||||||
|
if (!cid || !shortAddress || seenCids.has(cid)) continue;
|
||||||
|
|
||||||
|
seenCids.add(cid);
|
||||||
|
counts.set(shortAddress, (counts.get(shortAddress) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user