From 7af43d991ea1cda4db19cf5dca162042a09dd916 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Fri, 17 Apr 2026 18:53:08 -0500 Subject: [PATCH 1/5] Polish defaults and updater feedback --- package.json | 4 +-- src/main/updater.ts | 33 +++++++++++++++++-- .../src/components/FloatingNoteApp.tsx | 4 +-- src/renderer/src/components/SettingsModal.tsx | 23 ++++++++++++- src/renderer/src/lib/themes.ts | 2 +- src/renderer/src/store.ts | 18 +++++----- 6 files changed, 67 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 0afad80b..aee2a8a0 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "private": true, "main": "./out/main/index.js", "author": { - "name": "Adi Bhanna", + "name": "Adib Hanna", "email": "adibhanna@gmail.com" }, "license": "MIT", @@ -147,7 +147,7 @@ "AppImage", "deb" ], - "maintainer": "Adi Bhanna ", + "maintainer": "Adib Hanna ", "category": "Office" } } diff --git a/src/main/updater.ts b/src/main/updater.ts index de37a460..27583ec1 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -33,16 +33,45 @@ function makeState(overrides: Partial = {}): AppUpdateState { } } +function decodeBasicHtmlEntities(value: string): string { + return value + .replace(/ /gi, ' ') + .replace(/&/gi, '&') + .replace(/</gi, '<') + .replace(/>/gi, '>') + .replace(/"/gi, '"') + .replace(/'|'/gi, "'") +} + +function normalizeReleaseNoteBody(body: string): string { + const normalized = decodeBasicHtmlEntities( + body + .replace(/\r\n?/g, '\n') + .replace(//gi, '\n') + .replace(/]*>/gi, '- ') + .replace(/<\/li>/gi, '\n') + .replace(/<\/?(ul|ol)\b[^>]*>/gi, '\n') + .replace(/<\/?(p|div|section|article|header|footer|aside|blockquote|pre)\b[^>]*>/gi, '\n') + .replace(/<\/?h[1-6]\b[^>]*>/gi, '\n') + .replace(/<[^>]+>/g, '') + ) + .replace(/[ \t]+\n/g, '\n') + .replace(/\n{3,}/g, '\n\n') + .trim() + + return normalized +} + function normalizeReleaseNotes(notes: UpdateInfo['releaseNotes']): string | null { if (!notes) return null if (typeof notes === 'string') { - const trimmed = notes.trim() + const trimmed = normalizeReleaseNoteBody(notes) return trimmed.length > 0 ? trimmed : null } const merged = notes .map((note) => { const version = note.version ? `Version ${note.version}` : '' - const body = note.note?.trim() ?? '' + const body = normalizeReleaseNoteBody(note.note ?? '') return [version, body].filter(Boolean).join('\n') }) .filter(Boolean) diff --git a/src/renderer/src/components/FloatingNoteApp.tsx b/src/renderer/src/components/FloatingNoteApp.tsx index a3f95caf..168cc70d 100644 --- a/src/renderer/src/components/FloatingNoteApp.tsx +++ b/src/renderer/src/components/FloatingNoteApp.tsx @@ -105,8 +105,8 @@ function loadFloatingPrefs(): FloatingPrefs { vimMode: true, livePreview: true, themeId: DEFAULT_THEME_ID, - themeFamily: 'apple', - themeMode: 'auto', + themeFamily: 'gruvbox', + themeMode: 'dark', editorFontSize: 16, editorLineHeight: 1.7, lineNumberMode: 'off', diff --git a/src/renderer/src/components/SettingsModal.tsx b/src/renderer/src/components/SettingsModal.tsx index 4ecfef11..db017665 100644 --- a/src/renderer/src/components/SettingsModal.tsx +++ b/src/renderer/src/components/SettingsModal.tsx @@ -243,7 +243,28 @@ export function SettingsModal(): JSX.Element { }, []) const triggerUpdateCheck = useCallback(() => { - void window.zen.checkForAppUpdates() + void window.zen.checkForAppUpdates().then( + (state) => { + if (state.phase === 'available') { + window.alert( + `ZenNotes ${state.availableVersion ?? ''} is available. Use “Download Update” to fetch it.` + ) + return + } + if (state.phase === 'not-available') { + window.alert(state.message) + return + } + if (state.phase === 'unsupported' || state.phase === 'error') { + window.alert(state.message) + } + }, + (error) => { + const message = + error instanceof Error ? error.message : 'Could not check for updates.' + window.alert(message) + } + ) }, []) const triggerUpdateDownload = useCallback(() => { diff --git a/src/renderer/src/lib/themes.ts b/src/renderer/src/lib/themes.ts index 8e7b6231..430ab6dc 100644 --- a/src/renderer/src/lib/themes.ts +++ b/src/renderer/src/lib/themes.ts @@ -94,7 +94,7 @@ export const THEMES: ThemeOption[] = [ { id: 'tokyo-night-storm', label: 'Storm', family: 'tokyo-night', mode: 'dark' } ] -export const DEFAULT_THEME_ID = 'apple-light' +export const DEFAULT_THEME_ID = 'dark-hard' export function findTheme(id: string): ThemeOption { return THEMES.find((t) => t.id === id) ?? THEMES[1] // fallback: light-medium diff --git a/src/renderer/src/store.ts b/src/renderer/src/store.ts index 672d8ac8..f7228a0d 100644 --- a/src/renderer/src/store.ts +++ b/src/renderer/src/store.ts @@ -183,20 +183,20 @@ const DEFAULT_PREFS: Prefs = { ripgrepBinaryPath: null, fzfBinaryPath: null, livePreview: true, - tabsEnabled: false, + tabsEnabled: true, themeId: DEFAULT_THEME_ID, - themeFamily: 'apple', - themeMode: 'auto', + themeFamily: 'gruvbox', + themeMode: 'dark', editorFontSize: 16, editorLineHeight: 1.7, previewMaxWidth: 920, lineNumberMode: 'off', - // Ship with SF Mono everywhere — gives the app a single, consistent - // typographic identity out of the box. Users can still change any of - // the three slots from Settings → Fonts. - interfaceFont: 'SF Mono', - textFont: 'SF Mono', - monoFont: 'SF Mono', + // Leave all font slots on the built-in "Default" path. That lets the + // shipped CSS fallbacks choose sensible system fonts on each machine + // instead of forcing a specific family that may not exist. + interfaceFont: null, + textFont: null, + monoFont: null, sidebarWidth: 232, noteListWidth: 300, noteSortOrder: 'none', From f35674e0aa5cf41a7624951efc795cc965908cbe Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Fri, 17 Apr 2026 18:58:13 -0500 Subject: [PATCH 2/5] Show native update feedback from app menu --- src/main/index.ts | 87 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/src/main/index.ts b/src/main/index.ts index e7486891..320c6a7f 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -815,7 +815,7 @@ function installAppMenu(): void { { label: 'Check for Updates…', click: () => { - void checkForAppUpdates() + void runMenuUpdateCheck() } }, { type: 'separator' }, @@ -878,6 +878,91 @@ function installAppMenu(): void { Menu.setApplicationMenu(Menu.buildFromTemplate(template)) } +async function runMenuUpdateCheck(): Promise { + const parent = BrowserWindow.getFocusedWindow() ?? mainWindow ?? undefined + const showDialog = async ( + options: Electron.MessageBoxOptions + ): Promise => { + return parent + ? await dialog.showMessageBox(parent, options) + : await dialog.showMessageBox(options) + } + const state = await checkForAppUpdates() + + if (state.phase === 'available') { + const { response } = await showDialog({ + type: 'info', + buttons: ['Download Update', 'Later'], + defaultId: 0, + cancelId: 1, + title: 'ZenNotes Update Available', + message: `ZenNotes ${state.availableVersion ?? ''} is available.`, + detail: [ + state.message, + state.releaseNotes ? `\n\nRelease notes:\n${state.releaseNotes}` : '' + ] + .join('') + .trim() + }) + if (response === 0) { + void downloadAppUpdate() + await showDialog({ + type: 'info', + buttons: ['OK'], + defaultId: 0, + title: 'Downloading Update', + message: `ZenNotes ${state.availableVersion ?? ''} is downloading in the background.`, + detail: 'Open Settings → About to track progress and install when the download finishes.' + }) + } + return + } + + if (state.phase === 'downloaded') { + const { response } = await showDialog({ + type: 'info', + buttons: ['Install and Relaunch', 'Later'], + defaultId: 0, + cancelId: 1, + title: 'ZenNotes Update Ready', + message: `ZenNotes ${state.availableVersion ?? ''} is ready to install.`, + detail: state.message + }) + if (response === 0) { + installAppUpdate() + } + return + } + + if (state.phase === 'downloading' || state.phase === 'checking') { + await showDialog({ + type: 'info', + buttons: ['OK'], + defaultId: 0, + title: 'ZenNotes Updates', + message: state.phase === 'checking' ? 'Checking for updates…' : 'Downloading update…', + detail: state.message + }) + return + } + + await showDialog({ + type: state.phase === 'error' ? 'warning' : 'info', + buttons: ['OK'], + defaultId: 0, + title: 'ZenNotes Updates', + message: + state.phase === 'not-available' + ? 'ZenNotes is up to date.' + : state.phase === 'unsupported' + ? 'Update checks are unavailable.' + : state.phase === 'error' + ? 'Could not check for updates.' + : 'ZenNotes Updates', + detail: state.message + }) +} + app.whenReady().then(async () => { protocol.handle(LOCAL_ASSET_SCHEME, async (request) => { const abs = decodeLocalAssetRequestPath(request.url) From 62e37e05b9d2952e2fb0b3c308cf77e1c821ab91 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Fri, 17 Apr 2026 19:00:54 -0500 Subject: [PATCH 3/5] Harden release note normalization --- src/main/updater.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/updater.ts b/src/main/updater.ts index 27583ec1..be3f7528 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -36,11 +36,9 @@ function makeState(overrides: Partial = {}): AppUpdateState { function decodeBasicHtmlEntities(value: string): string { return value .replace(/ /gi, ' ') - .replace(/&/gi, '&') - .replace(/</gi, '<') - .replace(/>/gi, '>') .replace(/"/gi, '"') .replace(/'|'/gi, "'") + .replace(/&/gi, '&') } function normalizeReleaseNoteBody(body: string): string { @@ -53,7 +51,7 @@ function normalizeReleaseNoteBody(body: string): string { .replace(/<\/?(ul|ol)\b[^>]*>/gi, '\n') .replace(/<\/?(p|div|section|article|header|footer|aside|blockquote|pre)\b[^>]*>/gi, '\n') .replace(/<\/?h[1-6]\b[^>]*>/gi, '\n') - .replace(/<[^>]+>/g, '') + .replace(/<\/?(strong|b|em|i|code|span)\b[^>]*>/gi, '') ) .replace(/[ \t]+\n/g, '\n') .replace(/\n{3,}/g, '\n\n') From 64709d32e535cb0fb0d42851c0b3169a15412dde Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Fri, 17 Apr 2026 19:04:02 -0500 Subject: [PATCH 4/5] Strip remaining tag brackets from release notes --- src/main/updater.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/updater.ts b/src/main/updater.ts index be3f7528..b7f12db8 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -52,6 +52,7 @@ function normalizeReleaseNoteBody(body: string): string { .replace(/<\/?(p|div|section|article|header|footer|aside|blockquote|pre)\b[^>]*>/gi, '\n') .replace(/<\/?h[1-6]\b[^>]*>/gi, '\n') .replace(/<\/?(strong|b|em|i|code|span)\b[^>]*>/gi, '') + .replace(/[<>]/g, '') ) .replace(/[ \t]+\n/g, '\n') .replace(/\n{3,}/g, '\n\n') From d5ba2eb035708160203b56cda950a0ece3423144 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Fri, 17 Apr 2026 19:07:33 -0500 Subject: [PATCH 5/5] Move release note formatting to renderer --- src/main/index.ts | 7 +--- src/main/updater.ts | 32 ++----------------- src/renderer/src/components/SettingsModal.tsx | 29 +++++++++++++++-- 3 files changed, 30 insertions(+), 38 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 320c6a7f..91e02988 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -897,12 +897,7 @@ async function runMenuUpdateCheck(): Promise { cancelId: 1, title: 'ZenNotes Update Available', message: `ZenNotes ${state.availableVersion ?? ''} is available.`, - detail: [ - state.message, - state.releaseNotes ? `\n\nRelease notes:\n${state.releaseNotes}` : '' - ] - .join('') - .trim() + detail: state.message }) if (response === 0) { void downloadAppUpdate() diff --git a/src/main/updater.ts b/src/main/updater.ts index b7f12db8..de37a460 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -33,44 +33,16 @@ function makeState(overrides: Partial = {}): AppUpdateState { } } -function decodeBasicHtmlEntities(value: string): string { - return value - .replace(/ /gi, ' ') - .replace(/"/gi, '"') - .replace(/'|'/gi, "'") - .replace(/&/gi, '&') -} - -function normalizeReleaseNoteBody(body: string): string { - const normalized = decodeBasicHtmlEntities( - body - .replace(/\r\n?/g, '\n') - .replace(//gi, '\n') - .replace(/]*>/gi, '- ') - .replace(/<\/li>/gi, '\n') - .replace(/<\/?(ul|ol)\b[^>]*>/gi, '\n') - .replace(/<\/?(p|div|section|article|header|footer|aside|blockquote|pre)\b[^>]*>/gi, '\n') - .replace(/<\/?h[1-6]\b[^>]*>/gi, '\n') - .replace(/<\/?(strong|b|em|i|code|span)\b[^>]*>/gi, '') - .replace(/[<>]/g, '') - ) - .replace(/[ \t]+\n/g, '\n') - .replace(/\n{3,}/g, '\n\n') - .trim() - - return normalized -} - function normalizeReleaseNotes(notes: UpdateInfo['releaseNotes']): string | null { if (!notes) return null if (typeof notes === 'string') { - const trimmed = normalizeReleaseNoteBody(notes) + const trimmed = notes.trim() return trimmed.length > 0 ? trimmed : null } const merged = notes .map((note) => { const version = note.version ? `Version ${note.version}` : '' - const body = normalizeReleaseNoteBody(note.note ?? '') + const body = note.note?.trim() ?? '' return [version, body].filter(Boolean).join('\n') }) .filter(Boolean) diff --git a/src/renderer/src/components/SettingsModal.tsx b/src/renderer/src/components/SettingsModal.tsx index db017665..d7e9fe13 100644 --- a/src/renderer/src/components/SettingsModal.tsx +++ b/src/renderer/src/components/SettingsModal.tsx @@ -126,6 +126,26 @@ function formatBytes(bytes: number | null): string | null { return `${rounded} ${unit}` } +function formatReleaseNotesForDisplay(notes: string | null): string | null { + if (!notes) return null + const trimmed = notes.trim() + if (!trimmed) return null + if (!/[<&]/.test(trimmed)) return trimmed + + try { + const parser = new DOMParser() + const doc = parser.parseFromString(trimmed, 'text/html') + const text = (doc.body.innerText || doc.body.textContent || '') + .replace(/\r\n?/g, '\n') + .replace(/[ \t]+\n/g, '\n') + .replace(/\n{3,}/g, '\n\n') + .trim() + return text || trimmed + } catch { + return trimmed + } +} + export function SettingsModal(): JSX.Element { const setSettingsOpen = useStore((s) => s.setSettingsOpen) const vimMode = useStore((s) => s.vimMode) @@ -275,6 +295,11 @@ export function SettingsModal(): JSX.Element { void window.zen.installAppUpdate() }, []) + const displayedReleaseNotes = useMemo( + () => formatReleaseNotesForDisplay(appUpdateState?.releaseNotes ?? null), + [appUpdateState?.releaseNotes] + ) + // Family list — Apple is the default, followed by the other families. const familyOptions = useMemo<{ id: ThemeFamily; label: string }[]>( () => [ @@ -937,13 +962,13 @@ export function SettingsModal(): JSX.Element { )} - {appUpdateState?.releaseNotes && ( + {displayedReleaseNotes && (
Release notes
-                      {appUpdateState.releaseNotes}
+                      {displayedReleaseNotes}
                     
)}