Merge pull request #310 from plebbit/master

Development
This commit is contained in:
plebeius.eth
2023-12-16 17:58:33 +01:00
committed by GitHub
20 changed files with 483 additions and 598 deletions
+5
View File
@@ -52,6 +52,11 @@ jobs:
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: 16 node-version: 16
# install missing dep for sqlite
- run: python3 -m ensurepip
- run: pip install setuptools
- run: yarn install --frozen-lockfile - run: yarn install --frozen-lockfile
# make sure the ipfs executable is executable # make sure the ipfs executable is executable
- run: node electron/download-ipfs && sudo chmod +x bin/mac/ipfs - run: node electron/download-ipfs && sudo chmod +x bin/mac/ipfs
+1 -4
View File
@@ -11,10 +11,7 @@ const addPortableToPortableExecutableFileName = () => {
for (const file of files) { for (const file of files) {
if (file.endsWith('.exe') && !file.match('Setup')) { if (file.endsWith('.exe') && !file.match('Setup')) {
const filePath = path.resolve(distFolderPath, file); const filePath = path.resolve(distFolderPath, file);
const renamedFilePath = path.resolve( const renamedFilePath = path.resolve(distFolderPath, file.replace('plebchan', 'plebchan Portable'));
distFolderPath,
file.replace('plebchan', 'plebchan Portable')
);
fs.moveSync(filePath, renamedFilePath); fs.moveSync(filePath, renamedFilePath);
} }
} }
+38 -17
View File
@@ -10,12 +10,17 @@ const ipfsClientWindowsPath = path.join(ipfsClientsPath, 'win');
const ipfsClientMacPath = path.join(ipfsClientsPath, 'mac'); const ipfsClientMacPath = path.join(ipfsClientsPath, 'mac');
const ipfsClientLinuxPath = path.join(ipfsClientsPath, 'linux'); const ipfsClientLinuxPath = path.join(ipfsClientsPath, 'linux');
// kubo download links https://docs.ipfs.tech/install/command-line/#install-official-binary-distributions
// plebbit kubu download links https://github.com/plebbit/kubo/releases // plebbit kubu download links https://github.com/plebbit/kubo/releases
const ipfsClientVersion = '0.20.0'; // const ipfsClientVersion = '0.20.0'
const ipfsClientWindowsUrl = `https://github.com/plebbit/kubo/releases/download/v${ipfsClientVersion}/ipfs-windows-amd64`; // const ipfsClientWindowsUrl = `https://github.com/plebbit/kubo/releases/download/v${ipfsClientVersion}/ipfs-windows-amd64`
const ipfsClientMacUrl = `https://github.com/plebbit/kubo/releases/download/v${ipfsClientVersion}/ipfs-darwin-amd64`; // const ipfsClientMacUrl = `https://github.com/plebbit/kubo/releases/download/v${ipfsClientVersion}/ipfs-darwin-amd64`
const ipfsClientLinuxPUrl = `https://github.com/plebbit/kubo/releases/download/v${ipfsClientVersion}/ipfs-linux-amd64`; // const ipfsClientLinuxPUrl = `https://github.com/plebbit/kubo/releases/download/v${ipfsClientVersion}/ipfs-linux-amd64`
// official kubo download links https://docs.ipfs.tech/install/command-line/#install-official-binary-distributions
const ipfsClientVersion = '0.24.0';
const ipfsClientWindowsUrl = `https://dist.ipfs.io/kubo/v${ipfsClientVersion}/kubo_v${ipfsClientVersion}_windows-amd64.zip`;
const ipfsClientMacUrl = `https://dist.ipfs.io/kubo/v${ipfsClientVersion}/kubo_v${ipfsClientVersion}_darwin-amd64.tar.gz`;
const ipfsClientLinuxPUrl = `https://dist.ipfs.io/kubo/v${ipfsClientVersion}/kubo_v${ipfsClientVersion}_linux-amd64.tar.gz`;
const downloadWithProgress = (url) => const downloadWithProgress = (url) =>
new Promise((resolve) => { new Promise((resolve) => {
@@ -50,6 +55,7 @@ const downloadWithProgress = (url) =>
req.end(); req.end();
}); });
// plebbit kubo downloads dont need to be extracted
const download = async (url, destinationPath) => { const download = async (url, destinationPath) => {
let binName = 'ipfs'; let binName = 'ipfs';
if (destinationPath.endsWith('win')) { if (destinationPath.endsWith('win')) {
@@ -66,24 +72,39 @@ const download = async (url, destinationPath) => {
const file = await downloadWithProgress(url); const file = await downloadWithProgress(url);
fs.ensureDirSync(destinationPath); fs.ensureDirSync(destinationPath);
await fs.writeFile(binPath, file); await fs.writeFile(binPath, file);
};
// decompress // official kubo downloads need to be extracted
// await fs.writeFile(dowloadPath, file); const downloadAndExtract = async (url, destinationPath) => {
// await decompress(dowloadPath, destinationPath); let binName = 'ipfs';
// const extractedPath = path.join(destinationPath, 'kubo'); if (destinationPath.endsWith('win')) {
// const extractedBinPath = path.join(extractedPath, binName); binName += '.exe';
// fs.moveSync(extractedBinPath, binPath); }
// fs.removeSync(extractedPath); const binPath = path.join(destinationPath, binName);
// fs.removeSync(dowloadPath); if (fs.pathExistsSync(binPath)) {
return;
}
const split = url.split('/');
const fileName = split[split.length - 1];
const dowloadPath = path.join(destinationPath, fileName);
const file = await downloadWithProgress(url);
fs.ensureDirSync(destinationPath);
await fs.writeFile(dowloadPath, file);
await decompress(dowloadPath, destinationPath);
const extractedPath = path.join(destinationPath, 'kubo');
const extractedBinPath = path.join(extractedPath, binName);
fs.moveSync(extractedBinPath, binPath);
fs.removeSync(extractedPath);
fs.removeSync(dowloadPath);
}; };
const downloadIpfsClients = async () => { const downloadIpfsClients = async () => {
await download(ipfsClientWindowsUrl, ipfsClientWindowsPath); await downloadAndExtract(ipfsClientWindowsUrl, ipfsClientWindowsPath);
await download(ipfsClientMacUrl, ipfsClientMacPath); await downloadAndExtract(ipfsClientMacUrl, ipfsClientMacPath);
await download(ipfsClientLinuxPUrl, ipfsClientLinuxPath); await downloadAndExtract(ipfsClientLinuxPUrl, ipfsClientLinuxPath);
}; };
exports.downloadIpfsClients = downloadIpfsClients exports.downloadIpfsClients = downloadIpfsClients;
exports.default = async (context) => { exports.default = async (context) => {
await downloadIpfsClients(); await downloadIpfsClients();
+122 -143
View File
@@ -1,52 +1,40 @@
require('./log') require('./log');
const { const { app, BrowserWindow, Menu, MenuItem, Tray, screen: electronScreen, shell, dialog } = require('electron');
app, const isDev = require('electron-is-dev');
BrowserWindow, const path = require('path');
Menu, const startIpfs = require('./start-ipfs');
MenuItem, const startPlebbitRpcServer = require('./start-plebbit-rpc');
Tray, const { URL } = require('node:url');
screen: electronScreen, const tcpPortUsed = require('tcp-port-used');
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 // in case it was started by another client that shut down and shut down ipfs with it
let startIpfsError let startIpfsError;
setInterval(async () => { setInterval(async () => {
try { try {
const started = await tcpPortUsed.check(5001, '127.0.0.1') const started = await tcpPortUsed.check(5001, '127.0.0.1');
if (started) { if (started) {
return return;
} }
await startIpfs() await startIpfs();
} catch (e) {
console.log(e);
startIpfsError = e;
dialog.showErrorBox('IPFS error', startIpfsError.message);
} }
catch (e) { }, 10000);
console.log(e)
startIpfsError = e
dialog.showErrorBox('IPFS error', startIpfsError.message)
}
}, 10000)
// use common user agent instead of electron so img, video, audio, iframe elements don't get blocked // 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-version/chrome
// https://www.whatismybrowser.com/guides/the-latest-user-agent/chrome // https://www.whatismybrowser.com/guides/the-latest-user-agent/chrome
// NOTE: eventually should probably fake sec-ch-ua header as well // 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' 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') 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';
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';
if (process.platform === 'linux') const realUserAgent = `plebchan/${require('../package.json').version}`;
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 // add right click menu
const contextMenu = require('electron-context-menu') const contextMenu = require('electron-context-menu');
contextMenu({ contextMenu({
// prepend custom buttons to top // prepend custom buttons to top
prepend: (defaultActions, parameters, browserWindow) => [ prepend: (defaultActions, parameters, browserWindow) => [
@@ -76,7 +64,7 @@ contextMenu({
showInspectElement: true, showInspectElement: true,
showServices: false, showServices: false,
showSearchWithGoogle: false, showSearchWithGoogle: false,
}) });
const createMainWindow = () => { const createMainWindow = () => {
let mainWindow = new BrowserWindow({ let mainWindow = new BrowserWindow({
@@ -91,199 +79,192 @@ const createMainWindow = () => {
devTools: true, // TODO: change to isDev when no bugs left devTools: true, // TODO: change to isDev when no bugs left
preload: path.join(__dirname, 'preload.js'), preload: path.join(__dirname, 'preload.js'),
}, },
}) });
// set fake user agent // 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 // set custom user agent and other headers for window.fetch requests to prevent origin errors
mainWindow.webContents.session.webRequest.onBeforeSendHeaders({urls: ['*://*/*']}, (details, callback) => { mainWindow.webContents.session.webRequest.onBeforeSendHeaders({ urls: ['*://*/*'] }, (details, callback) => {
const isIframe = !!details.frame?.parent 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 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) { if (details.resourceType !== 'xhr' || isIframe) {
return callback({requestHeaders: details.requestHeaders}) return callback({ requestHeaders: details.requestHeaders });
} }
// add privacy // add privacy
details.requestHeaders['User-Agent'] = realUserAgent details.requestHeaders['User-Agent'] = realUserAgent;
details.requestHeaders['sec-ch-ua'] = undefined details.requestHeaders['sec-ch-ua'] = undefined;
details.requestHeaders['sec-ch-ua-platform'] = undefined details.requestHeaders['sec-ch-ua-platform'] = undefined;
details.requestHeaders['sec-ch-ua-mobile'] = undefined details.requestHeaders['sec-ch-ua-mobile'] = undefined;
details.requestHeaders['Sec-Fetch-Dest'] = undefined details.requestHeaders['Sec-Fetch-Dest'] = undefined;
details.requestHeaders['Sec-Fetch-Mode'] = undefined details.requestHeaders['Sec-Fetch-Mode'] = undefined;
details.requestHeaders['Sec-Fetch-Site'] = undefined details.requestHeaders['Sec-Fetch-Site'] = undefined;
// prevent origin errors // prevent origin errors
details.requestHeaders['Origin'] = undefined details.requestHeaders['Origin'] = undefined;
callback({requestHeaders: details.requestHeaders}) callback({ requestHeaders: details.requestHeaders });
}) });
// fix cors errors for window.fetch. must not be enabled for iframe or can cause remote code execution // 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) => { mainWindow.webContents.session.webRequest.onHeadersReceived({ urls: ['*://*/*'] }, (details, callback) => {
const isIframe = !!details.frame?.parent 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 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) { 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 // must delete lower case headers or both '*, *' could get added
delete details.responseHeaders['access-control-allow-origin'] delete details.responseHeaders['access-control-allow-origin'];
delete details.responseHeaders['access-control-allow-headers'] delete details.responseHeaders['access-control-allow-headers'];
delete details.responseHeaders['access-control-allow-methods'] delete details.responseHeaders['access-control-allow-methods'];
delete details.responseHeaders['access-control-expose-headers'] delete details.responseHeaders['access-control-expose-headers'];
details.responseHeaders['Access-Control-Allow-Origin'] = '*' details.responseHeaders['Access-Control-Allow-Origin'] = '*';
details.responseHeaders['Access-Control-Allow-Headers'] = '*' details.responseHeaders['Access-Control-Allow-Headers'] = '*';
details.responseHeaders['Access-Control-Allow-Methods'] = '*' details.responseHeaders['Access-Control-Allow-Methods'] = '*';
details.responseHeaders['Access-Control-Expose-Headers'] = '*' details.responseHeaders['Access-Control-Expose-Headers'] = '*';
callback({responseHeaders: details.responseHeaders}) callback({ responseHeaders: details.responseHeaders });
}) });
const startURL = isDev const startURL = isDev ? 'http://localhost:3000' : `file://${path.join(__dirname, '../build/index.html')}`;
? 'http://localhost:3000'
: `file://${path.join(__dirname, '../build/index.html')}`
mainWindow.loadURL(startURL) mainWindow.loadURL(startURL);
mainWindow.once('ready-to-show', async () => { mainWindow.once('ready-to-show', async () => {
// make sure back button is disabled on launch // make sure back button is disabled on launch
mainWindow.webContents.clearHistory() mainWindow.webContents.clearHistory();
mainWindow.show() mainWindow.show();
if (isDev) { if (isDev) {
mainWindow.openDevTools() mainWindow.openDevTools();
} }
if (startIpfsError) { if (startIpfsError) {
dialog.showErrorBox('IPFS error', startIpfsError.message) dialog.showErrorBox('IPFS error', startIpfsError.message);
} }
}) });
mainWindow.on('closed', () => { mainWindow.on('closed', () => {
mainWindow = null mainWindow = null;
}) });
// don't open new windows // don't open new windows
mainWindow.webContents.on('new-window', (event, url) => { mainWindow.webContents.on('new-window', (event, url) => {
event.preventDefault() event.preventDefault();
mainWindow.loadURL(url) mainWindow.loadURL(url);
}) });
// open links in external browser // open links in external browser
// do not open links in plebchan or will lead to remote execution // do not open links in plebchan or will lead to remote execution
mainWindow.webContents.on('will-navigate', (e, originalUrl) => { mainWindow.webContents.on('will-navigate', (e, originalUrl) => {
if (originalUrl != mainWindow.webContents.getURL()) { if (originalUrl != mainWindow.webContents.getURL()) {
e.preventDefault() e.preventDefault();
try { try {
// do not let the user open any url with shell.openExternal // 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/ // or it will lead to remote execution https://benjamin-altpeter.de/shell-openexternal-dangers/
// only open valid https urls to prevent remote execution // only open valid https urls to prevent remote execution
// will throw if url isn't valid // will throw if url isn't valid
const validatedUrl = new URL(originalUrl) const validatedUrl = new URL(originalUrl);
let serializedUrl = '' let serializedUrl = '';
// make an exception for ipfs stats // make an exception for ipfs stats
if (validatedUrl.toString() === 'http://localhost:5001/webui/') { if (validatedUrl.toString() === 'http://localhost:5001/webui/') {
serializedUrl = validatedUrl.toString() serializedUrl = validatedUrl.toString();
} else if (validatedUrl.protocol === 'https:') { } else if (validatedUrl.protocol === 'https:') {
// open serialized url to prevent remote execution // open serialized url to prevent remote execution
serializedUrl = validatedUrl.toString() serializedUrl = validatedUrl.toString();
} else { } 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) { } catch (e) {
console.warn(e) console.warn(e);
} }
} }
}) });
// open links (with target="_blank") in external browser // open links (with target="_blank") in external browser
// do not open links in plebchan or will lead to remote execution // do not open links in plebchan or will lead to remote execution
mainWindow.webContents.setWindowOpenHandler(({url}) => { mainWindow.webContents.setWindowOpenHandler(({ url }) => {
const originalUrl = url const originalUrl = url;
try { try {
// do not let the user open any url with shell.openExternal // 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/ // or it will lead to remote execution https://benjamin-altpeter.de/shell-openexternal-dangers/
// only open valid https urls to prevent remote execution // only open valid https urls to prevent remote execution
// will throw if url isn't valid // will throw if url isn't valid
const validatedUrl = new URL(originalUrl) const validatedUrl = new URL(originalUrl);
let serializedUrl = '' let serializedUrl = '';
// make an exception for ipfs stats // make an exception for ipfs stats
if (validatedUrl.toString() === 'http://localhost:5001/webui/') { if (validatedUrl.toString() === 'http://localhost:5001/webui/') {
serializedUrl = validatedUrl.toString() serializedUrl = validatedUrl.toString();
} else if (validatedUrl.protocol === 'https:') { } else if (validatedUrl.protocol === 'https:') {
// open serialized url to prevent remote execution // open serialized url to prevent remote execution
serializedUrl = validatedUrl.toString() serializedUrl = validatedUrl.toString();
} else { } 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) { } 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 // 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) => { mainWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
// deny all permissions // 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 // 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) => { mainWindow.webContents.on('will-attach-webview', (e, webPreferences, params) => {
// deny all // deny all
e.preventDefault() e.preventDefault();
}) });
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
// tray // tray
const trayIconPath = path.join( const trayIconPath = path.join(__dirname, '..', isDev ? 'public' : 'build', 'electron-tray-icon.png');
__dirname, const tray = new Tray(trayIconPath);
'..', tray.setToolTip('plebchan');
isDev ? 'public' : 'build',
'electron-tray-icon.png'
)
const tray = new Tray(trayIconPath)
tray.setToolTip('plebchan')
const trayMenu = Menu.buildFromTemplate([ const trayMenu = Menu.buildFromTemplate([
{ {
label: 'Open plebchan', label: 'Open plebchan',
click: () => { click: () => {
mainWindow.show() mainWindow.show();
}, },
}, },
{ {
label: 'Quit plebchan', label: 'Quit plebchan',
click: () => { click: () => {
mainWindow.destroy() mainWindow.destroy();
app.quit() app.quit();
}, },
}, },
]) ]);
tray.setContextMenu(trayMenu) tray.setContextMenu(trayMenu);
// show/hide on tray right click // show/hide on tray right click
tray.on('right-click', () => { tray.on('right-click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show() mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
}) });
// close to tray // close to tray
if (!isDev) { if (!isDev) {
let isQuiting = false let isQuiting = false;
app.on('before-quit', () => { app.on('before-quit', () => {
isQuiting = true isQuiting = true;
}) });
mainWindow.on('close', (event) => { mainWindow.on('close', (event) => {
if (!isQuiting) { if (!isQuiting) {
event.preventDefault() event.preventDefault();
mainWindow.hide() mainWindow.hide();
event.returnValue = false event.returnValue = false;
} }
}) });
} }
} }
@@ -291,48 +272,46 @@ const createMainWindow = () => {
label: '←', label: '←',
enabled: mainWindow?.webContents?.canGoBack(), enabled: mainWindow?.webContents?.canGoBack(),
click: () => mainWindow?.webContents?.goBack(), click: () => mainWindow?.webContents?.goBack(),
}) });
const appMenuForward = new MenuItem({ const appMenuForward = new MenuItem({
label: '→', label: '→',
enabled: mainWindow?.webContents?.canGoForward(), enabled: mainWindow?.webContents?.canGoForward(),
click: () => mainWindow?.webContents?.goForward(), click: () => mainWindow?.webContents?.goForward(),
}) });
const appMenuReload = new MenuItem({ const appMenuReload = new MenuItem({
label: '⟳', label: '⟳',
role: 'reload', role: 'reload',
click: () => mainWindow?.webContents?.reload(), click: () => mainWindow?.webContents?.reload(),
}) });
// application menu // application menu
// hide useless electron help menu // hide useless electron help menu
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
const appMenu = Menu.getApplicationMenu() const appMenu = Menu.getApplicationMenu();
appMenu.insert(1, appMenuBack) appMenu.insert(1, appMenuBack);
appMenu.insert(2, appMenuForward) appMenu.insert(2, appMenuForward);
appMenu.insert(3, appMenuReload) appMenu.insert(3, appMenuReload);
Menu.setApplicationMenu(appMenu) Menu.setApplicationMenu(appMenu);
} else { } else {
// Other platforms // Other platforms
const originalAppMenuWithoutHelp = Menu.getApplicationMenu()?.items.filter( const originalAppMenuWithoutHelp = Menu.getApplicationMenu()?.items.filter((item) => item.role !== 'help');
(item) => item.role !== 'help' const appMenu = [appMenuBack, appMenuForward, appMenuReload, ...originalAppMenuWithoutHelp];
) Menu.setApplicationMenu(Menu.buildFromTemplate(appMenu));
const appMenu = [appMenuBack, appMenuForward, appMenuReload, ...originalAppMenuWithoutHelp]
Menu.setApplicationMenu(Menu.buildFromTemplate(appMenu))
} }
} };
app.whenReady().then(() => { app.whenReady().then(() => {
createMainWindow() createMainWindow();
app.on('activate', () => { app.on('activate', () => {
if (!BrowserWindow.getAllWindows().length) { if (!BrowserWindow.getAllWindows().length) {
createMainWindow() createMainWindow();
} }
}) });
}) });
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
if (process.platform !== 'darwin') { 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 // dev uses http://localhost, prod uses file://...index.html
const isDev = window.location.protocol === 'http:' const isDev = window.location.protocol === 'http:';
const defaultPlebbitOptions = { 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 // expose a flag to indicate that we are running in electron
contextBridge.exposeInMainWorld('electron', { isElectron: true }) contextBridge.exposeInMainWorld('electron', { isElectron: true });
// uncomment for logs // uncomment for logs
// localStorage.debug = 'plebbit-js:*,plebbit-react-hooks:*,plebchan:*' // localStorage.debug = 'plebbit-js:*,plebbit-react-hooks:*,plebchan:*'
+3 -12
View File
@@ -13,12 +13,7 @@ const spawnAsync = (...args) =>
const spawedProcess = spawn(...args); const spawedProcess = spawn(...args);
spawedProcess.on('exit', (exitCode, signal) => { spawedProcess.on('exit', (exitCode, signal) => {
if (exitCode === 0) resolve(); if (exitCode === 0) resolve();
else else reject(Error(`spawnAsync process '${spawedProcess.pid}' exited with code '${exitCode}' signal '${signal}'`));
reject(
Error(
`spawnAsync process '${spawedProcess.pid}' exited with code '${exitCode}' signal '${signal}'`
)
);
}); });
spawedProcess.stderr.on('data', (data) => console.error(data.toString())); spawedProcess.stderr.on('data', (data) => console.error(data.toString()));
spawedProcess.stdin.on('data', (data) => console.log(data.toString())); spawedProcess.stdin.on('data', (data) => console.log(data.toString()));
@@ -59,7 +54,7 @@ const startIpfs = async () => {
} catch (e) {} } catch (e) {}
// dont use 8080 port because it's too common // 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, env,
hideWindows: true, hideWindows: true,
}); });
@@ -73,11 +68,7 @@ const startIpfs = async () => {
await spawnAsync(ipfsPath, ['config', 'Addresses.API', apiAddress], { env, hideWindows: true }); await spawnAsync(ipfsPath, ['config', 'Addresses.API', apiAddress], { env, hideWindows: true });
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const ipfsProcess = spawn( const ipfsProcess = spawn(ipfsPath, ['daemon', '--migrate', '--enable-pubsub-experiment', '--enable-namesys-pubsub'], { env, hideWindows: true });
ipfsPath,
['daemon', '--migrate', '--enable-pubsub-experiment', '--enable-namesys-pubsub'],
{ env, hideWindows: true }
);
console.log(`ipfs daemon process started with pid ${ipfsProcess.pid}`); console.log(`ipfs daemon process started with pid ${ipfsProcess.pid}`);
let lastError; let lastError;
ipfsProcess.stderr.on('data', (data) => { ipfsProcess.stderr.on('data', (data) => {
+35 -37
View File
@@ -1,69 +1,67 @@
const tcpPortUsed = require('tcp-port-used') const tcpPortUsed = require('tcp-port-used');
const {PlebbitWsServer} = require('@plebbit/plebbit-js/rpc') const { PlebbitWsServer } = require('@plebbit/plebbit-js/rpc');
const path = require('path') const path = require('path');
const envPaths = require('env-paths').default('plebbit', { suffix: false }) const envPaths = require('env-paths').default('plebbit', { suffix: false });
const {randomBytes} = require('crypto') const { randomBytes } = require('crypto');
const fs = require('fs-extra') const fs = require('fs-extra');
let isDev = true let isDev = true;
try { try {
isDev = require('electron-is-dev') isDev = require('electron-is-dev');
} catch (e) {} } catch (e) {}
// PLEB, always run plebbit rpc on this port so all clients can use it // PLEB, always run plebbit rpc on this port so all clients can use it
const port = 9138 const port = 9138;
const defaultPlebbitOptions = { const defaultPlebbitOptions = {
// find the user's OS data path // find the user's OS data path
dataPath: !isDev ? envPaths.data : path.join(__dirname, '..', '.plebbit'), dataPath: !isDev ? envPaths.data : path.join(__dirname, '..', '.plebbit'),
ipfsHttpClientsOptions: ['http://localhost:5001/api/v0'], ipfsHttpClientsOptions: ['http://localhost:5001/api/v0'],
// TODO: having to define pubsubHttpClientsOptions and ipfsHttpClientsOptions is a bug with plebbit-js // TODO: having to define pubsubHttpClientsOptions and ipfsHttpClientsOptions is a bug with plebbit-js
pubsubHttpClientsOptions: ['http://localhost:5001/api/v0'], pubsubHttpClientsOptions: ['http://localhost:5001/api/v0'],
} };
// generate plebbit rpc auth key if doesn't exist // generate plebbit rpc auth key if doesn't exist
const plebbitRpcAuthKeyPath = path.join(defaultPlebbitOptions.dataPath, 'auth-key') const plebbitRpcAuthKeyPath = path.join(defaultPlebbitOptions.dataPath, 'auth-key');
let plebbitRpcAuthKey let plebbitRpcAuthKey;
try { try {
plebbitRpcAuthKey = fs.readFileSync(plebbitRpcAuthKeyPath, 'utf8') plebbitRpcAuthKey = fs.readFileSync(plebbitRpcAuthKeyPath, 'utf8');
} } catch (e) {
catch (e) { plebbitRpcAuthKey = randomBytes(32).toString('base64').replace(/[/+=]/g, '').substring(0, 40);
plebbitRpcAuthKey = randomBytes(32).toString('base64').replace(/[/+=]/g, '').substring(0, 40) fs.ensureFileSync(plebbitRpcAuthKeyPath);
fs.ensureFileSync(plebbitRpcAuthKeyPath) fs.writeFileSync(plebbitRpcAuthKeyPath, plebbitRpcAuthKey);
fs.writeFileSync(plebbitRpcAuthKeyPath, plebbitRpcAuthKey)
} }
let pendingStart = false let pendingStart = false;
const start = async () => { const start = async () => {
if (pendingStart) { if (pendingStart) {
return return;
} }
pendingStart = true pendingStart = true;
try { try {
const started = await tcpPortUsed.check(port, '127.0.0.1') const started = await tcpPortUsed.check(port, '127.0.0.1');
if (started) { 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} (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}/${plebbitRpcAuthKey} (secret auth key for remote connections)`);
plebbitWebSocketServer.ws.on('connection', (socket, request) => { 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 // debug raw JSON RPC messages in console
if (isDev) { 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) { pendingStart = false;
console.log('failed starting plebbit rpc server', e) };
}
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 // in case it was started by another client that shut down and shut down the server with it
start() start();
setInterval(() => { setInterval(() => {
start() start();
}, 1000) }, 1000);
+3 -3
View File
@@ -67,7 +67,7 @@
"electron:before:download-ipfs": "node electron/download-ipfs", "electron:before:download-ipfs": "node electron/download-ipfs",
"electron:before:delete-data": "rimraf .plebbit", "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'", "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/**/*.{js,jsx} --write", "prettier": "prettier {src,electron}/**/*.{js,jsx} --write",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0" "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0"
}, },
"eslintConfig": { "eslintConfig": {
@@ -149,8 +149,8 @@
"styled-components": "^5" "styled-components": "^5"
}, },
"lint-staged": { "lint-staged": {
"{src,test,config}/**/*.{cjs,js,jsx,ts,tsx}": [ "{src,electron}/**/*.{js,jsx}": [
"prettier --config prettier.config.js --write" "prettier --write"
] ]
}, },
"husky": { "husky": {
+56 -58
View File
@@ -4,20 +4,24 @@ import useGeneralStore from '../../hooks/stores/useGeneralStore';
import Modal from 'react-modal'; import Modal from 'react-modal';
import Draggable from 'react-draggable'; import Draggable from 'react-draggable';
const CaptchaModal = () => { const CaptchaModal = () => {
const { const {
challengesArray, setChallengesArray, challengesArray,
setChallengesArray,
pendingComment, pendingComment,
selectedStyle, selectedStyle,
setCaptchaResponse, setCaptchaResponse,
isAuthorDelete, setIsAuthorDelete, isAuthorDelete,
isAuthorEdit, setIsAuthorEdit, setIsAuthorDelete,
isCaptchaOpen, setIsCaptchaOpen, isAuthorEdit,
isModEdit, setIsModEdit, setIsAuthorEdit,
isCaptchaOpen,
setIsCaptchaOpen,
isModEdit,
setIsModEdit,
resolveCaptchaPromise, resolveCaptchaPromise,
selectedShortCid, selectedShortCid,
} = useGeneralStore(state => state); } = useGeneralStore((state) => state);
const [imageSources, setImageSources] = useState([]); const [imageSources, setImageSources] = useState([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@@ -28,7 +32,6 @@ const CaptchaModal = () => {
const nodeRef = useRef(null); const nodeRef = useRef(null);
const [isPromiseResolved, setIsPromiseResolved] = useState(false); const [isPromiseResolved, setIsPromiseResolved] = useState(false);
useEffect(() => { useEffect(() => {
if (!isCaptchaOpen) { if (!isCaptchaOpen) {
setIsAuthorDelete(false); setIsAuthorDelete(false);
@@ -37,7 +40,6 @@ const CaptchaModal = () => {
} }
}, [isCaptchaOpen, setIsAuthorDelete, setIsAuthorEdit, setIsModEdit]); }, [isCaptchaOpen, setIsAuthorDelete, setIsAuthorEdit, setIsModEdit]);
useEffect(() => { useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth <= 480); const handleResize = () => setIsMobile(window.innerWidth <= 480);
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
@@ -48,7 +50,6 @@ const CaptchaModal = () => {
}; };
}, [setIsMobile]); }, [setIsMobile]);
useEffect(() => { useEffect(() => {
if (isCaptchaOpen && challengesArray) { if (isCaptchaOpen && challengesArray) {
setIsLoading(true); setIsLoading(true);
@@ -67,9 +68,8 @@ const CaptchaModal = () => {
} }
}, [challengesArray, isCaptchaOpen]); }, [challengesArray, isCaptchaOpen]);
const handleKeyDown = (event) => { const handleKeyDown = (event) => {
if (event.key === "Enter") { if (event.key === 'Enter') {
event.preventDefault(); event.preventDefault();
submitCaptcha((response) => { submitCaptcha((response) => {
setCaptchaResponse(response); setCaptchaResponse(response);
@@ -78,15 +78,13 @@ const CaptchaModal = () => {
} }
}; };
const handleReturnKeyDown = () => { const handleReturnKeyDown = () => {
submitCaptcha((response) => { submitCaptcha((response) => {
setCaptchaResponse(response); setCaptchaResponse(response);
resolveCaptchaPromise(response); resolveCaptchaPromise(response);
}); });
}; };
const submitCaptcha = (callback) => { const submitCaptcha = (callback) => {
if (!isPromiseResolved) { if (!isPromiseResolved) {
setCaptchaResponse(responseRef.current.value); setCaptchaResponse(responseRef.current.value);
@@ -116,76 +114,76 @@ const CaptchaModal = () => {
setIsCaptchaOpen(false); setIsCaptchaOpen(false);
}; };
return ( return (
<StyledModal <StyledModal
isOpen={isCaptchaOpen} isOpen={isCaptchaOpen}
onRequestClose={() => { onRequestClose={() => {
handleCloseModal(); handleCloseModal();
submitCaptcha();}} submitCaptcha();
contentLabel="Captcha Modal" }}
shouldCloseOnEsc={false} contentLabel='Captcha Modal'
shouldCloseOnOverlayClick={false} shouldCloseOnEsc={false}
selectedStyle={selectedStyle} shouldCloseOnOverlayClick={false}
overlayClassName="hide-modal-overlay"> selectedStyle={selectedStyle}
<Draggable handle=".modal-header" nodeRef={nodeRef} disabled={isMobile}> overlayClassName='hide-modal-overlay'
<div className="modal-content" ref={nodeRef}> >
<div className="modal-header"> <Draggable handle='.modal-header' nodeRef={nodeRef} disabled={isMobile}>
{isModEdit ? "Challenge for Moderator Action" : <div className='modal-content' ref={nodeRef}>
isAuthorEdit ? "Challenge for Editing Post" : <div className='modal-header'>
isAuthorDelete ? "Challenge for Deleting Post" : {isModEdit
pendingComment.parentCid ? ? 'Challenge for Moderator Action'
("Challenges for Reply to c/" + selectedShortCid) : : isAuthorEdit
"Challenges for New Thread"} ? 'Challenge for Editing Post'
<button className="icon" onClick={() => handleCloseModal()} title="close" /> : 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>
<div id="form"> <div id='form'>
{pendingComment.author?.displayName ? ( {pendingComment.author?.displayName ? (
<div> <div>
<input id="field" type="text" placeholder={pendingComment.author?.displayName || ''} disabled /> <input id='field' type='text' placeholder={pendingComment.author?.displayName || ''} disabled />
</div> </div>
) : null} ) : null}
{pendingComment.title ? ( {pendingComment.title ? (
<div> <div>
<input id="field" type="text" placeholder={pendingComment.title || ''} disabled /> <input id='field' type='text' placeholder={pendingComment.title || ''} disabled />
</div> </div>
) : null} ) : null}
{pendingComment.content ? ( {pendingComment.content ? (
<div> <div>
<textarea <textarea rows='4' placeholder={pendingComment.content || 'Comment'} wrap='soft' disabled />
rows="4"
placeholder={pendingComment.content || "Comment"}
wrap="soft"
disabled
/>
</div> </div>
) : null} ) : null}
{pendingComment.link ? ( {pendingComment.link ? (
<div> <div>
<input id="field" type="text" placeholder={pendingComment.link || ''} disabled /> <input id='field' type='text' placeholder={pendingComment.link || ''} disabled />
</div> </div>
) : null} ) : null}
<div id="captcha-container"> <div id='captcha-container'>
<input <input
id="response" id='response'
type="text" type='text'
autoComplete='off' autoComplete='off'
placeholder="TYPE THE CAPTCHA HERE AND PRESS ENTER" placeholder='TYPE THE CAPTCHA HERE AND PRESS ENTER'
ref={responseRef} ref={responseRef}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
autoFocus /> autoFocus
/>
{isLoading ? ( {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>
<div> <div>
<span style={{lineHeight: '1.7'}}> <span style={{ lineHeight: '1.7' }}>
Challenge {currentChallengeIndex + 1} of {totalChallenges} Challenge {currentChallengeIndex + 1} of {totalChallenges}
</span> </span>
<button <button
id="nav" id='nav'
onClick={() => { onClick={() => {
if (currentChallengeIndex + 1 < totalChallenges) { if (currentChallengeIndex + 1 < totalChallenges) {
setCurrentChallengeIndex((currentChallengeIndex + 1) % totalChallenges); setCurrentChallengeIndex((currentChallengeIndex + 1) % totalChallenges);
@@ -194,7 +192,7 @@ const CaptchaModal = () => {
} }
}} }}
> >
{currentChallengeIndex + 1 < totalChallenges ? "Next" : "Submit"} {currentChallengeIndex + 1 < totalChallenges ? 'Next' : 'Submit'}
</button> </button>
</div> </div>
</div> </div>
+54 -107
View File
@@ -1,8 +1,10 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useAccount, useCreateSubplebbit, import {
useAccount,
useCreateSubplebbit,
// useSubplebbits, useAccountSubplebbits // useSubplebbits, useAccountSubplebbits
} from '@plebbit/plebbit-react-hooks'; } from '@plebbit/plebbit-react-hooks';
import { StyledModal } from '../styled/modals/CreateBoardModal.styled'; import { StyledModal } from '../styled/modals/CreateBoardModal.styled';
import useGeneralStore from '../../hooks/stores/useGeneralStore'; import useGeneralStore from '../../hooks/stores/useGeneralStore';
import Modal from 'react-modal'; import Modal from 'react-modal';
@@ -10,7 +12,7 @@ import useError from '../../hooks/useError';
import useSuccess from '../../hooks/useSuccess'; import useSuccess from '../../hooks/useSuccess';
const CreateBoardModal = ({ isOpen, closeModal }) => { const CreateBoardModal = ({ isOpen, closeModal }) => {
const { selectedStyle } = useGeneralStore(state => state); const { selectedStyle } = useGeneralStore((state) => state);
const account = useAccount(); const account = useAccount();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -29,7 +31,6 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
const [rules, setRules] = useState([]); const [rules, setRules] = useState([]);
const [editIndex, setEditIndex] = useState(-1); const [editIndex, setEditIndex] = useState(-1);
const handleAddRule = () => { const handleAddRule = () => {
if (rule.trim() === '') { if (rule.trim() === '') {
setNewErrorMessage('Rule field is empty'); setNewErrorMessage('Rule field is empty');
@@ -41,15 +42,14 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
return; return;
} }
if(editIndex > -1){ if (editIndex > -1) {
setRules(rules.map((r, i) => i === editIndex ? rule : r)); setRules(rules.map((r, i) => (i === editIndex ? rule : r)));
setEditIndex(-1); setEditIndex(-1);
} else { } else {
setRules([...rules, rule]); setRules([...rules, rule]);
} }
setRule(''); setRule('');
} };
useEffect(() => { useEffect(() => {
if (ruleInputRef.current) { if (ruleInputRef.current) {
@@ -57,39 +57,34 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
} }
}, [rules]); }, [rules]);
const handleKeyDown = (e) => { const handleKeyDown = (e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
handleAddRule(); handleAddRule();
} }
} };
const handleEditRule = (index) => { const handleEditRule = (index) => {
setRule(rules[index]); setRule(rules[index]);
setEditIndex(index); setEditIndex(index);
ruleInputRef.current.focus(); ruleInputRef.current.focus();
} };
const handleDeleteRule = (index) => { const handleDeleteRule = (index) => {
if (index === editIndex) { if (index === editIndex) {
setEditIndex(-1); setEditIndex(-1);
} }
setRules(rules.filter((_, i) => i !== index)); setRules(rules.filter((_, i) => i !== index));
} };
const createSubplebbitOptions = { const createSubplebbitOptions = {
title: title || undefined, title: title || undefined,
description: description || undefined, description: description || undefined,
suggested: { suggested: {
avatarUrl : avatar || undefined, avatarUrl: avatar || undefined,
}, },
roles: moderators || undefined, roles: moderators || undefined,
rules: rules || undefined, rules: rules || undefined,
} };
const resetFields = () => { const resetFields = () => {
setTitle(''); setTitle('');
@@ -98,23 +93,21 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
setModerators(''); setModerators('');
setRule(''); setRule('');
setRules([]); setRules([]);
} };
const { createdSubplebbit, createSubplebbit } = useCreateSubplebbit(createSubplebbitOptions); const { createdSubplebbit, createSubplebbit } = useCreateSubplebbit(createSubplebbitOptions);
const handleCreateBoard = async () => { const handleCreateBoard = async () => {
let moderatorAddresses = moderators.trim() ? moderators.split(',').map(addr => addr.trim()) : []; 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 invalidAddresses = moderatorAddresses.filter((addr) => !(addr.endsWith('.eth') || (addr.startsWith('12D3KooW') && addr.length === 52)));
if (invalidAddresses.length > 0) { if (invalidAddresses.length > 0) {
setNewErrorMessage("Invalid moderator addresses: " + invalidAddresses.join(", ")); setNewErrorMessage('Invalid moderator addresses: ' + invalidAddresses.join(', '));
return; return;
} }
const roles = {}; const roles = {};
moderatorAddresses.forEach(addr => { moderatorAddresses.forEach((addr) => {
roles[addr] = { role: 'moderator' }; roles[addr] = { role: 'moderator' };
}); });
roles[account.author.address] = { role: 'admin' }; roles[account.author.address] = { role: 'admin' };
@@ -133,7 +126,7 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
if (avatar) { if (avatar) {
createSubplebbitOptions.suggested = { createSubplebbitOptions.suggested = {
avatarUrl : avatar, avatarUrl: avatar,
}; };
} }
@@ -153,7 +146,7 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
setNewSuccessMessage('Board created successfully, address: ' + createdSubplebbit.address); setNewSuccessMessage('Board created successfully, address: ' + createdSubplebbit.address);
navigate(`/p/${createdSubplebbit.address}`); navigate(`/p/${createdSubplebbit.address}`);
} }
} };
// remove after testing: // remove after testing:
// const {accountSubplebbits} = useAccountSubplebbits() // const {accountSubplebbits} = useAccountSubplebbits()
@@ -165,114 +158,68 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
<StyledModal <StyledModal
isOpen={isOpen} isOpen={isOpen}
onRequestClose={closeModal} onRequestClose={closeModal}
contentLabel="Create Board" contentLabel='Create Board'
selectedStyle={selectedStyle} 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-content' ref={nodeRef}>
<div className="modal-header"> <div className='modal-header'>
Create Board Create Board
<button className="icon" onClick={() => closeModal()} title="close" /> <button className='icon' onClick={() => closeModal()} title='close' />
</div> </div>
<ul id="form"> <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> <div id='explaination'>
<li className='settings-option disc'> 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.
Title
</li>
<div className='settings-tip'>
Optional, useful to describe the board next to its p/address.
</div> </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'> <div className='settings-input'>
<input <input id='name' type='text' value={title} onChange={(e) => setTitle(e.target.value)} placeholder='Board Title' />
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>
<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'> <div className='settings-input'>
<textarea <textarea id='description' type='text' value={description} onChange={(e) => setDescription(e.target.value)} placeholder='Add a description' />
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>
<li className='settings-option disc'>Avatar</li>
<div className='settings-tip'>Optional, set an image that represents your board.</div>
<div className='settings-input'> <div className='settings-input'>
<input <input id='name' type='text' value={avatar} onChange={(e) => setAvatar(e.target.value)} placeholder='https://example.com/image.png' />
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>
<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'> <div className='settings-input'>
<textarea <textarea id='name' type='text' value={moderators} onChange={(e) => setModerators(e.target.value)} placeholder='username.eth, 12D3KooW..., username2.eth' />
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>
<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'> <div className='settings-input'>
<input <input ref={ruleInputRef} id='rule' type='text' value={rule} onChange={(e) => setRule(e.target.value)} onKeyDown={handleKeyDown} placeholder='Add a rule' />
ref={ruleInputRef}
id="rule"
type="text"
value={rule}
onChange={(e) => setRule(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Add a rule"
/>
</div> </div>
<button style={{display: rules.length > 0 ? 'none' : 'block'}} id="rule-btn" className={rules.length > 0 ? "relative" : ""} onClick={handleAddRule}> <button style={{ display: rules.length > 0 ? 'none' : 'block' }} id='rule-btn' className={rules.length > 0 ? 'relative' : ''} onClick={handleAddRule}>
{editIndex > -1 ? "Update Rule" : "Add Rule"} {editIndex > -1 ? 'Update Rule' : 'Add Rule'}
</button> </button>
{rules.length > 0 && ( {rules.length > 0 && (
<fieldset> <fieldset>
<legend> <legend>
<button className={rules.length > 0 ? "relative" : ""} onClick={handleAddRule}> <button className={rules.length > 0 ? 'relative' : ''} onClick={handleAddRule}>
{editIndex > -1 ? "Update Rule" : "Add Rule"} {editIndex > -1 ? 'Update Rule' : 'Add Rule'}
</button> </button>
</legend> </legend>
{rules.map((rule, index) => ( {rules.map((rule, index) => (
<div key={index} className="rule-item"> <div key={index} className='rule-item'>
<p>{index+1}. {rule}</p> <p>
{index + 1}. {rule}
</p>
<button onClick={() => handleEditRule(index)}>Edit</button> <button onClick={() => handleEditRule(index)}>Edit</button>
<button onClick={() => handleDeleteRule(index)}>Delete</button> <button onClick={() => handleDeleteRule(index)}>Delete</button>
</div> </div>
))} ))}
</fieldset> </fieldset>
)} )}
<button onClick={handleCreateBoard} id="create-board-btn">Create Board</button> <button onClick={handleCreateBoard} id='create-board-btn'>
Create Board
</button>
</ul> </ul>
</div> </div>
</StyledModal> </StyledModal>
+27 -29
View File
@@ -4,19 +4,14 @@ import useGeneralStore from '../../hooks/stores/useGeneralStore';
import Modal from 'react-modal'; import Modal from 'react-modal';
import Draggable from 'react-draggable'; import Draggable from 'react-draggable';
const EditModal = ({ isOpen, closeModal, originalCommentContent }) => { const EditModal = ({ isOpen, closeModal, originalCommentContent }) => {
const { const { setEditedComment, selectedStyle } = useGeneralStore((state) => state);
setEditedComment,
selectedStyle,
} = useGeneralStore(state => state);
const nodeRef = useRef(null); const nodeRef = useRef(null);
const commentRef = useRef(); const commentRef = useRef();
const [isMobile, setIsMobile] = useState(window.innerWidth <= 480); const [isMobile, setIsMobile] = useState(window.innerWidth <= 480);
useEffect(() => { useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth <= 480); const handleResize = () => setIsMobile(window.innerWidth <= 480);
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
@@ -26,40 +21,43 @@ const EditModal = ({ isOpen, closeModal, originalCommentContent }) => {
}; };
}, [setIsMobile]); }, [setIsMobile]);
const handleSaveEdit = () => { const handleSaveEdit = () => {
setEditedComment(commentRef.current.value); setEditedComment(commentRef.current.value);
closeModal(); closeModal();
}; };
return ( return (
<StyledModal <StyledModal
isOpen={isOpen} isOpen={isOpen}
onRequestClose={closeModal} onRequestClose={closeModal}
contentLabel="Edit Comment" contentLabel='Edit Comment'
shouldCloseOnEsc={false} shouldCloseOnEsc={false}
shouldCloseOnOverlayClick={isMobile} shouldCloseOnOverlayClick={isMobile}
selectedStyle={selectedStyle} selectedStyle={selectedStyle}
style={isMobile ? ({ overlay: { backgroundColor: "rgba(0,0,0,.25)" }}) : ({ overlay: { backgroundColor: "rgba(0,0,0,0)" }})}> 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}> <Draggable handle='.modal-header' nodeRef={nodeRef} disabled={isMobile}>
<div className="modal-header"> <div className='modal-content' ref={nodeRef}>
<div className='modal-header'>
Edit Comment Edit Comment
<button className="icon" onClick={() => closeModal()} title="close" /> <button className='icon' onClick={() => closeModal()} title='close' />
</div> </div>
<div id="form"> <div id='form'>
<div className="textarea-wrapper"> <div className='textarea-wrapper'>
<textarea className="textarea" <textarea
rows="4" className='textarea'
style={{paddingTop: '0'}} rows='4'
placeholder="Comment" style={{ paddingTop: '0' }}
defaultValue={originalCommentContent} placeholder='Comment'
wrap="soft" defaultValue={originalCommentContent}
ref={commentRef} /> wrap='soft'
ref={commentRef}
/>
</div> </div>
<div> <div>
<button id="next" onClick={handleSaveEdit}>Save</button> <button id='next' onClick={handleSaveEdit}>
Save
</button>
</div> </div>
</div> </div>
</div> </div>
+50 -86
View File
@@ -1,26 +1,17 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react';
import Modal from "react-modal"; import Modal from 'react-modal';
import { Link } from "react-router-dom"; import { Link } from 'react-router-dom';
import { useComment, usePublishCommentEdit } from "@plebbit/plebbit-react-hooks"; import { useComment, usePublishCommentEdit } from '@plebbit/plebbit-react-hooks';
import { StyledModal } from "../styled/modals/ModerationModal.styled"; import { StyledModal } from '../styled/modals/ModerationModal.styled';
import useGeneralStore from "../../hooks/stores/useGeneralStore"; import useGeneralStore from '../../hooks/stores/useGeneralStore';
import useError from "../../hooks/useError"; import useError from '../../hooks/useError';
import useSuccess from "../../hooks/useSuccess"; import useSuccess from '../../hooks/useSuccess';
const ModerationModal = ({ isOpen, closeModal, deletePost }) => { const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
const { const { selectedAddress, selectedStyle, setCaptchaResponse, setChallengesArray, setIsCaptchaOpen, setIsModEdit, moderatingCommentCid, setResolveCaptchaPromise } =
selectedAddress, useGeneralStore((state) => state);
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 [pin, setPin] = useState(comment.pinned);
const [deleteThread, setDeleteThread] = useState(deletePost); const [deleteThread, setDeleteThread] = useState(deletePost);
@@ -31,56 +22,50 @@ const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
const [, setNewErrorMessage] = useError(); const [, setNewErrorMessage] = useError();
const [, setNewSuccessMessage] = useSuccess(); const [, setNewSuccessMessage] = useSuccess();
useEffect(() => { useEffect(() => {
setPin(comment.pinned); setPin(comment.pinned);
setClose(comment.locked); setClose(comment.locked);
}, [comment]); }, [comment]);
useEffect(() => { useEffect(() => {
setDeleteThread(deletePost); setDeleteThread(deletePost);
}, [deletePost]); }, [deletePost]);
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
setDeleteThread(deletePost); setDeleteThread(deletePost);
} }
}, [isOpen, deletePost]); }, [isOpen, deletePost]);
const handleCloseModal = () => { const handleCloseModal = () => {
setDeleteThread(false); setDeleteThread(false);
closeModal(); closeModal();
}; };
const onChallengeVerification = (challengeVerification) => { const onChallengeVerification = (challengeVerification) => {
if (challengeVerification.challengeSuccess === true) { 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) { } else if (challengeVerification.challengeSuccess === false) {
setNewErrorMessage(`Challenge Failed, reason: ${challengeVerification.reason}. Errors: ${challengeVerification.errors}`); setNewErrorMessage(`Challenge Failed, reason: ${challengeVerification.reason}. Errors: ${challengeVerification.errors}`);
console.log('challenge failed', challengeVerification); console.log('challenge failed', challengeVerification);
} }
}; };
const onChallenge = async (challenges, comment) => { const onChallenge = async (challenges, comment) => {
let challengeAnswers = []; let challengeAnswers = [];
try { try {
challengeAnswers = await getChallengeAnswersFromUser(challenges) challengeAnswers = await getChallengeAnswersFromUser(challenges);
} } catch (error) {
catch (error) { setNewErrorMessage(error.message);
setNewErrorMessage(error.message); console.log(error); console.log(error);
} }
if (challengeAnswers) { if (challengeAnswers) {
await comment.publishChallengeAnswers(challengeAnswers) await comment.publishChallengeAnswers(challengeAnswers);
} }
}; };
const getChallengeAnswersFromUser = async (challenges) => { const getChallengeAnswersFromUser = async (challenges) => {
setChallengesArray(challenges); setChallengesArray(challenges);
@@ -115,19 +100,17 @@ const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
}); });
}; };
const [publishCommentEditOptions, setPublishCommentEditOptions] = useState({ const [publishCommentEditOptions, setPublishCommentEditOptions] = useState({
commentCid: moderatingCommentCid, commentCid: moderatingCommentCid,
subplebbitAddress: selectedAddress, subplebbitAddress: selectedAddress,
onChallenge, onChallenge,
onChallengeVerification, onChallengeVerification,
onError: (error) => { onError: (error) => {
setNewErrorMessage(error.message); console.log(error); setNewErrorMessage(error.message);
console.log(error);
}, },
}); });
useEffect(() => { useEffect(() => {
if (selectedAddress) { if (selectedAddress) {
setPublishCommentEditOptions((prevOptions) => ({ setPublishCommentEditOptions((prevOptions) => ({
@@ -137,10 +120,8 @@ const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
} }
}, [selectedAddress]); }, [selectedAddress]);
const { publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions); const { publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions);
useEffect(() => { useEffect(() => {
setPublishCommentEditOptions((prevOptions) => ({ setPublishCommentEditOptions((prevOptions) => ({
...prevOptions, ...prevOptions,
@@ -148,7 +129,6 @@ const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
})); }));
}, [moderatingCommentCid]); }, [moderatingCommentCid]);
useEffect(() => { useEffect(() => {
let isActive = true; let isActive = true;
if (publishCommentEditOptions && triggerPublishCommentEdit) { if (publishCommentEditOptions && triggerPublishCommentEdit) {
@@ -165,80 +145,64 @@ const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
}; };
}, [triggerPublishCommentEdit, publishCommentEdit, publishCommentEditOptions]); }, [triggerPublishCommentEdit, publishCommentEdit, publishCommentEditOptions]);
return ( return (
<StyledModal <StyledModal
isOpen={isOpen} isOpen={isOpen}
onRequestClose={handleCloseModal} onRequestClose={handleCloseModal}
contentLabel="Moderator Tools" contentLabel='Moderator Tools'
style={{ overlay: { backgroundColor: "rgba(0,0,0,.25)" }}} style={{ overlay: { backgroundColor: 'rgba(0,0,0,.25)' } }}
selectedStyle={selectedStyle} selectedStyle={selectedStyle}
> >
<div className="panel"> <div className='panel'>
<div className="panel-header"> <div className='panel-header'>
Moderator Tools Moderator Tools
<Link to="" onClick={handleCloseModal}> <Link to='' onClick={handleCloseModal}>
<span className="icon" title="close" /> <span className='icon' title='close' />
</Link> </Link>
</div> </div>
<ul className="settings-cat"> <ul className='settings-cat'>
<li className="settings-cat-lbl"> <li className='settings-cat-lbl'>
<label> <label>
<input type="checkbox" style={{marginRight: "10px"}} <input type='checkbox' style={{ marginRight: '10px' }} checked={pin} onChange={() => setPin(!pin)} />
checked={pin} onChange={() => setPin(!pin)} />
Pin post Pin post
</label> </label>
</li> </li>
<li className="settings-tip"> <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>
Pin the post to make it a sticky, showed at the top of the board even as new posts are submitted.
</li>
</ul> </ul>
<ul className="settings-cat"> <ul className='settings-cat'>
<li className="settings-cat-lbl"> <li className='settings-cat-lbl'>
<label> <label>
<input type="checkbox" style={{marginRight: "10px"}} <input type='checkbox' style={{ marginRight: '10px' }} checked={deleteThread} onChange={() => setDeleteThread(!deleteThread)} />
checked={deleteThread} onChange={() => setDeleteThread(!deleteThread)} />
Delete post Delete post
</label> </label>
</li> </li>
<li className="settings-tip"> <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>
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>
<ul className="settings-cat"> <ul className='settings-cat'>
<li className="settings-cat-lbl"> <li className='settings-cat-lbl'>
<label> <label>
<input type="checkbox" style={{marginRight: "10px"}} <input type='checkbox' style={{ marginRight: '10px' }} checked={close} onChange={() => setClose(!close)} />
checked={close} onChange={() => setClose(!close)} />
Close post Close post
</label> </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> </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>
<ul className="settings-cat"> <ul className='settings-cat'>
<li className="settings-option disc"> <li className='settings-option disc'>Reason</li>
Reason <li className='settings-tip'>Help people become better posters by giving a short reason why their post was removed.</li>
</li> <li className='settings-input' style={{ marginTop: '-10px' }}>
<li className="settings-tip"> <textarea value={reason} placeholder='Enter reason here...' onChange={(e) => setReason(e.target.value)} />
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> </li>
</ul> </ul>
<button <button
className="save-button" className='save-button'
onClick={async () => { onClick={async () => {
setPublishCommentEditOptions(prevOptions => ({ setPublishCommentEditOptions((prevOptions) => ({
...prevOptions, ...prevOptions,
pinned: pin, pinned: pin,
removed: deleteThread, removed: deleteThread,
locked: close, locked: close,
reason: reason reason: reason,
})); }));
setTriggerPublishCommentEdit(true); setTriggerPublishCommentEdit(true);
setIsModEdit(true); setIsModEdit(true);
@@ -250,8 +214,8 @@ const ModerationModal = ({ isOpen, closeModal, deletePost }) => {
</div> </div>
</StyledModal> </StyledModal>
); );
} };
Modal.setAppElement("#root"); Modal.setAppElement('#root');
export default ModerationModal; export default ModerationModal;
+22 -24
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 { StyledModal } from '../styled/modals/ReplyModal.styled';
import useGeneralStore from "../../hooks/stores/useGeneralStore"; import useGeneralStore from '../../hooks/stores/useGeneralStore';
import Modal from "react-modal"; import Modal from 'react-modal';
import Draggable from "react-draggable"; import Draggable from 'react-draggable';
import getDate from "../../utils/getDate"; import getDate from '../../utils/getDate';
const OriginalCommentModal = ({ isOpen, closeModal, comment }) => { const OriginalCommentModal = ({ isOpen, closeModal, comment }) => {
const { const { selectedStyle } = useGeneralStore((state) => state);
selectedStyle,
} = useGeneralStore(state => state);
const [isMobile, setIsMobile] = useState(window.innerWidth <= 480); const [isMobile, setIsMobile] = useState(window.innerWidth <= 480);
const nodeRef = React.useRef(null); const nodeRef = React.useRef(null);
const originalCommentContent = comment.original?.content; const originalCommentContent = comment.original?.content;
const timestamp = getDate(comment.timestamp); const timestamp = getDate(comment.timestamp);
useEffect(() => { useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth <= 480); const handleResize = () => setIsMobile(window.innerWidth <= 480);
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
@@ -26,31 +22,33 @@ const OriginalCommentModal = ({ isOpen, closeModal, comment }) => {
}; };
}, [setIsMobile]); }, [setIsMobile]);
return ( return (
<StyledModal <StyledModal
isOpen={isOpen} isOpen={isOpen}
onRequestClose={closeModal} onRequestClose={closeModal}
contentLabel="Original Comment" contentLabel='Original Comment'
shouldCloseOnEsc={false} shouldCloseOnEsc={false}
shouldCloseOnOverlayClick={isMobile} shouldCloseOnOverlayClick={isMobile}
selectedStyle={selectedStyle} selectedStyle={selectedStyle}
style={isMobile ? ({ overlay: { backgroundColor: "rgba(0,0,0,.25)" }}) : ({ overlay: { backgroundColor: "rgba(0,0,0,0)" }})}> 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}> <Draggable handle='.modal-header' nodeRef={nodeRef} disabled={isMobile}>
<div className="modal-header"> <div className='modal-content' ref={nodeRef}>
<div className='modal-header'>
Original at {timestamp} Original at {timestamp}
<button className="icon" onClick={() => closeModal()} title="close" /> <button className='icon' onClick={() => closeModal()} title='close' />
</div> </div>
<div id="form"> <div id='form'>
<div className="textarea-wrapper"> <div className='textarea-wrapper'>
<textarea className="textarea" <textarea
rows="4" className='textarea'
rows='4'
style={{ paddingTop: '0' }} style={{ paddingTop: '0' }}
placeholder="Comment" placeholder='Comment'
defaultValue={originalCommentContent} defaultValue={originalCommentContent}
wrap="soft" wrap='soft'
readOnly={true} /> readOnly={true}
/>
</div> </div>
</div> </div>
</div> </div>
+2 -3
View File
@@ -1,7 +1,6 @@
import styled from "styled-components"; import styled from 'styled-components';
export const BoardStatsContainer = styled.div` export const BoardStatsContainer = styled.div`
@media (max-width: 480px) { @media (max-width: 480px) {
display: none; display: none;
} }
@@ -96,7 +95,7 @@ export const BoardStatsContainer = styled.div`
}`; }`;
default: default:
return ''; return '';
} }
}} }}
`; `;
+2 -2
View File
@@ -1,5 +1,5 @@
import { ToastContainer } from "react-toastify"; import { ToastContainer } from 'react-toastify';
import styled from "styled-components"; import styled from 'styled-components';
export const Toast = styled(ToastContainer)` export const Toast = styled(ToastContainer)`
.Toastify__toast { .Toastify__toast {
@@ -23,9 +23,6 @@ export const AlertModal = styled.div`
} }
} }
${({ selectedStyle }) => { ${({ selectedStyle }) => {
switch (selectedStyle) { switch (selectedStyle) {
case 'Yotsuba': case 'Yotsuba':
@@ -70,8 +67,8 @@ export const AlertModal = styled.div`
border: 1px solid #ccc; border: 1px solid #ccc;
}`; }`;
default: default:
return ''; return '';
} }
}} }}
`; `;
+12 -10
View File
@@ -1,15 +1,17 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
const useAnonModeStore = create(persist( const useAnonModeStore = create(
(set) => ({ persist(
anonymousMode: true, (set) => ({
setAnonymousMode: (mode) => set({ anonymousMode: mode }), anonymousMode: true,
}), setAnonymousMode: (mode) => set({ anonymousMode: mode }),
{ }),
name: "anonmode_store", {
getStorage: () => localStorage, name: 'anonmode_store',
} getStorage: () => localStorage,
)); },
),
);
export default useAnonModeStore; export default useAnonModeStore;
+2 -2
View File
@@ -21,14 +21,14 @@ root.render(
<App /> <App />
</HashRouter> </HashRouter>
</HelmetProvider> </HelmetProvider>
</React.StrictMode> </React.StrictMode>,
); );
// set up PWA https://cra.link/PWA // set up PWA https://cra.link/PWA
serviceWorkerRegistration.register(); serviceWorkerRegistration.register();
// add back button in android app // add back button in android app
CapacitorApp.addListener('backButton', ({canGoBack}) => { CapacitorApp.addListener('backButton', ({ canGoBack }) => {
if (canGoBack) { if (canGoBack) {
window.history.back(); window.history.back();
} else { } else {
+3 -3
View File
@@ -20,7 +20,7 @@ registerRoute(
// match index.html // match index.html
({ url }) => url.pathname === '/', ({ url }) => url.pathname === '/',
// serve cached index.html first but revalidate in background // serve cached index.html first but revalidate in background
new StaleWhileRevalidate() new StaleWhileRevalidate(),
); );
// Precache all of the assets generated by your build process. // Precache all of the assets generated by your build process.
@@ -51,7 +51,7 @@ registerRoute(
return true; 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 // An example runtime caching route for requests that aren't handled by the
@@ -66,7 +66,7 @@ registerRoute(
// least-recently used images are removed. // least-recently used images are removed.
new ExpirationPlugin({ maxEntries: 50 }), new ExpirationPlugin({ maxEntries: 50 }),
], ],
}) }),
); );
// This allows the web app to trigger skipWaiting via // 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. // [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' || window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4. // 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) { export function register(config) {
@@ -39,10 +39,7 @@ export function register(config) {
// Add some additional logging to localhost, pointing developers to the // Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation. // service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => { navigator.serviceWorker.ready.then(() => {
console.log( console.log('This web app is being served cache-first by a service ' + 'worker. To learn more, visit https://cra.link/PWA');
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://cra.link/PWA'
);
}); });
} else { } else {
// Is not localhost. Just register service worker // 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, // At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older // but the previous service worker will still serve the older
// content until all client tabs are closed. // content until all client tabs are closed.
console.log( console.log('New content is available and will be used when all ' + 'tabs for this page are closed. See https://cra.link/PWA.');
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://cra.link/PWA.'
);
// Execute callback // Execute callback
if (config && config.onUpdate) { if (config && config.onUpdate) {
@@ -104,10 +98,7 @@ function checkValidServiceWorker(swUrl, config) {
.then((response) => { .then((response) => {
// Ensure service worker exists, and that we really are getting a JS file. // Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type'); const contentType = response.headers.get('content-type');
if ( if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page. // No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => { navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => { registration.unregister().then(() => {