diff --git a/electron/after-all-artifact-build.js b/electron/after-all-artifact-build.cjs similarity index 60% rename from electron/after-all-artifact-build.js rename to electron/after-all-artifact-build.cjs index 6cbe6039..7b684bc2 100644 --- a/electron/after-all-artifact-build.js +++ b/electron/after-all-artifact-build.cjs @@ -1,14 +1,11 @@ -// hook that runs after electron-build - -import fs from 'fs-extra'; -import path from 'path'; -import { execSync } from 'child_process'; -import packageJson from '../package.json' assert { type: 'json' }; -import { fileURLToPath } from 'url'; -const rootPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const fs = require('fs-extra'); +const path = require('path'); +const { execSync } = require('child_process'); +const packageJson = require('../package.json'); +const rootPath = path.resolve(__dirname, '..'); const distFolderPath = path.resolve(rootPath, 'dist'); -const addPortableToPortableExecutableFileName = () => { +function addPortableToPortableExecutableFileName() { const files = fs.readdirSync(distFolderPath); for (const file of files) { if (file.endsWith('.exe') && !file.match('Setup')) { @@ -17,9 +14,9 @@ const addPortableToPortableExecutableFileName = () => { fs.moveSync(filePath, renamedFilePath); } } -}; +} -const createHtmlArchive = () => { +function createHtmlArchive() { if (process.platform !== 'linux') { return; } @@ -28,17 +25,14 @@ const createHtmlArchive = () => { const outputFile = path.resolve(distFolderPath, `${plebchanHtmlFolderName}.zip`); const inputFolder = path.resolve(rootPath, 'build'); try { - // will break if node_modules/7zip-bin changes execSync(`${zipBinPath} a ${outputFile} ${inputFolder}`); - // rename 'build' folder to 'plebchan-html-version' inside the archive execSync(`${zipBinPath} rn -r ${outputFile} build ${plebchanHtmlFolderName}`); } catch (e) { - e.message = 'electron build createHtmlArchive error: ' + e.message; - console.log(e); + console.error('electron build createHtmlArchive error:', e); } -}; +} -export default async (buildResult) => { +module.exports = async function afterAllArtifactBuild(buildResult) { addPortableToPortableExecutableFileName(); createHtmlArchive(); -}; +}; \ No newline at end of file diff --git a/electron/before-pack.js b/electron/before-pack.js index 135ff946..1ceef74a 100644 --- a/electron/before-pack.js +++ b/electron/before-pack.js @@ -24,12 +24,20 @@ const ipfsClientMacUrl = `https://dist.ipfs.io/kubo/v${ipfsClientVersion}/kubo_v const ipfsClientLinuxUrl = `https://dist.ipfs.io/kubo/v${ipfsClientVersion}/kubo_v${ipfsClientVersion}_linux-amd64.tar.gz`; const downloadWithProgress = (url) => - new Promise((resolve) => { + new Promise((resolve, reject) => { const split = url.split('/'); const fileName = split[split.length - 1]; const chunks = []; const req = https.request(url); + req.on('error', (err) => { + console.error(`Error making request for ${url}:`, err); + reject(err); + }); req.on('response', (res) => { + res.on('error', (err) => { + console.error(`Error in response for ${url}:`, err); + reject(err); + }); // handle redirects if (res.statusCode == 301 || res.statusCode === 302) { resolve(downloadWithProgress(res.headers.location)); @@ -43,6 +51,7 @@ const downloadWithProgress = (url) => incomplete: ' ', width: 20, total: len, + stream: process.stdout, }); res.on('data', (chunk) => { chunks.push(chunk); @@ -56,6 +65,20 @@ const downloadWithProgress = (url) => req.end(); }); +// add retry wrapper around downloadWithProgress to handle transient network errors +const downloadWithRetry = async (url, retries = 3) => { + for (let attempt = 1; attempt <= retries; attempt++) { + try { + return await downloadWithProgress(url); + } catch (err) { + console.warn(`Download attempt ${attempt} for ${url} failed:`, err); + if (attempt === retries) throw err; + // wait before retrying + await new Promise((res) => setTimeout(res, attempt * 1000)); + } + } +}; + // plebbit kubo downloads dont need to be extracted const download = async (url, destinationPath) => { let binName = 'ipfs'; @@ -69,8 +92,8 @@ const download = async (url, destinationPath) => { } const split = url.split('/'); const fileName = split[split.length - 1]; - const dowloadPath = path.join(destinationPath, fileName); - const file = await downloadWithProgress(url); + const downloadPath = path.join(destinationPath, fileName); + const file = await downloadWithRetry(url); fs.ensureDirSync(destinationPath); await fs.writeFile(binPath, file); }; @@ -85,24 +108,51 @@ const downloadAndExtract = async (url, destinationPath) => { if (fs.pathExistsSync(binPath)) { return; } + console.log(`Downloading IPFS client from ${url} to ${destinationPath}`); const split = url.split('/'); const fileName = split[split.length - 1]; - const dowloadPath = path.join(destinationPath, fileName); - const file = await downloadWithProgress(url); + const downloadPath = path.join(destinationPath, fileName); + const file = await downloadWithRetry(url); fs.ensureDirSync(destinationPath); - await fs.writeFile(dowloadPath, file); - await decompress(dowloadPath, destinationPath); + await fs.writeFile(downloadPath, file); + console.log(`Downloaded archive to ${downloadPath}`); + console.log(`Extracting ${downloadPath} to ${destinationPath}`); + try { + await decompress(downloadPath, destinationPath); + console.log('Decompression complete'); + } catch (err) { + console.error('Error during decompression:', err); + throw err; + } const extractedPath = path.join(destinationPath, 'kubo'); const extractedBinPath = path.join(extractedPath, binName); + console.log(`Moving binary from ${extractedBinPath} to ${binPath}`); fs.moveSync(extractedBinPath, binPath); - fs.removeSync(extractedPath); - fs.removeSync(dowloadPath); + console.log('Binary moved'); + console.log('Cleaning up temporary files'); + fs.removeSync(downloadPath); + console.log('Cleanup complete'); }; export const downloadIpfsClients = async () => { - await downloadAndExtract(ipfsClientWindowsUrl, ipfsClientWindowsPath); - await downloadAndExtract(ipfsClientMacUrl, ipfsClientMacPath); - await downloadAndExtract(ipfsClientLinuxUrl, ipfsClientLinuxPath); + const platform = process.platform; + console.log(`Starting IPFS client download for platform: ${platform}`); + switch (platform) { + case 'win32': + await downloadAndExtract(ipfsClientWindowsUrl, ipfsClientWindowsPath); + break; + case 'darwin': + await downloadAndExtract(ipfsClientMacUrl, ipfsClientMacPath); + break; + case 'linux': + await downloadAndExtract(ipfsClientLinuxUrl, ipfsClientLinuxPath); + break; + default: + console.warn(`Unknown platform: ${platform}, downloading all IPFS clients`); + await downloadAndExtract(ipfsClientWindowsUrl, ipfsClientWindowsPath); + await downloadAndExtract(ipfsClientMacUrl, ipfsClientMacPath); + await downloadAndExtract(ipfsClientLinuxUrl, ipfsClientLinuxPath); + } }; export default async (context) => { diff --git a/electron/download-ipfs.js b/electron/download-ipfs.js index 710e8b30..02812e84 100644 --- a/electron/download-ipfs.js +++ b/electron/download-ipfs.js @@ -1,2 +1,13 @@ import { downloadIpfsClients } from './before-pack.js'; -downloadIpfsClients(); + +// Wrap the download in an async IIFE and exit when done +(async () => { + try { + await downloadIpfsClients(); + console.log('IPFS clients downloaded successfully.'); + process.exit(0); + } catch (err) { + console.error('IPFS clients download error:', err); + process.exit(1); + } +})(); diff --git a/electron/main.js b/electron/main.js index a90c5d34..cc9b3775 100644 --- a/electron/main.js +++ b/electron/main.js @@ -1,5 +1,5 @@ import './log.js'; -import { app, BrowserWindow, Menu, MenuItem, Tray, screen as electronScreen, shell, dialog, nativeTheme, ipcMain } from 'electron'; +import { app, BrowserWindow, Menu, MenuItem, Tray, shell, dialog, nativeTheme, ipcMain } from 'electron'; import isDev from 'electron-is-dev'; import fs from 'fs'; import path from 'path'; @@ -8,8 +8,13 @@ import startIpfs from './start-ipfs.js'; import './start-plebbit-rpc.js'; import { URL, fileURLToPath } from 'node:url'; import contextMenu from 'electron-context-menu'; -import packageJson from '../package.json' with { type: 'json' }; -const dirname = path.join(path.dirname(fileURLToPath(import.meta.url))); + +// Determine __filename and dirname for ESM +const __filename = fileURLToPath(import.meta.url); +const dirname = path.dirname(__filename); + +// Load package.json dynamically +const packageJson = JSON.parse(fs.readFileSync(path.join(dirname, '../package.json'), 'utf-8')); let startIpfsError; startIpfs.onError = (error) => { @@ -78,7 +83,7 @@ const createMainWindow = () => { nodeIntegration: false, contextIsolation: true, devTools: true, // TODO: change to isDev when no bugs left - preload: path.join(dirname, 'preload.js'), + preload: path.join(dirname, '../build/electron/preload.cjs'), }, }); diff --git a/electron/preload.js b/electron/preload.mjs similarity index 77% rename from electron/preload.js rename to electron/preload.mjs index 40238671..c542bd61 100644 --- a/electron/preload.js +++ b/electron/preload.mjs @@ -1,10 +1,11 @@ -const { contextBridge, ipcRenderer } = require('electron'); +import { contextBridge, ipcRenderer } from 'electron'; // dev uses http://localhost, prod uses file://...index.html const isDev = window.location.protocol === 'http:'; const defaultPlebbitOptions = { plebbitRpcClientsOptions: ['ws://localhost:9138'], + httpRoutersOptions: ['https://peers.pleb.bot', 'https://routing.lol', 'https://peers.forumindex.com', 'https://peers.plebpubsub.xyz'], }; contextBridge.exposeInMainWorld('isElectron', true); @@ -14,6 +15,3 @@ contextBridge.exposeInMainWorld('defaultMediaIpfsGatewayUrl', 'http://localhost: // receive plebbit rpc auth key from main ipcRenderer.on('plebbit-rpc-auth-key', (event, plebbitRpcAuthKey) => contextBridge.exposeInMainWorld('plebbitRpcAuthKey', plebbitRpcAuthKey)); ipcRenderer.send('get-plebbit-rpc-auth-key'); - -// uncomment for logs -// localStorage.debug = 'plebbit-js:*,plebbit-react-hooks:*,plebchan:*' diff --git a/electron/vite.preload.config.js b/electron/vite.preload.config.js new file mode 100644 index 00000000..fcfd9462 --- /dev/null +++ b/electron/vite.preload.config.js @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export default defineConfig({ + resolve: { + conditions: ['node'], + }, + build: { + lib: { + entry: resolve(__dirname, 'preload.mjs'), + name: 'Preload', + fileName: () => 'preload.cjs', + formats: ['cjs'], + }, + outDir: resolve(__dirname, '../build/electron'), + emptyOutDir: true, + rollupOptions: { + external: ['electron'], + }, + target: 'node20', + minify: false, + }, +}); diff --git a/package.json b/package.json index 53d15612..9ff4daf0 100644 --- a/package.json +++ b/package.json @@ -49,17 +49,18 @@ "scripts": { "start": "vite", "build": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=false vite build", + "build:preload": "vite build --config electron/vite.preload.config.js", "build-netlify": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" PUBLIC_URL=./ GENERATE_SOURCEMAP=true VITE_COMMIT_REF=$COMMIT_REF CI='' vite build", "test": "vitest", "preview": "vite preview", - "analyze-bundle": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=true vite build && source-map-explorer 'build/assets/*.js'", - "electron": "yarn electron:before && electron .", + "analyze-bundle": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=true vite build && npx source-map-explorer 'build/assets/*.js'", + "electron": "yarn build:preload && cross-env ELECTRON_IS_DEV=1 yarn electron:before && cross-env ELECTRON_IS_DEV=1 electron .", "electron:no-delete-data": "yarn electron:before:download-ipfs && electron .", "electron:start": "concurrently \"cross-env BROWSER=none yarn start\" \"wait-on http://localhost:3000 && yarn electron\"", "electron:start:no-delete-data": "concurrently \"cross-env BROWSER=none yarn start\" \"wait-on http://localhost:3000 && yarn electron:no-delete-data\"", - "electron:build:linux": "yarn build && electron-rebuild && electron-builder build --publish never -l", - "electron:build:windows": "yarn build && yarn electron-rebuild && electron-builder build --publish never -w", - "electron:build:mac": "yarn build && yarn electron-rebuild && electron-builder build --publish never -m", + "electron:build:linux": "yarn build && yarn build:preload && electron-rebuild && electron-builder build --publish never -l", + "electron:build:windows": "yarn build && yarn build:preload && yarn electron-rebuild && electron-builder build --publish never -w", + "electron:build:mac": "yarn build && yarn build:preload && yarn electron-rebuild && electron-builder build --publish never -m", "electron:before": "yarn electron-rebuild && yarn electron:before:download-ipfs && yarn electron:before:delete-data", "electron:before:download-ipfs": "node electron/download-ipfs", "electron:before:delete-data": "rimraf .plebbit", @@ -83,9 +84,9 @@ ] }, "devDependencies": { - "@capacitor/android": "5.0.0", - "@capacitor/cli": "5.0.0", - "@capacitor/core": "5.0.0", + "@capacitor/android": "7.2.0", + "@capacitor/cli": "7.2.0", + "@capacitor/core": "7.2.0", "@electron/rebuild": "4.0.0", "@types/memoizee": "0.4.9", "@typescript-eslint/eslint-plugin": "8.29.0", @@ -112,12 +113,15 @@ "isomorphic-fetch": "3.0.0", "lint-staged": "12.3.8", "stream-browserify": "3.0.0", - "vite": "6.2.0", + "vite": "6.2.1", "vite-plugin-eslint": "1.8.1", "vite-plugin-node-polyfills": "0.23.0", "vite-plugin-pwa": "0.21.1", "wait-on": "7.0.1" }, + "resolutions": { + "@bonfida/spl-name-service": "3.0.0" + }, "main": "electron/main.js", "build": { "appId": "plebchan.desktop", diff --git a/src/components/catalog-row/catalog-row.tsx b/src/components/catalog-row/catalog-row.tsx index 5cc50d22..a381021f 100644 --- a/src/components/catalog-row/catalog-row.tsx +++ b/src/components/catalog-row/catalog-row.tsx @@ -242,7 +242,7 @@ const CatalogPost = ({ post }: { post: Comment }) => { ) : hasThumbnail ? ( <> - {shouldShowSnow() && hasThumbnail && } + {shouldShowSnow() && hasThumbnail && }
)}
- {shouldShowSnow() && hasThumbnail && } + {shouldShowSnow() && hasThumbnail && } {link && !isHidden && !(deleted || removed) && isValidURL(link) && ( - {shouldShowSnow() && } + {shouldShowSnow() && }