fix: consume enriched account comments directly (#1178)

* fix: consume enriched account comments directly

* fix(account comments): use empty lookup patch
This commit is contained in:
Tommaso Casaburi
2026-06-24 16:23:42 +07:00
committed by GitHub
parent dff0aef8f8
commit f1022fc2d9
22 changed files with 157 additions and 269 deletions
+3 -3
View File
@@ -1,7 +1,7 @@
import { lazy, Suspense, useEffect } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { Navigate, Outlet, Route, Routes, useLocation, useParams } from 'react-router-dom';
import { useAccount, useCommunity } from '@bitsocial/bitsocial-react-hooks';
import { useAccount, useAccountComment, useCommunity } from '@bitsocial/bitsocial-react-hooks';
import { initSnow, removeSnow, shouldShowSnow } from './lib/snow';
import { isAllView, isCatalogView, isModView, isSubscriptionsView } from './lib/utils/view-utils';
import { preloadReplyModal, preloadThemeAssets } from './lib/utils/preload-utils';
@@ -16,8 +16,8 @@ import { useDirectories } from './hooks/use-directories';
import { useBrowserPureP2PAccountUpgrade } from './hooks/use-browser-pure-p2p-account-upgrade';
import { useCommunityIdentifier } from './hooks/use-community-identifiers';
import { useResolvedCommunityAddress, useResolvedDirectoryBoardPath } from './hooks/use-resolved-community-address';
import useSafeAccountComment from './hooks/use-safe-account-comment';
import useSuspendOffscreenMediaPlayback from './hooks/use-suspend-offscreen-media-playback';
import { normalizeAccountCommentIndex } from './lib/utils/account-comment-index-utils';
import { getCommentCommunityAddress } from './lib/utils/comment-utils';
import {
getBoardPath,
@@ -82,7 +82,7 @@ const BoardLayout = () => {
const directories = useDirectories();
const communityAddress = useResolvedCommunityAddress(boardIdentifier);
const { boardPath: resolvedDirectoryBoardPath, isDirectoryCandidate } = useResolvedDirectoryBoardPath(boardIdentifier);
const pendingPost = useSafeAccountComment({ commentIndex: accountCommentIndex });
const pendingPost = useAccountComment({ commentIndex: normalizeAccountCommentIndex(accountCommentIndex) });
const pendingPostCommunityAddress = getCommentCommunityAddress(pendingPost);
const { closeCreateBoardModal } = useCreateBoardModalStore();
const isOnPostRoute = isPostRoute(pathname);
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
import { useAccount, useComment, useSubscribe } from '@bitsocial/bitsocial-react-hooks';
import { useAccount, useAccountComment, useComment, useSubscribe } from '@bitsocial/bitsocial-react-hooks';
import { isAllView, isCatalogView, isModView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { usePostPageNumber } from '../../hooks/use-post-page-number';
import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
@@ -9,7 +9,7 @@ import { useFilteredDirectoryAddresses } from '../../hooks/use-filtered-director
import { getBoardPath, isDirectoryRoute } from '../../lib/utils/route-utils';
import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
import { useCommunityIdentifier } from '../../hooks/use-community-identifiers';
import useSafeAccountComment from '../../hooks/use-safe-account-comment';
import { normalizeAccountCommentIndex } from '../../lib/utils/account-comment-index-utils';
import useHiddenCatalogThreads from '../../hooks/use-hidden-catalog-threads';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
import useCatalogStyleStore from '../../stores/use-catalog-style-store';
@@ -532,7 +532,7 @@ export const MobileBoardButtons = () => {
const isInModView = isModView(location.pathname);
const isInModQueueView = isModQueueView(location.pathname);
const accountComment = useSafeAccountComment({ commentIndex: params?.accountCommentIndex });
const accountComment = useAccountComment({ commentIndex: normalizeAccountCommentIndex(params?.accountCommentIndex) });
const resolvedAddress = useResolvedCommunityAddress();
const communityAddress = resolvedAddress || accountComment?.communityAddress;
@@ -646,7 +646,7 @@ export const PostPageStats = () => {
const autoUpdateEnabled = useThreadLiveUpdatesStore((state) => state.enabled);
const commentCid = params?.commentCid as string | undefined;
const resolvedAddress = useResolvedCommunityAddress();
const accountComment = useSafeAccountComment({ commentIndex: params?.accountCommentIndex });
const accountComment = useAccountComment({ commentIndex: normalizeAccountCommentIndex(params?.accountCommentIndex) });
const communityAddress = resolvedAddress || accountComment?.communityAddress;
const communityIdentifier = useCommunityIdentifier(communityAddress);
@@ -716,7 +716,7 @@ export const CatalogSearchResultsLabel = () => {
export const DesktopBoardButtons = () => {
const params = useParams();
const location = useLocation();
const accountComment = useSafeAccountComment({ commentIndex: params?.accountCommentIndex });
const accountComment = useAccountComment({ commentIndex: normalizeAccountCommentIndex(params?.accountCommentIndex) });
const resolvedAddress = useResolvedCommunityAddress();
const communityAddress = resolvedAddress || accountComment?.communityAddress;
const isInCatalogView = isCatalogView(location.pathname, params);
+3 -3
View File
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useParams, useNavigate } from 'react-router-dom';
import { useCommunity } from '@bitsocial/bitsocial-react-hooks';
import { useAccountComment, useCommunity } from '@bitsocial/bitsocial-react-hooks';
import { accountsStore as useAccountsStore } from '../../lib/bitsocial-internals/stores';
import getShortAddress from '../../lib/get-short-address';
import { useCommunityIdentifier } from '../../hooks/use-community-identifiers';
@@ -11,7 +11,7 @@ import { isArchiveRoute, isDirectoryListRoute } from '../../lib/utils/route-util
import styles from './board-header.module.css';
import { useDirectories } from '../../hooks/use-directories';
import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
import useSafeAccountComment from '../../hooks/use-safe-account-comment';
import { normalizeAccountCommentIndex } from '../../lib/utils/account-comment-index-utils';
import useIsMobile from '../../hooks/use-is-mobile';
import useIsCommunityOffline from '../../hooks/use-is-community-offline';
import { shouldShowSnow } from '../../lib/snow';
@@ -54,7 +54,7 @@ const BoardHeader = () => {
const isInModView = isModView(location.pathname);
const isInArchiveView = isArchiveRoute(location.pathname);
const isInDirectoryListView = isDirectoryListRoute(location.pathname);
const accountComment = useSafeAccountComment({ commentIndex: params?.accountCommentIndex });
const accountComment = useAccountComment({ commentIndex: normalizeAccountCommentIndex(params?.accountCommentIndex) });
const resolvedAddress = useResolvedCommunityAddress();
const communityAddress = resolvedAddress || accountComment?.communityAddress;
+3 -3
View File
@@ -1,13 +1,14 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAccountComment } from '@bitsocial/bitsocial-react-hooks';
import getShortAddress from '../../lib/get-short-address';
import { accountsStore as useAccountsStore } from '../../lib/bitsocial-internals/stores';
import { isAllView, isCatalogView, isModView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses';
import { useDirectories, DirectoryCommunity } from '../../hooks/use-directories';
import { useBoardPath, useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
import useSafeAccountComment from '../../hooks/use-safe-account-comment';
import { normalizeAccountCommentIndex } from '../../lib/utils/account-comment-index-utils';
import { getBoardPath, extractDirectoryFromTitle } from '../../lib/utils/route-utils';
import { getCommentCommunityAddress } from '../../lib/utils/comment-utils';
import useCreateBoardModalStore from '../../stores/use-create-board-modal-store';
@@ -418,8 +419,7 @@ const BoardsBarMobile = ({ communityAddress }: { communityAddress?: string }) =>
const BoardsBar = () => {
const params = useParams();
const commentIndex = params?.accountCommentIndex ? parseInt(params.accountCommentIndex, 10) : undefined;
const accountComment = useSafeAccountComment({ commentIndex });
const accountComment = useAccountComment({ commentIndex: normalizeAccountCommentIndex(params?.accountCommentIndex) });
const resolvedCommunityAddress = useResolvedCommunityAddress();
const communityAddress = resolvedCommunityAddress || getCommentCommunityAddress(accountComment);
+2 -3
View File
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
import { Comment, useEditedComment, useReplies, useAccount } from '@bitsocial/bitsocial-react-hooks';
import { Comment, useAccount, useAccountComment, useEditedComment, useReplies } from '@bitsocial/bitsocial-react-hooks';
import getShortAddress from '../../lib/get-short-address';
import styles from '../../views/post/post.module.css';
import { CommentMediaInfo, getHasThumbnail, getMediaDimensions, getPostMediaTypeLabel, getYouTubeEmbedPostMediaFileLink } from '../../lib/utils/media-utils';
@@ -22,7 +22,6 @@ import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
import useHide from '../../hooks/use-hide';
import useStateString from '../../hooks/use-state-string';
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/comment-content';
@@ -689,7 +688,7 @@ const Reply = ({
postsByAuthorInThread,
disableDeferredLayout,
}: PostProps & { directRepliesByParentCid?: Map<string, Comment[]>; postsByAuthorInThread?: Map<string, number>; disableDeferredLayout?: boolean }) => {
const accountReply = useSafeAccountComment({
const accountReply = useAccountComment({
commentCid: reply?.cid,
commentIndex: typeof reply?.index === 'number' ? reply.index : undefined,
});
+4 -4
View File
@@ -2,7 +2,7 @@ import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
import { Comment, setAccount, useAccount, useEditedComment } from '@bitsocial/bitsocial-react-hooks';
import { Comment, setAccount, useAccount, useAccountComment, useEditedComment } from '@bitsocial/bitsocial-react-hooks';
import getShortAddress from '../../lib/get-short-address';
import { communitiesPagesStore as useCommunitiesPagesStore } from '../../lib/bitsocial-internals/stores';
import { getDisplayMediaInfoType, getLinkMediaInfo, getTwimgMediaFilePublishUrl } from '../../lib/utils/media-utils';
@@ -41,7 +41,7 @@ import { useDirectoryEntry } from '../../hooks/use-directory-entry';
import { useCommunityField } from '../../hooks/use-stable-community';
import useIsMobile from '../../hooks/use-is-mobile';
import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
import useSafeAccountComment from '../../hooks/use-safe-account-comment';
import { normalizeAccountCommentIndex } from '../../lib/utils/account-comment-index-utils';
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
import { useYouTubeThumbnailLinkConversion } from '../../hooks/use-youtube-thumbnail-link-conversion';
import usePublishSubmissionGuard from '../../hooks/use-publish-submission-guard';
@@ -544,7 +544,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
const [url, setUrl] = useState('');
const author = account?.author || {};
const { displayName } = author || {};
const accountComment = useSafeAccountComment({ commentIndex: params?.accountCommentIndex });
const accountComment = useAccountComment({ commentIndex: normalizeAccountCommentIndex(params?.accountCommentIndex) });
const resolvedAddress = useResolvedCommunityAddress();
const communityAddress = resolvedAddress || accountComment?.communityAddress;
const { setPublishPostOptions, postIndex, publishPost, publishPostError, publishPostOptions, resetPublishPostOptions } = usePublishPost({
@@ -1028,7 +1028,7 @@ const PostForm = () => {
const [showForm, setShowForm] = useState(false);
const accountComment = useSafeAccountComment({ commentIndex: params?.accountCommentIndex });
const accountComment = useAccountComment({ commentIndex: normalizeAccountCommentIndex(params?.accountCommentIndex) });
const resolvedAddress = useResolvedCommunityAddress();
const communityAddress = resolvedAddress || accountComment?.communityAddress;
+2 -3
View File
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
import { Comment, useEditedComment, useReplies, useAccount } from '@bitsocial/bitsocial-react-hooks';
import { Comment, useAccount, useAccountComment, useEditedComment, useReplies } from '@bitsocial/bitsocial-react-hooks';
import getShortAddress from '../../lib/get-short-address';
import styles from '../../views/post/post.module.css';
import { shouldShowSnow } from '../../lib/snow';
@@ -21,7 +21,6 @@ import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
import useHide from '../../hooks/use-hide';
import useStateString from '../../hooks/use-state-string';
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/comment-content';
@@ -464,7 +463,7 @@ const Reply = ({
postsByAuthorInThread,
disableDeferredLayout,
}: PostProps & { directRepliesByParentCid?: Map<string, Comment[]>; postsByAuthorInThread?: Map<string, number>; disableDeferredLayout?: boolean }) => {
const accountReply = useSafeAccountComment({
const accountReply = useAccountComment({
commentCid: reply?.cid,
commentIndex: typeof reply?.index === 'number' ? reply.index : undefined,
});
@@ -1,10 +1,9 @@
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { Comment, useAccount } from '@bitsocial/bitsocial-react-hooks';
import { Comment, useAccount, useAccountComment } from '@bitsocial/bitsocial-react-hooks';
import { useFloating, offset, shift, size, autoUpdate, Placement } from '@floating-ui/react';
import { useDirectories } from '../../hooks/use-directories';
import useSafeAccountComment from '../../hooks/use-safe-account-comment';
import { getBoardPath } from '../../lib/utils/route-utils';
import { formatQuoteNumber, getQuoteTargetAvailability, shouldShowFloatingQuotePreview } from '../../lib/utils/quote-link-utils';
import { findPreferredScrollTarget, getThreadTopNavigationState, scrollThreadContainerToTop } from '../../lib/utils/thread-scroll-utils';
@@ -90,7 +89,7 @@ const scrollToReplyOnPage = (cid: string) => {
const useIsOwnQuotelink = (quotelinkReply?: Comment) => {
const account = useAccount();
const ownQuotelink = useSafeAccountComment({ commentCid: quotelinkReply?.cid });
const ownQuotelink = useAccountComment({ commentCid: quotelinkReply?.cid });
const quotedAuthorAddress = quotelinkReply?.author?.address;
const accountAuthorAddress = account?.author?.address;
@@ -1,102 +0,0 @@
import * as React from 'react';
import { createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import useSafeAccountComment from '../use-safe-account-comment';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
const testState = vi.hoisted(() => ({
account: undefined as { id?: string; name?: string } | undefined,
accountCommentResult: { cid: 'account-comment' } as { cid?: string },
calls: [] as Array<{ accountName?: string; commentCid?: string; commentIndex?: number }>,
options: undefined as { accountName?: string; commentCid?: string; commentIndex?: number | string } | undefined,
}));
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
useAccount: (options?: { accountName?: string }) => {
if (!options?.accountName) {
return testState.account;
}
return testState.account?.name === options.accountName ? testState.account : undefined;
},
useAccountComment: (options?: { accountName?: string; commentCid?: string; commentIndex?: number }) => {
testState.calls.push(options || {});
return testState.accountCommentResult;
},
}));
let container: HTMLDivElement;
let latestValue: ReturnType<typeof useSafeAccountComment>;
let root: Root;
const HookHarness = () => {
latestValue = useSafeAccountComment(testState.options);
return null;
};
const renderHook = () => {
act(() => {
root.render(createElement(HookHarness));
});
};
describe('useSafeAccountComment', () => {
beforeEach(() => {
testState.account = undefined;
testState.accountCommentResult = { cid: 'account-comment' };
testState.calls = [];
testState.options = undefined;
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
it('uses a sentinel lookup when there is no active account and no usable lookup input', () => {
renderHook();
expect(testState.calls).toEqual([{ commentIndex: -1 }]);
expect(latestValue?.cid).toBe('account-comment');
});
it('normalizes numeric comment indices before delegating to useAccountComment', () => {
testState.options = { commentIndex: '7' };
renderHook();
expect(testState.calls).toEqual([{ commentIndex: 7 }]);
});
it('falls back to the sentinel lookup for malformed string indices', () => {
testState.options = { commentIndex: '7abc' };
renderHook();
expect(testState.calls).toEqual([{ commentIndex: -1 }]);
});
it('falls back to the sentinel lookup when cid lookup is requested before an account exists', () => {
testState.options = { commentCid: 'reply-cid' };
renderHook();
expect(testState.calls).toEqual([{ commentIndex: -1 }]);
});
it('passes comment cid lookups through once the active account exists', () => {
testState.account = { id: 'account-1', name: 'Account 1' };
testState.options = { commentCid: 'reply-cid' };
renderHook();
expect(testState.calls).toEqual([{ commentCid: 'reply-cid' }]);
});
});
-47
View File
@@ -1,47 +0,0 @@
import { useMemo } from 'react';
import { useAccount, useAccountComment } from '@bitsocial/bitsocial-react-hooks';
type SafeAccountCommentOptions = {
accountName?: string;
commentCid?: string;
commentIndex?: number | string;
};
const EMPTY_ACCOUNT_COMMENT_LOOKUP = Object.freeze({ commentIndex: -1 as const });
const normalizeCommentIndex = (commentIndex: SafeAccountCommentOptions['commentIndex']) => {
if (commentIndex === undefined || commentIndex === null || commentIndex === '') {
return undefined;
}
const normalizedCommentIndex = Number(commentIndex);
return Number.isInteger(normalizedCommentIndex) && normalizedCommentIndex >= 0 ? normalizedCommentIndex : undefined;
};
const useSafeAccountComment = (options?: SafeAccountCommentOptions) => {
const account = useAccount(options?.accountName ? { accountName: options.accountName } : undefined);
const normalizedCommentIndex = normalizeCommentIndex(options?.commentIndex);
const safeOptions = useMemo(() => {
if (typeof normalizedCommentIndex === 'number') {
return {
...(options?.accountName ? { accountName: options.accountName } : {}),
commentIndex: normalizedCommentIndex,
};
}
if (options?.commentCid && account?.id) {
return {
...(options?.accountName ? { accountName: options.accountName } : {}),
commentCid: options.commentCid,
};
}
return EMPTY_ACCOUNT_COMMENT_LOOKUP;
}, [account?.id, normalizedCommentIndex, options?.accountName, options?.commentCid]);
return useAccountComment(safeOptions);
};
export default useSafeAccountComment;
+3 -3
View File
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { useAccountComment } from '@bitsocial/bitsocial-react-hooks';
import { isAllView, isModView, isSubscriptionsView } from '../lib/utils/view-utils';
import useThemeStore from '../stores/use-theme-store';
import { useDirectories } from './use-directories';
@@ -7,8 +8,8 @@ import { useResolvedCommunityAddress } from './use-resolved-community-address';
import useSpecialThemeStore from '../stores/use-special-theme-store';
import { getActiveSpecialTheme, getSpecialThemeClass } from '../lib/utils/time-utils';
import { isSfwBoard, updateFavicon } from '../lib/update-favicon';
import useSafeAccountComment from './use-safe-account-comment';
import { getCommentCommunityAddress } from '../lib/utils/comment-utils';
import { normalizeAccountCommentIndex } from '../lib/utils/account-comment-index-utils';
const themeClasses = ['yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon', 'spooky'];
@@ -23,8 +24,7 @@ const useTheme = (): [string, (theme: string) => void] => {
const location = useLocation();
const params = useParams<{ boardIdentifier?: string }>();
const pendingPostParams = useParams<{ accountCommentIndex?: string }>();
const pendingPostCommentIndex = pendingPostParams?.accountCommentIndex ? parseInt(pendingPostParams.accountCommentIndex, 10) : undefined;
const pendingPost = useSafeAccountComment({ commentIndex: pendingPostCommentIndex });
const pendingPost = useAccountComment({ commentIndex: normalizeAccountCommentIndex(pendingPostParams?.accountCommentIndex) });
const pendingPostCommunityAddress = getCommentCommunityAddress(pendingPost);
const { isEnabled, setIsEnabled } = useSpecialThemeStore();
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { normalizeAccountCommentIndex } from '../account-comment-index-utils';
describe('normalizeAccountCommentIndex', () => {
it('accepts non-negative integer numbers and strings', () => {
expect(normalizeAccountCommentIndex(0)).toBe(0);
expect(normalizeAccountCommentIndex(7)).toBe(7);
expect(normalizeAccountCommentIndex('0')).toBe(0);
expect(normalizeAccountCommentIndex('7')).toBe(7);
});
it('rejects missing, negative, fractional, and malformed values', () => {
expect(normalizeAccountCommentIndex(undefined)).toBeUndefined();
expect(normalizeAccountCommentIndex(null)).toBeUndefined();
expect(normalizeAccountCommentIndex('')).toBeUndefined();
expect(normalizeAccountCommentIndex(-1)).toBeUndefined();
expect(normalizeAccountCommentIndex('1.5')).toBeUndefined();
expect(normalizeAccountCommentIndex('1abc')).toBeUndefined();
});
});
@@ -1,51 +0,0 @@
import type { Account } from '@bitsocial/bitsocial-react-hooks';
type CommentAuthor = {
address?: string;
avatar?: unknown;
displayName?: string;
flair?: unknown;
shortAddress?: string;
[key: string]: unknown;
};
type AccountCommentWithAuthor = {
accountId?: string;
author?: CommentAuthor;
};
export const mergeDefinedFields = <T extends object>(base: T | undefined, override: T | undefined): T | undefined => {
if (!override) return base;
const merged = { ...base } as Record<string, unknown>;
for (const [key, value] of Object.entries(override)) {
if (value !== undefined) {
merged[key] = value;
}
}
return merged as T;
};
export function restoreActiveAccountAuthor<T extends object>(accountComment: T, account: Account | undefined): T;
export function restoreActiveAccountAuthor<T extends object>(accountComment: T | undefined, account: Account | undefined): T | undefined;
export function restoreActiveAccountAuthor<T extends object>(accountComment: T | undefined, account: Account | undefined): T | undefined {
const comment = accountComment as AccountCommentWithAuthor | undefined;
if (!comment || comment.author?.address || !account?.id || comment.accountId !== account.id || !account.author?.address) {
return accountComment;
}
const accountAuthor = {
address: account.author.address,
shortAddress: account.author.shortAddress,
displayName: account.author.displayName,
avatar: account.author.avatar,
flair: account.author.flair,
};
return {
...accountComment,
author: mergeDefinedFields(comment.author, accountAuthor),
} as T;
}
@@ -0,0 +1,8 @@
export const normalizeAccountCommentIndex = (commentIndex: number | string | null | undefined): number | undefined => {
if (commentIndex === undefined || commentIndex === null || commentIndex === '') {
return undefined;
}
const normalizedCommentIndex = Number(commentIndex);
return Number.isInteger(normalizedCommentIndex) && normalizedCommentIndex >= 0 ? normalizedCommentIndex : undefined;
};
+19 -1
View File
@@ -142,6 +142,24 @@ const getScopedAccountComments = (options?: { commentIndices?: number[]; communi
return scopedComments;
};
const enrichAccountCommentAuthor = (comment: TestComment): TestComment => {
if (comment.author?.address || !comment.accountId || comment.accountId !== testState.account.id || !testState.account.author?.address) {
return comment;
}
const accountAuthor = testState.account.author;
return {
...comment,
author: {
...accountAuthor,
...comment.author,
address: accountAuthor.address,
...(accountAuthor.shortAddress ? { shortAddress: accountAuthor.shortAddress } : {}),
},
};
};
const getScopedFeed = (options?: { filter?: { filter: (comment: TestComment) => boolean }; newerThan?: number; postsPerPage?: number }) => {
let scopedFeed = [...testState.feed];
@@ -165,7 +183,7 @@ vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
useAccount: () => testState.account,
useAccountComments: (options?: { commentIndices?: number[]; communityAddress?: string; newerThan?: number; sortType?: 'new' | 'old' }) => {
testState.accountCommentsCalls.push(options);
return { accountComments: getScopedAccountComments(options) };
return { accountComments: getScopedAccountComments(options).map(enrichAccountCommentAuthor) };
},
useFeed: (options?: {
communities?: unknown[];
+4 -9
View File
@@ -27,7 +27,6 @@ import { getPageSlice } from '../../lib/utils/board-feed-pagination';
import { getPageFromFeedPath, isDirectoryBoard, normalizeMultiboardFeedPath, stripPageFromFeedPath } from '../../lib/utils/route-utils';
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
import { getCommentCommunityAddress } from '../../lib/utils/comment-utils';
import { restoreActiveAccountAuthor } from '../../lib/utils/account-comment-author-utils';
import { getNonokoPendingAccountCommentIndex } from '../../lib/utils/post-options-utils';
import { getRawBoardThreadState } from '../../lib/utils/raw-board-thread-state';
import { getSearchWithTimeFilter, getTimeFilterSuggestion, type TimeFilterSuggestion } from '../../lib/utils/time-filter-utils';
@@ -482,15 +481,11 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
[recentAccountComments, communityAddress, feedCids, nowSeconds],
);
const localAccountComments = useMemo(() => {
const comments = (() => {
if (!nonokoPendingAccountComment) return filteredComments;
if (!nonokoPendingAccountComment.cid) return [nonokoPendingAccountComment, ...filteredComments];
if (!nonokoPendingAccountComment) return filteredComments;
if (!nonokoPendingAccountComment.cid) return [nonokoPendingAccountComment, ...filteredComments];
return [nonokoPendingAccountComment, ...filteredComments.filter((comment) => comment.cid !== nonokoPendingAccountComment.cid)];
})();
return comments.map((comment) => restoreActiveAccountAuthor(comment, account));
}, [nonokoPendingAccountComment, filteredComments, account]);
return [nonokoPendingAccountComment, ...filteredComments.filter((comment) => comment.cid !== nonokoPendingAccountComment.cid)];
}, [nonokoPendingAccountComment, filteredComments]);
const sortedFeed = useMemo(() => sortBoardActiveFeed(feed), [feed]);
const canShowRecentLocalAccountComments = !isSingleCommunityBoard || sortedFeed.length > 0 || isRawBoardThreadStateFullyLoaded;
@@ -15,6 +15,7 @@ type TestComment = {
const testState = vi.hoisted(() => ({
accountCommentIndex: undefined as string | undefined,
accountCommentLookupOptions: undefined as { commentIndex?: number } | undefined,
accountComments: [] as TestComment[],
challengeCount: 0,
directories: [] as Array<{ address: string; title?: string }>,
@@ -41,7 +42,10 @@ vi.mock('react-router-dom', async () => {
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
useAccount: () => undefined,
useAccountComment: () => testState.post,
useAccountComment: (options?: { commentIndex?: number }) => {
testState.accountCommentLookupOptions = options;
return testState.post;
},
useAccountComments: () => ({
accountComments: testState.accountComments,
}),
@@ -60,10 +64,11 @@ vi.mock('../../../stores/use-challenges-store', () => ({
}));
vi.mock('../../../stores/use-failed-post-retry-store', () => ({
default: (selector: (state: { retryingAccountCommentIndex: number | null }) => unknown) => selector({ retryingAccountCommentIndex: testState.retryingAccountCommentIndex }),
default: (selector: (state: { retryingAccountCommentIndex: number | null }) => unknown) =>
selector({ retryingAccountCommentIndex: testState.retryingAccountCommentIndex }),
}));
vi.mock('../../post', () => ({
vi.mock('../../post/post', () => ({
Post: ({ post }: { post?: TestComment }) => createElement('div', { 'data-testid': 'post-view' }, post?.cid ?? 'no-post'),
}));
@@ -91,6 +96,7 @@ describe('PendingPost', () => {
beforeEach(() => {
vi.clearAllMocks();
testState.accountCommentIndex = undefined;
testState.accountCommentLookupOptions = undefined;
testState.accountComments = [];
testState.challengeCount = 0;
testState.directories = [];
@@ -127,6 +133,16 @@ describe('PendingPost', () => {
expect(testState.navigateMock).not.toHaveBeenCalledWith('/not-found', { replace: true });
});
it('passes normalized numeric-string pending indices to the account comment lookup', async () => {
testState.accountCommentIndex = '01';
testState.accountComments = [{}, {}];
await renderPendingPost();
expect(testState.accountCommentLookupOptions).toEqual({ commentIndex: 1 });
expect(testState.navigateMock).not.toHaveBeenCalledWith('/not-found', { replace: true });
});
it('redirects invalid pending indices to not found', async () => {
testState.accountCommentIndex = '-1';
testState.accountComments = [{}, {}];
+7 -11
View File
@@ -1,13 +1,13 @@
import { useEffect, useRef } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { useAccountComments } from '@bitsocial/bitsocial-react-hooks';
import { useAccountComment, useAccountComments } from '@bitsocial/bitsocial-react-hooks';
import { useDirectories } from '../../hooks/use-directories';
import useSafeAccountComment from '../../hooks/use-safe-account-comment';
import { normalizeAccountCommentIndex } from '../../lib/utils/account-comment-index-utils';
import { getCommentCommunityAddress } from '../../lib/utils/comment-utils';
import { getBoardPath } from '../../lib/utils/route-utils';
import useChallengesStore from '../../stores/use-challenges-store';
import useFailedPostRetryStore from '../../stores/use-failed-post-retry-store';
import { Post } from '../post';
import { Post } from '../post/post';
type PendingAccountComment = {
index?: number;
@@ -46,9 +46,9 @@ const PendingPost = () => {
const { accountComments } = useAccountComments();
const { accountCommentIndex } = useParams<{ accountCommentIndex?: string }>();
const location = useLocation();
const normalizedAccountCommentIndex = accountCommentIndex === undefined ? undefined : Number(accountCommentIndex);
const hasNormalizedAccountCommentIndex = normalizedAccountCommentIndex !== undefined && !Number.isNaN(normalizedAccountCommentIndex);
const post = useSafeAccountComment({ commentIndex: accountCommentIndex });
const normalizedAccountCommentIndex = normalizeAccountCommentIndex(accountCommentIndex);
const hasNormalizedAccountCommentIndex = normalizedAccountCommentIndex !== undefined;
const post = useAccountComment({ commentIndex: normalizedAccountCommentIndex });
const postCommunityAddress = getCommentCommunityAddress(post);
const hasAddressablePost = Boolean(post?.cid || postCommunityAddress);
const navigate = useNavigate();
@@ -70,11 +70,7 @@ const PendingPost = () => {
}, [normalizedAccountCommentIndex, pendingBoardPath]);
const isValidAccountCommentIndex =
!accountCommentIndex ||
(hasNormalizedAccountCommentIndex &&
normalizedAccountCommentIndex >= 0 &&
Number.isInteger(normalizedAccountCommentIndex) &&
hasPendingAccountCommentIndex(accountComments, normalizedAccountCommentIndex));
!accountCommentIndex || (hasNormalizedAccountCommentIndex && hasPendingAccountCommentIndex(accountComments, normalizedAccountCommentIndex));
useEffect(() => {
// A retry deletes this pending row before republishing, briefly invalidating the index. Stay put;
+22 -2
View File
@@ -69,6 +69,26 @@ const testState = vi.hoisted(() => ({
evictThreadRefreshCachesMock: vi.fn(),
}));
const activeAccount = {
author: { address: 'account-author' },
id: 'active-account',
};
const enrichAccountCommentAuthor = (comment: TestComment | undefined): TestComment | undefined => {
if (!comment || comment.author?.address || comment.accountId !== activeAccount.id) {
return comment;
}
return {
...comment,
author: {
...activeAccount.author,
...comment.author,
address: activeAccount.author.address,
},
};
};
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
@@ -84,8 +104,8 @@ vi.mock('react-router-dom', async () => {
});
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
useAccount: () => ({ id: 'active-account', author: { address: 'account-author' } }),
useAccountComment: ({ commentCid }: { commentCid?: string }) => (commentCid ? testState.accountCommentsByCid[commentCid] : undefined),
useAccount: () => activeAccount,
useAccountComment: ({ commentCid }: { commentCid?: string }) => (commentCid ? enrichAccountCommentAuthor(testState.accountCommentsByCid[commentCid]) : undefined),
useComment: ({ commentCid, autoUpdate, community }: { commentCid?: string; autoUpdate?: boolean; community?: { name?: string; publicKey?: string } }) => {
testState.useCommentCalls.push({ commentCid, autoUpdate, community });
return commentCid ? testState.commentsByCid[commentCid] : undefined;
+25 -7
View File
@@ -1,6 +1,15 @@
import { memo, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { type Comment, type CommunityIdentifier, type Role, useAccount, useComment, useEditedComment, useCommunity, useReplies } from '@bitsocial/bitsocial-react-hooks';
import {
type Comment,
type CommunityIdentifier,
type Role,
useAccountComment,
useComment,
useEditedComment,
useCommunity,
useReplies,
} from '@bitsocial/bitsocial-react-hooks';
import { communitiesPagesStore as useCommunitiesPagesStore } from '../../lib/bitsocial-internals/stores';
import { useCommunityField } from '../../hooks/use-stable-community';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
@@ -11,7 +20,6 @@ import { useCommunityIdentifier } from '../../hooks/use-community-identifiers';
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
import { areSameBoardAddress, isDirectoryBoard } from '../../lib/utils/route-utils';
import { getCommentCommunityAddress } from '../../lib/utils/comment-utils';
import { mergeDefinedFields, restoreActiveAccountAuthor } from '../../lib/utils/account-comment-author-utils';
import useIsMobile from '../../hooks/use-is-mobile';
import ErrorDisplay from '../../components/error-display/error-display';
import { PageFooterDesktop, ThreadFooterFirstRow, ThreadFooterStyleRow, ThreadFooterMobile } from '../../components/footer/footer';
@@ -21,7 +29,6 @@ import { getRequestedThreadTopCid, scrollThreadContainerToTop } from '../../lib/
import { evictThreadRefreshCaches } from '../../lib/utils/thread-refresh-cache-utils';
import { REPLIES_PER_PAGE } from '../../lib/constants';
import useThreadLiveUpdatesStore from '../../stores/use-thread-live-updates-store';
import useSafeAccountComment from '../../hooks/use-safe-account-comment';
import type { QueuedCommentRouteState } from '../../lib/utils/mod-queue-utils';
import type { ReplyVirtualizationMode } from '../../lib/utils/pretext-height-estimates';
import styles from './post.module.css';
@@ -102,6 +109,19 @@ const mergeCommentFallback = (comment: CommentWithRefresh | undefined, fallback:
};
};
const mergeDefinedFields = <T extends object>(base: T | undefined, override: T | undefined): T | undefined => {
if (!override) return base;
const merged = { ...base } as Record<string, unknown>;
for (const [key, value] of Object.entries(override)) {
if (value !== undefined) {
merged[key] = value;
}
}
return merged as T;
};
const mergeLocalCommentAuthor = (comment: CommentWithRefresh | undefined, localComment: CommentWithRefresh | undefined): CommentWithRefresh | undefined => {
if (!localComment?.author) return comment;
if (!comment) return localComment;
@@ -130,9 +150,7 @@ const mergeLocalAccountComment = (comment: CommentWithRefresh | undefined, accou
const useCommentWithFeedCache = (options: { commentCid: string | undefined; autoUpdate?: boolean; community?: CommunityIdentifier }): CommentWithRefresh | undefined => {
const comment = useComment(options);
const cachedComment = useCommunitiesPagesStore((state) => state.comments[options?.commentCid || '']);
const account = useAccount();
const accountComment = useSafeAccountComment({ commentCid: options.commentCid }) as CommentWithRefresh | undefined;
const accountCommentWithAuthor = useMemo(() => restoreActiveAccountAuthor(accountComment, account), [accountComment, account]);
const accountComment = useAccountComment({ commentCid: options.commentCid }) as CommentWithRefresh | undefined;
const commentWithFeedCache = useMemo(() => {
if (!cachedComment || comment?.timestamp) return comment;
@@ -145,7 +163,7 @@ const useCommentWithFeedCache = (options: { commentCid: string | undefined; auto
} as CommentWithRefresh;
}, [comment, cachedComment]);
return useMemo(() => mergeLocalAccountComment(commentWithFeedCache, accountCommentWithAuthor), [commentWithFeedCache, accountCommentWithAuthor]);
return useMemo(() => mergeLocalAccountComment(commentWithFeedCache, accountComment), [commentWithFeedCache, accountComment]);
};
const mergeRepliesWithQueuedReply = (replies: Comment[], queuedReply: CommentWithRefresh | undefined): Comment[] => {