fix(thread): pass board identity to comments

This commit is contained in:
Tommaso Casaburi
2026-05-16 13:44:44 +07:00
parent 3545fb8845
commit 70cf337895
6 changed files with 71 additions and 18 deletions
@@ -14,6 +14,8 @@ const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise
type DirectoryEntry = { type DirectoryEntry = {
address: string; address: string;
features?: { requirePostLinkIsMedia?: boolean }; features?: { requirePostLinkIsMedia?: boolean };
name?: string;
publicKey?: string;
title?: string; title?: string;
}; };
@@ -25,7 +27,7 @@ const testState = vi.hoisted(() => ({
alertThresholdValue: 5, alertThresholdValue: 5,
commentsByCid: {} as Record<string, any>, commentsByCid: {} as Record<string, any>,
directories: [ directories: [
{ address: 'music-posting.eth', features: {}, title: '/mu/ - Music' }, { address: 'music-posting.eth', features: {}, name: 'music-posting.eth', publicKey: 'music-public-key', title: '/mu/ - Music' },
{ address: 'tech-posting.eth', features: { requirePostLinkIsMedia: true }, title: '/g/ - Technology' }, { address: 'tech-posting.eth', features: { requirePostLinkIsMedia: true }, title: '/g/ - Technology' },
] as DirectoryEntry[], ] as DirectoryEntry[],
enableInfiniteScroll: false, enableInfiniteScroll: false,
@@ -52,6 +54,7 @@ const testState = vi.hoisted(() => ({
subscribeMock: vi.fn(), subscribeMock: vi.fn(),
subscribed: false, subscribed: false,
unsubscribeMock: vi.fn(), unsubscribeMock: vi.fn(),
useCommentCalls: [] as Array<{ commentCid?: string; community?: { name?: string; publicKey?: string } }>,
viewMode: 'compact' as 'compact' | 'feed', viewMode: 'compact' as 'compact' | 'feed',
})); }));
@@ -80,7 +83,10 @@ vi.mock('react-router-dom', async () => {
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({ vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
useAccount: () => testState.account, useAccount: () => testState.account,
useAccountComment: () => testState.accountComment, useAccountComment: () => testState.accountComment,
useComment: ({ commentCid }: { commentCid?: string }) => (commentCid ? testState.commentsByCid[commentCid] : undefined), useComment: ({ commentCid, community }: { commentCid?: string; community?: { name?: string; publicKey?: string } }) => {
testState.useCommentCalls.push({ commentCid, community });
return commentCid ? testState.commentsByCid[commentCid] : undefined;
},
useSubscribe: () => ({ useSubscribe: () => ({
subscribe: testState.subscribeMock, subscribe: testState.subscribeMock,
subscribed: testState.subscribed, subscribed: testState.subscribed,
@@ -113,6 +119,8 @@ vi.mock('../../../hooks/use-post-page-number', () => ({
})); }));
vi.mock('../../../hooks/use-directories', () => ({ vi.mock('../../../hooks/use-directories', () => ({
findDirectoryByAddress: (directories: DirectoryEntry[], address?: string) =>
directories.find((entry) => address && [entry.address, entry.name, entry.publicKey].includes(address)),
useDirectories: () => testState.directories, useDirectories: () => testState.directories,
useDirectoryByAddress: (address: string | undefined) => testState.directories.find((entry) => entry.address === address), useDirectoryByAddress: (address: string | undefined) => testState.directories.find((entry) => entry.address === address),
})); }));
@@ -263,7 +271,7 @@ describe('BoardButtons', () => {
testState.alertThresholdValue = 5; testState.alertThresholdValue = 5;
testState.commentsByCid = {}; testState.commentsByCid = {};
testState.directories = [ testState.directories = [
{ address: 'music-posting.eth', features: {}, title: '/mu/ - Music' }, { address: 'music-posting.eth', features: {}, name: 'music-posting.eth', publicKey: 'music-public-key', title: '/mu/ - Music' },
{ address: 'tech-posting.eth', features: { requirePostLinkIsMedia: true }, title: '/g/ - Technology' }, { address: 'tech-posting.eth', features: { requirePostLinkIsMedia: true }, title: '/g/ - Technology' },
]; ];
testState.enableInfiniteScroll = false; testState.enableInfiniteScroll = false;
@@ -280,6 +288,7 @@ describe('BoardButtons', () => {
testState.showOPComment = false; testState.showOPComment = false;
testState.sortType = 'active'; testState.sortType = 'active';
testState.subscribed = false; testState.subscribed = false;
testState.useCommentCalls = [];
testState.viewMode = 'compact'; testState.viewMode = 'compact';
useThreadLiveUpdatesStore.getState().resetState(); useThreadLiveUpdatesStore.getState().resetState();
useHiddenCatalogThreadsStore.setState({ hiddenCommentsByCid: {}, scopeHiddenThreadsCounts: {}, shownScopeKey: null }); useHiddenCatalogThreadsStore.setState({ hiddenCommentsByCid: {}, scopeHiddenThreadsCounts: {}, shownScopeKey: null });
@@ -474,6 +483,14 @@ describe('BoardButtons', () => {
expect(container.textContent).toContain('Archived /'); expect(container.textContent).toContain('Archived /');
expect(container.textContent).toContain('Sticky /'); expect(container.textContent).toContain('Sticky /');
expect(container.textContent).toContain('Closed /'); expect(container.textContent).toContain('Closed /');
expect(testState.useCommentCalls).toEqual(
expect.arrayContaining([
expect.objectContaining({
commentCid: 'comment-1',
community: { name: 'music-posting.eth', publicKey: 'music-public-key' },
}),
]),
);
await clickButton('bottom'); await clickButton('bottom');
await clickButton('update'); await clickButton('update');
@@ -9,6 +9,7 @@ import { useAccountCommunityAddresses } from '../../hooks/use-account-community-
import { useFilteredDirectoryAddresses } from '../../hooks/use-filtered-directory-addresses'; import { useFilteredDirectoryAddresses } from '../../hooks/use-filtered-directory-addresses';
import { getBoardPath, isDirectoryBoard } from '../../lib/utils/route-utils'; import { getBoardPath, isDirectoryBoard } from '../../lib/utils/route-utils';
import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address'; import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
import { useCommunityIdentifier } from '../../hooks/use-community-identifiers';
import useSafeAccountComment from '../../hooks/use-safe-account-comment'; import useSafeAccountComment from '../../hooks/use-safe-account-comment';
import useHiddenCatalogThreads from '../../hooks/use-hidden-catalog-threads'; import useHiddenCatalogThreads from '../../hooks/use-hidden-catalog-threads';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store'; import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
@@ -643,10 +644,11 @@ export const PostPageStats = () => {
const resolvedAddress = useResolvedCommunityAddress(); const resolvedAddress = useResolvedCommunityAddress();
const accountComment = useSafeAccountComment({ commentIndex: params?.accountCommentIndex }); const accountComment = useSafeAccountComment({ commentIndex: params?.accountCommentIndex });
const communityAddress = resolvedAddress || accountComment?.communityAddress; const communityAddress = resolvedAddress || accountComment?.communityAddress;
const communityIdentifier = useCommunityIdentifier(communityAddress);
const comment = useComment({ commentCid, autoUpdate: autoUpdateEnabled }); const comment = useComment({ commentCid, autoUpdate: autoUpdateEnabled, community: communityIdentifier });
const postCid = comment?.postCid ?? commentCid; const postCid = comment?.postCid ?? commentCid;
const post = useComment({ commentCid: postCid, autoUpdate: autoUpdateEnabled }); const post = useComment({ commentCid: postCid, autoUpdate: autoUpdateEnabled, community: communityIdentifier });
const archived = isCommentArchived(post); const archived = isCommentArchived(post);
const { closed, pinned, replyCount } = post || {}; const { closed, pinned, replyCount } = post || {};
@@ -22,6 +22,7 @@ const testState = vi.hoisted(() => ({
openReplyModalEmptyMock: vi.fn(), openReplyModalEmptyMock: vi.fn(),
pageNumber: 4 as number | undefined, pageNumber: 4 as number | undefined,
post: { replyCount: 7 } as { replyCount?: number } | undefined, post: { replyCount: 7 } as { replyCount?: number } | undefined,
useCommentCalls: [] as Array<{ commentCid?: string; community?: { name?: string; publicKey?: string } }>,
})); }));
vi.mock('react-i18next', () => ({ vi.mock('react-i18next', () => ({
@@ -31,7 +32,10 @@ vi.mock('react-i18next', () => ({
})); }));
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({ vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
useComment: () => testState.post, useComment: ({ commentCid, community }: { commentCid?: string; community?: { name?: string; publicKey?: string } }) => {
testState.useCommentCalls.push({ commentCid, community });
return testState.post;
},
})); }));
vi.mock('../../boards-bar', () => ({ vi.mock('../../boards-bar', () => ({
@@ -94,6 +98,10 @@ vi.mock('../../../hooks/use-directories', () => ({
useDirectoryByAddress: () => testState.directoryEntry, useDirectoryByAddress: () => testState.directoryEntry,
})); }));
vi.mock('../../../hooks/use-community-identifiers', () => ({
useCommunityIdentifier: (address?: string) => (address ? { name: address, publicKey: `${address}-public-key` } : undefined),
}));
let container: HTMLDivElement; let container: HTMLDivElement;
let root: Root; let root: Root;
@@ -111,6 +119,7 @@ describe('footer', () => {
testState.openReplyModalEmptyMock.mockReset(); testState.openReplyModalEmptyMock.mockReset();
testState.pageNumber = 4; testState.pageNumber = 4;
testState.post = { replyCount: 7 }; testState.post = { replyCount: 7 };
testState.useCommentCalls = [];
container = document.createElement('div'); container = document.createElement('div');
document.body.appendChild(container); document.body.appendChild(container);
root = createRoot(container); root = createRoot(container);
@@ -210,6 +219,15 @@ describe('footer', () => {
); );
expect(container.textContent).toContain('Replies: 7 / Images: 2 / pagination.pageLabel: 4'); expect(container.textContent).toContain('Replies: 7 / Images: 2 / pagination.pageLabel: 4');
expect(testState.useCommentCalls).toEqual([
{
commentCid: 'post-cid',
community: {
name: 'music-posting.eth',
publicKey: 'music-posting.eth-public-key',
},
},
]);
const button = Array.from(container.querySelectorAll('button')).find((candidate) => candidate.textContent === 'post_a_reply'); const button = Array.from(container.querySelectorAll('button')).find((candidate) => candidate.textContent === 'post_a_reply');
await act(async () => { await act(async () => {
+3 -1
View File
@@ -11,6 +11,7 @@ import useThreadLiveUpdatesStore from '../../stores/use-thread-live-updates-stor
import useCountLinksInReplies from '../../hooks/use-count-links-in-replies'; import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
import { usePostPageNumber } from '../../hooks/use-post-page-number'; import { usePostPageNumber } from '../../hooks/use-post-page-number';
import { useDirectoryByAddress } from '../../hooks/use-directories'; import { useDirectoryByAddress } from '../../hooks/use-directories';
import { useCommunityIdentifier } from '../../hooks/use-community-identifiers';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
import styles from './footer.module.css'; import styles from './footer.module.css';
@@ -211,8 +212,9 @@ export const ThreadFooterMobile = ({ postCid, threadNumber, communityAddress, is
const isInAllView = isAllView(location.pathname); const isInAllView = isAllView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params); const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
const isInModView = isModView(location.pathname); const isInModView = isModView(location.pathname);
const communityIdentifier = useCommunityIdentifier(communityAddress);
const post = useComment({ commentCid: postCid, autoUpdate: autoUpdateEnabled }); const post = useComment({ commentCid: postCid, autoUpdate: autoUpdateEnabled, community: communityIdentifier });
const { replyCount } = post || {}; const { replyCount } = post || {};
const linkCount = useCountLinksInReplies(post); const linkCount = useCountLinksInReplies(post);
const directoryEntry = useDirectoryByAddress(communityAddress); const directoryEntry = useDirectoryByAddress(communityAddress);
+20 -7
View File
@@ -37,7 +37,12 @@ const testState = vi.hoisted(() => ({
cachedComments: {} as Record<string, TestComment>, cachedComments: {} as Record<string, TestComment>,
communityFieldAddress: undefined as string | undefined, communityFieldAddress: undefined as string | undefined,
commentsByCid: {} as Record<string, TestComment>, commentsByCid: {} as Record<string, TestComment>,
directories: [{ address: 'music-posting.eth', title: '/mu/ - Music' }] as Array<{ address: string; title?: string }>, directories: [{ address: 'music-posting.eth', name: 'music-posting.eth', publicKey: 'music-public-key', title: '/mu/ - Music' }] as Array<{
address: string;
name?: string;
publicKey?: string;
title?: string;
}>,
editedCommentsByCid: {} as Record<string, TestComment | undefined>, editedCommentsByCid: {} as Record<string, TestComment | undefined>,
isMobile: false, isMobile: false,
navigateMock: vi.fn(), navigateMock: vi.fn(),
@@ -53,7 +58,7 @@ const testState = vi.hoisted(() => ({
'0xmod': { role: 'admin' }, '0xmod': { role: 'admin' },
}, },
} as { roles?: Record<string, unknown> }, } as { roles?: Record<string, unknown> },
useCommentCalls: [] as Array<{ commentCid?: string; autoUpdate?: boolean }>, useCommentCalls: [] as Array<{ commentCid?: string; autoUpdate?: boolean; community?: { name?: string; publicKey?: string } }>,
})); }));
vi.mock('react-i18next', () => ({ vi.mock('react-i18next', () => ({
@@ -71,8 +76,8 @@ vi.mock('react-router-dom', async () => {
}); });
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({ vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
useComment: ({ commentCid, autoUpdate }: { commentCid?: string; autoUpdate?: boolean }) => { useComment: ({ commentCid, autoUpdate, community }: { commentCid?: string; autoUpdate?: boolean; community?: { name?: string; publicKey?: string } }) => {
testState.useCommentCalls.push({ commentCid, autoUpdate }); testState.useCommentCalls.push({ commentCid, autoUpdate, community });
return commentCid ? testState.commentsByCid[commentCid] : undefined; return commentCid ? testState.commentsByCid[commentCid] : undefined;
}, },
useEditedComment: ({ comment }: { comment?: TestComment }) => ({ useEditedComment: ({ comment }: { comment?: TestComment }) => ({
@@ -243,7 +248,7 @@ describe('Post', () => {
testState.cachedComments = {}; testState.cachedComments = {};
testState.communityFieldAddress = undefined; testState.communityFieldAddress = undefined;
testState.commentsByCid = {}; testState.commentsByCid = {};
testState.directories = [{ address: 'music-posting.eth', title: '/mu/ - Music' }]; testState.directories = [{ address: 'music-posting.eth', name: 'music-posting.eth', publicKey: 'music-public-key', title: '/mu/ - Music' }];
testState.editedCommentsByCid = {}; testState.editedCommentsByCid = {};
testState.isMobile = false; testState.isMobile = false;
testState.resolvedCommunityAddress = 'music-posting.eth'; testState.resolvedCommunityAddress = 'music-posting.eth';
@@ -719,8 +724,16 @@ describe('Post', () => {
expect(testState.useCommentCalls).toEqual( expect(testState.useCommentCalls).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ commentCid: 'reply-cid', autoUpdate: false }), expect.objectContaining({
expect.objectContaining({ commentCid: 'root-cid', autoUpdate: false }), autoUpdate: false,
commentCid: 'reply-cid',
community: { name: 'music-posting.eth', publicKey: 'music-public-key' },
}),
expect.objectContaining({
autoUpdate: false,
commentCid: 'root-cid',
community: { name: 'music-posting.eth', publicKey: 'music-public-key' },
}),
]), ]),
); );
}); });
+5 -4
View File
@@ -1,6 +1,6 @@
import { memo, useEffect, useMemo, useRef } from 'react'; import { memo, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Comment, Role, useComment, useEditedComment, useCommunity, useReplies } from '@bitsocial/bitsocial-react-hooks'; import { type Comment, type CommunityIdentifier, type Role, useComment, useEditedComment, useCommunity, useReplies } from '@bitsocial/bitsocial-react-hooks';
import useCommunitiesPagesStore from '@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages'; import useCommunitiesPagesStore from '@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages';
import { useCommunityField } from '../../hooks/use-stable-community'; import { useCommunityField } from '../../hooks/use-stable-community';
import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useLocation, useNavigate, useParams } from 'react-router-dom';
@@ -64,7 +64,7 @@ interface ReplyPaginationOverride {
// useComment may not return cached feed data immediately due to its updatedAt comparison logic. // useComment may not return cached feed data immediately due to its updatedAt comparison logic.
// This hook falls back to the communities pages store (populated by useFeed) so content // This hook falls back to the communities pages store (populated by useFeed) so content
// from the catalog appears instantly instead of going through a loading phase. // from the catalog appears instantly instead of going through a loading phase.
const useCommentWithFeedCache = (options: { commentCid: string | undefined; autoUpdate?: boolean }): CommentWithRefresh | undefined => { const useCommentWithFeedCache = (options: { commentCid: string | undefined; autoUpdate?: boolean; community?: CommunityIdentifier }): CommentWithRefresh | undefined => {
const comment = useComment(options); const comment = useComment(options);
const cachedComment = useCommunitiesPagesStore((state) => state.comments[options?.commentCid || '']); const cachedComment = useCommunitiesPagesStore((state) => state.comments[options?.commentCid || '']);
@@ -281,10 +281,11 @@ const PostPage = () => {
const finishUpdate = useThreadLiveUpdatesStore((state) => state.finishUpdate); const finishUpdate = useThreadLiveUpdatesStore((state) => state.finishUpdate);
const resetThreadLiveUpdates = useThreadLiveUpdatesStore((state) => state.resetState); const resetThreadLiveUpdates = useThreadLiveUpdatesStore((state) => state.resetState);
const resolvedCommunityAddress = useResolvedCommunityAddress(); const resolvedCommunityAddress = useResolvedCommunityAddress();
const resolvedCommunityIdentifier = useCommunityIdentifier(resolvedCommunityAddress);
const isInAllView = isAllView(pathname); const isInAllView = isAllView(pathname);
const routeState = useMemo(() => getEffectiveRouteUserState(locationState), [locationKey, pathname, locationState]); const routeState = useMemo(() => getEffectiveRouteUserState(locationState), [locationKey, pathname, locationState]);
const resolvedComment = useCommentWithFeedCache({ commentCid, autoUpdate: autoUpdateEnabled }); const resolvedComment = useCommentWithFeedCache({ commentCid, autoUpdate: autoUpdateEnabled, community: resolvedCommunityIdentifier });
const queuedComment = useMemo(() => getQueuedCommentFromRouteState(routeState, commentCid), [routeState, commentCid]); const queuedComment = useMemo(() => getQueuedCommentFromRouteState(routeState, commentCid), [routeState, commentCid]);
const comment = useMemo(() => mergeCommentFallback(resolvedComment, queuedComment), [resolvedComment, queuedComment]); const comment = useMemo(() => mergeCommentFallback(resolvedComment, queuedComment), [resolvedComment, queuedComment]);
const commentCommunityAddress = getCommentCommunityAddress(comment); const commentCommunityAddress = getCommentCommunityAddress(comment);
@@ -306,7 +307,7 @@ const PostPage = () => {
const directories = useDirectories(); const directories = useDirectories();
// if the comment is a reply, return the post comment instead, then the reply will be highlighted in the thread // if the comment is a reply, return the post comment instead, then the reply will be highlighted in the thread
const postComment = useCommentWithFeedCache({ commentCid: comment?.postCid, autoUpdate: autoUpdateEnabled }); const postComment = useCommentWithFeedCache({ commentCid: comment?.postCid, autoUpdate: autoUpdateEnabled, community: communityIdentifier });
const post = useMemo(() => (comment?.parentCid ? mergeCommentFallback(postComment, comment) : comment), [comment, postComment]); const post = useMemo(() => (comment?.parentCid ? mergeCommentFallback(postComment, comment) : comment), [comment, postComment]);
const requestedThreadTopCid = getRequestedThreadTopCid(routeState); const requestedThreadTopCid = getRequestedThreadTopCid(routeState);