Merge pull request #258 from plebbit/development

Development
This commit is contained in:
plebeius.eth
2023-09-22 21:46:59 +02:00
committed by GitHub
16 changed files with 19552 additions and 16942 deletions
+1 -1
View File
@@ -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",
+2 -2
View File
@@ -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 />
+8 -1
View File
@@ -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>
&nbsp;({replyMediaInfo?.type})
&nbsp;{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'>
+270 -292
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+261 -238
View File
@@ -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"}}>&nbsp;</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>&nbsp;
<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}&nbsp;
</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' }}>&nbsp;</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>
&nbsp;
<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}&nbsp;
</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;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+20 -5
View File
@@ -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"