1use crate::{use_supported, use_window};
2use cfg_if::cfg_if;
3use default_struct_builder::DefaultBuilder;
4use leptos::prelude::*;
5use leptos::reactive::wrappers::read::Signal;
6use std::rc::Rc;
7use wasm_bindgen::JsValue;
8
9pub fn use_web_notification() -> UseWebNotificationReturn<
49 impl Fn(ShowOptions) + Clone + Send + Sync,
50 impl Fn() + Clone + Send + Sync,
51> {
52 use_web_notification_with_options(UseWebNotificationOptions::default())
53}
54
55pub fn use_web_notification_with_options(
57 options: UseWebNotificationOptions,
58) -> UseWebNotificationReturn<
59 impl Fn(ShowOptions) + Clone + Send + Sync,
60 impl Fn() + Clone + Send + Sync,
61> {
62 let is_supported = use_supported(browser_supports_notifications);
63
64 let (notification, set_notification) = signal_local(None::<web_sys::Notification>);
65
66 let (permission, set_permission) = signal(NotificationPermission::default());
67
68 cfg_if! { if #[cfg(feature = "ssr")] {
69 let _ = options;
70 let _ = set_notification;
71 let _ = set_permission;
72
73 let show = move |_: ShowOptions| ();
74 let close = move || ();
75 } else {
76 use crate::use_event_listener;
77 use leptos::ev::visibilitychange;
78 use wasm_bindgen::closure::Closure;
79 use wasm_bindgen::JsCast;
80 use send_wrapper::SendWrapper;
81
82 let on_click_closure = Closure::<dyn Fn(web_sys::Event)>::new({
83 let on_click = Rc::clone(&options.on_click);
84 move |e: web_sys::Event| {
85 #[cfg(debug_assertions)]
86 let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
87
88 on_click(e);
89 }
90 })
91 .into_js_value();
92
93 let on_close_closure = Closure::<dyn Fn(web_sys::Event)>::new({
94 let on_close = Rc::clone(&options.on_close);
95 move |e: web_sys::Event| {
96 #[cfg(debug_assertions)]
97 let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
98
99 on_close(e);
100 }
101 })
102 .into_js_value();
103
104 let on_error_closure = Closure::<dyn Fn(web_sys::Event)>::new({
105 let on_error = Rc::clone(&options.on_error);
106 move |e: web_sys::Event| {
107 #[cfg(debug_assertions)]
108 let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
109
110 on_error(e);
111 }
112 })
113 .into_js_value();
114
115 let on_show_closure = Closure::<dyn Fn(web_sys::Event)>::new({
116 let on_show = Rc::clone(&options.on_show);
117 move |e: web_sys::Event| {
118 #[cfg(debug_assertions)]
119 let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
120
121 on_show(e);
122 }
123 })
124 .into_js_value();
125
126 let show = {
127 let options = options.clone();
128 let on_click_closure = on_click_closure.clone();
129 let on_close_closure = on_close_closure.clone();
130 let on_error_closure = on_error_closure.clone();
131 let on_show_closure = on_show_closure.clone();
132
133 let show = move |options_override: ShowOptions| {
134 if !is_supported.get_untracked() {
135 return;
136 }
137
138 let options = options.clone();
139 let on_click_closure = on_click_closure.clone();
140 let on_close_closure = on_close_closure.clone();
141 let on_error_closure = on_error_closure.clone();
142 let on_show_closure = on_show_closure.clone();
143
144 leptos::task::spawn_local(async move {
145 set_permission.set(request_web_notification_permission().await);
146
147 let mut notification_options = web_sys::NotificationOptions::from(&options);
148 options_override.override_notification_options(&mut notification_options);
149
150 let notification_value = web_sys::Notification::new_with_options(
151 &options_override.title.unwrap_or(options.title),
152 ¬ification_options,
153 )
154 .expect("Notification should be created");
155
156 notification_value.set_onclick(Some(on_click_closure.unchecked_ref()));
157 notification_value.set_onclose(Some(on_close_closure.unchecked_ref()));
158 notification_value.set_onerror(Some(on_error_closure.unchecked_ref()));
159 notification_value.set_onshow(Some(on_show_closure.unchecked_ref()));
160
161 set_notification.set(Some(notification_value));
162 });
163 };
164 let wrapped_show = SendWrapper::new(show);
165 move |options_override: ShowOptions| wrapped_show(options_override)
166 };
167
168 let close = {
169 move || {
170 notification.with_untracked(|notification| {
171 if let Some(notification) = notification {
172 notification.close();
173 }
174 });
175 set_notification.set(None);
176 }
177 };
178
179 leptos::task::spawn_local(async move {
180 set_permission.set(request_web_notification_permission().await);
181 });
182
183 on_cleanup(close);
184
185 if is_supported.get_untracked() {
190 let _ = use_event_listener(document(), visibilitychange, move |e: web_sys::Event| {
191 e.prevent_default();
192 if document().visibility_state() == web_sys::VisibilityState::Visible {
193 close()
195 }
196 });
197 }
198 }}
199
200 UseWebNotificationReturn {
201 is_supported,
202 notification: notification.into(),
203 show,
204 close,
205 permission: permission.into(),
206 }
207}
208
209#[derive(Default, Clone, Copy, Eq, PartialEq, Debug)]
210pub enum NotificationDirection {
211 #[default]
212 Auto,
213 LeftToRight,
214 RightToLeft,
215}
216
217impl From<NotificationDirection> for web_sys::NotificationDirection {
218 fn from(direction: NotificationDirection) -> Self {
219 match direction {
220 NotificationDirection::Auto => Self::Auto,
221 NotificationDirection::LeftToRight => Self::Ltr,
222 NotificationDirection::RightToLeft => Self::Rtl,
223 }
224 }
225}
226
227#[derive(DefaultBuilder, Clone)]
232#[cfg_attr(feature = "ssr", allow(dead_code))]
233pub struct UseWebNotificationOptions {
234 #[builder(into)]
237 title: String,
238
239 #[builder(into)]
242 body: Option<String>,
243
244 direction: NotificationDirection,
248
249 #[builder(into)]
252 language: Option<String>,
253
254 #[builder(into)]
257 tag: Option<String>,
258
259 #[builder(into)]
262 icon: Option<String>,
263
264 #[builder(into)]
267 image: Option<String>,
268
269 require_interaction: bool,
272
273 #[builder(into)]
276 renotify: bool,
277
278 #[builder(into)]
281 silent: Option<bool>,
282
283 #[builder(into)]
286 vibrate: Option<Vec<u16>>,
287
288 on_click: Rc<dyn Fn(web_sys::Event)>,
290
291 on_close: Rc<dyn Fn(web_sys::Event)>,
293
294 on_error: Rc<dyn Fn(web_sys::Event)>,
297
298 on_show: Rc<dyn Fn(web_sys::Event)>,
300}
301
302impl Default for UseWebNotificationOptions {
303 fn default() -> Self {
304 Self {
305 title: "".to_string(),
306 body: None,
307 direction: NotificationDirection::default(),
308 language: None,
309 tag: None,
310 icon: None,
311 image: None,
312 require_interaction: false,
313 renotify: false,
314 silent: None,
315 vibrate: None,
316 on_click: Rc::new(|_| {}),
317 on_close: Rc::new(|_| {}),
318 on_error: Rc::new(|_| {}),
319 on_show: Rc::new(|_| {}),
320 }
321 }
322}
323
324impl From<&UseWebNotificationOptions> for web_sys::NotificationOptions {
325 fn from(options: &UseWebNotificationOptions) -> Self {
326 let web_sys_options = Self::new();
327
328 web_sys_options.set_dir(options.direction.into());
329 web_sys_options.set_require_interaction(options.require_interaction);
330 web_sys_options.set_renotify(options.renotify);
331 web_sys_options.set_silent(options.silent);
332
333 if let Some(body) = &options.body {
334 web_sys_options.set_body(body);
335 }
336
337 if let Some(icon) = &options.icon {
338 web_sys_options.set_icon(icon);
339 }
340
341 if let Some(image) = &options.image {
342 web_sys_options.set_image(image);
343 }
344
345 if let Some(language) = &options.language {
346 web_sys_options.set_lang(language);
347 }
348
349 if let Some(tag) = &options.tag {
350 web_sys_options.set_tag(tag);
351 }
352
353 if let Some(vibrate) = &options.vibrate {
354 web_sys_options.set_vibrate(&vibration_pattern_to_jsvalue(vibrate));
355 }
356 web_sys_options
357 }
358}
359
360#[derive(DefaultBuilder, Default)]
367#[cfg_attr(feature = "ssr", allow(dead_code))]
368pub struct ShowOptions {
369 #[builder(into)]
372 title: Option<String>,
373
374 #[builder(into)]
377 body: Option<String>,
378
379 #[builder(into)]
383 direction: Option<NotificationDirection>,
384
385 #[builder(into)]
388 language: Option<String>,
389
390 #[builder(into)]
393 tag: Option<String>,
394
395 #[builder(into)]
398 icon: Option<String>,
399
400 #[builder(into)]
403 image: Option<String>,
404
405 #[builder(into)]
408 require_interaction: Option<bool>,
409
410 #[builder(into)]
413 renotify: Option<bool>,
414
415 #[builder(into)]
418 silent: Option<bool>,
419
420 #[builder(into)]
423 vibrate: Option<Vec<u16>>,
424}
425
426#[cfg(not(feature = "ssr"))]
427impl ShowOptions {
428 fn override_notification_options(&self, options: &mut web_sys::NotificationOptions) {
429 if let Some(direction) = self.direction {
430 options.set_dir(direction.into());
431 }
432
433 if let Some(require_interaction) = self.require_interaction {
434 options.set_require_interaction(require_interaction);
435 }
436
437 if let Some(body) = &self.body {
438 options.set_body(body);
439 }
440
441 if let Some(icon) = &self.icon {
442 options.set_icon(icon);
443 }
444
445 if let Some(image) = &self.image {
446 options.set_image(image);
447 }
448
449 if let Some(language) = &self.language {
450 options.set_lang(language);
451 }
452
453 if let Some(tag) = &self.tag {
454 options.set_tag(tag);
455 }
456
457 if let Some(renotify) = self.renotify {
458 options.set_renotify(renotify);
459 }
460
461 if let Some(silent) = self.silent {
462 options.set_silent(Some(silent));
463 }
464
465 if let Some(vibrate) = &self.vibrate {
466 options.set_vibrate(&vibration_pattern_to_jsvalue(vibrate));
467 }
468 }
469}
470
471fn browser_supports_notifications() -> bool {
473 if let Some(window) = use_window().as_ref() {
474 if window.has_own_property(&wasm_bindgen::JsValue::from_str("Notification")) {
475 return true;
476 }
477 }
478
479 false
480}
481
482fn vibration_pattern_to_jsvalue(pattern: &[u16]) -> JsValue {
484 let array = js_sys::Array::new();
485 for &value in pattern.iter() {
486 array.push(&JsValue::from(value));
487 }
488 array.into()
489}
490
491#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
492pub enum NotificationPermission {
494 #[default]
496 Default,
497 Granted,
499 Denied,
501}
502
503impl From<web_sys::NotificationPermission> for NotificationPermission {
504 fn from(permission: web_sys::NotificationPermission) -> Self {
505 match permission {
506 web_sys::NotificationPermission::Default => Self::Default,
507 web_sys::NotificationPermission::Granted => Self::Granted,
508 web_sys::NotificationPermission::Denied => Self::Denied,
509 _ => Self::Default,
510 }
511 }
512}
513
514#[cfg(not(feature = "ssr"))]
518async fn request_web_notification_permission() -> NotificationPermission {
519 if let Ok(notification_permission) = web_sys::Notification::request_permission() {
520 let _ = crate::js_fut!(notification_permission).await;
521 }
522
523 web_sys::Notification::permission().into()
524}
525
526pub struct UseWebNotificationReturn<ShowFn, CloseFn>
528where
529 ShowFn: Fn(ShowOptions) + Clone + Send + Sync,
530 CloseFn: Fn() + Clone + Send + Sync,
531{
532 pub is_supported: Signal<bool>,
533 pub notification: Signal<Option<web_sys::Notification>, LocalStorage>,
534 pub show: ShowFn,
535 pub close: CloseFn,
536 pub permission: Signal<NotificationPermission>,
537}