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",
|
||||
"neteq",
|
||||
"nostr",
|
||||
"notify-rust",
|
||||
"objc2-app-kit",
|
||||
"opus",
|
||||
"png 0.18.1",
|
||||
|
||||
@@ -32,6 +32,14 @@ tauri-build = { version = "2", features = [] }
|
||||
libc = "0.2"
|
||||
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]
|
||||
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 }
|
||||
@@ -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"] }
|
||||
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]
|
||||
atomic-write-file = "0.3"
|
||||
anyhow = "1"
|
||||
|
||||
@@ -17,6 +17,7 @@ mod media_transcode;
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
mod mesh_llm;
|
||||
mod messages;
|
||||
mod notifications;
|
||||
pub mod pairing;
|
||||
mod personas;
|
||||
mod prevent_sleep;
|
||||
@@ -45,6 +46,7 @@ pub use media_download::*;
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
pub use mesh_llm::*;
|
||||
pub use messages::*;
|
||||
pub use notifications::*;
|
||||
pub use pairing::*;
|
||||
pub use personas::*;
|
||||
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,
|
||||
remove_reaction,
|
||||
get_event,
|
||||
show_native_notification,
|
||||
upload_media,
|
||||
pick_and_upload_media,
|
||||
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 {
|
||||
isPermissionGranted,
|
||||
onAction,
|
||||
requestPermission,
|
||||
} 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 =
|
||||
| NotificationPermission
|
||||
@@ -172,6 +177,7 @@ export async function listenForDesktopNotificationActions(
|
||||
);
|
||||
|
||||
let pluginListener: { unregister: () => Promise<void> } | null = null;
|
||||
let nativeUnlisten: (() => void) | null = null;
|
||||
|
||||
if (isTauri()) {
|
||||
try {
|
||||
@@ -188,6 +194,24 @@ export async function listenForDesktopNotificationActions(
|
||||
} catch {
|
||||
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 () => {
|
||||
@@ -196,6 +220,7 @@ export async function listenForDesktopNotificationActions(
|
||||
handleNotificationAction,
|
||||
);
|
||||
void pluginListener?.unregister();
|
||||
nativeUnlisten?.();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -268,6 +293,23 @@ export async function sendDesktopNotification(
|
||||
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, {
|
||||
body: payload.body,
|
||||
silent: true,
|
||||
|
||||
@@ -12,6 +12,17 @@ export function isMacPlatform(): boolean {
|
||||
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:
|
||||
* - macOS: Command (Meta)
|
||||
|
||||
Reference in New Issue
Block a user