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}
55
56impl fmt::Debug for NotificationHandle {
57 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
58 formatter
59 .debug_struct("NotificationHandle")
60 .field("notification_id", &self.notification_id)
61 .field("timeout", &self.timeout)
62 .finish()
63 }
64}
65
66impl NotificationHandle {
67 pub fn notification_id(&self) -> &str {
72 &self.notification_id
73 }
74
75 pub async fn response(self) -> Result<NotificationResponse, Error> {
86 let notification_id = self.notification_id.clone();
87 let receiver = self.guard.into_receiver();
88
89 if let Some(duration) = self.timeout {
90 future::or(
91 async { receiver.await.map_err(|_| Error::NotificationRejected) },
92 async move {
93 futures_timer::Delay::new(duration).await;
94 close_delivered(¬ification_id).await;
95 Ok(NotificationResponse::timed_out(notification_id))
96 },
97 )
98 .await
99 } else {
100 receiver.await.map_err(|_| Error::NotificationRejected)
101 }
102 }
103}
104
105fn send_inner(
107 notification: Notification,
108 response_tx: Option<oneshot::Sender<NotificationResponse>>,
109 request_id: String,
110) -> impl Future<Output = Result<(), Error>> + Send + 'static {
111 let (scheduled_tx, scheduled_rx) = worker::sender::<Result<(), Error>>();
112
113 worker::dispatch(move || {
114 let NotificationContent {
115 content,
116 actions,
117 trigger,
118 } = notification.build_content();
119 log::debug!("request_id={request_id:?}");
120
121 if response_tx.is_some() || !actions.is_empty() {
125 let category_id = if actions.is_empty() {
126 "de.hoodie.mac-usernotifications.observe".to_owned()
127 } else {
128 let mut ids: Vec<&str> = actions.iter().map(|act| act.identifier()).collect();
129 ids.sort_unstable();
130 ids.join(",")
131 };
132 log::debug!("registering synthesised category {category_id:?}");
133 ActionCategory::from_actions(&category_id, actions).register_now();
134 content.setCategoryIdentifier(&NSString::from_str(&category_id));
135 }
136
137 if let Some(tx) = response_tx {
138 delegate::register_response_sender(request_id.clone(), tx);
139 }
140
141 use objc2_user_notifications::UNTimeIntervalNotificationTrigger;
142 let trigger_obj = trigger.map(|delay| {
143 UNTimeIntervalNotificationTrigger::triggerWithTimeInterval_repeats(
144 delay.as_secs_f64().max(0.1),
145 false,
146 )
147 });
148
149 let request = UNNotificationRequest::requestWithIdentifier_content_trigger(
150 &NSString::from_str(&request_id),
151 &content,
152 trigger_obj.as_deref().map(|val| &**val),
153 );
154
155 UNUserNotificationCenter::currentNotificationCenter()
156 .addNotificationRequest_withCompletionHandler(
157 &request,
158 Some(&RcBlock::new(move |err: *mut NSError| {
159 log::debug!("completion handler fired (err.is_null={})", err.is_null());
160 if let Some(err) = NonNull::new(err).map(|ptr| unsafe { ptr.as_ref() }) {
161 let desc = err.localizedDescription();
162 log::error!("notification request rejected: {desc}");
163 }
164 let result = if err.is_null() {
165 Ok(())
166 } else {
167 Err(Error::NotificationRejected)
168 };
169 scheduled_tx.send(result);
170 })),
171 );
172 });
173
174 async move { scheduled_rx.await.map_err(Into::into).flatten() }
175}
176
177#[cfg(feature = "blocking-wrappers")]
182pub fn close_delivered_blocking(notification_id: &str) {
183 let id = notification_id.to_owned();
184 worker::dispatch(move || {
185 let ids = NSArray::from_retained_slice(&[NSString::from_str(&id)]);
186 UNUserNotificationCenter::currentNotificationCenter()
187 .removeDeliveredNotificationsWithIdentifiers(&ids);
188 log::debug!("removed delivered notification {id:?}");
189 });
190}
191
192#[cfg(feature = "blocking-wrappers")]
197pub fn cancel_pending_blocking(notification_id: &str) {
198 let id = notification_id.to_owned();
199 worker::dispatch(move || {
200 let ids = NSArray::from_retained_slice(&[NSString::from_str(&id)]);
201 UNUserNotificationCenter::currentNotificationCenter()
202 .removePendingNotificationRequestsWithIdentifiers(&ids);
203 log::debug!("cancelled pending notification {id:?}");
204 });
205}
206
207pub fn close_delivered(notification_id: &str) -> impl Future<Output = ()> + Send + 'static {
214 let id = notification_id.to_owned();
215 let (tx, rx) = worker::sender::<()>();
216 worker::dispatch(move || {
217 let ids = NSArray::from_retained_slice(&[NSString::from_str(&id)]);
218 UNUserNotificationCenter::currentNotificationCenter()
219 .removeDeliveredNotificationsWithIdentifiers(&ids);
220 log::debug!("removed delivered notification {id:?}");
221 tx.send(());
222 });
223
224 async move { rx.await.unwrap_or(()) }
225}
226
227pub fn cancel_pending(notification_id: &str) -> impl Future<Output = ()> + Send + 'static {
233 let id = notification_id.to_owned();
234 let (tx, rx) = worker::sender::<()>();
235 worker::dispatch(move || {
236 let ids = NSArray::from_retained_slice(&[NSString::from_str(&id)]);
237 UNUserNotificationCenter::currentNotificationCenter()
238 .removePendingNotificationRequestsWithIdentifiers(&ids);
239 log::debug!("cancelled pending notification {id:?}");
240 tx.send(());
241 });
242 async move { rx.await.unwrap_or(()) }
243}
244
245pub fn get_pending_notification_ids() -> impl Future<Output = Vec<String>> + Send + 'static {
247 let (tx, rx) = worker::sender::<Vec<String>>();
248 worker::dispatch(move || {
249 UNUserNotificationCenter::currentNotificationCenter()
250 .getPendingNotificationRequestsWithCompletionHandler(&RcBlock::new(
251 move |requests: NonNull<NSArray<UNNotificationRequest>>| {
252 let ids: Vec<String> = unsafe { requests.as_ref() }
253 .iter()
254 .map(|req| req.identifier().to_string())
255 .collect();
256 tx.send(ids);
257 },
258 ));
259 });
260 async move { rx.await.unwrap_or_default() }
261}
262
263pub fn get_delivered_notification_ids() -> impl Future<Output = Vec<String>> + Send + 'static {
265 let (tx, rx) = worker::sender::<Vec<String>>();
266 worker::dispatch(move || {
267 UNUserNotificationCenter::currentNotificationCenter()
268 .getDeliveredNotificationsWithCompletionHandler(&RcBlock::new(
269 move |notifications: NonNull<NSArray<UNNotification>>| {
270 let ids: Vec<String> = unsafe { notifications.as_ref() }
271 .iter()
272 .map(|notif| notif.request().identifier().to_string())
273 .collect();
274 tx.send(ids);
275 },
276 ));
277 });
278 async move { rx.await.unwrap_or_default() }
279}
280
281pub async fn send(notification: Notification) -> Result<String, Error> {
289 check_bundle()?;
290 let request_id = notification
291 .notification_id
292 .clone()
293 .unwrap_or_else(|| NSUUID::new().UUIDString().to_string());
294 let id_copy = request_id.clone();
295 send_inner(notification, None, request_id)
296 .await
297 .map(|()| id_copy)
298}
299
300#[cfg(feature = "blocking-wrappers")]
306pub fn send_blocking(notification: Notification) -> Result<String, Error> {
307 check_bundle()?;
308 let request_id = notification
309 .notification_id
310 .clone()
311 .unwrap_or_else(|| NSUUID::new().UUIDString().to_string());
312 let id_copy = request_id.clone();
313 future::block_on(send_inner(notification, None, request_id)).map(|()| id_copy)
314}
315
316pub async fn send_and_wait_for_delivery(
322 notification: Notification,
323) -> Result<NotificationHandle, Error> {
324 check_bundle()?;
325 let request_id = notification
326 .notification_id
327 .clone()
328 .unwrap_or_else(|| NSUUID::new().UUIDString().to_string());
329
330 let (response_tx, response_rx) = oneshot::channel();
331 let timeout = notification.action_timeout;
332 let guard = PendingGuard::new(request_id.clone(), response_rx);
333
334 send_inner(notification, Some(response_tx), request_id.clone()).await?;
335
336 Ok(NotificationHandle {
337 notification_id: request_id,
338 guard,
339 timeout,
340 })
341}
342
343pub async fn send_with_actions(notification: Notification) -> Result<NotificationResponse, Error> {
357 check_bundle()?;
358
359 let request_id = notification
360 .notification_id
361 .clone()
362 .unwrap_or_else(|| NSUUID::new().UUIDString().to_string());
363 let (response_tx, response_rx) = oneshot::channel();
364 let timeout = notification.action_timeout;
365 let guard = PendingGuard::new(request_id.clone(), response_rx);
366
367 send_inner(notification, Some(response_tx), request_id.clone()).await?;
368
369 NotificationHandle {
370 notification_id: request_id,
371 guard,
372 timeout,
373 }
374 .response()
375 .await
376}