diff --git a/.changes/config.json b/.changes/config.json index 214ec4d9..5834d96b 100644 --- a/.changes/config.json +++ b/.changes/config.json @@ -128,6 +128,15 @@ "manager": "javascript-disabled" }, + "notification": { + "path": "./plugins/notification", + "manager": "rust-disabled" + }, + "notification-js": { + "path": "./plugins/notification", + "manager": "javascript-disabled" + }, + "persisted-scope": { "path": "./plugins/persisted-scope", "manager": "rust" diff --git a/Cargo.lock b/Cargo.lock index bb45602b..365ada67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1805,7 +1805,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb1a9325847aa46f1e96ffea37611b9d51fc4827e67f79e7de502a297560a67b" dependencies = [ "anyhow", - "heck", + "heck 0.4.1", "proc-macro-crate", "proc-macro-error", "proc-macro2", @@ -1959,6 +1959,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "heck" version = "0.4.1" @@ -2644,6 +2653,19 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mac-notification-sys" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e72d50edb17756489e79d52eb146927bec8eba9dd48faadf9ef08bca3791ad5" +dependencies = [ + "cc", + "dirs-next", + "objc-foundation", + "objc_id", + "time 0.3.20", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -2881,6 +2903,19 @@ dependencies = [ "serde", ] +[[package]] +name = "notify-rust" +version = "4.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfa211d18e360f08e36c364308f394b5eb23a6629150690e109a916dc6f610e" +dependencies = [ + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3415,7 +3450,7 @@ dependencies = [ "base64 0.21.0", "indexmap", "line-wrap", - "quick-xml", + "quick-xml 0.28.1", "serde", "time 0.3.20", ] @@ -3538,6 +3573,15 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-xml" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.28.1" @@ -4343,7 +4387,7 @@ checksum = "9966e64ae989e7e575b19d7265cb79d7fc3cbbdf179835cb0d716f294c2049c9" dependencies = [ "dotenvy", "either", - "heck", + "heck 0.4.1", "once_cell", "proc-macro2", "quote", @@ -4493,6 +4537,27 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7ac893c7d471c8a21f31cfe213ec4f6d9afeed25537c772e08ef3f005f8729e" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339f799d8b549e3744c7ac7feb216383e4005d94bdb22561b3ab8f3b808ae9fb" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "subtle" version = "2.4.1" @@ -4539,7 +4604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "555fc8147af6256f3931a36bb83ad0023240ce9cf2b319dec8236fd1f220b05f" dependencies = [ "cfg-expr", - "heck", + "heck 0.4.1", "pkg-config", "toml 0.7.3", "version-compare", @@ -4633,7 +4698,7 @@ dependencies = [ "glib", "glob", "gtk", - "heck", + "heck 0.4.1", "http", "ignore", "jni", @@ -4677,7 +4742,7 @@ dependencies = [ "anyhow", "cargo_toml", "filetime", - "heck", + "heck 0.4.1", "json-patch", "semver", "serde", @@ -4720,7 +4785,7 @@ version = "2.0.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3b596485d89003d2d7869469b2830e9a846de9ac2eecd69bc7c24890234aefc" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn 1.0.109", @@ -4856,6 +4921,20 @@ dependencies = [ "time 0.3.20", ] +[[package]] +name = "tauri-plugin-notification" +version = "0.1.0" +dependencies = [ + "log", + "notify-rust", + "serde", + "serde_json", + "tauri", + "tauri-build", + "thiserror", + "win7-notifications", +] + [[package]] name = "tauri-plugin-persisted-scope" version = "0.1.0" @@ -5049,7 +5128,7 @@ dependencies = [ "brotli", "ctor", "glob", - "heck", + "heck 0.4.1", "html5ever", "infer", "json-patch", @@ -5078,6 +5157,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "tauri-winrt-notification" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58de036c4d2e20717024de2a3c4bf56c301f07b21bc8ef9b57189fce06f1f3b" +dependencies = [ + "quick-xml 0.23.1", + "strum", + "windows 0.39.0", +] + [[package]] name = "tempfile" version = "3.5.0" @@ -5867,6 +5957,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "win7-notifications" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "210952d7163b9ed83a6fd9754ab2a101d14480f8491b5f1d6292771d88dbee70" +dependencies = [ + "once_cell", + "windows-sys 0.36.1", +] + [[package]] name = "winapi" version = "0.3.9" @@ -5920,6 +6020,19 @@ dependencies = [ "windows_x86_64_msvc 0.36.1", ] +[[package]] +name = "windows" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" +dependencies = [ + "windows_aarch64_msvc 0.39.0", + "windows_i686_gnu 0.39.0", + "windows_i686_msvc 0.39.0", + "windows_x86_64_gnu 0.39.0", + "windows_x86_64_msvc 0.39.0", +] + [[package]] name = "windows" version = "0.44.0" @@ -5978,6 +6091,19 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee78911e3f4ce32c1ad9d3c7b0bd95389662ad8d8f1a3155688fed70bd96e2b6" +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", +] + [[package]] name = "windows-sys" version = "0.42.0" @@ -6065,6 +6191,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[package]] +name = "windows_aarch64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -6083,6 +6215,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[package]] +name = "windows_i686_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -6101,6 +6239,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[package]] +name = "windows_i686_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -6119,6 +6263,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -6149,6 +6299,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "windows_x86_64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" diff --git a/plugins/notification/.gitignore b/plugins/notification/.gitignore new file mode 100644 index 00000000..1b0b469d --- /dev/null +++ b/plugins/notification/.gitignore @@ -0,0 +1 @@ +/.tauri diff --git a/plugins/notification/Cargo.toml b/plugins/notification/Cargo.toml new file mode 100644 index 00000000..57d5c149 --- /dev/null +++ b/plugins/notification/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "tauri-plugin-notification" +version = "0.1.0" +edition.workspace = true +authors.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +tauri-build.workspace = true + +[dependencies] +serde.workspace = true +serde_json.workspace = true +tauri.workspace = true +log.workspace = true +thiserror.workspace = true + +[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] +notify-rust = "4.5" + +[target."cfg(windows)".dependencies] +win7-notifications = { version = "0.3.1", optional = true } + +[features] +windows7-compat = [ "win7-notifications" ] diff --git a/plugins/notification/LICENSE.spdx b/plugins/notification/LICENSE.spdx new file mode 100644 index 00000000..cdd0df5a --- /dev/null +++ b/plugins/notification/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/notification/LICENSE_APACHE-2.0 b/plugins/notification/LICENSE_APACHE-2.0 new file mode 100644 index 00000000..4947287f --- /dev/null +++ b/plugins/notification/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/plugins/notification/LICENSE_MIT b/plugins/notification/LICENSE_MIT new file mode 100644 index 00000000..4d754725 --- /dev/null +++ b/plugins/notification/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/plugins/notification/README.md b/plugins/notification/README.md new file mode 100644 index 00000000..ed5545a2 --- /dev/null +++ b/plugins/notification/README.md @@ -0,0 +1,65 @@ +![{{plugin name}}](banner.jpg) + + + +## Install + +_This plugin requires a Rust version of at least **1.64**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] + = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +> Note: Since most JavaScript package managers are unable to install packages from git monorepos we provide read-only mirrors of each plugin. This makes installation option 2 more ergonomic to use. + +```sh +pnpm add +# or +npm add +# or +yarn add +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/main.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin() + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript + +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/notification/android/.gitignore b/plugins/notification/android/.gitignore new file mode 100644 index 00000000..c0f21ec2 --- /dev/null +++ b/plugins/notification/android/.gitignore @@ -0,0 +1,2 @@ +/build +/.tauri diff --git a/plugins/notification/android/build.gradle.kts b/plugins/notification/android/build.gradle.kts new file mode 100644 index 00000000..5fdedcf4 --- /dev/null +++ b/plugins/notification/android/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "app.tauri.notification" + compileSdk = 32 + + defaultConfig { + minSdk = 24 + targetSdk = 32 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.appcompat:appcompat:1.6.0") + implementation("com.google.android.material:material:1.7.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation(project(":tauri-android")) +} diff --git a/plugins/notification/android/proguard-rules.pro b/plugins/notification/android/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/plugins/notification/android/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/plugins/notification/android/settings.gradle b/plugins/notification/android/settings.gradle new file mode 100644 index 00000000..14a752e4 --- /dev/null +++ b/plugins/notification/android/settings.gradle @@ -0,0 +1,2 @@ +include ':tauri-android' +project(':tauri-android').projectDir = new File('./.tauri/tauri-api') diff --git a/plugins/notification/android/src/androidTest/java/ExampleInstrumentedTest.kt b/plugins/notification/android/src/androidTest/java/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..814a39af --- /dev/null +++ b/plugins/notification/android/src/androidTest/java/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package app.tauri.notification + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("app.tauri.notification", appContext.packageName) + } +} diff --git a/plugins/notification/android/src/main/AndroidManifest.xml b/plugins/notification/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..9a40236b --- /dev/null +++ b/plugins/notification/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/plugins/notification/android/src/main/java/NotificationPlugin.kt b/plugins/notification/android/src/main/java/NotificationPlugin.kt new file mode 100644 index 00000000..ab6c9df7 --- /dev/null +++ b/plugins/notification/android/src/main/java/NotificationPlugin.kt @@ -0,0 +1,31 @@ +package app.tauri.notification + +import android.app.Activity +import app.tauri.annotation.Command +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.JSObject +import app.tauri.plugin.Plugin +import app.tauri.plugin.Invoke + +@TauriPlugin +class NotificationPlugin(private val activity: Activity): Plugin(activity) { + @Command + fun requestPermission(invoke: Invoke) { + val ret = JSObject() + ret.put("permissionState", "granted") + invoke.resolve(ret) + } + + @Command + fun permissionState(invoke: Invoke) { + val ret = JSObject() + ret.put("permissionState", "granted") + invoke.resolve(ret) + } + + @Command + fun notify(invoke: Invoke) { + // TODO + invoke.resolve() + } +} diff --git a/plugins/notification/android/src/test/java/ExampleUnitTest.kt b/plugins/notification/android/src/test/java/ExampleUnitTest.kt new file mode 100644 index 00000000..46693a0f --- /dev/null +++ b/plugins/notification/android/src/test/java/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package app.tauri.notification + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/plugins/notification/build.rs b/plugins/notification/build.rs new file mode 100644 index 00000000..86ac3f0a --- /dev/null +++ b/plugins/notification/build.rs @@ -0,0 +1,12 @@ +use std::process::exit; + +fn main() { + if let Err(error) = tauri_build::mobile::PluginBuilder::new() + .android_path("android") + .ios_path("ios") + .run() + { + println!("{error:#}"); + exit(1); + } +} diff --git a/plugins/notification/guest-js/index.ts b/plugins/notification/guest-js/index.ts new file mode 100644 index 00000000..a480105b --- /dev/null +++ b/plugins/notification/guest-js/index.ts @@ -0,0 +1,113 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Send toast notifications (brief auto-expiring OS window element) to your user. + * Can also be used with the Notification Web API. + * + * This package is also accessible with `window.__TAURI__.notification` when [`build.withGlobalTauri`](https://tauri.app/v1/api/config/#buildconfig.withglobaltauri) in `tauri.conf.json` is set to `true`. + * + * The APIs must be added to [`tauri.allowlist.notification`](https://tauri.app/v1/api/config/#allowlistconfig.notification) in `tauri.conf.json`: + * ```json + * { + * "tauri": { + * "allowlist": { + * "notification": { + * "all": true // enable all notification APIs + * } + * } + * } + * } + * ``` + * It is recommended to allowlist only the APIs you use for optimal bundle size and security. + * @module + */ + +import { invoke } from '@tauri-apps/api/tauri' + +/** + * Options to send a notification. + * + * @since 1.0.0 + */ +interface Options { + /** Notification title. */ + title: string + /** Optional notification body. */ + body?: string + /** Optional notification icon. */ + icon?: string +} + +/** Possible permission values. */ +type Permission = 'granted' | 'denied' | 'default' + +/** + * Checks if the permission to send notifications is granted. + * @example + * ```typescript + * import { isPermissionGranted } from '@tauri-apps/api/notification'; + * const permissionGranted = await isPermissionGranted(); + * ``` + * + * @since 1.0.0 + */ +async function isPermissionGranted(): Promise { + if (window.Notification.permission !== 'default') { + return Promise.resolve(window.Notification.permission === 'granted') + } + return invoke('plugin:notification|is_permission_granted') +} + +/** + * Requests the permission to send notifications. + * @example + * ```typescript + * import { isPermissionGranted, requestPermission } from '@tauri-apps/api/notification'; + * let permissionGranted = await isPermissionGranted(); + * if (!permissionGranted) { + * const permission = await requestPermission(); + * permissionGranted = permission === 'granted'; + * } + * ``` + * + * @returns A promise resolving to whether the user granted the permission or not. + * + * @since 1.0.0 + */ +async function requestPermission(): Promise { + return window.Notification.requestPermission() +} + +/** + * Sends a notification to the user. + * @example + * ```typescript + * import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/api/notification'; + * let permissionGranted = await isPermissionGranted(); + * if (!permissionGranted) { + * const permission = await requestPermission(); + * permissionGranted = permission === 'granted'; + * } + * if (permissionGranted) { + * sendNotification('Tauri is awesome!'); + * sendNotification({ title: 'TAURI', body: 'Tauri is awesome!' }); + * } + * ``` + * + * @since 1.0.0 + */ +function sendNotification(options: Options | string): void { + if (typeof options === 'string') { + // eslint-disable-next-line no-new + new window.Notification(options) + } else { + // eslint-disable-next-line no-new + new window.Notification(options.title, options) + } +} + +export type { Options, Permission } + +export { sendNotification, requestPermission, isPermissionGranted } diff --git a/plugins/notification/ios/.gitignore b/plugins/notification/ios/.gitignore new file mode 100644 index 00000000..5922fdaa --- /dev/null +++ b/plugins/notification/ios/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved diff --git a/plugins/notification/ios/Package.swift b/plugins/notification/ios/Package.swift new file mode 100644 index 00000000..ff9991fa --- /dev/null +++ b/plugins/notification/ios/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "tauri-plugin-{{ plugin_name }}", + platforms: [ + .iOS(.v13), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "tauri-plugin-{{ plugin_name }}", + type: .static, + targets: ["tauri-plugin-{{ plugin_name }}"]), + ], + dependencies: [ + .package(name: "Tauri", path: "../.tauri/tauri-api") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "tauri-plugin-{{ plugin_name }}", + dependencies: [ + .byName(name: "Tauri") + ], + path: "Sources") + ] +) diff --git a/plugins/notification/ios/README.md b/plugins/notification/ios/README.md new file mode 100644 index 00000000..f4900bdd --- /dev/null +++ b/plugins/notification/ios/README.md @@ -0,0 +1,3 @@ +# Tauri Plugin {{ plugin_name_original }} + +A description of this package. diff --git a/plugins/notification/ios/Sources/NotificationPlugin.swift b/plugins/notification/ios/Sources/NotificationPlugin.swift new file mode 100644 index 00000000..3d520a92 --- /dev/null +++ b/plugins/notification/ios/Sources/NotificationPlugin.swift @@ -0,0 +1,24 @@ +import UIKit +import WebKit +import Tauri +import SwiftRs + +class NotificationPlugin: Plugin { + @objc public func requestPermission(_ invoke: Invoke) throws { + invoke.resolve(["permissionState": "granted"]) + } + + @objc public func permissionState(_ invoke: Invoke) throws { + invoke.resolve(["permissionState": "granted"]) + } + + @objc public func notify(_ invoke: Invoke) throws { + // TODO + invoke.resolve() + } +} + +@_cdecl("init_plugin_notification") +func initPlugin(name: SRString, webview: WKWebView?) { + Tauri.registerPlugin(webview: webview, name: name.toString(), plugin: NotificationPlugin()) +} diff --git a/plugins/notification/ios/Tests/PluginTests/PluginTests.swift b/plugins/notification/ios/Tests/PluginTests/PluginTests.swift new file mode 100644 index 00000000..4f8e9ace --- /dev/null +++ b/plugins/notification/ios/Tests/PluginTests/PluginTests.swift @@ -0,0 +1,8 @@ +import XCTest +@testable import ExamplePlugin + +final class ExamplePluginTests: XCTestCase { + func testExample() throws { + let plugin = ExamplePlugin() + } +} diff --git a/plugins/notification/package.json b/plugins/notification/package.json new file mode 100644 index 00000000..52d81e45 --- /dev/null +++ b/plugins/notification/package.json @@ -0,0 +1,32 @@ +{ + "name": "tauri-plugin-notification-api", + "version": "0.0.0", + "license": "MIT or APACHE-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "type": "module", + "browser": "dist-js/index.min.js", + "module": "dist-js/index.mjs", + "types": "dist-js/index.d.ts", + "exports": { + "import": "./dist-js/index.mjs", + "types": "./dist-js/index.d.ts", + "browser": "./dist-js/index.min.js" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "!dist-js/**/*.map", + "README.md", + "LICENSE" + ], + "devDependencies": { + "tslib": "^2.4.1" + }, + "dependencies": { + "@tauri-apps/api": "^1.2.0" + } +} diff --git a/plugins/notification/rollup.config.mjs b/plugins/notification/rollup.config.mjs new file mode 100644 index 00000000..6555e98b --- /dev/null +++ b/plugins/notification/rollup.config.mjs @@ -0,0 +1,11 @@ +import { readFileSync } from "fs"; + +import { createConfig } from "../../shared/rollup.config.mjs"; + +export default createConfig({ + input: "guest-js/index.ts", + pkg: JSON.parse( + readFileSync(new URL("./package.json", import.meta.url), "utf8") + ), + external: [/^@tauri-apps\/api/], +}); diff --git a/plugins/notification/src/commands.rs b/plugins/notification/src/commands.rs new file mode 100644 index 00000000..710235c1 --- /dev/null +++ b/plugins/notification/src/commands.rs @@ -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, + /// The notification icon. + pub icon: Option, +} + +#[command] +pub(crate) async fn is_permission_granted( + _app: AppHandle, + notification: State<'_, Notification>, +) -> Result { + notification + .permission_state() + .map(|s| s == PermissionState::Granted) +} + +#[command] +pub(crate) async fn request_permission( + _app: AppHandle, + notification: State<'_, Notification>, +) -> Result { + notification.request_permission() +} + +#[command] +pub(crate) async fn notify( + _app: AppHandle, + notification: State<'_, Notification>, + 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() +} diff --git a/plugins/notification/src/desktop.rs b/plugins/notification/src/desktop.rs new file mode 100644 index 00000000..47be71b5 --- /dev/null +++ b/plugins/notification/src/desktop.rs @@ -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( + app: &AppHandle, + _api: PluginApi, +) -> crate::Result> { + Ok(Notification(app.clone())) +} + +/// Access to the {{ plugin_name }} APIs. +pub struct Notification(AppHandle); + +impl crate::NotificationBuilder { + 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 Notification { + pub fn builder(&self) -> NotificationBuilder { + NotificationBuilder::new(self.0.clone()) + } + + pub fn request_permission(&self) -> crate::Result { + Ok(PermissionState::Granted) + } + + pub fn permission_state(&self) -> crate::Result { + 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, + /// The notification title. + title: Option, + /// The notification icon. + icon: Option, + /// The notification identifier + identifier: String, + } + + impl Notification { + /// Initializes a instance of a Notification. + pub fn new(identifier: impl Into) -> Self { + Self { + identifier: identifier.into(), + ..Default::default() + } + } + + /// Sets the notification body. + #[must_use] + pub fn body(mut self, body: impl Into) -> Self { + self.body = Some(body.into()); + self + } + + /// Sets the notification title. + #[must_use] + pub fn title(mut self, title: impl Into) -> Self { + self.title = Some(title.into()); + self + } + + /// Sets the notification icon. + #[must_use] + pub fn icon(mut self, icon: impl Into) -> 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(self, app: &tauri::AppHandle) -> 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(self, app: &tauri::AppHandle) -> 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(()) + } + } +} diff --git a/plugins/notification/src/error.rs b/plugins/notification/src/error.rs new file mode 100644 index 00000000..339e763b --- /dev/null +++ b/plugins/notification/src/error.rs @@ -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 = std::result::Result; + +#[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(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/plugins/notification/src/init.js b/plugins/notification/src/init.js new file mode 100644 index 00000000..105a7f9b --- /dev/null +++ b/plugins/notification/src/init.js @@ -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') + } + }) +})() diff --git a/plugins/notification/src/lib.rs b/plugins/notification/src/lib.rs new file mode 100644 index 00000000..cb63758a --- /dev/null +++ b/plugins/notification/src/lib.rs @@ -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, + /// The notification body. + body: Option, + /// The notification icon. + icon: Option, +} + +/// The notification builder. +#[derive(Debug)] +pub struct NotificationBuilder { + #[cfg(desktop)] + app: AppHandle, + #[cfg(mobile)] + handle: PluginHandle, + data: NotificationData, +} + +impl NotificationBuilder { + #[cfg(desktop)] + fn new(app: AppHandle) -> Self { + Self { + app, + data: Default::default(), + } + } + + #[cfg(mobile)] + fn new(handle: PluginHandle) -> Self { + Self { + handle, + data: Default::default(), + } + } + + /// Sets the notification title. + pub fn title(mut self, title: impl Into) -> Self { + self.data.title.replace(title.into()); + self + } + + /// Sets the notification body. + pub fn body(mut self, body: impl Into) -> Self { + self.data.body.replace(body.into()); + self + } + + /// Sets the notification icon. + pub fn icon(mut self, icon: impl Into) -> 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 { + fn notification(&self) -> &Notification; +} + +impl> crate::NotificationExt for T { + fn notification(&self) -> &Notification { + self.state::>().inner() + } +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin { + 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() +} diff --git a/plugins/notification/src/mobile.rs b/plugins/notification/src/mobile.rs new file mode 100644 index 00000000..abd196ed --- /dev/null +++ b/plugins/notification/src/mobile.rs @@ -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( + _app: &AppHandle, + api: PluginApi, +) -> crate::Result> { + #[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 crate::NotificationBuilder { + 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(PluginHandle); + +impl Notification { + pub fn builder(&self) -> crate::NotificationBuilder { + crate::NotificationBuilder::new(self.0.clone()) + } + + pub fn request_permission(&self) -> crate::Result { + self.0 + .run_mobile_plugin::("requestPermission", ()) + .map(|r| r.permission_state) + .map_err(Into::into) + } + + pub fn permission_state(&self) -> crate::Result { + self.0 + .run_mobile_plugin::("permissionState", ()) + .map(|r| r.permission_state) + .map_err(Into::into) + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct PermissionResponse { + permission_state: PermissionState, +} diff --git a/plugins/notification/src/models.rs b/plugins/notification/src/models.rs new file mode 100644 index 00000000..d1cf0e4b --- /dev/null +++ b/plugins/notification/src/models.rs @@ -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(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +impl<'de> Deserialize<'de> for PermissionState { + fn deserialize(deserializer: D) -> std::result::Result + 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}'"))), + } + } +} diff --git a/plugins/notification/tsconfig.json b/plugins/notification/tsconfig.json new file mode 100644 index 00000000..5098169a --- /dev/null +++ b/plugins/notification/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["guest-js/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8794a87..e9eba864 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -169,6 +169,16 @@ importers: specifier: ^2.5.0 version: 2.5.0 + plugins/notification: + dependencies: + '@tauri-apps/api': + specifier: ^1.2.0 + version: 1.2.0 + devDependencies: + tslib: + specifier: ^2.4.1 + version: 2.4.1 + plugins/positioner: dependencies: '@tauri-apps/api':