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
+11 -18
View File
@@ -1,24 +1,17 @@
import React, { useEffect, useState } from "react";
import { useSubplebbit } from "@plebbit/plebbit-react-hooks";
import React, { useEffect, useState } from 'react';
import { useSubplebbit } from '@plebbit/plebbit-react-hooks';
const BoardAvatar = ({ address }) => {
const [avatarUrl, setAvatarUrl] = useState("assets/plebchan.png");
const subplebbit = useSubplebbit({ subplebbitAddress: address });
const [avatarUrl, setAvatarUrl] = useState('assets/plebchan.png');
const subplebbit = useSubplebbit({ subplebbitAddress: address });
useEffect(() => {
if (subplebbit.suggested?.avatarUrl) {
setAvatarUrl(subplebbit.suggested?.avatarUrl);
}
}, [subplebbit.suggested?.avatarUrl, subplebbit]);
useEffect(() => {
if (subplebbit.suggested?.avatarUrl) {
setAvatarUrl(subplebbit.suggested?.avatarUrl);
}
}, [subplebbit.suggested?.avatarUrl, subplebbit]);
return (
<img
className="board-avatar"
alt="board avatar"
src={avatarUrl}
/>
);
return <img className='board-avatar' alt='board avatar' src={avatarUrl} />;
};
export default BoardAvatar;
export default BoardAvatar;
+274 -291
View File
@@ -1,332 +1,315 @@
import React, { useEffect, useRef, useState } from "react";
import { Link } from "react-router-dom";
import { usePublishSubplebbitEdit } from "@plebbit/plebbit-react-hooks";
import React, { useEffect, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import { usePublishSubplebbitEdit } from '@plebbit/plebbit-react-hooks';
import { StyledModal } from './styled/modals/ModerationModal.styled';
import useError from "../hooks/useError";
import useSuccess from "../hooks/useSuccess";
import useError from '../hooks/useError';
import useSuccess from '../hooks/useSuccess';
import useGeneralStore from '../hooks/stores/useGeneralStore';
const BoardSettings = ({ subplebbit }) => {
const {
setCaptchaResponse,
setChallengesArray,
setIsCaptchaOpen,
setResolveCaptchaPromise,
selectedAddress,
selectedStyle,
} = useGeneralStore(state => state);
const { setCaptchaResponse, setChallengesArray, setIsCaptchaOpen, setResolveCaptchaPromise, selectedAddress, selectedStyle } = useGeneralStore((state) => state);
const allowedSettings = {
address: subplebbit.address,
apiUrl: subplebbit.apiUrl,
description: subplebbit.description,
pubsubTopic: subplebbit.pubsubTopic,
settings: {
fetchThumbnailUrls: subplebbit.settings?.fetchThumbnailUrls,
fetchThumbnailUrlsProxyUrl: subplebbit.settings?.fetchThumbnailUrlsProxyUrl,
},
roles: subplebbit.roles,
rules: subplebbit.rules,
suggested: {
avatarUrl: subplebbit.suggested?.avatarUrl,
backgroundUrl: subplebbit.suggested?.backgroundUrl,
bannerUrl: subplebbit.suggested?.bannerUrl,
language: subplebbit.suggested?.language,
primaryColor: subplebbit.suggested?.primaryColor,
secondaryColor: subplebbit.suggested?.secondaryColor,
},
title: subplebbit.title,
};
const allowedSettings = {
address: subplebbit.address,
apiUrl: subplebbit.apiUrl,
description: subplebbit.description,
pubsubTopic: subplebbit.pubsubTopic,
settings: {
fetchThumbnailUrls: subplebbit.settings?.fetchThumbnailUrls,
fetchThumbnailUrlsProxyUrl: subplebbit.settings?.fetchThumbnailUrlsProxyUrl,
},
roles: subplebbit.roles,
rules: subplebbit.rules,
suggested: {
avatarUrl: subplebbit.suggested?.avatarUrl,
backgroundUrl: subplebbit.suggested?.backgroundUrl,
bannerUrl: subplebbit.suggested?.bannerUrl,
language: subplebbit.suggested?.language,
primaryColor: subplebbit.suggested?.primaryColor,
secondaryColor: subplebbit.suggested?.secondaryColor,
},
title: subplebbit.title,
};
const generateSettingsFromSubplebbit = (subplebbitData) => ({
address: subplebbitData.address,
apiUrl: subplebbitData.apiUrl,
description: subplebbitData.description,
pubsubTopic: subplebbitData.pubsubTopic,
settings: {
fetchThumbnailUrls: subplebbitData.settings?.fetchThumbnailUrls,
fetchThumbnailUrlsProxyUrl: subplebbitData.settings?.fetchThumbnailUrlsProxyUrl,
},
roles: subplebbitData.roles,
rules: subplebbitData.rules,
suggested: {
avatarUrl: subplebbitData.suggested?.avatarUrl,
backgroundUrl: subplebbitData.suggested?.backgroundUrl,
bannerUrl: subplebbitData.suggested?.bannerUrl,
language: subplebbitData.suggested?.language,
primaryColor: subplebbitData.suggested?.primaryColor,
secondaryColor: subplebbitData.suggested?.secondaryColor,
},
title: subplebbitData.title,
});
const generateSettingsFromSubplebbit = (subplebbitData) => ({
address: subplebbitData.address,
apiUrl: subplebbitData.apiUrl,
description: subplebbitData.description,
pubsubTopic: subplebbitData.pubsubTopic,
settings: {
fetchThumbnailUrls: subplebbitData.settings?.fetchThumbnailUrls,
fetchThumbnailUrlsProxyUrl: subplebbitData.settings?.fetchThumbnailUrlsProxyUrl,
},
roles: subplebbitData.roles,
rules: subplebbitData.rules,
suggested: {
avatarUrl: subplebbitData.suggested?.avatarUrl,
backgroundUrl: subplebbitData.suggested?.backgroundUrl,
bannerUrl: subplebbitData.suggested?.bannerUrl,
language: subplebbitData.suggested?.language,
primaryColor: subplebbitData.suggested?.primaryColor,
secondaryColor: subplebbitData.suggested?.secondaryColor,
},
title: subplebbitData.title,
});
const initialSettings = generateSettingsFromSubplebbit(subplebbit);
const [isModalOpen, setIsModalOpen] = useState(false);
const [boardSettingsJson, setBoardSettingsJson] = useState(JSON.stringify(initialSettings, null, 2));
const [triggerPublilshSubplebbitEdit, setTriggerPublishCommentEdit] = useState(false);
const initialSettings = generateSettingsFromSubplebbit(subplebbit);
const [isModalOpen, setIsModalOpen] = useState(false);
const [boardSettingsJson, setBoardSettingsJson] = useState(JSON.stringify(initialSettings, null, 2));
const [triggerPublilshSubplebbitEdit, setTriggerPublishCommentEdit] = useState(false);
const [, setNewErrorMessage] = useError();
const [, setNewSuccessMessage] = useSuccess();
const [, setNewErrorMessage] = useError();
const [, setNewSuccessMessage] = useSuccess();
const getDifferences = (oldObj, newObj) => {
let differences = {};
for (let key in oldObj) {
if (typeof oldObj[key] === 'object' && oldObj[key] !== null) {
const nestedDifferences = getDifferences(oldObj[key], newObj[key] || {});
if (Object.keys(nestedDifferences).length > 0) {
differences[key] = nestedDifferences;
}
} else if (oldObj[key] !== newObj[key]) {
differences[key] = newObj[key];
}
}
for (let key in newObj) {
if (!oldObj.hasOwnProperty(key)) {
differences[key] = newObj[key];
}
}
return differences;
};
const getDifferences = (oldObj, newObj) => {
let differences = {};
for (let key in oldObj) {
if (typeof oldObj[key] === 'object' && oldObj[key] !== null) {
const nestedDifferences = getDifferences(oldObj[key], newObj[key] || {});
if (Object.keys(nestedDifferences).length > 0) {
differences[key] = nestedDifferences;
}
} else if (oldObj[key] !== newObj[key]) {
differences[key] = newObj[key];
}
}
const isInitialMount = useRef(true);
for (let key in newObj) {
if (!oldObj.hasOwnProperty(key)) {
differences[key] = newObj[key];
}
}
useEffect(() => {
if (isInitialMount.current) {
setBoardSettingsJson(JSON.stringify(generateSettingsFromSubplebbit(subplebbit), null, 2));
isInitialMount.current = false;
}
}, [subplebbit]);
return differences;
};
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 (isInitialMount.current) {
setBoardSettingsJson(JSON.stringify(generateSettingsFromSubplebbit(subplebbit), null, 2));
isInitialMount.current = false;
}
}, [subplebbit]);
if (typeof updatedSettings[key] === 'object' && updatedSettings[key] !== null
&& !Array.isArray(updatedSettings[key])) {
if (typeof allowedSettings[key] !== 'object' || allowedSettings[key] === null
|| Array.isArray(allowedSettings[key])) {
throw new Error(`Expected ${key} to be an object in allowedSettings`);
}
validateSettings(updatedSettings[key], allowedSettings[key]);
}
}
}
function validateSettings(updatedSettings, allowedSettings) {
for (let key in updatedSettings) {
if (!allowedSettings.hasOwnProperty(key) && !initialSettings.hasOwnProperty(key)) {
throw new Error(`Unexpected setting: ${key}`);
}
if (typeof updatedSettings[key] === 'object' && updatedSettings[key] !== null && !Array.isArray(updatedSettings[key])) {
if (typeof allowedSettings[key] !== 'object' || allowedSettings[key] === null || Array.isArray(allowedSettings[key])) {
throw new Error(`Expected ${key} to be an object in allowedSettings`);
}
validateSettings(updatedSettings[key], allowedSettings[key]);
}
}
}
const onChallenge = async (challenges, subplebbitEdit) => {
let challengeAnswers = [];
try {
challengeAnswers = await getChallengeAnswersFromUser(challenges)
}
catch (error) {
setNewErrorMessage(error.message); console.log(error);
}
if (challengeAnswers) {
await subplebbitEdit.publishChallengeAnswers(challengeAnswers)
}
};
const onChallenge = async (challenges, subplebbitEdit) => {
let challengeAnswers = [];
try {
challengeAnswers = await getChallengeAnswersFromUser(challenges);
} catch (error) {
setNewErrorMessage(error.message);
console.log(error);
}
if (challengeAnswers) {
await subplebbitEdit.publishChallengeAnswers(challengeAnswers);
}
};
const onChallengeVerification = (challengeVerification) => {
if (challengeVerification.challengeSuccess === true) {
setNewSuccessMessage('Challenge Success'); console.log('challenge success', challengeVerification);
} else if (challengeVerification.challengeSuccess === false) {
setNewErrorMessage(`Challenge Failed, reason: ${challengeVerification.reason}. Errors: ${challengeVerification.errors}`);
console.log('challenge failed', challengeVerification);
}
};
const onChallengeVerification = (challengeVerification) => {
if (challengeVerification.challengeSuccess === true) {
setNewSuccessMessage('Challenge Success');
console.log('challenge success', challengeVerification);
} else if (challengeVerification.challengeSuccess === false) {
setNewErrorMessage(`Challenge Failed, reason: ${challengeVerification.reason}. Errors: ${challengeVerification.errors}`);
console.log('challenge failed', challengeVerification);
}
};
const getChallengeAnswersFromUser = async (challenges) => {
setChallengesArray(challenges);
const getChallengeAnswersFromUser = async (challenges) => {
setChallengesArray(challenges);
return new Promise((resolve, reject) => {
const imageString = challenges?.challenges[0].challenge;
const imageSource = `data:image/png;base64,${imageString}`;
const challengeImg = new Image();
challengeImg.src = imageSource;
challengeImg.onload = () => {
setIsCaptchaOpen(true);
const handleKeyDown = async (event) => {
if (event.key === 'Enter') {
const currentCaptchaResponse = useGeneralStore.getState().captchaResponse;
resolve(currentCaptchaResponse);
setIsCaptchaOpen(false);
document.removeEventListener('keydown', handleKeyDown);
event.preventDefault();
}
};
return new Promise((resolve, reject) => {
const imageString = challenges?.challenges[0].challenge;
const imageSource = `data:image/png;base64,${imageString}`;
const challengeImg = new Image();
challengeImg.src = imageSource;
setCaptchaResponse('');
document.addEventListener('keydown', handleKeyDown);
challengeImg.onload = () => {
setIsCaptchaOpen(true);
setResolveCaptchaPromise(resolve);
};
challengeImg.onerror = () => {
reject(setNewErrorMessage('Could not load challenges'));
};
});
};
const handleKeyDown = async (event) => {
if (event.key === 'Enter') {
const currentCaptchaResponse = useGeneralStore.getState().captchaResponse;
resolve(currentCaptchaResponse);
setIsCaptchaOpen(false);
document.removeEventListener('keydown', handleKeyDown);
event.preventDefault();
}
};
setCaptchaResponse('');
document.addEventListener('keydown', handleKeyDown);
const [editSubplebbitOptions, setEditSubplebbitOptions] = useState({
subplebbitAddress: selectedAddress,
onChallenge,
onChallengeVerification,
onError: (error) => {
setNewErrorMessage(error.message); console.log(error);
}
});
setResolveCaptchaPromise(resolve);
};
challengeImg.onerror = () => {
reject(setNewErrorMessage('Could not load challenges'));
};
});
};
const { publishSubplebbitEdit } = usePublishSubplebbitEdit(editSubplebbitOptions);
const [editSubplebbitOptions, setEditSubplebbitOptions] = useState({
subplebbitAddress: selectedAddress,
onChallenge,
onChallengeVerification,
onError: (error) => {
setNewErrorMessage(error.message);
console.log(error);
},
});
const { publishSubplebbitEdit } = usePublishSubplebbitEdit(editSubplebbitOptions);
useEffect(() => {
let isActive = true;
if (editSubplebbitOptions && triggerPublilshSubplebbitEdit) {
(async () => {
await publishSubplebbitEdit(editSubplebbitOptions);
if (isActive) {
setTriggerPublishCommentEdit(false);
}
})();
}
useEffect(() => {
let isActive = true;
if (editSubplebbitOptions && triggerPublilshSubplebbitEdit) {
(async () => {
await publishSubplebbitEdit(editSubplebbitOptions);
if (isActive) {
setTriggerPublishCommentEdit(false);
}
})();
}
return () => {
isActive = false;
};
}, [editSubplebbitOptions, publishSubplebbitEdit, triggerPublilshSubplebbitEdit]);
return () => {
isActive = false;
};
}, [editSubplebbitOptions, publishSubplebbitEdit, triggerPublilshSubplebbitEdit]);
const handleSaveChanges = async () => {
try {
const updatedSettings = JSON.parse(boardSettingsJson);
validateSettings(updatedSettings, allowedSettings);
const changes = getDifferences(initialSettings, updatedSettings);
if (Object.keys(changes).length > 0) {
setEditSubplebbitOptions(prevOptions => ({
...prevOptions,
...changes
}));
setTriggerPublishCommentEdit(true);
} else {
setNewErrorMessage("No changes detected");
}
} catch (error) {
setNewErrorMessage(`Error saving changes: ${error}`);
console.log(error);
}
};
const handleSaveChanges = async () => {
try {
const updatedSettings = JSON.parse(boardSettingsJson);
validateSettings(updatedSettings, allowedSettings);
const changes = getDifferences(initialSettings, updatedSettings);
if (Object.keys(changes).length > 0) {
setEditSubplebbitOptions((prevOptions) => ({
...prevOptions,
...changes,
}));
setTriggerPublishCommentEdit(true);
} else {
setNewErrorMessage('No changes detected');
}
} catch (error) {
setNewErrorMessage(`Error saving changes: ${error}`);
console.log(error);
}
};
const handleResetChanges = () => {
setBoardSettingsJson(JSON.stringify(initialSettings, null, 2));
};
const handleResetChanges = () => {
setBoardSettingsJson(JSON.stringify(initialSettings, null, 2));
};
function generateSettingsList(settingsObj, parentKey = '') {
let result = [];
function generateSettingsList(settingsObj, parentKey = '') {
let result = [];
for (let key in settingsObj) {
if (typeof settingsObj[key] === 'object' && settingsObj[key] !== null) {
const nestedItems = generateSettingsList(settingsObj[key], `${parentKey}${key}.`);
if (nestedItems.length > 1) {
result.push(`${parentKey}${key}: { ${nestedItems.join(', ')} }`);
} else {
result.push(...nestedItems);
}
} else {
result.push(`${parentKey}${key}`);
}
}
return result;
}
for (let key in settingsObj) {
if (typeof settingsObj[key] === 'object' && settingsObj[key] !== null) {
const nestedItems = generateSettingsList(settingsObj[key], `${parentKey}${key}.`);
if (nestedItems.length > 1) {
result.push(`${parentKey}${key}: { ${nestedItems.join(', ')} }`);
} else {
result.push(...nestedItems);
}
} else {
result.push(`${parentKey}${key}`);
}
}
const possibleSettingsList = generateSettingsList(initialSettings);
return result;
}
const possibleSettingsList = generateSettingsList(initialSettings);
const handleCloseModal = () => {
setIsModalOpen(false);
setBoardSettingsJson(JSON.stringify(initialSettings, null, 2));
};
const handleCloseModal = () => {
setIsModalOpen(false);
setBoardSettingsJson(JSON.stringify(initialSettings, null, 2));
};
const openModal = () => {
setIsModalOpen(true);
setBoardSettingsJson(JSON.stringify(generateSettingsFromSubplebbit(subplebbit), null, 2));
};
const openModal = () => {
setIsModalOpen(true);
setBoardSettingsJson(JSON.stringify(generateSettingsFromSubplebbit(subplebbit), null, 2));
};
return (
<>
<StyledModal
isOpen={isModalOpen}
onRequestClose={handleCloseModal}
contentLabel="Board Settings"
style={{ overlay: { backgroundColor: "rgba(0,0,0,.25)" }}}
selectedStyle={selectedStyle}
>
<div className="panel-board">
<div className="panel-header">
Board Settings
<Link to="" onClick={handleCloseModal}>
<span className="icon" title="close" />
</Link>
</div>
<div className="settings-info">
<div>
<strong>Allowed settings: </strong>
<span>
{`{ ${possibleSettingsList.join(', ')} }`}
</span>
</div>
<strong style={{marginTop: '10px', display: 'inline-block'}}>API docs: </strong><a style={{color: 'inherit'}} href="https://github.com/plebbit/plebbit-js#readme" target="_blank" rel="noreferrer">https://github.com/plebbit/plebbit-js#readme</a>
</div>
<textarea
value={boardSettingsJson}
onChange={e => setBoardSettingsJson(e.target.value)}
className="board-settings"
autoComplete="off"
autoCorrect="off"
spellCheck="false"
/>
<div className="button-group">
<button id="reset-board-settings" onClick={handleResetChanges}>Reset</button>
<button id="save-board-settings" onClick={handleSaveChanges}>Save Changes</button>
</div>
</div>
</StyledModal>
 [
<span id="subscribe" style={{ cursor: 'pointer' }}>
<span
onClick={() => {
window.electron && window.electron.isElectron
? openModal()
: alert(
'To edit this board you must be using the plebchan desktop app, which is a plebbit full node that seeds the board automatically.\n\nDownload plebchan here:\n\nhttps://github.com/plebbit/plebchan/releases/latest'
);
}}
>
Settings
</span>
</span>
]
</>
);
return (
<>
<StyledModal
isOpen={isModalOpen}
onRequestClose={handleCloseModal}
contentLabel='Board Settings'
style={{ overlay: { backgroundColor: 'rgba(0,0,0,.25)' } }}
selectedStyle={selectedStyle}
>
<div className='panel-board'>
<div className='panel-header'>
Board Settings
<Link to='' onClick={handleCloseModal}>
<span className='icon' title='close' />
</Link>
</div>
<div className='settings-info'>
<div>
<strong>Allowed settings: </strong>
<span>{`{ ${possibleSettingsList.join(', ')} }`}</span>
</div>
<strong style={{ marginTop: '10px', display: 'inline-block' }}>API docs: </strong>
<a style={{ color: 'inherit' }} href='https://github.com/plebbit/plebbit-js#readme' target='_blank' rel='noreferrer'>
https://github.com/plebbit/plebbit-js#readme
</a>
</div>
<textarea
value={boardSettingsJson}
onChange={(e) => setBoardSettingsJson(e.target.value)}
className='board-settings'
autoComplete='off'
autoCorrect='off'
spellCheck='false'
/>
<div className='button-group'>
<button id='reset-board-settings' onClick={handleResetChanges}>
Reset
</button>
<button id='save-board-settings' onClick={handleSaveChanges}>
Save Changes
</button>
</div>
</div>
</StyledModal>
 [
<span id='subscribe' style={{ cursor: 'pointer' }}>
<span
onClick={() => {
window.electron && window.electron.isElectron
? openModal()
: alert(
'To edit this board you must be using the plebchan desktop app, which is a plebbit full node that seeds the board automatically.\n\nDownload plebchan here:\n\nhttps://github.com/plebbit/plebchan/releases/latest',
);
}}
>
Settings
</span>
</span>
]
</>
);
};
export default BoardSettings;
export default BoardSettings;
+65 -53
View File
@@ -1,62 +1,74 @@
import React, { useState } from "react";
import { useSubplebbit, useSubplebbitStats } from "@plebbit/plebbit-react-hooks/dist";
import { BoardStatsContainer } from "./styled/BoardStats.styled";
import { Break } from "./styled/views/Board.styled";
import React, { useState } from 'react';
import { useSubplebbit, useSubplebbitStats } from '@plebbit/plebbit-react-hooks/dist';
import { BoardStatsContainer } from './styled/BoardStats.styled';
import { Break } from './styled/views/Board.styled';
import useGeneralStore from '../hooks/stores/useGeneralStore';
const BoardStats = ({ subplebbitAddress }) => {
const { selectedStyle } = useGeneralStore(state => state);
const stats = useSubplebbitStats({subplebbitAddress});
const [showStats, setShowStats] = useState(true);
const subplebbit = useSubplebbit({subplebbitAddress});
const { selectedStyle } = useGeneralStore((state) => state);
const stats = useSubplebbitStats({ subplebbitAddress });
const [showStats, setShowStats] = useState(true);
const subplebbit = useSubplebbit({ subplebbitAddress });
const handleToggleStats = () => {
setShowStats(!showStats);
};
const handleToggleStats = () => {
setShowStats(!showStats);
};
const unixToMMDDYYYY = (timestamp) => {
const date = new Date(timestamp * 1000);
const month = ('0' + (date.getMonth() + 1)).slice(-2);
const day = ('0' + date.getDate()).slice(-2);
const year = date.getFullYear().toString().slice(-2);
return month + '/' + day + '/' + year;
};
const unixToMMDDYYYY = (timestamp) => {
const date = new Date(timestamp * 1000);
const month = ('0' + (date.getMonth() + 1)).slice(-2);
const day = ('0' + date.getDate()).slice(-2);
const year = date.getFullYear().toString().slice(-2);
return month + '/' + day + '/' + year;
};
const pluralize = (count, singular, plural) => count === 1 ? singular : plural;
const pluralize = (count, singular, plural) => (count === 1 ? singular : plural);
return (
<BoardStatsContainer selectedStyle={selectedStyle}>
<Break selectedStyle={selectedStyle} style={{width: '468px'}}/>
<table id="blotter">
{showStats && (
<tbody id="blotter-msgs">
<tr>
<td>In the past hour, <span id="stat-number">{stats.hourActiveUserCount}</span> {pluralize(stats.hourActiveUserCount, 'user', 'users')} made <span id="stat-number">{stats.hourPostCount}</span> {pluralize(stats.hourPostCount, 'post', 'posts')} / in the past day, <span id="stat-number">{stats.dayActiveUserCount}</span> {pluralize(stats.dayActiveUserCount, 'user', 'users')} made <span id="stat-number">{stats.dayPostCount}</span> {pluralize(stats.dayPostCount, 'post', 'posts')}</td>
</tr>
<tr>
<td>In the past week, <span id="stat-number">{stats.weekActiveUserCount}</span> {pluralize(stats.weekActiveUserCount, 'user', 'users')} made <span id="stat-number">{stats.weekPostCount}</span> {pluralize(stats.weekPostCount, 'post', 'posts')} / in the past month, <span id="stat-number">{stats.monthActiveUserCount}</span> {pluralize(stats.monthActiveUserCount, 'user', 'users')} made <span id="stat-number">{stats.monthPostCount}</span> {pluralize(stats.monthPostCount, 'post', 'posts')}</td>
</tr>
<tr>
<td>{unixToMMDDYYYY(subplebbit.createdAt)} board created / since then, <span id="stat-number">{stats.allActiveUserCount}</span> {pluralize(stats.allActiveUserCount, 'user', 'users')} have made <span id="stat-number">{stats.allPostCount}</span> {pluralize(stats.allPostCount, 'post', 'posts')}</td>
</tr>
</tbody>
)}
<tfoot>
<tr>
<td colSpan={2}>
[
<span id="stat-number" className="hide-button" onClick={handleToggleStats}>
{showStats ? 'Hide' : 'Show Stats'}
</span>
]
</td>
</tr>
</tfoot>
</table>
</BoardStatsContainer>
);
return (
<BoardStatsContainer selectedStyle={selectedStyle}>
<Break selectedStyle={selectedStyle} style={{ width: '468px' }} />
<table id='blotter'>
{showStats && (
<tbody id='blotter-msgs'>
<tr>
<td>
In the past hour, <span id='stat-number'>{stats.hourActiveUserCount}</span> {pluralize(stats.hourActiveUserCount, 'user', 'users')} made{' '}
<span id='stat-number'>{stats.hourPostCount}</span> {pluralize(stats.hourPostCount, 'post', 'posts')} / in the past day,{' '}
<span id='stat-number'>{stats.dayActiveUserCount}</span> {pluralize(stats.dayActiveUserCount, 'user', 'users')} made{' '}
<span id='stat-number'>{stats.dayPostCount}</span> {pluralize(stats.dayPostCount, 'post', 'posts')}
</td>
</tr>
<tr>
<td>
In the past week, <span id='stat-number'>{stats.weekActiveUserCount}</span> {pluralize(stats.weekActiveUserCount, 'user', 'users')} made{' '}
<span id='stat-number'>{stats.weekPostCount}</span> {pluralize(stats.weekPostCount, 'post', 'posts')} / in the past month,{' '}
<span id='stat-number'>{stats.monthActiveUserCount}</span> {pluralize(stats.monthActiveUserCount, 'user', 'users')} made{' '}
<span id='stat-number'>{stats.monthPostCount}</span> {pluralize(stats.monthPostCount, 'post', 'posts')}
</td>
</tr>
<tr>
<td>
{unixToMMDDYYYY(subplebbit.createdAt)} board created / since then, <span id='stat-number'>{stats.allActiveUserCount}</span>{' '}
{pluralize(stats.allActiveUserCount, 'user', 'users')} have made <span id='stat-number'>{stats.allPostCount}</span>{' '}
{pluralize(stats.allPostCount, 'post', 'posts')}
</td>
</tr>
</tbody>
)}
<tfoot>
<tr>
<td colSpan={2}>
[
<span id='stat-number' className='hide-button' onClick={handleToggleStats}>
{showStats ? 'Hide' : 'Show Stats'}
</span>
]
</td>
</tr>
</tfoot>
</table>
</BoardStatsContainer>
);
};
export default BoardStats;
export default BoardStats;
+39 -41
View File
@@ -2,50 +2,48 @@ import React from 'react';
import ContentLoader from 'react-content-loader';
import useGeneralStore from '../hooks/stores/useGeneralStore';
const SingleRectLoader = () => {
const selectedStyle = useGeneralStore((state) => state.selectedStyle);
const backgroundColor = selectedStyle === 'Tomorrow' ? '#333' : '#f3f3f3';
const foregroundColor = selectedStyle === 'Tomorrow' ? '#555' : '#ecebeb';
return (
<ContentLoader
speed={2}
width={150}
height={215}
viewBox="0 0 150 215"
backgroundColor={backgroundColor}
foregroundColor={foregroundColor}
style={{
width: '150px',
height: '215px',
marginRight: '30px',
marginBottom: '30px',
}}
>
<rect x={0} y={0} width="150" height="150" />
<rect x={0} y={170} width="150" height="18" />
<rect x={0} y={195} width="80" height="20" />
</ContentLoader>
);
};
const selectedStyle = useGeneralStore((state) => state.selectedStyle);
const backgroundColor = selectedStyle === 'Tomorrow' ? '#333' : '#f3f3f3';
const foregroundColor = selectedStyle === 'Tomorrow' ? '#555' : '#ecebeb';
return (
<ContentLoader
speed={2}
width={150}
height={215}
viewBox='0 0 150 215'
backgroundColor={backgroundColor}
foregroundColor={foregroundColor}
style={{
width: '150px',
height: '215px',
marginRight: '30px',
marginBottom: '30px',
}}
>
<rect x={0} y={0} width='150' height='150' />
<rect x={0} y={170} width='150' height='18' />
<rect x={0} y={195} width='80' height='20' />
</ContentLoader>
);
};
const CatalogLoader = () => {
return (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
boxSizing: 'border-box',
}}
>
{[...Array(24)].map((_, index) => (
<SingleRectLoader key={index} />
))}
</div>
);
return (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
boxSizing: 'border-box',
}}
>
{[...Array(24)].map((_, index) => (
<SingleRectLoader key={index} />
))}
</div>
);
};
export default CatalogLoader;
export default CatalogLoader;
+57 -61
View File
@@ -1,71 +1,67 @@
import React, { useState } from "react";
import { useComment, useEditedComment } from "@plebbit/plebbit-react-hooks";
import { Link } from "react-router-dom";
import OriginalCommentModal from "./modals/OriginalCommentModal";
import getDate from "../utils/getDate";
import useGeneralStore from "../hooks/stores/useGeneralStore";
import React, { useState } from 'react';
import { useComment, useEditedComment } from '@plebbit/plebbit-react-hooks';
import { Link } from 'react-router-dom';
import OriginalCommentModal from './modals/OriginalCommentModal';
import getDate from '../utils/getDate';
import useGeneralStore from '../hooks/stores/useGeneralStore';
const EditLabel = ({ commentCid, className }) => {
const { editedComments, setEditedComments } = useGeneralStore(state => state);
const [isOriginalCommentModalOpen, setIsOriginalCommentModalOpen] = useState(false);
const comment = useComment({commentCid});
const timestamp = getDate(comment.edit?.timestamp);
const {state: editedCommentState, editedComment} = useEditedComment({comment});
const { editedComments, setEditedComments } = useGeneralStore((state) => state);
const [isOriginalCommentModalOpen, setIsOriginalCommentModalOpen] = useState(false);
const comment = useComment({ commentCid });
const timestamp = getDate(comment.edit?.timestamp);
const { state: editedCommentState, editedComment } = useEditedComment({ comment });
if (editedCommentState === 'pending' && !(commentCid in editedComments)) {
setEditedComments({...editedComments, [commentCid]: editedComment})
}
if (editedCommentState === 'pending' && !(commentCid in editedComments)) {
setEditedComments({ ...editedComments, [commentCid]: editedComment });
}
const conditionsCheck = () => {
let conditions = [];
const conditionsCheck = () => {
let conditions = [];
if (editedComment?.removed && !comment.removed) {
conditions.push("removal");
}
if (editedComment?.edit && !comment.edit) {
conditions.push("edit");
}
if (editedComment?.locked && !comment.locked) {
conditions.push("lock");
}
if (editedComment?.pinned && !comment.pinned) {
conditions.push("sticky");
}
if (editedComment?.removed && !comment.removed) {
conditions.push('removal');
}
if (editedComment?.edit && !comment.edit) {
conditions.push('edit');
}
if (editedComment?.locked && !comment.locked) {
conditions.push('lock');
}
if (editedComment?.pinned && !comment.pinned) {
conditions.push('sticky');
}
return conditions.length > 0 ? conditions.join(', ') : null;
};
return conditions.length > 0 ? conditions.join(', ') : null;
};
const conditionsString = conditionsCheck();
const conditionsString = conditionsCheck();
return (
<>
<OriginalCommentModal
isOpen={isOriginalCommentModalOpen}
closeModal={() => setIsOriginalCommentModalOpen(false)}
comment={comment}/>
{comment.edit && (
<>
<br />
<span className={className}>
(Edited at {timestamp}, <Link className="ttl-link" onClick={
() => setIsOriginalCommentModalOpen(true)
}>show original</Link>)
</span>
</>
)}
{((editedCommentState === 'pending' || editedCommentState === 'failed') && conditionsString) ? (
<>
<br />
<span className={className}>
({editedCommentState === 'pending' ? 'Pending' : 'Failed'} {conditionsString})
</span>
</>
) : null}
</>
);
return (
<>
<OriginalCommentModal isOpen={isOriginalCommentModalOpen} closeModal={() => setIsOriginalCommentModalOpen(false)} comment={comment} />
{comment.edit && (
<>
<br />
<span className={className}>
(Edited at {timestamp},{' '}
<Link className='ttl-link' onClick={() => setIsOriginalCommentModalOpen(true)}>
show original
</Link>
)
</span>
</>
)}
{(editedCommentState === 'pending' || editedCommentState === 'failed') && conditionsString ? (
<>
<br />
<span className={className}>
({editedCommentState === 'pending' ? 'Pending' : 'Failed'} {conditionsString})
</span>
</>
) : null}
</>
);
};
export default EditLabel;
export default EditLabel;
+210 -185
View File
@@ -1,96 +1,100 @@
const Embed = ({url}) => {
const parsedUrl = new URL(url);
const Embed = ({ url }) => {
const parsedUrl = new URL(url);
if (youtubeHosts.has(parsedUrl.host)) {
return <YoutubeEmbed parsedUrl={parsedUrl} />;
}
if (twitterHosts.has(parsedUrl.host)) {
return <TwitterEmbed parsedUrl={parsedUrl} />;
}
if (redditHosts.has(parsedUrl.host)) {
return <RedditEmbed parsedUrl={parsedUrl} />;
}
if (twitchHosts.has(parsedUrl.host)) {
return <TwitchEmbed parsedUrl={parsedUrl} />;
}
if (tiktokHosts.has(parsedUrl.host)) {
return <TiktokEmbed parsedUrl={parsedUrl} />;
}
if (instagramHosts.has(parsedUrl.host)) {
return <InstagramEmbed parsedUrl={parsedUrl} />;
}
if (odyseeHosts.has(parsedUrl.host)) {
return <OdyseeEmbed parsedUrl={parsedUrl} />;
}
if (bitchuteHosts.has(parsedUrl.host)) {
return <BitchuteEmbed parsedUrl={parsedUrl} />;
}
if (streamableHosts.has(parsedUrl.host)) {
return <StreamableEmbed parsedUrl={parsedUrl} />;
}
if (spotifyHosts.has(parsedUrl.host)) {
return <SpotifyEmbed parsedUrl={parsedUrl} />;
}
if (youtubeHosts.has(parsedUrl.host)) {
return <YoutubeEmbed parsedUrl={parsedUrl} />;
}
if (twitterHosts.has(parsedUrl.host)) {
return <TwitterEmbed parsedUrl={parsedUrl} />;
}
if (redditHosts.has(parsedUrl.host)) {
return <RedditEmbed parsedUrl={parsedUrl} />;
}
if (twitchHosts.has(parsedUrl.host)) {
return <TwitchEmbed parsedUrl={parsedUrl} />;
}
if (tiktokHosts.has(parsedUrl.host)) {
return <TiktokEmbed parsedUrl={parsedUrl} />;
}
if (instagramHosts.has(parsedUrl.host)) {
return <InstagramEmbed parsedUrl={parsedUrl} />;
}
if (odyseeHosts.has(parsedUrl.host)) {
return <OdyseeEmbed parsedUrl={parsedUrl} />;
}
if (bitchuteHosts.has(parsedUrl.host)) {
return <BitchuteEmbed parsedUrl={parsedUrl} />;
}
if (streamableHosts.has(parsedUrl.host)) {
return <StreamableEmbed parsedUrl={parsedUrl} />;
}
if (spotifyHosts.has(parsedUrl.host)) {
return <SpotifyEmbed parsedUrl={parsedUrl} />;
}
};
const youtubeHosts = new Set(['youtube.com', 'www.youtube.com', 'youtu.be', 'www.youtu.be']);
const YoutubeEmbed = ({parsedUrl}) => {
let youtubeId;
if (parsedUrl.host.endsWith('youtu.be')) {
youtubeId = parsedUrl.pathname.replaceAll('/', '');
}
else {
youtubeId = parsedUrl.searchParams.get('v');
}
return <iframe
className='enlarged youtube-embed'
height="100%"
width="100%"
frameborder="0"
credentialless
referrerpolicy='no-referrer'
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
title={parsedUrl.href}
src={`https://www.youtube-nocookie.com/embed/${youtubeId}`}
/>;
const YoutubeEmbed = ({ parsedUrl }) => {
let youtubeId;
if (parsedUrl.host.endsWith('youtu.be')) {
youtubeId = parsedUrl.pathname.replaceAll('/', '');
} else {
youtubeId = parsedUrl.searchParams.get('v');
}
return (
<iframe
className='enlarged youtube-embed'
height='100%'
width='100%'
frameborder='0'
credentialless
referrerpolicy='no-referrer'
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
allowfullscreen
title={parsedUrl.href}
src={`https://www.youtube-nocookie.com/embed/${youtubeId}`}
/>
);
};
const twitterHosts = new Set(['twitter.com', 'www.twitter.com', 'x.com', 'www.x.com']);
const TwitterEmbed = ({parsedUrl}) => {
return <iframe
className='enlarged twitter-embed'
height="100%"
width="100%"
frameborder="0"
credentialless
referrerpolicy='no-referrer'
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share"
title={parsedUrl.href}
srcdoc={`
const TwitterEmbed = ({ parsedUrl }) => {
return (
<iframe
className='enlarged twitter-embed'
height='100%'
width='100%'
frameborder='0'
credentialless
referrerpolicy='no-referrer'
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
title={parsedUrl.href}
srcdoc={`
<blockquote class="twitter-tweet" data-theme="dark">
<a href="${parsedUrl.href.replace('x.com', 'twitter.com')}"></a>
</blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
`}
/>;
/>
);
};
const redditHosts = new Set(['reddit.com', 'www.reddit.com', 'old.reddit.com']);
const RedditEmbed = ({parsedUrl}) => {
return <iframe
className='enlarged reddit-embed'
height="100%"
width="100%"
frameborder="0"
credentialless
referrerpolicy='no-referrer'
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share"
title={parsedUrl.href}
srcdoc={`
const RedditEmbed = ({ parsedUrl }) => {
return (
<iframe
className='enlarged reddit-embed'
height='100%'
width='100%'
frameborder='0'
credentialless
referrerpolicy='no-referrer'
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
title={parsedUrl.href}
srcdoc={`
<style>
/* fix reddit iframe being centered */
iframe {
@@ -102,158 +106,179 @@ const RedditEmbed = ({parsedUrl}) => {
</blockquote>
<script async src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script>
`}
/>;
/>
);
};
const twitchHosts = new Set(['twitch.tv', 'www.twitch.tv']);
const TwitchEmbed = ({parsedUrl}) => {
let iframeUrl;
if (parsedUrl.pathname.startsWith('/videos/')) {
const videoId = parsedUrl.pathname.replace('/videos/', '');
iframeUrl = `https://player.twitch.tv/?video=${videoId}&parent=${window.location.hostname}`;
}
else {
const channel = parsedUrl.pathname.replaceAll('/', '');
iframeUrl = `https://player.twitch.tv/?channel=${channel}&parent=${window.location.hostname}`;
}
return <iframe
className='enlarged twitch-embed'
height="100%"
width="100%"
frameborder="0"
credentialless
referrerpolicy='no-referrer'
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
title={parsedUrl.href}
src={iframeUrl}
/>;
const TwitchEmbed = ({ parsedUrl }) => {
let iframeUrl;
if (parsedUrl.pathname.startsWith('/videos/')) {
const videoId = parsedUrl.pathname.replace('/videos/', '');
iframeUrl = `https://player.twitch.tv/?video=${videoId}&parent=${window.location.hostname}`;
} else {
const channel = parsedUrl.pathname.replaceAll('/', '');
iframeUrl = `https://player.twitch.tv/?channel=${channel}&parent=${window.location.hostname}`;
}
return (
<iframe
className='enlarged twitch-embed'
height='100%'
width='100%'
frameborder='0'
credentialless
referrerpolicy='no-referrer'
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
allowfullscreen
title={parsedUrl.href}
src={iframeUrl}
/>
);
};
const tiktokHosts = new Set(['tiktok.com', 'www.tiktok.com']);
const TiktokEmbed = ({parsedUrl}) => {
const videoId = parsedUrl.pathname.replace(/.+\/video\//, '').replaceAll('/', '');
return <iframe
className='enlarged tiktok-embed'
height="100%"
width="100%"
frameborder="0"
credentialless
referrerpolicy='no-referrer'
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share"
title={parsedUrl.href}
srcdoc={`
const TiktokEmbed = ({ parsedUrl }) => {
const videoId = parsedUrl.pathname.replace(/.+\/video\//, '').replaceAll('/', '');
return (
<iframe
className='enlarged tiktok-embed'
height='100%'
width='100%'
frameborder='0'
credentialless
referrerpolicy='no-referrer'
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
title={parsedUrl.href}
srcdoc={`
<blockquote class="tiktok-embed" data-video-id="${videoId}">
<a></a>
</blockquote>
<script async src="https://www.tiktok.com/embed.js"></script>
`}
/>;
/>
);
};
const instagramHosts = new Set(['instagram.com', 'www.instagram.com']);
const InstagramEmbed = ({parsedUrl}) => {
const pathNames = parsedUrl.pathname.replace(/\/+$/, '').split('/');
const id = pathNames[pathNames.length - 1];
return <iframe
className='enlarged instagram-embed'
height="100%"
width="100%"
frameborder="0"
credentialless
referrerpolicy='no-referrer'
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share"
title={parsedUrl.href}
srcdoc={`
const InstagramEmbed = ({ parsedUrl }) => {
const pathNames = parsedUrl.pathname.replace(/\/+$/, '').split('/');
const id = pathNames[pathNames.length - 1];
return (
<iframe
className='enlarged instagram-embed'
height='100%'
width='100%'
frameborder='0'
credentialless
referrerpolicy='no-referrer'
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
title={parsedUrl.href}
srcdoc={`
<blockquote class="instagram-media">
<a href="https://www.instagram.com/p/${id}/"></a>
</blockquote>
<script async src="//www.instagram.com/embed.js"></script>
`}
/>;
/>
);
};
const odyseeHosts = new Set(['odysee.com', 'www.odysee.com']);
const OdyseeEmbed = ({parsedUrl}) => {
const iframeUrl = `https://odysee.com/$/embed${parsedUrl.pathname}`;
return <iframe
className='enlarged odysee-embed'
height="100%"
width="100%"
frameborder="0"
credentialless
referrerpolicy='no-referrer'
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
title={parsedUrl.href}
src={iframeUrl}
/>;
const OdyseeEmbed = ({ parsedUrl }) => {
const iframeUrl = `https://odysee.com/$/embed${parsedUrl.pathname}`;
return (
<iframe
className='enlarged odysee-embed'
height='100%'
width='100%'
frameborder='0'
credentialless
referrerpolicy='no-referrer'
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
allowfullscreen
title={parsedUrl.href}
src={iframeUrl}
/>
);
};
const bitchuteHosts = new Set(['bitchute.com', 'www.bitchute.com']);
const BitchuteEmbed = ({parsedUrl}) => {
const videoId = parsedUrl.pathname.replace(/\/video\//, '').replaceAll('/', '');
return <iframe
className='enlarged bitchute-embed'
height="100%"
width="100%"
frameborder="0"
credentialless
referrerpolicy='no-referrer'
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
title={parsedUrl.href}
src={`https://www.bitchute.com/embed/${videoId}/`}
/>;
const BitchuteEmbed = ({ parsedUrl }) => {
const videoId = parsedUrl.pathname.replace(/\/video\//, '').replaceAll('/', '');
return (
<iframe
className='enlarged bitchute-embed'
height='100%'
width='100%'
frameborder='0'
credentialless
referrerpolicy='no-referrer'
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
allowfullscreen
title={parsedUrl.href}
src={`https://www.bitchute.com/embed/${videoId}/`}
/>
);
};
const streamableHosts = new Set(['streamable.com', 'www.streamable.com']);
const StreamableEmbed = ({parsedUrl}) => {
const videoId = parsedUrl.pathname.replaceAll('/', '');
return <iframe
className='enlarged streamable-embed'
height="100%"
width="100%"
frameborder="0"
credentialless
referrerpolicy='no-referrer'
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
title={parsedUrl.href}
src={`https://streamable.com/e/${videoId}`}
/>;
const StreamableEmbed = ({ parsedUrl }) => {
const videoId = parsedUrl.pathname.replaceAll('/', '');
return (
<iframe
className='enlarged streamable-embed'
height='100%'
width='100%'
frameborder='0'
credentialless
referrerpolicy='no-referrer'
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
allowfullscreen
title={parsedUrl.href}
src={`https://streamable.com/e/${videoId}`}
/>
);
};
const spotifyHosts = new Set(['spotify.com', 'www.spotify.com', 'open.spotify.com']);
const SpotifyEmbed = ({parsedUrl}) => {
const iframeUrl = `https://open.spotify.com/embed${parsedUrl.pathname}?theme=0`
return <iframe
className='enlarged spotify-embed'
height="100%"
width="100%"
frameborder="0"
credentialless
referrerpolicy='no-referrer'
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
title={parsedUrl.href}
src={iframeUrl}
/>;
const SpotifyEmbed = ({ parsedUrl }) => {
const iframeUrl = `https://open.spotify.com/embed${parsedUrl.pathname}?theme=0`;
return (
<iframe
className='enlarged spotify-embed'
height='100%'
width='100%'
frameborder='0'
credentialless
referrerpolicy='no-referrer'
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
allowfullscreen
title={parsedUrl.href}
src={iframeUrl}
/>
);
};
const canEmbedHosts = new Set([
...youtubeHosts, ...twitterHosts, ...redditHosts, ...twitchHosts,
...tiktokHosts, ...instagramHosts, ...odyseeHosts, ...bitchuteHosts,
...streamableHosts, ...spotifyHosts
...youtubeHosts,
...twitterHosts,
...redditHosts,
...twitchHosts,
...tiktokHosts,
...instagramHosts,
...odyseeHosts,
...bitchuteHosts,
...streamableHosts,
...spotifyHosts,
]);
export const canEmbed = (parsedUrl) => canEmbedHosts.has(parsedUrl.host);
export default Embed;
export default Embed;
+22 -28
View File
@@ -1,35 +1,29 @@
import React, { useEffect } from "react";
import { Link } from "react-router-dom";
import React, { useEffect } from 'react';
import { Link } from 'react-router-dom';
const ForwardRefLink = React.forwardRef((props, ref) => {
const { children, setRefAndCid, onMouseOver, onMouseLeave, onClick, ...otherProps } = props;
const { children, setRefAndCid, onMouseOver, onMouseLeave, onClick, ...otherProps } = props;
const handleClick = (event) => {
if (otherProps.to === "#void") {
event.preventDefault();
}
if (onClick) {
onClick(event);
}
};
const handleClick = (event) => {
if (otherProps.to === '#void') {
event.preventDefault();
}
if (onClick) {
onClick(event);
}
};
useEffect(() => {
if (ref.current && typeof setRefAndCid === 'function') {
setRefAndCid(ref.current);
}
}, [ref, setRefAndCid]);
useEffect(() => {
if (ref.current && typeof setRefAndCid === 'function') {
setRefAndCid(ref.current);
}
}, [ref, setRefAndCid]);
return (
<Link
ref={ref}
{...otherProps}
onMouseOver={onMouseOver}
onMouseLeave={onMouseLeave}
onClick={handleClick}
>
{children}
</Link>
);
return (
<Link ref={ref} {...otherProps} onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} onClick={handleClick}>
{children}
</Link>
);
});
export default ForwardRefLink;
export default ForwardRefLink;
+25 -29
View File
@@ -2,44 +2,40 @@ import React, { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
const ImageBanner = () => {
const [currentImage, setCurrentImage] = useState(null);
const location = useLocation();
const parentRoute = location.pathname.split('/').slice(0, 3).join('/');
const [currentImage, setCurrentImage] = useState(null);
const location = useLocation();
useEffect(() => {
let isMounted = true;
const parentRoute = location.pathname.split('/').slice(0, 3).join('/');
const loadRandomImage = async () => {
const images = await importAll(require.context('../../public/assets/banners', false, /\.(png|jpe?g|svg)$/));
const randomImage = Math.floor(Math.random() * images.length) + 1;
useEffect(() => {
let isMounted = true;
const img = new Image();
img.src = `${process.env.PUBLIC_URL}/assets/banners/banner-${randomImage}.jpg`;
const loadRandomImage = async () => {
const images = await importAll(require.context('../../public/assets/banners', false, /\.(png|jpe?g|svg)$/));
const randomImage = Math.floor(Math.random() * images.length) + 1;
img.onload = () => {
if (isMounted) {
setCurrentImage(randomImage);
}
};
};
const img = new Image();
img.src = `${process.env.PUBLIC_URL}/assets/banners/banner-${randomImage}.jpg`;
loadRandomImage();
img.onload = () => {
if (isMounted) {
setCurrentImage(randomImage);
}
};
};
return () => {
isMounted = false;
};
}, [parentRoute]);
loadRandomImage();
return (
<>
{currentImage && <img id="banner-img" src={`${process.env.PUBLIC_URL}/assets/banners/banner-${currentImage}.jpg`} alt="banner" />}
</>
);
return () => {
isMounted = false;
};
}, [parentRoute]);
return <>{currentImage && <img id='banner-img' src={`${process.env.PUBLIC_URL}/assets/banners/banner-${currentImage}.jpg`} alt='banner' />}</>;
};
export function importAll(r) {
return r.keys().map(r);
return r.keys().map(r);
}
export default ImageBanner;
export default ImageBanner;
+23 -23
View File
@@ -1,28 +1,28 @@
import React from "react";
import { useSubplebbit } from "@plebbit/plebbit-react-hooks";
import React from 'react';
import { useSubplebbit } from '@plebbit/plebbit-react-hooks';
const OfflineIndicator = ({ address, className, tooltipPlace }) => {
const subplebbit = useSubplebbit({ subplebbitAddress: address });
const isOnline = subplebbit.updatedAt > Date.now() / 1000 - 60 * 20;
const subplebbit = useSubplebbit({ subplebbitAddress: address });
const isOnline = subplebbit.updatedAt > Date.now() / 1000 - 60 * 20;
return (
<>
{!isOnline && (
<>
{" "}
<img
className={className}
alt="offline"
src="assets/offline.png"
data-tooltip-id="tooltip"
data-tooltip-content="Offline"
data-tooltip-place={tooltipPlace}
style={{imageRendering: 'pixelated'}}
/>
</>
)}
</>
);
return (
<>
{!isOnline && (
<>
{' '}
<img
className={className}
alt='offline'
src='assets/offline.png'
data-tooltip-id='tooltip'
data-tooltip-content='Offline'
data-tooltip-place={tooltipPlace}
style={{ imageRendering: 'pixelated' }}
/>
</>
)}
</>
);
};
export default OfflineIndicator;
export default OfflineIndicator;
+13 -23
View File
@@ -1,28 +1,18 @@
import React from "react";
import { useAccountComment } from "@plebbit/plebbit-react-hooks";
import React from 'react';
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
const PendingLabel = ({ commentIndex }) => {
const comment = useAccountComment({commentIndex: commentIndex});
const comment = useAccountComment({ commentIndex: commentIndex });
if (commentIndex === undefined) return null;
return (
comment.cid ? (
<span>{comment.cid.slice(2, 14)}</span>
) : (
comment.state === "pending" ? (
<span style={{color: 'red', fontWeight: '700'}}>
Pending
</span>
) : (
comment.state === "failed" ? (
<span style={{color: 'red', fontWeight: '700'}}>
Failed
</span>
) : null
)
)
);
if (commentIndex === undefined) return null;
return comment.cid ? (
<span>{comment.cid.slice(2, 14)}</span>
) : comment.state === 'pending' ? (
<span style={{ color: 'red', fontWeight: '700' }}>Pending</span>
) : comment.state === 'failed' ? (
<span style={{ color: 'red', fontWeight: '700' }}>Failed</span>
) : null;
};
export default PendingLabel;
export default PendingLabel;
+119 -121
View File
@@ -4,134 +4,132 @@ import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
import breaks from 'remark-breaks';
import ForwardRefLink from './ForwardRefLink';
const Post = ({ content, postQuoteOnClick, postQuoteOnOver, postQuoteOnLeave, postQuoteRef }) => {
const doubleNewlineContent = content?.replace(/\n/g, '&nbsp;\n\n');
const doubleNewlineContent = content?.replace(/\n/g, '&nbsp;\n\n');
const customSchema = useMemo(() => ({
...defaultSchema,
tagNames: [...defaultSchema.tagNames, 'div'],
attributes: {
...defaultSchema.attributes,
div: ['className'],
},
}), []);
const customSchema = useMemo(
() => ({
...defaultSchema,
tagNames: [...defaultSchema.tagNames, 'div'],
attributes: {
...defaultSchema.attributes,
div: ['className'],
},
}),
[],
);
const blockquoteToGreentext = () => (tree) => {
tree.children.forEach((node) => {
if (node.type === 'blockquote') {
node.children.forEach((child) => {
if (child.type === 'paragraph' && child.children.length > 0) {
const prefix = {
type: 'text',
value: '>',
};
child.children.unshift(prefix);
}
});
node.type = 'div';
node.data = {
hName: 'div',
hProperties: {
className: 'greentext',
},
};
}
});
};
const blockquoteToGreentext = () => (tree) => {
tree.children.forEach((node) => {
if (node.type === 'blockquote') {
node.children.forEach((child) => {
if (child.type === 'paragraph' && child.children.length > 0) {
const prefix = {
type: 'text',
value: '>',
};
child.children.unshift(prefix);
}
});
node.type = 'div';
node.data = {
hName: 'div',
hProperties: {
className: 'greentext',
},
};
}
});
};
const createQuotelink = (children, postQuoteOnClick, postQuoteOnOver, postQuoteOnLeave, postQuoteRef) => {
const patternC = '(c/[A-Za-z0-9]{46}|c/[A-Za-z0-9]{12})';
const patternP = '(p/([A-Za-z0-9]{52}|[A-Za-z0-9-.]*\\.eth))';
const patternU = '(u/([A-Za-z0-9]{52}|[A-Za-z0-9-.]*\\.eth))';
const patternPC = '(p/([A-Za-z0-9]{52}|[A-Za-z0-9-.]*\\.eth)/c/[A-Za-z0-9]{46})';
const regex = new RegExp(`${patternC}|${patternPC}|${patternU}|${patternP}`, 'g');
const createQuotelink = (children, postQuoteOnClick, postQuoteOnOver, postQuoteOnLeave, postQuoteRef) => {
const patternC = "(c/[A-Za-z0-9]{46}|c/[A-Za-z0-9]{12})";
const patternP = "(p/([A-Za-z0-9]{52}|[A-Za-z0-9-.]*\\.eth))";
const patternU = "(u/([A-Za-z0-9]{52}|[A-Za-z0-9-.]*\\.eth))";
const patternPC = "(p/([A-Za-z0-9]{52}|[A-Za-z0-9-.]*\\.eth)/c/[A-Za-z0-9]{46})";
const regex = new RegExp(`${patternC}|${patternPC}|${patternU}|${patternP}`, 'g');
return children?.flatMap((child, i) => {
if (typeof child !== 'string') {
return child;
}
const parts = [];
let match;
let lastIndex = 0;
while ((match = regex.exec(child)) !== null) {
const matchedText = match[0];
const index = match.index;
if (index > lastIndex) {
parts.push(child.substring(lastIndex, index));
}
const cid = matchedText.replace('c/', '');
const linkRef = React.createRef();
let linkTo = () => {};
const linkTarget = matchedText.startsWith('u/') ? "_blank" : "_self";
if (matchedText.startsWith('u/')) {
linkTo = "#void";
} else if (matchedText.startsWith('p/') || matchedText.startsWith('p/')) {
linkTo = `/${matchedText}`;
}
parts.push(
<ForwardRefLink
key={`link-${i}-${matchedText}`}
className="quotelink"
to={linkTo}
target={linkTarget}
ref={linkRef}
setRefAndCid={(ref) => {
if (typeof postQuoteRef === 'function') {
postQuoteRef(cid, ref);
}
}}
onClick={() => {postQuoteOnClick(cid)}}
onMouseOver={() => {
postQuoteOnOver(cid);
}}
onMouseLeave={() => {
postQuoteOnLeave();
}}
>
{matchedText}
</ForwardRefLink>
);
lastIndex = index + matchedText.length;
}
if (lastIndex < child.length) {
parts.push(child.substring(lastIndex));
}
return parts;
});
};
return children?.flatMap((child, i) => {
if (typeof child !== 'string') {
return child;
}
const parts = [];
let match;
let lastIndex = 0;
return (
<ReactMarkdown
children={doubleNewlineContent}
remarkPlugins={[blockquoteToGreentext, breaks]}
rehypePlugins={[[rehypeSanitize, customSchema]]}
components={{
img: ({ src }) => <span>{src}</span>,
video: ({ src }) => <span>{src}</span>,
source: ({ src }) => <span>{src}</span>,
gif: ({ src }) => <span>{src}</span>,
p: ({ children }) => <div className='custom-paragraph'>
{createQuotelink(children, postQuoteOnClick, postQuoteOnOver, postQuoteOnLeave, postQuoteRef)}
</div>,
}}
/>
);
while ((match = regex.exec(child)) !== null) {
const matchedText = match[0];
const index = match.index;
if (index > lastIndex) {
parts.push(child.substring(lastIndex, index));
}
const cid = matchedText.replace('c/', '');
const linkRef = React.createRef();
let linkTo = () => {};
const linkTarget = matchedText.startsWith('u/') ? '_blank' : '_self';
if (matchedText.startsWith('u/')) {
linkTo = '#void';
} else if (matchedText.startsWith('p/') || matchedText.startsWith('p/')) {
linkTo = `/${matchedText}`;
}
parts.push(
<ForwardRefLink
key={`link-${i}-${matchedText}`}
className='quotelink'
to={linkTo}
target={linkTarget}
ref={linkRef}
setRefAndCid={(ref) => {
if (typeof postQuoteRef === 'function') {
postQuoteRef(cid, ref);
}
}}
onClick={() => {
postQuoteOnClick(cid);
}}
onMouseOver={() => {
postQuoteOnOver(cid);
}}
onMouseLeave={() => {
postQuoteOnLeave();
}}
>
{matchedText}
</ForwardRefLink>,
);
lastIndex = index + matchedText.length;
}
if (lastIndex < child.length) {
parts.push(child.substring(lastIndex));
}
return parts;
});
};
return (
<ReactMarkdown
children={doubleNewlineContent}
remarkPlugins={[blockquoteToGreentext, breaks]}
rehypePlugins={[[rehypeSanitize, customSchema]]}
components={{
img: ({ src }) => <span>{src}</span>,
video: ({ src }) => <span>{src}</span>,
source: ({ src }) => <span>{src}</span>,
gif: ({ src }) => <span>{src}</span>,
p: ({ children }) => <div className='custom-paragraph'>{createQuotelink(children, postQuoteOnClick, postQuoteOnOver, postQuoteOnLeave, postQuoteRef)}</div>,
}}
/>
);
};
export default React.memo(Post);
export default React.memo(Post);
+28 -33
View File
@@ -3,42 +3,37 @@ import ContentLoader from 'react-content-loader';
import useGeneralStore from '../hooks/stores/useGeneralStore';
const SinglePostLoader = () => {
const selectedStyle = useGeneralStore((state) => state.selectedStyle);
const backgroundColor = selectedStyle === 'Tomorrow' ? '#333' : '#f3f3f3';
const foregroundColor = selectedStyle === 'Tomorrow' ? '#555' : '#ecebeb';
const selectedStyle = useGeneralStore((state) => state.selectedStyle);
const backgroundColor = selectedStyle === 'Tomorrow' ? '#333' : '#f3f3f3';
const foregroundColor = selectedStyle === 'Tomorrow' ? '#555' : '#ecebeb';
return (
<div style={{ paddingLeft: '30px', paddingRight: '30px', marginBottom: '50px', marginTop: '30px' }}>
<ContentLoader
width="100%"
height={15 * 3 + 30}
backgroundColor={backgroundColor}
foregroundColor={foregroundColor}
>
<rect x="0" y="8" width="100%" height="15" />
<rect x="0" y="30" width="100%" height="15" />
<rect x="0" y="52" width="100%" height="15" />
</ContentLoader>
</div>
);
return (
<div style={{ paddingLeft: '30px', paddingRight: '30px', marginBottom: '50px', marginTop: '30px' }}>
<ContentLoader width='100%' height={15 * 3 + 30} backgroundColor={backgroundColor} foregroundColor={foregroundColor}>
<rect x='0' y='8' width='100%' height='15' />
<rect x='0' y='30' width='100%' height='15' />
<rect x='0' y='52' width='100%' height='15' />
</ContentLoader>
</div>
);
};
const PostLoader = () => {
return (
<div
style={{
display: 'block',
boxSizing: 'border-box',
paddingLeft: '30px',
paddingRight: '30px',
marginBottom: '30px',
}}
>
{[...Array(5)].map((_, index) => (
<SinglePostLoader key={index} />
))}
</div>
);
return (
<div
style={{
display: 'block',
boxSizing: 'border-box',
paddingLeft: '30px',
paddingRight: '30px',
marginBottom: '30px',
}}
>
{[...Array(5)].map((_, index) => (
<SinglePostLoader key={index} />
))}
</div>
);
};
export default PostLoader;
export default PostLoader;
+292 -317
View File
@@ -1,321 +1,296 @@
import React, { Fragment } from "react";
import { Link } from "react-router-dom";
import { useAccount, useComment } from "@plebbit/plebbit-react-hooks";
import getDate from "../utils/getDate";
import getCommentMediaInfo from "../utils/getCommentMediaInfo";
import findShortParentCid from "../utils/findShortParentCid";
import Post from "./Post";
import EditLabel from "./EditLabel";
import useStateString from "../hooks/useStateString";
import { BoardForm, Container } from "./styled/views/Board.styled";
import useGeneralStore from "../hooks/stores/useGeneralStore";
import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';
import { useAccount, useComment } from '@plebbit/plebbit-react-hooks';
import getDate from '../utils/getDate';
import getCommentMediaInfo from '../utils/getCommentMediaInfo';
import findShortParentCid from '../utils/findShortParentCid';
import Post from './Post';
import EditLabel from './EditLabel';
import useStateString from '../hooks/useStateString';
import { BoardForm, Container } from './styled/views/Board.styled';
import useGeneralStore from '../hooks/stores/useGeneralStore';
const PostOnHover = ({ cid, feed }) => {
const selectedStyle = useGeneralStore(state => state.selectedStyle);
const account = useAccount();
const reply = useComment({commentCid: cid});
const replyMediaInfo = getCommentMediaInfo(reply);
const fallbackImgUrl = "assets/filedeleted-res.gif";
const selectedFeed = feed;
const thread = useComment({commentCid: reply.parentCid});
const shortParentCid = findShortParentCid(reply.parentCid, selectedFeed);
const stateString = useStateString(reply);
const selectedStyle = useGeneralStore((state) => state.selectedStyle);
const account = useAccount();
const reply = useComment({ commentCid: cid });
const replyMediaInfo = getCommentMediaInfo(reply);
const fallbackImgUrl = 'assets/filedeleted-res.gif';
const selectedFeed = feed;
const thread = useComment({ commentCid: reply.parentCid });
const shortParentCid = findShortParentCid(reply.parentCid, selectedFeed);
const stateString = useStateString(reply);
return (
<Container selectedStyle={selectedStyle} style={{
margin: '0',
padding: '0',
whiteSpace: 'normal',
maxWidth: '100vw',
overflowWrap: 'break-word',
wordWrap: 'break-word',
wordBreak: 'break-all',
boxSizing: 'border-box',
}}>
<BoardForm selectedStyle={selectedStyle} style={{margin: '0', padding: '0'}}>
<div className="board" style={{margin: '0', padding: '0'}}>
<div className="thread" style={{margin: '0', padding: '0'}}>
{reply.state === 'succeeded' ? (
<div className="reply-container">
<div className="post-reply post-reply-desktop">
<div className="post-info">
<span className="nameblock">
{reply.author?.displayName
? reply.author?.displayName.length > 20
? <Fragment >
<span className="name"
data-tooltip-id="tooltip"
data-tooltip-content={reply.author?.displayName}
data-tooltip-place="top">
{reply.author?.displayName.slice(0, 20) + " (...)"}
</span>
</Fragment>
: <span className="name">
{reply.author?.displayName}</span>
: <span className="name">
Anonymous</span>}
&nbsp;
<span className="poster-address address-desktop"
id="reply-button" style={{cursor: "pointer"}}
>
(u/
{reply.author?.shortAddress ?
(
<span >
{reply.author?.shortAddress}
</span>
) : (
<span >
{account?.author?.shortAddress}
</span>
)
}
)
</span>
</span>
&nbsp;
<span className="date-time" data-utc="data">{getDate(reply.timestamp)}</span>
&nbsp;
<span className="post-number post-number-desktop">
<span >c/</span>
<Link to={() => {}} id="reply-button"
title="Reply to this post">{reply.shortCid}</Link>
</span>&nbsp;
<div id="backlink-id" className="backlink">
{reply.replies?.pages?.topAll.comments
.sort((a, b) => a.timestamp - b.timestamp)
.map((reply, index) => (
<div key={`div-${index}`} style={{display: 'inline-block'}}>
<Link key={`link-${index}`} to={() => {}}
className="quote-link">
c/{reply.shortCid}</Link>
&nbsp;
</div>
))
}
</div>
</div>
{replyMediaInfo?.url ? (
<div className="file"
style={{marginBottom: "5px"}}>
<div className="reply-file-text">
Link:&nbsp;
<a href={replyMediaInfo.url} target="_blank"
rel="noopener noreferrer">{
replyMediaInfo?.url.length > 30 ?
replyMediaInfo?.url.slice(0, 30) + "(...)" :
replyMediaInfo?.url
}</a>&nbsp;({replyMediaInfo?.type})
</div>
{replyMediaInfo?.type === "webpage" ? (
<div className="img-container">
<span className="file-thumb-reply">
{reply.thumbnailUrl ? (
<img
src={replyMediaInfo.thumbnail} alt={replyMediaInfo.type}
style={{cursor: "pointer"}}
onError={(e) => e.target.src = fallbackImgUrl} />
) : null}
</span>
</div>
) : null}
{replyMediaInfo?.type === "image" ? (
<div className="img-container">
<span className="file-thumb-reply">
<img
src={replyMediaInfo.url} alt={replyMediaInfo.type}
style={{cursor: "pointer"}}
onError={(e) => e.target.src = fallbackImgUrl} />
</span>
</div>
) : null}
{replyMediaInfo?.type === "video" ? (
<span className="file-thumb-reply">
<video controls
src={replyMediaInfo.url} alt={replyMediaInfo.type}
onError={(e) => e.target.src = fallbackImgUrl} />
</span>
) : null}
{replyMediaInfo?.type === "audio" ? (
<span className="file-thumb-reply">
<audio controls
src={replyMediaInfo.url} alt={replyMediaInfo.type}
onError={(e) => e.target.src = fallbackImgUrl} />
</span>
) : null}
</div>
) : null}
{reply.content ? (
reply.content?.length > 500 ?
<Fragment >
<blockquote comment={reply} className="post-message">
{shortParentCid ? (
<Link to={() => {}} className="quotelink">
{`c/${shortParentCid}`}{shortParentCid === thread.shortCid ? " (OP)" : null}
</Link>
) : null}
<Post content={reply.content?.slice(0, 500)} />
<span className="ttl"> (...)
<br /> <EditLabel
commentCid={reply.cid}
className="ttl"/><br />
Comment too long. Click the link to view.</span>
</blockquote>
</Fragment>
: <blockquote className="post-message">
{shortParentCid ? (
<Link to={() => {}} className="quotelink">
{`c/${shortParentCid}`}{shortParentCid === thread.shortCid ? " (OP)" : null}
</Link>
) : null}
<Post content={reply.content} comment={reply} />
<EditLabel
commentCid={reply.cid}
className="ttl"/>
</blockquote>)
: null}
</div>
</div>
) : (
<div className="reply-container">
<span className="ellipsis">{stateString}</span>
</div>
)}
</div>
<div className="thread-mobile">
{reply.state === 'succeeded' ? (
<div className="reply-container">
<div className="post-reply post-reply-mobile">
<div className="post-info-mobile">
<span className="name-block-mobile">
{reply.author?.displayName
? reply.author?.displayName.length > 20
? <Fragment>
<span className="name-mobile">
{reply.author?.displayName.slice(0, 20) + " (...)"}
</span>
</Fragment>
: <span className="name-mobile">
{reply.author?.displayName}</span>
: <span className="name-mobile">
Anonymous</span>}
&nbsp;
<span className="poster-address-mobile address-mobile"
id="reply-button" style={{cursor: "pointer"}}
>
(u/
{reply.author?.shortAddress ?
(
<span className="highlight-address-mobile">
{reply.author?.shortAddress}
</span>
) : (
<span>
{account?.author?.shortAddress}
</span>
)
}
)&nbsp;
</span>
<br />
</span>
<span className="date-time-mobile post-number-mobile">
{getDate(reply.timestamp)}&nbsp;
<span>c/</span>
<Link to={() => {}} id="reply-button"
>{reply.shortCid}
</Link>
</span>
</div>
{reply.link ? (
<div className="file-mobile">
{replyMediaInfo?.url ? (
replyMediaInfo.type === "webpage" ? (
<div className="img-container">
<span className="file-thumb-mobile">
{reply.thumbnailUrl ? (
<img
src={replyMediaInfo.thumbnail} alt="thumbnail"
style={{cursor: "pointer"}}
onError={(e) => e.target.src = fallbackImgUrl} />
) : null}
<div className="file-info-mobile">{replyMediaInfo.type}</div>
</span>
</div>
) : replyMediaInfo.type === "image" ? (
<div className="img-container">
<span className="file-thumb-mobile">
<img
src={replyMediaInfo.url} alt={replyMediaInfo.type}
style={{cursor: "pointer"}}
onError={(e) => e.target.src = fallbackImgUrl} />
<div className="file-info-mobile">{replyMediaInfo.type}</div>
</span>
</div>
) : replyMediaInfo.type === "video" ? (
<span className="file-thumb-mobile">
<video
src={replyMediaInfo.url} alt={replyMediaInfo.type}
style={{ pointerEvents: "none" }}
onError={(e) => e.target.src = fallbackImgUrl} />
<div className="file-info-mobile">{replyMediaInfo.type}</div>
</span>
) : replyMediaInfo.type === "audio" ? (
<span className="file-thumb-mobile">
<audio
src={replyMediaInfo.url} alt={replyMediaInfo.type}
onError={(e) => e.target.src = fallbackImgUrl} />
<div className="file-info-mobile">{replyMediaInfo.type}</div>
</span>
) : null
) : null}
</div>
) : null}
{reply.content ? (
reply.content?.length > 500 ?
<Fragment>
<blockquote className="post-message">
{shortParentCid ? (
<Link to={() => {}} className="quotelink">
{`c/${shortParentCid}`}{shortParentCid === thread.shortCid ? " (OP)" : null}
</Link>
) : null}
<Post content={reply.content?.slice(0, 500)} comment={reply} />
<span className="ttl"> (...)
<br />
<EditLabel
commentCid={reply.cid}
className="ttl"/>
<br />
Comment too long. Click the link to view. </span>
</blockquote>
</Fragment>
: <blockquote className="post-message">
{shortParentCid ? (
<Link to={() => {}} className="quotelink" >
{`c/${shortParentCid}`}{shortParentCid === thread.shortCid ? " (OP)" : null}
</Link>
) : null}
<Post content={reply.content} comment={reply} />
<EditLabel
commentCid={reply.cid}
className="ttl"/>
</blockquote>)
: null}
</div>
</div>
) : (
<div className="reply-container">
<span className="ellipsis">{stateString}</span>
</div>
)}
</div>
</div>
</BoardForm>
</Container>
);
}
return (
<Container
selectedStyle={selectedStyle}
style={{
margin: '0',
padding: '0',
whiteSpace: 'normal',
maxWidth: '100vw',
overflowWrap: 'break-word',
wordWrap: 'break-word',
wordBreak: 'break-all',
boxSizing: 'border-box',
}}
>
<BoardForm selectedStyle={selectedStyle} style={{ margin: '0', padding: '0' }}>
<div className='board' style={{ margin: '0', padding: '0' }}>
<div className='thread' style={{ margin: '0', padding: '0' }}>
{reply.state === 'succeeded' ? (
<div className='reply-container'>
<div className='post-reply post-reply-desktop'>
<div className='post-info'>
<span className='nameblock'>
{reply.author?.displayName ? (
reply.author?.displayName.length > 20 ? (
<Fragment>
<span className='name' data-tooltip-id='tooltip' data-tooltip-content={reply.author?.displayName} data-tooltip-place='top'>
{reply.author?.displayName.slice(0, 20) + ' (...)'}
</span>
</Fragment>
) : (
<span className='name'>{reply.author?.displayName}</span>
)
) : (
<span className='name'>Anonymous</span>
)}
&nbsp;
<span className='poster-address address-desktop' id='reply-button' style={{ cursor: 'pointer' }}>
(u/
{reply.author?.shortAddress ? <span>{reply.author?.shortAddress}</span> : <span>{account?.author?.shortAddress}</span>})
</span>
</span>
&nbsp;
<span className='date-time' data-utc='data'>
{getDate(reply.timestamp)}
</span>
&nbsp;
<span className='post-number post-number-desktop'>
<span>c/</span>
<Link to={() => {}} id='reply-button' title='Reply to this post'>
{reply.shortCid}
</Link>
</span>
&nbsp;
<div id='backlink-id' className='backlink'>
{reply.replies?.pages?.topAll.comments
.sort((a, b) => a.timestamp - b.timestamp)
.map((reply, index) => (
<div key={`div-${index}`} style={{ display: 'inline-block' }}>
<Link key={`link-${index}`} to={() => {}} className='quote-link'>
c/{reply.shortCid}
</Link>
&nbsp;
</div>
))}
</div>
</div>
{replyMediaInfo?.url ? (
<div className='file' style={{ marginBottom: '5px' }}>
<div className='reply-file-text'>
Link:&nbsp;
<a href={replyMediaInfo.url} target='_blank' rel='noopener noreferrer'>
{replyMediaInfo?.url.length > 30 ? replyMediaInfo?.url.slice(0, 30) + '(...)' : replyMediaInfo?.url}
</a>
&nbsp;({replyMediaInfo?.type})
</div>
{replyMediaInfo?.type === 'webpage' ? (
<div className='img-container'>
<span className='file-thumb-reply'>
{reply.thumbnailUrl ? (
<img
src={replyMediaInfo.thumbnail}
alt={replyMediaInfo.type}
style={{ cursor: 'pointer' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : null}
</span>
</div>
) : null}
{replyMediaInfo?.type === 'image' ? (
<div className='img-container'>
<span className='file-thumb-reply'>
<img src={replyMediaInfo.url} alt={replyMediaInfo.type} style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
</span>
</div>
) : null}
{replyMediaInfo?.type === 'video' ? (
<span className='file-thumb-reply'>
<video controls src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
</span>
) : null}
{replyMediaInfo?.type === 'audio' ? (
<span className='file-thumb-reply'>
<audio controls src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
</span>
) : null}
</div>
) : null}
{reply.content ? (
reply.content?.length > 500 ? (
<Fragment>
<blockquote comment={reply} className='post-message'>
{shortParentCid ? (
<Link to={() => {}} className='quotelink'>
{`c/${shortParentCid}`}
{shortParentCid === thread.shortCid ? ' (OP)' : null}
</Link>
) : null}
<Post content={reply.content?.slice(0, 500)} />
<span className='ttl'>
{' '}
(...)
<br /> <EditLabel commentCid={reply.cid} className='ttl' />
<br />
Comment too long. Click the link to view.
</span>
</blockquote>
</Fragment>
) : (
<blockquote className='post-message'>
{shortParentCid ? (
<Link to={() => {}} className='quotelink'>
{`c/${shortParentCid}`}
{shortParentCid === thread.shortCid ? ' (OP)' : null}
</Link>
) : null}
<Post content={reply.content} comment={reply} />
<EditLabel commentCid={reply.cid} className='ttl' />
</blockquote>
)
) : null}
</div>
</div>
) : (
<div className='reply-container'>
<span className='ellipsis'>{stateString}</span>
</div>
)}
</div>
<div className='thread-mobile'>
{reply.state === 'succeeded' ? (
<div className='reply-container'>
<div className='post-reply post-reply-mobile'>
<div className='post-info-mobile'>
<span className='name-block-mobile'>
{reply.author?.displayName ? (
reply.author?.displayName.length > 20 ? (
<Fragment>
<span className='name-mobile'>{reply.author?.displayName.slice(0, 20) + ' (...)'}</span>
</Fragment>
) : (
<span className='name-mobile'>{reply.author?.displayName}</span>
)
) : (
<span className='name-mobile'>Anonymous</span>
)}
&nbsp;
<span className='poster-address-mobile address-mobile' id='reply-button' style={{ cursor: 'pointer' }}>
(u/
{reply.author?.shortAddress ? (
<span className='highlight-address-mobile'>{reply.author?.shortAddress}</span>
) : (
<span>{account?.author?.shortAddress}</span>
)}
)&nbsp;
</span>
<br />
</span>
<span className='date-time-mobile post-number-mobile'>
{getDate(reply.timestamp)}&nbsp;
<span>c/</span>
<Link to={() => {}} id='reply-button'>
{reply.shortCid}
</Link>
</span>
</div>
{reply.link ? (
<div className='file-mobile'>
{replyMediaInfo?.url ? (
replyMediaInfo.type === 'webpage' ? (
<div className='img-container'>
<span className='file-thumb-mobile'>
{reply.thumbnailUrl ? (
<img src={replyMediaInfo.thumbnail} alt='thumbnail' style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
) : null}
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
</span>
</div>
) : replyMediaInfo.type === 'image' ? (
<div className='img-container'>
<span className='file-thumb-mobile'>
<img src={replyMediaInfo.url} alt={replyMediaInfo.type} style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
</span>
</div>
) : replyMediaInfo.type === 'video' ? (
<span className='file-thumb-mobile'>
<video
src={replyMediaInfo.url}
alt={replyMediaInfo.type}
style={{ pointerEvents: 'none' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
</span>
) : replyMediaInfo.type === 'audio' ? (
<span className='file-thumb-mobile'>
<audio src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
</span>
) : null
) : null}
</div>
) : null}
{reply.content ? (
reply.content?.length > 500 ? (
<Fragment>
<blockquote className='post-message'>
{shortParentCid ? (
<Link to={() => {}} className='quotelink'>
{`c/${shortParentCid}`}
{shortParentCid === thread.shortCid ? ' (OP)' : null}
</Link>
) : null}
<Post content={reply.content?.slice(0, 500)} comment={reply} />
<span className='ttl'>
{' '}
(...)
<br />
<EditLabel commentCid={reply.cid} className='ttl' />
<br />
Comment too long. Click the link to view.{' '}
</span>
</blockquote>
</Fragment>
) : (
<blockquote className='post-message'>
{shortParentCid ? (
<Link to={() => {}} className='quotelink'>
{`c/${shortParentCid}`}
{shortParentCid === thread.shortCid ? ' (OP)' : null}
</Link>
) : null}
<Post content={reply.content} comment={reply} />
<EditLabel commentCid={reply.cid} className='ttl' />
</blockquote>
)
) : null}
</div>
</div>
) : (
<div className='reply-container'>
<span className='ellipsis'>{stateString}</span>
</div>
)}
</div>
</div>
</BoardForm>
</Container>
);
};
export default PostOnHover;
export default PostOnHover;
+20 -25
View File
@@ -1,33 +1,28 @@
import React from "react";
import { useAccountComment } from "@plebbit/plebbit-react-hooks";
import useStateString from "../hooks/useStateString";
import React from 'react';
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
import useStateString from '../hooks/useStateString';
const StateLabel = ({ commentIndex, className }) => {
const comment = useAccountComment({commentIndex: commentIndex});
const stateString = useStateString(comment);
const comment = useAccountComment({ commentIndex: commentIndex });
const stateString = useStateString(comment);
if (comment.updatedAt !== undefined || comment.index === undefined) {
return null;
}
if (comment.updatedAt !== undefined || comment.index === undefined) {
return null;
}
if (comment.state === "failed" || comment.state === "succeeded") {
return null;
}
if (comment.state === 'failed' || comment.state === 'succeeded') {
return null;
}
if (!stateString) {
return null;
}
if (!stateString) {
return null;
}
return (
<span className="ttl">
<br />
(
<span className={className}>
{stateString}
</span>
)
</span>
);
return (
<span className='ttl'>
<br />(<span className={className}>{stateString}</span>)
</span>
);
};
export default StateLabel;
export default StateLabel;
+5 -5
View File
@@ -1,11 +1,11 @@
import React from 'react';
import {useComment, useAuthorAddress} from '@plebbit/plebbit-react-hooks';
import { useComment, useAuthorAddress } from '@plebbit/plebbit-react-hooks';
function VerifiedAuthor({ commentCid, children }) {
const comment = useComment({commentCid});
const {authorAddress, shortAuthorAddress} = useAuthorAddress({comment});
const comment = useComment({ commentCid });
const { authorAddress, shortAuthorAddress } = useAuthorAddress({ comment });
return children({ authorAddress, shortAuthorAddress });
return children({ authorAddress, shortAuthorAddress });
}
export default React.memo(VerifiedAuthor);
export default React.memo(VerifiedAuthor);