Make notifications appear on screen, and be clickable

Three separate faults, all mine:

set_application was never called, so mac-notification-sys looked up an
app named "use_default", failed, and posted every notification as
com.apple.Finder - wearing Finder's alert style rather than this app's.

NSUserNotificationCenter suppresses the banner whenever the posting app
is frontmost unless the delegate implements shouldPresentNotification:.
The crate's delegate implements only delivery and activation, so the
method is added to its class at runtime. Without it, Gmail notifying
while you sit in Odoo - same window, still frontmost - is never seen,
which is the case the whole design exists for.

send_notification only waits for a response when the options ask it to.
Passing None returned instantly with NotificationResponse::None, so the
click branch was unreachable. Waiting parks a thread and a notification
left unread never resolves, so waiters are capped at 32.

The diagnostic now records the raise before blocking, since with
wait_for_click a notification sitting on screen otherwise read as
"none yet".
This commit is contained in:
2026-09-01 13:25:02 +02:00
parent 3525d454bf
commit 21a9f065f0
3 changed files with 129 additions and 6 deletions
+43 -4
View File
@@ -6,6 +6,7 @@
//! the app first.
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use serde::Serialize;
use tauri::{
@@ -181,21 +182,52 @@ fn notify(
) {
// The app's own name leads, or a notification from four tools in one
// window says nothing about which one wants you.
// Waiting for a click costs a parked thread, and a notification left
// sitting in Notification Centre never resolves — so the wait is capped.
// Past the cap the notification still appears, it just cannot be clicked
// through, which is a far better failure than an unbounded thread count.
const MAX_WAITING: usize = 32;
static WAITING: AtomicUsize = AtomicUsize::new(0);
let heading = if app_name.is_empty() { "Work".to_string() } else { app_name.to_string() };
let subtitle = title.to_string();
let message = body.to_string();
let handle = handle.clone();
let app_id = app_id.to_string();
std::thread::spawn(move || {
let waiting = WAITING.fetch_add(1, Ordering::SeqCst) < MAX_WAITING;
if !waiting {
WAITING.fetch_sub(1, Ordering::SeqCst);
}
let spawned = std::thread::Builder::new()
// Small, because these park rather than compute, and there may be many.
.stack_size(512 * 1024)
.spawn(move || {
let state = handle.state::<crate::commands::AppState>();
// Recorded before the call, not after: `wait_for_click` blocks until
// the user acts, so waiting for the return made a notification that
// was sitting on screen read as "none yet".
*state.last_notification.lock().unwrap() = format!("{heading} / {subtitle} → raised");
// `wait_for_click` is what makes this block until the user acts.
// Without it the call returns immediately with `None`, and no click is
// ever observed — the notification appears, and clicking it does
// nothing at all.
let mut options = mac_notification_sys::Notification::new();
options.wait_for_click(waiting);
let response = mac_notification_sys::send_notification(
&heading,
if subtitle.is_empty() { None } else { Some(&subtitle) },
&message,
None,
Some(&options),
);
if waiting {
WAITING.fetch_sub(1, Ordering::SeqCst);
}
match response {
Ok(mac_notification_sys::NotificationResponse::Click) => {
*state.last_notification.lock().unwrap() =
@@ -205,9 +237,9 @@ fn notify(
NotificationClick { app_id, notification_id },
);
}
Ok(_) => {
Ok(other) => {
*state.last_notification.lock().unwrap() =
format!("{heading} / {subtitle}raised");
format!("{heading} / {subtitle}{other:?}");
}
Err(e) => {
eprintln!("could not raise a notification: {e}");
@@ -215,6 +247,13 @@ fn notify(
}
}
});
if let Err(e) = spawned {
eprintln!("could not start a notification thread: {e}");
if waiting {
WAITING.fetch_sub(1, Ordering::SeqCst);
}
}
}
/// Turns on WKWebView's two-finger back and forward swipes.