import React, { useMemo, useState, useEffect, useCallback, memo } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams, Link } from 'react-router-dom'; import { useFeed, Comment, usePublishCommentModeration, useEditedComment, useCommunity, useAccount } from '@bitsocialnet/bitsocial-react-hooks'; import { Virtuoso } from 'react-virtuoso'; import styles from './mod-queue.module.css'; import useModQueueStore from '../../stores/use-mod-queue-store'; import LoadingEllipsis from '../../components/loading-ellipsis'; import ErrorDisplay from '../../components/error-display/error-display'; import { useFeedStateString } from '../../hooks/use-state-string'; import { getCommunityAddress, getBoardPath, extractDirectoryFromTitle, areSameBoardAddress } from '../../lib/utils/route-utils'; import { useDirectories, DirectoryCommunity } from '../../hooks/use-directories'; import getShortAddress from '../../lib/get-short-address'; import { BOARD_CODE_GROUPS } from '../../constants/board-codes'; import { getHasThumbnail, getCommentMediaInfo } from '../../lib/utils/media-utils'; import { approvePendingCommentModeration, isPendingApprovalAwaiting, isPendingApprovalRejected, rejectPendingCommentModeration, } from '../../lib/utils/pending-approval-moderation'; import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils'; import useFeedResetStore from '../../stores/use-feed-reset-store'; import useChallengesStore from '../../stores/use-challenges-store'; import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils'; import { getCommentCommunityAddress } from '../../lib/utils/comment-utils'; import Tooltip from '../../components/tooltip'; import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses'; import { useCommunityIdentifier, useCommunityIdentifiers } from '../../hooks/use-community-identifiers'; import useIsMobile from '../../hooks/use-is-mobile'; import { useCurrentTime } from '../../hooks/use-current-time'; import { Post } from '../post/post'; import { canAccessBoardModQueue, hasModQueueAccessRole } from '../../lib/utils/mod-access'; import capitalize from 'lodash/capitalize'; import lowerCase from 'lodash/lowerCase'; import { PageFooterDesktop, PageFooterMobile, StyleOnlyFooterFirstRow } from '../../components/footer'; import footerStyles from '../../components/footer/footer.module.css'; const { addChallenge } = useChallengesStore.getState(); /** Path for display: directory code, or full address if has TLD, or shortened for long IPNS keys (no dot) */ const getBoardDisplayPath = (address: string, path: string): string => { if (path !== address) return path; if (address.includes('.')) return address; return getShortAddress(address) || address; }; interface ModQueueViewProps { boardIdentifier?: string; // If provided, shows queue for single board } interface ModQueueFooterProps { hasMore: boolean; communityAddresses: string[]; } // Defined outside ModQueueView to preserve component identity across renders (Virtuoso optimization) // The useFeedStateString hook is called here instead of in ModQueueView to isolate re-renders // caused by backend IPFS state changes to just this footer component const ModQueueFooter = ({ hasMore, communityAddresses }: ModQueueFooterProps) => { const { t } = useTranslation(); const loadingStateString = useFeedStateString(communityAddresses) || t('loading'); return hasMore ? (
) : null; }; interface ModQueueRowProps { comment: Comment; isOdd?: boolean; showBoard?: boolean; /** Board path for URLs (directory code or full address) */ boardPath: string | undefined; /** Board path for display (shortened when long IPNS key with no TLD) */ boardDisplayPath: string | undefined; } // Track which action was initiated to show appropriate completion message type ModerationAction = 'approve' | 'reject' | null; interface ModQueueActionState { status: 'approved' | 'rejected' | 'failed' | null; errorMessage?: string; isPublishing: boolean; handleApprove: () => Promise; handleReject: () => Promise; } interface ModQueueActionsProps { status: 'approved' | 'rejected' | 'failed' | null; errorMessage?: string; isPublishing: boolean; handleApprove: () => Promise; handleReject: () => Promise; variant: 'row' | 'card'; } const ModQueueActions = ({ status, errorMessage, isPublishing, handleApprove, handleReject, variant }: ModQueueActionsProps) => { const { t } = useTranslation(); if (status === 'approved') { const content = {t('approved')}; return variant === 'card' ?
{content}
: content; } if (status === 'rejected') { const content = {t('rejected')}; return variant === 'card' ?
{content}
: content; } if (status === 'failed') { const content = ( {t('failed')} {errorMessage ? `: ${errorMessage}` : ''} ); return variant === 'card' ?
{content}
: content; } if (isPublishing) { const content = ; return variant === 'card' ?
{content}
: content; } const buttons = variant === 'row' ? (
[ ] [ ]
) : (
); return buttons; }; const useModQueueActions = (comment: Comment): ModQueueActionState => { const { t } = useTranslation(); const { cid, approved, removed, pendingApproval } = comment || {}; const communityAddress = getCommentCommunityAddress(comment); const [initiatedAction, setInitiatedAction] = useState(null); const alreadyApproved = approved === true; const alreadyRejected = isPendingApprovalRejected({ approved, removed, pendingApproval }); const { publishCommentModeration: approve, state: approveState, error: approveError, } = usePublishCommentModeration({ commentCid: cid, communityAddress, commentModeration: approvePendingCommentModeration, onChallenge: async (...args: any) => { addChallenge([...args, comment]); }, onChallengeVerification: async (challengeVerification, comment) => { alertChallengeVerificationFailed(challengeVerification, comment); }, onError: (error: Error) => { console.error('Approve failed:', error); }, }); const { publishCommentModeration: reject, state: rejectState, error: rejectError, } = usePublishCommentModeration({ commentCid: cid, communityAddress, commentModeration: rejectPendingCommentModeration, onChallenge: async (...args: any) => { addChallenge([...args, comment]); }, onChallengeVerification: async (challengeVerification, comment) => { alertChallengeVerificationFailed(challengeVerification, comment); }, onError: (error: Error) => { console.error('Reject failed:', error); }, }); const handleApprove = useCallback(async () => { const confirm = window.confirm(t('double_confirm')); if (!confirm) { return; } setInitiatedAction('approve'); try { await approve(); } catch (e) { console.error(e); } }, [approve, t]); const handleReject = useCallback(async () => { const confirm = window.confirm(t('double_confirm')); if (!confirm) { return; } setInitiatedAction('reject'); try { await reject(); } catch (e) { console.error(e); } }, [reject, t]); const isApproving = initiatedAction === 'approve' && approveState !== 'initializing' && approveState !== 'succeeded' && approveState !== 'failed'; const isRejecting = initiatedAction === 'reject' && rejectState !== 'initializing' && rejectState !== 'succeeded' && rejectState !== 'failed'; const isPublishing = isApproving || isRejecting; const approveSucceeded = initiatedAction === 'approve' && approveState === 'succeeded'; const rejectSucceeded = initiatedAction === 'reject' && rejectState === 'succeeded'; const approveFailed = initiatedAction === 'approve' && approveState === 'failed'; const rejectFailed = initiatedAction === 'reject' && rejectState === 'failed'; const status = alreadyApproved || approveSucceeded ? 'approved' : alreadyRejected || rejectSucceeded ? 'rejected' : approveFailed || rejectFailed ? 'failed' : null; const errorMessage = approveFailed ? approveError?.message : rejectFailed ? rejectError?.message : undefined; return { status, errorMessage, isPublishing, handleApprove, handleReject }; }; const ModQueueRow = memo(({ comment, isOdd = false, showBoard = false, boardPath, boardDisplayPath }: ModQueueRowProps) => { const { t } = useTranslation(); const { getAlertThresholdSeconds } = useModQueueStore(); const isMobile = useIsMobile(); const currentTime = useCurrentTime(); const { editedComment } = useEditedComment({ comment }); const displayComment = editedComment || comment; const { content, title, timestamp, cid, threadCid, link, thumbnailUrl, linkWidth, linkHeight, number, parentCid } = displayComment; const timeWaiting = currentTime - timestamp; const alertThresholdSeconds = getAlertThresholdSeconds(); const isOverThreshold = timeWaiting > alertThresholdSeconds; // Only show alert animation for comments awaiting approval (not approved or rejected) const isAwaitingApproval = isPendingApprovalAwaiting(displayComment); const { status, errorMessage, isPublishing, handleApprove, handleReject } = useModQueueActions(displayComment); 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) || (hasThumbnail ? t('image') : null) || t('no_content') ).trim(); // Only truncate excerpt on desktop, allow wrapping on mobile const excerpt = !isMobile && rawExcerpt.length > 101 ? rawExcerpt.slice(0, 98) + '...' : rawExcerpt; const threadTargetCid = threadCid || cid; const postUrl = boardPath && threadTargetCid ? `/${boardPath}/thread/${threadTargetCid}` : undefined; const modQueueUrl = boardPath ? `/${boardPath}/mod/queue` : undefined; return (
{number ?? 'N/A'}
{showBoard && (
{modQueueUrl ? /{boardDisplayPath ?? '—'}/ : /{boardDisplayPath ?? '—'}/}
)}
{postUrl ? ( {excerpt} ) : ( {excerpt} )}
{isMobile ? ( // On mobile, show shorter time ago format without tooltip isAwaitingApproval && isOverThreshold ? ( {getFormattedTimeAgo(timestamp)} ) : ( {getFormattedTimeAgo(timestamp)} ) ) : // On desktop, show full date with tooltip isAwaitingApproval && isOverThreshold ? ( <> {getFormattedDate(timestamp)} {' '} ({getFormattedTimeAgo(timestamp)}) ) : ( {getFormattedDate(timestamp)} )}
{isReply ? capitalize(t('reply')) : capitalize(t('post'))}
{hasThumbnail ? t('yes') : t('no')}
); }); ModQueueRow.displayName = 'ModQueueRow'; interface ModQueueCardProps { comment: Comment; showBoard?: boolean; /** Board path for URLs (directory code or full address) */ boardPath: string | undefined; /** Board path for display (shortened when long IPNS key with no TLD) */ boardDisplayPath: string | undefined; } const ModQueueCard = memo(({ comment, showBoard = false, boardPath, boardDisplayPath }: ModQueueCardProps) => { const { t } = useTranslation(); const { getAlertThresholdSeconds } = useModQueueStore(); const currentTime = useCurrentTime(); const { editedComment } = useEditedComment({ comment }); const displayComment = editedComment || comment; const { content, title, timestamp, cid, threadCid, link, thumbnailUrl, linkWidth, linkHeight, number, parentCid } = displayComment; const timeWaiting = currentTime - timestamp; const alertThresholdSeconds = getAlertThresholdSeconds(); const isOverThreshold = timeWaiting > alertThresholdSeconds; const isAwaitingApproval = isPendingApprovalAwaiting(displayComment); const { status, errorMessage, isPublishing, handleApprove, handleReject } = useModQueueActions(displayComment); 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) || (hasThumbnail ? t('image') : null) || t('no_content') ).trim(); const excerpt = rawExcerpt.length > 140 ? rawExcerpt.slice(0, 137) + '...' : rawExcerpt; const threadTargetCid = threadCid || cid; const postUrl = boardPath && threadTargetCid ? `/${boardPath}/thread/${threadTargetCid}` : undefined; const modQueueUrl = boardPath ? `/${boardPath}/mod/queue` : undefined; return (
No. {number ?? 'N/A'} {showBoard && boardPath && ( <> {modQueueUrl ? /{boardDisplayPath}/ : /{boardDisplayPath}/} )} {isAwaitingApproval && isOverThreshold ? ( <> {getFormattedDate(timestamp)} ({getFormattedTimeAgo(timestamp)}) ) : ( getFormattedDate(timestamp) )}
{t('excerpt')}:{' '} {postUrl ? ( {excerpt} ) : ( {excerpt} )}{' '} / {t('type')}: {isReply ? t('reply') : t('post')} / {capitalize(t('image'))}: {hasThumbnail ? lowerCase(t('yes')) : lowerCase(t('no'))}
); }); ModQueueCard.displayName = 'ModQueueCard'; const ModQueueFeedPost = ({ comment }: { comment: Comment }) => { const { editedComment } = useEditedComment({ comment }); const displayComment = editedComment || comment; const { status, errorMessage, isPublishing, handleApprove, handleReject } = useModQueueActions(displayComment); return ( ); }; interface ModQueueBoardSummaryProps { feed: Comment[]; directories: DirectoryCommunity[]; accountCommunityAddresses: string[]; } const findBoardAddressByCode = (code: string, dirs: DirectoryCommunity[]): string | null => { const entry = dirs.find((sub) => { if (!sub.title) return false; const directory = extractDirectoryFromTitle(sub.title); return directory === code; }); return entry?.address || null; }; const ModQueueBoardSummary = ({ feed, directories, accountCommunityAddresses }: ModQueueBoardSummaryProps) => { const { t } = useTranslation(); const { selectedBoardFilter, setSelectedBoardFilter, getAlertThresholdSeconds } = useModQueueStore(); const currentTime = useCurrentTime(); const alertThresholdSeconds = getAlertThresholdSeconds(); const modAddressSet = useMemo(() => new Set(accountCommunityAddresses), [accountCommunityAddresses]); const boardCounts = useMemo(() => { const counts = new Map(); for (const address of accountCommunityAddresses) { counts.set(address, { normal: 0, urgent: 0 }); } for (const item of feed) { const addr = getCommentCommunityAddress(item); if (!addr) continue; const entry = counts.get(addr); if (!entry) continue; const isAwaiting = isPendingApprovalAwaiting(item); if (!isAwaiting) continue; const timeWaiting = currentTime - (item.timestamp ?? 0); const isUrgent = timeWaiting > alertThresholdSeconds; if (isUrgent) entry.urgent++; else entry.normal++; } return counts; }, [feed, accountCommunityAddresses, currentTime, alertThresholdSeconds]); const { totalNormal, totalUrgent } = useMemo(() => { let normal = 0; let urgent = 0; for (const entry of boardCounts.values()) { normal += entry.normal; urgent += entry.urgent; } return { totalNormal: normal, totalUrgent: urgent }; }, [boardCounts]); // Order: All first, then BOARD_CODE_GROUPS order (directory boards), then non-directory boards const orderedAddresses = useMemo(() => { const ordered: string[] = []; const seen = new Set(); for (const group of BOARD_CODE_GROUPS) { for (const code of group) { const address = findBoardAddressByCode(code, directories); if (address && modAddressSet.has(address) && !seen.has(address)) { ordered.push(address); seen.add(address); } } } // Directory boards not in BOARD_CODE_GROUPS (custom dirs) for (const addr of accountCommunityAddresses) { const path = getBoardPath(addr, directories); if (path !== addr && !seen.has(addr)) { ordered.push(addr); seen.add(addr); } } // Non-directory boards (own category, like subscriptions in boardsbar) for (const addr of accountCommunityAddresses) { if (!seen.has(addr)) { ordered.push(addr); } } return ordered; }, [accountCommunityAddresses, directories, modAddressSet]); const handleSelectAll = useCallback(() => setSelectedBoardFilter(null), [setSelectedBoardFilter]); const handleSelectBoard = useCallback((address: string) => setSelectedBoardFilter(address), [setSelectedBoardFilter]); if (accountCommunityAddresses.length === 0) { return null; } const renderCount = (normal: number, urgent: number) => { const total = normal + urgent; if (total === 0) return null; return ( ( {urgent > 0 && normal > 0 ? ( <> {normal} +{urgent} ) : urgent > 0 ? ( {urgent} ) : ( {total} )} ) ); }; return ( {orderedAddresses.map((address) => { const boardPath = getBoardPath(address, directories); const isInDirectory = boardPath !== address; const displayText = isInDirectory ? boardPath : address.endsWith('.eth') || address.endsWith('.sol') ? address : getShortAddress(address) || address; const isSelected = selectedBoardFilter === address; const { normal, urgent } = boardCounts.get(address) ?? { normal: 0, urgent: 0 }; return ( {' / '} ); })} ); }; interface ModQueueButtonProps { boardIdentifier?: string; isMobile?: boolean; } interface ModQueueCountItemProps { comment: Comment; alertThresholdSeconds: number; onStatusChange: (cid: string, status: { awaiting: boolean; urgent: boolean }) => void; } const ModQueueCountItem = ({ comment, alertThresholdSeconds, onStatusChange }: ModQueueCountItemProps) => { const { editedComment } = useEditedComment({ comment }); const displayComment = editedComment || comment; const currentTime = useCurrentTime(); const { cid, timestamp } = displayComment; const isAwaiting = isPendingApprovalAwaiting(displayComment); const timeWaiting = currentTime - timestamp; const isUrgent = isAwaiting && timeWaiting > alertThresholdSeconds; useEffect(() => { onStatusChange(cid, { awaiting: isAwaiting, urgent: isUrgent }); }, [cid, isAwaiting, isUrgent, onStatusChange]); return null; }; interface ModQueueButtonContentProps { feed: Comment[]; alertThresholdSeconds: number; boardIdentifier?: string; isMobile?: boolean; } const ModQueueButtonContent = ({ feed, alertThresholdSeconds, boardIdentifier, isMobile }: ModQueueButtonContentProps) => { const { t } = useTranslation(); const [statusMap, setStatusMap] = useState>(new Map()); const handleStatusChange = React.useCallback((cid: string, status: { awaiting: boolean; urgent: boolean }) => { setStatusMap((prev) => { const next = new Map(prev); next.set(cid, status); return next; }); }, []); // Clean up stale entries when comments leave the feed to prevent memory leaks const feedCids = useMemo(() => new Set(feed.map((item) => item.cid)), [feed]); useEffect(() => { setStatusMap((prev) => { const staleKeys = [...prev.keys()].filter((cid) => !feedCids.has(cid)); if (staleKeys.length === 0) return prev; const next = new Map(prev); for (const key of staleKeys) { next.delete(key); } return next; }); }, [feedCids]); const { normalCount, urgentCount } = useMemo(() => { let normal = 0; let urgent = 0; for (const { awaiting, urgent: isUrgent } of statusMap.values()) { if (awaiting) { if (isUrgent) urgent++; else normal++; } } return { normalCount: normal, urgentCount: urgent }; }, [statusMap]); const totalCount = normalCount + urgentCount; const to = boardIdentifier ? `/${boardIdentifier}/mod/queue` : '/mod/queue'; const buttonContent = ( ); return ( <> {feed.map((item) => ( ))} {isMobile ? buttonContent : <>[{buttonContent}]} ); }; export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProps) => { const { getAlertThresholdSeconds } = useModQueueStore(); const account = useAccount(); const accountAddress = account?.author?.address; const accountCommunityAddresses = useAccountCommunityAddresses(); const directories = useDirectories(); const resolvedAddress = useMemo(() => { if (boardIdentifier) { return getCommunityAddress(boardIdentifier, directories); } return undefined; }, [boardIdentifier, directories]); const resolvedCommunity = useCommunityIdentifier(resolvedAddress); const community = useCommunity(resolvedCommunity ? { community: resolvedCommunity } : undefined); const communityAddresses = useMemo(() => { if (resolvedAddress) { return [resolvedAddress]; } return accountCommunityAddresses; }, [resolvedAddress, accountCommunityAddresses]); const accountRole = accountAddress ? community?.roles?.[accountAddress]?.role : undefined; const hasBoardAccessFromAccountCommunities = resolvedAddress ? accountCommunityAddresses.some((address) => areSameBoardAddress(address, resolvedAddress)) : accountCommunityAddresses.length > 0; const hasBoardAccess = canAccessBoardModQueue({ boardAddress: resolvedAddress, accountCommunityAddresses, accountRole, }); const isBoardAccessLoading = Boolean(resolvedAddress) && Boolean(accountAddress) && !hasModQueueAccessRole(accountRole) && !hasBoardAccessFromAccountCommunities && community?.state !== 'succeeded' && community?.state !== 'failed'; // Only fetch if we have addresses to check and permissions const shouldFetch = !isBoardAccessLoading && communityAddresses.length > 0 && hasBoardAccess; const feedAddresses = shouldFetch ? communityAddresses : []; const feedCommunities = useCommunityIdentifiers(feedAddresses); const feedOptions = useMemo( () => ({ communities: feedCommunities, modQueue: ['pendingApproval'], sortType: 'new' as const, postsPerPage: 200, }), [feedCommunities], ); const { feed } = useFeed(feedOptions); if (!shouldFetch || communityAddresses.length === 0) { return null; } const alertThresholdSeconds = getAlertThresholdSeconds(); // Use key to reset statusMap state when switching boards (prevents stale counts from previous board) const contentKey = communityAddresses.join(','); return ; }; const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueViewProps) => { const { t } = useTranslation(); const params = useParams(); const { selectedBoardFilter, viewMode } = useModQueueStore(); const isMobile = useIsMobile(); const accountCommunityAddresses = useAccountCommunityAddresses(); const directories = useDirectories(); const boardIdentifier = propBoardIdentifier || params.boardIdentifier; const resolvedAddress = useMemo(() => { if (boardIdentifier) { return getCommunityAddress(boardIdentifier, directories); } return undefined; }, [boardIdentifier, directories]); const communityAddresses = useMemo(() => { if (resolvedAddress) return [resolvedAddress]; return accountCommunityAddresses; }, [resolvedAddress, accountCommunityAddresses]); const communities = useCommunityIdentifiers(communityAddresses); const communityAddress = communityAddresses[0]; const communityIdentifier = useCommunityIdentifier(communityAddress); const community = useCommunity(communityIdentifier ? { community: communityIdentifier } : undefined); const { error: communityError } = community || {}; const feedOptions = useMemo( () => ({ communities, modQueue: ['pendingApproval'], postsPerPage: 50, }), [communities], ); const { feed, hasMore, loadMore, reset } = useFeed(feedOptions); const filteredFeed = useMemo(() => { if (!selectedBoardFilter) return feed; return feed.filter((item) => getCommentCommunityAddress(item) === selectedBoardFilter); }, [feed, selectedBoardFilter]); const addressToPathMap = useMemo(() => { const map = new Map(); for (const addr of communityAddresses) { map.set(addr, getBoardPath(addr, directories)); } return map; }, [communityAddresses, directories]); const showBoardColumn = !resolvedAddress; const compactRowItemContent = useCallback( (index: number, comment: Comment) => { const commentCommunityAddress = getCommentCommunityAddress(comment); const path = addressToPathMap.get(commentCommunityAddress || '') ?? (commentCommunityAddress ? getBoardPath(commentCommunityAddress, directories) : undefined); return ( ); }, [addressToPathMap, showBoardColumn, directories], ); const compactCardItemContent = useCallback( (_index: number, comment: Comment) => { const commentCommunityAddress = getCommentCommunityAddress(comment); const path = addressToPathMap.get(commentCommunityAddress || '') ?? (commentCommunityAddress ? getBoardPath(commentCommunityAddress, directories) : undefined); return ( ); }, [addressToPathMap, showBoardColumn, directories], ); const setResetFunction = useFeedResetStore((state) => state.setResetFunction); useEffect(() => { setResetFunction(reset); }, [reset, setResetFunction]); // Memoize footer components object to preserve identity across renders (Virtuoso optimization) // Note: useFeedStateString is called inside ModQueueFooter to isolate re-renders from backend state changes const footerComponents = useMemo( () => ({ Footer: () => ( <> {communityError?.message && feed.length === 0 && (
)} ), }), [hasMore, communityAddresses, communityError, feed.length], ); const pageFooter = ( <> } />
); return ( <>
{!resolvedAddress && (
)} {filteredFeed.length === 0 && !hasMore ? (
{t('queue_is_empty')}
) : ( <> {viewMode === 'compact' && !isMobile && ( <>
No.
{!resolvedAddress &&
{t('board')}
}
{t('excerpt')}
{t('submitted')}
{t('type')}
{t('image')}
{t('actions')}
{hasMore ? ( ) : ( <> {filteredFeed.map((comment, index) => { const commentCommunityAddress = getCommentCommunityAddress(comment); const path = addressToPathMap.get(commentCommunityAddress || '') ?? (commentCommunityAddress ? getBoardPath(commentCommunityAddress, directories) : undefined); return ( ); })} {communityError?.message && feed.length === 0 && (
)} )} )} {viewMode === 'compact' && isMobile && ( <> {hasMore ? ( ) : ( <> {filteredFeed.map((comment) => { const commentCommunityAddress = getCommentCommunityAddress(comment); const path = addressToPathMap.get(commentCommunityAddress || '') ?? (commentCommunityAddress ? getBoardPath(commentCommunityAddress, directories) : undefined); return ( ); })} {communityError?.message && feed.length === 0 && (
)} )} )} {viewMode === 'feed' && ( <> {hasMore ? ( } components={footerComponents} /> ) : ( <> {filteredFeed.map((comment) => ( ))} {communityError?.message && feed.length === 0 && (
)} )} )} )}
{pageFooter} ); }; export default ModQueueView;