mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(accounts): adapt 5chan to compact account history hooks (#1118)
* chore(cursor): use composer-2 for subagents * fix(accounts): adapt 5chan to compact account history hooks * fix(accounts): address 5chan account-history review findings
This commit is contained in:
@@ -7,15 +7,38 @@ import useFreshReplies from '../use-fresh-replies';
|
||||
(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>;
|
||||
|
||||
type TestComment = {
|
||||
cid?: string;
|
||||
content?: string;
|
||||
index?: number;
|
||||
number?: number;
|
||||
subplebbitAddress?: string;
|
||||
};
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
accountComments: [] as Array<Record<string, unknown>>,
|
||||
replies: [] as Array<Record<string, unknown>>,
|
||||
accountComments: [] as TestComment[],
|
||||
accountCommentsCalls: [] as Array<{ commentIndices?: number[] } | undefined>,
|
||||
replies: [] as TestComment[],
|
||||
}));
|
||||
|
||||
vi.mock('@bitsocialnet/bitsocial-react-hooks', () => ({
|
||||
useAccountComments: () => ({
|
||||
accountComments: testState.accountComments,
|
||||
}),
|
||||
useAccountComments: (options?: { commentIndices?: number[] }) => {
|
||||
testState.accountCommentsCalls.push(options);
|
||||
|
||||
if (!options?.commentIndices?.length) {
|
||||
return {
|
||||
accountComments: testState.accountComments,
|
||||
};
|
||||
}
|
||||
|
||||
const normalizedCommentIndices = options.commentIndices.filter((commentIndex) => Number.isInteger(commentIndex) && commentIndex >= 0);
|
||||
|
||||
return {
|
||||
accountComments: normalizedCommentIndices
|
||||
.map((commentIndex) => testState.accountComments.find((accountComment) => accountComment.index === commentIndex))
|
||||
.filter(Boolean),
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
let container: HTMLDivElement;
|
||||
@@ -36,6 +59,7 @@ const renderHook = () => {
|
||||
describe('useFreshReplies', () => {
|
||||
beforeEach(() => {
|
||||
testState.accountComments = [];
|
||||
testState.accountCommentsCalls = [];
|
||||
testState.replies = [];
|
||||
|
||||
container = document.createElement('div');
|
||||
@@ -78,6 +102,7 @@ describe('useFreshReplies', () => {
|
||||
expect(latestValue[0]).toBe(testState.accountComments[0] as never);
|
||||
expect(latestValue[0]?.number).toBe(27);
|
||||
expect(latestValue[1]).toBe(testState.replies[1] as never);
|
||||
expect(testState.accountCommentsCalls).toContainEqual({ commentIndices: [3] });
|
||||
|
||||
testState.accountComments = [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
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('@bitsocialnet/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' }]);
|
||||
});
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useAccountComments, type Community } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
import { getCommentCommunityAddress } from '../lib/utils/comment-utils';
|
||||
|
||||
const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolean, community: Community) => {
|
||||
const { address } = community || {};
|
||||
|
||||
const { accountComments } = useAccountComments();
|
||||
|
||||
const feedWithFakePostsOnTop = useMemo(() => {
|
||||
if (!isFeedLoaded) {
|
||||
return []; // prevent temporary/mock posts from appearing while the actual feed is loading
|
||||
}
|
||||
|
||||
const _feed = [...feed];
|
||||
|
||||
// show account comments instantly in the feed once published (cid defined), instead of waiting for the feed to update
|
||||
const filteredComments = accountComments.filter((comment) => {
|
||||
const { cid, deleted, postCid, removed, state, timestamp } = comment || {};
|
||||
const communityAddress = getCommentCommunityAddress(comment);
|
||||
|
||||
return (
|
||||
!deleted &&
|
||||
!removed &&
|
||||
timestamp > Date.now() - 60 * 60 * 1000 &&
|
||||
state === 'succeeded' &&
|
||||
cid &&
|
||||
cid === postCid &&
|
||||
communityAddress === address &&
|
||||
!_feed.some((feedItem) => feedItem.cid === cid)
|
||||
);
|
||||
});
|
||||
|
||||
// show newest account comment at the top of the feed but after pinned posts
|
||||
const lastPinnedIndex = _feed.map((post) => post.pinned).lastIndexOf(true);
|
||||
if (filteredComments.length > 0) {
|
||||
_feed.splice(
|
||||
lastPinnedIndex + 1,
|
||||
0,
|
||||
...filteredComments.map((comment) => ({
|
||||
...comment,
|
||||
isAccountComment: true,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
return _feed;
|
||||
}, [accountComments, feed, address, isFeedLoaded]);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
const rows = [];
|
||||
for (let i = 0; i < feedWithFakePostsOnTop.length; i += columnCount) {
|
||||
rows.push(feedWithFakePostsOnTop.slice(i, i + columnCount));
|
||||
}
|
||||
return rows;
|
||||
}, [feedWithFakePostsOnTop, columnCount]);
|
||||
|
||||
return rows;
|
||||
};
|
||||
|
||||
export default useCatalogFeedRows;
|
||||
@@ -1,8 +1,16 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Comment, useAccountComments } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
|
||||
// Keep the hook on its indexed fast path when there are no reply indices to resolve.
|
||||
const EMPTY_ACCOUNT_COMMENT_LOOKUP = { commentIndices: [-1] };
|
||||
|
||||
const useFreshReplies = (replies: Comment[] = []) => {
|
||||
const { accountComments } = useAccountComments();
|
||||
const replyIndices = useMemo(
|
||||
() => Array.from(new Set(replies.map((reply) => reply?.index).filter((replyIndex): replyIndex is number => typeof replyIndex === 'number'))),
|
||||
[replies],
|
||||
);
|
||||
const accountCommentLookupOptions = useMemo(() => (replyIndices.length > 0 ? { commentIndices: replyIndices } : EMPTY_ACCOUNT_COMMENT_LOOKUP), [replyIndices]);
|
||||
const { accountComments } = useAccountComments(accountCommentLookupOptions);
|
||||
|
||||
return useMemo(() => {
|
||||
if (!replies.length || !accountComments?.length) {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useAccount, useAccountComment } from '@bitsocialnet/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;
|
||||
@@ -4,10 +4,10 @@ import { isAllView, isModView, isSubscriptionsView } from '../lib/utils/view-uti
|
||||
import useThemeStore from '../stores/use-theme-store';
|
||||
import { useDirectories } from './use-directories';
|
||||
import { useResolvedCommunityAddress } from './use-resolved-community-address';
|
||||
import { useAccountComment } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
import useSpecialThemeStore from '../stores/use-special-theme-store';
|
||||
import { isChristmas } from '../lib/utils/time-utils';
|
||||
import { isSfwBoard, updateFavicon } from '../lib/update-favicon';
|
||||
import useSafeAccountComment from './use-safe-account-comment';
|
||||
|
||||
const themeClasses = ['yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon'];
|
||||
|
||||
@@ -23,7 +23,7 @@ const useTheme = (): [string, (theme: string) => void] => {
|
||||
const params = useParams<{ boardIdentifier?: string; subplebbitAddress?: string }>();
|
||||
const pendingPostParams = useParams<{ accountCommentIndex?: string }>();
|
||||
const pendingPostCommentIndex = pendingPostParams?.accountCommentIndex ? parseInt(pendingPostParams.accountCommentIndex, 10) : undefined;
|
||||
const pendingPost = useAccountComment({ commentIndex: pendingPostCommentIndex });
|
||||
const pendingPost = useSafeAccountComment({ commentIndex: pendingPostCommentIndex });
|
||||
const pendingPostCommunityAddress =
|
||||
(pendingPost as { communityAddress?: string }).communityAddress ||
|
||||
// compatibility fallback for legacy inbound/persisted comment payloads
|
||||
|
||||
Reference in New Issue
Block a user