mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
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:
@@ -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)) && (
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user