Skip to content

fix: route native notification clicks back to the page - #1394

Merged
tw93 merged 2 commits into
tw93:mainfrom
yhcharles:chy/notification-click-routing-pr
Oct 5, 2026
Merged

tw93 merged 2 commits into
tw93:mainfrom
yhcharles:chy/notification-click-routing-pr

Conversation

@yhcharles

Copy link
Copy Markdown
Contributor

Problem

Pake replaces window.Notification with a bridge to tauri-plugin-notification, but that plugin is fire-and-forget on desktop: show() hands the notification to notify_rust and drops the handle, so nothing ever reports which notification the user clicked. The page's own click routing therefore never runs, and a packaged Slack/Discord/etc. never jumps to the conversation a notification came from — arguably one of the most common reasons someone would package a chat site with Pake in the first place.

Fix

macOS now delivers notifications itself through NSUserNotificationCenter with a delegate, tagging each one with <window label>|<id>. A click reveals the originating window through the same unminimize + show + reapply_window_icon + focus sequence as every other hidden-to-visible path (#1323), then hands the exact id back to that webview. Availability is probed once on the main thread during setup, so a process without a bundle identifier (e.g. pnpm run dev) or a future macOS without the deprecated API falls back to the plugin path instead of dereferencing nil. Other platforms keep the existing plugin path unchanged — this PR is macOS-first, since that's where the native click-report API is available today.

The page-side notification is now a real EventTarget rather than a bare object with an onclick slot, so addEventListener("click", ...) works and event.target is the notification itself — matching the standard Notification API pages actually code against. It also carries the standard tag/data/icon fields, lets a same-tag notification replace its predecessor, and caps how many stay click-addressable.

The focus heuristic Pake used before (treat regaining focus shortly after a notification as a click) is now a fallback only where the platform reports nothing: it fires once, requires the notification to have been raised while the window was unfocused, expires after 60s, and is cancelled by any in-page interaction. Rust reports nativeClick so macOS disables the heuristic entirely and an ordinary app switch cannot fire a phantom click.

Notification ids are minted by the packaged (untrusted) page and echoed into a native identifier and a webview eval, so they're validated against a strict charset and length on the Rust side before use.

Why NSUserNotification and not UserNotifications.framework

The whole NSUserNotification family is deprecated in favor of UserNotifications.framework, but that framework needs a provisioned bundle and a permission prompt that a webpage wrapper can't meaningfully ask for. It's also the API tauri-plugin-notification already reaches through notify-rust, so this isn't a new deprecated surface for the project — and the code probes for the class at startup, so a macOS version that finally drops it falls back to the plugin path rather than crashing.

Verification

  • cargo test --lib — 50 passed, including new coverage in src-tauri/src/app/notification.rs
  • npx vitest run — 510 passed, including tests/unit/notification-bridge.test.js (new)
  • cargo clippy --all-targets — no new warnings
  • cargo fmt --check, prettier --check — clean
  • Not yet verified: a real click-through on a packaged chat site on macOS. Happy to record that before merge if useful.

Pake replaces `window.Notification` with a bridge to `tauri-plugin-notification`, but that plugin is fire-and-forget on desktop: `show()` hands the notification to `notify_rust` and drops the handle, so nothing ever reports which notification the user clicked. The page's own click routing therefore never ran, and a packaged Slack never jumped to the conversation a notification came from.

macOS now delivers notifications itself through `NSUserNotificationCenter` with a delegate, tagging each one with `<window label>|<id>`. A click reveals the originating window through the same unminimize + show + `reapply_window_icon` + focus sequence as every other hidden-to-visible path (tw93#1323), then hands the exact id back to that webview. Availability is probed once on the main thread during setup, so a process without a bundle identifier or a future macOS without the deprecated API falls back to the plugin path instead of dereferencing nil. Other platforms keep the plugin path unchanged.

The page-side notification is now a real `EventTarget` rather than a bare object with an `onclick` slot, so `addEventListener("click", ...)` works and `event.target` is the notification itself. It also carries the standard `tag`/`data`/`icon` fields, lets a same-tag notification replace its predecessor, and caps how many stay click-addressable.

The focus heuristic that used to stand in for a click callback is now a fallback only where the platform reports nothing: it fires once, requires the notification to have been raised while the window was unfocused, expires after 60s, and is cancelled by any in-page interaction. Rust reports `nativeClick` so macOS disables the heuristic entirely and an ordinary app switch cannot fire a phantom click.

Ids are minted by the packaged (untrusted) page and echoed into a native identifier and a webview `eval`, so they are validated against a strict charset and length in Rust.

Covered by tests/unit/notification-bridge.test.js. The three harnesses that load event.js now provide real `Event`/`EventTarget`, and the injected bridge bails out early when they are missing rather than aborting the rest of the script.
@tw93

tw93 commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

@yhcharles Thanks for the contribution. This is merged and shipped in pake-cli 3.17.3: on macOS a notification click now reopens and focuses the window that raised it, and Windows and Linux keep the focus-based fallback. Your follow-up in #1408 landed in the same release.

Run npm install -g pake-cli@3.17.3 and rebuild an app to get it.

Withdraw closed and replaced macOS notifications, ignore stale IPC acknowledgements, and dispatch close events asynchronously to avoid lifecycle reentry. Keep native clicks tied to the originating window and notification.
@tw93
tw93 merged commit 865c587 into tw93:main Oct 5, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants