mac_usernotifications/
send.rs1use crate::{
2 Error,
3 action::ActionCategory,
4 check_bundle, delegate,
5 notification::{Notification, NotificationContent},
6 response::NotificationResponse,
7 worker,
8};
9use block2::RcBlock;
10use futures_channel::oneshot;
11use futures_lite::future;
12use objc2_foundation::{NSArray, NSError, NSString, NSUUID};
13use objc2_user_notifications::{UNNotification, UNNotificationRequest, UNUserNotificationCenter};
14use std::{fmt, future::Future, ptr::NonNull, time::Duration};
15
16struct PendingGuard {
18 request_id: String,
19 rx: oneshot::Receiver<NotificationResponse>,
20}
21
22impl PendingGuard {
23 fn new(request_id: String, rx: oneshot::Receiver<NotificationResponse>) -> Self {
24 Self { request_id, rx }
25 }
26
27 fn into_receiver(self) -> oneshot::Receiver<NotificationResponse> {
29 let manual = std::mem::ManuallyDrop::new(self);
30 unsafe { std::ptr::read(&manual.rx) }
33 }
34}
35
36impl Drop for PendingGuard {
37 fn drop(&mut self) {
38 delegate::deregister_response_sender(&self.request_id);
41 }
42}
43
44pub struct NotificationHandle {
51 notification_id: String,
52 guard: PendingGuard,
53 timeout: Option<Duration>,
54 has_actions: bool,
59}
60
61impl fmt::Debug for NotificationHandle {
62 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63 formatter
64 .debug_struct("NotificationHandle")
65 .field("notification_id", &self.notification_id)
66 .field("timeout", &self.timeout)
67 .field("has_actions", &self.has_actions)
68 .finish()
69 }
70}
71
72impl NotificationHandle {
73 pub fn notification_id(&self) -> &str {
78 &self.notification_id
79 }
80
81 pub async fn response(self) -> Result<NotificationResponse, Error> {
92 let notification_id = self.notification_id.clone();
93 let receiver = self.guard.into_receiver();
94 let delegate_future = async { receiver.await.map_err(|_| Error::NotificationRejected) };
95
96 if let Some(duration) = self.timeout {
97 future::or(delegate_future, async move {
98 futures_timer::Delay::new(duration).await;
99 close_delivered(¬ification_id).await;
100 Ok(NotificationResponse::timed_out(notification_id))
101 })
102 .await
103 } else if !self.has_actions {
104 future::or(delegate_future, poll_until_dismissed(notification_id)).await
108 } else {
109 delegate_future.await
110 }
111 }
112}
113
114const DISMISS_POLL_INTERVAL: Duration = Duration::from_millis(500);
115
116async fn poll_until_dismissed(notification_id: String) -> Result<NotificationResponse, Error> {
122 loop {
123 futures_timer::Delay::new(DISMISS_POLL_INTERVAL).await;
124 let delivered = get_delivered_notification_ids().await;
125 if !delivered.contains(¬ification_id) {
126 return Ok(NotificationResponse::dismissed(notification_id));
127 }
128 }
129}
130
131fn send_inner(
133 notification: Notification,
134 response_tx: Option<oneshot::Sender<NotificationResponse>>,
135 request_id: String,
136) -> impl Future<Output = Result<(), Error>> + Send + 'static {
137 let (scheduled_tx, scheduled_rx) = worker::sender::<Result<(), Error>>();
138
139 worker::dispatch(move || {
140 let NotificationContent {
141 content,
142 actions,
143 trigger,
144 } = notification.build_content();
145 log::debug!("request_id={request_id:?}");
146
147 if !actions.is_empty() {
152 let mut ids: Vec<&str> = actions.iter().map(|act| act.identifier()).collect();
153 ids.sort_unstable();
154 let category_id = ids.join(",");
155 log::debug!("registering synthesised category {category_id:?}");
156 ActionCategory::from_actions(&category_id, actions).register_now();
157 content.setCategoryIdentifier(&NSString::from_str(&category_id));
158 }
159
160 if let Some(tx) = response_tx {
161 delegate::register_response_sender(request_id.clone(), tx);
162 }
163
164 use objc2_user_notifications::UNTimeIntervalNotificationTrigger;
165 let trigger_obj = trigger.map(|delay| {
166 UNTimeIntervalNotificationTrigger::triggerWithTimeInterval_repeats(
167 delay.as_secs_f64().max(0.1),
168 false,
169 )
170 });
171
172 let request = UNNotificationRequest::requestWithIdentifier_content_trigger(
173 &NSString::from_str(&request_id),
174 &content,
175 trigger_obj.as_deref().map(|val| &**val),
176 );
177
178 UNUserNotificationCenter::currentNotificationCenter()
179 .addNotificationRequest_withCompletionHandler(
180 &request,
181 Some(&RcBlock::new(move |err: *mut NSError| {
182 log::trace!("send completed (err.is_null={})", err.is_null());
183 if let Some(err) = NonNull::new(err).map(|ptr| unsafe { ptr.as_ref() }) {
184 let desc = err.localizedDescription();
185 log::error!("notification request rejected: {desc}");
186 }
187 let result = if err.is_null() {
188 Ok(())
189 } else {
190 Err(Error::NotificationRejected)
191 };
192 scheduled_tx.send(result);
193 })),
194 );
195 });
196
197 async move { scheduled_rx.await.map_err(Into::into).flatten() }
198}
199
200#[cfg(feature = "blocking-wrappers")]
205pub fn close_delivered_blocking(notification_id: &str) {
206 let id = notification_id.to_owned();
207 worker::dispatch(move || {
208 let ids = NSArray::from_retained_slice(&[NSString::from_str(&id)]);
209 UNUserNotificationCenter::currentNotificationCenter()
210 .removeDeliveredNotificationsWithIdentifiers(&ids);
211 log::debug!("removed delivered notification {id:?}");
212 });
213}
214
215#[cfg(feature = "blocking-wrappers")]
220pub fn cancel_pending_blocking(notification_id: &str) {
221 let id = notification_id.to_owned();
222 worker::dispatch(move || {
223 let ids = NSArray::from_retained_slice(&[NSString::from_str(&id)]);
224 UNUserNotificationCenter::currentNotificationCenter()
225 .removePendingNotificationRequestsWithIdentifiers(&ids);
226 log::debug!("cancelled pending notification {id:?}");
227 });
228}
229
230pub fn close_delivered(notification_id: &str) -> impl Future<Output = ()> + Send + 'static {
237 let id = notification_id.to_owned();
238 let (tx, rx) = worker::sender::<()>();
239 worker::dispatch(move || {
240 let ids = NSArray::from_retained_slice(&[NSString::from_str(&id)]);
241 UNUserNotificationCenter::currentNotificationCenter()
242 .removeDeliveredNotificationsWithIdentifiers(&ids);
243 log::debug!("removed delivered notification {id:?}");
244 tx.send(());
245 });
246
247 async move { rx.await.unwrap_or(()) }
248}
249
250pub fn cancel_pending(notification_id: &str) -> impl Future<Output = ()> + Send + 'static {
256 let id = notification_id.to_owned();
257 let (tx, rx) = worker::sender::<()>();
258 worker::dispatch(move || {
259 let ids = NSArray::from_retained_slice(&[NSString::from_str(&id)]);
260 UNUserNotificationCenter::currentNotificationCenter()
261 .removePendingNotificationRequestsWithIdentifiers(&ids);
262 log::debug!("cancelled pending notification {id:?}");
263 tx.send(());
264 });
265 async move { rx.await.unwrap_or(()) }
266}
267
268pub fn get_pending_notification_ids() -> impl Future<Output = Vec<String>> + Send + 'static {
270 let (tx, rx) = worker::sender::<Vec<String>>();
271 worker::dispatch(move || {
272 UNUserNotificationCenter::currentNotificationCenter()
273 .getPendingNotificationRequestsWithCompletionHandler(&RcBlock::new(
274 move |requests: NonNull<NSArray<UNNotificationRequest>>| {
275 let ids: Vec<String> = unsafe { requests.as_ref() }
276 .iter()
277 .map(|req| req.identifier().to_string())
278 .collect();
279 tx.send(ids);
280 },
281 ));
282 });
283 async move { rx.await.unwrap_or_default() }
284}
285
286pub fn get_delivered_notification_ids() -> impl Future<Output = Vec<String>> + Send + 'static {
288 let (tx, rx) = worker::sender::<Vec<String>>();
289 worker::dispatch(move || {
290 UNUserNotificationCenter::currentNotificationCenter()
291 .getDeliveredNotificationsWithCompletionHandler(&RcBlock::new(
292 move |notifications: NonNull<NSArray<UNNotification>>| {
293 let ids: Vec<String> = unsafe { notifications.as_ref() }
294 .iter()
295 .map(|notif| notif.request().identifier().to_string())
296 .collect();
297 tx.send(ids);
298 },
299 ));
300 });
301 async move { rx.await.unwrap_or_default() }
302}
303
304pub async fn send(notification: Notification) -> Result<String, Error> {
312 check_bundle()?;
313 let request_id = notification
314 .notification_id
315 .clone()
316 .unwrap_or_else(|| NSUUID::new().UUIDString().to_string());
317 let id_copy = request_id.clone();
318 send_inner(notification, None, request_id)
319 .await
320 .map(|()| id_copy)
321}
322
323#[cfg(feature = "blocking-wrappers")]
329pub fn send_blocking(notification: Notification) -> Result<String, Error> {
330 check_bundle()?;
331 let request_id = notification
332 .notification_id
333 .clone()
334 .unwrap_or_else(|| NSUUID::new().UUIDString().to_string());
335 let id_copy = request_id.clone();
336 future::block_on(send_inner(notification, None, request_id)).map(|()| id_copy)
337}
338
339pub async fn send_and_wait_for_delivery(
345 notification: Notification,
346) -> Result<NotificationHandle, Error> {
347 check_bundle()?;
348 let request_id = notification
349 .notification_id
350 .clone()
351 .unwrap_or_else(|| NSUUID::new().UUIDString().to_string());
352
353 let (response_tx, response_rx) = oneshot::channel();
354 let timeout = notification.action_timeout;
355 let has_actions = !notification.actions.is_empty();
356 let guard = PendingGuard::new(request_id.clone(), response_rx);
357
358 send_inner(notification, Some(response_tx), request_id.clone()).await?;
359
360 Ok(NotificationHandle {
361 notification_id: request_id,
362 guard,
363 timeout,
364 has_actions,
365 })
366}
367
368pub async fn send_with_actions(notification: Notification) -> Result<NotificationResponse, Error> {
382 check_bundle()?;
383
384 let request_id = notification
385 .notification_id
386 .clone()
387 .unwrap_or_else(|| NSUUID::new().UUIDString().to_string());
388 let (response_tx, response_rx) = oneshot::channel();
389 let timeout = notification.action_timeout;
390 let has_actions = !notification.actions.is_empty();
391 let guard = PendingGuard::new(request_id.clone(), response_rx);
392
393 send_inner(notification, Some(response_tx), request_id.clone()).await?;
394
395 NotificationHandle {
396 notification_id: request_id,
397 guard,
398 timeout,
399 has_actions,
400 }
401 .response()
402 .await
403}