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<
47 impl Fn(ShowOptions) + Clone + Send + Sync,
48 impl Fn() + Clone + Send + Sync,
49> {
50 use_web_notification_with_options(UseWebNotificationOptions::default())
51}
52
53pub fn use_web_notification_with_options(
55 options: UseWebNotificationOptions,
56) -> UseWebNotificationReturn<
57 impl Fn(ShowOptions) + Clone + Send + Sync,
58 impl Fn() + Clone + Send + Sync,
59> {
60 let is_supported = use_supported(browser_supports_notifications);
61
62 let (notification, set_notification) = signal_local(None::<web_sys::Notification>);
63
64 let (permission, set_permission) = signal(NotificationPermission::default());
65
66 cfg_if! { if #[cfg(feature = "ssr")] {
67 let _ = options;
68 let _ = set_notification;
69 let _ = set_permission;
70
71 let show = move |_: ShowOptions| ();
72 let close = move || ();
73 } else {
74 use crate::use_event_listener;
75 use leptos::ev::visibilitychange;
76 use wasm_bindgen::closure::Closure;
77 use wasm_bindgen::JsCast;
78 use send_wrapper::SendWrapper;
79
80 let on_click_closure = Closure::<dyn Fn(web_sys::Event)>::new({
81 let on_click = Rc::clone(&options.on_click);
82 move |e: web_sys::Event| {
83 #[cfg(debug_assertions)]
84 let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
85
86 on_click(e);
87 }
88 })
89 .into_js_value();
90
91 let on_close_closure = Closure::<dyn Fn(web_sys::Event)>::new({
92 let on_close = Rc::clone(&options.on_close);
93 move |e: web_sys::Event| {
94 #[cfg(debug_assertions)]
95 let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
96
97 on_close(e);
98 }
99 })
100 .into_js_value();
101
102 let on_error_closure = Closure::<dyn Fn(web_sys::Event)>::new({
103 let on_error = Rc::clone(&options.on_error);
104 move |e: web_sys::Event| {
105 #[cfg(debug_assertions)]
106 let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
107
108 on_error(e);
109 }
110 })
111 .into_js_value();
112
113 let on_show_closure = Closure::<dyn Fn(web_sys::Event)>::new({
114 let on_show = Rc::clone(&options.on_show);
115 move |e: web_sys::Event| {
116 #[cfg(debug_assertions)]
117 let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
118
119 on_show(e);
120 }
121 })
122 .into_js_value();
123
124 let show = {
125 let options = options.clone();
126 let on_click_closure = on_click_closure.clone();
127 let on_close_closure = on_close_closure.clone();
128 let on_error_closure = on_error_closure.clone();
129 let on_show_closure = on_show_closure.clone();
130
131 let show = move |options_override: ShowOptions| {
132 if !is_supported.get_untracked() {
133 return;
134 }
135
136 let options = options.clone();
137 let on_click_closure = on_click_closure.clone();
138 let on_close_closure = on_close_closure.clone();
139 let on_error_closure = on_error_closure.clone();
140 let on_show_closure = on_show_closure.clone();
141
142 leptos::task::spawn_local(async move {
143 set_permission.set(request_web_notification_permission().await);
144
145 let mut notification_options = web_sys::NotificationOptions::from(&options);
146 options_override.override_notification_options(&mut notification_options);
147
148 let notification_value = web_sys::Notification::new_with_options(
149 &options_override.title.unwrap_or(options.title),
150 ¬ification_options,
151 )
152 .expect("Notification should be created");
153
154 notification_value.set_onclick(Some(on_click_closure.unchecked_ref()));
155 notification_value.set_onclose(Some(on_close_closure.unchecked_ref()));
156 notification_value.set_onerror(Some(on_error_closure.unchecked_ref()));
157 notification_value.set_onshow(Some(on_show_closure.unchecked_ref()));
158
159 set_notification.set(Some(notification_value));
160 });
161 };
162 let wrapped_show = SendWrapper::new(show);
163 move |options_override: ShowOptions| wrapped_show(options_override)
164 };
165
166 let close = {
167 move || {
168 notification.with_untracked(|notification| {
169 if let Some(notification) = notification {
170 notification.close();
171 }
172 });
173 set_notification.set(None);
174 }
175 };
176
177 leptos::task::spawn_local(async move {
178 set_permission.set(request_web_notification_permission().await);
179 });
180
181 on_cleanup(close);
182
183 if is_supported.get_untracked() {
188 let _ = use_event_listener(document(), visibilitychange, move |e: web_sys::Event| {
189 e.prevent_default();
190 if document().visibility_state() == web_sys::VisibilityState::Visible {
191 close()
193 }
194 });
195 }
196 }}
197
198 UseWebNotificationReturn {
199 is_supported,
200 notification: notification.into(),
201 show,
202 close,
203 permission: permission.into(),
204 }
205}
206
207#[derive(Default, Clone, Copy, Eq, PartialEq, Debug)]
208pub enum NotificationDirection {
209 #[default]
210 Auto,
211 LeftToRight,
212 RightToLeft,
213}
214
215impl From<NotificationDirection> for web_sys::NotificationDirection {
216 fn from(direction: NotificationDirection) -> Self {
217 match direction {
218 NotificationDirection::Auto => Self::Auto,
219 NotificationDirection::LeftToRight => Self::Ltr,
220 NotificationDirection::RightToLeft => Self::Rtl,
221 }
222 }
223}
224
225#[derive(DefaultBuilder, Clone)]
230#[cfg_attr(feature = "ssr", allow(dead_code))]
231pub struct UseWebNotificationOptions {
232 #[builder(into)]
235 title: String,
236
237 #[builder(into)]
240 body: Option<String>,
241
242 direction: NotificationDirection,
246
247 #[builder(into)]
250 language: Option<String>,
251
252 #[builder(into)]
255 tag: Option<String>,
256
257 #[builder(into)]
260 icon: Option<String>,
261
262 #[builder(into)]
265 image: Option<String>,
266
267 require_interaction: bool,
270
271 #[builder(into)]
274 renotify: bool,
275
276 #[builder(into)]
279 silent: Option<bool>,
280
281 #[builder(into)]
284 vibrate: Option<Vec<u16>>,
285
286 on_click: Rc<dyn Fn(web_sys::Event)>,
288
289 on_close: Rc<dyn Fn(web_sys::Event)>,
291
292 on_error: Rc<dyn Fn(web_sys::Event)>,
295
296 on_show: Rc<dyn Fn(web_sys::Event)>,
298}
299
300impl Default for UseWebNotificationOptions {
301 fn default() -> Self {
302 Self {
303 title: "".to_string(),
304 body: None,
305 direction: NotificationDirection::default(),
306 language: None,
307 tag: None,
308 icon: None,
309 image: None,
310 require_interaction: false,
311 renotify: false,
312 silent: None,
313 vibrate: None,
314 on_click: Rc::new(|_| {}),
315 on_close: Rc::new(|_| {}),
316 on_error: Rc::new(|_| {}),
317 on_show: Rc::new(|_| {}),
318 }
319 }
320}
321
322impl From<&UseWebNotificationOptions> for web_sys::NotificationOptions {
323 fn from(options: &UseWebNotificationOptions) -> Self {
324 let web_sys_options = Self::new();
325
326 web_sys_options.set_dir(options.direction.into());
327 web_sys_options.set_require_interaction(options.require_interaction);
328 web_sys_options.set_renotify(options.renotify);
329 web_sys_options.set_silent(options.silent);
330
331 if let Some(body) = &options.body {
332 web_sys_options.set_body(body);
333 }
334
335 if let Some(icon) = &options.icon {
336 web_sys_options.set_icon(icon);
337 }
338
339 if let Some(image) = &options.image {
340 web_sys_options.set_image(image);
341 }
342
343 if let Some(language) = &options.language {
344 web_sys_options.set_lang(language);
345 }
346
347 if let Some(tag) = &options.tag {
348 web_sys_options.set_tag(tag);
349 }
350
351 if let Some(vibrate) = &options.vibrate {
352 web_sys_options.set_vibrate(&vibration_pattern_to_jsvalue(vibrate));
353 }
354 web_sys_options
355 }
356}
357
358#[derive(DefaultBuilder, Default)]
365#[cfg_attr(feature = "ssr", allow(dead_code))]
366pub struct ShowOptions {
367 #[builder(into)]
370 title: Option<String>,
371
372 #[builder(into)]
375 body: Option<String>,
376
377 #[builder(into)]
381 direction: Option<NotificationDirection>,
382
383 #[builder(into)]
386 language: Option<String>,
387
388 #[builder(into)]
391 tag: Option<String>,
392
393 #[builder(into)]
396 icon: Option<String>,
397
398 #[builder(into)]
401 image: Option<String>,
402
403 #[builder(into)]
406 require_interaction: Option<bool>,
407
408 #[builder(into)]
411 renotify: Option<bool>,
412
413 #[builder(into)]
416 silent: Option<bool>,
417
418 #[builder(into)]
421 vibrate: Option<Vec<u16>>,
422}
423
424#[cfg(not(feature = "ssr"))]
425impl ShowOptions {
426 fn override_notification_options(&self, options: &mut web_sys::NotificationOptions) {
427 if let Some(direction) = self.direction {
428 options.set_dir(direction.into());
429 }
430
431 if let Some(require_interaction) = self.require_interaction {
432 options.set_require_interaction(require_interaction);
433 }
434
435 if let Some(body) = &self.body {
436 options.set_body(body);
437 }
438
439 if let Some(icon) = &self.icon {
440 options.set_icon(icon);
441 }
442
443 if let Some(image) = &self.image {
444 options.set_image(image);
445 }
446
447 if let Some(language) = &self.language {
448 options.set_lang(language);
449 }
450
451 if let Some(tag) = &self.tag {
452 options.set_tag(tag);
453 }
454
455 if let Some(renotify) = self.renotify {
456 options.set_renotify(renotify);
457 }
458
459 if let Some(silent) = self.silent {
460 options.set_silent(Some(silent));
461 }
462
463 if let Some(vibrate) = &self.vibrate {
464 options.set_vibrate(&vibration_pattern_to_jsvalue(vibrate));
465 }
466 }
467}
468
469fn browser_supports_notifications() -> bool {
471 if let Some(window) = use_window().as_ref() {
472 if window.has_own_property(&wasm_bindgen::JsValue::from_str("Notification")) {
473 return true;
474 }
475 }
476
477 false
478}
479
480fn vibration_pattern_to_jsvalue(pattern: &[u16]) -> JsValue {
482 let array = js_sys::Array::new();
483 for &value in pattern.iter() {
484 array.push(&JsValue::from(value));
485 }
486 array.into()
487}
488
489#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
490pub enum NotificationPermission {
492 #[default]
494 Default,
495 Granted,
497 Denied,
499}
500
501impl From<web_sys::NotificationPermission> for NotificationPermission {
502 fn from(permission: web_sys::NotificationPermission) -> Self {
503 match permission {
504 web_sys::NotificationPermission::Default => Self::Default,
505 web_sys::NotificationPermission::Granted => Self::Granted,
506 web_sys::NotificationPermission::Denied => Self::Denied,
507 _ => Self::Default,
508 }
509 }
510}
511
512#[cfg(not(feature = "ssr"))]
516async fn request_web_notification_permission() -> NotificationPermission {
517 if let Ok(notification_permission) = web_sys::Notification::request_permission() {
518 let _ = crate::js_fut!(notification_permission).await;
519 }
520
521 web_sys::Notification::permission().into()
522}
523
524pub struct UseWebNotificationReturn<ShowFn, CloseFn>
526where
527 ShowFn: Fn(ShowOptions) + Clone + Send + Sync,
528 CloseFn: Fn() + Clone + Send + Sync,
529{
530 pub is_supported: Signal<bool>,
531 pub notification: Signal<Option<web_sys::Notification>, LocalStorage>,
532 pub show: ShowFn,
533 pub close: CloseFn,
534 pub permission: Signal<NotificationPermission>,
535}