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
66struct NoopNotifier;
67
68impl Notifier for NoopNotifier {
69 fn request_permission(&self) {}
70 fn notify(&self, _request: NotifyRequest) {}
71 fn cancel(&self, _id: &str) {}
72}
73
74thread_local! {
75 static PLATFORM_NOTIFIER: RefCell<Option<NotifierRef>> = const { RefCell::new(None) };
76}
77
78pub fn set_platform_notifier(notifier: NotifierRef) {
80 PLATFORM_NOTIFIER.with(|cell| *cell.borrow_mut() = Some(notifier));
81}
82
83pub fn clear_platform_notifier() {
85 PLATFORM_NOTIFIER.with(|cell| *cell.borrow_mut() = None);
86}
87
88pub fn default_notifier() -> NotifierRef {
89 PLATFORM_NOTIFIER
90 .with(|cell| cell.borrow().clone())
91 .unwrap_or_else(|| Rc::new(NoopNotifier))
92}
93
94pub fn local_notifier() -> CompositionLocal<NotifierRef> {
95 thread_local! {
96 static LOCAL_NOTIFIER: RefCell<Option<CompositionLocal<NotifierRef>>> = const { RefCell::new(None) };
97 }
98
99 LOCAL_NOTIFIER.with(|cell| {
100 let mut local = cell.borrow_mut();
101 local
102 .get_or_insert_with(|| compositionLocalOfWithPolicy(default_notifier, Rc::ptr_eq))
103 .clone()
104 })
105}
106
107#[allow(non_snake_case)]
108#[composable]
109pub fn ProvideNotifier(content: impl FnOnce()) {
110 let notifier = cranpose_core::remember(default_notifier).with(|state| state.clone());
111 let local = local_notifier();
112 CompositionLocalProvider(vec![local.provides(notifier)], move || {
113 content();
114 });
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use std::cell::RefCell;
121
122 #[derive(Default)]
123 struct Recorder {
124 posted: RefCell<Vec<String>>,
125 }
126 impl Notifier for Recorder {
127 fn request_permission(&self) {}
128 fn notify(&self, request: NotifyRequest) {
129 self.posted.borrow_mut().push(request.id);
130 }
131 fn cancel(&self, _id: &str) {}
132 }
133
134 #[test]
135 fn default_is_noop_then_registered_takes_over() {
136 clear_platform_notifier();
137 default_notifier().notify(NotifyRequest::new("a", "t", "b"));
139 let rec = Rc::new(Recorder::default());
140 set_platform_notifier(rec.clone());
141 default_notifier().notify(NotifyRequest::new("done", "t", "b").with_deeplink("doc/1"));
142 assert_eq!(rec.posted.borrow().as_slice(), &["done".to_string()]);
143 clear_platform_notifier();
144 }
145}