2024-08-06 21:51:45 +02:00
import { useCallback , useEffect , useRef , useState } from 'react' ;
2024-06-12 17:38:22 +02:00
import { useLocation , useParams } from 'react-router-dom' ;
2024-04-28 20:08:41 +02:00
import { useTranslation } from 'react-i18next' ;
import Draggable from 'react-draggable' ;
2024-08-06 20:12:07 +02:00
import { setAccount , useAccount , useComment , useSubplebbit } from '@plebbit/plebbit-react-hooks' ;
2024-04-28 20:08:41 +02:00
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js' ;
2024-06-08 21:33:56 +02:00
import { getFormattedTimeAgo } from '../../lib/utils/time-utils' ;
2024-04-28 20:08:41 +02:00
import { isValidURL } from '../../lib/utils/url-utils' ;
2024-06-12 17:38:22 +02:00
import { isAllView , isSubscriptionsView } from '../../lib/utils/view-utils' ;
2024-06-26 18:20:25 +02:00
import useSelectedTextStore from '../../stores/use-selected-text-store' ;
2024-04-28 20:08:41 +02:00
import useReply from '../../hooks/use-reply' ;
2024-05-29 16:17:37 +02:00
import useIsMobile from '../../hooks/use-is-mobile' ;
2024-04-28 20:08:41 +02:00
import styles from './reply-modal.module.css' ;
2024-05-31 15:45:48 +02:00
import { LinkTypePreviewer } from '../post-form' ;
2024-04-28 20:08:41 +02:00
import _ from 'lodash' ;
2024-08-06 20:12:07 +02:00
import useAnonMode from '../../hooks/use-anon-mode' ;
2024-04-28 20:08:41 +02:00
interface ReplyModalProps {
closeModal : () => void ;
parentCid : string ;
2024-08-06 20:12:07 +02:00
postCid : string ;
2024-04-30 11:08:01 +02:00
scrollY : number ;
2024-04-28 20:08:41 +02:00
}
2024-08-06 20:12:07 +02:00
const ReplyModal = ({ closeModal , parentCid , postCid , scrollY } : ReplyModalProps ) => {
2024-04-28 20:08:41 +02:00
const { t } = useTranslation ();
const { subplebbitAddress } = useParams () as { subplebbitAddress : string };
2024-08-06 20:12:07 +02:00
const { setPublishReplyOptions , publishReply } = useReply ({ cid : parentCid , subplebbitAddress });
2024-04-28 20:08:41 +02:00
const account = useAccount ();
const { displayName } = account ? . author || {};
2024-05-31 15:45:48 +02:00
const [ url , setUrl ] = useState ( '' );
2024-06-26 18:20:25 +02:00
const textRef = useRef < HTMLTextAreaElement | null >( null );
2024-04-28 20:08:41 +02:00
const urlRef = useRef < HTMLInputElement >( null );
2024-06-26 18:20:25 +02:00
const { selectedText } = useSelectedTextStore ();
2024-04-28 20:08:41 +02:00
2024-08-06 20:12:07 +02:00
const { anonMode , getNewSigner , getExistingSigner } = useAnonMode ( postCid );
const comment = useComment ({ commentCid : postCid });
const address = comment ? . author ? . address ;
2024-08-06 21:51:45 +02:00
const hasCalledAnonAddressRef = useRef ( false );
const getAnonAddressForReply = useCallback ( async () => {
if ( anonMode && ! hasCalledAnonAddressRef . current ) {
hasCalledAnonAddressRef . current = true ;
let signer = getExistingSigner ( address );
if ( ! signer ) {
signer = await getNewSigner ();
if ( signer ) {
setPublishReplyOptions ({
signer ,
author : {
displayName ,
address : signer.address ,
},
});
}
2024-08-06 20:12:07 +02:00
}
}
2024-08-06 21:51:45 +02:00
}, [ anonMode , address , getExistingSigner , getNewSigner , displayName , setPublishReplyOptions ]);
useEffect (() => {
if ( anonMode ) {
getAnonAddressForReply ();
}
}, [ anonMode , getAnonAddressForReply ]);
2024-08-06 20:12:07 +02:00
const onPublishReply = async () => {
2024-04-28 20:08:41 +02:00
const currentContent = textRef . current ? . value || '' ;
const currentUrl = urlRef . current ? . value || '' ;
if ( ! currentContent . trim () && ! currentUrl ) {
alert ( `Cannot post empty comment` );
return ;
}
if ( currentUrl && ! isValidURL ( currentUrl )) {
alert ( 'The provided link is not a valid URL.' );
return ;
}
2024-08-06 20:12:07 +02:00
2024-08-06 21:51:45 +02:00
publishReply ();
closeModal ();
2024-04-28 20:08:41 +02:00
};
2024-04-29 13:28:04 +02:00
const nodeRef = useRef < HTMLDivElement >( null );
2024-05-29 16:17:37 +02:00
const isMobile = useIsMobile ();
2024-04-28 20:08:41 +02:00
2024-04-29 13:28:04 +02:00
// on mobile, the position is absolute instead of fixed, so we need to calculate the top position
useEffect (() => {
if ( nodeRef . current && isMobile ) {
const viewportHeight = window . innerHeight ;
const modalHeight = 150 ;
const centeredPosition = scrollY + viewportHeight / 2 - modalHeight / 2 ;
nodeRef . current . style . top = ` ${ centeredPosition } px` ;
}
2024-04-30 11:08:01 +02:00
}, [ isMobile , scrollY ]);
2024-04-29 13:28:04 +02:00
2024-05-09 22:55:57 +02:00
const parentCidRef = useRef < HTMLSpanElement >( null );
useEffect (() => {
if ( parentCidRef . current && parentCidRef . current ) {
const cidWidth = parentCidRef . current . offsetWidth ;
parentCidRef . current . style . width = ` ${ cidWidth } px` ;
}
}, [ parentCid ]);
2024-06-12 17:38:22 +02:00
const location = useLocation ();
2024-06-17 12:17:36 +02:00
const isInAllView = isAllView ( location . pathname , useParams ());
const isInSubscriptionsView = isSubscriptionsView ( location . pathname , useParams ());
2024-06-08 21:33:56 +02:00
const subplebbit = useSubplebbit ({ subplebbitAddress });
const { updatedAt } = subplebbit || {};
const isBoardOffline = subplebbit ? . updatedAt && subplebbit . updatedAt < Date . now () / 1000 - 60 * 60 ;
const offlineAlert = updatedAt
? isBoardOffline && (
< div className = { styles . offlineBoard }>{ `Posts last synced ${ getFormattedTimeAgo ( updatedAt ) } , the subplebbit might be offline and publishing might fail.` }</ div >
)
: `The subplebbit might be offline and publishing might fail.` ;
2024-06-26 18:20:25 +02:00
const setTextRef = ( ref : HTMLTextAreaElement | null ) => {
if ( ref ) {
textRef . current = ref ;
! isMobile && ref . focus ();
}
};
2024-07-22 12:29:05 +02:00
useEffect (() => {
if ( textRef . current ) {
const len = textRef . current . value . length ;
textRef . current . setSelectionRange ( len , len );
}
}, []);
2024-06-26 22:20:45 +02:00
const contentPrefix = `c/ ${ parentCid && Plebbit . getShortCid ( parentCid ) } \ n` ;
const handleContentInput = ( e : React.ChangeEvent < HTMLTextAreaElement >) => {
const { value } = e . target ;
if ( ! value . startsWith ( contentPrefix )) {
e . target . value = contentPrefix + value . slice ( contentPrefix . length );
}
};
const handleContentChange = ( e : React.ChangeEvent < HTMLTextAreaElement >) => {
// remove the prefix from the content to publish, and also add newlines for markdown
const contentWithoutPrefix = e . target . value . slice ( contentPrefix . length ). replace ( /\n/g , '\n\n' );
if ( textRef . current && textRef . current . value !== contentWithoutPrefix ) {
2024-08-06 20:12:07 +02:00
setPublishReplyOptions ({ content : contentWithoutPrefix });
2024-06-26 22:20:45 +02:00
}
};
2024-04-29 13:28:04 +02:00
const modalContent = (
< div className = { styles . container } ref = { nodeRef }>
2024-04-29 14:26:02 +02:00
< div className = { `replyModalHandle ${ styles . title } ` }>
{ t ( 'reply_to_cid' , { cid : `c/ ${ parentCid && Plebbit . getShortCid ( parentCid ) } ` , interpolation : { escapeValue : false } })}
< button
className = { styles . closeIcon }
onClick = {( e ) => {
e . stopPropagation ();
closeModal ();
}}
title = 'close'
/>
</ div >
< div className = { styles . replyForm }>
< div className = { styles . name }>
< input
type = 'text'
defaultValue = { displayName }
2024-06-05 22:18:45 +02:00
placeholder = { displayName ? undefined : _ . capitalize ( t ( 'name' ))}
2024-04-29 14:26:02 +02:00
onChange = {( e ) => setAccount ({ ... account , author : { ... account ? . author , displayName : e.target.value } })}
/>
</ div >
< div className = { styles . link }>
2024-05-31 15:45:48 +02:00
< input
type = 'text'
ref = { urlRef }
placeholder = { _ . capitalize ( t ( 'link' ))}
onChange = {( e ) => {
setUrl ( e . target . value );
2024-08-06 20:12:07 +02:00
setPublishReplyOptions ({ link : e.target.value });
2024-05-31 15:45:48 +02:00
}}
/>
2024-04-29 14:26:02 +02:00
</ div >
< div className = { styles . content }>
2024-04-29 14:53:46 +02:00
< textarea
cols = { 48 }
2024-06-26 22:20:45 +02:00
rows = { 4 }
2024-04-29 14:53:46 +02:00
wrap = 'soft'
2024-06-26 18:20:25 +02:00
ref = { setTextRef }
2024-06-26 22:20:45 +02:00
spellCheck = { false }
defaultValue = { contentPrefix + selectedText }
onInput = { handleContentInput }
onChange = { handleContentChange }
2024-04-29 14:53:46 +02:00
/>
2024-04-29 14:26:02 +02:00
</ div >
2024-06-12 17:38:22 +02:00
{ ! ( isInAllView || isInSubscriptionsView ) && offlineAlert }
2024-06-08 21:33:56 +02:00
< div className = { styles . offlineAlert }></ div >
2024-04-29 14:26:02 +02:00
< div className = { styles . footer }>
2024-05-31 15:45:48 +02:00
{ url && (
<>
2024-06-24 18:31:08 +02:00
{ t ( 'link_type' )} : < LinkTypePreviewer link = { url } />{ ' ' }
2024-05-31 15:45:48 +02:00
</>
)}
< span className = { styles . spoilerButton }>
[
< label >
2024-08-06 20:12:07 +02:00
< input type = 'checkbox' onChange = {( e ) => setPublishReplyOptions ({ spoiler : e.target.checked })} />
2024-05-31 17:29:19 +02:00
{ _ . capitalize ( t ( 'spoiler' ))} ?
2024-05-31 15:45:48 +02:00
</ label >
]
</ span >
2024-06-26 22:20:45 +02:00
< button className = { styles . publishButton } onClick = { onPublishReply }>
{ t ( 'post' )}
</ button >
2024-04-29 14:26:02 +02:00
</ div >
</ div >
2024-04-29 13:28:04 +02:00
</ div >
);
return isMobile ? (
modalContent
) : (
< Draggable handle = '.replyModalHandle' nodeRef = { nodeRef }>
{ modalContent }
2024-04-28 20:08:41 +02:00
</ Draggable >
);
};
export default ReplyModal ;