feat: replace native update dialog with toast notification#334
feat: replace native update dialog with toast notification#334AnthonyRonning merged 1 commit intomasterfrom
Conversation
WalkthroughReplaces a dialog-driven desktop update restart with an event-driven flow: backend emits an "update-ready" event with version payload and exposes Changes
Sequence DiagramsequenceDiagram
participant Updater as Updater (native)
participant Tauri as Tauri Backend
participant Frontend as Frontend Renderer
participant Listener as UpdateEventListener
participant Notifier as NotificationContext / UI
participant User as User
Updater->>Tauri: Download & install update
Tauri->>Tauri: Prepare update
Tauri->>Frontend: Emit "update-ready" event (version)
Frontend->>Listener: Listener subscribed to "update-ready"
Listener-->>Frontend: Receives event + version
Listener->>Notifier: Show update notification with actions ("Later", "Restart Now")
Notifier->>User: Display notification
User->>Notifier: Click "Restart Now"
Notifier->>Listener: Action handler invoked
Listener->>Tauri: Invoke `restart_for_update`
Tauri->>Tauri: Restart application to apply update
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
Deploying maple with
|
| Latest commit: |
4af2d4b
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://33ecf7f4.maple-ca8.pages.dev |
| Branch Preview URL: | https://feature-toast-update-notific.maple-ca8.pages.dev |
Greptile OverviewGreptile SummaryThis PR successfully replaces the intrusive native dialog update prompt with a non-blocking toast notification system, improving user experience. Key Changes
Implementation Quality
Confidence Score: 5/5
Important Files ChangedFile Analysis
Sequence DiagramsequenceDiagram
participant Updater as Tauri Updater
participant Backend as Rust Backend (lib.rs)
participant Frontend as Frontend App
participant Listener as UpdateEventListener
participant Notification as GlobalNotification
participant User as User
Note over Updater,Backend: Update Check & Download
Updater->>Backend: check_for_updates()
Backend->>Updater: Download update
Updater->>Backend: update.install(bytes)
Backend->>Backend: Mark UPDATE_DOWNLOADED flag
Note over Backend,Listener: Event Emission
Backend->>Frontend: emit("update-ready", {version})
Frontend->>Listener: Event received
Listener->>Notification: showNotification({type: "update", actions: [...], duration: 0})
Note over Notification,User: User Interaction
Notification->>User: Display toast with "Later" and "Restart Now" buttons
alt User clicks "Later"
User->>Notification: Click "Later"
Notification->>Notification: handleDismiss()
Notification->>Frontend: Clear notification
else User clicks "Restart Now"
User->>Notification: Click "Restart Now"
Notification->>Backend: invoke("restart_for_update")
Backend->>Backend: app_handle.restart()
Backend->>Updater: Apply update and restart
end
|
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (4)
frontend/src/components/GlobalNotification.tsx (2)
5-9: MakeNotificationAction.onClickasync‑friendly and simplify actions handlingRight now
NotificationAction.onClickis typed as() => void, butUpdateEventListenerpasses anasynchandler. This works at runtime, but it’s clearer to reflect that handlers may return a promise and to avoid the non‑null assertion when mapping actions.You can tighten this up and simplify the rendering like this:
-export interface NotificationAction { - label: string; - onClick: () => void; - variant?: "primary" | "secondary"; -} +export interface NotificationAction { + label: string; + onClick: () => void | Promise<void>; + variant?: "primary" | "secondary"; +} @@ export interface Notification { id: string; type: "success" | "error" | "info" | "update"; title: string; message?: string; icon?: React.ReactNode; duration?: number; // ms, 0 = permanent - actions?: NotificationAction[]; + actions?: NotificationAction[]; } @@ - const hasActions = notification.actions && notification.actions.length > 0; + const actions = notification.actions ?? []; + const hasActions = actions.length > 0; @@ - {hasActions && ( + {hasActions && ( <div className="flex justify-end gap-2"> - {notification.actions!.map((action, index) => ( + {actions.map((action, index) => ( <buttonThis keeps runtime behavior the same, better expresses the intent in the types, and removes the need for
!onnotification.actions.Also applies to: 18-19, 118-137
2-2: Minor UI/style nit: long Tailwind class listThe new "update" icon and border styling look consistent with the existing "info" style. One small optional tweak: the class list on Line 89 is quite long and exceeds the 100‑character guideline; you could split it across multiple strings for readability:
- className={cn( - "pointer-events-auto flex flex-col gap-3 rounded-lg border bg-card text-card-foreground p-4 shadow-lg transition-all duration-200 min-w-[320px] max-w-md", + className={cn( + "pointer-events-auto flex flex-col gap-3 rounded-lg border bg-card text-card-foreground p-4", + "shadow-lg transition-all duration-200 min-w-[320px] max-w-md",Purely cosmetic; behavior is unchanged.
Also applies to: 78-80, 89-95
frontend/src/components/UpdateEventListener.tsx (1)
14-62: Harden async listener setup to avoid a rare leak on unmountBecause
setupListenersis async and assignsunlistenUpdateReadyafterlistenresolves, there’s a small race where the component could unmount beforelistencompletes: the cleanup runs withunlistenUpdateReady === null, but the listener is still attached once the promise resolves.It’s unlikely in practice (root‑level component), but you can make this robust with a cancellation flag:
- useEffect(() => { + useEffect(() => { + let cancelled = false; @@ - const setupListeners = async () => { + const setupListeners = async () => { try { - unlistenUpdateReady = await listen<UpdateReadyPayload>("update-ready", (event) => { + const unlisten = await listen<UpdateReadyPayload>("update-ready", (event) => { const { version } = event.payload; showNotification({ // ... }); - }); + }); + + if (cancelled) { + unlisten(); + } else { + unlistenUpdateReady = unlisten; + } } catch (error) { console.error("Failed to setup update event listeners:", error); } }; @@ - return () => { - if (unlistenUpdateReady) unlistenUpdateReady(); - }; + return () => { + cancelled = true; + if (unlistenUpdateReady) unlistenUpdateReady(); + }; }, [showNotification]);This guarantees that any listener created after unmount is immediately cleaned up.
frontend/src-tauri/src/lib.rs (1)
373-388: Consider hoistingUpdateReadyPayloadout ofcheck_for_updatesThe inline
UpdateReadyPayloaddefinition right before emitting"update-ready"works, but moving it to a top‑level (desktop‑gated) struct would make the event contract easier to spot and reuse:+#[cfg(desktop)] +#[derive(Clone, serde::Serialize)] +struct UpdateReadyPayload { + version: String, +} @@ - // Emit event to frontend for toast notification - #[derive(Clone, serde::Serialize)] - struct UpdateReadyPayload { - version: String, - } - - if let Err(e) = app_handle.emit("update-ready", UpdateReadyPayload { - version: update.version.clone(), - }) { + // Emit event to frontend for toast notification + if let Err(e) = app_handle.emit( + "update-ready", + UpdateReadyPayload { + version: update.version.clone(), + }, + ) {Purely a readability/maintainability improvement; behavior is unchanged.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
frontend/src-tauri/src/lib.rs(3 hunks)frontend/src/app.tsx(2 hunks)frontend/src/components/GlobalNotification.tsx(2 hunks)frontend/src/components/UpdateEventListener.tsx(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: Use 2-space indentation, double quotes, and a 100-character line limit for formatting
Use camelCase for variable and function names
Use try/catch with specific error types for error handling
Files:
frontend/src/components/GlobalNotification.tsxfrontend/src/components/UpdateEventListener.tsxfrontend/src/app.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use strict TypeScript typing and avoid
anywhen possible
Files:
frontend/src/components/GlobalNotification.tsxfrontend/src/components/UpdateEventListener.tsxfrontend/src/app.tsx
🧬 Code graph analysis (3)
frontend/src/components/GlobalNotification.tsx (2)
frontend/src/utils/utils.ts (1)
cn(8-10)frontend/src/components/icons/X.tsx (1)
X(3-23)
frontend/src/components/UpdateEventListener.tsx (1)
frontend/src/contexts/NotificationContext.tsx (1)
useNotification(35-41)
frontend/src/app.tsx (1)
frontend/src/components/UpdateEventListener.tsx (1)
UpdateEventListener(11-65)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: build-macos (universal-apple-darwin)
- GitHub Check: build-linux
- GitHub Check: build-android
- GitHub Check: build-ios
- GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
frontend/src/app.tsx (1)
16-16: Good integration ofUpdateEventListenerin the app tree
UpdateEventListeneris correctly imported and mounted alongsideProxyEventListenerunderNotificationProvider, so it shares the same context and runs once at the app root. No issues here.Also applies to: 103-104
frontend/src-tauri/src/lib.rs (1)
8-13: Desktop‑onlyrestart_for_updatecommand wiring looks correctThe new
restart_for_updatecommand is gated behind#[cfg(desktop)]and registered only in the desktop builder’sinvoke_handler, which lines up with the frontend’s Tauri‑guarded usage. The simple log +app_handle.restart()implementation is appropriate for this flow.Also applies to: 39-48
Replace intrusive native dialog popup with a subtle toast notification that shows when an update is ready. The notification persists until user interaction with 'Later' and 'Restart Now' buttons. - Extend GlobalNotification with action button support and persistent mode - Create UpdateEventListener to handle update-ready events from backend - Modify Rust backend to emit events instead of showing native dialog - Add restart_for_update command for frontend-triggered restart - Remove tauri_plugin_dialog dependency (no longer needed) Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
b8694a0 to
4af2d4b
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
frontend/src-tauri/src/lib.rs (1)
374-386: Consider movingUpdateReadyPayloadto module level.The inline struct definition works, but moving it near the other
#[cfg(desktop)]items (around line 289) would improve discoverability and allow reuse if needed in the future.+#[cfg(desktop)] +#[derive(Clone, serde::Serialize)] +struct UpdateReadyPayload { + version: String, +} + #[cfg(desktop)] static UPDATE_DOWNLOADED: AtomicBool = AtomicBool::new(false);Then simplify the emit call:
- // Emit event to frontend for toast notification - #[derive(Clone, serde::Serialize)] - struct UpdateReadyPayload { - version: String, - } - - if let Err(e) = app_handle.emit("update-ready", UpdateReadyPayload { + // Emit event to frontend for toast notification + if let Err(e) = app_handle.emit("update-ready", UpdateReadyPayload { version: update.version.clone(), }) {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
frontend/src-tauri/Cargo.toml(0 hunks)frontend/src-tauri/capabilities/default.json(0 hunks)frontend/src-tauri/capabilities/mobile-android.json(0 hunks)frontend/src-tauri/capabilities/mobile-ios.json(0 hunks)frontend/src-tauri/src/lib.rs(3 hunks)frontend/src/app.tsx(2 hunks)frontend/src/components/GlobalNotification.tsx(2 hunks)frontend/src/components/UpdateEventListener.tsx(1 hunks)
💤 Files with no reviewable changes (4)
- frontend/src-tauri/capabilities/mobile-android.json
- frontend/src-tauri/capabilities/mobile-ios.json
- frontend/src-tauri/capabilities/default.json
- frontend/src-tauri/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (2)
- frontend/src/components/GlobalNotification.tsx
- frontend/src/components/UpdateEventListener.tsx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: Use 2-space indentation, double quotes, and a 100-character line limit for formatting
Use camelCase for variable and function names
Use try/catch with specific error types for error handling
Files:
frontend/src/app.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use strict TypeScript typing and avoid
anywhen possible
Files:
frontend/src/app.tsx
🧠 Learnings (2)
📚 Learning: 2025-08-30T22:07:39.291Z
Learnt from: AnthonyRonning
Repo: OpenSecretCloud/Maple PR: 212
File: frontend/src/billing/billingApi.ts:652-674
Timestamp: 2025-08-30T22:07:39.291Z
Learning: In frontend/src/billing/billingApi.ts and similar TypeScript files, the team prefers to rely on TypeScript's type inference through function return type declarations rather than adding explicit type casts to response.json() calls. This reduces code verbosity while maintaining type safety.
Applied to files:
frontend/src/app.tsx
📚 Learning: 2025-03-25T19:50:07.925Z
Learnt from: AnthonyRonning
Repo: OpenSecretCloud/Maple PR: 23
File: frontend/src-tauri/src/lib.rs:14-16
Timestamp: 2025-03-25T19:50:07.925Z
Learning: In Tauri applications, updater security configurations (including endpoints, public key for verification, and dialog settings) should be defined in the tauri.conf.json file rather than duplicated in the code. When initialized with tauri_plugin_updater::Builder::new().build(), the plugin automatically reads and uses these settings from the configuration file.
Applied to files:
frontend/src-tauri/src/lib.rs
🧬 Code graph analysis (1)
frontend/src/app.tsx (1)
frontend/src/components/UpdateEventListener.tsx (1)
UpdateEventListener(11-65)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: build-android
- GitHub Check: build-ios
- GitHub Check: build-linux
- GitHub Check: build-macos (universal-apple-darwin)
- GitHub Check: Cloudflare Pages
🔇 Additional comments (3)
frontend/src/app.tsx (1)
16-16: UpdateEventListener import and placement look correctImporting
UpdateEventListenerand rendering it alongsideProxyEventListenerandDeepLinkHandler, insideNotificationProvider, is a clean way to host global side-effect listeners. This ensuresuseNotificationcontext is available without affecting routing or other providers. No issues from this integration.Also applies to: 103-103
frontend/src-tauri/src/lib.rs (2)
8-13: LGTM!The command is correctly gated for desktop-only compilation and uses the proper Tauri API for triggering a restart. The logging provides good traceability for debugging.
46-46: LGTM!The command is properly registered within the desktop-only configuration block.
Summary
Replace the intrusive native dialog popup with a subtle toast notification when an update is ready to install.
Changes
GlobalNotification.tsxwith action button support and persistent mode (duration=0)UpdateEventListener.tsxto handleupdate-readyevents from the Rust backendlib.rsto emit events instead of showing native dialogrestart_for_updatecommand for frontend-triggered restartUser Experience
Testing
Tested locally by temporarily lowering version to trigger update detection. Toast appeared correctly with both buttons functioning as expected.
Summary by CodeRabbit
New Features
Refactor
Chores
✏️ Tip: You can customize this high-level summary in your review settings.