mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -7,11 +7,15 @@ import Draggable from 'react-draggable';
|
||||
|
||||
const CaptchaModal = () => {
|
||||
const {
|
||||
challengesArray,
|
||||
challengesArray, setChallengesArray,
|
||||
pendingComment,
|
||||
selectedStyle,
|
||||
setCaptchaResponse,
|
||||
isAuthorDelete, setIsAuthorDelete,
|
||||
isAuthorEdit, setIsAuthorEdit,
|
||||
isCaptchaOpen, setIsCaptchaOpen,
|
||||
isModEdit, setIsModEdit,
|
||||
resolveCaptchaPromise,
|
||||
selectedShortCid,
|
||||
} = useGeneralStore(state => state);
|
||||
|
||||
@@ -22,8 +26,18 @@ const CaptchaModal = () => {
|
||||
const responseRef = useRef();
|
||||
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);
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
@@ -50,6 +64,7 @@ const CaptchaModal = () => {
|
||||
}
|
||||
}, [challengesArray]);
|
||||
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
@@ -57,28 +72,35 @@ const CaptchaModal = () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleReturnKeyDown = () => {
|
||||
submitCaptcha((response) => {
|
||||
useGeneralStore.getState().setCaptchaResponse(response);
|
||||
useGeneralStore.getState().resolveCaptchaPromise(response);
|
||||
submitCaptcha((response) => {
|
||||
setCaptchaResponse(response);
|
||||
resolveCaptchaPromise(response);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const submitCaptcha = (callback) => {
|
||||
setCaptchaResponse(responseRef.current.value);
|
||||
setImageSources([]);
|
||||
setChallengesArray([]);
|
||||
setIsCaptchaOpen(false);
|
||||
|
||||
if (callback) {
|
||||
callback(responseRef.current.value);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
isOpen={isCaptchaOpen}
|
||||
onRequestClose={() => setIsCaptchaOpen(false)}
|
||||
onRequestClose={() => {
|
||||
submitCaptcha();
|
||||
setImageSources([]);
|
||||
setChallengesArray([]);
|
||||
setIsCaptchaOpen(false);}}
|
||||
contentLabel="Captcha Modal"
|
||||
shouldCloseOnEsc={false}
|
||||
shouldCloseOnOverlayClick={false}
|
||||
@@ -87,7 +109,10 @@ const CaptchaModal = () => {
|
||||
<Draggable handle=".modal-header" nodeRef={nodeRef} disabled={isMobile}>
|
||||
<div className="modal-content" ref={nodeRef}>
|
||||
<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 New Thread"}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
|
||||
setCaptchaResponse,
|
||||
setChallengesArray,
|
||||
setIsCaptchaOpen,
|
||||
setIsModEdit,
|
||||
moderatingCommentCid,
|
||||
setResolveCaptchaPromise,
|
||||
} = useGeneralStore(state => state);
|
||||
@@ -221,6 +222,7 @@ const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
|
||||
reason: reason
|
||||
}));
|
||||
setTriggerPublishCommentEdit(true);
|
||||
setIsModEdit(true);
|
||||
handleCloseModal();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 useGeneralStore from '../../hooks/stores/useGeneralStore';
|
||||
import Modal from 'react-modal';
|
||||
@@ -21,8 +21,9 @@ const ReplyModal = ({ isOpen, closeModal }) => {
|
||||
selectedStyle,
|
||||
} = useGeneralStore(state => state);
|
||||
|
||||
const account = useAccount();
|
||||
|
||||
const nodeRef = useRef(null);
|
||||
|
||||
const nameRef = useRef();
|
||||
const commentRef = useRef();
|
||||
const linkRef = useRef();
|
||||
@@ -224,7 +225,11 @@ const ReplyModal = ({ isOpen, closeModal }) => {
|
||||
</div>
|
||||
<div id="form">
|
||||
<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>
|
||||
<input id="name" type="text" placeholder="Embed link" ref={linkRef} />
|
||||
|
||||
@@ -11,7 +11,10 @@ const {version} = packageJson
|
||||
|
||||
|
||||
const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
const selectedStyle = useGeneralStore(state => state.selectedStyle);
|
||||
const {
|
||||
selectedStyle,
|
||||
} = useGeneralStore(state => state);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [expanded, setExpanded] = useState([]);
|
||||
@@ -30,6 +33,7 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
const pubsubRef = useRef();
|
||||
const dataPathRef = useRef();
|
||||
const importRef = useRef();
|
||||
const nameRef = useRef();
|
||||
|
||||
const isValidURL = (url) => {
|
||||
try {
|
||||
@@ -171,6 +175,23 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
const handleAccountChange = (e) => {
|
||||
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 (
|
||||
@@ -229,31 +250,27 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
</li>
|
||||
<ul className="settings-cat" style={{ display: expanded.includes(1) ? 'block' : 'none' }}>
|
||||
<li className="settings-option disc">
|
||||
Export or Import Your Data
|
||||
Account Data
|
||||
</li>
|
||||
<div className="plebbit-options-buttons"
|
||||
style={{ display: expanded.includes(1) ? 'block' : 'none' }}
|
||||
>
|
||||
<button className="save-button"
|
||||
onClick={handleExport}>Export</button>
|
||||
<button className="reset-button"
|
||||
onClick={handleImport}
|
||||
>Import</button>
|
||||
</div>
|
||||
<div className="plebbit-options-buttons">
|
||||
<button className="save-button"
|
||||
onClick={handleExport}>Export</button>
|
||||
<button className="reset-button"
|
||||
onClick={handleImport}
|
||||
>Import</button>
|
||||
</div>
|
||||
<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".
|
||||
</li>
|
||||
<div className="settings-input">
|
||||
<textarea ref={importRef} value={accountJson} />
|
||||
</div>
|
||||
<li className="settings-option disc">
|
||||
Current Account: {account?.name}
|
||||
<li className="settings-option disc" style={{marginTop: '15px'}}>
|
||||
Account Address: u/{account?.author.shortAddress}
|
||||
</li>
|
||||
<li className="settings-tip">
|
||||
Select a different account to use in the dropdown below.
|
||||
</li>
|
||||
</ul>
|
||||
<ul className="settings-cat" style={{ display: expanded.includes(1) ? 'block' : 'none' }}>
|
||||
<li>
|
||||
<div className="settings-input">
|
||||
<select className="settings-select"
|
||||
@@ -268,8 +285,24 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
</select>
|
||||
</div>
|
||||
</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>
|
||||
<li className="settings-cat-lbl">
|
||||
<span className={`${expanded.includes(2) ? 'minus' : 'plus'}`}
|
||||
|
||||
@@ -54,4 +54,45 @@ export const GlobalStyle = createGlobalStyle`
|
||||
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 {
|
||||
float: right;
|
||||
margin: 0;
|
||||
width: 40px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
${({ selectedStyle }) => {
|
||||
|
||||
@@ -107,6 +107,13 @@ export const StyledModal = styled(Modal)`
|
||||
}
|
||||
}
|
||||
|
||||
#save-name {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
height: 23px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.cache-button {
|
||||
margin: auto;
|
||||
margin-bottom: 10px;
|
||||
@@ -148,6 +155,12 @@ export const StyledModal = styled(Modal)`
|
||||
.settings-input {
|
||||
position: relative;
|
||||
padding-left: 23px;
|
||||
|
||||
input {
|
||||
padding-left: 5px;
|
||||
left: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-input::before {
|
||||
|
||||
@@ -54,8 +54,10 @@ export const NavBar = styled.div`
|
||||
transition: top 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.board-select {
|
||||
float: left;
|
||||
.nav-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
strong {
|
||||
@@ -77,7 +79,6 @@ export const NavBar = styled.div`
|
||||
text-decoration: none;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
${({ selectedStyle }) => {
|
||||
@@ -86,14 +87,14 @@ export const NavBar = styled.div`
|
||||
return `font-size: 9pt;
|
||||
color: #b86;
|
||||
|
||||
a, button {
|
||||
a, #button-span {
|
||||
font-weight: 400 !important;
|
||||
padding: 1px !important;
|
||||
text-decoration: none !important;
|
||||
color: maroon !important;
|
||||
}
|
||||
|
||||
a:hover, button:hover {
|
||||
a:hover, #button-span:hover {
|
||||
color: red !important;
|
||||
}
|
||||
|
||||
@@ -114,14 +115,14 @@ export const NavBar = styled.div`
|
||||
return `font-size: 9pt;
|
||||
color: #89a;
|
||||
|
||||
a, button {
|
||||
a, #button-span {
|
||||
font-weight: 400 !important;
|
||||
padding: 1px !important;
|
||||
text-decoration: none !important;
|
||||
color: #34345c !important;
|
||||
}
|
||||
|
||||
a:hover, button:hover {
|
||||
a:hover, #button-span:hover {
|
||||
color: #d00 !important;
|
||||
}
|
||||
|
||||
@@ -132,14 +133,14 @@ export const NavBar = styled.div`
|
||||
case 'Futaba':
|
||||
return `font-size: 11pt;
|
||||
|
||||
a, a:visited, button {
|
||||
a, a:visited, #button-span {
|
||||
font-weight: 400 !important;
|
||||
padding: 1px !important;
|
||||
color: #00e !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
a:hover, button:hover {
|
||||
a:hover, #button-span:hover {
|
||||
color: red !important;
|
||||
}
|
||||
|
||||
@@ -157,14 +158,14 @@ export const NavBar = styled.div`
|
||||
case 'Burichan':
|
||||
return `font-size: 11pt;
|
||||
|
||||
a, a:visited, button {
|
||||
a, a:visited, #button-span {
|
||||
font-weight: 400 !important;
|
||||
padding: 1px !important;
|
||||
color: #34345c !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
a:hover, button:hover {
|
||||
a:hover, #button-span:hover {
|
||||
color: #d00 !important;
|
||||
}
|
||||
|
||||
@@ -179,14 +180,14 @@ export const NavBar = styled.div`
|
||||
return `font-size: 9pt;
|
||||
color: #c5c8c6;
|
||||
|
||||
a, a:visited, button {
|
||||
a, a:visited, #button-span {
|
||||
font-weight: 400 !important;
|
||||
padding: 1px !important;
|
||||
text-decoration: none !important;
|
||||
color: #81a2be !important;
|
||||
}
|
||||
|
||||
a:hover, button:hover {
|
||||
a:hover, #button-span:hover {
|
||||
color: #5f89ab !important;
|
||||
}
|
||||
|
||||
@@ -205,14 +206,14 @@ export const NavBar = styled.div`
|
||||
return `font-size: 9pt;
|
||||
color: #333;
|
||||
|
||||
a, a:visited, button {
|
||||
a, a:visited, #button-span {
|
||||
font-weight: 400 !important;
|
||||
padding: 1px !important;
|
||||
text-decoration: none !important;
|
||||
color: #f60 !important;
|
||||
}
|
||||
|
||||
a:hover, button:hover {
|
||||
a:hover, #button-span:hover {
|
||||
color: #ff3300 !important;
|
||||
}
|
||||
|
||||
@@ -511,7 +512,7 @@ export const PostFormLink = styled.div`
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-wrap a {
|
||||
.btn-wrap a, .btn-wrap span {
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
color: inherit !important;
|
||||
@@ -532,7 +533,7 @@ export const PostFormLink = styled.div`
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-wrap a {
|
||||
.btn-wrap a, .btn-wrap span {
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
color: #34345c !important;
|
||||
@@ -553,7 +554,7 @@ export const PostFormLink = styled.div`
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn-wrap a {
|
||||
.btn-wrap a, .btn-wrap span {
|
||||
text-decoration: none !important;
|
||||
border: none;
|
||||
color: inherit !important;
|
||||
@@ -574,7 +575,7 @@ export const PostFormLink = styled.div`
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn-wrap a {
|
||||
.btn-wrap a, .btn-wrap span {
|
||||
text-decoration: none !important;
|
||||
border: none;
|
||||
color: inherit !important;
|
||||
@@ -595,7 +596,7 @@ export const PostFormLink = styled.div`
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-wrap a {
|
||||
.btn-wrap a, .btn-wrap span {
|
||||
text-decoration: none !important;
|
||||
border: none;
|
||||
color: #707070 !important;
|
||||
@@ -614,7 +615,7 @@ export const PostFormLink = styled.div`
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-wrap a {
|
||||
.btn-wrap a, .btn-wrap span {
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
color: #333 !important;
|
||||
@@ -1472,7 +1473,7 @@ export const TopBar = styled.div`
|
||||
|
||||
.subscribe-button-mobile {
|
||||
margin-right: 10px;
|
||||
margin-top: -2px;
|
||||
margin-top: 1px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
font-size: 13.3333px !important;
|
||||
@@ -1499,7 +1500,7 @@ export const TopBar = styled.div`
|
||||
font-size: 10pt !important;
|
||||
}
|
||||
|
||||
.btn-wrap a {
|
||||
.btn-wrap a, .btn-wrap span {
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
color: inherit !important;
|
||||
@@ -1520,7 +1521,7 @@ export const TopBar = styled.div`
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-wrap a {
|
||||
.btn-wrap a, .btn-wrap span {
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
color: #34345c !important;
|
||||
@@ -1542,11 +1543,9 @@ export const TopBar = styled.div`
|
||||
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;
|
||||
border: none;
|
||||
color: inherit !important;
|
||||
@@ -1569,10 +1568,10 @@ export const TopBar = styled.div`
|
||||
}
|
||||
|
||||
.subscribe-button-mobile {
|
||||
margin-top: -5px !important;
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
|
||||
.btn-wrap a {
|
||||
.btn-wrap a, .btn-wrap span {
|
||||
text-decoration: none !important;
|
||||
border: none;
|
||||
color: inherit !important;
|
||||
@@ -1593,7 +1592,7 @@ export const TopBar = styled.div`
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-wrap a {
|
||||
.btn-wrap a, .btn-wrap span {
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
color: #707070 !important;
|
||||
@@ -1618,7 +1617,7 @@ export const TopBar = styled.div`
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-wrap a {
|
||||
.btn-wrap a, .btn-wrap span {
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
color: #333 !important;
|
||||
@@ -1721,15 +1720,13 @@ export const BoardForm = styled.div`
|
||||
}
|
||||
|
||||
#post-menu {
|
||||
ul {
|
||||
background-color: #f0e0d6;
|
||||
border: 1px solid #d9bfb7;
|
||||
border-bottom: 1px solid #d9bfb7;
|
||||
border-right-width: 2px;
|
||||
}
|
||||
border-right: 1px solid #d9bfb7;
|
||||
border-bottom: 2px solid #d9bfb7;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid #ccc;
|
||||
border: 1px solid #d9bfb7;
|
||||
border-bottom: none;
|
||||
background-color: #f0e0d6;
|
||||
|
||||
:hover {
|
||||
background-color: #ffe;
|
||||
@@ -1746,14 +1743,13 @@ export const BoardForm = styled.div`
|
||||
}
|
||||
|
||||
#post-menu {
|
||||
ul {
|
||||
background-color: #d6daf0;
|
||||
border: 1px solid #b7c5d9;
|
||||
border-right-width: 2px;
|
||||
}
|
||||
|
||||
border-right: 1px solid #b7c5d9;
|
||||
border-bottom: 2px solid #b7c5d9;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid #b7c5d9;
|
||||
border: 1px solid #b7c5d9;
|
||||
border-bottom: none;
|
||||
background-color: #d6daf0;
|
||||
|
||||
:hover {
|
||||
background-color: #eef2ff;
|
||||
@@ -1768,14 +1764,11 @@ export const BoardForm = styled.div`
|
||||
|
||||
#post-menu {
|
||||
font-size: 13px !important;
|
||||
|
||||
ul {
|
||||
background-color: #f0e0d6;
|
||||
border: 1px solid #d9bfb7;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
border: 1px solid #d9bfb7;
|
||||
border-bottom: none;
|
||||
|
||||
li {
|
||||
background-color: #f0e0d6;
|
||||
border-bottom: 1px solid #d9bfb7;
|
||||
|
||||
:hover {
|
||||
@@ -1791,14 +1784,11 @@ export const BoardForm = styled.div`
|
||||
|
||||
#post-menu {
|
||||
font-size: 13px !important;
|
||||
|
||||
ul {
|
||||
background-color: #d6daf0;
|
||||
border: 1px solid #b7c5d9;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
border: 1px solid #b7c5d9;
|
||||
border-bottom: none;
|
||||
|
||||
li {
|
||||
background-color: #d6daf0;
|
||||
border-bottom: 1px solid #b7c5d9;
|
||||
|
||||
:hover {
|
||||
@@ -1814,21 +1804,18 @@ export const BoardForm = styled.div`
|
||||
}
|
||||
|
||||
#post-menu {
|
||||
ul {
|
||||
background-color: #282a2e;
|
||||
border: 1px solid #000;
|
||||
border-bottom: none;
|
||||
}
|
||||
border: 1px solid #000;
|
||||
border-bottom: none;
|
||||
|
||||
li {
|
||||
background-color: #282a2e;
|
||||
border-bottom: 1px solid #000;
|
||||
|
||||
:hover {
|
||||
background-color: #1d1f21;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}`;
|
||||
|
||||
case 'Photon':
|
||||
return `.highlighted {
|
||||
@@ -1837,13 +1824,11 @@ export const BoardForm = styled.div`
|
||||
}
|
||||
|
||||
#post-menu {
|
||||
ul {
|
||||
background-color: #ddd;
|
||||
border: 1px solid #ccc;
|
||||
border-bottom: none;
|
||||
}
|
||||
border: 1px solid #ccc;
|
||||
border-bottom: none;
|
||||
|
||||
li {
|
||||
background-color: #ddd;
|
||||
border-bottom: 1px solid #ccc;
|
||||
|
||||
:hover {
|
||||
|
||||
@@ -77,4 +77,171 @@ export const Threads = styled.div`
|
||||
display: block;
|
||||
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 '';
|
||||
}
|
||||
}}
|
||||
`;
|
||||
@@ -221,43 +221,43 @@ const All = () => {
|
||||
</span>
|
||||
<span className="nav">
|
||||
[
|
||||
<button style={{all: 'unset', cursor: 'pointer'}} onClick={
|
||||
<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</button>
|
||||
}>Create Board</span>
|
||||
]
|
||||
[
|
||||
<Link to={`/p/all/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
]
|
||||
[
|
||||
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}}
|
||||
)}>Home</Link>
|
||||
<Link to="/">Home</Link>
|
||||
]
|
||||
</span>
|
||||
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
|
||||
<div className="board-select">
|
||||
<strong>Board</strong>
|
||||
|
||||
<select id="board-select-mobile" value="all" onChange={handleSelectChange}>
|
||||
<option value="all">All</option>
|
||||
<option value="subscriptions">Subscriptions</option>
|
||||
{defaultSubplebbits.map(subplebbit => (
|
||||
<option key={`option-${subplebbit.address}`} value={subplebbit.address}
|
||||
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
|
||||
))}
|
||||
</select>
|
||||
<button style={{all: 'unset', cursor: 'pointer'}} onClick={
|
||||
<div className="nav-container">
|
||||
<div className="board-select">
|
||||
<strong>Board</strong>
|
||||
|
||||
<select id="board-select-mobile" value="all" onChange={handleSelectChange}>
|
||||
<option value="all">All</option>
|
||||
<option value="subscriptions">Subscriptions</option>
|
||||
{defaultSubplebbits.map(subplebbit => (
|
||||
<option key={`option-${subplebbit.address}`} value={subplebbit.address}
|
||||
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
|
||||
))}
|
||||
</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</button>
|
||||
</div>
|
||||
<div className="page-jump">
|
||||
<Link to={`/p/all/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
|
||||
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}}
|
||||
)}>Home</Link>
|
||||
}>Create Board</span>
|
||||
</div>
|
||||
<div className="page-jump">
|
||||
<Link to={`/p/all/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
|
||||
<Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="separator-mobile"> </div>
|
||||
@@ -485,7 +485,7 @@ const All = () => {
|
||||
onMouseOver={() => {setIsImageSearchOpen(true)}}
|
||||
onMouseLeave={() => {setIsImageSearchOpen(false)}}>
|
||||
Image search »
|
||||
<ul className="dropdown-menu"
|
||||
<ul className="dropdown-menu post-menu-catalog"
|
||||
style={{display: isImageSearchOpen ? 'block': 'none'}}>
|
||||
<li>
|
||||
<a
|
||||
@@ -673,7 +673,7 @@ const All = () => {
|
||||
onMouseOver={() => {setIsImageSearchOpen(true)}}
|
||||
onMouseLeave={() => {setIsImageSearchOpen(false)}}>
|
||||
Image search »
|
||||
<ul className="dropdown-menu"
|
||||
<ul className="dropdown-menu post-menu-catalog"
|
||||
style={{display: isImageSearchOpen ? 'block': 'none'}}>
|
||||
<li>
|
||||
<a
|
||||
|
||||
@@ -122,43 +122,43 @@ const AllCatalog = () => {
|
||||
</span>
|
||||
<span className="nav">
|
||||
[
|
||||
<button style={{all: 'unset', cursor: 'pointer'}} onClick={
|
||||
<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</button>
|
||||
}>Create Board</span>
|
||||
]
|
||||
[
|
||||
<Link to={`/p/all/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
]
|
||||
[
|
||||
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}}
|
||||
)}>Home</Link>
|
||||
<Link to="/">Home</Link>
|
||||
]
|
||||
</span>
|
||||
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
|
||||
<div className="board-select">
|
||||
<strong>Board</strong>
|
||||
|
||||
<select id="board-select-mobile" value="all" onChange={handleSelectChange}>
|
||||
<option value="All">All</option>
|
||||
<option value="subscriptions">Subscriptions</option>
|
||||
{defaultSubplebbits.map(subplebbit => (
|
||||
<option key={`option-${subplebbit.address}`} value={subplebbit.address}
|
||||
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
|
||||
))}
|
||||
</select>
|
||||
<button style={{all: 'unset', cursor: 'pointer'}} onClick={
|
||||
<div className="nav-container">
|
||||
<div className="board-select">
|
||||
<strong>Board</strong>
|
||||
|
||||
<select id="board-select-mobile" value="all" onChange={handleSelectChange}>
|
||||
<option value="all">All</option>
|
||||
<option value="subscriptions">Subscriptions</option>
|
||||
{defaultSubplebbits.map(subplebbit => (
|
||||
<option key={`option-${subplebbit.address}`} value={subplebbit.address}
|
||||
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
|
||||
))}
|
||||
</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</button>
|
||||
</div>
|
||||
<div className="page-jump">
|
||||
<Link to={`/p/all/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
|
||||
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}}
|
||||
)}>Home</Link>
|
||||
}>Create Board</span>
|
||||
</div>
|
||||
<div className="page-jump">
|
||||
<Link to={`/p/all/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
|
||||
<Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="separator-mobile"> </div>
|
||||
|
||||
+238
-185
@@ -1,4 +1,5 @@
|
||||
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { confirmAlert } from 'react-confirm-alert';
|
||||
@@ -10,6 +11,7 @@ import { debounce } from 'lodash';
|
||||
import useGeneralStore from '../../hooks/stores/useGeneralStore';
|
||||
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 { PostMenuCatalog } from '../styled/views/Catalog.styled';
|
||||
import EditModal from '../modals/EditModal';
|
||||
import ImageBanner from '../ImageBanner';
|
||||
import ModerationModal from '../modals/ModerationModal';
|
||||
@@ -39,6 +41,8 @@ const Board = () => {
|
||||
setChallengesArray,
|
||||
defaultSubplebbits,
|
||||
editedComment,
|
||||
setIsAuthorDelete,
|
||||
setIsAuthorEdit,
|
||||
setIsCaptchaOpen,
|
||||
isModerationOpen, setIsModerationOpen,
|
||||
isSettingsOpen, setIsSettingsOpen,
|
||||
@@ -66,6 +70,8 @@ const Board = () => {
|
||||
const linkRef = useRef();
|
||||
const threadMenuRefs = useRef({});
|
||||
const replyMenuRefs = useRef({});
|
||||
const postMenuRef = useRef(null);
|
||||
const postMenuCatalogRef = useRef(null);
|
||||
|
||||
const { feed, hasMore, loadMore } = useFeed({subplebbitAddresses: [`${selectedAddress}`], sortType: 'new'});
|
||||
const subplebbit = useSubplebbit({subplebbitAddress: selectedAddress});
|
||||
@@ -81,10 +87,11 @@ const Board = () => {
|
||||
const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false);
|
||||
const [selectedFeed, setSelectedFeed] = useState(feed);
|
||||
const [deletePost, setDeletePost] = useState(false);
|
||||
const [rotatedStates, setRotatedStates] = useState({});
|
||||
const [isImageSearchOpen, setIsImageSearchOpen] = useState(false);
|
||||
const [isModerator, setIsModerator] = useState(false);
|
||||
const [commentCid, setCommentCid] = useState(null);
|
||||
const [menuPosition, setMenuPosition] = useState({top: 0, left: 0});
|
||||
const [openMenuCid, setOpenMenuCid] = useState(null);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState(null);
|
||||
const [successMessage, setSuccessMessage] = useState(null);
|
||||
@@ -105,12 +112,29 @@ const Board = () => {
|
||||
}, [account?.author.address, subplebbit.roles]);
|
||||
|
||||
|
||||
const handleOptionClick = (threadCid) => {
|
||||
setRotatedStates(prevState => ({
|
||||
...prevState,
|
||||
[threadCid]: false
|
||||
}));
|
||||
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]);
|
||||
|
||||
|
||||
|
||||
const errorString = useMemo(() => {
|
||||
@@ -397,6 +421,8 @@ const Board = () => {
|
||||
<button onClick={onClose}>No</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsAuthorDelete(true);
|
||||
setIsAuthorEdit(false);
|
||||
setCommentCid(commentCid);
|
||||
setPublishCommentEditOptions(prevOptions => ({
|
||||
...prevOptions,
|
||||
@@ -418,6 +444,8 @@ const Board = () => {
|
||||
|
||||
const handleAuthorEditClick = (comment) => {
|
||||
handleOptionClick(comment.cid);
|
||||
setIsAuthorEdit(true);
|
||||
setIsAuthorDelete(false);
|
||||
setCommentCid(comment.cid);
|
||||
setOriginalCommentContent(comment.content);
|
||||
setIsEditModalOpen(true);
|
||||
@@ -437,7 +465,7 @@ const Board = () => {
|
||||
if (editedComment !== '') {
|
||||
setTriggerPublishCommentEdit(true);
|
||||
}
|
||||
}, [editedComment]);
|
||||
}, [editedComment, setIsAuthorEdit]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
@@ -536,42 +564,43 @@ const Board = () => {
|
||||
</span>
|
||||
<span className="nav">
|
||||
[
|
||||
<button style={{all: 'unset', cursor: 'pointer'}} onClick={
|
||||
<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</button>
|
||||
}>Create Board</span>
|
||||
]
|
||||
[
|
||||
<Link to={`/p/${selectedAddress}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
]
|
||||
[
|
||||
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}}
|
||||
)}>Home</Link>
|
||||
<Link to="/">Home</Link>
|
||||
]
|
||||
</span>
|
||||
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
|
||||
<div className="board-select">
|
||||
<strong>Board</strong>
|
||||
|
||||
<select id="board-select-mobile" value={selectedAddress} onChange={handleSelectChange}>
|
||||
<option value="all">All</option>
|
||||
<option value="subscriptions">Subscriptions</option>
|
||||
{defaultSubplebbits.map(subplebbit => (
|
||||
<option key={`option-${subplebbit.address}`} value={subplebbit.address}
|
||||
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
|
||||
))}
|
||||
</select>
|
||||
<button style={{all: 'unset', cursor: 'pointer'}} onClick={
|
||||
<div className="nav-container">
|
||||
<div className="board-select">
|
||||
<strong>Board</strong>
|
||||
|
||||
<select id="board-select-mobile" value={selectedAddress} onChange={handleSelectChange}>
|
||||
<option value="all">All</option>
|
||||
<option value="subscriptions">Subscriptions</option>
|
||||
{defaultSubplebbits.map(subplebbit => (
|
||||
<option key={`option-${subplebbit.address}`} value={subplebbit.address}
|
||||
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
|
||||
))}
|
||||
</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</button>
|
||||
</div>
|
||||
<div className="page-jump">
|
||||
<Link to={`/p/${selectedAddress}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
|
||||
<Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
|
||||
}>Create Board</span>
|
||||
</div>
|
||||
<div className="page-jump">
|
||||
<Link to={`/p/${selectedAddress}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
|
||||
<Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="separator-mobile"> </div>
|
||||
@@ -611,7 +640,11 @@ const Board = () => {
|
||||
<tr data-type="Name">
|
||||
<td id="td-name">Name</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>
|
||||
</tr>
|
||||
<tr data-type="Subject">
|
||||
@@ -663,14 +696,18 @@ const Board = () => {
|
||||
<>
|
||||
<span className="subscribe-button-desktop">
|
||||
[
|
||||
<button id="subscribe" style={{all: 'unset', cursor: 'pointer'}}
|
||||
onClick={() => handleSubscribe()}>{subscribed ? "Unsubscribe" : "Subscribe"}
|
||||
</button>
|
||||
<span id="subscribe" style={{cursor: 'pointer'}}>
|
||||
<span onClick={() => handleSubscribe()}>
|
||||
{subscribed ? "Unsubscribe" : "Subscribe"}
|
||||
</span>
|
||||
</span>
|
||||
]
|
||||
</span>
|
||||
<button className="subscribe-button-mobile btn-wrap"
|
||||
onClick={() => handleSubscribe()}>{subscribed ? "Unsubscribe" : "Subscribe"}
|
||||
</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"}}>
|
||||
@@ -829,91 +866,99 @@ const Board = () => {
|
||||
<PostMenu
|
||||
key={`pmb-${index}`}
|
||||
title="Post menu"
|
||||
ref={el => threadMenuRefs.current[thread.cid] = el}
|
||||
ref={el => {
|
||||
threadMenuRefs.current[thread.cid] = el;
|
||||
postMenuRef.current = el;
|
||||
}}
|
||||
className='post-menu-button'
|
||||
rotated={rotatedStates[thread.cid]}
|
||||
onClick={() => {
|
||||
rotated={openMenuCid === thread.cid}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
const rect = threadMenuRefs.current[thread.cid].getBoundingClientRect();
|
||||
const menu = document.querySelector(`.post-menu-thread-${thread.cid}`);
|
||||
menu.style.top = `calc(${rect.top}px + 17px)`;
|
||||
menu.style.left = `${rect.left}px`;
|
||||
|
||||
setRotatedStates(prevState => ({
|
||||
...prevState,
|
||||
[thread.cid]: !prevState[thread.cid]
|
||||
}));
|
||||
setMenuPosition({top: rect.top + window.scrollY, left: rect.left});
|
||||
setOpenMenuCid(prevCid => (prevCid === thread.cid ? null : thread.cid));
|
||||
}}
|
||||
>
|
||||
▶
|
||||
</PostMenu>
|
||||
<div id="post-menu" className={`post-menu-thread post-menu-thread-${thread.cid}`}
|
||||
style={{ display: rotatedStates[thread.cid] ? 'block' : 'none' }}>
|
||||
<ul>
|
||||
<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={() => {
|
||||
{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);
|
||||
setDeletePost(true);
|
||||
}}>
|
||||
Delete post
|
||||
Mod tools
|
||||
</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"
|
||||
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>
|
||||
</>
|
||||
) : 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
|
||||
)}
|
||||
<div key={`bi-${index}`} id="backlink-id" className="backlink">
|
||||
{thread.replies?.pages?.topAll.comments
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
@@ -1027,91 +1072,99 @@ const Board = () => {
|
||||
<PostMenu
|
||||
key={`pmb-${index}`}
|
||||
title="Post menu"
|
||||
ref={el => replyMenuRefs.current[reply.cid] = el}
|
||||
ref={el => {
|
||||
replyMenuRefs.current[reply.cid] = el;
|
||||
postMenuRef.current = el;
|
||||
}}
|
||||
className='post-menu-button'
|
||||
rotated={rotatedStates[reply.cid]}
|
||||
onClick={() => {
|
||||
rotated={openMenuCid === reply.cid}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
const rect = replyMenuRefs.current[reply.cid].getBoundingClientRect();
|
||||
const menu = document.querySelector(`.post-menu-reply-${reply.cid}`);
|
||||
menu.style.top = `calc(${rect.top + window.scrollY}px - 258px)`;
|
||||
menu.style.left = `${rect.left}px`;
|
||||
|
||||
setRotatedStates(prevState => ({
|
||||
...prevState,
|
||||
[reply.cid]: !prevState[reply.cid]
|
||||
}));
|
||||
}}
|
||||
setMenuPosition({top: rect.top + window.scrollY, left: rect.left});
|
||||
setOpenMenuCid(prevCid => (prevCid === reply.cid ? null : reply.cid));
|
||||
}}
|
||||
>
|
||||
▶
|
||||
</PostMenu>
|
||||
<div id="post-menu" className={`post-menu-reply post-menu-reply-${reply.cid}`}
|
||||
style={{ display: rotatedStates[reply.cid] ? 'block' : 'none' }}>
|
||||
<ul>
|
||||
<li onClick={() => handleOptionClick(reply.cid)}>Hide post</li>
|
||||
{reply.author.shortAddress === account?.author.shortAddress ? (
|
||||
<>
|
||||
<li onClick={() => handleAuthorEditClick(reply)}>Edit post</li>
|
||||
<li onClick={() => handleAuthorDeleteClick(reply.cid)}>Delete post</li>
|
||||
</>
|
||||
) : null}
|
||||
{isModerator ? (
|
||||
<>
|
||||
{reply.author.shortAddress === account?.author.shortAddress ? (
|
||||
null
|
||||
) : (
|
||||
<li onClick={() => {
|
||||
{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-reply post-menu-reply-${reply.cid}`}
|
||||
style={{ display: openMenuCid === reply.cid ? 'block' : 'none' }}
|
||||
>
|
||||
<ul className="post-menu-catalog">
|
||||
<li onClick={() => handleOptionClick(reply.cid)}>Hide post</li>
|
||||
{reply.author.shortAddress === account?.author.shortAddress ? (
|
||||
<>
|
||||
<li onClick={() => handleAuthorEditClick(reply)}>Edit post</li>
|
||||
<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)
|
||||
setIsModerationOpen(true);
|
||||
handleOptionClick(reply.cid);
|
||||
setDeletePost(true);
|
||||
}}>
|
||||
Delete post
|
||||
Mod tools
|
||||
</li>
|
||||
)}
|
||||
<li
|
||||
onClick={() => {
|
||||
setModeratingCommentCid(reply.cid)
|
||||
setIsModerationOpen(true);
|
||||
handleOptionClick(reply.cid);
|
||||
}}>
|
||||
Mod tools
|
||||
</li>
|
||||
</>
|
||||
) : null}
|
||||
{(replyMediaInfo && (
|
||||
replyMediaInfo.type === 'image' ||
|
||||
(replyMediaInfo.type === 'webpage' &&
|
||||
replyMediaInfo.thumbnail))) ? (
|
||||
<li
|
||||
onMouseOver={() => {setIsImageSearchOpen(true)}}
|
||||
onMouseLeave={() => {setIsImageSearchOpen(false)}}>
|
||||
Image search »
|
||||
<ul className="dropdown-menu"
|
||||
style={{display: isImageSearchOpen ? 'block': 'none'}}>
|
||||
<li onClick={() => handleOptionClick(reply.cid)}>
|
||||
<a
|
||||
href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Google</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(reply.cid)}>
|
||||
<a
|
||||
href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Yandex</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(reply.cid)}>
|
||||
<a
|
||||
href={`https://saucenao.com/search.php?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>SauceNAO</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
) : null
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
{(replyMediaInfo && (
|
||||
replyMediaInfo.type === 'image' ||
|
||||
(replyMediaInfo.type === 'webpage' &&
|
||||
replyMediaInfo.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(reply.cid)}>
|
||||
<a
|
||||
href={`https://lens.google.com/uploadbyurl?url=${replyMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Google</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(reply.cid)}>
|
||||
<a
|
||||
href={`https://yandex.com/images/search?url=${replyMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Yandex</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(reply.cid)}>
|
||||
<a
|
||||
href={`https://saucenao.com/search.php?url=${replyMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>SauceNAO</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
) : null
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</PostMenuCatalog>, document.body
|
||||
)}
|
||||
<div id="backlink-id" className="backlink">
|
||||
{reply.replies?.pages?.topAll.comments
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
|
||||
@@ -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 InfiniteScroll from 'react-infinite-scroller';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { confirmAlert } from 'react-confirm-alert';
|
||||
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 useGeneralStore from '../../hooks/stores/useGeneralStore';
|
||||
import { Container, NavBar, Header, Break, PostForm, PostFormLink, PostFormTable } from '../styled/views/Board.styled';
|
||||
import { Threads } from '../styled/views/Catalog.styled';
|
||||
import { TopBar, Footer } from '../styled/views/Thread.styled';
|
||||
import { Container, NavBar, Header, Break, PostForm, PostFormLink, PostFormTable, PostMenu, BoardForm } from '../styled/views/Board.styled';
|
||||
import { Threads, PostMenuCatalog } from '../styled/views/Catalog.styled';
|
||||
import { TopBar, Footer, AuthorDeleteAlert } from '../styled/views/Thread.styled';
|
||||
import CatalogLoader from '../CatalogLoader';
|
||||
import EditModal from '../modals/EditModal';
|
||||
import ImageBanner from '../ImageBanner';
|
||||
import ModerationModal from '../modals/ModerationModal';
|
||||
import OfflineIndicator from '../OfflineIndicator';
|
||||
import SettingsModal from '../modals/SettingsModal';
|
||||
import getCommentMediaInfo from '../../utils/getCommentMediaInfo';
|
||||
@@ -28,8 +32,13 @@ const Catalog = () => {
|
||||
captchaResponse, setCaptchaResponse,
|
||||
setChallengesArray,
|
||||
defaultSubplebbits,
|
||||
editedComment,
|
||||
setIsAuthorDelete,
|
||||
setIsAuthorEdit,
|
||||
setIsCaptchaOpen,
|
||||
isModerationOpen, setIsModerationOpen,
|
||||
isSettingsOpen, setIsSettingsOpen,
|
||||
setModeratingCommentCid,
|
||||
setPendingComment,
|
||||
setPendingCommentIndex,
|
||||
setResolveCaptchaPromise,
|
||||
@@ -45,25 +54,77 @@ const Catalog = () => {
|
||||
const subjectRef = useRef();
|
||||
const commentRef = useRef();
|
||||
const linkRef = useRef();
|
||||
const threadMenuRefs = useRef({});
|
||||
const postMenuRef = useRef(null);
|
||||
const postMenuCatalogRef = useRef(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState(null);
|
||||
const [successMessage] = useState(null);
|
||||
const [successMessage, setSuccessMessage] = useState(null);
|
||||
useError(errorMessage, [errorMessage]);
|
||||
useSuccess(successMessage, [successMessage]);
|
||||
|
||||
const [triggerPublishComment, setTriggerPublishComment] = useState(false);
|
||||
const [prevScrollPos, setPrevScrollPos] = useState(0);
|
||||
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 { subplebbitAddress } = useParams();
|
||||
const subplebbit = useSubplebbit({subplebbitAddress: selectedAddress});
|
||||
const account = useAccount();
|
||||
|
||||
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(() => {
|
||||
setSelectedAddress(subplebbitAddress);
|
||||
}, [subplebbitAddress, setSelectedAddress]);
|
||||
@@ -85,6 +146,8 @@ const Catalog = () => {
|
||||
setErrorMessage(errorString);
|
||||
}
|
||||
}, [errorString]);
|
||||
|
||||
|
||||
const { subscribed, subscribe, unsubscribe } = useSubscribe({subplebbitAddress: selectedAddress});
|
||||
|
||||
// temporary title from JSON, gets subplebbitAddress from URL
|
||||
@@ -119,8 +182,12 @@ const Catalog = () => {
|
||||
|
||||
const onChallengeVerification = (challengeVerification) => {
|
||||
if (challengeVerification.challengeSuccess === true) {
|
||||
navigate(`/p/${selectedAddress}/c/${challengeVerification.publication?.cid}`);
|
||||
console.log('challenge success');
|
||||
if (challengeVerification.publication?.cid !== undefined) {
|
||||
navigate(`/p/${subplebbitAddress}/c/${challengeVerification.publication?.cid}`);
|
||||
console.log('challenge success');
|
||||
} else {
|
||||
setSuccessMessage('Challenge Success');
|
||||
}
|
||||
}
|
||||
else if (challengeVerification.challengeSuccess === false) {
|
||||
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
|
||||
const handleSelectChange = (event) => {
|
||||
const selected = event.target.value;
|
||||
@@ -295,6 +454,16 @@ const Catalog = () => {
|
||||
selectedStyle={selectedStyle}
|
||||
isOpen={isSettingsOpen}
|
||||
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}>
|
||||
<>
|
||||
<span className="boardList">
|
||||
@@ -318,43 +487,43 @@ const Catalog = () => {
|
||||
</span>
|
||||
<span className="nav">
|
||||
[
|
||||
<button style={{all: 'unset', cursor: 'pointer'}} onClick={
|
||||
<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</button>
|
||||
}>Create Board</span>
|
||||
]
|
||||
[
|
||||
<Link to={`/p/${selectedAddress}/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
]
|
||||
[
|
||||
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}}
|
||||
)}>Home</Link>
|
||||
<Link to="/">Home</Link>
|
||||
]
|
||||
</span>
|
||||
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
|
||||
<div className="board-select">
|
||||
<strong>Board</strong>
|
||||
|
||||
<select id="board-select-mobile" value={selectedAddress} onChange={handleSelectChange}>
|
||||
<option value="all">All</option>
|
||||
<option value="subscriptions">Subscriptions</option>
|
||||
{defaultSubplebbits.map(subplebbit => (
|
||||
<option key={`option-${subplebbit.address}`} value={subplebbit.address}
|
||||
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
|
||||
))}
|
||||
</select>
|
||||
<button style={{all: 'unset', cursor: 'pointer'}} onClick={
|
||||
<div className="nav-container">
|
||||
<div className="board-select">
|
||||
<strong>Board</strong>
|
||||
|
||||
<select id="board-select-mobile" value={selectedAddress} onChange={handleSelectChange}>
|
||||
<option value="all">All</option>
|
||||
<option value="subscriptions">Subscriptions</option>
|
||||
{defaultSubplebbits.map(subplebbit => (
|
||||
<option key={`option-${subplebbit.address}`} value={subplebbit.address}
|
||||
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
|
||||
))}
|
||||
</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</button>
|
||||
</div>
|
||||
<div className="page-jump">
|
||||
<Link to={`/p/${selectedAddress}/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
|
||||
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}}
|
||||
)}>Home</Link>
|
||||
}>Create Board</span>
|
||||
</div>
|
||||
<div className="page-jump">
|
||||
<Link to={`/p/${selectedAddress}/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
|
||||
<Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="separator-mobile"> </div>
|
||||
@@ -396,7 +565,11 @@ const Catalog = () => {
|
||||
<tr data-type="Name">
|
||||
<td id="td-name">Name</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>
|
||||
</tr>
|
||||
<tr data-type="Subject">
|
||||
@@ -447,16 +620,20 @@ const Catalog = () => {
|
||||
{feed.length > 0 ? (
|
||||
<>
|
||||
<span className="subscribe-button-desktop">
|
||||
[
|
||||
<button id="subscribe" style={{all: 'unset', cursor: 'pointer'}}
|
||||
onClick={() => handleSubscribe()}>{subscribed ? "Unsubscribe" : "Subscribe"}
|
||||
</button>
|
||||
]
|
||||
</span>
|
||||
<button className="subscribe-button-mobile btn-wrap"
|
||||
onClick={() => handleSubscribe()}>{subscribed ? "Unsubscribe" : "Subscribe"}
|
||||
</button>
|
||||
</>
|
||||
[
|
||||
<span id="subscribe" style={{cursor: 'pointer'}}>
|
||||
<span onClick={() => handleSubscribe()}>
|
||||
{subscribed ? "Unsubscribe" : "Subscribe"}
|
||||
</span>
|
||||
</span>
|
||||
]
|
||||
</span>
|
||||
<span className="subscribe-button-mobile">
|
||||
<span className="btn-wrap" onClick={() => handleSubscribe()}>
|
||||
{subscribed ? "Unsubscribe" : "Subscribe"}
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<div id="stats" style={{float: "right", marginTop: "5px"}}>
|
||||
<span className={stateString ? "ellipsis" : ""}>{stateString}</span>
|
||||
@@ -484,45 +661,44 @@ const Catalog = () => {
|
||||
const commentMediaInfo = getCommentMediaInfo(thread);
|
||||
const fallbackImgUrl = "assets/filedeleted-res.gif";
|
||||
return (
|
||||
<Link style={{all: "unset", cursor: "pointer"}} key={`link-${index}`} to={`/p/${selectedAddress}/c/${thread.cid}`}
|
||||
onClick={() => setSelectedThread(thread.cid)}>
|
||||
<div key={`thread-${index}`} className="thread">
|
||||
{commentMediaInfo?.url ? (
|
||||
<Fragment key="f-catalog">
|
||||
{commentMediaInfo?.type === "webpage" ? (
|
||||
thread.thumbnailUrl ? (
|
||||
<img className="card" key={`img-${index}`}
|
||||
src={commentMediaInfo.thumbnail} alt={commentMediaInfo.type}
|
||||
onError={(e) => {
|
||||
e.target.src = fallbackImgUrl
|
||||
e.target.onerror = null;
|
||||
}} />
|
||||
) : null
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "image" ? (
|
||||
<img className="card" key={`img-${index}`}
|
||||
src={commentMediaInfo.url} alt={commentMediaInfo.type}
|
||||
onError={(e) => {
|
||||
e.target.src = fallbackImgUrl
|
||||
e.target.onerror = null;}} />
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "video" ? (
|
||||
<div key={`thread-${index}`} className="thread"
|
||||
onMouseOver={() => {setIsHoveringOnThread(thread.cid)}}
|
||||
onMouseLeave={() => {setIsHoveringOnThread('')}}>
|
||||
{commentMediaInfo?.url ? (
|
||||
<Link style={{all: "unset", cursor: "pointer"}} key={`link-${index}`} to={`/p/${selectedAddress}/c/${thread.cid}`}
|
||||
onClick={() => setSelectedThread(thread.cid)}>
|
||||
{commentMediaInfo?.type === "webpage" ? (
|
||||
thread.thumbnailUrl ? (
|
||||
<img className="card" key={`img-${index}`}
|
||||
src={commentMediaInfo.thumbnail} alt={commentMediaInfo.type}
|
||||
onError={(e) => {
|
||||
e.target.src = fallbackImgUrl
|
||||
e.target.onerror = null;
|
||||
}} />
|
||||
) : null
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "image" ? (
|
||||
<img className="card" key={`img-${index}`}
|
||||
src={commentMediaInfo.url} alt={commentMediaInfo.type}
|
||||
onError={(e) => {
|
||||
e.target.src = fallbackImgUrl
|
||||
e.target.onerror = null;}} />
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "video" ? (
|
||||
<video className="card" key={`fti-${index}`}
|
||||
src={commentMediaInfo.url}
|
||||
alt={commentMediaInfo.type}
|
||||
style={{ pointerEvents: "none" }}
|
||||
onError={(e) => e.target.src = fallbackImgUrl} />
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "audio" ? (
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "audio" ? (
|
||||
<audio className="card" controls
|
||||
key={`fti-${index}`}
|
||||
src={commentMediaInfo.url}
|
||||
alt={commentMediaInfo.type}
|
||||
style={{ pointerEvents: "none" }}
|
||||
onError={(e) => e.target.src = fallbackImgUrl} />
|
||||
) : null}
|
||||
</Fragment>
|
||||
) : null}
|
||||
) : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<div key={`ti-${index}`} className="thread-icons" >
|
||||
{thread.pinned ? (
|
||||
<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" />
|
||||
) : null}
|
||||
</div>
|
||||
<div key={`meta-${index}`} className="meta" title="(R)eplies / (I)mage Replies" >
|
||||
R:
|
||||
<b key={`b-${index}`}>{thread.replyCount}</b>
|
||||
</div>
|
||||
<div key={`t-${index}`} className="teaser">
|
||||
<BoardForm selectedStyle={selectedStyle}
|
||||
style={{ all: "unset"}}>
|
||||
<div key={`meta-${index}`} className="meta" title="(R)eplies / (I)mage Replies" >
|
||||
R:
|
||||
<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>
|
||||
{thread.content ? `: ${thread.content}` : null}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</Link>
|
||||
)})}
|
||||
</InfiniteScroll>
|
||||
) : (<CatalogLoader />)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { Fragment, useEffect, useRef } from 'react';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { useAccount } from '@plebbit/plebbit-react-hooks';
|
||||
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 {
|
||||
setBodyStyle,
|
||||
bodyStyle, setBodyStyle,
|
||||
defaultSubplebbits,
|
||||
setSelectedAddress,
|
||||
setSelectedStyle,
|
||||
selectedStyle, setSelectedStyle,
|
||||
setSelectedTitle
|
||||
} = useGeneralStore(state => state);
|
||||
|
||||
const account = useAccount();
|
||||
const inputRef = useRef(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const inputRef = useRef(null);
|
||||
const prevStyle = useRef(selectedStyle);
|
||||
const prevBodyStyle = useRef(bodyStyle);
|
||||
|
||||
// prevent dark mode
|
||||
useEffect(() => {
|
||||
const currentPrevStyle = prevStyle.current;
|
||||
const currentPrevBodyStyle = prevBodyStyle.current;
|
||||
|
||||
setBodyStyle({
|
||||
background: "#ffe url(assets/fade.png) top repeat-x",
|
||||
color: "maroon",
|
||||
fontFamily: "Helvetica, Arial, sans-serif"
|
||||
});
|
||||
setSelectedStyle("Yotsuba");
|
||||
|
||||
return () => {
|
||||
setSelectedStyle(currentPrevStyle);
|
||||
setBodyStyle(currentPrevBodyStyle);
|
||||
};
|
||||
}, [setBodyStyle, setSelectedStyle]);
|
||||
|
||||
|
||||
@@ -103,7 +114,7 @@ const Home = () => {
|
||||
<Link to="/p/subscriptions" id="view-all" onClick={()=> {window.scrollTo(0, 0)}}>[view all]</Link>
|
||||
<br />
|
||||
{account?.subscriptions?.map((subscription, index) => (
|
||||
<>
|
||||
<Fragment key={`frag-${index}`}>
|
||||
<Link key={`sub-${index}`} className="boardlink"
|
||||
onClick={()=> {window.scrollTo(0, 0)}}
|
||||
to={`/p/${subscription}`}>
|
||||
@@ -111,11 +122,12 @@ const Home = () => {
|
||||
{subscription}
|
||||
</Link>
|
||||
<OfflineIndicator
|
||||
key={`offline-${index}`}
|
||||
address={subscription}
|
||||
className="disconnected"
|
||||
tooltipPlace="top" />
|
||||
<br />
|
||||
</>
|
||||
<br key={`br-${index}`} />
|
||||
</Fragment>
|
||||
))}
|
||||
<br id="mobile-br" />
|
||||
</BoardsContent>
|
||||
@@ -126,26 +138,27 @@ const Home = () => {
|
||||
<h2>Popular Boards</h2>
|
||||
</BoardsTitle>
|
||||
<BoardsContent>
|
||||
{defaultSubplebbits.map(subplebbit => (
|
||||
<div className="board" key={subplebbit.address}>
|
||||
<div className="board-title">
|
||||
{defaultSubplebbits.map((subplebbit, index) => (
|
||||
<div className="board" key={`board-${index}`}>
|
||||
<div className="board-title" key="board-title">
|
||||
{subplebbit.title ? subplebbit.title : <span style={{userSelect: "none"}}> </span>}
|
||||
</div>
|
||||
<div className="board-avatar-container">
|
||||
<Link to={`/p/${subplebbit.address}`} onClick={() => {
|
||||
<div className="board-avatar-container" key="board-avatar-container">
|
||||
<Link to={`/p/${subplebbit.address}`} key="link" onClick={() => {
|
||||
setSelectedTitle(subplebbit.title);
|
||||
setSelectedAddress(subplebbit.address);
|
||||
window.scrollTo(0, 0);
|
||||
}} >
|
||||
<BoardAvatar address={subplebbit.address} />
|
||||
<BoardAvatar key="baordavatar" address={subplebbit.address} />
|
||||
</Link>
|
||||
<OfflineIndicator
|
||||
address={subplebbit.address}
|
||||
className="offline-indicator"
|
||||
tooltipPlace="top" />
|
||||
tooltipPlace="top"
|
||||
key="oi2"/>
|
||||
</div>
|
||||
<div className="board-text">
|
||||
<b>{subplebbit.address}</b>
|
||||
<div className="board-text" key="bt">
|
||||
<b key="b">{subplebbit.address}</b>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import useGeneralStore from '../../hooks/stores/useGeneralStore';
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -7,16 +7,31 @@ import packageJson from '../../../package.json'
|
||||
const {version} = packageJson
|
||||
|
||||
|
||||
const NotFound = ({ setBodyStyle }) => {
|
||||
const { setSelectedStyle } = useGeneralStore(state => state);
|
||||
const NotFound = () => {
|
||||
const {
|
||||
bodyStyle, setBodyStyle,
|
||||
selectedStyle, setSelectedStyle,
|
||||
} = useGeneralStore(state => state);
|
||||
|
||||
const prevStyle = useRef(selectedStyle);
|
||||
const prevBodyStyle = useRef(bodyStyle);
|
||||
|
||||
// prevent dark mode
|
||||
useEffect(() => {
|
||||
const currentPrevStyle = prevStyle.current;
|
||||
const currentPrevBodyStyle = prevBodyStyle.current;
|
||||
|
||||
setBodyStyle({
|
||||
background: "#ffe url(assets/fade.png) top repeat-x",
|
||||
color: "maroon",
|
||||
fontFamily: "Helvetica, Arial, sans-serif"
|
||||
});
|
||||
setSelectedStyle("Yotsuba");
|
||||
|
||||
return () => {
|
||||
setSelectedStyle(currentPrevStyle);
|
||||
setBodyStyle(currentPrevBodyStyle);
|
||||
};
|
||||
}, [setBodyStyle, setSelectedStyle]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -125,12 +125,18 @@ const Pending = () => {
|
||||
]
|
||||
</span>
|
||||
<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="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}}
|
||||
)}>Home</Link>
|
||||
<Link to="/">Home</Link>
|
||||
]
|
||||
</span>
|
||||
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
|
||||
@@ -145,6 +151,11 @@ const Pending = () => {
|
||||
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
|
||||
))}
|
||||
</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 className="page-jump">
|
||||
<Link to={`/profile/c/${index}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
@@ -285,7 +296,7 @@ const Pending = () => {
|
||||
|
||||
|
||||
<span className="poster-address">
|
||||
(u/{account.author.shortAddress})
|
||||
(u/{account.author?.shortAddress})
|
||||
</span>
|
||||
|
||||
<span className="date-time" data-utc="data">{getDate(comment?.timestamp)}</span>
|
||||
@@ -326,7 +337,7 @@ const Pending = () => {
|
||||
Anonymous</span>}
|
||||
|
||||
<span key={`mob-pa-${"pending"}`} className="poster-address-mobile">
|
||||
(u/{account.author.shortAddress})
|
||||
(u/{account.author?.shortAddress})
|
||||
</span>
|
||||
<br key={`mob-br1-${"pending"}`} />
|
||||
{comment.title ? (
|
||||
|
||||
@@ -220,43 +220,43 @@ const Subscriptions = () => {
|
||||
</span>
|
||||
<span className="nav">
|
||||
[
|
||||
<button style={{all: 'unset', cursor: 'pointer'}} onClick={
|
||||
<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</button>
|
||||
}>Create Board</span>
|
||||
]
|
||||
[
|
||||
<Link to={`/p/subscriptions/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
]
|
||||
[
|
||||
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}}
|
||||
)}>Home</Link>
|
||||
<Link to="/">Home</Link>
|
||||
]
|
||||
</span>
|
||||
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
|
||||
<div className="board-select">
|
||||
<strong>Board</strong>
|
||||
|
||||
<select id="board-select-mobile" value="subscriptions" onChange={handleSelectChange}>
|
||||
<option value="all">All</option>
|
||||
<option value="subscriptions">Subscriptions</option>
|
||||
{defaultSubplebbits.map(subplebbit => (
|
||||
<option key={`option-${subplebbit.address}`} value={subplebbit.address}
|
||||
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
|
||||
))}
|
||||
</select>
|
||||
<button style={{all: 'unset', cursor: 'pointer'}} onClick={
|
||||
<div className="nav-container">
|
||||
<div className="board-select">
|
||||
<strong>Board</strong>
|
||||
|
||||
<select id="board-select-mobile" value="subscriptions" onChange={handleSelectChange}>
|
||||
<option value="all">All</option>
|
||||
<option value="subscriptions">Subscriptions</option>
|
||||
{defaultSubplebbits.map(subplebbit => (
|
||||
<option key={`option-${subplebbit.address}`} value={subplebbit.address}
|
||||
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
|
||||
))}
|
||||
</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</button>
|
||||
</div>
|
||||
<div className="page-jump">
|
||||
<Link to={`/p/subscriptions/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
|
||||
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}}
|
||||
)}>Home</Link>
|
||||
}>Create Board</span>
|
||||
</div>
|
||||
<div className="page-jump">
|
||||
<Link to={`/p/subscriptions/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
|
||||
<Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="separator-mobile"> </div>
|
||||
@@ -490,7 +490,7 @@ const Subscriptions = () => {
|
||||
onMouseOver={() => {setIsImageSearchOpen(true)}}
|
||||
onMouseLeave={() => {setIsImageSearchOpen(false)}}>
|
||||
Image search »
|
||||
<ul className="dropdown-menu"
|
||||
<ul className="dropdown-menu post-menu-catalog"
|
||||
style={{display: isImageSearchOpen ? 'block': 'none'}}>
|
||||
<li>
|
||||
<a
|
||||
@@ -678,7 +678,7 @@ const Subscriptions = () => {
|
||||
onMouseOver={() => {setIsImageSearchOpen(true)}}
|
||||
onMouseLeave={() => {setIsImageSearchOpen(false)}}>
|
||||
Image search »
|
||||
<ul className="dropdown-menu"
|
||||
<ul className="dropdown-menu post-menu-catalog"
|
||||
style={{display: isImageSearchOpen ? 'block': 'none'}}>
|
||||
<li>
|
||||
<a
|
||||
|
||||
@@ -123,43 +123,43 @@ const SubscriptionsCatalog = () => {
|
||||
</span>
|
||||
<span className="nav">
|
||||
[
|
||||
<button style={{all: 'unset', cursor: 'pointer'}} onClick={
|
||||
<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</button>
|
||||
}>Create Board</span>
|
||||
]
|
||||
[
|
||||
<Link to={`/p/subscriptions/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
]
|
||||
[
|
||||
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}}
|
||||
)}>Home</Link>
|
||||
<Link to="/">Home</Link>
|
||||
]
|
||||
</span>
|
||||
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
|
||||
<div className="board-select">
|
||||
<strong>Board</strong>
|
||||
|
||||
<select id="board-select-mobile" value="subscriptions" onChange={handleSelectChange}>
|
||||
<option value="all">All</option>
|
||||
<option value="subscriptions">Subscriptions</option>
|
||||
{defaultSubplebbits.map(subplebbit => (
|
||||
<option key={`option-${subplebbit.address}`} value={subplebbit.address}
|
||||
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
|
||||
))}
|
||||
</select>
|
||||
<button style={{all: 'unset', cursor: 'pointer'}} onClick={
|
||||
<div className="nav-container">
|
||||
<div className="board-select">
|
||||
<strong>Board</strong>
|
||||
|
||||
<select id="board-select-mobile" value="subscriptions" onChange={handleSelectChange}>
|
||||
<option value="all">All</option>
|
||||
<option value="subscriptions">Subscriptions</option>
|
||||
{defaultSubplebbits.map(subplebbit => (
|
||||
<option key={`option-${subplebbit.address}`} value={subplebbit.address}
|
||||
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
|
||||
))}
|
||||
</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</button>
|
||||
</div>
|
||||
<div className="page-jump">
|
||||
<Link to={`/p/subscriptions/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
|
||||
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}}
|
||||
)}>Home</Link>
|
||||
}>Create Board</span>
|
||||
</div>
|
||||
<div className="page-jump">
|
||||
<Link to={`/p/subscriptions/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
|
||||
<Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="separator-mobile"> </div>
|
||||
|
||||
+225
-178
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { confirmAlert } from 'react-confirm-alert';
|
||||
@@ -9,6 +10,7 @@ import { debounce } from 'lodash';
|
||||
import useGeneralStore from '../../hooks/stores/useGeneralStore';
|
||||
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 { PostMenuCatalog } from '../styled/views/Catalog.styled';
|
||||
import EditModal from '../modals/EditModal';
|
||||
import ImageBanner from '../ImageBanner';
|
||||
import ModerationModal from '../modals/ModerationModal';
|
||||
@@ -38,6 +40,8 @@ const Thread = () => {
|
||||
setChallengesArray,
|
||||
defaultSubplebbits,
|
||||
editedComment,
|
||||
setIsAuthorDelete,
|
||||
setIsAuthorEdit,
|
||||
setIsCaptchaOpen,
|
||||
isModerationOpen, setIsModerationOpen,
|
||||
isSettingsOpen, setIsSettingsOpen,
|
||||
@@ -64,6 +68,8 @@ const Thread = () => {
|
||||
const linkRef = useRef();
|
||||
const threadMenuRefs = useRef({});
|
||||
const replyMenuRefs = useRef({});
|
||||
const postMenuRef = useRef(null);
|
||||
const postMenuCatalogRef = useRef(null);
|
||||
|
||||
const [triggerPublishComment, setTriggerPublishComment] = useState(false);
|
||||
const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false);
|
||||
@@ -75,10 +81,11 @@ const Thread = () => {
|
||||
const [originalCommentContent, setOriginalCommentContent] = useState(null);
|
||||
const [prevScrollPos, setPrevScrollPos] = useState(0);
|
||||
const [visible, setVisible] = useState(true);
|
||||
const [rotatedStates, setRotatedStates] = useState({});
|
||||
const [isImageSearchOpen, setIsImageSearchOpen] = useState(false);
|
||||
const [isModerator, setIsModerator] = useState(false);
|
||||
const [commentCid, setCommentCid] = useState(null);
|
||||
const [menuPosition, setMenuPosition] = useState({top: 0, left: 0});
|
||||
const [openMenuCid, setOpenMenuCid] = useState(null);
|
||||
|
||||
useError(errorMessage, [errorMessage]);
|
||||
useSuccess(successMessage, [successMessage]);
|
||||
@@ -107,13 +114,28 @@ const Thread = () => {
|
||||
}, [account?.author.address, subplebbit.roles]);
|
||||
|
||||
|
||||
const handleOptionClick = (threadCid) => {
|
||||
setRotatedStates(prevState => ({
|
||||
...prevState,
|
||||
[threadCid]: false
|
||||
}));
|
||||
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(() => {
|
||||
window.scrollTo(0, 0);
|
||||
@@ -371,6 +393,8 @@ const Thread = () => {
|
||||
<button onClick={onClose}>No</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsAuthorDelete(true);
|
||||
setIsAuthorEdit(false);
|
||||
setCommentCid(commentCid);
|
||||
setPublishCommentEditOptions(prevOptions => ({
|
||||
...prevOptions,
|
||||
@@ -392,6 +416,8 @@ const Thread = () => {
|
||||
|
||||
const handleAuthorEditClick = (comment) => {
|
||||
handleOptionClick(comment.cid);
|
||||
setIsAuthorEdit(true);
|
||||
setIsAuthorDelete(false);
|
||||
setCommentCid(comment.cid);
|
||||
setOriginalCommentContent(comment.content);
|
||||
setIsEditModalOpen(true);
|
||||
@@ -496,42 +522,43 @@ const Thread = () => {
|
||||
</span>
|
||||
<span className="nav">
|
||||
[
|
||||
<button style={{all: 'unset', cursor: 'pointer'}} onClick={
|
||||
<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</button>
|
||||
}>Create Board</span>
|
||||
]
|
||||
[
|
||||
<Link to={`/p/${selectedAddress}/c/${selectedThread}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
]
|
||||
[
|
||||
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}}
|
||||
)}>Home</Link>
|
||||
<Link to="/">Home</Link>
|
||||
]
|
||||
</span>
|
||||
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
|
||||
<div className="board-select">
|
||||
<strong>Board</strong>
|
||||
|
||||
<select id="board-select-mobile" value={selectedAddress} onChange={handleSelectChange}>
|
||||
<option value="all">All</option>
|
||||
<option value="subscriptions">Subscriptions</option>
|
||||
{defaultSubplebbits.map(subplebbit => (
|
||||
<option key={`option-${subplebbit.address}`} value={subplebbit.address}
|
||||
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
|
||||
))}
|
||||
</select>
|
||||
<button style={{all: 'unset', cursor: 'pointer'}} onClick={
|
||||
<div className="nav-container">
|
||||
<div className="board-select">
|
||||
<strong>Board</strong>
|
||||
|
||||
<select id="board-select-mobile" value={selectedAddress} onChange={handleSelectChange}>
|
||||
<option value="all">All</option>
|
||||
<option value="subscriptions">Subscriptions</option>
|
||||
{defaultSubplebbits.map(subplebbit => (
|
||||
<option key={`option-${subplebbit.address}`} value={subplebbit.address}
|
||||
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
|
||||
))}
|
||||
</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</button>
|
||||
</div>
|
||||
<div className="page-jump">
|
||||
<Link to={`/p/${selectedAddress}/c/${selectedThread}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
|
||||
<Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
|
||||
}>Create Board</span>
|
||||
</div>
|
||||
<div className="page-jump">
|
||||
<Link to={`/p/${selectedAddress}/c/${selectedThread}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
|
||||
|
||||
<Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="separator-mobile"> </div>
|
||||
@@ -588,7 +615,11 @@ const Thread = () => {
|
||||
<tr data-type="Name">
|
||||
<td id="td-name">Name</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}
|
||||
onClick={handleSubmit} />
|
||||
</td>
|
||||
@@ -790,91 +821,99 @@ const Thread = () => {
|
||||
<PostMenu
|
||||
key={`pmb-${index}`}
|
||||
title="Post menu"
|
||||
ref={el => threadMenuRefs.current[comment.cid] = el}
|
||||
ref={el => {
|
||||
threadMenuRefs.current[comment.cid] = el;
|
||||
postMenuRef.current = el;
|
||||
}}
|
||||
className='post-menu-button'
|
||||
rotated={rotatedStates[comment.cid]}
|
||||
onClick={() => {
|
||||
rotated={openMenuCid === comment.cid}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
const rect = threadMenuRefs.current[comment.cid].getBoundingClientRect();
|
||||
const menu = document.querySelector(`.post-menu-thread-${comment.cid}`);
|
||||
menu.style.top = `calc(${rect.top}px + 17px)`;
|
||||
menu.style.left = `${rect.left}px`;
|
||||
|
||||
setRotatedStates(prevState => ({
|
||||
...prevState,
|
||||
[comment.cid]: !prevState[comment.cid]
|
||||
}));
|
||||
setMenuPosition({top: rect.top + window.scrollY, left: rect.left});
|
||||
setOpenMenuCid(prevCid => (prevCid === comment.cid ? null : comment.cid));
|
||||
}}
|
||||
>
|
||||
▶
|
||||
</PostMenu>
|
||||
<div id="post-menu" className={`post-menu-thread post-menu-thread-${comment.cid}`}
|
||||
style={{ display: rotatedStates[comment.cid] ? 'block' : 'none' }}>
|
||||
<ul>
|
||||
<li onClick={() => handleOptionClick(comment.cid)}>Hide thread</li>
|
||||
{comment?.author?.shortAddress === account?.author?.shortAddress ? (
|
||||
<>
|
||||
<li onClick={() => handleAuthorEditClick(comment)}>Edit post</li>
|
||||
<li onClick={() => handleAuthorDeleteClick(comment.cid)}>Delete post</li>
|
||||
</>
|
||||
) : null}
|
||||
{isModerator ? (
|
||||
<>
|
||||
{comment?.author?.shortAddress === account?.author?.shortAddress ? (
|
||||
null
|
||||
) : (
|
||||
<li onClick={() => {
|
||||
{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-${comment.cid}`}
|
||||
style={{ display: openMenuCid === comment.cid ? 'block' : 'none' }}
|
||||
>
|
||||
<ul className="post-menu-catalog">
|
||||
<li onClick={() => handleOptionClick(comment.cid)}>Hide thread</li>
|
||||
{comment.author?.shortAddress === account?.author.shortAddress ? (
|
||||
<>
|
||||
<li onClick={() => handleAuthorEditClick(comment)}>Edit post</li>
|
||||
<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)
|
||||
setIsModerationOpen(true);
|
||||
handleOptionClick(comment.cid);
|
||||
setDeletePost(true);
|
||||
}}>
|
||||
Delete post
|
||||
Mod tools
|
||||
</li>
|
||||
)}
|
||||
<li
|
||||
onClick={() => {
|
||||
setModeratingCommentCid(comment.cid)
|
||||
setIsModerationOpen(true);
|
||||
handleOptionClick(comment.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"
|
||||
style={{display: isImageSearchOpen ? 'block': 'none'}}>
|
||||
<li onClick={() => handleOptionClick(comment.cid)}>
|
||||
<a
|
||||
href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Google</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(comment.cid)}>
|
||||
<a
|
||||
href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Yandex</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(comment.cid)}>
|
||||
<a
|
||||
href={`https://saucenao.com/search.php?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>SauceNAO</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
) : null
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
) : 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(comment.cid)}>
|
||||
<a
|
||||
href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Google</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(comment.cid)}>
|
||||
<a
|
||||
href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Yandex</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(comment.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
|
||||
)}
|
||||
<div id="backlink-id" className="backlink">
|
||||
{comment?.replies?.pages?.topAll.comments
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
@@ -962,91 +1001,99 @@ const Thread = () => {
|
||||
<PostMenu
|
||||
key={`pmb-${index}`}
|
||||
title="Post menu"
|
||||
ref={el => replyMenuRefs.current[reply.cid] = el}
|
||||
ref={el => {
|
||||
replyMenuRefs.current[reply.cid] = el;
|
||||
postMenuRef.current = el;
|
||||
}}
|
||||
className='post-menu-button'
|
||||
rotated={rotatedStates[reply.cid]}
|
||||
onClick={() => {
|
||||
rotated={openMenuCid === reply.cid}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
const rect = replyMenuRefs.current[reply.cid].getBoundingClientRect();
|
||||
const menu = document.querySelector(`.post-menu-reply-${reply.cid}`);
|
||||
menu.style.top = `calc(${rect.top}px + 17px)`;
|
||||
menu.style.left = `${rect.left}px`;
|
||||
|
||||
setRotatedStates(prevState => ({
|
||||
...prevState,
|
||||
[reply.cid]: !prevState[reply.cid]
|
||||
}));
|
||||
}}
|
||||
setMenuPosition({top: rect.top + window.scrollY, left: rect.left});
|
||||
setOpenMenuCid(prevCid => (prevCid === reply.cid ? null : reply.cid));
|
||||
}}
|
||||
>
|
||||
▶
|
||||
</PostMenu>
|
||||
<div id="post-menu" className={`post-menu-reply post-menu-reply-${reply.cid}`}
|
||||
style={{ display: rotatedStates[reply.cid] ? 'block' : 'none' }}>
|
||||
<ul>
|
||||
<li onClick={() => handleOptionClick(reply.cid)}>Hide post</li>
|
||||
{reply.author.shortAddress === account?.author?.shortAddress ? (
|
||||
<>
|
||||
<li onClick={() => handleAuthorEditClick(reply)}>Edit post</li>
|
||||
<li onClick={() => handleAuthorDeleteClick(reply.cid)}>Delete post</li>
|
||||
</>
|
||||
) : null}
|
||||
{isModerator ? (
|
||||
<>
|
||||
{reply.author.shortAddress === account?.author?.shortAddress ? (
|
||||
null
|
||||
) : (
|
||||
<li onClick={() => {
|
||||
{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-reply post-menu-reply-${reply.cid}`}
|
||||
style={{ display: openMenuCid === reply.cid ? 'block' : 'none' }}
|
||||
>
|
||||
<ul className="post-menu-catalog">
|
||||
<li onClick={() => handleOptionClick(reply.cid)}>Hide post</li>
|
||||
{reply.author.shortAddress === account?.author.shortAddress ? (
|
||||
<>
|
||||
<li onClick={() => handleAuthorEditClick(reply)}>Edit post</li>
|
||||
<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)
|
||||
setIsModerationOpen(true);
|
||||
handleOptionClick(reply.cid);
|
||||
setDeletePost(true);
|
||||
}}>
|
||||
Delete post
|
||||
Mod tools
|
||||
</li>
|
||||
)}
|
||||
<li
|
||||
onClick={() => {
|
||||
setModeratingCommentCid(reply.cid)
|
||||
setIsModerationOpen(true);
|
||||
handleOptionClick(reply.cid);
|
||||
}}>
|
||||
Mod tools
|
||||
</li>
|
||||
</>
|
||||
) : null}
|
||||
{(replyMediaInfo && (
|
||||
replyMediaInfo.type === 'image' ||
|
||||
(replyMediaInfo.type === 'webpage' &&
|
||||
replyMediaInfo.thumbnail))) ? (
|
||||
<li
|
||||
onMouseOver={() => {setIsImageSearchOpen(true)}}
|
||||
onMouseLeave={() => {setIsImageSearchOpen(false)}}>
|
||||
Image search »
|
||||
<ul className="dropdown-menu"
|
||||
style={{display: isImageSearchOpen ? 'block': 'none'}}>
|
||||
<li onClick={() => handleOptionClick(reply.cid)}>
|
||||
<a
|
||||
href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Google</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(reply.cid)}>
|
||||
<a
|
||||
href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Yandex</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(reply.cid)}>
|
||||
<a
|
||||
href={`https://saucenao.com/search.php?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>SauceNAO</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
) : null
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
{(replyMediaInfo && (
|
||||
replyMediaInfo.type === 'image' ||
|
||||
(replyMediaInfo.type === 'webpage' &&
|
||||
replyMediaInfo.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(reply.cid)}>
|
||||
<a
|
||||
href={`https://lens.google.com/uploadbyurl?url=${replyMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Google</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(reply.cid)}>
|
||||
<a
|
||||
href={`https://yandex.com/images/search?url=${replyMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Yandex</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(reply.cid)}>
|
||||
<a
|
||||
href={`https://saucenao.com/search.php?url=${replyMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>SauceNAO</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
) : null
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</PostMenuCatalog>, document.body
|
||||
)}
|
||||
<div id="backlink-id" className="backlink">
|
||||
{reply.replies?.pages?.topAll.comments
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
|
||||
@@ -23,12 +23,21 @@ const useGeneralStore = create((set) => ({
|
||||
editedComment: '',
|
||||
setEditedComment: (comment) => set({ editedComment: comment }),
|
||||
|
||||
isAuthorDelete: false,
|
||||
setIsAuthorDelete: (isAuthorDelete) => set({ isAuthorDelete }),
|
||||
|
||||
isAuthorEdit: false,
|
||||
setIsAuthorEdit: (isAuthorEdit) => set({ isAuthorEdit }),
|
||||
|
||||
isCaptchaOpen: false,
|
||||
setIsCaptchaOpen: (isOpen) => set({ isCaptchaOpen: isOpen }),
|
||||
|
||||
isModerationOpen: false,
|
||||
setIsModerationOpen: (isOpen) => set({ isModerationOpen: isOpen }),
|
||||
|
||||
isModEdit: false,
|
||||
setIsModEdit: (isModEdit) => set({ isModEdit }),
|
||||
|
||||
isSettingsOpen: false,
|
||||
setIsSettingsOpen: (isOpen) => set({ isSettingsOpen: isOpen }),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user