Skip to main content

mac_usernotifications/
response.rs

1//! The response a user gave to a notification.
2//!
3//! Delivered by the delegate after the user interacts with a notification.
4//! Callers receive this via the `Future` returned from `send_with_actions`.
5
6use std::sync::OnceLock;
7
8use objc2_user_notifications::{
9    UNNotificationDefaultActionIdentifier, UNNotificationDismissActionIdentifier,
10};
11
12/// Why a notification was closed without an explicit user action.
13///
14/// Carried by [`NotificationResponse::close_reason`]. A `None` value means the
15/// user took an active action (clicked the body, pressed a button, or submitted
16/// a reply).
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum CloseReason {
19    /// The notification timed out and was removed by this crate.
20    ///
21    /// Produced when the duration set via
22    /// [`Notification::timeout`](crate::Notification::timeout) elapses.
23    Expired,
24
25    /// The user explicitly dismissed the notification (e.g. swiped it away
26    /// or pressed "Clear" in Notification Center).
27    Dismissed,
28}
29
30/// What the user did with a notification.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct NotificationResponse {
33    /// The unique identifier of the notification this response belongs to.
34    ///
35    /// This is the same identifier that can be passed to [`close_delivered`] or
36    /// [`cancel_pending`]. Useful when the caller did not set an explicit ID via
37    /// [`Notification::id`] and therefore did not know the auto-generated UUID.
38    ///
39    /// [`close_delivered`]: crate::close_delivered
40    /// [`cancel_pending`]: crate::cancel_pending
41    /// [`Notification::id`]: crate::Notification::id
42    pub notification_id: String,
43
44    /// The action identifier the user chose.
45    ///
46    /// Use [`is_default_action`] and [`is_dismiss_action`] for built-in cases,
47    /// or compare directly with custom action identifiers.
48    ///
49    /// [`is_default_action`]: NotificationResponse::is_default_action
50    /// [`is_dismiss_action`]: NotificationResponse::is_dismiss_action
51    pub action_identifier: String,
52
53    /// Text entered in a reply action, `None` for button actions, default-action, and dismiss.
54    ///
55    /// Use [`is_reply`] to check, or call `.reply_text.as_deref()` to borrow as `&str`.
56    ///
57    /// [`is_reply`]: NotificationResponse::is_reply
58    pub reply_text: Option<String>,
59
60    /// Why the notification was closed passively, or `None` if the user took
61    /// an explicit action.
62    ///
63    /// Check [`CloseReason::Expired`] to detect a timeout auto-close, or
64    /// [`CloseReason::Dismissed`] for a user-initiated swipe/clear.
65    pub close_reason: Option<CloseReason>,
66}
67
68/// Returns the dismiss action identifier string, cached for the process lifetime.
69fn dismiss_action_id() -> &'static str {
70    static CACHED: OnceLock<String> = OnceLock::new();
71    CACHED.get_or_init(|| {
72        // SAFETY: `UNNotificationDismissActionIdentifier` is a valid, non-null
73        // `NSString` backed by a well-known Apple framework constant that lives
74        // for the lifetime of the process.
75        unsafe { UNNotificationDismissActionIdentifier.to_string() }
76    })
77}
78
79/// Returns the default action identifier string, cached for the process lifetime.
80fn default_action_id() -> &'static str {
81    static CACHED: OnceLock<String> = OnceLock::new();
82    CACHED.get_or_init(|| {
83        // SAFETY: same rationale as `dismiss_action_id`.
84        unsafe { UNNotificationDefaultActionIdentifier.to_string() }
85    })
86}
87
88impl NotificationResponse {
89    /// Build a response from raw macOS delegate data.
90    pub(crate) fn from_objc(
91        notification_id: String,
92        action_id: String,
93        reply_text: Option<String>,
94    ) -> Self {
95        let close_reason = if action_id == dismiss_action_id() {
96            Some(CloseReason::Dismissed)
97        } else {
98            None
99        };
100        Self {
101            notification_id,
102            action_identifier: action_id,
103            reply_text,
104            close_reason,
105        }
106    }
107
108    /// Construct a synthetic timed-out response for a notification with the given ID.
109    ///
110    /// Used internally when the caller's timeout elapses and the notification
111    /// is removed via [`close_delivered`](crate::close_delivered).
112    pub(crate) fn timed_out(notification_id: String) -> Self {
113        Self {
114            notification_id,
115            action_identifier: String::new(),
116            reply_text: None,
117            close_reason: Some(CloseReason::Expired),
118        }
119    }
120
121    /// Construct a synthetic dismissed response, used by the poll-based dismiss
122    /// fallback for buttonless notifications.
123    pub(crate) fn dismissed(notification_id: String) -> Self {
124        Self {
125            notification_id,
126            action_identifier: String::new(),
127            reply_text: None,
128            close_reason: Some(CloseReason::Dismissed),
129        }
130    }
131
132    /// Returns `true` if the notification was automatically closed because the
133    /// timeout set via [`Notification::timeout`](crate::Notification::timeout) elapsed.
134    ///
135    /// When this returns `true` the notification banner has already been removed
136    /// from the screen by the time the future resolves.
137    pub fn is_timed_out(&self) -> bool {
138        self.close_reason == Some(CloseReason::Expired)
139    }
140
141    /// Returns `true` if the user clicked the notification body (default action).
142    ///
143    /// Corresponds to [`UNNotificationDefaultActionIdentifier`](https://developer.apple.com/documentation/usernotifications/unnotificationdefaultactionidentifier).
144    pub fn is_default_action(&self) -> bool {
145        self.action_identifier == default_action_id()
146    }
147
148    /// Returns `true` if the user dismissed the notification.
149    ///
150    /// Corresponds to [`UNNotificationDismissActionIdentifier`](https://developer.apple.com/documentation/usernotifications/unnotificationdismissactionidentifier).
151    pub fn is_dismiss_action(&self) -> bool {
152        self.close_reason == Some(CloseReason::Dismissed)
153    }
154
155    /// Returns `true` if the user submitted text via a reply action.
156    pub fn is_reply(&self) -> bool {
157        self.reply_text.is_some()
158    }
159}