mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
+1
-1
@@ -9,7 +9,7 @@
|
||||
"@capacitor/android": "3.6.0",
|
||||
"@capacitor/app": "1.1.1",
|
||||
"@capacitor/core": "3.6.0",
|
||||
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#5aef9d028f25fbe3db6fe748df0841f970e39f63",
|
||||
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#0024401c390929e47b44331a8dadd61ba212b871",
|
||||
"@testing-library/dom": "9.0.1",
|
||||
"@testing-library/jest-dom": "5.14.1",
|
||||
"@testing-library/react": "14.0.0",
|
||||
|
||||
@@ -40,7 +40,7 @@ const EditLabel = ({ commentCid, className }) => {
|
||||
return (
|
||||
<>
|
||||
<OriginalCommentModal isOpen={isOriginalCommentModalOpen} closeModal={() => setIsOriginalCommentModalOpen(false)} comment={comment} />
|
||||
{comment.edit && (
|
||||
{comment.edit && comment.original?.content !== comment?.content ? (
|
||||
<>
|
||||
<br />
|
||||
<span className={className}>
|
||||
@@ -51,7 +51,7 @@ const EditLabel = ({ commentCid, className }) => {
|
||||
)
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
{(editedCommentState === 'pending' || editedCommentState === 'failed') && conditionsString ? (
|
||||
<>
|
||||
<br />
|
||||
|
||||
@@ -94,8 +94,15 @@ const PostOnHover = ({ cid, feed }) => {
|
||||
<a href={replyMediaInfo.url} target='_blank' rel='noopener noreferrer'>
|
||||
{replyMediaInfo?.url.length > 30 ? replyMediaInfo?.url.slice(0, 30) + '(...)' : replyMediaInfo?.url}
|
||||
</a>
|
||||
({replyMediaInfo?.type})
|
||||
{replyMediaInfo?.type === 'iframe' ? null : ` (${replyMediaInfo?.type})`}
|
||||
</div>
|
||||
{replyMediaInfo?.type === 'iframe' && (
|
||||
<div className='img-container'>
|
||||
<span className='file-thumb-reply'>
|
||||
{replyMediaInfo.thumbnail && <img src={replyMediaInfo.thumbnail} alt='thumbnail' onError={(e) => (e.target.src = fallbackImgUrl)} />}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{replyMediaInfo?.type === 'webpage' ? (
|
||||
<div className='img-container'>
|
||||
<span className='file-thumb-reply'>
|
||||
|
||||
@@ -8,332 +8,310 @@ import useError from '../../hooks/useError';
|
||||
import useAnonModeStore from '../../hooks/stores/useAnonModeStore';
|
||||
import useGeneralStore from '../../hooks/stores/useGeneralStore';
|
||||
|
||||
|
||||
const ReplyModal = ({ isOpen, closeModal }) => {
|
||||
const {
|
||||
captchaResponse, setCaptchaResponse,
|
||||
setChallengesArray,
|
||||
setIsCaptchaOpen,
|
||||
setPendingComment,
|
||||
setPendingCommentIndex,
|
||||
replyQuoteCid,
|
||||
setResolveCaptchaPromise,
|
||||
selectedAddress,
|
||||
selectedParentCid,
|
||||
selectedShortCid,
|
||||
selectedStyle,
|
||||
selectedText, setSelectedText,
|
||||
triggerInsertion,
|
||||
} = useGeneralStore(state => state);
|
||||
const {
|
||||
captchaResponse,
|
||||
setCaptchaResponse,
|
||||
setChallengesArray,
|
||||
setIsCaptchaOpen,
|
||||
setPendingComment,
|
||||
setPendingCommentIndex,
|
||||
replyQuoteCid,
|
||||
setResolveCaptchaPromise,
|
||||
selectedAddress,
|
||||
selectedParentCid,
|
||||
selectedShortCid,
|
||||
selectedStyle,
|
||||
selectedText,
|
||||
setSelectedText,
|
||||
triggerInsertion,
|
||||
} = useGeneralStore((state) => state);
|
||||
|
||||
const { anonymousMode } = useAnonModeStore();
|
||||
const { anonymousMode } = useAnonModeStore();
|
||||
|
||||
const account = useAccount();
|
||||
const account = useAccount();
|
||||
|
||||
const [, setNewErrorMessage] = useError();
|
||||
|
||||
const nodeRef = useRef(null);
|
||||
const nameRef = useRef();
|
||||
const commentRef = useRef();
|
||||
const linkRef = useRef();
|
||||
const [, setNewErrorMessage] = useError();
|
||||
|
||||
const [triggerPublishComment, setTriggerPublishComment] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 480);
|
||||
const [executeAnonMode, setExecuteAnonMode] = useState(false);
|
||||
|
||||
useAnonMode(selectedParentCid, anonymousMode && executeAnonMode);
|
||||
const nodeRef = useRef(null);
|
||||
const nameRef = useRef();
|
||||
const commentRef = useRef();
|
||||
const linkRef = useRef();
|
||||
|
||||
const [triggerPublishComment, setTriggerPublishComment] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 480);
|
||||
const [executeAnonMode, setExecuteAnonMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => setIsMobile(window.innerWidth <= 480);
|
||||
window.addEventListener('resize', handleResize);
|
||||
useAnonMode(selectedParentCid, anonymousMode && executeAnonMode);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, [setIsMobile]);
|
||||
useEffect(() => {
|
||||
const handleResize = () => setIsMobile(window.innerWidth <= 480);
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
|
||||
const onModalOpen = () => {
|
||||
if (commentRef.current) {
|
||||
if (selectedText) {
|
||||
commentRef.current.value += '\n';
|
||||
}
|
||||
commentRef.current.focus();
|
||||
const len = commentRef.current.value.length;
|
||||
commentRef.current.setSelectionRange(len, len);
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, [setIsMobile]);
|
||||
|
||||
const insertAtCursor = (inputElement, valueToInsert) => {
|
||||
const startPos = inputElement.selectionStart || inputElement.value.length;
|
||||
const endPos = startPos + valueToInsert.length;
|
||||
inputElement.setRangeText(valueToInsert, startPos, startPos, 'end');
|
||||
inputElement.setSelectionRange(endPos, endPos);
|
||||
}
|
||||
const onModalOpen = () => {
|
||||
if (commentRef.current) {
|
||||
if (selectedText) {
|
||||
commentRef.current.value += '\n';
|
||||
}
|
||||
commentRef.current.focus();
|
||||
const len = commentRef.current.value.length;
|
||||
commentRef.current.setSelectionRange(len, len);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (replyQuoteCid && commentRef.current) {
|
||||
const prefixedReplyQuoteCid = `c/${replyQuoteCid}\n`;
|
||||
insertAtCursor(commentRef.current, prefixedReplyQuoteCid);
|
||||
}
|
||||
}, [triggerInsertion, replyQuoteCid]);
|
||||
|
||||
const insertAtCursor = (inputElement, valueToInsert) => {
|
||||
const startPos = inputElement.selectionStart || inputElement.value.length;
|
||||
const endPos = startPos + valueToInsert.length;
|
||||
inputElement.setRangeText(valueToInsert, startPos, startPos, 'end');
|
||||
inputElement.setSelectionRange(endPos, endPos);
|
||||
};
|
||||
|
||||
const getSelectedText = useCallback(() => {
|
||||
const text = document.getSelection().toString();
|
||||
setSelectedText(text ? `>${text}\n` : '');
|
||||
}, [setSelectedText]);
|
||||
useEffect(() => {
|
||||
if (replyQuoteCid && commentRef.current) {
|
||||
const prefixedReplyQuoteCid = `c/${replyQuoteCid}\n`;
|
||||
insertAtCursor(commentRef.current, prefixedReplyQuoteCid);
|
||||
}
|
||||
}, [triggerInsertion, replyQuoteCid]);
|
||||
|
||||
const getSelectedText = useCallback(() => {
|
||||
const text = document.getSelection().toString();
|
||||
setSelectedText(text ? `>${text}\n` : '');
|
||||
}, [setSelectedText]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setTimeout(getSelectedText, 0);
|
||||
} else {
|
||||
setSelectedText('');
|
||||
}
|
||||
}, [isOpen, getSelectedText, setSelectedText]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setTimeout(getSelectedText, 0);
|
||||
} else {
|
||||
setSelectedText('');
|
||||
}
|
||||
}, [isOpen, getSelectedText, setSelectedText]);
|
||||
|
||||
const onChallengeVerification = (challengeVerification) => {
|
||||
if (challengeVerification.challengeSuccess === true) {
|
||||
return;
|
||||
} else if (challengeVerification.challengeSuccess === false) {
|
||||
setNewErrorMessage(`Challenge Failed, reason: ${challengeVerification.reason}. Errors: ${challengeVerification.errors}`);
|
||||
console.log('challenge failed', challengeVerification);
|
||||
}
|
||||
};
|
||||
const onChallengeVerification = (challengeVerification) => {
|
||||
if (challengeVerification.challengeSuccess === true) {
|
||||
return;
|
||||
} else if (challengeVerification.challengeSuccess === false) {
|
||||
setNewErrorMessage(`Challenge Failed, reason: ${challengeVerification.reason}. Errors: ${challengeVerification.errors}`);
|
||||
console.log('challenge failed', challengeVerification);
|
||||
}
|
||||
};
|
||||
|
||||
const onChallenge = async (challenges, comment) => {
|
||||
setPendingComment(comment);
|
||||
let challengeAnswers = [];
|
||||
|
||||
const onChallenge = async (challenges, comment) => {
|
||||
setPendingComment(comment);
|
||||
let challengeAnswers = [];
|
||||
|
||||
try {
|
||||
challengeAnswers = await getChallengeAnswersFromUser(challenges)
|
||||
}
|
||||
catch (error) {
|
||||
setNewErrorMessage(error.message); console.log(error);
|
||||
}
|
||||
if (challengeAnswers) {
|
||||
await comment.publishChallengeAnswers(challengeAnswers)
|
||||
}
|
||||
};
|
||||
try {
|
||||
challengeAnswers = await getChallengeAnswersFromUser(challenges);
|
||||
} catch (error) {
|
||||
setNewErrorMessage(error.message);
|
||||
console.log(error);
|
||||
}
|
||||
if (challengeAnswers) {
|
||||
await comment.publishChallengeAnswers(challengeAnswers);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setPublishCommentOptions((prevPublishCommentOptions) => ({
|
||||
...prevPublishCommentOptions,
|
||||
subplebbitAddress: selectedAddress,
|
||||
}));
|
||||
}, [selectedAddress]);
|
||||
|
||||
|
||||
const [publishCommentOptions, setPublishCommentOptions] = useState({
|
||||
subplebbitAddress: selectedAddress,
|
||||
onChallenge,
|
||||
onChallengeVerification,
|
||||
onError: (error) => {
|
||||
setNewErrorMessage(error.message); console.log(error);
|
||||
},
|
||||
});
|
||||
useEffect(() => {
|
||||
setPublishCommentOptions((prevPublishCommentOptions) => ({
|
||||
...prevPublishCommentOptions,
|
||||
subplebbitAddress: selectedAddress,
|
||||
}));
|
||||
}, [selectedAddress]);
|
||||
|
||||
const [publishCommentOptions, setPublishCommentOptions] = useState({
|
||||
subplebbitAddress: selectedAddress,
|
||||
onChallenge,
|
||||
onChallengeVerification,
|
||||
onError: (error) => {
|
||||
setNewErrorMessage(error.message);
|
||||
console.log(error);
|
||||
},
|
||||
});
|
||||
|
||||
const { publishComment, index } = usePublishComment(publishCommentOptions);
|
||||
const { publishComment, index } = usePublishComment(publishCommentOptions);
|
||||
|
||||
useEffect(() => {
|
||||
if (index !== undefined) {
|
||||
setPendingCommentIndex(index);
|
||||
}
|
||||
}, [index, setPendingCommentIndex]);
|
||||
useEffect(() => {
|
||||
if (index !== undefined) {
|
||||
setPendingCommentIndex(index);
|
||||
}
|
||||
}, [index, setPendingCommentIndex]);
|
||||
|
||||
|
||||
const resetFields = useCallback(() => {
|
||||
if (nameRef.current) {
|
||||
nameRef.current.value = '';
|
||||
}
|
||||
if (commentRef.current) {
|
||||
commentRef.current.value = '';
|
||||
}
|
||||
if (linkRef.current) {
|
||||
linkRef.current.value = '';
|
||||
}
|
||||
}, []);
|
||||
const resetFields = useCallback(() => {
|
||||
if (nameRef.current) {
|
||||
nameRef.current.value = '';
|
||||
}
|
||||
if (commentRef.current) {
|
||||
commentRef.current.value = '';
|
||||
}
|
||||
if (linkRef.current) {
|
||||
linkRef.current.value = '';
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
if (commentRef.current.value === '' && linkRef.current.value === '') {
|
||||
setNewErrorMessage('Please enter a comment or link.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
commentRef.current.value === "" &&
|
||||
linkRef.current.value === ""
|
||||
) {
|
||||
setNewErrorMessage("Please enter a comment or link.");
|
||||
return;
|
||||
}
|
||||
setPublishCommentOptions((prevPublishCommentOptions) => ({
|
||||
...prevPublishCommentOptions,
|
||||
author: {
|
||||
displayName: nameRef.current.value || undefined,
|
||||
...(anonymousMode ? {} : { address: account?.author.address }),
|
||||
},
|
||||
content: commentRef.current.value || undefined,
|
||||
link: linkRef.current.value || undefined,
|
||||
parentCid: selectedParentCid,
|
||||
}));
|
||||
|
||||
setPublishCommentOptions((prevPublishCommentOptions) => ({
|
||||
...prevPublishCommentOptions,
|
||||
author: {
|
||||
displayName: nameRef.current.value || undefined,
|
||||
...(anonymousMode ? {} : {address: account?.author.address}),
|
||||
},
|
||||
content: commentRef.current.value || undefined,
|
||||
link: linkRef.current.value || undefined,
|
||||
parentCid: selectedParentCid,
|
||||
}));
|
||||
|
||||
setTriggerPublishComment(true);
|
||||
};
|
||||
setTriggerPublishComment(true);
|
||||
};
|
||||
|
||||
|
||||
const updateSigner = useCallback(async () => {
|
||||
if (anonymousMode) {
|
||||
setExecuteAnonMode(true);
|
||||
|
||||
let storedSigners = JSON.parse(localStorage.getItem('storedSigners')) || {};
|
||||
let signer;
|
||||
|
||||
if (!storedSigners[selectedParentCid]) {
|
||||
signer = await account?.plebbit.createSigner();
|
||||
storedSigners[selectedParentCid] = { privateKey: signer?.privateKey, address: signer?.address };
|
||||
localStorage.setItem('storedSigners', JSON.stringify(storedSigners));
|
||||
} else {
|
||||
const signerPrivateKey = storedSigners[selectedParentCid].privateKey;
|
||||
|
||||
try {
|
||||
signer = await account?.plebbit.createSigner({type: 'ed25519', privateKey: signerPrivateKey});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
setPublishCommentOptions(prevPublishCommentOptions => {
|
||||
const newPublishCommentOptions = {
|
||||
...prevPublishCommentOptions,
|
||||
signer,
|
||||
author: {
|
||||
...prevPublishCommentOptions.author,
|
||||
address: signer?.address,
|
||||
},
|
||||
};
|
||||
|
||||
if (JSON.stringify(prevPublishCommentOptions) !== JSON.stringify(newPublishCommentOptions)) {
|
||||
return newPublishCommentOptions;
|
||||
}
|
||||
|
||||
return prevPublishCommentOptions;
|
||||
});
|
||||
}
|
||||
}, [selectedParentCid, anonymousMode, account]);
|
||||
|
||||
useEffect(() => {
|
||||
if (anonymousMode) {
|
||||
updateSigner();
|
||||
}
|
||||
}, [updateSigner, anonymousMode]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (publishCommentOptions && triggerPublishComment) {
|
||||
(async () => {
|
||||
await publishComment();
|
||||
resetFields();
|
||||
closeModal();
|
||||
})();
|
||||
setTriggerPublishComment(false);
|
||||
setExecuteAnonMode(false);
|
||||
}
|
||||
}, [publishCommentOptions, triggerPublishComment, publishComment, resetFields, closeModal]);
|
||||
const updateSigner = useCallback(async () => {
|
||||
if (anonymousMode) {
|
||||
setExecuteAnonMode(true);
|
||||
|
||||
let storedSigners = JSON.parse(localStorage.getItem('storedSigners')) || {};
|
||||
let signer;
|
||||
|
||||
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 = captchaResponse;
|
||||
resolve(currentCaptchaResponse);
|
||||
setIsCaptchaOpen(false);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
if (!storedSigners[selectedParentCid]) {
|
||||
signer = await account?.plebbit.createSigner();
|
||||
storedSigners[selectedParentCid] = { privateKey: signer?.privateKey, address: signer?.address };
|
||||
localStorage.setItem('storedSigners', JSON.stringify(storedSigners));
|
||||
} else {
|
||||
const signerPrivateKey = storedSigners[selectedParentCid].privateKey;
|
||||
|
||||
setCaptchaResponse('');
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
try {
|
||||
signer = await account?.plebbit.createSigner({ type: 'ed25519', privateKey: signerPrivateKey });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
setResolveCaptchaPromise(resolve);
|
||||
};
|
||||
|
||||
challengeImg.onerror = () => {
|
||||
reject(setNewErrorMessage('Could not load challenges'));
|
||||
};
|
||||
});
|
||||
};
|
||||
setPublishCommentOptions((prevPublishCommentOptions) => {
|
||||
const newPublishCommentOptions = {
|
||||
...prevPublishCommentOptions,
|
||||
signer,
|
||||
author: {
|
||||
...prevPublishCommentOptions.author,
|
||||
address: signer?.address,
|
||||
},
|
||||
};
|
||||
|
||||
if (JSON.stringify(prevPublishCommentOptions) !== JSON.stringify(newPublishCommentOptions)) {
|
||||
return newPublishCommentOptions;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
isOpen={isOpen}
|
||||
onAfterOpen={onModalOpen}
|
||||
onRequestClose={closeModal}
|
||||
contentLabel="Reply Modal"
|
||||
shouldCloseOnEsc={true}
|
||||
shouldCloseOnOverlayClick={isMobile}
|
||||
selectedStyle={selectedStyle}
|
||||
overlayClassName="overlay"
|
||||
style={isMobile ? ({ overlay: { backgroundColor: "rgba(0,0,0,.25)" }}) : ({ overlay: { backgroundColor: "rgba(0,0,0,0)" }})
|
||||
}
|
||||
>
|
||||
<Draggable handle=".modal-header" nodeRef={nodeRef} disabled={isMobile}>
|
||||
<div className="modal-content" ref={nodeRef}>
|
||||
<div className="modal-header">
|
||||
Reply to c/{selectedShortCid}
|
||||
<button className="icon" onClick={() => closeModal()} title="close" />
|
||||
</div>
|
||||
<div id="form">
|
||||
<div>
|
||||
{account && account?.author && account?.author.displayName ? (
|
||||
<input id="name" type="text" value={account?.author?.displayName} ref={nameRef} disabled />
|
||||
) : (
|
||||
<input id="name" type="text" placeholder="Anonymous" ref={nameRef} />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<input id="name" type="text" placeholder="Embed link" ref={linkRef} />
|
||||
</div>
|
||||
<div className="textarea-wrapper">
|
||||
<span className="fixed-text">{`c/${selectedShortCid}`}</span>
|
||||
<textarea className="textarea"
|
||||
rows="4"
|
||||
placeholder="Comment"
|
||||
defaultValue={selectedText}
|
||||
wrap="soft"
|
||||
ref={commentRef}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button id="next" onClick={handleSubmit}>Post</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Draggable>
|
||||
</StyledModal>
|
||||
);
|
||||
return prevPublishCommentOptions;
|
||||
});
|
||||
}
|
||||
}, [selectedParentCid, anonymousMode, account]);
|
||||
|
||||
useEffect(() => {
|
||||
if (anonymousMode) {
|
||||
updateSigner();
|
||||
}
|
||||
}, [updateSigner, anonymousMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (publishCommentOptions && triggerPublishComment) {
|
||||
(async () => {
|
||||
await publishComment();
|
||||
resetFields();
|
||||
closeModal();
|
||||
})();
|
||||
setTriggerPublishComment(false);
|
||||
setExecuteAnonMode(false);
|
||||
}
|
||||
}, [publishCommentOptions, triggerPublishComment, publishComment, resetFields, closeModal]);
|
||||
|
||||
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 = 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'));
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
isOpen={isOpen}
|
||||
onAfterOpen={onModalOpen}
|
||||
onRequestClose={closeModal}
|
||||
contentLabel='Reply Modal'
|
||||
shouldCloseOnEsc={true}
|
||||
shouldCloseOnOverlayClick={isMobile}
|
||||
selectedStyle={selectedStyle}
|
||||
overlayClassName='overlay'
|
||||
style={isMobile ? { overlay: { backgroundColor: 'rgba(0,0,0,.25)' } } : { overlay: { backgroundColor: 'rgba(0,0,0,0)' } }}
|
||||
>
|
||||
<Draggable handle='.modal-header' nodeRef={nodeRef} disabled={isMobile}>
|
||||
<div className='modal-content' ref={nodeRef}>
|
||||
<div className='modal-header'>
|
||||
Reply to c/{selectedShortCid}
|
||||
<button className='icon' onClick={() => closeModal()} title='close' />
|
||||
</div>
|
||||
<div id='form'>
|
||||
<div>
|
||||
{account?.author.displayName ? (
|
||||
<input id='name' type='text' defaultValue={account?.author?.displayName} ref={nameRef} />
|
||||
) : (
|
||||
<input id='name' type='text' placeholder='Anonymous' ref={nameRef} />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<input id='name' type='text' placeholder='Embed link' ref={linkRef} />
|
||||
</div>
|
||||
<div className='textarea-wrapper'>
|
||||
<span className='fixed-text'>{`c/${selectedShortCid}`}</span>
|
||||
<textarea className='textarea' rows='4' placeholder='Comment' defaultValue={selectedText} wrap='soft' ref={commentRef} />
|
||||
</div>
|
||||
<div>
|
||||
<button id='next' onClick={handleSubmit}>
|
||||
Post
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Draggable>
|
||||
</StyledModal>
|
||||
);
|
||||
};
|
||||
|
||||
Modal.setAppElement('#root');
|
||||
|
||||
export default ReplyModal;
|
||||
export default ReplyModal;
|
||||
|
||||
@@ -1,97 +1,95 @@
|
||||
import styled from "styled-components";
|
||||
import styled from 'styled-components';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
export const StyledModal = styled(Modal)`
|
||||
.hide-modal-overlay {
|
||||
display: none;
|
||||
}
|
||||
.hide-modal-overlay {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: fixed;
|
||||
top: calc(50% - 150px);
|
||||
left: calc(50% - 150px);
|
||||
display: block;
|
||||
padding: 2px;
|
||||
font-size: 10pt;
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
}
|
||||
.modal-content {
|
||||
position: fixed;
|
||||
top: calc(50% - 150px);
|
||||
left: calc(50% - 150px);
|
||||
display: block;
|
||||
padding: 2px;
|
||||
font-size: 10pt;
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
font-size: 10pt;
|
||||
text-align: center;
|
||||
margin-bottom: 1px;
|
||||
padding: 0;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
cursor: move;
|
||||
font-weight: 700;
|
||||
}
|
||||
.modal-header {
|
||||
font-size: 10pt;
|
||||
text-align: center;
|
||||
margin-bottom: 1px;
|
||||
padding: 0;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
cursor: move;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.icon {
|
||||
all: unset;
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
margin-bottom: -4px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: none;
|
||||
}
|
||||
.icon {
|
||||
all: unset;
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
margin-bottom: -4px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#field {
|
||||
border: 1px solid #aaa;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 10pt;
|
||||
outline: medium none;
|
||||
width: 298px;
|
||||
padding: 2px;
|
||||
}
|
||||
#field {
|
||||
border: 1px solid #aaa;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 10pt;
|
||||
outline: medium none;
|
||||
width: 298px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-width: 296px;
|
||||
float: left;
|
||||
font-size: 10pt;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
textarea {
|
||||
min-width: 296px;
|
||||
float: left;
|
||||
font-size: 10pt;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
#captcha-container {
|
||||
position: relative;
|
||||
clear: both;
|
||||
width: 302px;
|
||||
height: 100px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
#captcha-container {
|
||||
position: relative;
|
||||
width: 302px;
|
||||
height: 100px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 17px;
|
||||
}
|
||||
|
||||
#response {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-size: 11px;
|
||||
height: 18px;
|
||||
margin: 0px;
|
||||
padding: 0px 2px;
|
||||
font-family: monospace;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#response {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-size: 11px;
|
||||
height: 18px;
|
||||
margin: 0px;
|
||||
padding: 0px 2px;
|
||||
font-family: monospace;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#nav {
|
||||
float: right;
|
||||
margin: 0;
|
||||
width: auto;
|
||||
}
|
||||
#nav {
|
||||
float: right;
|
||||
margin: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
${({ selectedStyle }) => {
|
||||
switch (selectedStyle) {
|
||||
case 'Yotsuba':
|
||||
return `
|
||||
${({ selectedStyle }) => {
|
||||
switch (selectedStyle) {
|
||||
case 'Yotsuba':
|
||||
return `
|
||||
.modal-content {
|
||||
background-color: #f0e0d6;
|
||||
border: 1px solid #d9bfb7;
|
||||
@@ -111,8 +109,8 @@ export const StyledModal = styled(Modal)`
|
||||
color: maroon;
|
||||
}`;
|
||||
|
||||
case 'Yotsuba-B':
|
||||
return `
|
||||
case 'Yotsuba-B':
|
||||
return `
|
||||
.modal-content {
|
||||
background-color: #d6daf0;
|
||||
border: 1px solid #b7c5d9;
|
||||
@@ -132,8 +130,8 @@ export const StyledModal = styled(Modal)`
|
||||
color: #000;
|
||||
}`;
|
||||
|
||||
case 'Futaba':
|
||||
return `
|
||||
case 'Futaba':
|
||||
return `
|
||||
.modal-content {
|
||||
background-color: #f0e0d6;
|
||||
border: 1px solid #d9bfb7;
|
||||
@@ -153,8 +151,8 @@ export const StyledModal = styled(Modal)`
|
||||
color: maroon;
|
||||
}`;
|
||||
|
||||
case 'Burichan':
|
||||
return `
|
||||
case 'Burichan':
|
||||
return `
|
||||
.modal-content {
|
||||
background-color: #d6daf0;
|
||||
border: 1px solid #b7c5d9;
|
||||
@@ -174,8 +172,8 @@ export const StyledModal = styled(Modal)`
|
||||
color: #000;
|
||||
}`;
|
||||
|
||||
case 'Tomorrow':
|
||||
return `
|
||||
case 'Tomorrow':
|
||||
return `
|
||||
.modal-content {
|
||||
background-color: #282a2e;
|
||||
border: 1px solid #111;
|
||||
@@ -218,8 +216,8 @@ export const StyledModal = styled(Modal)`
|
||||
filter: brightness(80%);
|
||||
}`;
|
||||
|
||||
case 'Photon':
|
||||
return `
|
||||
case 'Photon':
|
||||
return `
|
||||
.modal-content {
|
||||
background-color: #ddd;
|
||||
border: 1px solid #ccc;
|
||||
@@ -236,10 +234,10 @@ export const StyledModal = styled(Modal)`
|
||||
|
||||
span {
|
||||
color: #333;
|
||||
}`;
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}}
|
||||
`;
|
||||
}`;
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}}
|
||||
`;
|
||||
|
||||
+3229
-2735
File diff suppressed because it is too large
Load Diff
+991
-901
File diff suppressed because it is too large
Load Diff
+4251
-3623
File diff suppressed because it is too large
Load Diff
+1754
-1524
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+261
-238
@@ -1,6 +1,6 @@
|
||||
import React, { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { useAccount, useBufferedFeeds, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { useAccount, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Container, Header, Logo, Page, Search, About, AboutTitle, AboutContent, Boards, BoardsTitle, BoardsContent, Footer } from '../styled/views/Home.styled';
|
||||
import BoardAvatar from '../BoardAvatar';
|
||||
@@ -9,245 +9,268 @@ import CreateBoardModal from '../modals/CreateBoardModal';
|
||||
import useGeneralStore from '../../hooks/stores/useGeneralStore';
|
||||
import packageJson from '../../../package.json';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
const {version} = packageJson;
|
||||
const { version } = packageJson;
|
||||
const commitRef = process?.env?.REACT_APP_COMMIT_REF ? ` ${process.env.REACT_APP_COMMIT_REF.slice(0, 7)}` : '';
|
||||
|
||||
const SubplebbitTitle = ({ address }) => {
|
||||
const subplebbitAddress = address;
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const title = subplebbit.title ?? null;
|
||||
|
||||
const SubplebbitTitle = ({address}) => {
|
||||
const subplebbitAddress = address;
|
||||
const subplebbit = useSubplebbit({subplebbitAddress});
|
||||
const title = subplebbit.title ?? null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{title ? (
|
||||
<span>{title}</span>
|
||||
) : (
|
||||
<span style={{userSelect: "none"}}> </span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const Home = () => {
|
||||
const {
|
||||
bodyStyle, setBodyStyle,
|
||||
defaultSubplebbits,
|
||||
setSelectedAddress,
|
||||
selectedStyle, setSelectedStyle,
|
||||
setSelectedTitle
|
||||
} = useGeneralStore(state => state);
|
||||
|
||||
const account = useAccount();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const inputRef = useRef(null);
|
||||
const prevStyle = useRef(selectedStyle);
|
||||
const prevBodyStyle = useRef(bodyStyle);
|
||||
|
||||
const [isCreateBoardOpen, setIsCreateBoardOpen] = useState(false);
|
||||
|
||||
// preload default subs and subscriptions
|
||||
useBufferedFeeds({
|
||||
feedsOptions: [
|
||||
{subplebbitAddresses: defaultSubplebbits.map(
|
||||
(subplebbit) => subplebbit.address
|
||||
), sortType: 'active'},
|
||||
{subplebbitAddresses: account?.subscriptions, sortType: 'active'}
|
||||
]
|
||||
});
|
||||
|
||||
// prevent dark mode
|
||||
useEffect(() => {
|
||||
const currentPrevStyle = prevStyle.current;
|
||||
const currentPrevBodyStyle = prevBodyStyle.current;
|
||||
|
||||
setBodyStyle({
|
||||
background: "#ffe url(assets/fade.png) top repeat-x",
|
||||
color: "maroon",
|
||||
fontFamily: "Helvetica, Arial, sans-serif"
|
||||
});
|
||||
setSelectedStyle("Yotsuba");
|
||||
|
||||
return () => {
|
||||
setSelectedStyle(currentPrevStyle);
|
||||
setBodyStyle(currentPrevBodyStyle);
|
||||
};
|
||||
}, [setBodyStyle, setSelectedStyle]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateBoardModal
|
||||
selectedStyle={selectedStyle}
|
||||
isOpen={isCreateBoardOpen}
|
||||
closeModal={() => setIsCreateBoardOpen(false)} />
|
||||
<Helmet>
|
||||
<title>plebchan</title>
|
||||
</Helmet>
|
||||
<Container>
|
||||
<Tooltip id="tooltip" className="tooltip" />
|
||||
<Header>
|
||||
<Logo>
|
||||
<Link to="/">
|
||||
<img alt="plebchan" src="assets/logo/logo-transparent.png" />
|
||||
</Link>
|
||||
</Logo>
|
||||
</Header>
|
||||
<Page>
|
||||
<Search>
|
||||
<input type="text" placeholder='"board.eth" or "12D3KooW..."' ref={inputRef} onKeyDown={
|
||||
(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
const address = inputRef.current.value;
|
||||
if (address) {
|
||||
setSelectedAddress(address);
|
||||
navigate(`/p/${address}`)
|
||||
}
|
||||
}
|
||||
}} />
|
||||
<input type="submit" value="Search" onClick={
|
||||
() => {
|
||||
const address = inputRef.current.value;
|
||||
if (address) {
|
||||
setSelectedAddress(address);
|
||||
navigate(`/p/${address}`)
|
||||
}
|
||||
}
|
||||
} />
|
||||
</Search>
|
||||
<About>
|
||||
<AboutTitle>
|
||||
<h2>What is plebchan?</h2>
|
||||
</AboutTitle>
|
||||
<AboutContent>
|
||||
<div id="content">
|
||||
<p>Plebchan is a serverless, adminless, decentralized 4chan alternative that uses the <a href="https://plebbit.com" target="_blank" rel="noreferrer">plebbit protocol</a>. Users do not need to register an account before participating in a community; anyone can post comments, share media links, and <button style={{all: 'unset', 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 a board</button>. All the content is just text, and plebchan's speed depends on how many users are <button style={{all: 'unset', cursor: 'pointer'}}
|
||||
onClick={() => {
|
||||
alert(
|
||||
'You can seed automatically simply by using the plebchan desktop app, which is a plebbit full node. Download it here:\nhttps://github.com/plebbit/plebchan/releases/latest\n\nIf you are comfortable with the command line, you can use plebbit-cli with the --seed flag:\nhttps://github.com/plebbit/plebbit-cli\n\n'
|
||||
)
|
||||
}}
|
||||
>seeding</button>. Search for any board address above, or feel free to click on a popular board below that interests you and jump right in! </p>
|
||||
<br />
|
||||
<p>There are no global rules, each board is completely independent and its owner decides how/if it should be moderated.</p>
|
||||
</div>
|
||||
</AboutContent>
|
||||
</About>
|
||||
{account?.subscriptions?.length > 0 ? (
|
||||
<Boards>
|
||||
<BoardsTitle>
|
||||
<h2>Subscriptions</h2>
|
||||
</BoardsTitle>
|
||||
<BoardsContent id="subscriptions">
|
||||
<h3 style={{textDecoration: 'underline', display: 'inline'}}>
|
||||
You have subscribed to {account?.subscriptions?.length} board{account?.subscriptions?.length > 1 ? "s" : null}
|
||||
</h3>
|
||||
<Link to="/p/subscriptions" id="view-all" onClick={()=> {window.scrollTo(0, 0)}}>[view all]</Link>
|
||||
<br />
|
||||
{account?.subscriptions?.map((subscription, index) => (
|
||||
<Fragment key={`frag-${index}`}>
|
||||
<Link key={`sub-${index}`} className="boardlink"
|
||||
onClick={()=> {window.scrollTo(0, 0)}}
|
||||
to={`/p/${subscription}`}>
|
||||
<br id="mobile-br" />
|
||||
{subscription}
|
||||
</Link>
|
||||
<OfflineIndicator
|
||||
key={`offline-${index}`}
|
||||
address={subscription}
|
||||
className="disconnected"
|
||||
isText={true} />
|
||||
<br key={`br-${index}`} />
|
||||
</Fragment>
|
||||
))}
|
||||
<br id="mobile-br" />
|
||||
</BoardsContent>
|
||||
</Boards>
|
||||
) : null}
|
||||
<Boards>
|
||||
<BoardsTitle>
|
||||
<h2>Popular Boards</h2>
|
||||
</BoardsTitle>
|
||||
<BoardsContent>
|
||||
{defaultSubplebbits.map((subplebbit, index) => (
|
||||
<div className="board" key={`board-${index}`}>
|
||||
<div className="board-title" key="board-title">
|
||||
{subplebbit.title ?? <SubplebbitTitle address={subplebbit.address} />}
|
||||
</div>
|
||||
<div className="board-avatar-container" key="board-avatar-container">
|
||||
<Link to={`/p/${subplebbit.address}`} key="link" onClick={() => {
|
||||
setSelectedTitle(subplebbit.title);
|
||||
setSelectedAddress(subplebbit.address);
|
||||
window.scrollTo(0, 0);
|
||||
}} >
|
||||
<BoardAvatar key="baordavatar" address={subplebbit.address} />
|
||||
</Link>
|
||||
<OfflineIndicator
|
||||
address={subplebbit.address}
|
||||
className="offline-indicator"
|
||||
tooltipPlace="top"
|
||||
key="oi2"/>
|
||||
</div>
|
||||
<div className="board-text" key="bt">
|
||||
<b key="b">{subplebbit.address}</b>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</BoardsContent>
|
||||
</Boards>
|
||||
</Page>
|
||||
<Footer>
|
||||
<ul>
|
||||
<li className="fill"></li>
|
||||
<li className="first">
|
||||
<a href="https://etherscan.io/token/0xEA81DaB2e0EcBc6B5c4172DE4c22B6Ef6E55Bd8f" target="_blank" rel="noopener noreferrer">Token</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://gitcoin.co/grants/5515/plebbit-a-serverless-adminless-decentralized-redd" target="_blank" rel="noopener noreferrer">Donate</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/plebbit/whitepaper/discussions/2" target="_blank" rel="noopener noreferrer">Whitepaper</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/plebbit/plebchan" target="_blank" rel="noopener noreferrer">GitHub</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://matrix.to/#/#plebbit:plebbitchat.org" target="_blank" rel="noopener noreferrer">Matrix</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://t.me/plebbit" target="_blank" rel="noopener noreferrer">Telegram</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://twitter.com/plebchan_eth" target="_blank" rel="noopener noreferrer">Twitter</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://discord.gg/E7ejphwzGW" target="_blank" rel="noopener noreferrer">Discord</a>
|
||||
</li>
|
||||
</ul>
|
||||
</Footer>
|
||||
<div style={{
|
||||
textAlign: "center",
|
||||
fontSize: "11px",
|
||||
marginBottom: "2em",
|
||||
}}>
|
||||
plebchan v{version}{commitRef}. GPL-2.0
|
||||
</div>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
return <>{title ? <span>{title}</span> : <span style={{ userSelect: 'none' }}> </span>}</>;
|
||||
};
|
||||
|
||||
export default Home;
|
||||
const Home = () => {
|
||||
const { bodyStyle, setBodyStyle, defaultSubplebbits, setSelectedAddress, selectedStyle, setSelectedStyle, setSelectedTitle } = useGeneralStore((state) => state);
|
||||
|
||||
const account = useAccount();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const inputRef = useRef(null);
|
||||
const prevStyle = useRef(selectedStyle);
|
||||
const prevBodyStyle = useRef(bodyStyle);
|
||||
|
||||
const [isCreateBoardOpen, setIsCreateBoardOpen] = useState(false);
|
||||
|
||||
// prevent dark mode
|
||||
useEffect(() => {
|
||||
const currentPrevStyle = prevStyle.current;
|
||||
const currentPrevBodyStyle = prevBodyStyle.current;
|
||||
|
||||
setBodyStyle({
|
||||
background: '#ffe url(assets/fade.png) top repeat-x',
|
||||
color: 'maroon',
|
||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
||||
});
|
||||
setSelectedStyle('Yotsuba');
|
||||
|
||||
return () => {
|
||||
setSelectedStyle(currentPrevStyle);
|
||||
setBodyStyle(currentPrevBodyStyle);
|
||||
};
|
||||
}, [setBodyStyle, setSelectedStyle]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateBoardModal selectedStyle={selectedStyle} isOpen={isCreateBoardOpen} closeModal={() => setIsCreateBoardOpen(false)} />
|
||||
<Helmet>
|
||||
<title>plebchan</title>
|
||||
</Helmet>
|
||||
<Container>
|
||||
<Tooltip id='tooltip' className='tooltip' />
|
||||
<Header>
|
||||
<Logo>
|
||||
<Link to='/'>
|
||||
<img alt='plebchan' src='assets/logo/logo-transparent.png' />
|
||||
</Link>
|
||||
</Logo>
|
||||
</Header>
|
||||
<Page>
|
||||
<Search>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='"board.eth" or "12D3KooW..."'
|
||||
ref={inputRef}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
const address = inputRef.current.value;
|
||||
if (address) {
|
||||
setSelectedAddress(address);
|
||||
navigate(`/p/${address}`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type='submit'
|
||||
value='Search'
|
||||
onClick={() => {
|
||||
const address = inputRef.current.value;
|
||||
if (address) {
|
||||
setSelectedAddress(address);
|
||||
navigate(`/p/${address}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Search>
|
||||
<About>
|
||||
<AboutTitle>
|
||||
<h2>What is plebchan?</h2>
|
||||
</AboutTitle>
|
||||
<AboutContent>
|
||||
<div id='content'>
|
||||
<p>
|
||||
Plebchan is the only serverless, adminless, fully decentralized 4chan alternative where any pleb can create and own unlimited boards, and nobody can
|
||||
take them down. Boards can always find competition, and board owners can monetize freely, so janitors aren't needed. Plebchan is a client for{' '}
|
||||
<a href='https://plebbit.com' target='_blank' rel='noreferrer'>
|
||||
plebbit
|
||||
</a>
|
||||
, a pure P2P protocol and tooling for creating decentralized forum applications. All data is just text, and plebchan's speed depends on how many users
|
||||
are{' '}
|
||||
<button
|
||||
style={{ all: 'unset', cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
alert(
|
||||
'You can seed automatically simply by using the plebchan desktop app, which is a plebbit full node. Download it here:\nhttps://github.com/plebbit/plebchan/releases/latest\n\nIf you are comfortable with the command line, you can use plebbit-cli with the --seed flag:\nhttps://github.com/plebbit/plebbit-cli\n\n',
|
||||
);
|
||||
}}
|
||||
>
|
||||
seeding
|
||||
</button>
|
||||
. Search for any board address above, or feel free to click on a popular board below that interests you and jump right in!{' '}
|
||||
</p>
|
||||
<br />
|
||||
<p>There are no global rules, each board is completely independent and its owner decides how/if it should be moderated.</p>
|
||||
<br />
|
||||
<p>
|
||||
{' '}
|
||||
HOW TO RUN A BOARD (currently via CLI only): <strong>(1)</strong> have a computer you can use as server, because users will connect to your board
|
||||
peer-to-peer; <strong>(2)</strong> go to{' '}
|
||||
<a href='https://github.com/plebbit/plebbit-cli#install' target='_blank' rel='noreferrer'>
|
||||
https://github.com/plebbit/plebbit-cli#install
|
||||
</a>{' '}
|
||||
and follow the instructions to run your daemon, create a subplebbit (board) and edit its settings; <strong>(3)</strong> users can always connect to your
|
||||
board via its address, but it can be whitelisted by submitting a pull request here:{' '}
|
||||
<a href='https://github.com/plebbit/temporary-default-subplebbits' target='_blank' rel='noreferrer'>
|
||||
https://github.com/plebbit/temporary-default-subplebbits
|
||||
</a>{' '}
|
||||
this will be automated with a plebbit DAO using the plebbit token.
|
||||
</p>
|
||||
</div>
|
||||
</AboutContent>
|
||||
</About>
|
||||
{account?.subscriptions?.length > 0 ? (
|
||||
<Boards>
|
||||
<BoardsTitle>
|
||||
<h2>Subscriptions</h2>
|
||||
</BoardsTitle>
|
||||
<BoardsContent id='subscriptions'>
|
||||
<h3 style={{ textDecoration: 'underline', display: 'inline' }}>
|
||||
You have subscribed to {account?.subscriptions?.length} board{account?.subscriptions?.length > 1 ? 's' : null}
|
||||
</h3>
|
||||
|
||||
<Link
|
||||
to='/p/subscriptions'
|
||||
id='view-all'
|
||||
onClick={() => {
|
||||
window.scrollTo(0, 0);
|
||||
}}
|
||||
>
|
||||
[view all]
|
||||
</Link>
|
||||
<br />
|
||||
{account?.subscriptions?.map((subscription, index) => (
|
||||
<Fragment key={`frag-${index}`}>
|
||||
<Link
|
||||
key={`sub-${index}`}
|
||||
className='boardlink'
|
||||
onClick={() => {
|
||||
window.scrollTo(0, 0);
|
||||
}}
|
||||
to={`/p/${subscription}`}
|
||||
>
|
||||
<br id='mobile-br' />
|
||||
{subscription}
|
||||
</Link>
|
||||
<OfflineIndicator key={`offline-${index}`} address={subscription} className='disconnected' isText={true} />
|
||||
<br key={`br-${index}`} />
|
||||
</Fragment>
|
||||
))}
|
||||
<br id='mobile-br' />
|
||||
</BoardsContent>
|
||||
</Boards>
|
||||
) : null}
|
||||
<Boards>
|
||||
<BoardsTitle>
|
||||
<h2>Whitelisted Boards</h2>
|
||||
</BoardsTitle>
|
||||
<BoardsContent>
|
||||
{defaultSubplebbits.map((subplebbit, index) => (
|
||||
<div className='board' key={`board-${index}`}>
|
||||
<div className='board-title' key='board-title'>
|
||||
{subplebbit.title ?? <SubplebbitTitle address={subplebbit.address} />}
|
||||
</div>
|
||||
<div className='board-avatar-container' key='board-avatar-container'>
|
||||
<Link
|
||||
to={`/p/${subplebbit.address}`}
|
||||
key='link'
|
||||
onClick={() => {
|
||||
setSelectedTitle(subplebbit.title);
|
||||
setSelectedAddress(subplebbit.address);
|
||||
window.scrollTo(0, 0);
|
||||
}}
|
||||
>
|
||||
<BoardAvatar key='baordavatar' address={subplebbit.address} />
|
||||
</Link>
|
||||
<OfflineIndicator address={subplebbit.address} className='offline-indicator' tooltipPlace='top' key='oi2' />
|
||||
</div>
|
||||
<div className='board-text' key='bt'>
|
||||
<b key='b'>{subplebbit.address}</b>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</BoardsContent>
|
||||
</Boards>
|
||||
</Page>
|
||||
<Footer>
|
||||
<ul>
|
||||
<li className='fill'></li>
|
||||
<li className='first'>
|
||||
<a href='https://etherscan.io/token/0xEA81DaB2e0EcBc6B5c4172DE4c22B6Ef6E55Bd8f' target='_blank' rel='noopener noreferrer'>
|
||||
Token
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='https://gitcoin.co/grants/5515/plebbit-a-serverless-adminless-decentralized-redd' target='_blank' rel='noopener noreferrer'>
|
||||
Donate
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='https://github.com/plebbit/whitepaper/discussions/2' target='_blank' rel='noopener noreferrer'>
|
||||
Whitepaper
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='https://github.com/plebbit/plebchan' target='_blank' rel='noopener noreferrer'>
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='https://matrix.to/#/#plebbit:plebbitchat.org' target='_blank' rel='noopener noreferrer'>
|
||||
Matrix
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='https://t.me/plebbit' target='_blank' rel='noopener noreferrer'>
|
||||
Telegram
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='https://twitter.com/plebchan_eth' target='_blank' rel='noopener noreferrer'>
|
||||
Twitter
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='https://discord.gg/E7ejphwzGW' target='_blank' rel='noopener noreferrer'>
|
||||
Discord
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</Footer>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
fontSize: '11px',
|
||||
marginBottom: '2em',
|
||||
}}
|
||||
>
|
||||
plebchan v{version}
|
||||
{commitRef}. GPL-2.0
|
||||
</div>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
|
||||
+658
-567
File diff suppressed because it is too large
Load Diff
+3235
-2741
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+3061
-2685
File diff suppressed because it is too large
Load Diff
@@ -2756,9 +2756,9 @@
|
||||
mkdirp "^1.0.4"
|
||||
rimraf "^3.0.2"
|
||||
|
||||
"@plebbit/plebbit-js@https://github.com/plebbit/plebbit-js.git#a68905109591f81c996514dd548af5cb25c4b9d2":
|
||||
"@plebbit/plebbit-js@https://github.com/plebbit/plebbit-js.git#6985b2a4194328752c6d76e80470c8079c562869":
|
||||
version "0.0.3"
|
||||
resolved "https://github.com/plebbit/plebbit-js.git#a68905109591f81c996514dd548af5cb25c4b9d2"
|
||||
resolved "https://github.com/plebbit/plebbit-js.git#6985b2a4194328752c6d76e80470c8079c562869"
|
||||
dependencies:
|
||||
"@ensdomains/eth-ens-namehash" "2.0.15"
|
||||
"@keyv/sqlite" "3.6.2"
|
||||
@@ -2817,11 +2817,26 @@
|
||||
dependencies:
|
||||
debug "4.3.3"
|
||||
|
||||
"@plebbit/plebbit-react-hooks@https://github.com/plebbit/plebbit-react-hooks.git#5aef9d028f25fbe3db6fe748df0841f970e39f63":
|
||||
"@plebbit/plebbit-react-hooks@https://github.com/plebbit/plebbit-react-hooks.git#0024401c390929e47b44331a8dadd61ba212b871":
|
||||
version "0.0.1"
|
||||
resolved "https://github.com/plebbit/plebbit-react-hooks.git#5aef9d028f25fbe3db6fe748df0841f970e39f63"
|
||||
resolved "https://github.com/plebbit/plebbit-react-hooks.git#0024401c390929e47b44331a8dadd61ba212b871"
|
||||
dependencies:
|
||||
"@plebbit/plebbit-js" "https://github.com/plebbit/plebbit-js.git#a68905109591f81c996514dd548af5cb25c4b9d2"
|
||||
"@plebbit/plebbit-js" "https://github.com/plebbit/plebbit-js.git#6985b2a4194328752c6d76e80470c8079c562869"
|
||||
"@plebbit/plebbit-logger" "https://github.com/plebbit/plebbit-logger.git"
|
||||
assert "2.0.0"
|
||||
ethers "5.6.9"
|
||||
localforage "1.10.0"
|
||||
lodash.isequal "4.5.0"
|
||||
memoizee "0.4.15"
|
||||
quick-lru "5.1.1"
|
||||
uuid "8.3.2"
|
||||
zustand "4.0.0"
|
||||
|
||||
"@plebbit/plebbit-react-hooks@https://github.com/plebbit/plebbit-react-hooks.git#16b2b9658a9f69272aece5db2da0b5a459593921":
|
||||
version "0.0.1"
|
||||
resolved "https://github.com/plebbit/plebbit-react-hooks.git#16b2b9658a9f69272aece5db2da0b5a459593921"
|
||||
dependencies:
|
||||
"@plebbit/plebbit-js" "https://github.com/plebbit/plebbit-js.git#6985b2a4194328752c6d76e80470c8079c562869"
|
||||
"@plebbit/plebbit-logger" "https://github.com/plebbit/plebbit-logger.git"
|
||||
assert "2.0.0"
|
||||
ethers "5.6.9"
|
||||
|
||||
Reference in New Issue
Block a user