mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
Merge branch 'codex/fix/mod-queue-history'
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user