diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d3d12a8a..58d4b0b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,7 +48,15 @@ jobs: allowUpdates: true mac: - runs-on: macOS-13 + strategy: + fail-fast: false + matrix: + include: + - runner: macOS-13 # Intel x64 + arch: x64 + - runner: macOS-14 # Apple Silicon arm64 + arch: arm64 + runs-on: ${{ matrix.runner }} permissions: contents: write steps: diff --git a/electron/before-pack.js b/electron/before-pack.js index 1ceef74a..9497e63b 100755 --- a/electron/before-pack.js +++ b/electron/before-pack.js @@ -20,7 +20,9 @@ const ipfsClientLinuxPath = path.join(ipfsClientsPath, 'linux'); // official kubo download links https://docs.ipfs.tech/install/command-line/#install-official-binary-distributions const ipfsClientVersion = '0.32.1'; 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`; +// Choose proper mac binary by builder architecture to avoid Rosetta on Apple Silicon +const macArch = process.arch === 'arm64' ? 'arm64' : 'amd64'; +const ipfsClientMacUrl = `https://dist.ipfs.io/kubo/v${ipfsClientVersion}/kubo_v${ipfsClientVersion}_darwin-${macArch}.tar.gz`; const ipfsClientLinuxUrl = `https://dist.ipfs.io/kubo/v${ipfsClientVersion}/kubo_v${ipfsClientVersion}_linux-amd64.tar.gz`; const downloadWithProgress = (url) => diff --git a/electron/log.js b/electron/log.js index 72404d26..e595e628 100755 --- a/electron/log.js +++ b/electron/log.js @@ -4,6 +4,7 @@ import util from 'util'; import fs from 'fs-extra'; import path from 'path'; import EnvPaths from 'env-paths'; +import isDev from 'electron-is-dev'; const envPaths = EnvPaths('plebbit', { suffix: false }); // previous version created a file instead of folder @@ -26,32 +27,52 @@ const writeLog = (...args) => { }; const consoleLog = console.log; -console.log = (...args) => { - writeLog(...args); - consoleLog(...args); -}; const consoleError = console.error; -console.error = (...args) => { - writeLog(...args); - consoleError(...args); -}; const consoleWarn = console.warn; -console.warn = (...args) => { - writeLog(...args); - consoleWarn(...args); -}; const consoleDebug = console.debug; -console.debug = (...args) => { - // don't add date for debug because it's usually already included - for (const arg of args) { - logFile.write(util.format(arg) + ' '); - } - logFile.write('\r\n'); - consoleDebug(...args); -}; + +// In production, avoid writing verbose logs (log/debug) to disk to prevent I/O thrash. +if (!isDev) { + console.log = (...args) => { + // keep stdout behavior but don't write to file + consoleLog(...args); + }; + console.debug = (...args) => { + consoleDebug(...args); + }; + console.warn = (...args) => { + writeLog(...args); + consoleWarn(...args); + }; + console.error = (...args) => { + writeLog(...args); + consoleError(...args); + }; +} else { + // In dev, mirror everything to file for easier debugging + console.log = (...args) => { + writeLog(...args); + consoleLog(...args); + }; + console.warn = (...args) => { + writeLog(...args); + consoleWarn(...args); + }; + console.error = (...args) => { + writeLog(...args); + consoleError(...args); + }; + console.debug = (...args) => { + for (const arg of args) { + logFile.write(util.format(arg) + ' '); + } + logFile.write('\r\n'); + consoleDebug(...args); + }; +} // errors aren't console logged process.on('uncaughtException', console.error); process.on('unhandledRejection', console.error); -console.log(envPaths); +if (isDev) console.log(envPaths); diff --git a/electron/main.js b/electron/main.js index cb01fa43..4630f586 100644 --- a/electron/main.js +++ b/electron/main.js @@ -175,7 +175,7 @@ const createMainWindow = () => { webSecurity: true, // must be true or iframe embeds like youtube can do remote code execution nodeIntegration: false, contextIsolation: true, - devTools: true, // TODO: change to isDev when no bugs left + devTools: isDev, preload: path.join(dirname, '../build/electron/preload.cjs'), }, }); diff --git a/electron/start-ipfs.js b/electron/start-ipfs.js index af6d56cb..fe8d0edb 100755 --- a/electron/start-ipfs.js +++ b/electron/start-ipfs.js @@ -14,15 +14,21 @@ const envPaths = EnvPaths('plebbit', { suffix: false }); // also spawnSync might have been causing crash on start on windows const spawnAsync = (...args) => new Promise((resolve, reject) => { - const spawedProcess = spawn(...args); - spawedProcess.on('exit', (exitCode, signal) => { + const spawnedProcess = spawn(...args); + spawnedProcess.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 '${spawnedProcess.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())); - spawedProcess.stdout.on('data', (data) => console.log(data.toString())); - spawedProcess.on('error', (data) => console.error(data.toString())); + // Always surface errors from short-lived commands + spawnedProcess.stderr.on('data', (data) => console.error(data.toString())); + // Short-lived command stdout can be useful in dev, but is noisy in prod + if (isDev) { + spawnedProcess.stdout.on('data', (data) => console.log(data.toString())); + } else { + // Drain to avoid backpressure without logging + spawnedProcess.stdout.on('data', () => {}); + } + spawnedProcess.on('error', (data) => console.error(data.toString?.() || String(data))); }); const startIpfs = async () => { @@ -51,7 +57,8 @@ const startIpfs = async () => { console.log({ ipfsPath, ipfsDataPath }); fs.ensureDirSync(ipfsDataPath); - const env = { IPFS_PATH: ipfsDataPath }; + // Reduce IPFS daemon log verbosity in production to avoid UI lag from excessive logging + const env = { ...process.env, IPFS_PATH: ipfsDataPath, ...(isDev ? {} : { GOLOG_LOG_LEVEL: 'error' }) }; // init ipfs client on first launch try { await spawnAsync(ipfsPath, ['init'], { env, hideWindows: true }); @@ -83,17 +90,16 @@ const startIpfs = async () => { let lastError; ipfsProcess.stderr.on('data', (data) => { lastError = data.toString(); - console.error(data.toString()); + if (isDev) console.error(lastError); }); - ipfsProcess.stdin.on('data', (data) => console.log(data.toString())); - ipfsProcess.stdout.on('data', (data) => { - data = data.toString(); - console.log(data); - if (data.includes('Daemon is ready')) { + ipfsProcess.stdout.on('data', (chunk) => { + const text = chunk.toString(); + if (isDev) console.log(text); + if (text.includes('Daemon is ready')) { resolve(); } }); - ipfsProcess.on('error', (data) => console.error(data.toString())); + ipfsProcess.on('error', (err) => console.error(err?.toString?.() || String(err))); ipfsProcess.on('exit', () => { console.error(`ipfs process with pid ${ipfsProcess.pid} exited`); reject(Error(lastError)); diff --git a/scripts/release-body.js b/scripts/release-body.js index ac930820..eb01125e 100644 --- a/scripts/release-body.js +++ b/scripts/release-body.js @@ -17,7 +17,7 @@ let releaseChangelog = // format releaseChangelog = releaseChangelog.trim().replace(/\n\n+/g, '\n\n') -const releaseBody = `This version fixes a bug that marked all the user's comment edits as "pending" and "failed". It also fixes an issue that prevented exporting accounts from the settings in the android app. +const releaseBody = `This version fixes a bug that caused extreme slowness in some desktop versions of the app. - Web app: https://seedit.app - Decentralized web app: https://seedit.eth (only works on [Brave Browser](https://brave.com/) or via [IPFS Companion](https://docs.ipfs.tech/install/ipfs-companion/#prerequisites)) diff --git a/src/components/post/post.tsx b/src/components/post/post.tsx index 13f810cf..f6ba540e 100644 --- a/src/components/post/post.tsx +++ b/src/components/post/post.tsx @@ -162,7 +162,12 @@ const Post = ({ index, post = {} }: PostProps) => { const postTitle = (title?.length > 300 ? title?.slice(0, 300) + '...' : title) || (content?.length > 300 ? content?.slice(0, 300) + '...' : content)?.replace(' ', ' ')?.replace('>', '')?.replace('<', '')?.trim(); - const displayedTitle = searchQuery ? highlightMatchedText(postTitle || '', searchQuery) : postTitle; + + // Ensure we have a meaningful title - if it's only whitespace/newlines, treat as empty + const cleanedTitle = postTitle?.trim(); + const finalTitle = cleanedTitle || '-'; + + const displayedTitle = searchQuery ? highlightMatchedText(finalTitle, searchQuery) : finalTitle; const hasThumbnail = getHasThumbnail(commentMediaInfo, link); const hostname = getHostname(link); @@ -237,11 +242,11 @@ const Post = ({ index, post = {} }: PostProps) => {
{isInPostPageView && link ? (
- {displayedTitle ?? '-'}
+ {displayedTitle}
) : (
- {displayedTitle ?? '-'}
+ {displayedTitle}
)}
{flair && (
diff --git a/src/views/settings/account-data-editor/account-data-editor.module.css b/src/views/settings/account-data-editor/account-data-editor.module.css
index 41724e81..fb14a556 100644
--- a/src/views/settings/account-data-editor/account-data-editor.module.css
+++ b/src/views/settings/account-data-editor/account-data-editor.module.css
@@ -188,4 +188,10 @@
resize: vertical;
white-space: pre;
overflow-wrap: normal;
+}
+
+.error {
+ color: var(--red);
+ font-size: 12px;
+ margin: 20px 10px 0 10px;
}
\ No newline at end of file
diff --git a/src/views/settings/account-data-editor/account-data-editor.tsx b/src/views/settings/account-data-editor/account-data-editor.tsx
index c8741f62..d7c5af07 100644
--- a/src/views/settings/account-data-editor/account-data-editor.tsx
+++ b/src/views/settings/account-data-editor/account-data-editor.tsx
@@ -7,6 +7,7 @@ import stringify from 'json-stringify-pretty-compact';
import styles from './account-data-editor.module.css';
import useIsMobile from '../../../hooks/use-is-mobile';
import LoadingEllipsis from '../../../components/loading-ellipsis';
+import ErrorDisplay from '../../../components/error-display';
class EditorErrorBoundary extends Component<{ children: React.ReactNode; fallback: React.ReactNode }> {
constructor(props: { children: React.ReactNode; fallback: React.ReactNode }) {
@@ -57,6 +58,7 @@ const AccountDataEditor = () => {
const theme = useTheme((state) => state.theme);
const [text, setText] = useState('');
const [showEditor, setShowEditor] = useState(false);
+ const [currentError, setCurrentError] = useState