Skip to content

feat(desktop): native notifications for hmz web's page - #379

Closed
futrime wants to merge 1 commit into
mainfrom
feat/desktop-notify
Closed

futrime wants to merge 1 commit into
mainfrom
feat/desktop-notify

Conversation

@futrime

@futrime futrime commented Oct 12, 2026

Copy link
Copy Markdown
Member

Unit 11 of the multi-daemon and notifications batch. It implements contract C8.

What

  • The notify command takes {title, body, address} and posts a native macOS notification through UNUserNotificationCenter, using objc2's objc2-user-notifications.
    • The first call asks for permission.
    • A delegate handles clicks, and shows the banner even while the app is in front.
  • Clicking a notification brings the window forward.
    • If address is a #/... hash, the window opens that hash at / on hmz web's own origin. It is a hash change only, so the page does not reload.
    • Anything that is not such a hash, or is longer than 2048 bytes, or contains control characters, only focuses the window. The click never goes anywhere else.
    • The address is carried in the request identifier, so a notification from an earlier launch still opens it. A click that launches the app opens the hash once the page is there, like a .hmzepic opened early.
  • ACL. build.rs declares an app manifest for status, retry, keep, update_now and notify.
    • capabilities/page.json (remote http://127.0.0.1:*) gets only allow-notify, next to drag and zoom.
    • capabilities/default.json (the app's own page) gets the other four.
    • The ACL works as intended: the remote page's call to status is refused.
  • Bundle guard. Outside a .app bundle (cargo run/tauri dev), notifications are only logged, because UNUserNotificationCenter throws for a process that is not in a bundle.
  • Size. No new crates in the lock: objc2, objc2-foundation, block2 and objc2-user-notifications were already transitive dependencies. The generated permissions/autogenerated/ is gitignored, like gen/.
  • Docs. docs/user/desktop.md gets a Notifications section.

I picked UNUserNotificationCenter over tauri-plugin-notification, which on macOS goes through notify-rust/mac-notification-sys, the deprecated NSUserNotification, and has no click handling. This way gives click-to-focus and click-to-navigate with no new crates.

Spec

specs/desktop.md still says the page is granted "none of the app's commands". Unit 1 rewrites that for C8. This PR does not edit specs.

Tests

  • Rust unit tests cover:
    • which addresses are valid, and that the result always stays on the server's origin
    • the key being kept on a launch click
    • unique identifiers that carry the address
    • detecting the bundle
  • cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings and cargo test --locked pass (46 tests).
  • pnpm tauri build --debug --bundles app builds.
  • I launched the debug .app with HUMANIZE_HMZ set to a stand-in hmz web that serves a page calling invoke('notify', ...):
    • notify resolves.
    • The usernoted log shows the app connecting and asking for authorization. macOS put up its "Allow notifications" prompt, which only the user can answer.
    • invoke('status') from the remote page is refused by the ACL.
  • What I could not check here:
    • I could not take a screenshot of the banner: there is no screen-recording permission, and the permission prompt was left for the user.
    • The click-to-navigate path is covered by unit tests only.
  • pre-commit passes except pyright on macOS. That failure is in Linux-only stdlib attributes in files this PR does not touch, and is the same on main.

🤖 Generated with Claude Code

The page in the app can call `notify` with {title, body, address}. The app
posts it through UNUserNotificationCenter, asking for permission the first
time. Clicking the notification brings the window forward. If the address is
a `#/...` hash, the app opens that hash on hmz web's own origin and goes
nowhere else. A click that launches the app opens the hash once the page is
there.

- The app's commands get an ACL manifest. The remote page
  (http://127.0.0.1:*) is granted only `notify`, next to drag and zoom. The
  app's own page keeps status, retry, keep and update_now.
- No new crates: objc2, objc2-foundation, block2 and
  objc2-user-notifications are already in the lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 12, 2026 14:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@futrime

futrime commented Oct 12, 2026

Copy link
Copy Markdown
Member Author

Landed together in #385.

@futrime futrime closed this Oct 12, 2026
@futrime
futrime deleted the feat/desktop-notify branch October 12, 2026 23:56
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