mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
perf: rewrite plebchan completely
This commit is contained in:
+136
-9
@@ -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
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"appId": "plebchan.android",
|
||||
"appName": "plebchan",
|
||||
"webDir": "build",
|
||||
"bundledWebRuntime": false
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.app {
|
||||
z-index: 1;
|
||||
min-height: 100%;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
+15
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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, ' \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);
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
<span className='poster-address address-desktop' id='reply-button' style={{ cursor: 'pointer' }}>
|
||||
(u/
|
||||
{reply.author?.shortAddress ? <span>{reply.author?.shortAddress}</span> : <span>{account?.author?.shortAddress}</span>})
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className='date-time' data-utc='data'>
|
||||
{getDate(reply.timestamp)}
|
||||
</span>
|
||||
|
||||
<span className='post-number post-number-desktop'>
|
||||
<span>c/</span>
|
||||
<Link to={() => {}} id='reply-button' title='Reply to this post'>
|
||||
{reply.shortCid}
|
||||
</Link>
|
||||
</span>
|
||||
|
||||
<div id='backlink-id' className='backlink'>
|
||||
{reply.replies?.pages?.topAll.comments
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
.map((reply, index) => (
|
||||
<div key={`div-${index}`} style={{ display: 'inline-block' }}>
|
||||
<Link key={`link-${index}`} to={() => {}} className='quote-link'>
|
||||
c/{reply.shortCid}
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{replyMediaInfo?.url ? (
|
||||
<div className='file' style={{ marginBottom: '5px' }}>
|
||||
<div className='reply-file-text'>
|
||||
Link:
|
||||
<a href={replyMediaInfo.url} target='_blank' rel='noopener noreferrer'>
|
||||
{replyMediaInfo?.url.length > 30 ? replyMediaInfo?.url.slice(0, 30) + '(...)' : replyMediaInfo?.url}
|
||||
</a>
|
||||
{replyMediaInfo?.type === 'iframe' ? null : ` (${replyMediaInfo?.type})`}
|
||||
</div>
|
||||
{replyMediaInfo?.type === 'iframe' && (
|
||||
<div className='img-container'>
|
||||
<span className='file-thumb-reply'>
|
||||
{replyMediaInfo.thumbnail && <img src={replyMediaInfo.thumbnail} alt='thumbnail' onError={(e) => (e.target.src = fallbackImgUrl)} />}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{replyMediaInfo?.type === 'webpage' ? (
|
||||
<div className='img-container'>
|
||||
<span className='file-thumb-reply'>
|
||||
{reply.thumbnailUrl ? (
|
||||
<img
|
||||
src={replyMediaInfo.thumbnail}
|
||||
alt={replyMediaInfo.type}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onError={(e) => (e.target.src = fallbackImgUrl)}
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{replyMediaInfo?.type === 'image' ? (
|
||||
<div className='img-container'>
|
||||
<span className='file-thumb-reply'>
|
||||
<img src={replyMediaInfo.url} alt={replyMediaInfo.type} style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{replyMediaInfo?.type === 'video' ? (
|
||||
<span className='file-thumb-reply'>
|
||||
<video controls src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
</span>
|
||||
) : null}
|
||||
{replyMediaInfo?.type === 'audio' ? (
|
||||
<span className='file-thumb-reply'>
|
||||
<audio controls src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{reply.content ? (
|
||||
reply.content?.length > 500 ? (
|
||||
<Fragment>
|
||||
<blockquote comment={reply} className='post-message'>
|
||||
{shortParentCid ? (
|
||||
<Link to={() => {}} className='quotelink'>
|
||||
{`c/${shortParentCid}`}
|
||||
{shortParentCid === thread.shortCid ? ' (OP)' : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<Post content={reply.content?.slice(0, 500)} />
|
||||
<span className='ttl'>
|
||||
{' '}
|
||||
(...)
|
||||
<br /> <EditLabel commentCid={reply.cid} className='ttl' />
|
||||
<br />
|
||||
Comment too long. Click the link to view.
|
||||
</span>
|
||||
</blockquote>
|
||||
</Fragment>
|
||||
) : (
|
||||
<blockquote className='post-message'>
|
||||
{shortParentCid ? (
|
||||
<Link to={() => {}} className='quotelink'>
|
||||
{`c/${shortParentCid}`}
|
||||
{shortParentCid === thread.shortCid ? ' (OP)' : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<Post content={reply.content} comment={reply} />
|
||||
<EditLabel commentCid={reply.cid} className='ttl' />
|
||||
</blockquote>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='reply-container'>
|
||||
<span className='ellipsis'>{stateString}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='thread-mobile'>
|
||||
{reply.state === 'succeeded' ? (
|
||||
<div className='reply-container'>
|
||||
<div className='post-reply post-reply-mobile'>
|
||||
<div className='post-info-mobile'>
|
||||
<span className='name-block-mobile'>
|
||||
{reply.author?.displayName ? (
|
||||
reply.author?.displayName.length > 20 ? (
|
||||
<Fragment>
|
||||
<span className='name-mobile'>{reply.author?.displayName.slice(0, 20) + ' (...)'}</span>
|
||||
</Fragment>
|
||||
) : (
|
||||
<span className='name-mobile'>{reply.author?.displayName}</span>
|
||||
)
|
||||
) : (
|
||||
<span className='name-mobile'>Anonymous</span>
|
||||
)}
|
||||
|
||||
<span className='poster-address-mobile address-mobile' id='reply-button' style={{ cursor: 'pointer' }}>
|
||||
(u/
|
||||
{reply.author?.shortAddress ? (
|
||||
<span className='highlight-address-mobile'>{reply.author?.shortAddress}</span>
|
||||
) : (
|
||||
<span>{account?.author?.shortAddress}</span>
|
||||
)}
|
||||
)
|
||||
</span>
|
||||
<br />
|
||||
</span>
|
||||
<span className='date-time-mobile post-number-mobile'>
|
||||
{getDate(reply.timestamp)}
|
||||
<span>c/</span>
|
||||
<Link to={() => {}} id='reply-button'>
|
||||
{reply.shortCid}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
{reply.link ? (
|
||||
<div className='file-mobile'>
|
||||
{replyMediaInfo?.url ? (
|
||||
replyMediaInfo.type === 'webpage' ? (
|
||||
<div className='img-container'>
|
||||
<span className='file-thumb-mobile'>
|
||||
{reply.thumbnailUrl ? (
|
||||
<img src={replyMediaInfo.thumbnail} alt='thumbnail' style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
) : null}
|
||||
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
|
||||
</span>
|
||||
</div>
|
||||
) : replyMediaInfo.type === 'image' ? (
|
||||
<div className='img-container'>
|
||||
<span className='file-thumb-mobile'>
|
||||
<img src={replyMediaInfo.url} alt={replyMediaInfo.type} style={{ cursor: 'pointer' }} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
|
||||
</span>
|
||||
</div>
|
||||
) : replyMediaInfo.type === 'video' ? (
|
||||
<span className='file-thumb-mobile'>
|
||||
<video
|
||||
src={replyMediaInfo.url}
|
||||
alt={replyMediaInfo.type}
|
||||
style={{ pointerEvents: 'none' }}
|
||||
onError={(e) => (e.target.src = fallbackImgUrl)}
|
||||
/>
|
||||
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
|
||||
</span>
|
||||
) : replyMediaInfo.type === 'audio' ? (
|
||||
<span className='file-thumb-mobile'>
|
||||
<audio src={replyMediaInfo.url} alt={replyMediaInfo.type} onError={(e) => (e.target.src = fallbackImgUrl)} />
|
||||
<div className='file-info-mobile'>{replyMediaInfo.type}</div>
|
||||
</span>
|
||||
) : null
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{reply.content ? (
|
||||
reply.content?.length > 500 ? (
|
||||
<Fragment>
|
||||
<blockquote className='post-message'>
|
||||
{shortParentCid ? (
|
||||
<Link to={() => {}} className='quotelink'>
|
||||
{`c/${shortParentCid}`}
|
||||
{shortParentCid === thread.shortCid ? ' (OP)' : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<Post content={reply.content?.slice(0, 500)} comment={reply} />
|
||||
<span className='ttl'>
|
||||
{' '}
|
||||
(...)
|
||||
<br />
|
||||
<EditLabel commentCid={reply.cid} className='ttl' />
|
||||
<br />
|
||||
Comment too long. Click the link to view.{' '}
|
||||
</span>
|
||||
</blockquote>
|
||||
</Fragment>
|
||||
) : (
|
||||
<blockquote className='post-message'>
|
||||
{shortParentCid ? (
|
||||
<Link to={() => {}} className='quotelink'>
|
||||
{`c/${shortParentCid}`}
|
||||
{shortParentCid === thread.shortCid ? ' (OP)' : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<Post content={reply.content} comment={reply} />
|
||||
<EditLabel commentCid={reply.cid} className='ttl' />
|
||||
</blockquote>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='reply-container'>
|
||||
<span className='ellipsis'>{stateString}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BoardForm>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostOnHover;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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} -
|
||||
{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();
|
||||
}}
|
||||
/>
|
||||
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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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 '';
|
||||
}
|
||||
}}
|
||||
`;
|
||||
@@ -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
@@ -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>
|
||||
] [
|
||||
{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>
|
||||
|
||||
<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>
|
||||
|
||||
<Link
|
||||
to='/'
|
||||
onClick={() => {
|
||||
handleStyleChange({ target: { value: 'Yotsuba' } });
|
||||
}}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id='separator-mobile'> </div>
|
||||
<div id='separator-mobile'> </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:
|
||||
<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>
|
||||
(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>
|
||||
|
||||
<span
|
||||
className='name capcode'
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setIsAdminListOpen(!isAdminListOpen);
|
||||
}}
|
||||
>
|
||||
## Board Mods
|
||||
</span>
|
||||
|
||||
<span className='date-time'>{getDate(subplebbit.createdAt)}</span>
|
||||
|
||||
<img src='assets/sticky.gif' alt='Sticky' title='Sticky' style={{ marginBottom: '-1px', imageRendering: 'pixelated' }} />
|
||||
|
||||
<img src='assets/closed.gif' alt='Closed' title='Closed' style={{ marginBottom: '-1px', imageRendering: 'pixelated' }} />
|
||||
<span>
|
||||
[
|
||||
<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>
|
||||
|
||||
<span className='thread-icons-mobile'>
|
||||
<img
|
||||
src='assets/sticky.gif'
|
||||
alt='Sticky'
|
||||
title='Sticky'
|
||||
style={{ all: 'unset', marginBottom: '-2px', paddingLeft: '2px', imageRendering: 'pixelated' }}
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
</span>
|
||||
<span className='date-time-mobile post-number-mobile'>{getDate(subplebbit.createdAt)}</span>
|
||||
|
||||
</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>
|
||||
]
|
||||
</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>
|
||||
•
|
||||
<a style={{ textDecoration: 'underline' }} href='https://github.com/plebbit/plebchan/releases/latest' target='_blank' rel='noopener noreferrer'>
|
||||
App
|
||||
</a>
|
||||
•
|
||||
<a style={{ textDecoration: 'underline' }} href='https://twitter.com/plebchan_eth' target='_blank' rel='noopener noreferrer'>
|
||||
Twitter
|
||||
</a>
|
||||
•
|
||||
<a style={{ textDecoration: 'underline' }} href='https://t.me/plebbit' target='_blank' rel='noopener noreferrer'>
|
||||
Telegram
|
||||
</a>
|
||||
</div>
|
||||
</Footer>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Description;
|
||||
@@ -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>
|
||||
|
||||
<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>
|
||||
|
||||
</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>
|
||||
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
</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>
|
||||
|
||||
</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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
] [
|
||||
{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>
|
||||
|
||||
<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>
|
||||
|
||||
<Link
|
||||
to='/'
|
||||
onClick={() => {
|
||||
handleStyleChange({ target: { value: 'Yotsuba' } });
|
||||
window.scrollTo(0, 0);
|
||||
}}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id='separator-mobile'> </div>
|
||||
<div id='separator-mobile'> </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> </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:
|
||||
<a key={`fa-${index}`} href={commentMediaInfo.url} target='_blank' rel='noopener noreferrer'>
|
||||
{commentMediaInfo?.url.length > 30 ? commentMediaInfo?.url.slice(0, 30) + '(...)' : commentMediaInfo?.url}
|
||||
</a>
|
||||
({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}
|
||||
|
||||
{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>
|
||||
)}
|
||||
|
||||
<span className='poster-address'>(u/{account?.author?.shortAddress})</span>
|
||||
|
||||
<span className='date-time' data-utc='data'>
|
||||
{getDate(comment?.timestamp)}
|
||||
</span>
|
||||
|
||||
<span className='post-number'>
|
||||
<Link to='' title='Link to this post'>
|
||||
c/
|
||||
</Link>
|
||||
<span key='pending' style={{ color: 'red', fontWeight: '700' }}>
|
||||
Pending
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<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>
|
||||
)}
|
||||
|
||||
<span key={`mob-pa-${'pending'}`} className='poster-address-mobile'>
|
||||
(u/{account?.author?.shortAddress})
|
||||
</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)}
|
||||
|
||||
<button id='reply-button' style={{ all: 'unset' }} key={`mob-no-${'pending'}`}>
|
||||
c/
|
||||
</button>
|
||||
|
||||
<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;
|
||||
@@ -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>
|
||||
] [
|
||||
{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>
|
||||
|
||||
<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>
|
||||
|
||||
<Link
|
||||
to='/'
|
||||
onClick={() => {
|
||||
handleStyleChange({ target: { value: 'Yotsuba' } });
|
||||
}}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id='separator-mobile'> </div>
|
||||
<div id='separator-mobile'> </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>
|
||||
|
||||
<span
|
||||
className='name capcode'
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setIsAdminListOpen(!isAdminListOpen);
|
||||
}}
|
||||
>
|
||||
## Board Mods
|
||||
</span>
|
||||
|
||||
<span className='date-time'>{getDate(subplebbit.createdAt)}</span>
|
||||
|
||||
<img src='assets/sticky.gif' alt='Sticky' title='Sticky' style={{ marginBottom: '-1px', imageRendering: 'pixelated' }} />
|
||||
|
||||
<img src='assets/closed.gif' alt='Closed' title='Closed' style={{ marginBottom: '-1px', imageRendering: 'pixelated' }} />
|
||||
<span>
|
||||
[
|
||||
<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>
|
||||
|
||||
<span className='thread-icons-mobile'>
|
||||
<img
|
||||
src='assets/sticky.gif'
|
||||
alt='Sticky'
|
||||
title='Sticky'
|
||||
style={{ all: 'unset', marginBottom: '-2px', paddingLeft: '2px', imageRendering: 'pixelated' }}
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
</span>
|
||||
<span className='date-time-mobile post-number-mobile'>{getDate(subplebbit.createdAt)}</span>
|
||||
|
||||
</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>
|
||||
]
|
||||
</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>
|
||||
•
|
||||
<a style={{ textDecoration: 'underline' }} href='https://github.com/plebbit/plebchan/releases/latest' target='_blank' rel='noopener noreferrer'>
|
||||
App
|
||||
</a>
|
||||
•
|
||||
<a style={{ textDecoration: 'underline' }} href='https://twitter.com/plebchan_eth' target='_blank' rel='noopener noreferrer'>
|
||||
Twitter
|
||||
</a>
|
||||
•
|
||||
<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
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
STICKY_MENU_SCROLL_LISTENER: boolean;
|
||||
isElectron: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
declare module '*.module.css' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module 'ext-name';
|
||||
|
||||
declare module 'lodash';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,10 +0,0 @@
|
||||
const isValidUrl = (url) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export default isValidUrl;
|
||||
@@ -1,8 +0,0 @@
|
||||
const preloadImages = (imageUrls) => {
|
||||
imageUrls.forEach((imageUrl) => {
|
||||
const img = new Image();
|
||||
img.src = imageUrl;
|
||||
});
|
||||
};
|
||||
|
||||
export default preloadImages;
|
||||
@@ -1,9 +0,0 @@
|
||||
function removeHighlight() {
|
||||
const highlightedElements = document.querySelectorAll('.highlighted');
|
||||
|
||||
highlightedElements.forEach((el) => {
|
||||
el.classList.remove('highlighted');
|
||||
});
|
||||
}
|
||||
|
||||
export default removeHighlight;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,3 @@
|
||||
.test {
|
||||
font-size: 20px;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import styles from './home.module.css';
|
||||
|
||||
const Home = () => {
|
||||
return <div className={styles.test}>plebchan</div>;
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './home';
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user