style: run prettier

This commit is contained in:
Esteban Abaroa
2023-12-16 16:49:26 +00:00
parent 0edc2613c8
commit 9dd2e3ce73
17 changed files with 437 additions and 578 deletions
+1 -4
View File
@@ -11,10 +11,7 @@ const addPortableToPortableExecutableFileName = () => {
for (const file of files) {
if (file.endsWith('.exe') && !file.match('Setup')) {
const filePath = path.resolve(distFolderPath, file);
const renamedFilePath = path.resolve(
distFolderPath,
file.replace('plebchan', 'plebchan Portable')
);
const renamedFilePath = path.resolve(distFolderPath, file.replace('plebchan', 'plebchan Portable'));
fs.moveSync(filePath, renamedFilePath);
}
}
+123 -144
View File
@@ -1,52 +1,40 @@
require('./log')
const {
app,
BrowserWindow,
Menu,
MenuItem,
Tray,
screen: electronScreen,
shell,
dialog
} = require('electron')
const isDev = require('electron-is-dev')
const path = require('path')
const startIpfs = require('./start-ipfs')
const startPlebbitRpcServer = require('./start-plebbit-rpc')
const { URL } = require('node:url')
const tcpPortUsed = require('tcp-port-used')
require('./log');
const { app, BrowserWindow, Menu, MenuItem, Tray, screen: electronScreen, shell, dialog } = require('electron');
const isDev = require('electron-is-dev');
const path = require('path');
const startIpfs = require('./start-ipfs');
const startPlebbitRpcServer = require('./start-plebbit-rpc');
const { URL } = require('node:url');
const tcpPortUsed = require('tcp-port-used');
// retry starting ipfs every 10 second,
// retry starting ipfs every 10 second,
// in case it was started by another client that shut down and shut down ipfs with it
let startIpfsError
let startIpfsError;
setInterval(async () => {
try {
const started = await tcpPortUsed.check(5001, '127.0.0.1')
const started = await tcpPortUsed.check(5001, '127.0.0.1');
if (started) {
return
return;
}
await startIpfs()
await startIpfs();
} catch (e) {
console.log(e);
startIpfsError = e;
dialog.showErrorBox('IPFS error', startIpfsError.message);
}
catch (e) {
console.log(e)
startIpfsError = e
dialog.showErrorBox('IPFS error', startIpfsError.message)
}
}, 10000)
}, 10000);
// use common user agent instead of electron so img, video, audio, iframe elements don't get blocked
// https://www.whatismybrowser.com/guides/the-latest-version/chrome
// https://www.whatismybrowser.com/guides/the-latest-user-agent/chrome
// NOTE: eventually should probably fake sec-ch-ua header as well
let fakeUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'
if (process.platform === 'darwin')
fakeUserAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'
if (process.platform === 'linux')
fakeUserAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'
const realUserAgent = `plebchan/${require('../package.json').version}`
let fakeUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36';
if (process.platform === 'darwin') fakeUserAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36';
if (process.platform === 'linux') fakeUserAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36';
const realUserAgent = `plebchan/${require('../package.json').version}`;
// add right click menu
const contextMenu = require('electron-context-menu')
const contextMenu = require('electron-context-menu');
contextMenu({
// prepend custom buttons to top
prepend: (defaultActions, parameters, browserWindow) => [
@@ -76,7 +64,7 @@ contextMenu({
showInspectElement: true,
showServices: false,
showSearchWithGoogle: false,
})
});
const createMainWindow = () => {
let mainWindow = new BrowserWindow({
@@ -91,199 +79,192 @@ const createMainWindow = () => {
devTools: true, // TODO: change to isDev when no bugs left
preload: path.join(__dirname, 'preload.js'),
},
})
});
// set fake user agent
mainWindow.webContents.userAgent = fakeUserAgent
mainWindow.webContents.userAgent = fakeUserAgent;
// set custom user agent and other headers for window.fetch requests to prevent origin errors
mainWindow.webContents.session.webRequest.onBeforeSendHeaders({urls: ['*://*/*']}, (details, callback) => {
const isIframe = !!details.frame?.parent
mainWindow.webContents.session.webRequest.onBeforeSendHeaders({ urls: ['*://*/*'] }, (details, callback) => {
const isIframe = !!details.frame?.parent;
// if not a fetch request (or fetch request is from within iframe), do nothing, filtering webRequest by types doesn't seem to work
if (details.resourceType !== 'xhr' || isIframe) {
return callback({requestHeaders: details.requestHeaders})
return callback({ requestHeaders: details.requestHeaders });
}
// add privacy
details.requestHeaders['User-Agent'] = realUserAgent
details.requestHeaders['sec-ch-ua'] = undefined
details.requestHeaders['sec-ch-ua-platform'] = undefined
details.requestHeaders['sec-ch-ua-mobile'] = undefined
details.requestHeaders['Sec-Fetch-Dest'] = undefined
details.requestHeaders['Sec-Fetch-Mode'] = undefined
details.requestHeaders['Sec-Fetch-Site'] = undefined
details.requestHeaders['User-Agent'] = realUserAgent;
details.requestHeaders['sec-ch-ua'] = undefined;
details.requestHeaders['sec-ch-ua-platform'] = undefined;
details.requestHeaders['sec-ch-ua-mobile'] = undefined;
details.requestHeaders['Sec-Fetch-Dest'] = undefined;
details.requestHeaders['Sec-Fetch-Mode'] = undefined;
details.requestHeaders['Sec-Fetch-Site'] = undefined;
// prevent origin errors
details.requestHeaders['Origin'] = undefined
callback({requestHeaders: details.requestHeaders})
})
details.requestHeaders['Origin'] = undefined;
callback({ requestHeaders: details.requestHeaders });
});
// fix cors errors for window.fetch. must not be enabled for iframe or can cause remote code execution
mainWindow.webContents.session.webRequest.onHeadersReceived({urls: ['*://*/*']}, (details, callback) => {
const isIframe = !!details.frame?.parent
mainWindow.webContents.session.webRequest.onHeadersReceived({ urls: ['*://*/*'] }, (details, callback) => {
const isIframe = !!details.frame?.parent;
// if not a fetch request (or fetch request is from within iframe), do nothing, filtering webRequest by types doesn't seem to work
if (details.resourceType !== 'xhr' || isIframe) {
return callback({responseHeaders: details.responseHeaders})
return callback({ responseHeaders: details.responseHeaders });
}
// must delete lower case headers or both '*, *' could get added
delete details.responseHeaders['access-control-allow-origin']
delete details.responseHeaders['access-control-allow-headers']
delete details.responseHeaders['access-control-allow-methods']
delete details.responseHeaders['access-control-expose-headers']
details.responseHeaders['Access-Control-Allow-Origin'] = '*'
details.responseHeaders['Access-Control-Allow-Headers'] = '*'
details.responseHeaders['Access-Control-Allow-Methods'] = '*'
details.responseHeaders['Access-Control-Expose-Headers'] = '*'
callback({responseHeaders: details.responseHeaders})
})
delete details.responseHeaders['access-control-allow-origin'];
delete details.responseHeaders['access-control-allow-headers'];
delete details.responseHeaders['access-control-allow-methods'];
delete details.responseHeaders['access-control-expose-headers'];
details.responseHeaders['Access-Control-Allow-Origin'] = '*';
details.responseHeaders['Access-Control-Allow-Headers'] = '*';
details.responseHeaders['Access-Control-Allow-Methods'] = '*';
details.responseHeaders['Access-Control-Expose-Headers'] = '*';
callback({ responseHeaders: details.responseHeaders });
});
const startURL = isDev
? 'http://localhost:3000'
: `file://${path.join(__dirname, '../build/index.html')}`
const startURL = isDev ? 'http://localhost:3000' : `file://${path.join(__dirname, '../build/index.html')}`;
mainWindow.loadURL(startURL)
mainWindow.loadURL(startURL);
mainWindow.once('ready-to-show', async () => {
// make sure back button is disabled on launch
mainWindow.webContents.clearHistory()
mainWindow.webContents.clearHistory();
mainWindow.show()
mainWindow.show();
if (isDev) {
mainWindow.openDevTools()
mainWindow.openDevTools();
}
if (startIpfsError) {
dialog.showErrorBox('IPFS error', startIpfsError.message)
dialog.showErrorBox('IPFS error', startIpfsError.message);
}
})
});
mainWindow.on('closed', () => {
mainWindow = null
})
mainWindow = null;
});
// don't open new windows
mainWindow.webContents.on('new-window', (event, url) => {
event.preventDefault()
mainWindow.loadURL(url)
})
event.preventDefault();
mainWindow.loadURL(url);
});
// open links in external browser
// do not open links in plebchan or will lead to remote execution
mainWindow.webContents.on('will-navigate', (e, originalUrl) => {
if (originalUrl != mainWindow.webContents.getURL()) {
e.preventDefault()
e.preventDefault();
try {
// do not let the user open any url with shell.openExternal
// or it will lead to remote execution https://benjamin-altpeter.de/shell-openexternal-dangers/
// only open valid https urls to prevent remote execution
// will throw if url isn't valid
const validatedUrl = new URL(originalUrl)
let serializedUrl = ''
const validatedUrl = new URL(originalUrl);
let serializedUrl = '';
// make an exception for ipfs stats
if (validatedUrl.toString() === 'http://localhost:5001/webui/') {
serializedUrl = validatedUrl.toString()
serializedUrl = validatedUrl.toString();
} else if (validatedUrl.protocol === 'https:') {
// open serialized url to prevent remote execution
serializedUrl = validatedUrl.toString()
serializedUrl = validatedUrl.toString();
} else {
throw Error(`can't open url '${originalUrl}', it's not https and not the allowed http exception`)
throw Error(`can't open url '${originalUrl}', it's not https and not the allowed http exception`);
}
shell.openExternal(serializedUrl)
shell.openExternal(serializedUrl);
} catch (e) {
console.warn(e)
console.warn(e);
}
}
})
});
// open links (with target="_blank") in external browser
// do not open links in plebchan or will lead to remote execution
mainWindow.webContents.setWindowOpenHandler(({url}) => {
const originalUrl = url
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
const originalUrl = url;
try {
// do not let the user open any url with shell.openExternal
// or it will lead to remote execution https://benjamin-altpeter.de/shell-openexternal-dangers/
// only open valid https urls to prevent remote execution
// will throw if url isn't valid
const validatedUrl = new URL(originalUrl)
let serializedUrl = ''
const validatedUrl = new URL(originalUrl);
let serializedUrl = '';
// make an exception for ipfs stats
if (validatedUrl.toString() === 'http://localhost:5001/webui/') {
serializedUrl = validatedUrl.toString()
serializedUrl = validatedUrl.toString();
} else if (validatedUrl.protocol === 'https:') {
// open serialized url to prevent remote execution
serializedUrl = validatedUrl.toString()
serializedUrl = validatedUrl.toString();
} else {
throw Error(`can't open url '${originalUrl}', it's not https and not the allowed http exception`)
throw Error(`can't open url '${originalUrl}', it's not https and not the allowed http exception`);
}
shell.openExternal(serializedUrl)
shell.openExternal(serializedUrl);
} catch (e) {
console.warn(e)
console.warn(e);
}
return {action: 'deny'}
})
return { action: 'deny' };
});
// deny permissions like location, notifications, etc https://www.electronjs.org/docs/latest/tutorial/security#5-handle-session-permission-requests-from-remote-content
mainWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
// deny all permissions
return callback(false)
})
return callback(false);
});
// deny attaching webview https://www.electronjs.org/docs/latest/tutorial/security#12-verify-webview-options-before-creation
mainWindow.webContents.on('will-attach-webview', (e, webPreferences, params) => {
// deny all
e.preventDefault()
})
e.preventDefault();
});
if (process.platform !== 'darwin') {
// tray
const trayIconPath = path.join(
__dirname,
'..',
isDev ? 'public' : 'build',
'electron-tray-icon.png'
)
const tray = new Tray(trayIconPath)
tray.setToolTip('plebchan')
const trayIconPath = path.join(__dirname, '..', isDev ? 'public' : 'build', 'electron-tray-icon.png');
const tray = new Tray(trayIconPath);
tray.setToolTip('plebchan');
const trayMenu = Menu.buildFromTemplate([
{
label: 'Open plebchan',
click: () => {
mainWindow.show()
mainWindow.show();
},
},
{
label: 'Quit plebchan',
click: () => {
mainWindow.destroy()
app.quit()
mainWindow.destroy();
app.quit();
},
},
])
tray.setContextMenu(trayMenu)
]);
tray.setContextMenu(trayMenu);
// show/hide on tray right click
tray.on('right-click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
})
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
});
// close to tray
if (!isDev) {
let isQuiting = false
let isQuiting = false;
app.on('before-quit', () => {
isQuiting = true
})
isQuiting = true;
});
mainWindow.on('close', (event) => {
if (!isQuiting) {
event.preventDefault()
mainWindow.hide()
event.returnValue = false
event.preventDefault();
mainWindow.hide();
event.returnValue = false;
}
})
});
}
}
@@ -291,48 +272,46 @@ const createMainWindow = () => {
label: '←',
enabled: mainWindow?.webContents?.canGoBack(),
click: () => mainWindow?.webContents?.goBack(),
})
});
const appMenuForward = new MenuItem({
label: '→',
enabled: mainWindow?.webContents?.canGoForward(),
click: () => mainWindow?.webContents?.goForward(),
})
});
const appMenuReload = new MenuItem({
label: '⟳',
role: 'reload',
click: () => mainWindow?.webContents?.reload(),
})
});
// application menu
// hide useless electron help menu
if (process.platform === 'darwin') {
const appMenu = Menu.getApplicationMenu()
appMenu.insert(1, appMenuBack)
appMenu.insert(2, appMenuForward)
appMenu.insert(3, appMenuReload)
Menu.setApplicationMenu(appMenu)
const appMenu = Menu.getApplicationMenu();
appMenu.insert(1, appMenuBack);
appMenu.insert(2, appMenuForward);
appMenu.insert(3, appMenuReload);
Menu.setApplicationMenu(appMenu);
} else {
// Other platforms
const originalAppMenuWithoutHelp = Menu.getApplicationMenu()?.items.filter(
(item) => item.role !== 'help'
)
const appMenu = [appMenuBack, appMenuForward, appMenuReload, ...originalAppMenuWithoutHelp]
Menu.setApplicationMenu(Menu.buildFromTemplate(appMenu))
const originalAppMenuWithoutHelp = Menu.getApplicationMenu()?.items.filter((item) => item.role !== 'help');
const appMenu = [appMenuBack, appMenuForward, appMenuReload, ...originalAppMenuWithoutHelp];
Menu.setApplicationMenu(Menu.buildFromTemplate(appMenu));
}
}
};
app.whenReady().then(() => {
createMainWindow()
createMainWindow();
app.on('activate', () => {
if (!BrowserWindow.getAllWindows().length) {
createMainWindow()
createMainWindow();
}
})
})
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
app.quit();
}
})
});
+6 -6
View File
@@ -1,16 +1,16 @@
const { contextBridge } = require('electron')
const { contextBridge } = require('electron');
// dev uses http://localhost, prod uses file://...index.html
const isDev = window.location.protocol === 'http:'
const isDev = window.location.protocol === 'http:';
const defaultPlebbitOptions = {
plebbitRpcClientsOptions: ['ws://localhost:9138']
}
plebbitRpcClientsOptions: ['ws://localhost:9138'],
};
contextBridge.exposeInMainWorld('defaultPlebbitOptions', defaultPlebbitOptions)
contextBridge.exposeInMainWorld('defaultPlebbitOptions', defaultPlebbitOptions);
// expose a flag to indicate that we are running in electron
contextBridge.exposeInMainWorld('electron', { isElectron: true })
contextBridge.exposeInMainWorld('electron', { isElectron: true });
// uncomment for logs
// localStorage.debug = 'plebbit-js:*,plebbit-react-hooks:*,plebchan:*'
+3 -12
View File
@@ -13,12 +13,7 @@ const spawnAsync = (...args) =>
const spawedProcess = spawn(...args);
spawedProcess.on('exit', (exitCode, signal) => {
if (exitCode === 0) resolve();
else
reject(
Error(
`spawnAsync process '${spawedProcess.pid}' exited with code '${exitCode}' signal '${signal}'`
)
);
else reject(Error(`spawnAsync process '${spawedProcess.pid}' exited with code '${exitCode}' signal '${signal}'`));
});
spawedProcess.stderr.on('data', (data) => console.error(data.toString()));
spawedProcess.stdin.on('data', (data) => console.log(data.toString()));
@@ -59,7 +54,7 @@ const startIpfs = async () => {
} catch (e) {}
// dont use 8080 port because it's too common
await spawnAsync(ipfsPath, ['config', '--json', 'Addresses.Gateway', "null"], {
await spawnAsync(ipfsPath, ['config', '--json', 'Addresses.Gateway', 'null'], {
env,
hideWindows: true,
});
@@ -73,11 +68,7 @@ const startIpfs = async () => {
await spawnAsync(ipfsPath, ['config', 'Addresses.API', apiAddress], { env, hideWindows: true });
await new Promise((resolve, reject) => {
const ipfsProcess = spawn(
ipfsPath,
['daemon', '--migrate', '--enable-pubsub-experiment', '--enable-namesys-pubsub'],
{ env, hideWindows: true }
);
const ipfsProcess = spawn(ipfsPath, ['daemon', '--migrate', '--enable-pubsub-experiment', '--enable-namesys-pubsub'], { env, hideWindows: true });
console.log(`ipfs daemon process started with pid ${ipfsProcess.pid}`);
let lastError;
ipfsProcess.stderr.on('data', (data) => {
+36 -38
View File
@@ -1,69 +1,67 @@
const tcpPortUsed = require('tcp-port-used')
const {PlebbitWsServer} = require('@plebbit/plebbit-js/rpc')
const path = require('path')
const envPaths = require('env-paths').default('plebbit', { suffix: false })
const {randomBytes} = require('crypto')
const fs = require('fs-extra')
const tcpPortUsed = require('tcp-port-used');
const { PlebbitWsServer } = require('@plebbit/plebbit-js/rpc');
const path = require('path');
const envPaths = require('env-paths').default('plebbit', { suffix: false });
const { randomBytes } = require('crypto');
const fs = require('fs-extra');
let isDev = true
let isDev = true;
try {
isDev = require('electron-is-dev')
isDev = require('electron-is-dev');
} catch (e) {}
// PLEB, always run plebbit rpc on this port so all clients can use it
const port = 9138
const port = 9138;
const defaultPlebbitOptions = {
// find the user's OS data path
dataPath: !isDev ? envPaths.data : path.join(__dirname, '..', '.plebbit'),
ipfsHttpClientsOptions: ['http://localhost:5001/api/v0'],
// TODO: having to define pubsubHttpClientsOptions and ipfsHttpClientsOptions is a bug with plebbit-js
pubsubHttpClientsOptions: ['http://localhost:5001/api/v0'],
}
};
// generate plebbit rpc auth key if doesn't exist
const plebbitRpcAuthKeyPath = path.join(defaultPlebbitOptions.dataPath, 'auth-key')
let plebbitRpcAuthKey
const plebbitRpcAuthKeyPath = path.join(defaultPlebbitOptions.dataPath, 'auth-key');
let plebbitRpcAuthKey;
try {
plebbitRpcAuthKey = fs.readFileSync(plebbitRpcAuthKeyPath, 'utf8')
}
catch (e) {
plebbitRpcAuthKey = randomBytes(32).toString('base64').replace(/[/+=]/g, '').substring(0, 40)
fs.ensureFileSync(plebbitRpcAuthKeyPath)
fs.writeFileSync(plebbitRpcAuthKeyPath, plebbitRpcAuthKey)
plebbitRpcAuthKey = fs.readFileSync(plebbitRpcAuthKeyPath, 'utf8');
} catch (e) {
plebbitRpcAuthKey = randomBytes(32).toString('base64').replace(/[/+=]/g, '').substring(0, 40);
fs.ensureFileSync(plebbitRpcAuthKeyPath);
fs.writeFileSync(plebbitRpcAuthKeyPath, plebbitRpcAuthKey);
}
let pendingStart = false
let pendingStart = false;
const start = async () => {
if (pendingStart) {
return
return;
}
pendingStart = true
pendingStart = true;
try {
const started = await tcpPortUsed.check(port, '127.0.0.1')
const started = await tcpPortUsed.check(port, '127.0.0.1');
if (started) {
return
return;
}
const plebbitWebSocketServer = await PlebbitWsServer({port, plebbitOptions: defaultPlebbitOptions, authKey: plebbitRpcAuthKey})
const plebbitWebSocketServer = await PlebbitWsServer({ port, plebbitOptions: defaultPlebbitOptions, authKey: plebbitRpcAuthKey });
console.log(`plebbit rpc: listening on ws://localhost:${port} (local connections only)`)
console.log(`plebbit rpc: listening on ws://localhost:${port}/${plebbitRpcAuthKey} (secret auth key for remote connections)`)
console.log(`plebbit rpc: listening on ws://localhost:${port} (local connections only)`);
console.log(`plebbit rpc: listening on ws://localhost:${port}/${plebbitRpcAuthKey} (secret auth key for remote connections)`);
plebbitWebSocketServer.ws.on('connection', (socket, request) => {
console.log('plebbit rpc: new connection')
console.log('plebbit rpc: new connection');
// debug raw JSON RPC messages in console
if (isDev) {
socket.on('message', (message) => console.log(`plebbit rpc: ${message.toString()}`))
socket.on('message', (message) => console.log(`plebbit rpc: ${message.toString()}`));
}
})
});
} catch (e) {
console.log('failed starting plebbit rpc server', e);
}
catch (e) {
console.log('failed starting plebbit rpc server', e)
}
pendingStart = false
}
pendingStart = false;
};
// retry starting the plebbit rpc server every 1 second,
// retry starting the plebbit rpc server every 1 second,
// in case it was started by another client that shut down and shut down the server with it
start()
start();
setInterval(() => {
start()
}, 1000)
start();
}, 1000);
+60 -62
View File
@@ -4,20 +4,24 @@ import useGeneralStore from '../../hooks/stores/useGeneralStore';
import Modal from 'react-modal';
import Draggable from 'react-draggable';
const CaptchaModal = () => {
const {
challengesArray, setChallengesArray,
const {
challengesArray,
setChallengesArray,
pendingComment,
selectedStyle,
setCaptchaResponse,
isAuthorDelete, setIsAuthorDelete,
isAuthorEdit, setIsAuthorEdit,
isCaptchaOpen, setIsCaptchaOpen,
isModEdit, setIsModEdit,
isAuthorDelete,
setIsAuthorDelete,
isAuthorEdit,
setIsAuthorEdit,
isCaptchaOpen,
setIsCaptchaOpen,
isModEdit,
setIsModEdit,
resolveCaptchaPromise,
selectedShortCid,
} = useGeneralStore(state => state);
} = useGeneralStore((state) => state);
const [imageSources, setImageSources] = useState([]);
const [isLoading, setIsLoading] = useState(true);
@@ -28,7 +32,6 @@ const CaptchaModal = () => {
const nodeRef = useRef(null);
const [isPromiseResolved, setIsPromiseResolved] = useState(false);
useEffect(() => {
if (!isCaptchaOpen) {
setIsAuthorDelete(false);
@@ -37,7 +40,6 @@ const CaptchaModal = () => {
}
}, [isCaptchaOpen, setIsAuthorDelete, setIsAuthorEdit, setIsModEdit]);
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth <= 480);
window.addEventListener('resize', handleResize);
@@ -48,7 +50,6 @@ const CaptchaModal = () => {
};
}, [setIsMobile]);
useEffect(() => {
if (isCaptchaOpen && challengesArray) {
setIsLoading(true);
@@ -67,9 +68,8 @@ const CaptchaModal = () => {
}
}, [challengesArray, isCaptchaOpen]);
const handleKeyDown = (event) => {
if (event.key === "Enter") {
if (event.key === 'Enter') {
event.preventDefault();
submitCaptcha((response) => {
setCaptchaResponse(response);
@@ -78,15 +78,13 @@ const CaptchaModal = () => {
}
};
const handleReturnKeyDown = () => {
submitCaptcha((response) => {
submitCaptcha((response) => {
setCaptchaResponse(response);
resolveCaptchaPromise(response);
});
};
const submitCaptcha = (callback) => {
if (!isPromiseResolved) {
setCaptchaResponse(responseRef.current.value);
@@ -102,7 +100,7 @@ const CaptchaModal = () => {
callback(responseRef.current.value);
}
};
useEffect(() => {
if (!isCaptchaOpen) {
setIsPromiseResolved(false);
@@ -116,76 +114,76 @@ const CaptchaModal = () => {
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" />
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">
<div id='form'>
{pendingComment.author?.displayName ? (
<div>
<input id="field" type="text" placeholder={pendingComment.author?.displayName || ''} disabled />
<input id='field' type='text' placeholder={pendingComment.author?.displayName || ''} disabled />
</div>
) : null}
{pendingComment.title ? (
<div>
<input id="field" type="text" placeholder={pendingComment.title || ''} disabled />
<input id='field' type='text' placeholder={pendingComment.title || ''} disabled />
</div>
) : null}
{pendingComment.content ? (
<div>
<textarea
rows="4"
placeholder={pendingComment.content || "Comment"}
wrap="soft"
disabled
/>
<textarea rows='4' placeholder={pendingComment.content || 'Comment'} wrap='soft' disabled />
</div>
) : null}
{pendingComment.link ? (
<div>
<input id="field" type="text" placeholder={pendingComment.link || ''} disabled />
<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 />
<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" }} />
<img src='' alt='loading...' style={{ visibility: 'hidden' }} />
) : (
imageSources[currentChallengeIndex] && <img src={imageSources[currentChallengeIndex]} alt="captcha" />
imageSources[currentChallengeIndex] && <img src={imageSources[currentChallengeIndex]} alt='captcha' />
)}
</div>
<div>
<span style={{lineHeight: '1.7'}}>
<span style={{ lineHeight: '1.7' }}>
Challenge {currentChallengeIndex + 1} of {totalChallenges}
</span>
<button
id="nav"
id='nav'
onClick={() => {
if (currentChallengeIndex + 1 < totalChallenges) {
setCurrentChallengeIndex((currentChallengeIndex + 1) % totalChallenges);
@@ -194,7 +192,7 @@ const CaptchaModal = () => {
}
}}
>
{currentChallengeIndex + 1 < totalChallenges ? "Next" : "Submit"}
{currentChallengeIndex + 1 < totalChallenges ? 'Next' : 'Submit'}
</button>
</div>
</div>
@@ -206,4 +204,4 @@ const CaptchaModal = () => {
Modal.setAppElement('#root');
export default CaptchaModal;
export default CaptchaModal;
+57 -110
View File
@@ -1,8 +1,10 @@
import React, { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAccount, useCreateSubplebbit,
import {
useAccount,
useCreateSubplebbit,
// useSubplebbits, useAccountSubplebbits
} from '@plebbit/plebbit-react-hooks';
} from '@plebbit/plebbit-react-hooks';
import { StyledModal } from '../styled/modals/CreateBoardModal.styled';
import useGeneralStore from '../../hooks/stores/useGeneralStore';
import Modal from 'react-modal';
@@ -10,7 +12,7 @@ import useError from '../../hooks/useError';
import useSuccess from '../../hooks/useSuccess';
const CreateBoardModal = ({ isOpen, closeModal }) => {
const { selectedStyle } = useGeneralStore(state => state);
const { selectedStyle } = useGeneralStore((state) => state);
const account = useAccount();
const navigate = useNavigate();
@@ -29,7 +31,6 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
const [rules, setRules] = useState([]);
const [editIndex, setEditIndex] = useState(-1);
const handleAddRule = () => {
if (rule.trim() === '') {
setNewErrorMessage('Rule field is empty');
@@ -41,15 +42,14 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
return;
}
if(editIndex > -1){
setRules(rules.map((r, i) => i === editIndex ? rule : r));
if (editIndex > -1) {
setRules(rules.map((r, i) => (i === editIndex ? rule : r)));
setEditIndex(-1);
} else {
setRules([...rules, rule]);
}
setRule('');
}
};
useEffect(() => {
if (ruleInputRef.current) {
@@ -57,40 +57,35 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
}
}, [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,
avatarUrl: avatar || undefined,
},
roles: moderators || undefined,
rules: rules || undefined,
}
};
const resetFields = () => {
setTitle('');
setDescription('');
@@ -98,23 +93,21 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
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)));
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(", "));
setNewErrorMessage('Invalid moderator addresses: ' + invalidAddresses.join(', '));
return;
}
const roles = {};
moderatorAddresses.forEach(addr => {
moderatorAddresses.forEach((addr) => {
roles[addr] = { role: 'moderator' };
});
roles[account.author.address] = { role: 'admin' };
@@ -133,27 +126,27 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
if (avatar) {
createSubplebbitOptions.suggested = {
avatarUrl : avatar,
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()
@@ -165,114 +158,68 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
<StyledModal
isOpen={isOpen}
onRequestClose={closeModal}
contentLabel="Create Board"
contentLabel='Create Board'
selectedStyle={selectedStyle}
style={{ overlay: { backgroundColor: "rgba(0,0,0,.25)" }}}
style={{ overlay: { backgroundColor: 'rgba(0,0,0,.25)' } }}
>
<div className="modal-content" ref={nodeRef}>
<div className="modal-header">
<div className='modal-content' ref={nodeRef}>
<div className='modal-header'>
Create Board
<button className="icon" onClick={() => closeModal()} title="close" />
<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.
<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.
<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.
<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.
<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.
<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"
/>
<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 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 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>
<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>
<button onClick={handleCreateBoard} id='create-board-btn'>
Create Board
</button>
</ul>
</div>
</StyledModal>
@@ -281,4 +228,4 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
Modal.setAppElement('#root');
export default CreateBoardModal;
export default CreateBoardModal;
+28 -30
View File
@@ -4,19 +4,14 @@ 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 { 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);
@@ -26,40 +21,43 @@ const EditModal = ({ isOpen, closeModal, originalCommentContent }) => {
};
}, [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">
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" />
<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 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>
<button id='next' onClick={handleSaveEdit}>
Save
</button>
</div>
</div>
</div>
@@ -70,4 +68,4 @@ const EditModal = ({ isOpen, closeModal, originalCommentContent }) => {
Modal.setAppElement('#root');
export default EditModal;
export default EditModal;
+62 -98
View File
@@ -1,26 +1,17 @@
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";
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 { selectedAddress, selectedStyle, setCaptchaResponse, setChallengesArray, setIsCaptchaOpen, setIsModEdit, moderatingCommentCid, setResolveCaptchaPromise } =
useGeneralStore((state) => state);
const comment = useComment({commentCid: moderatingCommentCid});
const comment = useComment({ commentCid: moderatingCommentCid });
const [pin, setPin] = useState(comment.pinned);
const [deleteThread, setDeleteThread] = useState(deletePost);
@@ -31,68 +22,62 @@ const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
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);
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);
challengeAnswers = await getChallengeAnswersFromUser(challenges);
} catch (error) {
setNewErrorMessage(error.message);
console.log(error);
}
if (challengeAnswers) {
await comment.publishChallengeAnswers(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;
@@ -108,26 +93,24 @@ const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
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);
setNewErrorMessage(error.message);
console.log(error);
},
});
useEffect(() => {
if (selectedAddress) {
setPublishCommentEditOptions((prevOptions) => ({
@@ -136,11 +119,9 @@ const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
}));
}
}, [selectedAddress]);
const { publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions);
useEffect(() => {
setPublishCommentEditOptions((prevOptions) => ({
...prevOptions,
@@ -148,7 +129,6 @@ const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
}));
}, [moderatingCommentCid]);
useEffect(() => {
let isActive = true;
if (publishCommentEditOptions && triggerPublishCommentEdit) {
@@ -165,80 +145,64 @@ const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
};
}, [triggerPublishCommentEdit, publishCommentEdit, publishCommentEditOptions]);
return (
<StyledModal
isOpen={isOpen}
onRequestClose={handleCloseModal}
contentLabel="Moderator Tools"
style={{ overlay: { backgroundColor: "rgba(0,0,0,.25)" }}}
contentLabel='Moderator Tools'
style={{ overlay: { backgroundColor: 'rgba(0,0,0,.25)' } }}
selectedStyle={selectedStyle}
>
<div className="panel">
<div className="panel-header">
<div className='panel'>
<div className='panel-header'>
Moderator Tools
<Link to="" onClick={handleCloseModal}>
<span className="icon" title="close" />
<Link to='' onClick={handleCloseModal}>
<span className='icon' title='close' />
</Link>
</div>
<ul className="settings-cat">
<li className="settings-cat-lbl">
<ul className='settings-cat'>
<li className='settings-cat-lbl'>
<label>
<input type="checkbox" style={{marginRight: "10px"}}
checked={pin} onChange={() => setPin(!pin)} />
<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>
<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={close} onChange={() => setClose(!close)} />
<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.
</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)}/>
<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"
className='save-button'
onClick={async () => {
setPublishCommentEditOptions(prevOptions => ({
setPublishCommentEditOptions((prevOptions) => ({
...prevOptions,
pinned: pin,
removed: deleteThread,
locked: close,
reason: reason
reason: reason,
}));
setTriggerPublishCommentEdit(true);
setIsModEdit(true);
@@ -250,8 +214,8 @@ const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
</div>
</StyledModal>
);
}
};
Modal.setAppElement("#root");
Modal.setAppElement('#root');
export default ModerationModal;
export default ModerationModal;
+23 -25
View File
@@ -1,22 +1,18 @@
import React, {useState, useEffect} from "react";
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";
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 { 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);
@@ -26,31 +22,33 @@ const OriginalCommentModal = ({ isOpen, closeModal, comment }) => {
};
}, [setIsMobile]);
return (
<StyledModal
isOpen={isOpen}
onRequestClose={closeModal}
contentLabel="Original Comment"
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">
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" />
<button className='icon' onClick={() => closeModal()} title='close' />
</div>
<div id="form">
<div className="textarea-wrapper">
<textarea className="textarea"
rows="4"
<div id='form'>
<div className='textarea-wrapper'>
<textarea
className='textarea'
rows='4'
style={{ paddingTop: '0' }}
placeholder="Comment"
placeholder='Comment'
defaultValue={originalCommentContent}
wrap="soft"
readOnly={true} />
wrap='soft'
readOnly={true}
/>
</div>
</div>
</div>
@@ -61,4 +59,4 @@ const OriginalCommentModal = ({ isOpen, closeModal, comment }) => {
Modal.setAppElement('#root');
export default OriginalCommentModal;
export default OriginalCommentModal;
+6 -7
View File
@@ -1,7 +1,6 @@
import styled from "styled-components";
import styled from 'styled-components';
export const BoardStatsContainer = styled.div`
@media (max-width: 480px) {
display: none;
}
@@ -11,14 +10,14 @@ export const BoardStatsContainer = styled.div`
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;
@@ -94,9 +93,9 @@ export const BoardStatsContainer = styled.div`
.hide-button:hover {
color: #ff3300 !important;
}`;
default:
return '';
return '';
}
}}
`;
`;
+3 -3
View File
@@ -1,5 +1,5 @@
import { ToastContainer } from "react-toastify";
import styled from "styled-components";
import { ToastContainer } from 'react-toastify';
import styled from 'styled-components';
export const Toast = styled(ToastContainer)`
.Toastify__toast {
@@ -9,4 +9,4 @@ export const Toast = styled(ToastContainer)`
border: 1px solid #fee9cd;
color: #c5c8c6;
}
`;
`;
@@ -16,16 +16,13 @@ export const AlertModal = styled.div`
& > button {
margin-top: 10px;
}
& > button:first-child {
margin-right: 20px;
}
}
}
${({ selectedStyle }) => {
switch (selectedStyle) {
case 'Yotsuba':
@@ -68,10 +65,10 @@ export const AlertModal = styled.div`
.author-delete-alert {
background-color: #ddd;
border: 1px solid #ccc;
}`;
default:
return '';
}
}`;
default:
return '';
}
}}
`;
`;
+13 -11
View File
@@ -1,15 +1,17 @@
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,
}
));
const useAnonModeStore = create(
persist(
(set) => ({
anonymousMode: true,
setAnonymousMode: (mode) => set({ anonymousMode: mode }),
}),
{
name: 'anonmode_store',
getStorage: () => localStorage,
},
),
);
export default useAnonModeStore;
export default useAnonModeStore;
+2 -2
View File
@@ -21,14 +21,14 @@ root.render(
<App />
</HashRouter>
</HelmetProvider>
</React.StrictMode>
</React.StrictMode>,
);
// set up PWA https://cra.link/PWA
serviceWorkerRegistration.register();
// add back button in android app
CapacitorApp.addListener('backButton', ({canGoBack}) => {
CapacitorApp.addListener('backButton', ({ canGoBack }) => {
if (canGoBack) {
window.history.back();
} else {
+3 -3
View File
@@ -20,7 +20,7 @@ registerRoute(
// match index.html
({ url }) => url.pathname === '/',
// serve cached index.html first but revalidate in background
new StaleWhileRevalidate()
new StaleWhileRevalidate(),
);
// Precache all of the assets generated by your build process.
@@ -51,7 +51,7 @@ registerRoute(
return true;
},
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html'),
);
// An example runtime caching route for requests that aren't handled by the
@@ -66,7 +66,7 @@ registerRoute(
// least-recently used images are removed.
new ExpirationPlugin({ maxEntries: 50 }),
],
})
}),
);
// This allows the web app to trigger skipWaiting via
+4 -13
View File
@@ -15,7 +15,7 @@ const isLocalhost = Boolean(
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/),
);
export function register(config) {
@@ -39,10 +39,7 @@ export function register(config) {
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://cra.link/PWA'
);
console.log('This web app is being served cache-first by a service ' + 'worker. To learn more, visit https://cra.link/PWA');
});
} else {
// Is not localhost. Just register service worker
@@ -67,10 +64,7 @@ function registerValidSW(swUrl, config) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://cra.link/PWA.'
);
console.log('New content is available and will be used when all ' + 'tabs for this page are closed. See https://cra.link/PWA.');
// Execute callback
if (config && config.onUpdate) {
@@ -104,10 +98,7 @@ function checkValidServiceWorker(swUrl, config) {
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {