diff --git a/public/translations/ar/default.json b/public/translations/ar/default.json index 71cd71a6..53384847 100644 --- a/public/translations/ar/default.json +++ b/public/translations/ar/default.json @@ -272,5 +272,9 @@ "alert_threshold": "عتبة التنبيه", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/bn/default.json b/public/translations/bn/default.json index dcca3f86..45625c5c 100644 --- a/public/translations/bn/default.json +++ b/public/translations/bn/default.json @@ -272,5 +272,9 @@ "alert_threshold": "সতর্কতা থ্রেশহোল্ড", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/cs/default.json b/public/translations/cs/default.json index 88b282ab..68185f25 100644 --- a/public/translations/cs/default.json +++ b/public/translations/cs/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Práh upozornění", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/da/default.json b/public/translations/da/default.json index 3f858add..61edc614 100644 --- a/public/translations/da/default.json +++ b/public/translations/da/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Alarmtærskel", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/de/default.json b/public/translations/de/default.json index 5dc5e378..da027f7d 100644 --- a/public/translations/de/default.json +++ b/public/translations/de/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Alarmschwellenwert", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/el/default.json b/public/translations/el/default.json index f5658b56..5d26b491 100644 --- a/public/translations/el/default.json +++ b/public/translations/el/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Όριο ειδοποίησης", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/en/default.json b/public/translations/en/default.json index 78f5c28e..1bdb3f20 100644 --- a/public/translations/en/default.json +++ b/public/translations/en/default.json @@ -272,5 +272,9 @@ "approved": "Approved", "rejected": "Rejected", "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" } diff --git a/public/translations/es/default.json b/public/translations/es/default.json index 619021df..3070b92c 100644 --- a/public/translations/es/default.json +++ b/public/translations/es/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Umbral de alerta", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/fa/default.json b/public/translations/fa/default.json index 90c0836e..eee42a7d 100644 --- a/public/translations/fa/default.json +++ b/public/translations/fa/default.json @@ -272,5 +272,9 @@ "alert_threshold": "آستانه هشدار", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/fi/default.json b/public/translations/fi/default.json index 39a05080..8e3956b3 100644 --- a/public/translations/fi/default.json +++ b/public/translations/fi/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Hälytyskynnys", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/fil/default.json b/public/translations/fil/default.json index ab6c0d5f..b89351bc 100644 --- a/public/translations/fil/default.json +++ b/public/translations/fil/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Alert threshold", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/fr/default.json b/public/translations/fr/default.json index 183d1af6..3cae3b5a 100644 --- a/public/translations/fr/default.json +++ b/public/translations/fr/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Seuil d'alerte", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/he/default.json b/public/translations/he/default.json index fc3d917c..4daa7ecc 100644 --- a/public/translations/he/default.json +++ b/public/translations/he/default.json @@ -272,5 +272,9 @@ "alert_threshold": "סף התרעה", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/hi/default.json b/public/translations/hi/default.json index 03395dae..33e0904d 100644 --- a/public/translations/hi/default.json +++ b/public/translations/hi/default.json @@ -272,5 +272,9 @@ "alert_threshold": "सतर्कता सीमा", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/hu/default.json b/public/translations/hu/default.json index 7a3ec449..384675c9 100644 --- a/public/translations/hu/default.json +++ b/public/translations/hu/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Riasztási küszöb", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/id/default.json b/public/translations/id/default.json index e8697747..c5bd27ea 100644 --- a/public/translations/id/default.json +++ b/public/translations/id/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Ambang peringatan", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/it/default.json b/public/translations/it/default.json index 3ee2f207..459605ac 100644 --- a/public/translations/it/default.json +++ b/public/translations/it/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Soglia di allerta", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/ja/default.json b/public/translations/ja/default.json index 917bb4b8..11b28a9a 100644 --- a/public/translations/ja/default.json +++ b/public/translations/ja/default.json @@ -272,5 +272,9 @@ "alert_threshold": "アラート閾値", "modQueue.viewLabel": "View", "modQueue.compact": "Compact", - "modQueue.feed": "Feed" + "modQueue.feed": "Feed", + "type": "タイプ", + "yes": "はい", + "no": "いいえ", + "pending_mod_approval": "モデレーターの承認待ち、ユーザーには表示されません" } diff --git a/public/translations/ko/default.json b/public/translations/ko/default.json index e3157c3b..655a10aa 100644 --- a/public/translations/ko/default.json +++ b/public/translations/ko/default.json @@ -272,5 +272,9 @@ "alert_threshold": "알림 임계값", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/mr/default.json b/public/translations/mr/default.json index 02beacf5..a1b11e7d 100644 --- a/public/translations/mr/default.json +++ b/public/translations/mr/default.json @@ -272,5 +272,9 @@ "alert_threshold": "सतर्कता मर्यादा", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/nl/default.json b/public/translations/nl/default.json index 8cf7e2ad..23b652dc 100644 --- a/public/translations/nl/default.json +++ b/public/translations/nl/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Drempelwaarde voor waarschuwing", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/no/default.json b/public/translations/no/default.json index c4171f0b..8e6ec069 100644 --- a/public/translations/no/default.json +++ b/public/translations/no/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Terskel for varsel", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/pl/default.json b/public/translations/pl/default.json index 780f71f5..05ab8909 100644 --- a/public/translations/pl/default.json +++ b/public/translations/pl/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Próg alertu", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/pt/default.json b/public/translations/pt/default.json index 16dcbde8..a0f5eb06 100644 --- a/public/translations/pt/default.json +++ b/public/translations/pt/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Limite de alerta", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/ro/default.json b/public/translations/ro/default.json index 4525740a..f69b3e7c 100644 --- a/public/translations/ro/default.json +++ b/public/translations/ro/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Prag de alertă", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/ru/default.json b/public/translations/ru/default.json index 5eb28ec0..8b4d4ad5 100644 --- a/public/translations/ru/default.json +++ b/public/translations/ru/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Порог предупреждения", "modQueue.viewLabel": "View", "modQueue.compact": "Compact", - "modQueue.feed": "Feed" + "modQueue.feed": "Feed", + "type": "Тип", + "yes": "Да", + "no": "Нет", + "pending_mod_approval": "Ожидает одобрения модератора, не видно пользователям" } diff --git a/public/translations/sq/default.json b/public/translations/sq/default.json index 02ecec76..a113ed56 100644 --- a/public/translations/sq/default.json +++ b/public/translations/sq/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Pragu i alarmit", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/sv/default.json b/public/translations/sv/default.json index 95fef456..7d82db67 100644 --- a/public/translations/sv/default.json +++ b/public/translations/sv/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Tröskelvärde för varning", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/te/default.json b/public/translations/te/default.json index 6e43a067..e7b546a8 100644 --- a/public/translations/te/default.json +++ b/public/translations/te/default.json @@ -272,5 +272,9 @@ "alert_threshold": "హెచ్చరిక థ్రెషోల్డ్", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/th/default.json b/public/translations/th/default.json index ca3e3dff..a9748681 100644 --- a/public/translations/th/default.json +++ b/public/translations/th/default.json @@ -272,5 +272,9 @@ "alert_threshold": "เกณฑ์การแจ้งเตือน", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/tr/default.json b/public/translations/tr/default.json index b3f675b4..778daa1b 100644 --- a/public/translations/tr/default.json +++ b/public/translations/tr/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Uyarı eşiği", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/uk/default.json b/public/translations/uk/default.json index 7089e305..e0d3169e 100644 --- a/public/translations/uk/default.json +++ b/public/translations/uk/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Поріг попередження", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/ur/default.json b/public/translations/ur/default.json index bd63e860..ef6384c1 100644 --- a/public/translations/ur/default.json +++ b/public/translations/ur/default.json @@ -272,5 +272,9 @@ "alert_threshold": "انتباہ کی حد", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/vi/default.json b/public/translations/vi/default.json index b9ea62d3..c4e450d2 100644 --- a/public/translations/vi/default.json +++ b/public/translations/vi/default.json @@ -272,5 +272,9 @@ "alert_threshold": "Ngưỡng cảnh báo", "modQueue.viewLabel": "View", "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" } diff --git a/public/translations/zh/default.json b/public/translations/zh/default.json index 64825a5e..784eb8ee 100644 --- a/public/translations/zh/default.json +++ b/public/translations/zh/default.json @@ -272,5 +272,9 @@ "alert_threshold": "警报阈值", "modQueue.viewLabel": "View", "modQueue.compact": "Compact", - "modQueue.feed": "Feed" + "modQueue.feed": "Feed", + "type": "类型", + "yes": "是", + "no": "否", + "pending_mod_approval": "等待版主批准,用户不可见" } diff --git a/src/components/comment-content/comment-content.tsx b/src/components/comment-content/comment-content.tsx index 5a530620..10fb4700 100644 --- a/src/components/comment-content/comment-content.tsx +++ b/src/components/comment-content/comment-content.tsx @@ -76,7 +76,7 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => { {pendingApproval && ( <>
- (Pending mod approval, not visible to users) + ({t('pending_mod_approval')}) )} {((!isInPostView && content?.length > 1000 && !showFullComment) || (isInPostView && content?.length > 2000 && !showFullComment)) && ( diff --git a/src/components/post-desktop/post-desktop.tsx b/src/components/post-desktop/post-desktop.tsx index 7f3fd0f8..166d60ac 100644 --- a/src/components/post-desktop/post-desktop.tsx +++ b/src/components/post-desktop/post-desktop.tsx @@ -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 = ({ )} )} + {shouldShowPendingApprovalButtons && ( + + {pendingStatus === 'approved' ? ( + {t('approved')} + ) : pendingStatus === 'rejected' ? ( + {t('rejected')} + ) : pendingStatus === 'failed' ? ( + + {t('failed')} + {pendingErrorMessage ? `: ${pendingErrorMessage}` : ''} + + ) : isPublishingPending ? ( + + ) : ( + <> + + [ + + ] + + + [ + + ] + + + )} + + )} {!(removed || deleted) && !isModQueue && } {cid && diff --git a/src/components/post-mobile/post-mobile.tsx b/src/components/post-mobile/post-mobile.tsx index 98559751..f8ecc8ba 100644 --- a/src/components/post-mobile/post-mobile.tsx +++ b/src/components/post-mobile/post-mobile.tsx @@ -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 )} + {shouldShowPendingApprovalButtons && ( +
+ {pendingStatus === 'approved' ? ( + {t('approved')} + ) : pendingStatus === 'rejected' ? ( + {t('rejected')} + ) : pendingStatus === 'failed' ? ( + + {t('failed')} + {pendingErrorMessage ? `: ${pendingErrorMessage}` : ''} + + ) : isPublishingPending ? ( + + ) : ( + <> + + + + )} +
+ )} diff --git a/src/views/mod-queue/mod-queue.module.css b/src/views/mod-queue/mod-queue.module.css index 2fa9bb2c..03344f61 100644 --- a/src/views/mod-queue/mod-queue.module.css +++ b/src/views/mod-queue/mod-queue.module.css @@ -66,6 +66,8 @@ .numberHeader, .excerptHeader, .timeHeader, +.typeHeader, +.imageHeader, .actionsHeader { background: var(--mod-queue-header-background-color); padding: 5px; @@ -80,6 +82,18 @@ flex-grow: 1; } +.typeHeader { + width: 70px; + flex-shrink: 0; + text-align: right; +} + +.imageHeader { + width: 60px; + flex-shrink: 0; + text-align: right; +} + .timeHeader { width: 210px; text-align: right; @@ -124,6 +138,8 @@ .number, .excerpt, .time, +.type, +.image, .actions { padding: 2px 5px; } @@ -135,6 +151,8 @@ :global(body.yotsuba) .rowOdd .number, :global(body.yotsuba) .rowOdd .excerpt, :global(body.yotsuba) .rowOdd .time, +:global(body.yotsuba) .rowOdd .type, +:global(body.yotsuba) .rowOdd .image, :global(body.yotsuba) .rowOdd .actions { background-color: #ede2d4; } @@ -142,6 +160,8 @@ :global(body.yotsuba-b) .rowOdd .number, :global(body.yotsuba-b) .rowOdd .excerpt, :global(body.yotsuba-b) .rowOdd .time, +:global(body.yotsuba-b) .rowOdd .type, +:global(body.yotsuba-b) .rowOdd .image, :global(body.yotsuba-b) .rowOdd .actions { background-color: #e0e5f6; } @@ -149,6 +169,8 @@ :global(body.futaba) .rowOdd .number, :global(body.futaba) .rowOdd .excerpt, :global(body.futaba) .rowOdd .time, +:global(body.futaba) .rowOdd .type, +:global(body.futaba) .rowOdd .image, :global(body.futaba) .rowOdd .actions { background-color: #ede2d4; } @@ -156,6 +178,8 @@ :global(body.burichan) .rowOdd .number, :global(body.burichan) .rowOdd .excerpt, :global(body.burichan) .rowOdd .time, +:global(body.burichan) .rowOdd .type, +:global(body.burichan) .rowOdd .image, :global(body.burichan) .rowOdd .actions { background-color: #e0e5f6; } @@ -163,6 +187,8 @@ :global(body.tomorrow) .rowOdd .number, :global(body.tomorrow) .rowOdd .excerpt, :global(body.tomorrow) .rowOdd .time, +:global(body.tomorrow) .rowOdd .type, +:global(body.tomorrow) .rowOdd .image, :global(body.tomorrow) .rowOdd .actions { background-color: rgba(255, 255, 255, 0.1); } @@ -170,6 +196,8 @@ :global(body.photon) .rowOdd .number, :global(body.photon) .rowOdd .excerpt, :global(body.photon) .rowOdd .time, +:global(body.photon) .rowOdd .type, +:global(body.photon) .rowOdd .image, :global(body.photon) .rowOdd .actions { background-color: #888; } @@ -187,6 +215,18 @@ min-width: 0; } +.type { + width: 70px; + flex-shrink: 0; + text-align: right; +} + +.image { + width: 60px; + flex-shrink: 0; + text-align: right; +} + .excerpt a { text-decoration: var(--post-link-text-decoration); color: var(--post-link-text-color); @@ -397,26 +437,38 @@ span.reject:hover { :global(body.yotsuba) .rowOdd .number, :global(body.yotsuba) .rowOdd .excerpt, :global(body.yotsuba) .rowOdd .time, + :global(body.yotsuba) .rowOdd .type, + :global(body.yotsuba) .rowOdd .image, :global(body.yotsuba) .rowOdd .actions, :global(body.yotsuba-b) .rowOdd .number, :global(body.yotsuba-b) .rowOdd .excerpt, :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.futaba) .rowOdd .number, :global(body.futaba) .rowOdd .excerpt, :global(body.futaba) .rowOdd .time, + :global(body.futaba) .rowOdd .type, + :global(body.futaba) .rowOdd .image, :global(body.futaba) .rowOdd .actions, :global(body.burichan) .rowOdd .number, :global(body.burichan) .rowOdd .excerpt, :global(body.burichan) .rowOdd .time, + :global(body.burichan) .rowOdd .type, + :global(body.burichan) .rowOdd .image, :global(body.burichan) .rowOdd .actions, :global(body.tomorrow) .rowOdd .number, :global(body.tomorrow) .rowOdd .excerpt, :global(body.tomorrow) .rowOdd .time, + :global(body.tomorrow) .rowOdd .type, + :global(body.tomorrow) .rowOdd .image, :global(body.tomorrow) .rowOdd .actions, :global(body.photon) .rowOdd .number, :global(body.photon) .rowOdd .excerpt, :global(body.photon) .rowOdd .time, + :global(body.photon) .rowOdd .type, + :global(body.photon) .rowOdd .image, :global(body.photon) .rowOdd .actions { background-color: transparent; } diff --git a/src/views/mod-queue/mod-queue.tsx b/src/views/mod-queue/mod-queue.tsx index bc520b1d..6bd3ec93 100644 --- a/src/views/mod-queue/mod-queue.tsx +++ b/src/views/mod-queue/mod-queue.tsx @@ -1,7 +1,7 @@ import React, { useMemo, useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; 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 { Virtuoso } from 'react-virtuoso'; import { formatDistanceToNow } from 'date-fns'; @@ -22,6 +22,7 @@ import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-util import Tooltip from '../../components/tooltip'; import useIsMobile from '../../hooks/use-is-mobile'; import { Post } from '../post/post'; +import _ from 'lodash'; const { addChallenge } = useChallengesStore.getState(); @@ -162,7 +163,8 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => { const { editedComment } = useEditedComment({ 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) // 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 hasContent = content && content.trim().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 = + (hasTitle && hasContent ? `${title}: ${content}` : null) || (hasTitle ? title : null) || (hasContent ? content : null) || (hasLink ? link : null) || - (getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link) ? t('image') : null) || + (hasThumbnail ? t('image') : null) || t('no_content'); // Only truncate excerpt on desktop, allow wrapping on mobile const excerpt = !isMobile && rawExcerpt.length > 101 ? rawExcerpt.slice(0, 98) + '...' : rawExcerpt; @@ -268,6 +274,8 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => { {getFormattedDate(timestamp)}} content={getFormattedTimeAgo(timestamp)} /> )} +
{isReply ? _.capitalize(t('reply')) : _.capitalize(t('post'))}
+
{hasThumbnail ? t('yes') : t('no')}
{renderActions()}
); @@ -284,7 +292,8 @@ const ModQueueCard = ({ comment }: ModQueueCardProps) => { const { editedComment } = useEditedComment({ 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 alreadyRejected = removed === true; @@ -300,11 +309,15 @@ const ModQueueCard = ({ comment }: ModQueueCardProps) => { const hasTitle = title && title.trim().length > 0; const hasContent = content && content.trim().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 = + (hasTitle && hasContent ? `${title}: ${content}` : null) || (hasTitle ? title : null) || (hasContent ? content : null) || (hasLink ? link : null) || - (getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link) ? t('image') : null) || + (hasThumbnail ? t('image') : null) || t('no_content'); const excerpt = rawExcerpt.length > 140 ? rawExcerpt.slice(0, 137) + '...' : rawExcerpt; const threadTargetCid = threadCid || cid; @@ -377,7 +390,8 @@ const ModQueueCard = ({ comment }: ModQueueCardProps) => { ) : ( {excerpt} - )} + )}{' '} + / {t('type')}: {isReply ? t('reply') : t('post')} / {_.capitalize(t('image'))}: {hasThumbnail ? _.lowerCase(t('yes')) : _.lowerCase(t('no'))} {renderActions()} @@ -388,19 +402,30 @@ const ModQueueFeedPost = ({ comment }: { comment: Comment }) => { const { editedComment } = useEditedComment({ comment }); const displayComment = editedComment || comment; 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 ( - + <> + {parentCid && parentComment && ( +
+ +
+ )} + + ); }; @@ -730,6 +755,8 @@ export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueV
No.
{t('excerpt')}
{t('submitted')}
+
{t('type')}
+
{t('image')}
{t('actions')}