use crate::{
Error,
action::ActionCategory,
check_bundle, delegate,
notification::{Notification, NotificationContent},
response::NotificationResponse,
worker,
};
use block2::RcBlock;
use futures_channel::oneshot;
use futures_lite::future;
use objc2_foundation::{NSArray, NSError, NSString, NSUUID};
use objc2_user_notifications::{UNNotification, UNNotificationRequest, UNUserNotificationCenter};
use std::{fmt, future::Future, ptr::NonNull, time::Duration};
struct PendingGuard {
request_id: String,
rx: oneshot::Receiver<NotificationResponse>,
}
impl PendingGuard {
fn new(request_id: String, rx: oneshot::Receiver<NotificationResponse>) -> Self {
Self { request_id, rx }
}
fn into_receiver(self) -> oneshot::Receiver<NotificationResponse> {
let manual = std::mem::ManuallyDrop::new(self);
unsafe { std::ptr::read(&manual.rx) }
}
}
impl Drop for PendingGuard {
fn drop(&mut self) {
delegate::deregister_response_sender(&self.request_id);
}
}
pub struct NotificationHandle {
notification_id: String,
guard: PendingGuard,
timeout: Option<Duration>,
has_actions: bool,
}
impl fmt::Debug for NotificationHandle {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("NotificationHandle")
.field("notification_id", &self.notification_id)
.field("timeout", &self.timeout)
.field("has_actions", &self.has_actions)
.finish()
}
}
impl NotificationHandle {
pub fn notification_id(&self) -> &str {
&self.notification_id
}
pub async fn response(self) -> Result<NotificationResponse, Error> {
let notification_id = self.notification_id.clone();
let receiver = self.guard.into_receiver();
let delegate_future = async { receiver.await.map_err(|_| Error::NotificationRejected) };
if let Some(duration) = self.timeout {
future::or(delegate_future, async move {
futures_timer::Delay::new(duration).await;
close_delivered(¬ification_id).await;
Ok(NotificationResponse::timed_out(notification_id))
})
.await
} else if !self.has_actions {
future::or(delegate_future, poll_until_dismissed(notification_id)).await
} else {
delegate_future.await
}
}
}
const DISMISS_POLL_INTERVAL: Duration = Duration::from_millis(500);
async fn poll_until_dismissed(notification_id: String) -> Result<NotificationResponse, Error> {
loop {
futures_timer::Delay::new(DISMISS_POLL_INTERVAL).await;
let delivered = get_delivered_notification_ids().await;
if !delivered.contains(¬ification_id) {
return Ok(NotificationResponse::dismissed(notification_id));
}
}
}
fn send_inner(
notification: Notification,
response_tx: Option<oneshot::Sender<NotificationResponse>>,
request_id: String,
) -> impl Future<Output = Result<(), Error>> + Send + 'static {
let (scheduled_tx, scheduled_rx) = worker::sender::<Result<(), Error>>();
worker::dispatch(move || {
let NotificationContent {
content,
actions,
trigger,
} = notification.build_content();
log::debug!("request_id={request_id:?}");
if !actions.is_empty() {
let mut ids: Vec<&str> = actions.iter().map(|act| act.identifier()).collect();
ids.sort_unstable();
let category_id = ids.join(",");
log::debug!("registering synthesised category {category_id:?}");
ActionCategory::from_actions(&category_id, actions).register_now();
content.setCategoryIdentifier(&NSString::from_str(&category_id));
}
if let Some(tx) = response_tx {
delegate::register_response_sender(request_id.clone(), tx);
}
use objc2_user_notifications::UNTimeIntervalNotificationTrigger;
let trigger_obj = trigger.map(|delay| {
UNTimeIntervalNotificationTrigger::triggerWithTimeInterval_repeats(
delay.as_secs_f64().max(0.1),
false,
)
});
let request = UNNotificationRequest::requestWithIdentifier_content_trigger(
&NSString::from_str(&request_id),
&content,
trigger_obj.as_deref().map(|val| &**val),
);
UNUserNotificationCenter::currentNotificationCenter()
.addNotificationRequest_withCompletionHandler(
&request,
Some(&RcBlock::new(move |err: *mut NSError| {
log::trace!("send completed (err.is_null={})", err.is_null());
if let Some(err) = NonNull::new(err).map(|ptr| unsafe { ptr.as_ref() }) {
let desc = err.localizedDescription();
log::error!("notification request rejected: {desc}");
}
let result = if err.is_null() {
Ok(())
} else {
Err(Error::NotificationRejected)
};
scheduled_tx.send(result);
})),
);
});
async move { scheduled_rx.await.map_err(Into::into).flatten() }
}
#[cfg(feature = "blocking-wrappers")]
pub fn close_delivered_blocking(notification_id: &str) {
let id = notification_id.to_owned();
worker::dispatch(move || {
let ids = NSArray::from_retained_slice(&[NSString::from_str(&id)]);
UNUserNotificationCenter::currentNotificationCenter()
.removeDeliveredNotificationsWithIdentifiers(&ids);
log::debug!("removed delivered notification {id:?}");
});
}
#[cfg(feature = "blocking-wrappers")]
pub fn cancel_pending_blocking(notification_id: &str) {
let id = notification_id.to_owned();
worker::dispatch(move || {
let ids = NSArray::from_retained_slice(&[NSString::from_str(&id)]);
UNUserNotificationCenter::currentNotificationCenter()
.removePendingNotificationRequestsWithIdentifiers(&ids);
log::debug!("cancelled pending notification {id:?}");
});
}
pub fn close_delivered(notification_id: &str) -> impl Future<Output = ()> + Send + 'static {
let id = notification_id.to_owned();
let (tx, rx) = worker::sender::<()>();
worker::dispatch(move || {
let ids = NSArray::from_retained_slice(&[NSString::from_str(&id)]);
UNUserNotificationCenter::currentNotificationCenter()
.removeDeliveredNotificationsWithIdentifiers(&ids);
log::debug!("removed delivered notification {id:?}");
tx.send(());
});
async move { rx.await.unwrap_or(()) }
}
pub fn cancel_pending(notification_id: &str) -> impl Future<Output = ()> + Send + 'static {
let id = notification_id.to_owned();
let (tx, rx) = worker::sender::<()>();
worker::dispatch(move || {
let ids = NSArray::from_retained_slice(&[NSString::from_str(&id)]);
UNUserNotificationCenter::currentNotificationCenter()
.removePendingNotificationRequestsWithIdentifiers(&ids);
log::debug!("cancelled pending notification {id:?}");
tx.send(());
});
async move { rx.await.unwrap_or(()) }
}
pub fn get_pending_notification_ids() -> impl Future<Output = Vec<String>> + Send + 'static {
let (tx, rx) = worker::sender::<Vec<String>>();
worker::dispatch(move || {
UNUserNotificationCenter::currentNotificationCenter()
.getPendingNotificationRequestsWithCompletionHandler(&RcBlock::new(
move |requests: NonNull<NSArray<UNNotificationRequest>>| {
let ids: Vec<String> = unsafe { requests.as_ref() }
.iter()
.map(|req| req.identifier().to_string())
.collect();
tx.send(ids);
},
));
});
async move { rx.await.unwrap_or_default() }
}
pub fn get_delivered_notification_ids() -> impl Future<Output = Vec<String>> + Send + 'static {
let (tx, rx) = worker::sender::<Vec<String>>();
worker::dispatch(move || {
UNUserNotificationCenter::currentNotificationCenter()
.getDeliveredNotificationsWithCompletionHandler(&RcBlock::new(
move |notifications: NonNull<NSArray<UNNotification>>| {
let ids: Vec<String> = unsafe { notifications.as_ref() }
.iter()
.map(|notif| notif.request().identifier().to_string())
.collect();
tx.send(ids);
},
));
});
async move { rx.await.unwrap_or_default() }
}
pub async fn send(notification: Notification) -> Result<String, Error> {
check_bundle()?;
let request_id = notification
.notification_id
.clone()
.unwrap_or_else(|| NSUUID::new().UUIDString().to_string());
let id_copy = request_id.clone();
send_inner(notification, None, request_id)
.await
.map(|()| id_copy)
}
#[cfg(feature = "blocking-wrappers")]
pub fn send_blocking(notification: Notification) -> Result<String, Error> {
check_bundle()?;
let request_id = notification
.notification_id
.clone()
.unwrap_or_else(|| NSUUID::new().UUIDString().to_string());
let id_copy = request_id.clone();
future::block_on(send_inner(notification, None, request_id)).map(|()| id_copy)
}
pub async fn send_and_wait_for_delivery(
notification: Notification,
) -> Result<NotificationHandle, Error> {
check_bundle()?;
let request_id = notification
.notification_id
.clone()
.unwrap_or_else(|| NSUUID::new().UUIDString().to_string());
let (response_tx, response_rx) = oneshot::channel();
let timeout = notification.action_timeout;
let has_actions = !notification.actions.is_empty();
let guard = PendingGuard::new(request_id.clone(), response_rx);
send_inner(notification, Some(response_tx), request_id.clone()).await?;
Ok(NotificationHandle {
notification_id: request_id,
guard,
timeout,
has_actions,
})
}
pub async fn send_with_actions(notification: Notification) -> Result<NotificationResponse, Error> {
check_bundle()?;
let request_id = notification
.notification_id
.clone()
.unwrap_or_else(|| NSUUID::new().UUIDString().to_string());
let (response_tx, response_rx) = oneshot::channel();
let timeout = notification.action_timeout;
let has_actions = !notification.actions.is_empty();
let guard = PendingGuard::new(request_id.clone(), response_rx);
send_inner(notification, Some(response_tx), request_id.clone()).await?;
NotificationHandle {
notification_id: request_id,
guard,
timeout,
has_actions,
}
.response()
.await
}