From dede4002969e116857a17817d22a0ccb8875dc2d Mon Sep 17 00:00:00 2001 From: lucaronin Date: Thu, 30 Apr 2026 06:46:45 +0200 Subject: [PATCH] fix: surface desktop menu update feedback --- docs/ABSTRACTIONS.md | 2 +- docs/ARCHITECTURE.md | 2 +- lara.lock | 1 + src-tauri/src/menu.rs | 35 ++++++++++++++++++++-------- src/components/UpdateBanner.test.tsx | 6 +++++ src/components/UpdateBanner.tsx | 11 +++++++++ src/hooks/useUpdater.test.ts | 32 ++++++++++++++++++++++++- src/hooks/useUpdater.ts | 4 ++++ src/lib/locales/de-DE.json | 1 + src/lib/locales/en.json | 1 + src/lib/locales/es-419.json | 1 + src/lib/locales/es-ES.json | 1 + src/lib/locales/fr-FR.json | 1 + src/lib/locales/it-IT.json | 1 + src/lib/locales/ja-JP.json | 1 + src/lib/locales/ko-KR.json | 1 + src/lib/locales/pt-BR.json | 1 + src/lib/locales/pt-PT.json | 1 + src/lib/locales/ru-RU.json | 1 + src/lib/locales/zh-CN.json | 1 + src/lib/locales/zh-TW.json | 1 + 21 files changed, 93 insertions(+), 13 deletions(-) diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index ecd367ee..fa9644f5 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -796,7 +796,7 @@ Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is ins ## Updates & Feature Flags ### Hooks -- **`useUpdater(releaseChannel)`** — Channel-aware updater state machine. Checks the selected feed, surfaces available/downloading/ready states, and delegates install work to Rust. +- **`useUpdater(releaseChannel)`** — Channel-aware updater state machine. Checks the selected feed, surfaces checking/available/downloading/ready states, and delegates install work to Rust. - **`useFeatureFlag(flag)`** — Returns boolean for a named feature flag. Checks `localStorage` override (`ff_`), then falls back to telemetry-backed evaluation. Type-safe via `FeatureFlagName` union. ### Frontend helpers diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9523d485..8ddb41ef 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -214,7 +214,7 @@ The main Tauri window derives its minimum width from the visible panes instead o The main Tauri window also persists its last normal size and screen position in the app config directory as `window-state.json`. The state stores logical window points, while `window_state.rs` migrates older physical-pixel state on read so Retina and non-Retina launches restore the same user-facing bounds. On startup, the restored frame applies only to the main window and clamps to the currently available monitor work areas, so stale coordinates from a disconnected display fall back to a visible placement. Maximized, fullscreen, minimized, and detached note-window frames are not written as the restore baseline. -Linux uses custom React-rendered window chrome instead of the native Tauri menu bar. `setup_linux_window_chrome()` drops server-side decorations on the main window, `openNoteInNewWindow()` does the same for detached note windows, and `LinuxTitlebar`/`LinuxMenuButton` route both window controls and menu actions back through the same shared command pipeline that macOS uses for native menu clicks. +Linux uses custom React-rendered window chrome instead of the native Tauri menu bar. `setup_linux_window_chrome()` drops server-side decorations on the main window, `openNoteInNewWindow()` does the same for detached note windows, and `LinuxTitlebar`/`LinuxMenuButton` route both window controls and menu actions back through the same shared command pipeline that the desktop native menus use. The native app menu keeps macOS-only Services/Hide entries off Windows and Linux, while cross-platform custom items such as Check for Updates emit Tolaria command IDs and show visible updater feedback. When Tolaria is launched from a Linux AppImage, `run()` also applies AppImage-only WebKitGTK startup safeguards without changing native package installs. It injects `WEBKIT_DISABLE_DMABUF_RENDERER=1` unless the user already set that variable, and on Wayland sessions it re-execs once with the first available system `libwayland-client.so` in `LD_PRELOAD` when the user has not provided their own preload. The re-exec addresses AppImage library-order failures that can surface as `Could not create default EGL display: EGL_BAD_PARAMETER` before GTK/WebKit create the display. ## Multi-Window (Note Windows) diff --git a/lara.lock b/lara.lock index ec433a9a..e53100ee 100644 --- a/lara.lock +++ b/lara.lock @@ -410,6 +410,7 @@ files: inspector.info.words: 6f15b8d4b7287d60a8ea3d1c5cbadc84 inspector.info.size: 6f6cb72d544962fa333e2e34ce64f719 update.available: 992477975168b876be8e77ce2975e390 + update.checking: a07813cc05571f23ce8dd7039583d7da update.releaseNotes: 5dd03e8d039863e563e049be198c3fd3 update.updateNow: 99b1054c0f320be9f109c877a5efd0b2 update.dismiss: c8a59e7135a20b362f9c768b09454fdb diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index 7e7c0748..cd4fc27b 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -136,6 +136,10 @@ const GIT_NO_REMOTE_DEPENDENT_IDS: &[&str] = &[VAULT_ADD_REMOTE]; type MenuResult = Result, Box>; +fn app_menu_includes_services(target_os: &str) -> bool { + target_os == "macos" +} + fn build_app_menu(app: &App) -> MenuResult { let settings_item = MenuItemBuilder::new("Settings...") .id(APP_SETTINGS) @@ -145,21 +149,25 @@ fn build_app_menu(app: &App) -> MenuResult { .id(APP_CHECK_FOR_UPDATES) .build(app)?; - Ok(SubmenuBuilder::new(app, "Tolaria") + let mut builder = SubmenuBuilder::new(app, "Tolaria") .about(None) .separator() .item(&check_updates_item) .separator() .item(&settings_item) - .separator() - .services() - .separator() - .hide() - .hide_others() - .show_all() - .separator() - .quit() - .build()?) + .separator(); + + if app_menu_includes_services(std::env::consts::OS) { + builder = builder + .services() + .separator() + .hide() + .hide_others() + .show_all() + .separator(); + } + + Ok(builder.quit().build()?) } fn build_file_menu(app: &App) -> MenuResult { @@ -628,4 +636,11 @@ mod tests { assert!(seen.insert(id), "duplicate custom ID: {id}"); } } + + #[test] + fn app_services_menu_is_macos_only() { + assert!(app_menu_includes_services("macos")); + assert!(!app_menu_includes_services("windows")); + assert!(!app_menu_includes_services("linux")); + } } diff --git a/src/components/UpdateBanner.test.tsx b/src/components/UpdateBanner.test.tsx index 15521c4c..55d59239 100644 --- a/src/components/UpdateBanner.test.tsx +++ b/src/components/UpdateBanner.test.tsx @@ -68,6 +68,12 @@ describe('UpdateBanner', () => { expect(container.innerHTML).toBe('') }) + it('shows immediate feedback while checking for updates', () => { + renderBanner({ state: 'checking' }) + + expect(screen.getByTestId('update-banner')).toHaveTextContent('Checking for updates') + }) + it('shows version and action buttons when update is available', () => { renderBanner(makeAvailableStatus({ version: '2026.4.16-alpha.3', diff --git a/src/components/UpdateBanner.tsx b/src/components/UpdateBanner.tsx index 1ee810d1..3c8de3f1 100644 --- a/src/components/UpdateBanner.tsx +++ b/src/components/UpdateBanner.tsx @@ -105,6 +105,15 @@ function renderAvailableContent(status: Extract + + {translate(locale, 'update.checking')} + + ) +} + function renderDownloadingContent(status: Extract, locale: AppLocale) { return ( <> @@ -153,6 +162,8 @@ function renderReadyContent(status: Extract { expect(result.current.status).toEqual({ state: 'idle' }) }) + it('shows checking state while a manual update check is in flight', async () => { + vi.mocked(isTauri).mockReturnValue(true) + let resolveCheck: (value: AppUpdateMetadata | null) => void = () => {} + mockInvoke.mockImplementation((command: string) => { + if (command === 'check_for_app_update') { + return new Promise((resolve) => { + resolveCheck = resolve + }) + } + return Promise.resolve(null) + }) + + const { result } = renderUpdater('stable') + + let checkPromise: Promise | null = null + await act(async () => { + checkPromise = result.current.actions.checkForUpdates() + await Promise.resolve() + }) + + expect(result.current.status).toEqual({ state: 'checking' }) + + await act(async () => { + resolveCheck(null) + expect(await checkPromise).toEqual({ kind: 'up-to-date' }) + }) + + expect(result.current.status).toEqual({ state: 'idle' }) + }) + it('returns available and sets status when an update exists', async () => { const { result, outcome } = await performManualCheck( 'stable', @@ -219,7 +249,7 @@ describe('useUpdater', () => { message: 'Could not check for updates: network error', }) expect(console.warn).toHaveBeenCalledWith('[updater] Failed to check for updates') - expect(result.current.status).toEqual({ state: 'idle' }) + expect(result.current.status).toEqual({ state: 'error' }) }) it('dismiss resets the banner state', async () => { diff --git a/src/hooks/useUpdater.ts b/src/hooks/useUpdater.ts index 71e5c276..537a2fde 100644 --- a/src/hooks/useUpdater.ts +++ b/src/hooks/useUpdater.ts @@ -18,6 +18,7 @@ interface UpdateVersionInfo { export type UpdateStatus = | { state: 'idle' } + | { state: 'checking' } | ({ state: 'available'; notes: string | undefined } & UpdateVersionInfo) | ({ state: 'downloading'; progress: number } & UpdateVersionInfo) | ({ state: 'ready' } & UpdateVersionInfo) @@ -111,6 +112,8 @@ export function useUpdater( const checkForUpdates = useCallback(async (): Promise => { if (!isTauri()) return { kind: 'up-to-date' } + setStatus({ state: 'checking' }) + try { const update = await checkForAppUpdate(releaseChannel) if (!update) { @@ -125,6 +128,7 @@ export function useUpdater( return { kind: 'available', ...versionInfo } } catch (error) { console.warn('[updater] Failed to check for updates') + setStatus({ state: 'error' }) return { kind: 'error', message: buildUpdateCheckErrorMessage(error) } } }, [releaseChannel]) diff --git a/src/lib/locales/de-DE.json b/src/lib/locales/de-DE.json index 48138082..902876cc 100644 --- a/src/lib/locales/de-DE.json +++ b/src/lib/locales/de-DE.json @@ -408,6 +408,7 @@ "inspector.info.words": "Wörter", "inspector.info.size": "Größe", "update.available": "ist verfügbar", + "update.checking": "Nach Updates suchen …", "update.releaseNotes": "Versionshinweise", "update.updateNow": "Jetzt aktualisieren", "update.dismiss": "Ausblenden", diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json index ddcc3868..547a186c 100644 --- a/src/lib/locales/en.json +++ b/src/lib/locales/en.json @@ -408,6 +408,7 @@ "inspector.info.words": "Words", "inspector.info.size": "Size", "update.available": "is available", + "update.checking": "Checking for updates...", "update.releaseNotes": "Release Notes", "update.updateNow": "Update Now", "update.dismiss": "Dismiss", diff --git a/src/lib/locales/es-419.json b/src/lib/locales/es-419.json index 2ed9eb32..a28f4c35 100644 --- a/src/lib/locales/es-419.json +++ b/src/lib/locales/es-419.json @@ -408,6 +408,7 @@ "inspector.info.words": "Palabras", "inspector.info.size": "Tamaño", "update.available": "está disponible", + "update.checking": "Buscando actualizaciones...", "update.releaseNotes": "Notas de la versión", "update.updateNow": "Actualizar ahora", "update.dismiss": "Omitir", diff --git a/src/lib/locales/es-ES.json b/src/lib/locales/es-ES.json index c10421ca..c214b6f9 100644 --- a/src/lib/locales/es-ES.json +++ b/src/lib/locales/es-ES.json @@ -408,6 +408,7 @@ "inspector.info.words": "Palabras", "inspector.info.size": "Tamaño", "update.available": "está disponible", + "update.checking": "Buscando actualizaciones...", "update.releaseNotes": "Notas de la versión", "update.updateNow": "Actualizar ahora", "update.dismiss": "Omitir", diff --git a/src/lib/locales/fr-FR.json b/src/lib/locales/fr-FR.json index f2f73860..dea1dbd6 100644 --- a/src/lib/locales/fr-FR.json +++ b/src/lib/locales/fr-FR.json @@ -408,6 +408,7 @@ "inspector.info.words": "Mots", "inspector.info.size": "Taille", "update.available": "est disponible", + "update.checking": "Recherche de mises à jour en cours…", "update.releaseNotes": "Notes de version", "update.updateNow": "Mettre à jour maintenant", "update.dismiss": "Ignorer", diff --git a/src/lib/locales/it-IT.json b/src/lib/locales/it-IT.json index ebadf8b6..ade4de59 100644 --- a/src/lib/locales/it-IT.json +++ b/src/lib/locales/it-IT.json @@ -408,6 +408,7 @@ "inspector.info.words": "Parole", "inspector.info.size": "Dimensione", "update.available": "è disponibile", + "update.checking": "Verifica degli aggiornamenti in corso...", "update.releaseNotes": "Note sulla versione", "update.updateNow": "Aggiorna ora", "update.dismiss": "Ignora", diff --git a/src/lib/locales/ja-JP.json b/src/lib/locales/ja-JP.json index 65b069a9..5fff825e 100644 --- a/src/lib/locales/ja-JP.json +++ b/src/lib/locales/ja-JP.json @@ -408,6 +408,7 @@ "inspector.info.words": "単語数", "inspector.info.size": "サイズ", "update.available": "利用可能", + "update.checking": "更新を確認しています...", "update.releaseNotes": "リリースノート", "update.updateNow": "今すぐ更新", "update.dismiss": "却下", diff --git a/src/lib/locales/ko-KR.json b/src/lib/locales/ko-KR.json index fd596f30..f5b6f4d9 100644 --- a/src/lib/locales/ko-KR.json +++ b/src/lib/locales/ko-KR.json @@ -408,6 +408,7 @@ "inspector.info.words": "단어 수", "inspector.info.size": "크기", "update.available": "사용 가능", + "update.checking": "업데이트 확인 중...", "update.releaseNotes": "릴리스 노트", "update.updateNow": "지금 업데이트", "update.dismiss": "닫기", diff --git a/src/lib/locales/pt-BR.json b/src/lib/locales/pt-BR.json index 919258de..009b5d4d 100644 --- a/src/lib/locales/pt-BR.json +++ b/src/lib/locales/pt-BR.json @@ -408,6 +408,7 @@ "inspector.info.words": "Palavras", "inspector.info.size": "Tamanho", "update.available": "está disponível", + "update.checking": "Verificando atualizações...", "update.releaseNotes": "Notas da versão", "update.updateNow": "Atualizar agora", "update.dismiss": "Ignorar", diff --git a/src/lib/locales/pt-PT.json b/src/lib/locales/pt-PT.json index 8ff5104d..040eb7f5 100644 --- a/src/lib/locales/pt-PT.json +++ b/src/lib/locales/pt-PT.json @@ -408,6 +408,7 @@ "inspector.info.words": "Palavras", "inspector.info.size": "Tamanho", "update.available": "está disponível", + "update.checking": "A procurar atualizações...", "update.releaseNotes": "Notas de lançamento", "update.updateNow": "Atualizar agora", "update.dismiss": "Ignorar", diff --git a/src/lib/locales/ru-RU.json b/src/lib/locales/ru-RU.json index b33dcf0b..b4b4ae26 100644 --- a/src/lib/locales/ru-RU.json +++ b/src/lib/locales/ru-RU.json @@ -408,6 +408,7 @@ "inspector.info.words": "Слов", "inspector.info.size": "Размер", "update.available": "доступно", + "update.checking": "Проверка наличия обновлений...", "update.releaseNotes": "Примечания к выпуску", "update.updateNow": "Обновить сейчас", "update.dismiss": "Закрыть", diff --git a/src/lib/locales/zh-CN.json b/src/lib/locales/zh-CN.json index d0afd0e1..7daf360e 100644 --- a/src/lib/locales/zh-CN.json +++ b/src/lib/locales/zh-CN.json @@ -408,6 +408,7 @@ "inspector.info.words": "字数", "inspector.info.size": "大小", "update.available": "可用", + "update.checking": "正在检查更新……", "update.releaseNotes": "发行说明", "update.updateNow": "立即更新", "update.dismiss": "关闭", diff --git a/src/lib/locales/zh-TW.json b/src/lib/locales/zh-TW.json index b99ba40f..47d88ff5 100644 --- a/src/lib/locales/zh-TW.json +++ b/src/lib/locales/zh-TW.json @@ -408,6 +408,7 @@ "inspector.info.words": "字數", "inspector.info.size": "大小", "update.available": "可用", + "update.checking": "正在檢查更新……", "update.releaseNotes": "發行說明", "update.updateNow": "立即更新", "update.dismiss": "關閉",