Merge pull request #82 from plebbit/development

Development
This commit is contained in:
plebeius.eth
2023-05-27 17:21:44 +02:00
committed by GitHub
21 changed files with 1362 additions and 662 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

+34 -9
View File
@@ -7,11 +7,15 @@ import Draggable from 'react-draggable';
const CaptchaModal = () => { const CaptchaModal = () => {
const { const {
challengesArray, challengesArray, setChallengesArray,
pendingComment, pendingComment,
selectedStyle, selectedStyle,
setCaptchaResponse, setCaptchaResponse,
isAuthorDelete, setIsAuthorDelete,
isAuthorEdit, setIsAuthorEdit,
isCaptchaOpen, setIsCaptchaOpen, isCaptchaOpen, setIsCaptchaOpen,
isModEdit, setIsModEdit,
resolveCaptchaPromise,
selectedShortCid, selectedShortCid,
} = useGeneralStore(state => state); } = useGeneralStore(state => state);
@@ -22,8 +26,18 @@ const CaptchaModal = () => {
const responseRef = useRef(); const responseRef = useRef();
const nodeRef = useRef(null); const nodeRef = useRef(null);
useEffect(() => {
if (!isCaptchaOpen) {
setIsAuthorDelete(false);
setIsAuthorEdit(false);
setIsModEdit(false);
}
}, [isCaptchaOpen, setIsAuthorDelete, setIsAuthorEdit, setIsModEdit]);
useEffect(() => {
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth <= 480); const handleResize = () => setIsMobile(window.innerWidth <= 480);
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
@@ -50,6 +64,7 @@ const CaptchaModal = () => {
} }
}, [challengesArray]); }, [challengesArray]);
const handleKeyDown = (event) => { const handleKeyDown = (event) => {
if (event.key === "Enter") { if (event.key === "Enter") {
event.preventDefault(); event.preventDefault();
@@ -57,28 +72,35 @@ const CaptchaModal = () => {
} }
}; };
const handleReturnKeyDown = () => { const handleReturnKeyDown = () => {
submitCaptcha((response) => { submitCaptcha((response) => {
useGeneralStore.getState().setCaptchaResponse(response); setCaptchaResponse(response);
useGeneralStore.getState().resolveCaptchaPromise(response); resolveCaptchaPromise(response);
}); });
}; };
const submitCaptcha = (callback) => { const submitCaptcha = (callback) => {
setCaptchaResponse(responseRef.current.value); setCaptchaResponse(responseRef.current.value);
setImageSources([]);
setChallengesArray([]);
setIsCaptchaOpen(false); setIsCaptchaOpen(false);
if (callback) { if (callback) {
callback(responseRef.current.value); callback(responseRef.current.value);
} }
}; };
return ( return (
<StyledModal <StyledModal
isOpen={isCaptchaOpen} isOpen={isCaptchaOpen}
onRequestClose={() => setIsCaptchaOpen(false)} onRequestClose={() => {
submitCaptcha();
setImageSources([]);
setChallengesArray([]);
setIsCaptchaOpen(false);}}
contentLabel="Captcha Modal" contentLabel="Captcha Modal"
shouldCloseOnEsc={false} shouldCloseOnEsc={false}
shouldCloseOnOverlayClick={false} shouldCloseOnOverlayClick={false}
@@ -87,7 +109,10 @@ const CaptchaModal = () => {
<Draggable handle=".modal-header" nodeRef={nodeRef} disabled={isMobile}> <Draggable handle=".modal-header" nodeRef={nodeRef} disabled={isMobile}>
<div className="modal-content" ref={nodeRef}> <div className="modal-content" ref={nodeRef}>
<div className="modal-header"> <div className="modal-header">
{pendingComment.parentCid ? {isModEdit ? "Challenge for Moderator Action" :
isAuthorEdit ? "Challenge for Editing Post" :
isAuthorDelete ? "Challenge for Deleting Post" :
pendingComment.parentCid ?
("Challenges for Reply to c/" + selectedShortCid) : ("Challenges for Reply to c/" + selectedShortCid) :
"Challenges for New Thread"} "Challenges for New Thread"}
<button className="icon" onClick={() => setIsCaptchaOpen(false)} title="close" /> <button className="icon" onClick={() => setIsCaptchaOpen(false)} title="close" />
@@ -146,7 +171,7 @@ const CaptchaModal = () => {
} }
}} }}
> >
{currentChallengeIndex + 1 < totalChallenges ? "Next" : "Post"} {currentChallengeIndex + 1 < totalChallenges ? "Next" : "Submit"}
</button> </button>
</div> </div>
</div> </div>
@@ -15,6 +15,7 @@ const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
setCaptchaResponse, setCaptchaResponse,
setChallengesArray, setChallengesArray,
setIsCaptchaOpen, setIsCaptchaOpen,
setIsModEdit,
moderatingCommentCid, moderatingCommentCid,
setResolveCaptchaPromise, setResolveCaptchaPromise,
} = useGeneralStore(state => state); } = useGeneralStore(state => state);
@@ -221,6 +222,7 @@ const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
reason: reason reason: reason
})); }));
setTriggerPublishCommentEdit(true); setTriggerPublishCommentEdit(true);
setIsModEdit(true);
handleCloseModal(); handleCloseModal();
}} }}
> >
+8 -3
View File
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { usePublishComment } from '@plebbit/plebbit-react-hooks'; import { useAccount, usePublishComment } from '@plebbit/plebbit-react-hooks';
import { StyledModal } from '../styled/modals/ReplyModal.styled'; import { StyledModal } from '../styled/modals/ReplyModal.styled';
import useGeneralStore from '../../hooks/stores/useGeneralStore'; import useGeneralStore from '../../hooks/stores/useGeneralStore';
import Modal from 'react-modal'; import Modal from 'react-modal';
@@ -21,8 +21,9 @@ const ReplyModal = ({ isOpen, closeModal }) => {
selectedStyle, selectedStyle,
} = useGeneralStore(state => state); } = useGeneralStore(state => state);
const account = useAccount();
const nodeRef = useRef(null); const nodeRef = useRef(null);
const nameRef = useRef(); const nameRef = useRef();
const commentRef = useRef(); const commentRef = useRef();
const linkRef = useRef(); const linkRef = useRef();
@@ -224,7 +225,11 @@ const ReplyModal = ({ isOpen, closeModal }) => {
</div> </div>
<div id="form"> <div id="form">
<div> <div>
<input id="name" type="text" placeholder="Name" ref={nameRef} /> {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>
<div> <div>
<input id="name" type="text" placeholder="Embed link" ref={linkRef} /> <input id="name" type="text" placeholder="Embed link" ref={linkRef} />
+49 -16
View File
@@ -11,7 +11,10 @@ const {version} = packageJson
const SettingsModal = ({ isOpen, closeModal }) => { const SettingsModal = ({ isOpen, closeModal }) => {
const selectedStyle = useGeneralStore(state => state.selectedStyle); const {
selectedStyle,
} = useGeneralStore(state => state);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const [expanded, setExpanded] = useState([]); const [expanded, setExpanded] = useState([]);
@@ -30,6 +33,7 @@ const SettingsModal = ({ isOpen, closeModal }) => {
const pubsubRef = useRef(); const pubsubRef = useRef();
const dataPathRef = useRef(); const dataPathRef = useRef();
const importRef = useRef(); const importRef = useRef();
const nameRef = useRef();
const isValidURL = (url) => { const isValidURL = (url) => {
try { try {
@@ -171,6 +175,23 @@ const SettingsModal = ({ isOpen, closeModal }) => {
const handleAccountChange = (e) => { const handleAccountChange = (e) => {
setActiveAccount(e.target.value); setActiveAccount(e.target.value);
}; };
const handleDisplayName = async () => {
const name = nameRef.current.value;
try {
await setAccount({
...account,
name: name || account.name,
author: {
...account.author,
displayName: name,
}});
setSuccessMessage("Account Name Saved");
} catch (error) {
setErrorMessage(error.message);
}
};
return ( return (
@@ -229,31 +250,27 @@ const SettingsModal = ({ isOpen, closeModal }) => {
</li> </li>
<ul className="settings-cat" style={{ display: expanded.includes(1) ? 'block' : 'none' }}> <ul className="settings-cat" style={{ display: expanded.includes(1) ? 'block' : 'none' }}>
<li className="settings-option disc"> <li className="settings-option disc">
Export or Import Your Data Account Data
</li> </li>
<div className="plebbit-options-buttons" <div className="plebbit-options-buttons">
style={{ display: expanded.includes(1) ? 'block' : 'none' }} <button className="save-button"
> onClick={handleExport}>Export</button>
<button className="save-button" <button className="reset-button"
onClick={handleExport}>Export</button> onClick={handleImport}
<button className="reset-button" >Import</button>
onClick={handleImport} </div>
>Import</button>
</div>
<li className="settings-tip"> <li className="settings-tip">
To export, click "Export", then save your account data displayed below in a safe place. To import, paste your account data into the box below, then click "Import". To export, click "Export", then save your account data displayed below in a safe place. To import, paste your account data into the box below, then click "Import".
</li> </li>
<div className="settings-input"> <div className="settings-input">
<textarea ref={importRef} value={accountJson} /> <textarea ref={importRef} value={accountJson} />
</div> </div>
<li className="settings-option disc"> <li className="settings-option disc" style={{marginTop: '15px'}}>
Current Account: {account?.name} Account Address: u/{account?.author.shortAddress}
</li> </li>
<li className="settings-tip"> <li className="settings-tip">
Select a different account to use in the dropdown below. Select a different account to use in the dropdown below.
</li> </li>
</ul>
<ul className="settings-cat" style={{ display: expanded.includes(1) ? 'block' : 'none' }}>
<li> <li>
<div className="settings-input"> <div className="settings-input">
<select className="settings-select" <select className="settings-select"
@@ -268,8 +285,24 @@ const SettingsModal = ({ isOpen, closeModal }) => {
</select> </select>
</div> </div>
</li> </li>
<li className="settings-option disc" style={{marginTop: '15px'}}>
Account Name
</li>
<li className="settings-tip">
Change both your account name (default "Account 1") and display name (default "Anonymous"). This will not change your address.
</li>
<li>
<div className="settings-input">
<input className="settings-input" style={{marginLeft: '20px'}}
type="text" ref={nameRef} defaultValue={account?.author.displayName}
placeholder="Anonymous"
/>
<button className="save-button" id="save-name"
onClick={handleDisplayName}>Save</button>
</div>
</li>
</ul>
</ul> </ul>
</ul>
<ul> <ul>
<li className="settings-cat-lbl"> <li className="settings-cat-lbl">
<span className={`${expanded.includes(2) ? 'minus' : 'plus'}`} <span className={`${expanded.includes(2) ? 'minus' : 'plus'}`}
@@ -54,4 +54,45 @@ export const GlobalStyle = createGlobalStyle`
max-height: 100% !important; max-height: 100% !important;
} }
} }
.post-menu-catalog {
position: absolute;
font-size: 12px;
line-height: 1.3em;
list-style: none;
ul {
padding: 0;
margin: 0;
white-space: nowrap;
}
li {
cursor: pointer;
position: relative;
padding: 2px 4px;
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-menu {
display: none;
position: absolute;
left: 100%;
top: 0;
list-style: none;
}
.dropdown:hover .dropdown-menu {
display: block;
}
a {
text-decoration: none;
color: inherit;
}
}
`; `;
@@ -84,7 +84,7 @@ export const StyledModal = styled(Modal)`
#nav { #nav {
float: right; float: right;
margin: 0; margin: 0;
width: 40px; width: auto;
} }
${({ selectedStyle }) => { ${({ selectedStyle }) => {
@@ -107,6 +107,13 @@ export const StyledModal = styled(Modal)`
} }
} }
#save-name {
display: inline-block;
margin-left: 10px;
height: 23px;
position: absolute;
}
.cache-button { .cache-button {
margin: auto; margin: auto;
margin-bottom: 10px; margin-bottom: 10px;
@@ -148,6 +155,12 @@ export const StyledModal = styled(Modal)`
.settings-input { .settings-input {
position: relative; position: relative;
padding-left: 23px; padding-left: 23px;
input {
padding-left: 5px;
left: 4px;
font-family: monospace;
}
} }
.settings-input::before { .settings-input::before {
+57 -72
View File
@@ -54,8 +54,10 @@ export const NavBar = styled.div`
transition: top 0.3s ease-in-out; transition: top 0.3s ease-in-out;
} }
.board-select { .nav-container {
float: left; display: flex;
align-items: center;
justify-content: space-between;
} }
strong { strong {
@@ -77,7 +79,6 @@ export const NavBar = styled.div`
text-decoration: none; text-decoration: none;
padding-right: 5px; padding-right: 5px;
} }
} }
${({ selectedStyle }) => { ${({ selectedStyle }) => {
@@ -86,14 +87,14 @@ export const NavBar = styled.div`
return `font-size: 9pt; return `font-size: 9pt;
color: #b86; color: #b86;
a, button { a, #button-span {
font-weight: 400 !important; font-weight: 400 !important;
padding: 1px !important; padding: 1px !important;
text-decoration: none !important; text-decoration: none !important;
color: maroon !important; color: maroon !important;
} }
a:hover, button:hover { a:hover, #button-span:hover {
color: red !important; color: red !important;
} }
@@ -114,14 +115,14 @@ export const NavBar = styled.div`
return `font-size: 9pt; return `font-size: 9pt;
color: #89a; color: #89a;
a, button { a, #button-span {
font-weight: 400 !important; font-weight: 400 !important;
padding: 1px !important; padding: 1px !important;
text-decoration: none !important; text-decoration: none !important;
color: #34345c !important; color: #34345c !important;
} }
a:hover, button:hover { a:hover, #button-span:hover {
color: #d00 !important; color: #d00 !important;
} }
@@ -132,14 +133,14 @@ export const NavBar = styled.div`
case 'Futaba': case 'Futaba':
return `font-size: 11pt; return `font-size: 11pt;
a, a:visited, button { a, a:visited, #button-span {
font-weight: 400 !important; font-weight: 400 !important;
padding: 1px !important; padding: 1px !important;
color: #00e !important; color: #00e !important;
text-decoration: underline !important; text-decoration: underline !important;
} }
a:hover, button:hover { a:hover, #button-span:hover {
color: red !important; color: red !important;
} }
@@ -157,14 +158,14 @@ export const NavBar = styled.div`
case 'Burichan': case 'Burichan':
return `font-size: 11pt; return `font-size: 11pt;
a, a:visited, button { a, a:visited, #button-span {
font-weight: 400 !important; font-weight: 400 !important;
padding: 1px !important; padding: 1px !important;
color: #34345c !important; color: #34345c !important;
text-decoration: underline !important; text-decoration: underline !important;
} }
a:hover, button:hover { a:hover, #button-span:hover {
color: #d00 !important; color: #d00 !important;
} }
@@ -179,14 +180,14 @@ export const NavBar = styled.div`
return `font-size: 9pt; return `font-size: 9pt;
color: #c5c8c6; color: #c5c8c6;
a, a:visited, button { a, a:visited, #button-span {
font-weight: 400 !important; font-weight: 400 !important;
padding: 1px !important; padding: 1px !important;
text-decoration: none !important; text-decoration: none !important;
color: #81a2be !important; color: #81a2be !important;
} }
a:hover, button:hover { a:hover, #button-span:hover {
color: #5f89ab !important; color: #5f89ab !important;
} }
@@ -205,14 +206,14 @@ export const NavBar = styled.div`
return `font-size: 9pt; return `font-size: 9pt;
color: #333; color: #333;
a, a:visited, button { a, a:visited, #button-span {
font-weight: 400 !important; font-weight: 400 !important;
padding: 1px !important; padding: 1px !important;
text-decoration: none !important; text-decoration: none !important;
color: #f60 !important; color: #f60 !important;
} }
a:hover, button:hover { a:hover, #button-span:hover {
color: #ff3300 !important; color: #ff3300 !important;
} }
@@ -511,7 +512,7 @@ export const PostFormLink = styled.div`
cursor: pointer; cursor: pointer;
} }
.btn-wrap a { .btn-wrap a, .btn-wrap span {
text-decoration: none; text-decoration: none;
border: none; border: none;
color: inherit !important; color: inherit !important;
@@ -532,7 +533,7 @@ export const PostFormLink = styled.div`
cursor: pointer; cursor: pointer;
} }
.btn-wrap a { .btn-wrap a, .btn-wrap span {
text-decoration: none; text-decoration: none;
border: none; border: none;
color: #34345c !important; color: #34345c !important;
@@ -553,7 +554,7 @@ export const PostFormLink = styled.div`
font-size: 16px; font-size: 16px;
} }
.btn-wrap a { .btn-wrap a, .btn-wrap span {
text-decoration: none !important; text-decoration: none !important;
border: none; border: none;
color: inherit !important; color: inherit !important;
@@ -574,7 +575,7 @@ export const PostFormLink = styled.div`
font-size: 16px; font-size: 16px;
} }
.btn-wrap a { .btn-wrap a, .btn-wrap span {
text-decoration: none !important; text-decoration: none !important;
border: none; border: none;
color: inherit !important; color: inherit !important;
@@ -595,7 +596,7 @@ export const PostFormLink = styled.div`
cursor: pointer; cursor: pointer;
} }
.btn-wrap a { .btn-wrap a, .btn-wrap span {
text-decoration: none !important; text-decoration: none !important;
border: none; border: none;
color: #707070 !important; color: #707070 !important;
@@ -614,7 +615,7 @@ export const PostFormLink = styled.div`
cursor: pointer; cursor: pointer;
} }
.btn-wrap a { .btn-wrap a, .btn-wrap span {
text-decoration: none; text-decoration: none;
border: none; border: none;
color: #333 !important; color: #333 !important;
@@ -1472,7 +1473,7 @@ export const TopBar = styled.div`
.subscribe-button-mobile { .subscribe-button-mobile {
margin-right: 10px; margin-right: 10px;
margin-top: -2px; margin-top: 1px;
position: absolute; position: absolute;
right: 0; right: 0;
font-size: 13.3333px !important; font-size: 13.3333px !important;
@@ -1499,7 +1500,7 @@ export const TopBar = styled.div`
font-size: 10pt !important; font-size: 10pt !important;
} }
.btn-wrap a { .btn-wrap a, .btn-wrap span {
text-decoration: none; text-decoration: none;
border: none; border: none;
color: inherit !important; color: inherit !important;
@@ -1520,7 +1521,7 @@ export const TopBar = styled.div`
cursor: pointer; cursor: pointer;
} }
.btn-wrap a { .btn-wrap a, .btn-wrap span {
text-decoration: none; text-decoration: none;
border: none; border: none;
color: #34345c !important; color: #34345c !important;
@@ -1542,11 +1543,9 @@ export const TopBar = styled.div`
font-family: times new roman !important; font-family: times new roman !important;
} }
.subscribe-button-mobile {
margin-top: -5px !important;
}
.btn-wrap a { .btn-wrap a, .btn-wrap span {
text-decoration: none !important; text-decoration: none !important;
border: none; border: none;
color: inherit !important; color: inherit !important;
@@ -1569,10 +1568,10 @@ export const TopBar = styled.div`
} }
.subscribe-button-mobile { .subscribe-button-mobile {
margin-top: -5px !important; margin-top: 0px !important;
} }
.btn-wrap a { .btn-wrap a, .btn-wrap span {
text-decoration: none !important; text-decoration: none !important;
border: none; border: none;
color: inherit !important; color: inherit !important;
@@ -1593,7 +1592,7 @@ export const TopBar = styled.div`
cursor: pointer; cursor: pointer;
} }
.btn-wrap a { .btn-wrap a, .btn-wrap span {
text-decoration: none; text-decoration: none;
border: none; border: none;
color: #707070 !important; color: #707070 !important;
@@ -1618,7 +1617,7 @@ export const TopBar = styled.div`
cursor: pointer; cursor: pointer;
} }
.btn-wrap a { .btn-wrap a, .btn-wrap span {
text-decoration: none; text-decoration: none;
border: none; border: none;
color: #333 !important; color: #333 !important;
@@ -1721,15 +1720,13 @@ export const BoardForm = styled.div`
} }
#post-menu { #post-menu {
ul { border-right: 1px solid #d9bfb7;
background-color: #f0e0d6; border-bottom: 2px solid #d9bfb7;
border: 1px solid #d9bfb7;
border-bottom: 1px solid #d9bfb7;
border-right-width: 2px;
}
li { li {
border-bottom: 1px solid #ccc; border: 1px solid #d9bfb7;
border-bottom: none;
background-color: #f0e0d6;
:hover { :hover {
background-color: #ffe; background-color: #ffe;
@@ -1746,14 +1743,13 @@ export const BoardForm = styled.div`
} }
#post-menu { #post-menu {
ul { border-right: 1px solid #b7c5d9;
background-color: #d6daf0; border-bottom: 2px solid #b7c5d9;
border: 1px solid #b7c5d9;
border-right-width: 2px;
}
li { li {
border-bottom: 1px solid #b7c5d9; border: 1px solid #b7c5d9;
border-bottom: none;
background-color: #d6daf0;
:hover { :hover {
background-color: #eef2ff; background-color: #eef2ff;
@@ -1768,14 +1764,11 @@ export const BoardForm = styled.div`
#post-menu { #post-menu {
font-size: 13px !important; font-size: 13px !important;
border: 1px solid #d9bfb7;
ul { border-bottom: none;
background-color: #f0e0d6;
border: 1px solid #d9bfb7;
border-bottom: none;
}
li { li {
background-color: #f0e0d6;
border-bottom: 1px solid #d9bfb7; border-bottom: 1px solid #d9bfb7;
:hover { :hover {
@@ -1791,14 +1784,11 @@ export const BoardForm = styled.div`
#post-menu { #post-menu {
font-size: 13px !important; font-size: 13px !important;
border: 1px solid #b7c5d9;
ul { border-bottom: none;
background-color: #d6daf0;
border: 1px solid #b7c5d9;
border-bottom: none;
}
li { li {
background-color: #d6daf0;
border-bottom: 1px solid #b7c5d9; border-bottom: 1px solid #b7c5d9;
:hover { :hover {
@@ -1814,21 +1804,18 @@ export const BoardForm = styled.div`
} }
#post-menu { #post-menu {
ul { border: 1px solid #000;
background-color: #282a2e; border-bottom: none;
border: 1px solid #000;
border-bottom: none;
}
li { li {
background-color: #282a2e;
border-bottom: 1px solid #000; border-bottom: 1px solid #000;
:hover { :hover {
background-color: #1d1f21; background-color: #1d1f21;
} }
} }
} }`;
`;
case 'Photon': case 'Photon':
return `.highlighted { return `.highlighted {
@@ -1837,13 +1824,11 @@ export const BoardForm = styled.div`
} }
#post-menu { #post-menu {
ul { border: 1px solid #ccc;
background-color: #ddd; border-bottom: none;
border: 1px solid #ccc;
border-bottom: none;
}
li { li {
background-color: #ddd;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
:hover { :hover {
@@ -77,4 +77,171 @@ export const Threads = styled.div`
display: block; display: block;
padding: 0 15px; padding: 0 15px;
} }
`;
export const PostMenuCatalog = styled.div`
.post-menu-catalog {
position: relative;
font-size: 12px;
padding: 0;
li {
cursor: pointer;
position: relative;
padding: 2px 4px;
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-menu {
display: none;
position: absolute;
}
.dropdown:hover .dropdown-menu {
display: block;
}
a {
text-decoration: none;
color: inherit;
}
}
${({ selectedStyle }) => {
switch (selectedStyle) {
case 'Yotsuba':
return `.highlighted {
background-color: #f0c0b0 !important;
border: 1px solid #d99f91 !important;
border-left: none !important;
border-top: none !important;
}
.post-menu-catalog {
border-right: 1px solid #d9bfb7;
border-bottom: 2px solid #d9bfb7;
li {
border: 1px solid #d9bfb7;
border-bottom: none;
background-color: #f0e0d6;
:hover {
background-color: #ffe;
}
}
}`;
case 'Yotsuba-B':
return `.highlighted {
background-color: #d6bad0 !important;
border: 1px solid #ba9dbf !important;
border-left: none !important;
border-top: none !important;
}
.post-menu-catalog {
border-right: 1px solid #b7c5d9;
border-bottom: 2px solid #b7c5d9;
li {
border: 1px solid #b7c5d9;
border-bottom: none;
background-color: #d6daf0;
:hover {
background-color: #eef2ff;
}
}
}`;
case 'Futaba':
return `.highlighted {
background-color: #f0c0b0 !important;
}
.post-menu-catalog {
font-size: 13px !important;
border: 1px solid #d9bfb7;
border-bottom: none;
li {
background-color: #f0e0d6;
border-bottom: 1px solid #d9bfb7;
:hover {
background-color: #ffe;
}
}
}`;
case 'Burichan':
return `.highlighted {
background-color: #d6bad0 !important;
}
.post-menu-catalog {
font-size: 13px !important;
border: 1px solid #b7c5d9;
border-bottom: none;
li {
background-color: #d6daf0;
border-bottom: 1px solid #b7c5d9;
:hover {
background-color: #eef2ff;
}
}
}`;
case 'Tomorrow':
return `.highlighted {
background-color: #1d1d21 !important;
border: 1px solid #111 !important;
}
.post-menu-catalog {
border: 1px solid #000;
border-bottom: none;
li {
background-color: #282a2e;
border-bottom: 1px solid #000;
:hover {
background-color: #1d1f21;
}
}
}`;
case 'Photon':
return `.highlighted {
background-color: #ccc !important;
border: 1px solid #ccc !important;
}
.post-menu-catalog {
border: 1px solid #ccc;
border-bottom: none;
li {
background-color: #ddd;
border-bottom: 1px solid #ccc;
:hover {
background-color: #eee;
}
}
}`;
default:
return '';
}
}}
`; `;
+25 -25
View File
@@ -221,43 +221,43 @@ const All = () => {
</span> </span>
<span className="nav"> <span className="nav">
[ [
<button style={{all: 'unset', cursor: 'pointer'}} onClick={ <span id="button-span" style={{cursor: 'pointer'}} onClick={
() => alert( () => alert(
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n' 'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
) )
}>Create Board</button> }>Create Board</span>
] ]
[ [
<Link to={`/p/all/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link> <Link to={`/p/all/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
] ]
[ [
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}} <Link to="/">Home</Link>
)}>Home</Link>
] ]
</span> </span>
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}> <div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
<div className="board-select"> <div className="nav-container">
<strong>Board</strong> <div className="board-select">
&nbsp; <strong>Board</strong>
<select id="board-select-mobile" value="all" onChange={handleSelectChange}> &nbsp;
<option value="all">All</option> <select id="board-select-mobile" value="all" onChange={handleSelectChange}>
<option value="subscriptions">Subscriptions</option> <option value="all">All</option>
{defaultSubplebbits.map(subplebbit => ( <option value="subscriptions">Subscriptions</option>
<option key={`option-${subplebbit.address}`} value={subplebbit.address} {defaultSubplebbits.map(subplebbit => (
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option> <option key={`option-${subplebbit.address}`} value={subplebbit.address}
))} >{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
</select>  ))}
<button style={{all: 'unset', cursor: 'pointer'}} onClick={ </select> 
<span style={{cursor: 'pointer'}} onClick={
() => alert( () => alert(
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n' 'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
) )
}>Create Board</button> }>Create Board</span>
</div> </div>
<div className="page-jump"> <div className="page-jump">
<Link to={`/p/all/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link> <Link to={`/p/all/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
&nbsp; &nbsp;
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}} <Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
)}>Home</Link> </div>
</div> </div>
</div> </div>
<div id="separator-mobile">&nbsp;</div> <div id="separator-mobile">&nbsp;</div>
@@ -485,7 +485,7 @@ const All = () => {
onMouseOver={() => {setIsImageSearchOpen(true)}} onMouseOver={() => {setIsImageSearchOpen(true)}}
onMouseLeave={() => {setIsImageSearchOpen(false)}}> onMouseLeave={() => {setIsImageSearchOpen(false)}}>
Image search » Image search »
<ul className="dropdown-menu" <ul className="dropdown-menu post-menu-catalog"
style={{display: isImageSearchOpen ? 'block': 'none'}}> style={{display: isImageSearchOpen ? 'block': 'none'}}>
<li> <li>
<a <a
@@ -673,7 +673,7 @@ const All = () => {
onMouseOver={() => {setIsImageSearchOpen(true)}} onMouseOver={() => {setIsImageSearchOpen(true)}}
onMouseLeave={() => {setIsImageSearchOpen(false)}}> onMouseLeave={() => {setIsImageSearchOpen(false)}}>
Image search » Image search »
<ul className="dropdown-menu" <ul className="dropdown-menu post-menu-catalog"
style={{display: isImageSearchOpen ? 'block': 'none'}}> style={{display: isImageSearchOpen ? 'block': 'none'}}>
<li> <li>
<a <a
+23 -23
View File
@@ -122,43 +122,43 @@ const AllCatalog = () => {
</span> </span>
<span className="nav"> <span className="nav">
[ [
<button style={{all: 'unset', cursor: 'pointer'}} onClick={ <span id="button-span" style={{cursor: 'pointer'}} onClick={
() => alert( () => alert(
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n' 'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
) )
}>Create Board</button> }>Create Board</span>
] ]
[ [
<Link to={`/p/all/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link> <Link to={`/p/all/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
] ]
[ [
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}} <Link to="/">Home</Link>
)}>Home</Link>
] ]
</span> </span>
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}> <div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
<div className="board-select"> <div className="nav-container">
<strong>Board</strong> <div className="board-select">
&nbsp; <strong>Board</strong>
<select id="board-select-mobile" value="all" onChange={handleSelectChange}> &nbsp;
<option value="All">All</option> <select id="board-select-mobile" value="all" onChange={handleSelectChange}>
<option value="subscriptions">Subscriptions</option> <option value="all">All</option>
{defaultSubplebbits.map(subplebbit => ( <option value="subscriptions">Subscriptions</option>
<option key={`option-${subplebbit.address}`} value={subplebbit.address} {defaultSubplebbits.map(subplebbit => (
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option> <option key={`option-${subplebbit.address}`} value={subplebbit.address}
))} >{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
</select>  ))}
<button style={{all: 'unset', cursor: 'pointer'}} onClick={ </select> 
<span style={{cursor: 'pointer'}} onClick={
() => alert( () => alert(
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n' 'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
) )
}>Create Board</button> }>Create Board</span>
</div> </div>
<div className="page-jump"> <div className="page-jump">
<Link to={`/p/all/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link> <Link to={`/p/all/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
&nbsp; &nbsp;
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}} <Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
)}>Home</Link> </div>
</div> </div>
</div> </div>
<div id="separator-mobile">&nbsp;</div> <div id="separator-mobile">&nbsp;</div>
+238 -185
View File
@@ -1,4 +1,5 @@
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Helmet } from 'react-helmet-async'; import { Helmet } from 'react-helmet-async';
import { Link, useNavigate, useParams } from 'react-router-dom'; import { Link, useNavigate, useParams } from 'react-router-dom';
import { confirmAlert } from 'react-confirm-alert'; import { confirmAlert } from 'react-confirm-alert';
@@ -10,6 +11,7 @@ import { debounce } from 'lodash';
import useGeneralStore from '../../hooks/stores/useGeneralStore'; import useGeneralStore from '../../hooks/stores/useGeneralStore';
import { Container, NavBar, Header, Break, PostFormLink, PostFormTable, PostForm, TopBar, BoardForm, PostMenu } from '../styled/views/Board.styled'; import { Container, NavBar, Header, Break, PostFormLink, PostFormTable, PostForm, TopBar, BoardForm, PostMenu } from '../styled/views/Board.styled';
import { Footer, AuthorDeleteAlert } from '../styled/views/Thread.styled'; import { Footer, AuthorDeleteAlert } from '../styled/views/Thread.styled';
import { PostMenuCatalog } from '../styled/views/Catalog.styled';
import EditModal from '../modals/EditModal'; import EditModal from '../modals/EditModal';
import ImageBanner from '../ImageBanner'; import ImageBanner from '../ImageBanner';
import ModerationModal from '../modals/ModerationModal'; import ModerationModal from '../modals/ModerationModal';
@@ -39,6 +41,8 @@ const Board = () => {
setChallengesArray, setChallengesArray,
defaultSubplebbits, defaultSubplebbits,
editedComment, editedComment,
setIsAuthorDelete,
setIsAuthorEdit,
setIsCaptchaOpen, setIsCaptchaOpen,
isModerationOpen, setIsModerationOpen, isModerationOpen, setIsModerationOpen,
isSettingsOpen, setIsSettingsOpen, isSettingsOpen, setIsSettingsOpen,
@@ -66,6 +70,8 @@ const Board = () => {
const linkRef = useRef(); const linkRef = useRef();
const threadMenuRefs = useRef({}); const threadMenuRefs = useRef({});
const replyMenuRefs = useRef({}); const replyMenuRefs = useRef({});
const postMenuRef = useRef(null);
const postMenuCatalogRef = useRef(null);
const { feed, hasMore, loadMore } = useFeed({subplebbitAddresses: [`${selectedAddress}`], sortType: 'new'}); const { feed, hasMore, loadMore } = useFeed({subplebbitAddresses: [`${selectedAddress}`], sortType: 'new'});
const subplebbit = useSubplebbit({subplebbitAddress: selectedAddress}); const subplebbit = useSubplebbit({subplebbitAddress: selectedAddress});
@@ -81,10 +87,11 @@ const Board = () => {
const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false); const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false);
const [selectedFeed, setSelectedFeed] = useState(feed); const [selectedFeed, setSelectedFeed] = useState(feed);
const [deletePost, setDeletePost] = useState(false); const [deletePost, setDeletePost] = useState(false);
const [rotatedStates, setRotatedStates] = useState({});
const [isImageSearchOpen, setIsImageSearchOpen] = useState(false); const [isImageSearchOpen, setIsImageSearchOpen] = useState(false);
const [isModerator, setIsModerator] = useState(false); const [isModerator, setIsModerator] = useState(false);
const [commentCid, setCommentCid] = useState(null); const [commentCid, setCommentCid] = useState(null);
const [menuPosition, setMenuPosition] = useState({top: 0, left: 0});
const [openMenuCid, setOpenMenuCid] = useState(null);
const [errorMessage, setErrorMessage] = useState(null); const [errorMessage, setErrorMessage] = useState(null);
const [successMessage, setSuccessMessage] = useState(null); const [successMessage, setSuccessMessage] = useState(null);
@@ -105,12 +112,29 @@ const Board = () => {
}, [account?.author.address, subplebbit.roles]); }, [account?.author.address, subplebbit.roles]);
const handleOptionClick = (threadCid) => { const handleOptionClick = () => {
setRotatedStates(prevState => ({ setOpenMenuCid(null);
...prevState,
[threadCid]: false
}));
}; };
const handleOutsideClick = useCallback((e) => {
if (openMenuCid !== null && !postMenuRef.current.contains(e.target) && !postMenuCatalogRef.current.contains(e.target)) {
setOpenMenuCid(null);
}
}, [openMenuCid, postMenuRef, postMenuCatalogRef]);
useEffect(() => {
if (openMenuCid !== null) {
document.addEventListener('click', handleOutsideClick);
} else {
document.removeEventListener('click', handleOutsideClick);
}
return () => {
document.removeEventListener('click', handleOutsideClick);
};
}, [openMenuCid, handleOutsideClick]);
const errorString = useMemo(() => { const errorString = useMemo(() => {
@@ -397,6 +421,8 @@ const Board = () => {
<button onClick={onClose}>No</button> <button onClick={onClose}>No</button>
<button <button
onClick={() => { onClick={() => {
setIsAuthorDelete(true);
setIsAuthorEdit(false);
setCommentCid(commentCid); setCommentCid(commentCid);
setPublishCommentEditOptions(prevOptions => ({ setPublishCommentEditOptions(prevOptions => ({
...prevOptions, ...prevOptions,
@@ -418,6 +444,8 @@ const Board = () => {
const handleAuthorEditClick = (comment) => { const handleAuthorEditClick = (comment) => {
handleOptionClick(comment.cid); handleOptionClick(comment.cid);
setIsAuthorEdit(true);
setIsAuthorDelete(false);
setCommentCid(comment.cid); setCommentCid(comment.cid);
setOriginalCommentContent(comment.content); setOriginalCommentContent(comment.content);
setIsEditModalOpen(true); setIsEditModalOpen(true);
@@ -437,7 +465,7 @@ const Board = () => {
if (editedComment !== '') { if (editedComment !== '') {
setTriggerPublishCommentEdit(true); setTriggerPublishCommentEdit(true);
} }
}, [editedComment]); }, [editedComment, setIsAuthorEdit]);
useEffect(() => { useEffect(() => {
@@ -536,42 +564,43 @@ const Board = () => {
</span> </span>
<span className="nav"> <span className="nav">
[ [
<button style={{all: 'unset', cursor: 'pointer'}} onClick={ <span id="button-span" style={{cursor: 'pointer'}} onClick={
() => alert( () => alert(
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n' 'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
) )
}>Create Board</button> }>Create Board</span>
] ]
[ [
<Link to={`/p/${selectedAddress}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link> <Link to={`/p/${selectedAddress}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
] ]
[ [
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}} <Link to="/">Home</Link>
)}>Home</Link>
] ]
</span> </span>
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}> <div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
<div className="board-select"> <div className="nav-container">
<strong>Board</strong> <div className="board-select">
&nbsp; <strong>Board</strong>
<select id="board-select-mobile" value={selectedAddress} onChange={handleSelectChange}> &nbsp;
<option value="all">All</option> <select id="board-select-mobile" value={selectedAddress} onChange={handleSelectChange}>
<option value="subscriptions">Subscriptions</option> <option value="all">All</option>
{defaultSubplebbits.map(subplebbit => ( <option value="subscriptions">Subscriptions</option>
<option key={`option-${subplebbit.address}`} value={subplebbit.address} {defaultSubplebbits.map(subplebbit => (
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option> <option key={`option-${subplebbit.address}`} value={subplebbit.address}
))} >{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
</select>  ))}
<button style={{all: 'unset', cursor: 'pointer'}} onClick={ </select> 
<span style={{cursor: 'pointer'}} onClick={
() => alert( () => alert(
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n' 'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
) )
}>Create Board</button> }>Create Board</span>
</div> </div>
<div className="page-jump"> <div className="page-jump">
<Link to={`/p/${selectedAddress}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link> <Link to={`/p/${selectedAddress}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
&nbsp; &nbsp;
<Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link> <Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
</div>
</div> </div>
</div> </div>
<div id="separator-mobile">&nbsp;</div> <div id="separator-mobile">&nbsp;</div>
@@ -611,7 +640,11 @@ const Board = () => {
<tr data-type="Name"> <tr data-type="Name">
<td id="td-name">Name</td> <td id="td-name">Name</td>
<td> <td>
<input name="name" type="text" tabIndex={1} placeholder="Anonymous" ref={nameRef} /> {account && account.author && account.author.displayName ? (
<input name="name" type="text" tabIndex={1} value={account.author?.displayName} ref={nameRef} disabled />
) : (
<input name="name" type="text" placeholder="Anonymous" tabIndex={1} ref={nameRef} />
)}
</td> </td>
</tr> </tr>
<tr data-type="Subject"> <tr data-type="Subject">
@@ -663,14 +696,18 @@ const Board = () => {
<> <>
<span className="subscribe-button-desktop"> <span className="subscribe-button-desktop">
[ [
<button id="subscribe" style={{all: 'unset', cursor: 'pointer'}} <span id="subscribe" style={{cursor: 'pointer'}}>
onClick={() => handleSubscribe()}>{subscribed ? "Unsubscribe" : "Subscribe"} <span onClick={() => handleSubscribe()}>
</button> {subscribed ? "Unsubscribe" : "Subscribe"}
</span>
</span>
] ]
</span> </span>
<button className="subscribe-button-mobile btn-wrap" <span className="subscribe-button-mobile">
onClick={() => handleSubscribe()}>{subscribed ? "Unsubscribe" : "Subscribe"} <span className="btn-wrap" onClick={() => handleSubscribe()}>
</button> {subscribed ? "Unsubscribe" : "Subscribe"}
</span>
</span>
</> </>
) : ( ) : (
<div id="stats" style={{float: "right", marginTop: "5px"}}> <div id="stats" style={{float: "right", marginTop: "5px"}}>
@@ -829,91 +866,99 @@ const Board = () => {
<PostMenu <PostMenu
key={`pmb-${index}`} key={`pmb-${index}`}
title="Post menu" title="Post menu"
ref={el => threadMenuRefs.current[thread.cid] = el} ref={el => {
threadMenuRefs.current[thread.cid] = el;
postMenuRef.current = el;
}}
className='post-menu-button' className='post-menu-button'
rotated={rotatedStates[thread.cid]} rotated={openMenuCid === thread.cid}
onClick={() => { onClick={(event) => {
event.stopPropagation();
const rect = threadMenuRefs.current[thread.cid].getBoundingClientRect(); const rect = threadMenuRefs.current[thread.cid].getBoundingClientRect();
const menu = document.querySelector(`.post-menu-thread-${thread.cid}`); setMenuPosition({top: rect.top + window.scrollY, left: rect.left});
menu.style.top = `calc(${rect.top}px + 17px)`; setOpenMenuCid(prevCid => (prevCid === thread.cid ? null : thread.cid));
menu.style.left = `${rect.left}px`;
setRotatedStates(prevState => ({
...prevState,
[thread.cid]: !prevState[thread.cid]
}));
}} }}
> >
</PostMenu> </PostMenu>
<div id="post-menu" className={`post-menu-thread post-menu-thread-${thread.cid}`} {createPortal(
style={{ display: rotatedStates[thread.cid] ? 'block' : 'none' }}> <PostMenuCatalog selectedStyle={selectedStyle}
<ul> ref={el => {postMenuCatalogRef.current = el}}
<li onClick={() => handleOptionClick(thread.cid)}>Hide thread</li> onClick={(event) => event.stopPropagation()}
{thread.author.shortAddress === account?.author.shortAddress ? ( style={{position: "absolute",
<> top: menuPosition.top + 7,
<li onClick={() => handleAuthorEditClick(thread)}>Edit post</li> left: menuPosition.left}}>
<li onClick={() => handleAuthorDeleteClick(thread.cid)}>Delete post</li> <div className={`post-menu-thread post-menu-thread-${thread.cid}`}
</> style={{ display: openMenuCid === thread.cid ? 'block' : 'none' }}
) : null} >
{isModerator ? ( <ul className="post-menu-catalog">
<> <li onClick={() => handleOptionClick(thread.cid)}>Hide thread</li>
{thread.author.shortAddress === account?.author.shortAddress ? ( {thread.author.shortAddress === account?.author.shortAddress ? (
null <>
) : ( <li onClick={() => handleAuthorEditClick(thread)}>Edit post</li>
<li onClick={() => { <li onClick={() => handleAuthorDeleteClick(thread.cid)}>Delete post</li>
</>
) : null}
{isModerator ? (
<>
{thread.author.shortAddress === account?.author.shortAddress ? (
null
) : (
<li onClick={() => {
setModeratingCommentCid(thread.cid)
setIsModerationOpen(true);
handleOptionClick(thread.cid);
setDeletePost(true);
}}>
Delete post
</li>
)}
<li
onClick={() => {
setModeratingCommentCid(thread.cid) setModeratingCommentCid(thread.cid)
setIsModerationOpen(true); setIsModerationOpen(true);
handleOptionClick(thread.cid); handleOptionClick(thread.cid);
setDeletePost(true);
}}> }}>
Delete post Mod tools
</li> </li>
)} </>
<li ) : null}
onClick={() => { {(commentMediaInfo && (
setModeratingCommentCid(thread.cid) commentMediaInfo.type === 'image' ||
setIsModerationOpen(true); (commentMediaInfo.type === 'webpage' &&
handleOptionClick(thread.cid); commentMediaInfo.thumbnail))) ? (
}}> <li
Mod tools onMouseOver={() => {setIsImageSearchOpen(true)}}
</li> onMouseLeave={() => {setIsImageSearchOpen(false)}}>
</> Image search »
) : null} <ul className="dropdown-menu post-menu-catalog"
{(commentMediaInfo && ( style={{display: isImageSearchOpen ? 'block': 'none'}}>
commentMediaInfo.type === 'image' || <li onClick={() => handleOptionClick(thread.cid)}>
(commentMediaInfo.type === 'webpage' && <a
commentMediaInfo.thumbnail))) ? ( href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`}
<li target="_blank" rel="noreferrer"
onMouseOver={() => {setIsImageSearchOpen(true)}} >Google</a>
onMouseLeave={() => {setIsImageSearchOpen(false)}}> </li>
Image search » <li onClick={() => handleOptionClick(thread.cid)}>
<ul className="dropdown-menu" <a
style={{display: isImageSearchOpen ? 'block': 'none'}}> href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`}
<li onClick={() => handleOptionClick(thread.cid)}> target="_blank" rel="noreferrer"
<a >Yandex</a>
href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`} </li>
target="_blank" rel="noreferrer" <li onClick={() => handleOptionClick(thread.cid)}>
>Google</a> <a
</li> href={`https://saucenao.com/search.php?url=${commentMediaInfo.url}`}
<li onClick={() => handleOptionClick(thread.cid)}> target="_blank" rel="noreferrer"
<a >SauceNAO</a>
href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`} </li>
target="_blank" rel="noreferrer" </ul>
>Yandex</a> </li>
</li> ) : null
<li onClick={() => handleOptionClick(thread.cid)}> }
<a </ul>
href={`https://saucenao.com/search.php?url=${commentMediaInfo.url}`} </div>
target="_blank" rel="noreferrer" </PostMenuCatalog>, document.body
>SauceNAO</a> )}
</li>
</ul>
</li>
) : null
}
</ul>
</div>
<div key={`bi-${index}`} id="backlink-id" className="backlink"> <div key={`bi-${index}`} id="backlink-id" className="backlink">
{thread.replies?.pages?.topAll.comments {thread.replies?.pages?.topAll.comments
.sort((a, b) => a.timestamp - b.timestamp) .sort((a, b) => a.timestamp - b.timestamp)
@@ -1027,91 +1072,99 @@ const Board = () => {
<PostMenu <PostMenu
key={`pmb-${index}`} key={`pmb-${index}`}
title="Post menu" title="Post menu"
ref={el => replyMenuRefs.current[reply.cid] = el} ref={el => {
replyMenuRefs.current[reply.cid] = el;
postMenuRef.current = el;
}}
className='post-menu-button' className='post-menu-button'
rotated={rotatedStates[reply.cid]} rotated={openMenuCid === reply.cid}
onClick={() => { onClick={(event) => {
event.stopPropagation();
const rect = replyMenuRefs.current[reply.cid].getBoundingClientRect(); const rect = replyMenuRefs.current[reply.cid].getBoundingClientRect();
const menu = document.querySelector(`.post-menu-reply-${reply.cid}`); setMenuPosition({top: rect.top + window.scrollY, left: rect.left});
menu.style.top = `calc(${rect.top + window.scrollY}px - 258px)`; setOpenMenuCid(prevCid => (prevCid === reply.cid ? null : reply.cid));
menu.style.left = `${rect.left}px`; }}
setRotatedStates(prevState => ({
...prevState,
[reply.cid]: !prevState[reply.cid]
}));
}}
> >
</PostMenu> </PostMenu>
<div id="post-menu" className={`post-menu-reply post-menu-reply-${reply.cid}`} {createPortal(
style={{ display: rotatedStates[reply.cid] ? 'block' : 'none' }}> <PostMenuCatalog selectedStyle={selectedStyle}
<ul> ref={el => {postMenuCatalogRef.current = el}}
<li onClick={() => handleOptionClick(reply.cid)}>Hide post</li> onClick={(event) => event.stopPropagation()}
{reply.author.shortAddress === account?.author.shortAddress ? ( style={{position: "absolute",
<> top: menuPosition.top + 7,
<li onClick={() => handleAuthorEditClick(reply)}>Edit post</li> left: menuPosition.left}}>
<li onClick={() => handleAuthorDeleteClick(reply.cid)}>Delete post</li> <div className={`post-menu-reply post-menu-reply-${reply.cid}`}
</> style={{ display: openMenuCid === reply.cid ? 'block' : 'none' }}
) : null} >
{isModerator ? ( <ul className="post-menu-catalog">
<> <li onClick={() => handleOptionClick(reply.cid)}>Hide post</li>
{reply.author.shortAddress === account?.author.shortAddress ? ( {reply.author.shortAddress === account?.author.shortAddress ? (
null <>
) : ( <li onClick={() => handleAuthorEditClick(reply)}>Edit post</li>
<li onClick={() => { <li onClick={() => handleAuthorDeleteClick(reply.cid)}>Delete post</li>
</>
) : null}
{isModerator ? (
<>
{reply.author.shortAddress === account?.author.shortAddress ? (
null
) : (
<li onClick={() => {
setModeratingCommentCid(reply.cid)
setIsModerationOpen(true);
handleOptionClick(reply.cid);
setDeletePost(true);
}}>
Delete post
</li>
)}
<li
onClick={() => {
setModeratingCommentCid(reply.cid) setModeratingCommentCid(reply.cid)
setIsModerationOpen(true); setIsModerationOpen(true);
handleOptionClick(reply.cid); handleOptionClick(reply.cid);
setDeletePost(true);
}}> }}>
Delete post Mod tools
</li> </li>
)} </>
<li ) : null}
onClick={() => { {(replyMediaInfo && (
setModeratingCommentCid(reply.cid) replyMediaInfo.type === 'image' ||
setIsModerationOpen(true); (replyMediaInfo.type === 'webpage' &&
handleOptionClick(reply.cid); replyMediaInfo.thumbnail))) ? (
}}> <li
Mod tools onMouseOver={() => {setIsImageSearchOpen(true)}}
</li> onMouseLeave={() => {setIsImageSearchOpen(false)}}>
</> Image search »
) : null} <ul className="dropdown-menu post-menu-catalog"
{(replyMediaInfo && ( style={{display: isImageSearchOpen ? 'block': 'none'}}>
replyMediaInfo.type === 'image' || <li onClick={() => handleOptionClick(reply.cid)}>
(replyMediaInfo.type === 'webpage' && <a
replyMediaInfo.thumbnail))) ? ( href={`https://lens.google.com/uploadbyurl?url=${replyMediaInfo.url}`}
<li target="_blank" rel="noreferrer"
onMouseOver={() => {setIsImageSearchOpen(true)}} >Google</a>
onMouseLeave={() => {setIsImageSearchOpen(false)}}> </li>
Image search » <li onClick={() => handleOptionClick(reply.cid)}>
<ul className="dropdown-menu" <a
style={{display: isImageSearchOpen ? 'block': 'none'}}> href={`https://yandex.com/images/search?url=${replyMediaInfo.url}`}
<li onClick={() => handleOptionClick(reply.cid)}> target="_blank" rel="noreferrer"
<a >Yandex</a>
href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`} </li>
target="_blank" rel="noreferrer" <li onClick={() => handleOptionClick(reply.cid)}>
>Google</a> <a
</li> href={`https://saucenao.com/search.php?url=${replyMediaInfo.url}`}
<li onClick={() => handleOptionClick(reply.cid)}> target="_blank" rel="noreferrer"
<a >SauceNAO</a>
href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`} </li>
target="_blank" rel="noreferrer" </ul>
>Yandex</a> </li>
</li> ) : null
<li onClick={() => handleOptionClick(reply.cid)}> }
<a </ul>
href={`https://saucenao.com/search.php?url=${commentMediaInfo.url}`} </div>
target="_blank" rel="noreferrer" </PostMenuCatalog>, document.body
>SauceNAO</a> )}
</li>
</ul>
</li>
) : null
}
</ul>
</div>
<div id="backlink-id" className="backlink"> <div id="backlink-id" className="backlink">
{reply.replies?.pages?.topAll.comments {reply.replies?.pages?.topAll.comments
.sort((a, b) => a.timestamp - b.timestamp) .sort((a, b) => a.timestamp - b.timestamp)
+360 -79
View File
@@ -1,16 +1,20 @@
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Helmet } from 'react-helmet-async'; import { Helmet } from 'react-helmet-async';
import InfiniteScroll from 'react-infinite-scroller'; import InfiniteScroll from 'react-infinite-scroller';
import { Link, useNavigate, useParams } from 'react-router-dom'; import { Link, useNavigate, useParams } from 'react-router-dom';
import { confirmAlert } from 'react-confirm-alert';
import { Tooltip } from 'react-tooltip'; import { Tooltip } from 'react-tooltip';
import { useFeed, usePublishComment, useSubplebbit, useSubscribe } from '@plebbit/plebbit-react-hooks'; import { useAccount, useFeed, usePublishComment, usePublishCommentEdit, useSubplebbit, useSubscribe } from '@plebbit/plebbit-react-hooks';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import useGeneralStore from '../../hooks/stores/useGeneralStore'; import useGeneralStore from '../../hooks/stores/useGeneralStore';
import { Container, NavBar, Header, Break, PostForm, PostFormLink, PostFormTable } from '../styled/views/Board.styled'; import { Container, NavBar, Header, Break, PostForm, PostFormLink, PostFormTable, PostMenu, BoardForm } from '../styled/views/Board.styled';
import { Threads } from '../styled/views/Catalog.styled'; import { Threads, PostMenuCatalog } from '../styled/views/Catalog.styled';
import { TopBar, Footer } from '../styled/views/Thread.styled'; import { TopBar, Footer, AuthorDeleteAlert } from '../styled/views/Thread.styled';
import CatalogLoader from '../CatalogLoader'; import CatalogLoader from '../CatalogLoader';
import EditModal from '../modals/EditModal';
import ImageBanner from '../ImageBanner'; import ImageBanner from '../ImageBanner';
import ModerationModal from '../modals/ModerationModal';
import OfflineIndicator from '../OfflineIndicator'; import OfflineIndicator from '../OfflineIndicator';
import SettingsModal from '../modals/SettingsModal'; import SettingsModal from '../modals/SettingsModal';
import getCommentMediaInfo from '../../utils/getCommentMediaInfo'; import getCommentMediaInfo from '../../utils/getCommentMediaInfo';
@@ -28,8 +32,13 @@ const Catalog = () => {
captchaResponse, setCaptchaResponse, captchaResponse, setCaptchaResponse,
setChallengesArray, setChallengesArray,
defaultSubplebbits, defaultSubplebbits,
editedComment,
setIsAuthorDelete,
setIsAuthorEdit,
setIsCaptchaOpen, setIsCaptchaOpen,
isModerationOpen, setIsModerationOpen,
isSettingsOpen, setIsSettingsOpen, isSettingsOpen, setIsSettingsOpen,
setModeratingCommentCid,
setPendingComment, setPendingComment,
setPendingCommentIndex, setPendingCommentIndex,
setResolveCaptchaPromise, setResolveCaptchaPromise,
@@ -45,25 +54,77 @@ const Catalog = () => {
const subjectRef = useRef(); const subjectRef = useRef();
const commentRef = useRef(); const commentRef = useRef();
const linkRef = useRef(); const linkRef = useRef();
const threadMenuRefs = useRef({});
const postMenuRef = useRef(null);
const postMenuCatalogRef = useRef(null);
const navigate = useNavigate(); const navigate = useNavigate();
const [errorMessage, setErrorMessage] = useState(null); const [errorMessage, setErrorMessage] = useState(null);
const [successMessage] = useState(null); const [successMessage, setSuccessMessage] = useState(null);
useError(errorMessage, [errorMessage]); useError(errorMessage, [errorMessage]);
useSuccess(successMessage, [successMessage]); useSuccess(successMessage, [successMessage]);
const [triggerPublishComment, setTriggerPublishComment] = useState(false); const [triggerPublishComment, setTriggerPublishComment] = useState(false);
const [prevScrollPos, setPrevScrollPos] = useState(0); const [prevScrollPos, setPrevScrollPos] = useState(0);
const [visible, setVisible] = useState(true); const [visible, setVisible] = useState(true);
const [isHoveringOnThread, setIsHoveringOnThread] = useState(false);
const [isModerator, setIsModerator] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [originalCommentContent, setOriginalCommentContent] = useState(null);
const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false);
const [deletePost, setDeletePost] = useState(false);
const [isImageSearchOpen, setIsImageSearchOpen] = useState(false);
const [commentCid, setCommentCid] = useState(null);
const [menuPosition, setMenuPosition] = useState({top: 0, left: 0});
const [openMenuCid, setOpenMenuCid] = useState(null);
const { feed, hasMore, loadMore } = useFeed({subplebbitAddresses: [`${selectedAddress}`], sortType: 'new'}); const { feed, hasMore, loadMore } = useFeed({subplebbitAddresses: [`${selectedAddress}`], sortType: 'new'});
const { subplebbitAddress } = useParams(); const { subplebbitAddress } = useParams();
const subplebbit = useSubplebbit({subplebbitAddress: selectedAddress}); const subplebbit = useSubplebbit({subplebbitAddress: selectedAddress});
const account = useAccount();
const stateString = useStateString(subplebbit); const stateString = useStateString(subplebbit);
useEffect(() => {
if (subplebbit.roles !== undefined) {
const role = subplebbit.roles[account?.author.address]?.role;
if (role === 'moderator' || role === 'admin' || role === 'owner') {
setIsModerator(true);
} else {
setIsModerator(false);
}
}
}, [account?.author.address, subplebbit.roles]);
const handleOptionClick = () => {
setOpenMenuCid(null);
};
const handleOutsideClick = useCallback((e) => {
if (openMenuCid !== null && !postMenuRef.current.contains(e.target) && !postMenuCatalogRef.current.contains(e.target)) {
setOpenMenuCid(null);
}
}, [openMenuCid, postMenuRef, postMenuCatalogRef]);
useEffect(() => {
if (openMenuCid !== null) {
document.addEventListener('click', handleOutsideClick);
} else {
document.removeEventListener('click', handleOutsideClick);
}
return () => {
document.removeEventListener('click', handleOutsideClick);
};
}, [openMenuCid, handleOutsideClick]);
useEffect(() => { useEffect(() => {
setSelectedAddress(subplebbitAddress); setSelectedAddress(subplebbitAddress);
}, [subplebbitAddress, setSelectedAddress]); }, [subplebbitAddress, setSelectedAddress]);
@@ -85,6 +146,8 @@ const Catalog = () => {
setErrorMessage(errorString); setErrorMessage(errorString);
} }
}, [errorString]); }, [errorString]);
const { subscribed, subscribe, unsubscribe } = useSubscribe({subplebbitAddress: selectedAddress}); const { subscribed, subscribe, unsubscribe } = useSubscribe({subplebbitAddress: selectedAddress});
// temporary title from JSON, gets subplebbitAddress from URL // temporary title from JSON, gets subplebbitAddress from URL
@@ -119,8 +182,12 @@ const Catalog = () => {
const onChallengeVerification = (challengeVerification) => { const onChallengeVerification = (challengeVerification) => {
if (challengeVerification.challengeSuccess === true) { if (challengeVerification.challengeSuccess === true) {
navigate(`/p/${selectedAddress}/c/${challengeVerification.publication?.cid}`); if (challengeVerification.publication?.cid !== undefined) {
console.log('challenge success'); navigate(`/p/${subplebbitAddress}/c/${challengeVerification.publication?.cid}`);
console.log('challenge success');
} else {
setSuccessMessage('Challenge Success');
}
} }
else if (challengeVerification.challengeSuccess === false) { else if (challengeVerification.challengeSuccess === false) {
setErrorMessage('Challenge Failed', {reason: challengeVerification.reason, errors: challengeVerification.errors}); setErrorMessage('Challenge Failed', {reason: challengeVerification.reason, errors: challengeVerification.errors});
@@ -254,6 +321,98 @@ const Catalog = () => {
}); });
}; };
const [publishCommentEditOptions, setPublishCommentEditOptions] = useState({
commentCid: commentCid,
content: editedComment || undefined,
subplebbitAddress: selectedAddress || subplebbitAddress,
onChallenge,
onChallengeVerification,
onError: (error) => {
setErrorMessage(error);
},
});
const {error, publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions);
useEffect(() => {
if (error) {
setErrorMessage(error);
}
}, [error]);
const handleAuthorDeleteClick = (commentCid) => {
handleOptionClick(commentCid);
confirmAlert({
customUI: ({ onClose }) => {
return (
<AuthorDeleteAlert selectedStyle={selectedStyle}>
<div className='author-delete-alert'>
<p>Are you sure you want to delete this post?</p>
<div className="author-delete-buttons">
<button onClick={onClose}>No</button>
<button
onClick={() => {
setIsAuthorDelete(true);
setIsAuthorEdit(false);
setCommentCid(commentCid);
setPublishCommentEditOptions(prevOptions => ({
...prevOptions,
deleted: true,
}));
setTriggerPublishCommentEdit(true);
onClose();
}}
>
Yes
</button>
</div>
</div>
</AuthorDeleteAlert>
);
}
});
};
const handleAuthorEditClick = (comment) => {
handleOptionClick(comment.cid);
setIsAuthorEdit(true);
setIsAuthorDelete(false);
setCommentCid(comment.cid);
setOriginalCommentContent(comment.content);
setIsEditModalOpen(true);
}
useEffect(() => {
setPublishCommentEditOptions((prevOptions) => ({
...prevOptions,
commentCid: commentCid,
content: editedComment || undefined,
}));
}, [commentCid, editedComment]);
useEffect(() => {
if (editedComment !== '') {
setTriggerPublishCommentEdit(true);
}
}, [editedComment]);
useEffect(() => {
if (publishCommentEditOptions && triggerPublishCommentEdit) {
(async () => {
await publishCommentEdit();
setTriggerPublishCommentEdit(false);
})();
}
}, [publishCommentEditOptions, triggerPublishCommentEdit, publishCommentEdit]);
// mobile navbar board select functionality // mobile navbar board select functionality
const handleSelectChange = (event) => { const handleSelectChange = (event) => {
const selected = event.target.value; const selected = event.target.value;
@@ -295,6 +454,16 @@ const Catalog = () => {
selectedStyle={selectedStyle} selectedStyle={selectedStyle}
isOpen={isSettingsOpen} isOpen={isSettingsOpen}
closeModal={() => setIsSettingsOpen(false)} /> closeModal={() => setIsSettingsOpen(false)} />
<ModerationModal
selectedStyle={selectedStyle}
isOpen={isModerationOpen}
closeModal={() => setIsModerationOpen(false)}
deletePost={deletePost} />
<EditModal
selectedStyle={selectedStyle}
isOpen={isEditModalOpen}
closeModal={() => setIsEditModalOpen(false)}
originalCommentContent={originalCommentContent} />
<NavBar selectedStyle={selectedStyle}> <NavBar selectedStyle={selectedStyle}>
<> <>
<span className="boardList"> <span className="boardList">
@@ -318,43 +487,43 @@ const Catalog = () => {
</span> </span>
<span className="nav"> <span className="nav">
[ [
<button style={{all: 'unset', cursor: 'pointer'}} onClick={ <span id="button-span" style={{cursor: 'pointer'}} onClick={
() => alert( () => alert(
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n' 'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
) )
}>Create Board</button> }>Create Board</span>
] ]
[ [
<Link to={`/p/${selectedAddress}/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link> <Link to={`/p/${selectedAddress}/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
] ]
[ [
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}} <Link to="/">Home</Link>
)}>Home</Link>
] ]
</span> </span>
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}> <div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
<div className="board-select"> <div className="nav-container">
<strong>Board</strong> <div className="board-select">
&nbsp; <strong>Board</strong>
<select id="board-select-mobile" value={selectedAddress} onChange={handleSelectChange}> &nbsp;
<option value="all">All</option> <select id="board-select-mobile" value={selectedAddress} onChange={handleSelectChange}>
<option value="subscriptions">Subscriptions</option> <option value="all">All</option>
{defaultSubplebbits.map(subplebbit => ( <option value="subscriptions">Subscriptions</option>
<option key={`option-${subplebbit.address}`} value={subplebbit.address} {defaultSubplebbits.map(subplebbit => (
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option> <option key={`option-${subplebbit.address}`} value={subplebbit.address}
))} >{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
</select>  ))}
<button style={{all: 'unset', cursor: 'pointer'}} onClick={ </select> 
<span style={{cursor: 'pointer'}} onClick={
() => alert( () => alert(
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n' 'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
) )
}>Create Board</button> }>Create Board</span>
</div> </div>
<div className="page-jump"> <div className="page-jump">
<Link to={`/p/${selectedAddress}/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link> <Link to={`/p/${selectedAddress}/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
&nbsp; &nbsp;
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}} <Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
)}>Home</Link> </div>
</div> </div>
</div> </div>
<div id="separator-mobile">&nbsp;</div> <div id="separator-mobile">&nbsp;</div>
@@ -396,7 +565,11 @@ const Catalog = () => {
<tr data-type="Name"> <tr data-type="Name">
<td id="td-name">Name</td> <td id="td-name">Name</td>
<td> <td>
<input name="name" type="text" tabIndex={1} placeholder="Anonymous" ref={nameRef} /> {account && account.author && account.author.displayName ? (
<input name="name" type="text" tabIndex={1} value={account.author?.displayName} ref={nameRef} disabled />
) : (
<input name="name" type="text" placeholder="Anonymous" tabIndex={1} ref={nameRef} />
)}
</td> </td>
</tr> </tr>
<tr data-type="Subject"> <tr data-type="Subject">
@@ -447,16 +620,20 @@ const Catalog = () => {
{feed.length > 0 ? ( {feed.length > 0 ? (
<> <>
<span className="subscribe-button-desktop"> <span className="subscribe-button-desktop">
[ [
<button id="subscribe" style={{all: 'unset', cursor: 'pointer'}} <span id="subscribe" style={{cursor: 'pointer'}}>
onClick={() => handleSubscribe()}>{subscribed ? "Unsubscribe" : "Subscribe"} <span onClick={() => handleSubscribe()}>
</button> {subscribed ? "Unsubscribe" : "Subscribe"}
] </span>
</span> </span>
<button className="subscribe-button-mobile btn-wrap" ]
onClick={() => handleSubscribe()}>{subscribed ? "Unsubscribe" : "Subscribe"} </span>
</button> <span className="subscribe-button-mobile">
</> <span className="btn-wrap" onClick={() => handleSubscribe()}>
{subscribed ? "Unsubscribe" : "Subscribe"}
</span>
</span>
</>
) : ( ) : (
<div id="stats" style={{float: "right", marginTop: "5px"}}> <div id="stats" style={{float: "right", marginTop: "5px"}}>
<span className={stateString ? "ellipsis" : ""}>{stateString}</span> <span className={stateString ? "ellipsis" : ""}>{stateString}</span>
@@ -484,45 +661,44 @@ const Catalog = () => {
const commentMediaInfo = getCommentMediaInfo(thread); const commentMediaInfo = getCommentMediaInfo(thread);
const fallbackImgUrl = "assets/filedeleted-res.gif"; const fallbackImgUrl = "assets/filedeleted-res.gif";
return ( return (
<Link style={{all: "unset", cursor: "pointer"}} key={`link-${index}`} to={`/p/${selectedAddress}/c/${thread.cid}`} <div key={`thread-${index}`} className="thread"
onClick={() => setSelectedThread(thread.cid)}> onMouseOver={() => {setIsHoveringOnThread(thread.cid)}}
<div key={`thread-${index}`} className="thread"> onMouseLeave={() => {setIsHoveringOnThread('')}}>
{commentMediaInfo?.url ? ( {commentMediaInfo?.url ? (
<Fragment key="f-catalog"> <Link style={{all: "unset", cursor: "pointer"}} key={`link-${index}`} to={`/p/${selectedAddress}/c/${thread.cid}`}
{commentMediaInfo?.type === "webpage" ? ( onClick={() => setSelectedThread(thread.cid)}>
thread.thumbnailUrl ? ( {commentMediaInfo?.type === "webpage" ? (
<img className="card" key={`img-${index}`} thread.thumbnailUrl ? (
src={commentMediaInfo.thumbnail} alt={commentMediaInfo.type} <img className="card" key={`img-${index}`}
onError={(e) => { src={commentMediaInfo.thumbnail} alt={commentMediaInfo.type}
e.target.src = fallbackImgUrl onError={(e) => {
e.target.onerror = null; e.target.src = fallbackImgUrl
}} /> e.target.onerror = null;
) : null }} />
) : null} ) : null
{commentMediaInfo?.type === "image" ? ( ) : null}
<img className="card" key={`img-${index}`} {commentMediaInfo?.type === "image" ? (
src={commentMediaInfo.url} alt={commentMediaInfo.type} <img className="card" key={`img-${index}`}
onError={(e) => { src={commentMediaInfo.url} alt={commentMediaInfo.type}
e.target.src = fallbackImgUrl onError={(e) => {
e.target.onerror = null;}} /> e.target.src = fallbackImgUrl
) : null} e.target.onerror = null;}} />
{commentMediaInfo?.type === "video" ? ( ) : null}
{commentMediaInfo?.type === "video" ? (
<video className="card" key={`fti-${index}`} <video className="card" key={`fti-${index}`}
src={commentMediaInfo.url} src={commentMediaInfo.url}
alt={commentMediaInfo.type} alt={commentMediaInfo.type}
style={{ pointerEvents: "none" }}
onError={(e) => e.target.src = fallbackImgUrl} /> onError={(e) => e.target.src = fallbackImgUrl} />
) : null} ) : null}
{commentMediaInfo?.type === "audio" ? ( {commentMediaInfo?.type === "audio" ? (
<audio className="card" controls <audio className="card" controls
key={`fti-${index}`} key={`fti-${index}`}
src={commentMediaInfo.url} src={commentMediaInfo.url}
alt={commentMediaInfo.type} alt={commentMediaInfo.type}
style={{ pointerEvents: "none" }}
onError={(e) => e.target.src = fallbackImgUrl} /> onError={(e) => e.target.src = fallbackImgUrl} />
) : null} ) : null}
</Fragment> </Link>
) : null} ) : null}
<div key={`ti-${index}`} className="thread-icons" > <div key={`ti-${index}`} className="thread-icons" >
{thread.pinned ? ( {thread.pinned ? (
<span key={`si-${index}`} className="thread-icon sticky-icon" title="Sticky" /> <span key={`si-${index}`} className="thread-icon sticky-icon" title="Sticky" />
@@ -531,16 +707,121 @@ const Catalog = () => {
<span key={`li-${index}`} className="thread-icon closed-icon" title="Closed" /> <span key={`li-${index}`} className="thread-icon closed-icon" title="Closed" />
) : null} ) : null}
</div> </div>
<div key={`meta-${index}`} className="meta" title="(R)eplies / (I)mage Replies" > <BoardForm selectedStyle={selectedStyle}
R: style={{ all: "unset"}}>
<b key={`b-${index}`}>{thread.replyCount}</b> <div key={`meta-${index}`} className="meta" title="(R)eplies / (I)mage Replies" >
</div> R:
<div key={`t-${index}`} className="teaser"> <b key={`b-${index}`}>{thread.replyCount}</b>
<PostMenu
style={{ display: isHoveringOnThread === thread.cid ? 'inline-block' : 'none',
position: 'absolute', lineHeight: '1em', marginTop: '-1px', outline: 'none',
zIndex: '999'}}
key={`pmb-${index}`}
title="Post menu"
ref={el => {
threadMenuRefs.current[thread.cid] = el;
postMenuRef.current = el;
}}
className='post-menu-button'
id='post-menu-button-catalog'
rotated={openMenuCid === thread.cid}
onClick={(event) => {
event.stopPropagation();
const rect = threadMenuRefs.current[thread.cid].getBoundingClientRect();
setMenuPosition({top: rect.top + window.scrollY, left: rect.left});
setOpenMenuCid(prevCid => (prevCid === thread.cid ? null : thread.cid));
}}
>
</PostMenu>
</div>
{createPortal(
<PostMenuCatalog selectedStyle={selectedStyle}
ref={el => {postMenuCatalogRef.current = el}}
onClick={(event) => event.stopPropagation()}
style={{position: "absolute",
top: menuPosition.top + 7,
left: menuPosition.left}}>
<div className={`post-menu-thread post-menu-thread-${thread.cid}`}
style={{ display: openMenuCid === thread.cid ? 'block' : 'none' }}
>
<ul className="post-menu-catalog">
<li onClick={() => handleOptionClick(thread.cid)}>Hide thread</li>
{thread.author.shortAddress === account?.author.shortAddress ? (
<>
<li onClick={() => handleAuthorEditClick(thread)}>Edit post</li>
<li onClick={() => handleAuthorDeleteClick(thread.cid)}>Delete post</li>
</>
) : null}
{isModerator ? (
<>
{thread.author.shortAddress === account?.author.shortAddress ? (
null
) : (
<li onClick={() => {
setModeratingCommentCid(thread.cid)
setIsModerationOpen(true);
handleOptionClick(thread.cid);
setDeletePost(true);
}}>
Delete post
</li>
)}
<li
onClick={() => {
setModeratingCommentCid(thread.cid)
setIsModerationOpen(true);
handleOptionClick(thread.cid);
}}>
Mod tools
</li>
</>
) : null}
{(commentMediaInfo && (
commentMediaInfo.type === 'image' ||
(commentMediaInfo.type === 'webpage' &&
commentMediaInfo.thumbnail))) ? (
<li
onMouseOver={() => {setIsImageSearchOpen(true)}}
onMouseLeave={() => {setIsImageSearchOpen(false)}}>
Image search »
<ul className="dropdown-menu post-menu-catalog"
style={{display: isImageSearchOpen ? 'block': 'none'}}>
<li onClick={() => handleOptionClick(thread.cid)}>
<a
href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`}
target="_blank" rel="noreferrer"
>Google</a>
</li>
<li onClick={() => handleOptionClick(thread.cid)}>
<a
href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`}
target="_blank" rel="noreferrer"
>Yandex</a>
</li>
<li onClick={() => handleOptionClick(thread.cid)}>
<a
href={`https://saucenao.com/search.php?url=${commentMediaInfo.url}`}
target="_blank" rel="noreferrer"
>SauceNAO</a>
</li>
</ul>
</li>
) : null
}
</ul>
</div>
</PostMenuCatalog>, document.body
)}
</BoardForm>
<Link style={{all: "unset", cursor: "pointer"}} key={`link2-${index}`} to={`/p/${selectedAddress}/c/${thread.cid}`}
onClick={() => setSelectedThread(thread.cid)}>
<div key={`t-${index}`} className="teaser">
<b key={`b2-${index}`}>{thread.title ? `${thread.title}` : null}</b> <b key={`b2-${index}`}>{thread.title ? `${thread.title}` : null}</b>
{thread.content ? `: ${thread.content}` : null} {thread.content ? `: ${thread.content}` : null}
</div> </div>
</Link>
</div> </div>
</Link>
)})} )})}
</InfiniteScroll> </InfiniteScroll>
) : (<CatalogLoader />) ) : (<CatalogLoader />)
+29 -16
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react'; import React, { Fragment, useEffect, useRef } from 'react';
import { Helmet } from 'react-helmet-async'; import { Helmet } from 'react-helmet-async';
import { useAccount } from '@plebbit/plebbit-react-hooks'; import { useAccount } from '@plebbit/plebbit-react-hooks';
import useGeneralStore from '../../hooks/stores/useGeneralStore'; import useGeneralStore from '../../hooks/stores/useGeneralStore';
@@ -15,25 +15,36 @@ const commitRef = process?.env?.REACT_APP_COMMIT_REF ? ` ${process.env.REACT_APP
const Home = () => { const Home = () => {
const { const {
setBodyStyle, bodyStyle, setBodyStyle,
defaultSubplebbits, defaultSubplebbits,
setSelectedAddress, setSelectedAddress,
setSelectedStyle, selectedStyle, setSelectedStyle,
setSelectedTitle setSelectedTitle
} = useGeneralStore(state => state); } = useGeneralStore(state => state);
const account = useAccount(); const account = useAccount();
const inputRef = useRef(null);
const navigate = useNavigate(); const navigate = useNavigate();
const inputRef = useRef(null);
const prevStyle = useRef(selectedStyle);
const prevBodyStyle = useRef(bodyStyle);
// prevent dark mode // prevent dark mode
useEffect(() => { useEffect(() => {
const currentPrevStyle = prevStyle.current;
const currentPrevBodyStyle = prevBodyStyle.current;
setBodyStyle({ setBodyStyle({
background: "#ffe url(assets/fade.png) top repeat-x", background: "#ffe url(assets/fade.png) top repeat-x",
color: "maroon", color: "maroon",
fontFamily: "Helvetica, Arial, sans-serif" fontFamily: "Helvetica, Arial, sans-serif"
}); });
setSelectedStyle("Yotsuba"); setSelectedStyle("Yotsuba");
return () => {
setSelectedStyle(currentPrevStyle);
setBodyStyle(currentPrevBodyStyle);
};
}, [setBodyStyle, setSelectedStyle]); }, [setBodyStyle, setSelectedStyle]);
@@ -103,7 +114,7 @@ const Home = () => {
<Link to="/p/subscriptions" id="view-all" onClick={()=> {window.scrollTo(0, 0)}}>[view all]</Link> <Link to="/p/subscriptions" id="view-all" onClick={()=> {window.scrollTo(0, 0)}}>[view all]</Link>
<br /> <br />
{account?.subscriptions?.map((subscription, index) => ( {account?.subscriptions?.map((subscription, index) => (
<> <Fragment key={`frag-${index}`}>
<Link key={`sub-${index}`} className="boardlink" <Link key={`sub-${index}`} className="boardlink"
onClick={()=> {window.scrollTo(0, 0)}} onClick={()=> {window.scrollTo(0, 0)}}
to={`/p/${subscription}`}> to={`/p/${subscription}`}>
@@ -111,11 +122,12 @@ const Home = () => {
{subscription}&nbsp; {subscription}&nbsp;
</Link> </Link>
<OfflineIndicator <OfflineIndicator
key={`offline-${index}`}
address={subscription} address={subscription}
className="disconnected" className="disconnected"
tooltipPlace="top" /> tooltipPlace="top" />
<br /> <br key={`br-${index}`} />
</> </Fragment>
))} ))}
<br id="mobile-br" /> <br id="mobile-br" />
</BoardsContent> </BoardsContent>
@@ -126,26 +138,27 @@ const Home = () => {
<h2>Popular Boards</h2> <h2>Popular Boards</h2>
</BoardsTitle> </BoardsTitle>
<BoardsContent> <BoardsContent>
{defaultSubplebbits.map(subplebbit => ( {defaultSubplebbits.map((subplebbit, index) => (
<div className="board" key={subplebbit.address}> <div className="board" key={`board-${index}`}>
<div className="board-title"> <div className="board-title" key="board-title">
{subplebbit.title ? subplebbit.title : <span style={{userSelect: "none"}}>&nbsp;</span>} {subplebbit.title ? subplebbit.title : <span style={{userSelect: "none"}}>&nbsp;</span>}
</div> </div>
<div className="board-avatar-container"> <div className="board-avatar-container" key="board-avatar-container">
<Link to={`/p/${subplebbit.address}`} onClick={() => { <Link to={`/p/${subplebbit.address}`} key="link" onClick={() => {
setSelectedTitle(subplebbit.title); setSelectedTitle(subplebbit.title);
setSelectedAddress(subplebbit.address); setSelectedAddress(subplebbit.address);
window.scrollTo(0, 0); window.scrollTo(0, 0);
}} > }} >
<BoardAvatar address={subplebbit.address} /> <BoardAvatar key="baordavatar" address={subplebbit.address} />
</Link> </Link>
<OfflineIndicator <OfflineIndicator
address={subplebbit.address} address={subplebbit.address}
className="offline-indicator" className="offline-indicator"
tooltipPlace="top" /> tooltipPlace="top"
key="oi2"/>
</div> </div>
<div className="board-text"> <div className="board-text" key="bt">
<b>{subplebbit.address}</b> <b key="b">{subplebbit.address}</b>
</div> </div>
</div> </div>
))} ))}
+18 -3
View File
@@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import React, { useEffect, useRef } from 'react';
import { Helmet } from 'react-helmet-async'; import { Helmet } from 'react-helmet-async';
import useGeneralStore from '../../hooks/stores/useGeneralStore'; import useGeneralStore from '../../hooks/stores/useGeneralStore';
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@@ -7,16 +7,31 @@ import packageJson from '../../../package.json'
const {version} = packageJson const {version} = packageJson
const NotFound = ({ setBodyStyle }) => { const NotFound = () => {
const { setSelectedStyle } = useGeneralStore(state => state); const {
bodyStyle, setBodyStyle,
selectedStyle, setSelectedStyle,
} = useGeneralStore(state => state);
const prevStyle = useRef(selectedStyle);
const prevBodyStyle = useRef(bodyStyle);
// prevent dark mode
useEffect(() => { useEffect(() => {
const currentPrevStyle = prevStyle.current;
const currentPrevBodyStyle = prevBodyStyle.current;
setBodyStyle({ setBodyStyle({
background: "#ffe url(assets/fade.png) top repeat-x", background: "#ffe url(assets/fade.png) top repeat-x",
color: "maroon", color: "maroon",
fontFamily: "Helvetica, Arial, sans-serif" fontFamily: "Helvetica, Arial, sans-serif"
}); });
setSelectedStyle("Yotsuba"); setSelectedStyle("Yotsuba");
return () => {
setSelectedStyle(currentPrevStyle);
setBodyStyle(currentPrevBodyStyle);
};
}, [setBodyStyle, setSelectedStyle]); }, [setBodyStyle, setSelectedStyle]);
return ( return (
+15 -4
View File
@@ -125,12 +125,18 @@ const Pending = () => {
] ]
</span> </span>
<span className="nav"> <span className="nav">
[
<span id="button-span" style={{cursor: 'pointer'}} onClick={
() => alert(
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
)
}>Create Board</span>
]
[ [
<Link to={`/profile/c/${index}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link> <Link to={`/profile/c/${index}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
] ]
[ [
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}} <Link to="/">Home</Link>
)}>Home</Link>
] ]
</span> </span>
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}> <div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
@@ -145,6 +151,11 @@ const Pending = () => {
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option> >{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
))} ))}
</select> </select>
<span style={{cursor: 'pointer'}} onClick={
() => alert(
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
)
}>Create Board</span>
</div> </div>
<div className="page-jump"> <div className="page-jump">
<Link to={`/profile/c/${index}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link> <Link to={`/profile/c/${index}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
@@ -285,7 +296,7 @@ const Pending = () => {
&nbsp; &nbsp;
&nbsp; &nbsp;
<span className="poster-address"> <span className="poster-address">
(u/{account.author.shortAddress}) (u/{account.author?.shortAddress})
</span> </span>
&nbsp; &nbsp;
<span className="date-time" data-utc="data">{getDate(comment?.timestamp)}</span> <span className="date-time" data-utc="data">{getDate(comment?.timestamp)}</span>
@@ -326,7 +337,7 @@ const Pending = () => {
Anonymous</span>} Anonymous</span>}
&nbsp; &nbsp;
<span key={`mob-pa-${"pending"}`} className="poster-address-mobile"> <span key={`mob-pa-${"pending"}`} className="poster-address-mobile">
(u/{account.author.shortAddress})&nbsp; (u/{account.author?.shortAddress})&nbsp;
</span> </span>
<br key={`mob-br1-${"pending"}`} /> <br key={`mob-br1-${"pending"}`} />
{comment.title ? ( {comment.title ? (
+25 -25
View File
@@ -220,43 +220,43 @@ const Subscriptions = () => {
</span> </span>
<span className="nav"> <span className="nav">
[ [
<button style={{all: 'unset', cursor: 'pointer'}} onClick={ <span id="button-span" style={{cursor: 'pointer'}} onClick={
() => alert( () => alert(
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n' 'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
) )
}>Create Board</button> }>Create Board</span>
] ]
[ [
<Link to={`/p/subscriptions/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link> <Link to={`/p/subscriptions/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
] ]
[ [
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}} <Link to="/">Home</Link>
)}>Home</Link>
] ]
</span> </span>
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}> <div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
<div className="board-select"> <div className="nav-container">
<strong>Board</strong> <div className="board-select">
&nbsp; <strong>Board</strong>
<select id="board-select-mobile" value="subscriptions" onChange={handleSelectChange}> &nbsp;
<option value="all">All</option> <select id="board-select-mobile" value="subscriptions" onChange={handleSelectChange}>
<option value="subscriptions">Subscriptions</option> <option value="all">All</option>
{defaultSubplebbits.map(subplebbit => ( <option value="subscriptions">Subscriptions</option>
<option key={`option-${subplebbit.address}`} value={subplebbit.address} {defaultSubplebbits.map(subplebbit => (
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option> <option key={`option-${subplebbit.address}`} value={subplebbit.address}
))} >{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
</select>  ))}
<button style={{all: 'unset', cursor: 'pointer'}} onClick={ </select> 
<span style={{cursor: 'pointer'}} onClick={
() => alert( () => alert(
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n' 'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
) )
}>Create Board</button> }>Create Board</span>
</div> </div>
<div className="page-jump"> <div className="page-jump">
<Link to={`/p/subscriptions/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link> <Link to={`/p/subscriptions/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
&nbsp; &nbsp;
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}} <Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
)}>Home</Link> </div>
</div> </div>
</div> </div>
<div id="separator-mobile">&nbsp;</div> <div id="separator-mobile">&nbsp;</div>
@@ -490,7 +490,7 @@ const Subscriptions = () => {
onMouseOver={() => {setIsImageSearchOpen(true)}} onMouseOver={() => {setIsImageSearchOpen(true)}}
onMouseLeave={() => {setIsImageSearchOpen(false)}}> onMouseLeave={() => {setIsImageSearchOpen(false)}}>
Image search » Image search »
<ul className="dropdown-menu" <ul className="dropdown-menu post-menu-catalog"
style={{display: isImageSearchOpen ? 'block': 'none'}}> style={{display: isImageSearchOpen ? 'block': 'none'}}>
<li> <li>
<a <a
@@ -678,7 +678,7 @@ const Subscriptions = () => {
onMouseOver={() => {setIsImageSearchOpen(true)}} onMouseOver={() => {setIsImageSearchOpen(true)}}
onMouseLeave={() => {setIsImageSearchOpen(false)}}> onMouseLeave={() => {setIsImageSearchOpen(false)}}>
Image search » Image search »
<ul className="dropdown-menu" <ul className="dropdown-menu post-menu-catalog"
style={{display: isImageSearchOpen ? 'block': 'none'}}> style={{display: isImageSearchOpen ? 'block': 'none'}}>
<li> <li>
<a <a
+23 -23
View File
@@ -123,43 +123,43 @@ const SubscriptionsCatalog = () => {
</span> </span>
<span className="nav"> <span className="nav">
[ [
<button style={{all: 'unset', cursor: 'pointer'}} onClick={ <span id="button-span" style={{cursor: 'pointer'}} onClick={
() => alert( () => alert(
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n' 'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
) )
}>Create Board</button> }>Create Board</span>
] ]
[ [
<Link to={`/p/subscriptions/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link> <Link to={`/p/subscriptions/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
] ]
[ [
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}} <Link to="/">Home</Link>
)}>Home</Link>
] ]
</span> </span>
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}> <div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
<div className="board-select"> <div className="nav-container">
<strong>Board</strong> <div className="board-select">
&nbsp; <strong>Board</strong>
<select id="board-select-mobile" value="subscriptions" onChange={handleSelectChange}> &nbsp;
<option value="all">All</option> <select id="board-select-mobile" value="subscriptions" onChange={handleSelectChange}>
<option value="subscriptions">Subscriptions</option> <option value="all">All</option>
{defaultSubplebbits.map(subplebbit => ( <option value="subscriptions">Subscriptions</option>
<option key={`option-${subplebbit.address}`} value={subplebbit.address} {defaultSubplebbits.map(subplebbit => (
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option> <option key={`option-${subplebbit.address}`} value={subplebbit.address}
))} >{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
</select>  ))}
<button style={{all: 'unset', cursor: 'pointer'}} onClick={ </select> 
<span style={{cursor: 'pointer'}} onClick={
() => alert( () => alert(
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n' 'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
) )
}>Create Board</button> }>Create Board</span>
</div> </div>
<div className="page-jump"> <div className="page-jump">
<Link to={`/p/subscriptions/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link> <Link to={`/p/subscriptions/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
&nbsp; &nbsp;
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}} <Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
)}>Home</Link> </div>
</div> </div>
</div> </div>
<div id="separator-mobile">&nbsp;</div> <div id="separator-mobile">&nbsp;</div>
+225 -178
View File
@@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Helmet } from 'react-helmet-async'; import { Helmet } from 'react-helmet-async';
import { Link, useNavigate, useParams } from 'react-router-dom'; import { Link, useNavigate, useParams } from 'react-router-dom';
import { confirmAlert } from 'react-confirm-alert'; import { confirmAlert } from 'react-confirm-alert';
@@ -9,6 +10,7 @@ import { debounce } from 'lodash';
import useGeneralStore from '../../hooks/stores/useGeneralStore'; import useGeneralStore from '../../hooks/stores/useGeneralStore';
import { Container, NavBar, Header, Break, PostForm, PostFormTable, PostMenu } from '../styled/views/Board.styled'; import { Container, NavBar, Header, Break, PostForm, PostFormTable, PostMenu } from '../styled/views/Board.styled';
import { ReplyFormLink, TopBar, BottomBar, BoardForm, Footer, AuthorDeleteAlert } from '../styled/views/Thread.styled'; import { ReplyFormLink, TopBar, BottomBar, BoardForm, Footer, AuthorDeleteAlert } from '../styled/views/Thread.styled';
import { PostMenuCatalog } from '../styled/views/Catalog.styled';
import EditModal from '../modals/EditModal'; import EditModal from '../modals/EditModal';
import ImageBanner from '../ImageBanner'; import ImageBanner from '../ImageBanner';
import ModerationModal from '../modals/ModerationModal'; import ModerationModal from '../modals/ModerationModal';
@@ -38,6 +40,8 @@ const Thread = () => {
setChallengesArray, setChallengesArray,
defaultSubplebbits, defaultSubplebbits,
editedComment, editedComment,
setIsAuthorDelete,
setIsAuthorEdit,
setIsCaptchaOpen, setIsCaptchaOpen,
isModerationOpen, setIsModerationOpen, isModerationOpen, setIsModerationOpen,
isSettingsOpen, setIsSettingsOpen, isSettingsOpen, setIsSettingsOpen,
@@ -64,6 +68,8 @@ const Thread = () => {
const linkRef = useRef(); const linkRef = useRef();
const threadMenuRefs = useRef({}); const threadMenuRefs = useRef({});
const replyMenuRefs = useRef({}); const replyMenuRefs = useRef({});
const postMenuRef = useRef(null);
const postMenuCatalogRef = useRef(null);
const [triggerPublishComment, setTriggerPublishComment] = useState(false); const [triggerPublishComment, setTriggerPublishComment] = useState(false);
const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false); const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false);
@@ -75,10 +81,11 @@ const Thread = () => {
const [originalCommentContent, setOriginalCommentContent] = useState(null); const [originalCommentContent, setOriginalCommentContent] = useState(null);
const [prevScrollPos, setPrevScrollPos] = useState(0); const [prevScrollPos, setPrevScrollPos] = useState(0);
const [visible, setVisible] = useState(true); const [visible, setVisible] = useState(true);
const [rotatedStates, setRotatedStates] = useState({});
const [isImageSearchOpen, setIsImageSearchOpen] = useState(false); const [isImageSearchOpen, setIsImageSearchOpen] = useState(false);
const [isModerator, setIsModerator] = useState(false); const [isModerator, setIsModerator] = useState(false);
const [commentCid, setCommentCid] = useState(null); const [commentCid, setCommentCid] = useState(null);
const [menuPosition, setMenuPosition] = useState({top: 0, left: 0});
const [openMenuCid, setOpenMenuCid] = useState(null);
useError(errorMessage, [errorMessage]); useError(errorMessage, [errorMessage]);
useSuccess(successMessage, [successMessage]); useSuccess(successMessage, [successMessage]);
@@ -107,13 +114,28 @@ const Thread = () => {
}, [account?.author.address, subplebbit.roles]); }, [account?.author.address, subplebbit.roles]);
const handleOptionClick = (threadCid) => { const handleOptionClick = () => {
setRotatedStates(prevState => ({ setOpenMenuCid(null);
...prevState,
[threadCid]: false
}));
}; };
const handleOutsideClick = useCallback((e) => {
if (openMenuCid !== null && !postMenuRef.current.contains(e.target) && !postMenuCatalogRef.current.contains(e.target)) {
setOpenMenuCid(null);
}
}, [openMenuCid, postMenuRef, postMenuCatalogRef]);
useEffect(() => {
if (openMenuCid !== null) {
document.addEventListener('click', handleOutsideClick);
} else {
document.removeEventListener('click', handleOutsideClick);
}
return () => {
document.removeEventListener('click', handleOutsideClick);
};
}, [openMenuCid, handleOutsideClick]);
useEffect(() => { useEffect(() => {
window.scrollTo(0, 0); window.scrollTo(0, 0);
@@ -371,6 +393,8 @@ const Thread = () => {
<button onClick={onClose}>No</button> <button onClick={onClose}>No</button>
<button <button
onClick={() => { onClick={() => {
setIsAuthorDelete(true);
setIsAuthorEdit(false);
setCommentCid(commentCid); setCommentCid(commentCid);
setPublishCommentEditOptions(prevOptions => ({ setPublishCommentEditOptions(prevOptions => ({
...prevOptions, ...prevOptions,
@@ -392,6 +416,8 @@ const Thread = () => {
const handleAuthorEditClick = (comment) => { const handleAuthorEditClick = (comment) => {
handleOptionClick(comment.cid); handleOptionClick(comment.cid);
setIsAuthorEdit(true);
setIsAuthorDelete(false);
setCommentCid(comment.cid); setCommentCid(comment.cid);
setOriginalCommentContent(comment.content); setOriginalCommentContent(comment.content);
setIsEditModalOpen(true); setIsEditModalOpen(true);
@@ -496,42 +522,43 @@ const Thread = () => {
</span> </span>
<span className="nav"> <span className="nav">
[ [
<button style={{all: 'unset', cursor: 'pointer'}} onClick={ <span id="button-span" style={{cursor: 'pointer'}} onClick={
() => alert( () => alert(
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n' 'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
) )
}>Create Board</button> }>Create Board</span>
] ]
[ [
<Link to={`/p/${selectedAddress}/c/${selectedThread}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link> <Link to={`/p/${selectedAddress}/c/${selectedThread}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
] ]
[ [
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}} <Link to="/">Home</Link>
)}>Home</Link>
] ]
</span> </span>
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}> <div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
<div className="board-select"> <div className="nav-container">
<strong>Board</strong> <div className="board-select">
&nbsp; <strong>Board</strong>
<select id="board-select-mobile" value={selectedAddress} onChange={handleSelectChange}> &nbsp;
<option value="all">All</option> <select id="board-select-mobile" value={selectedAddress} onChange={handleSelectChange}>
<option value="subscriptions">Subscriptions</option> <option value="all">All</option>
{defaultSubplebbits.map(subplebbit => ( <option value="subscriptions">Subscriptions</option>
<option key={`option-${subplebbit.address}`} value={subplebbit.address} {defaultSubplebbits.map(subplebbit => (
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option> <option key={`option-${subplebbit.address}`} value={subplebbit.address}
))} >{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
</select>  ))}
<button style={{all: 'unset', cursor: 'pointer'}} onClick={ </select> 
<span style={{cursor: 'pointer'}} onClick={
() => alert( () => alert(
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n' 'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
) )
}>Create Board</button> }>Create Board</span>
</div> </div>
<div className="page-jump"> <div className="page-jump">
<Link to={`/p/${selectedAddress}/c/${selectedThread}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link> <Link to={`/p/${selectedAddress}/c/${selectedThread}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
&nbsp; &nbsp;
<Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link> <Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
</div>
</div> </div>
</div> </div>
<div id="separator-mobile">&nbsp;</div> <div id="separator-mobile">&nbsp;</div>
@@ -588,7 +615,11 @@ const Thread = () => {
<tr data-type="Name"> <tr data-type="Name">
<td id="td-name">Name</td> <td id="td-name">Name</td>
<td> <td>
<input name="name" type="text" tabIndex={1} placeholder="Anonymous" ref={nameRef} /> {account && account.author && account.author.displayName ? (
<input name="name" type="text" tabIndex={1} value={account.author?.displayName} ref={nameRef} disabled />
) : (
<input name="name" type="text" placeholder="Anonymous" tabIndex={1} ref={nameRef} />
)}
<input id="post-button" type="submit" value="Post" tabIndex={6} <input id="post-button" type="submit" value="Post" tabIndex={6}
onClick={handleSubmit} /> onClick={handleSubmit} />
</td> </td>
@@ -790,91 +821,99 @@ const Thread = () => {
<PostMenu <PostMenu
key={`pmb-${index}`} key={`pmb-${index}`}
title="Post menu" title="Post menu"
ref={el => threadMenuRefs.current[comment.cid] = el} ref={el => {
threadMenuRefs.current[comment.cid] = el;
postMenuRef.current = el;
}}
className='post-menu-button' className='post-menu-button'
rotated={rotatedStates[comment.cid]} rotated={openMenuCid === comment.cid}
onClick={() => { onClick={(event) => {
event.stopPropagation();
const rect = threadMenuRefs.current[comment.cid].getBoundingClientRect(); const rect = threadMenuRefs.current[comment.cid].getBoundingClientRect();
const menu = document.querySelector(`.post-menu-thread-${comment.cid}`); setMenuPosition({top: rect.top + window.scrollY, left: rect.left});
menu.style.top = `calc(${rect.top}px + 17px)`; setOpenMenuCid(prevCid => (prevCid === comment.cid ? null : comment.cid));
menu.style.left = `${rect.left}px`;
setRotatedStates(prevState => ({
...prevState,
[comment.cid]: !prevState[comment.cid]
}));
}} }}
> >
</PostMenu> </PostMenu>
<div id="post-menu" className={`post-menu-thread post-menu-thread-${comment.cid}`} {createPortal(
style={{ display: rotatedStates[comment.cid] ? 'block' : 'none' }}> <PostMenuCatalog selectedStyle={selectedStyle}
<ul> ref={el => {postMenuCatalogRef.current = el}}
<li onClick={() => handleOptionClick(comment.cid)}>Hide thread</li> onClick={(event) => event.stopPropagation()}
{comment?.author?.shortAddress === account?.author?.shortAddress ? ( style={{position: "absolute",
<> top: menuPosition.top + 7,
<li onClick={() => handleAuthorEditClick(comment)}>Edit post</li> left: menuPosition.left}}>
<li onClick={() => handleAuthorDeleteClick(comment.cid)}>Delete post</li> <div className={`post-menu-thread post-menu-thread-${comment.cid}`}
</> style={{ display: openMenuCid === comment.cid ? 'block' : 'none' }}
) : null} >
{isModerator ? ( <ul className="post-menu-catalog">
<> <li onClick={() => handleOptionClick(comment.cid)}>Hide thread</li>
{comment?.author?.shortAddress === account?.author?.shortAddress ? ( {comment.author?.shortAddress === account?.author.shortAddress ? (
null <>
) : ( <li onClick={() => handleAuthorEditClick(comment)}>Edit post</li>
<li onClick={() => { <li onClick={() => handleAuthorDeleteClick(comment.cid)}>Delete post</li>
</>
) : null}
{isModerator ? (
<>
{comment.author?.shortAddress === account?.author.shortAddress ? (
null
) : (
<li onClick={() => {
setModeratingCommentCid(comment.cid)
setIsModerationOpen(true);
handleOptionClick(comment.cid);
setDeletePost(true);
}}>
Delete post
</li>
)}
<li
onClick={() => {
setModeratingCommentCid(comment.cid) setModeratingCommentCid(comment.cid)
setIsModerationOpen(true); setIsModerationOpen(true);
handleOptionClick(comment.cid); handleOptionClick(comment.cid);
setDeletePost(true);
}}> }}>
Delete post Mod tools
</li> </li>
)} </>
<li ) : null}
onClick={() => { {(commentMediaInfo && (
setModeratingCommentCid(comment.cid) commentMediaInfo.type === 'image' ||
setIsModerationOpen(true); (commentMediaInfo.type === 'webpage' &&
handleOptionClick(comment.cid); commentMediaInfo.thumbnail))) ? (
}}> <li
Mod tools onMouseOver={() => {setIsImageSearchOpen(true)}}
</li> onMouseLeave={() => {setIsImageSearchOpen(false)}}>
</> Image search »
) : null} <ul className="dropdown-menu post-menu-catalog"
{(commentMediaInfo && ( style={{display: isImageSearchOpen ? 'block': 'none'}}>
commentMediaInfo.type === 'image' || <li onClick={() => handleOptionClick(comment.cid)}>
(commentMediaInfo.type === 'webpage' && <a
commentMediaInfo.thumbnail))) ? ( href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`}
<li target="_blank" rel="noreferrer"
onMouseOver={() => {setIsImageSearchOpen(true)}} >Google</a>
onMouseLeave={() => {setIsImageSearchOpen(false)}}> </li>
Image search » <li onClick={() => handleOptionClick(comment.cid)}>
<ul className="dropdown-menu" <a
style={{display: isImageSearchOpen ? 'block': 'none'}}> href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`}
<li onClick={() => handleOptionClick(comment.cid)}> target="_blank" rel="noreferrer"
<a >Yandex</a>
href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`} </li>
target="_blank" rel="noreferrer" <li onClick={() => handleOptionClick(comment.cid)}>
>Google</a> <a
</li> href={`https://saucenao.com/search.php?url=${commentMediaInfo.url}`}
<li onClick={() => handleOptionClick(comment.cid)}> target="_blank" rel="noreferrer"
<a >SauceNAO</a>
href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`} </li>
target="_blank" rel="noreferrer" </ul>
>Yandex</a> </li>
</li> ) : null
<li onClick={() => handleOptionClick(comment.cid)}> }
<a </ul>
href={`https://saucenao.com/search.php?url=${commentMediaInfo.url}`} </div>
target="_blank" rel="noreferrer" </PostMenuCatalog>, document.body
>SauceNAO</a> )}
</li>
</ul>
</li>
) : null
}
</ul>
</div>
<div id="backlink-id" className="backlink"> <div id="backlink-id" className="backlink">
{comment?.replies?.pages?.topAll.comments {comment?.replies?.pages?.topAll.comments
.sort((a, b) => a.timestamp - b.timestamp) .sort((a, b) => a.timestamp - b.timestamp)
@@ -962,91 +1001,99 @@ const Thread = () => {
<PostMenu <PostMenu
key={`pmb-${index}`} key={`pmb-${index}`}
title="Post menu" title="Post menu"
ref={el => replyMenuRefs.current[reply.cid] = el} ref={el => {
replyMenuRefs.current[reply.cid] = el;
postMenuRef.current = el;
}}
className='post-menu-button' className='post-menu-button'
rotated={rotatedStates[reply.cid]} rotated={openMenuCid === reply.cid}
onClick={() => { onClick={(event) => {
event.stopPropagation();
const rect = replyMenuRefs.current[reply.cid].getBoundingClientRect(); const rect = replyMenuRefs.current[reply.cid].getBoundingClientRect();
const menu = document.querySelector(`.post-menu-reply-${reply.cid}`); setMenuPosition({top: rect.top + window.scrollY, left: rect.left});
menu.style.top = `calc(${rect.top}px + 17px)`; setOpenMenuCid(prevCid => (prevCid === reply.cid ? null : reply.cid));
menu.style.left = `${rect.left}px`; }}
setRotatedStates(prevState => ({
...prevState,
[reply.cid]: !prevState[reply.cid]
}));
}}
> >
</PostMenu> </PostMenu>
<div id="post-menu" className={`post-menu-reply post-menu-reply-${reply.cid}`} {createPortal(
style={{ display: rotatedStates[reply.cid] ? 'block' : 'none' }}> <PostMenuCatalog selectedStyle={selectedStyle}
<ul> ref={el => {postMenuCatalogRef.current = el}}
<li onClick={() => handleOptionClick(reply.cid)}>Hide post</li> onClick={(event) => event.stopPropagation()}
{reply.author.shortAddress === account?.author?.shortAddress ? ( style={{position: "absolute",
<> top: menuPosition.top + 7,
<li onClick={() => handleAuthorEditClick(reply)}>Edit post</li> left: menuPosition.left}}>
<li onClick={() => handleAuthorDeleteClick(reply.cid)}>Delete post</li> <div className={`post-menu-reply post-menu-reply-${reply.cid}`}
</> style={{ display: openMenuCid === reply.cid ? 'block' : 'none' }}
) : null} >
{isModerator ? ( <ul className="post-menu-catalog">
<> <li onClick={() => handleOptionClick(reply.cid)}>Hide post</li>
{reply.author.shortAddress === account?.author?.shortAddress ? ( {reply.author.shortAddress === account?.author.shortAddress ? (
null <>
) : ( <li onClick={() => handleAuthorEditClick(reply)}>Edit post</li>
<li onClick={() => { <li onClick={() => handleAuthorDeleteClick(reply.cid)}>Delete post</li>
</>
) : null}
{isModerator ? (
<>
{reply.author.shortAddress === account?.author.shortAddress ? (
null
) : (
<li onClick={() => {
setModeratingCommentCid(reply.cid)
setIsModerationOpen(true);
handleOptionClick(reply.cid);
setDeletePost(true);
}}>
Delete post
</li>
)}
<li
onClick={() => {
setModeratingCommentCid(reply.cid) setModeratingCommentCid(reply.cid)
setIsModerationOpen(true); setIsModerationOpen(true);
handleOptionClick(reply.cid); handleOptionClick(reply.cid);
setDeletePost(true);
}}> }}>
Delete post Mod tools
</li> </li>
)} </>
<li ) : null}
onClick={() => { {(replyMediaInfo && (
setModeratingCommentCid(reply.cid) replyMediaInfo.type === 'image' ||
setIsModerationOpen(true); (replyMediaInfo.type === 'webpage' &&
handleOptionClick(reply.cid); replyMediaInfo.thumbnail))) ? (
}}> <li
Mod tools onMouseOver={() => {setIsImageSearchOpen(true)}}
</li> onMouseLeave={() => {setIsImageSearchOpen(false)}}>
</> Image search »
) : null} <ul className="dropdown-menu post-menu-catalog"
{(replyMediaInfo && ( style={{display: isImageSearchOpen ? 'block': 'none'}}>
replyMediaInfo.type === 'image' || <li onClick={() => handleOptionClick(reply.cid)}>
(replyMediaInfo.type === 'webpage' && <a
replyMediaInfo.thumbnail))) ? ( href={`https://lens.google.com/uploadbyurl?url=${replyMediaInfo.url}`}
<li target="_blank" rel="noreferrer"
onMouseOver={() => {setIsImageSearchOpen(true)}} >Google</a>
onMouseLeave={() => {setIsImageSearchOpen(false)}}> </li>
Image search » <li onClick={() => handleOptionClick(reply.cid)}>
<ul className="dropdown-menu" <a
style={{display: isImageSearchOpen ? 'block': 'none'}}> href={`https://yandex.com/images/search?url=${replyMediaInfo.url}`}
<li onClick={() => handleOptionClick(reply.cid)}> target="_blank" rel="noreferrer"
<a >Yandex</a>
href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`} </li>
target="_blank" rel="noreferrer" <li onClick={() => handleOptionClick(reply.cid)}>
>Google</a> <a
</li> href={`https://saucenao.com/search.php?url=${replyMediaInfo.url}`}
<li onClick={() => handleOptionClick(reply.cid)}> target="_blank" rel="noreferrer"
<a >SauceNAO</a>
href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`} </li>
target="_blank" rel="noreferrer" </ul>
>Yandex</a> </li>
</li> ) : null
<li onClick={() => handleOptionClick(reply.cid)}> }
<a </ul>
href={`https://saucenao.com/search.php?url=${commentMediaInfo.url}`} </div>
target="_blank" rel="noreferrer" </PostMenuCatalog>, document.body
>SauceNAO</a> )}
</li>
</ul>
</li>
) : null
}
</ul>
</div>
<div id="backlink-id" className="backlink"> <div id="backlink-id" className="backlink">
{reply.replies?.pages?.topAll.comments {reply.replies?.pages?.topAll.comments
.sort((a, b) => a.timestamp - b.timestamp) .sort((a, b) => a.timestamp - b.timestamp)
+9
View File
@@ -23,12 +23,21 @@ const useGeneralStore = create((set) => ({
editedComment: '', editedComment: '',
setEditedComment: (comment) => set({ editedComment: comment }), setEditedComment: (comment) => set({ editedComment: comment }),
isAuthorDelete: false,
setIsAuthorDelete: (isAuthorDelete) => set({ isAuthorDelete }),
isAuthorEdit: false,
setIsAuthorEdit: (isAuthorEdit) => set({ isAuthorEdit }),
isCaptchaOpen: false, isCaptchaOpen: false,
setIsCaptchaOpen: (isOpen) => set({ isCaptchaOpen: isOpen }), setIsCaptchaOpen: (isOpen) => set({ isCaptchaOpen: isOpen }),
isModerationOpen: false, isModerationOpen: false,
setIsModerationOpen: (isOpen) => set({ isModerationOpen: isOpen }), setIsModerationOpen: (isOpen) => set({ isModerationOpen: isOpen }),
isModEdit: false,
setIsModEdit: (isModEdit) => set({ isModEdit }),
isSettingsOpen: false, isSettingsOpen: false,
setIsSettingsOpen: (isOpen) => set({ isSettingsOpen: isOpen }), setIsSettingsOpen: (isOpen) => set({ isSettingsOpen: isOpen }),