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(undefined); const accountJson = useMemo(() => stringify({ account: { ...account, plebbit: undefined, karma: undefined, unreadNotificationCount: undefined } }), [account]); @@ -66,6 +68,7 @@ const AccountDataEditor = () => { const saveAccount = async () => { try { + setCurrentError(undefined); const editorAccount = JSON.parse(text).account; await setAccount(editorAccount); alert(`Saved ${editorAccount.name}`); @@ -73,9 +76,12 @@ const AccountDataEditor = () => { window.location.reload(); } catch (error) { if (error instanceof Error) { + setCurrentError(error); alert(error.message); console.log(error); } else { + const unknownError = new Error('An unknown error occurred'); + setCurrentError(unknownError); console.error('An unknown error occurred:', error); } } @@ -99,7 +105,20 @@ const AccountDataEditor = () => { return (

- }> + { + setText(value); + if (currentError) { + setCurrentError(undefined); + } + }} + height={isMobile ? 'calc(80vh - 95px)' : 'calc(90vh - 77px)'} + /> + } + > @@ -111,7 +130,12 @@ const AccountDataEditor = () => { mode='json' theme={theme === 'dark' ? 'tomorrow_night' : 'github'} value={text} - onChange={setText} + onChange={(value) => { + setText(value); + if (currentError) { + setCurrentError(undefined); + } + }} name='ACCOUNT_DATA_EDITOR' editorProps={{ $blockScrolling: true }} className={styles.editor} @@ -132,6 +156,11 @@ const AccountDataEditor = () => { /> + {currentError && ( +
+ +
+ )}
{ const value = e.target.value === '' ? undefined : Number(e.target.value); handleExcludeChange(excludeIndex, 'postScore', value); @@ -253,7 +253,7 @@ const ChallengeSettings = ({ challenge, challengesSettings, index, isReadOnly, s { const value = e.target.value === '' ? undefined : Number(e.target.value); handleExcludeChange(excludeIndex, 'replyScore', value); @@ -274,7 +274,7 @@ const ChallengeSettings = ({ challenge, challengesSettings, index, isReadOnly, s { const value = e.target.value === '' ? undefined : Number(e.target.value); handleExcludeChange(excludeIndex, 'firstCommentTimestamp', value); @@ -362,7 +362,7 @@ const ChallengeSettings = ({ challenge, challengesSettings, index, isReadOnly, s { const value = e.target.value === '' ? undefined : Number(e.target.value); handleExcludeChange(excludeIndex, 'rateLimit', value); diff --git a/src/views/subplebbit-settings/subplebbit-data-editor/subplebbit-data-editor.tsx b/src/views/subplebbit-settings/subplebbit-data-editor/subplebbit-data-editor.tsx index e31ddfcd..4a0b9dd5 100644 --- a/src/views/subplebbit-settings/subplebbit-data-editor/subplebbit-data-editor.tsx +++ b/src/views/subplebbit-settings/subplebbit-data-editor/subplebbit-data-editor.tsx @@ -305,7 +305,11 @@ const SubplebbitDataEditor = () => { /> - {currentError &&
error: {currentError.message || 'unknown error'}
} + {currentError && ( +
+ +
+ )} {showSaving ? (