fix(codebase audit): preserve cleanup without regressions

Fix codebase audit regressions while preserving UI/UX behavior and adding review-driven hardening.
This commit is contained in:
Tommaso Casaburi
2026-04-24 15:48:07 +07:00
committed by GitHub
parent 5df994b2c7
commit 5dc5408a15
70 changed files with 1478 additions and 518 deletions
+5 -3
View File
@@ -233,6 +233,7 @@ vi.mock('../../../hooks/use-state-string', () => ({
vi.mock('../../../hooks/use-window-width', () => ({
default: () => testState.windowWidth,
useIsMobileBreakpoint: () => testState.windowWidth < 640,
}));
vi.mock('../../../stores/use-catalog-style-store', () => ({
@@ -250,9 +251,10 @@ vi.mock('../../../stores/use-feed-reset-store', () => ({
}));
vi.mock('../../../stores/use-sorting-store', () => ({
default: () => ({
sortType: testState.sortType,
}),
default: (selector?: (state: { sortType: typeof testState.sortType }) => unknown) => {
const state = { sortType: testState.sortType };
return selector ? selector(state) : state;
},
}));
vi.mock('../../../stores/use-catalog-filters-store', () => ({
+5 -4
View File
@@ -272,7 +272,8 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
return resolvedAddressFromUrl;
}, [boardIdentifierProp, directories, resolvedAddressFromUrl]);
const { filterItems, searchText } = useCatalogFiltersStore();
const filterItems = useCatalogFiltersStore((state) => state.filterItems);
const searchText = useCatalogFiltersStore((state) => state.searchText);
const account = useAccount();
const subscriptions = account?.subscriptions;
@@ -306,7 +307,8 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
const communities = useCommunityIdentifiers(communityAddresses);
const communityIdentifier = useCommunityIdentifier(communityAddress);
const { imageSize, showOPComment } = useCatalogStyleStore();
const imageSize = useCatalogStyleStore((state) => state.imageSize);
const showOPComment = useCatalogStyleStore((state) => state.showOPComment);
const columnWidth = imageSize === 'Large' ? 270 : 180;
const windowWidth = useWindowWidth();
const isMobile = useIsMobile();
@@ -325,7 +327,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
}
}, [isInAllView, isInSubscriptionsView, isInModView, location.pathname, location.search, navigate]);
const { sortType } = useSortingStore();
const sortType = useSortingStore((state) => state.sortType);
const feedSortType = sortType === 'new' ? 'new' : 'active';
const catalogVirtualizationMode = useMemo(() => resolveCatalogVirtualizationMode(location.search, 'item-size'), [location.search]);
const themeKey = typeof document !== 'undefined' ? document.body.className : '';
@@ -632,7 +634,6 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
effectiveInfiniteScroll,
],
);
const isFeedLoaded = feed.length > 0 || state === 'failed';
// Process the feed to move "top" posts to the top (applied after display sort)
@@ -66,7 +66,8 @@ const PopularThreadCard = memo(
const PopularThreadsBox = ({ directories, directoryAddresses }: { directories: DirectoryCommunity[]; directoryAddresses: string[] }) => {
const { t } = useTranslation();
const { showWorksafeContentOnly, showNsfwContentOnly } = usePopularThreadsOptionsStore();
const showWorksafeContentOnly = usePopularThreadsOptionsStore((state) => state.showWorksafeContentOnly);
const showNsfwContentOnly = usePopularThreadsOptionsStore((state) => state.showNsfwContentOnly);
const filteredBoardAddresses = useMemo(() => {
return directoryAddresses.flatMap((address) => {
+105 -94
View File
@@ -2,6 +2,7 @@ import React, { useMemo, useState, useEffect, useCallback, memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams, Link } from 'react-router-dom';
import { useFeed, Comment, usePublishCommentModeration, useEditedComment, useCommunity, useAccount } from '@bitsocial/bitsocial-react-hooks';
import useAccountsStore from '@bitsocial/bitsocial-react-hooks/dist/stores/accounts/index.js';
import { Virtuoso } from 'react-virtuoso';
import styles from './mod-queue.module.css';
import useModQueueStore from '../../stores/use-mod-queue-store';
@@ -47,6 +48,62 @@ const getBoardDisplayPath = (address: string, path: string): string => {
return getShortAddress(address) || address;
};
type LocalModerationEditSummary = Record<string, { timestamp?: number; value: unknown } | undefined>;
type LocalModerationEditSummaries = Record<string, LocalModerationEditSummary | undefined>;
const LOCAL_MODERATION_EDIT_FIELDS = ['approved', 'removed'] as const;
const LOCAL_EDIT_PENDING_SECONDS = 20 * 60;
const shouldApplyLocalModerationEdit = (
comment: Comment,
propertyName: (typeof LOCAL_MODERATION_EDIT_FIELDS)[number],
edit: { timestamp?: number; value: unknown },
now: number,
) => {
const editTimestamp = edit.timestamp ?? 0;
const updatedAt = (comment as { updatedAt?: number }).updatedAt;
const currentValue = (comment as Record<string, unknown>)[propertyName];
if (!updatedAt) {
return Object.is(currentValue, edit.value) || editTimestamp > now - LOCAL_EDIT_PENDING_SECONDS;
}
if (updatedAt < editTimestamp || Object.is(currentValue, edit.value)) {
return true;
}
return editTimestamp > now - LOCAL_EDIT_PENDING_SECONDS || updatedAt - editTimestamp < LOCAL_EDIT_PENDING_SECONDS;
};
const applyLocalModerationEdits = (comment: Comment, editSummary: LocalModerationEditSummary | undefined, now: number): Comment => {
if (!editSummary) {
return comment;
}
let editedComment: Comment | undefined;
for (const propertyName of LOCAL_MODERATION_EDIT_FIELDS) {
const edit = editSummary[propertyName];
if (!edit || edit.value === undefined || !shouldApplyLocalModerationEdit(comment, propertyName, edit, now)) {
continue;
}
editedComment = { ...(editedComment ?? comment), [propertyName]: edit.value } as Comment;
}
return editedComment ?? comment;
};
const useLocallyModeratedModQueueFeed = (feed: Comment[], currentTime: number) => {
const accountId = useAccountsStore((state) => state.activeAccountId);
const editSummaries = useAccountsStore((state) => (accountId ? state.accountsEditsSummaries[accountId] : undefined)) as LocalModerationEditSummaries | undefined;
return useMemo(() => {
if (!editSummaries) {
return feed;
}
return feed.map((comment) => (comment.cid ? applyLocalModerationEdits(comment, editSummaries[comment.cid], currentTime) : comment));
}, [currentTime, editSummaries, feed]);
};
interface ModQueueViewProps {
boardIdentifier?: string; // If provided, shows queue for single board
}
@@ -305,7 +362,7 @@ const useModQueueActions = (comment: Comment): ModQueueActionState => {
const ModQueueRow = memo(({ comment, isOdd = false, showBoard = false, boardPath, boardDisplayPath }: ModQueueRowProps) => {
const { t } = useTranslation();
const { getAlertThresholdSeconds } = useModQueueStore();
const getAlertThresholdSeconds = useModQueueStore((state) => state.getAlertThresholdSeconds);
const isMobile = useIsMobile();
const currentTime = useCurrentTime();
@@ -319,7 +376,7 @@ const ModQueueRow = memo(({ comment, isOdd = false, showBoard = false, boardPath
const isOverThreshold = timeWaiting > alertThresholdSeconds;
// Only show alert animation for comments awaiting approval (not approved or rejected)
const isAwaitingApproval = isPendingApprovalAwaiting(comment);
const isAwaitingApproval = isPendingApprovalAwaiting(displayComment);
const { status, error, errorMessage, isPublishing, handleApprove, handleReject, handleRemove } = useModQueueActions(comment);
const hasTitle = title && title.trim().length > 0;
@@ -413,7 +470,7 @@ interface ModQueueCardProps {
const ModQueueCard = memo(({ comment, showBoard = false, boardPath, boardDisplayPath }: ModQueueCardProps) => {
const { t } = useTranslation();
const { getAlertThresholdSeconds } = useModQueueStore();
const getAlertThresholdSeconds = useModQueueStore((state) => state.getAlertThresholdSeconds);
const currentTime = useCurrentTime();
const { editedComment } = useEditedComment({ comment });
@@ -424,7 +481,7 @@ const ModQueueCard = memo(({ comment, showBoard = false, boardPath, boardDisplay
const timeWaiting = currentTime - timestamp;
const alertThresholdSeconds = getAlertThresholdSeconds();
const isOverThreshold = timeWaiting > alertThresholdSeconds;
const isAwaitingApproval = isPendingApprovalAwaiting(comment);
const isAwaitingApproval = isPendingApprovalAwaiting(displayComment);
const { status, error, errorMessage, isPublishing, handleApprove, handleReject, handleRemove } = useModQueueActions(comment);
const hasTitle = title && title.trim().length > 0;
@@ -533,17 +590,20 @@ const findBoardAddressByCode = (code: string, dirs: DirectoryCommunity[]): strin
const ModQueueBoardSummary = ({ feed, directories, accountCommunityAddresses }: ModQueueBoardSummaryProps) => {
const { t } = useTranslation();
const { selectedBoardFilter, setSelectedBoardFilter, getAlertThresholdSeconds } = useModQueueStore();
const selectedBoardFilter = useModQueueStore((state) => state.selectedBoardFilter);
const setSelectedBoardFilter = useModQueueStore((state) => state.setSelectedBoardFilter);
const getAlertThresholdSeconds = useModQueueStore((state) => state.getAlertThresholdSeconds);
const currentTime = useCurrentTime();
const alertThresholdSeconds = getAlertThresholdSeconds();
const modAddressSet = useMemo(() => new Set(accountCommunityAddresses), [accountCommunityAddresses]);
const locallyModeratedFeed = useLocallyModeratedModQueueFeed(feed, currentTime);
const boardCounts = useMemo(() => {
const counts = new Map<string, { normal: number; urgent: number }>();
for (const address of accountCommunityAddresses) {
counts.set(address, { normal: 0, urgent: 0 });
}
for (const item of feed) {
for (const item of locallyModeratedFeed) {
const addr = getCommentCommunityAddress(item);
if (!addr) continue;
const entry = counts.get(addr);
@@ -556,7 +616,7 @@ const ModQueueBoardSummary = ({ feed, directories, accountCommunityAddresses }:
else entry.normal++;
}
return counts;
}, [feed, accountCommunityAddresses, currentTime, alertThresholdSeconds]);
}, [locallyModeratedFeed, accountCommunityAddresses, currentTime, alertThresholdSeconds]);
const { totalNormal, totalUrgent } = useMemo(() => {
let normal = 0;
@@ -663,29 +723,6 @@ interface ModQueueButtonProps {
isMobile?: boolean;
}
interface ModQueueCountItemProps {
comment: Comment;
alertThresholdSeconds: number;
onStatusChange: (cid: string, status: { awaiting: boolean; urgent: boolean }) => void;
}
const ModQueueCountItem = ({ comment, alertThresholdSeconds, onStatusChange }: ModQueueCountItemProps) => {
const { editedComment } = useEditedComment({ comment });
const displayComment = editedComment || comment;
const currentTime = useCurrentTime();
const { cid, timestamp } = displayComment;
const isAwaiting = isPendingApprovalAwaiting(displayComment);
const timeWaiting = currentTime - timestamp;
const isUrgent = isAwaiting && timeWaiting > alertThresholdSeconds;
useEffect(() => {
onStatusChange(cid, { awaiting: isAwaiting, urgent: isUrgent });
}, [cid, isAwaiting, isUrgent, onStatusChange]);
return null;
};
interface ModQueueButtonContentProps {
feed: Comment[];
alertThresholdSeconds: number;
@@ -695,84 +732,54 @@ interface ModQueueButtonContentProps {
const ModQueueButtonContent = ({ feed, alertThresholdSeconds, boardIdentifier, isMobile }: ModQueueButtonContentProps) => {
const { t } = useTranslation();
const [statusMap, setStatusMap] = useState<Map<string, { awaiting: boolean; urgent: boolean }>>(new Map());
const handleStatusChange = React.useCallback((cid: string, status: { awaiting: boolean; urgent: boolean }) => {
setStatusMap((prev) => {
const next = new Map(prev);
next.set(cid, status);
return next;
});
}, []);
// Clean up stale entries when comments leave the feed to prevent memory leaks
const feedCids = useMemo(() => new Set(feed.map((item) => item.cid)), [feed]);
useEffect(() => {
setStatusMap((prev) => {
const staleKeys = [...prev.keys()].filter((cid) => !feedCids.has(cid));
if (staleKeys.length === 0) return prev;
const next = new Map(prev);
for (const key of staleKeys) {
next.delete(key);
}
return next;
});
}, [feedCids]);
const currentTime = useCurrentTime();
const locallyModeratedFeed = useLocallyModeratedModQueueFeed(feed, currentTime);
const { normalCount, urgentCount } = useMemo(() => {
let normal = 0;
let urgent = 0;
for (const { awaiting, urgent: isUrgent } of statusMap.values()) {
if (awaiting) {
if (isUrgent) urgent++;
else normal++;
}
for (const comment of locallyModeratedFeed) {
if (!isPendingApprovalAwaiting(comment)) continue;
const timeWaiting = currentTime - (comment.timestamp ?? 0);
if (timeWaiting > alertThresholdSeconds) urgent++;
else normal++;
}
return { normalCount: normal, urgentCount: urgent };
}, [statusMap]);
}, [alertThresholdSeconds, currentTime, locallyModeratedFeed]);
const totalCount = normalCount + urgentCount;
const to = boardIdentifier ? `/${boardIdentifier}/mod/queue` : '/mod/queue';
const buttonContent = (
<button className='button'>
<Link to={to}>
{t('mod_queue')}
{totalCount > 0 && (
<strong>
(
{urgentCount > 0 && normalCount > 0 ? (
<>
<span className={styles.modQueueButtonCount}>{normalCount}</span>
<span className={`${styles.modQueueButtonCount} ${styles.modQueueButtonCountAlert}`}>
{'+'}
{urgentCount}
</span>
</>
) : urgentCount > 0 ? (
<span className={`${styles.modQueueButtonCount} ${styles.modQueueButtonCountAlert}`}>{urgentCount}</span>
) : (
<span className={styles.modQueueButtonCount}>{totalCount}</span>
)}
)
</strong>
)}
</Link>
</button>
<Link className='button' to={to}>
{t('mod_queue')}
{totalCount > 0 && (
<strong>
(
{urgentCount > 0 && normalCount > 0 ? (
<>
<span className={styles.modQueueButtonCount}>{normalCount}</span>
<span className={`${styles.modQueueButtonCount} ${styles.modQueueButtonCountAlert}`}>
{'+'}
{urgentCount}
</span>
</>
) : urgentCount > 0 ? (
<span className={`${styles.modQueueButtonCount} ${styles.modQueueButtonCountAlert}`}>{urgentCount}</span>
) : (
<span className={styles.modQueueButtonCount}>{totalCount}</span>
)}
)
</strong>
)}
</Link>
);
return (
<>
{feed.map((item) => (
<ModQueueCountItem key={item.cid} comment={item} alertThresholdSeconds={alertThresholdSeconds} onStatusChange={handleStatusChange} />
))}
{isMobile ? buttonContent : <>[{buttonContent}]</>}
</>
);
return isMobile ? buttonContent : <>[{buttonContent}]</>;
};
export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProps) => {
const { getAlertThresholdSeconds } = useModQueueStore();
const getAlertThresholdSeconds = useModQueueStore((state) => state.getAlertThresholdSeconds);
const account = useAccount();
const accountAddress = account?.author?.address;
@@ -834,7 +841,7 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
}
const alertThresholdSeconds = getAlertThresholdSeconds();
// Use key to reset statusMap state when switching boards (prevents stale counts from previous board)
// Remount when switching boards so memoized counts reset cleanly.
const contentKey = communityAddresses.join(',');
return <ModQueueButtonContent key={contentKey} feed={feed} alertThresholdSeconds={alertThresholdSeconds} boardIdentifier={boardIdentifier} isMobile={isMobile} />;
};
@@ -842,7 +849,11 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueViewProps) => {
const { t } = useTranslation();
const params = useParams();
const { selectedBoardFilter, viewMode, dismissedCommentCids, queuedCommentHistory, rememberCommentsInQueue } = useModQueueStore();
const selectedBoardFilter = useModQueueStore((state) => state.selectedBoardFilter);
const viewMode = useModQueueStore((state) => state.viewMode);
const dismissedCommentCids = useModQueueStore((state) => state.dismissedCommentCids);
const queuedCommentHistory = useModQueueStore((state) => state.queuedCommentHistory);
const rememberCommentsInQueue = useModQueueStore((state) => state.rememberCommentsInQueue);
const isMobile = useIsMobile();
const accountCommunityAddresses = useAccountCommunityAddresses();
+9 -7
View File
@@ -23,11 +23,16 @@ import type { QueuedCommentRouteState } from '../../lib/utils/mod-queue-utils';
import type { ReplyVirtualizationMode } from '../../lib/utils/pretext-height-estimates';
import styles from './post.module.css';
type CommentWithRefresh = Comment & {
export type CommentWithRefresh = Comment & {
approved?: boolean;
communityAddress?: string;
refresh?: () => Promise<void>;
state?: string;
pendingApproval?: boolean;
error?: Error;
errors?: Error[];
index?: number;
removed?: boolean;
};
const getRouteUserState = (state: unknown): QueuedCommentRouteState | undefined => {
@@ -134,9 +139,9 @@ export interface PostProps {
index?: number;
isHidden?: boolean;
hasThumbnail?: boolean;
post?: any;
post?: CommentWithRefresh;
postReplyCount?: number;
reply?: any;
reply?: Comment;
replyPaginationOverride?: ReplyPaginationOverride;
replyVirtualizationModeOverride?: ReplyVirtualizationMode;
roles?: Role[];
@@ -364,10 +369,7 @@ const PostPage = () => {
const queuedReplyHasMore = queuedReplyRepliesResult.hasMore;
const queuedReplyLoadMore = queuedReplyRepliesResult.loadMore;
const queuedReplyReset = (queuedReplyRepliesResult as { reset?: () => Promise<void> }).reset;
const queuedReplyReplies =
((queuedReplyRepliesResult as { updatedReplies?: Comment[] }).updatedReplies?.length
? (queuedReplyRepliesResult as { updatedReplies?: Comment[] }).updatedReplies
: queuedReplyRepliesResult.replies) || [];
const queuedReplyReplies = (queuedReplyRepliesResult.updatedReplies?.length ? queuedReplyRepliesResult.updatedReplies : queuedReplyRepliesResult.replies) || [];
const replyPaginationOverride = useMemo(() => {
if (!queuedReply || !post?.cid) return undefined;
return {