feat(notification): add plugin (#326)

This commit is contained in:
Lucas Fernandes Nogueira
2023-05-02 09:09:50 -07:00
committed by GitHub
parent 864b9d790f
commit e9bbe94181
34 changed files with 1536 additions and 8 deletions
+54
View File
@@ -0,0 +1,54 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::Deserialize;
use tauri::{command, AppHandle, Runtime, State};
use crate::{Notification, PermissionState, Result};
/// The options for the notification API.
#[derive(Debug, Clone, Deserialize)]
pub struct NotificationOptions {
/// The notification title.
pub title: String,
/// The notification body.
pub body: Option<String>,
/// The notification icon.
pub icon: Option<String>,
}
#[command]
pub(crate) async fn is_permission_granted<R: Runtime>(
_app: AppHandle<R>,
notification: State<'_, Notification<R>>,
) -> Result<bool> {
notification
.permission_state()
.map(|s| s == PermissionState::Granted)
}
#[command]
pub(crate) async fn request_permission<R: Runtime>(
_app: AppHandle<R>,
notification: State<'_, Notification<R>>,
) -> Result<PermissionState> {
notification.request_permission()
}
#[command]
pub(crate) async fn notify<R: Runtime>(
_app: AppHandle<R>,
notification: State<'_, Notification<R>>,
options: NotificationOptions,
) -> Result<()> {
let mut builder = notification.builder().title(options.title);
if let Some(body) = options.body {
builder = builder.body(body);
}
if let Some(icon) = options.icon {
builder = builder.icon(icon);
}
builder.show()
}
+267
View File
@@ -0,0 +1,267 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::de::DeserializeOwned;
use tauri::{plugin::PluginApi, AppHandle, Runtime};
use crate::{models::*, NotificationBuilder};
pub fn init<R: Runtime, C: DeserializeOwned>(
app: &AppHandle<R>,
_api: PluginApi<R, C>,
) -> crate::Result<Notification<R>> {
Ok(Notification(app.clone()))
}
/// Access to the {{ plugin_name }} APIs.
pub struct Notification<R: Runtime>(AppHandle<R>);
impl<R: Runtime> crate::NotificationBuilder<R> {
pub fn show(self) -> crate::Result<()> {
let mut notification =
imp::Notification::new(self.app.config().tauri.bundle.identifier.clone());
if let Some(title) = self
.data
.title
.or_else(|| self.app.config().package.product_name.clone())
{
notification = notification.title(title);
}
if let Some(body) = self.data.body {
notification = notification.body(body);
}
if let Some(icon) = self.data.icon {
notification = notification.icon(icon);
}
#[cfg(feature = "windows7-compat")]
{
notification.notify(&self.app)?;
}
#[cfg(not(feature = "windows7-compat"))]
notification.show()?;
Ok(())
}
}
impl<R: Runtime> Notification<R> {
pub fn builder(&self) -> NotificationBuilder<R> {
NotificationBuilder::new(self.0.clone())
}
pub fn request_permission(&self) -> crate::Result<PermissionState> {
Ok(PermissionState::Granted)
}
pub fn permission_state(&self) -> crate::Result<PermissionState> {
Ok(PermissionState::Granted)
}
}
mod imp {
//! Types and functions related to desktop notifications.
#[cfg(windows)]
use std::path::MAIN_SEPARATOR as SEP;
/// The desktop notification definition.
///
/// Allows you to construct a Notification data and send it.
///
/// # Examples
/// ```rust,no_run
/// use tauri::api::notification::Notification;
/// // first we build the application to access the Tauri configuration
/// let app = tauri::Builder::default()
/// // on an actual app, remove the string argument
/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
/// .expect("error while building tauri application");
///
/// // shows a notification with the given title and body
/// Notification::new(&app.config().tauri.bundle.identifier)
/// .title("New message")
/// .body("You've got a new message.")
/// .show();
///
/// // run the app
/// app.run(|_app_handle, _event| {});
/// ```
#[allow(dead_code)]
#[derive(Debug, Default)]
pub struct Notification {
/// The notification body.
body: Option<String>,
/// The notification title.
title: Option<String>,
/// The notification icon.
icon: Option<String>,
/// The notification identifier
identifier: String,
}
impl Notification {
/// Initializes a instance of a Notification.
pub fn new(identifier: impl Into<String>) -> Self {
Self {
identifier: identifier.into(),
..Default::default()
}
}
/// Sets the notification body.
#[must_use]
pub fn body(mut self, body: impl Into<String>) -> Self {
self.body = Some(body.into());
self
}
/// Sets the notification title.
#[must_use]
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
/// Sets the notification icon.
#[must_use]
pub fn icon(mut self, icon: impl Into<String>) -> Self {
self.icon = Some(icon.into());
self
}
/// Shows the notification.
///
/// # Examples
///
/// ```no_run
/// use tauri::api::notification::Notification;
///
/// // on an actual app, remove the string argument
/// let context = tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json");
/// Notification::new(&context.config().tauri.bundle.identifier)
/// .title("Tauri")
/// .body("Tauri is awesome!")
/// .show()
/// .unwrap();
/// ```
///
/// ## Platform-specific
///
/// - **Windows**: Not supported on Windows 7. If your app targets it, enable the `windows7-compat` feature and use [`Self::notify`].
#[cfg_attr(
all(not(doc_cfg), feature = "windows7-compat"),
deprecated = "This function does not work on Windows 7. Use `Self::notify` instead."
)]
pub fn show(self) -> crate::Result<()> {
let mut notification = notify_rust::Notification::new();
if let Some(body) = self.body {
notification.body(&body);
}
if let Some(title) = self.title {
notification.summary(&title);
}
if let Some(icon) = self.icon {
notification.icon(&icon);
} else {
notification.auto_icon();
}
#[cfg(windows)]
{
let exe = tauri::utils::platform::current_exe()?;
let exe_dir = exe.parent().expect("failed to get exe directory");
let curr_dir = exe_dir.display().to_string();
// set the notification's System.AppUserModel.ID only when running the installed app
if !(curr_dir.ends_with(format!("{SEP}target{SEP}debug").as_str())
|| curr_dir.ends_with(format!("{SEP}target{SEP}release").as_str()))
{
notification.app_id(&self.identifier);
}
}
#[cfg(target_os = "macos")]
{
let _ = notify_rust::set_application(if cfg!(feature = "custom-protocol") {
&self.identifier
} else {
"com.apple.Terminal"
});
}
tauri::async_runtime::spawn(async move {
let _ = notification.show();
});
Ok(())
}
/// Shows the notification. This API is similar to [`Self::show`], but it also works on Windows 7.
///
/// # Examples
///
/// ```no_run
/// use tauri::api::notification::Notification;
///
/// // on an actual app, remove the string argument
/// let context = tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json");
/// let identifier = context.config().tauri.bundle.identifier.clone();
///
/// tauri::Builder::default()
/// .setup(move |app| {
/// Notification::new(&identifier)
/// .title("Tauri")
/// .body("Tauri is awesome!")
/// .notify(&app.handle())
/// .unwrap();
/// Ok(())
/// })
/// .run(context)
/// .expect("error while running tauri application");
/// ```
#[cfg(feature = "windows7-compat")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "windows7-compat")))]
#[allow(unused_variables)]
pub fn notify<R: tauri::Runtime>(self, app: &tauri::AppHandle<R>) -> crate::Result<()> {
#[cfg(windows)]
{
if tauri::utils::platform::is_windows_7() {
self.notify_win7(app)
} else {
#[allow(deprecated)]
self.show()
}
}
#[cfg(not(windows))]
{
#[allow(deprecated)]
self.show()
}
}
#[cfg(all(windows, feature = "windows7-compat"))]
fn notify_win7<R: tauri::Runtime>(self, app: &tauri::AppHandle<R>) -> crate::Result<()> {
let app = app.clone();
let default_window_icon = app.manager.inner.default_window_icon.clone();
let _ = app.run_on_main_thread(move || {
let mut notification = win7_notifications::Notification::new();
if let Some(body) = self.body {
notification.body(&body);
}
if let Some(title) = self.title {
notification.summary(&title);
}
if let Some(tauri::Icon::Rgba {
rgba,
width,
height,
}) = default_window_icon
{
notification.icon(rgba, width, height);
}
let _ = notification.show();
});
Ok(())
}
}
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::{ser::Serializer, Serialize};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[cfg(mobile)]
#[error(transparent)]
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
+71
View File
@@ -0,0 +1,71 @@
(function () {
let permissionSettable = false
let permissionValue = 'default'
function isPermissionGranted() {
if (window.Notification.permission !== 'default') {
return Promise.resolve(window.Notification.permission === 'granted')
}
return __TAURI__.invoke('plugin:notification|is_permission_granted')
}
function setNotificationPermission(value) {
permissionSettable = true
// @ts-expect-error we can actually set this value on the webview
window.Notification.permission = value
permissionSettable = false
}
function requestPermission() {
return __TAURI__.invoke('plugin:notification|request_permission')
.then(function (permission) {
setNotificationPermission(permission)
return permission
})
}
function sendNotification(options) {
if (typeof options === 'object') {
Object.freeze(options)
}
return __TAURI__.invoke('plugin:notification|notify', {
options: typeof options === 'string'
? {
title: options
}
: options
})
}
// @ts-expect-error unfortunately we can't implement the whole type, so we overwrite it with our own version
window.Notification = function (title, options) {
const opts = options || {}
sendNotification(
Object.assign(opts, { title })
)
}
window.Notification.requestPermission = requestPermission
Object.defineProperty(window.Notification, 'permission', {
enumerable: true,
get: function () {
return permissionValue
},
set: function (v) {
if (!permissionSettable) {
throw new Error('Readonly property')
}
permissionValue = v
}
})
isPermissionGranted().then(function (response) {
if (response === null) {
setNotificationPermission('default')
} else {
setNotificationPermission(response ? 'granted' : 'denied')
}
})
})()
+118
View File
@@ -0,0 +1,118 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::Serialize;
#[cfg(mobile)]
use tauri::plugin::PluginHandle;
#[cfg(desktop)]
use tauri::AppHandle;
use tauri::{
plugin::{Builder, TauriPlugin},
Manager, Runtime,
};
pub use models::*;
#[cfg(desktop)]
mod desktop;
#[cfg(mobile)]
mod mobile;
mod commands;
mod error;
mod models;
pub use error::{Error, Result};
#[cfg(desktop)]
use desktop::Notification;
#[cfg(mobile)]
use mobile::Notification;
#[derive(Debug, Default, Serialize)]
struct NotificationData {
/// The notification title.
title: Option<String>,
/// The notification body.
body: Option<String>,
/// The notification icon.
icon: Option<String>,
}
/// The notification builder.
#[derive(Debug)]
pub struct NotificationBuilder<R: Runtime> {
#[cfg(desktop)]
app: AppHandle<R>,
#[cfg(mobile)]
handle: PluginHandle<R>,
data: NotificationData,
}
impl<R: Runtime> NotificationBuilder<R> {
#[cfg(desktop)]
fn new(app: AppHandle<R>) -> Self {
Self {
app,
data: Default::default(),
}
}
#[cfg(mobile)]
fn new(handle: PluginHandle<R>) -> Self {
Self {
handle,
data: Default::default(),
}
}
/// Sets the notification title.
pub fn title(mut self, title: impl Into<String>) -> Self {
self.data.title.replace(title.into());
self
}
/// Sets the notification body.
pub fn body(mut self, body: impl Into<String>) -> Self {
self.data.body.replace(body.into());
self
}
/// Sets the notification icon.
pub fn icon(mut self, icon: impl Into<String>) -> Self {
self.data.icon.replace(icon.into());
self
}
}
/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the notification APIs.
pub trait NotificationExt<R: Runtime> {
fn notification(&self) -> &Notification<R>;
}
impl<R: Runtime, T: Manager<R>> crate::NotificationExt<R> for T {
fn notification(&self) -> &Notification<R> {
self.state::<Notification<R>>().inner()
}
}
/// Initializes the plugin.
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("notification")
.invoke_handler(tauri::generate_handler![
commands::notify,
commands::request_permission,
commands::is_permission_granted
])
.js_init_script(include_str!("init.js").into())
.setup(|app, api| {
#[cfg(mobile)]
let notification = mobile::init(app, api)?;
#[cfg(desktop)]
let notification = desktop::init(app, api)?;
app.manage(notification);
Ok(())
})
.build()
}
+66
View File
@@ -0,0 +1,66 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::{de::DeserializeOwned, Deserialize};
use tauri::{
plugin::{PluginApi, PluginHandle},
AppHandle, Runtime,
};
use crate::models::*;
#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "app.tauri.notification";
#[cfg(target_os = "ios")]
tauri::ios_plugin_binding!(init_plugin_notification);
// initializes the Kotlin or Swift plugin classes
pub fn init<R: Runtime, C: DeserializeOwned>(
_app: &AppHandle<R>,
api: PluginApi<R, C>,
) -> crate::Result<Notification<R>> {
#[cfg(target_os = "android")]
let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "NotificationPlugin")?;
#[cfg(target_os = "ios")]
let handle = api.register_ios_plugin(init_plugin_notification)?;
Ok(Notification(handle))
}
impl<R: Runtime> crate::NotificationBuilder<R> {
pub fn show(self) -> crate::Result<()> {
self.handle
.run_mobile_plugin("notify", self.data)
.map_err(Into::into)
}
}
/// Access to the notification APIs.
pub struct Notification<R: Runtime>(PluginHandle<R>);
impl<R: Runtime> Notification<R> {
pub fn builder(&self) -> crate::NotificationBuilder<R> {
crate::NotificationBuilder::new(self.0.clone())
}
pub fn request_permission(&self) -> crate::Result<PermissionState> {
self.0
.run_mobile_plugin::<PermissionResponse>("requestPermission", ())
.map(|r| r.permission_state)
.map_err(Into::into)
}
pub fn permission_state(&self) -> crate::Result<PermissionState> {
self.0
.run_mobile_plugin::<PermissionResponse>("permissionState", ())
.map(|r| r.permission_state)
.map_err(Into::into)
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct PermissionResponse {
permission_state: PermissionState,
}
+48
View File
@@ -0,0 +1,48 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::fmt::Display;
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
/// Permission state.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PermissionState {
/// Permission access has been granted.
Granted,
/// Permission access has been denied.
Denied,
}
impl Display for PermissionState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Granted => write!(f, "granted"),
Self::Denied => write!(f, "denied"),
}
}
}
impl Serialize for PermissionState {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
impl<'de> Deserialize<'de> for PermissionState {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.to_lowercase().as_str() {
"granted" => Ok(Self::Granted),
"denied" => Ok(Self::Denied),
_ => Err(DeError::custom(format!("unknown permission state '{s}'"))),
}
}
}