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
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "عتبة التنبيه", "alert_threshold": "عتبة التنبيه",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "সতর্কতা থ্রেশহোল্ড", "alert_threshold": "সতর্কতা থ্রেশহোল্ড",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Práh upozornění", "alert_threshold": "Práh upozornění",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Alarmtærskel", "alert_threshold": "Alarmtærskel",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Alarmschwellenwert", "alert_threshold": "Alarmschwellenwert",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Typ",
"yes": "Ja",
"no": "Nein",
"pending_mod_approval": "Ausstehende Moderatorgenehmigung, für Benutzer nicht sichtbar"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Όριο ειδοποίησης", "alert_threshold": "Όριο ειδοποίησης",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"approved": "Approved", "approved": "Approved",
"rejected": "Rejected", "rejected": "Rejected",
"submitted": "Submitted", "submitted": "Submitted",
"downloading_board": "Downloading board" "downloading_board": "Downloading board",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Umbral de alerta", "alert_threshold": "Umbral de alerta",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Tipo",
"yes": "Sí",
"no": "No",
"pending_mod_approval": "Pendiente de aprobación del moderador, no visible para los usuarios"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "آستانه هشدار", "alert_threshold": "آستانه هشدار",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Hälytyskynnys", "alert_threshold": "Hälytyskynnys",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Alert threshold", "alert_threshold": "Alert threshold",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Seuil d'alerte", "alert_threshold": "Seuil d'alerte",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Oui",
"no": "Non",
"pending_mod_approval": "En attente d'approbation du modérateur, non visible pour les utilisateurs"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "סף התרעה", "alert_threshold": "סף התרעה",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "सतर्कता सीमा", "alert_threshold": "सतर्कता सीमा",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Riasztási küszöb", "alert_threshold": "Riasztási küszöb",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Ambang peringatan", "alert_threshold": "Ambang peringatan",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Soglia di allerta", "alert_threshold": "Soglia di allerta",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Tipo",
"yes": "Sì",
"no": "No",
"pending_mod_approval": "In attesa di approvazione del moderatore, non visibile agli utenti"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "アラート閾値", "alert_threshold": "アラート閾値",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "タイプ",
"yes": "はい",
"no": "いいえ",
"pending_mod_approval": "モデレーターの承認待ち、ユーザーには表示されません"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "알림 임계값", "alert_threshold": "알림 임계값",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "सतर्कता मर्यादा", "alert_threshold": "सतर्कता मर्यादा",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Drempelwaarde voor waarschuwing", "alert_threshold": "Drempelwaarde voor waarschuwing",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Terskel for varsel", "alert_threshold": "Terskel for varsel",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Próg alertu", "alert_threshold": "Próg alertu",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Limite de alerta", "alert_threshold": "Limite de alerta",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Tipo",
"yes": "Sim",
"no": "Não",
"pending_mod_approval": "Aguardando aprovação do moderador, não visível para os usuários"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Prag de alertă", "alert_threshold": "Prag de alertă",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Порог предупреждения", "alert_threshold": "Порог предупреждения",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Тип",
"yes": "Да",
"no": "Нет",
"pending_mod_approval": "Ожидает одобрения модератора, не видно пользователям"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Pragu i alarmit", "alert_threshold": "Pragu i alarmit",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Tröskelvärde för varning", "alert_threshold": "Tröskelvärde för varning",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "హెచ్చరిక థ్రెషోల్డ్", "alert_threshold": "హెచ్చరిక థ్రెషోల్డ్",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "เกณฑ์การแจ้งเตือน", "alert_threshold": "เกณฑ์การแจ้งเตือน",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Uyarı eşiği", "alert_threshold": "Uyarı eşiği",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Поріг попередження", "alert_threshold": "Поріг попередження",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "انتباہ کی حد", "alert_threshold": "انتباہ کی حد",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "Ngưỡng cảnh báo", "alert_threshold": "Ngưỡng cảnh báo",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
} }
+5 -1
View File
@@ -272,5 +272,9 @@
"alert_threshold": "警报阈值", "alert_threshold": "警报阈值",
"modQueue.viewLabel": "View", "modQueue.viewLabel": "View",
"modQueue.compact": "Compact", "modQueue.compact": "Compact",
"modQueue.feed": "Feed" "modQueue.feed": "Feed",
"type": "类型",
"yes": "是",
"no": "否",
"pending_mod_approval": "等待版主批准,用户不可见"
} }
@@ -76,7 +76,7 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
{pendingApproval && ( {pendingApproval && (
<> <>
<br /> <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)) && ( {((!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 { Trans, useTranslation } from 'react-i18next';
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom'; import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso'; 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 Plebbit from '@plebbit/plebbit-js';
import styles from '../../views/post/post.module.css'; import styles from '../../views/post/post.module.css';
import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils'; import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils';
@@ -35,6 +35,11 @@ import _ from 'lodash';
import { shouldShowSnow } from '../../lib/snow'; import { shouldShowSnow } from '../../lib/snow';
import useReplyModalStore from '../../stores/use-reply-modal-store'; import useReplyModalStore from '../../stores/use-reply-modal-store';
import { selectPostMenuProps } from '../../lib/utils/post-menu-props'; 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 // Store scroll position for replies virtuoso across navigations
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {}; const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
@@ -89,6 +94,97 @@ const PostInfo = ({
const isInPostPageView = isPostPageView(location.pathname, params); const isInPostPageView = isPostPageView(location.pathname, params);
const isInModQueueView = isModQueueView(location.pathname); const isInModQueueView = isModQueueView(location.pathname);
const { getAlertThresholdSeconds } = useModQueueStore(); 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) // Check if post is awaiting approval and over threshold (for mod queue view)
const approved = post?.approved; const approved = post?.approved;
@@ -277,6 +373,39 @@ const PostInfo = ({
)} )}
</span> </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> </span>
{!(removed || deleted) && !isModQueue && <PostMenuDesktop postMenu={postMenuProps} />} {!(removed || deleted) && !isModQueue && <PostMenuDesktop postMenu={postMenuProps} />}
{cid && {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 { useTranslation } from 'react-i18next';
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom'; import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso'; 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 Plebbit from '@plebbit/plebbit-js';
import styles from '../../views/post/post.module.css'; import styles from '../../views/post/post.module.css';
import { shouldShowSnow } from '../../lib/snow'; import { shouldShowSnow } from '../../lib/snow';
@@ -30,6 +30,10 @@ import { PostProps } from '../../views/post/post';
import _ from 'lodash'; import _ from 'lodash';
import useReplyModalStore from '../../stores/use-reply-modal-store'; import useReplyModalStore from '../../stores/use-reply-modal-store';
import { selectPostMenuProps } from '../../lib/utils/post-menu-props'; 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 // Store scroll position for replies virtuoso across navigations
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {}; const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
@@ -55,6 +59,97 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params); const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
const isInModQueueView = isModQueueView(location.pathname); const isInModQueueView = isModQueueView(location.pathname);
const { getAlertThresholdSeconds } = useModQueueStore(); 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 commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const hasThumbnail = getHasThumbnail(commentMediaInfo, link); const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
@@ -218,6 +313,31 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
</span> </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>
</span> </span>
</div> </div>
+52
View File
@@ -66,6 +66,8 @@
.numberHeader, .numberHeader,
.excerptHeader, .excerptHeader,
.timeHeader, .timeHeader,
.typeHeader,
.imageHeader,
.actionsHeader { .actionsHeader {
background: var(--mod-queue-header-background-color); background: var(--mod-queue-header-background-color);
padding: 5px; padding: 5px;
@@ -80,6 +82,18 @@
flex-grow: 1; flex-grow: 1;
} }
.typeHeader {
width: 70px;
flex-shrink: 0;
text-align: right;
}
.imageHeader {
width: 60px;
flex-shrink: 0;
text-align: right;
}
.timeHeader { .timeHeader {
width: 210px; width: 210px;
text-align: right; text-align: right;
@@ -124,6 +138,8 @@
.number, .number,
.excerpt, .excerpt,
.time, .time,
.type,
.image,
.actions { .actions {
padding: 2px 5px; padding: 2px 5px;
} }
@@ -135,6 +151,8 @@
:global(body.yotsuba) .rowOdd .number, :global(body.yotsuba) .rowOdd .number,
:global(body.yotsuba) .rowOdd .excerpt, :global(body.yotsuba) .rowOdd .excerpt,
:global(body.yotsuba) .rowOdd .time, :global(body.yotsuba) .rowOdd .time,
:global(body.yotsuba) .rowOdd .type,
:global(body.yotsuba) .rowOdd .image,
:global(body.yotsuba) .rowOdd .actions { :global(body.yotsuba) .rowOdd .actions {
background-color: #ede2d4; background-color: #ede2d4;
} }
@@ -142,6 +160,8 @@
:global(body.yotsuba-b) .rowOdd .number, :global(body.yotsuba-b) .rowOdd .number,
:global(body.yotsuba-b) .rowOdd .excerpt, :global(body.yotsuba-b) .rowOdd .excerpt,
:global(body.yotsuba-b) .rowOdd .time, :global(body.yotsuba-b) .rowOdd .time,
:global(body.yotsuba-b) .rowOdd .type,
:global(body.yotsuba-b) .rowOdd .image,
:global(body.yotsuba-b) .rowOdd .actions { :global(body.yotsuba-b) .rowOdd .actions {
background-color: #e0e5f6; background-color: #e0e5f6;
} }
@@ -149,6 +169,8 @@
:global(body.futaba) .rowOdd .number, :global(body.futaba) .rowOdd .number,
:global(body.futaba) .rowOdd .excerpt, :global(body.futaba) .rowOdd .excerpt,
:global(body.futaba) .rowOdd .time, :global(body.futaba) .rowOdd .time,
:global(body.futaba) .rowOdd .type,
:global(body.futaba) .rowOdd .image,
:global(body.futaba) .rowOdd .actions { :global(body.futaba) .rowOdd .actions {
background-color: #ede2d4; background-color: #ede2d4;
} }
@@ -156,6 +178,8 @@
:global(body.burichan) .rowOdd .number, :global(body.burichan) .rowOdd .number,
:global(body.burichan) .rowOdd .excerpt, :global(body.burichan) .rowOdd .excerpt,
:global(body.burichan) .rowOdd .time, :global(body.burichan) .rowOdd .time,
:global(body.burichan) .rowOdd .type,
:global(body.burichan) .rowOdd .image,
:global(body.burichan) .rowOdd .actions { :global(body.burichan) .rowOdd .actions {
background-color: #e0e5f6; background-color: #e0e5f6;
} }
@@ -163,6 +187,8 @@
:global(body.tomorrow) .rowOdd .number, :global(body.tomorrow) .rowOdd .number,
:global(body.tomorrow) .rowOdd .excerpt, :global(body.tomorrow) .rowOdd .excerpt,
:global(body.tomorrow) .rowOdd .time, :global(body.tomorrow) .rowOdd .time,
:global(body.tomorrow) .rowOdd .type,
:global(body.tomorrow) .rowOdd .image,
:global(body.tomorrow) .rowOdd .actions { :global(body.tomorrow) .rowOdd .actions {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }
@@ -170,6 +196,8 @@
:global(body.photon) .rowOdd .number, :global(body.photon) .rowOdd .number,
:global(body.photon) .rowOdd .excerpt, :global(body.photon) .rowOdd .excerpt,
:global(body.photon) .rowOdd .time, :global(body.photon) .rowOdd .time,
:global(body.photon) .rowOdd .type,
:global(body.photon) .rowOdd .image,
:global(body.photon) .rowOdd .actions { :global(body.photon) .rowOdd .actions {
background-color: #888; background-color: #888;
} }
@@ -187,6 +215,18 @@
min-width: 0; min-width: 0;
} }
.type {
width: 70px;
flex-shrink: 0;
text-align: right;
}
.image {
width: 60px;
flex-shrink: 0;
text-align: right;
}
.excerpt a { .excerpt a {
text-decoration: var(--post-link-text-decoration); text-decoration: var(--post-link-text-decoration);
color: var(--post-link-text-color); color: var(--post-link-text-color);
@@ -397,26 +437,38 @@ span.reject:hover {
:global(body.yotsuba) .rowOdd .number, :global(body.yotsuba) .rowOdd .number,
:global(body.yotsuba) .rowOdd .excerpt, :global(body.yotsuba) .rowOdd .excerpt,
:global(body.yotsuba) .rowOdd .time, :global(body.yotsuba) .rowOdd .time,
:global(body.yotsuba) .rowOdd .type,
:global(body.yotsuba) .rowOdd .image,
:global(body.yotsuba) .rowOdd .actions, :global(body.yotsuba) .rowOdd .actions,
:global(body.yotsuba-b) .rowOdd .number, :global(body.yotsuba-b) .rowOdd .number,
:global(body.yotsuba-b) .rowOdd .excerpt, :global(body.yotsuba-b) .rowOdd .excerpt,
:global(body.yotsuba-b) .rowOdd .time, :global(body.yotsuba-b) .rowOdd .time,
:global(body.yotsuba-b) .rowOdd .type,
:global(body.yotsuba-b) .rowOdd .image,
:global(body.yotsuba-b) .rowOdd .actions, :global(body.yotsuba-b) .rowOdd .actions,
:global(body.futaba) .rowOdd .number, :global(body.futaba) .rowOdd .number,
:global(body.futaba) .rowOdd .excerpt, :global(body.futaba) .rowOdd .excerpt,
:global(body.futaba) .rowOdd .time, :global(body.futaba) .rowOdd .time,
:global(body.futaba) .rowOdd .type,
:global(body.futaba) .rowOdd .image,
:global(body.futaba) .rowOdd .actions, :global(body.futaba) .rowOdd .actions,
:global(body.burichan) .rowOdd .number, :global(body.burichan) .rowOdd .number,
:global(body.burichan) .rowOdd .excerpt, :global(body.burichan) .rowOdd .excerpt,
:global(body.burichan) .rowOdd .time, :global(body.burichan) .rowOdd .time,
:global(body.burichan) .rowOdd .type,
:global(body.burichan) .rowOdd .image,
:global(body.burichan) .rowOdd .actions, :global(body.burichan) .rowOdd .actions,
:global(body.tomorrow) .rowOdd .number, :global(body.tomorrow) .rowOdd .number,
:global(body.tomorrow) .rowOdd .excerpt, :global(body.tomorrow) .rowOdd .excerpt,
:global(body.tomorrow) .rowOdd .time, :global(body.tomorrow) .rowOdd .time,
:global(body.tomorrow) .rowOdd .type,
:global(body.tomorrow) .rowOdd .image,
:global(body.tomorrow) .rowOdd .actions, :global(body.tomorrow) .rowOdd .actions,
:global(body.photon) .rowOdd .number, :global(body.photon) .rowOdd .number,
:global(body.photon) .rowOdd .excerpt, :global(body.photon) .rowOdd .excerpt,
:global(body.photon) .rowOdd .time, :global(body.photon) .rowOdd .time,
:global(body.photon) .rowOdd .type,
:global(body.photon) .rowOdd .image,
:global(body.photon) .rowOdd .actions { :global(body.photon) .rowOdd .actions {
background-color: transparent; background-color: transparent;
} }
+33 -6
View File
@@ -1,7 +1,7 @@
import React, { useMemo, useState, useEffect, useCallback } from 'react'; import React, { useMemo, useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams, Link } from 'react-router-dom'; import { useParams, Link } from 'react-router-dom';
import { useFeed, Comment, usePublishCommentModeration, useEditedComment, useSubplebbit } from '@plebbit/plebbit-react-hooks'; import { useFeed, Comment, usePublishCommentModeration, useEditedComment, useSubplebbit, useComment } from '@plebbit/plebbit-react-hooks';
import useAccountsStore from '@plebbit/plebbit-react-hooks/dist/stores/accounts'; import useAccountsStore from '@plebbit/plebbit-react-hooks/dist/stores/accounts';
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
@@ -22,6 +22,7 @@ import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-util
import Tooltip from '../../components/tooltip'; import Tooltip from '../../components/tooltip';
import useIsMobile from '../../hooks/use-is-mobile'; import useIsMobile from '../../hooks/use-is-mobile';
import { Post } from '../post/post'; import { Post } from '../post/post';
import _ from 'lodash';
const { addChallenge } = useChallengesStore.getState(); const { addChallenge } = useChallengesStore.getState();
@@ -162,7 +163,8 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
const { editedComment } = useEditedComment({ comment }); const { editedComment } = useEditedComment({ comment });
const displayComment = editedComment || comment; const displayComment = editedComment || comment;
const { content, title, timestamp, subplebbitAddress, cid, threadCid, link, thumbnailUrl, linkWidth, linkHeight, removed, approved, number } = displayComment; const { content, title, timestamp, subplebbitAddress, cid, threadCid, link, thumbnailUrl, linkWidth, linkHeight, removed, approved, number, parentCid } =
displayComment;
// Check if already moderated (from previous session or API update) // Check if already moderated (from previous session or API update)
// Note: `approved` and `removed` are direct fields on the comment from CommentUpdate, // Note: `approved` and `removed` are direct fields on the comment from CommentUpdate,
@@ -183,11 +185,15 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
const hasTitle = title && title.trim().length > 0; const hasTitle = title && title.trim().length > 0;
const hasContent = content && content.trim().length > 0; const hasContent = content && content.trim().length > 0;
const hasLink = link && link.length > 0; const hasLink = link && link.length > 0;
const isReply = !!parentCid;
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
const rawExcerpt = const rawExcerpt =
(hasTitle && hasContent ? `${title}: ${content}` : null) ||
(hasTitle ? title : null) || (hasTitle ? title : null) ||
(hasContent ? content : null) || (hasContent ? content : null) ||
(hasLink ? link : null) || (hasLink ? link : null) ||
(getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link) ? t('image') : null) || (hasThumbnail ? t('image') : null) ||
t('no_content'); t('no_content');
// Only truncate excerpt on desktop, allow wrapping on mobile // Only truncate excerpt on desktop, allow wrapping on mobile
const excerpt = !isMobile && rawExcerpt.length > 101 ? rawExcerpt.slice(0, 98) + '...' : rawExcerpt; const excerpt = !isMobile && rawExcerpt.length > 101 ? rawExcerpt.slice(0, 98) + '...' : rawExcerpt;
@@ -268,6 +274,8 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
<Tooltip children={<span>{getFormattedDate(timestamp)}</span>} content={getFormattedTimeAgo(timestamp)} /> <Tooltip children={<span>{getFormattedDate(timestamp)}</span>} content={getFormattedTimeAgo(timestamp)} />
)} )}
</div> </div>
<div className={styles.type}>{isReply ? _.capitalize(t('reply')) : _.capitalize(t('post'))}</div>
<div className={styles.image}>{hasThumbnail ? t('yes') : t('no')}</div>
<div className={styles.actions}>{renderActions()}</div> <div className={styles.actions}>{renderActions()}</div>
</div> </div>
); );
@@ -284,7 +292,8 @@ const ModQueueCard = ({ comment }: ModQueueCardProps) => {
const { editedComment } = useEditedComment({ comment }); const { editedComment } = useEditedComment({ comment });
const displayComment = editedComment || comment; const displayComment = editedComment || comment;
const { content, title, timestamp, subplebbitAddress, cid, threadCid, link, thumbnailUrl, linkWidth, linkHeight, removed, approved, number } = displayComment; const { content, title, timestamp, subplebbitAddress, cid, threadCid, link, thumbnailUrl, linkWidth, linkHeight, removed, approved, number, parentCid } =
displayComment;
const alreadyApproved = approved === true; const alreadyApproved = approved === true;
const alreadyRejected = removed === true; const alreadyRejected = removed === true;
@@ -300,11 +309,15 @@ const ModQueueCard = ({ comment }: ModQueueCardProps) => {
const hasTitle = title && title.trim().length > 0; const hasTitle = title && title.trim().length > 0;
const hasContent = content && content.trim().length > 0; const hasContent = content && content.trim().length > 0;
const hasLink = link && link.length > 0; const hasLink = link && link.length > 0;
const isReply = !!parentCid;
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
const rawExcerpt = const rawExcerpt =
(hasTitle && hasContent ? `${title}: ${content}` : null) ||
(hasTitle ? title : null) || (hasTitle ? title : null) ||
(hasContent ? content : null) || (hasContent ? content : null) ||
(hasLink ? link : null) || (hasLink ? link : null) ||
(getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link) ? t('image') : null) || (hasThumbnail ? t('image') : null) ||
t('no_content'); t('no_content');
const excerpt = rawExcerpt.length > 140 ? rawExcerpt.slice(0, 137) + '...' : rawExcerpt; const excerpt = rawExcerpt.length > 140 ? rawExcerpt.slice(0, 137) + '...' : rawExcerpt;
const threadTargetCid = threadCid || cid; const threadTargetCid = threadCid || cid;
@@ -377,7 +390,8 @@ const ModQueueCard = ({ comment }: ModQueueCardProps) => {
</Link> </Link>
) : ( ) : (
<span title={excerpt}>{excerpt}</span> <span title={excerpt}>{excerpt}</span>
)} )}{' '}
/ {t('type')}: {isReply ? t('reply') : t('post')} / {_.capitalize(t('image'))}: {hasThumbnail ? _.lowerCase(t('yes')) : _.lowerCase(t('no'))}
</div> </div>
{renderActions()} {renderActions()}
</div> </div>
@@ -388,8 +402,18 @@ const ModQueueFeedPost = ({ comment }: { comment: Comment }) => {
const { editedComment } = useEditedComment({ comment }); const { editedComment } = useEditedComment({ comment });
const displayComment = editedComment || comment; const displayComment = editedComment || comment;
const { status, errorMessage, isPublishing, handleApprove, handleReject } = useModQueueActions(displayComment); const { status, errorMessage, isPublishing, handleApprove, handleReject } = useModQueueActions(displayComment);
const { parentCid } = displayComment;
// Fetch parent comment if this is a reply
const parentComment = useComment({ commentCid: parentCid });
return ( return (
<>
{parentCid && parentComment && (
<div style={{ marginBottom: '10px', paddingLeft: '20px', borderLeft: '2px solid var(--mod-queue-alert-color, #ccc)' }}>
<Post post={parentComment} showAllReplies={false} showReplies={false} isModQueue={false} />
</div>
)}
<Post <Post
post={displayComment} post={displayComment}
showAllReplies={false} showAllReplies={false}
@@ -401,6 +425,7 @@ const ModQueueFeedPost = ({ comment }: { comment: Comment }) => {
onApprove={handleApprove} onApprove={handleApprove}
onReject={handleReject} onReject={handleReject}
/> />
</>
); );
}; };
@@ -730,6 +755,8 @@ export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueV
<div className={styles.numberHeader}>No.</div> <div className={styles.numberHeader}>No.</div>
<div className={styles.excerptHeader}>{t('excerpt')}</div> <div className={styles.excerptHeader}>{t('excerpt')}</div>
<div className={styles.timeHeader}>{t('submitted')}</div> <div className={styles.timeHeader}>{t('submitted')}</div>
<div className={styles.typeHeader}>{t('type')}</div>
<div className={styles.imageHeader}>{t('image')}</div>
<div className={styles.actionsHeader}>{t('actions')}</div> <div className={styles.actionsHeader}>{t('actions')}</div>
</div> </div>