use wasm_bindgen::closure::Closure;
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use web_sys::{Notification, NotificationOptions, NotificationPermission};
use crate::notification::{NotificationError, NotificationResponse, SystemNotification};
pub(crate) async fn show(
notification: &SystemNotification,
) -> Result<NotificationResponse, NotificationError> {
if !js_sys::Reflect::has(&js_sys::global(), &JsValue::from_str("Notification")).unwrap_or(false)
{
return Err(failed("this browser has no notifications"));
}
allowed().await?;
if !notification.buttons.is_empty() || notification.image.is_some() {
log::debug!(
"notification {:?}: a page's notification has no buttons and no picture; shown \
without them",
notification.title
);
}
let options = NotificationOptions::new();
if !notification.body.is_empty() {
options.set_body(¬ification.body);
}
options.set_silent(Some(notification.silent));
if let Some(icon) = page_icon() {
options.set_icon(&icon);
}
let shown = Notification::new_with_options(¬ification.title, &options)
.map_err(|err| failed(&describe(&err)))?;
let ended = js_sys::Promise::new(&mut |resolve, _reject| {
let answer = |resolve: js_sys::Function, what: &'static str| {
move || {
let _ = resolve.call1(&JsValue::NULL, &JsValue::from_str(what));
}
};
let clicked = {
let shown = shown.clone();
let answer = answer(resolve.clone(), "clicked");
Closure::once_into_js(move || {
if let Some(window) = web_sys::window() {
let _ = window.focus();
}
shown.close();
answer();
})
};
shown.set_onclick(Some(clicked.unchecked_ref()));
let closed = Closure::once_into_js(answer(resolve.clone(), "closed"));
shown.set_onclose(Some(closed.unchecked_ref()));
let refused = Closure::once_into_js(answer(resolve, "error"));
shown.set_onerror(Some(refused.unchecked_ref()));
});
let what = JsFuture::from(ended)
.await
.ok()
.and_then(|value| value.as_string());
match what.as_deref() {
Some("clicked") => Ok(NotificationResponse::Clicked),
Some("closed") => Ok(NotificationResponse::Closed),
_ => Err(failed("the browser could not show it")),
}
}
async fn allowed() -> Result<(), NotificationError> {
match Notification::permission() {
NotificationPermission::Granted => return Ok(()),
NotificationPermission::Denied => {
return Err(failed("notifications are blocked for this site"))
}
_ => {}
}
let asked = Notification::request_permission().map_err(|err| failed(&describe(&err)))?;
let answer = JsFuture::from(asked)
.await
.map_err(|err| failed(&describe(&err)))?;
match answer.as_string().as_deref() {
Some("granted") => Ok(()),
Some("denied") => Err(failed("the person did not allow notifications")),
_ => Err(failed(
"the person did not answer whether notifications are allowed; some browsers only \
ask after a click of theirs",
)),
}
}
fn page_icon() -> Option<String> {
web_sys::window()?
.document()?
.query_selector("link[rel~='icon']")
.ok()
.flatten()?
.get_attribute("href")
}
fn describe(err: &JsValue) -> String {
err.as_string()
.or_else(|| {
js_sys::Reflect::get(err, &JsValue::from_str("message"))
.ok()
.and_then(|message| message.as_string())
})
.unwrap_or_else(|| format!("{err:?}"))
}
fn failed(why: &str) -> NotificationError {
NotificationError(format!("the notification could not be shown: {why}"))
}