Merge branch 'codex/fix/mod-queue-history'

This commit is contained in:
Tommaso Casaburi
2026-04-19 15:02:49 +07:00
11 changed files with 282 additions and 79 deletions
+20 -2
View File
@@ -220,6 +220,7 @@ const PostInfo = ({
isPublishing,
onApprove,
onReject,
onRemoveFromModQueue,
quotedByMap,
directRepliesByParentCid,
postsByAuthorInThread,
@@ -304,6 +305,15 @@ const PostInfo = ({
const modQueueThreadRoute = getModQueueCommentRoute(boardPath, post?.cid);
const modQueueThreadRouteState = getQueuedCommentRouteState(post);
const modQueueErrorMessage = formatErrorForDisplay(modQueueError);
const modQueueRemoveButton = onRemoveFromModQueue ? (
<span className={styles.modQueueButtonWrapper}>
[
<button className={styles.modQueueActionButton} onClick={onRemoveFromModQueue} disabled={isPublishing}>
{t('remove')}
</button>
]
</span>
) : null;
const onLinkToPostClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
if (!cid || !threadRoute) {
@@ -471,9 +481,15 @@ const PostInfo = ({
{capitalize(t('type'))}: {capitalize(t(isReply ? 'reply' : 'post'))}
</span>
{modQueueStatus === 'approved' ? (
<span className={styles.modQueueStatusApproved}>{t('approved')}</span>
<>
<span className={styles.modQueueStatusApproved}>{t('approved')}</span>
{modQueueRemoveButton}
</>
) : modQueueStatus === 'rejected' ? (
<span className={styles.modQueueStatusRejected}>{t('rejected')}</span>
<>
<span className={styles.modQueueStatusRejected}>{t('rejected')}</span>
{modQueueRemoveButton}
</>
) : modQueueStatus === 'failed' ? (
<span className={styles.modQueueStatusRejected} title={modQueueErrorMessage}>
{t('failed')}
@@ -816,6 +832,7 @@ const PostDesktop = ({
isPublishing,
onApprove,
onReject,
onRemoveFromModQueue,
}: PostProps) => {
const { t } = useTranslation();
const resolvedPost = withResolvedCommentCommunityAddress(post);
@@ -1169,6 +1186,7 @@ const PostDesktop = ({
isPublishing={isPublishing}
onApprove={onApprove}
onReject={onReject}
onRemoveFromModQueue={onRemoveFromModQueue}
quotedByMap={quotedByMap}
directRepliesByParentCid={directRepliesByParentCid}
/>
+14 -2
View File
@@ -567,6 +567,7 @@ const PostMobile = ({
isPublishing,
onApprove,
onReject,
onRemoveFromModQueue,
}: PostProps) => {
const { t } = useTranslation();
const resolvedPost = withResolvedCommentCommunityAddress(post);
@@ -583,6 +584,11 @@ const PostMobile = ({
const modQueueThreadRoute = getModQueueCommentRoute(boardPath, resolvedPost?.cid);
const modQueueThreadRouteState = getQueuedCommentRouteState(resolvedPost);
const modQueueErrorMessage = formatErrorForDisplay(modQueueError);
const modQueueRemoveButton = onRemoveFromModQueue ? (
<button className={`button ${styles.rejectButton}`} onClick={onRemoveFromModQueue} disabled={isPublishing}>
{t('remove')}
</button>
) : null;
const linksCount = useCountLinksInReplies(resolvedPost);
const hasReplyPaginationOverride = !!replyPaginationOverride;
const shouldFetchReplies = showReplies && !isModQueue && !hasReplyPaginationOverride;
@@ -850,9 +856,15 @@ const PostMobile = ({
{isModQueue ? (
<div className={styles.modQueueActions}>
{modQueueStatus === 'approved' ? (
<span className={styles.modQueueStatusApproved}>{t('approved')}</span>
<>
<span className={styles.modQueueStatusApproved}>{t('approved')}</span>
{modQueueRemoveButton}
</>
) : modQueueStatus === 'rejected' ? (
<span className={styles.modQueueStatusRejected}>{t('rejected')}</span>
<>
<span className={styles.modQueueStatusRejected}>{t('rejected')}</span>
{modQueueRemoveButton}
</>
) : modQueueStatus === 'failed' ? (
<span className={styles.modQueueStatusRejected} title={modQueueErrorMessage}>
{t('failed')}
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
import { filterVisibleModQueueFeed, getModQueueCommentRoute, getQueuedCommentRouteState } from '../mod-queue-utils';
describe('mod queue utils', () => {
it('keeps only comments still awaiting approval', () => {
it('keeps all queue comments unless they were locally dismissed', () => {
const feed = [
{ cid: 'pending', communityAddress: 'tech.eth', pendingApproval: true },
{ cid: 'approved', approved: true, communityAddress: 'tech.eth', pendingApproval: true },
@@ -11,17 +11,24 @@ describe('mod queue utils', () => {
{ cid: 'published', communityAddress: 'tech.eth', pendingApproval: false },
];
expect(filterVisibleModQueueFeed(feed, null)).toEqual([{ cid: 'pending', communityAddress: 'tech.eth', pendingApproval: true }]);
expect(filterVisibleModQueueFeed(feed, null, new Set(['approved']))).toEqual([
{ cid: 'pending', communityAddress: 'tech.eth', pendingApproval: true },
{ cid: 'rejected', approved: false, communityAddress: 'tech.eth', pendingApproval: true },
{ cid: 'removed', communityAddress: 'tech.eth', pendingApproval: true, removed: true },
{ cid: 'published', communityAddress: 'tech.eth', pendingApproval: false },
]);
});
it('applies the selected board filter after removing non-pending items', () => {
it('applies the selected board filter after removing dismissed items', () => {
const feed = [
{ cid: 'tech-pending', communityAddress: 'tech.eth', pendingApproval: true },
{ cid: 'g-pending', communityAddress: 'g.eth', pendingApproval: true },
{ cid: 'tech-approved', approved: true, communityAddress: 'tech.eth', pendingApproval: true },
];
expect(filterVisibleModQueueFeed(feed, 'tech.eth')).toEqual([{ cid: 'tech-pending', communityAddress: 'tech.eth', pendingApproval: true }]);
expect(filterVisibleModQueueFeed(feed, 'tech.eth', new Set(['tech-pending']))).toEqual([
{ cid: 'tech-approved', approved: true, communityAddress: 'tech.eth', pendingApproval: true },
]);
});
it('builds excerpt routes from the comment permalink cid', () => {
@@ -11,8 +11,9 @@ describe('pending approval moderation utils', () => {
expect('removed' in rejectPendingCommentModeration).toBe(false);
});
it('treats pending approved=false as rejected for display', () => {
it('treats approved=false as rejected for display after pkc removes pendingApproval', () => {
expect(isPendingApprovalRejected({ pendingApproval: true, approved: false })).toBe(true);
expect(isPendingApprovalRejected({ pendingApproval: false, approved: false })).toBe(true);
expect(isPendingApprovalAwaiting({ pendingApproval: true, approved: false })).toBe(false);
});
+68 -52
View File
@@ -1,6 +1,5 @@
import { Comment } from '@bitsocialnet/bitsocial-react-hooks';
import type { Comment } from '@bitsocialnet/bitsocial-react-hooks';
import { getCommentCommunityAddress } from './comment-utils';
import { isPendingApprovalAwaiting } from './pending-approval-moderation';
import { getThreadTopNavigationState } from './thread-scroll-utils';
type ModQueueCommentLike = {
@@ -27,72 +26,89 @@ type ModQueueCommentLike = {
title?: Comment['title'];
};
const emptyDismissedCommentCids = new Set<string>();
export type QueuedCommentRouteState = {
scrollThreadContainerCid?: string;
queuedComment?: {
approved?: Comment['approved'];
author?: Comment['author'];
cid?: Comment['cid'];
commentModeration?: Comment['commentModeration'];
communityAddress?: string;
content?: Comment['content'];
deleted?: Comment['deleted'];
link?: Comment['link'];
linkHeight?: Comment['linkHeight'];
linkWidth?: Comment['linkWidth'];
number?: Comment['number'];
parentCid?: Comment['parentCid'];
pendingApproval?: Comment['pendingApproval'];
postCid?: Comment['postCid'];
reason?: Comment['reason'];
removed?: Comment['removed'];
replyCount?: Comment['replyCount'];
threadCid?: Comment['threadCid'];
thumbnailUrl?: Comment['thumbnailUrl'];
timestamp?: Comment['timestamp'];
title?: Comment['title'];
};
queuedComment?: QueuedCommentSnapshot;
};
export type QueuedCommentSnapshot = {
approved?: Comment['approved'];
author?: Comment['author'];
cid?: Comment['cid'];
commentModeration?: Comment['commentModeration'];
communityAddress?: string;
content?: Comment['content'];
deleted?: Comment['deleted'];
link?: Comment['link'];
linkHeight?: Comment['linkHeight'];
linkWidth?: Comment['linkWidth'];
number?: Comment['number'];
parentCid?: Comment['parentCid'];
pendingApproval?: Comment['pendingApproval'];
postCid?: Comment['postCid'];
reason?: Comment['reason'];
removed?: Comment['removed'];
replyCount?: Comment['replyCount'];
threadCid?: Comment['threadCid'];
thumbnailUrl?: Comment['thumbnailUrl'];
timestamp?: Comment['timestamp'];
title?: Comment['title'];
};
export const getModQueueCommentRoute = (boardPath: string | undefined, commentCid: string | undefined): string | undefined =>
boardPath && commentCid ? `/${boardPath}/thread/${commentCid}` : undefined;
export const getQueuedCommentRouteState = (comment: ModQueueCommentLike | undefined): QueuedCommentRouteState | undefined => {
export const getQueuedCommentSnapshot = (comment: ModQueueCommentLike | undefined): QueuedCommentSnapshot | undefined => {
if (!comment?.cid) {
return undefined;
}
return {
...(comment.parentCid ? {} : getThreadTopNavigationState(comment.cid)),
queuedComment: {
approved: comment.approved,
author: comment.author,
cid: comment.cid,
commentModeration: comment.commentModeration,
communityAddress: getCommentCommunityAddress(comment),
content: comment.content,
deleted: comment.deleted,
link: comment.link,
linkHeight: comment.linkHeight,
linkWidth: comment.linkWidth,
number: comment.number,
parentCid: comment.parentCid,
pendingApproval: comment.pendingApproval,
postCid: comment.postCid,
reason: comment.reason,
removed: comment.removed,
replyCount: comment.replyCount,
threadCid: comment.threadCid,
thumbnailUrl: comment.thumbnailUrl,
timestamp: comment.timestamp,
title: comment.title,
},
approved: comment.approved,
author: comment.author,
cid: comment.cid,
commentModeration: comment.commentModeration,
communityAddress: getCommentCommunityAddress(comment),
content: comment.content,
deleted: comment.deleted,
link: comment.link,
linkHeight: comment.linkHeight,
linkWidth: comment.linkWidth,
number: comment.number,
parentCid: comment.parentCid,
pendingApproval: comment.pendingApproval,
postCid: comment.postCid,
reason: comment.reason,
removed: comment.removed,
replyCount: comment.replyCount,
threadCid: comment.threadCid,
thumbnailUrl: comment.thumbnailUrl,
timestamp: comment.timestamp,
title: comment.title,
};
};
export const filterVisibleModQueueFeed = <T extends ModQueueCommentLike>(feed: T[], selectedBoardFilter: string | null): T[] =>
export const getQueuedCommentRouteState = (comment: ModQueueCommentLike | undefined): QueuedCommentRouteState | undefined => {
const queuedComment = getQueuedCommentSnapshot(comment);
if (!queuedComment) {
return undefined;
}
return {
...(queuedComment.parentCid ? {} : getThreadTopNavigationState(queuedComment.cid)),
queuedComment,
};
};
export const filterVisibleModQueueFeed = <T extends ModQueueCommentLike>(
feed: T[],
selectedBoardFilter: string | null,
dismissedCommentCids: ReadonlySet<string> = emptyDismissedCommentCids,
): T[] =>
feed.filter((comment) => {
if (!isPendingApprovalAwaiting(comment)) {
if (comment.cid && dismissedCommentCids.has(comment.cid)) {
return false;
}
+1 -2
View File
@@ -10,8 +10,7 @@ type PendingApprovalDisplayState = {
pendingApproval?: boolean;
};
export const isPendingApprovalRejected = (comment?: PendingApprovalDisplayState) =>
comment?.removed === true || (comment?.pendingApproval === true && comment?.approved === false);
export const isPendingApprovalRejected = (comment?: PendingApprovalDisplayState) => comment?.removed === true || comment?.approved === false;
export const isPendingApprovalAwaiting = (comment?: PendingApprovalDisplayState) =>
comment?.pendingApproval === true && comment?.approved !== true && !isPendingApprovalRejected(comment);
@@ -75,18 +75,29 @@ describe('persisted extra stores', () => {
expect(useModQueueStore.getState()).toMatchObject({
alertThresholdValue: 3,
alertThresholdUnit: 'hours',
dismissedCommentCids: [],
queuedCommentHistory: [],
selectedBoardFilter: 'music.eth',
viewMode: 'feed',
});
expect(useModQueueStore.getState().getAlertThresholdSeconds()).toBe(10_800);
useModQueueStore.getState().setAlertThreshold(15, 'minutes');
useModQueueStore.getState().dismissCommentFromQueue('approved-cid');
useModQueueStore.getState().dismissCommentFromQueue('approved-cid');
useModQueueStore.getState().rememberCommentsInQueue([{ cid: 'approved-cid', approved: true, content: 'approved body' }]);
useModQueueStore.getState().rememberCommentsInQueue([{ cid: 'rejected-cid', approved: false, content: 'rejected body' }]);
useModQueueStore.getState().setSelectedBoardFilter('tech.eth');
useModQueueStore.getState().setViewMode('compact');
expect(useModQueueStore.getState()).toMatchObject({
alertThresholdValue: 15,
alertThresholdUnit: 'minutes',
dismissedCommentCids: ['approved-cid'],
queuedCommentHistory: [
{ cid: 'rejected-cid', approved: false, content: 'rejected body' },
{ cid: 'approved-cid', approved: true, content: 'approved body' },
],
selectedBoardFilter: 'tech.eth',
viewMode: 'compact',
});
+41 -1
View File
@@ -1,14 +1,20 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { QueuedCommentSnapshot } from '../lib/utils/mod-queue-utils';
type AlertThresholdUnit = 'hours' | 'minutes';
type ModQueueViewMode = 'compact' | 'feed';
const MAX_QUEUE_HISTORY_COMMENTS = 500;
interface ModQueueState {
alertThresholdValue: number;
alertThresholdUnit: AlertThresholdUnit;
dismissedCommentCids: string[];
queuedCommentHistory: QueuedCommentSnapshot[];
selectedBoardFilter: string | null;
viewMode: ModQueueViewMode;
dismissCommentFromQueue: (cid: string) => void;
rememberCommentsInQueue: (comments: QueuedCommentSnapshot[]) => void;
setAlertThreshold: (value: number, unit: AlertThresholdUnit) => void;
setSelectedBoardFilter: (boardAddress: string | null) => void;
setViewMode: (viewMode: ModQueueViewMode) => void;
@@ -21,18 +27,48 @@ interface OldPersistedState {
alertThresholdHours?: number;
alertThresholdValue?: number;
alertThresholdUnit?: AlertThresholdUnit;
dismissedCommentCids?: string[];
queuedCommentHistory?: QueuedCommentSnapshot[];
selectedBoardFilter?: string | null;
viewMode?: ModQueueViewMode;
}
// Type for persisted data (without methods)
type PersistedModQueueData = Pick<ModQueueState, 'alertThresholdValue' | 'alertThresholdUnit' | 'selectedBoardFilter' | 'viewMode'>;
type PersistedModQueueData = Pick<
ModQueueState,
'alertThresholdValue' | 'alertThresholdUnit' | 'dismissedCommentCids' | 'queuedCommentHistory' | 'selectedBoardFilter' | 'viewMode'
>;
const useModQueueStore = create<ModQueueState>()(
persist(
(set, get) => ({
alertThresholdValue: 6,
alertThresholdUnit: 'hours' as AlertThresholdUnit,
dismissedCommentCids: [],
queuedCommentHistory: [],
dismissCommentFromQueue: (cid) =>
set((state) => {
if (state.dismissedCommentCids.includes(cid)) {
return state;
}
return { dismissedCommentCids: [...state.dismissedCommentCids, cid] };
}),
rememberCommentsInQueue: (comments) =>
set((state) => {
const rememberedByCid = new Map<string, QueuedCommentSnapshot>();
for (const comment of comments) {
if (comment.cid) {
rememberedByCid.set(comment.cid, comment);
}
}
for (const comment of state.queuedCommentHistory) {
if (comment.cid && !rememberedByCid.has(comment.cid)) {
rememberedByCid.set(comment.cid, comment);
}
}
return { queuedCommentHistory: [...rememberedByCid.values()].slice(0, MAX_QUEUE_HISTORY_COMMENTS) };
}),
selectedBoardFilter: null,
viewMode: 'compact',
setAlertThreshold: (value, unit) => set({ alertThresholdValue: value, alertThresholdUnit: unit }),
@@ -53,6 +89,8 @@ const useModQueueStore = create<ModQueueState>()(
const migrated: PersistedModQueueData = {
alertThresholdValue: state.alertThresholdHours,
alertThresholdUnit: 'hours' as AlertThresholdUnit,
dismissedCommentCids: state.dismissedCommentCids ?? [],
queuedCommentHistory: state.queuedCommentHistory ?? [],
selectedBoardFilter: state.selectedBoardFilter ?? null,
viewMode: state.viewMode ?? 'compact',
};
@@ -63,6 +101,8 @@ const useModQueueStore = create<ModQueueState>()(
const current: PersistedModQueueData = {
alertThresholdValue: state.alertThresholdValue ?? 6,
alertThresholdUnit: state.alertThresholdUnit ?? 'hours',
dismissedCommentCids: state.dismissedCommentCids ?? [],
queuedCommentHistory: state.queuedCommentHistory ?? [],
selectedBoardFilter: state.selectedBoardFilter ?? null,
viewMode: state.viewMode ?? 'compact',
};
+5
View File
@@ -351,6 +351,11 @@
color: #fff;
}
.cardRemoveButton {
background-color: #777;
color: #fff;
}
/* Desktop: buttons styled as text links (same as board buttons) */
@media (min-width: 640px) {
.actions {
+103 -14
View File
@@ -25,7 +25,7 @@ import useChallengesStore from '../../stores/use-challenges-store';
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
import { getCommentCommunityAddress } from '../../lib/utils/comment-utils';
import { formatErrorForDisplay } from '../../lib/utils/error-utils';
import { filterVisibleModQueueFeed, getModQueueCommentRoute, getQueuedCommentRouteState } from '../../lib/utils/mod-queue-utils';
import { filterVisibleModQueueFeed, getModQueueCommentRoute, getQueuedCommentRouteState, getQueuedCommentSnapshot } from '../../lib/utils/mod-queue-utils';
import Tooltip from '../../components/tooltip';
import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses';
import { useCommunityIdentifier, useCommunityIdentifiers } from '../../hooks/use-community-identifiers';
@@ -90,6 +90,7 @@ interface ModQueueActionState {
isPublishing: boolean;
handleApprove: () => Promise<void>;
handleReject: () => Promise<void>;
handleRemove?: () => void;
}
interface ModQueueActionsProps {
@@ -99,20 +100,55 @@ interface ModQueueActionsProps {
isPublishing: boolean;
handleApprove: () => Promise<void>;
handleReject: () => Promise<void>;
handleRemove?: () => void;
variant: 'row' | 'card';
}
const ModQueueActions = ({ status, error, errorMessage, isPublishing, handleApprove, handleReject, variant }: ModQueueActionsProps) => {
const ModQueueActions = ({ status, error, errorMessage, isPublishing, handleApprove, handleReject, handleRemove, variant }: ModQueueActionsProps) => {
const { t } = useTranslation();
const displayError = error || errorMessage;
const removeButton = handleRemove ? (
variant === 'row' ? (
<span className={styles.buttonWrapper}>
[
<button className={styles.button} onClick={handleRemove} disabled={isPublishing}>
{t('remove')}
</button>
]
</span>
) : (
<button className={`button ${styles.cardRemoveButton}`} onClick={handleRemove} disabled={isPublishing}>
{t('remove')}
</button>
)
) : null;
if (status === 'approved') {
const content = <span className={`${styles.button} ${styles.approve}`}>{t('approved')}</span>;
return variant === 'card' ? <div className={styles.cardActions}>{content}</div> : content;
return variant === 'card' ? (
<div className={styles.cardActions}>
{content}
{removeButton}
</div>
) : (
<span className={styles.actionButtons}>
{content} {removeButton}
</span>
);
}
if (status === 'rejected') {
const content = <span className={`${styles.button} ${styles.reject}`}>{t('rejected')}</span>;
return variant === 'card' ? <div className={styles.cardActions}>{content}</div> : content;
return variant === 'card' ? (
<div className={styles.cardActions}>
{content}
{removeButton}
</div>
) : (
<span className={styles.actionButtons}>
{content} {removeButton}
</span>
);
}
if (status === 'failed') {
const content = displayError ? (
@@ -163,6 +199,8 @@ const useModQueueActions = (comment: Comment): ModQueueActionState => {
const { t } = useTranslation();
const { cid, approved, removed, pendingApproval } = comment || {};
const communityAddress = getCommentCommunityAddress(comment);
const dismissCommentFromQueue = useModQueueStore((state) => state.dismissCommentFromQueue);
const rememberCommentsInQueue = useModQueueStore((state) => state.rememberCommentsInQueue);
const [initiatedAction, setInitiatedAction] = useState<ModerationAction>(null);
const alreadyApproved = approved === true;
@@ -206,7 +244,7 @@ const useModQueueActions = (comment: Comment): ModQueueActionState => {
},
});
const handleApprove = useCallback(async () => {
const handleApprove = async () => {
const confirm = window.confirm(t('double_confirm'));
if (!confirm) {
return;
@@ -215,12 +253,16 @@ const useModQueueActions = (comment: Comment): ModQueueActionState => {
setInitiatedAction('approve');
try {
await approve();
const approvedSnapshot = getQueuedCommentSnapshot({ ...comment, approved: true, pendingApproval: false });
if (approvedSnapshot) {
rememberCommentsInQueue([approvedSnapshot]);
}
} catch (e) {
console.error(e);
}
}, [approve, t]);
};
const handleReject = useCallback(async () => {
const handleReject = async () => {
const confirm = window.confirm(t('double_confirm'));
if (!confirm) {
return;
@@ -229,10 +271,20 @@ const useModQueueActions = (comment: Comment): ModQueueActionState => {
setInitiatedAction('reject');
try {
await reject();
const rejectedSnapshot = getQueuedCommentSnapshot({ ...comment, approved: false, pendingApproval: false });
if (rejectedSnapshot) {
rememberCommentsInQueue([rejectedSnapshot]);
}
} catch (e) {
console.error(e);
}
}, [reject, t]);
};
const handleRemove = () => {
if (cid) {
dismissCommentFromQueue(cid);
}
};
const isApproving = initiatedAction === 'approve' && approveState !== 'initializing' && approveState !== 'succeeded' && approveState !== 'failed';
const isRejecting = initiatedAction === 'reject' && rejectState !== 'initializing' && rejectState !== 'succeeded' && rejectState !== 'failed';
@@ -248,7 +300,7 @@ const useModQueueActions = (comment: Comment): ModQueueActionState => {
const error = approveFailed ? approveError : rejectFailed ? rejectError : undefined;
const errorMessage = formatErrorForDisplay(error);
return { status, error, errorMessage, isPublishing, handleApprove, handleReject };
return { status, error, errorMessage, isPublishing, handleApprove, handleReject, handleRemove: status ? handleRemove : undefined };
};
const ModQueueRow = memo(({ comment, isOdd = false, showBoard = false, boardPath, boardDisplayPath }: ModQueueRowProps) => {
@@ -269,7 +321,7 @@ const ModQueueRow = memo(({ comment, isOdd = false, showBoard = false, boardPath
// Only show alert animation for comments awaiting approval (not approved or rejected)
const isAwaitingApproval = isPendingApprovalAwaiting(comment);
const { status, error, errorMessage, isPublishing, handleApprove, handleReject } = useModQueueActions(comment);
const { status, error, errorMessage, isPublishing, handleApprove, handleReject, handleRemove } = useModQueueActions(comment);
const hasTitle = title && title.trim().length > 0;
const hasContent = content && content.trim().length > 0;
const hasLink = link && link.length > 0;
@@ -341,6 +393,7 @@ const ModQueueRow = memo(({ comment, isOdd = false, showBoard = false, boardPath
isPublishing={isPublishing}
handleApprove={handleApprove}
handleReject={handleReject}
handleRemove={handleRemove}
variant='row'
/>
</div>
@@ -373,7 +426,7 @@ const ModQueueCard = memo(({ comment, showBoard = false, boardPath, boardDisplay
const isOverThreshold = timeWaiting > alertThresholdSeconds;
const isAwaitingApproval = isPendingApprovalAwaiting(comment);
const { status, error, errorMessage, isPublishing, handleApprove, handleReject } = useModQueueActions(comment);
const { status, error, errorMessage, isPublishing, handleApprove, handleReject, handleRemove } = useModQueueActions(comment);
const hasTitle = title && title.trim().length > 0;
const hasContent = content && content.trim().length > 0;
const hasLink = link && link.length > 0;
@@ -434,6 +487,7 @@ const ModQueueCard = memo(({ comment, showBoard = false, boardPath, boardDisplay
isPublishing={isPublishing}
handleApprove={handleApprove}
handleReject={handleReject}
handleRemove={handleRemove}
variant='card'
/>
</div>
@@ -444,7 +498,7 @@ ModQueueCard.displayName = 'ModQueueCard';
const ModQueueFeedPost = ({ comment }: { comment: Comment }) => {
const { editedComment } = useEditedComment({ comment });
const displayComment = editedComment || comment;
const { status, error, errorMessage, isPublishing, handleApprove, handleReject } = useModQueueActions(comment);
const { status, error, errorMessage, isPublishing, handleApprove, handleReject, handleRemove } = useModQueueActions(comment);
return (
<Post
@@ -457,6 +511,7 @@ const ModQueueFeedPost = ({ comment }: { comment: Comment }) => {
isPublishing={isPublishing}
onApprove={handleApprove}
onReject={handleReject}
onRemoveFromModQueue={handleRemove}
/>
);
};
@@ -787,7 +842,7 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueViewProps) => {
const { t } = useTranslation();
const params = useParams();
const { selectedBoardFilter, viewMode } = useModQueueStore();
const { selectedBoardFilter, viewMode, dismissedCommentCids, queuedCommentHistory, rememberCommentsInQueue } = useModQueueStore();
const isMobile = useIsMobile();
const accountCommunityAddresses = useAccountCommunityAddresses();
@@ -824,7 +879,41 @@ const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueViewProp
);
const { feed, hasMore, loadMore, reset } = useFeed(feedOptions);
const filteredFeed = useMemo(() => filterVisibleModQueueFeed(feed, selectedBoardFilter), [feed, selectedBoardFilter]);
const queuedCommentSnapshots = useMemo(
() =>
feed.flatMap((comment) => {
const snapshot = getQueuedCommentSnapshot(comment);
return snapshot ? [snapshot] : [];
}),
[feed],
);
useEffect(() => {
if (queuedCommentSnapshots.length > 0) {
rememberCommentsInQueue(queuedCommentSnapshots);
}
}, [queuedCommentSnapshots, rememberCommentsInQueue]);
const feedWithHistory = useMemo(() => {
const liveCids = new Set(feed.map((comment) => comment.cid).filter(Boolean));
return [
...feed,
...(queuedCommentHistory.filter((comment) => {
const commentCommunityAddress = getCommentCommunityAddress(comment);
return (
comment.cid &&
!liveCids.has(comment.cid) &&
commentCommunityAddress &&
communityAddresses.some((communityAddress) => areSameBoardAddress(communityAddress, commentCommunityAddress))
);
}) as Comment[]),
];
}, [communityAddresses, feed, queuedCommentHistory]);
const dismissedCommentCidSet = useMemo(() => new Set(dismissedCommentCids), [dismissedCommentCids]);
const filteredFeed = useMemo(
() => filterVisibleModQueueFeed(feedWithHistory, selectedBoardFilter, dismissedCommentCidSet),
[feedWithHistory, selectedBoardFilter, dismissedCommentCidSet],
);
const addressToPathMap = useMemo(() => {
const map = new Map<string, string>();
+6 -1
View File
@@ -150,6 +150,7 @@ export interface PostProps {
isPublishing?: boolean;
onApprove?: () => void;
onReject?: () => void;
onRemoveFromModQueue?: () => void;
quotedByMap?: Map<string, Comment[]>;
}
@@ -165,6 +166,7 @@ export const Post = memo(
isPublishing,
onApprove,
onReject,
onRemoveFromModQueue,
feedVirtualizationModeOverride,
replyPaginationOverride,
replyVirtualizationModeOverride,
@@ -199,6 +201,7 @@ export const Post = memo(
isPublishing={isPublishing}
onApprove={onApprove}
onReject={onReject}
onRemoveFromModQueue={onRemoveFromModQueue}
/>
) : (
<PostDesktop
@@ -216,6 +219,7 @@ export const Post = memo(
isPublishing={isPublishing}
onApprove={onApprove}
onReject={onReject}
onRemoveFromModQueue={onRemoveFromModQueue}
/>
)}
</div>
@@ -251,7 +255,8 @@ export const Post = memo(
prevProps.modQueueError === nextProps.modQueueError &&
prevProps.isPublishing === nextProps.isPublishing &&
prevProps.onApprove === nextProps.onApprove &&
prevProps.onReject === nextProps.onReject
prevProps.onReject === nextProps.onReject &&
prevProps.onRemoveFromModQueue === nextProps.onRemoveFromModQueue
);
},
);