#![allow(unsafe_code)]
use std::{
cell::RefCell,
collections::HashMap,
sync::{Arc, Mutex, OnceLock},
};
use block2::RcBlock;
use cranpose_services::{
Notifier, NotifyRequest, push_notification_deeplink, set_platform_notifier,
};
use objc2::{
MainThreadMarker, MainThreadOnly, define_class, msg_send,
rc::Retained,
runtime::{Bool, ProtocolObject},
};
use objc2_foundation::{NSArray, NSError, NSObject, NSObjectProtocol, NSString};
use objc2_user_notifications::{
UNAuthorizationOptions, UNMutableNotificationContent, UNNotification,
UNNotificationPresentationOptions, UNNotificationRequest, UNNotificationResponse,
UNNotificationSound, UNUserNotificationCenter, UNUserNotificationCenterDelegate,
};
fn deeplink_map() -> &'static Mutex<HashMap<String, String>> {
static MAP: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
MAP.get_or_init(|| Mutex::new(HashMap::new()))
}
pub(crate) fn register() {
set_platform_notifier(Arc::new(IosNotifier));
install_delegate();
}
struct IosNotifier;
impl Notifier for IosNotifier {
fn request_permission(&self) {
let center = UNUserNotificationCenter::currentNotificationCenter();
let options = UNAuthorizationOptions::Alert
| UNAuthorizationOptions::Sound
| UNAuthorizationOptions::Badge;
let handler = RcBlock::new(|_granted: Bool, _error: *mut NSError| {});
center.requestAuthorizationWithOptions_completionHandler(options, &handler);
}
fn notify(&self, request: NotifyRequest) {
let center = UNUserNotificationCenter::currentNotificationCenter();
let content = UNMutableNotificationContent::new();
content.setTitle(&NSString::from_str(&request.title));
content.setBody(&NSString::from_str(&request.body));
content.setSound(Some(&UNNotificationSound::defaultSound()));
if let Some(link) = request.deeplink.as_deref()
&& let Ok(mut map) = deeplink_map().lock()
{
map.insert(request.id.clone(), link.to_owned());
}
let id = NSString::from_str(&request.id);
let un_request =
UNNotificationRequest::requestWithIdentifier_content_trigger(&id, &content, None);
center.addNotificationRequest_withCompletionHandler(&un_request, None);
}
fn cancel(&self, id: &str) {
if let Ok(mut map) = deeplink_map().lock() {
map.remove(id);
}
let center = UNUserNotificationCenter::currentNotificationCenter();
let ids = NSArray::from_retained_slice(&[NSString::from_str(id)]);
center.removePendingNotificationRequestsWithIdentifiers(&ids);
center.removeDeliveredNotificationsWithIdentifiers(&ids);
}
}
thread_local! {
static DELEGATE: RefCell<Option<Retained<NotificationDelegate>>> = const { RefCell::new(None) };
}
fn install_delegate() {
let delegate = NotificationDelegate::new();
let center = UNUserNotificationCenter::currentNotificationCenter();
center.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
DELEGATE.with(|cell| *cell.borrow_mut() = Some(delegate));
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "CranposeNotificationDelegate"]
#[ivars = ()]
struct NotificationDelegate;
unsafe impl NSObjectProtocol for NotificationDelegate {}
unsafe impl UNUserNotificationCenterDelegate for NotificationDelegate {
#[unsafe(method(userNotificationCenter:willPresentNotification:withCompletionHandler:))]
fn will_present(
&self,
_center: &UNUserNotificationCenter,
_notification: &UNNotification,
completion: &block2::DynBlock<dyn Fn(UNNotificationPresentationOptions)>,
) {
let options = UNNotificationPresentationOptions::Banner
| UNNotificationPresentationOptions::Sound;
completion.call((options,));
}
#[unsafe(method(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:))]
fn did_receive(
&self,
_center: &UNUserNotificationCenter,
response: &UNNotificationResponse,
completion: &block2::DynBlock<dyn Fn()>,
) {
if let Some(link) = deeplink_of(response) {
push_notification_deeplink(link);
}
completion.call(());
}
}
);
impl NotificationDelegate {
fn new() -> Retained<Self> {
let mtm =
MainThreadMarker::new().expect("notification delegate is installed on the main thread");
let this = Self::alloc(mtm).set_ivars(());
unsafe { msg_send![super(this), init] }
}
}
fn deeplink_of(response: &UNNotificationResponse) -> Option<String> {
let identifier = response.notification().request().identifier().to_string();
deeplink_map().lock().ok()?.remove(&identifier)
}