mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Fix desktop notifications on GNOME 46+ Linux (#1246)
Signed-off-by: benthecarman <benthecarman@live.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c6e3e947ab
commit
ca50d83289
Generated
+1
@@ -871,6 +871,7 @@ dependencies = [
|
|||||||
"mesh-llm-sdk",
|
"mesh-llm-sdk",
|
||||||
"neteq",
|
"neteq",
|
||||||
"nostr",
|
"nostr",
|
||||||
|
"notify-rust",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit",
|
||||||
"opus",
|
"opus",
|
||||||
"png 0.18.1",
|
"png 0.18.1",
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ tauri-build = { version = "2", features = [] }
|
|||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
ctrlc = { version = "3", features = ["termination"] }
|
ctrlc = { version = "3", features = ["termination"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
keyring = { version = "3.6.3", default-features = false, features = ["sync-secret-service", "vendored"], optional = true }
|
||||||
|
# Used directly (alongside tauri-plugin-notification) so we can hold the posting
|
||||||
|
# D-Bus connection open. GNOME 46+ dismisses a notification the moment that
|
||||||
|
# connection is dropped, which the plugin does immediately. Default features
|
||||||
|
# keep the pure-Rust zbus backend, matching the plugin (no libdbus needed).
|
||||||
|
notify-rust = "4"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
objc2-app-kit = { version = "0.3.2", default-features = false, features = ["NSHapticFeedback"] }
|
objc2-app-kit = { version = "0.3.2", default-features = false, features = ["NSHapticFeedback"] }
|
||||||
keyring = { version = "3.6.3", default-features = false, features = ["apple-native", "vendored"], optional = true }
|
keyring = { version = "3.6.3", default-features = false, features = ["apple-native", "vendored"], optional = true }
|
||||||
@@ -41,9 +49,6 @@ security-framework = { version = "3.7.0", features = ["OSX_10_15"] }
|
|||||||
windows-sys = { version = "0.61", features = ["Win32_Storage_FileSystem", "Win32_System_JobObjects", "Win32_System_Threading", "Win32_Foundation"] }
|
windows-sys = { version = "0.61", features = ["Win32_Storage_FileSystem", "Win32_System_JobObjects", "Win32_System_Threading", "Win32_Foundation"] }
|
||||||
keyring = { version = "3.6.3", default-features = false, features = ["windows-native", "vendored"], optional = true }
|
keyring = { version = "3.6.3", default-features = false, features = ["windows-native", "vendored"], optional = true }
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
|
||||||
keyring = { version = "3.6.3", default-features = false, features = ["sync-secret-service", "vendored"], optional = true }
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
atomic-write-file = "0.3"
|
atomic-write-file = "0.3"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ mod media_transcode;
|
|||||||
#[cfg(feature = "mesh-llm")]
|
#[cfg(feature = "mesh-llm")]
|
||||||
mod mesh_llm;
|
mod mesh_llm;
|
||||||
mod messages;
|
mod messages;
|
||||||
|
mod notifications;
|
||||||
pub mod pairing;
|
pub mod pairing;
|
||||||
mod personas;
|
mod personas;
|
||||||
mod prevent_sleep;
|
mod prevent_sleep;
|
||||||
@@ -45,6 +46,7 @@ pub use media_download::*;
|
|||||||
#[cfg(feature = "mesh-llm")]
|
#[cfg(feature = "mesh-llm")]
|
||||||
pub use mesh_llm::*;
|
pub use mesh_llm::*;
|
||||||
pub use messages::*;
|
pub use messages::*;
|
||||||
|
pub use notifications::*;
|
||||||
pub use pairing::*;
|
pub use pairing::*;
|
||||||
pub use personas::*;
|
pub use personas::*;
|
||||||
pub use prevent_sleep::*;
|
pub use prevent_sleep::*;
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
//! Native (Linux) desktop-notification helper.
|
||||||
|
//!
|
||||||
|
//! `tauri-plugin-notification` posts a notification by calling `notify_rust`'s
|
||||||
|
//! `show()` and then immediately dropping the returned `NotificationHandle`.
|
||||||
|
//! That handle owns the D-Bus connection used to post the notification, and on
|
||||||
|
//! GNOME 46+ (Ubuntu 24.04+, Fedora 41+) tearing that connection down dismisses
|
||||||
|
//! the notification the instant it appears — so notifications never show.
|
||||||
|
//! See tauri-apps/plugins-workspace#2566 and hoodie/notify-rust#218.
|
||||||
|
//!
|
||||||
|
//! We side-step the plugin on Linux by posting the notification from a
|
||||||
|
//! dedicated thread that holds the connection open (via `wait_for_action`)
|
||||||
|
//! until the notification is closed. The same wait surfaces the default click
|
||||||
|
//! action, which we forward to the frontend so it can focus the window and
|
||||||
|
//! route to the notification target.
|
||||||
|
|
||||||
|
/// Show a desktop notification natively.
|
||||||
|
///
|
||||||
|
/// On Linux this uses the connection-preserving path described above. On other
|
||||||
|
/// platforms the bundled notification plugin already works correctly, so the
|
||||||
|
/// frontend never calls this and we simply report that it is unused.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn show_native_notification(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
title: String,
|
||||||
|
body: Option<String>,
|
||||||
|
target: Option<serde_json::Value>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
linux::show(app, title, body, target);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
let _ = (&app, &title, &body, &target);
|
||||||
|
Err("show_native_notification is only supported on Linux".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
mod linux {
|
||||||
|
use tauri::Emitter;
|
||||||
|
|
||||||
|
/// Emitted to the frontend when the user clicks a native notification. The
|
||||||
|
/// payload is the opaque target object the frontend passed in.
|
||||||
|
const ACTIVATE_EVENT: &str = "native-notification-activated";
|
||||||
|
|
||||||
|
pub fn show(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
title: String,
|
||||||
|
body: Option<String>,
|
||||||
|
target: Option<serde_json::Value>,
|
||||||
|
) {
|
||||||
|
// notify_rust's `show()` blocks on D-Bus and the returned handle must
|
||||||
|
// outlive the notification, so this runs on its own thread rather than
|
||||||
|
// the async runtime.
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let mut builder = notify_rust::Notification::new();
|
||||||
|
builder.summary(&title);
|
||||||
|
if let Some(body) = body.as_deref() {
|
||||||
|
builder.body(body);
|
||||||
|
}
|
||||||
|
if let Some(name) = app.config().product_name.clone() {
|
||||||
|
builder.appname(&name);
|
||||||
|
}
|
||||||
|
// Tie the notification to the installed desktop entry so GNOME shows
|
||||||
|
// the app's name and icon and groups our notifications together.
|
||||||
|
builder.hint(notify_rust::Hint::DesktopEntry(
|
||||||
|
app.config().identifier.clone(),
|
||||||
|
));
|
||||||
|
builder.auto_icon();
|
||||||
|
// Match the silent posting used on other platforms; the app does its
|
||||||
|
// own unread cues and a per-message sound would be noisy.
|
||||||
|
builder.hint(notify_rust::Hint::SuppressSound(true));
|
||||||
|
// Declaring a default action makes the whole notification clickable.
|
||||||
|
builder.action("default", "Open");
|
||||||
|
|
||||||
|
let handle = match builder.show() {
|
||||||
|
Ok(handle) => handle,
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("buzz-desktop: failed to post native notification: {error}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Block until the notification is actioned or closed. Holding the
|
||||||
|
// handle keeps its D-Bus connection alive, which is what stops
|
||||||
|
// GNOME 46+ from dismissing the notification immediately. The wait
|
||||||
|
// also returns when the notification expires or is dismissed, so
|
||||||
|
// the thread does not leak.
|
||||||
|
handle.wait_for_action(|action| {
|
||||||
|
if action != "default" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The frontend focuses the window on activation (the same path
|
||||||
|
// every other platform uses), so we only forward the target.
|
||||||
|
let _ = app.emit(ACTIVATE_EVENT, target);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -802,6 +802,7 @@ pub fn run() {
|
|||||||
add_reaction,
|
add_reaction,
|
||||||
remove_reaction,
|
remove_reaction,
|
||||||
get_event,
|
get_event,
|
||||||
|
show_native_notification,
|
||||||
upload_media,
|
upload_media,
|
||||||
pick_and_upload_media,
|
pick_and_upload_media,
|
||||||
upload_media_bytes,
|
upload_media_bytes,
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { isTauri } from "@tauri-apps/api/core";
|
import { invoke, isTauri } from "@tauri-apps/api/core";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { UserAttentionType, getCurrentWindow } from "@tauri-apps/api/window";
|
import { UserAttentionType, getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import {
|
import {
|
||||||
isPermissionGranted,
|
isPermissionGranted,
|
||||||
onAction,
|
onAction,
|
||||||
requestPermission,
|
requestPermission,
|
||||||
} from "@tauri-apps/plugin-notification";
|
} from "@tauri-apps/plugin-notification";
|
||||||
import { isMacPlatform } from "@/shared/lib/platform";
|
import { isLinuxPlatform, isMacPlatform } from "@/shared/lib/platform";
|
||||||
|
|
||||||
|
// Backend event emitted when the user clicks a native (Linux) notification.
|
||||||
|
// See src-tauri/src/commands/notifications.rs.
|
||||||
|
const NATIVE_NOTIFICATION_ACTIVATED_EVENT = "native-notification-activated";
|
||||||
|
|
||||||
export type DesktopNotificationPermissionState =
|
export type DesktopNotificationPermissionState =
|
||||||
| NotificationPermission
|
| NotificationPermission
|
||||||
@@ -172,6 +177,7 @@ export async function listenForDesktopNotificationActions(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let pluginListener: { unregister: () => Promise<void> } | null = null;
|
let pluginListener: { unregister: () => Promise<void> } | null = null;
|
||||||
|
let nativeUnlisten: (() => void) | null = null;
|
||||||
|
|
||||||
if (isTauri()) {
|
if (isTauri()) {
|
||||||
try {
|
try {
|
||||||
@@ -188,6 +194,24 @@ export async function listenForDesktopNotificationActions(
|
|||||||
} catch {
|
} catch {
|
||||||
pluginListener = null;
|
pluginListener = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clicks on Linux notifications come back via a backend event rather than
|
||||||
|
// the plugin's onAction (whose connection is torn down before it can fire).
|
||||||
|
try {
|
||||||
|
nativeUnlisten = await listen<unknown>(
|
||||||
|
NATIVE_NOTIFICATION_ACTIVATED_EVENT,
|
||||||
|
(event) => {
|
||||||
|
const target = parseNotificationTarget(event.payload);
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchDesktopNotificationTarget(target);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
nativeUnlisten = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -196,6 +220,7 @@ export async function listenForDesktopNotificationActions(
|
|||||||
handleNotificationAction,
|
handleNotificationAction,
|
||||||
);
|
);
|
||||||
void pluginListener?.unregister();
|
void pluginListener?.unregister();
|
||||||
|
nativeUnlisten?.();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +293,23 @@ export async function sendDesktopNotification(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On Linux the bundled notification plugin posts via a D-Bus connection that
|
||||||
|
// it drops immediately; GNOME 46+ then dismisses the notification before it
|
||||||
|
// is seen. Route through a backend command that keeps the connection alive.
|
||||||
|
// See src-tauri/src/commands/notifications.rs.
|
||||||
|
if (isTauri() && isLinuxPlatform()) {
|
||||||
|
try {
|
||||||
|
await invoke("show_native_notification", {
|
||||||
|
title: payload.title,
|
||||||
|
body: payload.body,
|
||||||
|
target: payload.target ?? null,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const notification = new window.Notification(payload.title, {
|
const notification = new window.Notification(payload.title, {
|
||||||
body: payload.body,
|
body: payload.body,
|
||||||
silent: true,
|
silent: true,
|
||||||
|
|||||||
@@ -12,6 +12,17 @@ export function isMacPlatform(): boolean {
|
|||||||
return /mac|iphone|ipad|ipod/i.test(navigator.platform);
|
return /mac|iphone|ipad|ipod/i.test(navigator.platform);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns true on Linux desktops (excludes Android). */
|
||||||
|
export function isLinuxPlatform(): boolean {
|
||||||
|
if (typeof navigator === "undefined") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
/linux/i.test(navigator.platform) && !/android/i.test(navigator.userAgent)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The platform's normal application-shortcut modifier:
|
* The platform's normal application-shortcut modifier:
|
||||||
* - macOS: Command (Meta)
|
* - macOS: Command (Meta)
|
||||||
|
|||||||
Reference in New Issue
Block a user