mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(post ids): never display account identity as a post user id (#1184)
* fix(post ids): never display account identity as a post user id In per-post pseudonymity mode, local account comment data is overlaid on published comments so author edit/delete controls keep working, but the overlay also replaced the displayed User ID with the account author address in logged-in thread views. Split the two concerns: getCommentUserID now reads only the published author.shortAddress, and preservePublishedUserID mirrors the published ID back onto comments after any local account/edit overlay. * test(post): cover published user id restore over edited comment overlay
This commit is contained in:
@@ -15,6 +15,7 @@ type TestComment = {
|
||||
displayName?: string;
|
||||
shortAddress?: string;
|
||||
};
|
||||
accountId?: string;
|
||||
cid?: string;
|
||||
communityAddress?: string;
|
||||
content?: string;
|
||||
@@ -583,21 +584,46 @@ describe('post community address compatibility', () => {
|
||||
expect(container.querySelector('img[title="Anarcho-Capitalist"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('falls back to author shortAddress when the full author address cannot be shortened', async () => {
|
||||
it('renders the author shortAddress instead of deriving a user ID from the author address', async () => {
|
||||
testState.pseudonymityMode = 'per-post';
|
||||
const post = {
|
||||
...makeLegacyThreadWithoutReplies(),
|
||||
author: { address: 'short-address', shortAddress: 'B2mAZojE' },
|
||||
author: { address: 'account-author.bso', shortAddress: 'B2mAZojE' },
|
||||
};
|
||||
|
||||
await renderWithRoute(createElement(PostDesktop, { post } as any), '/mu/thread/post-1');
|
||||
expect(container.textContent).toContain('ID: B2mAZojE');
|
||||
expect(container.textContent).not.toContain('account-author.bso');
|
||||
|
||||
await renderWithRoute(createElement(PostMobile, { post } as any), '/mu/thread/post-1');
|
||||
expect(container.textContent).toContain('ID: B2mAZojE');
|
||||
expect(container.textContent).not.toContain('account-author.bso');
|
||||
});
|
||||
|
||||
it('uses safe account reply data by cid when a reply has no index', async () => {
|
||||
it('keeps the published reply shortAddress when local account reply data has account author identity', 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.author = { address: 'published-reply-address', shortAddress: 'ReplyKid9' };
|
||||
testState.accountCommentsByCid[reply.cid] = {
|
||||
...reply,
|
||||
accountId: 'viewer-account',
|
||||
author: { address: 'account-author.bso', shortAddress: 'account-author' },
|
||||
};
|
||||
|
||||
await renderWithRoute(createElement(PostDesktop, { post, showAllReplies: true }), '/mu/thread/post-1');
|
||||
expect(container.textContent).toContain('ID: ReplyKid');
|
||||
expect(container.textContent).not.toContain('ID: account');
|
||||
|
||||
await renderWithRoute(createElement(PostMobile, { post, showAllReplies: true }), '/mu/thread/post-1');
|
||||
expect(container.textContent).toContain('ID: ReplyKid');
|
||||
expect(container.textContent).not.toContain('ID: account');
|
||||
});
|
||||
|
||||
it('hides account author identity when a cid-matched reply has no published user ID yet', async () => {
|
||||
testState.pseudonymityMode = 'per-post';
|
||||
const post = makeLegacyThread();
|
||||
const reply = post.replies?.pages?.new?.comments?.[0];
|
||||
@@ -608,14 +634,17 @@ describe('post community address compatibility', () => {
|
||||
reply.author = {};
|
||||
testState.accountCommentsByCid[reply.cid] = {
|
||||
...reply,
|
||||
author: { shortAddress: 'ReplyKid9' },
|
||||
accountId: 'viewer-account',
|
||||
author: { address: 'account-author.bso', shortAddress: 'account-author' },
|
||||
};
|
||||
|
||||
await renderWithRoute(createElement(PostDesktop, { post, showAllReplies: true }), '/mu/thread/post-1');
|
||||
expect(container.textContent).toContain('ID: ReplyKid');
|
||||
expect(container.textContent).toContain('ID: Pending');
|
||||
expect(container.textContent).not.toContain('ID: account');
|
||||
|
||||
await renderWithRoute(createElement(PostMobile, { post, showAllReplies: true }), '/mu/thread/post-1');
|
||||
expect(container.textContent).toContain('ID: ReplyKid');
|
||||
expect(container.textContent).toContain('ID: Pending');
|
||||
expect(container.textContent).not.toContain('ID: account');
|
||||
});
|
||||
|
||||
it('shows a pending ID while published reply author metadata is missing', async () => {
|
||||
|
||||
@@ -67,7 +67,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 { getCommentUserID, preservePublishedUserID } 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';
|
||||
@@ -706,11 +706,11 @@ const Reply = ({
|
||||
});
|
||||
const hasReplyIndex = typeof reply?.index === 'number';
|
||||
const isAccountReply = (hasReplyIndex && accountReply?.index === reply.index) || (!!reply?.cid && accountReply?.cid === reply.cid);
|
||||
let post = isAccountReply ? accountReply : reply;
|
||||
let post = isAccountReply ? preservePublishedUserID(accountReply, reply) : reply;
|
||||
// handle pending mod or author edit
|
||||
const { editedComment } = useEditedComment({ comment: post });
|
||||
if (editedComment) {
|
||||
post = editedComment;
|
||||
post = preservePublishedUserID(editedComment, reply);
|
||||
}
|
||||
post = withResolvedCommentCommunityAddress(post);
|
||||
|
||||
|
||||
@@ -58,7 +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 { getCommentUserID, preservePublishedUserID } 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';
|
||||
@@ -472,11 +472,11 @@ const Reply = ({
|
||||
});
|
||||
const hasReplyIndex = typeof reply?.index === 'number';
|
||||
const isAccountReply = (hasReplyIndex && accountReply?.index === reply.index) || (!!reply?.cid && accountReply?.cid === reply.cid);
|
||||
let post = isAccountReply ? accountReply : reply;
|
||||
let post = isAccountReply ? preservePublishedUserID(accountReply, reply) : reply;
|
||||
// handle pending mod or author edit
|
||||
const { editedComment } = useEditedComment({ comment: post });
|
||||
if (editedComment) {
|
||||
post = editedComment;
|
||||
post = preservePublishedUserID(editedComment, reply);
|
||||
}
|
||||
post = withResolvedCommentCommunityAddress(post);
|
||||
const { cid, deleted, postCid, reason, removed, communityAddress } = post || {};
|
||||
|
||||
@@ -1,13 +1,59 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getCommentUserID } from '../comment-user-id-utils';
|
||||
import { getCommentUserID, preservePublishedUserID } from '../comment-user-id-utils';
|
||||
|
||||
describe('getCommentUserID', () => {
|
||||
it('uses resolved author identity data for per-post user IDs', () => {
|
||||
expect(getCommentUserID({ author: { address: 'poster.bso', shortAddress: 'short-poster' } } as any)).toBe('poster.bso');
|
||||
it('uses only the comment author short address for per-post user IDs', () => {
|
||||
expect(getCommentUserID({ author: { address: 'poster.bso', shortAddress: 'short-poster' } } as any)).toBe('short-poster');
|
||||
expect(getCommentUserID({ author: { shortAddress: 'short-poster' } } as any)).toBe('short-poster');
|
||||
});
|
||||
|
||||
it('does not derive a user ID from an author address', () => {
|
||||
expect(getCommentUserID({ author: { address: 'poster.bso' } } as any)).toBe('');
|
||||
});
|
||||
|
||||
it('does not fall back to the comment cid while author metadata is missing', () => {
|
||||
expect(getCommentUserID({ cid: 'QmQU6BiPkB77b3rAkdj973ptTp5A6X77tfPrwTvCQDL9cq' } as any)).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('preservePublishedUserID', () => {
|
||||
it('keeps local account author data but preserves the published user ID', () => {
|
||||
const merged = preservePublishedUserID(
|
||||
{
|
||||
accountId: 'viewer-account',
|
||||
author: { address: 'account-author.bso', shortAddress: 'account-author' },
|
||||
} as any,
|
||||
{ author: { address: 'published-reply-address', shortAddress: 'ReplyKid9' } } as any,
|
||||
);
|
||||
|
||||
expect(merged.author.address).toBe('account-author.bso');
|
||||
expect(getCommentUserID(merged)).toBe('ReplyKid9');
|
||||
});
|
||||
|
||||
it('removes an overlaid shortAddress when the published author has no user ID', () => {
|
||||
const merged = preservePublishedUserID(
|
||||
{
|
||||
accountId: 'viewer-account',
|
||||
author: { address: 'account-author.bso', shortAddress: 'account-author' },
|
||||
} as any,
|
||||
{ author: {} } as any,
|
||||
);
|
||||
|
||||
expect(merged.author.address).toBe('account-author.bso');
|
||||
expect(getCommentUserID(merged)).toBe('');
|
||||
});
|
||||
|
||||
it('mirrors the published user ID even when the overlaid comment is not an account comment', () => {
|
||||
const merged = preservePublishedUserID(
|
||||
{ author: { address: 'account-author.bso', shortAddress: 'account-author' } } as any,
|
||||
{ author: { shortAddress: 'ReplyKid9' } } as any,
|
||||
);
|
||||
|
||||
expect(getCommentUserID(merged)).toBe('ReplyKid9');
|
||||
});
|
||||
|
||||
it('returns the comment unchanged when the user ID already matches', () => {
|
||||
const comment = { author: { address: 'account-author.bso', shortAddress: 'ReplyKid9' } } as any;
|
||||
expect(preservePublishedUserID(comment, { author: { shortAddress: 'ReplyKid9' } } as any)).toBe(comment);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,30 @@
|
||||
import type { Comment } from '@bitsocial/bitsocial-react-hooks';
|
||||
import getShortAddress from '../get-short-address';
|
||||
|
||||
// The published comment's author.shortAddress is the pseudonymous per-post User ID.
|
||||
// author.address can carry the local account identity (overlaid so author edit/delete
|
||||
// controls keep working), so it must never be used for display.
|
||||
export function getCommentUserID(comment: Comment | undefined): string {
|
||||
const { address, shortAddress } = comment?.author || {};
|
||||
return (address ? getShortAddress(address) : '') || shortAddress || '';
|
||||
return comment?.author?.shortAddress || '';
|
||||
}
|
||||
|
||||
// After local account data (account comment, pending edit) is overlaid on a published
|
||||
// comment, mirror the published comment's User ID back onto the result so the account
|
||||
// identity is never displayed.
|
||||
export function preservePublishedUserID<T extends Comment | undefined>(comment: T, published: Comment | undefined): T {
|
||||
if (!comment || !published || comment === published) return comment;
|
||||
|
||||
const publishedShortAddress = published.author?.shortAddress;
|
||||
if (comment.author?.shortAddress === publishedShortAddress) return comment;
|
||||
|
||||
const author = { ...comment.author };
|
||||
if (publishedShortAddress) {
|
||||
author.shortAddress = publishedShortAddress;
|
||||
} else {
|
||||
delete author.shortAddress;
|
||||
}
|
||||
|
||||
return {
|
||||
...comment,
|
||||
author,
|
||||
} as T;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ type TestComment = {
|
||||
accountId?: string;
|
||||
author?: {
|
||||
address?: string;
|
||||
shortAddress?: string;
|
||||
community?: unknown;
|
||||
};
|
||||
cid?: string;
|
||||
@@ -214,6 +215,7 @@ vi.mock('../../../components/post-desktop/post-desktop', () => ({
|
||||
'data-testid': 'post-desktop',
|
||||
'data-approved': post?.approved === undefined ? '' : String(post.approved),
|
||||
'data-author-address': post?.author?.address || '',
|
||||
'data-author-short-address': post?.author?.shortAddress || '',
|
||||
'data-number': post?.number === undefined ? '' : String(post.number),
|
||||
'data-pending-approval': post?.pendingApproval === undefined ? '' : String(post.pendingApproval),
|
||||
'data-replies': replyPaginationOverride?.replies?.map((reply) => reply.cid).join(',') || '',
|
||||
@@ -247,6 +249,7 @@ vi.mock('../../../components/post-mobile/post-mobile', () => ({
|
||||
'data-testid': 'post-mobile',
|
||||
'data-approved': post?.approved === undefined ? '' : String(post.approved),
|
||||
'data-author-address': post?.author?.address || '',
|
||||
'data-author-short-address': post?.author?.shortAddress || '',
|
||||
'data-number': post?.number === undefined ? '' : String(post.number),
|
||||
'data-pending-approval': post?.pendingApproval === undefined ? '' : String(post.pendingApproval),
|
||||
'data-replies': replyPaginationOverride?.replies?.map((reply) => reply.cid).join(',') || '',
|
||||
@@ -390,6 +393,34 @@ describe('Post', () => {
|
||||
expect(container.querySelector('[data-testid="post-mobile"]')?.textContent).toBe('post-2:none:1');
|
||||
});
|
||||
|
||||
it('restores the published user ID when an edited comment overlays local author identity', async () => {
|
||||
testState.editedCommentsByCid = {
|
||||
'post-edited-id': {
|
||||
cid: 'post-edited-id',
|
||||
communityAddress: 'music-posting.eth',
|
||||
content: 'edited body',
|
||||
author: { address: 'account-author.bso', shortAddress: 'account-author' },
|
||||
},
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
createElement(Post, {
|
||||
post: {
|
||||
cid: 'post-edited-id',
|
||||
communityAddress: 'music-posting.eth',
|
||||
content: 'body',
|
||||
author: { address: 'published-address', shortAddress: 'PostKid9' },
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const presenter = container.querySelector('[data-testid="post-desktop"]');
|
||||
expect(presenter?.getAttribute('data-author-address')).toBe('account-author.bso');
|
||||
expect(presenter?.getAttribute('data-author-short-address')).toBe('PostKid9');
|
||||
});
|
||||
|
||||
it('keeps renderable post data when edited comments only resolve to a loading shell', async () => {
|
||||
testState.editedCommentsByCid = {
|
||||
'post-shell': {
|
||||
|
||||
+16
-6
@@ -29,6 +29,7 @@ import { getRequestedThreadTopCid, scrollThreadContainerToTop } from '../../lib/
|
||||
import { evictThreadRefreshCaches } from '../../lib/utils/thread-refresh-cache-utils';
|
||||
import { hasTransferredCommentMarker } from '../../lib/comment-transfer';
|
||||
import { REPLIES_PER_PAGE } from '../../lib/constants';
|
||||
import { preservePublishedUserID } from '../../lib/utils/comment-user-id-utils';
|
||||
import useThreadLiveUpdatesStore from '../../stores/use-thread-live-updates-store';
|
||||
import type { QueuedCommentRouteState } from '../../lib/utils/mod-queue-utils';
|
||||
import type { ReplyVirtualizationMode } from '../../lib/utils/pretext-height-estimates';
|
||||
@@ -130,10 +131,13 @@ const mergeLocalCommentAuthor = (comment: CommentWithRefresh | undefined, localC
|
||||
|
||||
const mergedAuthor = mergeDefinedFields(comment.author, localComment.author);
|
||||
|
||||
return {
|
||||
...comment,
|
||||
...(mergedAuthor ? { author: mergedAuthor } : {}),
|
||||
};
|
||||
return preservePublishedUserID(
|
||||
{
|
||||
...comment,
|
||||
...(mergedAuthor ? { author: mergedAuthor } : {}),
|
||||
},
|
||||
comment,
|
||||
);
|
||||
};
|
||||
|
||||
const mergeLocalAccountComment = (comment: CommentWithRefresh | undefined, accountComment: CommentWithRefresh | undefined): CommentWithRefresh | undefined => {
|
||||
@@ -142,7 +146,13 @@ const mergeLocalAccountComment = (comment: CommentWithRefresh | undefined, accou
|
||||
if (comment.cid && accountComment.cid && comment.cid !== accountComment.cid) return comment;
|
||||
|
||||
const mergedComment = mergeDefinedFields(comment, accountComment) ?? comment;
|
||||
return mergeLocalCommentAuthor(mergedComment, accountComment);
|
||||
// mergeDefinedFields replaces author wholesale, so re-merge author field-wise from the
|
||||
// published comment (mergeLocalCommentAuthor also preserves the published User ID)
|
||||
const commentWithMergedAuthor = mergeLocalCommentAuthor(comment, accountComment);
|
||||
return {
|
||||
...mergedComment,
|
||||
...(commentWithMergedAuthor?.author ? { author: commentWithMergedAuthor.author } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
// useComment may not return cached feed data immediately due to its updatedAt comparison logic.
|
||||
@@ -241,7 +251,7 @@ export const Post = memo(
|
||||
|
||||
// handle pending mod or author edit
|
||||
const { editedComment } = useEditedComment({ comment });
|
||||
comment = mergeCommentFallback(editedComment as CommentWithRefresh | undefined, comment as CommentWithRefresh | undefined);
|
||||
comment = preservePublishedUserID(mergeCommentFallback(editedComment as CommentWithRefresh | undefined, comment as CommentWithRefresh | undefined), post);
|
||||
const transferHandler = comment?.parentCid ? undefined : onTransfer;
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user