diff --git a/Cargo.toml b/Cargo.toml index bdc9e780..de40a5d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,10 +10,11 @@ version = "0.1.0" [dependencies] gloo-timers = { version = "0.1.0", path = "crates/timers" } gloo-console-timer = { version = "0.1.0", path = "crates/console-timer" } +gloo-notifications = { version = "0.1.0", path = "crates/notifications" } gloo-events = { version = "0.1.0", path = "crates/events" } [features] default = [] -futures = ["gloo-timers/futures"] +futures = ["gloo-timers/futures", "gloo-notifications/futures"] [workspace] diff --git a/crates/notifications/Cargo.toml b/crates/notifications/Cargo.toml new file mode 100644 index 00000000..8627c896 --- /dev/null +++ b/crates/notifications/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "gloo-notifications" +version = "0.1.0" +authors = ["Rust and WebAssembly Working Group"] +edition = "2018" + +[dependencies] +wasm-bindgen = "0.2.40" +js-sys = "0.3.17" + +[dependencies.futures_rs] +package = "futures" +version = "0.1.25" +optional = true + +[dependencies.wasm-bindgen-futures] +version = "0.3.17" +optional = true + +[dependencies.web-sys] +version = "0.3.17" +features = [ + "Notification", + "NotificationOptions", + "NotificationDirection", + "NotificationPermission", + "GetNotificationOptions", +] + +[features] +default = [] +futures = ["futures_rs", "wasm-bindgen-futures"] + + +[dev-dependencies] +wasm-bindgen-test = "0.2.40" diff --git a/crates/notifications/src/builder.rs b/crates/notifications/src/builder.rs new file mode 100644 index 00000000..172c0f25 --- /dev/null +++ b/crates/notifications/src/builder.rs @@ -0,0 +1,115 @@ +use crate::Notification; +use wasm_bindgen::JsValue; +use web_sys::{NotificationDirection, NotificationOptions}; + +/// A builder for a `Notification`. +/// +/// The builder is turned into a `Notification` by calling `.show()`, +/// which displays the notifcation on the screen. +/// +/// Example: +/// +/// ```rust +/// use gloo_notifications::Notification; +/// +/// Notification::request_permission() +/// .map(|mut builder| { +/// let _notification = builder +/// .title("Notification title") +/// .body("Notification body") +/// .show(); +/// }) +/// ``` +#[derive(Debug)] +pub struct NotificationBuilder<'a> { + title: &'a str, + sys_builder: NotificationOptions, +} + +impl<'a> NotificationBuilder<'a> { + #[inline] + pub(crate) fn new() -> Self { + NotificationBuilder { + title: "", + sys_builder: NotificationOptions::new(), + } + } + + #[inline] + pub(crate) fn get_inner(&self) -> (&str, &NotificationOptions) { + (self.title, &self.sys_builder) + } + + /// Sets the title of the notification + #[inline] + #[must_use = "You have to call .show() to display the notification"] + pub fn title(&mut self, title: &'a str) -> &mut Self { + self.title = title; + self + } + + /// Sets the body text of the notification + #[inline] + #[must_use = "You have to call .show() to display the notification"] + pub fn body(&mut self, body: &str) -> &mut Self { + self.sys_builder.body(body); + self + } + + /// Sets the data of the notification + #[inline] + #[must_use = "You have to call .show() to display the notification"] + pub fn data(&mut self, val: &JsValue) -> &mut Self { + self.sys_builder.data(val); + self + } + + /// Sets the direction of the notification, which is either Auto, Ltr or Rtl. + #[inline] + #[must_use = "You have to call .show() to display the notification"] + pub fn dir(&mut self, dir: NotificationDirection) -> &mut Self { + self.sys_builder.dir(dir); + self + } + + /// Sets the icon of the notification + #[inline] + #[must_use = "You have to call .show() to display the notification"] + pub fn icon(&mut self, val: &str) -> &mut Self { + self.sys_builder.icon(val); + self + } + + /// Sets the language of the notification + #[inline] + #[must_use = "You have to call .show() to display the notification"] + pub fn lang(&mut self, val: &str) -> &mut Self { + self.sys_builder.lang(val); + self + } + + /// Sets the requireInteraction property. + /// + /// If set to `true`, the notification stays visible until the user activates or closes it. + #[inline] + #[must_use = "You have to call .show() to display the notification"] + pub fn require_interaction(&mut self, val: bool) -> &mut Self { + self.sys_builder.require_interaction(val); + self + } + + /// Sets the tag of the notification + #[inline] + #[must_use = "You have to call .show() to display the notification"] + pub fn tag(&mut self, val: &str) -> &mut Self { + self.sys_builder.tag(val); + self + } + + /// Returns a new Notification from this builder + /// and displays it in the browser, if the permission is granted + #[inline] + pub fn show(&self) -> Notification { + Notification::new(self) + } +} diff --git a/crates/notifications/src/future.rs b/crates/notifications/src/future.rs new file mode 100644 index 00000000..b3315ac2 --- /dev/null +++ b/crates/notifications/src/future.rs @@ -0,0 +1,34 @@ +//! This module provides the `Notification::request_permission()` function, +//! which returns a `futures_rs::Future`. + +extern crate futures_rs as futures; + +use futures::Future; +use wasm_bindgen::{JsValue, UnwrapThrowExt}; +use wasm_bindgen_futures::JsFuture; + +use crate::{Notification, NotificationBuilder}; + +impl Notification { + /// ```rust + /// use gloo_notifications::Notification; + /// + /// Notification::request_permission() + /// .map(|mut builder| { + /// let _notification = builder + /// .title("Hello World") + /// .show(); + /// }) + /// .map_err(|_| { + /// // in case the permission is denied + /// }) + /// ``` + /// + #[must_use = "futures do nothing unless polled"] + pub fn request_permission<'a>() -> impl Future, Error = JsValue> + { + let promise = web_sys::Notification::request_permission().unwrap_throw(); + + JsFuture::from(promise).map(|_| NotificationBuilder::new()) + } +} diff --git a/crates/notifications/src/lib.rs b/crates/notifications/src/lib.rs new file mode 100644 index 00000000..58c50072 --- /dev/null +++ b/crates/notifications/src/lib.rs @@ -0,0 +1,238 @@ +//! Displaying notifications on the web. +//! +//! This API comes in two flavors: A callback style and `Future`s API. +//! +//! Before a notification can be displayed, the user of the browser has to give his permission. +//! +//! ## 1. Callback style +//! +//! ```rust +//! use gloo_notifications::Notification; +//! +//! Notification::request_permission_map(|mut builder| { +//! builder.title("Notification title").show(); +//! }); +//! +//! Notification::request_permission_map_or(|mut builder| { +//! builder.title("Notification title") +//! .body("Notification body") +//! .show(); +//! }, |_| { +//! // in case the permission is denied +//! }); +//! +//! // short form, if you only need a title +//! Notification::request_permission_with_title("Notification title"); +//! ``` +//! +//! ## 2. `Future` API: +//! +//! ```rust +//! use gloo_notifications::Notification; +//! +//! Notification::request_permission() +//! .map(|mut builder| { +//! builder.title("Notification title").show(); +//! }) +//! .map_err(|_| { +//! // in case the permission is denied +//! }) +//! ``` +//! +//! ## Adding event listeners +//! +//! This part of the API is **unstable**! +//! +//! ```rust +//! use gloo_notifications::Notification; +//! +//! Notification::request_permission_map(|mut builder| { +//! let notification = builder +//! .title("Notification title") +//! .show(); +//! +//! notification.onclick(|_| { ... }); +//! }) +//! ``` +//! +//! ## Macro +//! +//! ```rust +//! use gloo_notifications::{Notification, notification}; +//! +//! // requests permission, then displays the notification +//! // and adds a "click" event listener +//! notification! { +//! title: "Hello World", +//! body: "Foo", +//! icon: "/assets/notification.png"; +//! onclick: |_| {} +//! } +//! ``` + +#![cfg_attr(feature = "futures", doc = "```no_run")] +#![cfg_attr(not(feature = "futures"), doc = "```ignore")] +#![deny(missing_docs, missing_debug_implementations)] + +#[cfg(feature = "futures")] +extern crate futures_rs as futures; + +use wasm_bindgen::{closure::Closure, JsCast, JsValue, UnwrapThrowExt}; +pub use web_sys::{NotificationDirection, NotificationOptions, NotificationPermission}; + +#[cfg(feature = "futures")] +pub mod future; + +mod builder; +pub use builder::NotificationBuilder; + +/// A notification. +/// +/// This struct can not be created directly, you have to request permission first. +#[repr(transparent)] +#[derive(Debug)] +pub struct Notification { + sys_notification: web_sys::Notification, +} + +/// Requests permission, then displays the notification if the permission was granted. +/// +/// ### Example +/// +/// ```rust +/// notification! { +/// title: "Hello World", +/// body: "Foo", +/// } +/// ``` +/// +/// The identifiers are the same as the setter methods of `NotificationBuilder`. +/// +/// The macro can also add a "click" event listener. Simply add a semicolon `;` after the +/// properties and add a `onclick` property: +/// +/// +/// ```rust +/// notification! { +/// title: "Hello World", +/// body: "Foo" ; +/// onclick: |_| { ... } +/// } +/// ``` +#[macro_export] +macro_rules! notification { + ( $($k:ident : $v:expr),* $(,)? ) => ( + Notification::request_permission_map(|mut builder| { + builder + $( .$k($v) )* + .show(); + }); + ); + ( $($k:ident : $v:expr),* ; onclick: $e:expr $(,)? ) => ( + Notification::request_permission_map(|mut builder| { + let o = builder + $( .$k($v) )* + .show(); + o.onclick(Some($e)); + }); + ); +} + +impl Notification { + fn new<'a>(builder: &'a NotificationBuilder) -> Notification { + let (title, sys_builder) = builder.get_inner(); + let sys_notification = + web_sys::Notification::new_with_options(title, sys_builder).unwrap_throw(); + Notification { sys_notification } + } + + fn with_title<'a>(title: &'a str) -> Notification { + let sys_builder = &NotificationOptions::new(); + let sys_notification = + web_sys::Notification::new_with_options(title, sys_builder).unwrap_throw(); + Notification { sys_notification } + } + + /// This returns the permission to display notifications, which is one of the following values: + /// + /// - `default`: The user has neither granted, nor denied his permission. + /// Calling `Notification::request_permission()` displays a dialog window. + /// - `granted`: You are allowed to display notifications. + /// Calling `Notification::request_permission()` succeeds immediately. + /// - `denied`: You are forbidden to display notifications. + /// Calling `Notification::request_permission()` fails immediately. + #[inline] + pub fn permission() -> NotificationPermission { + web_sys::Notification::permission() + } + + /// Requests permission to display notifications, and asynchronously calls `f` + /// with a new `NotificationBuilder`, when the permission is granted. + /// + /// If the permission is denied, nothing happens. + pub fn request_permission_map(mut f: F) + where + F: FnMut(NotificationBuilder) + 'static, + { + let resolve = Closure::once(move |_| f(NotificationBuilder::new())); + + web_sys::Notification::request_permission() + .unwrap_throw() + .then(&resolve); + } + + /// Requests permission to display notifications, and asynchronously calls + /// + /// - `f(NotificationBuilder)`, if the permission is granted + /// - `g()`, if the permission is denied + pub fn request_permission_map_or(mut f: Ok, g: Err) + where + Ok: FnMut(NotificationBuilder) + 'static, + Err: FnMut(JsValue) + 'static, + { + let resolve = Closure::once(move |_| f(NotificationBuilder::new())); + let reject = Closure::once(g); + + web_sys::Notification::request_permission() + .unwrap_throw() + .then2(&resolve, &reject); + } + + + /// Requests permission to display notifications, + /// and displays a notification with a title, if the permission is granted. + /// + /// If the permission is denied, nothing happens. + pub fn request_permission_with_title(title: &'static str) { + let resolve = Closure::once(move |_| { + Notification::with_title(title); + }); + + web_sys::Notification::request_permission() + .unwrap_throw() + .then(&resolve); + } + + + /// Sets the "click" event listener + pub fn onclick(&self, listener: Option) -> &Self + where + F: Fn(JsValue) + 'static, + { + match listener { + Some(f) => { + let boxed: Box = Box::new(f); + self.sys_notification + .set_onclick(Some(Closure::wrap(boxed).as_ref().unchecked_ref())); + } + None => self.sys_notification.set_onclick(None), + } + self + } + + /// Closes the notification + #[inline] + pub fn close(&self) { + self.sys_notification.close() + } +} diff --git a/src/lib.rs b/src/lib.rs index f6ccbe49..3344f8fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,5 +5,6 @@ // Re-exports of toolkit crates. pub use gloo_console_timer as console_timer; +pub use gloo_notifications as notifications; pub use gloo_events as events; pub use gloo_timers as timers;