2023-03-23 15:49:57 +01:00
import React , { useState , useEffect , Fragment } from 'react' ;
2023-03-20 17:08:05 +01:00
import { Link , useNavigate , useParams } from 'react-router-dom' ;
2023-03-23 15:49:57 +01:00
import InfiniteScroll from 'react-infinite-scroller' ;
2023-03-24 18:08:07 +01:00
import { useFeed , usePublishComment } from '@plebbit/plebbit-react-hooks' ;
2023-03-25 14:54:04 +01:00
import { debounce } from 'lodash' ;
2023-03-20 17:08:05 +01:00
import useAppStore from '../../useAppStore' ;
2023-02-19 16:48:46 +01:00
import { Container , NavBar , Header , Break , PostForm , PostFormLink , PostFormTable } from './styles/Board.styled' ;
import { Threads } from './styles/Catalog.styled' ;
2023-03-23 15:49:57 +01:00
import { TopBar } from './styles/Thread.styled' ;
2023-03-19 11:49:05 +01:00
import CaptchaModal from '../CaptchaModal' ;
2023-03-19 18:30:07 +01:00
import CatalogLoader from '../CatalogLoader' ;
2023-03-23 15:49:57 +01:00
import ImageBanner from '../ImageBanner' ;
2023-03-29 13:37:36 +02:00
import Post from '../Post' ;
2023-03-19 11:49:05 +01:00
import SettingsModal from '../SettingsModal' ;
2023-03-23 15:49:57 +01:00
import getCommentMediaInfo from '../../utils/getCommentMediaInfo' ;
2023-03-20 17:08:05 +01:00
import handleStyleChange from '../../utils/handleStyleChange' ;
2023-03-19 11:49:05 +01:00
import onError from '../../utils/onError' ;
import onSuccess from '../../utils/onSuccess' ;
2023-03-20 17:08:05 +01:00
import useClickForm from '../../hooks/useClickForm' ;
2023-02-19 16:48:46 +01:00
2023-03-02 18:19:08 +01:00
2023-03-20 17:08:05 +01:00
const Catalog = () => {
2023-03-13 14:29:34 +01:00
const {
2023-03-20 17:08:05 +01:00
captchaResponse , setCaptchaResponse ,
defaultSubplebbits ,
isSettingsOpen , setIsSettingsOpen ,
selectedAddress , setSelectedAddress ,
2023-03-13 14:29:34 +01:00
selectedStyle ,
2023-03-20 17:08:05 +01:00
setSelectedThread ,
selectedTitle , setSelectedTitle ,
showPostForm ,
showPostFormLink
} = useAppStore ( state => state );
2023-03-13 14:29:34 +01:00
2023-02-19 16:48:46 +01:00
const [ name , setName ] = useState ( '' );
const [ subject , setSubject ] = useState ( '' );
const [ comment , setComment ] = useState ( '' );
2023-03-24 18:08:07 +01:00
const { publishComment } = usePublishComment ();
2023-03-12 22:07:15 +01:00
const [ isCaptchaOpen , setIsCaptchaOpen ] = useState ( false );
const [ captchaImage , setCaptchaImage ] = useState ( '' );
2023-02-19 16:48:46 +01:00
const navigate = useNavigate ();
2023-02-24 20:57:51 +01:00
const [ prevScrollPos , setPrevScrollPos ] = useState ( 0 );
const [ visible , setVisible ] = useState ( true );
2023-03-24 18:08:07 +01:00
const { feed , hasMore , loadMore } = useFeed ({ subplebbitAddresses : [ ` ${ selectedAddress } ` ], sortType : 'new' });
2023-03-02 18:19:08 +01:00
const { subplebbitAddress } = useParams ();
2023-03-20 17:08:05 +01:00
const handleClickForm = useClickForm ();
2023-03-02 18:19:08 +01:00
2023-03-25 15:34:10 +01:00
2023-03-02 18:19:08 +01:00
useEffect (() => {
setSelectedAddress ( subplebbitAddress );
const selectedSubplebbit = defaultSubplebbits . find (( subplebbit ) => subplebbit . address === subplebbitAddress );
if ( selectedSubplebbit ) {
setSelectedTitle ( selectedSubplebbit . title );
}
}, [ subplebbitAddress , setSelectedAddress , setSelectedTitle , defaultSubplebbits ]);
2023-03-25 14:54:04 +01:00
// mobile navbar scroll effect
2023-03-02 18:19:08 +01:00
useEffect (() => {
2023-03-25 14:54:04 +01:00
const debouncedHandleScroll = debounce (() => {
2023-03-02 18:19:08 +01:00
const currentScrollPos = window . pageYOffset ;
setVisible ( prevScrollPos > currentScrollPos || currentScrollPos < 10 );
setPrevScrollPos ( currentScrollPos );
2023-03-25 14:54:04 +01:00
}, 50 );
window . addEventListener ( 'scroll' , debouncedHandleScroll );
return () => window . removeEventListener ( 'scroll' , debouncedHandleScroll );
2023-03-02 18:19:08 +01:00
}, [ prevScrollPos , visible ]);
2023-02-19 16:48:46 +01:00
const tryLoadMore = async () => {
try { loadMore ()}
catch ( e )
{ await new Promise ( resolve => setTimeout ( resolve , 1000 ))}
2023-04-01 13:59:54 +02:00
};
2023-02-19 16:48:46 +01:00
const onChallengeVerification = ( challengeVerification ) => {
if ( challengeVerification . challengeSuccess === true ) {
2023-03-15 17:34:31 +01:00
onSuccess ( 'challenge success' , { publishedCid : challengeVerification . publication . cid })
2023-02-19 16:48:46 +01:00
}
else if ( challengeVerification . challengeSuccess === false ) {
2023-03-15 17:34:31 +01:00
onError ( 'challenge failed' , { reason : challengeVerification . reason , errors : challengeVerification . errors });
onError ( "Error: You seem to have mistyped the CAPTCHA. Please try again." );
2023-02-19 16:48:46 +01:00
}
2023-04-01 13:59:54 +02:00
};
2023-02-19 16:48:46 +01:00
2023-03-02 18:19:08 +01:00
2023-02-19 16:48:46 +01:00
const onChallenge = async ( challenges , comment ) => {
let challengeAnswers = [];
try {
challengeAnswers = await getChallengeAnswersFromUser ( challenges )
}
catch ( error ) {
2023-03-15 17:34:31 +01:00
onError ( error );
2023-02-19 16:48:46 +01:00
}
if ( challengeAnswers ) {
await comment . publishChallengeAnswers ( challengeAnswers )
}
2023-04-01 13:59:54 +02:00
};
2023-02-19 16:48:46 +01:00
2023-03-02 18:19:08 +01:00
2023-02-19 16:48:46 +01:00
const getChallengeAnswersFromUser = async ( 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 = () => {
2023-03-12 22:07:15 +01:00
setIsCaptchaOpen ( true );
setCaptchaImage ( imageSource );
2023-02-19 16:48:46 +01:00
const handleKeyDown = ( event ) => {
if ( event . key === 'Enter' ) {
2023-03-12 22:07:15 +01:00
setCaptchaImage ( '' );
resolve ( captchaResponse );
setIsCaptchaOpen ( false );
2023-02-19 16:48:46 +01:00
document . removeEventListener ( 'keydown' , handleKeyDown );
}
};
2023-03-12 22:07:15 +01:00
setCaptchaResponse ( '' );
2023-02-19 16:48:46 +01:00
document . addEventListener ( 'keydown' , handleKeyDown );
};
challengeImg . onerror = () => {
2023-03-15 17:34:31 +01:00
reject ( onError ( 'Could not load challenge image' ));
2023-02-19 16:48:46 +01:00
};
});
};
2023-02-27 18:16:47 +01:00
const handleSelectChange = ( event ) => {
const selected = event . target . value ;
const selectedTitle = defaultSubplebbits . find (( subplebbit ) => subplebbit . address === selected ). title ;
setSelectedTitle ( selectedTitle );
setSelectedAddress ( selected );
navigate ( `/ ${ selected } ` );
2023-02-19 16:48:46 +01:00
};
2023-03-02 18:19:08 +01:00
2023-02-19 16:48:46 +01:00
const handlePublishComment = async () => {
// Event.preventDefault();
try {
const pendingComment = await publishComment ({
content : comment ,
title : subject ,
subplebbitAddress : selectedAddress ,
onChallengeVerification ,
onChallenge ,
2023-03-13 22:15:57 +01:00
onError : onError ,
2023-02-19 16:48:46 +01:00
});
console . log ( `Comment pending with index: ${ pendingComment . index } ` );
setName ( '' );
setSubject ( '' );
setComment ( '' );
} catch ( error ) {
2023-03-13 22:15:57 +01:00
onError ( error );
2023-02-19 16:48:46 +01:00
}
};
2023-03-02 18:19:08 +01:00
2023-02-19 16:48:46 +01:00
return (
< Container >
2023-03-12 22:07:15 +01:00
< CaptchaModal
isOpen = { isCaptchaOpen }
2023-04-01 13:59:54 +02:00
closeModal = {() => setIsCaptchaOpen ( false )}
2023-03-12 22:07:15 +01:00
captchaImage = { captchaImage } />
2023-03-16 14:04:15 +01:00
< SettingsModal
selectedStyle = { selectedStyle }
isOpen = { isSettingsOpen }
2023-04-01 13:59:54 +02:00
closeModal = {() => setIsSettingsOpen ( false )} />
2023-02-19 16:48:46 +01:00
< NavBar selectedStyle = { selectedStyle }>
<>
{ defaultSubplebbits . map ( subplebbit => (
2023-03-11 12:32:42 +01:00
< span className = "boardList" key = { `span- ${ subplebbit . address } ` }>
2023-02-19 16:48:46 +01:00
[
2023-04-01 13:59:54 +02:00
< Link to = { `/ ${ subplebbit . address } ` } key = { `a- ${ subplebbit . address } ` }
onClick = {() => {
setSelectedTitle ( subplebbit . title );
setSelectedAddress ( subplebbit . address );
}}
2023-04-01 14:51:07 +02:00
>{ subplebbit . title ? subplebbit . title : subplebbit . address }</ Link >
2023-02-19 16:48:46 +01:00
] & nbsp ;
</ span >
))}
2023-02-23 16:45:29 +01:00
< span className = "nav" >
[
2023-04-01 13:59:54 +02:00
< Link to = { `/ ${ selectedAddress } /catalog/settings` } onClick = {() => setIsSettingsOpen ( true )}> Settings </ Link >
2023-02-23 16:45:29 +01:00
]
[
< Link to = "/" onClick = {() => handleStyleChange ({ target : { value : "Yotsuba" }}
)}> Home </ Link >
]
2023-02-19 16:48:46 +01:00
</ span >
2023-02-24 20:57:51 +01:00
< div id = "board-nav-mobile" style = {{ top : visible ? 0 : '-23px' }}>
2023-02-23 16:45:29 +01:00
< div className = "board-select" >
< strong > Board </ strong >
& nbsp ;
2023-02-27 18:16:47 +01:00
< select id = "board-select-mobile" value = { selectedAddress } onChange = { handleSelectChange }>
2023-02-23 16:45:29 +01:00
{ defaultSubplebbits . map ( subplebbit => (
2023-03-11 12:32:42 +01:00
< option key = { `option- ${ subplebbit . address } ` } value = { subplebbit . address }
2023-04-01 14:51:07 +02:00
>{ subplebbit . title ? subplebbit . title : subplebbit . address }</ option >
2023-02-27 15:32:16 +01:00
))}
2023-02-23 16:45:29 +01:00
</ select >
</ div >
< div className = "page-jump" >
2023-04-01 13:59:54 +02:00
< Link to = { `/ ${ selectedAddress } /catalog/settings` } onClick = {() => setIsSettingsOpen ( true )}> Settings </ Link >
2023-02-23 16:45:29 +01:00
& nbsp ;
< Link to = "/" onClick = {() => handleStyleChange ({ target : { value : "Yotsuba" }}
)}> Home </ Link >
</ div >
</ div >
< div id = "separator-mobile" > & nbsp ;</ div >
< div id = "separator-mobile" > & nbsp ;</ div >
2023-02-19 16:48:46 +01:00
</>
</ NavBar >
< Header selectedStyle = { selectedStyle }>
<>
< div className = "banner" >
< ImageBanner />
</ div >
<>
< div className = "board-title" >{ selectedTitle }</ div >
< div className = "board-address" > p / { selectedAddress }</ div >
</>
</>
</ Header >
< Break selectedStyle = { selectedStyle } />
< PostForm selectedStyle = { selectedStyle }>
2023-02-24 18:13:02 +01:00
< PostFormLink id = "post-form-link" showPostFormLink = { showPostFormLink } selectedStyle = { selectedStyle } >
< div id = "post-form-link-desktop" >
[
< a onClick = { handleClickForm } onMouseOver = {( event ) => event . target . style . cursor = 'pointer' }> Start a New Thread </ a >
]
</ div >
< div id = "post-form-link-mobile" >
< span className = "btn-wrap" >
< a onClick = { handleClickForm } onMouseOver = {( event ) => event . target . style . cursor = 'pointer' }> Start a New Thread </ a >
</ span >
</ div >
2023-02-19 16:48:46 +01:00
</ PostFormLink >
< PostFormTable id = "post-form" showPostForm = { showPostForm } selectedStyle = { selectedStyle } className = "post-form" >
2023-03-12 10:27:59 +01:00
< tbody >
2023-02-19 16:48:46 +01:00
< tr data - type = "Name" >
< td id = "td-name" > Name </ td >
< td >
< input name = "name" type = "text" tabIndex = { 1 } placeholder = "Anonymous" value = { name } onChange = {( event ) => setName ( event . target . value )} />
</ td >
</ tr >
< tr data - type = "Subject" >
< td > Subject </ td >
< td >
< input name = "sub" type = "text" tabIndex = { 3 } value = { subject } onChange = {( event ) => setSubject ( event . target . value )} />
< input id = "post-button" type = "submit" value = "Post" tabIndex = { 6 } onClick = { handlePublishComment } />
</ td >
</ tr >
< tr data - type = "Comment" >
< td > Comment </ td >
< td >
< textarea name = "com" cols = "48" rows = "4" tabIndex = { 4 } wrap = "soft" value = { comment } onChange = {( event ) => setComment ( event . target . value )}></ textarea >
</ td >
</ tr >
< tr data - type = "File" >
< td > Embed File </ td >
< td >
< input name = "embed" type = "text" tabIndex = { 7 } placeholder = "Paste link" />
2023-04-01 13:59:54 +02:00
< button id = "t-help" type = "button" onClick = {
() => alert ( "- Embedding media is optional, posts can be text-only. \n- A CAPTCHA challenge will appear after posting. \n- The CAPTCHA is case-sensitive." )
} data - tip = "Help" > ? </ button >
2023-03-12 10:27:59 +01:00
</ td >
</ tr >
2023-02-19 16:48:46 +01:00
</ tbody >
</ PostFormTable >
</ PostForm >
< TopBar selectedStyle = { selectedStyle }>
< hr />
< span className = "style-changer" >
Style :
< select id = "style-selector" onChange = { handleStyleChange } value = { selectedStyle }>
< option value = "Yotsuba" > Yotsuba </ option >
2023-03-08 12:49:44 +01:00
< option value = "Yotsuba-B" > Yotsuba B </ option >
2023-02-19 16:48:46 +01:00
< option value = "Futaba" > Futaba </ option >
< option value = "Burichan" > Burichan </ option >
< option value = "Tomorrow" > Tomorrow </ option >
< option value = "Photon" > Photon </ option >
</ select >
</ span >
2023-02-24 18:13:02 +01:00
< div className = "return-button" id = "return-button-desktop" >
2023-02-19 16:48:46 +01:00
[
2023-03-02 21:51:06 +01:00
< Link to = { `/ ${ selectedAddress } ` }> Return </ Link >
2023-02-19 16:48:46 +01:00
]
2023-02-24 18:13:02 +01:00
</ div >
< div id = "return-button-mobile" >
< span className = "btn-wrap-catalog btn-wrap" >
2023-03-02 21:51:06 +01:00
< Link to = { `/ ${ selectedAddress } ` }> Return </ Link >
2023-02-24 18:13:02 +01:00
</ span >
</ div >
2023-03-22 15:35:28 +01:00
< div id = "stats" style = {{ float : "right" , marginTop : "5px" }}>
{ feed . length > 0 ? ( null ) : (< span > Fetching IPFS ...</ span >)}
</ div >
2023-02-19 16:48:46 +01:00
< hr />
</ TopBar >
< Threads selectedStyle = { selectedStyle }>
2023-03-25 15:22:03 +01:00
{ feed . length < 1 ? (
< CatalogLoader />
) : (
< InfiniteScroll
pageStart = { 0 }
loadMore = { tryLoadMore }
hasMore = { hasMore }
>
2023-03-28 11:23:27 +02:00
{ feed . map (( thread ) => {
2023-03-25 15:22:03 +01:00
const commentMediaInfo = getCommentMediaInfo ( thread );
const fallbackImgUrl = "/assets/filedeleted-res.gif" ;
return (
2023-04-01 13:59:54 +02:00
< Link style = {{ all : "unset" , cursor : "pointer" }} key = { `link- ${ thread . cid } ` } to = { `/ ${ selectedAddress } /thread/ ${ thread . cid } ` } onClick = {() => setSelectedThread ( thread . cid )}>
2023-03-26 14:43:58 +02:00
< div key = { `thread- ${ thread . cid } ` } className = "thread" >
{ commentMediaInfo ? . url ? (
< Fragment key = "f-catalog" >
{ commentMediaInfo ? . type === "webpage" ? (
2023-03-28 14:52:36 +02:00
< img key = { `img- ${ thread . cid } ` } alt = "thread"
src = { commentMediaInfo . url }
2023-03-26 14:43:58 +02:00
onError = {( e ) => {
e . target . src = fallbackImgUrl
e . target . onerror = null ;}} />
) : null }
{ commentMediaInfo ? . type === "image" ? (
2023-03-28 14:52:36 +02:00
< img key = { `img- ${ thread . cid } ` } alt = "thread"
src = { commentMediaInfo . url }
2023-03-26 14:43:58 +02:00
onError = {( e ) => {
e . target . src = fallbackImgUrl
e . target . onerror = null ;}} />
) : null }
{ commentMediaInfo ? . type === "video" ? (
2023-03-28 14:52:36 +02:00
< video key = { `fti- ${ thread . cid } ` }
src = { commentMediaInfo . url }
alt = { commentMediaInfo . type }
style = {{ pointerEvents : "none" }}
onError = {( e ) => e . target . src = fallbackImgUrl } />
2023-03-26 14:43:58 +02:00
) : null }
{ commentMediaInfo ? . type === "audio" ? (
2023-03-28 14:52:36 +02:00
< audio controls
key = { `fti- ${ thread . cid } ` }
src = { commentMediaInfo . url }
alt = { commentMediaInfo . type }
style = {{ pointerEvents : "none" }}
onError = {( e ) => e . target . src = fallbackImgUrl } />
2023-03-26 14:43:58 +02:00
) : null }
</ Fragment >
) : null }
{ /* <div key={`ti-${thread.cid}`} className="thread-icons" >
<span key={`si-${thread.cid}`} className="thread-icon sticky-icon" title="Sticky"></span>
</div> */ }
< div key = { `meta- ${ thread . cid } ` } className = "meta" title = "(R)eplies / (I)mage Replies" >
R :
< b key = { `b- ${ thread . cid } ` }>{ thread . replyCount }</ b >
</ div >
< div key = { `t- ${ thread . cid } ` } className = "teaser" >
< b key = { `b2- ${ thread . cid } ` }>{ thread . title ? ` ${ thread . title } ` : null }</ b >
2023-03-29 13:37:36 +02:00
{ thread . content && `: ` }
{ thread . content && < Post content = { thread . content } key = "post" />}
2023-03-26 14:43:58 +02:00
</ div >
2023-03-25 15:22:03 +01:00
</ div >
2023-03-26 14:43:58 +02:00
</ Link >
2023-03-25 15:22:03 +01:00
)})}
</ InfiniteScroll >
)}
2023-02-19 16:48:46 +01:00
</ Threads >
</ Container >
);
}
export default Catalog ;