Merge branch 'development' into master

This commit is contained in:
plebeius.eth
2023-09-24 11:31:59 +02:00
committed by GitHub
48 changed files with 17714 additions and 17729 deletions
+1 -1
View File
@@ -7,5 +7,5 @@ module.exports = {
trailingComma: 'all',
jsxSingleQuote: true,
arrowParens: 'always',
useTabs: true,
useTabs: false,
}
+8 -8
View File
@@ -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
View File
@@ -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;
+60 -60
View File
@@ -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;
+37 -37
View File
@@ -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;
+50 -50
View File
@@ -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
View File
@@ -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);
+19 -19
View File
@@ -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;
+23 -23
View File
@@ -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;
+20 -20
View File
@@ -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;
+9 -9
View File
@@ -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
View File
@@ -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, '&nbsp;\n\n');
const doubleNewlineContent = content?.replace(/\n/g, '&nbsp;\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);
+27 -27
View File
@@ -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
View File
@@ -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>
)}
&nbsp;
<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>
&nbsp;
<span className='date-time' data-utc='data'>
{getDate(reply.timestamp)}
</span>
&nbsp;
<span className='post-number post-number-desktop'>
<span>c/</span>
<Link to={() => {}} id='reply-button' title='Reply to this post'>
{reply.shortCid}
</Link>
</span>
&nbsp;
<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>
&nbsp;
</div>
))}
</div>
</div>
{replyMediaInfo?.url ? (
<div className='file' style={{ marginBottom: '5px' }}>
<div className='reply-file-text'>
Link:&nbsp;
<a href={replyMediaInfo.url} target='_blank' rel='noopener noreferrer'>
{replyMediaInfo?.url.length > 30 ? replyMediaInfo?.url.slice(0, 30) + '(...)' : replyMediaInfo?.url}
</a>
&nbsp;{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>
)}
&nbsp;
<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>
)}
)&nbsp;
</span>
<br />
</span>
<span className='date-time-mobile post-number-mobile'>
{getDate(reply.timestamp)}&nbsp;
<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>
)}
&nbsp;
<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>
&nbsp;
<span className='date-time' data-utc='data'>
{getDate(reply.timestamp)}
</span>
&nbsp;
<span className='post-number post-number-desktop'>
<span>c/</span>
<Link to={() => {}} id='reply-button' title='Reply to this post'>
{reply.shortCid}
</Link>
</span>
&nbsp;
<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>
&nbsp;
</div>
))}
</div>
</div>
{replyMediaInfo?.url ? (
<div className='file' style={{ marginBottom: '5px' }}>
<div className='reply-file-text'>
Link:&nbsp;
<a href={replyMediaInfo.url} target='_blank' rel='noopener noreferrer'>
{replyMediaInfo?.url.length > 30 ? replyMediaInfo?.url.slice(0, 30) + '(...)' : replyMediaInfo?.url}
</a>
&nbsp;{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>
)}
&nbsp;
<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>
)}
)&nbsp;
</span>
<br />
</span>
<span className='date-time-mobile post-number-mobile'>
{getDate(reply.timestamp)}&nbsp;
<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;
+16 -16
View File
@@ -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;
+3 -3
View File
@@ -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);
+282 -300
View File
@@ -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} -&nbsp;
<div className='panel'>
<div className='panel-header'>
<span id='version'>
v{version}
{commitRef} -&nbsp;
{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();
}}
/>
&nbsp;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
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -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
View File
@@ -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;
+6 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+75 -75
View File
@@ -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
View File
@@ -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;
+8 -8
View File
@@ -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
View File
@@ -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;
+9 -9
View File
@@ -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
View File
@@ -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;
+13 -13
View File
@@ -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;
+62 -62
View File
@@ -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
View File
@@ -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;
+35 -35
View File
@@ -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;
+28 -28
View File
@@ -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;
+6 -6
View File
@@ -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;
+37 -37
View File
@@ -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;
+32 -32
View File
@@ -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;
+18 -23
View File
@@ -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;
+79 -79
View File
@@ -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;
+6 -6
View File
@@ -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;
+4 -4
View File
@@ -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;
+4 -4
View File
@@ -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;