diff --git a/package.json b/package.json index df598691..016a3ca0 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "eslint": "8.36.0", "eslint-config-react-app": "7.0.1", "ext-name": "5.0.0", + "json-stringify-pretty-compact": "^4.0.0", "lodash": "4.17.21", "mock-require": "3.0.3", "postcss": "8.4.21", diff --git a/src/components/BoardSettings.jsx b/src/components/BoardSettings.jsx new file mode 100644 index 00000000..811d360c --- /dev/null +++ b/src/components/BoardSettings.jsx @@ -0,0 +1,326 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Link } from "react-router-dom"; +import { usePublishSubplebbitEdit } from "@plebbit/plebbit-react-hooks"; +import { StyledModal } from './styled/modals/ModerationModal.styled'; +import useError from "../hooks/useError"; +import useSuccess from "../hooks/useSuccess"; +import useGeneralStore from '../hooks/stores/useGeneralStore'; + +const BoardSettings = ({ subplebbit }) => { + const { + setCaptchaResponse, + setChallengesArray, + setIsCaptchaOpen, + setResolveCaptchaPromise, + selectedAddress, + selectedStyle, + } = useGeneralStore(state => state); + + const allowedSettings = { + address: subplebbit.address, + apiUrl: subplebbit.apiUrl, + description: subplebbit.description, + pubsubTopic: subplebbit.pubsubTopic, + settings: { + fetchThumbnailUrls: subplebbit.settings?.fetchThumbnailUrls, + fetchThumbnailUrlsProxyUrl: subplebbit.settings?.fetchThumbnailUrlsProxyUrl, + }, + roles: subplebbit.roles, + rules: subplebbit.rules, + suggested: { + avatarUrl: subplebbit.suggested?.avatarUrl, + backgroundUrl: subplebbit.suggested?.backgroundUrl, + bannerUrl: subplebbit.suggested?.bannerUrl, + language: subplebbit.suggested?.language, + primaryColor: subplebbit.suggested?.primaryColor, + secondaryColor: subplebbit.suggested?.secondaryColor, + }, + title: subplebbit.title, + }; + + const generateSettingsFromSubplebbit = (subplebbitData) => ({ + address: subplebbitData.address, + apiUrl: subplebbitData.apiUrl, + description: subplebbitData.description, + pubsubTopic: subplebbitData.pubsubTopic, + settings: { + fetchThumbnailUrls: subplebbitData.settings?.fetchThumbnailUrls, + fetchThumbnailUrlsProxyUrl: subplebbitData.settings?.fetchThumbnailUrlsProxyUrl, + }, + roles: subplebbitData.roles, + rules: subplebbitData.rules, + suggested: { + avatarUrl: subplebbitData.suggested?.avatarUrl, + backgroundUrl: subplebbitData.suggested?.backgroundUrl, + bannerUrl: subplebbitData.suggested?.bannerUrl, + language: subplebbitData.suggested?.language, + primaryColor: subplebbitData.suggested?.primaryColor, + secondaryColor: subplebbitData.suggested?.secondaryColor, + }, + title: subplebbitData.title, + }); + + const initialSettings = generateSettingsFromSubplebbit(subplebbit); + const [isModalOpen, setIsModalOpen] = useState(false); + const [boardSettingsJson, setBoardSettingsJson] = useState(JSON.stringify(initialSettings, null, 2)); + const [triggerPublilshSubplebbitEdit, setTriggerPublishCommentEdit] = useState(false); + + const [, setNewErrorMessage] = useError(); + const [, setNewSuccessMessage] = useSuccess(); + + + const getDifferences = (oldObj, newObj) => { + let differences = {}; + + for (let key in oldObj) { + if (typeof oldObj[key] === 'object' && oldObj[key] !== null) { + const nestedDifferences = getDifferences(oldObj[key], newObj[key] || {}); + if (Object.keys(nestedDifferences).length > 0) { + differences[key] = nestedDifferences; + } + } else if (oldObj[key] !== newObj[key]) { + differences[key] = newObj[key]; + } + } + + for (let key in newObj) { + if (!oldObj.hasOwnProperty(key)) { + differences[key] = newObj[key]; + } + } + + return differences; + }; + + + const isInitialMount = useRef(true); + + useEffect(() => { + if (isInitialMount.current) { + setBoardSettingsJson(JSON.stringify(generateSettingsFromSubplebbit(subplebbit), null, 2)); + isInitialMount.current = false; + } + }, [subplebbit]); + + + function validateSettings(updatedSettings, allowedSettings) { + for (let key in updatedSettings) { + if (!allowedSettings.hasOwnProperty(key) && !initialSettings.hasOwnProperty(key)) { + throw new Error(`Unexpected setting: ${key}`); + } + + if (typeof updatedSettings[key] === 'object' && updatedSettings[key] !== null + && !Array.isArray(updatedSettings[key])) { + if (typeof allowedSettings[key] !== 'object' || allowedSettings[key] === null + || Array.isArray(allowedSettings[key])) { + throw new Error(`Expected ${key} to be an object in allowedSettings`); + } + validateSettings(updatedSettings[key], allowedSettings[key]); + } + } + } + + + const onChallenge = async (challenges, subplebbitEdit) => { + let challengeAnswers = []; + + try { + challengeAnswers = await getChallengeAnswersFromUser(challenges) + } + catch (error) { + setNewErrorMessage(error.message); console.log(error); + } + if (challengeAnswers) { + await subplebbitEdit.publishChallengeAnswers(challengeAnswers) + } + }; + + + const onChallengeVerification = (challengeVerification) => { + if (challengeVerification.challengeSuccess === true) { + setNewSuccessMessage('Challenge Success'); + } else if (challengeVerification.challengeSuccess === false) { + setNewErrorMessage(`Challenge Failed, reason: ${challengeVerification.reason}. Errors: ${challengeVerification.errors}`); + console.log('challenge failed', challengeVerification); + } + }; + + + 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(setNewErrorMessage('Could not load challenges')); + }; + }); + }; + + + const [editSubplebbitOptions, setEditSubplebbitOptions] = useState({ + subplebbitAddress: selectedAddress, + onChallenge, + onChallengeVerification, + onError: (error) => { + setNewErrorMessage(error.message); console.log(error); + } + }); + + + const { publishSubplebbitEdit } = usePublishSubplebbitEdit(editSubplebbitOptions); + + + useEffect(() => { + let isActive = true; + if (editSubplebbitOptions && triggerPublilshSubplebbitEdit) { + (async () => { + await publishSubplebbitEdit(editSubplebbitOptions); + if (isActive) { + setTriggerPublishCommentEdit(false); + } + })(); + } + + return () => { + isActive = false; + }; + }, [editSubplebbitOptions, publishSubplebbitEdit, triggerPublilshSubplebbitEdit]); + + + const handleSaveChanges = async () => { + try { + const updatedSettings = JSON.parse(boardSettingsJson); + validateSettings(updatedSettings, allowedSettings); + const changes = getDifferences(initialSettings, updatedSettings); + if (Object.keys(changes).length > 0) { + setEditSubplebbitOptions(prevOptions => ({ + ...prevOptions, + ...changes + })); + setTriggerPublishCommentEdit(true); + } else { + setNewErrorMessage("No changes detected"); + } + } catch (error) { + setNewErrorMessage(`Error saving changes: ${error}`); + console.log(error); + } + }; + + + const handleResetChanges = () => { + setBoardSettingsJson(JSON.stringify(initialSettings, null, 2)); + }; + + + function generateSettingsList(settingsObj, parentKey = '') { + let result = []; + + for (let key in settingsObj) { + if (typeof settingsObj[key] === 'object' && settingsObj[key] !== null) { + const nestedItems = generateSettingsList(settingsObj[key], `${parentKey}${key}.`); + if (nestedItems.length > 1) { + result.push(`${parentKey}${key}: { ${nestedItems.join(', ')} }`); + } else { + result.push(...nestedItems); + } + } else { + result.push(`${parentKey}${key}`); + } + } + + return result; + } + + + const possibleSettingsList = generateSettingsList(initialSettings); + + + const handleCloseModal = () => { + setIsModalOpen(false); + }; + + + return ( + <> + +
+
+ Board Settings + + + +
+
+
+ Allowed settings: + + {`{ ${possibleSettingsList.join(', ')} }`} + +
+ API docs: https://github.com/plebbit/plebbit-js#readme +
+