use_web_notification is now SSR-safe.

relates to #46
This commit is contained in:
Maccesch 2023-12-06 00:10:33 +00:00
parent a3dc18064f
commit 5f5779ed10
5 changed files with 119 additions and 96 deletions

View file

@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `use_scroll` now uses `try_get_untracked` in the debounced callback to avoid panics if the context has been destroyed - `use_scroll` now uses `try_get_untracked` in the debounced callback to avoid panics if the context has been destroyed
while the callback was waiting to be called. while the callback was waiting to be called.
- `use_idle` works properly now (no more idles too early). - `use_idle` works properly now (no more idles too early).
- `use_web_notification` doesn't panic on the server anymore.
## [0.8.2] - 2023-11-09 ## [0.8.2] - 2023-11-09

View file

@ -1,10 +1,8 @@
use crate::core::MaybeRwSignal; use crate::core::MaybeRwSignal;
use crate::use_window::use_window;
use cfg_if::cfg_if; use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::*; use leptos::*;
use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen::{JsCast, JsValue};
use web_sys::{DisplayMediaStreamConstraints, MediaStream};
/// Reactive [`mediaDevices.getDisplayMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia) streaming. /// Reactive [`mediaDevices.getDisplayMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia) streaming.
/// ///
@ -56,7 +54,7 @@ pub fn use_display_media_with_options(
let (enabled, set_enabled) = enabled.into_signal(); let (enabled, set_enabled) = enabled.into_signal();
let (stream, set_stream) = create_signal(None::<Result<MediaStream, JsValue>>); let (stream, set_stream) = create_signal(None::<Result<web_sys::MediaStream, JsValue>>);
let _start = move || async move { let _start = move || async move {
cfg_if! { if #[cfg(not(feature = "ssr"))] { cfg_if! { if #[cfg(not(feature = "ssr"))] {
@ -67,6 +65,8 @@ pub fn use_display_media_with_options(
let stream = create_media(audio).await; let stream = create_media(audio).await;
set_stream.update(|s| *s = Some(stream)); set_stream.update(|s| *s = Some(stream));
} else {
let _ = audio;
}} }}
}; };
@ -122,13 +122,15 @@ pub fn use_display_media_with_options(
} }
#[cfg(not(feature = "ssr"))] #[cfg(not(feature = "ssr"))]
async fn create_media(audio: bool) -> Result<MediaStream, JsValue> { async fn create_media(audio: bool) -> Result<web_sys::MediaStream, JsValue> {
use crate::use_window::use_window;
let media = use_window() let media = use_window()
.navigator() .navigator()
.ok_or_else(|| JsValue::from_str("Failed to access window.navigator")) .ok_or_else(|| JsValue::from_str("Failed to access window.navigator"))
.and_then(|n| n.media_devices())?; .and_then(|n| n.media_devices())?;
let mut constraints = DisplayMediaStreamConstraints::new(); let mut constraints = web_sys::DisplayMediaStreamConstraints::new();
if audio { if audio {
constraints.audio(&JsValue::from(true)); constraints.audio(&JsValue::from(true));
} }
@ -136,7 +138,7 @@ async fn create_media(audio: bool) -> Result<MediaStream, JsValue> {
let promise = media.get_display_media_with_constraints(&constraints)?; let promise = media.get_display_media_with_constraints(&constraints)?;
let res = wasm_bindgen_futures::JsFuture::from(promise).await?; let res = wasm_bindgen_futures::JsFuture::from(promise).await?;
Ok::<_, JsValue>(MediaStream::unchecked_from_js(res)) Ok::<_, JsValue>(web_sys::MediaStream::unchecked_from_js(res))
} }
// NOTE: there's no video value because it has to be `true`. Otherwise the stream would always resolve to an Error. // NOTE: there's no video value because it has to be `true`. Otherwise the stream would always resolve to an Error.
@ -172,7 +174,7 @@ where
/// Initially this is `None` until `start` resolved successfully. /// Initially this is `None` until `start` resolved successfully.
/// In case the stream couldn't be started, for example because the user didn't grant permission, /// In case the stream couldn't be started, for example because the user didn't grant permission,
/// this has the value `Some(Err(...))`. /// this has the value `Some(Err(...))`.
pub stream: Signal<Option<Result<MediaStream, JsValue>>>, pub stream: Signal<Option<Result<web_sys::MediaStream, JsValue>>>,
/// Starts the screen streaming. Triggers the ask for permission if not already granted. /// Starts the screen streaming. Triggers the ask for permission if not already granted.
pub start: StartFn, pub start: StartFn,

View file

@ -33,6 +33,10 @@ use crate::use_window;
/// # view! { } /// # view! { }
/// # } /// # }
/// ``` /// ```
///
/// ## Server-Side Rendering
///
/// This function does **not** support SSR. Call it inside a `create_effect`.
pub fn use_service_worker() -> UseServiceWorkerReturn<impl Fn() + Clone, impl Fn() + Clone> { pub fn use_service_worker() -> UseServiceWorkerReturn<impl Fn() + Clone, impl Fn() + Clone> {
use_service_worker_with_options(UseServiceWorkerOptions::default()) use_service_worker_with_options(UseServiceWorkerOptions::default())
} }

View file

@ -1,10 +1,8 @@
use crate::{use_event_listener, use_supported, use_window}; use crate::{use_supported, use_window};
use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::ev::visibilitychange;
use leptos::*; use leptos::*;
use std::rc::Rc; use std::rc::Rc;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;
/// Reactive [Notification API](https://developer.mozilla.org/en-US/docs/Web/API/Notification). /// Reactive [Notification API](https://developer.mozilla.org/en-US/docs/Web/API/Notification).
/// ///
@ -53,108 +51,122 @@ pub fn use_web_notification_with_options(
let (permission, set_permission) = create_signal(NotificationPermission::default()); let (permission, set_permission) = create_signal(NotificationPermission::default());
let on_click_closure = Closure::<dyn Fn(web_sys::Event)>::new({ cfg_if! { if #[cfg(feature = "ssr")] {
let on_click = Rc::clone(&options.on_click); let _ = options;
move |e: web_sys::Event| { let _ = set_notification;
on_click(e); let _ = set_permission;
}
})
.into_js_value();
let on_close_closure = Closure::<dyn Fn(web_sys::Event)>::new({ let show = move |_: ShowOptions| ();
let on_close = Rc::clone(&options.on_close); let close = move || ();
move |e: web_sys::Event| { } else {
on_close(e); use crate::use_event_listener;
} use leptos::ev::visibilitychange;
}) use wasm_bindgen::closure::Closure;
.into_js_value(); use wasm_bindgen::JsCast;
let on_error_closure = Closure::<dyn Fn(web_sys::Event)>::new({ let on_click_closure = Closure::<dyn Fn(web_sys::Event)>::new({
let on_error = Rc::clone(&options.on_error); let on_click = Rc::clone(&options.on_click);
move |e: web_sys::Event| { move |e: web_sys::Event| {
on_error(e); on_click(e);
}
})
.into_js_value();
let on_show_closure = Closure::<dyn Fn(web_sys::Event)>::new({
let on_show = Rc::clone(&options.on_show);
move |e: web_sys::Event| {
on_show(e);
}
})
.into_js_value();
let show = {
let options = options.clone();
let on_click_closure = on_click_closure.clone();
let on_close_closure = on_close_closure.clone();
let on_error_closure = on_error_closure.clone();
let on_show_closure = on_show_closure.clone();
move |options_override: ShowOptions| {
if !is_supported.get_untracked() {
return;
} }
})
.into_js_value();
let on_close_closure = Closure::<dyn Fn(web_sys::Event)>::new({
let on_close = Rc::clone(&options.on_close);
move |e: web_sys::Event| {
on_close(e);
}
})
.into_js_value();
let on_error_closure = Closure::<dyn Fn(web_sys::Event)>::new({
let on_error = Rc::clone(&options.on_error);
move |e: web_sys::Event| {
on_error(e);
}
})
.into_js_value();
let on_show_closure = Closure::<dyn Fn(web_sys::Event)>::new({
let on_show = Rc::clone(&options.on_show);
move |e: web_sys::Event| {
on_show(e);
}
})
.into_js_value();
let show = {
let options = options.clone(); let options = options.clone();
let on_click_closure = on_click_closure.clone(); let on_click_closure = on_click_closure.clone();
let on_close_closure = on_close_closure.clone(); let on_close_closure = on_close_closure.clone();
let on_error_closure = on_error_closure.clone(); let on_error_closure = on_error_closure.clone();
let on_show_closure = on_show_closure.clone(); let on_show_closure = on_show_closure.clone();
spawn_local(async move { move |options_override: ShowOptions| {
set_permission.set(request_web_notification_permission().await); if !is_supported.get_untracked() {
return;
}
let mut notification_options = web_sys::NotificationOptions::from(&options); let options = options.clone();
options_override.override_notification_options(&mut notification_options); let on_click_closure = on_click_closure.clone();
let on_close_closure = on_close_closure.clone();
let on_error_closure = on_error_closure.clone();
let on_show_closure = on_show_closure.clone();
let notification_value = web_sys::Notification::new_with_options( spawn_local(async move {
&options_override.title.unwrap_or(options.title), set_permission.set(request_web_notification_permission().await);
&notification_options,
)
.expect("Notification should be created");
notification_value.set_onclick(Some(on_click_closure.unchecked_ref())); let mut notification_options = web_sys::NotificationOptions::from(&options);
notification_value.set_onclose(Some(on_close_closure.unchecked_ref())); options_override.override_notification_options(&mut notification_options);
notification_value.set_onerror(Some(on_error_closure.unchecked_ref()));
notification_value.set_onshow(Some(on_show_closure.unchecked_ref()));
set_notification.set(Some(notification_value)); let notification_value = web_sys::Notification::new_with_options(
}); &options_override.title.unwrap_or(options.title),
} &notification_options,
}; )
.expect("Notification should be created");
let close = { notification_value.set_onclick(Some(on_click_closure.unchecked_ref()));
move || { notification_value.set_onclose(Some(on_close_closure.unchecked_ref()));
notification.with_untracked(|notification| { notification_value.set_onerror(Some(on_error_closure.unchecked_ref()));
if let Some(notification) = notification { notification_value.set_onshow(Some(on_show_closure.unchecked_ref()));
notification.close();
set_notification.set(Some(notification_value));
});
}
};
let close = {
move || {
notification.with_untracked(|notification| {
if let Some(notification) = notification {
notification.close();
}
});
set_notification.set(None);
}
};
spawn_local(async move {
set_permission.set(request_web_notification_permission().await);
});
on_cleanup(close);
// Use close() to remove a notification that is no longer relevant to to
// the user (e.g.the user already read the notification on the webpage).
// Most modern browsers dismiss notifications automatically after a few
// moments(around four seconds).
if is_supported.get_untracked() {
let _ = use_event_listener(document(), visibilitychange, move |e: web_sys::Event| {
e.prevent_default();
if document().visibility_state() == web_sys::VisibilityState::Visible {
// The tab has become visible so clear the now-stale Notification:
close()
} }
}); });
set_notification.set(None);
} }
}; }}
spawn_local(async move {
set_permission.set(request_web_notification_permission().await);
});
on_cleanup(close);
// Use close() to remove a notification that is no longer relevant to to
// the user (e.g.the user already read the notification on the webpage).
// Most modern browsers dismiss notifications automatically after a few
// moments(around four seconds).
if is_supported.get_untracked() {
let _ = use_event_listener(document(), visibilitychange, move |e: web_sys::Event| {
e.prevent_default();
if document().visibility_state() == web_sys::VisibilityState::Visible {
// The tab has become visible so clear the now-stale Notification:
close()
}
});
}
UseWebNotificationReturn { UseWebNotificationReturn {
is_supported, is_supported,
@ -192,6 +204,7 @@ impl From<NotificationDirection> for web_sys::NotificationDirection {
/// - `silent` /// - `silent`
/// - `image` /// - `image`
#[derive(DefaultBuilder, Clone)] #[derive(DefaultBuilder, Clone)]
#[cfg_attr(feature = "ssr", allow(dead_code))]
pub struct UseWebNotificationOptions { pub struct UseWebNotificationOptions {
/// The title property of the Notification interface indicates /// The title property of the Notification interface indicates
/// the title of the notification /// the title of the notification
@ -302,6 +315,7 @@ impl From<&UseWebNotificationOptions> for web_sys::NotificationOptions {
/// - `silent` /// - `silent`
/// - `image` /// - `image`
#[derive(DefaultBuilder, Default)] #[derive(DefaultBuilder, Default)]
#[cfg_attr(feature = "ssr", allow(dead_code))]
pub struct ShowOptions { pub struct ShowOptions {
/// The title property of the Notification interface indicates /// The title property of the Notification interface indicates
/// the title of the notification /// the title of the notification
@ -344,6 +358,7 @@ pub struct ShowOptions {
// renotify: Option<bool>, // renotify: Option<bool>,
} }
#[cfg(not(feature = "ssr"))]
impl ShowOptions { impl ShowOptions {
fn override_notification_options(&self, options: &mut web_sys::NotificationOptions) { fn override_notification_options(&self, options: &mut web_sys::NotificationOptions) {
if let Some(direction) = self.direction { if let Some(direction) = self.direction {
@ -413,6 +428,7 @@ impl From<web_sys::NotificationPermission> for NotificationPermission {
/// Use `window.Notification.requestPosition()`. Returns a future that should be awaited /// Use `window.Notification.requestPosition()`. Returns a future that should be awaited
/// at least once before using [`use_web_notification`] to make sure /// at least once before using [`use_web_notification`] to make sure
/// you have the permission to send notifications. /// you have the permission to send notifications.
#[cfg(not(feature = "ssr"))]
async fn request_web_notification_permission() -> NotificationPermission { async fn request_web_notification_permission() -> NotificationPermission {
if let Ok(notification_permission) = web_sys::Notification::request_permission() { if let Ok(notification_permission) = web_sys::Notification::request_permission() {
let _ = wasm_bindgen_futures::JsFuture::from(notification_permission).await; let _ = wasm_bindgen_futures::JsFuture::from(notification_permission).await;

View file

@ -485,7 +485,7 @@ pub struct UseWebSocketOptions {
/// If `false` you have to manually call the `open` function. /// If `false` you have to manually call the `open` function.
/// Defaults to `true`. /// Defaults to `true`.
immediate: bool, immediate: bool,
/// Sub protocols /// Sub protocols. See [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#protocols).
protocols: Option<Vec<String>>, protocols: Option<Vec<String>>,
} }