mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
Merge branch 'development' into master
This commit is contained in:
+1
-1
@@ -7,5 +7,5 @@ module.exports = {
|
||||
trailingComma: 'all',
|
||||
jsxSingleQuote: true,
|
||||
arrowParens: 'always',
|
||||
useTabs: true,
|
||||
useTabs: false,
|
||||
}
|
||||
@@ -2,16 +2,16 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
|
||||
const BoardAvatar = ({ address }) => {
|
||||
const [avatarUrl, setAvatarUrl] = useState('assets/plebchan.png');
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress: address });
|
||||
const [avatarUrl, setAvatarUrl] = useState('assets/plebchan.png');
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress: address });
|
||||
|
||||
useEffect(() => {
|
||||
if (subplebbit.suggested?.avatarUrl) {
|
||||
setAvatarUrl(subplebbit.suggested?.avatarUrl);
|
||||
}
|
||||
}, [subplebbit.suggested?.avatarUrl, subplebbit]);
|
||||
useEffect(() => {
|
||||
if (subplebbit.suggested?.avatarUrl) {
|
||||
setAvatarUrl(subplebbit.suggested?.avatarUrl);
|
||||
}
|
||||
}, [subplebbit.suggested?.avatarUrl, subplebbit]);
|
||||
|
||||
return <img className='board-avatar' alt='board avatar' src={avatarUrl} />;
|
||||
return <img className='board-avatar' alt='board avatar' src={avatarUrl} />;
|
||||
};
|
||||
|
||||
export default BoardAvatar;
|
||||
|
||||
+268
-268
@@ -7,309 +7,309 @@ 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 { 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 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 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 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 [, setNewErrorMessage] = useError();
|
||||
const [, setNewSuccessMessage] = useSuccess();
|
||||
|
||||
const getDifferences = (oldObj, newObj) => {
|
||||
let differences = {};
|
||||
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 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];
|
||||
}
|
||||
}
|
||||
for (let key in newObj) {
|
||||
if (!oldObj.hasOwnProperty(key)) {
|
||||
differences[key] = newObj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return differences;
|
||||
};
|
||||
return differences;
|
||||
};
|
||||
|
||||
const isInitialMount = useRef(true);
|
||||
const isInitialMount = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialMount.current) {
|
||||
setBoardSettingsJson(JSON.stringify(generateSettingsFromSubplebbit(subplebbit), null, 2));
|
||||
isInitialMount.current = false;
|
||||
}
|
||||
}, [subplebbit]);
|
||||
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}`);
|
||||
}
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 = [];
|
||||
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);
|
||||
}
|
||||
};
|
||||
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');
|
||||
console.log('challenge success', challengeVerification);
|
||||
} else if (challengeVerification.challengeSuccess === false) {
|
||||
setNewErrorMessage(`Challenge Failed, reason: ${challengeVerification.reason}. Errors: ${challengeVerification.errors}`);
|
||||
console.log('challenge failed', challengeVerification);
|
||||
}
|
||||
};
|
||||
const onChallengeVerification = (challengeVerification) => {
|
||||
if (challengeVerification.challengeSuccess === true) {
|
||||
setNewSuccessMessage('Challenge Success');
|
||||
console.log('challenge success', challengeVerification);
|
||||
} 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);
|
||||
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;
|
||||
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);
|
||||
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();
|
||||
}
|
||||
};
|
||||
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);
|
||||
setCaptchaResponse('');
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
setResolveCaptchaPromise(resolve);
|
||||
};
|
||||
setResolveCaptchaPromise(resolve);
|
||||
};
|
||||
|
||||
challengeImg.onerror = () => {
|
||||
reject(setNewErrorMessage('Could not load challenges'));
|
||||
};
|
||||
});
|
||||
};
|
||||
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 [editSubplebbitOptions, setEditSubplebbitOptions] = useState({
|
||||
subplebbitAddress: selectedAddress,
|
||||
onChallenge,
|
||||
onChallengeVerification,
|
||||
onError: (error) => {
|
||||
setNewErrorMessage(error.message);
|
||||
console.log(error);
|
||||
},
|
||||
});
|
||||
|
||||
const { publishSubplebbitEdit } = usePublishSubplebbitEdit(editSubplebbitOptions);
|
||||
const { publishSubplebbitEdit } = usePublishSubplebbitEdit(editSubplebbitOptions);
|
||||
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
if (editSubplebbitOptions && triggerPublilshSubplebbitEdit) {
|
||||
(async () => {
|
||||
await publishSubplebbitEdit(editSubplebbitOptions);
|
||||
if (isActive) {
|
||||
setTriggerPublishCommentEdit(false);
|
||||
}
|
||||
})();
|
||||
}
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
if (editSubplebbitOptions && triggerPublilshSubplebbitEdit) {
|
||||
(async () => {
|
||||
await publishSubplebbitEdit(editSubplebbitOptions);
|
||||
if (isActive) {
|
||||
setTriggerPublishCommentEdit(false);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, [editSubplebbitOptions, publishSubplebbitEdit, triggerPublilshSubplebbitEdit]);
|
||||
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 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));
|
||||
};
|
||||
const handleResetChanges = () => {
|
||||
setBoardSettingsJson(JSON.stringify(initialSettings, null, 2));
|
||||
};
|
||||
|
||||
function generateSettingsList(settingsObj, parentKey = '') {
|
||||
let result = [];
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const possibleSettingsList = generateSettingsList(initialSettings);
|
||||
const possibleSettingsList = generateSettingsList(initialSettings);
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
setBoardSettingsJson(JSON.stringify(initialSettings, null, 2));
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
setBoardSettingsJson(JSON.stringify(initialSettings, null, 2));
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
setIsModalOpen(true);
|
||||
setBoardSettingsJson(JSON.stringify(generateSettingsFromSubplebbit(subplebbit), null, 2));
|
||||
};
|
||||
const openModal = () => {
|
||||
setIsModalOpen(true);
|
||||
setBoardSettingsJson(JSON.stringify(generateSettingsFromSubplebbit(subplebbit), null, 2));
|
||||
};
|
||||
|
||||
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
|
||||
? openModal()
|
||||
: 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>
|
||||
]
|
||||
</>
|
||||
);
|
||||
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
|
||||
? openModal()
|
||||
: 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;
|
||||
|
||||
@@ -5,70 +5,70 @@ import { Break } from './styled/views/Board.styled';
|
||||
import useGeneralStore from '../hooks/stores/useGeneralStore';
|
||||
|
||||
const BoardStats = ({ subplebbitAddress }) => {
|
||||
const { selectedStyle } = useGeneralStore((state) => state);
|
||||
const stats = useSubplebbitStats({ subplebbitAddress });
|
||||
const [showStats, setShowStats] = useState(true);
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const { selectedStyle } = useGeneralStore((state) => state);
|
||||
const stats = useSubplebbitStats({ subplebbitAddress });
|
||||
const [showStats, setShowStats] = useState(true);
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
|
||||
const handleToggleStats = () => {
|
||||
setShowStats(!showStats);
|
||||
};
|
||||
const handleToggleStats = () => {
|
||||
setShowStats(!showStats);
|
||||
};
|
||||
|
||||
const unixToMMDDYYYY = (timestamp) => {
|
||||
const date = new Date(timestamp * 1000);
|
||||
const month = ('0' + (date.getMonth() + 1)).slice(-2);
|
||||
const day = ('0' + date.getDate()).slice(-2);
|
||||
const year = date.getFullYear().toString().slice(-2);
|
||||
return month + '/' + day + '/' + year;
|
||||
};
|
||||
const unixToMMDDYYYY = (timestamp) => {
|
||||
const date = new Date(timestamp * 1000);
|
||||
const month = ('0' + (date.getMonth() + 1)).slice(-2);
|
||||
const day = ('0' + date.getDate()).slice(-2);
|
||||
const year = date.getFullYear().toString().slice(-2);
|
||||
return month + '/' + day + '/' + year;
|
||||
};
|
||||
|
||||
const pluralize = (count, singular, plural) => (count === 1 ? singular : plural);
|
||||
const pluralize = (count, singular, plural) => (count === 1 ? singular : plural);
|
||||
|
||||
return (
|
||||
<BoardStatsContainer selectedStyle={selectedStyle}>
|
||||
<Break selectedStyle={selectedStyle} style={{ width: '468px' }} />
|
||||
<table id='blotter'>
|
||||
{showStats && (
|
||||
<tbody id='blotter-msgs'>
|
||||
<tr>
|
||||
<td>
|
||||
In the past hour, <span id='stat-number'>{stats.hourActiveUserCount}</span> {pluralize(stats.hourActiveUserCount, 'user', 'users')} made{' '}
|
||||
<span id='stat-number'>{stats.hourPostCount}</span> {pluralize(stats.hourPostCount, 'post', 'posts')} / in the past day,{' '}
|
||||
<span id='stat-number'>{stats.dayActiveUserCount}</span> {pluralize(stats.dayActiveUserCount, 'user', 'users')} made{' '}
|
||||
<span id='stat-number'>{stats.dayPostCount}</span> {pluralize(stats.dayPostCount, 'post', 'posts')}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
In the past week, <span id='stat-number'>{stats.weekActiveUserCount}</span> {pluralize(stats.weekActiveUserCount, 'user', 'users')} made{' '}
|
||||
<span id='stat-number'>{stats.weekPostCount}</span> {pluralize(stats.weekPostCount, 'post', 'posts')} / in the past month,{' '}
|
||||
<span id='stat-number'>{stats.monthActiveUserCount}</span> {pluralize(stats.monthActiveUserCount, 'user', 'users')} made{' '}
|
||||
<span id='stat-number'>{stats.monthPostCount}</span> {pluralize(stats.monthPostCount, 'post', 'posts')}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{unixToMMDDYYYY(subplebbit.createdAt)} board created / since then, <span id='stat-number'>{stats.allActiveUserCount}</span>{' '}
|
||||
{pluralize(stats.allActiveUserCount, 'user', 'users')} have made <span id='stat-number'>{stats.allPostCount}</span>{' '}
|
||||
{pluralize(stats.allPostCount, 'post', 'posts')}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
)}
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={2}>
|
||||
[
|
||||
<span id='stat-number' className='hide-button' onClick={handleToggleStats}>
|
||||
{showStats ? 'Hide' : 'Show Stats'}
|
||||
</span>
|
||||
]
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</BoardStatsContainer>
|
||||
);
|
||||
return (
|
||||
<BoardStatsContainer selectedStyle={selectedStyle}>
|
||||
<Break selectedStyle={selectedStyle} style={{ width: '468px' }} />
|
||||
<table id='blotter'>
|
||||
{showStats && (
|
||||
<tbody id='blotter-msgs'>
|
||||
<tr>
|
||||
<td>
|
||||
In the past hour, <span id='stat-number'>{stats.hourActiveUserCount}</span> {pluralize(stats.hourActiveUserCount, 'user', 'users')} made{' '}
|
||||
<span id='stat-number'>{stats.hourPostCount}</span> {pluralize(stats.hourPostCount, 'post', 'posts')} / in the past day,{' '}
|
||||
<span id='stat-number'>{stats.dayActiveUserCount}</span> {pluralize(stats.dayActiveUserCount, 'user', 'users')} made{' '}
|
||||
<span id='stat-number'>{stats.dayPostCount}</span> {pluralize(stats.dayPostCount, 'post', 'posts')}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
In the past week, <span id='stat-number'>{stats.weekActiveUserCount}</span> {pluralize(stats.weekActiveUserCount, 'user', 'users')} made{' '}
|
||||
<span id='stat-number'>{stats.weekPostCount}</span> {pluralize(stats.weekPostCount, 'post', 'posts')} / in the past month,{' '}
|
||||
<span id='stat-number'>{stats.monthActiveUserCount}</span> {pluralize(stats.monthActiveUserCount, 'user', 'users')} made{' '}
|
||||
<span id='stat-number'>{stats.monthPostCount}</span> {pluralize(stats.monthPostCount, 'post', 'posts')}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{unixToMMDDYYYY(subplebbit.createdAt)} board created / since then, <span id='stat-number'>{stats.allActiveUserCount}</span>{' '}
|
||||
{pluralize(stats.allActiveUserCount, 'user', 'users')} have made <span id='stat-number'>{stats.allPostCount}</span>{' '}
|
||||
{pluralize(stats.allPostCount, 'post', 'posts')}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
)}
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={2}>
|
||||
[
|
||||
<span id='stat-number' className='hide-button' onClick={handleToggleStats}>
|
||||
{showStats ? 'Hide' : 'Show Stats'}
|
||||
</span>
|
||||
]
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</BoardStatsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardStats;
|
||||
|
||||
@@ -3,47 +3,47 @@ import ContentLoader from 'react-content-loader';
|
||||
import useGeneralStore from '../hooks/stores/useGeneralStore';
|
||||
|
||||
const SingleRectLoader = () => {
|
||||
const selectedStyle = useGeneralStore((state) => state.selectedStyle);
|
||||
const backgroundColor = selectedStyle === 'Tomorrow' ? '#333' : '#f3f3f3';
|
||||
const foregroundColor = selectedStyle === 'Tomorrow' ? '#555' : '#ecebeb';
|
||||
const selectedStyle = useGeneralStore((state) => state.selectedStyle);
|
||||
const backgroundColor = selectedStyle === 'Tomorrow' ? '#333' : '#f3f3f3';
|
||||
const foregroundColor = selectedStyle === 'Tomorrow' ? '#555' : '#ecebeb';
|
||||
|
||||
return (
|
||||
<ContentLoader
|
||||
speed={2}
|
||||
width={150}
|
||||
height={215}
|
||||
viewBox='0 0 150 215'
|
||||
backgroundColor={backgroundColor}
|
||||
foregroundColor={foregroundColor}
|
||||
style={{
|
||||
width: '150px',
|
||||
height: '215px',
|
||||
marginRight: '30px',
|
||||
marginBottom: '30px',
|
||||
}}
|
||||
>
|
||||
<rect x={0} y={0} width='150' height='150' />
|
||||
<rect x={0} y={170} width='150' height='18' />
|
||||
<rect x={0} y={195} width='80' height='20' />
|
||||
</ContentLoader>
|
||||
);
|
||||
return (
|
||||
<ContentLoader
|
||||
speed={2}
|
||||
width={150}
|
||||
height={215}
|
||||
viewBox='0 0 150 215'
|
||||
backgroundColor={backgroundColor}
|
||||
foregroundColor={foregroundColor}
|
||||
style={{
|
||||
width: '150px',
|
||||
height: '215px',
|
||||
marginRight: '30px',
|
||||
marginBottom: '30px',
|
||||
}}
|
||||
>
|
||||
<rect x={0} y={0} width='150' height='150' />
|
||||
<rect x={0} y={170} width='150' height='18' />
|
||||
<rect x={0} y={195} width='80' height='20' />
|
||||
</ContentLoader>
|
||||
);
|
||||
};
|
||||
|
||||
const CatalogLoader = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
{[...Array(24)].map((_, index) => (
|
||||
<SingleRectLoader key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
{[...Array(24)].map((_, index) => (
|
||||
<SingleRectLoader key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CatalogLoader;
|
||||
|
||||
@@ -6,62 +6,62 @@ import getDate from '../utils/getDate';
|
||||
import useGeneralStore from '../hooks/stores/useGeneralStore';
|
||||
|
||||
const EditLabel = ({ commentCid, className }) => {
|
||||
const { editedComments, setEditedComments } = useGeneralStore((state) => state);
|
||||
const [isOriginalCommentModalOpen, setIsOriginalCommentModalOpen] = useState(false);
|
||||
const comment = useComment({ commentCid });
|
||||
const timestamp = getDate(comment.edit?.timestamp);
|
||||
const { state: editedCommentState, editedComment } = useEditedComment({ comment });
|
||||
const { editedComments, setEditedComments } = useGeneralStore((state) => state);
|
||||
const [isOriginalCommentModalOpen, setIsOriginalCommentModalOpen] = useState(false);
|
||||
const comment = useComment({ commentCid });
|
||||
const timestamp = getDate(comment.edit?.timestamp);
|
||||
const { state: editedCommentState, editedComment } = useEditedComment({ comment });
|
||||
|
||||
if (editedCommentState === 'pending' && !(commentCid in editedComments)) {
|
||||
setEditedComments({ ...editedComments, [commentCid]: editedComment });
|
||||
}
|
||||
if (editedCommentState === 'pending' && !(commentCid in editedComments)) {
|
||||
setEditedComments({ ...editedComments, [commentCid]: editedComment });
|
||||
}
|
||||
|
||||
const conditionsCheck = () => {
|
||||
let conditions = [];
|
||||
const conditionsCheck = () => {
|
||||
let conditions = [];
|
||||
|
||||
if (editedComment?.removed && !comment.removed) {
|
||||
conditions.push('removal');
|
||||
}
|
||||
if (editedComment?.edit && !comment.edit) {
|
||||
conditions.push('edit');
|
||||
}
|
||||
if (editedComment?.locked && !comment.locked) {
|
||||
conditions.push('lock');
|
||||
}
|
||||
if (editedComment?.pinned && !comment.pinned) {
|
||||
conditions.push('sticky');
|
||||
}
|
||||
if (editedComment?.removed && !comment.removed) {
|
||||
conditions.push('removal');
|
||||
}
|
||||
if (editedComment?.edit && !comment.edit) {
|
||||
conditions.push('edit');
|
||||
}
|
||||
if (editedComment?.locked && !comment.locked) {
|
||||
conditions.push('lock');
|
||||
}
|
||||
if (editedComment?.pinned && !comment.pinned) {
|
||||
conditions.push('sticky');
|
||||
}
|
||||
|
||||
return conditions.length > 0 ? conditions.join(', ') : null;
|
||||
};
|
||||
return conditions.length > 0 ? conditions.join(', ') : null;
|
||||
};
|
||||
|
||||
const conditionsString = conditionsCheck();
|
||||
const conditionsString = conditionsCheck();
|
||||
|
||||
return (
|
||||
<>
|
||||
<OriginalCommentModal isOpen={isOriginalCommentModalOpen} closeModal={() => setIsOriginalCommentModalOpen(false)} comment={comment} />
|
||||
{comment.edit && comment.original?.content !== comment?.content ? (
|
||||
<>
|
||||
<br />
|
||||
<span className={className}>
|
||||
(Edited at {timestamp},{' '}
|
||||
<Link className='ttl-link' onClick={() => setIsOriginalCommentModalOpen(true)}>
|
||||
show original
|
||||
</Link>
|
||||
)
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
{(editedCommentState === 'pending' || editedCommentState === 'failed') && conditionsString ? (
|
||||
<>
|
||||
<br />
|
||||
<span className={className}>
|
||||
({editedCommentState === 'pending' ? 'Pending' : 'Failed'} {conditionsString})
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<OriginalCommentModal isOpen={isOriginalCommentModalOpen} closeModal={() => setIsOriginalCommentModalOpen(false)} comment={comment} />
|
||||
{comment.edit && comment.original?.content !== comment?.content ? (
|
||||
<>
|
||||
<br />
|
||||
<span className={className}>
|
||||
(Edited at {timestamp},{' '}
|
||||
<Link className='ttl-link' onClick={() => setIsOriginalCommentModalOpen(true)}>
|
||||
show original
|
||||
</Link>
|
||||
)
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
{(editedCommentState === 'pending' || editedCommentState === 'failed') && conditionsString ? (
|
||||
<>
|
||||
<br />
|
||||
<span className={className}>
|
||||
({editedCommentState === 'pending' ? 'Pending' : 'Failed'} {conditionsString})
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditLabel;
|
||||
|
||||
+198
-198
@@ -1,100 +1,100 @@
|
||||
const Embed = ({ url }) => {
|
||||
const parsedUrl = new URL(url);
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
if (youtubeHosts.has(parsedUrl.host)) {
|
||||
return <YoutubeEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (twitterHosts.has(parsedUrl.host)) {
|
||||
return <TwitterEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (redditHosts.has(parsedUrl.host)) {
|
||||
return <RedditEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (twitchHosts.has(parsedUrl.host)) {
|
||||
return <TwitchEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (tiktokHosts.has(parsedUrl.host)) {
|
||||
return <TiktokEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (instagramHosts.has(parsedUrl.host)) {
|
||||
return <InstagramEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (odyseeHosts.has(parsedUrl.host)) {
|
||||
return <OdyseeEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (bitchuteHosts.has(parsedUrl.host)) {
|
||||
return <BitchuteEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (streamableHosts.has(parsedUrl.host)) {
|
||||
return <StreamableEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (spotifyHosts.has(parsedUrl.host)) {
|
||||
return <SpotifyEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (youtubeHosts.has(parsedUrl.host)) {
|
||||
return <YoutubeEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (twitterHosts.has(parsedUrl.host)) {
|
||||
return <TwitterEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (redditHosts.has(parsedUrl.host)) {
|
||||
return <RedditEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (twitchHosts.has(parsedUrl.host)) {
|
||||
return <TwitchEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (tiktokHosts.has(parsedUrl.host)) {
|
||||
return <TiktokEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (instagramHosts.has(parsedUrl.host)) {
|
||||
return <InstagramEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (odyseeHosts.has(parsedUrl.host)) {
|
||||
return <OdyseeEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (bitchuteHosts.has(parsedUrl.host)) {
|
||||
return <BitchuteEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (streamableHosts.has(parsedUrl.host)) {
|
||||
return <StreamableEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (spotifyHosts.has(parsedUrl.host)) {
|
||||
return <SpotifyEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
};
|
||||
|
||||
const youtubeHosts = new Set(['youtube.com', 'www.youtube.com', 'youtu.be', 'www.youtu.be']);
|
||||
|
||||
const YoutubeEmbed = ({ parsedUrl }) => {
|
||||
let youtubeId;
|
||||
if (parsedUrl.host.endsWith('youtu.be')) {
|
||||
youtubeId = parsedUrl.pathname.replaceAll('/', '');
|
||||
} else {
|
||||
youtubeId = parsedUrl.searchParams.get('v');
|
||||
}
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged youtube-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowfullscreen
|
||||
title={parsedUrl.href}
|
||||
src={`https://www.youtube-nocookie.com/embed/${youtubeId}`}
|
||||
/>
|
||||
);
|
||||
let youtubeId;
|
||||
if (parsedUrl.host.endsWith('youtu.be')) {
|
||||
youtubeId = parsedUrl.pathname.replaceAll('/', '');
|
||||
} else {
|
||||
youtubeId = parsedUrl.searchParams.get('v');
|
||||
}
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged youtube-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowfullscreen
|
||||
title={parsedUrl.href}
|
||||
src={`https://www.youtube-nocookie.com/embed/${youtubeId}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const twitterHosts = new Set(['twitter.com', 'www.twitter.com', 'x.com', 'www.x.com']);
|
||||
|
||||
const TwitterEmbed = ({ parsedUrl }) => {
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged twitter-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
title={parsedUrl.href}
|
||||
srcdoc={`
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged twitter-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
title={parsedUrl.href}
|
||||
srcdoc={`
|
||||
<blockquote class="twitter-tweet" data-theme="dark">
|
||||
<a href="${parsedUrl.href.replace('x.com', 'twitter.com')}"></a>
|
||||
</blockquote>
|
||||
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
`}
|
||||
/>
|
||||
);
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const redditHosts = new Set(['reddit.com', 'www.reddit.com', 'old.reddit.com']);
|
||||
|
||||
const RedditEmbed = ({ parsedUrl }) => {
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged reddit-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
title={parsedUrl.href}
|
||||
srcdoc={`
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged reddit-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
title={parsedUrl.href}
|
||||
srcdoc={`
|
||||
<style>
|
||||
/* fix reddit iframe being centered */
|
||||
iframe {
|
||||
@@ -106,177 +106,177 @@ const RedditEmbed = ({ parsedUrl }) => {
|
||||
</blockquote>
|
||||
<script async src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script>
|
||||
`}
|
||||
/>
|
||||
);
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const twitchHosts = new Set(['twitch.tv', 'www.twitch.tv']);
|
||||
|
||||
const TwitchEmbed = ({ parsedUrl }) => {
|
||||
let iframeUrl;
|
||||
if (parsedUrl.pathname.startsWith('/videos/')) {
|
||||
const videoId = parsedUrl.pathname.replace('/videos/', '');
|
||||
iframeUrl = `https://player.twitch.tv/?video=${videoId}&parent=${window.location.hostname}`;
|
||||
} else {
|
||||
const channel = parsedUrl.pathname.replaceAll('/', '');
|
||||
iframeUrl = `https://player.twitch.tv/?channel=${channel}&parent=${window.location.hostname}`;
|
||||
}
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged twitch-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowfullscreen
|
||||
title={parsedUrl.href}
|
||||
src={iframeUrl}
|
||||
/>
|
||||
);
|
||||
let iframeUrl;
|
||||
if (parsedUrl.pathname.startsWith('/videos/')) {
|
||||
const videoId = parsedUrl.pathname.replace('/videos/', '');
|
||||
iframeUrl = `https://player.twitch.tv/?video=${videoId}&parent=${window.location.hostname}`;
|
||||
} else {
|
||||
const channel = parsedUrl.pathname.replaceAll('/', '');
|
||||
iframeUrl = `https://player.twitch.tv/?channel=${channel}&parent=${window.location.hostname}`;
|
||||
}
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged twitch-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowfullscreen
|
||||
title={parsedUrl.href}
|
||||
src={iframeUrl}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const tiktokHosts = new Set(['tiktok.com', 'www.tiktok.com']);
|
||||
|
||||
const TiktokEmbed = ({ parsedUrl }) => {
|
||||
const videoId = parsedUrl.pathname.replace(/.+\/video\//, '').replaceAll('/', '');
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged tiktok-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
title={parsedUrl.href}
|
||||
srcdoc={`
|
||||
const videoId = parsedUrl.pathname.replace(/.+\/video\//, '').replaceAll('/', '');
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged tiktok-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
title={parsedUrl.href}
|
||||
srcdoc={`
|
||||
<blockquote class="tiktok-embed" data-video-id="${videoId}">
|
||||
<a></a>
|
||||
</blockquote>
|
||||
<script async src="https://www.tiktok.com/embed.js"></script>
|
||||
`}
|
||||
/>
|
||||
);
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const instagramHosts = new Set(['instagram.com', 'www.instagram.com']);
|
||||
|
||||
const InstagramEmbed = ({ parsedUrl }) => {
|
||||
const pathNames = parsedUrl.pathname.replace(/\/+$/, '').split('/');
|
||||
const id = pathNames[pathNames.length - 1];
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged instagram-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
title={parsedUrl.href}
|
||||
srcdoc={`
|
||||
const pathNames = parsedUrl.pathname.replace(/\/+$/, '').split('/');
|
||||
const id = pathNames[pathNames.length - 1];
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged instagram-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
title={parsedUrl.href}
|
||||
srcdoc={`
|
||||
<blockquote class="instagram-media">
|
||||
<a href="https://www.instagram.com/p/${id}/"></a>
|
||||
</blockquote>
|
||||
<script async src="//www.instagram.com/embed.js"></script>
|
||||
`}
|
||||
/>
|
||||
);
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const odyseeHosts = new Set(['odysee.com', 'www.odysee.com']);
|
||||
|
||||
const OdyseeEmbed = ({ parsedUrl }) => {
|
||||
const iframeUrl = `https://odysee.com/$/embed${parsedUrl.pathname}`;
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged odysee-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowfullscreen
|
||||
title={parsedUrl.href}
|
||||
src={iframeUrl}
|
||||
/>
|
||||
);
|
||||
const iframeUrl = `https://odysee.com/$/embed${parsedUrl.pathname}`;
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged odysee-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowfullscreen
|
||||
title={parsedUrl.href}
|
||||
src={iframeUrl}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const bitchuteHosts = new Set(['bitchute.com', 'www.bitchute.com']);
|
||||
|
||||
const BitchuteEmbed = ({ parsedUrl }) => {
|
||||
const videoId = parsedUrl.pathname.replace(/\/video\//, '').replaceAll('/', '');
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged bitchute-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowfullscreen
|
||||
title={parsedUrl.href}
|
||||
src={`https://www.bitchute.com/embed/${videoId}/`}
|
||||
/>
|
||||
);
|
||||
const videoId = parsedUrl.pathname.replace(/\/video\//, '').replaceAll('/', '');
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged bitchute-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowfullscreen
|
||||
title={parsedUrl.href}
|
||||
src={`https://www.bitchute.com/embed/${videoId}/`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const streamableHosts = new Set(['streamable.com', 'www.streamable.com']);
|
||||
|
||||
const StreamableEmbed = ({ parsedUrl }) => {
|
||||
const videoId = parsedUrl.pathname.replaceAll('/', '');
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged streamable-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowfullscreen
|
||||
title={parsedUrl.href}
|
||||
src={`https://streamable.com/e/${videoId}`}
|
||||
/>
|
||||
);
|
||||
const videoId = parsedUrl.pathname.replaceAll('/', '');
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged streamable-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowfullscreen
|
||||
title={parsedUrl.href}
|
||||
src={`https://streamable.com/e/${videoId}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const spotifyHosts = new Set(['spotify.com', 'www.spotify.com', 'open.spotify.com']);
|
||||
|
||||
const SpotifyEmbed = ({ parsedUrl }) => {
|
||||
const iframeUrl = `https://open.spotify.com/embed${parsedUrl.pathname}?theme=0`;
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged spotify-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowfullscreen
|
||||
title={parsedUrl.href}
|
||||
src={iframeUrl}
|
||||
/>
|
||||
);
|
||||
const iframeUrl = `https://open.spotify.com/embed${parsedUrl.pathname}?theme=0`;
|
||||
return (
|
||||
<iframe
|
||||
className='enlarged spotify-embed'
|
||||
height='100%'
|
||||
width='100%'
|
||||
frameborder='0'
|
||||
credentialless
|
||||
referrerpolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowfullscreen
|
||||
title={parsedUrl.href}
|
||||
src={iframeUrl}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const canEmbedHosts = new Set([
|
||||
...youtubeHosts,
|
||||
...twitterHosts,
|
||||
...redditHosts,
|
||||
...twitchHosts,
|
||||
...tiktokHosts,
|
||||
...instagramHosts,
|
||||
...odyseeHosts,
|
||||
...bitchuteHosts,
|
||||
...streamableHosts,
|
||||
...spotifyHosts,
|
||||
...youtubeHosts,
|
||||
...twitterHosts,
|
||||
...redditHosts,
|
||||
...twitchHosts,
|
||||
...tiktokHosts,
|
||||
...instagramHosts,
|
||||
...odyseeHosts,
|
||||
...bitchuteHosts,
|
||||
...streamableHosts,
|
||||
...spotifyHosts,
|
||||
]);
|
||||
|
||||
export const canEmbed = (parsedUrl) => canEmbedHosts.has(parsedUrl.host);
|
||||
|
||||
@@ -2,28 +2,28 @@ import React, { useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const ForwardRefLink = React.forwardRef((props, ref) => {
|
||||
const { children, setRefAndCid, onMouseOver, onMouseLeave, onClick, ...otherProps } = props;
|
||||
const { children, setRefAndCid, onMouseOver, onMouseLeave, onClick, ...otherProps } = props;
|
||||
|
||||
const handleClick = (event) => {
|
||||
if (otherProps.to === '#void') {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (onClick) {
|
||||
onClick(event);
|
||||
}
|
||||
};
|
||||
const handleClick = (event) => {
|
||||
if (otherProps.to === '#void') {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (onClick) {
|
||||
onClick(event);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && typeof setRefAndCid === 'function') {
|
||||
setRefAndCid(ref.current);
|
||||
}
|
||||
}, [ref, setRefAndCid]);
|
||||
useEffect(() => {
|
||||
if (ref.current && typeof setRefAndCid === 'function') {
|
||||
setRefAndCid(ref.current);
|
||||
}
|
||||
}, [ref, setRefAndCid]);
|
||||
|
||||
return (
|
||||
<Link ref={ref} {...otherProps} onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} onClick={handleClick}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
return (
|
||||
<Link ref={ref} {...otherProps} onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} onClick={handleClick}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
export default ForwardRefLink;
|
||||
|
||||
@@ -2,40 +2,40 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
const ImageBanner = () => {
|
||||
const [currentImage, setCurrentImage] = useState(null);
|
||||
const location = useLocation();
|
||||
const [currentImage, setCurrentImage] = useState(null);
|
||||
const location = useLocation();
|
||||
|
||||
const parentRoute = location.pathname.split('/').slice(0, 3).join('/');
|
||||
const parentRoute = location.pathname.split('/').slice(0, 3).join('/');
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const loadRandomImage = async () => {
|
||||
const images = await importAll(require.context('../../public/assets/banners', false, /\.(png|jpe?g|svg)$/));
|
||||
const randomImage = Math.floor(Math.random() * images.length) + 1;
|
||||
const loadRandomImage = async () => {
|
||||
const images = await importAll(require.context('../../public/assets/banners', false, /\.(png|jpe?g|svg)$/));
|
||||
const randomImage = Math.floor(Math.random() * images.length) + 1;
|
||||
|
||||
const img = new Image();
|
||||
img.src = `${process.env.PUBLIC_URL}/assets/banners/banner-${randomImage}.jpg`;
|
||||
const img = new Image();
|
||||
img.src = `${process.env.PUBLIC_URL}/assets/banners/banner-${randomImage}.jpg`;
|
||||
|
||||
img.onload = () => {
|
||||
if (isMounted) {
|
||||
setCurrentImage(randomImage);
|
||||
}
|
||||
};
|
||||
};
|
||||
img.onload = () => {
|
||||
if (isMounted) {
|
||||
setCurrentImage(randomImage);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
loadRandomImage();
|
||||
loadRandomImage();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [parentRoute]);
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [parentRoute]);
|
||||
|
||||
return <>{currentImage && <img id='banner-img' src={`${process.env.PUBLIC_URL}/assets/banners/banner-${currentImage}.jpg`} alt='banner' />}</>;
|
||||
return <>{currentImage && <img id='banner-img' src={`${process.env.PUBLIC_URL}/assets/banners/banner-${currentImage}.jpg`} alt='banner' />}</>;
|
||||
};
|
||||
|
||||
export function importAll(r) {
|
||||
return r.keys().map(r);
|
||||
return r.keys().map(r);
|
||||
}
|
||||
|
||||
export default ImageBanner;
|
||||
|
||||
@@ -2,27 +2,27 @@ import React from 'react';
|
||||
import { useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
|
||||
const OfflineIndicator = ({ address, className, tooltipPlace }) => {
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress: address });
|
||||
const isOnline = subplebbit.updatedAt > Date.now() / 1000 - 60 * 20;
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress: address });
|
||||
const isOnline = subplebbit.updatedAt > Date.now() / 1000 - 60 * 20;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isOnline && (
|
||||
<>
|
||||
{' '}
|
||||
<img
|
||||
className={className}
|
||||
alt='offline'
|
||||
src='assets/offline.png'
|
||||
data-tooltip-id='tooltip'
|
||||
data-tooltip-content='Offline'
|
||||
data-tooltip-place={tooltipPlace}
|
||||
style={{ imageRendering: 'pixelated' }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{!isOnline && (
|
||||
<>
|
||||
{' '}
|
||||
<img
|
||||
className={className}
|
||||
alt='offline'
|
||||
src='assets/offline.png'
|
||||
data-tooltip-id='tooltip'
|
||||
data-tooltip-content='Offline'
|
||||
data-tooltip-place={tooltipPlace}
|
||||
style={{ imageRendering: 'pixelated' }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OfflineIndicator;
|
||||
|
||||
@@ -2,17 +2,17 @@ import React from 'react';
|
||||
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
|
||||
|
||||
const PendingLabel = ({ commentIndex }) => {
|
||||
const comment = useAccountComment({ commentIndex: commentIndex });
|
||||
const comment = useAccountComment({ commentIndex: commentIndex });
|
||||
|
||||
if (commentIndex === undefined) return null;
|
||||
if (commentIndex === undefined) return null;
|
||||
|
||||
return comment.cid ? (
|
||||
<span>{comment.cid.slice(2, 14)}</span>
|
||||
) : comment.state === 'pending' ? (
|
||||
<span style={{ color: 'red', fontWeight: '700' }}>Pending</span>
|
||||
) : comment.state === 'failed' ? (
|
||||
<span style={{ color: 'red', fontWeight: '700' }}>Failed</span>
|
||||
) : null;
|
||||
return comment.cid ? (
|
||||
<span>{comment.cid.slice(2, 14)}</span>
|
||||
) : comment.state === 'pending' ? (
|
||||
<span style={{ color: 'red', fontWeight: '700' }}>Pending</span>
|
||||
) : comment.state === 'failed' ? (
|
||||
<span style={{ color: 'red', fontWeight: '700' }}>Failed</span>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default PendingLabel;
|
||||
|
||||
+109
-109
@@ -5,131 +5,131 @@ import breaks from 'remark-breaks';
|
||||
import ForwardRefLink from './ForwardRefLink';
|
||||
|
||||
const Post = ({ content, postQuoteOnClick, postQuoteOnOver, postQuoteOnLeave, postQuoteRef }) => {
|
||||
const doubleNewlineContent = content?.replace(/\n/g, ' \n\n');
|
||||
const doubleNewlineContent = content?.replace(/\n/g, ' \n\n');
|
||||
|
||||
const customSchema = useMemo(
|
||||
() => ({
|
||||
...defaultSchema,
|
||||
tagNames: [...defaultSchema.tagNames, 'div'],
|
||||
attributes: {
|
||||
...defaultSchema.attributes,
|
||||
div: ['className'],
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const customSchema = useMemo(
|
||||
() => ({
|
||||
...defaultSchema,
|
||||
tagNames: [...defaultSchema.tagNames, 'div'],
|
||||
attributes: {
|
||||
...defaultSchema.attributes,
|
||||
div: ['className'],
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const blockquoteToGreentext = () => (tree) => {
|
||||
tree.children.forEach((node) => {
|
||||
if (node.type === 'blockquote') {
|
||||
node.children.forEach((child) => {
|
||||
if (child.type === 'paragraph' && child.children.length > 0) {
|
||||
const prefix = {
|
||||
type: 'text',
|
||||
value: '>',
|
||||
};
|
||||
child.children.unshift(prefix);
|
||||
}
|
||||
});
|
||||
node.type = 'div';
|
||||
node.data = {
|
||||
hName: 'div',
|
||||
hProperties: {
|
||||
className: 'greentext',
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
const blockquoteToGreentext = () => (tree) => {
|
||||
tree.children.forEach((node) => {
|
||||
if (node.type === 'blockquote') {
|
||||
node.children.forEach((child) => {
|
||||
if (child.type === 'paragraph' && child.children.length > 0) {
|
||||
const prefix = {
|
||||
type: 'text',
|
||||
value: '>',
|
||||
};
|
||||
child.children.unshift(prefix);
|
||||
}
|
||||
});
|
||||
node.type = 'div';
|
||||
node.data = {
|
||||
hName: 'div',
|
||||
hProperties: {
|
||||
className: 'greentext',
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const createQuotelink = (children, postQuoteOnClick, postQuoteOnOver, postQuoteOnLeave, postQuoteRef) => {
|
||||
const patternC = '(c/[A-Za-z0-9]{46}|c/[A-Za-z0-9]{12})';
|
||||
const patternP = '(p/([A-Za-z0-9]{52}|[A-Za-z0-9-.]*\\.eth))';
|
||||
const patternU = '(u/([A-Za-z0-9]{52}|[A-Za-z0-9-.]*\\.eth))';
|
||||
const patternPC = '(p/([A-Za-z0-9]{52}|[A-Za-z0-9-.]*\\.eth)/c/[A-Za-z0-9]{46})';
|
||||
const createQuotelink = (children, postQuoteOnClick, postQuoteOnOver, postQuoteOnLeave, postQuoteRef) => {
|
||||
const patternC = '(c/[A-Za-z0-9]{46}|c/[A-Za-z0-9]{12})';
|
||||
const patternP = '(p/([A-Za-z0-9]{52}|[A-Za-z0-9-.]*\\.eth))';
|
||||
const patternU = '(u/([A-Za-z0-9]{52}|[A-Za-z0-9-.]*\\.eth))';
|
||||
const patternPC = '(p/([A-Za-z0-9]{52}|[A-Za-z0-9-.]*\\.eth)/c/[A-Za-z0-9]{46})';
|
||||
|
||||
const regex = new RegExp(`${patternC}|${patternPC}|${patternU}|${patternP}`, 'g');
|
||||
const regex = new RegExp(`${patternC}|${patternPC}|${patternU}|${patternP}`, 'g');
|
||||
|
||||
return children?.flatMap((child, i) => {
|
||||
if (typeof child !== 'string') {
|
||||
return child;
|
||||
}
|
||||
return children?.flatMap((child, i) => {
|
||||
if (typeof child !== 'string') {
|
||||
return child;
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
let match;
|
||||
let lastIndex = 0;
|
||||
const parts = [];
|
||||
let match;
|
||||
let lastIndex = 0;
|
||||
|
||||
while ((match = regex.exec(child)) !== null) {
|
||||
const matchedText = match[0];
|
||||
const index = match.index;
|
||||
while ((match = regex.exec(child)) !== null) {
|
||||
const matchedText = match[0];
|
||||
const index = match.index;
|
||||
|
||||
if (index > lastIndex) {
|
||||
parts.push(child.substring(lastIndex, index));
|
||||
}
|
||||
if (index > lastIndex) {
|
||||
parts.push(child.substring(lastIndex, index));
|
||||
}
|
||||
|
||||
const cid = matchedText.replace('c/', '');
|
||||
const linkRef = React.createRef();
|
||||
const cid = matchedText.replace('c/', '');
|
||||
const linkRef = React.createRef();
|
||||
|
||||
let linkTo = () => {};
|
||||
const linkTarget = matchedText.startsWith('u/') ? '_blank' : '_self';
|
||||
let linkTo = () => {};
|
||||
const linkTarget = matchedText.startsWith('u/') ? '_blank' : '_self';
|
||||
|
||||
if (matchedText.startsWith('u/')) {
|
||||
linkTo = '#void';
|
||||
} else if (matchedText.startsWith('p/') || matchedText.startsWith('p/')) {
|
||||
linkTo = `/${matchedText}`;
|
||||
}
|
||||
if (matchedText.startsWith('u/')) {
|
||||
linkTo = '#void';
|
||||
} else if (matchedText.startsWith('p/') || matchedText.startsWith('p/')) {
|
||||
linkTo = `/${matchedText}`;
|
||||
}
|
||||
|
||||
parts.push(
|
||||
<ForwardRefLink
|
||||
key={`link-${i}-${matchedText}`}
|
||||
className='quotelink'
|
||||
to={linkTo}
|
||||
target={linkTarget}
|
||||
ref={linkRef}
|
||||
setRefAndCid={(ref) => {
|
||||
if (typeof postQuoteRef === 'function') {
|
||||
postQuoteRef(cid, ref);
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
postQuoteOnClick(cid);
|
||||
}}
|
||||
onMouseOver={() => {
|
||||
postQuoteOnOver(cid);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
postQuoteOnLeave();
|
||||
}}
|
||||
>
|
||||
{matchedText}
|
||||
</ForwardRefLink>,
|
||||
);
|
||||
parts.push(
|
||||
<ForwardRefLink
|
||||
key={`link-${i}-${matchedText}`}
|
||||
className='quotelink'
|
||||
to={linkTo}
|
||||
target={linkTarget}
|
||||
ref={linkRef}
|
||||
setRefAndCid={(ref) => {
|
||||
if (typeof postQuoteRef === 'function') {
|
||||
postQuoteRef(cid, ref);
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
postQuoteOnClick(cid);
|
||||
}}
|
||||
onMouseOver={() => {
|
||||
postQuoteOnOver(cid);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
postQuoteOnLeave();
|
||||
}}
|
||||
>
|
||||
{matchedText}
|
||||
</ForwardRefLink>,
|
||||
);
|
||||
|
||||
lastIndex = index + matchedText.length;
|
||||
}
|
||||
lastIndex = index + matchedText.length;
|
||||
}
|
||||
|
||||
if (lastIndex < child.length) {
|
||||
parts.push(child.substring(lastIndex));
|
||||
}
|
||||
if (lastIndex < child.length) {
|
||||
parts.push(child.substring(lastIndex));
|
||||
}
|
||||
|
||||
return parts;
|
||||
});
|
||||
};
|
||||
return parts;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
children={doubleNewlineContent}
|
||||
remarkPlugins={[blockquoteToGreentext, breaks]}
|
||||
rehypePlugins={[[rehypeSanitize, customSchema]]}
|
||||
components={{
|
||||
img: ({ src }) => <span>{src}</span>,
|
||||
video: ({ src }) => <span>{src}</span>,
|
||||
source: ({ src }) => <span>{src}</span>,
|
||||
gif: ({ src }) => <span>{src}</span>,
|
||||
p: ({ children }) => <div className='custom-paragraph'>{createQuotelink(children, postQuoteOnClick, postQuoteOnOver, postQuoteOnLeave, postQuoteRef)}</div>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<ReactMarkdown
|
||||
children={doubleNewlineContent}
|
||||
remarkPlugins={[blockquoteToGreentext, breaks]}
|
||||
rehypePlugins={[[rehypeSanitize, customSchema]]}
|
||||
components={{
|
||||
img: ({ src }) => <span>{src}</span>,
|
||||
video: ({ src }) => <span>{src}</span>,
|
||||
source: ({ src }) => <span>{src}</span>,
|
||||
gif: ({ src }) => <span>{src}</span>,
|
||||
p: ({ children }) => <div className='custom-paragraph'>{createQuotelink(children, postQuoteOnClick, postQuoteOnOver, postQuoteOnLeave, postQuoteRef)}</div>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Post);
|
||||
|
||||
@@ -3,37 +3,37 @@ import ContentLoader from 'react-content-loader';
|
||||
import useGeneralStore from '../hooks/stores/useGeneralStore';
|
||||
|
||||
const SinglePostLoader = () => {
|
||||
const selectedStyle = useGeneralStore((state) => state.selectedStyle);
|
||||
const backgroundColor = selectedStyle === 'Tomorrow' ? '#333' : '#f3f3f3';
|
||||
const foregroundColor = selectedStyle === 'Tomorrow' ? '#555' : '#ecebeb';
|
||||
const selectedStyle = useGeneralStore((state) => state.selectedStyle);
|
||||
const backgroundColor = selectedStyle === 'Tomorrow' ? '#333' : '#f3f3f3';
|
||||
const foregroundColor = selectedStyle === 'Tomorrow' ? '#555' : '#ecebeb';
|
||||
|
||||
return (
|
||||
<div style={{ paddingLeft: '30px', paddingRight: '30px', marginBottom: '50px', marginTop: '30px' }}>
|
||||
<ContentLoader width='100%' height={15 * 3 + 30} backgroundColor={backgroundColor} foregroundColor={foregroundColor}>
|
||||
<rect x='0' y='8' width='100%' height='15' />
|
||||
<rect x='0' y='30' width='100%' height='15' />
|
||||
<rect x='0' y='52' width='100%' height='15' />
|
||||
</ContentLoader>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div style={{ paddingLeft: '30px', paddingRight: '30px', marginBottom: '50px', marginTop: '30px' }}>
|
||||
<ContentLoader width='100%' height={15 * 3 + 30} backgroundColor={backgroundColor} foregroundColor={foregroundColor}>
|
||||
<rect x='0' y='8' width='100%' height='15' />
|
||||
<rect x='0' y='30' width='100%' height='15' />
|
||||
<rect x='0' y='52' width='100%' height='15' />
|
||||
</ContentLoader>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PostLoader = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'block',
|
||||
boxSizing: 'border-box',
|
||||
paddingLeft: '30px',
|
||||
paddingRight: '30px',
|
||||
marginBottom: '30px',
|
||||
}}
|
||||
>
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<SinglePostLoader key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'block',
|
||||
boxSizing: 'border-box',
|
||||
paddingLeft: '30px',
|
||||
paddingRight: '30px',
|
||||
marginBottom: '30px',
|
||||
}}
|
||||
>
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<SinglePostLoader key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostLoader;
|
||||
|
||||
+286
-286
@@ -11,293 +11,293 @@ import { BoardForm, Container } from './styled/views/Board.styled';
|
||||
import useGeneralStore from '../hooks/stores/useGeneralStore';
|
||||
|
||||
const PostOnHover = ({ cid, feed }) => {
|
||||
const selectedStyle = useGeneralStore((state) => state.selectedStyle);
|
||||
const account = useAccount();
|
||||
const reply = useComment({ commentCid: cid });
|
||||
const replyMediaInfo = getCommentMediaInfo(reply);
|
||||
const fallbackImgUrl = 'assets/filedeleted-res.gif';
|
||||
const selectedFeed = feed;
|
||||
const thread = useComment({ commentCid: reply.parentCid });
|
||||
const shortParentCid = findShortParentCid(reply.parentCid, selectedFeed);
|
||||
const stateString = useStateString(reply);
|
||||
const selectedStyle = useGeneralStore((state) => state.selectedStyle);
|
||||
const account = useAccount();
|
||||
const reply = useComment({ commentCid: cid });
|
||||
const replyMediaInfo = getCommentMediaInfo(reply);
|
||||
const fallbackImgUrl = 'assets/filedeleted-res.gif';
|
||||
const selectedFeed = feed;
|
||||
const thread = useComment({ commentCid: reply.parentCid });
|
||||
const shortParentCid = findShortParentCid(reply.parentCid, selectedFeed);
|
||||
const stateString = useStateString(reply);
|
||||
|
||||
return (
|
||||
<Container
|
||||
selectedStyle={selectedStyle}
|
||||
style={{
|
||||
margin: '0',
|
||||
padding: '0',
|
||||
whiteSpace: 'normal',
|
||||
maxWidth: '100vw',
|
||||
overflowWrap: 'break-word',
|
||||
wordWrap: 'break-word',
|
||||
wordBreak: 'break-all',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
<BoardForm selectedStyle={selectedStyle} style={{ margin: '0', padding: '0' }}>
|
||||
<div className='board' style={{ margin: '0', padding: '0' }}>
|
||||
<div className='thread' style={{ margin: '0', padding: '0' }}>
|
||||
{reply.state === 'succeeded' ? (
|
||||
<div className='reply-container'>
|
||||
<div className='post-reply post-reply-desktop'>
|
||||
<div className='post-info'>
|
||||
<span className='nameblock'>
|
||||
{reply.author?.displayName ? (
|
||||
reply.author?.displayName.length > 20 ? (
|
||||
<Fragment>
|
||||
<span className='name' data-tooltip-id='tooltip' data-tooltip-content={reply.author?.displayName} data-tooltip-place='top'>
|
||||
{reply.author?.displayName.slice(0, 20) + ' (...)'}
|
||||
</span>
|
||||
</Fragment>
|
||||
) : (
|
||||
<span className='name'>{reply.author?.displayName}</span>
|
||||
)
|
||||
) : (
|
||||
<span className='name'>Anonymous</span>
|
||||
)}
|
||||
|
||||
<span className='poster-address address-desktop' id='reply-button' style={{ cursor: 'pointer' }}>
|
||||
(u/
|
||||
{reply.author?.shortAddress ? <span>{reply.author?.shortAddress}</span> : <span>{account?.author?.shortAddress}</span>})
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className='date-time' data-utc='data'>
|
||||
{getDate(reply.timestamp)}
|
||||
</span>
|
||||
|
||||
<span className='post-number post-number-desktop'>
|
||||
<span>c/</span>
|
||||
<Link to={() => {}} id='reply-button' title='Reply to this post'>
|
||||
{reply.shortCid}
|
||||
</Link>
|
||||
</span>
|
||||
|
||||
<div id='backlink-id' className='backlink'>
|
||||
{reply.replies?.pages?.topAll.comments
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
.map((reply, index) => (
|
||||
<div key={`div-${index}`} style={{ display: 'inline-block' }}>
|
||||
<Link key={`link-${index}`} to={() => {}} className='quote-link'>
|
||||
c/{reply.shortCid}
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{replyMediaInfo?.url ? (
|
||||
<div className='file' style={{ marginBottom: '5px' }}>
|
||||
<div className='reply-file-text'>
|
||||
Link:
|
||||
<a href={replyMediaInfo.url} target='_blank' rel='noopener noreferrer'>
|
||||
{replyMediaInfo?.url.length > 30 ? replyMediaInfo?.url.slice(0, 30) + '(...)' : replyMediaInfo?.url}
|
||||
</a>
|
||||
{replyMediaInfo?.type === 'iframe' ? null : ` (${replyMediaInfo?.type})`}
|
||||
</div>
|
||||
{replyMediaInfo?.type === 'iframe' && (
|
||||
<div className='img-container'>
|
||||
<span className='file-thumb-reply'>
|
||||
{replyMediaInfo.thumbnail && <img src={replyMediaInfo.thumbnail} alt='thumbnail' onError={(e) => (e.target.src = fallbackImgUrl)} />}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{replyMediaInfo?.type === 'webpage' ? (
|
||||
<div className='img-container'>
|
||||
<span className='file-thumb-reply'>
|
||||
{reply.thumbnailUrl ? (
|
||||
<img
|
||||
src={replyMediaInfo.thumbnail}
|
||||
alt={replyMediaInfo.type}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onError={(e) => (e.target.src = fallbackImgUrl)}
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{replyMediaInfo?.type === 'image' ? (
|
||||
<div className='img-container'>
|
||||
<span className='file-thumb-reply'>
|
||||
<img src={replyMediaInfo.url} alt={replyMediaInfo.type} style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{replyMediaInfo?.type === 'video' ? (
|
||||
<span className='file-thumb-reply'>
|
||||
<video controls src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
</span>
|
||||
) : null}
|
||||
{replyMediaInfo?.type === 'audio' ? (
|
||||
<span className='file-thumb-reply'>
|
||||
<audio controls src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{reply.content ? (
|
||||
reply.content?.length > 500 ? (
|
||||
<Fragment>
|
||||
<blockquote comment={reply} className='post-message'>
|
||||
{shortParentCid ? (
|
||||
<Link to={() => {}} className='quotelink'>
|
||||
{`c/${shortParentCid}`}
|
||||
{shortParentCid === thread.shortCid ? ' (OP)' : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<Post content={reply.content?.slice(0, 500)} />
|
||||
<span className='ttl'>
|
||||
{' '}
|
||||
(...)
|
||||
<br /> <EditLabel commentCid={reply.cid} className='ttl' />
|
||||
<br />
|
||||
Comment too long. Click the link to view.
|
||||
</span>
|
||||
</blockquote>
|
||||
</Fragment>
|
||||
) : (
|
||||
<blockquote className='post-message'>
|
||||
{shortParentCid ? (
|
||||
<Link to={() => {}} className='quotelink'>
|
||||
{`c/${shortParentCid}`}
|
||||
{shortParentCid === thread.shortCid ? ' (OP)' : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<Post content={reply.content} comment={reply} />
|
||||
<EditLabel commentCid={reply.cid} className='ttl' />
|
||||
</blockquote>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='reply-container'>
|
||||
<span className='ellipsis'>{stateString}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='thread-mobile'>
|
||||
{reply.state === 'succeeded' ? (
|
||||
<div className='reply-container'>
|
||||
<div className='post-reply post-reply-mobile'>
|
||||
<div className='post-info-mobile'>
|
||||
<span className='name-block-mobile'>
|
||||
{reply.author?.displayName ? (
|
||||
reply.author?.displayName.length > 20 ? (
|
||||
<Fragment>
|
||||
<span className='name-mobile'>{reply.author?.displayName.slice(0, 20) + ' (...)'}</span>
|
||||
</Fragment>
|
||||
) : (
|
||||
<span className='name-mobile'>{reply.author?.displayName}</span>
|
||||
)
|
||||
) : (
|
||||
<span className='name-mobile'>Anonymous</span>
|
||||
)}
|
||||
|
||||
<span className='poster-address-mobile address-mobile' id='reply-button' style={{ cursor: 'pointer' }}>
|
||||
(u/
|
||||
{reply.author?.shortAddress ? (
|
||||
<span className='highlight-address-mobile'>{reply.author?.shortAddress}</span>
|
||||
) : (
|
||||
<span>{account?.author?.shortAddress}</span>
|
||||
)}
|
||||
)
|
||||
</span>
|
||||
<br />
|
||||
</span>
|
||||
<span className='date-time-mobile post-number-mobile'>
|
||||
{getDate(reply.timestamp)}
|
||||
<span>c/</span>
|
||||
<Link to={() => {}} id='reply-button'>
|
||||
{reply.shortCid}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
{reply.link ? (
|
||||
<div className='file-mobile'>
|
||||
{replyMediaInfo?.url ? (
|
||||
replyMediaInfo.type === 'webpage' ? (
|
||||
<div className='img-container'>
|
||||
<span className='file-thumb-mobile'>
|
||||
{reply.thumbnailUrl ? (
|
||||
<img src={replyMediaInfo.thumbnail} alt='thumbnail' style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
) : null}
|
||||
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
|
||||
</span>
|
||||
</div>
|
||||
) : replyMediaInfo.type === 'image' ? (
|
||||
<div className='img-container'>
|
||||
<span className='file-thumb-mobile'>
|
||||
<img src={replyMediaInfo.url} alt={replyMediaInfo.type} style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
|
||||
</span>
|
||||
</div>
|
||||
) : replyMediaInfo.type === 'video' ? (
|
||||
<span className='file-thumb-mobile'>
|
||||
<video
|
||||
src={replyMediaInfo.url}
|
||||
alt={replyMediaInfo.type}
|
||||
style={{ pointerEvents: 'none' }}
|
||||
onError={(e) => (e.target.src = fallbackImgUrl)}
|
||||
/>
|
||||
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
|
||||
</span>
|
||||
) : replyMediaInfo.type === 'audio' ? (
|
||||
<span className='file-thumb-mobile'>
|
||||
<audio src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
|
||||
</span>
|
||||
) : null
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{reply.content ? (
|
||||
reply.content?.length > 500 ? (
|
||||
<Fragment>
|
||||
<blockquote className='post-message'>
|
||||
{shortParentCid ? (
|
||||
<Link to={() => {}} className='quotelink'>
|
||||
{`c/${shortParentCid}`}
|
||||
{shortParentCid === thread.shortCid ? ' (OP)' : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<Post content={reply.content?.slice(0, 500)} comment={reply} />
|
||||
<span className='ttl'>
|
||||
{' '}
|
||||
(...)
|
||||
<br />
|
||||
<EditLabel commentCid={reply.cid} className='ttl' />
|
||||
<br />
|
||||
Comment too long. Click the link to view.{' '}
|
||||
</span>
|
||||
</blockquote>
|
||||
</Fragment>
|
||||
) : (
|
||||
<blockquote className='post-message'>
|
||||
{shortParentCid ? (
|
||||
<Link to={() => {}} className='quotelink'>
|
||||
{`c/${shortParentCid}`}
|
||||
{shortParentCid === thread.shortCid ? ' (OP)' : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<Post content={reply.content} comment={reply} />
|
||||
<EditLabel commentCid={reply.cid} className='ttl' />
|
||||
</blockquote>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='reply-container'>
|
||||
<span className='ellipsis'>{stateString}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BoardForm>
|
||||
</Container>
|
||||
);
|
||||
return (
|
||||
<Container
|
||||
selectedStyle={selectedStyle}
|
||||
style={{
|
||||
margin: '0',
|
||||
padding: '0',
|
||||
whiteSpace: 'normal',
|
||||
maxWidth: '100vw',
|
||||
overflowWrap: 'break-word',
|
||||
wordWrap: 'break-word',
|
||||
wordBreak: 'break-all',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
<BoardForm selectedStyle={selectedStyle} style={{ margin: '0', padding: '0' }}>
|
||||
<div className='board' style={{ margin: '0', padding: '0' }}>
|
||||
<div className='thread' style={{ margin: '0', padding: '0' }}>
|
||||
{reply.state === 'succeeded' ? (
|
||||
<div className='reply-container'>
|
||||
<div className='post-reply post-reply-desktop'>
|
||||
<div className='post-info'>
|
||||
<span className='nameblock'>
|
||||
{reply.author?.displayName ? (
|
||||
reply.author?.displayName.length > 20 ? (
|
||||
<Fragment>
|
||||
<span className='name' data-tooltip-id='tooltip' data-tooltip-content={reply.author?.displayName} data-tooltip-place='top'>
|
||||
{reply.author?.displayName.slice(0, 20) + ' (...)'}
|
||||
</span>
|
||||
</Fragment>
|
||||
) : (
|
||||
<span className='name'>{reply.author?.displayName}</span>
|
||||
)
|
||||
) : (
|
||||
<span className='name'>Anonymous</span>
|
||||
)}
|
||||
|
||||
<span className='poster-address address-desktop' id='reply-button' style={{ cursor: 'pointer' }}>
|
||||
(u/
|
||||
{reply.author?.shortAddress ? <span>{reply.author?.shortAddress}</span> : <span>{account?.author?.shortAddress}</span>})
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className='date-time' data-utc='data'>
|
||||
{getDate(reply.timestamp)}
|
||||
</span>
|
||||
|
||||
<span className='post-number post-number-desktop'>
|
||||
<span>c/</span>
|
||||
<Link to={() => {}} id='reply-button' title='Reply to this post'>
|
||||
{reply.shortCid}
|
||||
</Link>
|
||||
</span>
|
||||
|
||||
<div id='backlink-id' className='backlink'>
|
||||
{reply.replies?.pages?.topAll.comments
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
.map((reply, index) => (
|
||||
<div key={`div-${index}`} style={{ display: 'inline-block' }}>
|
||||
<Link key={`link-${index}`} to={() => {}} className='quote-link'>
|
||||
c/{reply.shortCid}
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{replyMediaInfo?.url ? (
|
||||
<div className='file' style={{ marginBottom: '5px' }}>
|
||||
<div className='reply-file-text'>
|
||||
Link:
|
||||
<a href={replyMediaInfo.url} target='_blank' rel='noopener noreferrer'>
|
||||
{replyMediaInfo?.url.length > 30 ? replyMediaInfo?.url.slice(0, 30) + '(...)' : replyMediaInfo?.url}
|
||||
</a>
|
||||
{replyMediaInfo?.type === 'iframe' ? null : ` (${replyMediaInfo?.type})`}
|
||||
</div>
|
||||
{replyMediaInfo?.type === 'iframe' && (
|
||||
<div className='img-container'>
|
||||
<span className='file-thumb-reply'>
|
||||
{replyMediaInfo.thumbnail && <img src={replyMediaInfo.thumbnail} alt='thumbnail' onError={(e) => (e.target.src = fallbackImgUrl)} />}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{replyMediaInfo?.type === 'webpage' ? (
|
||||
<div className='img-container'>
|
||||
<span className='file-thumb-reply'>
|
||||
{reply.thumbnailUrl ? (
|
||||
<img
|
||||
src={replyMediaInfo.thumbnail}
|
||||
alt={replyMediaInfo.type}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onError={(e) => (e.target.src = fallbackImgUrl)}
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{replyMediaInfo?.type === 'image' ? (
|
||||
<div className='img-container'>
|
||||
<span className='file-thumb-reply'>
|
||||
<img src={replyMediaInfo.url} alt={replyMediaInfo.type} style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{replyMediaInfo?.type === 'video' ? (
|
||||
<span className='file-thumb-reply'>
|
||||
<video controls src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
</span>
|
||||
) : null}
|
||||
{replyMediaInfo?.type === 'audio' ? (
|
||||
<span className='file-thumb-reply'>
|
||||
<audio controls src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{reply.content ? (
|
||||
reply.content?.length > 500 ? (
|
||||
<Fragment>
|
||||
<blockquote comment={reply} className='post-message'>
|
||||
{shortParentCid ? (
|
||||
<Link to={() => {}} className='quotelink'>
|
||||
{`c/${shortParentCid}`}
|
||||
{shortParentCid === thread.shortCid ? ' (OP)' : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<Post content={reply.content?.slice(0, 500)} />
|
||||
<span className='ttl'>
|
||||
{' '}
|
||||
(...)
|
||||
<br /> <EditLabel commentCid={reply.cid} className='ttl' />
|
||||
<br />
|
||||
Comment too long. Click the link to view.
|
||||
</span>
|
||||
</blockquote>
|
||||
</Fragment>
|
||||
) : (
|
||||
<blockquote className='post-message'>
|
||||
{shortParentCid ? (
|
||||
<Link to={() => {}} className='quotelink'>
|
||||
{`c/${shortParentCid}`}
|
||||
{shortParentCid === thread.shortCid ? ' (OP)' : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<Post content={reply.content} comment={reply} />
|
||||
<EditLabel commentCid={reply.cid} className='ttl' />
|
||||
</blockquote>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='reply-container'>
|
||||
<span className='ellipsis'>{stateString}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='thread-mobile'>
|
||||
{reply.state === 'succeeded' ? (
|
||||
<div className='reply-container'>
|
||||
<div className='post-reply post-reply-mobile'>
|
||||
<div className='post-info-mobile'>
|
||||
<span className='name-block-mobile'>
|
||||
{reply.author?.displayName ? (
|
||||
reply.author?.displayName.length > 20 ? (
|
||||
<Fragment>
|
||||
<span className='name-mobile'>{reply.author?.displayName.slice(0, 20) + ' (...)'}</span>
|
||||
</Fragment>
|
||||
) : (
|
||||
<span className='name-mobile'>{reply.author?.displayName}</span>
|
||||
)
|
||||
) : (
|
||||
<span className='name-mobile'>Anonymous</span>
|
||||
)}
|
||||
|
||||
<span className='poster-address-mobile address-mobile' id='reply-button' style={{ cursor: 'pointer' }}>
|
||||
(u/
|
||||
{reply.author?.shortAddress ? (
|
||||
<span className='highlight-address-mobile'>{reply.author?.shortAddress}</span>
|
||||
) : (
|
||||
<span>{account?.author?.shortAddress}</span>
|
||||
)}
|
||||
)
|
||||
</span>
|
||||
<br />
|
||||
</span>
|
||||
<span className='date-time-mobile post-number-mobile'>
|
||||
{getDate(reply.timestamp)}
|
||||
<span>c/</span>
|
||||
<Link to={() => {}} id='reply-button'>
|
||||
{reply.shortCid}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
{reply.link ? (
|
||||
<div className='file-mobile'>
|
||||
{replyMediaInfo?.url ? (
|
||||
replyMediaInfo.type === 'webpage' ? (
|
||||
<div className='img-container'>
|
||||
<span className='file-thumb-mobile'>
|
||||
{reply.thumbnailUrl ? (
|
||||
<img src={replyMediaInfo.thumbnail} alt='thumbnail' style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
) : null}
|
||||
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
|
||||
</span>
|
||||
</div>
|
||||
) : replyMediaInfo.type === 'image' ? (
|
||||
<div className='img-container'>
|
||||
<span className='file-thumb-mobile'>
|
||||
<img src={replyMediaInfo.url} alt={replyMediaInfo.type} style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
|
||||
</span>
|
||||
</div>
|
||||
) : replyMediaInfo.type === 'video' ? (
|
||||
<span className='file-thumb-mobile'>
|
||||
<video
|
||||
src={replyMediaInfo.url}
|
||||
alt={replyMediaInfo.type}
|
||||
style={{ pointerEvents: 'none' }}
|
||||
onError={(e) => (e.target.src = fallbackImgUrl)}
|
||||
/>
|
||||
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
|
||||
</span>
|
||||
) : replyMediaInfo.type === 'audio' ? (
|
||||
<span className='file-thumb-mobile'>
|
||||
<audio src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
|
||||
</span>
|
||||
) : null
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{reply.content ? (
|
||||
reply.content?.length > 500 ? (
|
||||
<Fragment>
|
||||
<blockquote className='post-message'>
|
||||
{shortParentCid ? (
|
||||
<Link to={() => {}} className='quotelink'>
|
||||
{`c/${shortParentCid}`}
|
||||
{shortParentCid === thread.shortCid ? ' (OP)' : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<Post content={reply.content?.slice(0, 500)} comment={reply} />
|
||||
<span className='ttl'>
|
||||
{' '}
|
||||
(...)
|
||||
<br />
|
||||
<EditLabel commentCid={reply.cid} className='ttl' />
|
||||
<br />
|
||||
Comment too long. Click the link to view.{' '}
|
||||
</span>
|
||||
</blockquote>
|
||||
</Fragment>
|
||||
) : (
|
||||
<blockquote className='post-message'>
|
||||
{shortParentCid ? (
|
||||
<Link to={() => {}} className='quotelink'>
|
||||
{`c/${shortParentCid}`}
|
||||
{shortParentCid === thread.shortCid ? ' (OP)' : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<Post content={reply.content} comment={reply} />
|
||||
<EditLabel commentCid={reply.cid} className='ttl' />
|
||||
</blockquote>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='reply-container'>
|
||||
<span className='ellipsis'>{stateString}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BoardForm>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostOnHover;
|
||||
|
||||
@@ -3,26 +3,26 @@ 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 comment = useAccountComment({ commentIndex: commentIndex });
|
||||
const stateString = useStateString(comment);
|
||||
|
||||
if (comment.updatedAt !== undefined || comment.index === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (comment.updatedAt !== undefined || comment.index === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (comment.state === 'failed' || comment.state === 'succeeded') {
|
||||
return null;
|
||||
}
|
||||
if (comment.state === 'failed' || comment.state === 'succeeded') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!stateString) {
|
||||
return null;
|
||||
}
|
||||
if (!stateString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className='ttl'>
|
||||
<br />(<span className={className}>{stateString}</span>)
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<span className='ttl'>
|
||||
<br />(<span className={className}>{stateString}</span>)
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default StateLabel;
|
||||
|
||||
@@ -2,10 +2,10 @@ import React from 'react';
|
||||
import { useComment, useAuthorAddress } from '@plebbit/plebbit-react-hooks';
|
||||
|
||||
function VerifiedAuthor({ commentCid, children }) {
|
||||
const comment = useComment({ commentCid });
|
||||
const { authorAddress, shortAuthorAddress } = useAuthorAddress({ comment });
|
||||
const comment = useComment({ commentCid });
|
||||
const { authorAddress, shortAuthorAddress } = useAuthorAddress({ comment });
|
||||
|
||||
return children({ authorAddress, shortAuthorAddress });
|
||||
return children({ authorAddress, shortAuthorAddress });
|
||||
}
|
||||
|
||||
export default React.memo(VerifiedAuthor);
|
||||
|
||||
@@ -1,28 +1,38 @@
|
||||
import React, { useState, useEffect, useMemo, useRef } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import Modal from "react-modal";
|
||||
import { deleteAccount, deleteCaches, importAccount, setAccount, setActiveAccount, useAccount, useAccounts, useResolvedAuthorAddress } from "@plebbit/plebbit-react-hooks";
|
||||
import stringify from "json-stringify-pretty-compact"
|
||||
import { StyledModal } from "../styled/modals/SettingsModal.styled";
|
||||
import useError from "../../hooks/useError";
|
||||
import useSuccess from "../../hooks/useSuccess";
|
||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import Modal from 'react-modal';
|
||||
import {
|
||||
createAccount,
|
||||
deleteAccount,
|
||||
deleteCaches,
|
||||
importAccount,
|
||||
setAccount,
|
||||
setActiveAccount,
|
||||
useAccount,
|
||||
useAccounts,
|
||||
useResolvedAuthorAddress,
|
||||
} from '@plebbit/plebbit-react-hooks';
|
||||
import stringify from 'json-stringify-pretty-compact';
|
||||
import { StyledModal } from '../styled/modals/SettingsModal.styled';
|
||||
import useError from '../../hooks/useError';
|
||||
import useSuccess from '../../hooks/useSuccess';
|
||||
import useAnonModeStore from '../../hooks/stores/useAnonModeStore';
|
||||
import useGeneralStore from '../../hooks/stores/useGeneralStore';
|
||||
import packageJson from '../../../package.json';
|
||||
const {version} = packageJson;
|
||||
const { version } = packageJson;
|
||||
const commitRef = process?.env?.REACT_APP_COMMIT_REF ? ` ${process.env.REACT_APP_COMMIT_REF.slice(0, 7)}` : '';
|
||||
|
||||
|
||||
const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
const { selectedStyle } = useGeneralStore(state => state);
|
||||
const { selectedStyle } = useGeneralStore((state) => state);
|
||||
const { anonymousMode, setAnonymousMode } = useAnonModeStore();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
|
||||
const [expanded, setExpanded] = useState([]);
|
||||
const [copyStatus, setCopyStatus] = useState(false);
|
||||
const [ensName, setEnsName] = useState('');
|
||||
const [checkedENS, setCheckedENS] = useState(false);
|
||||
const [triggerSwitchAccount, setTriggerSwitchAccount] = useState(false);
|
||||
|
||||
const [, setNewErrorMessage] = useError();
|
||||
const [, setNewSuccessMessage] = useSuccess();
|
||||
@@ -38,7 +48,9 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
}, [account]);
|
||||
|
||||
const [editedAccountJson, setEditedAccountJson] = useState(accountJson);
|
||||
useEffect(() => { setEditedAccountJson(accountJson)}, [accountJson])
|
||||
useEffect(() => {
|
||||
setEditedAccountJson(accountJson);
|
||||
}, [accountJson]);
|
||||
|
||||
const gatewayRef = useRef();
|
||||
const ipfsRef = useRef();
|
||||
@@ -51,39 +63,22 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
const ensRef = useRef();
|
||||
|
||||
const isElectron = window.electron && window.electron.isElectron;
|
||||
const author = {...account?.author, address: ensName};
|
||||
const author = { ...account?.author, address: ensName };
|
||||
const { resolvedAddress, state } = useResolvedAuthorAddress({ author, cache: false });
|
||||
|
||||
const defaultGatewayUrls = isElectron ? undefined : [
|
||||
'https://ipfs.io',
|
||||
'https://ipfsgateway.xyz',
|
||||
'https://cloudflare-ipfs.com',
|
||||
'https://plebpubsub.live'
|
||||
];
|
||||
const defaultGatewayUrls = isElectron ? undefined : ['https://ipfs.io', 'https://ipfsgateway.xyz', 'https://cloudflare-ipfs.com', 'https://plebpubsub.live'];
|
||||
|
||||
const defaultPubsubHttpClientsOptions = isElectron ? undefined : [
|
||||
'https://pubsubprovider.xyz/api/v0',
|
||||
'https://plebpubsub.live/api/v0'
|
||||
];
|
||||
const defaultPubsubHttpClientsOptions = isElectron ? undefined : ['https://pubsubprovider.xyz/api/v0', 'https://plebpubsub.live/api/v0'];
|
||||
|
||||
const defaultEthereumRpcUrls = [
|
||||
'ethers.js',
|
||||
'https://ethrpc.xyz',
|
||||
'viem',
|
||||
];
|
||||
|
||||
const defaultPolygonRpcUrls = [
|
||||
'https://polygon-rpc.com',
|
||||
];
|
||||
const defaultEthereumRpcUrls = ['ethers.js', 'https://ethrpc.xyz', 'viem'];
|
||||
|
||||
const defaultPolygonRpcUrls = ['https://polygon-rpc.com'];
|
||||
|
||||
useEffect(() => {
|
||||
if (checkedENS && resolvedAddress && state === 'succeeded') {
|
||||
setCheckedENS(false);
|
||||
}
|
||||
}, [checkedENS, state, resolvedAddress]);
|
||||
|
||||
|
||||
|
||||
const handleENSChange = () => {
|
||||
const newEnsName = ensRef.current.value;
|
||||
@@ -106,8 +101,8 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
address: ensName,
|
||||
},
|
||||
});
|
||||
setNewSuccessMessage("ENS name saved successfully.");
|
||||
|
||||
setAnonymousMode(false);
|
||||
setNewSuccessMessage('ENS name saved successfully. Anon mode has been disabled.');
|
||||
} catch (error) {
|
||||
setNewErrorMessage(error.message);
|
||||
console.log(error);
|
||||
@@ -117,7 +112,6 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const isValidURL = (url) => {
|
||||
try {
|
||||
new URL(url);
|
||||
@@ -127,7 +121,6 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleCopyAddress = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(account?.author.address);
|
||||
@@ -135,8 +128,7 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let copyStatusTimeoutId;
|
||||
@@ -148,17 +140,16 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
checkedENSTimeoutId = setTimeout(() => setCheckedENS(false), 5000);
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(copyStatusTimeoutId);
|
||||
clearTimeout(checkedENSTimeoutId);
|
||||
clearTimeout(copyStatusTimeoutId);
|
||||
clearTimeout(checkedENSTimeoutId);
|
||||
};
|
||||
}, [copyStatus, checkedENS]);
|
||||
|
||||
|
||||
const handleSavePlebbitOptions = async () => {
|
||||
let gatewayUrls = gatewayRef.current.value.split('\n').filter((url) => url.trim());
|
||||
let ipfsClientsOptions = ipfsRef.current.value.split('\n').filter(url => url.trim());
|
||||
let ipfsClientsOptions = ipfsRef.current.value.split('\n').filter((url) => url.trim());
|
||||
let pubsubClientsOptions = pubsubRef.current.value.split('\n').filter((url) => url.trim());
|
||||
|
||||
|
||||
if (!isElectron) {
|
||||
if (!gatewayUrls.length) gatewayUrls = defaultGatewayUrls;
|
||||
if (!pubsubClientsOptions.length) pubsubClientsOptions = defaultPubsubHttpClientsOptions;
|
||||
@@ -172,9 +163,9 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
}
|
||||
|
||||
const invalidUrls = [
|
||||
...(Array.isArray(gatewayUrls) ? gatewayUrls : (Array.isArray(defaultGatewayUrls) ? defaultGatewayUrls : [])),
|
||||
...(Array.isArray(gatewayUrls) ? gatewayUrls : Array.isArray(defaultGatewayUrls) ? defaultGatewayUrls : []),
|
||||
...(Array.isArray(ipfsClientsOptions) ? ipfsClientsOptions : []),
|
||||
...(Array.isArray(pubsubClientsOptions) ? pubsubClientsOptions : (Array.isArray(defaultPubsubHttpClientsOptions) ? defaultPubsubHttpClientsOptions : [])),
|
||||
...(Array.isArray(pubsubClientsOptions) ? pubsubClientsOptions : Array.isArray(defaultPubsubHttpClientsOptions) ? defaultPubsubHttpClientsOptions : []),
|
||||
].filter((url) => !isValidURL(url));
|
||||
|
||||
if (invalidUrls.length > 0) {
|
||||
@@ -190,47 +181,44 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
|
||||
try {
|
||||
await setAccount({ ...account, plebbitOptions });
|
||||
localStorage.setItem("successToast", "Settings saved successfully.");
|
||||
localStorage.setItem('successToast', 'Settings saved successfully.');
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
setNewErrorMessage(error.message); console.log(error);
|
||||
setNewErrorMessage(error.message);
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleSaveChainProviders = async () => {
|
||||
let ethereumRpcUrls = ethereumRpcRef.current.value.split('\\n').filter(url => url.trim());
|
||||
let polygonRpcUrls = polygonRpcRef.current.value.split('\\n').filter(url => url.trim());
|
||||
|
||||
let ethereumRpcUrls = ethereumRpcRef.current.value.split('\\n').filter((url) => url.trim());
|
||||
let polygonRpcUrls = polygonRpcRef.current.value.split('\\n').filter((url) => url.trim());
|
||||
|
||||
if (!ethereumRpcUrls.length) {
|
||||
ethereumRpcUrls = defaultEthereumRpcUrls;
|
||||
}
|
||||
|
||||
|
||||
if (!polygonRpcUrls.length) {
|
||||
polygonRpcUrls = defaultPolygonRpcUrls;
|
||||
}
|
||||
|
||||
const invalidUrls = [
|
||||
...ethereumRpcUrls || defaultEthereumRpcUrls,
|
||||
...polygonRpcUrls || defaultPolygonRpcUrls,
|
||||
].filter((url) => !isValidURL(url));
|
||||
|
||||
|
||||
const invalidUrls = [...(ethereumRpcUrls || defaultEthereumRpcUrls), ...(polygonRpcUrls || defaultPolygonRpcUrls)].filter((url) => !isValidURL(url));
|
||||
|
||||
if (invalidUrls.length > 0) {
|
||||
setNewErrorMessage(`Invalid URL(s): ${invalidUrls.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const chainProviders = {
|
||||
'eth': {
|
||||
eth: {
|
||||
urls: ethereumRpcUrls,
|
||||
chainId: 1,
|
||||
},
|
||||
'matic': {
|
||||
matic: {
|
||||
urls: polygonRpcUrls,
|
||||
chainId: 137,
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await setAccount({
|
||||
...account,
|
||||
@@ -239,22 +227,22 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
chainProviders,
|
||||
},
|
||||
});
|
||||
localStorage.setItem("successToast", "Blockchain options saved successfully.");
|
||||
localStorage.setItem('successToast', 'Blockchain options saved successfully.');
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
setNewErrorMessage(error.message); console.log(error);
|
||||
setNewErrorMessage(error.message);
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleResetPlebbitOptions = async () => {
|
||||
setNewErrorMessage(null);
|
||||
setNewSuccessMessage(null);
|
||||
|
||||
gatewayRef.current.value = defaultGatewayUrls ? defaultGatewayUrls.join('\n') : "";
|
||||
ipfsRef.current.value = "";
|
||||
pubsubRef.current.value = defaultPubsubHttpClientsOptions ? defaultPubsubHttpClientsOptions.join('\n') : "";
|
||||
dataPathRef.current.value = "";
|
||||
gatewayRef.current.value = defaultGatewayUrls ? defaultGatewayUrls.join('\n') : '';
|
||||
ipfsRef.current.value = '';
|
||||
pubsubRef.current.value = defaultPubsubHttpClientsOptions ? defaultPubsubHttpClientsOptions.join('\n') : '';
|
||||
dataPathRef.current.value = '';
|
||||
|
||||
try {
|
||||
await setAccount({
|
||||
@@ -265,32 +253,32 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
pubsubHttpClientsOptions: defaultPubsubHttpClientsOptions,
|
||||
},
|
||||
});
|
||||
localStorage.setItem("successToast", "Settings reset successfully.");
|
||||
localStorage.setItem('successToast', 'Settings reset successfully.');
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
setNewErrorMessage(error.message); console.log(error);
|
||||
setNewErrorMessage(error.message);
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleResetChainProviders = async () => {
|
||||
setNewErrorMessage(null);
|
||||
setNewSuccessMessage(null);
|
||||
|
||||
|
||||
ethereumRpcRef.current.value = defaultEthereumRpcUrls.join('\n');
|
||||
polygonRpcRef.current.value = defaultPolygonRpcUrls.join('\n');
|
||||
|
||||
|
||||
const chainProviders = {
|
||||
'eth': {
|
||||
eth: {
|
||||
urls: defaultEthereumRpcUrls,
|
||||
chainId: 1,
|
||||
},
|
||||
'matic': {
|
||||
matic: {
|
||||
urls: defaultPolygonRpcUrls,
|
||||
chainId: 137,
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await setAccount({
|
||||
...account,
|
||||
@@ -299,25 +287,23 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
chainProviders,
|
||||
},
|
||||
});
|
||||
localStorage.setItem("successToast", "Blockchain options reset successfully.");
|
||||
localStorage.setItem('successToast', 'Blockchain options reset successfully.');
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
setNewErrorMessage(error.message); console.log(error);
|
||||
setNewErrorMessage(error.message);
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleCloseModal = () => {
|
||||
if (location.pathname.endsWith("/settings")) {
|
||||
if (location.pathname.endsWith('/settings')) {
|
||||
const newPath = location.pathname.slice(0, -9);
|
||||
closeModal();
|
||||
navigate(newPath, { replace: true });
|
||||
} else {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
const toggleExpanded = (index) => {
|
||||
setExpanded((prevExpanded) => {
|
||||
@@ -331,7 +317,6 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const expandAll = () => {
|
||||
if (expanded.length === 4) {
|
||||
setExpanded([]);
|
||||
@@ -339,29 +324,33 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
setExpanded([0, 1, 2, 3]);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem("cacheCleared") === "true") {
|
||||
setNewSuccessMessage("Cache cleared successfully.");
|
||||
localStorage.removeItem("cacheCleared");
|
||||
if (localStorage.getItem('cacheCleared') === 'true') {
|
||||
setNewSuccessMessage('Cache cleared successfully.');
|
||||
localStorage.removeItem('cacheCleared');
|
||||
}
|
||||
}, [setNewSuccessMessage]);
|
||||
|
||||
|
||||
const handleSaveAccount = async () => {
|
||||
const data = importRef.current.value;
|
||||
const oldData = accountJson;
|
||||
try {
|
||||
const parsedJson = JSON.parse(data);
|
||||
await setAccount(parsedJson.account);
|
||||
setNewSuccessMessage("Account data saved successfully.");
|
||||
|
||||
if (!oldData.account?.author?.address.endsWith('.eth') && parsedJson.account?.author?.address.endsWith('.eth') && anonymousMode === true) {
|
||||
setAnonymousMode(false);
|
||||
setNewSuccessMessage('Account data saved successfully. ENS address detected, Anon mode disabled.');
|
||||
} else {
|
||||
setNewSuccessMessage('Account data saved successfully.');
|
||||
}
|
||||
} catch (error) {
|
||||
setNewErrorMessage("Error saving account data: " + error.message);
|
||||
setNewErrorMessage('Error saving account data: ' + error.message);
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleImportAccount = async () => {
|
||||
const data = importRef.current.value;
|
||||
|
||||
@@ -369,27 +358,52 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
const parsedJson = JSON.parse(data);
|
||||
await importAccount(data);
|
||||
setActiveAccount(parsedJson.account?.name);
|
||||
setNewSuccessMessage("Account imported successfully.");
|
||||
|
||||
|
||||
if (parsedJson.account?.author?.address.endsWith('.eth') && anonymousMode === true) {
|
||||
setAnonymousMode(false);
|
||||
setNewSuccessMessage('Account imported successfully. ENS address detected, Anon mode disabled.');
|
||||
} else {
|
||||
setNewSuccessMessage('Account imported successfully.');
|
||||
}
|
||||
} catch (error) {
|
||||
setNewErrorMessage(error.message); console.log(error);
|
||||
setNewErrorMessage(error.message);
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleDeleteAccount = async () => {
|
||||
if (window.confirm("Are you sure you want to delete this account?")) {
|
||||
if (window.confirm('Are you sure you want to delete this account?')) {
|
||||
try {
|
||||
await deleteAccount(account?.name);
|
||||
localStorage.setItem("successToast", "Account deleted successfully.");
|
||||
localStorage.setItem('successToast', 'Account deleted successfully.');
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
setNewErrorMessage("Error deleting account: " + error.message);
|
||||
setNewErrorMessage('Error deleting account: ' + error.message);
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateAccount = async () => {
|
||||
try {
|
||||
await createAccount();
|
||||
setNewSuccessMessage('Account created successfully.');
|
||||
setTriggerSwitchAccount(true);
|
||||
} catch (error) {
|
||||
setNewErrorMessage('Error creating account: ' + error.message);
|
||||
console.error(error);
|
||||
}
|
||||
const lastAccount = accounts[accounts.length - 1];
|
||||
setActiveAccount(lastAccount.name);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerSwitchAccount) {
|
||||
const lastAccount = accounts[accounts.length - 1];
|
||||
setActiveAccount(lastAccount.name);
|
||||
setTriggerSwitchAccount(false);
|
||||
}
|
||||
}, [accounts, triggerSwitchAccount]);
|
||||
|
||||
const handleAccountChange = (e) => {
|
||||
setActiveAccount(e.target.value);
|
||||
@@ -399,181 +413,165 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
const name = nameRef.current.value;
|
||||
|
||||
try {
|
||||
await setAccount({
|
||||
...account,
|
||||
await setAccount({
|
||||
...account,
|
||||
name: name || account?.name,
|
||||
author: {
|
||||
...account?.author,
|
||||
displayName: name,
|
||||
}});
|
||||
setNewSuccessMessage("Account name saved successfully.");
|
||||
},
|
||||
});
|
||||
setNewSuccessMessage('Account name saved successfully.');
|
||||
} catch (error) {
|
||||
setNewErrorMessage(error.message); console.log(error);
|
||||
setNewErrorMessage(error.message);
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
isOpen={isOpen}
|
||||
onRequestClose={handleCloseModal}
|
||||
contentLabel="Settings"
|
||||
style={{ overlay: { backgroundColor: "rgba(0,0,0,.25)" }}}
|
||||
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}{commitRef} -
|
||||
<div className='panel'>
|
||||
<div className='panel-header'>
|
||||
<span id='version'>
|
||||
v{version}
|
||||
{commitRef} -
|
||||
{window.electron && window.electron.isElectron ? (
|
||||
<Link className="all-button" id="node-stats" to="http://localhost:5001/webui/" target="_blank" rel="noreferrer">
|
||||
<Link className='all-button' id='node-stats' to='http://localhost:5001/webui/' target='_blank' rel='noreferrer'>
|
||||
full node
|
||||
</Link>
|
||||
) : (<span>web</span>)
|
||||
}
|
||||
) : (
|
||||
<span>web</span>
|
||||
)}
|
||||
</span>
|
||||
Settings
|
||||
<Link to="" onClick={handleCloseModal}>
|
||||
<span className="icon" title="close" />
|
||||
<Link to='' onClick={handleCloseModal}>
|
||||
<span className='icon' title='close' />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="all-div">
|
||||
<div className='all-div'>
|
||||
[
|
||||
<button className="all-button"
|
||||
onClick={expandAll} style={{all: "unset", cursor: "pointer"}}>
|
||||
{expanded.length === 4 ? "Collapse All Settings" : "Expand All Settings"}
|
||||
<button className='all-button' onClick={expandAll} style={{ all: 'unset', cursor: 'pointer' }}>
|
||||
{expanded.length === 4 ? 'Collapse All Settings' : 'Expand All Settings'}
|
||||
</button>
|
||||
]
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
</li>
|
||||
<ul className="settings-cat" style={{ display: expanded.includes(1) ? 'block' : 'none' }}>
|
||||
<li className="anon-off">
|
||||
<label title={ !anonymousMode ?
|
||||
"Enable anon mode" :
|
||||
"Disable anon mode"
|
||||
}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={anonymousMode}
|
||||
onChange={() => {setAnonymousMode(!anonymousMode); window.location.reload();}}
|
||||
<ul className='settings-cat' style={{ display: expanded.includes(1) ? 'block' : 'none' }}>
|
||||
<li className='anon-off'>
|
||||
<label title={!anonymousMode ? 'Enable anon mode' : 'Disable anon mode'}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={anonymousMode}
|
||||
onChange={() => {
|
||||
setAnonymousMode(!anonymousMode);
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
Anon Mode
|
||||
</label>
|
||||
</li>
|
||||
<li className="settings-tip anon-tip">
|
||||
Use a newly generated u/address per thread to post.
|
||||
<li className='settings-tip anon-tip'>Use a newly generated u/address per thread to post.</li>
|
||||
<li className='settings-option disc'>Account Data</li>
|
||||
<li className='settings-tip'>
|
||||
Save manual changes, reset changes, import another account after pasting its whole data, delete the account, create a new account.
|
||||
</li>
|
||||
<li className="settings-option disc">
|
||||
Account Data
|
||||
</li>
|
||||
<li className="settings-tip">
|
||||
To save manual changes, click "Save". To undo changes, click "Reset". To delete the account and create a new one, click "Delete". To add another account, paste its data and click "Import".
|
||||
</li>
|
||||
<div className="settings-input">
|
||||
<textarea id="account-data-text"
|
||||
value={editedAccountJson || accountJson}
|
||||
ref={importRef}
|
||||
onChange={(e) => setEditedAccountJson(e.target.value)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false" />
|
||||
<div className="account-buttons">
|
||||
<button onClick={handleSaveAccount}>
|
||||
Save
|
||||
</button>
|
||||
<button onClick={() => setEditedAccountJson(accountJson)}>
|
||||
Reset
|
||||
</button>
|
||||
<button onClick={handleImportAccount}>
|
||||
Import
|
||||
</button>
|
||||
<button onClick={handleDeleteAccount}>
|
||||
Delete
|
||||
</button>
|
||||
<div className='settings-input'>
|
||||
<textarea
|
||||
id='account-data-text'
|
||||
value={editedAccountJson || accountJson}
|
||||
ref={importRef}
|
||||
onChange={(e) => setEditedAccountJson(e.target.value)}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
spellCheck='false'
|
||||
/>
|
||||
<div className='account-buttons'>
|
||||
<button onClick={handleSaveAccount}>Save</button>
|
||||
<button onClick={() => setEditedAccountJson(accountJson)}>Reset</button>
|
||||
<button onClick={handleImportAccount}>Import</button>
|
||||
<button onClick={handleDeleteAccount}>Delete</button>
|
||||
<button onClick={handleCreateAccount}>Create</button>
|
||||
</div>
|
||||
</div>
|
||||
<li className="settings-option disc">
|
||||
Account Address: u/{account?.author.shortAddress}
|
||||
</li>
|
||||
<li className="settings-tip">
|
||||
Select a different account to use in the dropdown below.
|
||||
</li>
|
||||
<li className='settings-option disc'>Account Address: u/{account?.author.shortAddress}</li>
|
||||
<li className='settings-tip'>Select a different account to use in the dropdown below.</li>
|
||||
<li>
|
||||
<div className="settings-input">
|
||||
<select className="settings-select"
|
||||
value={account?.name}
|
||||
onChange={handleAccountChange}
|
||||
>
|
||||
<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>
|
||||
{account?.author.address.endsWith(".eth") ? null : (
|
||||
<button style={{marginLeft: '35px'}} className="save-button" id="save-name" onClick={handleCopyAddress}>
|
||||
{copyStatus ? "Copied!" : "Copy Full Address"}
|
||||
{account?.author.address.endsWith('.eth') ? null : (
|
||||
<button style={{ marginLeft: '35px' }} className='save-button' id='save-name' onClick={handleCopyAddress}>
|
||||
{copyStatus ? 'Copied!' : 'Copy Full Address'}
|
||||
</button>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
<li className="settings-option disc">
|
||||
Crypto Address
|
||||
<li className='settings-option disc'>Crypto Address</li>
|
||||
<li className='settings-tip'>
|
||||
{account?.author.address.endsWith('.eth')
|
||||
? 'Your account address is already an ENS name.'
|
||||
: 'Change your account address to an ENS name you own: in your ENS name page on ens.domains, click on "Records", "Edit Records", "Add record", add "plebbit-author-address" as record name, add your full address as value (copy it with the button above) and save.'}
|
||||
</li>
|
||||
<li className="settings-tip">
|
||||
{account?.author.address.endsWith(".eth") ? "Your account address is already an ENS name." : (
|
||||
'Change your account address to an ENS name you own: in your ENS name page on ens.domains, click on "Records", "Edit Records", "Add record", add "plebbit-author-address" as record name, add your full address as value (copy it with the button above) and save.'
|
||||
)}
|
||||
</li>
|
||||
<div className="settings-input">
|
||||
<input
|
||||
className="settings-input"
|
||||
style={{marginLeft: '20px'}}
|
||||
placeholder="address.eth"
|
||||
<div className='settings-input'>
|
||||
<input
|
||||
className='settings-input'
|
||||
style={{ marginLeft: '20px' }}
|
||||
placeholder='address.eth'
|
||||
ref={ensRef}
|
||||
value={ensName}
|
||||
onChange={handleENSChange}
|
||||
disabled={checkedENS}
|
||||
/>
|
||||
<button className="save-button" id="save-name" onClick={handleENSSave}>
|
||||
/>
|
||||
<button className='save-button' id='save-name' onClick={handleENSSave}>
|
||||
Save
|
||||
</button>
|
||||
<button className="save-button check-button" id="save-name" onClick={handleENSCheck}>
|
||||
<button className='save-button check-button' id='save-name' onClick={handleENSCheck}>
|
||||
Check
|
||||
</button>
|
||||
</div>
|
||||
{checkedENS && ensName === account?.signer?.address && (
|
||||
<li className="settings-tip" style={{marginTop: '10px', color: 'green'}}>
|
||||
{ensName} has been acquired by you correctly.
|
||||
<li className='settings-tip' style={{ marginTop: '10px', color: 'green' }}>
|
||||
{ensName} has been acquired by you correctly.
|
||||
</li>
|
||||
)}
|
||||
{checkedENS && ensName !== account?.signer?.address && (
|
||||
<li className="settings-tip" style={{marginTop: '10px', color: 'red'}}>
|
||||
{ensName} has not been acquired by you yet.
|
||||
<li className='settings-tip' style={{ marginTop: '10px', color: 'red' }}>
|
||||
{ensName} has not been acquired by you yet.
|
||||
</li>
|
||||
)}
|
||||
<li className="settings-option disc">
|
||||
Account Name
|
||||
</li>
|
||||
<li className="settings-tip">
|
||||
<li className='settings-option disc'>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"
|
||||
<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}>
|
||||
<button className='save-button' id='save-name' onClick={handleDisplayName}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
@@ -581,110 +579,94 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
</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)}
|
||||
>IPFS 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>
|
||||
<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)}>
|
||||
IPFS 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', marginTop: '-10px' }}>
|
||||
<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={isElectron ? '' : account?.plebbitOptions?.ipfsGatewayUrls.join('\n')}
|
||||
ref={gatewayRef}
|
||||
/>
|
||||
<ul className='settings-cat' style={{ display: expanded.includes(2) ? 'block' : 'none', marginTop: '-10px' }}>
|
||||
<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={isElectron ? '' : 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") : ''}
|
||||
<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"
|
||||
<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={isElectron ? '' : account?.plebbitOptions?.pubsubHttpClientsOptions.join('\n')}
|
||||
ref={pubsubRef}
|
||||
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}
|
||||
/>
|
||||
<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>
|
||||
<ul>
|
||||
<li className="settings-cat-lbl">
|
||||
<span className={`${expanded.includes(3) ? 'minus' : 'plus'}`}
|
||||
onClick={() => toggleExpanded(3)}
|
||||
/>
|
||||
<span className="settings-pointer" style={{cursor: "pointer"}}
|
||||
onClick={() => toggleExpanded(3)}
|
||||
>Blockchain Options</span>
|
||||
<div className="plebbit-options-buttons"
|
||||
style={{ display: expanded.includes(3) ? 'block' : 'none' }}
|
||||
>
|
||||
<button className="save-button" onClick={handleSaveChainProviders}>Save</button>
|
||||
<button className="save-button" onClick={handleResetChainProviders}>Reset</button>
|
||||
<li className='settings-cat-lbl'>
|
||||
<span className={`${expanded.includes(3) ? 'minus' : 'plus'}`} onClick={() => toggleExpanded(3)} />
|
||||
<span className='settings-pointer' style={{ cursor: 'pointer' }} onClick={() => toggleExpanded(3)}>
|
||||
Blockchain Options
|
||||
</span>
|
||||
<div className='plebbit-options-buttons' style={{ display: expanded.includes(3) ? 'block' : 'none' }}>
|
||||
<button className='save-button' onClick={handleSaveChainProviders}>
|
||||
Save
|
||||
</button>
|
||||
<button className='save-button' onClick={handleResetChainProviders}>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<ul className="settings-cat" style={{ display: expanded.includes(3) ? 'block' : 'none', marginTop: '-10px'}}>
|
||||
<li className="settings-option disc">Ethereum RPC</li>
|
||||
<li className="settings-tip">Needed for .eth addresses.</li>
|
||||
<div className="settings-input">
|
||||
<textarea placeholder="Ethereum RPC URLs"
|
||||
defaultValue={account?.plebbitOptions?.chainProviders?.['eth']?.urls.join("\n")}
|
||||
ref={ethereumRpcRef}
|
||||
/>
|
||||
<ul className='settings-cat' style={{ display: expanded.includes(3) ? 'block' : 'none', marginTop: '-10px' }}>
|
||||
<li className='settings-option disc'>Ethereum RPC</li>
|
||||
<li className='settings-tip'>Needed for .eth addresses.</li>
|
||||
<div className='settings-input'>
|
||||
<textarea placeholder='Ethereum RPC URLs' defaultValue={account?.plebbitOptions?.chainProviders?.['eth']?.urls.join('\n')} ref={ethereumRpcRef} />
|
||||
</div>
|
||||
<li className="settings-option disc">Polygon RPC</li>
|
||||
<li className="settings-tip">Needed for XPLEB NFTs.</li>
|
||||
<div className="settings-input">
|
||||
<textarea placeholder="Polygon RPC URLs"
|
||||
defaultValue={account?.plebbitOptions?.chainProviders?.['matic']?.urls.join("\n")}
|
||||
ref={polygonRpcRef}
|
||||
/>
|
||||
<li className='settings-option disc'>Polygon RPC</li>
|
||||
<li className='settings-tip'>Needed for XPLEB NFTs.</li>
|
||||
<div className='settings-input'>
|
||||
<textarea placeholder='Polygon RPC URLs' defaultValue={account?.plebbitOptions?.chainProviders?.['matic']?.urls.join('\n')} ref={polygonRpcRef} />
|
||||
</div>
|
||||
</ul>
|
||||
</ul>
|
||||
<div>
|
||||
<button
|
||||
className="cache-button"
|
||||
className='cache-button'
|
||||
onClick={async () => {
|
||||
if (window.confirm("Are you sure you want to clear the cache?")) {
|
||||
if (window.confirm('Are you sure you want to clear the cache?')) {
|
||||
await deleteCaches();
|
||||
localStorage.setItem("cacheCleared", "true");
|
||||
localStorage.setItem('cacheCleared', 'true');
|
||||
window.location.reload();
|
||||
}
|
||||
}}
|
||||
@@ -695,8 +677,8 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
</div>
|
||||
</StyledModal>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Modal.setAppElement("#root");
|
||||
Modal.setAppElement('#root');
|
||||
|
||||
export default SettingsModal;
|
||||
export default SettingsModal;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import styled from "styled-components";
|
||||
import styled from 'styled-components';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
export const StyledModal = styled(Modal)`
|
||||
@@ -13,7 +13,7 @@ export const StyledModal = styled(Modal)`
|
||||
position: absolute;
|
||||
padding: 2px 5px 5px;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, .25);
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
#version {
|
||||
@@ -25,9 +25,9 @@ export const StyledModal = styled(Modal)`
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.panel {
|
||||
width: 320px !important;
|
||||
width: 350px !important;
|
||||
max-height: 60% !important;
|
||||
left: calc(50% + 15px) !important;
|
||||
left: calc(50% - 2px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,8 @@ export const StyledModal = styled(Modal)`
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.plus, .minus {
|
||||
.plus,
|
||||
.minus {
|
||||
vertical-align: text-bottom;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
@@ -82,7 +83,9 @@ export const StyledModal = styled(Modal)`
|
||||
}
|
||||
|
||||
.settings-cat {
|
||||
display: ${({ expanded }) => index => (expanded?.includes(index) ? "block" : "none")};
|
||||
display: ${({ expanded }) =>
|
||||
(index) =>
|
||||
expanded?.includes(index) ? 'block' : 'none'};
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
@@ -94,7 +97,7 @@ export const StyledModal = styled(Modal)`
|
||||
|
||||
#account-data-text {
|
||||
min-width: 70%;
|
||||
min-height: 105px;
|
||||
min-height: 131px;
|
||||
}
|
||||
|
||||
.account-buttons {
|
||||
@@ -204,7 +207,7 @@ export const StyledModal = styled(Modal)`
|
||||
}
|
||||
|
||||
.anon-off {
|
||||
margin: 10px 0 5px -2px
|
||||
margin: 10px 0 5px -2px;
|
||||
}
|
||||
|
||||
.anon-tip {
|
||||
@@ -233,7 +236,6 @@ export const StyledModal = styled(Modal)`
|
||||
top: 108px;
|
||||
}
|
||||
|
||||
|
||||
${({ selectedStyle }) => {
|
||||
switch (selectedStyle) {
|
||||
case 'Yotsuba':
|
||||
@@ -425,13 +427,13 @@ export const StyledModal = styled(Modal)`
|
||||
color: #f30 !important;
|
||||
}
|
||||
}`;
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}}
|
||||
|
||||
#node-stats {
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
`;
|
||||
|
||||
+3208
-3208
File diff suppressed because it is too large
Load Diff
+983
-982
File diff suppressed because it is too large
Load Diff
+4228
-4228
File diff suppressed because it is too large
Load Diff
+3212
-3212
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+3055
-3051
File diff suppressed because it is too large
Load Diff
+17
-17
@@ -3,27 +3,27 @@ import { useAccount } from '@plebbit/plebbit-react-hooks';
|
||||
import useAnonModeStore from './stores/useAnonModeStore';
|
||||
|
||||
const useAnonMode = (threadCid, execute) => {
|
||||
const account = useAccount();
|
||||
const { anonymousMode } = useAnonModeStore();
|
||||
const account = useAccount();
|
||||
const { anonymousMode } = useAnonModeStore();
|
||||
|
||||
useEffect(() => {
|
||||
const handleAnonMode = async () => {
|
||||
let storedSigners = JSON.parse(localStorage.getItem('storedSigners')) || {};
|
||||
useEffect(() => {
|
||||
const handleAnonMode = async () => {
|
||||
let storedSigners = JSON.parse(localStorage.getItem('storedSigners')) || {};
|
||||
|
||||
if (!anonymousMode) {
|
||||
if (execute && storedSigners[threadCid]) {
|
||||
const signerPrivateKey = storedSigners[threadCid];
|
||||
if (account) {
|
||||
await account.plebbit.createSigner({ type: 'ed25519', privateKey: signerPrivateKey });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (!anonymousMode) {
|
||||
if (execute && storedSigners[threadCid]) {
|
||||
const signerPrivateKey = storedSigners[threadCid];
|
||||
if (account) {
|
||||
await account.plebbit.createSigner({ type: 'ed25519', privateKey: signerPrivateKey });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleAnonMode();
|
||||
}, [threadCid, execute, account, anonymousMode]);
|
||||
handleAnonMode();
|
||||
}, [threadCid, execute, account, anonymousMode]);
|
||||
|
||||
return;
|
||||
return;
|
||||
};
|
||||
|
||||
export default useAnonMode;
|
||||
|
||||
+17
-17
@@ -3,26 +3,26 @@ import { useAccount } from '@plebbit/plebbit-react-hooks';
|
||||
import useAnonModeStore from './stores/useAnonModeStore';
|
||||
|
||||
const useAnonModeRef = (threadCidRef, execute) => {
|
||||
const account = useAccount();
|
||||
const { anonymousMode } = useAnonModeStore();
|
||||
const account = useAccount();
|
||||
const { anonymousMode } = useAnonModeStore();
|
||||
|
||||
useEffect(() => {
|
||||
const handleAnonMode = async () => {
|
||||
let storedSigners = JSON.parse(localStorage.getItem('storedSigners')) || {};
|
||||
useEffect(() => {
|
||||
const handleAnonMode = async () => {
|
||||
let storedSigners = JSON.parse(localStorage.getItem('storedSigners')) || {};
|
||||
|
||||
if (!anonymousMode) {
|
||||
if (execute && storedSigners[threadCidRef]) {
|
||||
const signerPrivateKey = storedSigners[threadCidRef];
|
||||
if (account) {
|
||||
await account.plebbit.createSigner({ type: 'ed25519', privateKey: signerPrivateKey });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (!anonymousMode) {
|
||||
if (execute && storedSigners[threadCidRef]) {
|
||||
const signerPrivateKey = storedSigners[threadCidRef];
|
||||
if (account) {
|
||||
await account.plebbit.createSigner({ type: 'ed25519', privateKey: signerPrivateKey });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleAnonMode();
|
||||
}, [threadCidRef, execute, account, anonymousMode]);
|
||||
handleAnonMode();
|
||||
}, [threadCidRef, execute, account, anonymousMode]);
|
||||
|
||||
return;
|
||||
return;
|
||||
};
|
||||
export default useAnonModeRef;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import useGeneralStore from './stores/useGeneralStore';
|
||||
|
||||
const useClickForm = () => {
|
||||
const { setShowPostForm, setShowPostFormLink } = useGeneralStore.getState();
|
||||
const { setShowPostForm, setShowPostFormLink } = useGeneralStore.getState();
|
||||
|
||||
const handleClickForm = () => {
|
||||
setShowPostForm(true);
|
||||
setShowPostFormLink(false);
|
||||
};
|
||||
const handleClickForm = () => {
|
||||
setShowPostForm(true);
|
||||
setShowPostFormLink(false);
|
||||
};
|
||||
|
||||
return handleClickForm;
|
||||
return handleClickForm;
|
||||
};
|
||||
|
||||
export default useClickForm;
|
||||
|
||||
+38
-38
@@ -2,51 +2,51 @@ import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const useError = () => {
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [renderCount, setRenderCount] = useState(0);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [renderCount, setRenderCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (errorMessage && errorMessage.length > 0) {
|
||||
const showErrorToast = () => {
|
||||
const toastId = toast.error(errorMessage.toString(), {
|
||||
position: 'top-right',
|
||||
autoClose: false,
|
||||
hideProgressBar: true,
|
||||
closeOnClick: false,
|
||||
pauseOnHover: false,
|
||||
draggable: false,
|
||||
progress: undefined,
|
||||
theme: 'dark',
|
||||
});
|
||||
useEffect(() => {
|
||||
if (errorMessage && errorMessage.length > 0) {
|
||||
const showErrorToast = () => {
|
||||
const toastId = toast.error(errorMessage.toString(), {
|
||||
position: 'top-right',
|
||||
autoClose: false,
|
||||
hideProgressBar: true,
|
||||
closeOnClick: false,
|
||||
pauseOnHover: false,
|
||||
draggable: false,
|
||||
progress: undefined,
|
||||
theme: 'dark',
|
||||
});
|
||||
|
||||
return () => {
|
||||
toast.dismiss(toastId);
|
||||
};
|
||||
};
|
||||
return () => {
|
||||
toast.dismiss(toastId);
|
||||
};
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(showErrorToast, 500);
|
||||
const timeoutId = setTimeout(showErrorToast, 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}
|
||||
}, [errorMessage, renderCount]);
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}
|
||||
}, [errorMessage, renderCount]);
|
||||
|
||||
const setNewErrorMessage = (error) => {
|
||||
let message;
|
||||
if (typeof error === 'string') {
|
||||
message = error;
|
||||
} else if (error instanceof Error) {
|
||||
message = error.message;
|
||||
} else {
|
||||
message = JSON.stringify(error);
|
||||
}
|
||||
const setNewErrorMessage = (error) => {
|
||||
let message;
|
||||
if (typeof error === 'string') {
|
||||
message = error;
|
||||
} else if (error instanceof Error) {
|
||||
message = error.message;
|
||||
} else {
|
||||
message = JSON.stringify(error);
|
||||
}
|
||||
|
||||
setErrorMessage(message);
|
||||
setRenderCount((prevCount) => prevCount + 1);
|
||||
};
|
||||
setErrorMessage(message);
|
||||
setRenderCount((prevCount) => prevCount + 1);
|
||||
};
|
||||
|
||||
return [errorMessage, setNewErrorMessage];
|
||||
return [errorMessage, setNewErrorMessage];
|
||||
};
|
||||
|
||||
export default useError;
|
||||
|
||||
+13
-13
@@ -1,19 +1,19 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
|
||||
const useFeedRows = (feedWithDescriptionAndRules, columnCount) => {
|
||||
const rowsRef = useRef([]);
|
||||
return useMemo(() => {
|
||||
const rows = [];
|
||||
for (let i = 0; i < feedWithDescriptionAndRules.length; i += columnCount) {
|
||||
if (rowsRef.current?.[rows.length] && rowsRef.current[rows.length].length === columnCount) {
|
||||
rows.push(rowsRef.current[rows.length]);
|
||||
} else {
|
||||
rows.push(feedWithDescriptionAndRules.slice(i, i + columnCount));
|
||||
}
|
||||
}
|
||||
rowsRef.current = rows;
|
||||
return rows;
|
||||
}, [feedWithDescriptionAndRules, columnCount]);
|
||||
const rowsRef = useRef([]);
|
||||
return useMemo(() => {
|
||||
const rows = [];
|
||||
for (let i = 0; i < feedWithDescriptionAndRules.length; i += columnCount) {
|
||||
if (rowsRef.current?.[rows.length] && rowsRef.current[rows.length].length === columnCount) {
|
||||
rows.push(rowsRef.current[rows.length]);
|
||||
} else {
|
||||
rows.push(feedWithDescriptionAndRules.slice(i, i + columnCount));
|
||||
}
|
||||
}
|
||||
rowsRef.current = rows;
|
||||
return rows;
|
||||
}, [feedWithDescriptionAndRules, columnCount]);
|
||||
};
|
||||
|
||||
export default useFeedRows;
|
||||
|
||||
@@ -4,95 +4,95 @@ import { useSubplebbit, useSubplebbitsStates } from '@plebbit/plebbit-react-hook
|
||||
|
||||
const clientHosts = {};
|
||||
const getClientHost = (clientUrl) => {
|
||||
if (!clientHosts[clientUrl]) {
|
||||
try {
|
||||
clientHosts[clientUrl] = new URL(clientUrl).hostname || clientUrl;
|
||||
} catch (e) {
|
||||
clientHosts[clientUrl] = clientUrl;
|
||||
}
|
||||
}
|
||||
return clientHosts[clientUrl];
|
||||
if (!clientHosts[clientUrl]) {
|
||||
try {
|
||||
clientHosts[clientUrl] = new URL(clientUrl).hostname || clientUrl;
|
||||
} catch (e) {
|
||||
clientHosts[clientUrl] = clientUrl;
|
||||
}
|
||||
}
|
||||
return clientHosts[clientUrl];
|
||||
};
|
||||
|
||||
const useFeedStateString = (subplebbitAddresses) => {
|
||||
// single subplebbit feed state string
|
||||
const subplebbitAddress = subplebbitAddresses?.length === 1 ? subplebbitAddresses[0] : undefined;
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const singleSubplebbitFeedStateString = useStateString(subplebbit);
|
||||
// single subplebbit feed state string
|
||||
const subplebbitAddress = subplebbitAddresses?.length === 1 ? subplebbitAddresses[0] : undefined;
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const singleSubplebbitFeedStateString = useStateString(subplebbit);
|
||||
|
||||
// multiple subplebbit feed state string
|
||||
const { states } = useSubplebbitsStates({ subplebbitAddresses });
|
||||
// multiple subplebbit feed state string
|
||||
const { states } = useSubplebbitsStates({ subplebbitAddresses });
|
||||
|
||||
const multipleSubplebbitsFeedStateString = useMemo(() => {
|
||||
if (subplebbitAddress) {
|
||||
return;
|
||||
}
|
||||
const multipleSubplebbitsFeedStateString = useMemo(() => {
|
||||
if (subplebbitAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. Resolving 2 addresses from infura.io, fetching 2 IPNS, 1 IPFS from cloudflare-ipfs.com, ipfs.io
|
||||
let stateString = '';
|
||||
// e.g. Resolving 2 addresses from infura.io, fetching 2 IPNS, 1 IPFS from cloudflare-ipfs.com, ipfs.io
|
||||
let stateString = '';
|
||||
|
||||
if (states['resolving-address']) {
|
||||
const { subplebbitAddresses, clientUrls } = states['resolving-address'];
|
||||
if (subplebbitAddresses.length && clientUrls.length) {
|
||||
stateString += `resolving ${subplebbitAddresses.length} ${subplebbitAddresses.length === 1 ? 'address' : 'addresses'} from ${clientUrls
|
||||
.map(getClientHost)
|
||||
.join(', ')}`;
|
||||
}
|
||||
}
|
||||
if (states['resolving-address']) {
|
||||
const { subplebbitAddresses, clientUrls } = states['resolving-address'];
|
||||
if (subplebbitAddresses.length && clientUrls.length) {
|
||||
stateString += `resolving ${subplebbitAddresses.length} ${subplebbitAddresses.length === 1 ? 'address' : 'addresses'} from ${clientUrls
|
||||
.map(getClientHost)
|
||||
.join(', ')}`;
|
||||
}
|
||||
}
|
||||
|
||||
// find all page client and sub addresses
|
||||
const pagesStatesClientHosts = new Set();
|
||||
const pagesStatesSubplebbitAddresses = new Set();
|
||||
for (const state in states) {
|
||||
if (state.match('page')) {
|
||||
states[state].clientUrls.forEach((clientUrl) => pagesStatesClientHosts.add(getClientHost(clientUrl)));
|
||||
states[state].subplebbitAddresses.forEach((subplebbitAddress) => pagesStatesSubplebbitAddresses.add(subplebbitAddress));
|
||||
}
|
||||
}
|
||||
// find all page client and sub addresses
|
||||
const pagesStatesClientHosts = new Set();
|
||||
const pagesStatesSubplebbitAddresses = new Set();
|
||||
for (const state in states) {
|
||||
if (state.match('page')) {
|
||||
states[state].clientUrls.forEach((clientUrl) => pagesStatesClientHosts.add(getClientHost(clientUrl)));
|
||||
states[state].subplebbitAddresses.forEach((subplebbitAddress) => pagesStatesSubplebbitAddresses.add(subplebbitAddress));
|
||||
}
|
||||
}
|
||||
|
||||
if (states['fetching-ipns'] || states['fetching-ipfs'] || pagesStatesSubplebbitAddresses.size) {
|
||||
// separate 2 different states using ', '
|
||||
if (stateString) {
|
||||
stateString += ', ';
|
||||
}
|
||||
if (states['fetching-ipns'] || states['fetching-ipfs'] || pagesStatesSubplebbitAddresses.size) {
|
||||
// separate 2 different states using ', '
|
||||
if (stateString) {
|
||||
stateString += ', ';
|
||||
}
|
||||
|
||||
// find all client urls
|
||||
const clientHosts = new Set([...pagesStatesClientHosts]);
|
||||
states['fetching-ipns']?.clientUrls.forEach((clientUrl) => clientHosts.add(getClientHost(clientUrl)));
|
||||
states['fetching-ipfs']?.clientUrls.forEach((clientUrl) => clientHosts.add(getClientHost(clientUrl)));
|
||||
// find all client urls
|
||||
const clientHosts = new Set([...pagesStatesClientHosts]);
|
||||
states['fetching-ipns']?.clientUrls.forEach((clientUrl) => clientHosts.add(getClientHost(clientUrl)));
|
||||
states['fetching-ipfs']?.clientUrls.forEach((clientUrl) => clientHosts.add(getClientHost(clientUrl)));
|
||||
|
||||
if (clientHosts.size) {
|
||||
stateString += 'fetching ';
|
||||
if (states['fetching-ipns']) {
|
||||
stateString += `${states['fetching-ipns'].subplebbitAddresses.length} IPNS`;
|
||||
}
|
||||
if (states['fetching-ipfs']) {
|
||||
if (states['fetching-ipns']) {
|
||||
stateString += ', ';
|
||||
}
|
||||
stateString += `${states['fetching-ipfs'].subplebbitAddresses.length} IPFS`;
|
||||
}
|
||||
if (pagesStatesSubplebbitAddresses.size) {
|
||||
if (states['fetching-ipns'] || states['fetching-ipfs']) {
|
||||
stateString += ', ';
|
||||
}
|
||||
stateString += `${pagesStatesSubplebbitAddresses.size} ${pagesStatesSubplebbitAddresses.size === 1 ? 'page' : 'pages'}`;
|
||||
}
|
||||
stateString += ` from ${[...clientHosts].join(', ')}`;
|
||||
}
|
||||
}
|
||||
if (clientHosts.size) {
|
||||
stateString += 'fetching ';
|
||||
if (states['fetching-ipns']) {
|
||||
stateString += `${states['fetching-ipns'].subplebbitAddresses.length} IPNS`;
|
||||
}
|
||||
if (states['fetching-ipfs']) {
|
||||
if (states['fetching-ipns']) {
|
||||
stateString += ', ';
|
||||
}
|
||||
stateString += `${states['fetching-ipfs'].subplebbitAddresses.length} IPFS`;
|
||||
}
|
||||
if (pagesStatesSubplebbitAddresses.size) {
|
||||
if (states['fetching-ipns'] || states['fetching-ipfs']) {
|
||||
stateString += ', ';
|
||||
}
|
||||
stateString += `${pagesStatesSubplebbitAddresses.size} ${pagesStatesSubplebbitAddresses.size === 1 ? 'page' : 'pages'}`;
|
||||
}
|
||||
stateString += ` from ${[...clientHosts].join(', ')}`;
|
||||
}
|
||||
}
|
||||
|
||||
// capitalize first letter
|
||||
stateString = stateString.charAt(0).toUpperCase() + stateString.slice(1);
|
||||
// capitalize first letter
|
||||
stateString = stateString.charAt(0).toUpperCase() + stateString.slice(1);
|
||||
|
||||
// if string is empty, return undefined instead
|
||||
return stateString === '' ? undefined : stateString;
|
||||
}, [states, subplebbitAddress]);
|
||||
// if string is empty, return undefined instead
|
||||
return stateString === '' ? undefined : stateString;
|
||||
}, [states, subplebbitAddress]);
|
||||
|
||||
if (singleSubplebbitFeedStateString) {
|
||||
return singleSubplebbitFeedStateString;
|
||||
}
|
||||
return multipleSubplebbitsFeedStateString;
|
||||
if (singleSubplebbitFeedStateString) {
|
||||
return singleSubplebbitFeedStateString;
|
||||
}
|
||||
return multipleSubplebbitsFeedStateString;
|
||||
};
|
||||
|
||||
export default useFeedStateString;
|
||||
|
||||
+30
-30
@@ -2,42 +2,42 @@ import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const useInfo = () => {
|
||||
const [infoMessage, setInfoMessage] = useState('');
|
||||
const [renderCount, setRenderCount] = useState(0);
|
||||
const [infoMessage, setInfoMessage] = useState('');
|
||||
const [renderCount, setRenderCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (infoMessage && infoMessage.length > 0) {
|
||||
const showInfoToast = () => {
|
||||
const toastId = toast.info(infoMessage.toString(), {
|
||||
position: 'top-right',
|
||||
autoClose: false,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: false,
|
||||
pauseOnHover: false,
|
||||
draggable: false,
|
||||
progress: undefined,
|
||||
theme: 'dark',
|
||||
});
|
||||
useEffect(() => {
|
||||
if (infoMessage && infoMessage.length > 0) {
|
||||
const showInfoToast = () => {
|
||||
const toastId = toast.info(infoMessage.toString(), {
|
||||
position: 'top-right',
|
||||
autoClose: false,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: false,
|
||||
pauseOnHover: false,
|
||||
draggable: false,
|
||||
progress: undefined,
|
||||
theme: 'dark',
|
||||
});
|
||||
|
||||
return () => {
|
||||
toast.dismiss(toastId);
|
||||
};
|
||||
};
|
||||
return () => {
|
||||
toast.dismiss(toastId);
|
||||
};
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(showInfoToast, 500);
|
||||
const timeoutId = setTimeout(showInfoToast, 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}
|
||||
}, [infoMessage, renderCount]);
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}
|
||||
}, [infoMessage, renderCount]);
|
||||
|
||||
const setNewInfoMessage = (message) => {
|
||||
setInfoMessage(message);
|
||||
setRenderCount((prevCount) => prevCount + 1);
|
||||
};
|
||||
const setNewInfoMessage = (message) => {
|
||||
setInfoMessage(message);
|
||||
setRenderCount((prevCount) => prevCount + 1);
|
||||
};
|
||||
|
||||
return [infoMessage, setNewInfoMessage];
|
||||
return [infoMessage, setNewInfoMessage];
|
||||
};
|
||||
|
||||
export default useInfo;
|
||||
|
||||
@@ -3,14 +3,14 @@ import { useClientsStates } from '@plebbit/plebbit-react-hooks';
|
||||
|
||||
const clientHosts = {};
|
||||
const getClientHost = (clientUrl) => {
|
||||
if (!clientHosts[clientUrl]) {
|
||||
try {
|
||||
clientHosts[clientUrl] = new URL(clientUrl).hostname || clientUrl;
|
||||
} catch (e) {
|
||||
clientHosts[clientUrl] = clientUrl;
|
||||
}
|
||||
}
|
||||
return clientHosts[clientUrl];
|
||||
if (!clientHosts[clientUrl]) {
|
||||
try {
|
||||
clientHosts[clientUrl] = new URL(clientUrl).hostname || clientUrl;
|
||||
} catch (e) {
|
||||
clientHosts[clientUrl] = clientUrl;
|
||||
}
|
||||
}
|
||||
return clientHosts[clientUrl];
|
||||
};
|
||||
|
||||
const useStateString = (commentOrSubplebbit) => {
|
||||
|
||||
+30
-30
@@ -2,42 +2,42 @@ import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const useSuccess = () => {
|
||||
const [successMessage, setSuccessMessage] = useState('');
|
||||
const [renderCount, setRenderCount] = useState(0);
|
||||
const [successMessage, setSuccessMessage] = useState('');
|
||||
const [renderCount, setRenderCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (successMessage && successMessage.length > 0) {
|
||||
const showSuccessToast = () => {
|
||||
const toastId = toast.success(successMessage.toString(), {
|
||||
position: 'top-right',
|
||||
autoClose: 3000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: false,
|
||||
pauseOnHover: false,
|
||||
draggable: false,
|
||||
progress: undefined,
|
||||
theme: 'dark',
|
||||
});
|
||||
useEffect(() => {
|
||||
if (successMessage && successMessage.length > 0) {
|
||||
const showSuccessToast = () => {
|
||||
const toastId = toast.success(successMessage.toString(), {
|
||||
position: 'top-right',
|
||||
autoClose: 3000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: false,
|
||||
pauseOnHover: false,
|
||||
draggable: false,
|
||||
progress: undefined,
|
||||
theme: 'dark',
|
||||
});
|
||||
|
||||
return () => {
|
||||
toast.dismiss(toastId);
|
||||
};
|
||||
};
|
||||
return () => {
|
||||
toast.dismiss(toastId);
|
||||
};
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(showSuccessToast, 500);
|
||||
const timeoutId = setTimeout(showSuccessToast, 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}
|
||||
}, [successMessage, renderCount]);
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}
|
||||
}, [successMessage, renderCount]);
|
||||
|
||||
const setNewSuccessMessage = (message) => {
|
||||
setSuccessMessage(message);
|
||||
setRenderCount((prevCount) => prevCount + 1);
|
||||
};
|
||||
const setNewSuccessMessage = (message) => {
|
||||
setSuccessMessage(message);
|
||||
setRenderCount((prevCount) => prevCount + 1);
|
||||
};
|
||||
|
||||
return [successMessage, setNewSuccessMessage];
|
||||
return [successMessage, setNewSuccessMessage];
|
||||
};
|
||||
|
||||
export default useSuccess;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function useWindowWidth() {
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
setWindowWidth(window.innerWidth);
|
||||
}
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
setWindowWidth(window.innerWidth);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return windowWidth;
|
||||
return windowWidth;
|
||||
}
|
||||
|
||||
+12
-12
@@ -1,19 +1,19 @@
|
||||
function countLinks(comment) {
|
||||
let linkCount = 0;
|
||||
let linkCount = 0;
|
||||
|
||||
if (comment.replyCount > 0) {
|
||||
for (let reply of comment.replies.pages.topAll.comments) {
|
||||
if (reply.link) {
|
||||
linkCount++;
|
||||
}
|
||||
if (comment.replyCount > 0) {
|
||||
for (let reply of comment.replies.pages.topAll.comments) {
|
||||
if (reply.link) {
|
||||
linkCount++;
|
||||
}
|
||||
|
||||
if (reply.replyCount > 0) {
|
||||
linkCount += countLinks(reply);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reply.replyCount > 0) {
|
||||
linkCount += countLinks(reply);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return linkCount;
|
||||
return linkCount;
|
||||
}
|
||||
|
||||
export default countLinks;
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
function findShortParentCid(parentCid, input) {
|
||||
const feed = Array.isArray(input) ? input : [input];
|
||||
const feed = Array.isArray(input) ? input : [input];
|
||||
|
||||
for (const thread of feed) {
|
||||
if (thread.cid === parentCid) {
|
||||
return thread.shortCid;
|
||||
}
|
||||
if (thread.replyCount > 0 && thread.replies.pages.topAll.comments) {
|
||||
const shortCid = findShortParentCid(parentCid, thread.replies.pages.topAll.comments);
|
||||
if (shortCid) {
|
||||
return shortCid;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
for (const thread of feed) {
|
||||
if (thread.cid === parentCid) {
|
||||
return thread.shortCid;
|
||||
}
|
||||
if (thread.replyCount > 0 && thread.replies.pages.topAll.comments) {
|
||||
const shortCid = findShortParentCid(parentCid, thread.replies.pages.topAll.comments);
|
||||
if (shortCid) {
|
||||
return shortCid;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default findShortParentCid;
|
||||
|
||||
@@ -2,77 +2,77 @@ import extName from 'ext-name';
|
||||
import { canEmbed } from '../components/Embed';
|
||||
|
||||
const getCommentMediaInfo = (comment) => {
|
||||
if (!comment?.thumbnailUrl && !comment?.link) {
|
||||
return;
|
||||
}
|
||||
if (!comment?.thumbnailUrl && !comment?.link) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (comment?.link) {
|
||||
try {
|
||||
const url = new URL(comment.link);
|
||||
const host = url.hostname;
|
||||
let scrapedThumbnailUrl;
|
||||
if (comment?.link) {
|
||||
try {
|
||||
const url = new URL(comment.link);
|
||||
const host = url.hostname;
|
||||
let scrapedThumbnailUrl;
|
||||
|
||||
if (['youtube.com', 'www.youtube.com', 'youtu.be'].includes(host)) {
|
||||
const videoId = host === 'youtu.be' ? url.pathname.slice(1) : url.searchParams.get('v');
|
||||
scrapedThumbnailUrl = `https://img.youtube.com/vi/${videoId}/sddefault.jpg`;
|
||||
} else if (host.includes('bitchute.com')) {
|
||||
const videoId = url.pathname.split('/')[2];
|
||||
scrapedThumbnailUrl = `https://static-3.bitchute.com/live/cover_images/F61vWF4shy8s/${videoId}_640x360.jpg`;
|
||||
} else if (host.includes('streamable.com')) {
|
||||
const videoId = url.pathname.split('/')[1];
|
||||
scrapedThumbnailUrl = `https://cdn-cf-east.streamable.com/image/${videoId}.jpg`;
|
||||
}
|
||||
if (['youtube.com', 'www.youtube.com', 'youtu.be'].includes(host)) {
|
||||
const videoId = host === 'youtu.be' ? url.pathname.slice(1) : url.searchParams.get('v');
|
||||
scrapedThumbnailUrl = `https://img.youtube.com/vi/${videoId}/sddefault.jpg`;
|
||||
} else if (host.includes('bitchute.com')) {
|
||||
const videoId = url.pathname.split('/')[2];
|
||||
scrapedThumbnailUrl = `https://static-3.bitchute.com/live/cover_images/F61vWF4shy8s/${videoId}_640x360.jpg`;
|
||||
} else if (host.includes('streamable.com')) {
|
||||
const videoId = url.pathname.split('/')[1];
|
||||
scrapedThumbnailUrl = `https://cdn-cf-east.streamable.com/image/${videoId}.jpg`;
|
||||
}
|
||||
|
||||
if (canEmbed(url)) {
|
||||
return {
|
||||
url: comment.link,
|
||||
type: 'iframe',
|
||||
thumbnail: comment.thumbnailUrl || scrapedThumbnailUrl,
|
||||
};
|
||||
}
|
||||
if (canEmbed(url)) {
|
||||
return {
|
||||
url: comment.link,
|
||||
type: 'iframe',
|
||||
thumbnail: comment.thumbnailUrl || scrapedThumbnailUrl,
|
||||
};
|
||||
}
|
||||
|
||||
const mime = extName(url.pathname.toLowerCase().replace('/', ''))[0]?.mime;
|
||||
const mime = extName(url.pathname.toLowerCase().replace('/', ''))[0]?.mime;
|
||||
|
||||
if (mime?.startsWith('image')) {
|
||||
return {
|
||||
url: comment.link,
|
||||
type: 'image',
|
||||
};
|
||||
}
|
||||
if (mime?.startsWith('image')) {
|
||||
return {
|
||||
url: comment.link,
|
||||
type: 'image',
|
||||
};
|
||||
}
|
||||
|
||||
if (mime?.startsWith('video')) {
|
||||
return {
|
||||
url: comment.link,
|
||||
type: 'video',
|
||||
thumbnail: comment.thumbnailUrl,
|
||||
};
|
||||
}
|
||||
if (mime?.startsWith('video')) {
|
||||
return {
|
||||
url: comment.link,
|
||||
type: 'video',
|
||||
thumbnail: comment.thumbnailUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (mime?.startsWith('audio')) {
|
||||
return {
|
||||
url: comment.link,
|
||||
type: 'audio',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (mime?.startsWith('audio')) {
|
||||
return {
|
||||
url: comment.link,
|
||||
type: 'audio',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (comment?.thumbnailUrl && comment?.thumbnailUrl !== comment?.link) {
|
||||
return {
|
||||
url: comment.link,
|
||||
type: 'webpage',
|
||||
thumbnail: comment.thumbnailUrl,
|
||||
};
|
||||
}
|
||||
if (comment?.thumbnailUrl && comment?.thumbnailUrl !== comment?.link) {
|
||||
return {
|
||||
url: comment.link,
|
||||
type: 'webpage',
|
||||
thumbnail: comment.thumbnailUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (comment?.link) {
|
||||
return {
|
||||
url: comment.link,
|
||||
type: 'webpage',
|
||||
};
|
||||
}
|
||||
if (comment?.link) {
|
||||
return {
|
||||
url: comment.link,
|
||||
type: 'webpage',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default getCommentMediaInfo;
|
||||
|
||||
+31
-31
@@ -1,35 +1,35 @@
|
||||
const getDate = (commentTimestamp) => {
|
||||
if (commentTimestamp === undefined || isNaN(commentTimestamp)) {
|
||||
return '';
|
||||
}
|
||||
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
const string = new Intl.DateTimeFormat(locale, {
|
||||
hour12: false,
|
||||
year: '2-digit',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
weekday: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
}).format(new Date(commentTimestamp * 1000));
|
||||
if (locale.startsWith('ar')) {
|
||||
return string;
|
||||
}
|
||||
const items = string.split(/,* /);
|
||||
if (items.length === 3) {
|
||||
const itemIsNumber = [items[0][0].match(/[0-9]/), items[1][0].match(/[0-9]/)];
|
||||
if (itemIsNumber[0] && itemIsNumber[1]) {
|
||||
return `${items[0]}(${items[2]})${items[1]}`;
|
||||
}
|
||||
if (itemIsNumber[0] && !itemIsNumber[1]) {
|
||||
return `${items[0]}(${items[1]})${items[2]}`;
|
||||
}
|
||||
if (!itemIsNumber[0] && itemIsNumber[1]) {
|
||||
return `${items[1]}(${items[0]})${items[2]}`;
|
||||
}
|
||||
}
|
||||
return string;
|
||||
if (commentTimestamp === undefined || isNaN(commentTimestamp)) {
|
||||
return '';
|
||||
}
|
||||
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
const string = new Intl.DateTimeFormat(locale, {
|
||||
hour12: false,
|
||||
year: '2-digit',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
weekday: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
}).format(new Date(commentTimestamp * 1000));
|
||||
if (locale.startsWith('ar')) {
|
||||
return string;
|
||||
}
|
||||
const items = string.split(/,* /);
|
||||
if (items.length === 3) {
|
||||
const itemIsNumber = [items[0][0].match(/[0-9]/), items[1][0].match(/[0-9]/)];
|
||||
if (itemIsNumber[0] && itemIsNumber[1]) {
|
||||
return `${items[0]}(${items[2]})${items[1]}`;
|
||||
}
|
||||
if (itemIsNumber[0] && !itemIsNumber[1]) {
|
||||
return `${items[0]}(${items[1]})${items[2]}`;
|
||||
}
|
||||
if (!itemIsNumber[0] && itemIsNumber[1]) {
|
||||
return `${items[1]}(${items[0]})${items[2]}`;
|
||||
}
|
||||
}
|
||||
return string;
|
||||
};
|
||||
|
||||
export default getDate;
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
const pluralize = (unit, value) => {
|
||||
return `${value} ${unit}${value > 1 ? 's' : ''}`;
|
||||
return `${value} ${unit}${value > 1 ? 's' : ''}`;
|
||||
};
|
||||
|
||||
const getFormattedTime = (timestamp) => {
|
||||
try {
|
||||
const currentTime = new Date().getTime();
|
||||
const differenceInMilliseconds = currentTime - timestamp * 1000;
|
||||
try {
|
||||
const currentTime = new Date().getTime();
|
||||
const differenceInMilliseconds = currentTime - timestamp * 1000;
|
||||
|
||||
const years = Math.floor(differenceInMilliseconds / (1000 * 60 * 60 * 24 * 365.25));
|
||||
const months = Math.floor((differenceInMilliseconds % (1000 * 60 * 60 * 24 * 365.25)) / (1000 * 60 * 60 * 24 * 30));
|
||||
const days = Math.floor((differenceInMilliseconds % (1000 * 60 * 60 * 24 * 30)) / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((differenceInMilliseconds % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((differenceInMilliseconds % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((differenceInMilliseconds % (1000 * 60)) / 1000);
|
||||
const years = Math.floor(differenceInMilliseconds / (1000 * 60 * 60 * 24 * 365.25));
|
||||
const months = Math.floor((differenceInMilliseconds % (1000 * 60 * 60 * 24 * 365.25)) / (1000 * 60 * 60 * 24 * 30));
|
||||
const days = Math.floor((differenceInMilliseconds % (1000 * 60 * 60 * 24 * 30)) / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((differenceInMilliseconds % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((differenceInMilliseconds % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((differenceInMilliseconds % (1000 * 60)) / 1000);
|
||||
|
||||
if (years > 0) {
|
||||
return months > 0 ? `${pluralize('year', years)} and ${pluralize('month', months)} ago` : `${pluralize('year', years)} ago`;
|
||||
} else if (months > 0) {
|
||||
return days > 0 ? `${pluralize('month', months)} and ${pluralize('day', days)} ago` : `${pluralize('month', months)} ago`;
|
||||
} else if (days > 0) {
|
||||
if (hours > 0) {
|
||||
return `${pluralize('day', days)} and ${pluralize('hour', hours)} ago`;
|
||||
} else if (minutes > 0) {
|
||||
return `${pluralize('day', days)} and ${pluralize('minute', minutes)} ago`;
|
||||
} else {
|
||||
return `${pluralize('day', days)} ago`;
|
||||
}
|
||||
} else if (hours > 0) {
|
||||
return minutes > 0 ? `${pluralize('hour', hours)} and ${pluralize('minute', minutes)} ago` : `${pluralize('hour', hours)} ago`;
|
||||
} else if (minutes > 0) {
|
||||
return seconds > 0 ? `${pluralize('minute', minutes)} and ${pluralize('second', seconds)} ago` : `${pluralize('minute', minutes)} ago`;
|
||||
} else if (seconds > 30) {
|
||||
return `${pluralize('second', seconds)} ago`;
|
||||
} else {
|
||||
return 'just now';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error in getFormattedTime:', e);
|
||||
return '[error]';
|
||||
}
|
||||
if (years > 0) {
|
||||
return months > 0 ? `${pluralize('year', years)} and ${pluralize('month', months)} ago` : `${pluralize('year', years)} ago`;
|
||||
} else if (months > 0) {
|
||||
return days > 0 ? `${pluralize('month', months)} and ${pluralize('day', days)} ago` : `${pluralize('month', months)} ago`;
|
||||
} else if (days > 0) {
|
||||
if (hours > 0) {
|
||||
return `${pluralize('day', days)} and ${pluralize('hour', hours)} ago`;
|
||||
} else if (minutes > 0) {
|
||||
return `${pluralize('day', days)} and ${pluralize('minute', minutes)} ago`;
|
||||
} else {
|
||||
return `${pluralize('day', days)} ago`;
|
||||
}
|
||||
} else if (hours > 0) {
|
||||
return minutes > 0 ? `${pluralize('hour', hours)} and ${pluralize('minute', minutes)} ago` : `${pluralize('hour', hours)} ago`;
|
||||
} else if (minutes > 0) {
|
||||
return seconds > 0 ? `${pluralize('minute', minutes)} and ${pluralize('second', seconds)} ago` : `${pluralize('minute', minutes)} ago`;
|
||||
} else if (seconds > 30) {
|
||||
return `${pluralize('second', seconds)} ago`;
|
||||
} else {
|
||||
return 'just now';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error in getFormattedTime:', e);
|
||||
return '[error]';
|
||||
}
|
||||
};
|
||||
|
||||
export default getFormattedTime;
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
function handleAddressClick(shortAddress) {
|
||||
const isMobile = window.innerWidth <= 480;
|
||||
const addressSelector = isMobile ? '.address-mobile' : '.address-desktop';
|
||||
const postReplySelector = isMobile ? '.post-reply-mobile' : '.post-reply-desktop';
|
||||
const opSelector = isMobile ? '.op-mobile' : '.op-desktop';
|
||||
const isMobile = window.innerWidth <= 480;
|
||||
const addressSelector = isMobile ? '.address-mobile' : '.address-desktop';
|
||||
const postReplySelector = isMobile ? '.post-reply-mobile' : '.post-reply-desktop';
|
||||
const opSelector = isMobile ? '.op-mobile' : '.op-desktop';
|
||||
|
||||
const matchingElements = [...document.querySelectorAll(postReplySelector + ',' + opSelector)].filter((el) => {
|
||||
const addressElement = el.querySelector(addressSelector);
|
||||
return addressElement && addressElement.textContent.includes(shortAddress);
|
||||
});
|
||||
const matchingElements = [...document.querySelectorAll(postReplySelector + ',' + opSelector)].filter((el) => {
|
||||
const addressElement = el.querySelector(addressSelector);
|
||||
return addressElement && addressElement.textContent.includes(shortAddress);
|
||||
});
|
||||
|
||||
if (matchingElements.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (matchingElements.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const highlightedElements = document.querySelectorAll('.highlighted-address');
|
||||
const highlightedElements = document.querySelectorAll('.highlighted-address');
|
||||
|
||||
let allHighlighted = true;
|
||||
matchingElements.forEach((el) => {
|
||||
if (!el.classList.contains('highlighted-address')) {
|
||||
allHighlighted = false;
|
||||
}
|
||||
});
|
||||
let allHighlighted = true;
|
||||
matchingElements.forEach((el) => {
|
||||
if (!el.classList.contains('highlighted-address')) {
|
||||
allHighlighted = false;
|
||||
}
|
||||
});
|
||||
|
||||
highlightedElements.forEach((el) => {
|
||||
el.classList.remove('highlighted-address');
|
||||
});
|
||||
highlightedElements.forEach((el) => {
|
||||
el.classList.remove('highlighted-address');
|
||||
});
|
||||
|
||||
if (!allHighlighted) {
|
||||
matchingElements.forEach((el) => {
|
||||
if (!el.classList.contains('op-mobile') && !el.classList.contains('op-desktop')) {
|
||||
el.classList.add('highlighted-address');
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!allHighlighted) {
|
||||
matchingElements.forEach((el) => {
|
||||
if (!el.classList.contains('op-mobile') && !el.classList.contains('op-desktop')) {
|
||||
el.classList.add('highlighted-address');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default handleAddressClick;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const handleImageClick = (e) => {
|
||||
const image = e.target;
|
||||
const container = image.closest('.img-container');
|
||||
const image = e.target;
|
||||
const container = image.closest('.img-container');
|
||||
|
||||
image.classList.toggle('enlarged');
|
||||
image.classList.toggle('enlarged');
|
||||
|
||||
if (container) {
|
||||
container.classList.toggle('expanded-container');
|
||||
}
|
||||
if (container) {
|
||||
container.classList.toggle('expanded-container');
|
||||
}
|
||||
};
|
||||
|
||||
export default handleImageClick;
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
function handleQuoteClick(reply, parentCid, threadCid) {
|
||||
const cid = parentCid ? parentCid : reply.shortCid;
|
||||
const isMobile = window.innerWidth <= 480;
|
||||
const postNumberSelector = isMobile ? '.post-number-mobile' : '.post-number-desktop';
|
||||
const cid = parentCid ? parentCid : reply.shortCid;
|
||||
const isMobile = window.innerWidth <= 480;
|
||||
const postNumberSelector = isMobile ? '.post-number-mobile' : '.post-number-desktop';
|
||||
|
||||
if (threadCid && cid === threadCid) {
|
||||
const highlightedElements = document.querySelectorAll('.highlighted');
|
||||
if (threadCid && cid === threadCid) {
|
||||
const highlightedElements = document.querySelectorAll('.highlighted');
|
||||
|
||||
highlightedElements.forEach((el) => {
|
||||
el.classList.remove('highlighted');
|
||||
});
|
||||
highlightedElements.forEach((el) => {
|
||||
el.classList.remove('highlighted');
|
||||
});
|
||||
|
||||
const opElementSelector = isMobile ? '.op-mobile' : '.op-desktop';
|
||||
const opElementSelector = isMobile ? '.op-mobile' : '.op-desktop';
|
||||
|
||||
const opElement = [...document.querySelectorAll(opElementSelector)].find((el) => {
|
||||
const postNumberElement = el.querySelector(postNumberSelector);
|
||||
return postNumberElement && postNumberElement.innerHTML.includes(threadCid);
|
||||
});
|
||||
const opElement = [...document.querySelectorAll(opElementSelector)].find((el) => {
|
||||
const postNumberElement = el.querySelector(postNumberSelector);
|
||||
return postNumberElement && postNumberElement.innerHTML.includes(threadCid);
|
||||
});
|
||||
|
||||
if (opElement) {
|
||||
opElement.scrollIntoView({ behavior: 'auto', block: 'start' });
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (opElement) {
|
||||
opElement.scrollIntoView({ behavior: 'auto', block: 'start' });
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const targetElementSelector = isMobile ? '.post-reply-mobile, .op-mobile' : '.post-reply-desktop, .op-desktop';
|
||||
const targetElementSelector = isMobile ? '.post-reply-mobile, .op-mobile' : '.post-reply-desktop, .op-desktop';
|
||||
|
||||
const targetElement = [...document.querySelectorAll(targetElementSelector)].find((el) => {
|
||||
const postNumberElement = el.querySelector(postNumberSelector);
|
||||
return postNumberElement && postNumberElement.innerHTML.includes(cid);
|
||||
});
|
||||
const targetElement = [...document.querySelectorAll(targetElementSelector)].find((el) => {
|
||||
const postNumberElement = el.querySelector(postNumberSelector);
|
||||
return postNumberElement && postNumberElement.innerHTML.includes(cid);
|
||||
});
|
||||
|
||||
if (targetElement) {
|
||||
const highlightedElements = document.querySelectorAll('.highlighted-click');
|
||||
if (targetElement) {
|
||||
const highlightedElements = document.querySelectorAll('.highlighted-click');
|
||||
|
||||
highlightedElements.forEach((el) => {
|
||||
el.classList.remove('highlighted-click');
|
||||
});
|
||||
highlightedElements.forEach((el) => {
|
||||
el.classList.remove('highlighted-click');
|
||||
});
|
||||
|
||||
targetElement.scrollIntoView({ behavior: 'auto', block: 'start' });
|
||||
targetElement.scrollIntoView({ behavior: 'auto', block: 'start' });
|
||||
|
||||
if (!targetElement.classList.contains('op-mobile') && !targetElement.classList.contains('op-desktop')) {
|
||||
targetElement.classList.add('highlighted-click');
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (!targetElement.classList.contains('op-mobile') && !targetElement.classList.contains('op-desktop')) {
|
||||
targetElement.classList.add('highlighted-click');
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export default handleQuoteClick;
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
function handleQuoteHover(reply, parentCid, onElementOutOfView) {
|
||||
const cid = parentCid ? parentCid : reply.shortCid;
|
||||
const isMobile = window.innerWidth <= 480;
|
||||
const postNumberSelector = isMobile ? '.post-number-mobile' : '.post-number-desktop';
|
||||
const cid = parentCid ? parentCid : reply.shortCid;
|
||||
const isMobile = window.innerWidth <= 480;
|
||||
const postNumberSelector = isMobile ? '.post-number-mobile' : '.post-number-desktop';
|
||||
|
||||
const targetElementSelector = isMobile ? '.post-reply-mobile, .op-mobile' : '.post-reply-desktop, .op-desktop';
|
||||
const targetElementSelector = isMobile ? '.post-reply-mobile, .op-mobile' : '.post-reply-desktop, .op-desktop';
|
||||
|
||||
const targetElement = [...document.querySelectorAll(targetElementSelector)].find((el) => {
|
||||
const postNumberElement = el.querySelector(postNumberSelector);
|
||||
return postNumberElement && postNumberElement.innerHTML.includes(cid);
|
||||
});
|
||||
const targetElement = [...document.querySelectorAll(targetElementSelector)].find((el) => {
|
||||
const postNumberElement = el.querySelector(postNumberSelector);
|
||||
return postNumberElement && postNumberElement.innerHTML.includes(cid);
|
||||
});
|
||||
|
||||
if (targetElement) {
|
||||
const isInViewport = (element) => {
|
||||
const bounding = element.getBoundingClientRect();
|
||||
return (
|
||||
bounding.top >= 0 &&
|
||||
bounding.left >= 0 &&
|
||||
bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
};
|
||||
if (targetElement) {
|
||||
const isInViewport = (element) => {
|
||||
const bounding = element.getBoundingClientRect();
|
||||
return (
|
||||
bounding.top >= 0 &&
|
||||
bounding.left >= 0 &&
|
||||
bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
};
|
||||
|
||||
if (isInViewport(targetElement)) {
|
||||
const highlightedElements = document.querySelectorAll('.highlighted');
|
||||
if (isInViewport(targetElement)) {
|
||||
const highlightedElements = document.querySelectorAll('.highlighted');
|
||||
|
||||
highlightedElements.forEach((el) => {
|
||||
el.classList.remove('highlighted');
|
||||
});
|
||||
highlightedElements.forEach((el) => {
|
||||
el.classList.remove('highlighted');
|
||||
});
|
||||
|
||||
if (!targetElement.classList.contains('op-mobile') && !targetElement.classList.contains('op-desktop')) {
|
||||
targetElement.classList.add('highlighted');
|
||||
}
|
||||
} else {
|
||||
onElementOutOfView();
|
||||
}
|
||||
} else {
|
||||
onElementOutOfView();
|
||||
}
|
||||
if (!targetElement.classList.contains('op-mobile') && !targetElement.classList.contains('op-desktop')) {
|
||||
targetElement.classList.add('highlighted');
|
||||
}
|
||||
} else {
|
||||
onElementOutOfView();
|
||||
}
|
||||
} else {
|
||||
onElementOutOfView();
|
||||
}
|
||||
}
|
||||
|
||||
export default handleQuoteHover;
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
function handleShareClick(selectedAddress, cid) {
|
||||
let shareLink;
|
||||
let shareLink;
|
||||
|
||||
if (cid === 'rules' || cid === 'description') {
|
||||
shareLink = `https://plebchan.eth.limo/#/p/${selectedAddress}`;
|
||||
} else {
|
||||
const plebBzBaseURL = 'https://pleb.bz/p/';
|
||||
shareLink = `${plebBzBaseURL}${selectedAddress}/`;
|
||||
if (cid === 'rules' || cid === 'description') {
|
||||
shareLink = `https://plebchan.eth.limo/#/p/${selectedAddress}`;
|
||||
} else {
|
||||
const plebBzBaseURL = 'https://pleb.bz/p/';
|
||||
shareLink = `${plebBzBaseURL}${selectedAddress}/`;
|
||||
|
||||
if (cid !== 'rules' && cid !== 'description') {
|
||||
shareLink += `c/${cid}`;
|
||||
}
|
||||
if (cid !== 'rules' && cid !== 'description') {
|
||||
shareLink += `c/${cid}`;
|
||||
}
|
||||
|
||||
shareLink += `?redirect=plebchan.eth.limo`;
|
||||
}
|
||||
shareLink += `?redirect=plebchan.eth.limo`;
|
||||
}
|
||||
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard
|
||||
.writeText(shareLink)
|
||||
.then(() => {
|
||||
console.log('Link copied to clipboard!');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Could not copy text: ', err);
|
||||
});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(shareLink).catch((err) => {
|
||||
console.error('Could not copy text: ', err);
|
||||
});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export default handleShareClick;
|
||||
|
||||
@@ -1,92 +1,92 @@
|
||||
import useGeneralStore from '../hooks/stores/useGeneralStore';
|
||||
|
||||
const handleStyleChange = (event) => {
|
||||
const { setBodyStyle, setSelectedStyle } = useGeneralStore.getState();
|
||||
const { setBodyStyle, setSelectedStyle } = useGeneralStore.getState();
|
||||
|
||||
switch (event.target.value) {
|
||||
case 'Yotsuba':
|
||||
const yotsubaBodyStyle = {
|
||||
background: '#ffe url(assets/fade.png) top repeat-x',
|
||||
color: 'maroon',
|
||||
fontFamily: 'Arial, Helvetica, sans-serif',
|
||||
};
|
||||
setBodyStyle(yotsubaBodyStyle);
|
||||
setSelectedStyle('Yotsuba');
|
||||
localStorage.setItem('selectedStyle', 'Yotsuba');
|
||||
localStorage.setItem('bodyStyle', JSON.stringify(yotsubaBodyStyle));
|
||||
break;
|
||||
switch (event.target.value) {
|
||||
case 'Yotsuba':
|
||||
const yotsubaBodyStyle = {
|
||||
background: '#ffe url(assets/fade.png) top repeat-x',
|
||||
color: 'maroon',
|
||||
fontFamily: 'Arial, Helvetica, sans-serif',
|
||||
};
|
||||
setBodyStyle(yotsubaBodyStyle);
|
||||
setSelectedStyle('Yotsuba');
|
||||
localStorage.setItem('selectedStyle', 'Yotsuba');
|
||||
localStorage.setItem('bodyStyle', JSON.stringify(yotsubaBodyStyle));
|
||||
break;
|
||||
|
||||
case 'Yotsuba-B':
|
||||
const yotsubaBBodyStyle = {
|
||||
background: '#eef2ff url(assets/fade-blue.png) top center repeat-x',
|
||||
color: '#000',
|
||||
fontFamily: 'Arial, Helvetica, sans-serif',
|
||||
};
|
||||
setBodyStyle(yotsubaBBodyStyle);
|
||||
setSelectedStyle('Yotsuba-B');
|
||||
localStorage.setItem('selectedStyle', 'Yotsuba-B');
|
||||
localStorage.setItem('bodyStyle', JSON.stringify(yotsubaBBodyStyle));
|
||||
break;
|
||||
case 'Yotsuba-B':
|
||||
const yotsubaBBodyStyle = {
|
||||
background: '#eef2ff url(assets/fade-blue.png) top center repeat-x',
|
||||
color: '#000',
|
||||
fontFamily: 'Arial, Helvetica, sans-serif',
|
||||
};
|
||||
setBodyStyle(yotsubaBBodyStyle);
|
||||
setSelectedStyle('Yotsuba-B');
|
||||
localStorage.setItem('selectedStyle', 'Yotsuba-B');
|
||||
localStorage.setItem('bodyStyle', JSON.stringify(yotsubaBBodyStyle));
|
||||
break;
|
||||
|
||||
case 'Futaba':
|
||||
const futabaBodyStyle = {
|
||||
background: '#ffe',
|
||||
color: 'maroon',
|
||||
fontFamily: 'times new roman, serif',
|
||||
};
|
||||
setBodyStyle(futabaBodyStyle);
|
||||
setSelectedStyle('Futaba');
|
||||
localStorage.setItem('selectedStyle', 'Futaba');
|
||||
localStorage.setItem('bodyStyle', JSON.stringify(futabaBodyStyle));
|
||||
break;
|
||||
case 'Futaba':
|
||||
const futabaBodyStyle = {
|
||||
background: '#ffe',
|
||||
color: 'maroon',
|
||||
fontFamily: 'times new roman, serif',
|
||||
};
|
||||
setBodyStyle(futabaBodyStyle);
|
||||
setSelectedStyle('Futaba');
|
||||
localStorage.setItem('selectedStyle', 'Futaba');
|
||||
localStorage.setItem('bodyStyle', JSON.stringify(futabaBodyStyle));
|
||||
break;
|
||||
|
||||
case 'Burichan':
|
||||
const burichanBodyStyle = {
|
||||
background: '#eef2ff',
|
||||
color: '#000',
|
||||
fontFamily: 'times new roman, serif',
|
||||
};
|
||||
setBodyStyle(burichanBodyStyle);
|
||||
setSelectedStyle('Burichan');
|
||||
localStorage.setItem('selectedStyle', 'Burichan');
|
||||
localStorage.setItem('bodyStyle', JSON.stringify(burichanBodyStyle));
|
||||
break;
|
||||
case 'Burichan':
|
||||
const burichanBodyStyle = {
|
||||
background: '#eef2ff',
|
||||
color: '#000',
|
||||
fontFamily: 'times new roman, serif',
|
||||
};
|
||||
setBodyStyle(burichanBodyStyle);
|
||||
setSelectedStyle('Burichan');
|
||||
localStorage.setItem('selectedStyle', 'Burichan');
|
||||
localStorage.setItem('bodyStyle', JSON.stringify(burichanBodyStyle));
|
||||
break;
|
||||
|
||||
case 'Tomorrow':
|
||||
const tomorrowBodyStyle = {
|
||||
background: '#1d1f21 none',
|
||||
color: '#c5c8c6',
|
||||
fontFamily: 'Arial, Helvetica, sans-serif',
|
||||
};
|
||||
setBodyStyle(tomorrowBodyStyle);
|
||||
setSelectedStyle('Tomorrow');
|
||||
localStorage.setItem('selectedStyle', 'Tomorrow');
|
||||
localStorage.setItem('bodyStyle', JSON.stringify(tomorrowBodyStyle));
|
||||
break;
|
||||
case 'Tomorrow':
|
||||
const tomorrowBodyStyle = {
|
||||
background: '#1d1f21 none',
|
||||
color: '#c5c8c6',
|
||||
fontFamily: 'Arial, Helvetica, sans-serif',
|
||||
};
|
||||
setBodyStyle(tomorrowBodyStyle);
|
||||
setSelectedStyle('Tomorrow');
|
||||
localStorage.setItem('selectedStyle', 'Tomorrow');
|
||||
localStorage.setItem('bodyStyle', JSON.stringify(tomorrowBodyStyle));
|
||||
break;
|
||||
|
||||
case 'Photon':
|
||||
const photonBodyStyle = {
|
||||
background: '#eee none',
|
||||
color: '#333',
|
||||
fontFamily: 'Arial, Helvetica, sans-serif',
|
||||
};
|
||||
setBodyStyle(photonBodyStyle);
|
||||
setSelectedStyle('Photon');
|
||||
localStorage.setItem('selectedStyle', 'Photon');
|
||||
localStorage.setItem('bodyStyle', JSON.stringify(photonBodyStyle));
|
||||
break;
|
||||
case 'Photon':
|
||||
const photonBodyStyle = {
|
||||
background: '#eee none',
|
||||
color: '#333',
|
||||
fontFamily: 'Arial, Helvetica, sans-serif',
|
||||
};
|
||||
setBodyStyle(photonBodyStyle);
|
||||
setSelectedStyle('Photon');
|
||||
localStorage.setItem('selectedStyle', 'Photon');
|
||||
localStorage.setItem('bodyStyle', JSON.stringify(photonBodyStyle));
|
||||
break;
|
||||
|
||||
default:
|
||||
const defaultBodyStyle = {
|
||||
background: '#ffe url(assets/fade.png) top repeat-x',
|
||||
color: 'maroon',
|
||||
fontFamily: 'Arial, Helvetica, sans-serif',
|
||||
};
|
||||
setBodyStyle(defaultBodyStyle);
|
||||
setSelectedStyle('Yotsuba');
|
||||
localStorage.setItem('selectedStyle', 'Yotsuba');
|
||||
localStorage.setItem('bodyStyle', JSON.stringify(defaultBodyStyle));
|
||||
}
|
||||
default:
|
||||
const defaultBodyStyle = {
|
||||
background: '#ffe url(assets/fade.png) top repeat-x',
|
||||
color: 'maroon',
|
||||
fontFamily: 'Arial, Helvetica, sans-serif',
|
||||
};
|
||||
setBodyStyle(defaultBodyStyle);
|
||||
setSelectedStyle('Yotsuba');
|
||||
localStorage.setItem('selectedStyle', 'Yotsuba');
|
||||
localStorage.setItem('bodyStyle', JSON.stringify(defaultBodyStyle));
|
||||
}
|
||||
};
|
||||
|
||||
export default handleStyleChange;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const isValidUrl = (url) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export default isValidUrl;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const preloadImages = (imageUrls) => {
|
||||
imageUrls.forEach((imageUrl) => {
|
||||
const img = new Image();
|
||||
img.src = imageUrl;
|
||||
});
|
||||
imageUrls.forEach((imageUrl) => {
|
||||
const img = new Image();
|
||||
img.src = imageUrl;
|
||||
});
|
||||
};
|
||||
|
||||
export default preloadImages;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
function removeHighlight() {
|
||||
const highlightedElements = document.querySelectorAll('.highlighted');
|
||||
const highlightedElements = document.querySelectorAll('.highlighted');
|
||||
|
||||
highlightedElements.forEach((el) => {
|
||||
el.classList.remove('highlighted');
|
||||
});
|
||||
highlightedElements.forEach((el) => {
|
||||
el.classList.remove('highlighted');
|
||||
});
|
||||
}
|
||||
|
||||
export default removeHighlight;
|
||||
|
||||
Reference in New Issue
Block a user