use cranpose_core::compositionLocalOfWithPolicy;
use cranpose_core::CompositionLocal;
use cranpose_core::CompositionLocalProvider;
use cranpose_macros::composable;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NotifyRequest {
pub id: String,
pub title: String,
pub body: String,
pub ongoing: bool,
pub deeplink: Option<String>,
}
impl NotifyRequest {
pub fn new(id: impl Into<String>, title: impl Into<String>, body: impl Into<String>) -> Self {
Self {
id: id.into(),
title: title.into(),
body: body.into(),
ongoing: false,
deeplink: None,
}
}
pub fn ongoing(mut self, ongoing: bool) -> Self {
self.ongoing = ongoing;
self
}
pub fn with_deeplink(mut self, deeplink: impl Into<String>) -> Self {
self.deeplink = Some(deeplink.into());
self
}
}
pub trait Notifier {
fn request_permission(&self);
fn notify(&self, request: NotifyRequest);
fn cancel(&self, id: &str);
}
pub type NotifierRef = Rc<dyn Notifier>;
use std::sync::Mutex;
use std::sync::OnceLock;
fn pending_deeplink_slot() -> &'static Mutex<Option<String>> {
static SLOT: OnceLock<Mutex<Option<String>>> = OnceLock::new();
SLOT.get_or_init(|| Mutex::new(None))
}
pub fn push_notification_deeplink(link: String) {
if let Ok(mut slot) = pending_deeplink_slot().lock() {
*slot = Some(link);
}
}
pub fn take_notification_deeplink() -> Option<String> {
pending_deeplink_slot()
.lock()
.ok()
.and_then(|mut s| s.take())
}
struct NoopNotifier;
impl Notifier for NoopNotifier {
fn request_permission(&self) {}
fn notify(&self, _request: NotifyRequest) {}
fn cancel(&self, _id: &str) {}
}
thread_local! {
static PLATFORM_NOTIFIER: RefCell<Option<NotifierRef>> = const { RefCell::new(None) };
}
pub fn set_platform_notifier(notifier: NotifierRef) {
PLATFORM_NOTIFIER.with(|cell| *cell.borrow_mut() = Some(notifier));
}
pub fn clear_platform_notifier() {
PLATFORM_NOTIFIER.with(|cell| *cell.borrow_mut() = None);
}
pub fn default_notifier() -> NotifierRef {
PLATFORM_NOTIFIER
.with(|cell| cell.borrow().clone())
.unwrap_or_else(|| Rc::new(NoopNotifier))
}
pub fn local_notifier() -> CompositionLocal<NotifierRef> {
thread_local! {
static LOCAL_NOTIFIER: RefCell<Option<CompositionLocal<NotifierRef>>> = const { RefCell::new(None) };
}
LOCAL_NOTIFIER.with(|cell| {
let mut local = cell.borrow_mut();
local
.get_or_insert_with(|| compositionLocalOfWithPolicy(default_notifier, Rc::ptr_eq))
.clone()
})
}
#[allow(non_snake_case)]
#[composable]
pub fn ProvideNotifier(content: impl FnOnce()) {
let notifier = cranpose_core::remember(default_notifier).with(|state| state.clone());
let local = local_notifier();
CompositionLocalProvider(vec![local.provides(notifier)], move || {
content();
});
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
#[derive(Default)]
struct Recorder {
posted: RefCell<Vec<String>>,
}
impl Notifier for Recorder {
fn request_permission(&self) {}
fn notify(&self, request: NotifyRequest) {
self.posted.borrow_mut().push(request.id);
}
fn cancel(&self, _id: &str) {}
}
#[test]
fn default_is_noop_then_registered_takes_over() {
clear_platform_notifier();
default_notifier().notify(NotifyRequest::new("a", "t", "b"));
let rec = Rc::new(Recorder::default());
set_platform_notifier(rec.clone());
default_notifier().notify(NotifyRequest::new("done", "t", "b").with_deeplink("doc/1"));
assert_eq!(rec.posted.borrow().as_slice(), &["done".to_string()]);
clear_platform_notifier();
}
}