2023-07-01 13:23:03 +02:00
import React , { Fragment , useCallback , useEffect , useMemo , useState , useRef } from 'react' ;
2023-06-13 21:20:19 +02:00
import { confirmAlert } from 'react-confirm-alert' ;
2023-06-03 16:33:59 +02:00
import { createPortal } from 'react-dom' ;
2023-05-03 11:01:41 +02:00
import { Helmet } from 'react-helmet-async' ;
2023-05-07 17:48:38 +02:00
import { Link , useNavigate } from 'react-router-dom' ;
2023-05-03 11:01:41 +02:00
import { Tooltip } from 'react-tooltip' ;
import { Virtuoso } from 'react-virtuoso' ;
2023-06-13 21:20:19 +02:00
import { useAccount , useAccountComments , useFeed , usePublishCommentEdit , useSubplebbits } from '@plebbit/plebbit-react-hooks' ;
2023-09-21 14:38:12 +02:00
import { flattenCommentsPages } from '@plebbit/plebbit-react-hooks/dist/lib/utils' ;
2023-05-03 11:01:41 +02:00
import { debounce } from 'lodash' ;
import useGeneralStore from '../../hooks/stores/useGeneralStore' ;
2023-09-14 13:41:24 +02:00
import { Container , NavBar , Header , Break , TopBar , BoardForm , PostMenu , PostMenuMobile } from '../styled/views/Board.styled' ;
2023-08-13 21:53:04 +02:00
import { Footer } from '../styled/views/Thread.styled' ;
import { AlertModal } from '../styled/modals/AlertModal.styled' ;
2023-06-13 21:20:19 +02:00
import { PostMenuCatalog } from '../styled/views/Catalog.styled' ;
import EditModal from '../modals/EditModal' ;
import EditLabel from '../EditLabel' ;
2023-05-03 11:01:41 +02:00
import ImageBanner from '../ImageBanner' ;
2023-06-13 21:20:19 +02:00
import ModerationModal from '../modals/ModerationModal' ;
2023-09-10 13:43:33 +02:00
import Embed from '../Embed' ;
2023-05-06 15:40:30 +02:00
import OfflineIndicator from '../OfflineIndicator' ;
2023-06-15 14:17:33 +02:00
import PendingLabel from '../PendingLabel' ;
2023-05-03 11:01:41 +02:00
import Post from '../Post' ;
import PostLoader from '../PostLoader' ;
2023-06-03 16:33:59 +02:00
import PostOnHover from '../PostOnHover' ;
2023-06-13 21:20:19 +02:00
import StateLabel from '../StateLabel' ;
2023-07-08 14:37:08 +02:00
import VerifiedAuthor from '../VerifiedAuthor' ;
2023-07-19 18:16:01 +02:00
import CreateBoardModal from '../modals/CreateBoardModal' ;
2023-05-24 12:15:43 +02:00
import ReplyModal from '../modals/ReplyModal' ;
import SettingsModal from '../modals/SettingsModal' ;
2023-05-03 11:01:41 +02:00
import findShortParentCid from '../../utils/findShortParentCid' ;
import getCommentMediaInfo from '../../utils/getCommentMediaInfo' ;
import getDate from '../../utils/getDate' ;
2023-08-17 13:15:37 +02:00
import getFormattedTime from '../../utils/getFormattedTime' ;
2023-05-03 11:01:41 +02:00
import handleAddressClick from '../../utils/handleAddressClick' ;
import handleImageClick from '../../utils/handleImageClick' ;
import handleQuoteClick from '../../utils/handleQuoteClick' ;
2023-06-01 21:59:29 +02:00
import handleQuoteHover from '../../utils/handleQuoteHover' ;
2023-08-22 14:17:38 +02:00
import handleShareClick from '../../utils/handleShareClick' ;
2023-05-03 11:01:41 +02:00
import handleStyleChange from '../../utils/handleStyleChange' ;
2023-06-01 21:59:29 +02:00
import removeHighlight from '../../utils/removeHighlight' ;
2023-06-17 21:55:20 +02:00
import useAnonModeRef from '../../hooks/useAnonModeRef' ;
2023-05-03 11:01:41 +02:00
import useError from '../../hooks/useError' ;
2023-05-09 21:20:30 +02:00
import useFeedStateString from '../../hooks/useFeedStateString' ;
2023-06-13 21:20:19 +02:00
import useSuccess from '../../hooks/useSuccess' ;
2023-06-17 21:55:20 +02:00
import useAnonModeStore from '../../hooks/stores/useAnonModeStore' ;
2023-08-13 16:25:16 +02:00
import packageJson from '../../../package.json' ;
2023-09-21 14:38:12 +02:00
const { version } = packageJson ;
2023-07-14 14:37:12 +02:00
let lastVirtuosoStates = {};
2023-09-10 14:12:25 +02:00
const commitRef = process ? . env ? . REACT_APP_COMMIT_REF ? ` ${ process . env . REACT_APP_COMMIT_REF . slice ( 0 , 7 ) } ` : '' ;
2023-05-03 11:01:41 +02:00
const Subscriptions = () => {
2023-09-21 14:38:12 +02:00
const {
defaultSubplebbits ,
editedComment ,
editedComments ,
feedCacheStates ,
setFeedCacheState ,
isSettingsOpen ,
setIsSettingsOpen ,
setModeratingCommentCid ,
selectedAddress ,
setCaptchaResponse ,
setChallengesArray ,
setIsAuthorDelete ,
setIsAuthorEdit ,
setIsCaptchaOpen ,
isModerationOpen ,
setIsModerationOpen ,
setReplyQuoteCid ,
setResolveCaptchaPromise ,
setSelectedAddress ,
setSelectedParentCid ,
setSelectedShortCid ,
selectedStyle ,
setSelectedThread ,
setSelectedText ,
setSelectedTitle ,
setTriggerInsertion ,
} = useGeneralStore (( state ) => state );
const { anonymousMode } = useAnonModeStore ();
const account = useAccount ();
const navigate = useNavigate ();
const [, setNewErrorMessage ] = useError ();
const [, setNewSuccessMessage ] = useSuccess ();
const threadMenuRefs = useRef ({});
const threadMenuRefsMobile = useRef ({});
const replyMenuRefs = useRef ({});
const replyMenuRefsMobile = useRef ({});
const postMenuCatalogRef = useRef ( null );
const postMenuMobileRef = useRef ( null );
const backlinkRefs = useRef ({});
const quoteRefs = useRef ({});
const postRefs = useRef ({});
const backlinkRefsMobile = useRef ({});
const quoteRefsMobile = useRef ({});
const postOnHoverRef = useRef ( null );
const selectedThreadCidRef = useRef ( null );
const virtuosoRef = useRef ();
const [ isReplyOpen , setIsReplyOpen ] = useState ( false );
const [ isEditModalOpen , setIsEditModalOpen ] = useState ( false );
const [ originalCommentContent , setOriginalCommentContent ] = useState ( null );
const [ prevScrollPos , setPrevScrollPos ] = useState ( 0 );
const [ visible , setVisible ] = useState ( true );
const [ isImageSearchOpen , setIsImageSearchOpen ] = useState ( false );
const [ isClientRedirectMenuOpen , setIsClientRedirectMenuOpen ] = useState ( false );
const [ commentCid , setCommentCid ] = useState ( null );
const [ menuPosition , setMenuPosition ] = useState ({ top : 0 , left : 0 });
const [ mobileMenuPosition , setMobileMenuPosition ] = useState ({ top : 0 , left : 0 });
const [ triggerPublishCommentEdit , setTriggerPublishCommentEdit ] = useState ( false );
const [ openMenuCid , setOpenMenuCid ] = useState ( null );
const [ openMobileMenuCid , setOpenMobileMenuCid ] = useState ( null );
const [ outOfViewCid , setOutOfViewCid ] = useState ( null );
const [ outOfViewPosition , setOutOfViewPosition ] = useState ({ top : 0 , left : 0 });
const [ postOnHoverHeight , setPostOnHoverHeight ] = useState ( 0 );
const [ deletePost , setDeletePost ] = useState ( false );
const [ moderatorPermissions , setModeratorPermissions ] = useState ({});
const [ executeAnonMode , setExecuteAnonMode ] = useState ( false );
const [ cidTracker , setCidTracker ] = useState ({});
const [ isThreadThumbnailClicked , setIsThreadThumbnailClicked ] = useState ({});
const [ isReplyThumbnailClicked , setIsReplyThumbnailClicked ] = useState ({});
const [ isMobileThreadThumbnailClicked , setIsMobileThreadThumbnailClicked ] = useState ({});
const [ isMobileReplyThumbnailClicked , setIsMobileReplyThumbnailClicked ] = useState ({});
const [ isCreateBoardOpen , setIsCreateBoardOpen ] = useState ( false );
const setSelectedThreadCid = ( cid ) => {
selectedThreadCidRef . current = cid ;
};
const isFeedCached = feedCacheStates [ 'subscriptions' ];
useAnonModeRef ( selectedThreadCidRef , anonymousMode && executeAnonMode );
const { feed , loadMore } = useFeed ({ subplebbitAddresses : account ? . subscriptions , sortType : 'active' });
const [ selectedFeed , setSelectedFeed ] = useState ( feed . sort (( a , b ) => b . timestamp - a . timestamp ));
const { subplebbits } = useSubplebbits ({ subplebbitAddresses : account ? . subscriptions , sortType : 'active' });
const stateString = useFeedStateString ( subplebbits );
useEffect (() => {
if ( feed ) {
setFeedCacheState ( 'subscriptions' , true );
}
}, [ feed , setFeedCacheState ]);
const handleThumbnailClick = ( index , type ) => {
switch ( type ) {
case 'thread' :
setIsThreadThumbnailClicked (( prevState ) => ({
... prevState ,
[ index ] : ! prevState [ index ],
}));
break ;
case 'reply' :
setIsReplyThumbnailClicked (( prevState ) => ({
... prevState ,
[ index ] : ! prevState [ index ],
}));
break ;
case 'mobileThread' :
setIsMobileThreadThumbnailClicked (( prevState ) => ({
... prevState ,
[ index ] : ! prevState [ index ],
}));
break ;
case 'mobileReply' :
setIsMobileReplyThumbnailClicked (( prevState ) => ({
... prevState ,
[ index ] : ! prevState [ index ],
}));
break ;
default :
break ;
}
};
useEffect (() => {
let permissions = {};
selectedFeed . forEach (( thread ) => {
const subplebbit = subplebbits . find (( s ) => s && s . address === thread . subplebbitAddress );
if ( subplebbit && subplebbit . roles ) {
const role = subplebbit . roles [ account ? . author . address ] ? . role ;
if ( role === 'moderator' || role === 'admin' || role === 'owner' ) {
permissions [ thread . subplebbitAddress ] = true ;
} else {
permissions [ thread . subplebbitAddress ] = false ;
}
}
});
setModeratorPermissions ( permissions );
}, [ account ? . author . address , selectedFeed , subplebbits ]);
const handleOptionClick = () => {
setOpenMenuCid ( null );
};
const handleMobileOptionClick = () => {
setOpenMobileMenuCid ( null );
};
const handleOutsideClick = useCallback (
( e ) => {
if ( openMenuCid !== null && ! postMenuCatalogRef . current . contains ( e . target )) {
setOpenMenuCid ( null );
}
},
[ openMenuCid , postMenuCatalogRef ],
);
const handleMobileOutsideClick = useCallback (
( e ) => {
if ( openMobileMenuCid !== null && ! postMenuMobileRef . current . contains ( e . target )) {
setOpenMobileMenuCid ( null );
}
},
[ openMobileMenuCid , postMenuMobileRef ],
);
useEffect (() => {
if ( openMenuCid !== null ) {
document . addEventListener ( 'click' , handleOutsideClick );
} else {
document . removeEventListener ( 'click' , handleOutsideClick );
}
return () => {
document . removeEventListener ( 'click' , handleOutsideClick );
};
}, [ openMenuCid , handleOutsideClick ]);
useEffect (() => {
if ( openMobileMenuCid !== null ) {
document . addEventListener ( 'click' , handleMobileOutsideClick );
} else {
document . removeEventListener ( 'click' , handleMobileOutsideClick );
}
return () => {
document . removeEventListener ( 'click' , handleMobileOutsideClick );
};
}, [ openMobileMenuCid , handleMobileOutsideClick ]);
useEffect (() => {
if ( postOnHoverRef . current ) {
const rect = postOnHoverRef . current . getBoundingClientRect ();
setPostOnHoverHeight ( rect . height );
}
}, [ outOfViewCid ]);
const errorString = useMemo (() => {
for ( const subplebbit of subplebbits ) {
if ( subplebbit ? . updatingState !== 'failed' ) {
return ;
}
}
for ( const subplebbit of subplebbits ) {
if ( subplebbit ? . error ) {
return `Failed fetching subplebbit: ${ subplebbit ? . error . toString (). slice ( 0 , 300 ) } ` ;
}
}
}, [ subplebbits ]);
useEffect (() => {
if ( errorString ) {
setNewErrorMessage ( errorString );
}
}, [ errorString , setNewErrorMessage ]);
useEffect (() => {
setSelectedFeed ( feed . sort (( a , b ) => b . timestamp - a . timestamp ));
}, [ feed ]);
const flattenedRepliesByThread = useMemo (() => {
return selectedFeed . reduce (( acc , thread ) => {
const replies = flattenCommentsPages ( thread . replies );
acc [ thread . cid ] = replies ;
return acc ;
}, {});
}, [ selectedFeed ]);
const allParentCids = useMemo (() => {
const allRepliesCids = Object . values ( flattenedRepliesByThread ). flatMap (( replies ) => replies . map (( reply ) => reply . cid ));
const allThreadCids = selectedFeed . map (( thread ) => thread . cid );
return new Set ([... allThreadCids , ... allRepliesCids ]);
}, [ flattenedRepliesByThread , selectedFeed ]);
const filter = useCallback (( accountComment ) => allParentCids . has ( accountComment . parentCid ), [ allParentCids ]);
const { accountComments } = useAccountComments ({ filter });
const filteredRepliesByThread = useMemo (() => {
const maxRepliesPerThread = 5 ;
const accountRepliesNotYetInCommentReplies = selectedFeed . reduce (( acc , thread ) => {
const replyCids = new Set ( flattenedRepliesByThread [ thread . cid ]. map (( reply ) => reply . cid ));
acc [ thread . cid ] = accountComments . filter (( accountReply ) => ! replyCids . has ( accountReply . cid ) && accountReply . parentCid === thread . cid );
return acc ;
}, {});
return selectedFeed . reduce (( acc , thread ) => {
const combinedReplies = [... flattenedRepliesByThread [ thread . cid ], ... accountRepliesNotYetInCommentReplies [ thread . cid ]]. sort (( a , b ) => a . timestamp - b . timestamp );
acc [ thread . cid ] = {
displayedReplies : combinedReplies . slice ( 0 , maxRepliesPerThread ),
omittedCount : Math . max ( combinedReplies . length - maxRepliesPerThread , 0 ),
};
return acc ;
}, {});
}, [ flattenedRepliesByThread , accountComments , selectedFeed ]);
const pendingReplyCounts = useMemo (() => {
return selectedFeed . reduce (( acc , thread ) => {
const replyCids = new Set ( flattenedRepliesByThread [ thread . cid ]. map (( reply ) => reply . cid ));
acc [ thread . cid ] = accountComments . filter (( accountReply ) => ! replyCids . has ( accountReply . cid ) && accountReply . parentCid === thread . cid ). length ;
return acc ;
}, {});
}, [ flattenedRepliesByThread , accountComments , selectedFeed ]);
const allReplies = useMemo (() => {
return selectedFeed . flatMap (( thread ) => filteredRepliesByThread [ thread . cid ] ? . displayedReplies || []);
}, [ selectedFeed , filteredRepliesByThread ]);
// let post.jsx access full cid of user-typed short cid
useEffect (() => {
const newCidTracker = {};
allReplies . forEach (( reply ) => {
newCidTracker [ reply . shortCid ] = reply . cid ;
});
setCidTracker ( newCidTracker );
}, [ allReplies ]);
// mobile navbar scroll effect
useEffect (() => {
const debouncedHandleScroll = debounce (() => {
const currentScrollPos = window . pageYOffset ;
setVisible ( prevScrollPos > currentScrollPos || currentScrollPos < 10 );
setPrevScrollPos ( currentScrollPos );
}, 50 );
window . addEventListener ( 'scroll' , debouncedHandleScroll );
return () => window . removeEventListener ( 'scroll' , debouncedHandleScroll );
}, [ prevScrollPos , visible ]);
const tryLoadMore = async () => {
try {
await loadMore ();
} catch ( e ) {
await new Promise (( resolve ) => setTimeout ( resolve , 1000 ));
}
};
const onChallengeVerification = ( challengeVerification ) => {
if ( challengeVerification . challengeSuccess === true ) {
setNewSuccessMessage ( 'Challenge Success' );
console . log ( 'challenge success' , challengeVerification );
} else if ( challengeVerification . challengeSuccess === false ) {
setNewErrorMessage ( `Challenge Failed, reason: ${ challengeVerification . reason } . Errors: ${ challengeVerification . errors } ` );
console . log ( 'challenge failed' , challengeVerification );
}
};
const onChallenge = async ( challenges , comment ) => {
let challengeAnswers = [];
try {
challengeAnswers = await getChallengeAnswersFromUser ( challenges );
} catch ( error ) {
setNewErrorMessage ( error . message );
console . log ( error );
}
if ( challengeAnswers ) {
await comment . publishChallengeAnswers ( challengeAnswers );
}
};
const getChallengeAnswersFromUser = async ( challenges ) => {
setChallengesArray ( challenges );
return new Promise (( resolve , reject ) => {
const imageString = challenges ? . challenges [ 0 ]. challenge ;
const imageSource = `data:image/png;base64, ${ imageString } ` ;
const challengeImg = new Image ();
challengeImg . src = imageSource ;
challengeImg . onload = () => {
setIsCaptchaOpen ( true );
const handleKeyDown = async ( event ) => {
if ( event . key === 'Enter' ) {
const currentCaptchaResponse = useGeneralStore . getState (). captchaResponse ;
resolve ( currentCaptchaResponse );
setIsCaptchaOpen ( false );
document . removeEventListener ( 'keydown' , handleKeyDown );
event . preventDefault ();
}
};
setCaptchaResponse ( '' );
document . addEventListener ( 'keydown' , handleKeyDown );
setResolveCaptchaPromise ( resolve );
};
challengeImg . onerror = () => {
reject ( setNewErrorMessage ( 'Could not load challenges' ));
};
});
};
const [ publishCommentEditOptions , setPublishCommentEditOptions ] = useState ({
commentCid : commentCid ,
content : editedComment || undefined ,
subplebbitAddress : selectedAddress ,
onChallenge ,
onChallengeVerification ,
onError : ( error ) => {
setNewErrorMessage ( error . message );
console . log ( error );
},
});
useEffect (() => {
if ( anonymousMode ) {
setExecuteAnonMode ( true );
}
}, [ anonymousMode , selectedThreadCidRef ]);
const { publishCommentEdit } = usePublishCommentEdit ( publishCommentEditOptions );
const handleAuthorDeleteClick = ( comment ) => {
handleOptionClick ( comment . cid );
confirmAlert ({
customUI : ({ onClose }) => {
return (
< AlertModal selectedStyle = { selectedStyle }>
< div className = 'author-delete-alert' >
< p > Are you sure you want to delete this post ? </ p >
< div className = 'author-delete-buttons' >
< button onClick = { onClose }> No </ button >
< button
onClick = {() => {
setIsAuthorDelete ( true );
setIsAuthorEdit ( false );
setPublishCommentEditOptions (( prevOptions ) => ({
... prevOptions ,
commentCid : comment . cid ,
subplebbitAddress : comment . subplebbitAddress ,
deleted : true ,
}));
setTriggerPublishCommentEdit ( true );
onClose ();
}}
>
Yes
</ button >
</ div >
</ div >
</ AlertModal >
);
},
});
};
const handleAuthorEditClick = ( comment ) => {
handleOptionClick ( comment . cid );
setIsAuthorEdit ( true );
setIsAuthorDelete ( false );
setCommentCid ( comment . cid );
setOriginalCommentContent ( comment . content );
setIsEditModalOpen ( true );
};
useEffect (() => {
setPublishCommentEditOptions (( prevOptions ) => ({
... prevOptions ,
commentCid : commentCid ,
content : editedComment || undefined ,
}));
}, [ commentCid , editedComment ]);
useEffect (() => {
if ( editedComment !== '' ) {
setTriggerPublishCommentEdit ( true );
}
}, [ editedComment , setIsAuthorEdit ]);
useEffect (() => {
let isActive = true ;
if ( publishCommentEditOptions && triggerPublishCommentEdit ) {
( async () => {
await publishCommentEdit ();
if ( isActive ) {
setTriggerPublishCommentEdit ( false );
}
})();
}
return () => {
isActive = false ;
};
}, [ triggerPublishCommentEdit , publishCommentEdit , publishCommentEditOptions ]);
// desktop navbar board select functionality
const handleClickTitle = ( title , address ) => {
setSelectedTitle ( title );
setSelectedAddress ( address );
setSelectedFeed ( feed . filter (( feed ) => feed . title === title ));
};
// mobile navbar board select functionality
const handleSelectChange = ( event ) => {
const selected = event . target . value ;
if ( selected === 'subscriptions' ) {
navigate ( `/p/subscriptions` );
return ;
} else if ( selected === 'all' ) {
navigate ( `/p/all` );
return ;
}
const selectedTitle = defaultSubplebbits ? . find (( subplebbit ) => subplebbit . address === selected ). title ;
setSelectedTitle ( selectedTitle );
setSelectedAddress ( selected );
navigate ( `/p/ ${ selected } ` );
};
useEffect (() => {
const setLastVirtuosoState = () => {
virtuosoRef . current ? . getState (( snapshot ) => {
if ( snapshot ? . scrollTop === 0 || snapshot ? . ranges ? . length ) {
lastVirtuosoStates [ 'subscriptions' ] = snapshot ;
}
});
};
window . addEventListener ( 'scroll' , setLastVirtuosoState );
return () => window . removeEventListener ( 'scroll' , setLastVirtuosoState );
}, []);
const lastVirtuosoState = lastVirtuosoStates [ 'subscriptions' ];
return (
<>
< Helmet >
< title > Subscriptions - plebchan </ title >
</ Helmet >
< Container >
< CreateBoardModal selectedStyle = { selectedStyle } isOpen = { isCreateBoardOpen } closeModal = {() => setIsCreateBoardOpen ( false )} />
< ReplyModal selectedStyle = { selectedStyle } isOpen = { isReplyOpen } closeModal = {() => setIsReplyOpen ( false )} />
< SettingsModal selectedStyle = { selectedStyle } isOpen = { isSettingsOpen } closeModal = {() => setIsSettingsOpen ( false )} />
< ModerationModal
selectedStyle = { selectedStyle }
isOpen = { isModerationOpen }
closeModal = {() => {
setIsModerationOpen ( false );
setDeletePost ( false );
}}
deletePost = { deletePost }
/>
< EditModal selectedStyle = { selectedStyle } isOpen = { isEditModalOpen } closeModal = {() => setIsEditModalOpen ( false )} originalCommentContent = { originalCommentContent } />
< NavBar selectedStyle = { selectedStyle }>
<>
< span className = 'boardList' >
[
< Link to = { `/p/all` } onClick = {() => window . scrollTo ( 0 , 0 )}>
All
</ Link >
/
< Link to = { `/p/subscriptions` } onClick = {() => window . scrollTo ( 0 , 0 )}>
Subscriptions
</ Link >
] & nbsp ;[
{ defaultSubplebbits . map (( subplebbit , index ) => (
< span className = 'boardList' key = { `span- ${ subplebbit . address } ` }>
{ index === 0 ? null : '\u00a0' }
< Link to = { `/p/ ${ subplebbit . address } ` } key = { `a- ${ subplebbit . address } ` } onClick = {() => handleClickTitle ( subplebbit . title , subplebbit . address )}>
{ subplebbit . title ? subplebbit . title : subplebbit . address }
</ Link >
{ index !== defaultSubplebbits . length - 1 ? ' /' : null }
</ span >
))}
]
</ span >
< span className = 'nav' >
[
< span
id = 'button-span'
style = {{ cursor : 'pointer' }}
onClick = {() => {
window . electron && window . electron . isElectron
? setIsCreateBoardOpen ( true )
: alert (
'You can create a board with the desktop version of plebchan:\nhttps://github.com/plebbit/plebchan/releases/latest\n\nIf you are comfortable with the command line, use plebbit-cli:\nhttps://github.com/plebbit/plebbit-cli\n\n' ,
);
}}
>
Create Board
</ span >
] [
< Link to = { `/p/subscriptions/settings` } onClick = {() => setIsSettingsOpen ( true )}>
Settings
</ Link >
] [< Link to = '/' > Home </ Link >]
</ span >
< div id = 'board-nav-mobile' style = {{ top : visible ? 0 : '-23px' }}>
< div className = 'nav-container' >
< div className = 'board-select' >
< strong > Board </ strong >
& nbsp ;
< select id = 'board-select-mobile' value = 'subscriptions' onChange = { handleSelectChange }>
< option value = 'all' > All </ option >
< option value = 'subscriptions' > Subscriptions </ option >
{ defaultSubplebbits . map (( subplebbit ) => (
< option key = { `option- ${ subplebbit . address } ` } value = { subplebbit . address }>
{ subplebbit . title ? subplebbit . title : subplebbit . address }
</ option >
))}
</ select >
< span
id = 'button-span'
style = {{ cursor : 'pointer' }}
onClick = {() =>
alert (
'You can create a board with the desktop version of plebchan:\nhttps://github.com/plebbit/plebchan/releases/latest\n\nIf you are comfortable with the command line, use plebbit-cli:\nhttps://github.com/plebbit/plebbit-cli\n\n' ,
)
}
>
Create Board
</ span >
</ div >
< div className = 'page-jump' >
< Link to = { `/p/subscriptions/settings` } onClick = {() => setIsSettingsOpen ( true )}>
Settings
</ Link >
& nbsp ;
< Link
to = '/'
onClick = {() => {
handleStyleChange ({ target : { value : 'Yotsuba' } });
window . scrollTo ( 0 , 0 );
}}
>
Home
</ Link >
</ div >
</ div >
</ div >
< div id = 'separator-mobile' > & nbsp ;</ div >
< div id = 'separator-mobile' > & nbsp ;</ div >
</>
</ NavBar >
< Header selectedStyle = { selectedStyle }>
<>
< div className = 'banner' >
< ImageBanner />
</ div >
<>
< div className = 'board-title' > Subscriptions </ div >
{ account ? . subscriptions . length < 1 ? (
< div className = 'board-address' > You haven 't subscribed to any board yet.</div>
) : (
<div className=' board - address '>
You have subscribed to {account?.subscriptions.length} board{account?.subscriptions.length > 1 ? ' s ' : null}.
</div>
)}
</>
</>
</Header>
<Break selectedStyle={selectedStyle} />
<TopBar selectedStyle={selectedStyle}>
<hr />
<span className=' style - changer '>
Style:
<select id=' style - selector ' onChange={handleStyleChange} value={selectedStyle}>
<option value=' Yotsuba '>Yotsuba</option>
<option value=' Yotsuba - B '>Yotsuba B</option>
<option value=' Futaba '>Futaba</option>
<option value=' Burichan '>Burichan</option>
<option value=' Tomorrow '>Tomorrow</option>
<option value=' Photon '>Photon</option>
</select>
</span>
<div id=' catalog - button - desktop '>
[<Link to={`/p/subscriptions/catalog`}>Catalog</Link>]
</div>
{subplebbits.state === ' succeeded ' ? null : (
<div id=' stats ' style={{ float: ' right ', marginTop: ' 5 px ' }}>
<span className={stateString ? ' ellipsis ' : ''}>{stateString}</span>
</div>
)}
<div id=' catalog - button - mobile '>
<span className=' btn - wrap '>
<Link to={`/p/subscriptions/catalog`}>Catalog</Link>
</span>
</div>
</TopBar>
<Tooltip id=' tooltip ' className=' tooltip ' />
<BoardForm selectedStyle={selectedStyle}>
<div className=' board '>
{!feed ? null : (
<Virtuoso
increaseViewportBy={{ bottom: 600, top: 600 }}
data={selectedFeed}
itemContent={(index, thread) => {
if (editedComments[thread.cid]) {
thread = editedComments[thread.cid];
if (thread.removed) {
thread.content = ' [ removed ] ';
thread.link = undefined;
}
}
const { displayedReplies, omittedCount } = filteredRepliesByThread[thread.cid] || {};
const commentMediaInfo = getCommentMediaInfo(thread);
const fallbackImgUrl = ' assets / filedeleted - res . gif ';
const isModerator = moderatorPermissions[thread.subplebbitAddress];
let displayWidth, displayHeight, displayWidthMobile, displayHeightMobile;
if (thread.linkWidth && thread.linkHeight) {
let scale = Math.min(1, 250 / Math.max(thread.linkWidth, thread.linkHeight));
displayWidth = `${thread.linkWidth * scale}px`;
displayHeight = `${thread.linkHeight * scale}px`;
scale = Math.min(1, 100 / Math.max(thread.linkWidth, thread.linkHeight));
displayWidthMobile = `${thread.linkWidth * scale}px`;
displayHeightMobile = `${thread.linkHeight * scale}px`;
} else {
displayWidth = ' 250 px ';
displayHeight = ' 250 px ';
displayWidthMobile = ' 100 px ';
displayHeightMobile = ' 100 px ';
}
return (
<Fragment key={`fr-${index}`}>
<div key={`t-${index}`} className=' thread '>
<div key={`c-${index}`} className=' op - container '>
<div key={`po-${index}`} className=' post op op - desktop '>
<hr key={`hr-${index}`} />
<div key={`pi-${index}`} className=' post - info '>
{commentMediaInfo?.url ? (
<div key={`f-${index}`} className=' file ' style={{ marginBottom: ' 5 px ' }}>
<div key={`ft-${index}`} className=' file - text '>
Link:
<a key={`fa-${index}`} href={commentMediaInfo.url} target=' _blank ' rel=' noopener noreferrer '>
{commentMediaInfo?.url.length > 30 ? commentMediaInfo?.url.slice(0, 30) + ' (...) ' : commentMediaInfo?.url}
</a>
{commentMediaInfo?.type === ' iframe ' ? null : ` (${commentMediaInfo?.type})`}
{((isThreadThumbnailClicked[index] && (commentMediaInfo.type === ' iframe ' || commentMediaInfo.type === ' video ')) ||
(commentMediaInfo.type === ' iframe ' && !commentMediaInfo.thumbnail)) && (
<span>
[
<span
className=' reply - link '
style={{ textDecoration: ' underline ', cursor: ' pointer ' }}
onClick={() => {
handleThumbnailClick(index, ' thread ');
}}
>
{isThreadThumbnailClicked[index] ? ' Close ' : ' Embed '}
</span>
]
</span>
)}
</div>
{commentMediaInfo?.type === ' iframe ' && (
<div key={`enlarge-${index}`} className={`img-container ${isThreadThumbnailClicked[index] ? ' expanded - container ' : ''}`}>
<span
key={`fta-${index}`}
className=' file - thumb '
style={isThreadThumbnailClicked[index] || !commentMediaInfo.thumbnail ? {} : { width: displayWidth, height: displayHeight }}
>
{isThreadThumbnailClicked[index] && commentMediaInfo.url ? (
<Embed url={commentMediaInfo.url} key={`fti-${index}`} />
) : commentMediaInfo.thumbnail ? (
<img
key={`fti-${index}`}
src={commentMediaInfo.thumbnail}
alt=' thumbnail '
onClick={() => {
handleThumbnailClick(index, ' thread ');
}}
style={{ cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : null}
</span>
</div>
)}
{commentMediaInfo?.type === ' webpage ' ? (
<div key={`enlarge-${index}`} className=' img - container '>
<span
key={`fta-${index}`}
className=' file - thumb '
style={isThreadThumbnailClicked[index] || !thread.thumbnailUrl ? {} : { width: displayWidth, height: displayHeight }}
>
{thread.thumbnailUrl ? (
<img
key={`fti-${index}`}
src={commentMediaInfo.thumbnail}
alt={commentMediaInfo.type}
onClick={(e) => {
handleImageClick(e);
handleThumbnailClick(index, ' thread ');
}}
style={{ cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : null}
</span>
</div>
) : null}
{commentMediaInfo?.type === ' image ' ? (
<div key={`enlarge-${index}`} className=' img - container '>
<span
key={`fta-${index}`}
className=' file - thumb '
style={isThreadThumbnailClicked[index] ? {} : { width: displayWidth, height: displayHeight }}
>
<img
key={`fti-${index}`}
src={commentMediaInfo.url}
alt={commentMediaInfo.type}
onClick={(e) => {
handleImageClick(e);
handleThumbnailClick(index, ' thread ');
}}
style={{ cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
</span>
</div>
) : null}
{commentMediaInfo?.type === ' video ' ? (
<div key={`enlarge-${index}`} className={`img-container ${isThreadThumbnailClicked[index] ? ' expanded - container ' : ''}`}>
<span
key={`fta-${index}`}
className=' file - thumb '
style={isThreadThumbnailClicked[index] ? {} : { width: displayWidth, height: displayHeight }}
>
{isThreadThumbnailClicked[index] ? (
<video
className=' enlarged '
key={`fti-${index}`}
src={commentMediaInfo.url}
controls
style={{ cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : (
<video
key={`fti-${index}`}
src={commentMediaInfo.url}
alt=' thumbnail '
onClick={() => {
handleThumbnailClick(index, ' thread ');
}}
style={{ cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
)}
</span>
</div>
) : null}
{commentMediaInfo?.type === ' audio ' ? (
<span key={`fta-${index}`} className=' file - thumb '>
<audio
controls
key={`fti-${index}`}
src={commentMediaInfo.url}
alt={commentMediaInfo.type}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
</span>
) : null}
</div>
) : null}
<span key={`nb-${index}`} className=' name - block '>
{thread.title ? (
thread.title.length > 75 ? (
<Fragment key={`fragment2-${index}`}>
<span key={`q-${index}`} className=' title ' data-tooltip-id=' tooltip ' data-tooltip-content={thread.title} data-tooltip-place=' top '>
{thread.title.slice(0, 75) + ' (...) '}
</span>
</Fragment>
) : (
<span key={`q-${index}`} className=' title '>
{thread.title}
</span>
)
) : null}
{thread.author.displayName ? (
thread.author.displayName.length > 20 ? (
<Fragment key={`fragment3-${index}`}>
<span
key={`n-${index}`}
className=' name '
data-tooltip-id=' tooltip '
data-tooltip-content={thread.author.displayName}
data-tooltip-place=' top '
>
{thread.author.displayName.slice(0, 20) + ' (...) '}
</span>
</Fragment>
) : (
<span key={`n-${index}`} className=' name '>
{thread.author.displayName}
</span>
)
) : (
<span key={`n-${index}`} className=' name '>
Anonymous
</span>
)}
(u/
<span
key={`pa-${index}`}
className=' poster - address address - desktop '
id=' reply - button '
style={{ cursor: ' pointer ' }}
onClick={() =>
handleAddressClick(
<VerifiedAuthor commentCid={thread.cid}>{({ shortAuthorAddress }) => shortAuthorAddress || ' aPL4c3H0Ld3r '}</VerifiedAuthor>,
)
}
>
{<VerifiedAuthor commentCid={thread.cid}>{({ shortAuthorAddress }) => shortAuthorAddress || ' aPL4c3H0Ld3r '}</VerifiedAuthor>}
</span>
)
<span
key={`dt-${index}`}
className=' date - time '
data-tooltip-id=' tooltip '
data-tooltip-content={getFormattedTime(thread.timestamp)}
data-tooltip-place=' top '
>
{getDate(thread.timestamp)}
</span>
<span key={`pn-${index}`} className=' post - number post - number - desktop '>
<span key={`pl1-${index}`}>c/</span>
<Link
to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
id=' reply - button '
key={`pl2-${index}`}
onClick={(e) => {
if (e.button === 2) return;
e.preventDefault();
setSelectedThreadCid(thread.cid);
setSelectedAddress(thread.subplebbitAddress);
let text = document.getSelection().toString();
text = text ? `>${text}` : text;
setSelectedText(text);
if (isReplyOpen) {
setReplyQuoteCid(thread.shortCid);
setTriggerInsertion(Date.now());
} else {
setIsReplyOpen(true);
setSelectedShortCid(thread.shortCid);
setSelectedParentCid(thread.cid);
}
}}
title=' Reply to this post '
>
{thread.shortCid}
</Link>
p/
<Link key={`p-t-${index}`} to={`/p/${thread.subplebbitAddress}`} id=' reply - button ' title=' Visit this board '>
{thread.subplebbitAddress.includes(' . eth ') ? (
thread.subplebbitAddress
) : (
<span key={`short-add${index}`} data-tooltip-id=' tooltip ' data-tooltip-content={thread.subplebbitAddress} data-tooltip-place=' top '>
{thread.subplebbitAddress.slice(0, 10) + ' (...) '}
</span>
)}
</Link>
<OfflineIndicator key={`oi-${index}`} address={thread.subplebbitAddress} className=' offline - sub ' tooltipPlace=' top ' />
<span key={`rl1-${index}`}>
[
<Link
key={`rl2-${index}`}
to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
onClick={() => setSelectedThread(thread.cid)}
className=' reply - link '
>
Reply
</Link>
]
</span>
</span>
<PostMenu
key={`pmb-${index}`}
title=' Post menu '
ref={(el) => {
threadMenuRefs.current[thread.cid] = el;
}}
className=' post - menu - button '
rotated={openMenuCid === thread.cid}
onClick={(event) => {
event.stopPropagation();
const rect = threadMenuRefs.current[thread.cid].getBoundingClientRect();
setMenuPosition({ top: rect.top + window.scrollY, left: rect.left });
setOpenMenuCid((prevCid) => (prevCid === thread.cid ? null : thread.cid));
}}
>
▶
</PostMenu>
{createPortal(
<PostMenuCatalog
selectedStyle={selectedStyle}
ref={(el) => {
postMenuCatalogRef.current = el;
}}
onClick={(event) => event.stopPropagation()}
style={{ position: ' absolute ', top: menuPosition.top + 7, left: menuPosition.left }}
>
<div className={`post-menu-thread post-menu-thread-${thread.cid}`} style={{ display: openMenuCid === thread.cid ? ' block ' : ' none ' }}>
<ul className=' post - menu - catalog '>
<li
onClick={() => {
handleOptionClick(thread.cid);
handleShareClick(thread.subplebbitAddress, thread.cid);
}}
>
Share thread
</li>
<VerifiedAuthor commentCid={thread.cid}>
{({ authorAddress }) => (
<>
{authorAddress === account?.author.address || authorAddress === account?.signer.address ? (
<>
<li onClick={() => handleAuthorEditClick(thread)}>Edit post</li>
<li onClick={() => handleAuthorDeleteClick(thread)}>Delete post</li>
</>
) : null}
{isModerator ? (
<>
{authorAddress === account?.author.address || authorAddress === account?.signer.address ? null : (
<li
onClick={() => {
setSelectedAddress(thread.subplebbitAddress);
setModeratingCommentCid(thread.cid);
setIsModerationOpen(true);
handleOptionClick(thread.cid);
setDeletePost(true);
}}
>
Delete post
</li>
)}
<li
onClick={() => {
setSelectedAddress(thread.subplebbitAddress);
setModeratingCommentCid(thread.cid);
setIsModerationOpen(true);
handleOptionClick(thread.cid);
}}
>
Mod tools
</li>
</>
) : null}
</>
)}
</VerifiedAuthor>
{commentMediaInfo &&
(commentMediaInfo.type === ' image ' || (commentMediaInfo.type === ' webpage ' && commentMediaInfo.thumbnail)) ? (
<li
onMouseOver={() => {
setIsImageSearchOpen(true);
}}
onMouseLeave={() => {
setIsImageSearchOpen(false);
}}
>
Image search »
<ul className=' dropdown - menu post - menu - catalog ' style={{ display: isImageSearchOpen ? ' block ' : ' none ' }}>
<li onClick={() => handleOptionClick(thread.cid)}>
<a href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`} target=' _blank ' rel=' noreferrer '>
Google
</a>
</li>
<li onClick={() => handleOptionClick(thread.cid)}>
<a href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`} target=' _blank ' rel=' noreferrer '>
Yandex
</a>
</li>
<li onClick={() => handleOptionClick(thread.cid)}>
<a href={`https://saucenao.com/search.php?url=${commentMediaInfo.url}`} target=' _blank ' rel=' noreferrer '>
SauceNAO
</a>
</li>
</ul>
</li>
) : null}
<li
onMouseOver={() => {
setIsClientRedirectMenuOpen(true);
}}
onMouseLeave={() => {
setIsClientRedirectMenuOpen(false);
}}
>
View on »
<ul className=' dropdown - menu post - menu - catalog ' style={{ display: isClientRedirectMenuOpen ? ' block ' : ' none ' }}>
<li onClick={() => handleOptionClick(thread.cid)}>
<a href={`https://plebbitapp.eth.limo/#/p/${thread.subplebbitAddress}/c/${thread.cid}`} target=' _blank ' rel=' noreferrer '>
Plebbit
</a>
</li>
{/* <li onClick={() => handleOptionClick(thread.cid)}>
2023-08-09 13:03:20 +02:00
<a
2023-08-31 11:55:11 +02:00
href={`https://seedit.eth.limo/#/p/${thread.subplebbitAddress}/c/${thread.cid}`}
2023-08-09 13:03:20 +02:00
target="_blank" rel="noreferrer"
>Seedit</a>
</li> */}
2023-09-21 14:38:12 +02:00
<li onClick={() => handleOptionClick(thread.cid)}>
<a href={`https://plebones.eth.limo/#/p/${thread.subplebbitAddress}/c/${thread.cid}`} target=' _blank ' rel=' noreferrer '>
Plebones
</a>
</li>
</ul>
</li>
</ul>
</div>
</PostMenuCatalog>,
document.body,
)}
<div key={`bi-${index}`} id=' backlink - id ' className=' backlink '>
{thread.replies?.pages?.topAll.comments
.sort((a, b) => a.timestamp - b.timestamp)
.map((reply, index) => (
<div
key={`div-${index}`}
style={{ display: ' inline - block ' }}
ref={(el) => {
backlinkRefs.current[reply.cid] = el;
}}
>
<Link
key={`ql-${index}`}
to={() => {}}
className=' quote - link '
onClick={(event) => handleQuoteClick(reply, null, event)}
onMouseOver={(event) => {
event.stopPropagation();
handleQuoteHover(reply, null, () => {
setOutOfViewCid(reply.cid);
const rect = backlinkRefs.current[reply.cid].getBoundingClientRect();
const distanceToRight = window.innerWidth - rect.right;
const distanceToTop = rect.top;
const distanceToBottom = window.innerHeight - rect.bottom;
let top;
2023-06-10 12:28:21 +02:00
2023-09-21 14:38:12 +02:00
if (distanceToTop < postOnHoverHeight / 2) {
top = window.scrollY - 5;
} else if (distanceToBottom < postOnHoverHeight / 2) {
top = window.scrollY - postOnHoverHeight + window.innerHeight - 10;
} else {
top = rect.top + window.scrollY - postOnHoverHeight / 2;
}
if (distanceToRight < 200) {
setOutOfViewPosition({
top,
right: window.innerWidth - rect.left - 10,
maxWidth: rect.left - 5,
});
} else {
setOutOfViewPosition({
top,
left: rect.left + rect.width + 5,
maxWidth: window.innerWidth - rect.left - rect.width - 5,
});
}
});
}}
onMouseLeave={() => {
removeHighlight();
setOutOfViewCid(null);
}}
>
c/{reply.shortCid}
</Link>
</div>
))}
</div>
</span>
{thread.content ? (
thread.content?.length > 1000 ? (
<Fragment key={`fragment5-${index}`}>
<blockquote key={`bq-${index}`}>
<Post content={thread.content?.slice(0, 1000)} key={`post-${index}`} />
<span key={`ttl-s-${index}`} className=' ttl '>
{' '}
(...)
<br key={`ttl-s-br1-${index}`} />
<EditLabel key={`edit-label-thread-${index}`} commentCid={thread.cid} className=' ttl ' />
<StateLabel key={`state-label-thread-${index}`} commentIndex={thread.index} className=' ttl ellipsis ' />
<br key={`ttl-s-br2${index}`} />
Post too long.
<Link
key={`ttl-l-${index}`}
to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
onClick={() => setSelectedThread(thread.cid)}
className=' ttl - link '
>
Click here
</Link>
to view.
</span>
</blockquote>
</Fragment>
) : (
<blockquote key={`bq-${index}`}>
<Post content={thread.content} key={`post-${index}`} />
<EditLabel key={`edit-label-thread-${index}`} commentCid={thread.cid} className=' ttl ' />
<StateLabel key={`state-label-thread-${index}`} commentIndex={thread.index} className=' ttl ellipsis ' />
</blockquote>
)
) : null}
</div>
</div>
</div>
<span key={`summary-${index}`} className=' summary '>
{omittedCount > 0 ? (
<span key={`oc-${index}`} className=' ttl '>
<span key={`oc1-${index}`}>
{omittedCount} post{omittedCount > 1 ? ' s ' : ''} omitted. Click
<Link
key={`oc2-${index}`}
to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
onClick={() => setSelectedThread(thread.cid)}
className=' ttl - link '
>
here
</Link>
to view.
</span>
</span>
) : null}
</span>
{displayedReplies?.map((reply, index) => {
if (editedComments[reply.cid]) {
reply = editedComments[reply.cid];
if (reply.removed) {
reply.content = ' [ removed ] ';
reply.link = undefined;
}
}
const replyMediaInfo = getCommentMediaInfo(reply);
const fallbackImgUrl = ' assets / filedeleted - res . gif ';
const shortParentCid = findShortParentCid(reply.parentCid, selectedFeed);
let replyDisplayWidth, replyDisplayHeight;
if (reply.linkWidth && reply.linkHeight) {
const scale = Math.min(1, 125 / Math.max(reply.linkWidth, reply.linkHeight));
replyDisplayWidth = `${reply.linkWidth * scale}px`;
replyDisplayHeight = `${reply.linkHeight * scale}px`;
} else {
replyDisplayWidth = ' 125 px ';
replyDisplayHeight = ' 125 px ';
}
return (
<div key={`rc-${index}`} className=' reply - container '>
<div key={`sa-${index}`} className=' side - arrows '>
{' >> '}
</div>
<div key={`pr-${index}`} className=' post - reply post - reply - desktop '>
<div key={`pi-${index}`} className=' post - info '>
<span key={`nb-${index}`} className=' nameblock '>
{reply.author.displayName ? (
reply.author.displayName.length > 20 ? (
<Fragment key={`fragment6-${index}`}>
<span
key={`mob-n-${index}`}
className=' name '
data-tooltip-id=' tooltip '
data-tooltip-content={reply.author.displayName}
data-tooltip-place=' top '
>
{reply.author.displayName.slice(0, 20) + ' (...) '}
</span>
</Fragment>
) : (
<span key={`mob-n-${index}`} className=' name '>
{reply.author.displayName}
</span>
)
) : (
<span key={`mob-n-${index}`} className=' name '>
Anonymous
</span>
)}
<span
key={`pa-${index}`}
className=' poster - address address - desktop '
id=' reply - button '
style={{ cursor: ' pointer ' }}
onClick={() =>
handleAddressClick(
<VerifiedAuthor commentCid={reply.cid}>{({ shortAuthorAddress }) => shortAuthorAddress || ' aPL4c3H0Ld3r '}</VerifiedAuthor>,
)
}
>
(u/
{reply.author?.shortAddress ? (
<span key={`mob-ha-${index}`}>{reply.author?.shortAddress}</span>
) : (
<span key={`mob-ha-${index}`} data-tooltip-id=' tooltip ' data-tooltip-content={account?.author?.address} data-tooltip-place=' top '>
{account?.author?.address.slice(0, 10) + ' (...) '}
</span>
)}
)
</span>
</span>
<span
key={`dt-${index}`}
className=' date - time '
data-tooltip-id=' tooltip '
data-tooltip-content={getFormattedTime(reply.timestamp)}
data-tooltip-place=' top '
>
{getDate(reply.timestamp)}
</span>
<span key={`pn-${index}`} className=' post - number post - number - desktop '>
<span id=' reply - button ' style={{ cursor: ' pointer ' }} key={`pl1-${index}`} title=' Link to this post '>
c/
</span>
{reply.shortCid ? (
<Link
to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
id=' reply - button '
key={`pl2-${index}`}
onClick={(e) => {
if (e.button === 2) return;
e.preventDefault();
setSelectedThreadCid(thread.cid);
setSelectedAddress(thread.subplebbitAddress);
let text = document.getSelection().toString();
text = text ? `>${text}` : text;
setSelectedText(text);
if (isReplyOpen) {
setReplyQuoteCid(reply.shortCid);
setTriggerInsertion(Date.now());
} else {
setIsReplyOpen(true);
setSelectedShortCid(reply.shortCid);
setSelectedParentCid(reply.cid);
}
}}
title=' Reply to this post '
>
{reply.shortCid}
</Link>
) : (
<PendingLabel key=' pending ' commentIndex={reply.index} />
)}
p/
<Link key={`p-t-${index}`} to={`/p/${thread.subplebbitAddress}`} id=' reply - button ' title=' Visit this board '>
{thread.subplebbitAddress.includes(' . eth ') ? (
thread.subplebbitAddress
) : (
<span
key={`short-add${index}`}
data-tooltip-id=' tooltip '
data-tooltip-content={thread.subplebbitAddress}
data-tooltip-place=' top '
>
{thread.subplebbitAddress.slice(0, 10) + ' (...) '}
</span>
)}
</Link>
<OfflineIndicator key={`offline-reply-${index}`} address={thread.subplebbitAddress} tooltipPlace=' top ' className=' offline - reply ' />
</span>
<PostMenu
key={`pmb-${index}`}
title=' Post menu '
ref={(el) => {
replyMenuRefs.current[reply.cid] = el;
}}
className=' post - menu - button '
rotated={openMenuCid === reply.cid}
onClick={(event) => {
event.stopPropagation();
const rect = replyMenuRefs.current[reply.cid].getBoundingClientRect();
setMenuPosition({ top: rect.top + window.scrollY, left: rect.left });
setOpenMenuCid((prevCid) => (prevCid === reply.cid ? null : reply.cid));
}}
>
▶
</PostMenu>
{createPortal(
<PostMenuCatalog
selectedStyle={selectedStyle}
ref={(el) => {
postMenuCatalogRef.current = el;
}}
onClick={(event) => event.stopPropagation()}
style={{ position: ' absolute ', top: menuPosition.top + 7, left: menuPosition.left }}
>
<div className={`post-menu-reply post-menu-reply-${reply.cid}`} style={{ display: openMenuCid === reply.cid ? ' block ' : ' none ' }}>
<ul className=' post - menu - catalog '>
<li
onClick={() => {
handleOptionClick(reply.cid);
handleShareClick(thread.subplebbitAddress, reply.cid);
}}
>
Share post
</li>
<VerifiedAuthor commentCid={reply.cid}>
{({ authorAddress }) => (
<>
{authorAddress === account?.author.address || authorAddress === account?.signer.address ? (
<>
<li onClick={() => handleAuthorEditClick(reply)}>Edit post</li>
<li onClick={() => handleAuthorDeleteClick(reply)}>Delete post</li>
</>
) : null}
{isModerator ? (
<>
{authorAddress === account?.author.address || authorAddress === account?.signer.address ? null : (
<li
onClick={() => {
setSelectedAddress(thread.subplebbitAddress);
setModeratingCommentCid(reply.cid);
setIsModerationOpen(true);
handleOptionClick(reply.cid);
setDeletePost(true);
}}
>
Delete post
</li>
)}
<li
onClick={() => {
setSelectedAddress(thread.subplebbitAddress);
setModeratingCommentCid(reply.cid);
setIsModerationOpen(true);
handleOptionClick(reply.cid);
}}
>
Mod tools
</li>
</>
) : null}
</>
)}
</VerifiedAuthor>
{replyMediaInfo && (replyMediaInfo.type === ' image ' || (replyMediaInfo.type === ' webpage ' && replyMediaInfo.thumbnail)) ? (
<li
onMouseOver={() => {
setIsImageSearchOpen(true);
}}
onMouseLeave={() => {
setIsImageSearchOpen(false);
}}
>
Image search »
<ul className=' dropdown - menu post - menu - catalog ' style={{ display: isImageSearchOpen ? ' block ' : ' none ' }}>
<li onClick={() => handleOptionClick(reply.cid)}>
<a href={`https://lens.google.com/uploadbyurl?url=${replyMediaInfo.url}`} target=' _blank ' rel=' noreferrer '>
Google
</a>
</li>
<li onClick={() => handleOptionClick(reply.cid)}>
<a
href={`https://yandex.com/images/search?img_url=${replyMediaInfo.url}&rpt=imageview`}
target=' _blank '
rel=' noreferrer '
>
Yandex
</a>
</li>
<li onClick={() => handleOptionClick(reply.cid)}>
<a href={`https://saucenao.com/search.php?url=${replyMediaInfo.url}`} target=' _blank ' rel=' noreferrer '>
SauceNAO
</a>
</li>
</ul>
</li>
) : null}
<li
onMouseOver={() => {
setIsClientRedirectMenuOpen(true);
}}
onMouseLeave={() => {
setIsClientRedirectMenuOpen(false);
}}
>
View on »
<ul className=' dropdown - menu post - menu - catalog ' style={{ display: isClientRedirectMenuOpen ? ' block ' : ' none ' }}>
<li onClick={() => handleOptionClick(reply.cid)}>
<a href={`https://plebbitapp.eth.limo/#/p/${thread.subplebbitAddress}/c/${reply.cid}`} target=' _blank ' rel=' noreferrer '>
Plebbit
</a>
</li>
{/* <li onClick={() => handleOptionClick(reply.cid)}>
2023-08-09 13:03:20 +02:00
<a
2023-08-31 11:55:11 +02:00
href={`https://seedit.eth.limo/#/p/${thread.subplebbitAddress}/c/${reply.cid}`}
2023-08-09 13:03:20 +02:00
target="_blank" rel="noreferrer"
>Seedit</a>
</li> */}
2023-09-21 14:38:12 +02:00
<li onClick={() => handleOptionClick(reply.cid)}>
<a href={`https://plebones.eth.limo/#/p/${thread.subplebbitAddress}/c/${reply.cid}`} target=' _blank ' rel=' noreferrer '>
Plebones
</a>
</li>
</ul>
</li>
</ul>
</div>
</PostMenuCatalog>,
document.body,
)}
<div key={`bi-${index}`} id=' backlink - id ' className=' backlink '>
{reply.replies?.pages?.topAll.comments
.sort((a, b) => a.timestamp - b.timestamp)
.map((reply, index) => (
<div
key={`div-${index}`}
style={{ display: ' inline - block ' }}
ref={(el) => {
backlinkRefs.current[reply.cid] = el;
}}
>
<Link
key={`ql-${index}`}
to={() => {}}
className=' quote - link '
onClick={(event) => handleQuoteClick(reply, null, event)}
onMouseOver={(event) => {
event.stopPropagation();
handleQuoteHover(reply, null, () => {
setOutOfViewCid(reply.cid);
const rect = backlinkRefs.current[reply.cid].getBoundingClientRect();
const distanceToRight = window.innerWidth - rect.right;
const distanceToTop = rect.top;
const distanceToBottom = window.innerHeight - rect.bottom;
let top;
2023-06-11 13:59:56 +02:00
2023-09-21 14:38:12 +02:00
if (distanceToTop < postOnHoverHeight / 2) {
top = window.scrollY - 5;
} else if (distanceToBottom < postOnHoverHeight / 2) {
top = window.scrollY - postOnHoverHeight + window.innerHeight - 10;
} else {
top = rect.top + window.scrollY - postOnHoverHeight / 2;
}
2023-06-10 12:28:21 +02:00
2023-09-21 14:38:12 +02:00
if (distanceToRight < 200) {
setOutOfViewPosition({
top,
right: window.innerWidth - rect.left - 10,
maxWidth: rect.left - 5,
});
} else {
setOutOfViewPosition({
top,
left: rect.left + rect.width + 5,
maxWidth: window.innerWidth - rect.left - rect.width - 5,
});
}
});
}}
onMouseLeave={() => {
removeHighlight();
setOutOfViewCid(null);
}}
>
c/{reply.shortCid}
</Link>
</div>
))}
</div>
</div>
{replyMediaInfo?.url ? (
<div key={`f-${index}`} className=' file ' style={{ marginBottom: ' 5 px ' }}>
<div key={`ft-${index}`} className=' file - text '>
Link:
<a key={`fa-${index}`} href={replyMediaInfo.url} target=' _blank ' rel=' noopener noreferrer '>
{replyMediaInfo?.url.length > 30 ? replyMediaInfo?.url.slice(0, 30) + ' (...) ' : replyMediaInfo?.url}
</a>
{replyMediaInfo?.type === ' iframe ' ? null : ` (${replyMediaInfo?.type})`}
{((isReplyThumbnailClicked[index] && (replyMediaInfo.type === ' iframe ' || replyMediaInfo.type === ' video ')) ||
(replyMediaInfo.type === ' iframe ' && !replyMediaInfo.thumbnail)) && (
<span>
[
<span
className=' reply - link '
style={{ textDecoration: ' underline ', cursor: ' pointer ' }}
onClick={() => {
handleThumbnailClick(index, ' reply ');
}}
>
{isReplyThumbnailClicked[index] ? ' Close ' : ' Embed '}
</span>
]
</span>
)}
</div>
{replyMediaInfo?.type === ' iframe ' && (
<div key={`enlarge-${index}`} className={`img-container ${isReplyThumbnailClicked[index] ? ' expanded - container ' : ''}`}>
<span
key={`fta-${index}`}
className=' file - thumb - reply '
style={
isReplyThumbnailClicked[index] || !replyMediaInfo.thumbnail ? {} : { width: replyDisplayWidth, height: replyDisplayHeight }
}
>
{isReplyThumbnailClicked[index] && replyMediaInfo.url ? (
<Embed url={replyMediaInfo.url} key={`fti-${index}`} />
) : replyMediaInfo.thumbnail ? (
<img
key={`fti-${index}`}
src={replyMediaInfo.thumbnail}
alt=' thumbnail '
onClick={() => {
handleThumbnailClick(index, ' reply ');
}}
style={{ cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : null}
</span>
</div>
)}
{replyMediaInfo?.type === ' webpage ' && (
<div key={`enlarge-${index}`} className=' img - container '>
<span
key={`fta-${index}`}
className=' file - thumb - reply '
style={isReplyThumbnailClicked[index] || !reply.thumbnailUrl ? {} : { width: replyDisplayWidth, height: replyDisplayHeight }}
>
{reply.thumbnailUrl ? (
<img
key={`fti-${index}`}
src={replyMediaInfo.thumbnail}
alt={replyMediaInfo.type}
onClick={(e) => {
handleImageClick(e);
handleThumbnailClick(index, ' reply ');
}}
style={{ cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : null}
</span>
</div>
)}
{replyMediaInfo?.type === ' image ' && (
<div key={`enlarge-${index}`} className=' img - container '>
<span
key={`fta-${index}`}
className=' file - thumb - reply '
style={isReplyThumbnailClicked[index] ? {} : { width: replyDisplayWidth, height: replyDisplayHeight }}
>
<img
key={`fti-${index}`}
src={replyMediaInfo.url}
alt={replyMediaInfo.type}
onClick={(e) => {
handleImageClick(e);
handleThumbnailClick(index, ' reply ');
}}
style={{ cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
</span>
</div>
)}
{replyMediaInfo?.type === ' video ' && (
<div key={`enlarge-${index}`} className={`img-container ${isReplyThumbnailClicked[index] ? ' expanded - container ' : ''}`}>
<span
key={`fta-${index}`}
className=' file - thumb - reply '
style={isReplyThumbnailClicked[index] ? {} : { width: replyDisplayWidth, height: replyDisplayHeight }}
>
{isReplyThumbnailClicked[index] ? (
<video
className=' enlarged '
key={`fti-${index}`}
src={replyMediaInfo.url}
controls
style={{ cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : (
<video
key={`fti-${index}`}
src={replyMediaInfo.url}
alt=' thumbnail '
onClick={() => {
handleThumbnailClick(index, ' reply ');
}}
style={{ cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
)}
</span>
</div>
)}
{replyMediaInfo?.type === ' audio ' && (
<span key={`fta-${index}`} className=' file - thumb - reply '>
<audio
controls
key={`fti-${index}`}
src={replyMediaInfo.url}
alt={replyMediaInfo.type}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
</span>
)}
</div>
) : null}
{reply.content ? (
reply.content?.length > 500 ? (
<Fragment key={`fragment8-${index}`}>
<blockquote key={`pm-${index}`} comment={reply} className=' post - message '>
<Link
to={() => {}}
key={`r-pm-${index}`}
className=' quotelink '
ref={(el) => {
quoteRefs.current[reply.cid] = el;
}}
onClick={(event) => handleQuoteClick(reply, shortParentCid, event)}
onMouseOver={(event) => {
event.stopPropagation();
handleQuoteHover(reply, shortParentCid, () => {
setOutOfViewCid(reply.parentCid);
const rect = quoteRefs.current[reply.cid].getBoundingClientRect();
const distanceToRight = window.innerWidth - rect.right;
const distanceToTop = rect.top;
const distanceToBottom = window.innerHeight - rect.bottom;
let top;
2023-06-10 12:28:21 +02:00
2023-09-21 14:38:12 +02:00
if (distanceToTop < postOnHoverHeight / 2) {
top = window.scrollY - 5;
} else if (distanceToBottom < postOnHoverHeight / 2) {
top = window.scrollY - postOnHoverHeight + window.innerHeight - 10;
} else {
top = rect.top + window.scrollY - postOnHoverHeight / 2;
}
2023-06-13 21:20:19 +02:00
2023-09-21 14:38:12 +02:00
if (distanceToRight < 200) {
setOutOfViewPosition({
top,
right: window.innerWidth - rect.left - 10,
maxWidth: rect.left - 5,
});
} else {
setOutOfViewPosition({
top,
left: rect.left + rect.width + 5,
maxWidth: window.innerWidth - rect.left - rect.width - 5,
});
}
});
}}
onMouseLeave={() => {
removeHighlight();
setOutOfViewCid(null);
}}
>
{`c/${shortParentCid}`}
{shortParentCid === thread.shortCid ? ' ( OP ) ' : null}
</Link>
<Post
key={`post-${index}`}
content={reply.content?.slice(0, 500)}
postQuoteRef={(quoteShortParentCid, ref) => {
postRefs.current[quoteShortParentCid] = ref;
}}
postQuoteOnClick={(quoteShortParentCid) => {
handleQuoteClick(reply, quoteShortParentCid, null);
}}
postQuoteOnOver={(quoteShortParentCid) => {
const quoteParentCid = cidTracker[quoteShortParentCid];
if (outOfViewCid !== quoteParentCid) {
handleQuoteHover(reply, quoteShortParentCid, () => {
setOutOfViewCid(quoteParentCid);
2023-06-13 21:20:19 +02:00
2023-09-21 14:38:12 +02:00
const rect = postRefs.current[quoteShortParentCid].getBoundingClientRect();
const distanceToRight = window.innerWidth - rect.right;
const distanceToTop = rect.top;
const distanceToBottom = window.innerHeight - rect.bottom;
let top;
2023-06-13 21:20:19 +02:00
2023-09-21 14:38:12 +02:00
if (distanceToTop < postOnHoverHeight / 2) {
top = window.scrollY - 5;
} else if (distanceToBottom < postOnHoverHeight / 2) {
top = window.scrollY - postOnHoverHeight + window.innerHeight - 10;
} else {
top = rect.top + window.scrollY - postOnHoverHeight / 2;
}
2023-05-03 11:01:41 +02:00
2023-09-21 14:38:12 +02:00
if (distanceToRight < 200) {
setOutOfViewPosition({
top,
right: window.innerWidth - rect.left - 10,
maxWidth: rect.left - 5,
});
} else {
setOutOfViewPosition({
top,
left: rect.left + rect.width + 5,
maxWidth: window.innerWidth - rect.left - rect.width - 5,
});
}
});
}
}}
postQuoteOnLeave={() => {
removeHighlight();
setOutOfViewCid(null);
}}
/>
<span key={`ttl-s-${index}`} className=' ttl '>
{' '}
(...)
<br key={`ttl-s-br1-${index}`} />
<EditLabel key={`edit-label-reply-${index}`} commentCid={reply.cid} className=' ttl ' />
<StateLabel key={`state-label-reply-${index}`} commentIndex={reply.index} className=' ttl ellipsis ' />
<br key={`ttl-s-br2${reply.cid}`} />
Comment too long.
<Link
key={`ttl-l-${index}`}
to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
onClick={() => setSelectedThread(thread.cid)}
className=' ttl - link '
>
Click here
</Link>
to view.{' '}
</span>
</blockquote>
</Fragment>
) : (
<blockquote key={`pm-${index}`} className=' post - message '>
<Link
to={() => {}}
key={`r-pm-${index}`}
className=' quotelink '
ref={(el) => {
quoteRefs.current[reply.cid] = el;
}}
onClick={(event) => handleQuoteClick(reply, shortParentCid, event)}
onMouseOver={(event) => {
event.stopPropagation();
handleQuoteHover(reply, shortParentCid, () => {
setOutOfViewCid(reply.parentCid);
const rect = quoteRefs.current[reply.cid].getBoundingClientRect();
const distanceToRight = window.innerWidth - rect.right;
const distanceToTop = rect.top;
const distanceToBottom = window.innerHeight - rect.bottom;
let top;
if (distanceToTop < postOnHoverHeight / 2) {
top = window.scrollY - 5;
} else if (distanceToBottom < postOnHoverHeight / 2) {
top = window.scrollY - postOnHoverHeight + window.innerHeight - 10;
} else {
top = rect.top + window.scrollY - postOnHoverHeight / 2;
}
if (distanceToRight < 200) {
setOutOfViewPosition({
top,
right: window.innerWidth - rect.left - 10,
maxWidth: rect.left - 5,
});
} else {
setOutOfViewPosition({
top,
left: rect.left + rect.width + 5,
maxWidth: window.innerWidth - rect.left - rect.width - 5,
});
}
});
}}
onMouseLeave={() => {
removeHighlight();
setOutOfViewCid(null);
}}
>
{`c/${shortParentCid}`}
{shortParentCid === thread.shortCid ? ' ( OP ) ' : null}
</Link>
<Post
key={`post-${index}`}
content={reply.content}
postQuoteRef={(quoteShortParentCid, ref) => {
postRefs.current[quoteShortParentCid] = ref;
}}
postQuoteOnClick={(quoteShortParentCid) => {
handleQuoteClick(reply, quoteShortParentCid, null);
}}
postQuoteOnOver={(quoteShortParentCid) => {
const quoteParentCid = cidTracker[quoteShortParentCid];
if (outOfViewCid !== quoteParentCid) {
handleQuoteHover(reply, quoteShortParentCid, () => {
setOutOfViewCid(quoteParentCid);
const rect = postRefs.current[quoteShortParentCid].getBoundingClientRect();
const distanceToRight = window.innerWidth - rect.right;
const distanceToTop = rect.top;
const distanceToBottom = window.innerHeight - rect.bottom;
let top;
if (distanceToTop < postOnHoverHeight / 2) {
top = window.scrollY - 5;
} else if (distanceToBottom < postOnHoverHeight / 2) {
top = window.scrollY - postOnHoverHeight + window.innerHeight - 10;
} else {
top = rect.top + window.scrollY - postOnHoverHeight / 2;
}
if (distanceToRight < 200) {
setOutOfViewPosition({
top,
right: window.innerWidth - rect.left - 10,
maxWidth: rect.left - 5,
});
} else {
setOutOfViewPosition({
top,
left: rect.left + rect.width + 5,
maxWidth: window.innerWidth - rect.left - rect.width - 5,
});
}
});
}
}}
postQuoteOnLeave={() => {
removeHighlight();
setOutOfViewCid(null);
}}
/>
<EditLabel key={`edit-label-reply-${index}`} commentCid={reply.cid} className=' ttl ' />
<StateLabel key={`state-label-reply-${index}`} commentIndex={reply.index} className=' ttl ellipsis ' />
</blockquote>
)
) : null}
</div>
</div>
);
})}
</div>
<div key={`mob-t-${index}`} className=' thread - mobile '>
{index === 0 ? <hr key={`mob-hr-${index}`} style={{ marginTop: ' 10 px ' }} /> : <hr key={`mob-hr-${index}`} />}
<div key={`mob-c-${index}`} className=' op - container '>
<div key={`mob-po-${index}`} className=' post op op - mobile '>
<div key={`mob-pi-${index}`} className=' post - info - mobile '>
<button
key={`mob-pb-${index}`}
className=' post - menu - button - mobile '
ref={(el) => {
threadMenuRefsMobile.current[thread.cid] = el;
}}
onClick={(event) => {
event.stopPropagation();
const rect = threadMenuRefsMobile.current[thread.cid].getBoundingClientRect();
setMobileMenuPosition({ top: rect.top + window.scrollY, left: rect.left });
setOpenMobileMenuCid((prevCid) => (prevCid === thread.cid ? null : thread.cid));
}}
style={{ all: ' unset ', cursor: ' pointer ' }}
>
...
</button>
{createPortal(
<PostMenuMobile
selectedStyle={selectedStyle}
ref={(el) => {
postMenuMobileRef.current = el;
}}
onClick={(event) => event.stopPropagation()}
style={{
position: ' absolute ',
display: openMobileMenuCid === thread.cid ? ' block ' : ' none ',
top: mobileMenuPosition.top + 20,
left: mobileMenuPosition.left,
}}
>
<ul className={`post-menu-mobile-thread-${thread.cid}`}>
<li
onClick={() => {
handleMobileOptionClick(thread.cid);
handleShareClick(selectedAddress, thread.cid);
}}
>
Share thread
</li>
<VerifiedAuthor commentCid={thread.cid}>
{({ authorAddress }) => (
<>
{authorAddress === account?.author.address || authorAddress === account?.signer.address ? (
<>
<li onClick={() => handleAuthorEditClick(thread)}>Edit post</li>
<li onClick={() => handleAuthorDeleteClick(thread)}>Delete post</li>
</>
) : null}
{isModerator ? (
<>
{authorAddress === account?.author.address || authorAddress === account?.signer.address ? null : (
<li
onClick={() => {
setModeratingCommentCid(thread.cid);
setIsModerationOpen(true);
handleMobileOptionClick(thread.cid);
setDeletePost(true);
}}
>
Delete post
</li>
)}
<li
onClick={() => {
setModeratingCommentCid(thread.cid);
setIsModerationOpen(true);
handleMobileOptionClick(thread.cid);
}}
>
Mod tools
</li>
</>
) : null}
</>
)}
</VerifiedAuthor>
{commentMediaInfo && (commentMediaInfo.type === ' image ' || (commentMediaInfo.type === ' webpage ' && commentMediaInfo.thumbnail)) ? (
<>
<a
style={{ color: ' inherit ', textDecoration: ' none ' }}
href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`}
target=' _blank '
rel=' noreferrer '
>
<li onClick={() => handleMobileOptionClick(thread.cid)}>Search image on Google</li>
</a>
<a
style={{ color: ' inherit ', textDecoration: ' none ' }}
href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`}
target=' _blank '
rel=' noreferrer '
>
<li onClick={() => handleMobileOptionClick(thread.cid)}>Search image on Yandex</li>
</a>
<a
style={{ color: ' inherit ', textDecoration: ' none ' }}
href={`https://saucenao.com/search.php?url=${commentMediaInfo.url}`}
target=' _blank '
rel=' noreferrer '
>
<li onClick={() => handleMobileOptionClick(thread.cid)}>Search image on SauceNAO</li>
</a>
</>
) : null}
</ul>
</PostMenuMobile>,
document.body,
)}
<span key={`mob-nbm-${index}`} className=' name - block - mobile '>
{thread.author.displayName ? (
thread.author.displayName.length > 20 ? (
<Fragment key={`fragment9-${index}`}>
<span
key={`mob-n-${index}`}
className=' name - mobile '
data-tooltip-id=' tooltip '
data-tooltip-content={thread.author.displayName}
data-tooltip-place=' top '
>
{thread.author.displayName.slice(0, 20) + ' (...) '}
</span>
</Fragment>
) : (
<span key={`mob-n-${index}`} className=' name - mobile '>
{thread.author.displayName}
</span>
)
) : (
<span key={`mob-n-${index}`} className=' name - mobile '>
Anonymous
</span>
)}
<span
key={`mob-pa-${index}`}
className=' poster - address - mobile address - mobile '
id=' reply - button '
style={{ cursor: ' pointer ' }}
onClick={() =>
handleAddressClick(
<VerifiedAuthor commentCid={thread.cid}>{({ shortAuthorAddress }) => shortAuthorAddress || ' aPL4c3H0Ld3r '}</VerifiedAuthor>,
)
}
>
(u/
<span key={`mob-ha-${index}`} className=' highlight - address - mobile '>
{<VerifiedAuthor commentCid={thread.cid}>{({ shortAuthorAddress }) => shortAuthorAddress || ' aPL4c3H0Ld3r '}</VerifiedAuthor>}
</span>
)
</span>
<br key={`mob-br1-${index}`} />
{thread.title ? (
thread.title.length > 30 ? (
<Fragment key={`fragment11-${index}`}>
<span
key={`mob-t-${index}`}
className=' subject - mobile '
data-tooltip-id=' tooltip '
data-tooltip-content={thread.title}
data-tooltip-place=' top '
>
{thread.title.slice(0, 30) + ' (...) '}
</span>
</Fragment>
) : (
<span key={`mob-t-${index}`} className=' subject - mobile '>
{thread.title}
</span>
)
) : null}
</span>
<span key={`p-t-mob-span-${index}`} className=' date - time - mobile highlight - address - mobile '>
p/
<Link key={`p-t-${index}`} to={`/p/${thread.subplebbitAddress}`} id=' reply - button ' title=' Visit this board '>
{thread.subplebbitAddress.includes(' . eth ') ? (
thread.subplebbitAddress
) : (
<span key={`short-add${index}`} data-tooltip-id=' tooltip ' data-tooltip-content={thread.subplebbitAddress} data-tooltip-place=' top '>
{thread.subplebbitAddress.slice(0, 10) + ' (...) '}
</span>
)}
</Link>
<OfflineIndicator
key={`offline-indicator-${index}`}
address={thread.subplebbitAddress}
tooltipPlace=' top '
className=' offline - mobile - sub '
/>
</span>
<span
key={`mob-dt-${index}`}
className=' date - time - mobile post - number - mobile '
data-tooltip-id=' tooltip '
data-tooltip-content={getFormattedTime(thread.timestamp)}
data-tooltip-place=' top '
>
{getDate(thread.timestamp)}
<span key={`mob-no-${index}`}>c/</span>
<Link
to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
id=' reply - button '
key={`mob-no2-${index}`}
onClick={(e) => {
if (e.button === 2) return;
e.preventDefault();
setSelectedThreadCid(thread.cid);
setSelectedAddress(thread.subplebbitAddress);
let text = document.getSelection().toString();
text = text ? `>${text}` : text;
setSelectedText(text);
if (isReplyOpen) {
setReplyQuoteCid(thread.shortCid);
setTriggerInsertion(Date.now());
} else {
setIsReplyOpen(true);
setSelectedShortCid(thread.shortCid);
setSelectedParentCid(thread.cid);
}
}}
title=' Reply to this post '
>
{thread.shortCid}
</Link>
</span>
</div>
{thread.link ? (
<div key={`mob-f-${index}`} className=' file - mobile '>
{commentMediaInfo?.type === ' iframe ' && (
<div key={`enlarge-mob-${index}`} className=' img - container '>
<span
key={`mob-fta-${index}`}
className=' file - thumb - mobile '
style={isMobileThreadThumbnailClicked[index] ? {} : { marginBottom: ' 25 px ' }}
>
{isMobileThreadThumbnailClicked[index] && commentMediaInfo.url ? (
<div style={{ width: ' 92 vw ' }}>
<Embed url={commentMediaInfo.url} key={`fti-mobile-${index}`} />
</div>
) : commentMediaInfo.thumbnail ? (
<img
key={`mob-fti-${index}`}
src={commentMediaInfo.thumbnail}
alt=' thumbnail '
onClick={() => {
handleThumbnailClick(index, ' mobileThread ');
}}
style={{ cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : (
<span
onClick={() => {
handleThumbnailClick(index, ' mobileThread ');
}}
>
Embed
</span>
)}
{commentMediaInfo?.type === ' video ' || commentMediaInfo?.type === ' iframe ' ? (
isMobileThreadThumbnailClicked[index] ? (
<div style={{ textAlign: ' center ', marginTop: ' 15 px ', marginBottom: ' 15 px ' }}>
<span
className=' button - mobile '
style={{ float: ' none ', cursor: ' pointer ' }}
onClick={() => {
handleThumbnailClick(index, ' mobileThread ');
}}
>
Close
</span>
</div>
) : (
<div key={`mob-fi-${index}`} className=' file - info - mobile '>
webpage
</div>
)
) : (
<div key={`mob-fi-${index}`} className=' file - info - mobile '>
webpage
</div>
)}
</span>
</div>
)}
{commentMediaInfo?.url ? (
commentMediaInfo.type === ' webpage ' ? (
<div key={`enlarge-mob-${index}`} className=' img - container '>
<span
key={`mob-ft${thread.cid}`}
className=' file - thumb - mobile '
style={
isMobileThreadThumbnailClicked[index] || !thread.thumbnailUrl
? {}
: { width: displayWidthMobile, height: displayHeightMobile, marginBottom: ' 25 px ' }
}
>
{thread.thumbnailUrl ? (
<img
key={`mob-img-${index}`}
src={commentMediaInfo.thumbnail}
alt=' thumbnail '
onClick={(e) => {
handleImageClick(e);
handleThumbnailClick(index, ' mobileThread ');
}}
style={{ cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : null}
<div key={`mob-fi-${index}`} className=' file - info - mobile '>
{commentMediaInfo?.type}
</div>
</span>
</div>
) : commentMediaInfo.type === ' image ' ? (
<div key={`enlarge-mob-${index}`} className=' img - container '>
<span
key={`mob-ft${thread.cid}`}
className=' file - thumb - mobile '
style={
isMobileThreadThumbnailClicked[index] ? {} : { width: displayWidthMobile, height: displayHeightMobile, marginBottom: ' 25 px ' }
}
>
<img
key={`mob-img-${index}`}
src={commentMediaInfo.url}
alt={commentMediaInfo.type}
onClick={(e) => {
handleImageClick(e);
handleThumbnailClick(index, ' mobileThread ');
}}
style={{ cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
<div key={`mob-fi-${index}`} className=' file - info - mobile '>
{commentMediaInfo?.type}
</div>
</span>
</div>
) : commentMediaInfo.type === ' video ' ? (
<span
key={`mob-ft${thread.cid}`}
className=' file - thumb - mobile '
style={isMobileThreadThumbnailClicked[index] ? {} : { marginBottom: ' 25 px ' }}
>
{isMobileThreadThumbnailClicked[index] ? (
<video
key={`fti-${index}`}
src={commentMediaInfo.url}
alt={commentMediaInfo.type}
controls
style={{ cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : (
<video
key={`fti-${index}`}
src={commentMediaInfo.url}
alt=' thumbnail '
onClick={() => {
handleThumbnailClick(index, ' mobileThread ');
}}
style={{ cursor: ' pointer ' }}
id=' video - thumbnail - mobile '
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
)}
{commentMediaInfo?.type === ' video ' || commentMediaInfo?.type === ' iframe ' ? (
isMobileThreadThumbnailClicked[index] ? (
<div style={{ textAlign: ' center ', marginTop: ' 15 px ', marginBottom: ' 15 px ' }}>
<span
className=' button - mobile '
style={{ float: ' none ', cursor: ' pointer ' }}
onClick={() => {
handleThumbnailClick(index, ' mobileThread ');
}}
>
Close
</span>
</div>
) : (
<div key={`mob-fi-${index}`} className=' file - info - mobile '>
video
</div>
)
) : (
<div key={`mob-fi-${index}`} className=' file - info - mobile '>
video
</div>
)}
</span>
) : commentMediaInfo.type === ' audio ' ? (
<span
key={`mob-ft${thread.cid}`}
className=' file - thumb - mobile '
style={
isMobileThreadThumbnailClicked[index] ? {} : { width: displayWidthMobile, height: displayHeightMobile, marginBottom: ' 25 px ' }
}
>
<audio
key={`mob-img-${index}`}
src={commentMediaInfo.url}
alt={commentMediaInfo.type}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
<div key={`mob-fi-${index}`} className=' file - info - mobile '>
{commentMediaInfo?.type}
</div>
</span>
) : null
) : null}
</div>
) : null}
{thread.content ? (
thread.content?.length > 500 ? (
<Fragment key={`fragment12-${index}`}>
<blockquote key={`mob-bq-${index}`} className=' post - message - mobile '>
<Post content={thread.content?.slice(0, 500)} key={`post-mobile-${index}`} />
<span key={`mob-ttl-s-${index}`} className=' ttl '>
{' '}
(...)
<br key={`mob-ttl-s-br1-${index}`} />
<EditLabel key={`edit-label-thread-mob-${index}`} commentCid={thread.cid} className=' ttl ' />
<StateLabel key={`state-label-thread-mob-${index}`} commentIndex={thread.index} className=' ttl ellipsis ' />
<br key={`mob-ttl-s-br2${thread.cid}`} />
Post too long.
<Link
key={`mob-ttl-l-${index}`}
to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
onClick={() => setSelectedThread(thread.cid)}
className=' ttl - link '
>
Click here
</Link>
to view.{' '}
</span>
</blockquote>
</Fragment>
) : (
<blockquote key={`mob-bq-${index}`} className=' post - message - mobile '>
<Post content={thread.content} key={`post-mobile-${index}`} />
<EditLabel key={`edit-label-thread-mob-${index}`} commentCid={thread.cid} className=' ttl ' />
<StateLabel key={`state-label-thread-mob-${index}`} commentIndex={thread.index} className=' ttl ellipsis ' />
</blockquote>
)
) : null}
</div>
<div key={`mob-pl-${index}`} className=' post - link - mobile '>
<span key={`mob-info-${index}`} className=' info - mobile '>
{thread.replyCount + pendingReplyCounts[thread.cid] === 0
? ' No replies '
: thread.replyCount + pendingReplyCounts[thread.cid] === 1
? ' 1 reply '
: thread.replyCount + pendingReplyCounts[thread.cid] > 1
? thread.replyCount + pendingReplyCounts[thread.cid] + ' replies '
: null}
</span>
<Link
key={`rl2-${index}`}
to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
onClick={() => setSelectedThread(thread.cid)}
className=' button - mobile '
>
View Thread
</Link>
</div>
</div>
{displayedReplies?.map((reply, index) => {
if (editedComments[reply.cid]) {
reply = editedComments[reply.cid];
if (reply.removed) {
reply.content = ' [ removed ] ';
reply.link = undefined;
}
}
const replyMediaInfo = getCommentMediaInfo(reply);
const shortParentCid = findShortParentCid(reply.parentCid, selectedFeed);
return (
<div key={`mob-rc-${index}`} className=' reply - container '>
<div key={`mob-pr-${index}`} className=' post - reply post - reply - mobile '>
<div key={`mob-pi-${index}`} className=' post - info - mobile '>
<button
key={`mob-pb-${index}`}
className=' post - menu - button - mobile '
ref={(el) => {
replyMenuRefsMobile.current[reply.cid] = el;
}}
onClick={(event) => {
event.stopPropagation();
const rect = replyMenuRefsMobile.current[reply.cid].getBoundingClientRect();
setMobileMenuPosition({ top: rect.top + window.scrollY, left: rect.left });
setOpenMobileMenuCid((prevCid) => (prevCid === reply.cid ? null : reply.cid));
}}
style={{ all: ' unset ', cursor: ' pointer ' }}
>
...
</button>
{createPortal(
<PostMenuMobile
selectedStyle={selectedStyle}
ref={(el) => {
postMenuMobileRef.current = el;
}}
onClick={(event) => event.stopPropagation()}
style={{
position: ' absolute ',
display: openMobileMenuCid === reply.cid ? ' block ' : ' none ',
top: mobileMenuPosition.top + 20,
left: mobileMenuPosition.left,
}}
>
<ul className={`post-menu-mobile-thread-${reply.cid}`}>
<li
onClick={() => {
handleMobileOptionClick(reply.cid);
handleShareClick(selectedAddress, reply.cid);
}}
>
Share post
</li>
<VerifiedAuthor commentCid={reply.cid}>
{({ authorAddress }) => (
<>
{authorAddress === account?.author.address || authorAddress === account?.signer.address ? (
<>
<li onClick={() => handleAuthorEditClick(reply)}>Edit post</li>
<li onClick={() => handleAuthorDeleteClick(reply)}>Delete post</li>
</>
) : null}
{isModerator ? (
<>
{authorAddress === account?.author.address || authorAddress === account?.signer.address ? null : (
<li
onClick={() => {
setModeratingCommentCid(reply.cid);
setIsModerationOpen(true);
handleMobileOptionClick(reply.cid);
setDeletePost(true);
}}
>
Delete post
</li>
)}
<li
onClick={() => {
setModeratingCommentCid(reply.cid);
setIsModerationOpen(true);
handleMobileOptionClick(reply.cid);
}}
>
Mod tools
</li>
</>
) : null}
</>
)}
</VerifiedAuthor>
{replyMediaInfo && (replyMediaInfo.type === ' image ' || (replyMediaInfo.type === ' webpage ' && replyMediaInfo.thumbnail)) ? (
<>
<a
style={{ color: ' inherit ', textDecoration: ' none ' }}
href={`https://lens.google.com/uploadbyurl?url=${replyMediaInfo.url}`}
target=' _blank '
rel=' noreferrer '
>
<li onClick={() => handleMobileOptionClick(reply.cid)}>Search image on Google</li>
</a>
<a
style={{ color: ' inherit ', textDecoration: ' none ' }}
href={`https://yandex.com/images/search?url=${replyMediaInfo.url}`}
target=' _blank '
rel=' noreferrer '
>
<li onClick={() => handleMobileOptionClick(reply.cid)}>Search image on Yandex</li>
</a>
<a
style={{ color: ' inherit ', textDecoration: ' none ' }}
href={`https://saucenao.com/search.php?url=${replyMediaInfo.url}`}
target=' _blank '
rel=' noreferrer '
>
<li onClick={() => handleMobileOptionClick(reply.cid)}>Search image on SauceNAO</li>
</a>
</>
) : null}
</ul>
</PostMenuMobile>,
document.body,
)}
<span key={`mob-nb-${index}`} className=' name - block - mobile '>
{reply.author.displayName ? (
reply.author.displayName.length > 20 ? (
<Fragment key={`fragment13-${index}`}>
<span
key={`mob-n-${index}`}
className=' name - mobile '
data-tooltip-id=' tooltip '
data-tooltip-content={reply.author.displayName}
data-tooltip-place=' top '
>
{reply.author.displayName.slice(0, 20) + ' (...) '}
</span>
</Fragment>
) : (
<span key={`mob-n-${index}`} className=' name - mobile '>
{reply.author.displayName}
</span>
)
) : (
<span key={`mob-n-${index}`} className=' name - mobile '>
Anonymous
</span>
)}
<span
key={`mob-pa-${index}`}
className=' poster - address - mobile address - mobile '
id=' reply - button '
style={{ cursor: ' pointer ' }}
onClick={() =>
handleAddressClick(
<VerifiedAuthor commentCid={reply.cid}>{({ shortAuthorAddress }) => shortAuthorAddress || ' aPL4c3H0Ld3r '}</VerifiedAuthor>,
)
}
>
(u/
{reply.author?.shortAddress ? (
<span key={`mob-ha-${index}`} className=' highlight - address - mobile '>
{reply.author?.shortAddress}
</span>
) : (
<span key={`mob-ha-${index}`} data-tooltip-id=' tooltip ' data-tooltip-content={account?.author?.address} data-tooltip-place=' top '>
{account?.author?.address.slice(0, 8) + ' (...) '}
</span>
)}
)
</span>
<br key={`mob-br-${index}`} />
</span>
<span key={`p-t-mob-span-${index}`} className=' date - time - mobile highlight - address - mobile '>
p/
<Link key={`p-t-${index}`} to={`/p/${thread.subplebbitAddress}`} id=' reply - button ' title=' Visit this board '>
{thread.subplebbitAddress.includes(' . eth ') ? (
thread.subplebbitAddress
) : (
<span
key={`short-add${index}`}
data-tooltip-id=' tooltip '
data-tooltip-content={thread.subplebbitAddress}
data-tooltip-place=' top '
>
{thread.subplebbitAddress.slice(0, 10) + ' (...) '}
</span>
)}
</Link>
<OfflineIndicator
key={`offline-indicator-${index}`}
address={thread.subplebbitAddress}
tooltipPlace=' top '
className=' offline - mobile - sub - reply '
/>
</span>
<span
key={`mob-dt-${index}`}
className=' date - time - mobile post - number - mobile '
data-tooltip-id=' tooltip '
data-tooltip-content={getFormattedTime(reply.timestamp)}
data-tooltip-place=' top '
>
{getDate(reply.timestamp)}
<span key={`mob-pl1-${index}`}>c/</span>
{reply.shortCid ? (
<Link
to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
id=' reply - button '
key={`mob-pl2-${index}`}
onClick={(e) => {
if (e.button === 2) return;
e.preventDefault();
setSelectedThreadCid(thread.cid);
setSelectedAddress(thread.subplebbitAddress);
let text = document.getSelection().toString();
text = text ? `>${text}` : text;
setSelectedText(text);
if (isReplyOpen) {
setReplyQuoteCid(reply.shortCid);
setTriggerInsertion(Date.now());
} else {
setIsReplyOpen(true);
setSelectedShortCid(reply.shortCid);
setSelectedParentCid(reply.cid);
}
}}
title=' Reply to this post '
>
{reply.shortCid}
</Link>
) : (
<PendingLabel key=' pending - mob ' commentIndex={reply.index} />
)}
</span>
</div>
{reply.link ? (
<div key={`mob-f-${index}`} className=' file - mobile '>
{replyMediaInfo?.url ? (
replyMediaInfo.type === ' iframe ' ? (
<div key={`enlarge-mob-${index}`} className=' img - container '>
<span
key={`mob-fta-${index}`}
className=' file - thumb - mobile '
style={isMobileReplyThumbnailClicked[index] ? {} : { marginBottom: ' 25 px ' }}
>
{isMobileReplyThumbnailClicked[index] && replyMediaInfo.url ? (
<div style={{ width: ' 92 vw ' }}>
<Embed url={replyMediaInfo.url} key={`fti-mobile-reply-${index}`} />
</div>
) : replyMediaInfo.thumbnail ? (
<img
key={`mob-fti-${index}`}
src={replyMediaInfo.thumbnail}
alt=' thumbnail '
onClick={() => {
handleThumbnailClick(index, ' mobileReply ');
}}
style={{ cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : (
<span
onClick={() => {
handleThumbnailClick(index, ' mobileReply ');
}}
>
Embed
</span>
)}
{replyMediaInfo?.type === ' video ' || replyMediaInfo?.type === ' iframe ' ? (
isMobileReplyThumbnailClicked[index] ? (
<div style={{ textAlign: ' center ', marginTop: ' 15 px ', marginBottom: ' 15 px ' }}>
<span
className=' button - mobile '
style={{ float: ' none ', cursor: ' pointer ' }}
onClick={() => {
handleThumbnailClick(index, ' mobileReply ');
}}
>
Close
</span>
</div>
) : (
<div key={`mob-fi-${index}`} className=' file - info - mobile '>
webpage
</div>
)
) : (
<div key={`mob-fi-${index}`} className=' file - info - mobile '>
webpage
</div>
)}
</span>
</div>
) : replyMediaInfo.type === ' webpage ' ? (
<div key={`enlarge-mob-reply-${index}`} className=' img - container '>
<span
key={`mob-ft${reply.cid}`}
className=' file - thumb - mobile '
style={
isMobileReplyThumbnailClicked[index] || !reply.thumbnailUrl
? {}
: { width: displayWidthMobile, height: displayHeightMobile }
}
>
{reply.thumbnailUrl ? (
<img
key={`mob-img-${index}`}
src={replyMediaInfo.thumbnail}
alt=' thumbnail '
onClick={(e) => {
handleImageClick(e);
handleThumbnailClick(index, ' mobileReply ');
}}
style={{ cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : null}
<div key={`mob-fi-${index}`} className=' file - info - mobile '>
{replyMediaInfo.type}
</div>
</span>
</div>
) : replyMediaInfo.type === ' image ' ? (
<div key={`enlarge-mob-reply-${index}`} className=' img - container '>
<span
key={`mob-ft${reply.cid}`}
className=' file - thumb - mobile '
style={isMobileReplyThumbnailClicked[index] ? {} : { width: displayWidthMobile, height: displayHeightMobile }}
>
<img
key={`mob-img-${index}`}
src={replyMediaInfo.url}
alt={replyMediaInfo.type}
onClick={(e) => {
handleImageClick(e);
handleThumbnailClick(index, ' mobileReply ');
}}
style={{ cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
<div key={`mob-fi-${index}`} className=' file - info - mobile '>
{replyMediaInfo.type}
</div>
</span>
</div>
) : replyMediaInfo.type === ' video ' ? (
<span
key={`mob-ft${reply.cid}`}
className=' file - thumb - mobile '
style={isMobileReplyThumbnailClicked[index] ? {} : { marginBottom: ' 25 px ' }}
>
{isMobileReplyThumbnailClicked[index] ? (
<video
key={`fti-${index}`}
src={replyMediaInfo.url}
alt={replyMediaInfo.type}
controls
style={{ all: ' unset ', width: ' 100 % ', height: ' 100 % ', cursor: ' pointer ' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : (
<video
key={`fti-${index}`}
src={replyMediaInfo.url}
alt=' thumbnail '
onClick={() => {
handleThumbnailClick(index, ' mobileReply ');
}}
style={{ cursor: ' pointer ' }}
id=' video - thumbnail - mobile '
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
)}
{replyMediaInfo?.type === ' video ' || replyMediaInfo?.type === ' iframe ' ? (
isMobileReplyThumbnailClicked[index] ? (
<div style={{ textAlign: ' center ', marginTop: ' 15 px ', marginBottom: ' 15 px ' }}>
<span
className=' button - mobile '
style={{ float: ' none ', cursor: ' pointer ' }}
onClick={() => {
handleThumbnailClick(index, ' mobileReply ');
}}
>
Close
</span>
</div>
) : (
<div key={`mob-fi-${index}`} className=' file - info - mobile '>
video
</div>
)
) : (
<div key={`mob-fi-${index}`} className=' file - info - mobile '>
video
</div>
)}
</span>
) : replyMediaInfo.type === ' audio ' ? (
<span
key={`mob-ft${reply.cid}`}
className=' file - thumb - mobile '
style={isMobileReplyThumbnailClicked[index] ? {} : { width: displayWidthMobile, height: displayHeightMobile }}
>
<audio
key={`mob-img-${index}`}
src={replyMediaInfo.url}
alt={replyMediaInfo.type}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
<div key={`mob-fi-${index}`} className=' file - info - mobile '>
{replyMediaInfo.type}
</div>
</span>
) : null
) : null}
</div>
) : null}
{reply.content ? (
reply.content?.length > 500 ? (
<Fragment key={`fragment15-${index}`}>
<blockquote key={`mob-pm-${index}`} className=' post - message '>
<Link
to={() => {}}
key={`mob-r-pm-${index}`}
className=' quotelink '
ref={(el) => {
quoteRefsMobile.current[reply.cid] = el;
}}
onClick={(event) => handleQuoteClick(reply, shortParentCid, event)}
onMouseOver={(event) => {
event.stopPropagation();
handleQuoteHover(reply, shortParentCid, () => {
setOutOfViewCid(reply.parentCid);
const rect = quoteRefsMobile.current[reply.cid].getBoundingClientRect();
const distanceToRight = window.innerWidth - rect.right;
const distanceToTop = rect.top;
const distanceToBottom = window.innerHeight - rect.bottom;
let top;
if (distanceToTop < postOnHoverHeight / 2) {
top = window.scrollY - 5;
} else if (distanceToBottom < postOnHoverHeight / 2) {
top = window.scrollY - postOnHoverHeight + window.innerHeight - 10;
} else {
top = rect.top + window.scrollY - postOnHoverHeight / 2;
}
if (distanceToRight < 200) {
setOutOfViewPosition({
top,
right: window.innerWidth - rect.left - 10,
maxWidth: rect.left - 5,
});
} else {
setOutOfViewPosition({
top,
left: rect.left + rect.width + 5,
maxWidth: window.innerWidth - rect.left - rect.width - 5,
});
}
});
}}
onMouseLeave={() => {
removeHighlight();
setOutOfViewCid(null);
}}
>
{`c/${shortParentCid}`}
{shortParentCid === thread.shortCid ? ' ( OP ) ' : null}
</Link>
<Post
key={`post-mobile-${index}`}
content={reply.content?.slice(0, 500)}
postQuoteRef={(quoteShortParentCid, ref) => {
postRefs.current[quoteShortParentCid] = ref;
}}
postQuoteOnClick={(quoteShortParentCid) => {
handleQuoteClick(reply, quoteShortParentCid, null);
}}
postQuoteOnOver={(quoteShortParentCid) => {
const quoteParentCid = cidTracker[quoteShortParentCid];
if (outOfViewCid !== quoteParentCid) {
handleQuoteHover(reply, quoteShortParentCid, () => {
setOutOfViewCid(quoteParentCid);
const rect = postRefs.current[quoteShortParentCid].getBoundingClientRect();
const distanceToRight = window.innerWidth - rect.right;
const distanceToTop = rect.top;
const distanceToBottom = window.innerHeight - rect.bottom;
let top;
if (distanceToTop < postOnHoverHeight / 2) {
top = window.scrollY - 5;
} else if (distanceToBottom < postOnHoverHeight / 2) {
top = window.scrollY - postOnHoverHeight + window.innerHeight - 10;
} else {
top = rect.top + window.scrollY - postOnHoverHeight / 2;
}
if (distanceToRight < 200) {
setOutOfViewPosition({
top,
right: window.innerWidth - rect.left - 10,
maxWidth: rect.left - 5,
});
} else {
setOutOfViewPosition({
top,
left: rect.left + rect.width + 5,
maxWidth: window.innerWidth - rect.left - rect.width - 5,
});
}
});
}
}}
postQuoteOnLeave={() => {
removeHighlight();
setOutOfViewCid(null);
}}
/>
<span key={`mob-ttl-s-${index}`} className=' ttl '>
{' '}
(...)
<br key={`mob-ttl-s-br1-${index}`} />
<EditLabel key={`edit-label-reply-mob-${index}`} commentCid={reply.cid} className=' ttl ' />
<StateLabel key={`state-label-reply-mob-${index}`} commentIndex={reply.index} className=' ttl ellipsis ' />
<br key={`mob-ttl-s-br2${reply.cid}`} />
Comment too long.
<Link
key={`mob-ttl-l-${index}`}
to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
onClick={() => setSelectedThread(thread.cid)}
className=' ttl - link '
>
Click here
</Link>
to view.{' '}
</span>
</blockquote>
</Fragment>
) : (
<blockquote key={`mob-pm-${index}`} className=' post - message '>
<Link
to={() => {}}
key={`mob-r-pm-${index}`}
className=' quotelink '
ref={(el) => {
quoteRefsMobile.current[reply.cid] = el;
}}
onClick={(event) => handleQuoteClick(reply, shortParentCid, event)}
onMouseOver={(event) => {
event.stopPropagation();
handleQuoteHover(reply, shortParentCid, () => {
setOutOfViewCid(reply.parentCid);
const rect = quoteRefsMobile.current[reply.cid].getBoundingClientRect();
const distanceToRight = window.innerWidth - rect.right;
const distanceToTop = rect.top;
const distanceToBottom = window.innerHeight - rect.bottom;
let top;
if (distanceToTop < postOnHoverHeight / 2) {
top = window.scrollY - 5;
} else if (distanceToBottom < postOnHoverHeight / 2) {
top = window.scrollY - postOnHoverHeight + window.innerHeight - 10;
} else {
top = rect.top + window.scrollY - postOnHoverHeight / 2;
}
if (distanceToRight < 200) {
setOutOfViewPosition({
top,
right: window.innerWidth - rect.left - 10,
maxWidth: rect.left - 5,
});
} else {
setOutOfViewPosition({
top,
left: rect.left + rect.width + 5,
maxWidth: window.innerWidth - rect.left - rect.width - 5,
});
}
});
}}
onMouseLeave={() => {
removeHighlight();
setOutOfViewCid(null);
}}
>
{`c/${shortParentCid}`}
{shortParentCid === thread.shortCid ? ' ( OP ) ' : null}
</Link>
<Post
key={`post-mobile-${index}`}
content={reply.content}
postQuoteRef={(quoteShortParentCid, ref) => {
postRefs.current[quoteShortParentCid] = ref;
}}
postQuoteOnClick={(quoteShortParentCid) => {
handleQuoteClick(reply, quoteShortParentCid, null);
}}
postQuoteOnOver={(quoteShortParentCid) => {
const quoteParentCid = cidTracker[quoteShortParentCid];
if (outOfViewCid !== quoteParentCid) {
handleQuoteHover(reply, quoteShortParentCid, () => {
setOutOfViewCid(quoteParentCid);
const rect = postRefs.current[quoteShortParentCid].getBoundingClientRect();
const distanceToRight = window.innerWidth - rect.right;
const distanceToTop = rect.top;
const distanceToBottom = window.innerHeight - rect.bottom;
let top;
if (distanceToTop < postOnHoverHeight / 2) {
top = window.scrollY - 5;
} else if (distanceToBottom < postOnHoverHeight / 2) {
top = window.scrollY - postOnHoverHeight + window.innerHeight - 10;
} else {
top = rect.top + window.scrollY - postOnHoverHeight / 2;
}
if (distanceToRight < 200) {
setOutOfViewPosition({
top,
right: window.innerWidth - rect.left - 10,
maxWidth: rect.left - 5,
});
} else {
setOutOfViewPosition({
top,
left: rect.left + rect.width + 5,
maxWidth: window.innerWidth - rect.left - rect.width - 5,
});
}
});
}
}}
postQuoteOnLeave={() => {
removeHighlight();
setOutOfViewCid(null);
}}
/>
<EditLabel key={`edit-label-reply-mob-${index}`} commentCid={reply.cid} className=' ttl ' />
<StateLabel key={`state-label-reply-mob-${index}`} commentIndex={reply.index} className=' ttl ellipsis ' />
</blockquote>
)
) : null}
{reply.replyCount > 0 ? (
<div key={`back-mob-${index}`} className=' backlink backlink - mobile '>
{reply.replies?.pages?.topAll.comments
.sort((a, b) => a.timestamp - b.timestamp)
.map((reply, index) => (
<div
key={`div-back${index}`}
style={{ display: ' inline - block ' }}
ref={(el) => {
backlinkRefsMobile.current[reply.cid] = el;
}}
>
<Link
key={`ql-${index}`}
to={() => {}}
onClick={(event) => handleQuoteClick(reply, reply.shortCid, event)}
onMouseOver={(event) => {
event.stopPropagation();
handleQuoteHover(reply, reply.shortCid, () => {
setOutOfViewCid(reply.cid);
const rect = backlinkRefsMobile.current[reply.cid].getBoundingClientRect();
const distanceToRight = window.innerWidth - rect.right;
const distanceToTop = rect.top;
const distanceToBottom = window.innerHeight - rect.bottom;
let top;
if (distanceToTop < postOnHoverHeight / 2) {
top = window.scrollY - 5;
} else if (distanceToBottom < postOnHoverHeight / 2) {
top = window.scrollY - postOnHoverHeight + window.innerHeight - 10;
} else {
top = rect.top + window.scrollY - postOnHoverHeight / 2;
}
if (distanceToRight < 200) {
setOutOfViewPosition({
top,
right: window.innerWidth - rect.left - 10,
maxWidth: rect.left - 5,
});
} else {
setOutOfViewPosition({
top,
left: rect.left + rect.width + 5,
maxWidth: window.innerWidth - rect.left - rect.width - 5,
});
}
});
}}
onMouseLeave={() => {
removeHighlight();
setOutOfViewCid(null);
}}
className=' quote - link '
>
c/{reply.shortCid}
</Link>
</div>
))}
</div>
) : null}
</div>
</div>
);
})}
</div>
</Fragment>
);
}}
ref={virtuosoRef}
restoreStateFrom={lastVirtuosoState}
initialScrollTop={lastVirtuosoState?.scrollTop}
endReached={tryLoadMore}
useWindowScroll={true}
components={{
Footer: () => {
return isFeedCached ? null : <PostLoader />;
},
}}
/>
)}
</div>
{outOfViewCid &&
outOfViewPosition &&
createPortal(
<div
ref={postOnHoverRef}
style={{
display: ' block ',
position: ' absolute ',
top: outOfViewPosition.top,
right: outOfViewPosition.right,
left: outOfViewPosition.left,
maxWidth: outOfViewPosition.maxWidth,
}}
>
<PostOnHover cid={outOfViewCid} feed={feed} />
</div>,
document.body,
)}
</BoardForm>
<Footer selectedStyle={selectedStyle}>
<Break
id=' break '
selectedStyle={selectedStyle}
style={{
marginTop: ' - 36 px ',
width: ' 100 % ',
}}
/>
<Break
selectedStyle={selectedStyle}
style={{
width: ' 100 % ',
}}
/>
<span
className=' style - changer '
style={{
float: ' right ',
marginTop: ' 2 px ',
}}
>
Style:
<select id=' style - selector ' onChange={handleStyleChange} value={selectedStyle}>
<option value=' Yotsuba '>Yotsuba</option>
<option value=' Yotsuba - B '>Yotsuba B</option>
<option value=' Futaba '>Futaba</option>
<option value=' Burichan '>Burichan</option>
<option value=' Tomorrow '>Tomorrow</option>
<option value=' Photon '>Photon</option>
</select>
</span>
<NavBar
selectedStyle={selectedStyle}
style={{
marginTop: ' 42 px ',
}}
>
<>
<span className=' boardList '>
[
<Link to='' onClick={() => {}}>
Subscriptions
</Link>
] [
</span>
{defaultSubplebbits.map((subplebbit, index) => (
<span className=' boardList ' key={`span-${subplebbit.address}`}>
{index === 0 ? null : ' \u00a0 '}
<Link to={`/p/${subplebbit.address}`} key={`a-${subplebbit.address}`} onClick={() => handleClickTitle(subplebbit.title, subplebbit.address)}>
{subplebbit.title ? subplebbit.title : subplebbit.address}
</Link>
{index !== defaultSubplebbits.length - 1 ? ' / ' : null}
</span>
))}
<span className=' nav '>
[
<span
id=' button - span '
style={{ cursor: ' pointer ' }}
onClick={() => {
window.electron && window.electron.isElectron
? setIsCreateBoardOpen(true)
: alert(
' You can create a board with the desktop version of plebchan : \ nhttps : //github.com/plebbit/plebchan/releases/latest\n\nIf you are comfortable with the command line, use plebbit-cli:\nhttps://github.com/plebbit/plebbit-cli\n\n',
);
}}
>
Create Board
</ span >
] [
< Link to = { `/p/subscriptions/settings` } onClick = {() => setIsSettingsOpen ( true )}>
Settings
</ Link >
] [
< Link to = '/' onClick = {() => handleStyleChange ({ target : { value : 'Yotsuba' } })}>
Home
</ Link >
]
</ span >
</>
</ NavBar >
< div id = 'version' >
plebchan v { version }
{ commitRef }. GPL - 2.0
</ div >
< div
className = 'footer-links'
style = {{
textAlign : 'center' ,
fontSize : 'x-small' ,
fontFamily : 'arial' ,
marginTop : '5px' ,
marginBottom : '15px' ,
}}
>
< a style = {{ textDecoration : 'underline' }} href = 'https://plebbit.com' target = '_blank' rel = 'noopener noreferrer' >
About
</ a >
& nbsp ; • & nbsp ;
< a style = {{ textDecoration : 'underline' }} href = 'https://github.com/plebbit/plebchan/releases/latest' target = '_blank' rel = 'noopener noreferrer' >
App
</ a >
& nbsp ; • & nbsp ;
< a style = {{ textDecoration : 'underline' }} href = 'https://twitter.com/plebchan_eth' target = '_blank' rel = 'noopener noreferrer' >
Twitter
</ a >
& nbsp ; • & nbsp ;
< a style = {{ textDecoration : 'underline' }} href = 'https://t.me/plebbit' target = '_blank' rel = 'noopener noreferrer' >
Telegram
</ a >
</ div >
</ Footer >
</ Container >
</>
);
};
export default Subscriptions ;