Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/plebbit/plebchan
Browse files Browse the repository at this point in the history
  • Loading branch information
plebeius-eth committed Aug 20, 2023
2 parents c079895 + 8958917 commit 1243c38
Show file tree
Hide file tree
Showing 10 changed files with 456 additions and 42 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
326 changes: 326 additions & 0 deletions src/components/BoardSettings.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<StyledModal
isOpen={isModalOpen}
onRequestClose={handleCloseModal}
contentLabel="Board Settings"
style={{ overlay: { backgroundColor: "rgba(0,0,0,.25)" }}}
selectedStyle={selectedStyle}
>
<div className="panel-board">
<div className="panel-header">
Board Settings
<Link to="" onClick={handleCloseModal}>
<span className="icon" title="close" />
</Link>
</div>
<div className="settings-info">
<div>
<strong>Allowed settings: </strong>
<span>
{`{ ${possibleSettingsList.join(', ')} }`}
</span>
</div>
<strong style={{marginTop: '10px', display: 'inline-block'}}>API docs: </strong><a style={{color: 'inherit'}} href="https://github.com/plebbit/plebbit-js#readme" target="_blank" rel="noreferrer">https://github.com/plebbit/plebbit-js#readme</a>
</div>
<textarea
value={boardSettingsJson}
onChange={e => setBoardSettingsJson(e.target.value)}
className="board-settings"
autoComplete="off"
autoCorrect="off"
spellCheck="false"
/>
<div className="button-group">
<button id="reset-board-settings" onClick={handleResetChanges}>Reset</button>
<button id="save-board-settings" onClick={handleSaveChanges}>Save Changes</button>
</div>
</div>
</StyledModal>
 [
<span id="subscribe" style={{ cursor: 'pointer' }}>
<span
onClick={() => {
window.electron && window.electron.isElectron
? setIsModalOpen(true)
: alert(
'To edit this board you must be using the plebchan desktop app, which is a plebbit full node that seeds the board automatically.\n\nDownload plebchan here:\n\nhttps://github.com/plebbit/plebchan/releases/latest'
);
}}
>
Settings
</span>
</span>
]
</>
);
};

export default BoardSettings;
40 changes: 18 additions & 22 deletions src/components/StateLabel.jsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,32 @@
import React, { useState, useEffect } from "react";
import React from "react";
import { useAccountComment } from "@plebbit/plebbit-react-hooks";
import useStateString from "../hooks/useStateString";

const StateLabel = ({ commentIndex, className }) => {
const comment = useAccountComment({commentIndex: commentIndex});
const stateString = useStateString(comment);

const [isLoading, setIsLoading] = useState(true);
if (comment.updatedAt !== undefined) {
return null;
}

useEffect(() => {
const timer = setTimeout(() => setIsLoading(false), 2000);
return () => clearTimeout(timer);
}, [commentIndex]);
if (comment.state === "failed") {
return null;
}

if (!stateString) {
return null;
}

return (
commentIndex !== undefined && stateString !== "Succeeded" ? (
(comment.state === "failed") ? (
null
) : (
stateString === undefined && !isLoading && comment.cid === undefined ? (
null
) : (
<span className="ttl">
<br />
(
<span className={className}>
{stateString}
</span>
)
<span className="ttl">
<br />
(
<span className={className}>
{stateString}
</span>
))
) : null
)
</span>
);
};

Expand Down
Loading

0 comments on commit 1243c38

Please sign in to comment.