Skip to main content

tauri_plugin_notification/
lib.rs

1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API.
6//!
7//! ## Cargo features
8//!
9//! - **windows7-compat**: Deprecated and does nothing, since Tauri no longer supports Windows 7.
10
11#![doc(
12    html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
13    html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
14)]
15
16use serde::Serialize;
17#[cfg(all(desktop, unix, not(target_os = "macos")))]
18use std::fmt;
19#[cfg(desktop)]
20use tauri::AppHandle;
21#[cfg(mobile)]
22use tauri::plugin::PluginHandle;
23use tauri::{
24    Manager, Runtime,
25    plugin::{Builder, TauriPlugin},
26};
27
28pub use models::*;
29pub use tauri::plugin::PermissionState;
30
31#[cfg(desktop)]
32mod desktop;
33#[cfg(mobile)]
34mod mobile;
35
36mod commands;
37mod error;
38mod models;
39
40pub use error::{Error, Result};
41
42#[cfg(desktop)]
43pub use desktop::Notification;
44#[cfg(mobile)]
45pub use mobile::Notification;
46
47/// The notification builder.
48#[derive(Debug)]
49pub struct NotificationBuilder<R: Runtime> {
50    #[cfg(desktop)]
51    app: AppHandle<R>,
52    #[cfg(mobile)]
53    handle: PluginHandle<R>,
54    pub(crate) data: NotificationData,
55    #[cfg(all(desktop, unix, not(target_os = "macos")))]
56    on_close: Option<OnClose>,
57}
58
59#[cfg(all(desktop, unix, not(target_os = "macos")))]
60pub(crate) struct OnClose(Box<dyn FnOnce() + Send + 'static>);
61
62#[cfg(all(desktop, unix, not(target_os = "macos")))]
63impl fmt::Debug for OnClose {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        f.write_str("OnClose")
66    }
67}
68
69impl<R: Runtime> NotificationBuilder<R> {
70    #[cfg(desktop)]
71    fn new(app: AppHandle<R>) -> Self {
72        Self {
73            app,
74            data: Default::default(),
75            #[cfg(all(desktop, unix, not(target_os = "macos")))]
76            on_close: None,
77        }
78    }
79
80    #[cfg(mobile)]
81    fn new(handle: PluginHandle<R>) -> Self {
82        Self {
83            handle,
84            data: Default::default(),
85        }
86    }
87
88    /// Sets the notification identifier.
89    pub fn id(mut self, id: i32) -> Self {
90        self.data.id = id;
91        self
92    }
93
94    /// Sets the identifier of the notification channel that delivers this notification.
95    ///
96    /// If the channel does not exist, the notification won't fire.
97    /// Make sure the channel exists with `Notification::list_channels` and
98    /// `Notification::create_channel`.
99    ///
100    /// Only used on Android.
101    pub fn channel_id(mut self, id: impl Into<String>) -> Self {
102        self.data.channel_id.replace(id.into());
103        self
104    }
105
106    /// Sets the notification title.
107    pub fn title(mut self, title: impl Into<String>) -> Self {
108        self.data.title.replace(title.into());
109        self
110    }
111
112    /// Sets the notification body.
113    pub fn body(mut self, body: impl Into<String>) -> Self {
114        self.data.body.replace(body.into());
115        self
116    }
117
118    /// Schedule this notification to fire on a later time or a fixed interval.
119    pub fn schedule(mut self, schedule: Schedule) -> Self {
120        self.data.schedule.replace(schedule);
121        self
122    }
123
124    /// Multiline text.
125    /// Changes the notification style to big text.
126    /// Cannot be used with `inboxLines`.
127    pub fn large_body(mut self, large_body: impl Into<String>) -> Self {
128        self.data.large_body.replace(large_body.into());
129        self
130    }
131
132    /// Detail text for the notification with `largeBody`, `inboxLines` or `groupSummary`.
133    pub fn summary(mut self, summary: impl Into<String>) -> Self {
134        self.data.summary.replace(summary.into());
135        self
136    }
137
138    /// Defines an action type for this notification.
139    pub fn action_type_id(mut self, action_type_id: impl Into<String>) -> Self {
140        self.data.action_type_id.replace(action_type_id.into());
141        self
142    }
143
144    /// Identifier used to group multiple notifications.
145    ///
146    /// <https://developer.apple.com/documentation/usernotifications/unmutablenotificationcontent/1649872-threadidentifier>
147    pub fn group(mut self, group: impl Into<String>) -> Self {
148        self.data.group.replace(group.into());
149        self
150    }
151
152    /// Instructs the system that this notification is the summary of a group on Android.
153    pub fn group_summary(mut self) -> Self {
154        self.data.group_summary = true;
155        self
156    }
157
158    /// The sound resource name for the notification.
159    pub fn sound(mut self, sound: impl Into<String>) -> Self {
160        self.data.sound.replace(sound.into());
161        self
162    }
163
164    /// Append an inbox line to the notification.
165    /// Changes the notification style to inbox.
166    /// Cannot be used with `largeBody`.
167    ///
168    /// Only supports up to 5 lines.
169    pub fn inbox_line(mut self, line: impl Into<String>) -> Self {
170        self.data.inbox_lines.push(line.into());
171        self
172    }
173
174    /// Notification icon.
175    ///
176    /// On Android the icon must be placed in the app's `res/drawable` folder.
177    pub fn icon(mut self, icon: impl Into<String>) -> Self {
178        self.data.icon.replace(icon.into());
179        self
180    }
181
182    /// Notification large icon (Android).
183    ///
184    /// The icon must be placed in the app's `res/drawable` folder.
185    pub fn large_icon(mut self, large_icon: impl Into<String>) -> Self {
186        self.data.large_icon.replace(large_icon.into());
187        self
188    }
189
190    /// Icon color on Android.
191    pub fn icon_color(mut self, icon_color: impl Into<String>) -> Self {
192        self.data.icon_color.replace(icon_color.into());
193        self
194    }
195
196    /// Append an attachment to the notification.
197    pub fn attachment(mut self, attachment: Attachment) -> Self {
198        self.data.attachments.push(attachment);
199        self
200    }
201
202    /// Adds an extra payload to store in the notification.
203    pub fn extra(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
204        self.data
205            .extra
206            .insert(key.into(), serde_json::to_value(value).unwrap());
207        self
208    }
209
210    /// If true, the notification cannot be dismissed by the user on Android.
211    ///
212    /// An application service must manage the dismissal of the notification.
213    /// It is typically used to indicate a background task that is pending (e.g. a file download)
214    /// or the user is engaged with (e.g. playing music).
215    pub fn ongoing(mut self) -> Self {
216        self.data.ongoing = true;
217        self
218    }
219
220    /// Automatically cancel the notification when the user clicks on it.
221    pub fn auto_cancel(mut self) -> Self {
222        self.data.auto_cancel = true;
223        self
224    }
225
226    /// Changes the notification presentation to be silent on iOS (no badge, no sound, not listed).
227    pub fn silent(mut self) -> Self {
228        self.data.silent = true;
229        self
230    }
231
232    /// Sets a callback to run when the notification closes, whether the user dismissed it, it
233    /// expired or the app removed it.
234    ///
235    /// The callback runs on a background thread and should return quickly. It does not run when
236    /// the session bus cannot be reached, or when the app exits first.
237    ///
238    /// ## Platform-specific
239    ///
240    /// - **Linux and the BSDs**: only available there.
241    #[cfg(all(desktop, unix, not(target_os = "macos")))]
242    pub fn on_close<F: FnOnce() + Send + 'static>(mut self, handler: F) -> Self {
243        self.on_close = Some(OnClose(Box::new(handler)));
244        self
245    }
246}
247
248/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the notification APIs.
249pub trait NotificationExt<R: Runtime> {
250    /// Returns the notification APIs managed by the plugin.
251    ///
252    /// # Examples
253    ///
254    /// ```no_run
255    /// use tauri_plugin_notification::NotificationExt;
256    ///
257    /// fn notify<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
258    ///   app.notification()
259    ///     .builder()
260    ///     .title("Tauri")
261    ///     .body("Tauri is awesome!")
262    ///     .show()
263    ///     .unwrap();
264    /// }
265    /// ```
266    fn notification(&self) -> &Notification<R>;
267}
268
269impl<R: Runtime, T: Manager<R>> crate::NotificationExt<R> for T {
270    fn notification(&self) -> &Notification<R> {
271        self.state::<Notification<R>>().inner()
272    }
273}
274
275/// Initializes the plugin.
276pub fn init<R: Runtime>() -> TauriPlugin<R> {
277    let builder = Builder::new("notification");
278    // On mobile the other commands are implemented by the Kotlin and Swift plugins.
279    #[cfg(mobile)]
280    let builder = builder.invoke_handler(tauri::generate_handler![
281        commands::notify,
282        commands::request_permission,
283        commands::is_permission_granted
284    ]);
285    #[cfg(desktop)]
286    let builder = builder
287        .invoke_handler(tauri::generate_handler![
288            commands::notify,
289            commands::request_permission,
290            commands::is_permission_granted,
291            commands::register_action_types,
292            commands::remove_active,
293            commands::register_listener,
294            commands::remove_listener
295        ])
296        // the listeners of a page are gone once it navigates away or its window is destroyed
297        .on_page_load(|webview, payload| {
298            if payload.event() == tauri::webview::PageLoadEvent::Started
299                && let Some(notification) = webview.try_state::<Notification<R>>()
300            {
301                notification.remove_webview_listeners(webview.label());
302            }
303        })
304        .on_event(|app, event| {
305            if let tauri::RunEvent::WindowEvent {
306                label,
307                event: tauri::WindowEvent::Destroyed,
308                ..
309            } = event
310                && let Some(notification) = app.try_state::<Notification<R>>()
311            {
312                notification.remove_window_listeners(label);
313            }
314        });
315    builder
316        .js_init_script(include_str!("init-iife.js").replace(
317            "__TEMPLATE_windows__",
318            if cfg!(windows) { "true" } else { "false" },
319        ))
320        .setup(|app, api| {
321            #[cfg(mobile)]
322            let notification = mobile::init(app, api)?;
323            #[cfg(desktop)]
324            let notification = desktop::init(app, api)?;
325            app.manage(notification);
326            Ok(())
327        })
328        .build()
329}