feat(mod-queue): improve compact view and add pending approval features

- Update excerpt format to show both title and content (title: content)
- Add Type and Image columns to compact view (desktop and mobile)
- Show parent post above replies in feed view
- Add approve/reject buttons to post page for pending posts when user is mod
- Translate pending approval string across all languages
- Reorder columns: No., Excerpt, Submitted, Type, Image, Actions
- Right-align Type and Image columns in desktop view
This commit is contained in:
plebeius
2026-01-26 16:31:15 +08:00
parent 28873478a2
commit fa4ec6da40
40 changed files with 525 additions and 57 deletions
@@ -76,7 +76,7 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
{pendingApproval && (
<>
<br />
<span className={styles.pendingApproval}>(Pending mod approval, not visible to users)</span>
<span className={styles.pendingApproval}>({t('pending_mod_approval')})</span>
</>
)}
{((!isInPostView && content?.length > 1000 && !showFullComment) || (isInPostView && content?.length > 2000 && !showFullComment)) && (
+131 -2
View File
@@ -1,8 +1,8 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
import { Comment, useAuthorAvatar, useEditedComment, useReplies } from '@plebbit/plebbit-react-hooks';
import { Comment, useAuthorAvatar, useEditedComment, useReplies, useAccount } from '@plebbit/plebbit-react-hooks';
import Plebbit from '@plebbit/plebbit-js';
import styles from '../../views/post/post.module.css';
import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils';
@@ -35,6 +35,11 @@ import _ from 'lodash';
import { shouldShowSnow } from '../../lib/snow';
import useReplyModalStore from '../../stores/use-reply-modal-store';
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
import useChallengesStore from '../../stores/use-challenges-store';
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
import { usePublishCommentModeration } from '@plebbit/plebbit-react-hooks';
const { addChallenge } = useChallengesStore.getState();
// Store scroll position for replies virtuoso across navigations
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
@@ -89,6 +94,97 @@ const PostInfo = ({
const isInPostPageView = isPostPageView(location.pathname, params);
const isInModQueueView = isModQueueView(location.pathname);
const { getAlertThresholdSeconds } = useModQueueStore();
const account = useAccount();
const accountAddress = account?.author?.address;
// Check if user is mod of this board
const accountRole = roles?.[accountAddress]?.role;
const isAccountMod = accountRole === 'admin' || accountRole === 'owner' || accountRole === 'moderator';
// Check if post is pending approval and user is mod (for post page view)
const pendingApproval = post?.pendingApproval;
const shouldShowPendingApprovalButtons = isInPostPageView && !isInModQueueView && pendingApproval && isAccountMod && subplebbitAddress;
// Moderation actions for pending approval posts
const {
publishCommentModeration: approvePending,
state: approvePendingState,
error: approvePendingError,
} = usePublishCommentModeration({
commentCid: cid,
subplebbitAddress: shouldShowPendingApprovalButtons ? subplebbitAddress : undefined,
commentModeration: { approved: true },
onChallenge: async (...args: any) => {
addChallenge([...args, post]);
},
onChallengeVerification: async (challengeVerification, comment) => {
alertChallengeVerificationFailed(challengeVerification, comment);
},
onError: (error: Error) => {
console.error('Approve failed:', error);
},
});
const {
publishCommentModeration: rejectPending,
state: rejectPendingState,
error: rejectPendingError,
} = usePublishCommentModeration({
commentCid: cid,
subplebbitAddress: shouldShowPendingApprovalButtons ? subplebbitAddress : undefined,
commentModeration: { removed: true },
onChallenge: async (...args: any) => {
addChallenge([...args, post]);
},
onChallengeVerification: async (challengeVerification, comment) => {
alertChallengeVerificationFailed(challengeVerification, comment);
},
onError: (error: Error) => {
console.error('Reject failed:', error);
},
});
const [initiatedPendingAction, setInitiatedPendingAction] = useState<'approve' | 'reject' | null>(null);
const handlePendingApprove = useCallback(async () => {
const confirm = window.confirm(t('double_confirm'));
if (!confirm) {
return;
}
setInitiatedPendingAction('approve');
try {
await approvePending();
} catch (e) {
console.error(e);
}
}, [approvePending, t]);
const handlePendingReject = useCallback(async () => {
const confirm = window.confirm(t('double_confirm'));
if (!confirm) {
return;
}
setInitiatedPendingAction('reject');
try {
await rejectPending();
} catch (e) {
console.error(e);
}
}, [rejectPending, t]);
const isApprovingPending =
initiatedPendingAction === 'approve' && approvePendingState !== 'initializing' && approvePendingState !== 'succeeded' && approvePendingState !== 'failed';
const isRejectingPending =
initiatedPendingAction === 'reject' && rejectPendingState !== 'initializing' && rejectPendingState !== 'succeeded' && rejectPendingState !== 'failed';
const isPublishingPending = isApprovingPending || isRejectingPending;
const approvePendingSucceeded = initiatedPendingAction === 'approve' && approvePendingState === 'succeeded';
const rejectPendingSucceeded = initiatedPendingAction === 'reject' && rejectPendingState === 'succeeded';
const approvePendingFailed = initiatedPendingAction === 'approve' && approvePendingState === 'failed';
const rejectPendingFailed = initiatedPendingAction === 'reject' && rejectPendingState === 'failed';
const pendingStatus = approvePendingSucceeded ? 'approved' : rejectPendingSucceeded ? 'rejected' : approvePendingFailed || rejectPendingFailed ? 'failed' : null;
const pendingErrorMessage = approvePendingFailed ? approvePendingError?.message : rejectPendingFailed ? rejectPendingError?.message : undefined;
// Check if post is awaiting approval and over threshold (for mod queue view)
const approved = post?.approved;
@@ -277,6 +373,39 @@ const PostInfo = ({
)}
</span>
)}
{shouldShowPendingApprovalButtons && (
<span className={styles.modQueueActions}>
{pendingStatus === 'approved' ? (
<span className={styles.modQueueStatusApproved}>{t('approved')}</span>
) : pendingStatus === 'rejected' ? (
<span className={styles.modQueueStatusRejected}>{t('rejected')}</span>
) : pendingStatus === 'failed' ? (
<span className={styles.modQueueStatusRejected}>
{t('failed')}
{pendingErrorMessage ? `: ${pendingErrorMessage}` : ''}
</span>
) : isPublishingPending ? (
<LoadingEllipsis string={t('publishing')} />
) : (
<>
<span className={styles.modQueueButtonWrapper}>
[
<button className={styles.modQueueActionButton} onClick={handlePendingApprove} disabled={isPublishingPending}>
{t('approve')}
</button>
]
</span>
<span className={styles.modQueueButtonWrapper}>
[
<button className={styles.modQueueActionButton} onClick={handlePendingReject} disabled={isPublishingPending}>
{t('reject')}
</button>
]
</span>
</>
)}
</span>
)}
</span>
{!(removed || deleted) && !isModQueue && <PostMenuDesktop postMenu={postMenuProps} />}
{cid &&
+122 -2
View File
@@ -1,8 +1,8 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
import { Comment, useAuthorAvatar, useEditedComment, useReplies } from '@plebbit/plebbit-react-hooks';
import { Comment, useAuthorAvatar, useEditedComment, useReplies, useAccount, usePublishCommentModeration } from '@plebbit/plebbit-react-hooks';
import Plebbit from '@plebbit/plebbit-js';
import styles from '../../views/post/post.module.css';
import { shouldShowSnow } from '../../lib/snow';
@@ -30,6 +30,10 @@ import { PostProps } from '../../views/post/post';
import _ from 'lodash';
import useReplyModalStore from '../../stores/use-reply-modal-store';
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
import useChallengesStore from '../../stores/use-challenges-store';
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
const { addChallenge } = useChallengesStore.getState();
// Store scroll position for replies virtuoso across navigations
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
@@ -55,6 +59,97 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
const isInModQueueView = isModQueueView(location.pathname);
const { getAlertThresholdSeconds } = useModQueueStore();
const account = useAccount();
const accountAddress = account?.author?.address;
// Check if user is mod of this board
const accountRole = roles?.[accountAddress]?.role;
const isAccountMod = accountRole === 'admin' || accountRole === 'owner' || accountRole === 'moderator';
// Check if post is pending approval and user is mod (for post page view)
const pendingApproval = post?.pendingApproval;
const shouldShowPendingApprovalButtons = isInPostPageView && !isInModQueueView && pendingApproval && isAccountMod && subplebbitAddress;
// Moderation actions for pending approval posts
const {
publishCommentModeration: approvePending,
state: approvePendingState,
error: approvePendingError,
} = usePublishCommentModeration({
commentCid: cid,
subplebbitAddress: shouldShowPendingApprovalButtons ? subplebbitAddress : undefined,
commentModeration: { approved: true },
onChallenge: async (...args: any) => {
addChallenge([...args, post]);
},
onChallengeVerification: async (challengeVerification, comment) => {
alertChallengeVerificationFailed(challengeVerification, comment);
},
onError: (error: Error) => {
console.error('Approve failed:', error);
},
});
const {
publishCommentModeration: rejectPending,
state: rejectPendingState,
error: rejectPendingError,
} = usePublishCommentModeration({
commentCid: cid,
subplebbitAddress: shouldShowPendingApprovalButtons ? subplebbitAddress : undefined,
commentModeration: { removed: true },
onChallenge: async (...args: any) => {
addChallenge([...args, post]);
},
onChallengeVerification: async (challengeVerification, comment) => {
alertChallengeVerificationFailed(challengeVerification, comment);
},
onError: (error: Error) => {
console.error('Reject failed:', error);
},
});
const [initiatedPendingAction, setInitiatedPendingAction] = useState<'approve' | 'reject' | null>(null);
const handlePendingApprove = useCallback(async () => {
const confirm = window.confirm(t('double_confirm'));
if (!confirm) {
return;
}
setInitiatedPendingAction('approve');
try {
await approvePending();
} catch (e) {
console.error(e);
}
}, [approvePending, t]);
const handlePendingReject = useCallback(async () => {
const confirm = window.confirm(t('double_confirm'));
if (!confirm) {
return;
}
setInitiatedPendingAction('reject');
try {
await rejectPending();
} catch (e) {
console.error(e);
}
}, [rejectPending, t]);
const isApprovingPending =
initiatedPendingAction === 'approve' && approvePendingState !== 'initializing' && approvePendingState !== 'succeeded' && approvePendingState !== 'failed';
const isRejectingPending =
initiatedPendingAction === 'reject' && rejectPendingState !== 'initializing' && rejectPendingState !== 'succeeded' && rejectPendingState !== 'failed';
const isPublishingPending = isApprovingPending || isRejectingPending;
const approvePendingSucceeded = initiatedPendingAction === 'approve' && approvePendingState === 'succeeded';
const rejectPendingSucceeded = initiatedPendingAction === 'reject' && rejectPendingState === 'succeeded';
const approvePendingFailed = initiatedPendingAction === 'approve' && approvePendingState === 'failed';
const rejectPendingFailed = initiatedPendingAction === 'reject' && rejectPendingState === 'failed';
const pendingStatus = approvePendingSucceeded ? 'approved' : rejectPendingSucceeded ? 'rejected' : approvePendingFailed || rejectPendingFailed ? 'failed' : null;
const pendingErrorMessage = approvePendingFailed ? approvePendingError?.message : rejectPendingFailed ? rejectPendingError?.message : undefined;
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
@@ -218,6 +313,31 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
</span>
</>
)}
{shouldShowPendingApprovalButtons && (
<div className={styles.modQueueActions}>
{pendingStatus === 'approved' ? (
<span className={styles.modQueueStatusApproved}>{t('approved')}</span>
) : pendingStatus === 'rejected' ? (
<span className={styles.modQueueStatusRejected}>{t('rejected')}</span>
) : pendingStatus === 'failed' ? (
<span className={styles.modQueueStatusRejected}>
{t('failed')}
{pendingErrorMessage ? `: ${pendingErrorMessage}` : ''}
</span>
) : isPublishingPending ? (
<LoadingEllipsis string={t('publishing')} />
) : (
<>
<button className={`button ${styles.approveButton}`} onClick={handlePendingApprove} disabled={isPublishingPending}>
{t('approve')}
</button>
<button className={`button ${styles.rejectButton}`} onClick={handlePendingReject} disabled={isPublishingPending}>
{t('reject')}
</button>
</>
)}
</div>
)}
</span>
</span>
</div>