cranpose_services/
notifier.rs1use cranpose_core::compositionLocalOfWithPolicy;
9use cranpose_core::CompositionLocal;
10use cranpose_core::CompositionLocalProvider;
11use cranpose_macros::composable;
12use std::cell::RefCell;
13use std::rc::Rc;
14
15#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct NotifyRequest {
18 pub id: String,
21 pub title: String,
22 pub body: String,
23 pub ongoing: bool,
26 pub deeplink: Option<String>,
28}
29
30impl NotifyRequest {
31 pub fn new(id: impl Into<String>, title: impl Into<String>, body: impl Into<String>) -> Self {
32 Self {
33 id: id.into(),
34 title: title.into(),
35 body: body.into(),
36 ongoing: false,
37 deeplink: None,
38 }
39 }
40
41 pub fn ongoing(mut self, ongoing: bool) -> Self {
42 self.ongoing = ongoing;
43 self
44 }
45
46 pub fn with_deeplink(mut self, deeplink: impl Into<String>) -> Self {
47 self.deeplink = Some(deeplink.into());
48 self
49 }
50}
51
52pub trait Notifier {
55 fn request_permission(&self);
58 fn notify(&self, request: NotifyRequest);
60 fn cancel(&self, id: &str);
62}
63
64pub type NotifierRef = Rc<dyn Notifier>;
65
66use std::sync::Mutex;
67use std::sync::OnceLock;
68
69fn pending_deeplink_slot() -> &'static Mutex<Option<String>> {
70 static SLOT: OnceLock<Mutex<Option<String>>> = OnceLock::new();
71 SLOT.get_or_init(|| Mutex::new(None))
72}
73
74pub fn push_notification_deeplink(link: String) {
78 if let Ok(mut slot) = pending_deeplink_slot().lock() {
79 *slot = Some(link);
80 }
81}
82
83pub fn take_notification_deeplink() -> Option<String> {
86 pending_deeplink_slot()
87 .lock()
88 .ok()
89 .and_then(|mut s| s.take())
90}
91
92struct NoopNotifier;
93
94impl Notifier for NoopNotifier {
95 fn request_permission(&self) {}
96 fn notify(&self, _request: NotifyRequest) {}
97 fn cancel(&self, _id: &str) {}
98}
99
100thread_local! {
101 static PLATFORM_NOTIFIER: RefCell<Option<NotifierRef>> = const { RefCell::new(None) };
102}
103
104pub fn set_platform_notifier(notifier: NotifierRef) {
106 PLATFORM_NOTIFIER.with(|cell| *cell.borrow_mut() = Some(notifier));
107}
108
109pub fn clear_platform_notifier() {
111 PLATFORM_NOTIFIER.with(|cell| *cell.borrow_mut() = None);
112}
113
114pub fn default_notifier() -> NotifierRef {
115 PLATFORM_NOTIFIER
116 .with(|cell| cell.borrow().clone())
117 .unwrap_or_else(|| Rc::new(NoopNotifier))
118}
119
120pub fn local_notifier() -> CompositionLocal<NotifierRef> {
121 thread_local! {
122 static LOCAL_NOTIFIER: RefCell<Option<CompositionLocal<NotifierRef>>> = const { RefCell::new(None) };
123 }
124
125 LOCAL_NOTIFIER.with(|cell| {
126 let mut local = cell.borrow_mut();
127 local
128 .get_or_insert_with(|| compositionLocalOfWithPolicy(default_notifier, Rc::ptr_eq))
129 .clone()
130 })
131}
132
133#[allow(non_snake_case)]
134#[composable]
135pub fn ProvideNotifier(content: impl FnOnce()) {
136 let notifier = cranpose_core::remember(default_notifier).with(|state| state.clone());
137 let local = local_notifier();
138 CompositionLocalProvider(vec![local.provides(notifier)], move || {
139 content();
140 });
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use std::cell::RefCell;
147
148 #[derive(Default)]
149 struct Recorder {
150 posted: RefCell<Vec<String>>,
151 }
152 impl Notifier for Recorder {
153 fn request_permission(&self) {}
154 fn notify(&self, request: NotifyRequest) {
155 self.posted.borrow_mut().push(request.id);
156 }
157 fn cancel(&self, _id: &str) {}
158 }
159
160 #[test]
161 fn default_is_noop_then_registered_takes_over() {
162 clear_platform_notifier();
163 default_notifier().notify(NotifyRequest::new("a", "t", "b"));
165 let rec = Rc::new(Recorder::default());
166 set_platform_notifier(rec.clone());
167 default_notifier().notify(NotifyRequest::new("done", "t", "b").with_deeplink("doc/1"));
168 assert_eq!(rec.posted.borrow().as_slice(), &["done".to_string()]);
169 clear_platform_notifier();
170 }
171}