user_notify/
notification.rs

1use std::{collections::HashMap, fmt::Debug, path::PathBuf};
2
3use async_trait::async_trait;
4
5use crate::{Error, xdg_category::XdgNotificationCategory};
6
7/// A builder struct for building notifications.
8#[derive(Debug, Default)]
9pub struct NotificationBuilder {
10    pub(crate) body: Option<String>,
11    pub(crate) title: Option<String>,
12    pub(crate) subtitle: Option<String>,
13    pub(crate) image: Option<std::path::PathBuf>,
14    pub(crate) icon: Option<std::path::PathBuf>,
15    pub(crate) icon_round_crop: bool,
16    pub(crate) thread_id: Option<String>,
17    pub(crate) category_id: Option<String>,
18    pub(crate) xdg_category: Option<XdgNotificationCategory>,
19    pub(crate) xdg_app_name: Option<String>,
20    pub(crate) user_info: Option<HashMap<String, String>>,
21}
22
23impl NotificationBuilder
24where
25    Self: Sized,
26{
27    /// Create a new notification builder
28    pub fn new() -> Self {
29        NotificationBuilder {
30            ..Default::default()
31        }
32    }
33    /// Main content of notification
34    ///
35    /// Plaform specific:
36    /// - MacOS: [UNNotificationContent/body](https://developer.apple.com/documentation/usernotifications/unnotificationcontent/body)
37    /// - Linux / XDG: [body](https://specifications.freedesktop.org/notification-spec/latest/basic-design.html#:~:text=This%20is%20a%20multi,the%20summary%20is%20displayed.)
38    /// - Windows: [text2](https://docs.rs/tauri-winrt-notification/latest/tauri_winrt_notification/struct.Toast.html#method.text2)
39    pub fn body(mut self, body: &str) -> Self {
40        self.body = Some(body.to_owned());
41        self
42    }
43    /// Primary description of notification
44    ///
45    /// Plaform specific:
46    /// - MacOS: [UNNotificationContent/title](https://developer.apple.com/documentation/usernotifications/unnotificationcontent/title)
47    /// - Linux / XDG: [summary](https://specifications.freedesktop.org/notification-spec/latest/basic-design.html#:~:text=This%20is%20a,using%20UTF%2D8.)
48    /// - Windows: [text2](https://docs.rs/tauri-winrt-notification/latest/tauri_winrt_notification/struct.Toast.html#method.text2)
49    pub fn title(mut self, title: &str) -> Self {
50        self.title = Some(title.to_owned());
51        self
52    }
53    /// Sets secondary description of Notification
54    ///
55    /// Plaform specific:
56    /// - MacOS [UNNotificationContent/subtitle](https://developer.apple.com/documentation/usernotifications/unnotificationcontent/subtitle)
57    /// - Linux / XDG: **not suported!**
58    /// - Windows [text1](https://docs.rs/tauri-winrt-notification/latest/tauri_winrt_notification/struct.Toast.html#method.text1)
59    pub fn subtitle(mut self, subtitle: &str) -> Self {
60        self.subtitle = Some(subtitle.to_owned());
61        self
62    }
63
64    /// Set Image Attachment
65    ///
66    /// Plaform specific:
67    /// - MacOS: passed by file path, must be gif, jpg, or png
68    /// - For linux the file is read and transfered over dbus (in case you are in a flatpak and it can't read from files) ["image-data"](https://specifications.freedesktop.org/notification-spec/latest/icons-and-images.html#icons-and-images-formats)
69    /// - Windows: passed by file path. [image](https://docs.rs/tauri-winrt-notification/latest/tauri_winrt_notification/struct.Toast.html#method.image)
70    pub fn set_image(mut self, path: PathBuf) -> Self {
71        self.image = Some(path);
72        self
73    }
74
75    /// Set App icon
76    ///
77    /// Plaform specific:
78    /// - MacOS: not supported to change the app icon?
79    /// - For linux the file is read and transfered over dbus (in case you are in a flatpak and it can't read from files) [app_icon](https://specifications.freedesktop.org/notification-spec/latest/icons-and-images.html#icons-and-images-formats)
80    /// - Windows: [`<image placement="appLogoOverride" />`](https://learn.microsoft.com/uwp/schemas/tiles/toastschema/element-image)
81    pub fn set_icon(mut self, path: PathBuf) -> Self {
82        self.icon = Some(path);
83        self
84    }
85
86    /// Set App icon to be round
87    ///
88    /// Plaform specific:
89    /// - MacOS: not supported
90    /// - Linux: not supported
91    /// - Windows: [`<image placement='appLogoOverride' hint-crop='circle' />`](https://learn.microsoft.com/uwp/schemas/tiles/toastschema/element-image)
92    pub fn set_icon_round_crop(mut self, icon_round_crop: bool) -> Self {
93        self.icon_round_crop = icon_round_crop;
94        self
95    }
96
97    /// Set Thread id, this is used to group related notifications
98    ///
99    /// Plaform specific:
100    /// - MacOS: [UNNotificationContent/threadIdentifier](https://developer.apple.com/documentation/usernotifications/unnotificationcontent/threadidentifier)
101    /// - Linux not specified yet:
102    /// - Windows: not supported
103    pub fn set_thread_id(mut self, thread_id: &str) -> Self {
104        self.thread_id = Some(thread_id.to_owned());
105        self
106    }
107
108    /// Set the notification Category, those are basically templates how the notification should be displayed
109    ///
110    /// It is used to add a text field or buttons to the notification.
111    ///
112    /// Categories are defined by passing them to [NotificationManager::register] on app startup
113    pub fn set_category_id(mut self, category_id: &str) -> Self {
114        self.category_id = Some(category_id.to_owned());
115        self
116    }
117
118    /// Set the xdg notification Category
119    ///
120    /// The type of notification this is acording to <https://specifications.freedesktop.org/notification-spec/latest/categories.html>
121    ///
122    /// Platform specific: only work on linux, this does nothing on other platforms
123    pub fn set_xdg_category(mut self, category: XdgNotificationCategory) -> Self {
124        self.xdg_category = Some(category);
125        self
126    }
127
128    /// Set the xdg App Name
129    ///
130    /// Platform specific: only work on linux, this does nothing on other platforms
131    pub fn set_xdg_app_name(mut self, name: String) -> Self {
132        self.xdg_app_name = Some(name);
133        self
134    }
135
136    /// Set metadata for a notification
137    ///
138    /// ## Platform Specific
139    /// - on MacOS this uses UserInfo field in the notification content, so it works accross sessions
140    /// - windows stores this in toast [NotificationData](https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.notificationdata?view=winrt-26100)
141    /// - linux: on linux we emulate this by storing this info inside of NotificationManager
142    pub fn set_user_info(mut self, user_info: HashMap<String, String>) -> Self {
143        self.user_info = Some(user_info);
144        self
145    }
146}
147
148/// A Handle to a sent notification
149pub trait NotificationHandle
150where
151    Self: Send + Sync + Debug,
152{
153    /// Close the notification
154    fn close(&self) -> Result<(), Error>;
155
156    /// Returns the id of the notification
157    fn get_id(&self) -> String;
158
159    /// Returns the data stored inside of the notification
160    fn get_user_info(&self) -> &HashMap<String, String>;
161}
162
163/// Manager for active notifications.
164///
165/// It is needed to display notifications and to manage active notifications.
166///
167/// ## Send a notification with a button
168/// ```rust
169/// let manager = get_notification_manager("com.example.my.app".to_string(), None);
170/// let categories = vec![
171///     NotificationCategory {
172///         identifier: "my.app.question".to_string(),
173///         actions: vec![
174///             NotificationCategoryAction::Action {
175///                 identifier: "my.app.question.yes".to_string(),
176///                 title: "Yes".to_string(),
177///             },
178///             NotificationCategoryAction::Action {
179///                 identifier: "my.app.question.no".to_string(),
180///                 title: "No".to_string(),
181///             },
182///         ],
183///     },
184/// ];
185/// manager.register(
186///     Box::new(|response| {
187///         log::info!("got notification response: {response:?}");
188///     }),
189///     categories,
190/// )?;
191/// let notification = user_notify::NotificationBuilder::new()
192///    .title("Question")
193///    .body("are you fine?")
194///    .set_category_id("my.app.question");
195/// let notification_handle = manager.send_notification(notification).await?;
196/// ```
197///
198/// Note that on macOS you need to ask for permission on first start of your app, before you can send notifications:
199/// ```rust
200/// if let Err(err) = manager
201///    .first_time_ask_for_notification_permission()
202///    .await
203/// {
204///    println!("failed to ask for notification permission: {err:?}");
205/// }
206/// ```
207#[async_trait]
208pub trait NotificationManager
209where
210    Self: Send + Sync + Debug,
211{
212    /// Returns whether the app is allowed to send notifications
213    ///
214    /// Needs to be called from **main thread**.
215    ///
216    /// ## Platform specific:
217    /// - MacOS: "Authorized", "Provisional" and "Ephemeral" return `true`.
218    /// "Denied", "NotDetermined" and unknown return `false`.
219    /// - Other: no-op on other platforms (always returns true)
220    async fn get_notification_permission_state(&self) -> Result<bool, crate::Error>;
221
222    /// Ask for notification permission.
223    ///
224    /// Needs to be called from **main thread**.
225    ///
226    /// ## Platform specific:
227    /// - MacOS: only asks the user on the first time this method is called.
228    /// - Other: no-op on other platforms (always returns true)
229    async fn first_time_ask_for_notification_permission(&self) -> Result<bool, Error>;
230
231    /// Registers and initializes the notification handler and categories.
232    /// Set a function to handle user responses (clicking notification, closing it, clicking an action on it)
233    ///
234    /// ## Platform specific:
235    /// - MacOS: sets the UNUserNotificationCenterDelegate
236    fn register(
237        &self,
238        handler_callback: Box<dyn Fn(crate::NotificationResponse) + Send + Sync + 'static>,
239        categories: Vec<NotificationCategory>,
240    ) -> Result<(), Error>;
241
242    /// Removes all of your app’s delivered notifications from Notification Center.
243    ///
244    /// ## Platform specific:
245    /// - MacOS: [UNUserNotificationCenter.removeAllDeliveredNotifications](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/removealldeliverednotifications())
246    /// - Linux: only works for notifications from current session, because notification handles are tracked in memory
247    fn remove_all_delivered_notifications(&self) -> Result<(), Error>;
248
249    /// Removes specific delivered notifications by their id from Notification Center.
250    ///
251    /// ## Platform specific:
252    /// - Linux: only works for notifications from current session, because notification handles are tracked in memory
253    fn remove_delivered_notifications(&self, ids: Vec<&str>) -> Result<(), Error>;
254
255    /// Get all deliverd notifications from UNUserNotificationCenter that are still active.
256    ///
257    /// ## Platform specific:
258    /// - MacOS:
259    ///   - also includes notifications from previous sessions
260    ///   - [UNUserNotificationCenter.getDeliveredNotificationsWithCompletionHandler](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/getdeliverednotifications(completionhandler:))
261    /// - Others: TODO: implemented/emulated by keeping track of all notifications in memory
262    async fn get_active_notifications(&self) -> Result<Vec<Box<dyn NotificationHandle>>, Error>;
263
264    /// Shows notification and returns Notification handle
265    async fn send_notification(
266        &self,
267        builder: NotificationBuilder,
268    ) -> Result<Box<dyn NotificationHandle>, Error>;
269}
270
271/// Emmited when user clicked on a notification
272///
273/// ## Platform-specific
274///
275/// - **macOS**: <https://developer.apple.com/documentation/usernotifications/unusernotificationcenterdelegate/usernotificationcenter(_:didreceive:withcompletionhandler:)?language=objc>
276/// - **Other**: Unsupported.
277#[non_exhaustive]
278#[derive(Debug, Clone, PartialEq)]
279pub struct NotificationResponse {
280    /// id of the notification that was assigned by the system
281    pub notification_id: String,
282    /// The action the user took to trigger the response
283    pub action: NotificationResponseAction,
284    /// The text that the user typed in as reponse
285    ///
286    /// ## Platform Specific
287    /// - MacOS: corresponds to [UNTextInputNotificationResponse.userText](https://developer.apple.com/documentation/usernotifications/untextinputnotificationresponse/usertext?language=objc)
288    /// - Linux: not supported
289    pub user_text: Option<String>,
290    /// Data stored inside of the notification
291    pub user_info: HashMap<String, String>,
292}
293
294/// An action the user took to trigger the [NotificationResponse]
295#[derive(Debug, Clone, PartialEq)]
296pub enum NotificationResponseAction {
297    /// When user clicks on the notification
298    ///
299    /// ## Platform Specific
300    /// - MacOS: corresponds to [UNNotificationDefaultActionIdentifier](https://developer.apple.com/documentation/usernotifications/unnotificationdefaultactionidentifier?language=objc)
301    Default,
302    /// When user closes the notification
303    ///
304    /// ## Platform Specific
305    /// - MacOS: corresponds to [UNNotificationDismissActionIdentifier](https://developer.apple.com/documentation/usernotifications/unnotificationdismissactionidentifier?language=objc)
306    Dismiss,
307    /// The identifier string of the action that the user selected, if it is not one of the other actions in [NotificationResponseAction]
308    Other(String),
309}
310
311/// Notification Categories are used to define actions
312/// for notifications that have this category set.
313///
314/// Think of it like a template for notications.
315/// To store data for a notification,
316/// use [NotificationBuilder::set_user_info]
317/// and retrieve it via [NotificationHandle::get_user_info]
318/// or [NotificationResponse::user_info].
319#[derive(Debug, Clone)]
320pub struct NotificationCategory {
321    /// Id of the category by which it is referenced on notifications [NotificationBuilder::set_category_id]
322    pub identifier: String,
323    /// The actions to display when the system delivers notifications of this type.
324    pub actions: Vec<NotificationCategoryAction>,
325}
326
327/// An action to display in a notifications.
328#[derive(Debug, Clone)]
329pub enum NotificationCategoryAction {
330    /// Action button in a notification
331    /// ## Platform specific
332    /// - macOS: <https://developer.apple.com/documentation/usernotifications/unnotificationaction?language=objc>
333    /// - Linux: not implemented yet (<https://github.com/Simon-Laux/user-notify/issues/1>)
334    /// - Windows: not implemented yet (<https://github.com/Simon-Laux/user-notify/issues/2>)
335    Action {
336        /// id of the action
337        identifier: String,
338        /// Label of the button
339        title: String,
340        /* IDEA: also support icon https://developer.apple.com/documentation/usernotifications/unnotificationaction/init(identifier:title:options:icon:)?language=objc */
341    },
342    /// Text input field in a notification.
343    ///
344    /// Example Usage: Can be used to reply to notifications of a messenger.
345    ///
346    /// ## Platform specific
347    /// - macOS: <https://developer.apple.com/documentation/usernotifications/untextinputnotificationaction>
348    /// - Linux: not supported
349    /// - Windows: not implemented yet (<https://github.com/Simon-Laux/user-notify/issues/2>)
350    TextInputAction {
351        /// id of the action
352        identifier: String,
353        /// Label of the input field
354        title: String,
355        /* IDEA: also support icon and option https://developer.apple.com/documentation/usernotifications/untextinputnotificationaction/init(identifier:title:options:textinputbuttontitle:textinputplaceholder:)?language=objc */
356        /// Label of the input button
357        input_button_title: String,
358        /// Placeholder for the input field
359        input_placeholder: String,
360    },
361}