diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 096d7bf4..c77b5b5a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -806,6 +806,15 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -1786,6 +1795,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "interprocess" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb" +dependencies = [ + "cfg-if", + "libc", + "rustc_version", + "to_method", + "winapi", +] + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -2235,6 +2257,28 @@ dependencies = [ "objc_id", ] +[[package]] +name = "objc-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c71324e4180d0899963fc83d9d241ac39e699609fc1025a850aadac8257459" + +[[package]] +name = "objc2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666" + [[package]] name = "objc_exception" version = "0.1.2" @@ -2398,15 +2442,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "path_trav" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d097b5a2259048b00d3f1ed5ceb4017efe8be4dd336d10b8f09fadf007155d" -dependencies = [ - "substring", -] - [[package]] name = "pathdiff" version = "0.2.1" @@ -2712,13 +2747,15 @@ version = "0.0.0" dependencies = [ "cocoa 0.25.0", "directories", + "mac-notification-sys", "objc", - "path_trav", + "percent-encoding", "reqwest", "serde", "serde_json", "tauri", "tauri-build", + "tauri-plugin-deep-link", "thiserror", "tokio", ] @@ -3410,15 +3447,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "substring" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" -dependencies = [ - "autocfg", -] - [[package]] name = "syn" version = "1.0.109" @@ -3679,6 +3707,22 @@ dependencies = [ "tauri-utils", ] +[[package]] +name = "tauri-plugin-deep-link" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4536f5f6602e8fdfaa7b3b185076c2a0704f8eb7015f4e58461eb483ec3ed1f8" +dependencies = [ + "dirs", + "interprocess", + "log", + "objc2", + "once_cell", + "tauri-utils", + "windows-sys 0.48.0", + "winreg 0.50.0", +] + [[package]] name = "tauri-runtime" version = "0.14.2" @@ -3875,6 +3919,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "to_method" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8" + [[package]] name = "tokio" version = "1.36.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index cc7b5cee..b396dccf 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -13,12 +13,14 @@ tauri = { version = "1", features = [ "window-start-dragging", "shell-open", "no serde = { version = "1", features = ["derive"] } serde_json = "1" directories = "5.0.1" -path_trav = "2.0.0" -reqwest = { version = "0.11.25", features = ["blocking"] } +reqwest = "0.11.25" tokio = { version = "1.36.0", features = ["full"] } thiserror = "1.0.58" cocoa = "0.25.0" objc = "0.2.7" +percent-encoding = "2.3.1" +mac-notification-sys = "0.6.1" +tauri-plugin-deep-link = "0.1.2" [features] # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! diff --git a/src-tauri/src/download.rs b/src-tauri/src/download.rs new file mode 100644 index 00000000..1c2e70b5 --- /dev/null +++ b/src-tauri/src/download.rs @@ -0,0 +1,221 @@ +use directories::UserDirs; +use percent_encoding::percent_decode; +use serde::{Deserialize, Serialize}; +use std::cmp::min; +use std::time::Instant; +use tauri::plugin::{Builder, TauriPlugin}; +use tauri::{Runtime, Window}; +use thiserror::Error; +use tokio::fs::File; +use tokio::io::AsyncWriteExt; + +#[derive(Debug, Clone, serde::Serialize)] +struct DownloadProgress { + id: u64, + progress: usize, + total: usize, +} + +#[derive(Serialize, Deserialize, Debug, Error, PartialEq, Eq)] +pub enum DownloadError { + #[error("Could not obtain download directory")] + CouldNotObtainDirectory, + #[error("Packet is too small, missing bytes")] + CouldNotCreateFile, + #[error("Could not download file")] + DownloadError, +} + +#[tauri::command] +pub async fn download_file( + window: Window, + id: u64, + url: &str, + filename: &str, +) -> Result { + let mut filename = filename.to_string(); + // if no filename provided, use the last part of the url + if filename.is_empty() || filename == "undefined" { + let url_fragment = url.split('/').last().unwrap_or(""); + filename = percent_decode(url_fragment.as_ref()) + .decode_utf8_lossy() + .to_string(); + } + + filename = remove_path_traversal(&filename); + + if filename.is_empty() { + filename = "file".to_string(); + } + + // fetch the download directory + let user_dirs = UserDirs::new().ok_or_else(|| DownloadError::CouldNotObtainDirectory)?; + let download_dir = user_dirs + .download_dir() + .ok_or_else(|| DownloadError::CouldNotObtainDirectory)?; + let mut download_path = download_dir.join(&filename); + + // if the file already exists, add a number to the filename + let (pure_filename, filename_extension) = split_filename(&filename); + let mut i = 1; + while download_path.exists() { + download_path = download_dir.join(format!("{pure_filename} ({i}){filename_extension}")); + i += 1; + } + + let mut response = reqwest::get(url) + .await + .map_err(|_| DownloadError::DownloadError)?; + + let mut file = File::create(&download_path) + .await + .map_err(|_| DownloadError::CouldNotCreateFile)?; + + let total_size = response.content_length().unwrap_or(0) as usize; + let mut downloaded = 0; + let mut last_report = Instant::now(); + while let Some(chunk) = response + .chunk() + .await + .map_err(|_| DownloadError::DownloadError)? + { + file.write_all(&chunk) + .await + .map_err(|_| DownloadError::DownloadError)?; + + downloaded = min(downloaded + chunk.len(), total_size); + + if last_report.elapsed().as_millis() > 100 || downloaded == total_size { + last_report = Instant::now(); + window + .emit( + "download://progress", + DownloadProgress { + id, + progress: downloaded, + total: total_size, + }, + ) + .unwrap(); + } + } + + file.flush() + .await + .map_err(|_| DownloadError::DownloadError)?; + println!("Downloaded {}", download_path.to_string_lossy()); + Ok(download_path.to_string_lossy().to_string()) +} + +/// Splits file into pure filename and extension while conserving double file extensions (.tar.gz, .tar.bz2, .tar.xz) +fn split_filename(filename: &str) -> (String, String) { + const DOUBLE_FILE_EXTENSION: [&str; 3] = [".tar.gz", ".tar.bz2", ".tar.xz"]; + for extension in DOUBLE_FILE_EXTENSION.iter() { + if filename.ends_with(extension) { + let pure_filename = filename.strip_suffix(extension).unwrap_or(filename); + return (pure_filename.to_string(), extension.to_string()); + } + } + let extension = if filename.contains('.') { + filename + .split('.') + .last() + .map(|ext| format!(".{}", ext)) + .unwrap_or("".to_string()) + } else { + "".to_string() + }; + let pure_filename = filename.strip_suffix(&extension).unwrap_or(filename); + (pure_filename.to_string(), extension) +} + +pub fn init() -> TauriPlugin { + Builder::new("downloader") + .invoke_handler(tauri::generate_handler![download_file]) + .setup(|_app_handle| { + // setup plugin specific state here + //app_handle.manage(MyState::default()); + Ok(()) + }) + .build() +} + +fn remove_path_traversal(filename: &str) -> String { + // todo path traversal not secure yet + // can't use fs::canonicalize because it doesn't work with non-existing files + // many path traversal crates are based on fs::canonicalize, therefore they also can't be used + filename + .replace(|c| c < ' ', "") // remove control characters + .replace(['/', '\\', ':', '~', '@', '?', '[', ']'], "") // remove all path separators + .replace("..", "") // remove path traversal +} + +// Test +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_split_filename() { + assert_eq!( + split_filename("file.tar.gz"), + ("file".to_string(), ".tar.gz".to_string()) + ); + assert_eq!( + split_filename("file.tar.bz2"), + ("file".to_string(), ".tar.bz2".to_string()) + ); + assert_eq!( + split_filename("file.tar.xz"), + ("file".to_string(), ".tar.xz".to_string()) + ); + assert_eq!( + split_filename("file.txt"), + ("file".to_string(), ".txt".to_string()) + ); + assert_eq!(split_filename("file"), ("file".to_string(), "".to_string())); + + assert_eq!( + split_filename("file..."), + ("file..".to_string(), ".".to_string()) + ); + assert_eq!( + split_filename("file.tar.gz.tar.gz"), + ("file.tar.gz".to_string(), ".tar.gz".to_string()) + ); + } + + #[test] + fn test_path_traversal() { + assert_eq!(remove_path_traversal("file"), "file"); + assert_eq!(remove_path_traversal("file.txt"), "file.txt"); + assert_eq!(remove_path_traversal("file.tar.gz"), "file.tar.gz"); + assert_eq!(remove_path_traversal("file.tar.gz/"), "file.tar.gz"); + assert_eq!(remove_path_traversal("file.tar.gz\\"), "file.tar.gz"); + assert_eq!(remove_path_traversal("file.tar.gz:"), "file.tar.gz"); + assert_eq!(remove_path_traversal("file.tar.gz~"), "file.tar.gz"); + assert_eq!(remove_path_traversal("file.tar.gz@"), "file.tar.gz"); + assert_eq!(remove_path_traversal("file.tar.gz//"), "file.tar.gz"); + assert_eq!(remove_path_traversal("file.tar.gz\\\\"), "file.tar.gz"); + assert_eq!(remove_path_traversal("file.tar.gz::"), "file.tar.gz"); + assert_eq!(remove_path_traversal("file.tar.gz~~"), "file.tar.gz"); + assert_eq!(remove_path_traversal("file.tar.gz@@"), "file.tar.gz"); + assert_eq!(remove_path_traversal("file.tar.gz.."), "file.tar.gz"); + assert_eq!(remove_path_traversal("file.tar.gz...."), "file.tar.gz"); + assert_eq!(remove_path_traversal("file.tar.gz..\\.."), "file.tar.gz"); + assert_eq!( + remove_path_traversal("C:\\file.tar.gz..//.."), + "Cfile.tar.gz" + ); + assert_eq!(remove_path_traversal("~/file.tar.gz..:.."), "file.tar.gz"); + assert_eq!( + remove_path_traversal("../../../../file.tar.gz..~~.."), + "file.tar.gz" + ); + assert_eq!(remove_path_traversal("/file.tar.gz..@@.."), "file.tar.gz"); + assert_eq!(remove_path_traversal("/./."), ""); + assert_eq!(remove_path_traversal("/.../..."), ""); + assert_eq!(remove_path_traversal("\x00hi"), "hi"); + assert_eq!(remove_path_traversal("🤰🏽¨¬ø¡你好"), "🤰🏽¨¬ø¡你好"); + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 73b1c78f..b7be1269 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -5,115 +5,32 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -/************************************************************************** - * IMPORTS - * ************************************************************************* */ +mod download; +mod notifications; -use directories::UserDirs; -use path_trav::*; -use serde::{Deserialize, Serialize}; -use std::path::Path; use tauri::{Manager, WindowEvent}; -use thiserror::Error; -use tokio::fs::File; -use tokio::io::AsyncWriteExt; - -/************************************************************************** - * ENUMERATIONS - * ************************************************************************* */ - -#[derive(Serialize, Deserialize, Debug, Error, PartialEq, Eq)] -pub enum DownloadError { - #[error("Could not obtain download directory")] - CouldNotObtainDirectory, - #[error("Packet is too small, missing bytes")] - CouldNotCreateFile, - #[error("File name is dangerous, attempting path traversal")] - DangerousFileName, - #[error("Could not download file")] - DownloadError, -} - -/************************************************************************** - * COMMANDS - * ************************************************************************* */ - -#[tauri::command] -async fn download_file(url: &str, filename: &str) -> Result<(), DownloadError> { - // Acquire filename as path - let filename_path = Path::new(filename); - - // Acquire directories - let user_dirs = UserDirs::new().ok_or(DownloadError::CouldNotObtainDirectory)?; - let download_dir = user_dirs - .download_dir() - .ok_or(DownloadError::CouldNotObtainDirectory)?; - - // Security: assert that provided filename is not attempting to perform a \ - // path traversal. For instance, passing a filename '../dangerous.txt' \ - // to store files outside of the Downloads folder. - if download_dir.is_path_trav(filename_path) == Ok(true) { - return Err(DownloadError::DangerousFileName); - } - - // Generate download path - let download_path = download_dir.join(filename); - - // Download file - let mut response = reqwest::get(url) - .await - .map_err(|_| DownloadError::DownloadError)?; - - // Create file on filesystem - let mut file = File::create(download_path) - .await - .map_err(|_| DownloadError::CouldNotCreateFile)?; - - // Drain bytes from HTTP response to file - while let Some(chunk) = response - .chunk() - .await - .map_err(|_| DownloadError::DownloadError)? - { - file.write_all(&chunk) - .await - .map_err(|_| DownloadError::DownloadError)?; - } - - Ok(()) -} - -#[cfg(target_os = "macos")] -#[tauri::command] -fn set_badge_count(count: u32) { - // Reference: https://github.com/tauri-apps/tauri/issues/4489 - use cocoa::{appkit::NSApp, foundation::NSString}; - use objc::{msg_send, sel, sel_impl}; - - unsafe { - let label = if count == 0 { - cocoa::base::nil - } else { - NSString::alloc(cocoa::base::nil).init_str(&format!("{}", count)) - }; - - let dock_tile: cocoa::base::id = msg_send![NSApp(), dockTile]; - let _: cocoa::base::id = msg_send![dock_tile, setBadgeLabel: label]; - } -} - -#[tauri::command] -#[cfg(not(target_os = "macos"))] -fn set_badge_count(count: u32) { - println!("set_badge_count is not implemented for this platform"); -} - -/************************************************************************** - * MAIN - * ************************************************************************* */ fn main() { + // if there are problems with deep linking, check this + tauri_plugin_deep_link::prepare("prose"); tauri::Builder::default() + .setup(|app| { + let handle = app.handle(); + let _ = tauri_plugin_deep_link::register("xmpp", move |request| { + if let Some(window) = handle.get_window("main") { + // rather fail silently than crash the app + let _ = window.set_focus(); + let _ = window.emit("scheme-request-received", request); + } + }) + .unwrap(); + #[cfg(not(target_os = "macos"))] + if let Some(url) = std::env::args().nth(1) { + app.emit_all("scheme-request-received", url).unwrap(); + } + + Ok(()) + }) .on_window_event(|event| { if let WindowEvent::CloseRequested { api, .. } = event.event() { #[cfg(not(target_os = "macos"))] @@ -129,7 +46,8 @@ fn main() { api.prevent_close(); } }) - .invoke_handler(tauri::generate_handler![download_file, set_badge_count]) + .plugin(download::init()) + .plugin(notifications::init()) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/notifications.rs b/src-tauri/src/notifications.rs new file mode 100644 index 00000000..e310a9b7 --- /dev/null +++ b/src-tauri/src/notifications.rs @@ -0,0 +1,57 @@ +use mac_notification_sys::{Notification, Sound}; +use tauri::plugin::{Builder, TauriPlugin}; +use tauri::Runtime; + +#[cfg(target_os = "macos")] +#[tauri::command] +fn set_badge_count(count: u32) { + // Reference: https://github.com/tauri-apps/tauri/issues/4489 + use cocoa::{appkit::NSApp, foundation::NSString}; + use objc::{msg_send, sel, sel_impl}; + + unsafe { + let label = if count == 0 { + cocoa::base::nil + } else { + NSString::alloc(cocoa::base::nil).init_str(&format!("{}", count)) + }; + + let dock_tile: cocoa::base::id = msg_send![NSApp(), dockTile]; + let _: cocoa::base::id = msg_send![dock_tile, setBadgeLabel: label]; + } +} + +#[cfg(target_os = "macos")] +#[tauri::command] +fn send_notification(title: String, body: String) { + let _ = Notification::default() + .title(&title) + .message(&body) + .sound(Sound::Default) + .send(); +} + +#[cfg(not(target_os = "macos"))] +#[tauri::command] +fn send_notification(title: String, body: String) {} + +pub fn init() -> TauriPlugin { + init_notifications(); + Builder::new("notifications") + .invoke_handler(tauri::generate_handler![send_notification, set_badge_count]) + .build() +} + +#[cfg(target_os = "macos")] +pub fn init_notifications() { + let bundle = mac_notification_sys::get_bundle_identifier_or_default("prose"); + mac_notification_sys::set_application(&bundle).unwrap(); +} +#[cfg(not(target_os = "macos"))] +fn init_notifications() {} + +#[tauri::command] +#[cfg(not(target_os = "macos"))] +fn set_badge_count(count: u32) { + println!("set_badge_count is not implemented for this platform"); +} diff --git a/src/assemblies/inbox/InboxMessaging.vue b/src/assemblies/inbox/InboxMessaging.vue index 53aa617b..e1f1b6b0 100644 --- a/src/assemblies/inbox/InboxMessaging.vue +++ b/src/assemblies/inbox/InboxMessaging.vue @@ -1587,10 +1587,14 @@ export default { try { await UtilitiesRuntime.requestFileDownload( event.file.url, - event.file.name + event.file.name, + (actual, total) => { + let percent = Math.round(actual / total * 100) + console.log(`${percent}% done`) + } ); - BaseAlert.info("File saved", "The file has been downloaded"); + BaseAlert.success("File saved", "The file has been downloaded"); } catch (error) { this.$log.error( `Could not download file from message file view event at URL: ` + diff --git a/src/utilities/runtime.ts b/src/utilities/runtime.ts index 6322549c..fff0aac2 100644 --- a/src/utilities/runtime.ts +++ b/src/utilities/runtime.ts @@ -11,11 +11,7 @@ // NPM import { invoke as tauriInvoke } from "@tauri-apps/api"; import { open as tauriOpen } from "@tauri-apps/api/shell"; -import { - isPermissionGranted as tauriIsPermissionGranted, - requestPermission as tauriRequestPermission, - sendNotification as tauriSendNotification -} from "@tauri-apps/api/notification"; +import { appWindow as tauriAppWindow } from "@tauri-apps/api/window"; import FileDownloader from "js-file-downloader"; // PROJECT: COMMONS @@ -23,9 +19,13 @@ import CONFIG from "@/commons/config"; // PROJECT: UTILITIES import logger from "@/utilities/logger"; + +import BaseAlert from "@/components/base/BaseAlert.vue"; + import UtilitiesFile from "@/utilities/file"; import UtilitiesTitle from "@/utilities/title"; + /************************************************************************** * CONSTANTS * ************************************************************************* */ @@ -41,18 +41,39 @@ const NOTIFICATION_PERMISSIONS = { * RUNTIME * ************************************************************************* */ +interface ProgressPayload { + id: number; + progress: number; + total: number; +} + +type ProgressHandler = (progress: number, total: number) => void; + class UtilitiesRuntime { private readonly __isBrowser: boolean; private readonly __isApp: boolean; + private __download_progress_handlers: Map; constructor() { // Initialize markers - this.__isBrowser = platform === "web" ? true : false; - this.__isApp = !this.__isBrowser; + this.__isBrowser = platform === "web"; + this.__isApp = window.__TAURI__ !== undefined; + this.__download_progress_handlers = new Map(); + + tauriAppWindow.listen("download://progress", ({ payload }) => { + const handler = this.__download_progress_handlers.get(payload.id) + if (handler != null) { + handler(payload.progress, payload.total) + } + }); + + tauriAppWindow.listen("scheme-request-received", ({ payload }) => { + BaseAlert.info("opened", payload) + }); } async requestOpenUrl(url: string, target = "_blank"): Promise { - if (this.__isApp === true) { + if (this.__isApp) { // Request to open via Tauri API (application build) await tauriOpen(url); } else { @@ -64,73 +85,76 @@ class UtilitiesRuntime { } } + async requestFileDownload( url: string, - name: string | null = null + filename: string | null = null, + progressHandler?: ProgressHandler ): Promise { - // Generate download options - // Notice: attempt to extract file name from URL (if none given) - const downloadOptions = { - url, - filename: - name || UtilitiesFile.detectAttributesFromUrl(url).name || undefined - }; - - if (this.__isApp === true) { + // Tauri build + if (this.__isApp) { // Request to download file via Tauri API (application build) - await tauriInvoke("download_file", downloadOptions); + const ids = new Uint32Array(1); + window.crypto.getRandomValues(ids); + const id = ids[0]; + + if (progressHandler != undefined) { + this.__download_progress_handlers.set(id, progressHandler); + } + await tauriInvoke("plugin:downloader|download_file", { + id, + url, + filename, + }) + this.__download_progress_handlers.delete(id) } else { // Request to download file via browser APIs (Web build) - await new FileDownloader(downloadOptions); + await new FileDownloader({ + url + }); } } async requestNotificationSend(title: string, body: string): Promise { - const hasPermission = await this.requestNotificationPermission(); - - if (hasPermission === true) { - if (this.__isApp === true) { - // Request to show notification via Tauri API (application build) - tauriSendNotification({ - title, - body - }); - } else { + if (this.__isApp) { + // Request to show notification via Tauri API (application build) + await tauriInvoke("plugin:notifications|send_notification", { + title, + body + }) + } else { + const hasPermission = await this.requestNotificationPermission(); + if (hasPermission) { // Request to show notification via browser APIs (Web build) new Notification(title, { body }); + } else { + logger.warn( + "Not sending notification since permission is denied:", + title + ); } - } else { - logger.warn( - "Not sending notification since permission is denied:", - title - ); } } - async requestNotificationPermission(): Promise { - let hasPermission = false; - - if (this.__isApp === true) { - // Request to show notification via Tauri API (application build) - hasPermission = await tauriIsPermissionGranted(); + async setBadgeCount(count: number) { + if (this.__isApp) { + await tauriInvoke("plugin:notifications|set_badge_count", { + count + }) + } + } - if (hasPermission === false) { - hasPermission = - (await tauriRequestPermission()) === NOTIFICATION_PERMISSIONS.granted; - } - } else { - // Request to show notification via browser APIs (Web build) + async requestNotificationPermission(): Promise { + // Request to show notification via browser APIs (Web build) + let hasPermission = + Notification.permission === NOTIFICATION_PERMISSIONS.granted; + if ( + !hasPermission && + Notification.permission !== NOTIFICATION_PERMISSIONS.denied + ) { hasPermission = - Notification.permission === NOTIFICATION_PERMISSIONS.granted; - - if ( - hasPermission === false && - Notification.permission !== NOTIFICATION_PERMISSIONS.denied - ) { - hasPermission = - (await Notification.requestPermission()) === - NOTIFICATION_PERMISSIONS.granted; - } + (await Notification.requestPermission()) === + NOTIFICATION_PERMISSIONS.granted; } return hasPermission;