mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
chore(architecture): finish refactor audit
Merge the architecture audit refactors and follow-up UX fixes: remove release notes from generated LLM context, keep author edit controls delete-only, stabilize board refresh rendering, and address review feedback for role loading, refresh holds, moderation actions, and P2P stats polling.
This commit is contained in:
@@ -64,7 +64,7 @@ const testState = vi.hoisted(() => ({
|
||||
roles: {
|
||||
'0xmod': { role: 'admin' },
|
||||
},
|
||||
} as { roles?: Record<string, unknown> },
|
||||
} as { roles?: Record<string, unknown> } | undefined,
|
||||
useCommentCalls: [] as Array<{ commentCid?: string; autoUpdate?: boolean; community?: { name?: string; publicKey?: string } }>,
|
||||
evictThreadRefreshCachesMock: vi.fn(),
|
||||
}));
|
||||
@@ -116,7 +116,7 @@ vi.mock('@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages', () =>
|
||||
vi.mock('../../../hooks/use-stable-community', () => ({
|
||||
useCommunityField: (address: string | undefined, selector: (community: typeof testState.communitySnapshot) => unknown) => {
|
||||
testState.communityFieldAddress = address;
|
||||
return selector(testState.communitySnapshot);
|
||||
return testState.communitySnapshot ? selector(testState.communitySnapshot) : undefined;
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -189,6 +189,7 @@ vi.mock('../../../components/post-desktop/post-desktop', () => ({
|
||||
'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(',') || '',
|
||||
'data-roles-present': String(roles !== undefined),
|
||||
},
|
||||
createElement('div', { 'data-thread-container-cid': post?.cid }),
|
||||
createElement('div', { 'data-post-info-cid': post?.cid }),
|
||||
@@ -217,6 +218,7 @@ vi.mock('../../../components/post-mobile/post-mobile', () => ({
|
||||
'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(',') || '',
|
||||
'data-roles-present': String(roles !== undefined),
|
||||
},
|
||||
createElement('div', { 'data-thread-container-cid': post?.cid }),
|
||||
createElement('div', { 'data-post-info-cid': post?.cid }),
|
||||
@@ -370,6 +372,44 @@ describe('Post', () => {
|
||||
expect(testState.communityFieldAddress).toBe('music-posting.eth');
|
||||
});
|
||||
|
||||
it('passes an empty role map after a community with no roles is loaded', async () => {
|
||||
testState.communitySnapshot = {};
|
||||
|
||||
await act(async () => {
|
||||
root.render(createElement(Post, { post: { cid: 'post-no-roles', communityAddress: 'music-posting.eth', content: '[b]raw[/b]' } }));
|
||||
});
|
||||
|
||||
const postDesktop = container.querySelector('[data-testid="post-desktop"]');
|
||||
expect(postDesktop?.getAttribute('data-roles-present')).toBe('true');
|
||||
expect(postDesktop?.textContent).toBe('post-no-roles:none:0');
|
||||
});
|
||||
|
||||
it('keeps roles pending for matching board routes until the community loads', async () => {
|
||||
testState.communitySnapshot = undefined;
|
||||
testState.resolvedCommunityAddress = 'music-posting.eth';
|
||||
|
||||
await act(async () => {
|
||||
root.render(createElement(Post, { post: { cid: 'post-pending-roles', communityAddress: 'music-posting.eth', content: '[color=red]raw[/color]' } }));
|
||||
});
|
||||
|
||||
const postDesktop = container.querySelector('[data-testid="post-desktop"]');
|
||||
expect(postDesktop?.getAttribute('data-roles-present')).toBe('false');
|
||||
expect(postDesktop?.textContent).toBe('post-pending-roles:none:0');
|
||||
});
|
||||
|
||||
it('uses an empty role map for posts outside a resolved board route when the community is unavailable', async () => {
|
||||
testState.communitySnapshot = undefined;
|
||||
testState.resolvedCommunityAddress = undefined;
|
||||
|
||||
await act(async () => {
|
||||
root.render(createElement(Post, { post: { cid: 'post-multiboard', communityAddress: 'other-board.eth', content: '[color=red]raw[/color]' } }));
|
||||
});
|
||||
|
||||
const postDesktop = container.querySelector('[data-testid="post-desktop"]');
|
||||
expect(postDesktop?.getAttribute('data-roles-present')).toBe('true');
|
||||
expect(postDesktop?.textContent).toBe('post-multiboard:none:0');
|
||||
});
|
||||
|
||||
it('rerenders posts when pending approval turns into an approved numbered post', async () => {
|
||||
await act(async () => {
|
||||
root.render(
|
||||
|
||||
+9
-45
@@ -1,17 +1,7 @@
|
||||
import { memo, useEffect, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
type Account,
|
||||
type Comment,
|
||||
type CommunityIdentifier,
|
||||
type Role,
|
||||
useAccount,
|
||||
useComment,
|
||||
useEditedComment,
|
||||
useCommunity,
|
||||
useReplies,
|
||||
} from '@bitsocial/bitsocial-react-hooks';
|
||||
import useCommunitiesPagesStore from '@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages';
|
||||
import { type Comment, type CommunityIdentifier, type Role, useAccount, 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';
|
||||
import { isAllView } from '../../lib/utils/view-utils';
|
||||
@@ -21,6 +11,7 @@ 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';
|
||||
@@ -35,6 +26,8 @@ import type { QueuedCommentRouteState } from '../../lib/utils/mod-queue-utils';
|
||||
import type { ReplyVirtualizationMode } from '../../lib/utils/pretext-height-estimates';
|
||||
import styles from './post.module.css';
|
||||
|
||||
const EMPTY_ROLE_MAP = {};
|
||||
|
||||
export type CommentWithRefresh = Comment & {
|
||||
approved?: boolean;
|
||||
communityAddress?: string;
|
||||
@@ -47,19 +40,6 @@ export type CommentWithRefresh = Comment & {
|
||||
removed?: boolean;
|
||||
};
|
||||
|
||||
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 getRouteUserState = (state: unknown): QueuedCommentRouteState | undefined => {
|
||||
if (!state || typeof state !== 'object') return undefined;
|
||||
if ('queuedComment' in state || 'scrollThreadContainerCid' in state) {
|
||||
@@ -144,25 +124,6 @@ const mergeLocalAccountComment = (comment: CommentWithRefresh | undefined, accou
|
||||
return mergeLocalCommentAuthor(mergedComment, accountComment);
|
||||
};
|
||||
|
||||
const restoreActiveAccountAuthor = (accountComment: CommentWithRefresh | undefined, account: Account | undefined): CommentWithRefresh | undefined => {
|
||||
if (!accountComment || accountComment.author?.address || !account?.id || accountComment.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(accountComment.author, accountAuthor),
|
||||
};
|
||||
};
|
||||
|
||||
// useComment may not return cached feed data immediately due to its updatedAt comparison logic.
|
||||
// This hook falls back to the communities pages store and then overlays a matching
|
||||
// local account comment so author controls keep working after publish navigation.
|
||||
@@ -249,7 +210,10 @@ export const Post = memo(
|
||||
}: PostProps) => {
|
||||
// Only subscribe to roles field to avoid rerenders from updatingState changes
|
||||
const communityAddress = getCommentCommunityAddress(post);
|
||||
const roles = useCommunityField(communityAddress, (community) => community?.roles);
|
||||
const routeCommunityAddress = useResolvedCommunityAddress();
|
||||
const rawRoles = useCommunityField(communityAddress, (community) => community?.roles ?? EMPTY_ROLE_MAP);
|
||||
const shouldWaitForRoles = Boolean(routeCommunityAddress && communityAddress && areSameBoardAddress(routeCommunityAddress, communityAddress));
|
||||
const roles = rawRoles ?? (shouldWaitForRoles ? undefined : EMPTY_ROLE_MAP);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
let comment = post;
|
||||
|
||||
Reference in New Issue
Block a user