mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
update directories
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { StyledModal } from '../styled/modals/CaptchaModal.styled';
|
||||
import useGeneralStore from '../../hooks/stores/useGeneralStore';
|
||||
import Modal from 'react-modal';
|
||||
import Draggable from 'react-draggable';
|
||||
|
||||
|
||||
const CaptchaModal = () => {
|
||||
const {
|
||||
challengesArray,
|
||||
pendingComment,
|
||||
selectedStyle,
|
||||
setCaptchaResponse,
|
||||
isCaptchaOpen, setIsCaptchaOpen,
|
||||
selectedShortCid,
|
||||
} = useGeneralStore(state => state);
|
||||
|
||||
const [imageSources, setImageSources] = useState([]);
|
||||
const [currentChallengeIndex, setCurrentChallengeIndex] = useState(0);
|
||||
const [totalChallenges, setTotalChallenges] = useState(0);
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 480);
|
||||
const responseRef = useRef();
|
||||
const nodeRef = useRef(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => setIsMobile(window.innerWidth <= 480);
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, [setIsMobile]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (challengesArray) {
|
||||
const challenges = challengesArray.challenges;
|
||||
const decryptedChallenges = [];
|
||||
|
||||
for (let i = 0; i < challenges?.length; i++) {
|
||||
const imageString = challenges[i].challenge;
|
||||
const imageSource = `data:image/png;base64,${imageString}`;
|
||||
decryptedChallenges.push(imageSource);
|
||||
}
|
||||
|
||||
setImageSources(decryptedChallenges);
|
||||
setTotalChallenges(decryptedChallenges.length);
|
||||
}
|
||||
}, [challengesArray]);
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
submitCaptcha();
|
||||
}
|
||||
};
|
||||
|
||||
const handleReturnKeyDown = () => {
|
||||
submitCaptcha((response) => {
|
||||
useGeneralStore.getState().setCaptchaResponse(response);
|
||||
useGeneralStore.getState().resolveCaptchaPromise(response);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const submitCaptcha = (callback) => {
|
||||
setCaptchaResponse(responseRef.current.value);
|
||||
setIsCaptchaOpen(false);
|
||||
|
||||
if (callback) {
|
||||
callback(responseRef.current.value);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
isOpen={isCaptchaOpen}
|
||||
onRequestClose={() => setIsCaptchaOpen(false)}
|
||||
contentLabel="Captcha Modal"
|
||||
shouldCloseOnEsc={false}
|
||||
shouldCloseOnOverlayClick={false}
|
||||
selectedStyle={selectedStyle}
|
||||
overlayClassName="hide-modal-overlay">
|
||||
<Draggable handle=".modal-header" nodeRef={nodeRef} disabled={isMobile}>
|
||||
<div className="modal-content" ref={nodeRef}>
|
||||
<div className="modal-header">
|
||||
{pendingComment.parentCid ?
|
||||
("Challenges for Reply to c/" + selectedShortCid) :
|
||||
"Challenges for New Thread"}
|
||||
<button className="icon" onClick={() => setIsCaptchaOpen(false)} title="close" />
|
||||
</div>
|
||||
<div id="form">
|
||||
{pendingComment.author?.displayName ? (
|
||||
<div>
|
||||
<input id="field" type="text" placeholder={pendingComment.author?.displayName} disabled />
|
||||
</div>
|
||||
) : null}
|
||||
{pendingComment.title ? (
|
||||
<div>
|
||||
<input id="field" type="text" placeholder={pendingComment.title} disabled />
|
||||
</div>
|
||||
) : null}
|
||||
{pendingComment.content ? (
|
||||
<div>
|
||||
<textarea
|
||||
rows="4"
|
||||
placeholder={pendingComment.content || "Comment"}
|
||||
wrap="soft"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{pendingComment.link ? (
|
||||
<div>
|
||||
<input id="field" type="text" placeholder={pendingComment.link} disabled />
|
||||
</div>
|
||||
) : null}
|
||||
<div id="captcha-container">
|
||||
<input
|
||||
id="response"
|
||||
type="text"
|
||||
autoComplete='off'
|
||||
placeholder="TYPE THE CAPTCHA HERE AND PRESS ENTER"
|
||||
ref={responseRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus />
|
||||
<img src={imageSources[currentChallengeIndex]} alt="captcha" />
|
||||
</div>
|
||||
<div>
|
||||
<span style={{lineHeight: '1.7'}}>
|
||||
Challenge {currentChallengeIndex + 1} of {totalChallenges}
|
||||
</span>
|
||||
<button
|
||||
id="nav"
|
||||
onClick={() => {
|
||||
if (currentChallengeIndex + 1 < totalChallenges) {
|
||||
setCurrentChallengeIndex((currentChallengeIndex + 1) % totalChallenges);
|
||||
} else {
|
||||
handleReturnKeyDown();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{currentChallengeIndex + 1 < totalChallenges ? "Next" : "Post"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Draggable>
|
||||
</StyledModal>
|
||||
);
|
||||
};
|
||||
|
||||
Modal.setAppElement('#root');
|
||||
|
||||
export default CaptchaModal;
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { StyledModal } from '../styled/modals/ReplyModal.styled';
|
||||
import useGeneralStore from '../../hooks/stores/useGeneralStore';
|
||||
import Modal from 'react-modal';
|
||||
import Draggable from 'react-draggable';
|
||||
|
||||
|
||||
const EditModal = ({ isOpen, closeModal, originalCommentContent }) => {
|
||||
const {
|
||||
setEditedComment,
|
||||
selectedStyle,
|
||||
} = useGeneralStore(state => state);
|
||||
|
||||
const nodeRef = useRef(null);
|
||||
const commentRef = useRef();
|
||||
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 480);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => setIsMobile(window.innerWidth <= 480);
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, [setIsMobile]);
|
||||
|
||||
|
||||
const handleSaveEdit = () => {
|
||||
setEditedComment(commentRef.current.value);
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
isOpen={isOpen}
|
||||
onRequestClose={closeModal}
|
||||
contentLabel="Edit Comment"
|
||||
shouldCloseOnEsc={false}
|
||||
shouldCloseOnOverlayClick={isMobile}
|
||||
selectedStyle={selectedStyle}
|
||||
style={isMobile ? ({ overlay: { backgroundColor: "rgba(0,0,0,.25)" }}) : ({ overlay: { backgroundColor: "rgba(0,0,0,0)" }})}>
|
||||
<Draggable handle=".modal-header" nodeRef={nodeRef} disabled={isMobile}>
|
||||
<div className="modal-content" ref={nodeRef}>
|
||||
<div className="modal-header">
|
||||
Edit Comment
|
||||
<button className="icon" onClick={() => closeModal()} title="close" />
|
||||
</div>
|
||||
<div id="form">
|
||||
<div className="textarea-wrapper">
|
||||
<textarea className="textarea"
|
||||
rows="4"
|
||||
style={{paddingTop: '0'}}
|
||||
placeholder="Comment"
|
||||
defaultValue={originalCommentContent}
|
||||
wrap="soft"
|
||||
ref={commentRef} />
|
||||
</div>
|
||||
<div>
|
||||
<button id="next" onClick={handleSaveEdit}>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Draggable>
|
||||
</StyledModal>
|
||||
);
|
||||
};
|
||||
|
||||
Modal.setAppElement('#root');
|
||||
|
||||
export default EditModal;
|
||||
@@ -0,0 +1,236 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Modal from "react-modal";
|
||||
import { Link } from "react-router-dom";
|
||||
import { usePublishCommentEdit } from "@plebbit/plebbit-react-hooks";
|
||||
import { StyledModal } from "../styled/modals/ModerationModal.styled";
|
||||
import useGeneralStore from "../../hooks/stores/useGeneralStore";
|
||||
import useError from "../../hooks/useError";
|
||||
import useSuccess from "../../hooks/useSuccess";
|
||||
|
||||
|
||||
const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
|
||||
const {
|
||||
selectedAddress,
|
||||
selectedStyle,
|
||||
setCaptchaResponse,
|
||||
setChallengesArray,
|
||||
setIsCaptchaOpen,
|
||||
moderatingCommentCid,
|
||||
setResolveCaptchaPromise,
|
||||
} = useGeneralStore(state => state);
|
||||
|
||||
const [pin, setPin] = useState(false);
|
||||
const [deleteThread, setDeleteThread] = useState(deletePost);
|
||||
const [close, setClose] = useState(false);
|
||||
const [reason, setReason] = useState('');
|
||||
const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState(null);
|
||||
const [successMessage, setSuccessMessage] = useState(null);
|
||||
useError(errorMessage, [errorMessage]);
|
||||
useSuccess(successMessage, [successMessage]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setDeleteThread(deletePost);
|
||||
}, [deletePost]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setDeleteThread(deletePost);
|
||||
}
|
||||
}, [isOpen, deletePost]);
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setDeleteThread(false);
|
||||
closeModal();
|
||||
};
|
||||
|
||||
|
||||
const onChallengeVerification = (challengeVerification) => {
|
||||
if (challengeVerification.challengeSuccess === true) {
|
||||
setSuccessMessage('Challenge Success');
|
||||
} else if (challengeVerification.challengeSuccess === false) {
|
||||
setErrorMessage('Challenge Failed', {reason: challengeVerification.reason, errors: challengeVerification.errors});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const onChallenge = async (challenges, comment) => {
|
||||
|
||||
let challengeAnswers = [];
|
||||
|
||||
try {
|
||||
challengeAnswers = await getChallengeAnswersFromUser(challenges)
|
||||
}
|
||||
catch (error) {
|
||||
setErrorMessage(error);
|
||||
}
|
||||
if (challengeAnswers) {
|
||||
await comment.publishChallengeAnswers(challengeAnswers)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const getChallengeAnswersFromUser = async (challenges) => {
|
||||
setChallengesArray(challenges);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const imageString = challenges?.challenges[0].challenge;
|
||||
const imageSource = `data:image/png;base64,${imageString}`;
|
||||
const challengeImg = new Image();
|
||||
challengeImg.src = imageSource;
|
||||
|
||||
challengeImg.onload = () => {
|
||||
setIsCaptchaOpen(true);
|
||||
|
||||
const handleKeyDown = async (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
const currentCaptchaResponse = useGeneralStore.getState().captchaResponse;
|
||||
resolve(currentCaptchaResponse);
|
||||
setIsCaptchaOpen(false);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
setCaptchaResponse('');
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
setResolveCaptchaPromise(resolve);
|
||||
};
|
||||
|
||||
challengeImg.onerror = () => {
|
||||
reject(setErrorMessage('Could not load challenges'));
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
const [publishCommentEditOptions, setPublishCommentEditOptions] = useState({
|
||||
commentCid: moderatingCommentCid,
|
||||
subplebbitAddress: selectedAddress,
|
||||
onChallenge,
|
||||
onChallengeVerification,
|
||||
onError: (error) => {
|
||||
setErrorMessage(error);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const {error, publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
setErrorMessage(error);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setPublishCommentEditOptions((prevOptions) => ({
|
||||
...prevOptions,
|
||||
commentCid: moderatingCommentCid,
|
||||
}));
|
||||
}, [moderatingCommentCid]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (publishCommentEditOptions && triggerPublishCommentEdit) {
|
||||
(async () => {
|
||||
await publishCommentEdit();
|
||||
setTriggerPublishCommentEdit(false);
|
||||
})();
|
||||
}
|
||||
}, [publishCommentEditOptions, triggerPublishCommentEdit, publishCommentEdit]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
isOpen={isOpen}
|
||||
onRequestClose={handleCloseModal}
|
||||
contentLabel="Moderator Tools"
|
||||
style={{ overlay: { backgroundColor: "rgba(0,0,0,.25)" }}}
|
||||
selectedStyle={selectedStyle}
|
||||
>
|
||||
<div className="panel">
|
||||
<div className="panel-header">
|
||||
Moderator Tools
|
||||
<Link to="" onClick={handleCloseModal}>
|
||||
<span className="icon" title="close" />
|
||||
</Link>
|
||||
</div>
|
||||
<ul className="settings-cat">
|
||||
<li className="settings-cat-lbl">
|
||||
<label>
|
||||
<input type="checkbox" style={{marginRight: "10px"}}
|
||||
checked={pin} onChange={() => setPin(!pin)} />
|
||||
Pin post
|
||||
</label>
|
||||
</li>
|
||||
<li className="settings-tip">
|
||||
Pin the post to make it a sticky, showed at the top of the board even as new posts are submitted.
|
||||
</li>
|
||||
</ul>
|
||||
<ul className="settings-cat">
|
||||
<li className="settings-cat-lbl">
|
||||
<label>
|
||||
<input type="checkbox" style={{marginRight: "10px"}}
|
||||
checked={deleteThread} onChange={() => setDeleteThread(!deleteThread)} />
|
||||
Delete post
|
||||
</label>
|
||||
</li>
|
||||
<li className="settings-tip">
|
||||
The post will no longer visible to other users, but the person who posted it can still see it in their own account.
|
||||
</li>
|
||||
</ul>
|
||||
<ul className="settings-cat">
|
||||
<li className="settings-cat-lbl">
|
||||
<label>
|
||||
<input type="checkbox" style={{marginRight: "10px"}}
|
||||
checked={close} onChange={() => setClose(!close)} />
|
||||
Close post
|
||||
</label>
|
||||
</li>
|
||||
<li className="settings-tip">
|
||||
Closing a post allows users to still see the content, but they cannot add any new replies to it.
|
||||
</li>
|
||||
</ul>
|
||||
<ul className="settings-cat">
|
||||
<li className="settings-option disc">
|
||||
Reason
|
||||
</li>
|
||||
<li className="settings-tip">
|
||||
Help people become better posters by giving a short reason why their post was removed.
|
||||
</li>
|
||||
<li className="settings-input" style={{marginTop: "-10px"}}>
|
||||
<textarea value={reason} placeholder="Enter reason here..."
|
||||
onChange={e => setReason(e.target.value)}/>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
className="save-button"
|
||||
onClick={async () => {
|
||||
setPublishCommentEditOptions(prevOptions => ({
|
||||
...prevOptions,
|
||||
pinned: pin,
|
||||
removed: deleteThread,
|
||||
locked: close,
|
||||
reason: reason
|
||||
}));
|
||||
setTriggerPublishCommentEdit(true);
|
||||
handleCloseModal();
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledModal>
|
||||
);
|
||||
}
|
||||
|
||||
Modal.setAppElement("#root");
|
||||
|
||||
export default ModerationModal;
|
||||
@@ -0,0 +1,253 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { usePublishComment } from '@plebbit/plebbit-react-hooks';
|
||||
import { StyledModal } from '../styled/modals/ReplyModal.styled';
|
||||
import useGeneralStore from '../../hooks/stores/useGeneralStore';
|
||||
import Modal from 'react-modal';
|
||||
import Draggable from 'react-draggable';
|
||||
import useError from '../../hooks/useError';
|
||||
|
||||
|
||||
const ReplyModal = ({ isOpen, closeModal }) => {
|
||||
const {
|
||||
captchaResponse, setCaptchaResponse,
|
||||
setChallengesArray,
|
||||
setIsCaptchaOpen,
|
||||
setPendingComment,
|
||||
setPendingCommentIndex,
|
||||
setResolveCaptchaPromise,
|
||||
selectedAddress,
|
||||
selectedParentCid,
|
||||
selectedShortCid,
|
||||
selectedStyle,
|
||||
} = useGeneralStore(state => state);
|
||||
|
||||
const nodeRef = useRef(null);
|
||||
|
||||
const nameRef = useRef();
|
||||
const commentRef = useRef();
|
||||
const linkRef = useRef();
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState(null);
|
||||
const [triggerPublishComment, setTriggerPublishComment] = useState(false);
|
||||
const [selectedText, setSelectedText] = useState('');
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 480);
|
||||
|
||||
useError(errorMessage, [errorMessage]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => setIsMobile(window.innerWidth <= 480);
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, [setIsMobile]);
|
||||
|
||||
|
||||
const onModalOpen = () => {
|
||||
if (commentRef.current) {
|
||||
commentRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const getSelectedText = useCallback(() => {
|
||||
const text = document.getSelection().toString();
|
||||
setSelectedText(text ? `>${text}\n` : '');
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
getSelectedText();
|
||||
} else {
|
||||
setSelectedText('');
|
||||
}
|
||||
}, [isOpen, getSelectedText]);
|
||||
|
||||
|
||||
const onChallengeVerification = (challengeVerification) => {
|
||||
if (challengeVerification.challengeSuccess === true) {
|
||||
console.log('challenge success');
|
||||
}
|
||||
else if (challengeVerification.challengeSuccess === false) {
|
||||
setErrorMessage('challenge failed', {reason: challengeVerification.reason, errors: challengeVerification.errors});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const onChallenge = async (challenges, comment) => {
|
||||
setPendingComment(comment);
|
||||
let challengeAnswers = [];
|
||||
|
||||
try {
|
||||
challengeAnswers = await getChallengeAnswersFromUser(challenges)
|
||||
}
|
||||
catch (error) {
|
||||
setErrorMessage(error);
|
||||
}
|
||||
if (challengeAnswers) {
|
||||
await comment.publishChallengeAnswers(challengeAnswers)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setPublishCommentOptions((prevPublishCommentOptions) => ({
|
||||
...prevPublishCommentOptions,
|
||||
subplebbitAddress: selectedAddress,
|
||||
}));
|
||||
}, [selectedAddress]);
|
||||
|
||||
|
||||
const [publishCommentOptions, setPublishCommentOptions] = useState({
|
||||
subplebbitAddress: selectedAddress,
|
||||
onChallenge,
|
||||
onChallengeVerification,
|
||||
onError: (error) => {
|
||||
setErrorMessage(error);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const { publishComment, index } = usePublishComment(publishCommentOptions);
|
||||
|
||||
useEffect(() => {
|
||||
if (index !== undefined) {
|
||||
setPendingCommentIndex(index);
|
||||
}
|
||||
}, [index, setPendingCommentIndex]);
|
||||
|
||||
|
||||
const resetFields = useCallback(() => {
|
||||
if (nameRef.current) {
|
||||
nameRef.current.value = '';
|
||||
}
|
||||
if (commentRef.current) {
|
||||
commentRef.current.value = '';
|
||||
}
|
||||
if (linkRef.current) {
|
||||
linkRef.current.value = '';
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (
|
||||
commentRef.current.value === "" &&
|
||||
linkRef.current.value === ""
|
||||
) {
|
||||
setErrorMessage("Please enter a comment or link.");
|
||||
return;
|
||||
}
|
||||
|
||||
setPublishCommentOptions((prevPublishCommentOptions) => ({
|
||||
...prevPublishCommentOptions,
|
||||
author: {
|
||||
displayName: nameRef.current.value || undefined,
|
||||
},
|
||||
content: commentRef.current.value || undefined,
|
||||
link: linkRef.current.value || undefined,
|
||||
parentCid: selectedParentCid,
|
||||
}));
|
||||
|
||||
setTriggerPublishComment(true);
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (publishCommentOptions && triggerPublishComment) {
|
||||
(async () => {
|
||||
await publishComment();
|
||||
resetFields();
|
||||
closeModal();
|
||||
})();
|
||||
setTriggerPublishComment(false);
|
||||
}
|
||||
}, [publishCommentOptions, triggerPublishComment, publishComment, resetFields, closeModal]);
|
||||
|
||||
|
||||
const getChallengeAnswersFromUser = async (challenges) => {
|
||||
setChallengesArray(challenges);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const imageString = challenges?.challenges[0].challenge;
|
||||
const imageSource = `data:image/png;base64,${imageString}`;
|
||||
const challengeImg = new Image();
|
||||
challengeImg.src = imageSource;
|
||||
|
||||
challengeImg.onload = () => {
|
||||
setIsCaptchaOpen(true);
|
||||
|
||||
const handleKeyDown = async (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
const currentCaptchaResponse = captchaResponse;
|
||||
resolve(currentCaptchaResponse);
|
||||
setIsCaptchaOpen(false);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
setCaptchaResponse('');
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
setResolveCaptchaPromise(resolve);
|
||||
};
|
||||
|
||||
challengeImg.onerror = () => {
|
||||
reject(setErrorMessage('Could not load challenges'));
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
isOpen={isOpen}
|
||||
onAfterOpen={onModalOpen}
|
||||
onRequestClose={closeModal}
|
||||
contentLabel="Reply Modal"
|
||||
shouldCloseOnEsc={false}
|
||||
shouldCloseOnOverlayClick={isMobile}
|
||||
selectedStyle={selectedStyle}
|
||||
style={isMobile ? ({ overlay: { backgroundColor: "rgba(0,0,0,.25)" }}) : ({ overlay: { backgroundColor: "rgba(0,0,0,0)" }})}>
|
||||
<Draggable handle=".modal-header" nodeRef={nodeRef} disabled={isMobile}>
|
||||
<div className="modal-content" ref={nodeRef}>
|
||||
<div className="modal-header">
|
||||
Reply to c/{selectedShortCid}
|
||||
<button className="icon" onClick={() => closeModal()} title="close" />
|
||||
</div>
|
||||
<div id="form">
|
||||
<div>
|
||||
<input id="name" type="text" placeholder="Name" ref={nameRef} />
|
||||
</div>
|
||||
<div>
|
||||
<input id="name" type="text" placeholder="Embed link" ref={linkRef} />
|
||||
</div>
|
||||
<div className="textarea-wrapper">
|
||||
<span className="fixed-text">{`c/${selectedShortCid}`}</span>
|
||||
<textarea className="textarea"
|
||||
rows="4"
|
||||
placeholder="Comment"
|
||||
defaultValue={selectedText}
|
||||
wrap="soft"
|
||||
ref={commentRef} />
|
||||
</div>
|
||||
<div>
|
||||
<button id="next" onClick={handleSubmit}>Post</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Draggable>
|
||||
</StyledModal>
|
||||
);
|
||||
};
|
||||
|
||||
Modal.setAppElement('#root');
|
||||
|
||||
export default ReplyModal;
|
||||
@@ -0,0 +1,368 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import Modal from "react-modal";
|
||||
import { deleteCaches, exportAccount, importAccount, setAccount, setActiveAccount, useAccount, useAccounts } from "@plebbit/plebbit-react-hooks";
|
||||
import { StyledModal } from "../styled/modals/SettingsModal.styled";
|
||||
import useGeneralStore from "../../hooks/stores/useGeneralStore";
|
||||
import useError from "../../hooks/useError";
|
||||
import useSuccess from "../../hooks/useSuccess";
|
||||
import packageJson from '../../../package.json'
|
||||
const {version} = packageJson
|
||||
|
||||
|
||||
const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
const selectedStyle = useGeneralStore(state => state.selectedStyle);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [expanded, setExpanded] = useState([]);
|
||||
const [accountJson, setAccountJson] = useState(null);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState(null);
|
||||
const [successMessage, setSuccessMessage] = useState(null);
|
||||
useError(errorMessage, [errorMessage]);
|
||||
useSuccess(successMessage, [successMessage]);
|
||||
|
||||
const account = useAccount();
|
||||
const { accounts } = useAccounts();
|
||||
|
||||
const gatewayRef = useRef();
|
||||
const ipfsRef = useRef();
|
||||
const pubsubRef = useRef();
|
||||
const dataPathRef = useRef();
|
||||
const importRef = useRef();
|
||||
|
||||
const isValidURL = (url) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSavePlebbitOptions = async () => {
|
||||
const gatewayUrls = gatewayRef.current.value.split('\n').filter(url => url.trim());
|
||||
const ipfsClientsOptions = ipfsRef.current.value.split('\n').filter(url => url.trim()) || undefined;
|
||||
const pubsubClientsOptions = pubsubRef.current.value.split('\n').filter(url => url.trim());
|
||||
|
||||
const invalidUrls = [
|
||||
...gatewayUrls,
|
||||
...(ipfsClientsOptions || []),
|
||||
...pubsubClientsOptions,
|
||||
].filter((url) => !isValidURL(url));
|
||||
|
||||
if (invalidUrls.length > 0) {
|
||||
setErrorMessage(`Invalid URL(s): ${invalidUrls.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const plebbitOptions = {
|
||||
ipfsGatewayUrls: gatewayUrls,
|
||||
ipfsHttpClientsOptions: ipfsClientsOptions,
|
||||
pubsubHttpClientsOptions: pubsubClientsOptions,
|
||||
};
|
||||
|
||||
try {
|
||||
await setAccount({ ...account, plebbitOptions });
|
||||
localStorage.setItem("successToast", "Settings Saved");
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
setErrorMessage(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleResetPlebbitOptions = async () => {
|
||||
setErrorMessage(null);
|
||||
setSuccessMessage(null);
|
||||
|
||||
const defaultGatewayUrls = [
|
||||
'https://ipfs.io',
|
||||
'https://ipfsgateway.xyz',
|
||||
'https://cloudflare-ipfs.com'
|
||||
];
|
||||
const defaultPubsubHttpClientsOptions = ['https://pubsubprovider.xyz/api/v0'];
|
||||
|
||||
gatewayRef.current.value = defaultGatewayUrls.join('\n');
|
||||
ipfsRef.current.value = "";
|
||||
pubsubRef.current.value = defaultPubsubHttpClientsOptions.join('\n');
|
||||
dataPathRef.current.value = "";
|
||||
|
||||
try {
|
||||
await setAccount({
|
||||
...account,
|
||||
plebbitOptions: {
|
||||
ipfsGatewayUrls: defaultGatewayUrls,
|
||||
ipfsHttpClientsOptions: undefined,
|
||||
pubsubHttpClientsOptions: defaultPubsubHttpClientsOptions,
|
||||
},
|
||||
});
|
||||
localStorage.setItem("successToast", "Settings Reset");
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
setErrorMessage(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setAccountJson(null);
|
||||
|
||||
if (location.pathname.endsWith("/settings")) {
|
||||
const newPath = location.pathname.slice(0, -9);
|
||||
closeModal();
|
||||
navigate(newPath, { replace: true });
|
||||
} else {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const toggleExpanded = (index) => {
|
||||
setExpanded((prevExpanded) => {
|
||||
const newExpanded = [...prevExpanded];
|
||||
if (newExpanded.includes(index)) {
|
||||
newExpanded.splice(newExpanded.indexOf(index), 1);
|
||||
} else {
|
||||
newExpanded.push(index);
|
||||
}
|
||||
return newExpanded;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const expandAll = () => {
|
||||
if (expanded.length === 3) {
|
||||
setExpanded([]);
|
||||
} else {
|
||||
setExpanded([0, 1, 2]);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem("cacheCleared") === "true") {
|
||||
setSuccessMessage("Cache Cleared");
|
||||
localStorage.removeItem("cacheCleared");
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
const handleExport = async () => {
|
||||
const activeAccountJson = await exportAccount();
|
||||
setAccountJson(activeAccountJson);
|
||||
};
|
||||
|
||||
|
||||
const handleImport = async () => {
|
||||
const accountJson = importRef.current.value;
|
||||
|
||||
try {
|
||||
const parsedJson = JSON.parse(accountJson);
|
||||
await importAccount(accountJson);
|
||||
setActiveAccount(parsedJson.account?.name);
|
||||
setSuccessMessage("Account Imported");
|
||||
|
||||
} catch (error) {
|
||||
setErrorMessage(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAccountChange = (e) => {
|
||||
setActiveAccount(e.target.value);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
isOpen={isOpen}
|
||||
onRequestClose={handleCloseModal}
|
||||
contentLabel="Settings"
|
||||
style={{ overlay: { backgroundColor: "rgba(0,0,0,.25)" }}}
|
||||
selectedStyle={selectedStyle}
|
||||
expanded={expanded}
|
||||
>
|
||||
<div className="panel">
|
||||
<div className="panel-header">
|
||||
<span id="version">
|
||||
v{version}
|
||||
</span>
|
||||
Settings
|
||||
<Link to="" onClick={handleCloseModal}>
|
||||
<span className="icon" title="close" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="all-div">
|
||||
[
|
||||
<button className="all-button"
|
||||
onClick={expandAll} style={{all: "unset", cursor: "pointer"}}>
|
||||
{expanded.length === 3 ? "Collapse All Settings" : "Expand All Settings"}
|
||||
</button>
|
||||
]
|
||||
</div>
|
||||
{/* <ul>
|
||||
<li className="settings-cat-lbl">
|
||||
<span className={`${expanded.includes(0) ? 'minus' : 'plus'}`}
|
||||
onClick={() => toggleExpanded(0)}
|
||||
/>
|
||||
<span className="settings-pointer" style={{cursor: "pointer"}}
|
||||
onClick={() => toggleExpanded(0)}
|
||||
>Account</span>
|
||||
</li>
|
||||
<ul className="settings-cat" style={{ display: expanded.includes(0) ? 'block' : 'none' }}>
|
||||
<li>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>*/}
|
||||
<ul>
|
||||
<li className="settings-cat-lbl">
|
||||
<span className={`${expanded.includes(1) ? 'minus' : 'plus'}`}
|
||||
onClick={() => toggleExpanded(1)}
|
||||
/>
|
||||
<span className="settings-pointer" style={{cursor: "pointer"}}
|
||||
onClick={() => toggleExpanded(1)}
|
||||
>Account</span>
|
||||
<div className="plebbit-options-buttons"
|
||||
style={{ display: expanded.includes(1) ? 'block' : 'none' }}
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
<ul className="settings-cat" style={{ display: expanded.includes(1) ? 'block' : 'none' }}>
|
||||
<li className="settings-option disc">
|
||||
Export or Import Your 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>
|
||||
<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>
|
||||
<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"
|
||||
value={account?.name}
|
||||
onChange={handleAccountChange}
|
||||
>
|
||||
{accounts.map((account) => (
|
||||
<option key={account?.name} value={account?.name}>
|
||||
{account?.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
<ul>
|
||||
<li className="settings-cat-lbl">
|
||||
<span className={`${expanded.includes(2) ? 'minus' : 'plus'}`}
|
||||
onClick={() => toggleExpanded(2)}
|
||||
/>
|
||||
<span className="settings-pointer" style={{cursor: "pointer"}}
|
||||
onClick={() => toggleExpanded(2)}
|
||||
>Plebbit Options</span>
|
||||
<div className="plebbit-options-buttons"
|
||||
style={{ display: expanded.includes(2) ? 'block' : 'none' }}
|
||||
>
|
||||
<button className="save-button"
|
||||
onClick={handleSavePlebbitOptions}>Save</button>
|
||||
<button className="reset-button"
|
||||
onClick={handleResetPlebbitOptions}>Reset</button>
|
||||
</div>
|
||||
</li>
|
||||
<ul className="settings-cat" style={{ display: expanded.includes(2) ? 'block' : 'none' }}>
|
||||
<li className="settings-option disc">
|
||||
IPFS Gateway URLs
|
||||
</li>
|
||||
<li className="settings-tip">
|
||||
Optional URLs of IPFS gateways.
|
||||
</li>
|
||||
<div className="settings-input">
|
||||
<textarea placeholder="IPFS Gateway URLs"
|
||||
defaultValue={account?.plebbitOptions?.ipfsGatewayUrls.join("\n")}
|
||||
ref={gatewayRef}
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
<ul className="settings-cat" style={{ display: expanded.includes(2) ? 'block' : 'none' }}>
|
||||
<li className="settings-option disc">
|
||||
IPFS HTTP Clients Options</li>
|
||||
<li className="settings-tip">Optional URLs of IPFS APIs or IpfsHttpClientOptions, 'http://localhost:5001/api/v0' to use a local IPFS node.</li>
|
||||
<div className="settings-input">
|
||||
<textarea placeholder="IPFS HTTP Clients Options"
|
||||
defaultValue={account?.plebbitOptions?.ipfsHttpClientsOptions ? account?.plebbitOptions?.ipfsHttpClientsOptions.join("\n") : ''}
|
||||
ref={ipfsRef}
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
<ul className="settings-cat" style={{ display: expanded.includes(2) ? 'block' : 'none' }}>
|
||||
<li className="settings-option disc">
|
||||
PubSub HTTP Clients Options</li>
|
||||
<li className="settings-tip">Optional URLs or IpfsHttpClientOptions used for pubsub publishing when ipfsHttpClientOptions isn't available, like in the browser.</li>
|
||||
<div className="settings-input">
|
||||
<textarea placeholder="PubSub HTTP Clients Options"
|
||||
defaultValue={account?.plebbitOptions?.pubsubHttpClientsOptions.join("\n")}
|
||||
ref={pubsubRef}
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
<ul className="settings-cat" style={{ display: expanded.includes(2) ? 'block' : 'none' }}>
|
||||
<li className="settings-option disc">
|
||||
Data Path (Node Only)</li>
|
||||
<li className="settings-tip">Optional folder path to create/resume the user and subplebbit databases.</li>
|
||||
<div className="settings-input">
|
||||
<textarea placeholder="Data Path (Node Only)"
|
||||
ref={dataPathRef}
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
{/* <ul className="settings-cat" style={{ display: expanded.includes(2) ? 'block' : 'none' }}>
|
||||
<li className="settings-option disc">
|
||||
Chain Providers</li>
|
||||
<li className="settings-tip">Optional provider RPC URLs and chain IDs.</li>
|
||||
<ul>
|
||||
<li className="settings-option disc">Ethereum</li>
|
||||
<li className="settings-option disc">Avalanche</li>
|
||||
<li className="settings-option disc">Polygon</li>
|
||||
</ul>
|
||||
</ul> */}
|
||||
</ul>
|
||||
<div>
|
||||
<button
|
||||
className="cache-button"
|
||||
onClick={async () => {
|
||||
if (window.confirm("Are you sure you want to clear the cache?")) {
|
||||
await deleteCaches();
|
||||
localStorage.setItem("cacheCleared", "true");
|
||||
window.location.reload();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Clear Cache
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</StyledModal>
|
||||
);
|
||||
}
|
||||
|
||||
Modal.setAppElement("#root");
|
||||
|
||||
export default SettingsModal;
|
||||
Reference in New Issue
Block a user