2025-12-27 18:07:23 +01:00
import { useEffect , useMemo , useRef , useState } 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' ;
2025-12-27 17:47:17 +01:00
import { Comment , useAuthorAvatar , useEditedComment , useReplies } 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' ;
2024-06-11 15:49:26 +02:00
import { isValidURL } from '../../lib/utils/url-utils' ;
import { isAllView , isPendingPostView , isPostPageView , isSubscriptionsView } from '../../lib/utils/view-utils' ;
2025-11-07 12:50:58 +01:00
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits' ;
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' ;
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' ;
2024-05-16 23:05:00 +02:00
import _ from 'lodash' ;
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' ;
2024-05-16 23:05:00 +02:00
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
}));
2025-12-28 22:16:38 +01:00
const PostInfo = ({ post , postReplyCount = 0 , roles , isHidden , threadNumber } : PostProps ) => {
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 ();
2025-12-27 17:47:17 +01:00
const { replies } = useReplies ({ comment : post });
2024-06-27 11:03:58 +02:00
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' );
2024-05-16 23:05:00 +02:00
const stateString = useStateString ( post );
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 ();
2025-11-07 12:50:58 +01:00
const defaultSubplebbits = useDefaultSubplebbits ();
2025-11-07 18:34:06 +01:00
const boardPath = subplebbitAddress ? getBoardPath ( subplebbitAddress , defaultSubplebbits ) : 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 );
2024-05-17 15:29:03 +02:00
2025-12-24 18:06:56 +01:00
const userID = address && Plebbit . getShortAddress ({ address }); // should not be shortened to less than 12 characters, because users can create unlimited addresses/IDs before authenticating or passing challenges, so if the ID is short enough they can spoof it to troll users with the same ID
2024-09-19 17:33:00 +02:00
const userIDBackgroundColor = hashStringToColor ( userID );
const userIDTextColor = getTextColorForBackground ( userIDBackgroundColor );
2024-06-04 16:06:28 +02:00
2024-06-28 22:09:11 +02:00
const handleUserAddressClick = useAuthorAddressClick ();
2024-07-01 10:26:38 +02:00
const numberOfPostsByAuthor = document . querySelectorAll ( `[data-author-address=" ${ shortAddress } "][data-post-cid=" ${ postCid } "]` ). length ;
2024-06-28 22:09:11 +02: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 ? (
_ . capitalize ( t ( 'deleted' ))
) : removed ? (
_ . capitalize ( t ( 'removed' ))
) : 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' ) } ` }
/>
)
) : (
_ . capitalize ( t ( 'anonymous' ))
)}
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 }
( 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 }}
>
{ userID }
2024-09-18 17:08:42 +02:00
</ span >
2025-12-23 19:14:25 +01:00
}
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
)}
2025-12-23 19:14:25 +01:00
){ ' ' }
2024-09-18 17:08:42 +02:00
</ span >
< span className = { styles . dateTime }>
2025-12-23 19:14:25 +01:00
< 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 >
) : (
<>
< span > CID : </ span >
< span className = { styles . pendingCid }>
{ state === 'failed' || stateString === 'Failed' ? _ . capitalize ( t ( 'failed' )) : state === 'pending' ? _ . capitalize ( t ( 'pending' )) : '' }
</ span >
</>
)}
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 >
)}
{ ! isInPostPageView && ! isReply && ! isHidden && (
< 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 ()}>
2024-09-18 17:08:42 +02:00
{ _ . capitalize ( t ( 'reply' ))}
</ Link >
]
</ span >
)}
</ span >
2025-12-05 17:38:17 +01:00
{ ! ( removed || deleted ) && < PostMenuDesktop postMenu = { postMenuProps } />}
2024-09-18 17:08:42 +02:00
{ cid &&
parentCid &&
replies &&
replies . map (
2024-12-01 15:08:40 +01:00
( reply : Comment , index : number ) =>
reply ? . parentCid === cid &&
reply ? . cid &&
2025-03-05 21:38:01 +01:00
! ( reply ? . deleted || reply ? . removed ) && < ReplyQuotePreview key = { index } isBacklinkReply = { true } backlinkReply = { reply } />,
2024-09-18 17:08:42 +02:00
)}
2024-05-16 23:05:00 +02:00
</ span >
</ div >
);
};
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 );
2025-11-14 14:32:59 +01:00
const defaultSubplebbits = useDefaultSubplebbits ();
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 );
2025-11-14 14:32:59 +01:00
const boardPath = getBoardPath ( subplebbitAddress , defaultSubplebbits );
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' >
2024-06-19 18:50:24 +02:00
{ spoiler ? _ . capitalize ( t ( 'spoiler' )) : url && url . length > 30 ? url . slice ( 0 , 30 ) + '...' : url }
2024-05-16 23:17:10 +02:00
</ a >{ ' ' }
2024-09-07 14:11:33 +02:00
({ type && _ . lowerCase ( getDisplayMediaInfoType ( type , t ))}
{ 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
);
};
2025-12-28 22:16:38 +01:00
const Reply = ({ postReplyCount , reply , roles , threadNumber } : PostProps ) => {
2024-06-21 22:02:56 +02:00
let post = reply ;
// handle pending mod or author edit
const { editedComment } = useEditedComment ({ comment : reply });
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 || {};
2025-11-07 12:50:58 +01:00
const defaultSubplebbits = useDefaultSubplebbits ();
2025-11-07 18:34:06 +01:00
const boardPath = subplebbitAddress ? getBoardPath ( subplebbitAddress , defaultSubplebbits ) : 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 }>
2025-12-28 22:16:38 +01:00
< PostInfo post = { post } postReplyCount = { postReplyCount } roles = { roles } isHidden = { hidden } threadNumber = { threadNumber } />
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 >
);
};
2025-03-05 21:38:01 +01:00
const PostDesktop = ({ post , roles , showAllReplies , showReplies = true } : 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 );
2025-11-07 12:50:58 +01:00
const defaultSubplebbits = useDefaultSubplebbits ();
2025-11-07 18:34:06 +01:00
const boardPath = subplebbitAddress ? getBoardPath ( subplebbitAddress , defaultSubplebbits ) : 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-01-02 16:42:16 +01:00
const { replies , hasMore , loadMore } = useReplies ({ comment : post , flat : true });
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 );
2024-07-16 13:13:04 +02:00
const replyCount = replies ? . 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' );
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
const filteredReplies = useMemo (() => ( replies || []). filter (( reply ) => ! ( reply . deleted && ( reply . replyCount === 0 || ! reply . replyCount ))), [ replies ]);
// 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 ;
}
});
};
window . addEventListener ( 'scroll' , setLastVirtuosoState );
return () => window . removeEventListener ( 'scroll' , setLastVirtuosoState );
}, [ virtuosoStateKey , showAllReplies , isInPostPageView ]);
const lastVirtuosoState = navigationType === 'POP' ? lastVirtuosoStates ? .[ virtuosoStateKey ] : undefined ;
// Footer component for Virtuoso showing loading state
const RepliesFooter = () =>
hasMore ? (
< div className = { styles . stateString }>
< LoadingEllipsis string = { t ( 'loading' )} />
</ div >
) : null ;
2024-05-16 23:05:00 +02:00
return (
< div className = { styles . postDesktop }>
2024-06-07 16:55:23 +02:00
{ showReplies ? (
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
)}
2024-12-24 17:13:12 +01:00
< div data-cid = { cid } data-author-address = { author ? . shortAddress } data-post-cid = { postCid } className = { 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
/>
)}
2025-12-28 22:16:38 +01:00
< PostInfo isHidden = { hidden } post = { post } postReplyCount = { replyCount } roles = { roles } threadNumber = { post ? . number } />
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 >
)}
2025-12-27 18:25:35 +01:00
{ /* Virtuoso infinite scroll for post page view with more than 25 replies */ }
{ ! isHidden && showAllReplies && ! isInPendingPostView && showReplies && replyCount > 25 && (
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 }>
2025-12-28 22:16:38 +01:00
< Reply reply = { reply } roles = { roles } postReplyCount = { replyCount } threadNumber = { post ? . number } />
2025-12-27 18:07:23 +01:00
</ div >
)}
useWindowScroll = { true }
components = {{ Footer : RepliesFooter }}
endReached = { loadMore }
ref = { virtuosoRef }
restoreStateFrom = { lastVirtuosoState }
initialScrollTop = { lastVirtuosoState ? . scrollTop }
/>
)}
2025-12-27 18:25:35 +01:00
{ /* Non-virtualized rendering for post page view with 25 or fewer replies */ }
{ ! isHidden &&
showAllReplies &&
! isInPendingPostView &&
showReplies &&
replyCount <= 25 &&
filteredReplies . map (( reply , index ) => (
< div key = { index } className = { styles . replyContainer }>
2025-12-28 22:16:38 +01:00
< Reply reply = { reply } roles = { roles } postReplyCount = { replyCount } threadNumber = { post ? . number } />
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 &&
replies &&
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 }>
2025-12-28 22:16:38 +01:00
< Reply reply = { reply } roles = { roles } postReplyCount = { replyCount } threadNumber = { post ? . number } />
2025-12-27 18:07:23 +01:00
</ div >
))}
2024-05-30 14:29:02 +02:00
</ div >
2025-12-23 19:14:25 +01:00
{ ! isInPendingPostView && stateString && stateString !== 'Failed' && state !== 'succeeded' && isInPostPageView && ! ( ! showReplies && ! showAllReplies ) ? (
2025-02-23 16:24:31 +01:00
< div className = { styles . stateString }>
< br />
< LoadingEllipsis string = { stateString } />
</ div >
) : (
state === 'failed' && < span className = { styles . error }>{ t ( 'failed' )}</ span >
)}
2024-05-16 23:05:00 +02:00
</ div >
);
};
export default PostDesktop ;