#![allow(unsafe_code)]
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::Once;
use block2::{DynBlock, RcBlock};
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, Bool, NSObject, NSObjectProtocol, ProtocolObject};
use objc2::{AnyThread, define_class, msg_send};
use objc2_foundation::{NSArray, NSBundle, NSDictionary, NSError, NSString};
use objc2_user_notifications::{
UNAuthorizationOptions, UNMutableNotificationContent, UNNotification, UNNotificationPresentationOptions, UNNotificationRequest,
UNNotificationResponse, UNNotificationSound, UNUserNotificationCenter, UNUserNotificationCenterDelegate,
};
use super::{Click, Prepared, decode_launch, deliver, encode_launch};
use crate::{NativeError, Result};
const INFO_KEY: &str = "ashlar";
fn bundled() -> bool {
let b = NSBundle::mainBundle();
b.bundleIdentifier().is_some() && b.bundlePath().to_string().ends_with(".app")
}
fn click_of(response: &UNNotificationResponse) -> Click {
let request = response.notification().request();
let id = request.identifier().to_string();
let key = NSString::from_str(INFO_KEY);
let key: &AnyObject = key.as_ref();
let text = request.content().userInfo().objectForKey(key).and_then(|o| o.downcast::<NSString>().ok()).map(|s| s.to_string());
let info = text.and_then(|t| decode_launch(&t)).map(|c| c.info).unwrap_or_default();
Click { id, info }
}
define_class!(
#[unsafe(super(NSObject))]
#[name = "AshlarNativeNotificationDelegate"]
struct Delegate;
unsafe impl NSObjectProtocol for Delegate {}
unsafe impl UNUserNotificationCenterDelegate for Delegate {
#[unsafe(method(userNotificationCenter:willPresentNotification:withCompletionHandler:))]
fn will_present(
&self,
_center: &UNUserNotificationCenter,
notification: &UNNotification,
handler: &DynBlock<dyn Fn(UNNotificationPresentationOptions)>,
) {
let shown = UNNotificationPresentationOptions::Banner | UNNotificationPresentationOptions::List;
let options = catch_unwind(AssertUnwindSafe(|| {
if notification.request().content().sound().is_some() { shown | UNNotificationPresentationOptions::Sound } else { shown }
}))
.unwrap_or(shown);
handler.call((options,));
}
#[unsafe(method(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:))]
fn did_receive(&self, _center: &UNUserNotificationCenter, response: &UNNotificationResponse, handler: &DynBlock<dyn Fn()>) {
let _ = catch_unwind(AssertUnwindSafe(|| deliver(click_of(response))));
handler.call(());
}
}
);
impl Delegate {
fn new() -> Retained<Self> {
let this = Self::alloc().set_ivars(());
unsafe { msg_send![super(this), init] }
}
}
fn install() {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
let delegate = Delegate::new();
UNUserNotificationCenter::currentNotificationCenter().setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
std::mem::forget(delegate);
});
}
pub fn show(p: &Prepared) -> Result<()> {
if !bundled() {
return Err(NativeError::Unsupported);
}
install();
let p = p.clone();
let mut options = UNAuthorizationOptions::Alert;
if !p.silent {
options |= UNAuthorizationOptions::Sound;
}
let done = RcBlock::new(move |granted: Bool, _err: *mut NSError| {
let _ = catch_unwind(|| {
if !granted.as_bool() {
log::info!("notifications: not allowed by the user");
return;
}
let content = UNMutableNotificationContent::new();
content.setTitle(&NSString::from_str(&p.title));
content.setBody(&NSString::from_str(&p.body));
if !p.silent {
content.setSound(Some(&UNNotificationSound::defaultSound()));
}
let key = NSString::from_str(INFO_KEY);
let value = NSString::from_str(&encode_launch(&p.id, &p.info));
let info = NSDictionary::<NSString, NSString>::from_slices(&[&*key], &[&*value]);
unsafe { content.setUserInfo(info.cast_unchecked()) };
let id = NSString::from_str(&p.id);
let request = UNNotificationRequest::requestWithIdentifier_content_trigger(&id, &content, None);
UNUserNotificationCenter::currentNotificationCenter().addNotificationRequest_withCompletionHandler(&request, None);
});
});
UNUserNotificationCenter::currentNotificationCenter().requestAuthorizationWithOptions_completionHandler(options, &done);
Ok(())
}
pub fn remove(ids: &[String]) -> Result<()> {
if !bundled() {
return Err(NativeError::Unsupported);
}
let ids: Vec<Retained<NSString>> = ids.iter().map(|s| NSString::from_str(s)).collect();
let ids = NSArray::from_retained_slice(&ids);
let center = UNUserNotificationCenter::currentNotificationCenter();
center.removePendingNotificationRequestsWithIdentifiers(&ids);
center.removeDeliveredNotificationsWithIdentifiers(&ids);
Ok(())
}
pub fn listen() -> Result<()> {
if !bundled() {
return Err(NativeError::Unsupported);
}
install();
Ok(())
}