use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
use block2::RcBlock;
use futures::StreamExt as _;
use futures::channel::mpsc;
use gpui::{
ForegroundExecutor, SharedString, SystemNotification, SystemNotificationAction,
SystemNotificationResponse, Task,
};
use objc2::rc::Retained;
use objc2::runtime::{Bool, ProtocolObject};
use objc2::{AnyThread, DefinedClass, define_class, msg_send};
use objc2_foundation::{NSArray, NSBundle, NSError, NSObject, NSObjectProtocol, NSSet, NSString};
use objc2_user_notifications::{
UNAuthorizationOptions, UNMutableNotificationContent, UNNotification, UNNotificationAction,
UNNotificationActionOptions, UNNotificationCategory, UNNotificationCategoryOptions,
UNNotificationDefaultActionIdentifier, UNNotificationPresentationOptions,
UNNotificationRequest, UNNotificationResponse, UNUserNotificationCenter,
UNUserNotificationCenterDelegate,
};
type ResponseCallback = Rc<RefCell<Option<Box<dyn FnMut(SystemNotificationResponse)>>>>;
pub(crate) struct SystemNotificationState {
initialized: bool,
center: Option<NotificationCenter>,
callback: ResponseCallback,
_response_task: Option<Task<()>>,
}
impl SystemNotificationState {
pub(crate) fn new() -> Self {
Self {
initialized: false,
center: None,
callback: Rc::new(RefCell::new(None)),
_response_task: None,
}
}
pub(crate) fn show(&mut self, executor: &ForegroundExecutor, notification: SystemNotification) {
self.initialize(executor);
if let Some(center) = &self.center {
center.show(notification);
}
}
pub(crate) fn dismiss(&mut self, executor: &ForegroundExecutor, tag: &str) {
self.initialize(executor);
if let Some(center) = &self.center {
center.dismiss(tag);
}
}
pub(crate) fn on_response(
&mut self,
executor: &ForegroundExecutor,
callback: Box<dyn FnMut(SystemNotificationResponse)>,
) {
self.initialize(executor);
*self.callback.borrow_mut() = Some(callback);
}
fn initialize(&mut self, executor: &ForegroundExecutor) {
if self.initialized {
return;
}
self.initialized = true;
let (sender, mut receiver) = mpsc::unbounded();
self.center = NotificationCenter::new(sender);
if self.center.is_none() {
return;
}
let callback = self.callback.clone();
self._response_task = Some(executor.spawn(async move {
while let Some(response) = receiver.next().await {
let taken = callback.borrow_mut().take();
if let Some(mut taken) = taken {
taken(response);
callback.borrow_mut().get_or_insert(taken);
}
}
}));
}
}
struct NotificationCenter {
center: Retained<UNUserNotificationCenter>,
_delegate: Retained<NotificationResponseDelegate>,
categories: RefCell<
HashMap<Vec<SystemNotificationAction>, (SharedString, Retained<UNNotificationCategory>)>,
>,
authorization_requested: Cell<bool>,
}
impl NotificationCenter {
fn new(sender: mpsc::UnboundedSender<SystemNotificationResponse>) -> Option<Self> {
if NSBundle::mainBundle().bundleIdentifier().is_none() {
log::info!("system notifications disabled: not running from an app bundle");
return None;
}
let center = UNUserNotificationCenter::currentNotificationCenter();
let delegate = NotificationResponseDelegate::new(sender);
center.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
Some(Self {
center,
_delegate: delegate,
categories: RefCell::new(HashMap::new()),
authorization_requested: Cell::new(false),
})
}
fn request_authorization(&self) {
if self.authorization_requested.replace(true) {
return;
}
let completion = RcBlock::new(|granted: Bool, error: *mut NSError| {
if let Some(error) = unsafe { error.as_ref() } {
log::warn!(
"system notification authorization failed: {}",
error.localizedDescription()
);
} else if !granted.as_bool() {
log::info!("system notification authorization denied");
}
});
self.center
.requestAuthorizationWithOptions_completionHandler(
UNAuthorizationOptions::Alert | UNAuthorizationOptions::Sound,
&completion,
);
}
fn show(&self, notification: SystemNotification) {
self.request_authorization();
let content = UNMutableNotificationContent::new();
content.setTitle(&NSString::from_str(¬ification.title));
content.setBody(&NSString::from_str(¬ification.body));
if !notification.actions.is_empty() {
let category_identifier = self.register_category(¬ification.actions);
content.setCategoryIdentifier(&NSString::from_str(&category_identifier));
}
let request = UNNotificationRequest::requestWithIdentifier_content_trigger(
&NSString::from_str(¬ification.tag),
&content,
None,
);
let completion = RcBlock::new(|error: *mut NSError| {
if let Some(error) = unsafe { error.as_ref() } {
log::warn!(
"failed to deliver system notification: {}",
error.localizedDescription()
);
}
});
self.center
.addNotificationRequest_withCompletionHandler(&request, Some(&completion));
}
fn register_category(&self, actions: &[SystemNotificationAction]) -> SharedString {
let mut categories = self.categories.borrow_mut();
if let Some((identifier, _category)) = categories.get(actions) {
return identifier.clone();
}
let identifier =
SharedString::from(format!("gpui-system-notification-{}", categories.len()));
let platform_actions: Vec<Retained<UNNotificationAction>> = actions
.iter()
.map(|action| {
UNNotificationAction::actionWithIdentifier_title_options(
&NSString::from_str(&action.id),
&NSString::from_str(&action.label),
UNNotificationActionOptions::empty(),
)
})
.collect();
let category =
UNNotificationCategory::categoryWithIdentifier_actions_intentIdentifiers_options(
&NSString::from_str(&identifier),
&NSArray::from_retained_slice(&platform_actions),
&NSArray::new(),
UNNotificationCategoryOptions::empty(),
);
categories.insert(actions.to_vec(), (identifier.clone(), category));
let all: Vec<Retained<UNNotificationCategory>> = categories
.values()
.map(|(_identifier, category)| category.clone())
.collect();
self.center
.setNotificationCategories(&NSSet::from_retained_slice(&all));
identifier
}
fn dismiss(&self, tag: &str) {
let identifiers = NSArray::from_retained_slice(&[NSString::from_str(tag)]);
self.center
.removePendingNotificationRequestsWithIdentifiers(&identifiers);
self.center
.removeDeliveredNotificationsWithIdentifiers(&identifiers);
}
}
struct DelegateIvars {
sender: mpsc::UnboundedSender<SystemNotificationResponse>,
}
define_class!(
#[unsafe(super(NSObject))]
#[ivars = DelegateIvars]
struct NotificationResponseDelegate;
unsafe impl NSObjectProtocol for NotificationResponseDelegate {}
unsafe impl UNUserNotificationCenterDelegate for NotificationResponseDelegate {
#[unsafe(method(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:))]
fn did_receive_notification_response(
&self,
_center: &UNUserNotificationCenter,
response: &UNNotificationResponse,
completion_handler: &block2::DynBlock<dyn Fn()>,
) {
let tag = response.notification().request().identifier().to_string();
let action = response.actionIdentifier();
let action_id = if &*action == unsafe { UNNotificationDefaultActionIdentifier } {
None
} else {
Some(SharedString::from(action.to_string()))
};
self.ivars()
.sender
.unbounded_send(SystemNotificationResponse {
tag: SharedString::from(tag),
action_id,
})
.ok();
completion_handler.call(());
}
#[unsafe(method(userNotificationCenter:willPresentNotification:withCompletionHandler:))]
fn will_present_notification(
&self,
_center: &UNUserNotificationCenter,
_notification: &UNNotification,
completion_handler: &block2::DynBlock<dyn Fn(UNNotificationPresentationOptions)>,
) {
completion_handler
.call((UNNotificationPresentationOptions::Banner
| UNNotificationPresentationOptions::List,));
}
}
);
impl NotificationResponseDelegate {
fn new(sender: mpsc::UnboundedSender<SystemNotificationResponse>) -> Retained<Self> {
let this = Self::alloc().set_ivars(DelegateIvars { sender });
unsafe { msg_send![super(this), init] }
}
}