2026-01-26 16:31:15 +08:00
import { useEffect , useMemo , useRef , useState , useCallback } from 'react' ;
2024-05-16 23:05:00 +02:00
import { Trans , useTranslation } from 'react-i18next' ;
2025-12-27 18:07:23 +01:00
import { Link , useLocation , useNavigationType , useParams } from 'react-router-dom' ;
import { Virtuoso , VirtuosoHandle , StateSnapshot } from 'react-virtuoso' ;
2026-02-13 19:42:38 +08:00
import { Comment , useAuthorAvatar , useEditedComment , useReplies , useAccount , useAccountComment } from '@plebbit/plebbit-react-hooks' ;
2025-05-12 23:54:44 +02:00
import Plebbit from '@plebbit/plebbit-js' ;
2024-06-11 15:49:26 +02:00
import styles from '../../views/post/post.module.css' ;
2025-03-03 23:47:33 +01:00
import { CommentMediaInfo , getDisplayMediaInfoType , getHasThumbnail , getMediaDimensions } from '../../lib/utils/media-utils' ;
2024-08-13 18:27:56 +02:00
import { hashStringToColor , getTextColorForBackground } from '../../lib/utils/post-utils' ;
2024-06-27 11:03:58 +02:00
import { getFormattedDate , getFormattedTimeAgo } from '../../lib/utils/time-utils' ;
2026-02-12 18:41:21 +08:00
import { isValidURL } from '../../lib/utils/url-utils' ;
2026-01-25 16:29:15 +08:00
import { isAllView , isModQueueView , isPendingPostView , isPostPageView , isSubscriptionsView } from '../../lib/utils/view-utils' ;
2026-02-10 14:06:43 +08:00
import { formatUserIDForDisplay } from '../../lib/utils/string-utils' ;
2026-01-25 16:29:15 +08:00
import useModQueueStore from '../../stores/use-mod-queue-store' ;
2026-01-28 16:51:27 +08:00
import { useDirectories } from '../../hooks/use-directories' ;
2025-11-14 14:52:13 +01:00
import { getBoardPath } from '../../lib/utils/route-utils' ;
2024-09-03 10:14:15 +02:00
import useAvatarVisibilityStore from '../../stores/use-avatar-visibility-store' ;
2024-06-28 22:09:11 +02:00
import useAuthorAddressClick from '../../hooks/use-author-address-click' ;
2024-12-05 15:37:23 +01:00
import { useCommentMediaInfo } from '../../hooks/use-comment-media-info' ;
2024-06-13 17:50:05 +02:00
import useCountLinksInReplies from '../../hooks/use-count-links-in-replies' ;
2024-09-03 10:14:15 +02:00
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame' ;
2024-06-13 17:50:05 +02:00
import useHide from '../../hooks/use-hide' ;
2024-06-11 15:49:26 +02:00
import useStateString from '../../hooks/use-state-string' ;
2026-01-19 16:02:45 +01:00
import useScrollToReply from '../../hooks/use-scroll-to-reply' ;
2026-02-18 15:31:01 +08:00
import { useCurrentTime } from '../../hooks/use-current-time' ;
2026-02-09 16:22:18 +08:00
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit' ;
2025-02-22 22:22:42 +01:00
import CommentContent from '../comment-content' ;
2024-06-11 15:49:26 +02:00
import CommentMedia from '../comment-media' ;
2024-06-27 11:03:58 +02:00
import EditMenu from '../edit-menu/edit-menu' ;
2024-06-11 15:49:26 +02:00
import { canEmbed } from '../embed' ;
import LoadingEllipsis from '../loading-ellipsis' ;
import PostMenuDesktop from './post-menu-desktop' ;
2024-06-06 17:28:16 +02:00
import ReplyQuotePreview from '../reply-quote-preview' ;
2024-06-27 11:03:58 +02:00
import Tooltip from '../tooltip' ;
2024-06-11 15:49:26 +02:00
import { PostProps } from '../../views/post/post' ;
2024-07-01 10:26:38 +02:00
import { create } from 'zustand' ;
2026-02-18 15:31:01 +08:00
import capitalize from 'lodash/capitalize' ;
import lowerCase from 'lodash/lowerCase' ;
2024-12-24 17:13:12 +01:00
import { shouldShowSnow } from '../../lib/snow' ;
2025-03-05 12:01:48 +01:00
import useReplyModalStore from '../../stores/use-reply-modal-store' ;
2025-12-05 17:38:17 +01:00
import { selectPostMenuProps } from '../../lib/utils/post-menu-props' ;
2026-01-26 16:31:15 +08:00
import useChallengesStore from '../../stores/use-challenges-store' ;
2026-02-12 17:44:26 +08:00
import useFeedResetStore from '../../stores/use-feed-reset-store' ;
2026-02-10 16:23:06 +08:00
import usePostNumberStore from '../../stores/use-post-number-store' ;
2026-01-26 16:31:15 +08:00
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils' ;
import { usePublishCommentModeration } from '@plebbit/plebbit-react-hooks' ;
2026-02-12 18:41:21 +08:00
import useQuotedByMap from '../../hooks/use-quoted-by-map' ;
2026-02-15 18:46:49 +08:00
import useProgressiveRender from '../../hooks/use-progressive-render' ;
import { REPLIES_PER_PAGE } from '../../lib/constants' ;
2026-01-26 16:31:15 +08:00
const { addChallenge } = useChallengesStore . getState ();
2024-05-16 23:05:00 +02:00
2026-02-18 15:31:01 +08:00
const RepliesFooter = ({ hasMore , loadingString } : { hasMore : boolean ; loadingString : string }) =>
hasMore ? (
< div className = { styles . stateString }>
< LoadingEllipsis string = { loadingString } />
</ div >
) : null ;
2025-12-27 18:07:23 +01:00
// Store scroll position for replies virtuoso across navigations
const lastVirtuosoStates : { [ key : string ] : StateSnapshot } = {};
2024-07-01 10:26:38 +02:00
interface ShowOmittedRepliesState {
2024-07-02 08:55:55 +02:00
showOmittedReplies : Record < string , boolean >;
setShowOmittedReplies : ( cid : string , showOmittedReplies : boolean ) => void ;
2024-07-01 10:26:38 +02:00
}
const useShowOmittedReplies = create < ShowOmittedRepliesState >(( set ) => ({
2024-07-02 08:55:55 +02:00
showOmittedReplies : {},
setShowOmittedReplies : ( cid , showOmittedReplies ) =>
set (( state ) => ({
showOmittedReplies : {
... state . showOmittedReplies ,
[ cid ] : showOmittedReplies ,
},
})),
2024-07-01 10:26:38 +02:00
}));
2026-01-25 16:29:15 +08:00
const PostInfo = ({
post ,
postReplyCount = 0 ,
roles ,
isHidden ,
threadNumber ,
isModQueue ,
modQueueStatus ,
modQueueError ,
isPublishing ,
onApprove ,
onReject ,
2026-02-09 19:56:07 +08:00
quotedByMap ,
2026-02-12 20:00:32 +08:00
directRepliesByParentCid ,
} : PostProps & { directRepliesByParentCid? : Map < string , Comment [] > }) => {
2024-05-16 23:05:00 +02:00
const { t } = useTranslation ();
2025-12-24 18:07:14 +01:00
const { author , cid , deleted , locked , pinned , parentCid , postCid , reason , removed , state , subplebbitAddress , timestamp } = post || {};
2024-06-27 11:03:58 +02:00
const title = post ? . title ? . trim ();
const { address , shortAddress } = author || {};
const displayName = author ? . displayName ? . trim ();
2025-12-23 19:33:46 +01:00
const authorRole = roles ? .[ address ] ? . role ? . replace ( 'moderator' , 'mod' );
2026-02-13 17:48:27 +08:00
const hasFailedState = state === 'failed' ;
2024-05-16 23:05:00 +02:00
const isReply = parentCid ;
2024-07-01 10:26:38 +02:00
const { showOmittedReplies } = useShowOmittedReplies ();
2024-07-12 22:06:14 +02:00
const { imageUrl : avatarImageUrl } = useAuthorAvatar ({ author });
2024-09-03 10:14:15 +02:00
const { hideAvatars } = useAvatarVisibilityStore ();
2026-01-28 16:51:27 +08:00
const directories = useDirectories ();
const boardPath = subplebbitAddress ? getBoardPath ( subplebbitAddress , directories ) : undefined ;
2025-12-05 17:38:17 +01:00
const postMenuProps = useMemo (() => selectPostMenuProps ( post ), [ post ]);
2024-05-16 23:05:00 +02:00
2024-05-17 15:29:03 +02:00
const params = useParams ();
const location = useLocation ();
2024-06-29 11:55:36 +02:00
const isInPostPageView = isPostPageView ( location . pathname , params );
2026-01-25 16:29:15 +08:00
const isInModQueueView = isModQueueView ( location . pathname );
const { getAlertThresholdSeconds } = useModQueueStore ();
2026-02-18 15:31:01 +08:00
const currentTime = useCurrentTime ();
2026-01-26 16:31:15 +08:00
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 ;
2026-01-25 16:29:15 +08:00
// Check if post is awaiting approval and over threshold (for mod queue view)
const approved = post ? . approved ;
const alreadyApproved = approved === true ;
const alreadyRejected = removed === true ;
const isAwaitingApproval = isInModQueueView && ! alreadyApproved && ! alreadyRejected ;
2026-02-18 15:31:01 +08:00
const timeWaiting = timestamp ? currentTime - timestamp : 0 ;
2026-01-25 16:29:15 +08:00
const alertThresholdSeconds = getAlertThresholdSeconds ();
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds ;
2024-05-17 15:29:03 +02:00
2026-02-10 13:45:55 +08:00
const userID = address && Plebbit . getShortAddress ({ address }); // shortened to 8 chars for display; users can verify the full user ID via "Copy user ID" in the post menu to guard against spoofing
2024-09-19 17:33:00 +02:00
const userIDBackgroundColor = hashStringToColor ( userID );
const userIDTextColor = getTextColorForBackground ( userIDBackgroundColor );
2024-06-04 16:06:28 +02:00
2026-02-09 16:22:18 +08:00
const pseudonymityMode = useSubplebbitField ( subplebbitAddress , ( sub ) => sub ? . features ? . pseudonymityMode );
const showUserID = pseudonymityMode !== 'per-reply' ;
2026-02-12 20:00:32 +08:00
const handleUserAddressClick = useAuthorAddressClick ();
const numberOfPostsByAuthor = useMemo (() => {
if ( ! showUserID || deleted || removed || ! shortAddress || ! postCid || typeof document === 'undefined' ) {
return 0 ;
}
return document . querySelectorAll ( `[data-author-address=" ${ shortAddress } "][data-post-cid=" ${ postCid } "]` ). length ;
2026-02-15 14:07:12 +08:00
}, [ showUserID , deleted , removed , shortAddress , postCid , postReplyCount ]);
2026-02-12 20:00:32 +08:00
2024-09-18 17:08:42 +02:00
const { hidden } = useHide ( post );
2025-03-05 12:01:48 +01:00
const { openReplyModal } = useReplyModalStore ();
2025-03-02 12:10:16 +01:00
const onReplyModalClick = () => {
deleted
? isReply
? alert ( t ( 'this_reply_was_deleted' ))
: alert ( t ( 'this_thread_was_deleted' ))
: removed
2025-12-27 17:47:17 +01:00
? isReply
? alert ( t ( 'this_reply_was_removed' ))
: alert ( t ( 'this_thread_was_removed' ))
2025-12-28 22:16:38 +01:00
: openReplyModal && openReplyModal ( cid , post ? . number , postCid , threadNumber , subplebbitAddress );
2025-03-02 12:10:16 +01:00
};
2024-05-16 23:05:00 +02:00
return (
< div className = { styles . postInfo }>
2024-11-04 18:52:15 +01:00
{ isHidden ? parentCid && < span className = { styles . hiddenReplyEditMenuSpacer } /> : < EditMenu post = { post } />}
2024-09-21 13:01:23 +02:00
< span className = {( hidden || (( removed || deleted ) && ! reason )) && parentCid ? styles . postDesktopHidden : '' }>
2024-09-18 17:08:42 +02:00
{ title &&
( title . length <= 75 ? (
< span className = { styles . subject }>{ title } </ span >
2024-06-27 11:03:58 +02:00
) : (
2024-09-18 17:08:42 +02:00
< Tooltip
children = {< span className = { styles . subject }>{ title . slice ( 0 , 75 ) + '(...)' } </ span >}
content = { title . length < 1000 ? title : title.slice ( 0 , 1000 ) + `... ${ t ( 'title_too_long' ) } ` }
/>
2024-08-29 12:56:09 +02:00
))}
2024-09-18 17:08:42 +02:00
< span className = { styles . nameBlock }>
2025-11-28 13:29:31 +01:00
< span className = { ` ${ styles . name } ${ authorRole && ! ( deleted || removed ) && ( authorRole === 'mod' ? styles.capcodeMod : styles.capcodeAdmin ) } ` }>
2024-09-18 17:08:42 +02:00
{ deleted ? (
2026-02-11 19:01:09 +08:00
capitalize ( t ( 'deleted' ))
2024-09-18 17:08:42 +02:00
) : removed ? (
2026-02-11 19:01:09 +08:00
capitalize ( t ( 'removed' ))
2024-09-18 17:08:42 +02:00
) : displayName ? (
displayName . length <= 20 ? (
displayName
) : (
< Tooltip
children = { displayName . slice ( 0 , 20 ) + '(...)' }
content = { displayName . length < 1000 ? displayName : displayName.slice ( 0 , 1000 ) + `... ${ t ( 'display_name_too_long' ) } ` }
/>
)
) : (
2026-02-11 19:01:09 +08:00
capitalize ( t ( 'anonymous' ))
2024-09-18 17:08:42 +02:00
)}
2025-11-28 13:29:31 +01:00
{ ! ( deleted || removed ) && authorRole && (
< span className = 'capitalize' >
{ ' ' }
## Board { authorRole }{ ' ' }
< span
className = { ` ${ styles . capcodeIcon } ${ authorRole === 'mod' ? styles.capcodeModIcon : styles.capcodeAdminIcon } ` }
title = { authorRole === 'mod' ? t ( 'moderator_of_this_board' ) : t ( 'administrator_of_this_board' )}
2025-11-30 22:33:06 +01:00
/>
2025-11-28 13:29:31 +01:00
</ span >
2025-11-30 22:33:06 +01:00
)}{ ' ' }
2024-05-16 23:05:00 +02:00
</ span >
2025-12-23 19:14:25 +01:00
{ author ? . avatar && ! ( deleted || removed ) && ! hideAvatars && avatarImageUrl ? (
< span className = { styles . authorAvatar }>
< img src = { avatarImageUrl } alt = '' />
</ span >
) : null }
2026-02-09 16:22:18 +08:00
{ showUserID && (
<>
( ID : { ' ' }
{ deleted ? (
t ( 'deleted' )
) : removed ? (
t ( 'removed' )
) : (
< Tooltip
children = {
< span
title = { t ( 'highlight_posts' )}
className = { styles . userAddress }
onClick = {() => handleUserAddressClick ( userID , postCid )}
style = {{ backgroundColor : userIDBackgroundColor , color : userIDTextColor }}
>
2026-02-10 14:06:43 +08:00
{ formatUserIDForDisplay ( userID )}
2026-02-09 16:22:18 +08:00
</ span >
}
content = { ` ${ numberOfPostsByAuthor === 1 ? t ( '1_post_by_this_id' ) : t ( 'x_posts_by_this_id' , { number : numberOfPostsByAuthor } )}` }
showTooltip = { isInPostPageView || showOmittedReplies [ postCid ] || ( postReplyCount < 6 && ! pinned )}
/>
)}
){ ' ' }
</>
2024-09-18 17:08:42 +02:00
)}
</ span >
< span className = { styles . dateTime }>
2026-01-25 16:29:15 +08:00
{ isInModQueueView && isOverThreshold ? (
<>
< Tooltip children = {< span >{ getFormattedDate ( timestamp )}</ span >} content = { getFormattedTimeAgo ( timestamp )} /> (
< span className = { styles . alert }>{ getFormattedTimeAgo ( timestamp )}</ span >)
</>
) : (
< Tooltip children = {< span >{ getFormattedDate ( timestamp )}</ span >} content = { getFormattedTimeAgo ( timestamp )} />
)}{ ' ' }
2024-09-18 17:08:42 +02:00
</ span >
< span className = { styles . postNum }>
2025-12-23 19:14:25 +01:00
{ cid ? (
< span className = { styles . postNumLink }>
< Link
to = { boardPath ? `/ ${ boardPath } /thread/ ${ cid } ` : `/thread/ ${ cid } ` }
className = { styles . linkToPost }
title = { t ( 'link_to_post' )}
onClick = {( e ) => ! cid && e . preventDefault ()}
>
2025-12-24 18:07:14 +01:00
No .
2025-12-23 19:14:25 +01:00
</ Link >
< span className = { styles . replyToPost } title = { t ( 'reply_to_post' )} onMouseDown = { onReplyModalClick }>
2025-12-24 18:07:14 +01:00
{ post ? . number || '?' }
2024-09-18 17:08:42 +02:00
</ span >
2025-12-23 19:14:25 +01:00
</ span >
) : (
<>
2026-01-18 18:13:35 +01:00
< span > No .</ span >
2026-02-13 17:48:27 +08:00
< span className = { styles . pendingCid }>{ hasFailedState ? capitalize ( t ( 'failed' )) : capitalize ( t ( 'pending' ))}</ span >
2025-12-23 19:14:25 +01:00
</>
)}
2024-09-18 17:08:42 +02:00
{ pinned && (
< span className = { ` ${ styles . stickyIconWrapper } ${ ! locked && styles . addPaddingBeforeReply } ` }>
< img src = 'assets/icons/sticky.gif' alt = '' className = { styles . stickyIcon } title = { t ( 'sticky' )} />
</ span >
)}
{ locked && (
< span className = { ` ${ styles . closedIconWrapper } ${ styles . addPaddingBeforeReply } ${ pinned && styles . addPaddingInBetween } ` }>
< img src = 'assets/icons/closed.gif' alt = '' className = { styles . closedIcon } title = { t ( 'closed' )} />
</ span >
)}
2026-01-25 16:29:15 +08:00
{ ! isInPostPageView && ! isReply && ! isHidden && ! isModQueue && (
2024-09-18 17:08:42 +02:00
< span className = { styles . replyButton }>
[
2025-12-23 19:14:25 +01:00
< Link to = { boardPath ? `/ ${ boardPath } /thread/ ${ postCid } ` : `/thread/ ${ postCid } ` } onClick = {( e ) => ! cid && e . preventDefault ()}>
2026-02-11 19:01:09 +08:00
{ capitalize ( t ( 'reply' ))}
2024-09-18 17:08:42 +02:00
</ Link >
]
</ span >
)}
2026-01-25 16:29:15 +08:00
{ isModQueue && (
< span className = { styles . modQueueActions }>
{ modQueueStatus === 'approved' ? (
< span className = { styles . modQueueStatusApproved }>{ t ( 'approved' )}</ span >
) : modQueueStatus === 'rejected' ? (
< span className = { styles . modQueueStatusRejected }>{ t ( 'rejected' )}</ span >
) : modQueueStatus === 'failed' ? (
< span className = { styles . modQueueStatusRejected }>
{ t ( 'failed' )}
{ modQueueError ? `: ${ modQueueError } ` : '' }
</ span >
) : isPublishing ? (
< LoadingEllipsis string = { t ( 'publishing' )} />
) : (
<>
< span className = { styles . modQueueButtonWrapper }>
[
< button className = { styles . modQueueActionButton } onClick = { onApprove } disabled = { isPublishing }>
{ t ( 'approve' )}
</ button >
]
</ span >
< span className = { styles . modQueueButtonWrapper }>
[
< button className = { styles . modQueueActionButton } onClick = { onReject } disabled = { isPublishing }>
{ t ( 'reject' )}
</ button >
]
</ span >
</>
)}
</ span >
)}
2026-01-26 16:31:15 +08:00
{ 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 >
)}
2024-09-18 17:08:42 +02:00
</ span >
2026-01-25 16:29:15 +08:00
{ ! ( removed || deleted ) && ! isModQueue && < PostMenuDesktop postMenu = { postMenuProps } />}
2026-02-12 20:00:32 +08:00
{ cid && parentCid && < ReplyBacklinks post = { post } quotedByMap = { quotedByMap } directRepliesByParentCid = { directRepliesByParentCid } />}
{ cid && ! parentCid && < OpBacklinks cid = { cid } quotedByMap = { quotedByMap } />}
2024-05-16 23:05:00 +02:00
</ span >
</ div >
);
};
2026-02-12 20:00:32 +08:00
const ReplyBacklinks = ({
post ,
quotedByMap ,
directRepliesByParentCid ,
} : {
post : Comment ;
quotedByMap? : Map < string , Comment [] >;
directRepliesByParentCid? : Map < string , Comment [] >;
}) => {
const { cid , parentCid } = post || {};
if ( ! cid || ! parentCid ) {
return null ;
}
const directReplies = directRepliesByParentCid ? . get ( cid ) || [];
return (
<>
{ directReplies . map (
( reply : Comment , index : number ) =>
reply ? . parentCid === cid && reply ? . cid && ! ( reply ? . deleted || reply ? . removed ) && < ReplyQuotePreview key = { index } isBacklinkReply = { true } backlinkReply = { reply } />,
)}
{ quotedByMap
? . get ( cid )
? . map (
( reply : Comment , index : number ) =>
reply ? . parentCid !== cid &&
reply ? . cid &&
! ( reply ? . deleted || reply ? . removed ) && < ReplyQuotePreview key = { `qb- ${ index } ` } isBacklinkReply = { true } backlinkReply = { reply } />,
)}
</>
);
};
const OpBacklinks = ({ cid , quotedByMap } : { cid : string ; quotedByMap? : Map < string , Comment [] > }) => (
<>
{ quotedByMap
? . get ( cid )
? . map (
( reply : Comment ) =>
reply ? . cid && ! ( reply ? . deleted || reply ? . removed ) && < ReplyQuotePreview key = { `op-bl- ${ reply . cid } ` } isBacklinkReply = { true } backlinkReply = { reply } />,
)}
</>
);
2025-03-03 23:47:33 +01:00
interface PostMediaProps {
commentMediaInfo : CommentMediaInfo | undefined ;
hasThumbnail : boolean ;
spoiler : boolean ;
deleted : boolean ;
removed : boolean ;
linkHeight : number ;
linkWidth : number ;
parentCid : string ;
2025-11-14 14:32:59 +01:00
subplebbitAddress : string ;
isInAllView : boolean ;
isInSubscriptionsView : boolean ;
2025-03-03 23:47:33 +01:00
}
2025-11-14 14:32:59 +01:00
const PostMedia = ({
commentMediaInfo ,
hasThumbnail ,
spoiler ,
deleted ,
removed ,
linkHeight ,
linkWidth ,
parentCid ,
subplebbitAddress ,
isInAllView ,
isInSubscriptionsView ,
} : PostMediaProps ) => {
2024-05-16 23:05:00 +02:00
const { t } = useTranslation ();
2024-07-20 16:25:20 +02:00
const { url } = commentMediaInfo || {};
let type = commentMediaInfo ? . type ;
const gifFrameUrl = useFetchGifFirstFrame ( url );
2026-01-28 16:51:27 +08:00
const directories = useDirectories ();
2024-07-20 16:25:20 +02:00
if ( type === 'gif' && gifFrameUrl !== null ) {
type = 'animated gif' ;
} else if ( type === 'gif' && gifFrameUrl === null ) {
type = 'static gif' ;
}
2024-05-16 23:05:00 +02:00
const embedUrl = url && new URL ( url );
const [ showThumbnail , setShowThumbnail ] = useState ( true );
2024-09-07 14:11:33 +02:00
const mediaDimensions = getMediaDimensions ( commentMediaInfo );
2026-01-28 16:51:27 +08:00
const boardPath = getBoardPath ( subplebbitAddress , directories );
2025-03-04 16:45:34 +01:00
2024-05-16 23:05:00 +02:00
return (
2024-05-16 23:17:10 +02:00
< div className = { styles . file }>
< div className = { styles . fileText }>
2025-11-14 14:32:59 +01:00
{ subplebbitAddress && ( isInAllView || isInSubscriptionsView ) && boardPath && ! parentCid && (
<>
2025-11-14 14:52:13 +01:00
{ t ( 'board' )} : < Link to = { `/ ${ boardPath } ` }>{ boardPath }</ Link >{ ' ' }
2025-11-14 14:32:59 +01:00
</>
)}
2024-05-16 23:17:10 +02:00
{ t ( 'link' )} : { ' ' }
< a href = { url } target = '_blank' rel = 'noopener noreferrer' >
2026-02-11 19:01:09 +08:00
{ spoiler ? capitalize ( t ( 'spoiler' )) : url && url . length > 30 ? url . slice ( 0 , 30 ) + '...' : url }
2024-05-16 23:17:10 +02:00
</ a >{ ' ' }
2026-02-11 19:01:09 +08:00
({ type && lowerCase ( getDisplayMediaInfoType ( type , t ))}
2024-09-07 14:11:33 +02:00
{ mediaDimensions && `, ${ mediaDimensions } ` })
2024-05-16 23:17:10 +02:00
{ ! showThumbnail && ( type === 'iframe' || type === 'video' || type === 'audio' ) && (
< span >
2025-02-22 23:04:01 +01:00
- [
2024-05-16 23:17:10 +02:00
< span className = { styles . closeMedia } onClick = {() => setShowThumbnail ( true )}>
{ t ( 'close' )}
2024-05-16 23:05:00 +02:00
</ span >
2024-05-16 23:17:10 +02:00
]
</ span >
)}
{ showThumbnail && ! hasThumbnail && embedUrl && canEmbed ( embedUrl ) && (
< span >
2025-03-03 23:47:33 +01:00
- [
2024-05-16 23:17:10 +02:00
< span className = { styles . closeMedia } onClick = {() => setShowThumbnail ( false )}>
{ t ( 'open' )}
2024-05-16 23:05:00 +02:00
</ span >
2024-05-16 23:17:10 +02:00
]
</ span >
2024-05-16 23:05:00 +02:00
)}
</ div >
2024-07-04 21:38:01 +02:00
{( hasThumbnail || ( ! hasThumbnail && ! showThumbnail ) || spoiler ) && (
2024-05-30 14:29:02 +02:00
< div className = { styles . fileThumbnail }>
2024-11-29 21:53:53 +01:00
< CommentMedia
commentMediaInfo = { commentMediaInfo }
2025-03-03 23:47:33 +01:00
deleted = { deleted }
removed = { removed }
linkHeight = { linkHeight }
linkWidth = { linkWidth }
2024-11-29 21:53:53 +01:00
showThumbnail = { showThumbnail }
setShowThumbnail = { setShowThumbnail }
2025-03-03 23:47:33 +01:00
parentCid = { parentCid }
spoiler = { spoiler }
2024-11-29 21:53:53 +01:00
/>
2024-05-30 14:29:02 +02:00
</ div >
2024-05-16 23:17:10 +02:00
)}
</ div >
2024-05-16 23:05:00 +02:00
);
};
2026-02-12 20:00:32 +08:00
const Reply = ({
postReplyCount ,
reply ,
roles ,
threadNumber ,
quotedByMap ,
directRepliesByParentCid ,
} : PostProps & { directRepliesByParentCid? : Map < string , Comment [] > }) => {
2026-02-13 19:42:38 +08:00
const accountReply = useAccountComment ({
commentIndex : typeof reply ? . index === 'number' ? reply.index : undefined ,
});
const hasReplyIndex = typeof reply ? . index === 'number' ;
let post = hasReplyIndex && accountReply ? . index === reply . index ? accountReply : reply ;
2024-06-21 22:02:56 +02:00
// handle pending mod or author edit
2026-02-13 19:42:38 +08:00
const { editedComment } = useEditedComment ({ comment : post });
2024-06-21 22:02:56 +02:00
if ( editedComment ) {
post = editedComment ;
}
2025-03-03 23:47:33 +01:00
const { author , cid , deleted , link , linkHeight , linkWidth , postCid , reason , removed , spoiler , subplebbitAddress , thumbnailUrl , parentCid } = post || {};
2026-01-28 16:51:27 +08:00
const directories = useDirectories ();
const boardPath = subplebbitAddress ? getBoardPath ( subplebbitAddress , directories ) : undefined ;
2025-03-03 23:47:33 +01:00
2025-11-07 18:34:06 +01:00
const location = useLocation ();
const route = boardPath ? `/ ${ boardPath } /thread/ ${ cid } ` : `/thread/ ${ cid } ` ;
const isRouteLinkToReply = cid ? location . pathname . startsWith ( route ) : false ;
2024-06-14 14:33:22 +02:00
const { hidden } = useHide ({ cid });
2025-11-14 14:32:59 +01:00
const isInAllView = isAllView ( location . pathname );
const params = useParams ();
const isInSubscriptionsView = isSubscriptionsView ( location . pathname , params );
2025-03-03 23:47:33 +01:00
const commentMediaInfo = useCommentMediaInfo ( link , thumbnailUrl , linkWidth , linkHeight );
2025-01-21 15:28:20 +01:00
const hasThumbnail = getHasThumbnail ( commentMediaInfo , link );
2024-06-14 14:33:22 +02:00
return (
< div className = { styles . replyDesktop }>
< div className = { styles . sideArrows }>{ '>>' }</ div >
2024-09-18 17:08:42 +02:00
< div className = { ` ${ styles . reply } ${ isRouteLinkToReply && styles . highlight } ` } data-cid = { cid } data-author-address = { author ? . shortAddress } data-post-cid = { postCid }>
2026-02-12 20:00:32 +08:00
< PostInfo
post = { post }
postReplyCount = { postReplyCount }
roles = { roles }
isHidden = { hidden }
threadNumber = { threadNumber }
quotedByMap = { quotedByMap }
directRepliesByParentCid = { directRepliesByParentCid }
/>
2025-03-03 23:47:33 +01:00
{ link && ! hidden && ! ( deleted || removed ) && isValidURL ( link ) && (
< PostMedia
commentMediaInfo = { commentMediaInfo }
hasThumbnail = { hasThumbnail }
spoiler = { spoiler }
deleted = { deleted }
removed = { removed }
linkHeight = { linkHeight }
linkWidth = { linkWidth }
parentCid = { parentCid }
2025-11-14 14:32:59 +01:00
subplebbitAddress = { subplebbitAddress }
isInAllView = { isInAllView }
isInSubscriptionsView = { isInSubscriptionsView }
2025-03-03 23:47:33 +01:00
/>
)}
2025-03-05 21:38:01 +01:00
{ ! hidden && ( ! ( removed || deleted ) || (( removed || deleted ) && reason )) && < CommentContent comment = { post } />}
2024-06-14 14:33:22 +02:00
</ div >
</ div >
);
};
2026-01-25 16:29:15 +08:00
const PostDesktop = ({
post ,
roles ,
showAllReplies ,
showReplies = true ,
targetReplyCid ,
isModQueue ,
modQueueStatus ,
modQueueError ,
isPublishing ,
onApprove ,
onReject ,
} : PostProps ) => {
2024-07-01 10:26:38 +02:00
const { t } = useTranslation ();
2025-03-03 23:47:33 +01:00
const { author , cid , content , deleted , link , linkHeight , linkWidth , pinned , postCid , removed , spoiler , state , subplebbitAddress , thumbnailUrl , parentCid } = post || {};
2024-05-17 15:29:03 +02:00
const params = useParams ();
const location = useLocation ();
2025-12-27 18:07:23 +01:00
const navigationType = useNavigationType ();
2024-05-17 15:29:03 +02:00
const isInPendingPostView = isPendingPostView ( location . pathname , params );
2024-05-30 14:29:02 +02:00
const isInPostPageView = isPostPageView ( location . pathname , params );
2025-11-14 14:32:59 +01:00
const isInAllView = isAllView ( location . pathname );
const isInSubscriptionsView = isSubscriptionsView ( location . pathname , params );
2026-01-28 16:51:27 +08:00
const directories = useDirectories ();
const boardPath = subplebbitAddress ? getBoardPath ( subplebbitAddress , directories ) : undefined ;
2024-05-17 15:29:03 +02:00
2024-06-13 17:50:05 +02:00
const { hidden , unhide , hide } = useHide ({ cid });
const isHidden = hidden && ! isInPostPageView ;
2024-05-30 18:01:37 +02:00
2026-02-13 19:42:38 +08:00
const repliesResult = useReplies ({
comment : post ,
sortType : 'old' ,
flat : true ,
2026-02-15 18:46:49 +08:00
repliesPerPage : REPLIES_PER_PAGE ,
2026-02-13 19:42:38 +08:00
accountComments : { newerThan : Infinity , append : true },
});
2026-02-12 17:44:26 +08:00
const { replies , hasMore , loadMore } = repliesResult ;
2026-02-13 19:42:38 +08:00
const updatedReplies = ( repliesResult as { updatedReplies? : Comment [] }). updatedReplies ;
const repliesForRender = updatedReplies ? . length ? updatedReplies : replies || [];
2026-02-12 17:44:26 +08:00
const reset = ( repliesResult as { reset ?: () => Promise < void > }). reset ;
const setResetFunction = useFeedResetStore (( s ) => s . setResetFunction );
useEffect (() => {
if (( isInPostPageView || isInPendingPostView ) && reset ) {
setResetFunction (() => {
reset ();
});
}
}, [ isInPostPageView , isInPendingPostView , reset , setResetFunction ]);
2026-02-10 16:23:06 +08:00
const registerComments = usePostNumberStore (( s ) => s . registerComments );
const prevCidsRef = useRef < string >( '' );
useEffect (() => {
2026-02-13 19:42:38 +08:00
const all = post ? [ post , ... repliesForRender ] : repliesForRender ;
2026-02-10 16:23:06 +08:00
if ( ! all . length ) return ;
const cidsKey = all
. map (( c ) => c ? . cid )
. filter ( Boolean )
. sort ()
. join ( ',' );
if ( cidsKey === prevCidsRef . current ) return ;
prevCidsRef . current = cidsKey ;
registerComments ( all );
2026-02-13 19:42:38 +08:00
}, [ post , repliesForRender , registerComments ]);
2024-05-16 23:05:00 +02:00
const visiblelinksCount = useCountLinksInReplies ( post , 5 );
2024-07-05 10:53:23 +02:00
const totalLinksCount = useCountLinksInReplies ( post );
2026-02-13 19:42:38 +08:00
const replyCount = repliesForRender . length ;
2024-08-03 13:21:09 +02:00
2024-05-16 23:05:00 +02:00
const repliesCount = pinned ? replyCount : replyCount - 5 ;
2024-07-05 10:53:23 +02:00
const linksCount = pinned ? totalLinksCount : totalLinksCount - visiblelinksCount ;
2024-07-01 10:26:38 +02:00
const { showOmittedReplies , setShowOmittedReplies } = useShowOmittedReplies ();
2024-05-16 23:05:00 +02:00
2025-12-14 16:36:14 +01:00
const stateString = useStateString ( post ) || t ( 'downloading_board' );
2026-02-13 17:48:27 +08:00
const hasFailedState = state === 'failed' ;
2024-07-21 12:17:40 +02:00
2025-03-03 23:47:33 +01:00
const commentMediaInfo = useCommentMediaInfo ( link , thumbnailUrl , linkWidth , linkHeight );
2024-12-24 17:13:12 +01:00
const hasThumbnail = getHasThumbnail ( commentMediaInfo , link );
2025-12-27 18:07:23 +01:00
// Filter out deleted replies with no children for both virtuoso and non-virtuoso rendering
2026-02-13 19:42:38 +08:00
const filteredReplies = useMemo (() => repliesForRender . filter (( reply ) => ! ( reply . deleted && ( reply . replyCount === 0 || ! reply . replyCount ))), [ repliesForRender ]);
2026-02-12 20:00:32 +08:00
const directRepliesByParentCid = useMemo (() => {
const map = new Map < string , Comment [] >();
for ( const reply of filteredReplies ) {
const directParentCid = reply ? . parentCid ;
if ( ! directParentCid || ! reply ? . cid ) {
continue ;
}
const existingReplies = map . get ( directParentCid );
if ( existingReplies ) {
existingReplies . push ( reply );
} else {
map . set ( directParentCid , [ reply ]);
}
}
return map ;
}, [ filteredReplies ]);
2025-12-27 18:07:23 +01:00
2026-02-12 18:41:21 +08:00
const quotedByMap = useQuotedByMap ( filteredReplies );
2026-02-09 19:56:07 +08:00
2026-02-15 18:46:49 +08:00
const visibleReplies = useProgressiveRender ( filteredReplies , {
batchSize : 50 ,
intervalMs : 100 ,
resetKey : cid ,
disabled : hasMore || !! targetReplyCid || ! showAllReplies ,
});
2025-12-27 18:07:23 +01:00
// Virtuoso scroll position management for infinite replies
const virtuosoRef = useRef < VirtuosoHandle | null >( null );
const virtuosoStateKey = `replies-desktop- ${ cid } ` ;
useEffect (() => {
if ( ! showAllReplies || ! isInPostPageView ) return ;
const currentKey = virtuosoStateKey ;
const setLastVirtuosoState = () => {
virtuosoRef . current ? . getState (( snapshot : StateSnapshot ) => {
if ( snapshot ? . ranges ? . length ) {
lastVirtuosoStates [ currentKey ] = snapshot ;
}
});
};
2026-02-18 15:31:01 +08:00
window . addEventListener ( 'scroll' , setLastVirtuosoState , { passive : true });
2025-12-27 18:07:23 +01:00
return () => window . removeEventListener ( 'scroll' , setLastVirtuosoState );
}, [ virtuosoStateKey , showAllReplies , isInPostPageView ]);
const lastVirtuosoState = navigationType === 'POP' ? lastVirtuosoStates ? .[ virtuosoStateKey ] : undefined ;
2026-01-19 16:02:45 +01:00
const shouldScrollToReply = showAllReplies && showReplies && ! isInPendingPostView && !! targetReplyCid ;
useScrollToReply ({
targetReplyCid ,
replies : filteredReplies ,
hasMore ,
loadMore ,
virtuosoRef ,
enabled : shouldScrollToReply ,
});
2026-02-18 15:31:01 +08:00
const virtuosoFooter = useCallback (() => < RepliesFooter hasMore = { hasMore } loadingString = { t ( 'loading' )} />, [ hasMore , t ]);
2025-12-27 18:07:23 +01:00
2024-05-16 23:05:00 +02:00
return (
< div className = { styles . postDesktop }>
2026-01-25 16:29:15 +08:00
{ showReplies || isModQueue ? (
2024-06-05 22:18:45 +02:00
< div className = { styles . hrWrapper }>
< hr />
</ div >
) : (
< div className = { styles . replyQuotePreviewSpacer } />
)}
2024-06-14 14:33:22 +02:00
< div className = { isHidden ? styles . postDesktopHidden : '' }>
2025-12-23 19:14:25 +01:00
{ ! isInPostPageView && showReplies && (
2025-02-05 15:55:08 +01:00
< span className = { ` ${ styles . hideButtonWrapper } ${ ! hasThumbnail ? styles . hideButtonWrapperNoImage : '' } ` }>
2024-06-13 17:50:05 +02:00
< span className = { ` ${ styles . hideButton } ${ hidden ? styles.unhideThread : styles.hideThread } ` } onClick = { hidden ? unhide : hide } />
2024-05-16 23:05:00 +02:00
</ span >
2024-05-30 14:29:02 +02:00
)}
2026-01-18 18:41:05 +01:00
< div
data-cid = { cid }
data-author-address = { author ? . shortAddress }
data-post-cid = { postCid }
className = { ` ${ styles . opContainer } ${ shouldShowSnow () && hasThumbnail ? styles . xmasHatWrapper : '' } ` }
>
2025-05-22 13:15:40 +02:00
{ shouldShowSnow () && hasThumbnail && < img src = 'assets/xmashat.gif' className = { styles . xmasHat } alt = '' />}
2025-03-03 23:47:33 +01:00
{ link && ! isHidden && ! ( deleted || removed ) && isValidURL ( link ) && (
< PostMedia
commentMediaInfo = { commentMediaInfo }
hasThumbnail = { hasThumbnail }
spoiler = { spoiler }
deleted = { deleted }
removed = { removed }
linkHeight = { linkHeight }
linkWidth = { linkWidth }
parentCid = { parentCid }
2025-11-14 14:32:59 +01:00
subplebbitAddress = { subplebbitAddress }
isInAllView = { isInAllView }
isInSubscriptionsView = { isInSubscriptionsView }
2025-03-03 23:47:33 +01:00
/>
)}
2026-01-25 16:29:15 +08:00
< PostInfo
isHidden = { hidden }
post = { post }
postReplyCount = { replyCount }
roles = { roles }
threadNumber = { post ? . number }
isModQueue = { isModQueue }
modQueueStatus = { modQueueStatus }
modQueueError = { modQueueError }
isPublishing = { isPublishing }
onApprove = { onApprove }
onReject = { onReject }
2026-02-11 18:52:10 +08:00
quotedByMap = { quotedByMap }
2026-02-12 20:00:32 +08:00
directRepliesByParentCid = { directRepliesByParentCid }
2026-01-25 16:29:15 +08:00
/>
2024-10-14 17:11:55 +02:00
{ ! isHidden && ! content && ! ( deleted || removed ) && < div className = { styles . spacer } />}
2025-03-05 21:38:01 +01:00
{ ! isHidden && < CommentContent comment = { post } />}
2024-06-29 11:55:36 +02:00
</ div >
2025-12-23 19:14:25 +01:00
{ ! isHidden && ! isInPendingPostView && ( replyCount > 5 || ( pinned && repliesCount > 0 )) && ! isInPostPageView && (
2024-05-30 14:29:02 +02:00
< span className = { styles . summary }>
2024-07-01 10:26:38 +02:00
< span
2024-07-02 08:55:55 +02:00
className = { ` ${ showOmittedReplies [ cid ] ? styles.hideOmittedReplies : styles.showOmittedReplies } ${ styles . omittedRepliesButtonWrapper } ` }
onClick = {() => setShowOmittedReplies ( cid , ! showOmittedReplies [ cid ])}
2024-07-01 10:26:38 +02:00
/>
2024-07-02 08:55:55 +02:00
{ showOmittedReplies [ cid ] ? (
2024-07-01 10:26:38 +02:00
t ( 'showing_all_replies' )
) : linksCount > 0 ? (
2024-05-30 14:29:02 +02:00
< Trans
i18nKey = { 'replies_and_links_omitted' }
shouldUnescape = { true }
2025-11-07 18:34:06 +01:00
components = {{ 1 : < Link key = { cid } to = { boardPath ? `/ ${ boardPath } /thread/ ${ cid } ` : `/thread/ ${ cid } ` } /> }}
2024-05-30 14:29:02 +02:00
values = {{ repliesCount , linksCount }}
/>
) : (
2025-03-05 18:30:41 +01:00
< Trans
i18nKey = { 'replies_omitted' }
shouldUnescape = { true }
2025-11-07 18:34:06 +01:00
components = {{ 1 : < Link key = { cid } to = { boardPath ? `/ ${ boardPath } /thread/ ${ cid } ` : `/thread/ ${ cid } ` } /> }}
2025-03-05 18:30:41 +01:00
values = {{ repliesCount }}
/>
2024-05-30 14:29:02 +02:00
)}
</ span >
)}
2026-01-13 17:01:51 +01:00
{ /* Virtuoso infinite scroll for post page view when there's more content to paginate */ }
{ ! isHidden && showAllReplies && ! isInPendingPostView && showReplies && hasMore && (
2025-12-27 18:07:23 +01:00
< Virtuoso
increaseViewportBy = {{ bottom : 1200 , top : 1200 }}
totalCount = { filteredReplies . length }
data = { filteredReplies }
itemContent = {( index , reply ) => (
< div className = { styles . replyContainer }>
2026-02-12 20:00:32 +08:00
< Reply
reply = { reply }
roles = { roles }
postReplyCount = { replyCount }
threadNumber = { post ? . number }
quotedByMap = { quotedByMap }
directRepliesByParentCid = { directRepliesByParentCid }
/>
2025-12-27 18:07:23 +01:00
</ div >
)}
useWindowScroll = { true }
2026-02-18 15:31:01 +08:00
components = {{ Footer : virtuosoFooter }}
2025-12-27 18:07:23 +01:00
endReached = { loadMore }
ref = { virtuosoRef }
restoreStateFrom = { lastVirtuosoState }
initialScrollTop = { lastVirtuosoState ? . scrollTop }
/>
)}
2026-01-13 17:01:51 +01:00
{ /* Non-virtualized rendering for post page view when all replies fit on one page */ }
2025-12-27 18:25:35 +01:00
{ ! isHidden &&
showAllReplies &&
! isInPendingPostView &&
showReplies &&
2026-01-13 17:01:51 +01:00
! hasMore &&
2026-02-15 18:46:49 +08:00
visibleReplies . map (( reply , index ) => (
2025-12-27 18:25:35 +01:00
< div key = { index } className = { styles . replyContainer }>
2026-02-12 20:00:32 +08:00
< Reply
reply = { reply }
roles = { roles }
postReplyCount = { replyCount }
threadNumber = { post ? . number }
quotedByMap = { quotedByMap }
directRepliesByParentCid = { directRepliesByParentCid }
/>
2025-12-27 18:25:35 +01:00
</ div >
))}
2025-12-27 18:07:23 +01:00
{ /* Non-virtualized rendering for board view (last 5 replies or show omitted) */ }
2024-05-30 18:01:37 +02:00
{ ! isHidden &&
2025-12-27 18:07:23 +01:00
! showAllReplies &&
2024-07-02 09:08:24 +02:00
! ( pinned && ! isInPostPageView && ! showOmittedReplies [ cid ]) &&
2024-05-30 14:29:02 +02:00
! isInPendingPostView &&
2026-02-13 19:42:38 +08:00
repliesForRender &&
2024-06-05 22:18:45 +02:00
showReplies &&
2025-12-27 18:07:23 +01:00
( showOmittedReplies [ cid ] ? filteredReplies : filteredReplies.slice ( - 5 )). map (( reply , index ) => (
< div key = { index } className = { styles . replyContainer }>
2026-02-12 20:00:32 +08:00
< Reply
reply = { reply }
roles = { roles }
postReplyCount = { replyCount }
threadNumber = { post ? . number }
quotedByMap = { quotedByMap }
directRepliesByParentCid = { directRepliesByParentCid }
/>
2025-12-27 18:07:23 +01:00
</ div >
))}
2024-05-30 14:29:02 +02:00
</ div >
2026-02-13 17:48:27 +08:00
{ ! isInPendingPostView && stateString && ! hasFailedState && state !== 'succeeded' && isInPostPageView && ! ( ! showReplies && ! showAllReplies ) ? (
2025-02-23 16:24:31 +01:00
< div className = { styles . stateString }>
< br />
< LoadingEllipsis string = { stateString } />
</ div >
) : (
2026-02-13 17:48:27 +08:00
hasFailedState && < span className = { styles . error }>{ t ( 'failed' )}</ span >
2025-02-23 16:24:31 +01:00
)}
2024-05-16 23:05:00 +02:00
</ div >
);
};
export default PostDesktop ;