perf: rewrite plebchan completely

This commit is contained in:
plebeius.eth
2024-02-27 17:58:08 +01:00
parent 8f1831b69f
commit c4a5cfec49
95 changed files with 2658 additions and 39527 deletions
+136 -9
View File
@@ -1,11 +1,138 @@
# scripts/deploy.sh env
.deploy-env
# electron builder binaries
/bin
# data created by plebbit-js
# plebbit temp files
.plebbit
bin
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
@@ -17,11 +144,9 @@
# production
/build
/dist
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
@@ -30,3 +155,5 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.vscode
-6
View File
@@ -1,6 +0,0 @@
{
"appId": "plebchan.android",
"appName": "plebchan",
"webDir": "build",
"bundledWebRuntime": false
}
+10
View File
@@ -0,0 +1,10 @@
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'plebchan.android',
appName: 'plebchan',
webDir: 'build',
bundledWebRuntime: false
};
export default config;
+41 -51
View File
@@ -5,50 +5,40 @@
"author": "plebeius.eth",
"private": true,
"dependencies": {
"@babel/core": "7.21.3",
"@babel/plugin-syntax-flow": "7.18.6",
"@babel/plugin-transform-react-jsx": "7.21.0",
"@capacitor/android": "3.6.0",
"@capacitor/app": "1.1.1",
"@capacitor/core": "3.6.0",
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#4d667f7ad7a77cfd416184acffd6f561761ebc9b",
"@testing-library/dom": "9.0.1",
"@floating-ui/react": "0.26.1",
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#a2d2e9d4dd66f71bfd55768fb30e6799a77ee197",
"@testing-library/jest-dom": "5.14.1",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.4.3",
"@types/node": "18.15.5",
"@types/react": "18.0.30",
"babel-plugin-styled-components": "2.0.7",
"@testing-library/react": "13.0.0",
"@testing-library/user-event": "13.2.1",
"@types/jest": "29.5.5",
"@types/node": "20.8.2",
"@types/react": "18.2.25",
"@types/react-dom": "18.2.10",
"cross-env": "7.0.3",
"electron-context-menu": "3.3.0",
"electron-is-dev": "2.0.0",
"eslint": "8.36.0",
"eslint-config-react-app": "7.0.1",
"ext-name": "5.0.0",
"i18next": "23.5.1",
"i18next-browser-languagedetector": "7.1.0",
"i18next-http-backend": "2.2.2",
"json-stringify-pretty-compact": "4.0.0",
"lodash": "4.17.21",
"mock-require": "3.0.3",
"postcss": "8.4.21",
"memoizee": "0.4.15",
"prettier": "3.0.3",
"react": "18.2.0",
"react-confirm-alert": "3.0.6",
"react-content-loader": "6.2.1",
"react-dom": "18.2.0",
"react-draggable": "4.4.5",
"react-helmet-async": "1.3.0",
"react-infinite-scroller": "1.2.6",
"react-is": "18.2.0",
"react-i18next": "13.2.2",
"react-markdown": "8.0.6",
"react-modal": "3.16.1",
"react-router-dom": "6.9.0",
"react-router-dom": "6.16.0",
"react-scripts": "5.0.1",
"react-toastify": "9.1.1",
"react-tooltip": "5.10.0",
"react-virtuoso": "4.5.0",
"react-virtuoso": "4.6.0",
"rehype-sanitize": "5.0.1",
"remark-breaks": "3.0.2",
"styled-components": "5.3.9",
"remark-gfm": "3.0.1",
"remark-supersub": "1.0.0",
"tcp-port-used": "1.0.2",
"web-vitals": "3.3.0",
"zustand": "4.3.6"
"typescript": "5.2.2",
"zustand": "4.4.3"
},
"scripts": {
"start": "react-scripts start",
@@ -56,6 +46,7 @@
"build-netlify": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=true REACT_APP_COMMIT_REF=$COMMIT_REF react-scripts build",
"test": "react-scripts test",
"analyze-bundle": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=true react-scripts build && source-map-explorer 'build/static/js/*.js'",
"eject": "react-scripts eject",
"electron": "yarn electron:before && electron .",
"electron:no-delete-data": "yarn electron:before:download-ipfs && electron .",
"electron:start": "concurrently \"cross-env BROWSER=none yarn start\" \"wait-on http://localhost:3000 && yarn electron\"",
@@ -67,7 +58,7 @@
"electron:before:download-ipfs": "node electron/download-ipfs",
"electron:before:delete-data": "rimraf .plebbit",
"android:build:icons": "cordova-res android --skip-config --copy --resources /tmp/plebbit-react-android-icons --icon-source ./android/icons/icon.png --splash-source ./android/icons/splash.png --icon-foreground-source ./android/icons/icon-foreground.png --icon-background-source '#ffffee'",
"prettier": "prettier {src,electron}/**/*.{js,jsx} --write",
"prettier": "prettier {src,electron}/**/*.{js,ts,tsx} --write",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0"
},
"eslintConfig": {
@@ -90,6 +81,23 @@
"last 1 safari version"
]
},
"devDependencies": {
"@capacitor/android": "3.6.0",
"@capacitor/cli": "3.6.0",
"@capacitor/core": "3.6.0",
"@types/memoizee": "0.4.9",
"concurrently": "8.0.1",
"conventional-changelog-cli": "4.1.0",
"cordova-res": "0.15.4",
"cross-env": "7.0.3",
"cz-conventional-changelog": "3.3.0",
"decompress": "4.2.1",
"electron": "19.1.8",
"electron-builder": "23.0.9",
"husky": "4.3.8",
"lint-staged": "12.3.8",
"wait-on": "7.0.1"
},
"main": "electron/main.js",
"build": {
"appId": "plebchan.desktop",
@@ -127,29 +135,11 @@
"category": "Network"
}
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
"@capacitor/cli": "3.6.0",
"concurrently": "8.0.1",
"conventional-changelog-cli": "4.1.0",
"cordova-res": "0.15.4",
"cross-env": "7.0.3",
"cz-conventional-changelog": "3.3.0",
"decompress": "4.2.1",
"electron": "19.1.8",
"electron-builder": "23.0.9",
"husky": "4.3.8",
"lint-staged": "12.3.8",
"prettier": "2.3.2",
"source-map-explorer": "2.5.3",
"typescript": "5.0.2",
"wait-on": "7.0.1"
},
"resolutions": {
"styled-components": "^5"
},
"lint-staged": {
"{src,electron}/**/*.{js,jsx}": [
"{src,electron}/**/*.{js,ts,tsx}": [
"prettier --write"
]
},
+12 -1
View File
@@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="referrer" content="no-referrer" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="serverless, adminless, decentralized 4chan alternative" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png" />
<!--
@@ -22,6 +22,17 @@
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>plebchan</title>
<script>
(function() {
// This checks if the URL does not contain a hash
if (!window.location.hash) {
// Calculate the base URL (protocol + host)
const baseURL = window.location.protocol + '//' + window.location.host + '/';
// Redirect to the base URL with `#/`
window.location.replace(baseURL + '#/');
}
})();
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
-193
View File
@@ -1,193 +0,0 @@
import React, { useEffect, useRef } from 'react';
import { Route, Routes, useLocation } from 'react-router-dom';
import { Helmet } from 'react-helmet-async';
import 'react-tooltip/dist/react-tooltip.css';
import 'react-toastify/dist/ReactToastify.css';
import useGeneralStore from './hooks/stores/useGeneralStore';
import { GlobalStyle } from './components/styled/GlobalStyle.styled';
import { Toast } from './components/styled/Toast.styled';
import All from './components/views/All';
import AllCatalog from './components/views/AllCatalog';
import Board from './components/views/Board';
import Catalog from './components/views/Catalog';
import Description from './components/views/Description';
import Home from './components/views/Home';
import NotFound from './components/views/NotFound';
import Pending from './components/views/Pending';
import Rules from './components/views/Rules';
import Subscriptions from './components/views/Subscriptions';
import SubscriptionsCatalog from './components/views/SubscriptionsCatalog';
import Thread from './components/views/Thread';
import CaptchaModal from './components/modals/CaptchaModal';
import { importAll } from './components/ImageBanner';
import preloadImages from './utils/preloadImages';
import showNewVersionToast from './utils/showNewVersionToast';
import useError from './hooks/useError';
import useInfo from './hooks/useInfo';
import useSuccess from './hooks/useSuccess';
import packageJson from '../package.json';
const commitRef = process?.env?.REACT_APP_COMMIT_REF || '';
export default function App() {
const {
bodyStyle,
setBodyStyle,
setDefaultSubplebbits,
setDefaultNsfwSubplebbits,
isCaptchaOpen,
setIsCaptchaOpen,
setIsSettingsOpen,
selectedStyle,
setShowPostForm,
setShowPostFormLink,
} = useGeneralStore((state) => state);
const location = useLocation();
const isElectron = window.electron && window.electron.isElectron;
useEffect(() => {
if (location.state?.scrollToTop) {
window.scrollTo(0, 0);
}
}, [location]);
const [, setNewErrorMessage] = useError();
const [, setNewSuccessMessage] = useSuccess();
const [, setNewInfoMessage] = useInfo();
useEffect(() => {
const successToast = localStorage.getItem('successToast');
const errorToast = localStorage.getItem('errorToast');
if (successToast) {
setNewSuccessMessage(successToast);
localStorage.removeItem('successToast');
} else if (errorToast) {
setNewErrorMessage(errorToast);
localStorage.removeItem('errorToast');
} else {
return;
}
}, [setNewErrorMessage, setNewSuccessMessage]);
// TODO: delete this code and toast when v1.0.0 is released (minor updates expected)
let hasRun = useRef(false);
useEffect(() => {
if (!hasRun.current) {
showNewVersionToast(setNewInfoMessage, isElectron, packageJson, commitRef);
hasRun.current = true;
}
}, [setNewInfoMessage, isElectron]);
// preload banners
useEffect(() => {
const loadImages = async () => {
const images = await importAll(require.context('../public/assets', true, /\.(png|jpe?g|svg)$/));
const imageUrls = images.map((image) => image.default);
preloadImages(imageUrls);
};
loadImages();
}, []);
// fetch default subplebbits
useEffect(() => {
let didCancel = false;
fetch('https://raw.githubusercontent.com/plebbit/temporary-default-subplebbits/master/multisub.json', { cache: 'no-cache' })
.then((res) => res.json())
.then((res) => {
if (!didCancel) {
setDefaultSubplebbits(res.subplebbits);
}
});
return () => {
didCancel = true;
};
}, [setDefaultSubplebbits]);
// fetch default NSFW subplebbits
useEffect(() => {
let didCancel = false;
fetch('https://raw.githubusercontent.com/plebbit/temporary-default-subplebbits/master/subplebbits-nsfw.json', { cache: 'no-cache' })
.then((res) => res.json())
.then((res) => {
if (!didCancel) {
setDefaultNsfwSubplebbits(res);
}
});
return () => {
didCancel = true;
};
}, [setDefaultNsfwSubplebbits]);
// handle nested routes
useEffect(() => {
const path = location.pathname;
if (path.endsWith('/post')) {
setShowPostFormLink(false);
setShowPostForm(true);
} else {
setShowPostFormLink(true);
setShowPostForm(false);
}
if (path.endsWith('/settings')) {
setIsSettingsOpen(true);
} else {
setIsSettingsOpen(false);
}
}, [location.pathname, setIsSettingsOpen, setShowPostForm, setShowPostFormLink]);
return (
<div>
<Helmet>
<link rel='apple-touch-icon' sizes='180x180' href='assets/logo/apple-touch-icon.png' />
<link rel='icon' type='image/png' sizes='32x32' href='assets/logo/favicon-32x32.png' />
<link rel='icon' type='image/png' sizes='16x16' href='assets/logo/favicon-16x16.png' />
<link rel='manifest' href='site.webmanifest' />
<meta name='msapplication-TileColor' content='#fee9cd' />
<meta name='theme-color' content='#ffffff' />
</Helmet>
<GlobalStyle background={bodyStyle.background} color={bodyStyle.color} fontFamily={bodyStyle.fontFamily} />
<Routes>
<Route exact path='/' element={<Home setBodyStyle={setBodyStyle} />} />
<Route path={`/p/:subplebbitAddress`} element={<Board setBodyStyle={setBodyStyle} />}>
<Route path='post' element={<Board />} />
<Route path='settings' element={<Board />} />
</Route>
<Route path={`/p/:subplebbitAddress/c/:threadCid`} element={<Thread setBodyStyle={setBodyStyle} />}>
<Route path='post' element={<Thread />} />
<Route path='settings' element={<Thread />} />
</Route>
<Route path={`/p/:subplebbitAddress/catalog`} element={<Catalog setBodyStyle={setBodyStyle} />}>
<Route path='post' element={<Catalog />} />
<Route path='settings' element={<Catalog />} />
</Route>
<Route path={`/p/:subplebbitAddress/description`} element={<Description setBodyStyle={setBodyStyle} />}>
<Route path='settings' element={<Description />} />
</Route>
<Route path={`/p/:subplebbitAddress/rules`} element={<Rules setBodyStyle={setBodyStyle} />}>
<Route path='settings' element={<Rules />} />
</Route>
<Route path={`/profile/c/:index`} element={<Pending setBodyStyle={setBodyStyle} />}>
<Route path='settings' element={<Pending />} />
</Route>
<Route path={`/p/subscriptions`} element={<Subscriptions setBodyStyle={setBodyStyle} />}>
<Route path='settings' element={<Subscriptions />} />
</Route>
<Route path={`p/subscriptions/catalog`} element={<SubscriptionsCatalog setBodyStyle={setBodyStyle} />}>
<Route path='settings' element={<SubscriptionsCatalog />} />
</Route>
<Route path={`p/all`} element={<All setBodyStyle={setBodyStyle} />}>
<Route path='settings' element={<All />} />
</Route>
<Route path={`p/all/catalog`} element={<AllCatalog setBodyStyle={setBodyStyle} />}>
<Route path='settings' element={<AllCatalog />} />
</Route>
<Route path='*' element={<NotFound setBodyStyle={setBodyStyle} />} />
</Routes>
<Toast />
<CaptchaModal selectedStyle={selectedStyle} isOpen={isCaptchaOpen} closeModal={() => setIsCaptchaOpen(false)} />
</div>
);
}
+7
View File
@@ -0,0 +1,7 @@
.app {
z-index: 1;
min-height: 100%;
overflow-y: hidden;
overflow-x: hidden;
min-height: 100vh;
}
+15
View File
@@ -0,0 +1,15 @@
import { Route, Routes } from 'react-router-dom';
import Home from './views/home';
import styles from './app.module.css';
const App = () => {
return (
<div className={styles.app}>
<Routes>
<Route path='/' element={<Home />} />
</Routes>
</div>
);
};
export default App;
-17
View File
@@ -1,17 +0,0 @@
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 });
useEffect(() => {
if (subplebbit.suggested?.avatarUrl) {
setAvatarUrl(subplebbit.suggested?.avatarUrl);
}
}, [subplebbit.suggested?.avatarUrl, subplebbit]);
return <img className='board-avatar' alt='board avatar' src={avatarUrl} />;
};
export default BoardAvatar;
-315
View File
@@ -1,315 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import { usePublishSubplebbitEdit } from '@plebbit/plebbit-react-hooks';
import { StyledModal } from './styled/modals/ModerationModal.styled';
import useError from '../hooks/useError';
import useSuccess from '../hooks/useSuccess';
import useGeneralStore from '../hooks/stores/useGeneralStore';
const BoardSettings = ({ subplebbit }) => {
const { setCaptchaResponse, setChallengesArray, setIsCaptchaOpen, setResolveCaptchaPromise, selectedAddress, selectedStyle } = useGeneralStore((state) => state);
const allowedSettings = {
address: subplebbit.address,
apiUrl: subplebbit.apiUrl,
description: subplebbit.description,
pubsubTopic: subplebbit.pubsubTopic,
settings: {
fetchThumbnailUrls: subplebbit.settings?.fetchThumbnailUrls,
fetchThumbnailUrlsProxyUrl: subplebbit.settings?.fetchThumbnailUrlsProxyUrl,
},
roles: subplebbit.roles,
rules: subplebbit.rules,
suggested: {
avatarUrl: subplebbit.suggested?.avatarUrl,
backgroundUrl: subplebbit.suggested?.backgroundUrl,
bannerUrl: subplebbit.suggested?.bannerUrl,
language: subplebbit.suggested?.language,
primaryColor: subplebbit.suggested?.primaryColor,
secondaryColor: subplebbit.suggested?.secondaryColor,
},
title: subplebbit.title,
};
const generateSettingsFromSubplebbit = (subplebbitData) => ({
address: subplebbitData.address,
apiUrl: subplebbitData.apiUrl,
description: subplebbitData.description,
pubsubTopic: subplebbitData.pubsubTopic,
settings: {
fetchThumbnailUrls: subplebbitData.settings?.fetchThumbnailUrls,
fetchThumbnailUrlsProxyUrl: subplebbitData.settings?.fetchThumbnailUrlsProxyUrl,
},
roles: subplebbitData.roles,
rules: subplebbitData.rules,
suggested: {
avatarUrl: subplebbitData.suggested?.avatarUrl,
backgroundUrl: subplebbitData.suggested?.backgroundUrl,
bannerUrl: subplebbitData.suggested?.bannerUrl,
language: subplebbitData.suggested?.language,
primaryColor: subplebbitData.suggested?.primaryColor,
secondaryColor: subplebbitData.suggested?.secondaryColor,
},
title: subplebbitData.title,
});
const initialSettings = generateSettingsFromSubplebbit(subplebbit);
const [isModalOpen, setIsModalOpen] = useState(false);
const [boardSettingsJson, setBoardSettingsJson] = useState(JSON.stringify(initialSettings, null, 2));
const [triggerPublilshSubplebbitEdit, setTriggerPublishCommentEdit] = useState(false);
const [, setNewErrorMessage] = useError();
const [, setNewSuccessMessage] = useSuccess();
const getDifferences = (oldObj, newObj) => {
let differences = {};
for (let key in oldObj) {
if (typeof oldObj[key] === 'object' && oldObj[key] !== null) {
const nestedDifferences = getDifferences(oldObj[key], newObj[key] || {});
if (Object.keys(nestedDifferences).length > 0) {
differences[key] = nestedDifferences;
}
} else if (oldObj[key] !== newObj[key]) {
differences[key] = newObj[key];
}
}
for (let key in newObj) {
if (!oldObj.hasOwnProperty(key)) {
differences[key] = newObj[key];
}
}
return differences;
};
const isInitialMount = useRef(true);
useEffect(() => {
if (isInitialMount.current) {
setBoardSettingsJson(JSON.stringify(generateSettingsFromSubplebbit(subplebbit), null, 2));
isInitialMount.current = false;
}
}, [subplebbit]);
function validateSettings(updatedSettings, allowedSettings) {
for (let key in updatedSettings) {
if (!allowedSettings.hasOwnProperty(key) && !initialSettings.hasOwnProperty(key)) {
throw new Error(`Unexpected setting: ${key}`);
}
if (typeof updatedSettings[key] === 'object' && updatedSettings[key] !== null && !Array.isArray(updatedSettings[key])) {
if (typeof allowedSettings[key] !== 'object' || allowedSettings[key] === null || Array.isArray(allowedSettings[key])) {
throw new Error(`Expected ${key} to be an object in allowedSettings`);
}
validateSettings(updatedSettings[key], allowedSettings[key]);
}
}
}
const onChallenge = async (challenges, subplebbitEdit) => {
let challengeAnswers = [];
try {
challengeAnswers = await getChallengeAnswersFromUser(challenges);
} catch (error) {
setNewErrorMessage(error.message);
console.log(error);
}
if (challengeAnswers) {
await subplebbitEdit.publishChallengeAnswers(challengeAnswers);
}
};
const onChallengeVerification = (challengeVerification) => {
if (challengeVerification.challengeSuccess === true) {
setNewSuccessMessage('Challenge Success');
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);
return new Promise((resolve, reject) => {
const imageString = challenges?.challenges[0].challenge;
const imageSource = `data:image/png;base64,${imageString}`;
const challengeImg = new Image();
challengeImg.src = imageSource;
challengeImg.onload = () => {
setIsCaptchaOpen(true);
const handleKeyDown = async (event) => {
if (event.key === 'Enter') {
const currentCaptchaResponse = useGeneralStore.getState().captchaResponse;
resolve(currentCaptchaResponse);
setIsCaptchaOpen(false);
document.removeEventListener('keydown', handleKeyDown);
event.preventDefault();
}
};
setCaptchaResponse('');
document.addEventListener('keydown', handleKeyDown);
setResolveCaptchaPromise(resolve);
};
challengeImg.onerror = () => {
reject(setNewErrorMessage('Could not load challenges'));
};
});
};
const [editSubplebbitOptions, setEditSubplebbitOptions] = useState({
subplebbitAddress: selectedAddress,
onChallenge,
onChallengeVerification,
onError: (error) => {
setNewErrorMessage(error.message);
console.log(error);
},
});
const { publishSubplebbitEdit } = usePublishSubplebbitEdit(editSubplebbitOptions);
useEffect(() => {
let isActive = true;
if (editSubplebbitOptions && triggerPublilshSubplebbitEdit) {
(async () => {
await publishSubplebbitEdit(editSubplebbitOptions);
if (isActive) {
setTriggerPublishCommentEdit(false);
}
})();
}
return () => {
isActive = false;
};
}, [editSubplebbitOptions, publishSubplebbitEdit, triggerPublilshSubplebbitEdit]);
const handleSaveChanges = async () => {
try {
const updatedSettings = JSON.parse(boardSettingsJson);
validateSettings(updatedSettings, allowedSettings);
const changes = getDifferences(initialSettings, updatedSettings);
if (Object.keys(changes).length > 0) {
setEditSubplebbitOptions((prevOptions) => ({
...prevOptions,
...changes,
}));
setTriggerPublishCommentEdit(true);
} else {
setNewErrorMessage('No changes detected');
}
} catch (error) {
setNewErrorMessage(`Error saving changes: ${error}`);
console.log(error);
}
};
const handleResetChanges = () => {
setBoardSettingsJson(JSON.stringify(initialSettings, null, 2));
};
function generateSettingsList(settingsObj, parentKey = '') {
let result = [];
for (let key in settingsObj) {
if (typeof settingsObj[key] === 'object' && settingsObj[key] !== null) {
const nestedItems = generateSettingsList(settingsObj[key], `${parentKey}${key}.`);
if (nestedItems.length > 1) {
result.push(`${parentKey}${key}: { ${nestedItems.join(', ')} }`);
} else {
result.push(...nestedItems);
}
} else {
result.push(`${parentKey}${key}`);
}
}
return result;
}
const possibleSettingsList = generateSettingsList(initialSettings);
const handleCloseModal = () => {
setIsModalOpen(false);
setBoardSettingsJson(JSON.stringify(initialSettings, 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>
]
</>
);
};
export default BoardSettings;
-74
View File
@@ -1,74 +0,0 @@
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 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 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>
);
};
export default BoardStats;
-49
View File
@@ -1,49 +0,0 @@
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 CatalogLoader = () => {
return (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
boxSizing: 'border-box',
}}
>
{[...Array(24)].map((_, index) => (
<SingleRectLoader key={index} />
))}
</div>
);
};
export default CatalogLoader;
-9
View File
@@ -1,9 +0,0 @@
import { useComment } from '@plebbit/plebbit-react-hooks';
const DisplayName = ({ commentCid }) => {
const comment = useComment({ commentCid });
return <span>{comment?.author?.displayName ?? 'Anonymous'}</span>;
};
export default DisplayName;
-67
View File
@@ -1,67 +0,0 @@
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 });
if (editedCommentState === 'pending' && !(commentCid in editedComments)) {
setEditedComments({ ...editedComments, [commentCid]: editedComment });
}
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');
}
return conditions.length > 0 ? conditions.join(', ') : null;
};
const conditionsString = conditionsCheck();
return (
<>
<OriginalCommentModal isOpen={isOriginalCommentModalOpen} closeModal={() => setIsOriginalCommentModalOpen(false)} comment={comment} />
{comment.edit && comment.original?.content !== comment?.content ? (
<>
<br />
<span className={className}>
(Edited at {timestamp},{' '}
<Link className='ttl-link' onClick={() => setIsOriginalCommentModalOpen(true)}>
show original
</Link>
)
</span>
</>
) : null}
{(editedCommentState === 'pending' || editedCommentState === 'failed') && conditionsString ? (
<>
<br />
<span className={className}>
({editedCommentState === 'pending' ? 'Pending' : 'Failed'} {conditionsString})
</span>
</>
) : null}
</>
);
};
export default EditLabel;
-284
View File
@@ -1,284 +0,0 @@
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} />;
}
};
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 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={`
<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={`
<style>
/* fix reddit iframe being centered */
iframe {
margin: 0!important;
}
</style>
<blockquote class="reddit-embed-bq" style="height:240px" data-embed-theme="dark">
<a href="${parsedUrl.href}"></a>
</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 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={`
<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={`
<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 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 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 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 canEmbedHosts = new Set([
...youtubeHosts,
...twitterHosts,
...redditHosts,
...twitchHosts,
...tiktokHosts,
...instagramHosts,
...odyseeHosts,
...bitchuteHosts,
...streamableHosts,
...spotifyHosts,
]);
export const canEmbed = (parsedUrl) => canEmbedHosts.has(parsedUrl.host);
export default Embed;
-29
View File
@@ -1,29 +0,0 @@
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 handleClick = (event) => {
if (otherProps.to === '#void') {
event.preventDefault();
}
if (onClick) {
onClick(event);
}
};
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>
);
});
export default ForwardRefLink;
-41
View File
@@ -1,41 +0,0 @@
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('/');
useEffect(() => {
let isMounted = true;
const loadRandomImage = async () => {
const images = await importAll(require.context('../../public/assets/banners', false, /\.(png|jpe?g|svg)$/));
const randomImage = Math.floor(Math.random() * images.length) + 1;
const img = new Image();
img.src = `${process.env.PUBLIC_URL}/assets/banners/banner-${randomImage}.jpg`;
img.onload = () => {
if (isMounted) {
setCurrentImage(randomImage);
}
};
};
loadRandomImage();
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);
}
export default ImageBanner;
-28
View File
@@ -1,28 +0,0 @@
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 * 30;
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;
-18
View File
@@ -1,18 +0,0 @@
import React from 'react';
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
const PendingLabel = ({ 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;
};
export default PendingLabel;
-136
View File
@@ -1,136 +0,0 @@
import React, { useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
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 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 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={`quotelink-${i}-${lastIndex}`}
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={{
a: ({ children, href }) => <span>{`[${children}](${href})`}</span>,
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);
-39
View File
@@ -1,39 +0,0 @@
import React from 'react';
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';
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>
);
};
export default PostLoader;
-303
View File
@@ -1,303 +0,0 @@
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);
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'>
<div className='thread'>
{reply.state === 'succeeded' ? (
<div className='reply-container'>
<div className='post-reply post-reply-desktop'>
<div className='post-info'>
<span className='nameblock'>
{reply.author?.displayName ? (
reply.author?.displayName.length > 20 ? (
<Fragment>
<span className='name' data-tooltip-id='tooltip' data-tooltip-content={reply.author?.displayName} data-tooltip-place='top'>
{reply.author?.displayName.slice(0, 20) + ' (...)'}
</span>
</Fragment>
) : (
<span className='name'>{reply.author?.displayName}</span>
)
) : (
<span className='name'>Anonymous</span>
)}
&nbsp;
<span className='poster-address address-desktop' id='reply-button' style={{ cursor: 'pointer' }}>
(u/
{reply.author?.shortAddress ? <span>{reply.author?.shortAddress}</span> : <span>{account?.author?.shortAddress}</span>})
</span>
</span>
&nbsp;
<span className='date-time' data-utc='data'>
{getDate(reply.timestamp)}
</span>
&nbsp;
<span className='post-number post-number-desktop'>
<span>c/</span>
<Link to={() => {}} id='reply-button' title='Reply to this post'>
{reply.shortCid}
</Link>
</span>
&nbsp;
<div id='backlink-id' className='backlink'>
{reply.replies?.pages?.topAll.comments
.sort((a, b) => a.timestamp - b.timestamp)
.map((reply, index) => (
<div key={`div-${index}`} style={{ display: 'inline-block' }}>
<Link key={`link-${index}`} to={() => {}} className='quote-link'>
c/{reply.shortCid}
</Link>
&nbsp;
</div>
))}
</div>
</div>
{replyMediaInfo?.url ? (
<div className='file' style={{ marginBottom: '5px' }}>
<div className='reply-file-text'>
Link:&nbsp;
<a href={replyMediaInfo.url} target='_blank' rel='noopener noreferrer'>
{replyMediaInfo?.url.length > 30 ? replyMediaInfo?.url.slice(0, 30) + '(...)' : replyMediaInfo?.url}
</a>
&nbsp;{replyMediaInfo?.type === 'iframe' ? null : ` (${replyMediaInfo?.type})`}
</div>
{replyMediaInfo?.type === 'iframe' && (
<div className='img-container'>
<span className='file-thumb-reply'>
{replyMediaInfo.thumbnail && <img src={replyMediaInfo.thumbnail} alt='thumbnail' onError={(e) => (e.target.src = fallbackImgUrl)} />}
</span>
</div>
)}
{replyMediaInfo?.type === 'webpage' ? (
<div className='img-container'>
<span className='file-thumb-reply'>
{reply.thumbnailUrl ? (
<img
src={replyMediaInfo.thumbnail}
alt={replyMediaInfo.type}
style={{ cursor: 'pointer' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : null}
</span>
</div>
) : null}
{replyMediaInfo?.type === 'image' ? (
<div className='img-container'>
<span className='file-thumb-reply'>
<img src={replyMediaInfo.url} alt={replyMediaInfo.type} style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
</span>
</div>
) : null}
{replyMediaInfo?.type === 'video' ? (
<span className='file-thumb-reply'>
<video controls src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
</span>
) : null}
{replyMediaInfo?.type === 'audio' ? (
<span className='file-thumb-reply'>
<audio controls src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
</span>
) : null}
</div>
) : null}
{reply.content ? (
reply.content?.length > 500 ? (
<Fragment>
<blockquote comment={reply} className='post-message'>
{shortParentCid ? (
<Link to={() => {}} className='quotelink'>
{`c/${shortParentCid}`}
{shortParentCid === thread.shortCid ? ' (OP)' : null}
</Link>
) : null}
<Post content={reply.content?.slice(0, 500)} />
<span className='ttl'>
{' '}
(...)
<br /> <EditLabel commentCid={reply.cid} className='ttl' />
<br />
Comment too long. Click the link to view.
</span>
</blockquote>
</Fragment>
) : (
<blockquote className='post-message'>
{shortParentCid ? (
<Link to={() => {}} className='quotelink'>
{`c/${shortParentCid}`}
{shortParentCid === thread.shortCid ? ' (OP)' : null}
</Link>
) : null}
<Post content={reply.content} comment={reply} />
<EditLabel commentCid={reply.cid} className='ttl' />
</blockquote>
)
) : null}
</div>
</div>
) : (
<div className='reply-container'>
<span className='ellipsis'>{stateString}</span>
</div>
)}
</div>
<div className='thread-mobile'>
{reply.state === 'succeeded' ? (
<div className='reply-container'>
<div className='post-reply post-reply-mobile'>
<div className='post-info-mobile'>
<span className='name-block-mobile'>
{reply.author?.displayName ? (
reply.author?.displayName.length > 20 ? (
<Fragment>
<span className='name-mobile'>{reply.author?.displayName.slice(0, 20) + ' (...)'}</span>
</Fragment>
) : (
<span className='name-mobile'>{reply.author?.displayName}</span>
)
) : (
<span className='name-mobile'>Anonymous</span>
)}
&nbsp;
<span className='poster-address-mobile address-mobile' id='reply-button' style={{ cursor: 'pointer' }}>
(u/
{reply.author?.shortAddress ? (
<span className='highlight-address-mobile'>{reply.author?.shortAddress}</span>
) : (
<span>{account?.author?.shortAddress}</span>
)}
)&nbsp;
</span>
<br />
</span>
<span className='date-time-mobile post-number-mobile'>
{getDate(reply.timestamp)}&nbsp;
<span>c/</span>
<Link to={() => {}} id='reply-button'>
{reply.shortCid}
</Link>
</span>
</div>
{reply.link ? (
<div className='file-mobile'>
{replyMediaInfo?.url ? (
replyMediaInfo.type === 'webpage' ? (
<div className='img-container'>
<span className='file-thumb-mobile'>
{reply.thumbnailUrl ? (
<img src={replyMediaInfo.thumbnail} alt='thumbnail' style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
) : null}
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
</span>
</div>
) : replyMediaInfo.type === 'image' ? (
<div className='img-container'>
<span className='file-thumb-mobile'>
<img src={replyMediaInfo.url} alt={replyMediaInfo.type} style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
</span>
</div>
) : replyMediaInfo.type === 'video' ? (
<span className='file-thumb-mobile'>
<video
src={replyMediaInfo.url}
alt={replyMediaInfo.type}
style={{ pointerEvents: 'none' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
</span>
) : replyMediaInfo.type === 'audio' ? (
<span className='file-thumb-mobile'>
<audio src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
</span>
) : null
) : null}
</div>
) : null}
{reply.content ? (
reply.content?.length > 500 ? (
<Fragment>
<blockquote className='post-message'>
{shortParentCid ? (
<Link to={() => {}} className='quotelink'>
{`c/${shortParentCid}`}
{shortParentCid === thread.shortCid ? ' (OP)' : null}
</Link>
) : null}
<Post content={reply.content?.slice(0, 500)} comment={reply} />
<span className='ttl'>
{' '}
(...)
<br />
<EditLabel commentCid={reply.cid} className='ttl' />
<br />
Comment too long. Click the link to view.{' '}
</span>
</blockquote>
</Fragment>
) : (
<blockquote className='post-message'>
{shortParentCid ? (
<Link to={() => {}} className='quotelink'>
{`c/${shortParentCid}`}
{shortParentCid === thread.shortCid ? ' (OP)' : null}
</Link>
) : null}
<Post content={reply.content} comment={reply} />
<EditLabel commentCid={reply.cid} className='ttl' />
</blockquote>
)
) : null}
</div>
</div>
) : (
<div className='reply-container'>
<span className='ellipsis'>{stateString}</span>
</div>
)}
</div>
</div>
</BoardForm>
</Container>
);
};
export default PostOnHover;
-28
View File
@@ -1,28 +0,0 @@
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);
if (comment.updatedAt !== undefined || comment.index === undefined) {
return null;
}
if (comment.state === 'failed' || comment.state === 'succeeded') {
return null;
}
if (!stateString) {
return null;
}
return (
<span className='ttl'>
<br />(<span className={className}>{stateString}</span>)
</span>
);
};
export default StateLabel;
-11
View File
@@ -1,11 +0,0 @@
import React from 'react';
import { useComment, useAuthorAddress } from '@plebbit/plebbit-react-hooks';
function VerifiedAuthor({ commentCid, children }) {
const comment = useComment({ commentCid });
const { authorAddress, shortAuthorAddress, authorAddressChanged } = useAuthorAddress({ comment });
return children({ authorAddress, shortAuthorAddress, authorAddressChanged });
}
export default React.memo(VerifiedAuthor);
-84
View File
@@ -1,84 +0,0 @@
import React, { useEffect, useState, useRef } from 'react';
import Modal from 'react-modal';
import Draggable from 'react-draggable';
import { StyledModal } from '../styled/modals/ReplyModal.styled';
import useGeneralStore from '../../hooks/stores/useGeneralStore';
import { BoardForm } from '../styled/views/Board.styled';
import { Link } from 'react-router-dom';
const AdminListModal = ({ isOpen, closeModal, roles }) => {
const { selectedStyle } = useGeneralStore((state) => state);
const [isMobile, setIsMobile] = useState(window.innerWidth <= 480);
const nodeRef = useRef(null);
const rolesList = roles ? Object.entries(roles).map(([address, { role }]) => ({ address, role })) : [];
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth <= 480);
window.addEventListener('resize', handleResize);
// Cleanup function
return () => {
window.removeEventListener('resize', handleResize);
};
}, [setIsMobile]);
return (
<StyledModal
isOpen={isOpen}
onRequestClose={closeModal}
contentLabel='Reply Modal'
shouldCloseOnEsc={true}
shouldCloseOnOverlayClick={isMobile}
selectedStyle={selectedStyle}
overlayClassName='overlay'
style={isMobile ? { overlay: { backgroundColor: 'rgba(0,0,0,.25)' } } : { overlay: { backgroundColor: 'rgba(0,0,0,0)' } }}
>
<Draggable handle='.modal-header' nodeRef={nodeRef} disabled={isMobile}>
<div
className='modal-content'
ref={nodeRef}
style={{
maxWidth: '300px',
maxHeight: '250px',
}}
>
<button className='icon' style={{ imageRendering: 'pixelated', margin: '1px 1px 0 0' }} onClick={() => closeModal()} title='close' />
<div className='modal-header'>   Moderators</div>
<div
className='list'
style={{
padding: '10px',
maxHeight: '200px',
maxWidth: '300px',
overflowY: 'auto',
overflowX: 'hidden',
wordWrap: 'break-word',
wordBreak: 'break-all',
}}
>
<BoardForm selectedStyle={selectedStyle} style={{ all: 'unset' }}>
{roles ? (
rolesList.map(({ address, role }, index) => (
<p key={index}>
{' '}
<Link to={() => {}} className='quote-link'>
u/{address}
</Link>
: {role}
</p>
))
) : (
<p>This board doesn't have moderators yet.</p>
)}
</BoardForm>
</div>
</div>
</Draggable>
</StyledModal>
);
};
Modal.setAppElement('#root');
export default AdminListModal;
-207
View File
@@ -1,207 +0,0 @@
import React, { useEffect, useState, useRef } from 'react';
import { StyledModal } from '../styled/modals/CaptchaModal.styled';
import useGeneralStore from '../../hooks/stores/useGeneralStore';
import Modal from 'react-modal';
import Draggable from 'react-draggable';
const CaptchaModal = () => {
const {
challengesArray,
setChallengesArray,
pendingComment,
selectedStyle,
setCaptchaResponse,
isAuthorDelete,
setIsAuthorDelete,
isAuthorEdit,
setIsAuthorEdit,
isCaptchaOpen,
setIsCaptchaOpen,
isModEdit,
setIsModEdit,
resolveCaptchaPromise,
selectedShortCid,
} = useGeneralStore((state) => state);
const [imageSources, setImageSources] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [currentChallengeIndex, setCurrentChallengeIndex] = useState(0);
const [totalChallenges, setTotalChallenges] = useState(0);
const [isMobile, setIsMobile] = useState(window.innerWidth <= 480);
const responseRef = useRef();
const nodeRef = useRef(null);
const [isPromiseResolved, setIsPromiseResolved] = useState(false);
useEffect(() => {
if (!isCaptchaOpen) {
setIsAuthorDelete(false);
setIsAuthorEdit(false);
setIsModEdit(false);
}
}, [isCaptchaOpen, setIsAuthorDelete, setIsAuthorEdit, setIsModEdit]);
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth <= 480);
window.addEventListener('resize', handleResize);
// Cleanup function
return () => {
window.removeEventListener('resize', handleResize);
};
}, [setIsMobile]);
useEffect(() => {
if (isCaptchaOpen && challengesArray) {
setIsLoading(true);
const challenges = challengesArray.challenges;
const decryptedChallenges = [];
for (let i = 0; i < challenges?.length; i++) {
const imageString = challenges[i].challenge;
const imageSource = `data:image/png;base64,${imageString}`;
decryptedChallenges.push(imageSource);
}
setImageSources(decryptedChallenges);
setTotalChallenges(decryptedChallenges.length);
setIsLoading(false);
}
}, [challengesArray, isCaptchaOpen]);
const handleKeyDown = (event) => {
if (event.key === 'Enter') {
event.preventDefault();
submitCaptcha((response) => {
setCaptchaResponse(response);
resolveCaptchaPromise(response);
});
}
};
const handleReturnKeyDown = () => {
submitCaptcha((response) => {
setCaptchaResponse(response);
resolveCaptchaPromise(response);
});
};
const submitCaptcha = (callback) => {
if (!isPromiseResolved) {
setCaptchaResponse(responseRef.current.value);
resolveCaptchaPromise(responseRef.current.value);
setIsPromiseResolved(true);
}
setImageSources([]);
setChallengesArray([]);
setIsCaptchaOpen(false);
if (callback) {
callback(responseRef.current.value);
}
};
useEffect(() => {
if (!isCaptchaOpen) {
setIsPromiseResolved(false);
}
}, [isCaptchaOpen]);
const handleCloseModal = () => {
setImageSources([]);
setChallengesArray([]);
setCurrentChallengeIndex(0);
setIsCaptchaOpen(false);
};
return (
<StyledModal
isOpen={isCaptchaOpen}
onRequestClose={() => {
handleCloseModal();
submitCaptcha();
}}
contentLabel='Captcha Modal'
shouldCloseOnEsc={false}
shouldCloseOnOverlayClick={false}
selectedStyle={selectedStyle}
overlayClassName='hide-modal-overlay'
>
<Draggable handle='.modal-header' nodeRef={nodeRef} disabled={isMobile}>
<div className='modal-content' ref={nodeRef}>
<div className='modal-header'>
{isModEdit
? 'Challenge for Moderator Action'
: isAuthorEdit
? 'Challenge for Editing Post'
: isAuthorDelete
? 'Challenge for Deleting Post'
: pendingComment.parentCid
? 'Challenges for Reply to c/' + selectedShortCid
: 'Challenges for New Thread'}
<button className='icon' onClick={() => handleCloseModal()} title='close' />
</div>
<div id='form'>
{pendingComment.author?.displayName ? (
<div>
<input id='field' type='text' placeholder={pendingComment.author?.displayName || ''} disabled />
</div>
) : null}
{pendingComment.title ? (
<div>
<input id='field' type='text' placeholder={pendingComment.title || ''} disabled />
</div>
) : null}
{pendingComment.content ? (
<div>
<textarea rows='4' placeholder={pendingComment.content || 'Comment'} wrap='soft' disabled />
</div>
) : null}
{pendingComment.link ? (
<div>
<input id='field' type='text' placeholder={pendingComment.link || ''} disabled />
</div>
) : null}
<div id='captcha-container'>
<input
id='response'
type='text'
autoComplete='off'
placeholder='TYPE THE CAPTCHA HERE AND PRESS ENTER'
ref={responseRef}
onKeyDown={handleKeyDown}
autoFocus
/>
{isLoading ? (
<img src='' alt='loading...' style={{ visibility: 'hidden' }} />
) : (
imageSources[currentChallengeIndex] && <img src={imageSources[currentChallengeIndex]} alt='captcha' />
)}
</div>
<div>
<span style={{ lineHeight: '1.7' }}>
Challenge {currentChallengeIndex + 1} of {totalChallenges}
</span>
<button
id='nav'
onClick={() => {
if (currentChallengeIndex + 1 < totalChallenges) {
setCurrentChallengeIndex((currentChallengeIndex + 1) % totalChallenges);
} else {
handleReturnKeyDown();
}
}}
>
{currentChallengeIndex + 1 < totalChallenges ? 'Next' : 'Submit'}
</button>
</div>
</div>
</div>
</Draggable>
</StyledModal>
);
};
Modal.setAppElement('#root');
export default CaptchaModal;
-231
View File
@@ -1,231 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
useAccount,
useCreateSubplebbit,
// useSubplebbits, useAccountSubplebbits
} from '@plebbit/plebbit-react-hooks';
import { StyledModal } from '../styled/modals/CreateBoardModal.styled';
import useGeneralStore from '../../hooks/stores/useGeneralStore';
import Modal from 'react-modal';
import useError from '../../hooks/useError';
import useSuccess from '../../hooks/useSuccess';
const CreateBoardModal = ({ isOpen, closeModal }) => {
const { selectedStyle } = useGeneralStore((state) => state);
const account = useAccount();
const navigate = useNavigate();
const nodeRef = useRef(null);
const ruleInputRef = useRef(null);
const [, setNewErrorMessage] = useError();
const [, setNewSuccessMessage] = useSuccess();
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [avatar, setAvatar] = useState('');
const [moderators, setModerators] = useState('');
const [rule, setRule] = useState('');
const [rules, setRules] = useState([]);
const [editIndex, setEditIndex] = useState(-1);
const handleAddRule = () => {
if (rule.trim() === '') {
setNewErrorMessage('Rule field is empty');
return;
}
if (rules.length >= 15) {
setNewErrorMessage('Maximum number of rules is 15');
return;
}
if (editIndex > -1) {
setRules(rules.map((r, i) => (i === editIndex ? rule : r)));
setEditIndex(-1);
} else {
setRules([...rules, rule]);
}
setRule('');
};
useEffect(() => {
if (ruleInputRef.current) {
ruleInputRef.current.focus();
}
}, [rules]);
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
handleAddRule();
}
};
const handleEditRule = (index) => {
setRule(rules[index]);
setEditIndex(index);
ruleInputRef.current.focus();
};
const handleDeleteRule = (index) => {
if (index === editIndex) {
setEditIndex(-1);
}
setRules(rules.filter((_, i) => i !== index));
};
const createSubplebbitOptions = {
title: title || undefined,
description: description || undefined,
suggested: {
avatarUrl: avatar || undefined,
},
roles: moderators || undefined,
rules: rules || undefined,
};
const resetFields = () => {
setTitle('');
setDescription('');
setAvatar('');
setModerators('');
setRule('');
setRules([]);
};
const { createdSubplebbit, createSubplebbit } = useCreateSubplebbit(createSubplebbitOptions);
const handleCreateBoard = async () => {
let moderatorAddresses = moderators.trim() ? moderators.split(',').map((addr) => addr.trim()) : [];
let invalidAddresses = moderatorAddresses.filter((addr) => !(addr.endsWith('.eth') || (addr.startsWith('12D3KooW') && addr.length === 52)));
if (invalidAddresses.length > 0) {
setNewErrorMessage('Invalid moderator addresses: ' + invalidAddresses.join(', '));
return;
}
const roles = {};
moderatorAddresses.forEach((addr) => {
roles[addr] = { role: 'moderator' };
});
roles[account.author.address] = { role: 'admin' };
let createSubplebbitOptions = {
roles: roles,
};
if (title) {
createSubplebbitOptions.title = title;
}
if (description) {
createSubplebbitOptions.description = description;
}
if (avatar) {
createSubplebbitOptions.suggested = {
avatarUrl: avatar,
};
}
if (rules.length > 0) {
createSubplebbitOptions.rules = rules;
}
try {
await createSubplebbit(createSubplebbitOptions);
} catch (error) {
setNewErrorMessage(error.message);
}
if (createdSubplebbit) {
resetFields();
closeModal();
setNewSuccessMessage('Board created successfully, address: ' + createdSubplebbit.address);
navigate(`/p/${createdSubplebbit.address}`);
}
};
// remove after testing:
// const {accountSubplebbits} = useAccountSubplebbits()
// const ownerSubplebbitAddresses = Object.keys(accountSubplebbits).map(subplebbitAddress => accountSubplebbits[subplebbitAddress].role === 'owner')
// const subplebbits = useSubplebbits({subplebbitAddresses: ownerSubplebbitAddresses})
// console.log(subplebbits);
return (
<StyledModal
isOpen={isOpen}
onRequestClose={closeModal}
contentLabel='Create Board'
selectedStyle={selectedStyle}
style={{ overlay: { backgroundColor: 'rgba(0,0,0,.25)' } }}
>
<div className='modal-content' ref={nodeRef}>
<div className='modal-header'>
Create Board
<button className='icon' onClick={() => closeModal()} title='close' />
</div>
<ul id='form'>
<div id='explaination'>
NOTE: plebbit is P2P, your board will stay online for as long as you leave the plebchan desktop app (full node) connected to the Internet.
</div>
<li className='settings-option disc'>Title</li>
<div className='settings-tip'>Optional, useful to describe the board next to its p/address.</div>
<div className='settings-input'>
<input id='name' type='text' value={title} onChange={(e) => setTitle(e.target.value)} placeholder='Board Title' />
</div>
<li className='settings-option disc'>Description</li>
<div className='settings-tip'>Optional, displayed as sticky with the board's avatar if set.</div>
<div className='settings-input'>
<textarea id='description' type='text' value={description} onChange={(e) => setDescription(e.target.value)} placeholder='Add a description' />
</div>
<li className='settings-option disc'>Avatar</li>
<div className='settings-tip'>Optional, set an image that represents your board.</div>
<div className='settings-input'>
<input id='name' type='text' value={avatar} onChange={(e) => setAvatar(e.target.value)} placeholder='https://example.com/image.png' />
</div>
<li className='settings-option disc'>Moderators</li>
<div className='settings-tip'>Optional, let other users help you moderate your board.</div>
<div className='settings-input'>
<textarea id='name' type='text' value={moderators} onChange={(e) => setModerators(e.target.value)} placeholder='username.eth, 12D3KooW..., username2.eth' />
</div>
<li className='settings-option disc'>Rules</li>
<div className='settings-tip'>Optional, listed in a sticky on top. Maximum 15 rules.</div>
<div className='settings-input'>
<input ref={ruleInputRef} id='rule' type='text' value={rule} onChange={(e) => setRule(e.target.value)} onKeyDown={handleKeyDown} placeholder='Add a rule' />
</div>
<button style={{ display: rules.length > 0 ? 'none' : 'block' }} id='rule-btn' className={rules.length > 0 ? 'relative' : ''} onClick={handleAddRule}>
{editIndex > -1 ? 'Update Rule' : 'Add Rule'}
</button>
{rules.length > 0 && (
<fieldset>
<legend>
<button className={rules.length > 0 ? 'relative' : ''} onClick={handleAddRule}>
{editIndex > -1 ? 'Update Rule' : 'Add Rule'}
</button>
</legend>
{rules.map((rule, index) => (
<div key={index} className='rule-item'>
<p>
{index + 1}. {rule}
</p>
<button onClick={() => handleEditRule(index)}>Edit</button>
<button onClick={() => handleDeleteRule(index)}>Delete</button>
</div>
))}
</fieldset>
)}
<button onClick={handleCreateBoard} id='create-board-btn'>
Create Board
</button>
</ul>
</div>
</StyledModal>
);
};
Modal.setAppElement('#root');
export default CreateBoardModal;
-71
View File
@@ -1,71 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import { StyledModal } from '../styled/modals/ReplyModal.styled';
import useGeneralStore from '../../hooks/stores/useGeneralStore';
import Modal from 'react-modal';
import Draggable from 'react-draggable';
const EditModal = ({ isOpen, closeModal, originalCommentContent }) => {
const { setEditedComment, selectedStyle } = useGeneralStore((state) => state);
const nodeRef = useRef(null);
const commentRef = useRef();
const [isMobile, setIsMobile] = useState(window.innerWidth <= 480);
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth <= 480);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, [setIsMobile]);
const handleSaveEdit = () => {
setEditedComment(commentRef.current.value);
closeModal();
};
return (
<StyledModal
isOpen={isOpen}
onRequestClose={closeModal}
contentLabel='Edit Comment'
shouldCloseOnEsc={false}
shouldCloseOnOverlayClick={isMobile}
selectedStyle={selectedStyle}
style={isMobile ? { overlay: { backgroundColor: 'rgba(0,0,0,.25)' } } : { overlay: { backgroundColor: 'rgba(0,0,0,0)' } }}
>
<Draggable handle='.modal-header' nodeRef={nodeRef} disabled={isMobile}>
<div className='modal-content' ref={nodeRef}>
<div className='modal-header'>
Edit Comment
<button className='icon' onClick={() => closeModal()} title='close' />
</div>
<div id='form'>
<div className='textarea-wrapper'>
<textarea
className='textarea'
rows='4'
style={{ paddingTop: '0' }}
placeholder='Comment'
defaultValue={originalCommentContent}
wrap='soft'
ref={commentRef}
/>
</div>
<div>
<button id='next' onClick={handleSaveEdit}>
Save
</button>
</div>
</div>
</div>
</Draggable>
</StyledModal>
);
};
Modal.setAppElement('#root');
export default EditModal;
-221
View File
@@ -1,221 +0,0 @@
import React, { useState, useEffect } from 'react';
import Modal from 'react-modal';
import { Link } from 'react-router-dom';
import { useComment, usePublishCommentEdit } from '@plebbit/plebbit-react-hooks';
import { StyledModal } from '../styled/modals/ModerationModal.styled';
import useGeneralStore from '../../hooks/stores/useGeneralStore';
import useError from '../../hooks/useError';
import useSuccess from '../../hooks/useSuccess';
const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
const { selectedAddress, selectedStyle, setCaptchaResponse, setChallengesArray, setIsCaptchaOpen, setIsModEdit, moderatingCommentCid, setResolveCaptchaPromise } =
useGeneralStore((state) => state);
const comment = useComment({ commentCid: moderatingCommentCid });
const [pin, setPin] = useState(comment.pinned);
const [deleteThread, setDeleteThread] = useState(deletePost);
const [close, setClose] = useState(comment.locked);
const [reason, setReason] = useState('');
const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false);
const [, setNewErrorMessage] = useError();
const [, setNewSuccessMessage] = useSuccess();
useEffect(() => {
setPin(comment.pinned);
setClose(comment.locked);
}, [comment]);
useEffect(() => {
setDeleteThread(deletePost);
}, [deletePost]);
useEffect(() => {
if (!isOpen) {
setDeleteThread(deletePost);
}
}, [isOpen, deletePost]);
const handleCloseModal = () => {
setDeleteThread(false);
closeModal();
};
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 onChallenge = async (challenges, comment) => {
let challengeAnswers = [];
try {
challengeAnswers = await getChallengeAnswersFromUser(challenges);
} catch (error) {
setNewErrorMessage(error.message);
console.log(error);
}
if (challengeAnswers) {
await comment.publishChallengeAnswers(challengeAnswers);
}
};
const getChallengeAnswersFromUser = async (challenges) => {
setChallengesArray(challenges);
return new Promise((resolve, reject) => {
const imageString = challenges?.challenges[0].challenge;
const imageSource = `data:image/png;base64,${imageString}`;
const challengeImg = new Image();
challengeImg.src = imageSource;
challengeImg.onload = () => {
setIsCaptchaOpen(true);
const handleKeyDown = async (event) => {
if (event.key === 'Enter') {
const currentCaptchaResponse = useGeneralStore.getState().captchaResponse;
resolve(currentCaptchaResponse);
setIsCaptchaOpen(false);
document.removeEventListener('keydown', handleKeyDown);
event.preventDefault();
}
};
setCaptchaResponse('');
document.addEventListener('keydown', handleKeyDown);
setResolveCaptchaPromise(resolve);
};
challengeImg.onerror = () => {
reject(setNewErrorMessage('Could not load challenges'));
};
});
};
const [publishCommentEditOptions, setPublishCommentEditOptions] = useState({
commentCid: moderatingCommentCid,
subplebbitAddress: selectedAddress,
onChallenge,
onChallengeVerification,
onError: (error) => {
setNewErrorMessage(error.message);
console.log(error);
},
});
useEffect(() => {
if (selectedAddress) {
setPublishCommentEditOptions((prevOptions) => ({
...prevOptions,
subplebbitAddress: selectedAddress,
}));
}
}, [selectedAddress]);
const { publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions);
useEffect(() => {
setPublishCommentEditOptions((prevOptions) => ({
...prevOptions,
commentCid: moderatingCommentCid,
}));
}, [moderatingCommentCid]);
useEffect(() => {
let isActive = true;
if (publishCommentEditOptions && triggerPublishCommentEdit) {
(async () => {
await publishCommentEdit();
if (isActive) {
setTriggerPublishCommentEdit(false);
}
})();
}
return () => {
isActive = false;
};
}, [triggerPublishCommentEdit, publishCommentEdit, publishCommentEditOptions]);
return (
<StyledModal
isOpen={isOpen}
onRequestClose={handleCloseModal}
contentLabel='Moderator Tools'
style={{ overlay: { backgroundColor: 'rgba(0,0,0,.25)' } }}
selectedStyle={selectedStyle}
>
<div className='panel'>
<div className='panel-header'>
Moderator Tools
<Link to='' onClick={handleCloseModal}>
<span className='icon' title='close' />
</Link>
</div>
<ul className='settings-cat'>
<li className='settings-cat-lbl'>
<label>
<input type='checkbox' style={{ marginRight: '10px' }} checked={pin} onChange={() => setPin(!pin)} />
Pin post
</label>
</li>
<li className='settings-tip'>Pin the post to make it a sticky, showed at the top of the board even as new posts are submitted.</li>
</ul>
<ul className='settings-cat'>
<li className='settings-cat-lbl'>
<label>
<input type='checkbox' style={{ marginRight: '10px' }} checked={deleteThread} onChange={() => setDeleteThread(!deleteThread)} />
Delete post
</label>
</li>
<li className='settings-tip'>The post will no longer visible to other users, but the person who posted it can still see it in their own account.</li>
</ul>
<ul className='settings-cat'>
<li className='settings-cat-lbl'>
<label>
<input type='checkbox' style={{ marginRight: '10px' }} checked={close} onChange={() => setClose(!close)} />
Close post
</label>
</li>
<li className='settings-tip'>Closing a post allows users to still see the content, but they cannot add any new replies to it.</li>
</ul>
<ul className='settings-cat'>
<li className='settings-option disc'>Reason</li>
<li className='settings-tip'>Help people become better posters by giving a short reason why their post was removed.</li>
<li className='settings-input' style={{ marginTop: '-10px' }}>
<textarea value={reason} placeholder='Enter reason here...' onChange={(e) => setReason(e.target.value)} />
</li>
</ul>
<button
className='save-button'
onClick={async () => {
setPublishCommentEditOptions((prevOptions) => ({
...prevOptions,
pinned: pin,
removed: deleteThread,
locked: close,
reason: reason,
}));
setTriggerPublishCommentEdit(true);
setIsModEdit(true);
handleCloseModal();
}}
>
Save
</button>
</div>
</StyledModal>
);
};
Modal.setAppElement('#root');
export default ModerationModal;
@@ -1,62 +0,0 @@
import React, { useState, useEffect } from 'react';
import { StyledModal } from '../styled/modals/ReplyModal.styled';
import useGeneralStore from '../../hooks/stores/useGeneralStore';
import Modal from 'react-modal';
import Draggable from 'react-draggable';
import getDate from '../../utils/getDate';
const OriginalCommentModal = ({ isOpen, closeModal, comment }) => {
const { selectedStyle } = useGeneralStore((state) => state);
const [isMobile, setIsMobile] = useState(window.innerWidth <= 480);
const nodeRef = React.useRef(null);
const originalCommentContent = comment.original?.content;
const timestamp = getDate(comment.timestamp);
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth <= 480);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, [setIsMobile]);
return (
<StyledModal
isOpen={isOpen}
onRequestClose={closeModal}
contentLabel='Original Comment'
shouldCloseOnEsc={false}
shouldCloseOnOverlayClick={isMobile}
selectedStyle={selectedStyle}
style={isMobile ? { overlay: { backgroundColor: 'rgba(0,0,0,.25)' } } : { overlay: { backgroundColor: 'rgba(0,0,0,0)' } }}
>
<Draggable handle='.modal-header' nodeRef={nodeRef} disabled={isMobile}>
<div className='modal-content' ref={nodeRef}>
<div className='modal-header'>
Original at {timestamp}
<button className='icon' onClick={() => closeModal()} title='close' />
</div>
<div id='form'>
<div className='textarea-wrapper'>
<textarea
className='textarea'
rows='4'
style={{ paddingTop: '0' }}
placeholder='Comment'
defaultValue={originalCommentContent}
wrap='soft'
readOnly={true}
/>
</div>
</div>
</div>
</Draggable>
</StyledModal>
);
};
Modal.setAppElement('#root');
export default OriginalCommentModal;
-317
View File
@@ -1,317 +0,0 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useAccount, usePublishComment } from '@plebbit/plebbit-react-hooks';
import Modal from 'react-modal';
import Draggable from 'react-draggable';
import { StyledModal } from '../styled/modals/ReplyModal.styled';
import useAnonMode from '../../hooks/useAnonMode';
import useError from '../../hooks/useError';
import useAnonModeStore from '../../hooks/stores/useAnonModeStore';
import useGeneralStore from '../../hooks/stores/useGeneralStore';
const ReplyModal = ({ isOpen, closeModal }) => {
const {
captchaResponse,
setCaptchaResponse,
setChallengesArray,
setIsCaptchaOpen,
setPendingComment,
setPendingCommentIndex,
replyQuoteCid,
setResolveCaptchaPromise,
selectedAddress,
selectedParentCid,
selectedShortCid,
selectedStyle,
selectedText,
setSelectedText,
triggerInsertion,
} = useGeneralStore((state) => state);
const { anonymousMode } = useAnonModeStore();
const account = useAccount();
const [, setNewErrorMessage] = useError();
const nodeRef = useRef(null);
const nameRef = useRef();
const commentRef = useRef();
const linkRef = useRef();
const [triggerPublishComment, setTriggerPublishComment] = useState(false);
const [isMobile, setIsMobile] = useState(window.innerWidth <= 480);
const [executeAnonMode, setExecuteAnonMode] = useState(false);
useAnonMode(selectedParentCid, anonymousMode && executeAnonMode);
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth <= 480);
window.addEventListener('resize', handleResize);
// Cleanup function
return () => {
window.removeEventListener('resize', handleResize);
};
}, [setIsMobile]);
const onModalOpen = () => {
if (commentRef.current) {
if (selectedText) {
commentRef.current.value += '\n';
}
commentRef.current.focus();
const len = commentRef.current.value.length;
commentRef.current.setSelectionRange(len, len);
}
};
const insertAtCursor = (inputElement, valueToInsert) => {
const startPos = inputElement.selectionStart || inputElement.value.length;
const endPos = startPos + valueToInsert.length;
inputElement.setRangeText(valueToInsert, startPos, startPos, 'end');
inputElement.setSelectionRange(endPos, endPos);
};
useEffect(() => {
if (replyQuoteCid && commentRef.current) {
const prefixedReplyQuoteCid = `c/${replyQuoteCid}\n`;
insertAtCursor(commentRef.current, prefixedReplyQuoteCid);
}
}, [triggerInsertion, replyQuoteCid]);
const getSelectedText = useCallback(() => {
const text = document.getSelection().toString();
setSelectedText(text ? `>${text}\n` : '');
}, [setSelectedText]);
useEffect(() => {
if (isOpen) {
setTimeout(getSelectedText, 0);
} else {
setSelectedText('');
}
}, [isOpen, getSelectedText, setSelectedText]);
const onChallengeVerification = (challengeVerification) => {
if (challengeVerification.challengeSuccess === true) {
return;
} else if (challengeVerification.challengeSuccess === false) {
setNewErrorMessage(`Challenge Failed, reason: ${challengeVerification.reason}. Errors: ${challengeVerification.errors}`);
console.log('challenge failed', challengeVerification);
}
};
const onChallenge = async (challenges, comment) => {
setPendingComment(comment);
let challengeAnswers = [];
try {
challengeAnswers = await getChallengeAnswersFromUser(challenges);
} catch (error) {
setNewErrorMessage(error.message);
console.log(error);
}
if (challengeAnswers) {
await comment.publishChallengeAnswers(challengeAnswers);
}
};
useEffect(() => {
setPublishCommentOptions((prevPublishCommentOptions) => ({
...prevPublishCommentOptions,
subplebbitAddress: selectedAddress,
}));
}, [selectedAddress]);
const [publishCommentOptions, setPublishCommentOptions] = useState({
subplebbitAddress: selectedAddress,
onChallenge,
onChallengeVerification,
onError: (error) => {
setNewErrorMessage(error.message);
console.log(error);
},
});
const { publishComment, index } = usePublishComment(publishCommentOptions);
useEffect(() => {
if (index !== undefined) {
setPendingCommentIndex(index);
}
}, [index, setPendingCommentIndex]);
const resetFields = useCallback(() => {
if (nameRef.current) {
nameRef.current.value = '';
}
if (commentRef.current) {
commentRef.current.value = '';
}
if (linkRef.current) {
linkRef.current.value = '';
}
}, []);
const handleSubmit = async (event) => {
event.preventDefault();
if (commentRef.current.value === '' && linkRef.current.value === '') {
setNewErrorMessage('Please enter a comment or link.');
return;
}
setPublishCommentOptions((prevPublishCommentOptions) => ({
...prevPublishCommentOptions,
author: {
displayName: nameRef.current.value || undefined,
...(anonymousMode ? {} : { address: account?.author.address }),
},
content: commentRef.current.value || undefined,
link: linkRef.current.value || undefined,
parentCid: selectedParentCid,
}));
setTriggerPublishComment(true);
};
const updateSigner = useCallback(async () => {
if (anonymousMode) {
setExecuteAnonMode(true);
let storedSigners = JSON.parse(localStorage.getItem('storedSigners')) || {};
let signer;
if (!storedSigners[selectedParentCid]) {
signer = await account?.plebbit.createSigner();
storedSigners[selectedParentCid] = { privateKey: signer?.privateKey, address: signer?.address };
localStorage.setItem('storedSigners', JSON.stringify(storedSigners));
} else {
const signerPrivateKey = storedSigners[selectedParentCid].privateKey;
try {
signer = await account?.plebbit.createSigner({ type: 'ed25519', privateKey: signerPrivateKey });
} catch (error) {
console.log(error);
}
}
setPublishCommentOptions((prevPublishCommentOptions) => {
const newPublishCommentOptions = {
...prevPublishCommentOptions,
signer,
author: {
...prevPublishCommentOptions.author,
address: signer?.address,
},
};
if (JSON.stringify(prevPublishCommentOptions) !== JSON.stringify(newPublishCommentOptions)) {
return newPublishCommentOptions;
}
return prevPublishCommentOptions;
});
}
}, [selectedParentCid, anonymousMode, account]);
useEffect(() => {
if (anonymousMode) {
updateSigner();
}
}, [updateSigner, anonymousMode]);
useEffect(() => {
if (publishCommentOptions && triggerPublishComment) {
(async () => {
await publishComment();
resetFields();
closeModal();
})();
setTriggerPublishComment(false);
setExecuteAnonMode(false);
}
}, [publishCommentOptions, triggerPublishComment, publishComment, resetFields, closeModal]);
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 = captchaResponse;
resolve(currentCaptchaResponse);
setIsCaptchaOpen(false);
document.removeEventListener('keydown', handleKeyDown);
event.preventDefault();
}
};
setCaptchaResponse('');
document.addEventListener('keydown', handleKeyDown);
setResolveCaptchaPromise(resolve);
};
challengeImg.onerror = () => {
reject(setNewErrorMessage('Could not load challenges'));
};
});
};
return (
<StyledModal
isOpen={isOpen}
onAfterOpen={onModalOpen}
onRequestClose={closeModal}
contentLabel='Reply Modal'
shouldCloseOnEsc={true}
shouldCloseOnOverlayClick={isMobile}
selectedStyle={selectedStyle}
overlayClassName='overlay'
style={isMobile ? { overlay: { backgroundColor: 'rgba(0,0,0,.25)' } } : { overlay: { backgroundColor: 'rgba(0,0,0,0)' } }}
>
<Draggable handle='.modal-header' nodeRef={nodeRef} disabled={isMobile}>
<div className='modal-content' ref={nodeRef}>
<div className='modal-header'>
Reply to c/{selectedShortCid}
<button className='icon' onClick={() => closeModal()} title='close' />
</div>
<div id='form'>
<div>
{account?.author.displayName ? (
<input id='name' type='text' defaultValue={account?.author?.displayName} ref={nameRef} />
) : (
<input id='name' type='text' placeholder='Anonymous' ref={nameRef} />
)}
</div>
<div>
<input id='name' type='text' placeholder='Link' ref={linkRef} />
</div>
<div className='textarea-wrapper'>
<span className='fixed-text'>{`c/${selectedShortCid}`}</span>
<textarea className='textarea' rows='4' placeholder='Comment' defaultValue={selectedText} wrap='soft' ref={commentRef} />
</div>
<div>
<button id='next' onClick={handleSubmit}>
Post
</button>
</div>
</div>
</div>
</Draggable>
</StyledModal>
);
};
Modal.setAppElement('#root');
export default ReplyModal;
-763
View File
@@ -1,763 +0,0 @@
import React, { useState, useEffect, useMemo, useRef } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import Modal from 'react-modal';
import {
createAccount,
deleteAccount,
deleteCaches,
exportAccount,
importAccount,
setAccount,
setActiveAccount,
useAccount,
useAccounts,
useResolvedAuthorAddress,
} from '@plebbit/plebbit-react-hooks';
import stringify from 'json-stringify-pretty-compact';
import { StyledModal } from '../styled/modals/SettingsModal.styled';
import useError from '../../hooks/useError';
import useSuccess from '../../hooks/useSuccess';
import useAnonModeStore from '../../hooks/stores/useAnonModeStore';
import useGeneralStore from '../../hooks/stores/useGeneralStore';
import packageJson from '../../../package.json';
const { version } = packageJson;
const commitRef = process?.env?.REACT_APP_COMMIT_REF ? ` ${process.env.REACT_APP_COMMIT_REF.slice(0, 7)}` : '';
const SettingsModal = ({ isOpen, closeModal }) => {
const { selectedStyle } = useGeneralStore((state) => state);
const { anonymousMode, setAnonymousMode } = useAnonModeStore();
const navigate = useNavigate();
const location = useLocation();
const [expanded, setExpanded] = useState([]);
const [copyStatus, setCopyStatus] = useState(false);
const [ensName, setEnsName] = useState('');
const [checkedENS, setCheckedENS] = useState(false);
const [triggerSwitchAccount, setTriggerSwitchAccount] = useState(false);
const [, setNewErrorMessage] = useError();
const [, setNewSuccessMessage] = useSuccess();
const account = useAccount();
const { accounts } = useAccounts();
const accountJson = useMemo(() => {
if (account) {
const { plebbit, karma, unreadNotificationCount, ...restOfAccount } = account;
return stringify({ account: restOfAccount });
}
}, [account]);
const [editedAccountJson, setEditedAccountJson] = useState(accountJson);
useEffect(() => {
setEditedAccountJson(accountJson);
}, [accountJson]);
const gatewayRef = useRef();
const ipfsRef = useRef();
const pubsubRef = useRef();
const ethereumRpcRef = useRef();
const polygonRpcRef = useRef();
const dataPathRef = useRef();
const importRef = useRef();
const nameRef = useRef();
const ensRef = useRef();
const isElectron = window.electron && window.electron.isElectron;
const author = { ...account?.author, address: ensName };
const { resolvedAddress, state } = useResolvedAuthorAddress({ author, cache: false });
const defaultGatewayUrls = isElectron ? undefined : ['https://ipfs.io', 'https://ipfsgateway.xyz', 'https://cloudflare-ipfs.com', 'https://plebpubsub.live'];
const defaultPubsubHttpClientsOptions = isElectron ? undefined : ['https://pubsubprovider.xyz/api/v0', 'https://plebpubsub.live/api/v0'];
const defaultEthereumRpcUrls = ['ethers.js', 'https://ethrpc.xyz', 'viem'];
const defaultPolygonRpcUrls = ['https://polygon-rpc.com'];
useEffect(() => {
if (checkedENS && resolvedAddress && state === 'succeeded') {
setCheckedENS(false);
}
}, [checkedENS, state, resolvedAddress]);
const handleENSChange = () => {
const newEnsName = ensRef.current.value;
setEnsName(newEnsName);
};
const handleENSCheck = () => {
const newEnsName = ensRef.current.value;
setEnsName(newEnsName);
setCheckedENS(true);
};
const handleENSSave = async () => {
if (resolvedAddress === account?.signer?.address) {
try {
await setAccount({
...account,
author: {
...account?.author,
address: ensName,
},
});
setAnonymousMode(false);
localStorage.setitem('successToast', 'ENS name saved successfully. Anon mode has been disabled.');
window.location.reload();
} catch (error) {
setNewErrorMessage(error.message);
console.log(error);
}
} else if (resolvedAddress !== account?.signer?.address) {
setNewErrorMessage(`Failed resolving address, ${ensName}`);
}
};
const isValidURL = (url) => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
const handleCopyAddress = async () => {
try {
await navigator.clipboard.writeText(account?.author.address);
setCopyStatus(true);
} catch (err) {
console.error('Failed to copy text: ', err);
}
};
useEffect(() => {
let copyStatusTimeoutId;
let checkedENSTimeoutId;
if (copyStatus) {
copyStatusTimeoutId = setTimeout(() => setCopyStatus(false), 3000);
}
if (checkedENS) {
checkedENSTimeoutId = setTimeout(() => setCheckedENS(false), 5000);
}
return () => {
clearTimeout(copyStatusTimeoutId);
clearTimeout(checkedENSTimeoutId);
};
}, [copyStatus, checkedENS]);
const handleSavePlebbitOptions = async () => {
let gatewayUrls = gatewayRef.current.value.split('\n').filter((url) => url.trim());
let ipfsClientsOptions = ipfsRef.current.value.split('\n').filter((url) => url.trim());
let pubsubClientsOptions = pubsubRef.current.value.split('\n').filter((url) => url.trim());
if (!isElectron) {
if (!gatewayUrls.length) gatewayUrls = defaultGatewayUrls;
if (!pubsubClientsOptions.length) pubsubClientsOptions = defaultPubsubHttpClientsOptions;
} else {
if (!gatewayUrls.length) gatewayUrls = undefined;
if (!pubsubClientsOptions.length) pubsubClientsOptions = undefined;
}
if (!ipfsClientsOptions.length) {
ipfsClientsOptions = undefined;
}
const invalidUrls = [
...(Array.isArray(gatewayUrls) ? gatewayUrls : Array.isArray(defaultGatewayUrls) ? defaultGatewayUrls : []),
...(Array.isArray(ipfsClientsOptions) ? ipfsClientsOptions : []),
...(Array.isArray(pubsubClientsOptions) ? pubsubClientsOptions : Array.isArray(defaultPubsubHttpClientsOptions) ? defaultPubsubHttpClientsOptions : []),
].filter((url) => !isValidURL(url));
if (invalidUrls.length > 0) {
setNewErrorMessage(`Invalid URL(s): ${invalidUrls.join(', ')}`);
return;
}
const plebbitOptions = {
ipfsGatewayUrls: gatewayUrls,
ipfsHttpClientsOptions: ipfsClientsOptions,
pubsubHttpClientsOptions: pubsubClientsOptions,
};
try {
await setAccount({ ...account, plebbitOptions });
localStorage.setItem('successToast', 'Settings saved successfully.');
window.location.reload();
} catch (error) {
setNewErrorMessage(error.message);
console.log(error);
}
};
const handleSaveChainProviders = async () => {
let ethereumRpcUrls = ethereumRpcRef.current.value.split('\\n').filter((url) => url.trim());
let polygonRpcUrls = polygonRpcRef.current.value.split('\\n').filter((url) => url.trim());
if (!ethereumRpcUrls.length) {
ethereumRpcUrls = defaultEthereumRpcUrls;
}
if (!polygonRpcUrls.length) {
polygonRpcUrls = defaultPolygonRpcUrls;
}
const invalidUrls = [...(ethereumRpcUrls || defaultEthereumRpcUrls), ...(polygonRpcUrls || defaultPolygonRpcUrls)].filter((url) => !isValidURL(url));
if (invalidUrls.length > 0) {
setNewErrorMessage(`Invalid URL(s): ${invalidUrls.join(', ')}`);
return;
}
const chainProviders = {
eth: {
urls: ethereumRpcUrls,
chainId: 1,
},
matic: {
urls: polygonRpcUrls,
chainId: 137,
},
};
try {
await setAccount({
...account,
plebbitOptions: {
...account?.plebbitOptions,
chainProviders,
},
});
localStorage.setItem('successToast', 'Blockchain options saved successfully.');
window.location.reload();
} catch (error) {
setNewErrorMessage(error.message);
console.log(error);
}
};
const handleResetPlebbitOptions = async () => {
setNewErrorMessage(null);
setNewSuccessMessage(null);
gatewayRef.current.value = defaultGatewayUrls ? defaultGatewayUrls.join('\n') : '';
ipfsRef.current.value = '';
pubsubRef.current.value = defaultPubsubHttpClientsOptions ? defaultPubsubHttpClientsOptions.join('\n') : '';
dataPathRef.current.value = '';
try {
await setAccount({
...account,
plebbitOptions: {
ipfsGatewayUrls: defaultGatewayUrls,
ipfsHttpClientsOptions: undefined,
pubsubHttpClientsOptions: defaultPubsubHttpClientsOptions,
},
});
localStorage.setItem('successToast', 'Settings reset successfully.');
window.location.reload();
} catch (error) {
setNewErrorMessage(error.message);
console.log(error);
}
};
const handleResetChainProviders = async () => {
setNewErrorMessage(null);
setNewSuccessMessage(null);
ethereumRpcRef.current.value = defaultEthereumRpcUrls.join('\n');
polygonRpcRef.current.value = defaultPolygonRpcUrls.join('\n');
const chainProviders = {
eth: {
urls: defaultEthereumRpcUrls,
chainId: 1,
},
matic: {
urls: defaultPolygonRpcUrls,
chainId: 137,
},
};
try {
await setAccount({
...account,
plebbitOptions: {
...account?.plebbitOptions,
chainProviders,
},
});
localStorage.setItem('successToast', 'Blockchain options reset successfully.');
window.location.reload();
} catch (error) {
setNewErrorMessage(error.message);
console.log(error);
}
};
const handleCloseModal = () => {
if (location.pathname.endsWith('/settings')) {
const newPath = location.pathname.slice(0, -9);
closeModal();
navigate(newPath, { replace: true });
} else {
closeModal();
}
};
const toggleExpanded = (index) => {
setExpanded((prevExpanded) => {
const newExpanded = [...prevExpanded];
if (newExpanded.includes(index)) {
newExpanded.splice(newExpanded.indexOf(index), 1);
} else {
newExpanded.push(index);
}
return newExpanded;
});
};
const expandAll = () => {
if (expanded.length === 4) {
setExpanded([]);
} else {
setExpanded([0, 1, 2, 3]);
}
};
useEffect(() => {
if (localStorage.getItem('cacheCleared') === 'true') {
setNewSuccessMessage('Cache cleared successfully.');
localStorage.removeItem('cacheCleared');
}
}, [setNewSuccessMessage]);
const handleSaveAccount = async () => {
const data = importRef.current.value;
const oldData = accountJson;
try {
const parsedJson = JSON.parse(data);
const newAccount = parsedJson.account;
await setAccount({ ...newAccount, id: account?.id });
if (!oldData.account?.author?.address.endsWith('.eth') && parsedJson.account?.author?.address.endsWith('.eth') && anonymousMode === true) {
setAnonymousMode(false);
localStorage.setItem('successToast', 'Account data saved successfully. ENS address detected, Anon mode disabled.');
window.location.reload();
} else {
setNewSuccessMessage('Account data saved successfully.');
}
} catch (error) {
setNewErrorMessage('Error saving account data: ' + error.message);
console.error(error);
}
};
const handleDeleteAccount = async () => {
if (window.confirm('Are you sure you want to delete this account?')) {
try {
await deleteAccount(account?.name);
localStorage.setItem('successToast', 'Account deleted successfully.');
window.location.reload();
} catch (error) {
setNewErrorMessage('Error deleting account: ' + error.message);
console.error(error);
}
}
};
const handleCreateAccount = async () => {
try {
await createAccount();
setNewSuccessMessage('Account created successfully.');
setTriggerSwitchAccount(true);
} catch (error) {
setNewErrorMessage('Error creating account: ' + error.message);
console.error(error);
}
const lastAccount = accounts[accounts.length - 1];
setActiveAccount(lastAccount.name);
};
useEffect(() => {
if (triggerSwitchAccount) {
const lastAccount = accounts[accounts.length - 1];
setActiveAccount(lastAccount.name);
setTriggerSwitchAccount(false);
}
}, [accounts, triggerSwitchAccount]);
const handleAccountChange = (e) => {
setActiveAccount(e.target.value);
};
const handleDisplayName = async () => {
const name = nameRef.current.value;
try {
await setAccount({
...account,
name: name || account?.name,
author: {
...account?.author,
displayName: name,
},
});
setNewSuccessMessage('Account name saved successfully.');
} catch (error) {
setNewErrorMessage(error.message);
console.log(error);
}
};
const [switchToLastAccount, setSwitchToLastAccount] = useState(false);
useEffect(() => {
if (switchToLastAccount && accounts.length > 0) {
const lastAccount = accounts[accounts.length - 1];
setActiveAccount(lastAccount.name);
setSwitchToLastAccount(false);
}
}, [accounts, switchToLastAccount]);
const _importAccount = async () => {
// Create a file input element
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json';
// Handle file selection
fileInput.onchange = async (event) => {
try {
const files = event.target.files;
if (!files || files.length === 0) {
throw new Error('No file selected.');
}
const file = files[0];
// Read the file content
const reader = new FileReader();
reader.onload = async (e) => {
const fileContent = e.target.result;
if (typeof fileContent !== 'string') {
throw new Error('File content is not a string.');
}
const newAccount = JSON.parse(fileContent);
await importAccount(fileContent);
setSwitchToLastAccount(true);
alert(`Imported ${newAccount.account?.name}`);
};
reader.readAsText(file);
} catch (error) {
if (error instanceof Error) {
alert(error.message);
console.log(error);
} else {
console.error('An unknown error occurred:', error);
}
}
};
// Trigger file selection dialog
fileInput.click();
};
const _exportAccount = async () => {
try {
const accountString = await exportAccount();
const accountObject = JSON.parse(accountString);
const formattedAccountJson = JSON.stringify(accountObject, null, 2);
// Create a Blob from the JSON string
const blob = new Blob([formattedAccountJson], { type: 'application/json' });
// Create a URL for the Blob
const fileUrl = URL.createObjectURL(blob);
// Create a temporary download link
const link = document.createElement('a');
link.href = fileUrl;
link.download = `${account.name}.json`;
// Append the link, trigger the download, then remove the link
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Release the Blob URL
URL.revokeObjectURL(fileUrl);
} catch (error) {
if (error instanceof Error) {
alert(error.message);
console.log(error);
} else {
console.error('An unknown error occurred:', error);
}
}
};
return (
<StyledModal
isOpen={isOpen}
onRequestClose={handleCloseModal}
contentLabel='Settings'
style={{ overlay: { backgroundColor: 'rgba(0,0,0,.25)' } }}
selectedStyle={selectedStyle}
expanded={expanded}
>
<div className='panel'>
<div className='panel-header'>
<span id='version'>
v{version}
{commitRef} -&nbsp;
{window.electron && window.electron.isElectron ? (
<Link className='all-button' id='node-stats' to='http://localhost:5001/webui/' target='_blank' rel='noreferrer'>
full node
</Link>
) : (
<span>web</span>
)}
</span>
Settings
<Link to='' onClick={handleCloseModal}>
<span className='icon' title='close' />
</Link>
</div>
<div className='all-div'>
[
<button className='all-button' onClick={expandAll} style={{ all: 'unset', cursor: 'pointer' }}>
{expanded.length === 4 ? 'Collapse All Settings' : 'Expand All Settings'}
</button>
]
</div>
<ul>
<li className='settings-cat-lbl'>
<span className={`${expanded.includes(1) ? 'minus' : 'plus'}`} onClick={() => toggleExpanded(1)} />
<span className='settings-pointer' style={{ cursor: 'pointer' }} onClick={() => toggleExpanded(1)}>
Account
</span>
</li>
<ul className='settings-cat' style={{ display: expanded.includes(1) ? 'block' : 'none' }}>
<li className='anon-off'>
<label title={!anonymousMode ? 'Enable anon mode' : 'Disable anon mode'}>
<input
type='checkbox'
checked={anonymousMode}
onChange={() => {
setAnonymousMode(!anonymousMode);
window.location.reload();
}}
/>
&nbsp;Anon Mode
</label>
</li>
<li className='settings-tip anon-tip'>Use a newly generated u/address per thread to post.</li>
<li className='settings-option disc'>Account Data</li>
<li className='settings-tip'>Manually edit your account data using the preview below.</li>
<div className='settings-input'>
<textarea
id='account-data-text'
value={editedAccountJson || accountJson}
ref={importRef}
onChange={(e) => setEditedAccountJson(e.target.value)}
autoComplete='off'
autoCorrect='off'
spellCheck='false'
/>
<div className='account-buttons'>
<button onClick={handleSaveAccount}>Save</button>
<button onClick={() => setEditedAccountJson(accountJson)}>Reset</button>
</div>
</div>
<div className='account-options'>
<li className='settings-tip'>
<button onClick={_exportAccount}>Export</button> your full account data
</li>
<li className='settings-tip'>
<button onClick={_importAccount}>Import</button> full account data
</li>
<li className='settings-tip'>
<button onClick={handleCreateAccount}>Create</button> a new account
</li>
<li className='settings-tip'>
<button onClick={handleDeleteAccount}>Delete</button> this account
</li>
</div>
<li className='settings-option disc'>Account Address: u/{account?.author.shortAddress}</li>
<li className='settings-tip'>Select a different account to use in the dropdown below.</li>
<li>
<div className='settings-input'>
<select className='settings-select' value={account?.name} onChange={handleAccountChange}>
{accounts.map((account) => (
<option key={account?.name} value={account?.name}>
{account?.name}
</option>
))}
</select>
{account?.author.address.endsWith('.eth') ? null : (
<button style={{ marginLeft: '35px' }} className='save-button' id='save-name' onClick={handleCopyAddress}>
{copyStatus ? 'Copied!' : 'Copy Full Address'}
</button>
)}
</div>
</li>
<li className='settings-option disc'>Crypto Address</li>
<li className='settings-tip'>
{account?.author.address.endsWith('.eth')
? 'Your account address is already an ENS name.'
: 'Change your account address to an ENS name you own: in your ENS name page on ens.domains, click on "Records", "Edit Records", "Add record", add "plebbit-author-address" as record name, add your full address as value (copy it with the button above) and save.'}
</li>
<div className='settings-input'>
<input
className='settings-input'
style={{ marginLeft: '20px' }}
placeholder='address.eth'
ref={ensRef}
value={ensName}
onChange={handleENSChange}
disabled={checkedENS}
/>
<button className='save-button' id='save-name' onClick={handleENSSave}>
Save
</button>
<button className='save-button check-button' id='save-name' onClick={handleENSCheck}>
Check
</button>
</div>
{checkedENS && ensName === account?.signer?.address && (
<li className='settings-tip' style={{ marginTop: '10px', color: 'green' }}>
{ensName} has been acquired by you correctly.
</li>
)}
{checkedENS && ensName !== account?.signer?.address && (
<li className='settings-tip' style={{ marginTop: '10px', color: 'red' }}>
{ensName} has not been acquired by you yet.
</li>
)}
<li className='settings-option disc'>Account Name</li>
<li className='settings-tip'>
Change both your account name (default "Account 1") and display name (default "Anonymous"). This will not change your address.
</li>
<li>
<div className='settings-input'>
<input
className='settings-input'
style={{ marginLeft: '20px' }}
type='text'
ref={nameRef}
defaultValue={account?.author.displayName}
placeholder='Anonymous'
/>
<button className='save-button' id='save-name' onClick={handleDisplayName}>
Save
</button>
</div>
</li>
</ul>
</ul>
<ul>
<li className='settings-cat-lbl'>
<span className={`${expanded.includes(2) ? 'minus' : 'plus'}`} onClick={() => toggleExpanded(2)} />
<span className='settings-pointer' style={{ cursor: 'pointer' }} onClick={() => toggleExpanded(2)}>
IPFS Options
</span>
<div className='plebbit-options-buttons' style={{ display: expanded.includes(2) ? 'block' : 'none' }}>
<button className='save-button' onClick={handleSavePlebbitOptions}>
Save
</button>
<button className='reset-button' onClick={handleResetPlebbitOptions}>
Reset
</button>
</div>
</li>
<ul className='settings-cat' style={{ display: expanded.includes(2) ? 'block' : 'none', marginTop: '-10px' }}>
<li className='settings-option disc'>IPFS Gateway URLs</li>
<li className='settings-tip'>Optional URLs of IPFS gateways.</li>
<div className='settings-input'>
<textarea placeholder='IPFS Gateway URLs' defaultValue={isElectron ? '' : account?.plebbitOptions?.ipfsGatewayUrls.join('\n')} ref={gatewayRef} />
</div>
</ul>
<ul className='settings-cat' style={{ display: expanded.includes(2) ? 'block' : 'none' }}>
<li className='settings-option disc'>IPFS HTTP Clients Options</li>
<li className='settings-tip'>Optional URLs of IPFS APIs or IpfsHttpClientOptions, 'http://localhost:5001/api/v0' to use a local IPFS node.</li>
<div className='settings-input'>
<textarea
placeholder='IPFS HTTP Clients Options'
defaultValue={account?.plebbitOptions?.ipfsHttpClientsOptions ? account?.plebbitOptions?.ipfsHttpClientsOptions.join('\n') : ''}
ref={ipfsRef}
/>
</div>
</ul>
<ul className='settings-cat' style={{ display: expanded.includes(2) ? 'block' : 'none' }}>
<li className='settings-option disc'>PubSub HTTP Clients Options</li>
<li className='settings-tip'>
Optional URLs or IpfsHttpClientOptions used for pubsub publishing when ipfsHttpClientOptions isn't available, like in the browser.
</li>
<div className='settings-input'>
<textarea
placeholder='PubSub HTTP Clients Options'
defaultValue={isElectron ? '' : account?.plebbitOptions?.pubsubHttpClientsOptions.join('\n')}
ref={pubsubRef}
/>
</div>
</ul>
<ul className='settings-cat' style={{ display: expanded.includes(2) ? 'block' : 'none' }}>
<li className='settings-option disc'>Data Path (Node Only)</li>
<li className='settings-tip'>Optional folder path to create/resume the user and subplebbit databases.</li>
<div className='settings-input'>
<textarea placeholder='Data Path (Node Only)' ref={dataPathRef} />
</div>
</ul>
</ul>
<ul>
<li className='settings-cat-lbl'>
<span className={`${expanded.includes(3) ? 'minus' : 'plus'}`} onClick={() => toggleExpanded(3)} />
<span className='settings-pointer' style={{ cursor: 'pointer' }} onClick={() => toggleExpanded(3)}>
Blockchain Options
</span>
<div className='plebbit-options-buttons' style={{ display: expanded.includes(3) ? 'block' : 'none' }}>
<button className='save-button' onClick={handleSaveChainProviders}>
Save
</button>
<button className='save-button' onClick={handleResetChainProviders}>
Reset
</button>
</div>
</li>
<ul className='settings-cat' style={{ display: expanded.includes(3) ? 'block' : 'none', marginTop: '-10px' }}>
<li className='settings-option disc'>Ethereum RPC</li>
<li className='settings-tip'>Needed for .eth addresses.</li>
<div className='settings-input'>
<textarea placeholder='Ethereum RPC URLs' defaultValue={account?.plebbitOptions?.chainProviders?.['eth']?.urls.join('\n')} ref={ethereumRpcRef} />
</div>
<li className='settings-option disc'>Polygon RPC</li>
<li className='settings-tip'>Needed for XPLEB NFTs.</li>
<div className='settings-input'>
<textarea placeholder='Polygon RPC URLs' defaultValue={account?.plebbitOptions?.chainProviders?.['matic']?.urls.join('\n')} ref={polygonRpcRef} />
</div>
</ul>
</ul>
<div>
<button
className='cache-button'
onClick={async () => {
if (window.confirm('Are you sure you want to clear the cache?')) {
await deleteCaches();
localStorage.setItem('cacheCleared', 'true');
window.location.reload();
}
}}
>
Clear Cache
</button>
</div>
</div>
</StyledModal>
);
};
Modal.setAppElement('#root');
export default SettingsModal;
-101
View File
@@ -1,101 +0,0 @@
import styled from 'styled-components';
export const BoardStatsContainer = styled.div`
@media (max-width: 480px) {
display: none;
}
@media (min-width: 480px) {
width: 468px;
margin: auto;
line-height: 1;
margin-top: -1px;
table {
border-spacing: 0px;
width: 100%;
margin-top: -1px;
padding-left: 3px;
}
tr {
vertical-align: top;
font-size: 11px;
}
}
tfoot {
text-align: right;
}
.hide-button:hover {
cursor: pointer;
}
${({ selectedStyle }) => {
switch (selectedStyle) {
case 'Yotsuba':
return `
#stat-number {
color: #00e;
}
.hide-button:hover {
color: red !important;
}`;
case 'Yotsuba-B':
return `
#stat-number {
color: #34345c;
}
.hide-button:hover {
color: #d00 !important;
}`;
case 'Futaba':
return `
#stat-number {
color: #00e;
}
.hide-button:hover {
color: red !important;
}`;
case 'Burichan':
return `
#stat-number {
color: #34345c;
}
.hide-button:hover {
color: red !important;
}`;
case 'Tomorrow':
return `
#stat-number {
color: #81a2be;
}
.hide-button:hover {
color: #5f89ab !important;
}`;
case 'Photon':
return `
#stat-number {
color: #f60;
}
.hide-button:hover {
color: #ff3300 !important;
}`;
default:
return '';
}
}}
`;
@@ -1,203 +0,0 @@
import { createGlobalStyle } from 'styled-components';
export const GlobalStyle = createGlobalStyle`
*:focus {
outline: none;
}
body {
margin: 0;
padding: 0;
background: ${(props) => props.background};
color: ${(props) => props.color};
font-family: ${(props) => props.fontFamily};
}
.tooltip {
z-index: 999;
border-radius: 0;
max-width: 40%;
font-size: 11px;
padding: 3px;
opacity: 100%;
word-wrap: break-word;
}
.line-break {
white-space: pre-line;
}
.custom-paragraph {
margin: 0;
padding: 0;
}
.custom-linebreak {
display: block;
margin: 0;
padding: 0;
}
.expanded-container {
display: inline-block;
}
.poster-address {
display: inline-block;
overflow-x: clip;
}
.author-address-hidden {
visibility: hidden;
user-select: none;
}
.author-address-visible {
float: left;
width: 0;
white-space: nowrap;
}
@keyframes authorAddressChangedAnimation {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.author-address-changed {
animation-name: authorAddressChangedAnimation;
animation-duration: 0.2s;
animation-timing-function: ease-out;
animation-delay: 0s;
animation-iteration-count: 1;
}
@media (min-width: 480px) {
.enlarged {
display: block;
max-width: 100% !important;
max-height: 100% !important;
}
.instagram-embed {
height: 420px;
width: 360px;
}
.streamable-embed,
.bitchute-embed,
.odysee-embed,
.youtube-embed,
.rumble-embed,
.twitch-embed {
height: 450px;
width: 800px;
}
.spotify-embed {
height: 240px;
width: 700px;
}
.tiktok-embed {
width: 400px;
height: 780px;
}
.twitter-embed {
width: 400px;
height: 580px;
}
.reddit-embed {
width: 500px;
height: 520px;
}
}
@media (max-width: 480px) {
.enlarged {
max-width: 100% !important;
max-height: 100% !important;
}
.instagram-embed {
width: 100vw !important;
height: 45vh !important;
}
.streamable-embed,
.bitchute-embed,
.odysee-embed,
.youtube-embed,
.rumble-embed,
.twitch-embed {
width: 100vw !important;
height: 30vh !important;
}
.spotify-embed {
width: 100vw !important;
height: 40vh !important;
}
.tiktok-embed {
width: 100vw !important;
height: 70vh !important;
}
.twitter-embed {
width: 100vw !important;
height: 65vh !important;
}
.reddit-embed {
width: 100vw !important;
height: 55vh !important;
}
}
.post-menu-catalog {
position: absolute;
font-size: 12px;
line-height: 1.3em;
list-style: none;
ul {
padding: 0;
margin: 0;
white-space: nowrap;
}
li {
cursor: pointer;
position: relative;
padding: 2px 4px;
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-menu {
display: none;
position: absolute;
left: 100%;
top: 0;
list-style: none;
}
.dropdown:hover .dropdown-menu {
display: block;
}
a {
text-decoration: none;
color: inherit;
}
}
`;
-12
View File
@@ -1,12 +0,0 @@
import { ToastContainer } from 'react-toastify';
import styled from 'styled-components';
export const Toast = styled(ToastContainer)`
.Toastify__toast {
border-radius: 0px;
font-size: 11pt;
animation-duration: 0s;
border: 1px solid #fee9cd;
color: #c5c8c6;
}
`;
@@ -1,74 +0,0 @@
import styled from 'styled-components';
export const AlertModal = styled.div`
.author-delete-alert {
padding: 20px;
border: 1px solid #ccc;
position: fixed;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
.author-delete-buttons {
display: flex;
justify-content: center;
& > button {
margin-top: 10px;
}
& > button:first-child {
margin-right: 20px;
}
}
}
${({ selectedStyle }) => {
switch (selectedStyle) {
case 'Yotsuba':
return `
.author-delete-alert {
background-color: #f0e0d6;
border: 1px solid #d9bfb7;
}`;
case 'Yotsuba-B':
return `
.author-delete-alert {
background-color: #d6daf0;
border: 1px solid #b7c5d9;
}`;
case 'Futaba':
return `
.author-delete-alert {
background-color: #f0e0d6;
border: 1px solid #d9bfb7;
}`;
case 'Burichan':
return `
.author-delete-alert {
background-color: #d6daf0;
border: 1px solid #b7c5d9;
}`;
case 'Tomorrow':
return `
.author-delete-alert {
background-color: #282a2e;
border: 1px solid #111;
}`;
case 'Photon':
return `
.author-delete-alert {
background-color: #ddd;
border: 1px solid #ccc;
}`;
default:
return '';
}
}}
`;
@@ -1,244 +0,0 @@
import styled from 'styled-components';
import Modal from 'react-modal';
export const StyledModal = styled(Modal)`
.hide-modal-overlay {
display: none;
}
.modal-content {
position: fixed;
top: calc(50% - 150px);
left: calc(50% - 150px);
display: block;
padding: 2px;
font-size: 10pt;
border-left: none;
border-top: none;
}
.modal-header {
font-size: 10pt;
text-align: center;
margin-bottom: 1px;
padding: 0;
height: 18px;
line-height: 18px;
cursor: move;
font-weight: 700;
}
.icon {
all: unset;
float: right;
cursor: pointer;
margin-bottom: -4px;
width: 18px;
height: 18px;
border: none;
image-rendering: pixelated;
}
#field {
border: 1px solid #aaa;
font-family: Arial, Helvetica, sans-serif;
font-size: 10pt;
outline: medium none;
width: 298px;
padding: 2px;
}
textarea {
min-width: 296px;
float: left;
font-size: 10pt;
font-family: Arial, Helvetica, sans-serif;
}
#captcha-container {
position: relative;
width: 302px;
height: 100px;
overflow: hidden;
margin-bottom: 17px;
}
#response {
width: 100%;
box-sizing: border-box;
font-size: 11px;
height: 18px;
margin: 0px;
padding: 0px 2px;
font-family: monospace;
vertical-align: middle;
}
img {
height: 100%;
width: 100%;
position: relative;
display: block;
}
#nav {
float: right;
margin: 0;
width: auto;
}
${({ selectedStyle }) => {
switch (selectedStyle) {
case 'Yotsuba':
return `
.modal-content {
background-color: #f0e0d6;
border: 1px solid #d9bfb7;
}
.modal-header {
background-color: #ea8;
color: #800;
border: 1px solid #800;
}
.icon {
background-image: url(assets/buttons/cross_red.png);
}
span {
color: maroon;
}`;
case 'Yotsuba-B':
return `
.modal-content {
background-color: #d6daf0;
border: 1px solid #b7c5d9;
}
.modal-header {
background-color: #98e;
color: #000;
border: 1px solid #000;
}
.icon {
background-image: url(assets/buttons/cross_blue.png);
}
span {
color: #000;
}`;
case 'Futaba':
return `
.modal-content {
background-color: #f0e0d6;
border: 1px solid #d9bfb7;
}
.modal-header {
background-color: #ea8;
color: #800;
border: 1px solid #800;
}
.icon {
background-image: url(assets/buttons/cross_red.png);
}
span {
color: maroon;
}`;
case 'Burichan':
return `
.modal-content {
background-color: #d6daf0;
border: 1px solid #b7c5d9;
}
.modal-header {
background-color: #98e;
color: #000;
border: 1px solid #000;
}
.icon {
background-image: url(assets/buttons/cross_blue.png);
}
span {
color: #000;
}`;
case 'Tomorrow':
return `
.modal-content {
background-color: #282a2e;
border: 1px solid #111;
}
.modal-header {
background-color: #282a2e;
color: #c5c8c6;
}
.icon {
background-image: url(assets/buttons/cross_dark.png);
}
#field {
background-color: #282a2e;
color: #c5c8c6;
border: 1px solid #515151;
width: 296px;
}
textarea {
background-color: #282a2e;
color: #c5c8c6;
border: 1px solid #515151;
}
#response {
background-color: #282a2e;
color: #c5c8c6;
outline: none;
border: 1px solid #515151;
}
span {
color: #c5c8c6;
}
#next {
filter: brightness(80%);
}`;
case 'Photon':
return `
.modal-content {
background-color: #ddd;
border: 1px solid #ccc;
}
.modal-header {
background-color: #ddd;
color: #333;
}
.icon {
background-image: url(assets/buttons/cross_photon.png);
}
span {
color: #333;
}`;
default:
return '';
}
}}
`;
@@ -1,335 +0,0 @@
import styled from 'styled-components';
import Modal from 'react-modal';
export const StyledModal = styled(Modal)`
.modal-content {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 450px;
height: auto;
max-height: calc(100vh - 2 * 50px);
overflow-y: auto;
padding: 5px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
}
.modal-header {
font-size: 16px;
font-weight: 700;
margin-bottom: 5px;
margin-top: 1px;
padding-bottom: 5px;
text-align: center;
line-height: 14px;
}
.icon {
all: unset;
float: right;
cursor: pointer;
margin-bottom: -4px;
width: 18px;
height: 18px;
border: none;
image-rendering: pixelated;
}
#name,
#description,
#rule {
border: 1px solid #aaa;
font-family: Arial, Helvetica, sans-serif;
font-size: 10pt;
outline: medium none;
width: 298px;
padding: 2px;
margin-bottom: 10px;
}
#form {
margin: 20px;
}
textarea {
min-width: 296px;
float: left;
font-size: 10pt;
font-family: Arial, Helvetica, sans-serif;
margin-top: 0.5px;
margin-left: 25px;
}
.rule-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1px;
}
.rule-item p {
margin: 0;
padding: 5px;
width: 200px;
font-size: 0.8em;
}
#rule {
}
#rule-btn {
margin-left: 281px;
}
fieldset {
margin-left: 5px;
width: 90%;
}
legend {
margin-left: 260px;
}
#create-board-btn {
margin: auto;
margin-bottom: -10px;
margin-top: 10px;
display: block;
text-align: center;
}
.settings-option {
margin: 5px 0 5px 0;
padding-left: 23px;
display: inline-block;
}
.settings-tip {
font-size: 0.85em;
margin: 0 0 5px 0;
padding-left: 23px;
}
.settings-option.disc::before {
content: '';
display: inline-block;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: currentColor;
position: absolute;
margin-left: -15px;
margin-top: 6px;
}
.settings-option {
margin: 5px 0 5px 0;
padding-left: 23px;
display: inline-block;
position: relative;
}
.settings-input {
position: relative;
padding-left: 23px;
input {
padding-left: 5px;
left: 4px;
font-family: monospace;
}
}
.settings-input::before {
content: '';
display: block;
width: 10px;
height: 1px;
background-color: currentColor;
position: absolute;
left: 28px;
top: 13px;
}
.settings-input::after {
content: '';
display: block;
width: 1px;
height: 12px;
background-color: currentColor;
position: absolute;
left: 28px;
top: 1px;
}
input {
margin-left: 25px;
margin-top: 3px;
}
ul {
list-style: none;
padding: 0;
margin: 0 0 10px;
}
#explaination {
font-size: 0.8em;
margin: -10px 0 10px 0;
}
${({ selectedStyle }) => {
switch (selectedStyle) {
case 'Yotsuba':
return `
.modal-content {
background-color: #f0e0d6;
border: 1px solid #d9bfb7;
}
.modal-header {
border-bottom: 1px solid #d9bfb7;
}
.icon {
background-image: url(assets/buttons/cross_red.png);
}
span {
color: #000;
}`;
case 'Yotsuba-B':
return `
.modal-content {
background-color: #d6daf0;
border: 1px solid #b7c5d9;
}
.modal-header {
border-bottom: 1px solid #b7c5d9;
}
.icon {
background-image: url(assets/buttons/cross_blue.png);
}
span {
color: #000;
}`;
case 'Futaba':
return `
.modal-content {
background-color: #f0e0d6;
border: 1px solid #d9bfb7;
}
.modal-header {
border-bottom: 1px solid rgba(0,0,0,.2);
}
.icon {
background-image: url(assets/buttons/cross_red.png);
}
span {
color: #000;
}`;
case 'Burichan':
return `
.modal-content {
background-color: #d6daf0;
border: 1px solid #b7c5d9;
}
.modal-header {
border-bottom: 1px solid rgba(0,0,0,.2);
}
.icon {
background-image: url(assets/buttons/cross_blue.png);
}
span {
color: #000;
}`;
case 'Tomorrow':
return `
.modal-content {
background-color: #282a2e;
border: 1px solid #111;
}
.modal-header {
border-bottom: 1px solid #111;
}
.icon {
background-image: url(assets/buttons/cross_dark.png);
}
#name, #description, #rule {
background-color: #282a2e;
color: #c5c8c6;
border: 1px solid #515151;
width: 296px;
}
textarea {
background-color: #282a2e;
color: #c5c8c6;
border: 1px solid #515151;
}
#response {
background-color: #282a2e;
color: #c5c8c6;
outline: none;
border: 1px solid #515151;
}
span {
color: #c5c8c6;
}
button, select, input, textarea, fieldset {
filter: brightness(80%);
}
fieldset button {
filter: brightness(100%);
}
textarea, input {
color: white !important;
}`;
case 'Photon':
return `
.modal-content {
background-color: #ddd;
border: 1px solid #ccc;
border-bottom: 1px solid rgba(0, 0, 0, 0.20);
}
.modal-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.20);
}
.icon {
background-image: url(assets/buttons/cross_photon.png);
}
span {
color: #000;
}`;
default:
return '';
}
}}
`;
@@ -1,388 +0,0 @@
import styled from 'styled-components';
import Modal from 'react-modal';
export const StyledModal = styled(Modal)`
.panel {
top: 120px;
width: 400px;
height: auto;
max-height: 80%;
left: calc(50% - 25px);
overflow-y: auto;
margin-left: -178px;
position: absolute;
padding: 2px 5px 5px;
font-size: 14px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
@media (max-width: 480px) {
width: 320px !important;
max-height: 60% !important;
left: calc(50% + 15px) !important;
}
}
.panel-board {
top: 120px;
width: 700px;
height: auto;
max-height: 80%;
left: calc(50% - 350px);
overflow-y: auto;
position: absolute;
padding: 2px 5px 5px;
font-size: 14px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
@media (max-width: 800px) {
width: 80%;
max-height: 60%;
left: calc(50% - 40%);
}
@media (max-width: 480px) {
width: 320px;
max-height: 60%;
left: calc(50% - 160px);
}
}
.panel-header {
font-size: 16px;
font-weight: 700;
margin-bottom: 5px;
margin-top: 5px;
padding-bottom: 5px;
text-align: center;
line-height: 14px;
}
.icon {
right: 5px;
width: 18px;
height: 18px;
display: block;
background-size: 100%;
cursor: pointer;
position: absolute;
top: 5px;
image-rendering: pixelated;
}
h4 {
font-size: 14px;
padding: 0;
margin: 10px 0 5px;
}
ul {
list-style: none;
padding: 0;
margin: 0 0 10px;
}
.settings-cat {
margin: 5px;
}
.settings-cat-lbl {
font-weight: 700;
margin: 10px 0 5px;
padding-left: 5px;
}
.settings-option {
margin: 5px 0 5px 0;
padding-left: 33px;
display: inline-block;
position: relative;
font-weight: 700;
}
.settings-tip {
font-size: 0.85em;
margin: 0 0 20px 0;
padding-left: 33px;
}
.settings-option.disc::before {
content: '';
display: inline-block;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: currentColor;
position: absolute;
margin-left: -19px;
margin-top: 6px;
}
.settings-input {
position: relative;
padding-left: 23px;
}
.settings-input::before {
content: '';
display: block;
width: 10px;
height: 1px;
background-color: currentColor;
position: absolute;
left: 35px;
top: 13px;
}
.settings-input::after {
content: '';
display: block;
width: 1px;
height: 12px;
background-color: currentColor;
position: absolute;
left: 35px;
top: 1px;
}
textarea {
width: 80%;
margin-left: 7%;
min-height: 50px;
}
.board-settings {
width: 83%;
margin-bottom: 20px;
margin-top: 20px;
height: 400px;
}
.save-button {
margin: auto;
margin-bottom: 10px;
margin-top: 20px;
display: block;
text-align: center;
}
.button-group {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 10px;
}
#save-board-settings,
#reset-board-settings {
text-align: center;
}
.settings-info {
overflow-x: auto;
max-width: 100%;
margin: 10px 10px 0 10px;
font-size: 0.9em;
line-break: anywhere;
}
.settings-info strong {
margin-bottom: 10px;
}
${({ selectedStyle }) => {
switch (selectedStyle) {
case 'Yotsuba':
return `
.panel, .panel-board {
background-color: #f0e0d6;
}
.panel-header {
border-bottom: 1px solid #d9bfb7;
}
.icon {
background-image: url(assets/buttons/cross_red.png);
}
.plus {
background-image: url(assets/buttons/post_expand_plus_red.png);
}
.minus {
background-image: url(assets/buttons/post_expand_minus_red.png);
}
.all-button {
color: #00e !important;
:hover {
color: red !important;
}
}`;
case 'Yotsuba-B':
return `
.panel, .panel-board {
background-color: #d6daf0;
}
.panel-header {
border-bottom: 1px solid #b7c5d9;
}
.icon {
background-image: url(assets/buttons/cross_blue.png);
}
.plus {
background-image: url(assets/buttons/post_expand_plus_blue.png);
}
.minus {
background-image: url(assets/buttons/post_expand_minus_blue.png);
}
.all-button {
color: #34345c !important;
:hover {
color: #d00 !important;
}
}`;
case 'Futaba':
return `
.panel, .panel-board {
background-color: #f0e0d6;
}
.panel-header {
border-bottom: 1px solid rgba(0,0,0,.2);
}
.icon {
background-image: url(assets/buttons/cross_red.png);
}
.plus {
background-image: url(assets/buttons/post_expand_plus_red.png);
}
.minus {
background-image: url(assets/buttons/post_expand_minus_red.png);
}
.all-button {
color: #00e !important;
:hover {
color: red !important;
}
}`;
case 'Burichan':
return `
.panel, .panel-board {
background-color: #d6daf0;
}
.panel-header {
border-bottom: 1px solid rgba(0,0,0,.2);
}
.icon {
background-image: url(assets/buttons/cross_blue.png);
}
.plus {
background-image: url(assets/buttons/post_expand_plus_blue.png);
}
.minus {
background-image: url(assets/buttons/post_expand_minus_blue.png);
}
.all-button {
color: #34345c !important;
:hover {
color: #d00 !important;
}
}`;
case 'Tomorrow':
return `
.panel, .panel-board {
background-color: #282a2e;
}
.panel-header {
border-bottom: 1px solid #111;
}
.icon {
background-image: url(assets/buttons/cross_dark.png);
}
.plus {
background-image: url(assets/buttons/post_expand_plus_dark.png);
}
.minus {
background-image: url(assets/buttons/post_expand_minus_dark.png);
}
.all-button {
color: #81a2be !important;
:hover {
color: #5f89ac !important;
}
}
button, select, input {
filter: brightness(80%);
}
textarea {
background-color: #282a2e;
color: #c5c8c6;
border: 1px solid #000;
}`;
case 'Photon':
return `
.panel, .panel-board {
background-color: #ddd;
}
.panel-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.20);
}
.icon {
background-image: url(assets/buttons/cross_photon.png);
}
.plus {
background-image: url(assets/buttons/post_expand_plus_photon.png);
}
.minus {
background-image: url(assets/buttons/post_expand_minus_photon.png);
}
.all-button {
color: #f60 !important;
:hover {
color: #f30 !important;
}
}`;
default:
return '';
}
}}
`;
@@ -1,259 +0,0 @@
import styled from 'styled-components';
import Modal from 'react-modal';
export const StyledModal = styled(Modal)`
.modal-content {
position: fixed;
top: calc(50% - 150px);
left: calc(50% - 150px);
display: block;
padding: 2px;
font-size: 10pt;
border-left: none;
border-top: none;
}
.modal-header {
font-size: 10pt;
text-align: center;
margin-bottom: 1px;
padding: 0;
height: 18px;
line-height: 18px;
cursor: move;
font-weight: 700;
}
.icon {
all: unset;
float: right;
cursor: pointer;
margin-bottom: -4px;
width: 18px;
height: 18px;
border: none;
image-rendering: pixelated;
}
#name {
border: 1px solid #aaa;
font-family: Arial, Helvetica, sans-serif;
font-size: 10pt;
outline: medium none;
width: 298px;
padding: 2px;
margin-bottom: 1px;
}
textarea {
min-width: 296px;
float: left;
font-size: 10pt;
font-family: Arial, Helvetica, sans-serif;
margin-top: 0.5px;
}
.textarea-wrapper {
position: relative;
}
.textarea {
padding-top: 2em;
}
.fixed-text {
position: absolute;
top: 4px;
left: 5px;
text-decoration: underline;
pointer-events: none;
}
#captcha-container {
position: relative;
clear: both;
width: 302px;
overflow: hidden;
margin-bottom: 3px;
}
#response {
width: 100%;
box-sizing: border-box;
font-size: 11px;
height: 18px;
margin: 0px;
padding: 0px 2px;
font-family: monospace;
vertical-align: middle;
}
img {
height: 100%;
width: 100%;
margin-top: 2px;
position: relative;
display: block;
}
#next {
float: right;
margin: 0;
width: 75px;
}
${({ selectedStyle }) => {
switch (selectedStyle) {
case 'Yotsuba':
return `
.modal-content {
background-color: #f0e0d6;
border: 1px solid #d9bfb7;
}
.modal-header {
background-color: #ea8;
color: #800;
border: 1px solid #800;
}
.icon {
background-image: url(assets/buttons/cross_red.png);
}
span {
color: #000;
}`;
case 'Yotsuba-B':
return `
.modal-content {
background-color: #d6daf0;
border: 1px solid #b7c5d9;
}
.modal-header {
background-color: #98e;
color: #000;
border: 1px solid #000;
}
.icon {
background-image: url(assets/buttons/cross_blue.png);
}
span {
color: #000;
}`;
case 'Futaba':
return `
.modal-content {
background-color: #f0e0d6;
border: 1px solid #d9bfb7;
}
.modal-header {
background-color: #ea8;
color: #800;
border: 1px solid #800;
}
.icon {
background-image: url(assets/buttons/cross_red.png);
}
span {
color: #000;
}`;
case 'Burichan':
return `
.modal-content {
background-color: #d6daf0;
border: 1px solid #b7c5d9;
}
.modal-header {
background-color: #98e;
color: #000;
border: 1px solid #000;
}
.icon {
background-image: url(assets/buttons/cross_blue.png);
}
span {
color: #000;
}`;
case 'Tomorrow':
return `
.modal-content {
background-color: #282a2e;
border: 1px solid #111;
}
.modal-header {
background-color: #282a2e;
color: #c5c8c6;
}
.icon {
background-image: url(assets/buttons/cross_dark.png);
}
#name {
background-color: #282a2e;
color: #c5c8c6;
border: 1px solid #515151;
width: 296px;
}
textarea {
background-color: #282a2e;
color: #c5c8c6;
border: 1px solid #515151;
}
#response {
background-color: #282a2e;
color: #c5c8c6;
outline: none;
border: 1px solid #515151;
}
span {
color: #c5c8c6;
}
#next {
filter: brightness(80%);
}`;
case 'Photon':
return `
.modal-content {
background-color: #ddd;
border: 1px solid #ccc;
}
.modal-header {
background-color: #ddd;
color: #333;
}
.icon {
background-image: url(assets/buttons/cross_photon.png);
}
span {
color: #000;
}`;
default:
return '';
}
}}
`;
@@ -1,447 +0,0 @@
import styled from 'styled-components';
import Modal from 'react-modal';
export const StyledModal = styled(Modal)`
.account-options {
padding-top: 5px;
padding-left: 25px;
margin-bottom: -10px;
}
.panel {
top: 60px;
width: 400px;
height: auto;
max-height: 80%;
left: calc(50% - 25px);
overflow-y: auto;
margin-left: -178px;
position: absolute;
padding: 2px 5px 5px;
font-size: 14px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
}
#version {
font-size: 11px;
position: absolute;
display: block;
margin-left: 5px;
}
@media (max-width: 768px) {
.panel {
width: 350px !important;
max-height: 60% !important;
left: calc(50% - 2px) !important;
}
}
.panel-header {
font-size: 16px;
font-weight: 700;
margin-bottom: 5px;
margin-top: 5px;
padding-bottom: 5px;
text-align: center;
line-height: 14px;
}
.icon {
right: 5px;
width: 18px;
height: 18px;
display: block;
background-size: 100%;
cursor: pointer;
position: absolute;
top: 5px;
image-rendering: pixelated;
}
h4 {
font-size: 14px;
padding: 0;
margin: 10px 0 5px;
}
ul {
list-style: none;
padding: 0;
margin: 0 0 10px;
}
.settings-cat-lbl {
font-weight: 700;
margin: 10px 0 5px;
padding-left: 5px;
}
.plus,
.minus {
vertical-align: text-bottom;
margin-right: 5px;
cursor: pointer;
width: 18px;
height: 18px;
background-repeat: no-repeat;
background-position: center;
display: inline-block;
image-rendering: pixelated;
}
.settings-cat {
display: ${({ expanded }) =>
(index) =>
expanded?.includes(index) ? 'block' : 'none'};
margin: 5px;
}
textarea {
min-width: 80%;
min-height: 50px;
margin-left: 7%;
}
#account-data-text {
min-width: 70%;
min-height: 131px;
}
.account-buttons {
display: inline-block;
position: absolute;
margin: 5px 5px 5px 10px;
button {
margin-bottom: 5px;
}
}
.all-div {
text-align: center;
}
.plebbit-options-buttons {
display: block;
position: relative;
float: right;
button {
margin-right: 3px;
margin-left: 3px;
}
}
#save-name {
display: inline-block;
margin-left: 10px;
height: 23px;
position: absolute;
}
.check-button {
margin-left: 58px !important;
}
.cache-button {
margin: auto;
margin-bottom: 10px;
display: block;
text-align: center;
}
.settings-option {
margin: 15px 0 5px 0 !important;
padding-left: 23px;
display: inline-block;
position: relative;
}
.settings-tip {
font-size: 0.85em;
margin: 0 0 5px 0;
padding-left: 23px;
}
.settings-option.disc::before {
content: '';
display: inline-block;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: currentColor;
position: absolute;
margin-left: -15px;
margin-top: 6px;
}
.settings-input {
position: relative;
padding-left: 23px;
input {
padding-left: 5px;
left: 4px;
font-family: monospace;
}
}
.settings-input::before {
content: '';
display: block;
width: 10px;
height: 1px;
background-color: currentColor;
position: absolute;
left: 28px;
top: 13px;
}
.settings-input::after {
content: '';
display: block;
width: 1px;
height: 12px;
background-color: currentColor;
position: absolute;
left: 28px;
top: 1px;
}
.settings-select {
position: relative;
left: 23px;
}
.anon-off {
margin: 10px 0 5px -2px;
}
.anon-tip {
margin-bottom: 0px;
}
.anon-tip::before {
content: '';
display: block;
width: 10px;
height: 1px;
background-color: currentColor;
position: absolute;
left: 18px;
top: 120px;
}
.anon-tip::after {
content: '';
display: block;
width: 1px;
height: 12px;
background-color: currentColor;
position: absolute;
left: 18px;
top: 108px;
}
${({ selectedStyle }) => {
switch (selectedStyle) {
case 'Yotsuba':
return `
.panel {
background-color: #f0e0d6;
}
.panel-header {
border-bottom: 1px solid #d9bfb7;
}
.icon {
background-image: url(assets/buttons/cross_red.png);
}
.plus {
background-image: url(assets/buttons/post_expand_plus_red.png);
}
.minus {
background-image: url(assets/buttons/post_expand_minus_red.png);
}
.all-button {
color: #00e !important;
:hover {
color: red !important;
}
}`;
case 'Yotsuba-B':
return `
.panel {
background-color: #d6daf0;
}
.panel-header {
border-bottom: 1px solid #b7c5d9;
}
.icon {
background-image: url(assets/buttons/cross_blue.png);
}
.plus {
background-image: url(assets/buttons/post_expand_plus_blue.png);
}
.minus {
background-image: url(assets/buttons/post_expand_minus_blue.png);
}
.all-button {
color: #34345c !important;
:hover {
color: #d00 !important;
}
}`;
case 'Futaba':
return `
.panel {
background-color: #f0e0d6;
}
.panel-header {
border-bottom: 1px solid rgba(0,0,0,.2);
}
.icon {
background-image: url(assets/buttons/cross_red.png);
}
.plus {
background-image: url(assets/buttons/post_expand_plus_red.png);
}
.minus {
background-image: url(assets/buttons/post_expand_minus_red.png);
}
.all-button {
color: #00e !important;
:hover {
color: red !important;
}
}`;
case 'Burichan':
return `
.panel {
background-color: #d6daf0;
}
.panel-header {
border-bottom: 1px solid rgba(0,0,0,.2);
}
.icon {
background-image: url(assets/buttons/cross_blue.png);
}
.plus {
background-image: url(assets/buttons/post_expand_plus_blue.png);
}
.minus {
background-image: url(assets/buttons/post_expand_minus_blue.png);
}
.all-button {
color: #34345c !important;
:hover {
color: #d00 !important;
}
}`;
case 'Tomorrow':
return `
.panel {
background-color: #282a2e;
}
.panel-header {
border-bottom: 1px solid #111;
}
.icon {
background-image: url(assets/buttons/cross_dark.png);
}
.plus {
background-image: url(assets/buttons/post_expand_plus_dark.png);
}
.minus {
background-image: url(assets/buttons/post_expand_minus_dark.png);
}
.all-button {
color: #81a2be !important;
:hover {
color: #5f89ac !important;
}
}
button, select, input, textarea {
filter: brightness(80%);
}
textarea {
background-color: #282a2e;
color: #c5c8c6;
border: 1px solid #000;
}`;
case 'Photon':
return `
.panel {
background-color: #ddd;
}
.panel-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.20);
}
.icon {
background-image: url(assets/buttons/cross_photon.png);
}
.plus {
background-image: url(assets/buttons/post_expand_plus_photon.png);
}
.minus {
background-image: url(assets/buttons/post_expand_minus_photon.png);
}
.all-button {
color: #f60 !important;
:hover {
color: #f30 !important;
}
}`;
default:
return '';
}
}}
#node-stats {
text-decoration: none;
}
`;
File diff suppressed because it is too large Load Diff
@@ -1,420 +0,0 @@
import styled from 'styled-components';
export const Threads = styled.div`
padding: 20px 0;
text-align: center;
margin: 0;
overflow: hidden;
.thread {
width: 180px;
position: relative;
max-height: 320px;
vertical-align: top;
display: inline-block;
margin-top: 5px;
margin-bottom: 20px;
padding: 5px 0 3px;
}
video,
audio,
img {
max-width: 150px;
max-height: 150px;
}
.file-thumb {
background-color: rgba(0, 0, 0, 0.05);
display: inline-flex;
justify-content: center;
align-items: center;
margin-bottom: 5px;
}
.card {
box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
border: 0;
}
.thread-icons {
display: inline;
height: 16px;
margin: 2px 0 0 -101px;
position: absolute;
width: 100px;
text-align: right;
}
.offline-icon {
height: 16px;
margin: 2px;
}
.offline-icon-no-link {
height: 11px;
position: absolute;
margin-left: 13px;
margin-top: -2px;
}
.offline-icon-no-link-shifted {
height: 11px;
position: absolute;
margin-left: 35px;
margin-top: -2px;
}
.offline-icon-no-link-hovered {
transform: translate(16px, 0);
}
.sticky-icon {
background-image: url(assets/sticky.gif);
width: 16px;
height: 16px;
display: inline-block;
}
.closed-icon {
background-image: url(assets/closed.gif);
width: 16px;
height: 16px;
display: inline-block;
}
.meta {
font-size: 11px;
line-height: 8px;
margin-top: 2px;
margin-bottom: 1px;
}
.teaser {
display: block;
padding: 0 15px;
overflow: hidden;
word-wrap: break-word;
}
${({ selectedStyle }) => {
switch (selectedStyle) {
case 'Yotsuba':
return ``;
case 'Yotsuba-B':
return ``;
case 'Futaba':
return ``;
case 'Burichan':
return ``;
case 'Tomorrow':
return `
.file-thumb {
background-color: rgba(255, 255, 255, 0.01) !important;
}`;
case 'Photon':
return ``;
default:
return '';
}
}}
`;
export const PostPreview = styled.div`
.post-preview {
position: absolute;
background-color: #000;
border-radius: 3px;
padding: 5px 8px 4px;
z-index: 100000;
word-wrap: break-word;
white-space: normal;
max-width: 500px;
width: fit-content;
color: #dedede;
font-size: 10pt;
text-align: left;
}
.post-author {
font-weight: 700;
}
.post-last {
margin-top: 3px;
color: #bbbfbd;
font-size: 90%;
}
.post-author-admin {
color: purple;
font-weight: 700;
}
${({ selectedStyle }) => {
switch (selectedStyle) {
case 'Yotsuba':
return `
.post-subject{
color:#cc1105;
font-weight:700;
};
.post-author{
color:#00a550;font-weight:bold
}`;
case 'Yotsuba-B':
return `
.post-subject{
color:#dedede;
font-weight:700;
};
.post-author{
color:#00a550;font-weight:bold
}`;
case 'Futaba':
return `
.post-preview{
font-family: arial,helvetica,sans-serif;
}
.post-subject{
color:#cc1105;
font-weight:700;
};
.post-author{
color:#00a550;font-weight:700;
}`;
case 'Burichan':
return `
.post-preview{
font-family: arial,helvetica,sans-serif;
}
.post-subject{
color:#dedede;
font-weight:700;
};
.post-author{
color:#00a550;font-weight:700;
}`;
case 'Tomorrow':
return `
.post-preview{
font-family: arial,helvetica,sans-serif;
}
.file-thumb {
background-color: rgba(255, 255, 255, 0.01) !important;
}
.post-subject{
color:#b294bb;
font-weight:700;
};
.post-author{
color:#00a550;font-weight:700;
}`;
case 'Photon':
return `
.post-preview{
font-family: arial,helvetica,sans-serif;
}
.post-subject{
color:#dedede;
font-weight:700;
};
.post-author{
color:#dedede;font-weight:700;
}`;
default:
return '';
}
}}
`;
export const PostMenuCatalog = styled.div`
.post-menu-catalog {
position: relative;
font-size: 12px;
padding: 0;
li {
cursor: pointer;
position: relative;
padding: 2px 4px;
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-menu {
display: none;
position: absolute;
}
.dropdown:hover .dropdown-menu {
display: block;
}
a {
text-decoration: none;
color: inherit;
}
}
${({ selectedStyle }) => {
switch (selectedStyle) {
case 'Yotsuba':
return `.highlighted, .highlighted-click, .highlighted-address {
background-color: #f0c0b0 !important;
border: 1px solid #d9bfb7 !important;
border-left: none !important;
border-top: none !important;
}
.post-menu-catalog {
border-right: 1px solid #d9bfb7;
border-bottom: 2px solid #d9bfb7;
li {
border: 1px solid #d9bfb7;
background-color: #f0e0d6;
border-bottom: none;
:hover {
background-color: #ffe;
}
}
}`;
case 'Yotsuba-B':
return `.highlighted, .highlighted-click, .highlighted-address {
background-color: #d6bad0 !important;
border: 1px solid #ba9dbf !important;
border-left: none !important;
border-top: none !important;
}
.post-menu-catalog {
border-right: 1px solid #b7c5d9;
border-bottom: 2px solid #b7c5d9;
li {
border: 1px solid #b7c5d9;
border-bottom: none;
background-color: #d6daf0;
:hover {
background-color: #eef2ff;
}
}
}`;
case 'Futaba':
return `.highlighted, .highlighted-click, .highlighted-address {
background-color: #f0c0b0 !important;
}
.post-menu-catalog {
font-size: 13px !important;
border: 1px solid #d9bfb7;
border-bottom: none;
li {
background-color: #f0e0d6;
border-bottom: 1px solid #d9bfb7;
:hover {
background-color: #ffe;
}
}
}`;
case 'Burichan':
return `.highlighted, .highlighted-click, .highlighted-address {
background-color: #d6bad0 !important;
}
.post-menu-catalog {
font-size: 13px !important;
border: 1px solid #b7c5d9;
border-bottom: none;
li {
background-color: #d6daf0;
border-bottom: 1px solid #b7c5d9;
:hover {
background-color: #eef2ff;
}
}
}`;
case 'Tomorrow':
return `.highlighted, .highlighted-click, .highlighted-address {
background-color: #1d1d21 !important;
border: 1px solid #111 !important;
}
.catalog-media-wrapper {
background-color: rgba(255, 255, 255, 0.01) !important;
}
.post-menu-catalog {
border: 1px solid #000;
border-bottom: none;
li {
background-color: #282a2e;
border-bottom: 1px solid #000;
:hover {
background-color: #1d1f21;
}
}
}`;
case 'Photon':
return `.highlighted, .highlighted-click, .highlighted-address {
background-color: #ccc !important;
border: 1px solid #ccc !important;
}
.post-menu-catalog {
border: 1px solid #ccc;
border-bottom: none;
li {
background-color: #ddd;
border-bottom: 1px solid #ccc;
:hover {
background-color: #eee;
}
}
}`;
default:
return '';
}
}}
`;
-458
View File
@@ -1,458 +0,0 @@
import styled from 'styled-components';
export const Container = styled.div`
height: 100%;
color: #800;
font: 13px/1.231 arial, helvetica, clean, sans-serif;
text-align: center;
margin: auto;
text-align: left;
width: 57.69em;
min-width: 750px;
padding: 0;
@media (max-device-width: 640px) {
min-width: 0;
width: auto;
padding: 10px;
}
`;
export const Header = styled.div`
margin-bottom: 0;
margin: 0;
padding: 0;
`;
export const Logo = styled.div`
font-size: 1px;
line-height: 0;
height: 120px;
overflow: hidden;
margin: 0 auto;
width: 300px;
img {
margin: auto;
display: block;
border: none;
width: 100%;
}
`;
export const Search = styled.div`
text-align: center;
margin-bottom: 20px;
input[type='text'] {
width: 400px;
height: 30px;
font-size: 18px;
padding: 3px 10px;
box-sizing: border-box;
-webkit-appearance: none;
-webkit-border-radius: 0;
border-radius: 0;
margin: 0;
margin-right: 2px;
border: 1px solid #aaa;
outline: none;
font-family: arial, helvetica, sans-serif;
}
input[type='submit'] {
width: 35px;
height: 30px;
font-size: 14px;
margin-left: 5px;
}
@-moz-document url-prefix() {
input[type='submit'] {
vertical-align: top;
}
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
input[type='submit'] {
vertical-align: center;
}
}
@media not all and (min-resolution: 0.001dpcm) {
input[type='submit'] {
vertical-align: top;
}
}
@media only screen and (max-width: 480px) {
text-align: center;
input[type='text'] {
width: 80%;
height: 35px;
font-size: 20px;
padding: 5px;
box-sizing: border-box;
border-radius: 0;
}
input[type='submit'] {
height: 35px;
font-size: 14px;
padding: 5px;
box-sizing: border-box;
vertical-align: top;
width: 70px;
-webkit-appearance: none;
}
}
`;
export const Page = styled.div`
margin: 0;
padding: 0;
`;
export const About = styled.div`
background: #fff;
color: #000;
border: 1px solid;
margin-bottom: 0.5em;
padding-bottom: 0.5em;
`;
export const AboutTitle = styled.div`
background: #800;
color: #fff;
position: relative;
padding-left: 0.5em;
line-height: 2em;
h2 {
font-size: 131%;
font-weight: 700;
margin: 0;
padding: 0;
}
`;
export const AboutContent = styled.div`
line-height: 1.5em;
font-size: 93%;
padding: 0.5em;
padding-top: 0.25em;
padding-bottom: 0;
min-height: 91px;
#content {
min-height: 91px;
}
button {
color: blue !important;
text-decoration: underline !important;
}
a:visited {
color: blue;
}
p {
margin: 0;
padding: 0;
}
`;
export const BoardsBox = styled.div`
border: 1px solid;
padding-bottom: 0.5em;
background: #fff;
margin-bottom: 6px;
&:after {
content: '';
display: table;
clear: both;
}
.boxbar {
background: #fca;
color: #800;
padding-left: 0.5em;
line-height: 2em;
h2 {
font-size: 131%;
font-weight: 700;
margin: 0;
padding: 0;
}
}
.boxcontent {
font-size: 93%;
padding: 0.5em;
padding-top: 0.25em;
padding-bottom: 0;
line-height: 130%;
.column {
float: left;
width: 163px;
margin-right: 20px;
overflow: hidden;
h3 {
font-size: 100%;
font-weight: 700;
}
ul {
padding: 0;
margin: 0;
li {
list-style: none;
white-space: nowrap;
a {
color: #800;
text-decoration: none;
:hover {
text-decoration: underline;
color: #e00;
}
}
}
.disconnected {
width: 13px;
margin: 0 2px -3px 0;
position: relative;
}
}
}
#button {
color: #800;
font-weight: 700;
text-decoration: none;
cursor: pointer;
:hover {
color: #e00;
}
}
}
@media (max-width: 480px) {
.boxbar {
padding-left: 0.25em;
}
.boxcontent {
font-size: 1.05em;
padding: 0.25em;
line-height: 2em;
.column {
width: 44%;
margin-bottom: 10px;
margin-right: 20px;
}
}
}
`;
export const BoardsContent = styled.div`
font-size: 93%;
padding: 0.5em;
padding-top: 0.25em;
padding-bottom: 0;
line-height: 130%;
text-align: center;
.board {
vertical-align: top;
display: inline-block;
word-wrap: break-word;
overflow: hidden;
margin-top: 5px;
padding: 5px 0 3px;
position: relative;
width: 175px;
max-height: 320px;
margin-bottom: 10px;
}
.board-title {
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
display: block;
white-space: nowrap;
margin-bottom: 5px;
}
.board-avatar-container {
width: auto;
height: auto;
border: 1px solid #800;
display: inline-block;
overflow: hidden;
position: relative;
line-height: 0;
}
img,
video {
max-width: 150px;
max-height: 150px;
}
.offline-indicator {
position: absolute;
top: 3px;
right: 3px;
width: 16px;
}
.board-text {
margin-bottom: 5px;
padding: 0 2px;
white-space: pre-line;
}
h3 {
font-size: 100%;
font-weight: 700;
text-align: left;
}
.boardlink {
color: #800;
text-decoration: none;
:hover {
text-decoration: underline;
color: #e00;
}
}
#view-all {
color: #800;
font-weight: 700;
text-decoration: none;
:hover {
color: #e00;
}
}
@media (max-width: 480px) {
.boardlink,
#view-all {
font-size: 1.2em;
}
.disconnected {
width: 13px;
margin-bottom: -1px;
position: absolute;
}
}
@media (min-width: 480px) {
#mobile-br {
display: none;
}
.disconnected {
width: 13px;
margin-top: 0px;
position: absolute;
}
}
`;
export const Footer = styled.div`
font-size: 93%;
text-align: center;
clear: both;
padding-top: 0.5em;
padding-bottom: 2em;
ul {
border-top: 1px solid;
display: table;
margin: auto;
width: 750px;
padding: 0;
}
li {
background: #fed;
display: block;
float: left;
border: 1px solid;
padding: 2px 1em 2px 1em;
border-left: none;
margin-top: -1px;
list-style: none;
}
.fill {
border-top: 1x solid;
border-right: 0;
border-bottom: 0;
border-left: 0;
background: #ffe;
width: 10.5%;
}
.first {
border-left: 1px solid;
}
a {
color: #800;
text-decoration: none;
}
@media (max-device-width: 640px) {
padding-top: 2em;
padding-bottom: 3em;
ul {
width: auto;
border-top: none;
line-height: 2;
display: flex;
flex-wrap: wrap;
justify-content: center;
row-gap: 10px;
}
.fill {
display: none;
background: inherit;
}
li {
background: inherit;
display: inline;
float: none;
border: none;
white-space: nowrap;
}
.first {
border-left: none;
}
a {
color: blue;
text-decoration: underline;
}
}
`;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-770
View File
@@ -1,770 +0,0 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Helmet } from 'react-helmet-async';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { Tooltip } from 'react-tooltip';
import { useSubplebbit } from '@plebbit/plebbit-react-hooks';
import { debounce } from 'lodash';
import { Container, NavBar, Header, Break, PostMenu, PostForm, PostMenuMobile } from '../styled/views/Board.styled';
import { TopBar, BoardForm, Footer, ReplyFormLink } from '../styled/views/Thread.styled';
import { PostMenuCatalog } from '../styled/views/Catalog.styled';
import BoardStats from '../BoardStats';
import ImageBanner from '../ImageBanner';
import Post from '../Post';
import CreateBoardModal from '../modals/CreateBoardModal';
import AdminListModal from '../modals/AdminListModal';
import OfflineIndicator from '../OfflineIndicator';
import SettingsModal from '../modals/SettingsModal';
import getDate from '../../utils/getDate';
import handleImageClick from '../../utils/handleImageClick';
import handleStyleChange from '../../utils/handleStyleChange';
import useGeneralStore from '../../hooks/stores/useGeneralStore';
import packageJson from '../../../package.json';
const { version } = packageJson;
const commitRef = process?.env?.REACT_APP_COMMIT_REF ? ` ${process.env.REACT_APP_COMMIT_REF.slice(0, 7)}` : '';
const Description = () => {
const { defaultSubplebbits, isSettingsOpen, setIsSettingsOpen, selectedAddress, setSelectedAddress, selectedStyle, setSelectedTitle } = useGeneralStore(
(state) => state,
);
const navigate = useNavigate();
const threadMenuRefs = useRef({});
const threadMenuRefsMobile = useRef({});
const postMenuMobileRef = useRef(null);
const postMenuCatalogRef = useRef(null);
const [isAdminListOpen, setIsAdminListOpen] = useState(false);
const [prevScrollPos, setPrevScrollPos] = useState(0);
const [visible, setVisible] = useState(true);
const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
const [mobileMenuPosition, setMobileMenuPosition] = useState({ top: 0, left: 0 });
const [openMenuCid, setOpenMenuCid] = useState(null);
const [openMobileMenuCid, setOpenMobileMenuCid] = useState(null);
const [isCreateBoardOpen, setIsCreateBoardOpen] = useState(false);
const [isImageSearchOpen, setIsImageSearchOpen] = useState(false);
const [isClientRedirectMenuOpen, setIsClientRedirectMenuOpen] = useState(false);
const { subplebbitAddress } = useParams();
const subplebbit = useSubplebbit({ subplebbitAddress: selectedAddress });
const handleOptionClick = () => {
setOpenMenuCid(null);
};
const handleMobileOptionClick = () => {
setOpenMobileMenuCid(null);
};
const handleOutsideClick = useCallback(
(e) => {
if (openMenuCid !== null && !postMenuCatalogRef.current.contains(e.target)) {
setOpenMenuCid(null);
}
},
[openMenuCid, postMenuCatalogRef],
);
const handleMobileOutsideClick = useCallback(
(e) => {
if (openMobileMenuCid !== null && !postMenuMobileRef.current.contains(e.target)) {
setOpenMobileMenuCid(null);
}
},
[openMobileMenuCid, postMenuMobileRef],
);
useEffect(() => {
if (openMenuCid !== null) {
document.addEventListener('click', handleOutsideClick);
} else {
document.removeEventListener('click', handleOutsideClick);
}
return () => {
document.removeEventListener('click', handleOutsideClick);
};
}, [openMenuCid, handleOutsideClick]);
useEffect(() => {
if (openMobileMenuCid !== null) {
document.addEventListener('click', handleMobileOutsideClick);
} else {
document.removeEventListener('click', handleMobileOutsideClick);
}
return () => {
document.removeEventListener('click', handleMobileOutsideClick);
};
}, [openMobileMenuCid, handleMobileOutsideClick]);
useEffect(() => {
window.scrollTo(0, 0);
}, []);
useEffect(() => {
const selectedSubplebbit = defaultSubplebbits.find((subplebbit) => subplebbit.address === subplebbitAddress);
if (subplebbitAddress) {
setSelectedAddress(subplebbitAddress);
} else if (subplebbit?.address) {
setSelectedAddress(subplebbit.address);
}
if (selectedSubplebbit) {
setSelectedTitle(selectedSubplebbit.title);
} else if (subplebbit?.title) {
setSelectedTitle(subplebbit.title);
}
}, [subplebbitAddress, setSelectedAddress, setSelectedTitle, defaultSubplebbits, subplebbit?.address, subplebbit?.title]);
// mobile navbar scroll effect
useEffect(() => {
const debouncedHandleScroll = debounce(() => {
const currentScrollPos = window.scrollY;
setVisible(prevScrollPos > currentScrollPos || currentScrollPos < 10);
setPrevScrollPos(currentScrollPos);
}, 50);
window.addEventListener('scroll', debouncedHandleScroll);
return () => window.removeEventListener('scroll', debouncedHandleScroll);
}, [prevScrollPos, visible]);
// mobile navbar board select functionality
const handleSelectChange = (event) => {
const selected = event.target.value;
if (selected === 'subscriptions') {
navigate(`/p/subscriptions`);
return;
} else if (selected === 'all') {
navigate(`/p/all`);
return;
}
const selectedTitle = defaultSubplebbits.find((subplebbit) => subplebbit.address === selected).title;
setSelectedTitle(selectedTitle);
setSelectedAddress(selected);
navigate(`/p/${selected}`);
};
return (
<>
<Helmet>
<title>{'Board Description - ' + (subplebbit.title || subplebbit.address) + ' - plebchan'}</title>
</Helmet>
<Container>
<CreateBoardModal selectedStyle={selectedStyle} isOpen={isCreateBoardOpen} closeModal={() => setIsCreateBoardOpen(false)} />
<AdminListModal selectedStyle={selectedStyle} isOpen={isAdminListOpen} closeModal={() => setIsAdminListOpen(false)} roles={subplebbit.roles} />
<SettingsModal selectedStyle={selectedStyle} isOpen={isSettingsOpen} closeModal={() => setIsSettingsOpen(false)} />
<NavBar selectedStyle={selectedStyle}>
<>
<span className='boardList'>
[<Link to={{ pathname: `/p/all`, state: { scrollToTop: true } }}>All</Link>
 / 
<Link to={{ pathname: `/p/subscriptions`, state: { scrollToTop: true } }}>Subscriptions</Link>
]&nbsp;[
{defaultSubplebbits.map((subplebbit, index) => (
<span className='boardList' key={`span-${subplebbit.address}`}>
{index === 0 ? null : '\u00a0'}
<Link
to={`/p/${subplebbit.address}`}
key={`a-${subplebbit.address}`}
onClick={() => {
setSelectedTitle(subplebbit.title);
setSelectedAddress(subplebbit.address);
}}
>
{subplebbit.title ? subplebbit.title : subplebbit.address}
</Link>
{index !== defaultSubplebbits.length - 1 ? ' /' : null}
</span>
))}
]
</span>
<span className='nav'>
[
<span
id='button-span'
style={{ cursor: 'pointer' }}
onClick={() => {
window.electron && window.electron.isElectron
? setIsCreateBoardOpen(true)
: alert(
'HOW TO CREATE A BOARD (currently via CLI only):\n\n(1) have a computer you can use as server, because users will connect to your board peer-to-peer;\n\n(2) go to https://github.com/plebbit/plebbit-cli#install and follow the instructions to run your daemon, create a subplebbit (board) and edit its settings;\n\n(3) users can always connect to your board via its address, but it can be added below by submitting a pull request here: https://github.com/plebbit/temporary-default-subplebbits. This will be automated with a plebbit DAO using the plebbit token.',
);
}}
>
Create Board
</span>
] [
<Link to={`/p/${selectedAddress}/description/settings`} onClick={() => setIsSettingsOpen(true)}>
Settings
</Link>
] [<Link to='/'>Home</Link>]
</span>
<div id='board-nav-mobile' style={{ top: visible ? 0 : '-23px' }}>
<div className='nav-container'>
<div className='board-select'>
<strong>Board</strong>
&nbsp;
<select id='board-select-mobile' value={selectedAddress} onChange={handleSelectChange}>
<option value='all'>All</option>
<option value='subscriptions'>Subscriptions</option>
{!defaultSubplebbits.some((subplebbit) => subplebbit.address === selectedAddress) && <option value={selectedAddress}>{selectedAddress}</option>}
{defaultSubplebbits.map((subplebbit) => (
<option key={`option-${subplebbit.address}`} value={subplebbit.address}>
{subplebbit.title ? subplebbit.title : subplebbit.address}
</option>
))}
</select>
 
<span
id='button-span'
style={{ cursor: 'pointer' }}
onClick={() =>
alert(
'HOW TO CREATE A BOARD (currently via CLI only):\n\n(1) have a computer you can use as server, because users will connect to your board peer-to-peer;\n\n(2) go to https://github.com/plebbit/plebbit-cli#install and follow the instructions to run your daemon, create a subplebbit (board) and edit its settings;\n\n(3) users can always connect to your board via its address, but it can be added below by submitting a pull request here: https://github.com/plebbit/temporary-default-subplebbits. This will be automated with a plebbit DAO using the plebbit token.',
)
}
>
Create Board
</span>
</div>
<div className='page-jump'>
<Link to={`/p/${selectedAddress}/description/settings`} onClick={() => setIsSettingsOpen(true)}>
Settings
</Link>
&nbsp;
<Link
to='/'
onClick={() => {
handleStyleChange({ target: { value: 'Yotsuba' } });
}}
>
Home
</Link>
</div>
</div>
</div>
<div id='separator-mobile'>&nbsp;</div>
<div id='separator-mobile'>&nbsp;</div>
</>
</NavBar>
<Header selectedStyle={selectedStyle}>
<>
<div className='banner'>
<ImageBanner />
</div>
<>
<div className='board-title'>{subplebbit.title ?? null}</div>
<div className='board-address'>
p/{subplebbit.address}
<OfflineIndicator address={subplebbit.address} className='offline' tooltipPlace='top' />
</div>
</>
</>
</Header>
<Break selectedStyle={selectedStyle} />
<PostForm selectedStyle={selectedStyle}>
<ReplyFormLink id='post-form-link' selectedStyle={selectedStyle} style={{ marginBottom: '10px' }}>
<div id='return-button-mobile'>
<span className='btn-wrap'>
<Link to={{ pathname: `/p/${selectedAddress}`, state: { scrollToTop: true } }}>Return</Link>
</span>
</div>
<div id='catalog-button-mobile'>
<span className='btn-wrap'>
<Link to={{ pathname: `/p/${selectedAddress}/catalog`, state: { scrollToTop: true } }}>Catalog</Link>
</span>
</div>
<div id='bottom-button-mobile'>
<span className='btn-wrap'>
<span
style={{ cursor: 'pointer' }}
onClick={() => window.scrollTo(0, document.body.scrollHeight)}
onMouseOver={(event) => (event.target.style.cursor = 'pointer')}
>
Bottom
</span>
</span>
</div>
</ReplyFormLink>
</PostForm>
<BoardStats subplebbitAddress={selectedAddress} />
<TopBar selectedStyle={selectedStyle}>
<span className='style-changer'>
Style:  
<select id='style-selector' onChange={handleStyleChange} value={selectedStyle}>
<option value='Yotsuba'>Yotsuba</option>
<option value='Yotsuba-B'>Yotsuba B</option>
<option value='Futaba'>Futaba</option>
<option value='Burichan'>Burichan</option>
<option value='Tomorrow'>Tomorrow</option>
<option value='Photon'>Photon</option>
</select>
</span>
<span className='return-button' id='return-button-desktop'>
[<Link to={{ pathname: `/p/${selectedAddress}`, state: { scrollToTop: true } }}>Return</Link>]
</span>
<span className='return-button catalog-button' id='catalog-button-desktop'>
[<Link to={{ pathname: `/p/${selectedAddress}/catalog`, state: { scrollToTop: true } }}>Catalog</Link>]
</span>
<span className='return-button catalog-button' id='bottom-button-desktop'>
[
<span
id='button'
style={{ cursor: 'pointer' }}
onClick={() => window.scrollTo(0, document.body.scrollHeight)}
onMouseOver={(event) => (event.target.style.cursor = 'pointer')}
onTouchStart={() => window.scrollTo(0, document.body.scrollHeight)}
>
Bottom
</span>
]
</span>
</TopBar>
<Tooltip id='tooltip' className='tooltip' />
<BoardForm selectedStyle={selectedStyle}>
<>
<div className='thread'>
<div
className='op-container'
style={{
display: subplebbit.rules ? 'grid' : 'block',
marginBottom: '10px',
}}
>
<div className='post op op-desktop'>
<hr />
<div className='post-info'>
{subplebbit.suggested?.avatarUrl ? (
<div className='file' style={{ marginBottom: '5px' }}>
<div className='file-text'>
Link:&nbsp;
<a href={subplebbit.suggested?.avatarUrl} target='_blank' rel='noopener noreferrer'>
{subplebbit.suggested?.avatarUrl.length > 30 ? subplebbit.suggested?.avatarUrl.slice(0, 30) + '...' : subplebbit.suggested?.avatarUrl}
</a>
&nbsp;(image)
</div>
<div className='img-container'>
<span className='file-thumb'>
<img src={subplebbit.suggested?.avatarUrl} alt='board avatar' onClick={handleImageClick} style={{ cursor: 'pointer' }} />
</span>
</div>
</div>
) : null}
<span className='name-block'>
<span className='title'>Welcome to {subplebbit.title || subplebbit.address}</span>
&nbsp;
<span
className='name capcode'
style={{ cursor: 'pointer' }}
onClick={() => {
setIsAdminListOpen(!isAdminListOpen);
}}
>
## Board Mods
</span>
&nbsp;
<span className='date-time'>{getDate(subplebbit.createdAt)}</span>
&nbsp;
<img src='assets/sticky.gif' alt='Sticky' title='Sticky' style={{ marginBottom: '-1px', imageRendering: 'pixelated' }} />
&nbsp;
<img src='assets/closed.gif' alt='Closed' title='Closed' style={{ marginBottom: '-1px', imageRendering: 'pixelated' }} />
<span>
&nbsp; [
<Link to={() => {}} style={{ textDecoration: 'none' }} className='reply-link'>
Reply
</Link>
]
</span>
<PostMenu
title='Post menu'
ref={(el) => {
threadMenuRefs.current[subplebbit.pubsubTopic] = el;
}}
className='post-menu-button'
rotated={openMenuCid === subplebbit.pubsubTopic}
onClick={(event) => {
event.stopPropagation();
const rect = threadMenuRefs.current[subplebbit.pubsubTopic].getBoundingClientRect();
setMenuPosition({ top: rect.top + window.scrollY, left: rect.left });
setOpenMenuCid((prevCid) => (prevCid === subplebbit.pubsubTopic ? null : subplebbit.pubsubTopic));
}}
>
</PostMenu>
{createPortal(
<PostMenuCatalog
selectedStyle={selectedStyle}
ref={(el) => {
postMenuCatalogRef.current = el;
}}
onClick={(event) => event.stopPropagation()}
style={{ position: 'absolute', top: menuPosition.top + 7, left: menuPosition.left }}
>
<div
className={`post-menu-thread post-menu-thread-${subplebbit.pubsubTopic}`}
style={{ display: openMenuCid === subplebbit.pubsubTopic ? 'block' : 'none' }}
>
<ul className='post-menu-catalog'>
{/* {canModerate ? (
<>
change description
</>
) : null} */}
{subplebbit.suggested?.avatarUrl ? (
<li
onMouseOver={() => {
setIsImageSearchOpen(true);
}}
onMouseLeave={() => {
setIsImageSearchOpen(false);
}}
>
Image search »
<ul className='dropdown-menu post-menu-catalog' style={{ display: isImageSearchOpen ? 'block' : 'none' }}>
<li onClick={() => handleOptionClick('description')}>
<a href={`https://lens.google.com/uploadbyurl?url=${subplebbit.suggested?.avatarUrl}`} target='_blank' rel='noreferrer'>
Google
</a>
</li>
<li onClick={() => handleOptionClick('description')}>
<a href={`https://yandex.com/images/search?url=${subplebbit.suggested?.avatarUrl}`} target='_blank' rel='noreferrer'>
Yandex
</a>
</li>
<li onClick={() => handleOptionClick('description')}>
<a href={`https://saucenao.com/search.php?url=${subplebbit.suggested?.avatarUrl}`} target='_blank' rel='noreferrer'>
SauceNAO
</a>
</li>
</ul>
</li>
) : null}
<li
onMouseOver={() => {
setIsClientRedirectMenuOpen(true);
}}
onMouseLeave={() => {
setIsClientRedirectMenuOpen(false);
}}
>
View on »
<ul className='dropdown-menu post-menu-catalog' style={{ display: isClientRedirectMenuOpen ? 'block' : 'none' }}>
<li onClick={() => handleOptionClick('description')}>
<a href={`https://plebbitapp.eth.limo/#/p/${selectedAddress}`} target='_blank' rel='noreferrer'>
Plebbit
</a>
</li>
<li onClick={() => handleOptionClick('description')}>
<a href={`https://seedit.eth.limo/#/p/${selectedAddress}`} target='_blank' rel='noreferrer'>
Seedit
</a>
</li>
<li onClick={() => handleOptionClick('description')}>
<a href={`https://plebones.eth.limo/#/p/${selectedAddress}`} target='_blank' rel='noreferrer'>
Plebones
</a>
</li>
</ul>
</li>
</ul>
</div>
</PostMenuCatalog>,
document.body,
)}
</span>
<blockquote>
<div className='custom-paragraph'>
<Post content={subplebbit.description} />
</div>
</blockquote>
</div>
</div>
</div>
</div>
<div className='thread-mobile'>
<hr />
<div className='op-container'>
<div className='post op op-mobile'>
<div className='post-info-mobile'>
<button
key={`mob-pb-`}
className='post-menu-button-mobile'
ref={(el) => {
threadMenuRefsMobile.current['description'] = el;
}}
onClick={(event) => {
event.stopPropagation();
const rect = threadMenuRefsMobile.current['description'].getBoundingClientRect();
setMobileMenuPosition({ top: rect.top + window.scrollY, left: rect.left });
setOpenMobileMenuCid((prevCid) => (prevCid === 'description' ? null : 'description'));
}}
style={{ all: 'unset', cursor: 'pointer' }}
>
...
</button>
{createPortal(
<PostMenuMobile
selectedStyle={selectedStyle}
ref={(el) => {
postMenuMobileRef.current = el;
}}
onClick={(event) => event.stopPropagation()}
style={{
position: 'absolute',
display: openMobileMenuCid === 'description' ? 'block' : 'none',
top: mobileMenuPosition.top + 20,
left: mobileMenuPosition.left,
}}
>
<ul className={`post-menu-mobile-thread-description`}>
{subplebbit.suggested?.avatarUrl ? (
<>
<li onClick={() => handleMobileOptionClick('description')}>
<a
style={{ color: 'inherit', textDecoration: 'none' }}
href={`https://lens.google.com/uploadbyurl?url=${subplebbit.suggested?.avatarUrl}`}
target='_blank'
rel='noreferrer'
>
Search image on Google
</a>
</li>
<li onClick={() => handleMobileOptionClick('description')}>
<a
style={{ color: 'inherit', textDecoration: 'none' }}
href={`https://yandex.com/images/search?url=${subplebbit.suggested?.avatarUrl}`}
target='_blank'
rel='noreferrer'
>
Search image on Yandex
</a>
</li>
<li onClick={() => handleMobileOptionClick('description')}>
<a
style={{ color: 'inherit', textDecoration: 'none' }}
href={`https://saucenao.com/search.php?url=${subplebbit.suggested?.avatarUrl}`}
target='_blank'
rel='noreferrer'
>
Search image on SauceNAO
</a>
</li>
</>
) : null}
<a
style={{ color: 'inherit', textDecoration: 'none' }}
href={`https://plebbitapp.eth.limo/#/p/${selectedAddress}`}
target='_blank'
rel='noreferrer'
>
<li onClick={() => handleMobileOptionClick('description')}>View on plebbit</li>
</a>
<a
style={{ color: 'inherit', textDecoration: 'none' }}
href={`https://seedit.eth.limo/#/p/${selectedAddress}`}
target='_blank'
rel='noreferrer'
>
<li onClick={() => handleMobileOptionClick('description')}>View on seedit</li>
</a>
<a
style={{ color: 'inherit', textDecoration: 'none' }}
href={`https://plebones.eth.limo/#/p/${selectedAddress}`}
target='_blank'
rel='noreferrer'
>
<li onClick={() => handleMobileOptionClick('description')}>View on plebones</li>
</a>
</ul>
</PostMenuMobile>,
document.body,
)}
<span className='name-block-mobile'>
<span
className='name-mobile capcode'
style={{ cursor: 'pointer' }}
onClick={() => {
setIsAdminListOpen(!isAdminListOpen);
}}
>
## Board Mods
</span>
&nbsp;
<span className='thread-icons-mobile'>
<img
src='assets/sticky.gif'
alt='Sticky'
title='Sticky'
style={{ all: 'unset', marginBottom: '-2px', paddingLeft: '2px', imageRendering: 'pixelated' }}
/>
&nbsp;
<img
src='assets/closed.gif'
alt='Closed'
title='Closed'
style={{ all: 'unset', marginBottom: '-2px', paddingLeft: '2px', imageRendering: 'pixelated' }}
/>
</span>
<br />
<span className='subject-mobile' style={{ marginBottom: '-15px' }}>
Welcome to {subplebbit.title || subplebbit.address}
</span>
&nbsp;
</span>
<span className='date-time-mobile post-number-mobile'>{getDate(subplebbit.createdAt)}</span>
&nbsp;
</div>
{subplebbit.suggested?.avatarUrl ? (
<div className='file-mobile'>
<div className='img-container'>
<span className='file-thumb-mobile'>
<img src={subplebbit.suggested?.avatarUrl} alt='board avatar' onClick={handleImageClick} style={{ cursor: 'pointer' }} />
<div className='file-info-mobile'>image</div>
</span>
</div>
</div>
) : null}
<blockquote className='post-message-mobile'>
<div className='custom-paragraph'>
<Post content={subplebbit.description} />
</div>
</blockquote>
</div>
<div className='post-link-mobile'>
<Link to={() => {}} className='button-mobile'>
View Thread
</Link>
</div>
</div>
</div>
</>
</BoardForm>
<Footer selectedStyle={selectedStyle}>
<Break
id='break'
selectedStyle={selectedStyle}
style={{
marginTop: '-36px',
width: '100%',
}}
/>
<Break
selectedStyle={selectedStyle}
style={{
width: '100%',
}}
/>
<span
className='style-changer'
style={{
float: 'right',
marginTop: '2px',
}}
>
Style:  
<select id='style-selector' onChange={handleStyleChange} value={selectedStyle}>
<option value='Yotsuba'>Yotsuba</option>
<option value='Yotsuba-B'>Yotsuba B</option>
<option value='Futaba'>Futaba</option>
<option value='Burichan'>Burichan</option>
<option value='Tomorrow'>Tomorrow</option>
<option value='Photon'>Photon</option>
</select>
</span>
<NavBar
selectedStyle={selectedStyle}
style={{
marginTop: '42px',
}}
>
<>
<span className='boardList'>
[<Link to={{ pathname: `/p/all`, state: { scrollToTop: true } }}>All</Link>
 / 
<Link to={{ pathname: `/p/subscriptions`, state: { scrollToTop: true } }}>Subscriptions</Link>
]&nbsp;
</span>
{defaultSubplebbits.map((subplebbit, index) => (
<span className='boardList' key={`span-${subplebbit.address}`}>
{index === 0 ? null : '\u00a0'}
<Link
to={`/p/${subplebbit.address}`}
key={`a-${subplebbit.address}`}
onClick={() => {
setSelectedTitle(subplebbit.title);
setSelectedAddress(subplebbit.address);
}}
>
{subplebbit.title ? subplebbit.title : subplebbit.address}
</Link>
{index !== defaultSubplebbits.length - 1 ? ' /' : null}
</span>
))}
<span className='nav'>
[
<span
id='button-span'
style={{ cursor: 'pointer' }}
onClick={() => {
window.electron && window.electron.isElectron
? setIsCreateBoardOpen(true)
: alert(
'HOW TO CREATE A BOARD (currently via CLI only):\n\n(1) have a computer you can use as server, because users will connect to your board peer-to-peer;\n\n(2) go to https://github.com/plebbit/plebbit-cli#install and follow the instructions to run your daemon, create a subplebbit (board) and edit its settings;\n\n(3) users can always connect to your board via its address, but it can be added below by submitting a pull request here: https://github.com/plebbit/temporary-default-subplebbits. This will be automated with a plebbit DAO using the plebbit token.',
);
}}
>
Create Board
</span>
] [
<Link to={`/p/${selectedAddress}/description/settings`} onClick={() => setIsSettingsOpen(true)}>
Settings
</Link>
] [
<Link to='/' onClick={() => handleStyleChange({ target: { value: 'Yotsuba' } })}>
Home
</Link>
]
</span>
</>
</NavBar>
<div id='version'>
plebchan v{version}
{commitRef}. GPL-2.0
</div>
<div
className='footer-links'
style={{
textAlign: 'center',
fontSize: 'x-small',
fontFamily: 'arial',
marginTop: '5px',
marginBottom: '15px',
}}
>
<a style={{ textDecoration: 'underline' }} href='https://plebbit.com' target='_blank' rel='noopener noreferrer'>
About
</a>
&nbsp;&nbsp;
<a style={{ textDecoration: 'underline' }} href='https://github.com/plebbit/plebchan/releases/latest' target='_blank' rel='noopener noreferrer'>
App
</a>
&nbsp;&nbsp;
<a style={{ textDecoration: 'underline' }} href='https://twitter.com/plebchan_eth' target='_blank' rel='noopener noreferrer'>
Twitter
</a>
&nbsp;&nbsp;
<a style={{ textDecoration: 'underline' }} href='https://t.me/plebbit' target='_blank' rel='noopener noreferrer'>
Telegram
</a>
</div>
</Footer>
</Container>
</>
);
};
export default Description;
-397
View File
@@ -1,397 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import { Helmet } from 'react-helmet-async';
import { useAccount, useAccountSubplebbits, useComment, useSubplebbit, useSubplebbits } from '@plebbit/plebbit-react-hooks';
import { Link, useNavigate } from 'react-router-dom';
import { Container, Header, Logo, Page, Search, About, AboutTitle, AboutContent, BoardsBox, BoardsContent, Footer } from '../styled/views/Home.styled';
import BoardAvatar from '../BoardAvatar';
import OfflineIndicator from '../OfflineIndicator';
import CreateBoardModal from '../modals/CreateBoardModal';
import useGeneralStore from '../../hooks/stores/useGeneralStore';
import packageJson from '../../../package.json';
import getCommentMediaInfo from '../../utils/getCommentMediaInfo';
import { Tooltip } from 'react-tooltip';
const { version } = packageJson;
const commitRef = process?.env?.REACT_APP_COMMIT_REF ? ` ${process.env.REACT_APP_COMMIT_REF.slice(0, 7)}` : '';
const PopularThreads = ({ commentCid }) => {
const comment = useComment({ commentCid });
const subplebbit = useSubplebbit({ subplebbitAddress: comment?.subplebbitAddress });
const { setSelectedAddress, setSelectedTitle } = useGeneralStore((state) => state);
const commentMediaInfo = getCommentMediaInfo(comment);
return (
<div className='board' key={`${commentCid}`}>
<div className='board-title' key='board-title'>
<span>{subplebbit.title || subplebbit.address}</span>
</div>
<div className='board-avatar-container' key='board-avatar-container'>
<Link
to={`/p/${comment?.subplebbitAddress}/c/${comment?.cid}`}
key='link'
onClick={() => {
setSelectedTitle(subplebbit?.title);
setSelectedAddress(comment?.subplebbitAddress);
}}
>
{commentMediaInfo?.type === 'webpage' && commentMediaInfo?.thumbnail ? (
<img className='board-avatar' src={commentMediaInfo?.thumbnail} alt='post' />
) : commentMediaInfo?.type === 'image' ? (
<img className='board-avatar' src={commentMediaInfo?.url} alt='post' />
) : commentMediaInfo?.type === 'video' ? (
<video className='board-avatar' src={commentMediaInfo?.url} alt='post' />
) : commentMediaInfo?.type === 'iframe' && commentMediaInfo?.thumbnail ? (
<img className='board-avatar' src={commentMediaInfo?.thumbnail} alt='post' />
) : (
<BoardAvatar address={comment.subplebbitAddress} />
)}
</Link>
<OfflineIndicator address={comment?.subplebbitAddress} className='offline-indicator' tooltipPlace='top' key='oi2' />
</div>
<div className='board-text' key='bt'>
{comment?.title ? <b>{comment?.title}</b> : null}
{comment?.content ? (comment.content.length > 99 ? `: ${comment.content.substring(0, 99)}...` : `: ${comment.content}`) : null}
</div>
</div>
);
};
const Home = () => {
const { bodyStyle, setBodyStyle, defaultSubplebbits, defaultNsfwSubplebbits, setSelectedAddress, selectedStyle, setSelectedStyle } = useGeneralStore((state) => state);
const { accountSubplebbits } = useAccountSubplebbits();
const accountSubplebbitsAddresses = Object.values(accountSubplebbits).map((subplebbit) => subplebbit.address);
const sfwAddresses = defaultSubplebbits.map((subplebbit) => subplebbit.address);
const sfwSubs = useSubplebbits({ subplebbitAddresses: sfwAddresses });
const sfwList = sfwSubs.subplebbits.map((subplebbit) => subplebbit?.address);
const [sfwListCids, setSfwListCids] = useState([]);
useEffect(() => {
let subplebbitToCid = {};
sfwSubs.subplebbits.forEach((s) => {
let maxTimestamp = -Infinity;
let mostRecentCid = null;
if (s && s.posts && s.posts.pages && s.posts.pages.hot && s.posts.pages.hot.comments) {
for (const comment of Object.values(s.posts.pages.hot.comments)) {
const commentMediaInfo = getCommentMediaInfo(comment);
const isMediaShowed =
comment.link &&
commentMediaInfo &&
(commentMediaInfo.type === 'image' ||
commentMediaInfo.type === 'video' ||
(commentMediaInfo.type === 'webpage' && commentMediaInfo.thumbnail) ||
(commentMediaInfo.type === 'iframe' && commentMediaInfo.thumbnail));
if (
isMediaShowed &&
comment.replyCount > 2 &&
!comment.removed &&
!comment.locked &&
!comment.pinned &&
comment.timestamp > Date.now() / 1000 - 60 * 60 * 24 * 30 &&
comment.timestamp > maxTimestamp
) {
maxTimestamp = comment.timestamp;
mostRecentCid = comment.cid;
}
}
if (mostRecentCid) {
subplebbitToCid[s.address] = { cid: mostRecentCid, timestamp: maxTimestamp };
}
}
});
const newSfwListCids = Object.values(subplebbitToCid)
.map((item) => item.cid)
.slice(0, 8);
setSfwListCids((prevOrderedCids) => {
const uniqueCids = Array.from(new Set([...prevOrderedCids, ...newSfwListCids]));
return uniqueCids.slice(0, 8);
});
}, [sfwSubs.subplebbits]);
const account = useAccount();
const navigate = useNavigate();
const inputRef = useRef(null);
const prevStyle = useRef(selectedStyle);
const prevBodyStyle = useRef(bodyStyle);
const [isCreateBoardOpen, setIsCreateBoardOpen] = useState(false);
const [showAllSFWBoards, setShowAllSFWBoards] = useState(false);
const [showAllSubscriptions, setShowAllSubscriptions] = useState(false);
// prevent dark mode
useEffect(() => {
const currentPrevStyle = prevStyle.current;
const currentPrevBodyStyle = prevBodyStyle.current;
setBodyStyle({
background: '#ffe url(assets/fade.png) top repeat-x',
color: 'maroon',
fontFamily: 'Helvetica, Arial, sans-serif',
});
setSelectedStyle('Yotsuba');
return () => {
setSelectedStyle(currentPrevStyle);
setBodyStyle(currentPrevBodyStyle);
};
}, [setBodyStyle, setSelectedStyle]);
return (
<>
<CreateBoardModal selectedStyle={selectedStyle} isOpen={isCreateBoardOpen} closeModal={() => setIsCreateBoardOpen(false)} />
<Helmet>
<title>plebchan</title>
</Helmet>
<Container>
<Tooltip id='tooltip' className='tooltip' />
<Header>
<Logo>
<Link to='/'>
<img alt='plebchan' src='assets/logo/logo-transparent.png' />
</Link>
</Logo>
</Header>
<Page>
<Search>
<input
type='text'
placeholder='"board.eth" or "12D3KooW..."'
ref={inputRef}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
const address = inputRef.current.value;
if (address) {
setSelectedAddress(address);
navigate(`/p/${address}`);
}
}
}}
/>
<input
type='submit'
value='Go'
onClick={() => {
const address = inputRef.current.value;
if (address) {
setSelectedAddress(address);
navigate(`/p/${address}`);
}
}}
/>
</Search>
<About>
<AboutTitle>
<h2>What is plebchan?</h2>
</AboutTitle>
<AboutContent>
<div id='content'>
<p>
Plebchan is a serverless, adminless, decentralized 4chan alternative where any pleb can create and own unlimited boards. All data comes from the 
<a href='https://plebbit.com' target='_blank' rel='noreferrer'>
plebbit
</a>{' '}
protocol, it's all text including links from which media is embedded, shared peer-to-peer. Users do not need to register an account before participating
in the community. Feel free to click on a board below that interests you and jump right in!{' '}
</p>
<br />
<p>There are no global rules, each board is completely independent and its owner decides how/if it should be moderated.</p>
</div>
</AboutContent>
</About>
<BoardsBox>
<div className='boxbar'>
<h2>Boards</h2>
</div>
<div className='boxcontent'>
<div className='column'>
<h3 style={{ textDecoration: 'underline', display: 'inline' }}>
<a
href='https://github.com/plebbit/temporary-default-subplebbits'
target='_blank'
rel='noopener noreferrer'
style={{ all: 'unset', cursor: 'pointer' }}
>
Default SFW
</a>
</h3>
&nbsp;
<Link to={{ pathname: '/p/all', state: { scrollToTop: true } }} id='button'>
[view all]
</Link>
<ul>
{sfwList.slice(0, showAllSFWBoards ? undefined : 18).map((address, index) => (
<li key={`default-${index}`}>
<OfflineIndicator address={address} className='disconnected' isText={true} />
<Link
key={`default-link-${index}`}
className='boardlink'
to={{
pathname: `/p/${address}`,
state: { scrollToTop: true },
}}
>
{address}
</Link>
&nbsp;
</li>
))}
{sfwList.length > 17 && (
<li onClick={() => setShowAllSFWBoards(!showAllSFWBoards)}>
{!showAllSFWBoards ? '... ' : null}
<span id='button'>{showAllSFWBoards ? '[show less]' : '[show more]'}</span>
</li>
)}
</ul>
</div>
<div className='column'>
<h3 style={{ textDecoration: 'underline', display: 'inline' }}>
<a
href='https://github.com/plebbit/temporary-default-subplebbits'
target='_blank'
rel='noopener noreferrer'
style={{ all: 'unset', cursor: 'pointer' }}
>
Default NSFW
</a>
</h3>
<ul>
{defaultNsfwSubplebbits.length === 0 && <li style={{ color: 'black' }}>No boards yet in this list.</li>}
{defaultNsfwSubplebbits.slice(0, showAllSFWBoards ? undefined : 18).map((subplebbit, index) => (
<li key={`default-${index}`}>
<OfflineIndicator address={subplebbit.address} className='disconnected' isText={true} />
<Link key={`default-link-${index}`} className='boardlink' to={{ pathname: `/p/${subplebbit.address}`, state: { scrollToTop: true } }}>
{subplebbit.address}
</Link>
&nbsp;
</li>
))}
{defaultNsfwSubplebbits.length > 17 && (
<li onClick={() => setShowAllSFWBoards(!showAllSFWBoards)}>
{!showAllSFWBoards ? '... ' : null}
<span id='button'>{showAllSFWBoards ? '[show less]' : '[show more]'}</span>
</li>
)}
</ul>
</div>
<div className='column'>
<h3 style={{ textDecoration: 'underline', display: 'inline' }}>Subscriptions</h3>
&nbsp;
<Link to={{ pathname: '/p/subscriptions', state: { scrollToTop: true } }} id='button'>
[view all]
</Link>
<br />
<ul>
{account?.subscriptions?.length === 0 && <li style={{ color: 'black' }}>Not subscribed to any board.</li>}
{account?.subscriptions?.slice(0, showAllSubscriptions ? undefined : 18).map((subscription, index) => (
<li key={`sub-${index}`}>
<OfflineIndicator address={subscription} className='disconnected' isText={true} />
<Link key={`sub-link-${index}`} className='boardlink' to={{ pathname: `/p/${subscription}`, state: { scrollToTop: true } }}>
{subscription}
</Link>
&nbsp;
</li>
))}
{account?.subscriptions?.length > 17 && (
<li onClick={() => setShowAllSubscriptions(!showAllSubscriptions)}>
{!showAllSubscriptions ? '... ' : null}
<span id='button'>{showAllSubscriptions ? '[show less]' : '[show more]'}</span>
</li>
)}
</ul>
</div>
<div className='column'>
<h3 style={{ textDecoration: 'underline', display: 'inline' }}>Moderating</h3>
<ul>
{accountSubplebbitsAddresses?.length === 0 && <li style={{ color: 'black' }}>Not moderating any board.</li>}
{accountSubplebbitsAddresses?.slice(0, showAllSubscriptions ? undefined : 18).map((subplebbit, index) => (
<li key={`sub-${index}`}>
<OfflineIndicator address={subplebbit} className='disconnected' isText={true} />
<Link key={`sub-link-${index}`} className='boardlink' to={{ pathname: `/p/${subplebbit}`, state: { scrollToTop: true } }}>
{subplebbit}
</Link>
&nbsp;
</li>
))}
</ul>
</div>
</div>
</BoardsBox>
<BoardsBox>
<div className='boxbar'>
<h2>Popular Threads</h2>
</div>
<BoardsContent>
{sfwListCids.map((cid, index) => (
<PopularThreads key={index} commentCid={cid} />
))}
</BoardsContent>
</BoardsBox>
</Page>
<Footer>
<ul>
<li className='fill'></li>
<li className='first'>
<a href='https://etherscan.io/token/0xEA81DaB2e0EcBc6B5c4172DE4c22B6Ef6E55Bd8f' target='_blank' rel='noopener noreferrer'>
Token
</a>
</li>
<li>
<a href='https://gitcoin.co/grants/5515/plebbit-a-serverless-adminless-decentralized-redd' target='_blank' rel='noopener noreferrer'>
Donate
</a>
</li>
<li>
<a href='https://github.com/plebbit/whitepaper/discussions/2' target='_blank' rel='noopener noreferrer'>
Whitepaper
</a>
</li>
<li>
<a href='https://github.com/plebbit/plebchan' target='_blank' rel='noopener noreferrer'>
GitHub
</a>
</li>
<li>
<a href='https://matrix.to/#/#plebbit:plebbitchat.org' target='_blank' rel='noopener noreferrer'>
Matrix
</a>
</li>
<li>
<a href='https://t.me/plebbit' target='_blank' rel='noopener noreferrer'>
Telegram
</a>
</li>
<li>
<a href='https://twitter.com/plebchan_eth' target='_blank' rel='noopener noreferrer'>
Twitter
</a>
</li>
<li>
<a href='https://discord.gg/E7ejphwzGW' target='_blank' rel='noopener noreferrer'>
Discord
</a>
</li>
</ul>
</Footer>
<div
style={{
textAlign: 'center',
fontSize: '11px',
marginBottom: '2em',
}}
>
plebchan v{version}
{commitRef}. GPL-2.0
</div>
</Container>
</>
);
};
export default Home;
-78
View File
@@ -1,78 +0,0 @@
import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet-async';
import { Link } from 'react-router-dom';
import { Container, Header, Logo, Page, BoardsBox } from '../styled/views/Home.styled';
import packageJson from '../../../package.json';
const { version } = packageJson;
const commitRef = process?.env?.REACT_APP_COMMIT_REF ? ` ${process.env.REACT_APP_COMMIT_REF.slice(0, 7)}` : '';
const NotFound = () => {
useEffect(() => {
const yotsubaBodyStyle = {
background: '#ffe url(assets/fade.png) top repeat-x',
color: 'maroon',
fontFamily: 'Arial, Helvetica, sans-serif',
};
const originalBodyStyles = {
background: document.body.style.background,
color: document.body.style.color,
fontFamily: document.body.style.fontFamily,
};
for (const [key, value] of Object.entries(yotsubaBodyStyle)) {
document.body.style[key] = value;
}
return () => {
for (const [key, value] of Object.entries(originalBodyStyles)) {
document.body.style[key] = value;
}
};
}, []);
return (
<>
<Helmet>
<title>plebchan - 404 Not Found</title>
</Helmet>
<Container>
<Header>
<Logo>
<Link to='/'>
<img alt='plebchan' src='assets/logo/logo-transparent.png' />
</Link>
</Logo>
</Header>
<Page>
<BoardsBox>
<div className='boxbar'>
<h2 style={{ textAlign: 'center' }}>404 Not Found</h2>
</div>
<img
src='assets/plebchan-husbando.jpg'
alt='plebchan'
style={{
display: 'block',
margin: 'auto',
padding: '15px',
width: '50%',
}}
></img>
</BoardsBox>
</Page>
<div
style={{
textAlign: 'center',
fontSize: '11px',
marginTop: '2em',
}}
>
plebchan v{version}
{commitRef}. GPL-2.0
</div>
</Container>
</>
);
};
export default NotFound;
-750
View File
@@ -1,750 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Helmet } from 'react-helmet-async';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { Tooltip } from 'react-tooltip';
import { useAccount, useAccountComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
import useGeneralStore from '../../hooks/stores/useGeneralStore';
import { Container, NavBar, Header, Break, PostForm, BoardForm } from '../styled/views/Board.styled';
import { ReplyFormLink, TopBar, BottomBar } from '../styled/views/Thread.styled';
import BoardStats from '../BoardStats';
import ImageBanner from '../ImageBanner';
import Post from '../Post';
import PostLoader from '../PostLoader';
import CreateBoardModal from '../modals/CreateBoardModal';
import SettingsModal from '../modals/SettingsModal';
import getCommentMediaInfo from '../../utils/getCommentMediaInfo';
import getDate from '../../utils/getDate';
import handleImageClick from '../../utils/handleImageClick';
import handleQuoteClick from '../../utils/handleQuoteClick';
import handleStyleChange from '../../utils/handleStyleChange';
import useError from '../../hooks/useError';
import useStateString from '../../hooks/useStateString';
import packageJson from '../../../package.json';
const { version } = packageJson;
const commitRef = process?.env?.REACT_APP_COMMIT_REF ? ` ${process.env.REACT_APP_COMMIT_REF.slice(0, 7)}` : '';
const Pending = () => {
const { defaultSubplebbits, isSettingsOpen, setIsSettingsOpen, selectedAddress, setSelectedAddress, selectedStyle, setSelectedTitle, showPostFormLink } =
useGeneralStore((state) => state);
const { index } = useParams();
const account = useAccount();
const comment = useAccountComment({ commentIndex: index });
const [, setNewErrorMessage] = useError();
const [isThumbnailClicked, setIsThumbnailClicked] = useState({});
const [isMobileThumbnailClicked, setIsMobileThumbnailClicked] = useState({});
const [isCreateBoardOpen, setIsCreateBoardOpen] = useState(false);
const subplebbitAddress = comment?.subplebbitAddress;
const subplebbit = useSubplebbit({ subplebbitAddress });
const selectedTitle = subplebbit?.title;
const handleThumbnailClick = (index, isMobile = false) => {
if (isMobile) {
setIsMobileThumbnailClicked((prevState) => ({
...prevState,
[index]: !prevState[index],
}));
} else {
setIsThumbnailClicked((prevState) => ({
...prevState,
[index]: !prevState[index],
}));
}
};
useEffect(() => {
setSelectedAddress(comment?.subplebbitAddress);
}, [comment, setSelectedAddress]);
const stateString = useStateString(comment);
const errorString = useMemo(() => {
if (comment?.state === 'failed') {
let errorString = 'Failed fetching pending thread. Pending index: ' + index;
if (comment.error) {
errorString += `: ${comment.error.toString().slice(0, 300)}`;
}
return errorString;
}
}, [comment?.state, comment?.error, index]);
useEffect(() => {
if (errorString) {
setNewErrorMessage(errorString);
}
}, [errorString, setNewErrorMessage]);
const [visible] = useState(true);
const navigate = useNavigate();
const [commentMediaInfo, setCommentMediaInfo] = useState(null);
const fallbackImgUrl = 'assets/filedeleted-res.gif';
// mobile navbar board select functionality
const handleSelectChange = (event) => {
const selected = event.target.value;
if (selected === 'subscriptions') {
navigate(`/p/subscriptions`);
return;
} else if (selected === 'all') {
navigate(`/p/all`);
return;
}
const selectedTitle = defaultSubplebbits.find((subplebbit) => subplebbit.address === selected).title;
setSelectedTitle(selectedTitle);
setSelectedAddress(selected);
navigate(`/p/${selected}`);
};
// get comment media info
useEffect(() => {
if (comment && comment.link) {
const mediaInfo = getCommentMediaInfo(comment);
setCommentMediaInfo(mediaInfo);
}
}, [comment]);
return (
<>
<Helmet>
<title>{`plebchan - Pending Thread #${parseInt(index) + 1}`}</title>
</Helmet>
<Container>
<CreateBoardModal selectedStyle={selectedStyle} isOpen={isCreateBoardOpen} closeModal={() => setIsCreateBoardOpen(false)} />
<SettingsModal selectedStyle={selectedStyle} isOpen={isSettingsOpen} closeModal={() => setIsSettingsOpen(false)} />
<NavBar selectedStyle={selectedStyle}>
<>
<span className='boardList'>
[<Link to={{ pathname: `/p/all`, state: { scrollToTop: true } }}>All</Link>
 / 
<Link to={{ pathname: `/p/subscriptions`, state: { scrollToTop: true } }}>Subscriptions</Link>
]&nbsp;[
{defaultSubplebbits.map((subplebbit, index) => (
<span className='boardList' key={`span-${subplebbit.address}`}>
{index === 0 ? null : '\u00a0'}
<Link
to={`/p/${subplebbit.address}`}
key={`a-${subplebbit.address}`}
onClick={() => {
setSelectedTitle(subplebbit.title);
setSelectedAddress(subplebbit.address);
}}
>
{subplebbit.title ? subplebbit.title : subplebbit.address}
</Link>
{index !== defaultSubplebbits.length - 1 ? ' /' : null}
</span>
))}
]
</span>
<span className='nav'>
[
<span
id='button-span'
style={{ cursor: 'pointer' }}
onClick={() => {
window.electron && window.electron.isElectron
? setIsCreateBoardOpen(true)
: alert(
'HOW TO CREATE A BOARD (currently via CLI only):\n\n(1) have a computer you can use as server, because users will connect to your board peer-to-peer;\n\n(2) go to https://github.com/plebbit/plebbit-cli#install and follow the instructions to run your daemon, create a subplebbit (board) and edit its settings;\n\n(3) users can always connect to your board via its address, but it can be added below by submitting a pull request here: https://github.com/plebbit/temporary-default-subplebbits. This will be automated with a plebbit DAO using the plebbit token.',
);
}}
>
Create Board
</span>
] [
<Link to={`/profile/c/${index}/settings`} onClick={() => setIsSettingsOpen(true)}>
Settings
</Link>
] [<Link to='/'>Home</Link>]
</span>
<div id='board-nav-mobile' style={{ top: visible ? 0 : '-23px' }}>
<div className='nav-container'>
<div className='board-select'>
<strong>Board</strong>
&nbsp;
<select id='board-select-mobile' value={selectedAddress} onChange={handleSelectChange}>
<option value='all'>All</option>
<option value='subscriptions'>Subscriptions</option>
{!defaultSubplebbits.some((subplebbit) => subplebbit.address === selectedAddress) && <option value={selectedAddress}>{selectedAddress}</option>}
{defaultSubplebbits.map((subplebbit) => (
<option key={`option-${subplebbit.address}`} value={subplebbit.address}>
{subplebbit.title ? subplebbit.title : subplebbit.address}
</option>
))}
</select>
 
<span
id='button-span'
style={{ cursor: 'pointer' }}
onClick={() =>
alert(
'HOW TO CREATE A BOARD (currently via CLI only):\n\n(1) have a computer you can use as server, because users will connect to your board peer-to-peer;\n\n(2) go to https://github.com/plebbit/plebbit-cli#install and follow the instructions to run your daemon, create a subplebbit (board) and edit its settings;\n\n(3) users can always connect to your board via its address, but it can be added below by submitting a pull request here: https://github.com/plebbit/temporary-default-subplebbits. This will be automated with a plebbit DAO using the plebbit token.',
)
}
>
Create Board
</span>
</div>
<div className='page-jump'>
<Link to={`/p/${selectedAddress}/settings`} onClick={() => setIsSettingsOpen(true)}>
Settings
</Link>
&nbsp;
<Link
to='/'
onClick={() => {
handleStyleChange({ target: { value: 'Yotsuba' } });
window.scrollTo(0, 0);
}}
>
Home
</Link>
</div>
</div>
</div>
<div id='separator-mobile'>&nbsp;</div>
<div id='separator-mobile'>&nbsp;</div>
</>
</NavBar>
<Header selectedStyle={selectedStyle}>
<>
<div className='banner'>
<ImageBanner />
</div>
<>
<div className='board-title'>{selectedTitle ?? null}</div>
<div className='board-address'>{selectedAddress ? 'p/' + selectedAddress : null}</div>
</>
</>
</Header>
<Break selectedStyle={selectedStyle} />
<PostForm selectedStyle={selectedStyle} name='post' action='' method='post' enctype='multipart/form-data'>
<ReplyFormLink id='post-form-link' showReplyFormLink={showPostFormLink} selectedStyle={selectedStyle}>
<div id='return-button-mobile'>
<span className='btn-wrap'>
<Link
to={`/p/${selectedAddress}`}
onClick={() => {
window.scrollTo(0, 0);
}}
>
Return
</Link>
</span>
</div>
<div id='catalog-button-mobile'>
<span className='btn-wrap'>
<Link
to={`/p/${selectedAddress}/catalog`}
onClick={() => {
window.scrollTo(0, 0);
}}
>
Catalog
</Link>
</span>
</div>
<div>&nbsp;</div>
</ReplyFormLink>
</PostForm>
<BoardStats subplebbitAddress={subplebbitAddress} />
<TopBar selectedStyle={selectedStyle}>
<hr />
<span className='style-changer'>
Style:  
<select id='style-selector' onChange={handleStyleChange} value={selectedStyle}>
<option value='Yotsuba'>Yotsuba</option>
<option value='Yotsuba-B'>Yotsuba B</option>
<option value='Futaba'>Futaba</option>
<option value='Burichan'>Burichan</option>
<option value='Tomorrow'>Tomorrow</option>
<option value='Photon'>Photon</option>
</select>
</span>
<span className='return-button' id='return-button-desktop'>
[
<Link
to={`/p/${selectedAddress}`}
onClick={() => {
window.scrollTo(0, 0);
}}
>
Return
</Link>
]
</span>
<span className='return-button catalog-button' id='catalog-button-desktop'>
[
<Link
to={`/p/${selectedAddress}/catalog`}
onClick={() => {
window.scrollTo(0, 0);
}}
>
Catalog
</Link>
]
</span>
<div className='stats-pending-container'>
<span id='stats-pending'>{stateString}</span>
<span className={stateString ? 'ellipsis-all' : ''} />
</div>
<hr />
</TopBar>
<Tooltip id='tooltip' className='tooltip' />
<BoardForm selectedStyle={selectedStyle}>
{comment !== undefined ? (
<>
<div className='thread'>
<div className='op-container'>
<div className='post op'>
<div className='post-info'>
{commentMediaInfo?.url ? (
<div key={`f-${index}`} className='file' style={{ marginBottom: '5px' }}>
<div key={`ft-${index}`} className='file-text'>
Link:&nbsp;
<a key={`fa-${index}`} href={commentMediaInfo.url} target='_blank' rel='noopener noreferrer'>
{commentMediaInfo?.url.length > 30 ? commentMediaInfo?.url.slice(0, 30) + '(...)' : commentMediaInfo?.url}
</a>
&nbsp;({commentMediaInfo?.type === 'iframe' ? 'video' : commentMediaInfo?.type})
{isThumbnailClicked[index] ? (
<span>
-[
<span
className='reply-link'
style={{ textDecoration: 'underline', cursor: 'pointer' }}
onClick={() => {
handleThumbnailClick(index);
}}
>
Close
</span>
]
</span>
) : null}
</div>
{commentMediaInfo?.type === 'iframe' && (
<div key={`enlarge-${index}`} className={`img-container ${isThumbnailClicked[index] ? 'expanded-container' : ''}`}>
<span key={`fta-${index}`} className='file-thumb'>
{(isThumbnailClicked[index] || !commentMediaInfo.thumbnail) && commentMediaInfo.embedUrl ? (
<iframe
className='enlarged'
key={`fti-${index}`}
src={commentMediaInfo.embedUrl}
width={commentMediaInfo.thumbnail ? '560' : '250'}
height='315'
style={{ border: 'none' }}
title='Embedded content'
allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
allowFullScreen
/>
) : (
<img
key={`fti-${index}`}
src={commentMediaInfo.thumbnail}
alt='thumbnail'
onClick={() => {
handleThumbnailClick(index);
}}
style={{ cursor: 'pointer' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
)}
</span>
</div>
)}
{commentMediaInfo?.type === 'webpage' ? (
<div key={`enlarge-${index}`} className='img-container'>
<span key={`fta-${index}`} className='file-thumb'>
{comment.thumbnailUrl ? (
<img
key={`fti-${index}`}
src={commentMediaInfo.thumbnail}
alt={commentMediaInfo.type}
onClick={handleImageClick}
style={{ cursor: 'pointer' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : null}
</span>
</div>
) : null}
{commentMediaInfo?.type === 'image' ? (
<div key={`enlarge-${index}`} className='img-container'>
<span key={`fta-${index}`} className='file-thumb'>
<img
key={`fti-${index}`}
src={commentMediaInfo.url}
alt={commentMediaInfo.type}
onClick={handleImageClick}
style={{ cursor: 'pointer' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
</span>
</div>
) : null}
{commentMediaInfo?.type === 'video' ? (
<div key={`enlarge-${index}`} className={`img-container ${isThumbnailClicked[index] ? 'expanded-container' : ''}`}>
<span key={`fta-${index}`} className='file-thumb'>
{isThumbnailClicked[index] ? (
<video
className='enlarged'
key={`fti-${index}`}
src={commentMediaInfo.url}
controls
style={{ cursor: 'pointer' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : (
<video
key={`fti-${index}`}
src={commentMediaInfo.url}
alt='thumbnail'
onClick={() => {
handleThumbnailClick(index);
}}
style={{ cursor: 'pointer' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
)}
</span>
</div>
) : null}
{commentMediaInfo?.type === 'audio' ? (
<span key={`fta-${index}`} className='file-thumb'>
<audio
controls
key={`fti-${index}`}
src={commentMediaInfo.url}
alt={commentMediaInfo.type}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
</span>
) : null}
</div>
) : null}
<span className='name-block'>
{comment.title ? (
comment.title.length > 75 ? (
<>
<span key={`q-${'pending'}`} className='title' data-tooltip-id='tooltip' data-tooltip-content={comment.title} data-tooltip-place='top'>
{comment.title.slice(0, 75) + ' (...)'}
</span>
</>
) : (
<span key={`q-${'pending'}`} className='title'>
{comment.title}
</span>
)
) : null}
&nbsp;
{comment.author?.displayName ? (
comment.author?.displayName.length > 20 ? (
<>
<span
key={`n-${'pending'}`}
className='name'
data-tooltip-id='tooltip'
data-tooltip-content={comment.author?.displayName}
data-tooltip-place='top'
>
{comment.author?.displayName.slice(0, 20) + ' (...)'}
</span>
</>
) : (
<span key={`n-${'pending'}`} className='name'>
{comment.author?.displayName}
</span>
)
) : (
<span key={`n-${'pending'}`} className='name'>
Anonymous
</span>
)}
&nbsp; &nbsp;
<span className='poster-address'>(u/{account?.author?.shortAddress})</span>
&nbsp;
<span className='date-time' data-utc='data'>
{getDate(comment?.timestamp)}
</span>
&nbsp;
<span className='post-number'>
<Link to='' title='Link to this post'>
c/
</Link>
<span key='pending' style={{ color: 'red', fontWeight: '700' }}>
Pending
</span>
</span>
&nbsp;&nbsp;
<button key={`pmb-${'pending'}`} className='post-menu-button' title='Post menu' style={{ all: 'unset', cursor: 'pointer' }}>
</button>
</span>
<blockquote key={`blockquote-${'pending'}`}>
<Post content={comment.content} comment={comment} handleQuoteClick={handleQuoteClick} key={`post-${'pending'}`} />
</blockquote>
</div>
</div>
</div>
</div>
<div className='thread-mobile' key='thread-mobile'>
<hr />
<div className='op-container' key='op-container'>
<div key={`mob-po-${'pending'}`} className='post op'>
<div key={`mob-pi-${'pending'}`} className='post-info-mobile'>
<button style={{ all: 'unset', cursor: 'pointer' }} key={`mob-pb-${'pending'}`} className='post-menu-button-mobile' onClick={() => {}}>
...
</button>
<span className='name-block-mobile'>
{comment.author?.displayName ? (
comment.author?.displayName.length > 15 ? (
<>
<span
key={`mob-n-${'pending'}`}
className='name-mobile'
data-tooltip-id='tooltip'
data-tooltip-content={comment.author?.displayName}
data-tooltip-place='top'
>
{comment.author?.displayName.slice(0, 15) + ' (...)'}
</span>
</>
) : (
<span key={`mob-n-${'pending'}`} className='name-mobile'>
{comment.author?.displayName}
</span>
)
) : (
<span key={`mob-n-${'pending'}`} className='name-mobile'>
Anonymous
</span>
)}
&nbsp;
<span key={`mob-pa-${'pending'}`} className='poster-address-mobile'>
(u/{account?.author?.shortAddress})&nbsp;
</span>
<br key={`mob-br1-${'pending'}`} />
{comment.title ? (
comment.title.length > 30 ? (
<>
<span
key={`mob-t-${'pending'}`}
className='subject-mobile'
data-tooltip-id='tooltip'
data-tooltip-content={comment.title}
data-tooltip-place='top'
>
{comment.title.slice(0, 30) + ' (...)'}
</span>
</>
) : (
<span key={`mob-t-${'pending'}`} className='subject-mobile'>
{comment.title}
</span>
)
) : null}
</span>
<span key={`mob-dt-${'pending'}`} className='date-time-mobile'>
{getDate(comment?.timestamp)}
&nbsp;
<button id='reply-button' style={{ all: 'unset' }} key={`mob-no-${'pending'}`}>
c/
</button>
&nbsp;
<span key='pending-mob' style={{ color: 'red', fontWeight: '700' }}>
Pending
</span>
</span>
</div>
{comment.link ? (
<div key={`mob-f-${index}`} className='file-mobile'>
{commentMediaInfo?.type === 'iframe' && (
<div key={`enlarge-mob-${index}`} className='img-container'>
<span key={`mob-fta-${index}`} className='file-thumb-mobile'>
{(isMobileThumbnailClicked[index] || !commentMediaInfo.thumbnail) && commentMediaInfo.embedUrl ? (
<div style={{ width: '92vw' }}>
<iframe
key={`mob-fti-${index}`}
src={commentMediaInfo.embedUrl}
style={{ border: 'none', height: '250px' }}
title='Embedded content'
allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
allowFullScreen
/>
</div>
) : (
<img
key={`mob-fti-${index}`}
src={commentMediaInfo.thumbnail}
alt='thumbnail'
onClick={() => {
handleThumbnailClick(index, true);
}}
style={{ cursor: 'pointer' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
)}
{commentMediaInfo?.type === 'video' || commentMediaInfo?.type === 'iframe' ? (
isMobileThumbnailClicked[index] ? (
<div style={{ textAlign: 'center', marginTop: '15px', marginBottom: '15px' }}>
<span
className='button-mobile'
style={{ float: 'none', cursor: 'pointer' }}
onClick={() => {
handleThumbnailClick(index, true);
}}
>
Close
</span>
</div>
) : (
<div key={`mob-fi-${index}`} className='file-info-mobile'>
video
</div>
)
) : (
<div key={`mob-fi-${index}`} className='file-info-mobile'>
video
</div>
)}
</span>
</div>
)}
{commentMediaInfo?.url ? (
commentMediaInfo.type === 'webpage' ? (
<div key={`enlarge-mob-${index}`} className='img-container'>
<span key={`mob-ft${comment.cid}`} className='file-thumb-mobile'>
{comment.thumbnailUrl ? (
<img
key={`mob-img-${index}`}
src={commentMediaInfo.thumbnail}
alt='thumbnail'
onClick={handleImageClick}
style={{ cursor: 'pointer' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : null}
<div key={`mob-fi-${index}`} className='file-info-mobile'>
{commentMediaInfo?.type}
</div>
</span>
</div>
) : commentMediaInfo.type === 'image' ? (
<div key={`enlarge-mob-${index}`} className='img-container'>
<span key={`mob-ft${comment.cid}`} className='file-thumb-mobile'>
<img
key={`mob-img-${index}`}
src={commentMediaInfo.url}
alt={commentMediaInfo.type}
onClick={handleImageClick}
style={{ cursor: 'pointer' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
<div key={`mob-fi-${index}`} className='file-info-mobile'>
{commentMediaInfo?.type}
</div>
</span>
</div>
) : commentMediaInfo.type === 'video' ? (
<span key={`mob-ft${comment.cid}`} className='file-thumb-mobile'>
{isMobileThumbnailClicked[index] ? (
<video
key={`fti-${index}`}
src={commentMediaInfo.url}
alt={commentMediaInfo.type}
controls
style={{ cursor: 'pointer' }}
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
) : (
<video
key={`fti-${index}`}
src={commentMediaInfo.url}
alt='thumbnail'
onClick={() => {
handleThumbnailClick(index, true);
}}
style={{ cursor: 'pointer' }}
id='video-thumbnail-mobile'
onError={(e) => (e.target.src = fallbackImgUrl)}
/>
)}
{commentMediaInfo?.type === 'video' || commentMediaInfo?.type === 'iframe' ? (
isMobileThumbnailClicked[index] ? (
<div style={{ textAlign: 'center', marginTop: '15px', marginBottom: '15px' }}>
<span
className='button-mobile'
style={{ float: 'none', cursor: 'pointer' }}
onClick={() => {
handleThumbnailClick(index, true);
}}
>
Close
</span>
</div>
) : (
<div key={`mob-fi-${index}`} className='file-info-mobile'>
video
</div>
)
) : (
<div key={`mob-fi-${index}`} className='file-info-mobile'>
video
</div>
)}
</span>
) : commentMediaInfo.type === 'audio' ? (
<span key={`mob-ft${comment.cid}`} className='file-thumb-mobile'>
<audio key={`mob-img-${index}`} src={commentMediaInfo.url} alt={commentMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
<div key={`mob-fi-${index}`} className='file-info-mobile'>
{commentMediaInfo?.type}
</div>
</span>
) : null
) : null}
</div>
) : null}
<blockquote key={`mob-bq-${'pending'}`} className='post-message-mobile'>
{comment.content ? (
<>
<Post content={comment.content} handleQuoteClick={handleQuoteClick} comment={comment} key={`post-mobile-${'pending'}`} />
</>
) : null}
</blockquote>
</div>
</div>
</div>
<BottomBar selectedStyle={selectedStyle}>
<div id='bottombar-desktop'>
<hr />
</div>
</BottomBar>
</>
) : (
<PostLoader />
)}
</BoardForm>
<div
style={{
textAlign: 'center',
fontSize: '11px',
marginTop: '2em',
marginBottom: '2em',
}}
>
plebchan v{version}
{commitRef}. GPL-2.0
</div>
</Container>
</>
);
};
export default Pending;
-706
View File
@@ -1,706 +0,0 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Helmet } from 'react-helmet-async';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { Tooltip } from 'react-tooltip';
import { useSubplebbit } from '@plebbit/plebbit-react-hooks';
import { debounce } from 'lodash';
import { Container, NavBar, Header, Break, PostMenu, PostForm, PostMenuMobile } from '../styled/views/Board.styled';
import { TopBar, BoardForm, Footer, ReplyFormLink } from '../styled/views/Thread.styled';
import { PostMenuCatalog } from '../styled/views/Catalog.styled';
import BoardStats from '../BoardStats';
import ImageBanner from '../ImageBanner';
import CreateBoardModal from '../modals/CreateBoardModal';
import AdminListModal from '../modals/AdminListModal';
import OfflineIndicator from '../OfflineIndicator';
import SettingsModal from '../modals/SettingsModal';
import getDate from '../../utils/getDate';
import handleStyleChange from '../../utils/handleStyleChange';
import useGeneralStore from '../../hooks/stores/useGeneralStore';
import packageJson from '../../../package.json';
const { version } = packageJson;
const commitRef = process?.env?.REACT_APP_COMMIT_REF ? ` ${process.env.REACT_APP_COMMIT_REF.slice(0, 7)}` : '';
const Rules = () => {
const { defaultSubplebbits, isSettingsOpen, setIsSettingsOpen, selectedAddress, setSelectedAddress, selectedStyle, setSelectedTitle } = useGeneralStore(
(state) => state,
);
// temporary hardcode
const plebtokenRules = [
'This community is strictly SFW. Any NSFW language or content will be removed.',
<span>
Only post about the plebbit token ($PLEB). For general cryptocurrency discussion, go to{' '}
<Link className='quotelink' to='/p/business-and-finance.eth'>
p/business-and-finance.eth
</Link>
.
</span>,
'FUD is allowed unless blatantly meaningless and spammy.',
];
const navigate = useNavigate();
const threadMenuRefs = useRef({});
const threadMenuRefsMobile = useRef({});
const postMenuMobileRef = useRef(null);
const postMenuCatalogRef = useRef(null);
const [isAdminListOpen, setIsAdminListOpen] = useState(false);
const [prevScrollPos, setPrevScrollPos] = useState(0);
const [visible, setVisible] = useState(true);
const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
const [mobileMenuPosition, setMobileMenuPosition] = useState({ top: 0, left: 0 });
const [openMenuCid, setOpenMenuCid] = useState(null);
const [openMobileMenuCid, setOpenMobileMenuCid] = useState(null);
const [isCreateBoardOpen, setIsCreateBoardOpen] = useState(false);
const [isClientRedirectMenuOpen, setIsClientRedirectMenuOpen] = useState(false);
const { subplebbitAddress } = useParams();
const subplebbit = useSubplebbit({ subplebbitAddress: selectedAddress });
const handleOptionClick = () => {
setOpenMenuCid(null);
};
const handleMobileOptionClick = () => {
setOpenMobileMenuCid(null);
};
const handleOutsideClick = useCallback(
(e) => {
if (openMenuCid !== null && !postMenuCatalogRef.current.contains(e.target)) {
setOpenMenuCid(null);
}
},
[openMenuCid, postMenuCatalogRef],
);
const handleMobileOutsideClick = useCallback(
(e) => {
if (openMobileMenuCid !== null && !postMenuMobileRef.current.contains(e.target)) {
setOpenMobileMenuCid(null);
}
},
[openMobileMenuCid, postMenuMobileRef],
);
useEffect(() => {
if (openMenuCid !== null) {
document.addEventListener('click', handleOutsideClick);
} else {
document.removeEventListener('click', handleOutsideClick);
}
return () => {
document.removeEventListener('click', handleOutsideClick);
};
}, [openMenuCid, handleOutsideClick]);
useEffect(() => {
if (openMobileMenuCid !== null) {
document.addEventListener('click', handleMobileOutsideClick);
} else {
document.removeEventListener('click', handleMobileOutsideClick);
}
return () => {
document.removeEventListener('click', handleMobileOutsideClick);
};
}, [openMobileMenuCid, handleMobileOutsideClick]);
useEffect(() => {
window.scrollTo(0, 0);
}, []);
useEffect(() => {
const selectedSubplebbit = defaultSubplebbits.find((subplebbit) => subplebbit.address === subplebbitAddress);
if (subplebbitAddress) {
setSelectedAddress(subplebbitAddress);
} else if (subplebbit?.address) {
setSelectedAddress(subplebbit.address);
}
if (selectedSubplebbit) {
setSelectedTitle(selectedSubplebbit.title);
} else if (subplebbit?.title) {
setSelectedTitle(subplebbit.title);
}
}, [subplebbitAddress, setSelectedAddress, setSelectedTitle, defaultSubplebbits, subplebbit?.address, subplebbit?.title]);
// mobile navbar scroll effect
useEffect(() => {
const debouncedHandleScroll = debounce(() => {
const currentScrollPos = window.scrollY;
setVisible(prevScrollPos > currentScrollPos || currentScrollPos < 10);
setPrevScrollPos(currentScrollPos);
}, 50);
window.addEventListener('scroll', debouncedHandleScroll);
return () => window.removeEventListener('scroll', debouncedHandleScroll);
}, [prevScrollPos, visible]);
// mobile navbar board select functionality
const handleSelectChange = (event) => {
const selected = event.target.value;
if (selected === 'subscriptions') {
navigate(`/p/subscriptions`);
return;
} else if (selected === 'all') {
navigate(`/p/all`);
return;
}
const selectedTitle = defaultSubplebbits.find((subplebbit) => subplebbit.address === selected).title;
setSelectedTitle(selectedTitle);
setSelectedAddress(selected);
navigate(`/p/${selected}`);
};
return (
<>
<Helmet>
<title>{'Board Rules - ' + (subplebbit.title || subplebbit.address) + ' - plebchan'}</title>
</Helmet>
<Container>
<CreateBoardModal selectedStyle={selectedStyle} isOpen={isCreateBoardOpen} closeModal={() => setIsCreateBoardOpen(false)} />
<AdminListModal selectedStyle={selectedStyle} isOpen={isAdminListOpen} closeModal={() => setIsAdminListOpen(false)} roles={subplebbit.roles} />
<SettingsModal selectedStyle={selectedStyle} isOpen={isSettingsOpen} closeModal={() => setIsSettingsOpen(false)} />
<NavBar selectedStyle={selectedStyle}>
<>
<span className='boardList'>
[<Link to={{ pathname: `/p/all`, state: { scrollToTop: true } }}>All</Link>
 / 
<Link to={{ pathname: `/p/subscriptions`, state: { scrollToTop: true } }}>Subscriptions</Link>
]&nbsp;[
{defaultSubplebbits.map((subplebbit, index) => (
<span className='boardList' key={`span-${subplebbit.address}`}>
{index === 0 ? null : '\u00a0'}
<Link
to={`/p/${subplebbit.address}`}
key={`a-${subplebbit.address}`}
onClick={() => {
setSelectedTitle(subplebbit.title);
setSelectedAddress(subplebbit.address);
}}
>
{subplebbit.title ? subplebbit.title : subplebbit.address}
</Link>
{index !== defaultSubplebbits.length - 1 ? ' /' : null}
</span>
))}
]
</span>
<span className='nav'>
[
<span
id='button-span'
style={{ cursor: 'pointer' }}
onClick={() => {
window.electron && window.electron.isElectron
? setIsCreateBoardOpen(true)
: alert(
'HOW TO CREATE A BOARD (currently via CLI only):\n\n(1) have a computer you can use as server, because users will connect to your board peer-to-peer;\n\n(2) go to https://github.com/plebbit/plebbit-cli#install and follow the instructions to run your daemon, create a subplebbit (board) and edit its settings;\n\n(3) users can always connect to your board via its address, but it can be added below by submitting a pull request here: https://github.com/plebbit/temporary-default-subplebbits. This will be automated with a plebbit DAO using the plebbit token.',
);
}}
>
Create Board
</span>
] [
<Link to={`/p/${selectedAddress}/rules/settings`} onClick={() => setIsSettingsOpen(true)}>
Settings
</Link>
] [<Link to='/'>Home</Link>]
</span>
<div id='board-nav-mobile' style={{ top: visible ? 0 : '-23px' }}>
<div className='nav-container'>
<div className='board-select'>
<strong>Board</strong>
&nbsp;
<select id='board-select-mobile' value={selectedAddress} onChange={handleSelectChange}>
<option value='all'>All</option>
<option value='subscriptions'>Subscriptions</option>
{!defaultSubplebbits.some((subplebbit) => subplebbit.address === selectedAddress) && <option value={selectedAddress}>{selectedAddress}</option>}
{defaultSubplebbits.map((subplebbit) => (
<option key={`option-${subplebbit.address}`} value={subplebbit.address}>
{subplebbit.title ? subplebbit.title : subplebbit.address}
</option>
))}
</select>
 
<span
id='button-span'
style={{ cursor: 'pointer' }}
onClick={() =>
alert(
'HOW TO CREATE A BOARD (currently via CLI only):\n\n(1) have a computer you can use as server, because users will connect to your board peer-to-peer;\n\n(2) go to https://github.com/plebbit/plebbit-cli#install and follow the instructions to run your daemon, create a subplebbit (board) and edit its settings;\n\n(3) users can always connect to your board via its address, but it can be added below by submitting a pull request here: https://github.com/plebbit/temporary-default-subplebbits. This will be automated with a plebbit DAO using the plebbit token.',
)
}
>
Create Board
</span>
</div>
<div className='page-jump'>
<Link to={`/p/${selectedAddress}/rules/settings`} onClick={() => setIsSettingsOpen(true)}>
Settings
</Link>
&nbsp;
<Link
to='/'
onClick={() => {
handleStyleChange({ target: { value: 'Yotsuba' } });
}}
>
Home
</Link>
</div>
</div>
</div>
<div id='separator-mobile'>&nbsp;</div>
<div id='separator-mobile'>&nbsp;</div>
</>
</NavBar>
<Header selectedStyle={selectedStyle}>
<>
<div className='banner'>
<ImageBanner />
</div>
<>
<div className='board-title'>{subplebbit.title ?? null}</div>
<div className='board-address'>
p/{subplebbit.address}
<OfflineIndicator address={subplebbit.address} className='offline' tooltipPlace='top' />
</div>
</>
</>
</Header>
<Break selectedStyle={selectedStyle} />
<PostForm selectedStyle={selectedStyle}>
<ReplyFormLink id='post-form-link' selectedStyle={selectedStyle} style={{ marginBottom: '10px' }}>
<div id='return-button-mobile'>
<span className='btn-wrap'>
<Link to={{ pathname: `/p/${selectedAddress}`, state: { scrollToTop: true } }}>Return</Link>
</span>
</div>
<div id='catalog-button-mobile'>
<span className='btn-wrap'>
<Link to={{ pathname: `/p/${selectedAddress}/catalog`, state: { scrollToTop: true } }}>Catalog</Link>
</span>
</div>
<div id='bottom-button-mobile'>
<span className='btn-wrap'>
<span
style={{ cursor: 'pointer' }}
onClick={() => window.scrollTo(0, document.body.scrollHeight)}
onMouseOver={(event) => (event.target.style.cursor = 'pointer')}
>
Bottom
</span>
</span>
</div>
</ReplyFormLink>
</PostForm>
<BoardStats subplebbitAddress={selectedAddress} />
<TopBar selectedStyle={selectedStyle}>
<span className='style-changer'>
Style:  
<select id='style-selector' onChange={handleStyleChange} value={selectedStyle}>
<option value='Yotsuba'>Yotsuba</option>
<option value='Yotsuba-B'>Yotsuba B</option>
<option value='Futaba'>Futaba</option>
<option value='Burichan'>Burichan</option>
<option value='Tomorrow'>Tomorrow</option>
<option value='Photon'>Photon</option>
</select>
</span>
<span className='return-button' id='return-button-desktop'>
[<Link to={{ pathname: `/p/${selectedAddress}`, state: { scrollToTop: true } }}>Return</Link>]
</span>
<span className='return-button catalog-button' id='catalog-button-desktop'>
[<Link to={{ pathname: `/p/${selectedAddress}/catalog`, state: { scrollToTop: true } }}>Catalog</Link>]
</span>
<span className='return-button catalog-button' id='bottom-button-desktop'>
[
<span
id='button'
style={{ cursor: 'pointer' }}
onClick={() => window.scrollTo(0, document.body.scrollHeight)}
onMouseOver={(event) => (event.target.style.cursor = 'pointer')}
onTouchStart={() => window.scrollTo(0, document.body.scrollHeight)}
>
Bottom
</span>
]
</span>
</TopBar>
<Tooltip id='tooltip' className='tooltip' />
<BoardForm selectedStyle={selectedStyle}>
<>
<div className='thread'>
<div className='op-container'>
<div className='post op op-desktop'>
<hr />
<div className='post-info'>
<span className='name-block'>
<span className='title'>Rules</span>
&nbsp;
<span
className='name capcode'
style={{ cursor: 'pointer' }}
onClick={() => {
setIsAdminListOpen(!isAdminListOpen);
}}
>
## Board Mods
</span>
&nbsp;
<span className='date-time'>{getDate(subplebbit.createdAt)}</span>
&nbsp;
<img src='assets/sticky.gif' alt='Sticky' title='Sticky' style={{ marginBottom: '-1px', imageRendering: 'pixelated' }} />
&nbsp;
<img src='assets/closed.gif' alt='Closed' title='Closed' style={{ marginBottom: '-1px', imageRendering: 'pixelated' }} />
<span>
&nbsp; [
<Link to={() => {}} style={{ textDecoration: 'none' }} className='reply-link'>
Reply
</Link>
]
</span>
<PostMenu
title='Post menu'
ref={(el) => {
threadMenuRefs.current['rules'] = el;
}}
className='post-menu-button'
rotated={openMenuCid === 'rules'}
onClick={(event) => {
event.stopPropagation();
const rect = threadMenuRefs.current['rules'].getBoundingClientRect();
setMenuPosition({ top: rect.top + window.scrollY, left: rect.left });
setOpenMenuCid((prevCid) => (prevCid === 'rules' ? null : 'rules'));
}}
>
</PostMenu>
{createPortal(
<PostMenuCatalog
selectedStyle={selectedStyle}
ref={(el) => {
postMenuCatalogRef.current = el;
}}
onClick={(event) => event.stopPropagation()}
style={{ position: 'absolute', top: menuPosition.top + 7, left: menuPosition.left }}
>
<div className={`post-menu-thread post-menu-thread-${'rules'}`} style={{ display: openMenuCid === 'rules' ? 'block' : 'none' }}>
<ul className='post-menu-catalog'>
{/* {canModerate ? (
<>
change rules
</>
) : null} */}
<li
onMouseOver={() => {
setIsClientRedirectMenuOpen(true);
}}
onMouseLeave={() => {
setIsClientRedirectMenuOpen(false);
}}
>
View on »
<ul className='dropdown-menu post-menu-catalog' style={{ display: isClientRedirectMenuOpen ? 'block' : 'none' }}>
<li onClick={() => handleOptionClick('rules')}>
<a href={`https://plebbitapp.eth.limo/#/p/${selectedAddress}`} target='_blank' rel='noreferrer'>
Plebbit
</a>
</li>
<li onClick={() => handleOptionClick('rules')}>
<a href={`https://seedit.eth.limo/#/p/${selectedAddress}`} target='_blank' rel='noreferrer'>
Seedit
</a>
</li>
<li onClick={() => handleOptionClick('rules')}>
<a href={`https://plebones.eth.limo/#/p/${selectedAddress}`} target='_blank' rel='noreferrer'>
Plebones
</a>
</li>
</ul>
</li>
</ul>
</div>
</PostMenuCatalog>,
document.body,
)}
</span>
<blockquote>
<div className='custom-paragraph' style={{ paddingBottom: '17px' }}>
{subplebbit.address === 'plebtoken.eth'
? plebtokenRules.map((rule, index) => (
<React.Fragment key={index}>
{index + 1}. {rule}
<br />
</React.Fragment>
))
: subplebbit.rules?.map((rule, index) => (
<React.Fragment key={index}>
{index + 1}. {rule}
<br />
</React.Fragment>
))}
</div>
</blockquote>
</div>
</div>
</div>
</div>
<div className='thread-mobile'>
<hr />
<div className='op-container'>
<div className='post op op-mobile'>
<div className='post-info-mobile'>
<button
key={`mob-pb-`}
className='post-menu-button-mobile'
ref={(el) => {
threadMenuRefsMobile.current['rules'] = el;
}}
onClick={(event) => {
event.stopPropagation();
const rect = threadMenuRefsMobile.current['rules'].getBoundingClientRect();
setMobileMenuPosition({ top: rect.top + window.scrollY, left: rect.left });
setOpenMobileMenuCid((prevCid) => (prevCid === 'rules' ? null : 'rules'));
}}
style={{ all: 'unset', cursor: 'pointer' }}
>
...
</button>
{createPortal(
<PostMenuMobile
selectedStyle={selectedStyle}
ref={(el) => {
postMenuMobileRef.current = el;
}}
onClick={(event) => event.stopPropagation()}
style={{
position: 'absolute',
display: openMobileMenuCid === 'rules' ? 'block' : 'none',
top: mobileMenuPosition.top + 20,
left: mobileMenuPosition.left,
}}
>
<ul className={`post-menu-mobile-thread-description`}>
<a
style={{ color: 'inherit', textDecoration: 'none' }}
href={`https://plebbitapp.eth.limo/#/p/${selectedAddress}`}
target='_blank'
rel='noreferrer'
>
<li onClick={() => handleMobileOptionClick('rules')}>View on plebbit</li>
</a>
<a
style={{ color: 'inherit', textDecoration: 'none' }}
href={`https://seedit.eth.limo/#/p/${selectedAddress}`}
target='_blank'
rel='noreferrer'
>
<li onClick={() => handleMobileOptionClick('rules')}>View on seedit</li>
</a>
<a
style={{ color: 'inherit', textDecoration: 'none' }}
href={`https://plebones.eth.limo/#/p/${selectedAddress}`}
target='_blank'
rel='noreferrer'
>
<li onClick={() => handleMobileOptionClick('rules')}>View on plebones</li>
</a>
</ul>
</PostMenuMobile>,
document.body,
)}
<span className='name-block-mobile'>
<span
className='name-mobile capcode'
style={{ cursor: 'pointer' }}
onClick={() => {
setIsAdminListOpen(!isAdminListOpen);
}}
>
## Board Mods
</span>
&nbsp;
<span className='thread-icons-mobile'>
<img
src='assets/sticky.gif'
alt='Sticky'
title='Sticky'
style={{ all: 'unset', marginBottom: '-2px', paddingLeft: '2px', imageRendering: 'pixelated' }}
/>
&nbsp;
<img
src='assets/closed.gif'
alt='Closed'
title='Closed'
style={{ all: 'unset', marginBottom: '-2px', paddingLeft: '2px', imageRendering: 'pixelated' }}
/>
</span>
<br />
<span className='subject-mobile' style={{ marginBottom: '-15px' }}>
Rules
</span>
&nbsp;
</span>
<span className='date-time-mobile post-number-mobile'>{getDate(subplebbit.createdAt)}</span>
&nbsp;
</div>
<blockquote className='post-message-mobile'>
<div className='custom-paragraph'>
{subplebbit.address === 'plebtoken.eth'
? plebtokenRules.map((rule, index) => (
<React.Fragment key={index}>
{index + 1}. {rule}
<br />
</React.Fragment>
))
: subplebbit.rules?.map((rule, index) => (
<React.Fragment key={index}>
{index + 1}. {rule}
<br />
</React.Fragment>
))}
</div>
</blockquote>
</div>
<div className='post-link-mobile'>
<Link to={() => {}} className='button-mobile'>
View Thread
</Link>
</div>
</div>
</div>
</>
</BoardForm>
<Footer selectedStyle={selectedStyle}>
<Break
id='break'
selectedStyle={selectedStyle}
style={{
marginTop: '-36px',
width: '100%',
}}
/>
<Break
selectedStyle={selectedStyle}
style={{
width: '100%',
}}
/>
<span
className='style-changer'
style={{
float: 'right',
marginTop: '2px',
}}
>
Style:  
<select id='style-selector' onChange={handleStyleChange} value={selectedStyle}>
<option value='Yotsuba'>Yotsuba</option>
<option value='Yotsuba-B'>Yotsuba B</option>
<option value='Futaba'>Futaba</option>
<option value='Burichan'>Burichan</option>
<option value='Tomorrow'>Tomorrow</option>
<option value='Photon'>Photon</option>
</select>
</span>
<NavBar
selectedStyle={selectedStyle}
style={{
marginTop: '42px',
}}
>
<>
<span className='boardList'>
[<Link to={{ pathname: `/p/all`, state: { scrollToTop: true } }}>All</Link>
 / 
<Link to={{ pathname: `/p/subscriptions`, state: { scrollToTop: true } }}>Subscriptions</Link>
]&nbsp;
</span>
{defaultSubplebbits.map((subplebbit, index) => (
<span className='boardList' key={`span-${subplebbit.address}`}>
{index === 0 ? null : '\u00a0'}
<Link
to={`/p/${subplebbit.address}`}
key={`a-${subplebbit.address}`}
onClick={() => {
setSelectedTitle(subplebbit.title);
setSelectedAddress(subplebbit.address);
}}
>
{subplebbit.title ? subplebbit.title : subplebbit.address}
</Link>
{index !== defaultSubplebbits.length - 1 ? ' /' : null}
</span>
))}
<span className='nav'>
[
<span
id='button-span'
style={{ cursor: 'pointer' }}
onClick={() => {
window.electron && window.electron.isElectron
? setIsCreateBoardOpen(true)
: alert(
'HOW TO CREATE A BOARD (currently via CLI only):\n\n(1) have a computer you can use as server, because users will connect to your board peer-to-peer;\n\n(2) go to https://github.com/plebbit/plebbit-cli#install and follow the instructions to run your daemon, create a subplebbit (board) and edit its settings;\n\n(3) users can always connect to your board via its address, but it can be added below by submitting a pull request here: https://github.com/plebbit/temporary-default-subplebbits. This will be automated with a plebbit DAO using the plebbit token.',
);
}}
>
Create Board
</span>
] [
<Link to={`/p/${selectedAddress}/rules/settings`} onClick={() => setIsSettingsOpen(true)}>
Settings
</Link>
] [
<Link to='/' onClick={() => handleStyleChange({ target: { value: 'Yotsuba' } })}>
Home
</Link>
]
</span>
</>
</NavBar>
<div id='version'>
plebchan v{version}
{commitRef}. GPL-2.0
</div>
<div
className='footer-links'
style={{
textAlign: 'center',
fontSize: 'x-small',
fontFamily: 'arial',
marginTop: '5px',
marginBottom: '15px',
}}
>
<a style={{ textDecoration: 'underline' }} href='https://plebbit.com' target='_blank' rel='noopener noreferrer'>
About
</a>
&nbsp;&nbsp;
<a style={{ textDecoration: 'underline' }} href='https://github.com/plebbit/plebchan/releases/latest' target='_blank' rel='noopener noreferrer'>
App
</a>
&nbsp;&nbsp;
<a style={{ textDecoration: 'underline' }} href='https://twitter.com/plebchan_eth' target='_blank' rel='noopener noreferrer'>
Twitter
</a>
&nbsp;&nbsp;
<a style={{ textDecoration: 'underline' }} href='https://t.me/plebbit' target='_blank' rel='noopener noreferrer'>
Telegram
</a>
</div>
</Footer>
</Container>
</>
);
};
export default Rules;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
declare global {
interface Window {
STICKY_MENU_SCROLL_LISTENER: boolean;
isElectron: boolean;
}
}
export {};
-17
View File
@@ -1,17 +0,0 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useAnonModeStore = create(
persist(
(set) => ({
anonymousMode: true,
setAnonymousMode: (mode) => set({ anonymousMode: mode }),
}),
{
name: 'anonmode_store',
getStorage: () => localStorage,
},
),
);
export default useAnonModeStore;
-123
View File
@@ -1,123 +0,0 @@
import { create } from 'zustand';
const useGeneralStore = create((set) => ({
bodyStyle: JSON.parse(localStorage.getItem('bodyStyle')) || {
background: '#ffe url(assets/fade.png) top repeat-x',
color: 'maroon',
fontFamily: 'Helvetica, Arial, sans-serif',
},
setBodyStyle: (bodyStyle) => {
localStorage.setItem('bodyStyle', JSON.stringify(bodyStyle));
set(() => ({ bodyStyle }));
},
captchaResponse: '',
setCaptchaResponse: (response) => set({ captchaResponse: response }),
challengesArray: [],
setChallengesArray: (challengesArray) => set({ challengesArray }),
defaultSubplebbits: [],
setDefaultSubplebbits: (subplebbits) => set({ defaultSubplebbits: subplebbits }),
defaultNsfwSubplebbits: [],
setDefaultNsfwSubplebbits: (subplebbits) => set({ defaultNsfwSubplebbits: subplebbits }),
deletePost: false,
setDeletePost: (deletePost) => set({ deletePost }),
editedComment: '',
setEditedComment: (comment) => set({ editedComment: comment }),
editedComments: {},
setEditedComments: (comments) => set({ editedComments: comments }),
feedCacheStates: {},
setFeedCacheState: (address, isCached) =>
set((prev) => ({
feedCacheStates: {
...prev.feedCacheStates,
[address]: isCached,
},
})),
isAuthorDelete: false,
setIsAuthorDelete: (isAuthorDelete) => set({ isAuthorDelete }),
isAuthorEdit: false,
setIsAuthorEdit: (isAuthorEdit) => set({ isAuthorEdit }),
isCaptchaOpen: false,
setIsCaptchaOpen: (isOpen) => set({ isCaptchaOpen: isOpen }),
isEditModalOpen: false,
setIsEditModalOpen: (isOpen) => set({ isEditModalOpen: isOpen }),
canModerate: false,
setCanModerate: (canModerate) => set({ canModerate }),
isModerationOpen: false,
setIsModerationOpen: (isOpen) => set({ isModerationOpen: isOpen }),
isModEdit: false,
setIsModEdit: (isModEdit) => set({ isModEdit }),
isSettingsOpen: false,
setIsSettingsOpen: (isOpen) => set({ isSettingsOpen: isOpen }),
moderatingCommentCid: '',
setModeratingCommentCid: (cid) => set({ moderatingCommentCid: cid }),
originalCommentContent: null,
setOriginalCommentContent: (content) => set({ originalCommentContent: content }),
pendingComment: '',
setPendingComment: (comment) => set({ pendingComment: comment }),
pendingCommentIndex: null,
setPendingCommentIndex: (index) => set({ pendingCommentIndex: index }),
publishedComment: '',
setPublishedComment: (comment) => set({ publishedComment: comment }),
replyQuoteCid: '',
setReplyQuoteCid: (cid) => set({ replyQuoteCid: cid }),
resolveCaptchaPromise: null,
setResolveCaptchaPromise: (resolve) => set({ resolveCaptchaPromise: resolve }),
selectedAddress: '',
setSelectedAddress: (address) => set({ selectedAddress: address }),
selectedParentCid: '',
setSelectedParentCid: (parentCid) => set({ selectedParentCid: parentCid }),
selectedShortCid: '',
setSelectedShortCid: (shortCid) => set({ selectedShortCid: shortCid }),
selectedStyle: localStorage.getItem('selectedStyle') || 'Yotsuba',
setSelectedStyle: (style) => {
localStorage.setItem('selectedStyle', style);
set({ selectedStyle: style });
},
selectedText: '',
setSelectedText: (text) => set({ selectedText: text }),
selectedThread: '',
setSelectedThread: (thread) => set({ selectedThread: thread }),
selectedTitle: '',
setSelectedTitle: (title) => set({ selectedTitle: title }),
showPostForm: false,
setShowPostForm: (show) => set({ showPostForm: show }),
showPostFormLink: true,
setShowPostFormLink: (show) => set({ showPostFormLink: show }),
triggerInsertion: 0,
setTriggerInsertion: (trigger) => set({ triggerInsertion: trigger }),
}));
export default useGeneralStore;
-29
View File
@@ -1,29 +0,0 @@
import { useEffect } from 'react';
import { useAccount } from '@plebbit/plebbit-react-hooks';
import useAnonModeStore from './stores/useAnonModeStore';
const useAnonMode = (threadCid, execute) => {
const account = useAccount();
const { anonymousMode } = useAnonModeStore();
useEffect(() => {
const handleAnonMode = async () => {
let storedSigners = JSON.parse(localStorage.getItem('storedSigners')) || {};
if (!anonymousMode) {
if (execute && storedSigners[threadCid]) {
const signerPrivateKey = storedSigners[threadCid];
if (account) {
await account.plebbit.createSigner({ type: 'ed25519', privateKey: signerPrivateKey });
}
}
}
};
handleAnonMode();
}, [threadCid, execute, account, anonymousMode]);
return;
};
export default useAnonMode;
-28
View File
@@ -1,28 +0,0 @@
import { useEffect } from 'react';
import { useAccount } from '@plebbit/plebbit-react-hooks';
import useAnonModeStore from './stores/useAnonModeStore';
const useAnonModeRef = (threadCidRef, execute) => {
const account = useAccount();
const { anonymousMode } = useAnonModeStore();
useEffect(() => {
const handleAnonMode = async () => {
let storedSigners = JSON.parse(localStorage.getItem('storedSigners')) || {};
if (!anonymousMode) {
if (execute && storedSigners[threadCidRef]) {
const signerPrivateKey = storedSigners[threadCidRef];
if (account) {
await account.plebbit.createSigner({ type: 'ed25519', privateKey: signerPrivateKey });
}
}
}
};
handleAnonMode();
}, [threadCidRef, execute, account, anonymousMode]);
return;
};
export default useAnonModeRef;
-14
View File
@@ -1,14 +0,0 @@
import useGeneralStore from './stores/useGeneralStore';
const useClickForm = () => {
const { setShowPostForm, setShowPostFormLink } = useGeneralStore.getState();
const handleClickForm = () => {
setShowPostForm(true);
setShowPostFormLink(false);
};
return handleClickForm;
};
export default useClickForm;
-52
View File
@@ -1,52 +0,0 @@
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
const useError = () => {
const [errorMessage, setErrorMessage] = useState('');
const [renderCount, setRenderCount] = useState(0);
useEffect(() => {
if (errorMessage && errorMessage.length > 0) {
const showErrorToast = () => {
const toastId = toast.error(errorMessage.toString(), {
position: 'top-right',
autoClose: false,
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
progress: undefined,
theme: 'dark',
});
return () => {
toast.dismiss(toastId);
};
};
const timeoutId = setTimeout(showErrorToast, 500);
return () => {
clearTimeout(timeoutId);
};
}
}, [errorMessage, renderCount]);
const setNewErrorMessage = (error) => {
let message;
if (typeof error === 'string') {
message = error;
} else if (error instanceof Error) {
message = error.message;
} else {
message = JSON.stringify(error);
}
setErrorMessage(message);
setRenderCount((prevCount) => prevCount + 1);
};
return [errorMessage, setNewErrorMessage];
};
export default useError;
-19
View File
@@ -1,19 +0,0 @@
import { useMemo, useRef } from 'react';
const useFeedRows = (feedWithDescriptionAndRules, columnCount) => {
const rowsRef = useRef([]);
return useMemo(() => {
const rows = [];
for (let i = 0; i < feedWithDescriptionAndRules.length; i += columnCount) {
if (rowsRef.current?.[rows.length] && rowsRef.current[rows.length].length === columnCount) {
rows.push(rowsRef.current[rows.length]);
} else {
rows.push(feedWithDescriptionAndRules.slice(i, i + columnCount));
}
}
rowsRef.current = rows;
return rows;
}, [feedWithDescriptionAndRules, columnCount]);
};
export default useFeedRows;
-98
View File
@@ -1,98 +0,0 @@
import { useMemo } from 'react';
import useStateString from './useStateString';
import { useSubplebbit, useSubplebbitsStates } from '@plebbit/plebbit-react-hooks';
const clientHosts = {};
const getClientHost = (clientUrl) => {
if (!clientHosts[clientUrl]) {
try {
clientHosts[clientUrl] = new URL(clientUrl).hostname || clientUrl;
} catch (e) {
clientHosts[clientUrl] = clientUrl;
}
}
return clientHosts[clientUrl];
};
const useFeedStateString = (subplebbitAddresses) => {
// single subplebbit feed state string
const subplebbitAddress = subplebbitAddresses?.length === 1 ? subplebbitAddresses[0] : undefined;
const subplebbit = useSubplebbit({ subplebbitAddress });
const singleSubplebbitFeedStateString = useStateString(subplebbit);
// multiple subplebbit feed state string
const { states } = useSubplebbitsStates({ subplebbitAddresses });
const multipleSubplebbitsFeedStateString = useMemo(() => {
if (subplebbitAddress) {
return;
}
// e.g. Resolving 2 addresses from infura.io, fetching 2 IPNS, 1 IPFS from cloudflare-ipfs.com, ipfs.io
let stateString = '';
if (states['resolving-address']) {
const { subplebbitAddresses, clientUrls } = states['resolving-address'];
if (subplebbitAddresses.length && clientUrls.length) {
stateString += `resolving ${subplebbitAddresses.length} ${subplebbitAddresses.length === 1 ? 'address' : 'addresses'} from ${clientUrls
.map(getClientHost)
.join(', ')}`;
}
}
// find all page client and sub addresses
const pagesStatesClientHosts = new Set();
const pagesStatesSubplebbitAddresses = new Set();
for (const state in states) {
if (state.match('page')) {
states[state].clientUrls.forEach((clientUrl) => pagesStatesClientHosts.add(getClientHost(clientUrl)));
states[state].subplebbitAddresses.forEach((subplebbitAddress) => pagesStatesSubplebbitAddresses.add(subplebbitAddress));
}
}
if (states['fetching-ipns'] || states['fetching-ipfs'] || pagesStatesSubplebbitAddresses.size) {
// separate 2 different states using ', '
if (stateString) {
stateString += ', ';
}
// find all client urls
const clientHosts = new Set([...pagesStatesClientHosts]);
states['fetching-ipns']?.clientUrls.forEach((clientUrl) => clientHosts.add(getClientHost(clientUrl)));
states['fetching-ipfs']?.clientUrls.forEach((clientUrl) => clientHosts.add(getClientHost(clientUrl)));
if (clientHosts.size) {
stateString += 'fetching ';
if (states['fetching-ipns']) {
stateString += `${states['fetching-ipns'].subplebbitAddresses.length} IPNS`;
}
if (states['fetching-ipfs']) {
if (states['fetching-ipns']) {
stateString += ', ';
}
stateString += `${states['fetching-ipfs'].subplebbitAddresses.length} IPFS`;
}
if (pagesStatesSubplebbitAddresses.size) {
if (states['fetching-ipns'] || states['fetching-ipfs']) {
stateString += ', ';
}
stateString += `${pagesStatesSubplebbitAddresses.size} ${pagesStatesSubplebbitAddresses.size === 1 ? 'page' : 'pages'}`;
}
stateString += ` from ${[...clientHosts].join(', ')}`;
}
}
// capitalize first letter
stateString = stateString.charAt(0).toUpperCase() + stateString.slice(1);
// if string is empty, return undefined instead
return stateString === '' ? undefined : stateString;
}, [states, subplebbitAddress]);
if (singleSubplebbitFeedStateString) {
return singleSubplebbitFeedStateString;
}
return multipleSubplebbitsFeedStateString;
};
export default useFeedStateString;
-43
View File
@@ -1,43 +0,0 @@
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
const useInfo = () => {
const [infoMessage, setInfoMessage] = useState('');
const [renderCount, setRenderCount] = useState(0);
useEffect(() => {
if (infoMessage && infoMessage.length > 0) {
const showInfoToast = () => {
const toastId = toast.info(infoMessage.toString(), {
position: 'top-right',
autoClose: false,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
progress: undefined,
theme: 'dark',
});
return () => {
toast.dismiss(toastId);
};
};
const timeoutId = setTimeout(showInfoToast, 500);
return () => {
clearTimeout(timeoutId);
};
}
}, [infoMessage, renderCount]);
const setNewInfoMessage = (message) => {
setInfoMessage(message);
setRenderCount((prevCount) => prevCount + 1);
};
return [infoMessage, setNewInfoMessage];
};
export default useInfo;
-61
View File
@@ -1,61 +0,0 @@
import { useMemo } from 'react';
import { useClientsStates } from '@plebbit/plebbit-react-hooks';
const clientHosts = {};
const getClientHost = (clientUrl) => {
if (!clientHosts[clientUrl]) {
try {
clientHosts[clientUrl] = new URL(clientUrl).hostname || clientUrl;
} catch (e) {
clientHosts[clientUrl] = clientUrl;
}
}
return clientHosts[clientUrl];
};
const useStateString = (commentOrSubplebbit) => {
const { states } = useClientsStates({ comment: commentOrSubplebbit });
return useMemo(() => {
let stateString = '';
for (const state in states) {
const clientUrls = states[state];
const clientHosts = clientUrls.map((clientUrl) => getClientHost(clientUrl));
// if there are no valid hosts, skip this state
if (clientHosts.length === 0) {
continue;
}
// separate 2 different states using ' '
if (stateString) {
stateString += ', ';
}
// e.g. 'fetching IPFS from cloudflare-ipfs.com, ipfs.io'
const formattedState = state.replaceAll('-', ' ').replace('ipfs', 'IPFS').replace('ipns', 'IPNS');
stateString += `${formattedState} from ${clientHosts.join(', ')}`;
}
// fallback to comment or subplebbit state when possible
if (!stateString && commentOrSubplebbit?.state !== 'succeeded') {
if (commentOrSubplebbit?.publishingState && commentOrSubplebbit?.publishingState !== 'stopped' && commentOrSubplebbit?.publishingState !== 'succeeded') {
stateString = commentOrSubplebbit.publishingState;
} else if (commentOrSubplebbit?.updatingState !== 'stopped' && commentOrSubplebbit?.updatingState !== 'succeeded') {
stateString = commentOrSubplebbit.updatingState;
}
if (stateString) {
stateString = stateString.replaceAll('-', ' ').replace('ipfs', 'IPFS').replace('ipns', 'IPNS');
}
}
// capitalize first letter
if (stateString) {
stateString = stateString.charAt(0).toUpperCase() + stateString.slice(1);
}
// if string is empty, return undefined instead
return stateString === '' ? undefined : stateString;
}, [states, commentOrSubplebbit]);
};
export default useStateString;
-43
View File
@@ -1,43 +0,0 @@
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
const useSuccess = () => {
const [successMessage, setSuccessMessage] = useState('');
const [renderCount, setRenderCount] = useState(0);
useEffect(() => {
if (successMessage && successMessage.length > 0) {
const showSuccessToast = () => {
const toastId = toast.success(successMessage.toString(), {
position: 'top-right',
autoClose: 3000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
progress: undefined,
theme: 'dark',
});
return () => {
toast.dismiss(toastId);
};
};
const timeoutId = setTimeout(showSuccessToast, 500);
return () => {
clearTimeout(timeoutId);
};
}
}, [successMessage, renderCount]);
const setNewSuccessMessage = (message) => {
setSuccessMessage(message);
setRenderCount((prevCount) => prevCount + 1);
};
return [successMessage, setNewSuccessMessage];
};
export default useSuccess;
-16
View File
@@ -1,16 +0,0 @@
import { useState, useEffect } from 'react';
export default function useWindowWidth() {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWindowWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowWidth;
}
+13
View File
@@ -0,0 +1,13 @@
html, body {
max-width: 100%;
overflow-x: hidden;
}
body {
background-color: white;
}
body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, form, fieldset, input, p, blockquote, th, td, iframe {
margin: 0;
padding: 0;
}
-37
View File
@@ -1,37 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { HelmetProvider } from 'react-helmet-async';
import { HashRouter } from 'react-router-dom';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
import { App as CapacitorApp } from '@capacitor/app';
import PlebbitJs from '@plebbit/plebbit-js';
// inject native functions into renderer
// https://github.com/plebbit/plebbit-js/blob/master/docs/cross-platform-native-functions.md
if (window.plebbitJsNativeFunctions) {
PlebbitJs.setNativeFunctions(window.plebbitJsNativeFunctions);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<HelmetProvider>
<HashRouter>
<App />
</HashRouter>
</HelmetProvider>
</React.StrictMode>,
);
// set up PWA https://cra.link/PWA
serviceWorkerRegistration.register();
// add back button in android app
CapacitorApp.addListener('backButton', ({ canGoBack }) => {
if (canGoBack) {
window.history.back();
} else {
CapacitorApp.exitApp();
}
});
+28
View File
@@ -0,0 +1,28 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './app';
import { HashRouter as Router } from 'react-router-dom';
import './index.css';
import { App as CapacitorApp } from '@capacitor/app';
import * as serviceWorkerRegistration from './service-worker-registration';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>,
);
// set up PWA https://cra.link/PWA
serviceWorkerRegistration.register();
// add back button in android app
CapacitorApp.addListener('backButton', ({ canGoBack }) => {
if (canGoBack) {
window.history.back();
} else {
CapacitorApp.exitApp();
}
});
+8
View File
@@ -0,0 +1,8 @@
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}
declare module 'ext-name';
declare module 'lodash';
-19
View File
@@ -1,19 +0,0 @@
function countLinks(comment) {
let linkCount = 0;
if (comment.replyCount > 0) {
for (let reply of comment.replies.pages.topAll.comments) {
if (reply.link) {
linkCount++;
}
if (reply.replyCount > 0) {
linkCount += countLinks(reply);
}
}
}
return linkCount;
}
export default countLinks;
-18
View File
@@ -1,18 +0,0 @@
function findShortParentCid(parentCid, input) {
const feed = Array.isArray(input) ? input : [input];
for (const thread of feed) {
if (thread.cid === parentCid) {
return thread.shortCid;
}
if (thread.replyCount > 0 && thread.replies.pages.topAll.comments) {
const shortCid = findShortParentCid(parentCid, thread.replies.pages.topAll.comments);
if (shortCid) {
return shortCid;
}
}
}
return null;
}
export default findShortParentCid;
-78
View File
@@ -1,78 +0,0 @@
import extName from 'ext-name';
import { canEmbed } from '../components/Embed';
const getCommentMediaInfo = (comment) => {
if (!comment?.thumbnailUrl && !comment?.link) {
return;
}
if (comment?.link) {
try {
const url = new URL(comment.link);
const host = url.hostname;
let scrapedThumbnailUrl;
if (['youtube.com', 'www.youtube.com', 'youtu.be'].includes(host)) {
const videoId = host === 'youtu.be' ? url.pathname.slice(1) : url.searchParams.get('v');
scrapedThumbnailUrl = `https://img.youtube.com/vi/${videoId}/sddefault.jpg`;
} else if (host.includes('bitchute.com')) {
const videoId = url.pathname.split('/')[2];
scrapedThumbnailUrl = `https://static-3.bitchute.com/live/cover_images/F61vWF4shy8s/${videoId}_640x360.jpg`;
} else if (host.includes('streamable.com')) {
const videoId = url.pathname.split('/')[1];
scrapedThumbnailUrl = `https://cdn-cf-east.streamable.com/image/${videoId}.jpg`;
}
if (canEmbed(url)) {
return {
url: comment.link,
type: 'iframe',
thumbnail: comment.thumbnailUrl || scrapedThumbnailUrl,
};
}
const mime = extName(url.pathname.toLowerCase().replace('/', ''))[0]?.mime;
if (mime?.startsWith('image')) {
return {
url: comment.link,
type: 'image',
};
}
if (mime?.startsWith('video')) {
return {
url: comment.link,
type: 'video',
thumbnail: comment.thumbnailUrl,
};
}
if (mime?.startsWith('audio')) {
return {
url: comment.link,
type: 'audio',
};
}
} catch (error) {
return;
}
}
if (comment?.thumbnailUrl && comment?.thumbnailUrl !== comment?.link) {
return {
url: comment.link,
type: 'webpage',
thumbnail: comment.thumbnailUrl,
};
}
if (comment?.link) {
return {
url: comment.link,
type: 'webpage',
};
}
};
export default getCommentMediaInfo;
-35
View File
@@ -1,35 +0,0 @@
const getDate = (commentTimestamp) => {
if (commentTimestamp === undefined || isNaN(commentTimestamp)) {
return '';
}
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
const string = new Intl.DateTimeFormat(locale, {
hour12: false,
year: '2-digit',
month: '2-digit',
day: '2-digit',
weekday: 'short',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}).format(new Date(commentTimestamp * 1000));
if (locale.startsWith('ar')) {
return string;
}
const items = string.split(/,* /);
if (items.length === 3) {
const itemIsNumber = [items[0][0].match(/[0-9]/), items[1][0].match(/[0-9]/)];
if (itemIsNumber[0] && itemIsNumber[1]) {
return `${items[0]}(${items[2]})${items[1]}`;
}
if (itemIsNumber[0] && !itemIsNumber[1]) {
return `${items[0]}(${items[1]})${items[2]}`;
}
if (!itemIsNumber[0] && itemIsNumber[1]) {
return `${items[1]}(${items[0]})${items[2]}`;
}
}
return string;
};
export default getDate;
-44
View File
@@ -1,44 +0,0 @@
const pluralize = (unit, value) => {
return `${value} ${unit}${value > 1 ? 's' : ''}`;
};
const getFormattedTime = (timestamp) => {
try {
const currentTime = new Date().getTime();
const differenceInMilliseconds = currentTime - timestamp * 1000;
const years = Math.floor(differenceInMilliseconds / (1000 * 60 * 60 * 24 * 365.25));
const months = Math.floor((differenceInMilliseconds % (1000 * 60 * 60 * 24 * 365.25)) / (1000 * 60 * 60 * 24 * 30));
const days = Math.floor((differenceInMilliseconds % (1000 * 60 * 60 * 24 * 30)) / (1000 * 60 * 60 * 24));
const hours = Math.floor((differenceInMilliseconds % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((differenceInMilliseconds % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((differenceInMilliseconds % (1000 * 60)) / 1000);
if (years > 0) {
return months > 0 ? `${pluralize('year', years)} and ${pluralize('month', months)} ago` : `${pluralize('year', years)} ago`;
} else if (months > 0) {
return days > 0 ? `${pluralize('month', months)} and ${pluralize('day', days)} ago` : `${pluralize('month', months)} ago`;
} else if (days > 0) {
if (hours > 0) {
return `${pluralize('day', days)} and ${pluralize('hour', hours)} ago`;
} else if (minutes > 0) {
return `${pluralize('day', days)} and ${pluralize('minute', minutes)} ago`;
} else {
return `${pluralize('day', days)} ago`;
}
} else if (hours > 0) {
return minutes > 0 ? `${pluralize('hour', hours)} and ${pluralize('minute', minutes)} ago` : `${pluralize('hour', hours)} ago`;
} else if (minutes > 0) {
return seconds > 0 ? `${pluralize('minute', minutes)} and ${pluralize('second', seconds)} ago` : `${pluralize('minute', minutes)} ago`;
} else if (seconds > 30) {
return `${pluralize('second', seconds)} ago`;
} else {
return 'just now';
}
} catch (e) {
console.error('Error in getFormattedTime:', e);
return '[error]';
}
};
export default getFormattedTime;
-38
View File
@@ -1,38 +0,0 @@
function handleAddressClick(shortAddress) {
const isMobile = window.innerWidth <= 480;
const addressSelector = isMobile ? '.address-mobile' : '.address-desktop';
const postReplySelector = isMobile ? '.post-reply-mobile' : '.post-reply-desktop';
const opSelector = isMobile ? '.op-mobile' : '.op-desktop';
const matchingElements = [...document.querySelectorAll(postReplySelector + ',' + opSelector)].filter((el) => {
const addressElement = el.querySelector(addressSelector);
return addressElement && addressElement.textContent.includes(shortAddress);
});
if (matchingElements.length === 0) {
return;
}
const highlightedElements = document.querySelectorAll('.highlighted-address');
let allHighlighted = true;
matchingElements.forEach((el) => {
if (!el.classList.contains('highlighted-address')) {
allHighlighted = false;
}
});
highlightedElements.forEach((el) => {
el.classList.remove('highlighted-address');
});
if (!allHighlighted) {
matchingElements.forEach((el) => {
if (!el.classList.contains('op-mobile') && !el.classList.contains('op-desktop')) {
el.classList.add('highlighted-address');
}
});
}
}
export default handleAddressClick;
-12
View File
@@ -1,12 +0,0 @@
const handleImageClick = (e) => {
const image = e.target;
const container = image.closest('.img-container');
image.classList.toggle('enlarged');
if (container) {
container.classList.toggle('expanded-container');
}
};
export default handleImageClick;
-53
View File
@@ -1,53 +0,0 @@
function handleQuoteClick(reply, parentCid, threadCid) {
const cid = parentCid ? parentCid : reply.shortCid;
const isMobile = window.innerWidth <= 480;
const postNumberSelector = isMobile ? '.post-number-mobile' : '.post-number-desktop';
if (threadCid && cid === threadCid) {
const highlightedElements = document.querySelectorAll('.highlighted');
highlightedElements.forEach((el) => {
el.classList.remove('highlighted');
});
const opElementSelector = isMobile ? '.op-mobile' : '.op-desktop';
const opElement = [...document.querySelectorAll(opElementSelector)].find((el) => {
const postNumberElement = el.querySelector(postNumberSelector);
return postNumberElement && postNumberElement.innerHTML.includes(threadCid);
});
if (opElement) {
opElement.scrollIntoView({ behavior: 'auto', block: 'start' });
} else {
return;
}
return;
}
const targetElementSelector = isMobile ? '.post-reply-mobile, .op-mobile' : '.post-reply-desktop, .op-desktop';
const targetElement = [...document.querySelectorAll(targetElementSelector)].find((el) => {
const postNumberElement = el.querySelector(postNumberSelector);
return postNumberElement && postNumberElement.innerHTML.includes(cid);
});
if (targetElement) {
const highlightedElements = document.querySelectorAll('.highlighted-click');
highlightedElements.forEach((el) => {
el.classList.remove('highlighted-click');
});
targetElement.scrollIntoView({ behavior: 'auto', block: 'start' });
if (!targetElement.classList.contains('op-mobile') && !targetElement.classList.contains('op-desktop')) {
targetElement.classList.add('highlighted-click');
}
} else {
return;
}
}
export default handleQuoteClick;
-42
View File
@@ -1,42 +0,0 @@
function handleQuoteHover(reply, parentCid, onElementOutOfView) {
const cid = parentCid ? parentCid : reply.shortCid;
const isMobile = window.innerWidth <= 480;
const postNumberSelector = isMobile ? '.post-number-mobile' : '.post-number-desktop';
const targetElementSelector = isMobile ? '.post-reply-mobile, .op-mobile' : '.post-reply-desktop, .op-desktop';
const targetElement = [...document.querySelectorAll(targetElementSelector)].find((el) => {
const postNumberElement = el.querySelector(postNumberSelector);
return postNumberElement && postNumberElement.innerHTML.includes(cid);
});
if (targetElement) {
const isInViewport = (element) => {
const bounding = element.getBoundingClientRect();
return (
bounding.top >= 0 &&
bounding.left >= 0 &&
bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
);
};
if (isInViewport(targetElement)) {
const highlightedElements = document.querySelectorAll('.highlighted');
highlightedElements.forEach((el) => {
el.classList.remove('highlighted');
});
if (!targetElement.classList.contains('op-mobile') && !targetElement.classList.contains('op-desktop')) {
targetElement.classList.add('highlighted');
}
} else {
onElementOutOfView();
}
} else {
onElementOutOfView();
}
}
export default handleQuoteHover;
-26
View File
@@ -1,26 +0,0 @@
function handleShareClick(selectedAddress, cid) {
let shareLink;
if (cid === 'rules' || cid === 'description') {
shareLink = `https://plebchan.eth.limo/#/p/${selectedAddress}`;
} else {
const plebBzBaseURL = 'https://pleb.bz/p/';
shareLink = `${plebBzBaseURL}${selectedAddress}/`;
if (cid !== 'rules' && cid !== 'description') {
shareLink += `c/${cid}`;
}
shareLink += `?redirect=plebchan.eth.limo`;
}
if (navigator.clipboard) {
navigator.clipboard.writeText(shareLink).catch((err) => {
console.error('Could not copy text: ', err);
});
} else {
return;
}
}
export default handleShareClick;
-92
View File
@@ -1,92 +0,0 @@
import useGeneralStore from '../hooks/stores/useGeneralStore';
const handleStyleChange = (event) => {
const { setBodyStyle, setSelectedStyle } = useGeneralStore.getState();
switch (event.target.value) {
case 'Yotsuba':
const yotsubaBodyStyle = {
background: '#ffe url(assets/fade.png) top repeat-x',
color: 'maroon',
fontFamily: 'Arial, Helvetica, sans-serif',
};
setBodyStyle(yotsubaBodyStyle);
setSelectedStyle('Yotsuba');
localStorage.setItem('selectedStyle', 'Yotsuba');
localStorage.setItem('bodyStyle', JSON.stringify(yotsubaBodyStyle));
break;
case 'Yotsuba-B':
const yotsubaBBodyStyle = {
background: '#eef2ff url(assets/fade-blue.png) top center repeat-x',
color: '#000',
fontFamily: 'Arial, Helvetica, sans-serif',
};
setBodyStyle(yotsubaBBodyStyle);
setSelectedStyle('Yotsuba-B');
localStorage.setItem('selectedStyle', 'Yotsuba-B');
localStorage.setItem('bodyStyle', JSON.stringify(yotsubaBBodyStyle));
break;
case 'Futaba':
const futabaBodyStyle = {
background: '#ffe',
color: 'maroon',
fontFamily: 'times new roman, serif',
};
setBodyStyle(futabaBodyStyle);
setSelectedStyle('Futaba');
localStorage.setItem('selectedStyle', 'Futaba');
localStorage.setItem('bodyStyle', JSON.stringify(futabaBodyStyle));
break;
case 'Burichan':
const burichanBodyStyle = {
background: '#eef2ff',
color: '#000',
fontFamily: 'times new roman, serif',
};
setBodyStyle(burichanBodyStyle);
setSelectedStyle('Burichan');
localStorage.setItem('selectedStyle', 'Burichan');
localStorage.setItem('bodyStyle', JSON.stringify(burichanBodyStyle));
break;
case 'Tomorrow':
const tomorrowBodyStyle = {
background: '#1d1f21 none',
color: '#c5c8c6',
fontFamily: 'Arial, Helvetica, sans-serif',
};
setBodyStyle(tomorrowBodyStyle);
setSelectedStyle('Tomorrow');
localStorage.setItem('selectedStyle', 'Tomorrow');
localStorage.setItem('bodyStyle', JSON.stringify(tomorrowBodyStyle));
break;
case 'Photon':
const photonBodyStyle = {
background: '#eee none',
color: '#333',
fontFamily: 'Arial, Helvetica, sans-serif',
};
setBodyStyle(photonBodyStyle);
setSelectedStyle('Photon');
localStorage.setItem('selectedStyle', 'Photon');
localStorage.setItem('bodyStyle', JSON.stringify(photonBodyStyle));
break;
default:
const defaultBodyStyle = {
background: '#ffe url(assets/fade.png) top repeat-x',
color: 'maroon',
fontFamily: 'Arial, Helvetica, sans-serif',
};
setBodyStyle(defaultBodyStyle);
setSelectedStyle('Yotsuba');
localStorage.setItem('selectedStyle', 'Yotsuba');
localStorage.setItem('bodyStyle', JSON.stringify(defaultBodyStyle));
}
};
export default handleStyleChange;
-10
View File
@@ -1,10 +0,0 @@
const isValidUrl = (url) => {
try {
new URL(url);
return true;
} catch (e) {
return false;
}
};
export default isValidUrl;
-8
View File
@@ -1,8 +0,0 @@
const preloadImages = (imageUrls) => {
imageUrls.forEach((imageUrl) => {
const img = new Image();
img.src = imageUrl;
});
};
export default preloadImages;
-9
View File
@@ -1,9 +0,0 @@
function removeHighlight() {
const highlightedElements = document.querySelectorAll('.highlighted');
highlightedElements.forEach((el) => {
el.classList.remove('highlighted');
});
}
export default removeHighlight;
-29
View File
@@ -1,29 +0,0 @@
export const showNewVersionToast = async (setNewInfoMessage, isElectron, packageJson, commitRef) => {
try {
const packageRes = await fetch('https://raw.githubusercontent.com/plebbit/plebchan/master/package.json', { cache: 'no-cache' });
const packageData = await packageRes.json();
if (packageJson.version !== packageData.version) {
const newVersionInfo = isElectron
? `New version available, plebchan v${packageData.version}. You are using v${packageJson.version}. Download the latest version here: https://github.com/plebbit/plebchan/releases/latest`
: `New version available, plebchan v${packageData.version}. You are using v${packageJson.version}. Refresh to update.`;
setNewInfoMessage(newVersionInfo);
}
if (commitRef.length > 0) {
const commitRes = await fetch('https://api.github.com/repos/plebbit/plebchan/commits?per_page=1&sha=development', { cache: 'no-cache' });
const commitData = await commitRes.json();
const latestCommitHash = commitData[0].sha;
if (latestCommitHash.trim() !== commitRef.trim()) {
const newVersionInfo = `New dev version available, commit ${latestCommitHash.slice(0, 7)}. You are using commit ${commitRef.slice(0, 7)}. Refresh to update.`;
setNewInfoMessage(newVersionInfo);
}
}
} catch (error) {
console.error('Failed to fetch latest version info:', error);
}
};
export default showNewVersionToast;
+3
View File
@@ -0,0 +1,3 @@
.test {
font-size: 20px;
}
+7
View File
@@ -0,0 +1,7 @@
import styles from './home.module.css';
const Home = () => {
return <div className={styles.test}>plebchan</div>;
};
export default Home;
+1
View File
@@ -0,0 +1 @@
export { default } from './home';
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2015",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}
+2343 -2252
View File
File diff suppressed because it is too large Load Diff