This commit is contained in:
plebeius.eth
2023-09-16 21:40:23 +02:00
parent d963c90d26
commit 020e509b71
39 changed files with 1970 additions and 2035 deletions
+10 -17
View File
@@ -1,24 +1,17 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from 'react';
import { useSubplebbit } from "@plebbit/plebbit-react-hooks"; import { useSubplebbit } from '@plebbit/plebbit-react-hooks';
const BoardAvatar = ({ address }) => { const BoardAvatar = ({ address }) => {
const [avatarUrl, setAvatarUrl] = useState("assets/plebchan.png"); const [avatarUrl, setAvatarUrl] = useState('assets/plebchan.png');
const subplebbit = useSubplebbit({ subplebbitAddress: address }); const subplebbit = useSubplebbit({ subplebbitAddress: address });
useEffect(() => { useEffect(() => {
if (subplebbit.suggested?.avatarUrl) { if (subplebbit.suggested?.avatarUrl) {
setAvatarUrl(subplebbit.suggested?.avatarUrl); setAvatarUrl(subplebbit.suggested?.avatarUrl);
} }
}, [subplebbit.suggested?.avatarUrl, subplebbit]); }, [subplebbit.suggested?.avatarUrl, subplebbit]);
return ( return <img className='board-avatar' alt='board avatar' src={avatarUrl} />;
<img
className="board-avatar"
alt="board avatar"
src={avatarUrl}
/>
);
}; };
export default BoardAvatar; export default BoardAvatar;
+273 -290
View File
@@ -1,332 +1,315 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from 'react';
import { Link } from "react-router-dom"; import { Link } from 'react-router-dom';
import { usePublishSubplebbitEdit } from "@plebbit/plebbit-react-hooks"; import { usePublishSubplebbitEdit } from '@plebbit/plebbit-react-hooks';
import { StyledModal } from './styled/modals/ModerationModal.styled'; import { StyledModal } from './styled/modals/ModerationModal.styled';
import useError from "../hooks/useError"; import useError from '../hooks/useError';
import useSuccess from "../hooks/useSuccess"; import useSuccess from '../hooks/useSuccess';
import useGeneralStore from '../hooks/stores/useGeneralStore'; import useGeneralStore from '../hooks/stores/useGeneralStore';
const BoardSettings = ({ subplebbit }) => { const BoardSettings = ({ subplebbit }) => {
const { const { setCaptchaResponse, setChallengesArray, setIsCaptchaOpen, setResolveCaptchaPromise, selectedAddress, selectedStyle } = useGeneralStore((state) => state);
setCaptchaResponse,
setChallengesArray,
setIsCaptchaOpen,
setResolveCaptchaPromise,
selectedAddress,
selectedStyle,
} = useGeneralStore(state => state);
const allowedSettings = { const allowedSettings = {
address: subplebbit.address, address: subplebbit.address,
apiUrl: subplebbit.apiUrl, apiUrl: subplebbit.apiUrl,
description: subplebbit.description, description: subplebbit.description,
pubsubTopic: subplebbit.pubsubTopic, pubsubTopic: subplebbit.pubsubTopic,
settings: { settings: {
fetchThumbnailUrls: subplebbit.settings?.fetchThumbnailUrls, fetchThumbnailUrls: subplebbit.settings?.fetchThumbnailUrls,
fetchThumbnailUrlsProxyUrl: subplebbit.settings?.fetchThumbnailUrlsProxyUrl, fetchThumbnailUrlsProxyUrl: subplebbit.settings?.fetchThumbnailUrlsProxyUrl,
}, },
roles: subplebbit.roles, roles: subplebbit.roles,
rules: subplebbit.rules, rules: subplebbit.rules,
suggested: { suggested: {
avatarUrl: subplebbit.suggested?.avatarUrl, avatarUrl: subplebbit.suggested?.avatarUrl,
backgroundUrl: subplebbit.suggested?.backgroundUrl, backgroundUrl: subplebbit.suggested?.backgroundUrl,
bannerUrl: subplebbit.suggested?.bannerUrl, bannerUrl: subplebbit.suggested?.bannerUrl,
language: subplebbit.suggested?.language, language: subplebbit.suggested?.language,
primaryColor: subplebbit.suggested?.primaryColor, primaryColor: subplebbit.suggested?.primaryColor,
secondaryColor: subplebbit.suggested?.secondaryColor, secondaryColor: subplebbit.suggested?.secondaryColor,
}, },
title: subplebbit.title, title: subplebbit.title,
}; };
const generateSettingsFromSubplebbit = (subplebbitData) => ({ const generateSettingsFromSubplebbit = (subplebbitData) => ({
address: subplebbitData.address, address: subplebbitData.address,
apiUrl: subplebbitData.apiUrl, apiUrl: subplebbitData.apiUrl,
description: subplebbitData.description, description: subplebbitData.description,
pubsubTopic: subplebbitData.pubsubTopic, pubsubTopic: subplebbitData.pubsubTopic,
settings: { settings: {
fetchThumbnailUrls: subplebbitData.settings?.fetchThumbnailUrls, fetchThumbnailUrls: subplebbitData.settings?.fetchThumbnailUrls,
fetchThumbnailUrlsProxyUrl: subplebbitData.settings?.fetchThumbnailUrlsProxyUrl, fetchThumbnailUrlsProxyUrl: subplebbitData.settings?.fetchThumbnailUrlsProxyUrl,
}, },
roles: subplebbitData.roles, roles: subplebbitData.roles,
rules: subplebbitData.rules, rules: subplebbitData.rules,
suggested: { suggested: {
avatarUrl: subplebbitData.suggested?.avatarUrl, avatarUrl: subplebbitData.suggested?.avatarUrl,
backgroundUrl: subplebbitData.suggested?.backgroundUrl, backgroundUrl: subplebbitData.suggested?.backgroundUrl,
bannerUrl: subplebbitData.suggested?.bannerUrl, bannerUrl: subplebbitData.suggested?.bannerUrl,
language: subplebbitData.suggested?.language, language: subplebbitData.suggested?.language,
primaryColor: subplebbitData.suggested?.primaryColor, primaryColor: subplebbitData.suggested?.primaryColor,
secondaryColor: subplebbitData.suggested?.secondaryColor, secondaryColor: subplebbitData.suggested?.secondaryColor,
}, },
title: subplebbitData.title, title: subplebbitData.title,
}); });
const initialSettings = generateSettingsFromSubplebbit(subplebbit); const initialSettings = generateSettingsFromSubplebbit(subplebbit);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [boardSettingsJson, setBoardSettingsJson] = useState(JSON.stringify(initialSettings, null, 2)); const [boardSettingsJson, setBoardSettingsJson] = useState(JSON.stringify(initialSettings, null, 2));
const [triggerPublilshSubplebbitEdit, setTriggerPublishCommentEdit] = useState(false); const [triggerPublilshSubplebbitEdit, setTriggerPublishCommentEdit] = useState(false);
const [, setNewErrorMessage] = useError(); const [, setNewErrorMessage] = useError();
const [, setNewSuccessMessage] = useSuccess(); const [, setNewSuccessMessage] = useSuccess();
const getDifferences = (oldObj, newObj) => {
let differences = {};
const getDifferences = (oldObj, newObj) => { for (let key in oldObj) {
let differences = {}; 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) { for (let key in newObj) {
if (typeof oldObj[key] === 'object' && oldObj[key] !== null) { if (!oldObj.hasOwnProperty(key)) {
const nestedDifferences = getDifferences(oldObj[key], newObj[key] || {}); differences[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) { return differences;
if (!oldObj.hasOwnProperty(key)) { };
differences[key] = newObj[key];
}
}
return differences; const isInitialMount = useRef(true);
};
useEffect(() => {
if (isInitialMount.current) {
setBoardSettingsJson(JSON.stringify(generateSettingsFromSubplebbit(subplebbit), null, 2));
isInitialMount.current = false;
}
}, [subplebbit]);
const isInitialMount = useRef(true); function validateSettings(updatedSettings, allowedSettings) {
for (let key in updatedSettings) {
if (!allowedSettings.hasOwnProperty(key) && !initialSettings.hasOwnProperty(key)) {
throw new Error(`Unexpected setting: ${key}`);
}
useEffect(() => { if (typeof updatedSettings[key] === 'object' && updatedSettings[key] !== null && !Array.isArray(updatedSettings[key])) {
if (isInitialMount.current) { if (typeof allowedSettings[key] !== 'object' || allowedSettings[key] === null || Array.isArray(allowedSettings[key])) {
setBoardSettingsJson(JSON.stringify(generateSettingsFromSubplebbit(subplebbit), null, 2)); throw new Error(`Expected ${key} to be an object in allowedSettings`);
isInitialMount.current = false; }
} validateSettings(updatedSettings[key], allowedSettings[key]);
}, [subplebbit]); }
}
}
const onChallenge = async (challenges, subplebbitEdit) => {
let challengeAnswers = [];
function validateSettings(updatedSettings, allowedSettings) { try {
for (let key in updatedSettings) { challengeAnswers = await getChallengeAnswersFromUser(challenges);
if (!allowedSettings.hasOwnProperty(key) && !initialSettings.hasOwnProperty(key)) { } catch (error) {
throw new Error(`Unexpected setting: ${key}`); setNewErrorMessage(error.message);
} console.log(error);
}
if (challengeAnswers) {
await subplebbitEdit.publishChallengeAnswers(challengeAnswers);
}
};
if (typeof updatedSettings[key] === 'object' && updatedSettings[key] !== null const onChallengeVerification = (challengeVerification) => {
&& !Array.isArray(updatedSettings[key])) { if (challengeVerification.challengeSuccess === true) {
if (typeof allowedSettings[key] !== 'object' || allowedSettings[key] === null setNewSuccessMessage('Challenge Success');
|| Array.isArray(allowedSettings[key])) { console.log('challenge success', challengeVerification);
throw new Error(`Expected ${key} to be an object in allowedSettings`); } else if (challengeVerification.challengeSuccess === false) {
} setNewErrorMessage(`Challenge Failed, reason: ${challengeVerification.reason}. Errors: ${challengeVerification.errors}`);
validateSettings(updatedSettings[key], allowedSettings[key]); console.log('challenge failed', challengeVerification);
} }
} };
}
const getChallengeAnswersFromUser = async (challenges) => {
setChallengesArray(challenges);
const onChallenge = async (challenges, subplebbitEdit) => { return new Promise((resolve, reject) => {
let challengeAnswers = []; const imageString = challenges?.challenges[0].challenge;
const imageSource = `data:image/png;base64,${imageString}`;
const challengeImg = new Image();
challengeImg.src = imageSource;
try { challengeImg.onload = () => {
challengeAnswers = await getChallengeAnswersFromUser(challenges) setIsCaptchaOpen(true);
}
catch (error) {
setNewErrorMessage(error.message); console.log(error);
}
if (challengeAnswers) {
await subplebbitEdit.publishChallengeAnswers(challengeAnswers)
}
};
const handleKeyDown = async (event) => {
if (event.key === 'Enter') {
const currentCaptchaResponse = useGeneralStore.getState().captchaResponse;
resolve(currentCaptchaResponse);
setIsCaptchaOpen(false);
document.removeEventListener('keydown', handleKeyDown);
event.preventDefault();
}
};
const onChallengeVerification = (challengeVerification) => { setCaptchaResponse('');
if (challengeVerification.challengeSuccess === true) { document.addEventListener('keydown', handleKeyDown);
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);
}
};
setResolveCaptchaPromise(resolve);
};
const getChallengeAnswersFromUser = async (challenges) => { challengeImg.onerror = () => {
setChallengesArray(challenges); reject(setNewErrorMessage('Could not load challenges'));
};
});
};
return new Promise((resolve, reject) => { const [editSubplebbitOptions, setEditSubplebbitOptions] = useState({
const imageString = challenges?.challenges[0].challenge; subplebbitAddress: selectedAddress,
const imageSource = `data:image/png;base64,${imageString}`; onChallenge,
const challengeImg = new Image(); onChallengeVerification,
challengeImg.src = imageSource; onError: (error) => {
setNewErrorMessage(error.message);
console.log(error);
},
});
challengeImg.onload = () => { const { publishSubplebbitEdit } = usePublishSubplebbitEdit(editSubplebbitOptions);
setIsCaptchaOpen(true);
const handleKeyDown = async (event) => { useEffect(() => {
if (event.key === 'Enter') { let isActive = true;
const currentCaptchaResponse = useGeneralStore.getState().captchaResponse; if (editSubplebbitOptions && triggerPublilshSubplebbitEdit) {
resolve(currentCaptchaResponse); (async () => {
setIsCaptchaOpen(false); await publishSubplebbitEdit(editSubplebbitOptions);
document.removeEventListener('keydown', handleKeyDown); if (isActive) {
event.preventDefault(); setTriggerPublishCommentEdit(false);
} }
}; })();
}
setCaptchaResponse(''); return () => {
document.addEventListener('keydown', handleKeyDown); isActive = false;
};
}, [editSubplebbitOptions, publishSubplebbitEdit, triggerPublilshSubplebbitEdit]);
setResolveCaptchaPromise(resolve); 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);
}
};
challengeImg.onerror = () => { const handleResetChanges = () => {
reject(setNewErrorMessage('Could not load challenges')); setBoardSettingsJson(JSON.stringify(initialSettings, null, 2));
}; };
});
};
function generateSettingsList(settingsObj, parentKey = '') {
let result = [];
const [editSubplebbitOptions, setEditSubplebbitOptions] = useState({ for (let key in settingsObj) {
subplebbitAddress: selectedAddress, if (typeof settingsObj[key] === 'object' && settingsObj[key] !== null) {
onChallenge, const nestedItems = generateSettingsList(settingsObj[key], `${parentKey}${key}.`);
onChallengeVerification, if (nestedItems.length > 1) {
onError: (error) => { result.push(`${parentKey}${key}: { ${nestedItems.join(', ')} }`);
setNewErrorMessage(error.message); console.log(error); } else {
} result.push(...nestedItems);
}); }
} else {
result.push(`${parentKey}${key}`);
}
}
return result;
}
const { publishSubplebbitEdit } = usePublishSubplebbitEdit(editSubplebbitOptions); const possibleSettingsList = generateSettingsList(initialSettings);
const handleCloseModal = () => {
setIsModalOpen(false);
setBoardSettingsJson(JSON.stringify(initialSettings, null, 2));
};
useEffect(() => { const openModal = () => {
let isActive = true; setIsModalOpen(true);
if (editSubplebbitOptions && triggerPublilshSubplebbitEdit) { setBoardSettingsJson(JSON.stringify(generateSettingsFromSubplebbit(subplebbit), null, 2));
(async () => { };
await publishSubplebbitEdit(editSubplebbitOptions);
if (isActive) {
setTriggerPublishCommentEdit(false);
}
})();
}
return () => { return (
isActive = false; <>
}; <StyledModal
}, [editSubplebbitOptions, publishSubplebbitEdit, triggerPublilshSubplebbitEdit]); isOpen={isModalOpen}
onRequestClose={handleCloseModal}
contentLabel='Board Settings'
const handleSaveChanges = async () => { style={{ overlay: { backgroundColor: 'rgba(0,0,0,.25)' } }}
try { selectedStyle={selectedStyle}
const updatedSettings = JSON.parse(boardSettingsJson); >
validateSettings(updatedSettings, allowedSettings); <div className='panel-board'>
const changes = getDifferences(initialSettings, updatedSettings); <div className='panel-header'>
if (Object.keys(changes).length > 0) { Board Settings
setEditSubplebbitOptions(prevOptions => ({ <Link to='' onClick={handleCloseModal}>
...prevOptions, <span className='icon' title='close' />
...changes </Link>
})); </div>
setTriggerPublishCommentEdit(true); <div className='settings-info'>
} else { <div>
setNewErrorMessage("No changes detected"); <strong>Allowed settings: </strong>
} <span>{`{ ${possibleSettingsList.join(', ')} }`}</span>
} catch (error) { </div>
setNewErrorMessage(`Error saving changes: ${error}`); <strong style={{ marginTop: '10px', display: 'inline-block' }}>API docs: </strong>
console.log(error); <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
const handleResetChanges = () => { value={boardSettingsJson}
setBoardSettingsJson(JSON.stringify(initialSettings, null, 2)); onChange={(e) => setBoardSettingsJson(e.target.value)}
}; className='board-settings'
autoComplete='off'
autoCorrect='off'
function generateSettingsList(settingsObj, parentKey = '') { spellCheck='false'
let result = []; />
<div className='button-group'>
for (let key in settingsObj) { <button id='reset-board-settings' onClick={handleResetChanges}>
if (typeof settingsObj[key] === 'object' && settingsObj[key] !== null) { Reset
const nestedItems = generateSettingsList(settingsObj[key], `${parentKey}${key}.`); </button>
if (nestedItems.length > 1) { <button id='save-board-settings' onClick={handleSaveChanges}>
result.push(`${parentKey}${key}: { ${nestedItems.join(', ')} }`); Save Changes
} else { </button>
result.push(...nestedItems); </div>
} </div>
} else { </StyledModal>
result.push(`${parentKey}${key}`);  [
} <span id='subscribe' style={{ cursor: 'pointer' }}>
} <span
onClick={() => {
return result; 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',
const possibleSettingsList = generateSettingsList(initialSettings); );
}}
>
const handleCloseModal = () => { Settings
setIsModalOpen(false); </span>
setBoardSettingsJson(JSON.stringify(initialSettings, null, 2)); </span>
}; ]
</>
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>
]
</>
);
}; };
export default BoardSettings; export default BoardSettings;
+64 -52
View File
@@ -1,62 +1,74 @@
import React, { useState } from "react"; import React, { useState } from 'react';
import { useSubplebbit, useSubplebbitStats } from "@plebbit/plebbit-react-hooks/dist"; import { useSubplebbit, useSubplebbitStats } from '@plebbit/plebbit-react-hooks/dist';
import { BoardStatsContainer } from "./styled/BoardStats.styled"; import { BoardStatsContainer } from './styled/BoardStats.styled';
import { Break } from "./styled/views/Board.styled"; import { Break } from './styled/views/Board.styled';
import useGeneralStore from '../hooks/stores/useGeneralStore'; import useGeneralStore from '../hooks/stores/useGeneralStore';
const BoardStats = ({ subplebbitAddress }) => { const BoardStats = ({ subplebbitAddress }) => {
const { selectedStyle } = useGeneralStore(state => state); const { selectedStyle } = useGeneralStore((state) => state);
const stats = useSubplebbitStats({subplebbitAddress}); const stats = useSubplebbitStats({ subplebbitAddress });
const [showStats, setShowStats] = useState(true); const [showStats, setShowStats] = useState(true);
const subplebbit = useSubplebbit({subplebbitAddress}); const subplebbit = useSubplebbit({ subplebbitAddress });
const handleToggleStats = () => { const handleToggleStats = () => {
setShowStats(!showStats); setShowStats(!showStats);
}; };
const unixToMMDDYYYY = (timestamp) => { const unixToMMDDYYYY = (timestamp) => {
const date = new Date(timestamp * 1000); const date = new Date(timestamp * 1000);
const month = ('0' + (date.getMonth() + 1)).slice(-2); const month = ('0' + (date.getMonth() + 1)).slice(-2);
const day = ('0' + date.getDate()).slice(-2); const day = ('0' + date.getDate()).slice(-2);
const year = date.getFullYear().toString().slice(-2); const year = date.getFullYear().toString().slice(-2);
return month + '/' + day + '/' + year; return month + '/' + day + '/' + year;
}; };
const pluralize = (count, singular, plural) => count === 1 ? singular : plural; const pluralize = (count, singular, plural) => (count === 1 ? singular : plural);
return ( return (
<BoardStatsContainer selectedStyle={selectedStyle}> <BoardStatsContainer selectedStyle={selectedStyle}>
<Break selectedStyle={selectedStyle} style={{width: '468px'}}/> <Break selectedStyle={selectedStyle} style={{ width: '468px' }} />
<table id="blotter"> <table id='blotter'>
{showStats && ( {showStats && (
<tbody id="blotter-msgs"> <tbody id='blotter-msgs'>
<tr> <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> <td>
</tr> In the past hour, <span id='stat-number'>{stats.hourActiveUserCount}</span> {pluralize(stats.hourActiveUserCount, 'user', 'users')} made{' '}
<tr> <span id='stat-number'>{stats.hourPostCount}</span> {pluralize(stats.hourPostCount, 'post', 'posts')} / in the past day,{' '}
<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> <span id='stat-number'>{stats.dayActiveUserCount}</span> {pluralize(stats.dayActiveUserCount, 'user', 'users')} made{' '}
</tr> <span id='stat-number'>{stats.dayPostCount}</span> {pluralize(stats.dayPostCount, 'post', 'posts')}
<tr> </td>
<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>
</tr> <tr>
</tbody> <td>
)} In the past week, <span id='stat-number'>{stats.weekActiveUserCount}</span> {pluralize(stats.weekActiveUserCount, 'user', 'users')} made{' '}
<tfoot> <span id='stat-number'>{stats.weekPostCount}</span> {pluralize(stats.weekPostCount, 'post', 'posts')} / in the past month,{' '}
<tr> <span id='stat-number'>{stats.monthActiveUserCount}</span> {pluralize(stats.monthActiveUserCount, 'user', 'users')} made{' '}
<td colSpan={2}> <span id='stat-number'>{stats.monthPostCount}</span> {pluralize(stats.monthPostCount, 'post', 'posts')}
[ </td>
<span id="stat-number" className="hide-button" onClick={handleToggleStats}> </tr>
{showStats ? 'Hide' : 'Show Stats'} <tr>
</span> <td>
] {unixToMMDDYYYY(subplebbit.createdAt)} board created / since then, <span id='stat-number'>{stats.allActiveUserCount}</span>{' '}
</td> {pluralize(stats.allActiveUserCount, 'user', 'users')} have made <span id='stat-number'>{stats.allPostCount}</span>{' '}
</tr> {pluralize(stats.allPostCount, 'post', 'posts')}
</tfoot> </td>
</table> </tr>
</BoardStatsContainer> </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; export default BoardStats;
+37 -39
View File
@@ -2,50 +2,48 @@ import React from 'react';
import ContentLoader from 'react-content-loader'; import ContentLoader from 'react-content-loader';
import useGeneralStore from '../hooks/stores/useGeneralStore'; import useGeneralStore from '../hooks/stores/useGeneralStore';
const SingleRectLoader = () => { const SingleRectLoader = () => {
const selectedStyle = useGeneralStore((state) => state.selectedStyle); const selectedStyle = useGeneralStore((state) => state.selectedStyle);
const backgroundColor = selectedStyle === 'Tomorrow' ? '#333' : '#f3f3f3'; const backgroundColor = selectedStyle === 'Tomorrow' ? '#333' : '#f3f3f3';
const foregroundColor = selectedStyle === 'Tomorrow' ? '#555' : '#ecebeb'; const foregroundColor = selectedStyle === 'Tomorrow' ? '#555' : '#ecebeb';
return ( return (
<ContentLoader <ContentLoader
speed={2} speed={2}
width={150} width={150}
height={215} height={215}
viewBox="0 0 150 215" viewBox='0 0 150 215'
backgroundColor={backgroundColor} backgroundColor={backgroundColor}
foregroundColor={foregroundColor} foregroundColor={foregroundColor}
style={{ style={{
width: '150px', width: '150px',
height: '215px', height: '215px',
marginRight: '30px', marginRight: '30px',
marginBottom: '30px', marginBottom: '30px',
}} }}
> >
<rect x={0} y={0} width="150" height="150" /> <rect x={0} y={0} width='150' height='150' />
<rect x={0} y={170} width="150" height="18" /> <rect x={0} y={170} width='150' height='18' />
<rect x={0} y={195} width="80" height="20" /> <rect x={0} y={195} width='80' height='20' />
</ContentLoader> </ContentLoader>
); );
}; };
const CatalogLoader = () => { const CatalogLoader = () => {
return ( return (
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexWrap: 'wrap', flexWrap: 'wrap',
justifyContent: 'center', justifyContent: 'center',
boxSizing: 'border-box', boxSizing: 'border-box',
}} }}
> >
{[...Array(24)].map((_, index) => ( {[...Array(24)].map((_, index) => (
<SingleRectLoader key={index} /> <SingleRectLoader key={index} />
))} ))}
</div> </div>
); );
}; };
export default CatalogLoader; export default CatalogLoader;
+56 -60
View File
@@ -1,71 +1,67 @@
import React, { useState } from "react"; import React, { useState } from 'react';
import { useComment, useEditedComment } from "@plebbit/plebbit-react-hooks"; import { useComment, useEditedComment } from '@plebbit/plebbit-react-hooks';
import { Link } from "react-router-dom"; import { Link } from 'react-router-dom';
import OriginalCommentModal from "./modals/OriginalCommentModal"; import OriginalCommentModal from './modals/OriginalCommentModal';
import getDate from "../utils/getDate"; import getDate from '../utils/getDate';
import useGeneralStore from "../hooks/stores/useGeneralStore"; import useGeneralStore from '../hooks/stores/useGeneralStore';
const EditLabel = ({ commentCid, className }) => { const EditLabel = ({ commentCid, className }) => {
const { editedComments, setEditedComments } = useGeneralStore(state => state); const { editedComments, setEditedComments } = useGeneralStore((state) => state);
const [isOriginalCommentModalOpen, setIsOriginalCommentModalOpen] = useState(false); const [isOriginalCommentModalOpen, setIsOriginalCommentModalOpen] = useState(false);
const comment = useComment({commentCid}); const comment = useComment({ commentCid });
const timestamp = getDate(comment.edit?.timestamp); const timestamp = getDate(comment.edit?.timestamp);
const {state: editedCommentState, editedComment} = useEditedComment({comment}); const { state: editedCommentState, editedComment } = useEditedComment({ comment });
if (editedCommentState === 'pending' && !(commentCid in editedComments)) { if (editedCommentState === 'pending' && !(commentCid in editedComments)) {
setEditedComments({...editedComments, [commentCid]: editedComment}) setEditedComments({ ...editedComments, [commentCid]: editedComment });
} }
const conditionsCheck = () => { const conditionsCheck = () => {
let conditions = []; let conditions = [];
if (editedComment?.removed && !comment.removed) { if (editedComment?.removed && !comment.removed) {
conditions.push("removal"); conditions.push('removal');
} }
if (editedComment?.edit && !comment.edit) { if (editedComment?.edit && !comment.edit) {
conditions.push("edit"); conditions.push('edit');
} }
if (editedComment?.locked && !comment.locked) { if (editedComment?.locked && !comment.locked) {
conditions.push("lock"); conditions.push('lock');
} }
if (editedComment?.pinned && !comment.pinned) { if (editedComment?.pinned && !comment.pinned) {
conditions.push("sticky"); 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} />
return ( {comment.edit && (
<> <>
<OriginalCommentModal <br />
isOpen={isOriginalCommentModalOpen} <span className={className}>
closeModal={() => setIsOriginalCommentModalOpen(false)} (Edited at {timestamp},{' '}
comment={comment}/> <Link className='ttl-link' onClick={() => setIsOriginalCommentModalOpen(true)}>
{comment.edit && ( show original
<> </Link>
<br /> )
<span className={className}> </span>
(Edited at {timestamp}, <Link className="ttl-link" onClick={ </>
() => setIsOriginalCommentModalOpen(true) )}
}>show original</Link>) {(editedCommentState === 'pending' || editedCommentState === 'failed') && conditionsString ? (
</span> <>
</> <br />
)} <span className={className}>
{((editedCommentState === 'pending' || editedCommentState === 'failed') && conditionsString) ? ( ({editedCommentState === 'pending' ? 'Pending' : 'Failed'} {conditionsString})
<> </span>
<br /> </>
<span className={className}> ) : null}
({editedCommentState === 'pending' ? 'Pending' : 'Failed'} {conditionsString}) </>
</span> );
</>
) : null}
</>
);
}; };
export default EditLabel; export default EditLabel;
+209 -184
View File
@@ -1,96 +1,100 @@
const Embed = ({url}) => { const Embed = ({ url }) => {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
if (youtubeHosts.has(parsedUrl.host)) { if (youtubeHosts.has(parsedUrl.host)) {
return <YoutubeEmbed parsedUrl={parsedUrl} />; return <YoutubeEmbed parsedUrl={parsedUrl} />;
} }
if (twitterHosts.has(parsedUrl.host)) { if (twitterHosts.has(parsedUrl.host)) {
return <TwitterEmbed parsedUrl={parsedUrl} />; return <TwitterEmbed parsedUrl={parsedUrl} />;
} }
if (redditHosts.has(parsedUrl.host)) { if (redditHosts.has(parsedUrl.host)) {
return <RedditEmbed parsedUrl={parsedUrl} />; return <RedditEmbed parsedUrl={parsedUrl} />;
} }
if (twitchHosts.has(parsedUrl.host)) { if (twitchHosts.has(parsedUrl.host)) {
return <TwitchEmbed parsedUrl={parsedUrl} />; return <TwitchEmbed parsedUrl={parsedUrl} />;
} }
if (tiktokHosts.has(parsedUrl.host)) { if (tiktokHosts.has(parsedUrl.host)) {
return <TiktokEmbed parsedUrl={parsedUrl} />; return <TiktokEmbed parsedUrl={parsedUrl} />;
} }
if (instagramHosts.has(parsedUrl.host)) { if (instagramHosts.has(parsedUrl.host)) {
return <InstagramEmbed parsedUrl={parsedUrl} />; return <InstagramEmbed parsedUrl={parsedUrl} />;
} }
if (odyseeHosts.has(parsedUrl.host)) { if (odyseeHosts.has(parsedUrl.host)) {
return <OdyseeEmbed parsedUrl={parsedUrl} />; return <OdyseeEmbed parsedUrl={parsedUrl} />;
} }
if (bitchuteHosts.has(parsedUrl.host)) { if (bitchuteHosts.has(parsedUrl.host)) {
return <BitchuteEmbed parsedUrl={parsedUrl} />; return <BitchuteEmbed parsedUrl={parsedUrl} />;
} }
if (streamableHosts.has(parsedUrl.host)) { if (streamableHosts.has(parsedUrl.host)) {
return <StreamableEmbed parsedUrl={parsedUrl} />; return <StreamableEmbed parsedUrl={parsedUrl} />;
} }
if (spotifyHosts.has(parsedUrl.host)) { if (spotifyHosts.has(parsedUrl.host)) {
return <SpotifyEmbed parsedUrl={parsedUrl} />; return <SpotifyEmbed parsedUrl={parsedUrl} />;
} }
}; };
const youtubeHosts = new Set(['youtube.com', 'www.youtube.com', 'youtu.be', 'www.youtu.be']); const youtubeHosts = new Set(['youtube.com', 'www.youtube.com', 'youtu.be', 'www.youtu.be']);
const YoutubeEmbed = ({parsedUrl}) => { const YoutubeEmbed = ({ parsedUrl }) => {
let youtubeId; let youtubeId;
if (parsedUrl.host.endsWith('youtu.be')) { if (parsedUrl.host.endsWith('youtu.be')) {
youtubeId = parsedUrl.pathname.replaceAll('/', ''); youtubeId = parsedUrl.pathname.replaceAll('/', '');
} } else {
else { youtubeId = parsedUrl.searchParams.get('v');
youtubeId = parsedUrl.searchParams.get('v'); }
} return (
return <iframe <iframe
className='enlarged youtube-embed' className='enlarged youtube-embed'
height="100%" height='100%'
width="100%" width='100%'
frameborder="0" frameborder='0'
credentialless credentialless
referrerpolicy='no-referrer' referrerpolicy='no-referrer'
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share" allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
allowfullscreen allowfullscreen
title={parsedUrl.href} title={parsedUrl.href}
src={`https://www.youtube-nocookie.com/embed/${youtubeId}`} src={`https://www.youtube-nocookie.com/embed/${youtubeId}`}
/>; />
);
}; };
const twitterHosts = new Set(['twitter.com', 'www.twitter.com', 'x.com', 'www.x.com']); const twitterHosts = new Set(['twitter.com', 'www.twitter.com', 'x.com', 'www.x.com']);
const TwitterEmbed = ({parsedUrl}) => { const TwitterEmbed = ({ parsedUrl }) => {
return <iframe return (
className='enlarged twitter-embed' <iframe
height="100%" className='enlarged twitter-embed'
width="100%" height='100%'
frameborder="0" width='100%'
credentialless frameborder='0'
referrerpolicy='no-referrer' credentialless
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy='no-referrer'
title={parsedUrl.href} allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
srcdoc={` title={parsedUrl.href}
srcdoc={`
<blockquote class="twitter-tweet" data-theme="dark"> <blockquote class="twitter-tweet" data-theme="dark">
<a href="${parsedUrl.href.replace('x.com', 'twitter.com')}"></a> <a href="${parsedUrl.href.replace('x.com', 'twitter.com')}"></a>
</blockquote> </blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> <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 redditHosts = new Set(['reddit.com', 'www.reddit.com', 'old.reddit.com']);
const RedditEmbed = ({parsedUrl}) => { const RedditEmbed = ({ parsedUrl }) => {
return <iframe return (
className='enlarged reddit-embed' <iframe
height="100%" className='enlarged reddit-embed'
width="100%" height='100%'
frameborder="0" width='100%'
credentialless frameborder='0'
referrerpolicy='no-referrer' credentialless
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy='no-referrer'
title={parsedUrl.href} allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
srcdoc={` title={parsedUrl.href}
srcdoc={`
<style> <style>
/* fix reddit iframe being centered */ /* fix reddit iframe being centered */
iframe { iframe {
@@ -102,156 +106,177 @@ const RedditEmbed = ({parsedUrl}) => {
</blockquote> </blockquote>
<script async src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script> <script async src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script>
`} `}
/>; />
);
}; };
const twitchHosts = new Set(['twitch.tv', 'www.twitch.tv']); const twitchHosts = new Set(['twitch.tv', 'www.twitch.tv']);
const TwitchEmbed = ({parsedUrl}) => { const TwitchEmbed = ({ parsedUrl }) => {
let iframeUrl; let iframeUrl;
if (parsedUrl.pathname.startsWith('/videos/')) { if (parsedUrl.pathname.startsWith('/videos/')) {
const videoId = parsedUrl.pathname.replace('/videos/', ''); const videoId = parsedUrl.pathname.replace('/videos/', '');
iframeUrl = `https://player.twitch.tv/?video=${videoId}&parent=${window.location.hostname}`; iframeUrl = `https://player.twitch.tv/?video=${videoId}&parent=${window.location.hostname}`;
} } else {
else { const channel = parsedUrl.pathname.replaceAll('/', '');
const channel = parsedUrl.pathname.replaceAll('/', ''); iframeUrl = `https://player.twitch.tv/?channel=${channel}&parent=${window.location.hostname}`;
iframeUrl = `https://player.twitch.tv/?channel=${channel}&parent=${window.location.hostname}`; }
} return (
return <iframe <iframe
className='enlarged twitch-embed' className='enlarged twitch-embed'
height="100%" height='100%'
width="100%" width='100%'
frameborder="0" frameborder='0'
credentialless credentialless
referrerpolicy='no-referrer' referrerpolicy='no-referrer'
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share" allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
allowfullscreen allowfullscreen
title={parsedUrl.href} title={parsedUrl.href}
src={iframeUrl} src={iframeUrl}
/>; />
);
}; };
const tiktokHosts = new Set(['tiktok.com', 'www.tiktok.com']); const tiktokHosts = new Set(['tiktok.com', 'www.tiktok.com']);
const TiktokEmbed = ({parsedUrl}) => { const TiktokEmbed = ({ parsedUrl }) => {
const videoId = parsedUrl.pathname.replace(/.+\/video\//, '').replaceAll('/', ''); const videoId = parsedUrl.pathname.replace(/.+\/video\//, '').replaceAll('/', '');
return <iframe return (
className='enlarged tiktok-embed' <iframe
height="100%" className='enlarged tiktok-embed'
width="100%" height='100%'
frameborder="0" width='100%'
credentialless frameborder='0'
referrerpolicy='no-referrer' credentialless
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy='no-referrer'
title={parsedUrl.href} allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
srcdoc={` title={parsedUrl.href}
srcdoc={`
<blockquote class="tiktok-embed" data-video-id="${videoId}"> <blockquote class="tiktok-embed" data-video-id="${videoId}">
<a></a> <a></a>
</blockquote> </blockquote>
<script async src="https://www.tiktok.com/embed.js"></script> <script async src="https://www.tiktok.com/embed.js"></script>
`} `}
/>; />
);
}; };
const instagramHosts = new Set(['instagram.com', 'www.instagram.com']); const instagramHosts = new Set(['instagram.com', 'www.instagram.com']);
const InstagramEmbed = ({parsedUrl}) => { const InstagramEmbed = ({ parsedUrl }) => {
const pathNames = parsedUrl.pathname.replace(/\/+$/, '').split('/'); const pathNames = parsedUrl.pathname.replace(/\/+$/, '').split('/');
const id = pathNames[pathNames.length - 1]; const id = pathNames[pathNames.length - 1];
return <iframe return (
className='enlarged instagram-embed' <iframe
height="100%" className='enlarged instagram-embed'
width="100%" height='100%'
frameborder="0" width='100%'
credentialless frameborder='0'
referrerpolicy='no-referrer' credentialless
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy='no-referrer'
title={parsedUrl.href} allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
srcdoc={` title={parsedUrl.href}
srcdoc={`
<blockquote class="instagram-media"> <blockquote class="instagram-media">
<a href="https://www.instagram.com/p/${id}/"></a> <a href="https://www.instagram.com/p/${id}/"></a>
</blockquote> </blockquote>
<script async src="//www.instagram.com/embed.js"></script> <script async src="//www.instagram.com/embed.js"></script>
`} `}
/>; />
);
}; };
const odyseeHosts = new Set(['odysee.com', 'www.odysee.com']); const odyseeHosts = new Set(['odysee.com', 'www.odysee.com']);
const OdyseeEmbed = ({parsedUrl}) => { const OdyseeEmbed = ({ parsedUrl }) => {
const iframeUrl = `https://odysee.com/$/embed${parsedUrl.pathname}`; const iframeUrl = `https://odysee.com/$/embed${parsedUrl.pathname}`;
return <iframe return (
className='enlarged odysee-embed' <iframe
height="100%" className='enlarged odysee-embed'
width="100%" height='100%'
frameborder="0" width='100%'
credentialless frameborder='0'
referrerpolicy='no-referrer' credentialless
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy='no-referrer'
allowfullscreen allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
title={parsedUrl.href} allowfullscreen
src={iframeUrl} title={parsedUrl.href}
/>; src={iframeUrl}
/>
);
}; };
const bitchuteHosts = new Set(['bitchute.com', 'www.bitchute.com']); const bitchuteHosts = new Set(['bitchute.com', 'www.bitchute.com']);
const BitchuteEmbed = ({parsedUrl}) => { const BitchuteEmbed = ({ parsedUrl }) => {
const videoId = parsedUrl.pathname.replace(/\/video\//, '').replaceAll('/', ''); const videoId = parsedUrl.pathname.replace(/\/video\//, '').replaceAll('/', '');
return <iframe return (
className='enlarged bitchute-embed' <iframe
height="100%" className='enlarged bitchute-embed'
width="100%" height='100%'
frameborder="0" width='100%'
credentialless frameborder='0'
referrerpolicy='no-referrer' credentialless
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy='no-referrer'
allowfullscreen allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
title={parsedUrl.href} allowfullscreen
src={`https://www.bitchute.com/embed/${videoId}/`} title={parsedUrl.href}
/>; src={`https://www.bitchute.com/embed/${videoId}/`}
/>
);
}; };
const streamableHosts = new Set(['streamable.com', 'www.streamable.com']); const streamableHosts = new Set(['streamable.com', 'www.streamable.com']);
const StreamableEmbed = ({parsedUrl}) => { const StreamableEmbed = ({ parsedUrl }) => {
const videoId = parsedUrl.pathname.replaceAll('/', ''); const videoId = parsedUrl.pathname.replaceAll('/', '');
return <iframe return (
className='enlarged streamable-embed' <iframe
height="100%" className='enlarged streamable-embed'
width="100%" height='100%'
frameborder="0" width='100%'
credentialless frameborder='0'
referrerpolicy='no-referrer' credentialless
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy='no-referrer'
allowfullscreen allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
title={parsedUrl.href} allowfullscreen
src={`https://streamable.com/e/${videoId}`} title={parsedUrl.href}
/>; src={`https://streamable.com/e/${videoId}`}
/>
);
}; };
const spotifyHosts = new Set(['spotify.com', 'www.spotify.com', 'open.spotify.com']); const spotifyHosts = new Set(['spotify.com', 'www.spotify.com', 'open.spotify.com']);
const SpotifyEmbed = ({parsedUrl}) => { const SpotifyEmbed = ({ parsedUrl }) => {
const iframeUrl = `https://open.spotify.com/embed${parsedUrl.pathname}?theme=0` const iframeUrl = `https://open.spotify.com/embed${parsedUrl.pathname}?theme=0`;
return <iframe return (
className='enlarged spotify-embed' <iframe
height="100%" className='enlarged spotify-embed'
width="100%" height='100%'
frameborder="0" width='100%'
credentialless frameborder='0'
referrerpolicy='no-referrer' credentialless
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy='no-referrer'
allowfullscreen allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
title={parsedUrl.href} allowfullscreen
src={iframeUrl} title={parsedUrl.href}
/>; src={iframeUrl}
/>
);
}; };
const canEmbedHosts = new Set([ const canEmbedHosts = new Set([
...youtubeHosts, ...twitterHosts, ...redditHosts, ...twitchHosts, ...youtubeHosts,
...tiktokHosts, ...instagramHosts, ...odyseeHosts, ...bitchuteHosts, ...twitterHosts,
...streamableHosts, ...spotifyHosts ...redditHosts,
...twitchHosts,
...tiktokHosts,
...instagramHosts,
...odyseeHosts,
...bitchuteHosts,
...streamableHosts,
...spotifyHosts,
]); ]);
export const canEmbed = (parsedUrl) => canEmbedHosts.has(parsedUrl.host); export const canEmbed = (parsedUrl) => canEmbedHosts.has(parsedUrl.host);
+21 -27
View File
@@ -1,35 +1,29 @@
import React, { useEffect } from "react"; import React, { useEffect } from 'react';
import { Link } from "react-router-dom"; import { Link } from 'react-router-dom';
const ForwardRefLink = React.forwardRef((props, ref) => { 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) => { const handleClick = (event) => {
if (otherProps.to === "#void") { if (otherProps.to === '#void') {
event.preventDefault(); event.preventDefault();
} }
if (onClick) { if (onClick) {
onClick(event); onClick(event);
} }
}; };
useEffect(() => { useEffect(() => {
if (ref.current && typeof setRefAndCid === 'function') { if (ref.current && typeof setRefAndCid === 'function') {
setRefAndCid(ref.current); setRefAndCid(ref.current);
} }
}, [ref, setRefAndCid]); }, [ref, setRefAndCid]);
return ( return (
<Link <Link ref={ref} {...otherProps} onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} onClick={handleClick}>
ref={ref} {children}
{...otherProps} </Link>
onMouseOver={onMouseOver} );
onMouseLeave={onMouseLeave}
onClick={handleClick}
>
{children}
</Link>
);
}); });
export default ForwardRefLink; export default ForwardRefLink;
+23 -27
View File
@@ -2,44 +2,40 @@ import React, { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
const ImageBanner = () => { const ImageBanner = () => {
const [currentImage, setCurrentImage] = useState(null); const [currentImage, setCurrentImage] = useState(null);
const location = useLocation(); const location = useLocation();
const parentRoute = location.pathname.split('/').slice(0, 3).join('/'); const parentRoute = location.pathname.split('/').slice(0, 3).join('/');
useEffect(() => { useEffect(() => {
let isMounted = true; let isMounted = true;
const loadRandomImage = async () => { const loadRandomImage = async () => {
const images = await importAll(require.context('../../public/assets/banners', false, /\.(png|jpe?g|svg)$/)); const images = await importAll(require.context('../../public/assets/banners', false, /\.(png|jpe?g|svg)$/));
const randomImage = Math.floor(Math.random() * images.length) + 1; const randomImage = Math.floor(Math.random() * images.length) + 1;
const img = new Image(); const img = new Image();
img.src = `${process.env.PUBLIC_URL}/assets/banners/banner-${randomImage}.jpg`; img.src = `${process.env.PUBLIC_URL}/assets/banners/banner-${randomImage}.jpg`;
img.onload = () => { img.onload = () => {
if (isMounted) { if (isMounted) {
setCurrentImage(randomImage); setCurrentImage(randomImage);
} }
}; };
}; };
loadRandomImage(); loadRandomImage();
return () => { return () => {
isMounted = false; isMounted = false;
}; };
}, [parentRoute]); }, [parentRoute]);
return ( return <>{currentImage && <img id='banner-img' src={`${process.env.PUBLIC_URL}/assets/banners/banner-${currentImage}.jpg`} alt='banner' />}</>;
<>
{currentImage && <img id="banner-img" src={`${process.env.PUBLIC_URL}/assets/banners/banner-${currentImage}.jpg`} alt="banner" />}
</>
);
}; };
export function importAll(r) { export function importAll(r) {
return r.keys().map(r); return r.keys().map(r);
} }
export default ImageBanner; export default ImageBanner;
+22 -22
View File
@@ -1,28 +1,28 @@
import React from "react"; import React from 'react';
import { useSubplebbit } from "@plebbit/plebbit-react-hooks"; import { useSubplebbit } from '@plebbit/plebbit-react-hooks';
const OfflineIndicator = ({ address, className, tooltipPlace }) => { const OfflineIndicator = ({ address, className, tooltipPlace }) => {
const subplebbit = useSubplebbit({ subplebbitAddress: address }); const subplebbit = useSubplebbit({ subplebbitAddress: address });
const isOnline = subplebbit.updatedAt > Date.now() / 1000 - 60 * 20; const isOnline = subplebbit.updatedAt > Date.now() / 1000 - 60 * 20;
return ( return (
<> <>
{!isOnline && ( {!isOnline && (
<> <>
{" "} {' '}
<img <img
className={className} className={className}
alt="offline" alt='offline'
src="assets/offline.png" src='assets/offline.png'
data-tooltip-id="tooltip" data-tooltip-id='tooltip'
data-tooltip-content="Offline" data-tooltip-content='Offline'
data-tooltip-place={tooltipPlace} data-tooltip-place={tooltipPlace}
style={{imageRendering: 'pixelated'}} style={{ imageRendering: 'pixelated' }}
/> />
</> </>
)} )}
</> </>
); );
}; };
export default OfflineIndicator; export default OfflineIndicator;
+11 -21
View File
@@ -1,28 +1,18 @@
import React from "react"; import React from 'react';
import { useAccountComment } from "@plebbit/plebbit-react-hooks"; import { useAccountComment } from '@plebbit/plebbit-react-hooks';
const PendingLabel = ({ commentIndex }) => { const PendingLabel = ({ commentIndex }) => {
const comment = useAccountComment({commentIndex: commentIndex}); const comment = useAccountComment({ commentIndex: commentIndex });
if (commentIndex === undefined) return null; if (commentIndex === undefined) return null;
return ( return comment.cid ? (
comment.cid ? ( <span>{comment.cid.slice(2, 14)}</span>
<span>{comment.cid.slice(2, 14)}</span> ) : comment.state === 'pending' ? (
) : ( <span style={{ color: 'red', fontWeight: '700' }}>Pending</span>
comment.state === "pending" ? ( ) : comment.state === 'failed' ? (
<span style={{color: 'red', fontWeight: '700'}}> <span style={{ color: 'red', fontWeight: '700' }}>Failed</span>
Pending ) : null;
</span>
) : (
comment.state === "failed" ? (
<span style={{color: 'red', fontWeight: '700'}}>
Failed
</span>
) : null
)
)
);
}; };
export default PendingLabel; export default PendingLabel;
+109 -111
View File
@@ -4,134 +4,132 @@ import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
import breaks from 'remark-breaks'; import breaks from 'remark-breaks';
import ForwardRefLink from './ForwardRefLink'; import ForwardRefLink from './ForwardRefLink';
const Post = ({ content, postQuoteOnClick, postQuoteOnOver, postQuoteOnLeave, postQuoteRef }) => { 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(() => ({ const customSchema = useMemo(
...defaultSchema, () => ({
tagNames: [...defaultSchema.tagNames, 'div'], ...defaultSchema,
attributes: { tagNames: [...defaultSchema.tagNames, 'div'],
...defaultSchema.attributes, attributes: {
div: ['className'], ...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) => { const createQuotelink = (children, postQuoteOnClick, postQuoteOnOver, postQuoteOnLeave, postQuoteRef) => {
tree.children.forEach((node) => { const patternC = '(c/[A-Za-z0-9]{46}|c/[A-Za-z0-9]{12})';
if (node.type === 'blockquote') { const patternP = '(p/([A-Za-z0-9]{52}|[A-Za-z0-9-.]*\\.eth))';
node.children.forEach((child) => { const patternU = '(u/([A-Za-z0-9]{52}|[A-Za-z0-9-.]*\\.eth))';
if (child.type === 'paragraph' && child.children.length > 0) { const patternPC = '(p/([A-Za-z0-9]{52}|[A-Za-z0-9-.]*\\.eth)/c/[A-Za-z0-9]{46})';
const prefix = {
type: 'text',
value: '>',
};
child.children.unshift(prefix);
}
});
node.type = 'div';
node.data = {
hName: 'div',
hProperties: {
className: 'greentext',
},
};
}
});
};
const regex = new RegExp(`${patternC}|${patternPC}|${patternU}|${patternP}`, 'g');
const createQuotelink = (children, postQuoteOnClick, postQuoteOnOver, postQuoteOnLeave, postQuoteRef) => { return children?.flatMap((child, i) => {
const patternC = "(c/[A-Za-z0-9]{46}|c/[A-Za-z0-9]{12})"; if (typeof child !== 'string') {
const patternP = "(p/([A-Za-z0-9]{52}|[A-Za-z0-9-.]*\\.eth))"; return child;
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 parts = [];
let match;
let lastIndex = 0;
return children?.flatMap((child, i) => { while ((match = regex.exec(child)) !== null) {
if (typeof child !== 'string') { const matchedText = match[0];
return child; const index = match.index;
}
const parts = []; if (index > lastIndex) {
let match; parts.push(child.substring(lastIndex, index));
let lastIndex = 0; }
while ((match = regex.exec(child)) !== null) { const cid = matchedText.replace('c/', '');
const matchedText = match[0]; const linkRef = React.createRef();
const index = match.index;
if (index > lastIndex) { let linkTo = () => {};
parts.push(child.substring(lastIndex, index)); const linkTarget = matchedText.startsWith('u/') ? '_blank' : '_self';
}
const cid = matchedText.replace('c/', ''); if (matchedText.startsWith('u/')) {
const linkRef = React.createRef(); linkTo = '#void';
} else if (matchedText.startsWith('p/') || matchedText.startsWith('p/')) {
linkTo = `/${matchedText}`;
}
let linkTo = () => {}; parts.push(
const linkTarget = matchedText.startsWith('u/') ? "_blank" : "_self"; <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>,
);
if (matchedText.startsWith('u/')) { lastIndex = index + matchedText.length;
linkTo = "#void"; }
} else if (matchedText.startsWith('p/') || matchedText.startsWith('p/')) {
linkTo = `/${matchedText}`;
}
parts.push( if (lastIndex < child.length) {
<ForwardRefLink parts.push(child.substring(lastIndex));
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; return parts;
} });
};
if (lastIndex < child.length) { return (
parts.push(child.substring(lastIndex)); <ReactMarkdown
} children={doubleNewlineContent}
remarkPlugins={[blockquoteToGreentext, breaks]}
return parts; rehypePlugins={[[rehypeSanitize, customSchema]]}
}); components={{
}; img: ({ src }) => <span>{src}</span>,
video: ({ src }) => <span>{src}</span>,
source: ({ src }) => <span>{src}</span>,
gif: ({ src }) => <span>{src}</span>,
return ( p: ({ children }) => <div className='custom-paragraph'>{createQuotelink(children, postQuoteOnClick, postQuoteOnOver, postQuoteOnLeave, postQuoteRef)}</div>,
<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); export default React.memo(Post);
+27 -32
View File
@@ -3,42 +3,37 @@ import ContentLoader from 'react-content-loader';
import useGeneralStore from '../hooks/stores/useGeneralStore'; import useGeneralStore from '../hooks/stores/useGeneralStore';
const SinglePostLoader = () => { const SinglePostLoader = () => {
const selectedStyle = useGeneralStore((state) => state.selectedStyle); const selectedStyle = useGeneralStore((state) => state.selectedStyle);
const backgroundColor = selectedStyle === 'Tomorrow' ? '#333' : '#f3f3f3'; const backgroundColor = selectedStyle === 'Tomorrow' ? '#333' : '#f3f3f3';
const foregroundColor = selectedStyle === 'Tomorrow' ? '#555' : '#ecebeb'; const foregroundColor = selectedStyle === 'Tomorrow' ? '#555' : '#ecebeb';
return ( return (
<div style={{ paddingLeft: '30px', paddingRight: '30px', marginBottom: '50px', marginTop: '30px' }}> <div style={{ paddingLeft: '30px', paddingRight: '30px', marginBottom: '50px', marginTop: '30px' }}>
<ContentLoader <ContentLoader width='100%' height={15 * 3 + 30} backgroundColor={backgroundColor} foregroundColor={foregroundColor}>
width="100%" <rect x='0' y='8' width='100%' height='15' />
height={15 * 3 + 30} <rect x='0' y='30' width='100%' height='15' />
backgroundColor={backgroundColor} <rect x='0' y='52' width='100%' height='15' />
foregroundColor={foregroundColor} </ContentLoader>
> </div>
<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 = () => { const PostLoader = () => {
return ( return (
<div <div
style={{ style={{
display: 'block', display: 'block',
boxSizing: 'border-box', boxSizing: 'border-box',
paddingLeft: '30px', paddingLeft: '30px',
paddingRight: '30px', paddingRight: '30px',
marginBottom: '30px', marginBottom: '30px',
}} }}
> >
{[...Array(5)].map((_, index) => ( {[...Array(5)].map((_, index) => (
<SinglePostLoader key={index} /> <SinglePostLoader key={index} />
))} ))}
</div> </div>
); );
}; };
export default PostLoader; export default PostLoader;
+291 -316
View File
@@ -1,321 +1,296 @@
import React, { Fragment } from "react"; import React, { Fragment } from 'react';
import { Link } from "react-router-dom"; import { Link } from 'react-router-dom';
import { useAccount, useComment } from "@plebbit/plebbit-react-hooks"; import { useAccount, useComment } from '@plebbit/plebbit-react-hooks';
import getDate from "../utils/getDate"; import getDate from '../utils/getDate';
import getCommentMediaInfo from "../utils/getCommentMediaInfo"; import getCommentMediaInfo from '../utils/getCommentMediaInfo';
import findShortParentCid from "../utils/findShortParentCid"; import findShortParentCid from '../utils/findShortParentCid';
import Post from "./Post"; import Post from './Post';
import EditLabel from "./EditLabel"; import EditLabel from './EditLabel';
import useStateString from "../hooks/useStateString"; import useStateString from '../hooks/useStateString';
import { BoardForm, Container } from "./styled/views/Board.styled"; import { BoardForm, Container } from './styled/views/Board.styled';
import useGeneralStore from "../hooks/stores/useGeneralStore"; import useGeneralStore from '../hooks/stores/useGeneralStore';
const PostOnHover = ({ cid, feed }) => { const PostOnHover = ({ cid, feed }) => {
const selectedStyle = useGeneralStore(state => state.selectedStyle); const selectedStyle = useGeneralStore((state) => state.selectedStyle);
const account = useAccount(); const account = useAccount();
const reply = useComment({commentCid: cid}); const reply = useComment({ commentCid: cid });
const replyMediaInfo = getCommentMediaInfo(reply); const replyMediaInfo = getCommentMediaInfo(reply);
const fallbackImgUrl = "assets/filedeleted-res.gif"; const fallbackImgUrl = 'assets/filedeleted-res.gif';
const selectedFeed = feed; const selectedFeed = feed;
const thread = useComment({commentCid: reply.parentCid}); const thread = useComment({ commentCid: reply.parentCid });
const shortParentCid = findShortParentCid(reply.parentCid, selectedFeed); const shortParentCid = findShortParentCid(reply.parentCid, selectedFeed);
const stateString = useStateString(reply); const stateString = useStateString(reply);
return ( return (
<Container selectedStyle={selectedStyle} style={{ <Container
margin: '0', selectedStyle={selectedStyle}
padding: '0', style={{
whiteSpace: 'normal', margin: '0',
maxWidth: '100vw', padding: '0',
overflowWrap: 'break-word', whiteSpace: 'normal',
wordWrap: 'break-word', maxWidth: '100vw',
wordBreak: 'break-all', overflowWrap: 'break-word',
boxSizing: 'border-box', wordWrap: 'break-word',
}}> wordBreak: 'break-all',
<BoardForm selectedStyle={selectedStyle} style={{margin: '0', padding: '0'}}> boxSizing: 'border-box',
<div className="board" style={{margin: '0', padding: '0'}}> }}
<div className="thread" style={{margin: '0', padding: '0'}}> >
{reply.state === 'succeeded' ? ( <BoardForm selectedStyle={selectedStyle} style={{ margin: '0', padding: '0' }}>
<div className="reply-container"> <div className='board' style={{ margin: '0', padding: '0' }}>
<div className="post-reply post-reply-desktop"> <div className='thread' style={{ margin: '0', padding: '0' }}>
<div className="post-info"> {reply.state === 'succeeded' ? (
<span className="nameblock"> <div className='reply-container'>
{reply.author?.displayName <div className='post-reply post-reply-desktop'>
? reply.author?.displayName.length > 20 <div className='post-info'>
? <Fragment > <span className='nameblock'>
<span className="name" {reply.author?.displayName ? (
data-tooltip-id="tooltip" reply.author?.displayName.length > 20 ? (
data-tooltip-content={reply.author?.displayName} <Fragment>
data-tooltip-place="top"> <span className='name' data-tooltip-id='tooltip' data-tooltip-content={reply.author?.displayName} data-tooltip-place='top'>
{reply.author?.displayName.slice(0, 20) + " (...)"} {reply.author?.displayName.slice(0, 20) + ' (...)'}
</span> </span>
</Fragment> </Fragment>
: <span className="name"> ) : (
{reply.author?.displayName}</span> <span className='name'>{reply.author?.displayName}</span>
: <span className="name"> )
Anonymous</span>} ) : (
&nbsp; <span className='name'>Anonymous</span>
<span className="poster-address address-desktop" )}
id="reply-button" style={{cursor: "pointer"}} &nbsp;
> <span className='poster-address address-desktop' id='reply-button' style={{ cursor: 'pointer' }}>
(u/ (u/
{reply.author?.shortAddress ? {reply.author?.shortAddress ? <span>{reply.author?.shortAddress}</span> : <span>{account?.author?.shortAddress}</span>})
( </span>
<span > </span>
{reply.author?.shortAddress} &nbsp;
</span> <span className='date-time' data-utc='data'>
) : ( {getDate(reply.timestamp)}
<span > </span>
{account?.author?.shortAddress} &nbsp;
</span> <span className='post-number post-number-desktop'>
) <span>c/</span>
} <Link to={() => {}} id='reply-button' title='Reply to this post'>
) {reply.shortCid}
</span> </Link>
</span> </span>
&nbsp; &nbsp;
<span className="date-time" data-utc="data">{getDate(reply.timestamp)}</span> <div id='backlink-id' className='backlink'>
&nbsp; {reply.replies?.pages?.topAll.comments
<span className="post-number post-number-desktop"> .sort((a, b) => a.timestamp - b.timestamp)
<span >c/</span> .map((reply, index) => (
<Link to={() => {}} id="reply-button" <div key={`div-${index}`} style={{ display: 'inline-block' }}>
title="Reply to this post">{reply.shortCid}</Link> <Link key={`link-${index}`} to={() => {}} className='quote-link'>
</span>&nbsp; c/{reply.shortCid}
<div id="backlink-id" className="backlink"> </Link>
{reply.replies?.pages?.topAll.comments &nbsp;
.sort((a, b) => a.timestamp - b.timestamp) </div>
.map((reply, index) => ( ))}
<div key={`div-${index}`} style={{display: 'inline-block'}}> </div>
<Link key={`link-${index}`} to={() => {}} </div>
className="quote-link"> {replyMediaInfo?.url ? (
c/{reply.shortCid}</Link> <div className='file' style={{ marginBottom: '5px' }}>
&nbsp; <div className='reply-file-text'>
</div> Link:&nbsp;
)) <a href={replyMediaInfo.url} target='_blank' rel='noopener noreferrer'>
} {replyMediaInfo?.url.length > 30 ? replyMediaInfo?.url.slice(0, 30) + '(...)' : replyMediaInfo?.url}
</div> </a>
</div> &nbsp;({replyMediaInfo?.type})
{replyMediaInfo?.url ? ( </div>
<div className="file" {replyMediaInfo?.type === 'webpage' ? (
style={{marginBottom: "5px"}}> <div className='img-container'>
<div className="reply-file-text"> <span className='file-thumb-reply'>
Link:&nbsp; {reply.thumbnailUrl ? (
<a href={replyMediaInfo.url} target="_blank" <img
rel="noopener noreferrer">{ src={replyMediaInfo.thumbnail}
replyMediaInfo?.url.length > 30 ? alt={replyMediaInfo.type}
replyMediaInfo?.url.slice(0, 30) + "(...)" : style={{ cursor: 'pointer' }}
replyMediaInfo?.url onError={(e) => (e.target.src = fallbackImgUrl)}
}</a>&nbsp;({replyMediaInfo?.type}) />
</div> ) : null}
{replyMediaInfo?.type === "webpage" ? ( </span>
<div className="img-container"> </div>
<span className="file-thumb-reply"> ) : null}
{reply.thumbnailUrl ? ( {replyMediaInfo?.type === 'image' ? (
<img <div className='img-container'>
src={replyMediaInfo.thumbnail} alt={replyMediaInfo.type} <span className='file-thumb-reply'>
style={{cursor: "pointer"}} <img src={replyMediaInfo.url} alt={replyMediaInfo.type} style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
onError={(e) => e.target.src = fallbackImgUrl} /> </span>
) : null} </div>
</span> ) : null}
</div> {replyMediaInfo?.type === 'video' ? (
) : null} <span className='file-thumb-reply'>
{replyMediaInfo?.type === "image" ? ( <video controls src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
<div className="img-container"> </span>
<span className="file-thumb-reply"> ) : null}
<img {replyMediaInfo?.type === 'audio' ? (
src={replyMediaInfo.url} alt={replyMediaInfo.type} <span className='file-thumb-reply'>
style={{cursor: "pointer"}} <audio controls src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
onError={(e) => e.target.src = fallbackImgUrl} /> </span>
</span> ) : null}
</div> </div>
) : null} ) : null}
{replyMediaInfo?.type === "video" ? ( {reply.content ? (
<span className="file-thumb-reply"> reply.content?.length > 500 ? (
<video controls <Fragment>
<blockquote comment={reply} className='post-message'>
src={replyMediaInfo.url} alt={replyMediaInfo.type} {shortParentCid ? (
onError={(e) => e.target.src = fallbackImgUrl} /> <Link to={() => {}} className='quotelink'>
</span> {`c/${shortParentCid}`}
) : null} {shortParentCid === thread.shortCid ? ' (OP)' : null}
{replyMediaInfo?.type === "audio" ? ( </Link>
<span className="file-thumb-reply"> ) : null}
<audio controls <Post content={reply.content?.slice(0, 500)} />
<span className='ttl'>
src={replyMediaInfo.url} alt={replyMediaInfo.type} {' '}
onError={(e) => e.target.src = fallbackImgUrl} /> (...)
</span> <br /> <EditLabel commentCid={reply.cid} className='ttl' />
) : null} <br />
</div> Comment too long. Click the link to view.
) : null} </span>
{reply.content ? ( </blockquote>
reply.content?.length > 500 ? </Fragment>
<Fragment > ) : (
<blockquote comment={reply} className="post-message"> <blockquote className='post-message'>
{shortParentCid ? ( {shortParentCid ? (
<Link to={() => {}} className="quotelink"> <Link to={() => {}} className='quotelink'>
{`c/${shortParentCid}`}{shortParentCid === thread.shortCid ? " (OP)" : null} {`c/${shortParentCid}`}
</Link> {shortParentCid === thread.shortCid ? ' (OP)' : null}
) : null} </Link>
<Post content={reply.content?.slice(0, 500)} /> ) : null}
<span className="ttl"> (...) <Post content={reply.content} comment={reply} />
<br /> <EditLabel <EditLabel commentCid={reply.cid} className='ttl' />
commentCid={reply.cid} </blockquote>
className="ttl"/><br /> )
Comment too long. Click the link to view.</span> ) : null}
</blockquote> </div>
</Fragment> </div>
: <blockquote className="post-message"> ) : (
{shortParentCid ? ( <div className='reply-container'>
<Link to={() => {}} className="quotelink"> <span className='ellipsis'>{stateString}</span>
{`c/${shortParentCid}`}{shortParentCid === thread.shortCid ? " (OP)" : null} </div>
</Link> )}
) : null} </div>
<Post content={reply.content} comment={reply} /> <div className='thread-mobile'>
<EditLabel {reply.state === 'succeeded' ? (
commentCid={reply.cid} <div className='reply-container'>
className="ttl"/> <div className='post-reply post-reply-mobile'>
</blockquote>) <div className='post-info-mobile'>
: null} <span className='name-block-mobile'>
</div> {reply.author?.displayName ? (
</div> reply.author?.displayName.length > 20 ? (
) : ( <Fragment>
<div className="reply-container"> <span className='name-mobile'>{reply.author?.displayName.slice(0, 20) + ' (...)'}</span>
<span className="ellipsis">{stateString}</span> </Fragment>
</div> ) : (
)} <span className='name-mobile'>{reply.author?.displayName}</span>
</div> )
<div className="thread-mobile"> ) : (
{reply.state === 'succeeded' ? ( <span className='name-mobile'>Anonymous</span>
<div className="reply-container"> )}
<div className="post-reply post-reply-mobile"> &nbsp;
<div className="post-info-mobile"> <span className='poster-address-mobile address-mobile' id='reply-button' style={{ cursor: 'pointer' }}>
<span className="name-block-mobile"> (u/
{reply.author?.displayName {reply.author?.shortAddress ? (
? reply.author?.displayName.length > 20 <span className='highlight-address-mobile'>{reply.author?.shortAddress}</span>
? <Fragment> ) : (
<span className="name-mobile"> <span>{account?.author?.shortAddress}</span>
{reply.author?.displayName.slice(0, 20) + " (...)"} )}
</span> )&nbsp;
</Fragment> </span>
: <span className="name-mobile"> <br />
{reply.author?.displayName}</span> </span>
: <span className="name-mobile"> <span className='date-time-mobile post-number-mobile'>
Anonymous</span>} {getDate(reply.timestamp)}&nbsp;
&nbsp; <span>c/</span>
<span className="poster-address-mobile address-mobile" <Link to={() => {}} id='reply-button'>
id="reply-button" style={{cursor: "pointer"}} {reply.shortCid}
> </Link>
(u/ </span>
{reply.author?.shortAddress ? </div>
( {reply.link ? (
<span className="highlight-address-mobile"> <div className='file-mobile'>
{reply.author?.shortAddress} {replyMediaInfo?.url ? (
</span> replyMediaInfo.type === 'webpage' ? (
) : ( <div className='img-container'>
<span> <span className='file-thumb-mobile'>
{account?.author?.shortAddress} {reply.thumbnailUrl ? (
</span> <img src={replyMediaInfo.thumbnail} alt='thumbnail' style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
) ) : null}
} <div className='file-info-mobile'>{replyMediaInfo.type}</div>
)&nbsp; </span>
</span> </div>
<br /> ) : replyMediaInfo.type === 'image' ? (
</span> <div className='img-container'>
<span className="date-time-mobile post-number-mobile"> <span className='file-thumb-mobile'>
{getDate(reply.timestamp)}&nbsp; <img src={replyMediaInfo.url} alt={replyMediaInfo.type} style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
<span>c/</span> <div className='file-info-mobile'>{replyMediaInfo.type}</div>
<Link to={() => {}} id="reply-button" </span>
>{reply.shortCid} </div>
</Link> ) : replyMediaInfo.type === 'video' ? (
</span> <span className='file-thumb-mobile'>
</div> <video
{reply.link ? ( src={replyMediaInfo.url}
<div className="file-mobile"> alt={replyMediaInfo.type}
{replyMediaInfo?.url ? ( style={{ pointerEvents: 'none' }}
replyMediaInfo.type === "webpage" ? ( onError={(e) => (e.target.src = fallbackImgUrl)}
<div className="img-container"> />
<span className="file-thumb-mobile"> <div className='file-info-mobile'>{replyMediaInfo.type}</div>
{reply.thumbnailUrl ? ( </span>
<img ) : replyMediaInfo.type === 'audio' ? (
src={replyMediaInfo.thumbnail} alt="thumbnail" <span className='file-thumb-mobile'>
style={{cursor: "pointer"}} <audio src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
onError={(e) => e.target.src = fallbackImgUrl} /> <div className='file-info-mobile'>{replyMediaInfo.type}</div>
) : null} </span>
<div className="file-info-mobile">{replyMediaInfo.type}</div> ) : null
</span> ) : null}
</div> </div>
) : replyMediaInfo.type === "image" ? ( ) : null}
<div className="img-container"> {reply.content ? (
<span className="file-thumb-mobile"> reply.content?.length > 500 ? (
<img <Fragment>
src={replyMediaInfo.url} alt={replyMediaInfo.type} <blockquote className='post-message'>
style={{cursor: "pointer"}} {shortParentCid ? (
onError={(e) => e.target.src = fallbackImgUrl} /> <Link to={() => {}} className='quotelink'>
<div className="file-info-mobile">{replyMediaInfo.type}</div> {`c/${shortParentCid}`}
</span> {shortParentCid === thread.shortCid ? ' (OP)' : null}
</div> </Link>
) : replyMediaInfo.type === "video" ? ( ) : null}
<span className="file-thumb-mobile"> <Post content={reply.content?.slice(0, 500)} comment={reply} />
<video <span className='ttl'>
src={replyMediaInfo.url} alt={replyMediaInfo.type} {' '}
style={{ pointerEvents: "none" }} (...)
onError={(e) => e.target.src = fallbackImgUrl} /> <br />
<div className="file-info-mobile">{replyMediaInfo.type}</div> <EditLabel commentCid={reply.cid} className='ttl' />
</span> <br />
) : replyMediaInfo.type === "audio" ? ( Comment too long. Click the link to view.{' '}
<span className="file-thumb-mobile"> </span>
<audio </blockquote>
src={replyMediaInfo.url} alt={replyMediaInfo.type} </Fragment>
onError={(e) => e.target.src = fallbackImgUrl} /> ) : (
<div className="file-info-mobile">{replyMediaInfo.type}</div> <blockquote className='post-message'>
</span> {shortParentCid ? (
) : null <Link to={() => {}} className='quotelink'>
) : null} {`c/${shortParentCid}`}
</div> {shortParentCid === thread.shortCid ? ' (OP)' : null}
) : null} </Link>
{reply.content ? ( ) : null}
reply.content?.length > 500 ? <Post content={reply.content} comment={reply} />
<Fragment> <EditLabel commentCid={reply.cid} className='ttl' />
<blockquote className="post-message"> </blockquote>
{shortParentCid ? ( )
<Link to={() => {}} className="quotelink"> ) : null}
{`c/${shortParentCid}`}{shortParentCid === thread.shortCid ? " (OP)" : null} </div>
</Link> </div>
) : null} ) : (
<Post content={reply.content?.slice(0, 500)} comment={reply} /> <div className='reply-container'>
<span className="ttl"> (...) <span className='ellipsis'>{stateString}</span>
<br /> </div>
<EditLabel )}
commentCid={reply.cid} </div>
className="ttl"/> </div>
<br /> </BoardForm>
Comment too long. Click the link to view. </span> </Container>
</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; export default PostOnHover;
+19 -24
View File
@@ -1,33 +1,28 @@
import React from "react"; import React from 'react';
import { useAccountComment } from "@plebbit/plebbit-react-hooks"; import { useAccountComment } from '@plebbit/plebbit-react-hooks';
import useStateString from "../hooks/useStateString"; import useStateString from '../hooks/useStateString';
const StateLabel = ({ commentIndex, className }) => { const StateLabel = ({ commentIndex, className }) => {
const comment = useAccountComment({commentIndex: commentIndex}); const comment = useAccountComment({ commentIndex: commentIndex });
const stateString = useStateString(comment); const stateString = useStateString(comment);
if (comment.updatedAt !== undefined || comment.index === undefined) { if (comment.updatedAt !== undefined || comment.index === undefined) {
return null; return null;
} }
if (comment.state === "failed" || comment.state === "succeeded") { if (comment.state === 'failed' || comment.state === 'succeeded') {
return null; return null;
} }
if (!stateString) { if (!stateString) {
return null; return null;
} }
return ( return (
<span className="ttl"> <span className='ttl'>
<br /> <br />(<span className={className}>{stateString}</span>)
( </span>
<span className={className}> );
{stateString}
</span>
)
</span>
);
}; };
export default StateLabel; export default StateLabel;
+4 -4
View File
@@ -1,11 +1,11 @@
import React from 'react'; import React from 'react';
import {useComment, useAuthorAddress} from '@plebbit/plebbit-react-hooks'; import { useComment, useAuthorAddress } from '@plebbit/plebbit-react-hooks';
function VerifiedAuthor({ commentCid, children }) { function VerifiedAuthor({ commentCid, children }) {
const comment = useComment({commentCid}); const comment = useComment({ commentCid });
const {authorAddress, shortAuthorAddress} = useAuthorAddress({comment}); const { authorAddress, shortAuthorAddress } = useAuthorAddress({ comment });
return children({ authorAddress, shortAuthorAddress }); return children({ authorAddress, shortAuthorAddress });
} }
export default React.memo(VerifiedAuthor); export default React.memo(VerifiedAuthor);
+21 -22
View File
@@ -1,30 +1,29 @@
import { useEffect } from "react"; import { useEffect } from 'react';
import { useAccount } from "@plebbit/plebbit-react-hooks"; import { useAccount } from '@plebbit/plebbit-react-hooks';
import useAnonModeStore from "./stores/useAnonModeStore"; import useAnonModeStore from './stores/useAnonModeStore';
const useAnonMode = (threadCid, execute) => { const useAnonMode = (threadCid, execute) => {
const account = useAccount(); const account = useAccount();
const { anonymousMode } = useAnonModeStore(); const { anonymousMode } = useAnonModeStore();
useEffect(() => { useEffect(() => {
const handleAnonMode = async () => { const handleAnonMode = async () => {
let storedSigners = JSON.parse(localStorage.getItem('storedSigners')) || {}; let storedSigners = JSON.parse(localStorage.getItem('storedSigners')) || {};
if (!anonymousMode) { if (!anonymousMode) {
if (execute && storedSigners[threadCid]) { if (execute && storedSigners[threadCid]) {
const signerPrivateKey = storedSigners[threadCid]; const signerPrivateKey = storedSigners[threadCid];
if (account) { if (account) {
await account.plebbit.createSigner({type: 'ed25519', privateKey: signerPrivateKey}); await account.plebbit.createSigner({ type: 'ed25519', privateKey: signerPrivateKey });
} }
} }
} }
} };
handleAnonMode(); handleAnonMode();
}, [threadCid, execute, account, anonymousMode]); }, [threadCid, execute, account, anonymousMode]);
return;
return; };
}
export default useAnonMode; export default useAnonMode;
+21 -21
View File
@@ -1,28 +1,28 @@
import { useEffect } from "react"; import { useEffect } from 'react';
import { useAccount } from "@plebbit/plebbit-react-hooks"; import { useAccount } from '@plebbit/plebbit-react-hooks';
import useAnonModeStore from "./stores/useAnonModeStore"; import useAnonModeStore from './stores/useAnonModeStore';
const useAnonModeRef = (threadCidRef, execute) => { const useAnonModeRef = (threadCidRef, execute) => {
const account = useAccount(); const account = useAccount();
const { anonymousMode } = useAnonModeStore(); const { anonymousMode } = useAnonModeStore();
useEffect(() => { useEffect(() => {
const handleAnonMode = async () => { const handleAnonMode = async () => {
let storedSigners = JSON.parse(localStorage.getItem('storedSigners')) || {}; let storedSigners = JSON.parse(localStorage.getItem('storedSigners')) || {};
if (!anonymousMode) { if (!anonymousMode) {
if (execute && storedSigners[threadCidRef]) { if (execute && storedSigners[threadCidRef]) {
const signerPrivateKey = storedSigners[threadCidRef]; const signerPrivateKey = storedSigners[threadCidRef];
if (account) { if (account) {
await account.plebbit.createSigner({type: 'ed25519', privateKey: signerPrivateKey}); await account.plebbit.createSigner({ type: 'ed25519', privateKey: signerPrivateKey });
} }
} }
} }
} };
handleAnonMode(); handleAnonMode();
}, [threadCidRef, execute, account, anonymousMode]); }, [threadCidRef, execute, account, anonymousMode]);
return; return;
} };
export default useAnonModeRef; export default useAnonModeRef;
+7 -7
View File
@@ -1,14 +1,14 @@
import useGeneralStore from "./stores/useGeneralStore"; import useGeneralStore from './stores/useGeneralStore';
const useClickForm = () => { const useClickForm = () => {
const { setShowPostForm, setShowPostFormLink } = useGeneralStore.getState(); const { setShowPostForm, setShowPostFormLink } = useGeneralStore.getState();
const handleClickForm = () => { const handleClickForm = () => {
setShowPostForm(true); setShowPostForm(true);
setShowPostFormLink(false); setShowPostFormLink(false);
}; };
return handleClickForm; return handleClickForm;
}; };
export default useClickForm; export default useClickForm;
+40 -41
View File
@@ -1,53 +1,52 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import { toast } from "react-toastify"; import { toast } from 'react-toastify';
const useError = () => { const useError = () => {
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
const [renderCount, setRenderCount] = useState(0); const [renderCount, setRenderCount] = useState(0);
useEffect(() => { useEffect(() => {
if (errorMessage && errorMessage.length > 0) { if (errorMessage && errorMessage.length > 0) {
const showErrorToast = () => { const showErrorToast = () => {
const toastId = toast.error(errorMessage.toString(), { const toastId = toast.error(errorMessage.toString(), {
position: "top-right", position: 'top-right',
autoClose: false, autoClose: false,
hideProgressBar: true, hideProgressBar: true,
closeOnClick: false, closeOnClick: false,
pauseOnHover: false, pauseOnHover: false,
draggable: false, draggable: false,
progress: undefined, progress: undefined,
theme: "dark", theme: 'dark',
}); });
return () => { return () => {
toast.dismiss(toastId); toast.dismiss(toastId);
}; };
}; };
const timeoutId = setTimeout(showErrorToast, 500); const timeoutId = setTimeout(showErrorToast, 500);
return () => { return () => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
}; };
} }
}, [errorMessage, renderCount]); }, [errorMessage, renderCount]);
const setNewErrorMessage = (error) => { const setNewErrorMessage = (error) => {
let message; let message;
if (typeof error === 'string') { if (typeof error === 'string') {
message = error; message = error;
} else if (error instanceof Error) { } else if (error instanceof Error) {
message = error.message; message = error.message;
} else { } else {
message = JSON.stringify(error); message = JSON.stringify(error);
} }
setErrorMessage(message); setErrorMessage(message);
setRenderCount(prevCount => prevCount + 1); setRenderCount((prevCount) => prevCount + 1);
}; };
return [errorMessage, setNewErrorMessage];
return [errorMessage, setNewErrorMessage];
}; };
export default useError; export default useError;
+15 -16
View File
@@ -1,20 +1,19 @@
import { useMemo, useRef } from "react" import { useMemo, useRef } from 'react';
const useFeedRows = (feedWithDescriptionAndRules, columnCount) => { const useFeedRows = (feedWithDescriptionAndRules, columnCount) => {
const rowsRef = useRef([]); const rowsRef = useRef([]);
return useMemo(() => { return useMemo(() => {
const rows = []; const rows = [];
for (let i = 0; i < feedWithDescriptionAndRules.length; i += columnCount) { for (let i = 0; i < feedWithDescriptionAndRules.length; i += columnCount) {
if (rowsRef.current?.[rows.length] && rowsRef.current[rows.length].length === columnCount) { if (rowsRef.current?.[rows.length] && rowsRef.current[rows.length].length === columnCount) {
rows.push(rowsRef.current[rows.length]) rows.push(rowsRef.current[rows.length]);
} } else {
else { rows.push(feedWithDescriptionAndRules.slice(i, i + columnCount));
rows.push(feedWithDescriptionAndRules.slice(i, i + columnCount)) }
} }
} rowsRef.current = rows;
rowsRef.current = rows return rows;
return rows; }, [feedWithDescriptionAndRules, columnCount]);
}, [feedWithDescriptionAndRules, columnCount]); };
}
export default useFeedRows; export default useFeedRows;
+87 -88
View File
@@ -1,96 +1,95 @@
import { useMemo } from "react" import { useMemo } from 'react';
const useFeedStateString = (subplebbits) => { const useFeedStateString = (subplebbits) => {
return useMemo(() => { return useMemo(() => {
const getClientHost = (clientUrl) => { const getClientHost = (clientUrl) => {
try { try {
clientUrl = new URL(clientUrl).hostname || clientUrl clientUrl = new URL(clientUrl).hostname || clientUrl;
} } catch (e) {}
catch (e) {} return clientUrl;
return clientUrl };
} const getClientUrls = (regex) => {
const getClientUrls = (regex) => { const clientUrls = new Set();
const clientUrls = new Set() const addClientUrl = (client, clientUrl) => client?.state?.match?.(regex) && clientUrls.add(getClientHost(clientUrl));
const addClientUrl = (client, clientUrl) => client?.state?.match?.(regex) && clientUrls.add(getClientHost(clientUrl)) for (const subplebbit of subplebbits) {
for (const subplebbit of subplebbits) { for (const clientUrl in subplebbit?.clients?.ipfsGateways) {
for (const clientUrl in subplebbit?.clients?.ipfsGateways) { addClientUrl(subplebbit.clients.ipfsGateways[clientUrl], clientUrl);
addClientUrl(subplebbit.clients.ipfsGateways[clientUrl], clientUrl) }
} for (const clientUrl in subplebbit?.clients?.ipfsClients) {
for (const clientUrl in subplebbit?.clients?.ipfsClients) { addClientUrl(subplebbit.clients.ipfsClients[clientUrl], clientUrl);
addClientUrl(subplebbit.clients.ipfsClients[clientUrl], clientUrl) }
} for (const chainTicker in subplebbit?.clients?.chainProviders) {
for (const chainTicker in subplebbit?.clients?.chainProviders) { for (const clientUrl in subplebbit.clients.chainProviders[chainTicker]) {
for (const clientUrl in subplebbit.clients.chainProviders[chainTicker]) { addClientUrl(subplebbit.clients.chainProviders[chainTicker][clientUrl], clientUrl);
addClientUrl(subplebbit.clients.chainProviders[chainTicker][clientUrl], clientUrl) }
} }
} // find subplebbit pages states
// find subplebbit pages states if (subplebbit?.posts?.clients) {
if (subplebbit?.posts?.clients) { for (const clientType in subplebbit.posts.clients) {
for (const clientType in subplebbit.posts.clients) { for (const sortType in subplebbit.posts.clients[clientType]) {
for (const sortType in subplebbit.posts.clients[clientType]) { for (const clientUrl in subplebbit.posts.clients[clientType][sortType]) {
for (const clientUrl in subplebbit.posts.clients[clientType][sortType]) { addClientUrl(subplebbit.posts.clients[clientType][sortType][clientUrl], clientUrl);
addClientUrl(subplebbit.posts.clients[clientType][sortType][clientUrl], clientUrl) }
} }
} }
} }
} }
} return [...clientUrls];
return [...clientUrls] };
}
if (!subplebbits) { if (!subplebbits) {
return return;
} }
const states = {} const states = {};
for (const subplebbit of subplebbits) { for (const subplebbit of subplebbits) {
states[subplebbit?.updatingState] = (states[subplebbit?.updatingState] || 0) + 1 states[subplebbit?.updatingState] = (states[subplebbit?.updatingState] || 0) + 1;
// find subplebbit pages states // find subplebbit pages states
for (const clientType in subplebbit?.posts?.clients) { for (const clientType in subplebbit?.posts?.clients) {
for (const sortType in subplebbit.posts.clients[clientType]) { for (const sortType in subplebbit.posts.clients[clientType]) {
for (const clientUrl in subplebbit.posts.clients[clientType][sortType]) { for (const clientUrl in subplebbit.posts.clients[clientType][sortType]) {
const state = subplebbit.posts.clients[clientType][sortType][clientUrl].state const state = subplebbit.posts.clients[clientType][sortType][clientUrl].state;
states[state] = (states[state] || 0) + 1 states[state] = (states[state] || 0) + 1;
} }
} }
} }
} }
// e.g. Resolving 2 addresses from infura.io, fetching 2 IPNS, 1 IPFS from cloudflare-ipfs.com, ipfs.io // e.g. Resolving 2 addresses from infura.io, fetching 2 IPNS, 1 IPFS from cloudflare-ipfs.com, ipfs.io
let stateString = '' let stateString = '';
if (states['resolving-address']) { if (states['resolving-address']) {
stateString += `resolving ${states['resolving-address']} addresses` stateString += `resolving ${states['resolving-address']} addresses`;
const clientUrls = getClientUrls(/address/) const clientUrls = getClientUrls(/address/);
if (clientUrls.length) { if (clientUrls.length) {
stateString += ` from ${clientUrls.join(', ')}` stateString += ` from ${clientUrls.join(', ')}`;
} }
} }
if (states['fetching-ipns'] || states['fetching-ipfs']) { if (states['fetching-ipns'] || states['fetching-ipfs']) {
if (stateString) { if (stateString) {
stateString += ', ' stateString += ', ';
} }
stateString += `fetching ` stateString += `fetching `;
if (states['fetching-ipns']) { if (states['fetching-ipns']) {
stateString += `${states['fetching-ipns']} IPNS` stateString += `${states['fetching-ipns']} IPNS`;
} }
if (states['fetching-ipfs']) { if (states['fetching-ipfs']) {
if (states['fetching-ipns']) { if (states['fetching-ipns']) {
stateString += ', ' stateString += ', ';
} }
stateString += `${states['fetching-ipfs']} IPFS` stateString += `${states['fetching-ipfs']} IPFS`;
} }
const clientUrls = getClientUrls(/ipfs|ipns/) const clientUrls = getClientUrls(/ipfs|ipns/);
if (clientUrls.length) { if (clientUrls.length) {
stateString += ` from ${clientUrls.join(', ')}` stateString += ` from ${clientUrls.join(', ')}`;
} }
} }
// capitalize first letter // capitalize first letter
stateString = stateString.charAt(0).toUpperCase() + stateString.slice(1) stateString = stateString.charAt(0).toUpperCase() + stateString.slice(1);
// if string is empty, return undefined instead // if string is empty, return undefined instead
return stateString || undefined return stateString || undefined;
}, [subplebbits]) }, [subplebbits]);
} };
export default useFeedStateString export default useFeedStateString;
+32 -32
View File
@@ -1,43 +1,43 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import { toast } from "react-toastify"; import { toast } from 'react-toastify';
const useInfo = () => { const useInfo = () => {
const [infoMessage, setInfoMessage] = useState(''); const [infoMessage, setInfoMessage] = useState('');
const [renderCount, setRenderCount] = useState(0); const [renderCount, setRenderCount] = useState(0);
useEffect(() => { useEffect(() => {
if (infoMessage && infoMessage.length > 0) { if (infoMessage && infoMessage.length > 0) {
const showInfoToast = () => { const showInfoToast = () => {
const toastId = toast.info(infoMessage.toString(), { const toastId = toast.info(infoMessage.toString(), {
position: "top-right", position: 'top-right',
autoClose: false, autoClose: false,
hideProgressBar: false, hideProgressBar: false,
closeOnClick: false, closeOnClick: false,
pauseOnHover: false, pauseOnHover: false,
draggable: false, draggable: false,
progress: undefined, progress: undefined,
theme: "dark", theme: 'dark',
}); });
return () => { return () => {
toast.dismiss(toastId); toast.dismiss(toastId);
}; };
}; };
const timeoutId = setTimeout(showInfoToast, 500); const timeoutId = setTimeout(showInfoToast, 500);
return () => { return () => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
}; };
} }
}, [infoMessage, renderCount]); }, [infoMessage, renderCount]);
const setNewInfoMessage = (message) => { const setNewInfoMessage = (message) => {
setInfoMessage(message); setInfoMessage(message);
setRenderCount(prevCount => prevCount + 1); setRenderCount((prevCount) => prevCount + 1);
}; };
return [infoMessage, setNewInfoMessage]; return [infoMessage, setNewInfoMessage];
}; };
export default useInfo; export default useInfo;
+94 -96
View File
@@ -1,110 +1,108 @@
import { useMemo } from 'react' import { useMemo } from 'react';
const useStateString = (commentOrSubplebbit) => { const useStateString = (commentOrSubplebbit) => {
return useMemo(() => { return useMemo(() => {
// dont show state string if the data is already fetched // dont show state string if the data is already fetched
if (commentOrSubplebbit?.updatedAt || commentOrSubplebbit?.state === 'succeeded') { if (commentOrSubplebbit?.updatedAt || commentOrSubplebbit?.state === 'succeeded') {
return return;
} }
if (!commentOrSubplebbit?.clients) { if (!commentOrSubplebbit?.clients) {
return return;
} }
const clients = commentOrSubplebbit?.clients const clients = commentOrSubplebbit?.clients;
const states = {} const states = {};
const addState = (state, clientUrl) => { const addState = (state, clientUrl) => {
if (!state || state === 'stopped') { if (!state || state === 'stopped') {
return return;
} }
if (!states[state]) { if (!states[state]) {
states[state] = [] states[state] = [];
} }
states[state].push(clientUrl) states[state].push(clientUrl);
} };
for (const clientUrl in clients?.ipfsGateways) { for (const clientUrl in clients?.ipfsGateways) {
addState(clients.ipfsGateways[clientUrl]?.state, clientUrl) addState(clients.ipfsGateways[clientUrl]?.state, clientUrl);
} }
for (const clientUrl in clients?.ipfsClients) { for (const clientUrl in clients?.ipfsClients) {
addState(clients.ipfsClients[clientUrl]?.state, clientUrl) addState(clients.ipfsClients[clientUrl]?.state, clientUrl);
} }
for (const clientUrl in clients?.pubsubClients) { for (const clientUrl in clients?.pubsubClients) {
addState(clients.pubsubClients[clientUrl]?.state, clientUrl) addState(clients.pubsubClients[clientUrl]?.state, clientUrl);
} }
for (const chainTicker in clients?.chainProviders) { for (const chainTicker in clients?.chainProviders) {
for (const clientUrl in clients.chainProviders[chainTicker]) { for (const clientUrl in clients.chainProviders[chainTicker]) {
addState(clients.chainProviders[chainTicker][clientUrl]?.state, clientUrl) addState(clients.chainProviders[chainTicker][clientUrl]?.state, clientUrl);
} }
} }
// find subplebbit pages states // find subplebbit pages states
if (commentOrSubplebbit?.posts?.clients) { if (commentOrSubplebbit?.posts?.clients) {
for (const clientType in commentOrSubplebbit.posts.clients) { for (const clientType in commentOrSubplebbit.posts.clients) {
for (const sortType in commentOrSubplebbit.posts.clients[clientType]) { for (const sortType in commentOrSubplebbit.posts.clients[clientType]) {
for (const clientUrl in commentOrSubplebbit.posts.clients[clientType][sortType]) { for (const clientUrl in commentOrSubplebbit.posts.clients[clientType][sortType]) {
let state = commentOrSubplebbit.posts.clients[clientType][sortType][clientUrl].state let state = commentOrSubplebbit.posts.clients[clientType][sortType][clientUrl].state;
if (state === 'stopped') { if (state === 'stopped') {
continue continue;
} }
state += `-page-${sortType}` state += `-page-${sortType}`;
if (!states[state]) { if (!states[state]) {
states[state] = [] states[state] = [];
} }
states[state].push(clientUrl) states[state].push(clientUrl);
} }
} }
} }
} }
const getClientHost = (clientUrl) => { const getClientHost = (clientUrl) => {
try { try {
clientUrl = new URL(clientUrl).hostname || clientUrl clientUrl = new URL(clientUrl).hostname || clientUrl;
} } catch (e) {}
catch (e) {} return clientUrl;
return clientUrl };
}
let stateString = '' let stateString = '';
for (const state in states) { for (const state in states) {
const clientUrls = states[state] const clientUrls = states[state];
const clientHosts = clientUrls.map(clientUrl => getClientHost(clientUrl)) const clientHosts = clientUrls.map((clientUrl) => getClientHost(clientUrl));
// if there are no valid hosts, skip this state // if there are no valid hosts, skip this state
if (clientHosts.length === 0) { if (clientHosts.length === 0) {
continue continue;
} }
// separate 2 different states using ', ' // separate 2 different states using ', '
if (stateString) { if (stateString) {
stateString += ', ' stateString += ', ';
} }
// e.g. 'fetching IPFS from cloudflare-ipfs.com, ipfs.io' // e.g. 'fetching IPFS from cloudflare-ipfs.com, ipfs.io'
const formattedState = state.replaceAll('-', ' ').replace('ipfs', 'IPFS').replace('ipns', 'IPNS') const formattedState = state.replaceAll('-', ' ').replace('ipfs', 'IPFS').replace('ipns', 'IPNS');
stateString += `${formattedState} from ${clientHosts.join(', ')}` stateString += `${formattedState} from ${clientHosts.join(', ')}`;
} }
// fallback to comment or subplebbit state when possible // fallback to comment or subplebbit state when possible
if (!stateString) { if (!stateString) {
if (commentOrSubplebbit?.publishingState && commentOrSubplebbit?.publishingState !== 'stopped' && commentOrSubplebbit?.publishingState !== 'succeeded') { if (commentOrSubplebbit?.publishingState && commentOrSubplebbit?.publishingState !== 'stopped' && commentOrSubplebbit?.publishingState !== 'succeeded') {
stateString = commentOrSubplebbit.publishingState stateString = commentOrSubplebbit.publishingState;
} } else if (commentOrSubplebbit?.updatingState !== 'stopped' && commentOrSubplebbit?.updatingState !== 'succeeded') {
else if (commentOrSubplebbit?.updatingState !== 'stopped' && commentOrSubplebbit?.updatingState !== 'succeeded') { stateString = commentOrSubplebbit.updatingState;
stateString = commentOrSubplebbit.updatingState }
} if (stateString) {
if (stateString) { stateString = stateString.replaceAll('-', ' ').replace('ipfs', 'IPFS').replace('ipns', 'IPNS');
stateString = stateString.replaceAll('-', ' ').replace('ipfs', 'IPFS').replace('ipns', 'IPNS') }
} }
}
// capitalize first letter // capitalize first letter
if (stateString) { if (stateString) {
stateString = stateString.charAt(0).toUpperCase() + stateString.slice(1) stateString = stateString.charAt(0).toUpperCase() + stateString.slice(1);
} }
// if string is empty, return undefined instead // if string is empty, return undefined instead
return stateString === '' ? undefined : stateString return stateString === '' ? undefined : stateString;
}, [commentOrSubplebbit]) }, [commentOrSubplebbit]);
} };
export default useStateString export default useStateString;
+32 -32
View File
@@ -1,43 +1,43 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import { toast } from "react-toastify"; import { toast } from 'react-toastify';
const useSuccess = () => { const useSuccess = () => {
const [successMessage, setSuccessMessage] = useState(''); const [successMessage, setSuccessMessage] = useState('');
const [renderCount, setRenderCount] = useState(0); const [renderCount, setRenderCount] = useState(0);
useEffect(() => { useEffect(() => {
if (successMessage && successMessage.length > 0) { if (successMessage && successMessage.length > 0) {
const showSuccessToast = () => { const showSuccessToast = () => {
const toastId = toast.success(successMessage.toString(), { const toastId = toast.success(successMessage.toString(), {
position: "top-right", position: 'top-right',
autoClose: 3000, autoClose: 3000,
hideProgressBar: false, hideProgressBar: false,
closeOnClick: false, closeOnClick: false,
pauseOnHover: false, pauseOnHover: false,
draggable: false, draggable: false,
progress: undefined, progress: undefined,
theme: "dark", theme: 'dark',
}); });
return () => { return () => {
toast.dismiss(toastId); toast.dismiss(toastId);
}; };
}; };
const timeoutId = setTimeout(showSuccessToast, 500); const timeoutId = setTimeout(showSuccessToast, 500);
return () => { return () => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
}; };
} }
}, [successMessage, renderCount]); }, [successMessage, renderCount]);
const setNewSuccessMessage = (message) => { const setNewSuccessMessage = (message) => {
setSuccessMessage(message); setSuccessMessage(message);
setRenderCount(prevCount => prevCount + 1); setRenderCount((prevCount) => prevCount + 1);
}; };
return [successMessage, setNewSuccessMessage]; return [successMessage, setNewSuccessMessage];
}; };
export default useSuccess; export default useSuccess;
+10 -10
View File
@@ -1,16 +1,16 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react';
export default function useWindowWidth() { export default function useWindowWidth() {
const [windowWidth, setWindowWidth] = useState(window.innerWidth) const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => { useEffect(() => {
function handleResize() { function handleResize() {
setWindowWidth(window.innerWidth) setWindowWidth(window.innerWidth);
} }
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize) return () => window.removeEventListener('resize', handleResize);
}, []) }, []);
return windowWidth return windowWidth;
} }
+12 -13
View File
@@ -1,20 +1,19 @@
function countLinks(comment) { function countLinks(comment) {
let linkCount = 0; let linkCount = 0;
if (comment.replyCount > 0) { if (comment.replyCount > 0) {
for (let reply of comment.replies.pages.topAll.comments) { for (let reply of comment.replies.pages.topAll.comments) {
if (reply.link) {
linkCount++;
}
if (reply.link) { if (reply.replyCount > 0) {
linkCount++; linkCount += countLinks(reply);
} }
}
}
if (reply.replyCount > 0) { return linkCount;
linkCount += countLinks(reply);
}
}
}
return linkCount;
} }
export default countLinks; export default countLinks;
+13 -13
View File
@@ -1,18 +1,18 @@
function findShortParentCid(parentCid, input) { function findShortParentCid(parentCid, input) {
const feed = Array.isArray(input) ? input : [input]; const feed = Array.isArray(input) ? input : [input];
for (const thread of feed) { for (const thread of feed) {
if (thread.cid === parentCid) { if (thread.cid === parentCid) {
return thread.shortCid; return thread.shortCid;
} }
if (thread.replyCount > 0 && thread.replies.pages.topAll.comments) { if (thread.replyCount > 0 && thread.replies.pages.topAll.comments) {
const shortCid = findShortParentCid(parentCid, thread.replies.pages.topAll.comments); const shortCid = findShortParentCid(parentCid, thread.replies.pages.topAll.comments);
if (shortCid) { if (shortCid) {
return shortCid; return shortCid;
} }
} }
} }
return null; return null;
} }
export default findShortParentCid; export default findShortParentCid;
+62 -64
View File
@@ -2,79 +2,77 @@ import extName from 'ext-name';
import { canEmbed } from '../components/Embed'; import { canEmbed } from '../components/Embed';
const getCommentMediaInfo = (comment) => { const getCommentMediaInfo = (comment) => {
if (!comment?.thumbnailUrl && !comment?.link) { if (!comment?.thumbnailUrl && !comment?.link) {
return; return;
} }
if (comment?.link) { if (comment?.link) {
try { try {
const url = new URL(comment.link); const url = new URL(comment.link);
const host = url.hostname; const host = url.hostname;
let scrapedThumbnailUrl; let scrapedThumbnailUrl;
if (['youtube.com', 'www.youtube.com', 'youtu.be'].includes(host)) { if (['youtube.com', 'www.youtube.com', 'youtu.be'].includes(host)) {
const videoId = host === 'youtu.be' ? url.pathname.slice(1) : url.searchParams.get('v'); const videoId = host === 'youtu.be' ? url.pathname.slice(1) : url.searchParams.get('v');
scrapedThumbnailUrl = `https://img.youtube.com/vi/${videoId}/sddefault.jpg`; 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`;
}
} else if (host.includes('bitchute.com')) { if (canEmbed(url)) {
const videoId = url.pathname.split('/')[2]; return {
scrapedThumbnailUrl = `https://static-3.bitchute.com/live/cover_images/F61vWF4shy8s/${videoId}_640x360.jpg`; url: comment.link,
type: 'iframe',
thumbnail: comment.thumbnailUrl || scrapedThumbnailUrl,
};
}
} else if (host.includes('streamable.com')) { const mime = extName(url.pathname.toLowerCase().replace('/', ''))[0]?.mime;
const videoId = url.pathname.split('/')[1];
scrapedThumbnailUrl = `https://cdn-cf-east.streamable.com/image/${videoId}.jpg`;
}
if (canEmbed(url)) { if (mime?.startsWith('image')) {
return { return {
url: comment.link, url: comment.link,
type: 'iframe', type: 'image',
thumbnail: comment.thumbnailUrl || scrapedThumbnailUrl, };
}; }
}
const mime = extName(url.pathname.toLowerCase().replace('/', ''))[0]?.mime; if (mime?.startsWith('video')) {
return {
url: comment.link,
type: 'video',
thumbnail: comment.thumbnailUrl,
};
}
if (mime?.startsWith('image')) { if (mime?.startsWith('audio')) {
return { return {
url: comment.link, url: comment.link,
type: 'image', type: 'audio',
}; };
} }
} catch (error) {
return;
}
}
if (mime?.startsWith('video')) { if (comment?.thumbnailUrl && comment?.thumbnailUrl !== comment?.link) {
return { return {
url: comment.link, url: comment.link,
type: 'video', type: 'webpage',
thumbnail: comment.thumbnailUrl, thumbnail: comment.thumbnailUrl,
}; };
} }
if (mime?.startsWith('audio')) { if (comment?.link) {
return { return {
url: comment.link, url: comment.link,
type: 'audio', type: 'webpage',
}; };
} }
} catch (error) {
return;
}
}
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',
};
}
}; };
export default getCommentMediaInfo; export default getCommentMediaInfo;
+32 -35
View File
@@ -1,38 +1,35 @@
const getDate = (commentTimestamp) => { const getDate = (commentTimestamp) => {
if (commentTimestamp === undefined || isNaN(commentTimestamp)) { if (commentTimestamp === undefined || isNaN(commentTimestamp)) {
return ""; return '';
} }
const locale = Intl.DateTimeFormat().resolvedOptions().locale; const locale = Intl.DateTimeFormat().resolvedOptions().locale;
const string = new Intl.DateTimeFormat(locale, { const string = new Intl.DateTimeFormat(locale, {
hour12: false, hour12: false,
year: '2-digit', year: '2-digit',
month: '2-digit', month: '2-digit',
day: '2-digit', day: '2-digit',
weekday: 'short', weekday: 'short',
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
second: '2-digit' second: '2-digit',
}).format(new Date(commentTimestamp * 1000)); }).format(new Date(commentTimestamp * 1000));
if (locale.startsWith('ar')) { if (locale.startsWith('ar')) {
return string return string;
} }
const items = string.split(/,* /) const items = string.split(/,* /);
if (items.length === 3) { if (items.length === 3) {
const itemIsNumber = [ const itemIsNumber = [items[0][0].match(/[0-9]/), items[1][0].match(/[0-9]/)];
items[0][0].match(/[0-9]/), if (itemIsNumber[0] && itemIsNumber[1]) {
items[1][0].match(/[0-9]/) return `${items[0]}(${items[2]})${items[1]}`;
] }
if (itemIsNumber[0] && itemIsNumber[1]) { if (itemIsNumber[0] && !itemIsNumber[1]) {
return `${items[0]}(${items[2]})${items[1]}` return `${items[0]}(${items[1]})${items[2]}`;
} }
if (itemIsNumber[0] && !itemIsNumber[1]) { if (!itemIsNumber[0] && itemIsNumber[1]) {
return `${items[0]}(${items[1]})${items[2]}` return `${items[1]}(${items[0]})${items[2]}`;
} }
if (!itemIsNumber[0] && itemIsNumber[1]) { }
return `${items[1]}(${items[0]})${items[2]}` return string;
} };
}
return string
}
export default getDate; export default getDate;
+35 -36
View File
@@ -1,45 +1,44 @@
const pluralize = (unit, value) => { const pluralize = (unit, value) => {
return `${value} ${unit}${value > 1 ? 's' : ''}`; return `${value} ${unit}${value > 1 ? 's' : ''}`;
}; };
const getFormattedTime = (timestamp) => { const getFormattedTime = (timestamp) => {
try { try {
const currentTime = new Date().getTime(); const currentTime = new Date().getTime();
const differenceInMilliseconds = currentTime - (timestamp * 1000); const differenceInMilliseconds = currentTime - timestamp * 1000;
const years = Math.floor(differenceInMilliseconds / (1000 * 60 * 60 * 24 * 365.25)); 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 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 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 hours = Math.floor((differenceInMilliseconds % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((differenceInMilliseconds % (1000 * 60 * 60)) / (1000 * 60)); const minutes = Math.floor((differenceInMilliseconds % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((differenceInMilliseconds % (1000 * 60)) / 1000); const seconds = Math.floor((differenceInMilliseconds % (1000 * 60)) / 1000);
if (years > 0) { if (years > 0) {
return months > 0 ? `${pluralize('year', years)} and ${pluralize('month', months)} ago` : `${pluralize('year', years)} ago`; return months > 0 ? `${pluralize('year', years)} and ${pluralize('month', months)} ago` : `${pluralize('year', years)} ago`;
} else if (months > 0) { } else if (months > 0) {
return days > 0 ? `${pluralize('month', months)} and ${pluralize('day', days)} ago` : `${pluralize('month', months)} ago`; return days > 0 ? `${pluralize('month', months)} and ${pluralize('day', days)} ago` : `${pluralize('month', months)} ago`;
} else if (days > 0) { } else if (days > 0) {
if (hours > 0) { if (hours > 0) {
return `${pluralize('day', days)} and ${pluralize('hour', hours)} ago`; return `${pluralize('day', days)} and ${pluralize('hour', hours)} ago`;
} else if (minutes > 0) { } else if (minutes > 0) {
return `${pluralize('day', days)} and ${pluralize('minute', minutes)} ago`; return `${pluralize('day', days)} and ${pluralize('minute', minutes)} ago`;
} else { } else {
return `${pluralize('day', days)} ago`; return `${pluralize('day', days)} ago`;
} }
} else if (hours > 0) { } else if (hours > 0) {
return minutes > 0 ? `${pluralize('hour', hours)} and ${pluralize('minute', minutes)} ago` : `${pluralize('hour', hours)} ago`; return minutes > 0 ? `${pluralize('hour', hours)} and ${pluralize('minute', minutes)} ago` : `${pluralize('hour', hours)} ago`;
} else if (minutes > 0) { } else if (minutes > 0) {
return seconds > 0 ? `${pluralize('minute', minutes)} and ${pluralize('second', seconds)} ago` : `${pluralize('minute', minutes)} ago`; return seconds > 0 ? `${pluralize('minute', minutes)} and ${pluralize('second', seconds)} ago` : `${pluralize('minute', minutes)} ago`;
} else if (seconds > 30) { } else if (seconds > 30) {
return `${pluralize('second', seconds)} ago`; return `${pluralize('second', seconds)} ago`;
} else { } else {
return "just now"; return 'just now';
} }
} } catch (e) {
catch (e) { console.error('Error in getFormattedTime:', e);
console.error("Error in getFormattedTime:", e); return '[error]';
return "[error]"; }
}
}; };
export default getFormattedTime; export default getFormattedTime;
+28 -28
View File
@@ -1,38 +1,38 @@
function handleAddressClick(shortAddress) { function handleAddressClick(shortAddress) {
const isMobile = window.innerWidth <= 480; const isMobile = window.innerWidth <= 480;
const addressSelector = isMobile ? '.address-mobile' : '.address-desktop'; const addressSelector = isMobile ? '.address-mobile' : '.address-desktop';
const postReplySelector = isMobile ? '.post-reply-mobile' : '.post-reply-desktop'; const postReplySelector = isMobile ? '.post-reply-mobile' : '.post-reply-desktop';
const opSelector = isMobile ? '.op-mobile' : '.op-desktop'; const opSelector = isMobile ? '.op-mobile' : '.op-desktop';
const matchingElements = [...document.querySelectorAll(postReplySelector + ',' + opSelector)].filter(el => { const matchingElements = [...document.querySelectorAll(postReplySelector + ',' + opSelector)].filter((el) => {
const addressElement = el.querySelector(addressSelector); const addressElement = el.querySelector(addressSelector);
return addressElement && addressElement.textContent.includes(shortAddress); return addressElement && addressElement.textContent.includes(shortAddress);
}); });
if (matchingElements.length === 0) { if (matchingElements.length === 0) {
return; return;
} }
const highlightedElements = document.querySelectorAll('.highlighted-address'); const highlightedElements = document.querySelectorAll('.highlighted-address');
let allHighlighted = true; let allHighlighted = true;
matchingElements.forEach(el => { matchingElements.forEach((el) => {
if (!el.classList.contains('highlighted-address')) { if (!el.classList.contains('highlighted-address')) {
allHighlighted = false; allHighlighted = false;
} }
}); });
highlightedElements.forEach(el => { highlightedElements.forEach((el) => {
el.classList.remove('highlighted-address'); el.classList.remove('highlighted-address');
}); });
if (!allHighlighted) { if (!allHighlighted) {
matchingElements.forEach(el => { matchingElements.forEach((el) => {
if (!el.classList.contains('op-mobile') && !el.classList.contains('op-desktop')) { if (!el.classList.contains('op-mobile') && !el.classList.contains('op-desktop')) {
el.classList.add('highlighted-address'); el.classList.add('highlighted-address');
} }
}); });
} }
} }
export default handleAddressClick; export default handleAddressClick;
+6 -6
View File
@@ -1,12 +1,12 @@
const handleImageClick = (e) => { const handleImageClick = (e) => {
const image = e.target; const image = e.target;
const container = image.closest('.img-container'); const container = image.closest('.img-container');
image.classList.toggle('enlarged'); image.classList.toggle('enlarged');
if (container) { if (container) {
container.classList.toggle('expanded-container'); container.classList.toggle('expanded-container');
} }
}; };
export default handleImageClick; export default handleImageClick;
+38 -40
View File
@@ -1,55 +1,53 @@
function handleQuoteClick(reply, parentCid, threadCid) { function handleQuoteClick(reply, parentCid, threadCid) {
const cid = parentCid ? parentCid : reply.shortCid; const cid = parentCid ? parentCid : reply.shortCid;
const isMobile = window.innerWidth <= 480; const isMobile = window.innerWidth <= 480;
const postNumberSelector = isMobile ? '.post-number-mobile' : '.post-number-desktop'; const postNumberSelector = isMobile ? '.post-number-mobile' : '.post-number-desktop';
if (threadCid && cid === threadCid) { if (threadCid && cid === threadCid) {
const highlightedElements = document.querySelectorAll('.highlighted'); const highlightedElements = document.querySelectorAll('.highlighted');
highlightedElements.forEach(el => { highlightedElements.forEach((el) => {
el.classList.remove('highlighted'); el.classList.remove('highlighted');
}); });
const opElementSelector = isMobile ? '.op-mobile' : '.op-desktop'; const opElementSelector = isMobile ? '.op-mobile' : '.op-desktop';
const opElement = [...document.querySelectorAll(opElementSelector)] const opElement = [...document.querySelectorAll(opElementSelector)].find((el) => {
.find(el => { const postNumberElement = el.querySelector(postNumberSelector);
const postNumberElement = el.querySelector(postNumberSelector); return postNumberElement && postNumberElement.innerHTML.includes(threadCid);
return postNumberElement && postNumberElement.innerHTML.includes(threadCid); });
});
if (opElement) { if (opElement) {
opElement.scrollIntoView({ behavior: "auto", block: "start" }); opElement.scrollIntoView({ behavior: 'auto', block: 'start' });
} else { } else {
return; 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)] const targetElement = [...document.querySelectorAll(targetElementSelector)].find((el) => {
.find(el => { const postNumberElement = el.querySelector(postNumberSelector);
const postNumberElement = el.querySelector(postNumberSelector); return postNumberElement && postNumberElement.innerHTML.includes(cid);
return postNumberElement && postNumberElement.innerHTML.includes(cid); });
});
if (targetElement) { if (targetElement) {
const highlightedElements = document.querySelectorAll('.highlighted-click'); const highlightedElements = document.querySelectorAll('.highlighted-click');
highlightedElements.forEach(el => { highlightedElements.forEach((el) => {
el.classList.remove('highlighted-click'); 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')) { if (!targetElement.classList.contains('op-mobile') && !targetElement.classList.contains('op-desktop')) {
targetElement.classList.add('highlighted-click'); targetElement.classList.add('highlighted-click');
} }
} else { } else {
return; return;
} }
}; }
export default handleQuoteClick; export default handleQuoteClick;
+33 -34
View File
@@ -1,43 +1,42 @@
function handleQuoteHover(reply, parentCid, onElementOutOfView) { function handleQuoteHover(reply, parentCid, onElementOutOfView) {
const cid = parentCid ? parentCid : reply.shortCid; const cid = parentCid ? parentCid : reply.shortCid;
const isMobile = window.innerWidth <= 480; const isMobile = window.innerWidth <= 480;
const postNumberSelector = isMobile ? '.post-number-mobile' : '.post-number-desktop'; 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)] const targetElement = [...document.querySelectorAll(targetElementSelector)].find((el) => {
.find(el => { const postNumberElement = el.querySelector(postNumberSelector);
const postNumberElement = el.querySelector(postNumberSelector); return postNumberElement && postNumberElement.innerHTML.includes(cid);
return postNumberElement && postNumberElement.innerHTML.includes(cid); });
});
if (targetElement) { if (targetElement) {
const isInViewport = (element) => { const isInViewport = (element) => {
const bounding = element.getBoundingClientRect(); const bounding = element.getBoundingClientRect();
return ( return (
bounding.top >= 0 && bounding.top >= 0 &&
bounding.left >= 0 && bounding.left >= 0 &&
bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) && bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
bounding.right <= (window.innerWidth || document.documentElement.clientWidth) bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
); );
}; };
if (isInViewport(targetElement)) { if (isInViewport(targetElement)) {
const highlightedElements = document.querySelectorAll('.highlighted'); const highlightedElements = document.querySelectorAll('.highlighted');
highlightedElements.forEach(el => { highlightedElements.forEach((el) => {
el.classList.remove('highlighted'); el.classList.remove('highlighted');
}); });
if (!targetElement.classList.contains('op-mobile') && !targetElement.classList.contains('op-desktop')) { if (!targetElement.classList.contains('op-mobile') && !targetElement.classList.contains('op-desktop')) {
targetElement.classList.add('highlighted'); targetElement.classList.add('highlighted');
} }
} else { } else {
onElementOutOfView(); onElementOutOfView();
} }
} else { } else {
onElementOutOfView(); onElementOutOfView();
} }
}; }
export default handleQuoteHover; export default handleQuoteHover;
+23 -20
View File
@@ -1,28 +1,31 @@
function handleShareClick(selectedAddress, cid) { function handleShareClick(selectedAddress, cid) {
let shareLink; let shareLink;
if (cid === "rules" || cid === "description") { if (cid === 'rules' || cid === 'description') {
shareLink = `https://plebchan.eth.limo/#/p/${selectedAddress}`; shareLink = `https://plebchan.eth.limo/#/p/${selectedAddress}`;
} else { } else {
const plebBzBaseURL = "https://pleb.bz/p/"; const plebBzBaseURL = 'https://pleb.bz/p/';
shareLink = `${plebBzBaseURL}${selectedAddress}/`; shareLink = `${plebBzBaseURL}${selectedAddress}/`;
if (cid !== "rules" && cid !== "description") { if (cid !== 'rules' && cid !== 'description') {
shareLink += `c/${cid}`; shareLink += `c/${cid}`;
} }
shareLink += `?redirect=plebchan.eth.limo`; shareLink += `?redirect=plebchan.eth.limo`;
} }
if (navigator.clipboard) { if (navigator.clipboard) {
navigator.clipboard.writeText(shareLink).then(() => { navigator.clipboard
console.log("Link copied to clipboard!"); .writeText(shareLink)
}).catch(err => { .then(() => {
console.error('Could not copy text: ', err); console.log('Link copied to clipboard!');
}); })
} else { .catch((err) => {
return; console.error('Could not copy text: ', err);
} });
} else {
return;
}
} }
export default handleShareClick; export default handleShareClick;
+81 -83
View File
@@ -1,94 +1,92 @@
import useGeneralStore from "../hooks/stores/useGeneralStore"; import useGeneralStore from '../hooks/stores/useGeneralStore';
const handleStyleChange = (event) => { const handleStyleChange = (event) => {
const { const { setBodyStyle, setSelectedStyle } = useGeneralStore.getState();
setBodyStyle, setSelectedStyle
} = useGeneralStore.getState();
switch (event.target.value) { switch (event.target.value) {
case "Yotsuba": case 'Yotsuba':
const yotsubaBodyStyle = { const yotsubaBodyStyle = {
background: "#ffe url(assets/fade.png) top repeat-x", background: '#ffe url(assets/fade.png) top repeat-x',
color: "maroon", color: 'maroon',
fontFamily: "Arial, Helvetica, sans-serif" fontFamily: 'Arial, Helvetica, sans-serif',
}; };
setBodyStyle(yotsubaBodyStyle); setBodyStyle(yotsubaBodyStyle);
setSelectedStyle("Yotsuba"); setSelectedStyle('Yotsuba');
localStorage.setItem("selectedStyle", "Yotsuba"); localStorage.setItem('selectedStyle', 'Yotsuba');
localStorage.setItem("bodyStyle", JSON.stringify(yotsubaBodyStyle)); localStorage.setItem('bodyStyle', JSON.stringify(yotsubaBodyStyle));
break; break;
case "Yotsuba-B": case 'Yotsuba-B':
const yotsubaBBodyStyle = { const yotsubaBBodyStyle = {
background: "#eef2ff url(assets/fade-blue.png) top center repeat-x", background: '#eef2ff url(assets/fade-blue.png) top center repeat-x',
color: "#000", color: '#000',
fontFamily: "Arial, Helvetica, sans-serif" fontFamily: 'Arial, Helvetica, sans-serif',
}; };
setBodyStyle(yotsubaBBodyStyle); setBodyStyle(yotsubaBBodyStyle);
setSelectedStyle("Yotsuba-B"); setSelectedStyle('Yotsuba-B');
localStorage.setItem("selectedStyle", "Yotsuba-B"); localStorage.setItem('selectedStyle', 'Yotsuba-B');
localStorage.setItem("bodyStyle", JSON.stringify(yotsubaBBodyStyle)); localStorage.setItem('bodyStyle', JSON.stringify(yotsubaBBodyStyle));
break; break;
case "Futaba": case 'Futaba':
const futabaBodyStyle = { const futabaBodyStyle = {
background: "#ffe", background: '#ffe',
color: "maroon", color: 'maroon',
fontFamily: "times new roman, serif" fontFamily: 'times new roman, serif',
}; };
setBodyStyle(futabaBodyStyle); setBodyStyle(futabaBodyStyle);
setSelectedStyle("Futaba"); setSelectedStyle('Futaba');
localStorage.setItem("selectedStyle", "Futaba"); localStorage.setItem('selectedStyle', 'Futaba');
localStorage.setItem("bodyStyle", JSON.stringify(futabaBodyStyle)); localStorage.setItem('bodyStyle', JSON.stringify(futabaBodyStyle));
break; break;
case "Burichan": case 'Burichan':
const burichanBodyStyle = { const burichanBodyStyle = {
background: "#eef2ff", background: '#eef2ff',
color: "#000", color: '#000',
fontFamily: "times new roman, serif" fontFamily: 'times new roman, serif',
}; };
setBodyStyle(burichanBodyStyle); setBodyStyle(burichanBodyStyle);
setSelectedStyle("Burichan"); setSelectedStyle('Burichan');
localStorage.setItem("selectedStyle", "Burichan"); localStorage.setItem('selectedStyle', 'Burichan');
localStorage.setItem("bodyStyle", JSON.stringify(burichanBodyStyle)); localStorage.setItem('bodyStyle', JSON.stringify(burichanBodyStyle));
break; break;
case "Tomorrow": case 'Tomorrow':
const tomorrowBodyStyle = { const tomorrowBodyStyle = {
background: "#1d1f21 none", background: '#1d1f21 none',
color: "#c5c8c6", color: '#c5c8c6',
fontFamily: "Arial, Helvetica, sans-serif" fontFamily: 'Arial, Helvetica, sans-serif',
}; };
setBodyStyle(tomorrowBodyStyle); setBodyStyle(tomorrowBodyStyle);
setSelectedStyle("Tomorrow"); setSelectedStyle('Tomorrow');
localStorage.setItem("selectedStyle", "Tomorrow"); localStorage.setItem('selectedStyle', 'Tomorrow');
localStorage.setItem("bodyStyle", JSON.stringify(tomorrowBodyStyle)); localStorage.setItem('bodyStyle', JSON.stringify(tomorrowBodyStyle));
break; break;
case "Photon": case 'Photon':
const photonBodyStyle = { const photonBodyStyle = {
background: "#eee none", background: '#eee none',
color: "#333", color: '#333',
fontFamily: "Arial, Helvetica, sans-serif" fontFamily: 'Arial, Helvetica, sans-serif',
}; };
setBodyStyle(photonBodyStyle); setBodyStyle(photonBodyStyle);
setSelectedStyle("Photon"); setSelectedStyle('Photon');
localStorage.setItem("selectedStyle", "Photon"); localStorage.setItem('selectedStyle', 'Photon');
localStorage.setItem("bodyStyle", JSON.stringify(photonBodyStyle)); localStorage.setItem('bodyStyle', JSON.stringify(photonBodyStyle));
break; break;
default: default:
const defaultBodyStyle = { const defaultBodyStyle = {
background: "#ffe url(assets/fade.png) top repeat-x", background: '#ffe url(assets/fade.png) top repeat-x',
color: "maroon", color: 'maroon',
fontFamily: "Arial, Helvetica, sans-serif" fontFamily: 'Arial, Helvetica, sans-serif',
}; };
setBodyStyle(defaultBodyStyle); setBodyStyle(defaultBodyStyle);
setSelectedStyle("Yotsuba"); setSelectedStyle('Yotsuba');
localStorage.setItem("selectedStyle", "Yotsuba"); localStorage.setItem('selectedStyle', 'Yotsuba');
localStorage.setItem("bodyStyle", JSON.stringify(defaultBodyStyle)); localStorage.setItem('bodyStyle', JSON.stringify(defaultBodyStyle));
} }
} };
export default handleStyleChange; export default handleStyleChange;
+6 -6
View File
@@ -1,10 +1,10 @@
const isValidUrl = (url) => { const isValidUrl = (url) => {
try { try {
new URL(url); new URL(url);
return true; return true;
} catch (e) { } catch (e) {
return false; return false;
} }
}; };
export default isValidUrl; export default isValidUrl;
+4 -4
View File
@@ -1,8 +1,8 @@
const preloadImages = (imageUrls) => { const preloadImages = (imageUrls) => {
imageUrls.forEach((imageUrl) => { imageUrls.forEach((imageUrl) => {
const img = new Image(); const img = new Image();
img.src = imageUrl; img.src = imageUrl;
}); });
}; };
export default preloadImages; export default preloadImages;
+5 -5
View File
@@ -1,9 +1,9 @@
function removeHighlight() { function removeHighlight() {
const highlightedElements = document.querySelectorAll('.highlighted'); const highlightedElements = document.querySelectorAll('.highlighted');
highlightedElements.forEach(el => { highlightedElements.forEach((el) => {
el.classList.remove('highlighted'); el.classList.remove('highlighted');
}); });
}; }
export default removeHighlight; export default removeHighlight;