Files
tauri-plugins-workspace/plugins/notification/src/lib.rs
T
BestCodesandLucas Nogueira 4356356dab fix: keep GNOME notifications open (fix #2566) (#3588)
* experimenting...

Signed-off-by: BestCodes <106822363+The-Best-Codes@users.noreply.github.com>

* omit unconfirmed fact from .changes

Signed-off-by: BestCodes <106822363+The-Best-Codes@users.noreply.github.com>

* fix(notification): send untracked Linux notifications on the shared connection

Holding notify-rust's handle with `on_close` blocked an async runtime
worker until the server closed the notification, and opened a D-Bus
connection per notification. Since GNOME keeps notifications in its tray,
a handful of them could stall every async task of the app.

Send every notification on the connection the plugin already keeps open
for its action listener instead: it keeps the sender's name alive for
GNOME at the cost of a single connection and thread, and the listener
runs the `on_close` callbacks.

* fix(notification): offer `on_close` on the BSDs too

The BSDs share the D-Bus implementation with Linux, including the GNOME
issue, so gate `NotificationBuilder::on_close` like the rest of that
implementation and document when the callback runs.

* fix(notification): run `on_close` without the tracker lock held

The callback ran on the listener thread while the guard of the callback
map was alive, and a panic in it ended that thread, which then reported
no action of any notification for the rest of the app's life. Take the
callback out of the map before running it and contain its panics.

* fix(notification): use the covector package names in the change files

`tauri-plugin-notification` and `@tauri-apps/plugin-notification` are
not packages of .changes/config.json, so covector refused the change
file. Use `notification` and `notification-js`, and record the new
`on_close` API as a feature of its own.

Also let `on_close` take an `FnOnce`, since it runs at most once.

---------

Signed-off-by: BestCodes <106822363+The-Best-Codes@users.noreply.github.com>
Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2026-10-12 09:46:43 -03:00

330 lines
10 KiB
Rust

// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API.
//!
//! ## Cargo features
//!
//! - **windows7-compat**: Deprecated and does nothing, since Tauri no longer supports Windows 7.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use serde::Serialize;
#[cfg(all(desktop, unix, not(target_os = "macos")))]
use std::fmt;
#[cfg(desktop)]
use tauri::AppHandle;
#[cfg(mobile)]
use tauri::plugin::PluginHandle;
use tauri::{
Manager, Runtime,
plugin::{Builder, TauriPlugin},
};
pub use models::*;
pub use tauri::plugin::PermissionState;
#[cfg(desktop)]
mod desktop;
#[cfg(mobile)]
mod mobile;
mod commands;
mod error;
mod models;
pub use error::{Error, Result};
#[cfg(desktop)]
pub use desktop::Notification;
#[cfg(mobile)]
pub use mobile::Notification;
/// The notification builder.
#[derive(Debug)]
pub struct NotificationBuilder<R: Runtime> {
#[cfg(desktop)]
app: AppHandle<R>,
#[cfg(mobile)]
handle: PluginHandle<R>,
pub(crate) data: NotificationData,
#[cfg(all(desktop, unix, not(target_os = "macos")))]
on_close: Option<OnClose>,
}
#[cfg(all(desktop, unix, not(target_os = "macos")))]
pub(crate) struct OnClose(Box<dyn FnOnce() + Send + 'static>);
#[cfg(all(desktop, unix, not(target_os = "macos")))]
impl fmt::Debug for OnClose {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("OnClose")
}
}
impl<R: Runtime> NotificationBuilder<R> {
#[cfg(desktop)]
fn new(app: AppHandle<R>) -> Self {
Self {
app,
data: Default::default(),
#[cfg(all(desktop, unix, not(target_os = "macos")))]
on_close: None,
}
}
#[cfg(mobile)]
fn new(handle: PluginHandle<R>) -> Self {
Self {
handle,
data: Default::default(),
}
}
/// Sets the notification identifier.
pub fn id(mut self, id: i32) -> Self {
self.data.id = id;
self
}
/// Sets the identifier of the notification channel that delivers this notification.
///
/// If the channel does not exist, the notification won't fire.
/// Make sure the channel exists with `Notification::list_channels` and
/// `Notification::create_channel`.
///
/// Only used on Android.
pub fn channel_id(mut self, id: impl Into<String>) -> Self {
self.data.channel_id.replace(id.into());
self
}
/// Sets the notification title.
pub fn title(mut self, title: impl Into<String>) -> Self {
self.data.title.replace(title.into());
self
}
/// Sets the notification body.
pub fn body(mut self, body: impl Into<String>) -> Self {
self.data.body.replace(body.into());
self
}
/// Schedule this notification to fire on a later time or a fixed interval.
pub fn schedule(mut self, schedule: Schedule) -> Self {
self.data.schedule.replace(schedule);
self
}
/// Multiline text.
/// Changes the notification style to big text.
/// Cannot be used with `inboxLines`.
pub fn large_body(mut self, large_body: impl Into<String>) -> Self {
self.data.large_body.replace(large_body.into());
self
}
/// Detail text for the notification with `largeBody`, `inboxLines` or `groupSummary`.
pub fn summary(mut self, summary: impl Into<String>) -> Self {
self.data.summary.replace(summary.into());
self
}
/// Defines an action type for this notification.
pub fn action_type_id(mut self, action_type_id: impl Into<String>) -> Self {
self.data.action_type_id.replace(action_type_id.into());
self
}
/// Identifier used to group multiple notifications.
///
/// <https://developer.apple.com/documentation/usernotifications/unmutablenotificationcontent/1649872-threadidentifier>
pub fn group(mut self, group: impl Into<String>) -> Self {
self.data.group.replace(group.into());
self
}
/// Instructs the system that this notification is the summary of a group on Android.
pub fn group_summary(mut self) -> Self {
self.data.group_summary = true;
self
}
/// The sound resource name for the notification.
pub fn sound(mut self, sound: impl Into<String>) -> Self {
self.data.sound.replace(sound.into());
self
}
/// Append an inbox line to the notification.
/// Changes the notification style to inbox.
/// Cannot be used with `largeBody`.
///
/// Only supports up to 5 lines.
pub fn inbox_line(mut self, line: impl Into<String>) -> Self {
self.data.inbox_lines.push(line.into());
self
}
/// Notification icon.
///
/// On Android the icon must be placed in the app's `res/drawable` folder.
pub fn icon(mut self, icon: impl Into<String>) -> Self {
self.data.icon.replace(icon.into());
self
}
/// Notification large icon (Android).
///
/// The icon must be placed in the app's `res/drawable` folder.
pub fn large_icon(mut self, large_icon: impl Into<String>) -> Self {
self.data.large_icon.replace(large_icon.into());
self
}
/// Icon color on Android.
pub fn icon_color(mut self, icon_color: impl Into<String>) -> Self {
self.data.icon_color.replace(icon_color.into());
self
}
/// Append an attachment to the notification.
pub fn attachment(mut self, attachment: Attachment) -> Self {
self.data.attachments.push(attachment);
self
}
/// Adds an extra payload to store in the notification.
pub fn extra(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
self.data
.extra
.insert(key.into(), serde_json::to_value(value).unwrap());
self
}
/// If true, the notification cannot be dismissed by the user on Android.
///
/// An application service must manage the dismissal of the notification.
/// It is typically used to indicate a background task that is pending (e.g. a file download)
/// or the user is engaged with (e.g. playing music).
pub fn ongoing(mut self) -> Self {
self.data.ongoing = true;
self
}
/// Automatically cancel the notification when the user clicks on it.
pub fn auto_cancel(mut self) -> Self {
self.data.auto_cancel = true;
self
}
/// Changes the notification presentation to be silent on iOS (no badge, no sound, not listed).
pub fn silent(mut self) -> Self {
self.data.silent = true;
self
}
/// Sets a callback to run when the notification closes, whether the user dismissed it, it
/// expired or the app removed it.
///
/// The callback runs on a background thread and should return quickly. It does not run when
/// the session bus cannot be reached, or when the app exits first.
///
/// ## Platform-specific
///
/// - **Linux and the BSDs**: only available there.
#[cfg(all(desktop, unix, not(target_os = "macos")))]
pub fn on_close<F: FnOnce() + Send + 'static>(mut self, handler: F) -> Self {
self.on_close = Some(OnClose(Box::new(handler)));
self
}
}
/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the notification APIs.
pub trait NotificationExt<R: Runtime> {
/// Returns the notification APIs managed by the plugin.
///
/// # Examples
///
/// ```no_run
/// use tauri_plugin_notification::NotificationExt;
///
/// fn notify<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
/// app.notification()
/// .builder()
/// .title("Tauri")
/// .body("Tauri is awesome!")
/// .show()
/// .unwrap();
/// }
/// ```
fn notification(&self) -> &Notification<R>;
}
impl<R: Runtime, T: Manager<R>> crate::NotificationExt<R> for T {
fn notification(&self) -> &Notification<R> {
self.state::<Notification<R>>().inner()
}
}
/// Initializes the plugin.
pub fn init<R: Runtime>() -> TauriPlugin<R> {
let builder = Builder::new("notification");
// On mobile the other commands are implemented by the Kotlin and Swift plugins.
#[cfg(mobile)]
let builder = builder.invoke_handler(tauri::generate_handler![
commands::notify,
commands::request_permission,
commands::is_permission_granted
]);
#[cfg(desktop)]
let builder = builder
.invoke_handler(tauri::generate_handler![
commands::notify,
commands::request_permission,
commands::is_permission_granted,
commands::register_action_types,
commands::remove_active,
commands::register_listener,
commands::remove_listener
])
// the listeners of a page are gone once it navigates away or its window is destroyed
.on_page_load(|webview, payload| {
if payload.event() == tauri::webview::PageLoadEvent::Started
&& let Some(notification) = webview.try_state::<Notification<R>>()
{
notification.remove_webview_listeners(webview.label());
}
})
.on_event(|app, event| {
if let tauri::RunEvent::WindowEvent {
label,
event: tauri::WindowEvent::Destroyed,
..
} = event
&& let Some(notification) = app.try_state::<Notification<R>>()
{
notification.remove_window_listeners(label);
}
});
builder
.js_init_script(include_str!("init-iife.js").replace(
"__TEMPLATE_windows__",
if cfg!(windows) { "true" } else { "false" },
))
.setup(|app, api| {
#[cfg(mobile)]
let notification = mobile::init(app, api)?;
#[cfg(desktop)]
let notification = desktop::init(app, api)?;
app.manage(notification);
Ok(())
})
.build()
}