leptos_use/
use_web_notification.rs

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
9/// Reactive [Notification API](https://developer.mozilla.org/en-US/docs/Web/API/Notification).
10///
11/// The Web Notification interface of the Notifications API is used to configure and display desktop notifications to the user.
12///
13/// ## Demo
14///
15/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_web_notification)
16///
17/// ## Usage
18///
19/// ```
20/// # use leptos::prelude::*;
21/// # use leptos_use::{use_web_notification_with_options, UseWebNotificationOptions, ShowOptions, UseWebNotificationReturn, NotificationDirection};
22/// #
23/// # #[component]
24/// # fn Demo() -> impl IntoView {
25/// let UseWebNotificationReturn {
26///     show,
27///     close,
28///     ..
29/// } = use_web_notification_with_options(
30///     UseWebNotificationOptions::default()
31///         .direction(NotificationDirection::Auto)
32///         .language("en")
33///         .renotify(true)
34///         .tag("test"),
35/// );
36///
37/// show(ShowOptions::default().title("Hello World from leptos-use"));
38/// #
39/// # view! { }
40/// # }
41/// ```
42///
43/// ## Server-Side Rendering
44///
45/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
46///
47/// This function is basically ignored on the server. You can safely call `show` but it will do nothing.
48pub 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
55/// Version of [`use_web_notification`] which takes an [`UseWebNotificationOptions`].
56pub 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                        &notification_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        // Use close() to remove a notification that is no longer relevant to to
186        // the user (e.g.the user already read the notification on the webpage).
187        // Most modern browsers dismiss notifications automatically after a few
188        // moments(around four seconds).
189        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                    // The tab has become visible so clear the now-stale Notification:
194                    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/// Options for [`use_web_notification_with_options`].
228/// See [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/API/notification) for more info.
229///
230/// The following implementations are missing:
231#[derive(DefaultBuilder, Clone)]
232#[cfg_attr(feature = "ssr", allow(dead_code))]
233pub struct UseWebNotificationOptions {
234    /// The title property of the Notification interface indicates
235    /// the title of the notification
236    #[builder(into)]
237    title: String,
238
239    /// The body string of the notification as specified in the constructor's
240    /// options parameter.
241    #[builder(into)]
242    body: Option<String>,
243
244    /// The text direction of the notification as specified in the constructor's
245    /// options parameter. Can be `LeftToRight`, `RightToLeft` or `Auto` (default).
246    /// See [`web_sys::NotificationDirection`] for more info.
247    direction: NotificationDirection,
248
249    /// The language code of the notification as specified in the constructor's
250    /// options parameter.
251    #[builder(into)]
252    language: Option<String>,
253
254    /// The ID of the notification(if any) as specified in the constructor's options
255    /// parameter.
256    #[builder(into)]
257    tag: Option<String>,
258
259    /// The URL of the image used as an icon of the notification as specified
260    /// in the constructor's options parameter.
261    #[builder(into)]
262    icon: Option<String>,
263
264    /// The URL of the image to be displayed as part of the notification as specified
265    /// in the constructor's options parameter.
266    #[builder(into)]
267    image: Option<String>,
268
269    /// A boolean value indicating that a notification should remain active until the
270    /// user clicks or dismisses it, rather than closing automatically.
271    require_interaction: bool,
272
273    /// A boolean value specifying whether the user should be notified after a new notification replaces an old one.
274    /// The default is `false`, which means they won't be notified. If `true`, then `tag` also must be set.
275    #[builder(into)]
276    renotify: bool,
277
278    /// A boolean value specifying whether the notification should be silent, regardless of the device settings.
279    /// The default is `null`, which means the notification is not silent. If `true`, then the notification will be silent.
280    #[builder(into)]
281    silent: Option<bool>,
282
283    /// A `Vec<u16>` specifying the vibration pattern in milliseconds for vibrating and not vibrating.
284    /// The last entry can be a vibration since it stops automatically after each period.
285    #[builder(into)]
286    vibrate: Option<Vec<u16>>,
287
288    /// Called when the user clicks on displayed `Notification`.
289    on_click: Rc<dyn Fn(web_sys::Event)>,
290
291    /// Called when the user closes a `Notification`.
292    on_close: Rc<dyn Fn(web_sys::Event)>,
293
294    /// Called when something goes wrong with a `Notification`
295    /// (in many cases an error preventing the notification from being displayed.)
296    on_error: Rc<dyn Fn(web_sys::Event)>,
297
298    /// Called when a `Notification` is displayed
299    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/// Options for [`UseWebNotificationReturn::show`].
361///
362/// This can be used to override options passed to [`use_web_notification`].
363/// See [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/API/notification) for more info.
364///
365/// The following implementations are missing:
366#[derive(DefaultBuilder, Default)]
367#[cfg_attr(feature = "ssr", allow(dead_code))]
368pub struct ShowOptions {
369    /// The title property of the Notification interface indicates
370    /// the title of the notification
371    #[builder(into)]
372    title: Option<String>,
373
374    /// The body string of the notification as specified in the constructor's
375    /// options parameter.
376    #[builder(into)]
377    body: Option<String>,
378
379    /// The text direction of the notification as specified in the constructor's
380    /// options parameter. Can be `LeftToRight`, `RightToLeft` or `Auto` (default).
381    /// See [`web_sys::NotificationDirection`] for more info.
382    #[builder(into)]
383    direction: Option<NotificationDirection>,
384
385    /// The language code of the notification as specified in the constructor's
386    /// options parameter.
387    #[builder(into)]
388    language: Option<String>,
389
390    /// The ID of the notification(if any) as specified in the constructor's options
391    /// parameter.
392    #[builder(into)]
393    tag: Option<String>,
394
395    /// The URL of the image used as an icon of the notification as specified
396    /// in the constructor's options parameter.
397    #[builder(into)]
398    icon: Option<String>,
399
400    /// The URL of the image to be displayed as part of the notification as specified
401    /// in the constructor's options parameter.
402    #[builder(into)]
403    image: Option<String>,
404
405    /// A boolean value indicating that a notification should remain active until the
406    /// user clicks or dismisses it, rather than closing automatically.
407    #[builder(into)]
408    require_interaction: Option<bool>,
409
410    /// A boolean value specifying whether the user should be notified after a new notification replaces an old one.
411    /// The default is `false`, which means they won't be notified. If `true`, then `tag` also must be set.
412    #[builder(into)]
413    renotify: Option<bool>,
414
415    /// A boolean value specifying whether the notification should be silent, regardless of the device settings.
416    /// The default is `null`, which means the notification is not silent. If `true`, then the notification will be silent.
417    #[builder(into)]
418    silent: Option<bool>,
419
420    /// A `Vec<u16>` specifying the vibration pattern in milliseconds for vibrating and not vibrating.
421    /// The last entry can be a vibration since it stops automatically after each period.
422    #[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
471/// Helper function to determine if browser supports notifications
472fn 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
482/// Helper function to convert a slice of `u16` into a `JsValue` array that represents a vibration pattern
483fn 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)]
492/// The permission to send notifications
493pub enum NotificationPermission {
494    /// Notification has not been requested. In effect this is the same as `Denied`.
495    #[default]
496    Default,
497    /// You are allowed to send notifications
498    Granted,
499    /// You are *not* allowed to send notifications
500    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/// Use `window.Notification.requestPosition()`. Returns a future that should be awaited
515/// at least once before using [`use_web_notification`] to make sure
516/// you have the permission to send notifications.
517#[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
526/// Return type for [`use_web_notification`].
527pub 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}