import React, { useMemo, useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams, Link } from 'react-router-dom'; import { useAccountSubplebbits, useFeed, Comment, usePublishCommentModeration } from '@plebbit/plebbit-react-hooks'; import { Virtuoso } from 'react-virtuoso'; import { formatDistanceToNow } from 'date-fns'; import styles from './mod-queue.module.css'; import useModQueueStore from '../../stores/use-mod-queue-store'; import { useAccountSubplebbitsWithMetadata } from '../../hooks/use-account-subplebbits-with-metadata'; import LoadingEllipsis from '../../components/loading-ellipsis'; import { useFeedStateString } from '../../hooks/use-state-string'; import { getSubplebbitAddress, getBoardPath } from '../../lib/utils/route-utils'; import { useDefaultSubplebbits, MultisubSubplebbit } from '../../hooks/use-default-subplebbits'; import { useBoardPath } from '../../hooks/use-resolved-subplebbit-address'; import { getHasThumbnail, getCommentMediaInfo } from '../../lib/utils/media-utils'; import useFeedResetStore from '../../stores/use-feed-reset-store'; interface ModQueueViewProps { boardIdentifier?: string; // If provided, shows queue for single board } interface ModQueueRowProps { comment: Comment; showBoardColumn?: boolean; } const ModQueueRow = ({ comment, showBoardColumn = false }: ModQueueRowProps) => { const { t } = useTranslation(); const { alertThresholdHours } = useModQueueStore(); const [isModerating, setIsModerating] = useState(false); const { content, title, timestamp, subplebbitAddress, cid, threadCid, link, thumbnailUrl, linkWidth, linkHeight } = comment; const boardPath = useBoardPath(subplebbitAddress); const timeWaiting = Date.now() / 1000 - timestamp; const isOverThreshold = timeWaiting > alertThresholdHours * 3600; const { publishCommentModeration: approve } = usePublishCommentModeration({ commentCid: cid, subplebbitAddress, commentModeration: { approved: true }, }); const { publishCommentModeration: reject } = usePublishCommentModeration({ commentCid: cid, subplebbitAddress, commentModeration: { removed: true }, }); const handleApprove = async () => { try { setIsModerating(true); await approve(); } catch (e) { console.error(e); } finally { setIsModerating(false); } }; const handleReject = async () => { try { setIsModerating(true); await reject(); } catch (e) { console.error(e); } finally { setIsModerating(false); } }; const excerpt = title || content || (getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link) ? t('image') : t('no_content')); const postUrl = `/${boardPath}/thread/${threadCid || cid}`; return (
{showBoardColumn && (
/{boardPath}/
)}
{excerpt}
{formatDistanceToNow(timestamp * 1000, { addSuffix: false })}
{t('view')}
); }; interface ModQueueBoardFilterProps { subplebbits: MultisubSubplebbit[]; } const ModQueueBoardFilter = ({ subplebbits }: ModQueueBoardFilterProps) => { const { t } = useTranslation(); const { selectedBoardFilter, setSelectedBoardFilter } = useModQueueStore(); const handleChange = (e: React.ChangeEvent) => { const value = e.target.value; setSelectedBoardFilter(value === '' ? null : value); }; if (!subplebbits || subplebbits.length === 0) { return null; } return (
); }; interface ModQueueButtonProps { boardIdentifier?: string; isMobile?: boolean; } export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProps) => { const { t } = useTranslation(); const { alertThresholdHours } = useModQueueStore(); const { accountSubplebbits } = useAccountSubplebbits(); const accountSubplebbitAddresses = useMemo(() => Object.keys(accountSubplebbits || {}), [accountSubplebbits]); const defaultSubplebbits = useDefaultSubplebbits(); // Resolve boardIdentifier to address if it exists const resolvedAddress = useMemo(() => { if (boardIdentifier) { return getSubplebbitAddress(boardIdentifier, defaultSubplebbits); } return undefined; }, [boardIdentifier, defaultSubplebbits]); const subplebbitAddresses = useMemo(() => { if (resolvedAddress) { return [resolvedAddress]; } return accountSubplebbitAddresses; }, [resolvedAddress, accountSubplebbitAddresses]); // If specific board, check if user is mod using resolved address const isModOfBoard = resolvedAddress ? accountSubplebbitAddresses.includes(resolvedAddress) : true; // Only fetch if we have addresses to check and permissions const shouldFetch = subplebbitAddresses.length > 0 && isModOfBoard; const { feed } = useFeed({ subplebbitAddresses: shouldFetch ? subplebbitAddresses : [], modQueue: ['pendingApproval'], postsPerPage: 100, // Fetch enough to check timestamps }); if (!shouldFetch || subplebbitAddresses.length === 0) { return null; } // Separate comments into normal and urgent based on threshold // Match ModQueueRow logic: use > (strictly greater) to be consistent const { normalCount, urgentCount } = useMemo(() => { const thresholdSeconds = alertThresholdHours * 3600; const now = Date.now() / 1000; let normal = 0; let urgent = 0; for (const item of feed) { const timeWaiting = now - item.timestamp; if (timeWaiting > thresholdSeconds) { urgent++; } else { normal++; } } return { normalCount: normal, urgentCount: urgent }; }, [feed, alertThresholdHours]); const totalCount = normalCount + urgentCount; // Use boardIdentifier for the route, but resolvedAddress for mod check const to = boardIdentifier ? `/${boardIdentifier}/queue` : '/mod/queue'; return ( ); }; export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueViewProps) => { const { t } = useTranslation(); const params = useParams(); const { selectedBoardFilter, alertThresholdHours, setAlertThresholdHours } = useModQueueStore(); const { accountSubplebbits } = useAccountSubplebbits(); const accountSubplebbitAddresses = Object.keys(accountSubplebbits); const defaultSubplebbits = useDefaultSubplebbits(); const boardIdentifier = propBoardIdentifier || params.boardIdentifier; // Resolve boardIdentifier to address if it exists const resolvedAddress = useMemo(() => { if (boardIdentifier) { return getSubplebbitAddress(boardIdentifier, defaultSubplebbits); } return undefined; }, [boardIdentifier, defaultSubplebbits]); // Get metadata for filter dropdown const subplebbitsWithMetadata = useAccountSubplebbitsWithMetadata(); const subplebbitAddresses = useMemo(() => { if (resolvedAddress) { return [resolvedAddress]; } if (selectedBoardFilter) { return [selectedBoardFilter]; } return accountSubplebbitAddresses; }, [resolvedAddress, selectedBoardFilter, accountSubplebbitAddresses]); const { feed, hasMore, loadMore, reset } = useFeed({ subplebbitAddresses, modQueue: ['pendingApproval'], postsPerPage: 50, }); // Register reset function with feed reset store so refresh button works const setResetFunction = useFeedResetStore((state) => state.setResetFunction); useEffect(() => { setResetFunction(reset); }, [reset, setResetFunction]); const loadingStateString = useFeedStateString(subplebbitAddresses) || t('loading'); const showBoardColumn = !resolvedAddress && !selectedBoardFilter; const Footer = () => { return hasMore ? (
) : null; }; return (
{t('moderation_queue')}
{!resolvedAddress && } {feed.length === 0 && !hasMore ? (
{t('queue_is_empty')}
) : ( <>
{showBoardColumn &&
{t('board')}
}
{t('excerpt')}
{t('waiting')}
{t('actions')}
} components={{ Footer }} /> )}
); }; export default ModQueueView;