diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1e988a8d9..49b0f5fde 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1603,6 +1603,16 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -3612,6 +3622,19 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mac-notification-sys" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce8f34f3717aa37177e723df6c1fc5fb02b2a1087374ea3fe0ea42316dc8f91" +dependencies = [ + "cc", + "dirs-next 2.0.0", + "objc-foundation", + "objc_id", + "time", +] + [[package]] name = "mach2" version = "0.4.2" @@ -4124,6 +4147,19 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify-rust" +version = "4.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5134a72dc570b178bff81b01e81ab14a6fcc015391ed4b3b14853090658cd3a3" +dependencies = [ + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + [[package]] name = "ntapi" version = "0.4.1" @@ -4241,6 +4277,17 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -4459,6 +4506,15 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "object" version = "0.36.5" @@ -4965,7 +5021,7 @@ checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", "indexmap 2.7.0", - "quick-xml", + "quick-xml 0.32.0", "serde", "time", ] @@ -5269,6 +5325,15 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.32.0" @@ -6777,6 +6842,7 @@ dependencies = [ "minotari_wallet_grpc_client", "monero-address-creator", "nix 0.29.0", + "notify-rust", "nvml-wrapper", "open", "openssl", @@ -6803,6 +6869,7 @@ dependencies = [ "tari_utilities", "tauri", "tauri-build", + "tauri-plugin-notification", "tauri-plugin-os", "tauri-plugin-process", "tauri-plugin-sentry", @@ -6845,7 +6912,7 @@ source = "git+https://github.com/tari-project/tari.git?branch=development#b12788 dependencies = [ "anyhow", "config", - "dirs-next", + "dirs-next 1.0.2", "log", "log4rs", "multiaddr", @@ -7008,7 +7075,7 @@ dependencies = [ "decimal-rs", "derivative", "digest", - "dirs-next", + "dirs-next 1.0.2", "fs2", "futures 0.3.31", "hex", @@ -7406,6 +7473,25 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-notification" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46ab803095f14ac6521fdb6477210a49e86fed6623c3c97d8e4b2b35e045e922" +dependencies = [ + "log", + "notify-rust", + "rand 0.8.5", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror 2.0.6", + "time", + "url", +] + [[package]] name = "tauri-plugin-os" version = "2.2.0" @@ -7608,6 +7694,17 @@ dependencies = [ "toml 0.7.8", ] +[[package]] +name = "tauri-winrt-notification" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89f5fb70d6f62381f5d9b2ba9008196150b40b75f3068eb24faeddf1c686871" +dependencies = [ + "quick-xml 0.31.0", + "windows 0.56.0", + "windows-version", +] + [[package]] name = "tempfile" version = "3.14.0" @@ -8735,6 +8832,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" +dependencies = [ + "windows-core 0.56.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.57.0" @@ -8764,6 +8871,18 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +dependencies = [ + "windows-implement 0.56.0", + "windows-interface 0.56.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.57.0" @@ -8789,6 +8908,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-implement" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "windows-implement" version = "0.57.0" @@ -8811,6 +8941,17 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "windows-interface" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "windows-interface" version = "0.57.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 466cb128b..e6bb991a6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -42,6 +42,7 @@ minotari_node_grpc_client = { git = "https://github.com/tari-project/tari.git", minotari_wallet_grpc_client = { git = "https://github.com/tari-project/tari.git", branch = "development" } monero-address-creator = { git = "https://github.com/tari-project/monero-address-creator.git", rev = "6129ca0" } nix = { version = "0.29.0", features = ["signal"] } +notify-rust = "4.11.3" nvml-wrapper = "0.10.0" open = "5" phraze = "0.3.15" @@ -87,6 +88,7 @@ xz2 = { version = "0.1.7", features = ["static"] } # static bind lzma zip = "2.2.0" dirs = "5.0.1" tauri-plugin-process = "2" +tauri-plugin-notification = "2" # temporary fix for openssl openssl = { version = "0.10", features = ["vendored"] } @@ -102,6 +104,7 @@ log = "0.4.22" nvml-wrapper = "0.10.0" rand = "0.8.5" sys-locale = "0.3.1" +notify-rust = "4.11.3" # tonic = "0.12.0" [features] diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 540160433..27f023c98 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -16,6 +16,12 @@ "shell:allow-open", "sentry:default", "process:allow-restart", - "process:default" + "process:default", + "notification:allow-show", + "notification:allow-request-permission", + "notification:allow-permission-state", + "notification:allow-notify", + "notification:allow-is-permission-granted", + "notification:default" ] } diff --git a/src-tauri/src/auto_launcher.rs b/src-tauri/src/auto_launcher.rs index e43185327..ee52f5bc0 100644 --- a/src-tauri/src/auto_launcher.rs +++ b/src-tauri/src/auto_launcher.rs @@ -29,16 +29,12 @@ use log::{info, warn}; use tauri::utils::platform::current_exe; use tokio::sync::RwLock; +use crate::utils::platform_utils::{CurrentOperatingSystem, PlatformUtils}; + const LOG_TARGET: &str = "tari::universe::auto_launcher"; static INSTANCE: LazyLock = LazyLock::new(AutoLauncher::new); -pub enum CurrentOperatingSystem { - Windows, - Linux, - MacOS, -} - pub struct AutoLauncher { auto_launcher: RwLock>, } @@ -50,22 +46,10 @@ impl AutoLauncher { } } - fn detect_current_os() -> CurrentOperatingSystem { - if cfg!(target_os = "windows") { - CurrentOperatingSystem::Windows - } else if cfg!(target_os = "linux") { - CurrentOperatingSystem::Linux - } else if cfg!(target_os = "macos") { - CurrentOperatingSystem::MacOS - } else { - panic!("Unsupported OS"); - } - } - fn build_auto_launcher(app_name: &str, app_path: &str) -> Result { info!(target: LOG_TARGET, "Building auto-launcher with app_name: {} and app_path: {}", app_name, app_path); - match AutoLauncher::detect_current_os() { + match PlatformUtils::detect_current_os() { CurrentOperatingSystem::Windows => { return AutoLaunchBuilder::new() .set_app_name(app_name) @@ -101,7 +85,7 @@ impl AutoLauncher { if config_is_auto_launcher_enabled && !is_auto_launcher_enabled { info!(target: LOG_TARGET, "Enabling auto-launcher"); - match AutoLauncher::detect_current_os() { + match PlatformUtils::detect_current_os() { CurrentOperatingSystem::MacOS => { // This for some reason fixes the issue where macOS starts two instances of the app // when auto-launcher is enabled and when during shutdown user selects to reopen the apps after restart diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index fc3f05ed3..de156be20 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -32,6 +32,7 @@ use crate::gpu_miner_adapter::{GpuMinerStatus, GpuNodeSource}; use crate::hardware::hardware_status_monitor::{HardwareStatusMonitor, PublicDeviceProperties}; use crate::internal_wallet::{InternalWallet, PaperWalletConfig}; use crate::node_manager::NodeManagerError; +use crate::notification_manager::NotificationManager; use crate::p2pool::models::{Connections, Stats}; use crate::progress_tracker::ProgressTracker; use crate::tor_adapter::TorConfig; @@ -44,6 +45,7 @@ use log::{debug, error, info, warn}; use monero_address_creator::Seed as MoneroSeed; use regex::Regex; use serde::Serialize; +use tauri_plugin_notification::NotificationExt; use std::fs::{read_dir, remove_dir_all, remove_file, File}; use std::sync::atomic::Ordering; use std::thread::{available_parallelism, sleep}; @@ -524,6 +526,27 @@ pub async fn get_p2pool_stats( Ok(p2pool_stats) } +#[tauri::command] +pub async fn trigger_notification( + summary: String, + body: String, + app: tauri::AppHandle, +) -> Result<(), String> { + let timer = Instant::now(); + // NotificationManager::current().trigger_notification(&summary, &body).map_err(|e| e.to_string())?; + let notification = app.notification().builder() + .title(summary) + .body(body) + .icon("assets/icons/icon.png"); + + notification.show().map_err(|e| e.to_string())?; + + if timer.elapsed() > MAX_ACCEPTABLE_COMMAND_TIME { + warn!(target: LOG_TARGET, "trigger_notification took too long: {:?}", timer.elapsed()); + } + Ok(()) +} + #[tauri::command] pub async fn get_p2pool_connections( state: tauri::State<'_, UniverseAppState>, diff --git a/src-tauri/src/hardware_monitor.rs b/src-tauri/src/hardware_monitor.rs index 4af6d432f..a5b7f9208 100644 --- a/src-tauri/src/hardware_monitor.rs +++ b/src-tauri/src/hardware_monitor.rs @@ -29,16 +29,12 @@ use serde::{Deserialize, Serialize}; use sysinfo::{Component, Components, CpuRefreshKind, RefreshKind, System}; use tokio::sync::RwLock; +use crate::utils::platform_utils::{CurrentOperatingSystem, PlatformUtils}; + const LOG_TARGET: &str = "tari::universe::hardware_monitor"; static INSTANCE: LazyLock> = LazyLock::new(|| RwLock::new(HardwareMonitor::new())); -enum CurrentOperatingSystem { - Windows, - Linux, - MacOS, -} - #[derive(Clone, Debug, Serialize)] pub struct HardwareParameters { pub label: String, @@ -103,8 +99,8 @@ pub struct HardwareMonitor { impl HardwareMonitor { pub fn new() -> Self { HardwareMonitor { - current_os: HardwareMonitor::detect_current_os(), - current_implementation: match HardwareMonitor::detect_current_os() { + current_os: PlatformUtils::detect_current_os(), + current_implementation: match PlatformUtils::detect_current_os() { CurrentOperatingSystem::Windows => Box::new(WindowsHardwareMonitor { nvml: HardwareMonitor::initialize_nvml(), gpu_status_file: None, @@ -141,18 +137,6 @@ impl HardwareMonitor { } } - fn detect_current_os() -> CurrentOperatingSystem { - if cfg!(target_os = "windows") { - CurrentOperatingSystem::Windows - } else if cfg!(target_os = "linux") { - CurrentOperatingSystem::Linux - } else if cfg!(target_os = "macos") { - CurrentOperatingSystem::MacOS - } else { - panic!("Unsupported OS"); - } - } - pub fn read_hardware_parameters(&mut self) -> HardwareStatus { // USED FOR DEBUGGING // println!("Reading hardware parameters for {}", self.current_implementation._get_implementation_name()); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ba5596c56..03503f8da 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -99,6 +99,7 @@ mod mm_proxy_manager; mod network_utils; mod node_adapter; mod node_manager; +mod notification_manager; mod p2pool; mod p2pool_adapter; mod p2pool_manager; @@ -692,6 +693,7 @@ fn main() { .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_sentry::init_with_no_injection(&client)) .plugin(tauri_plugin_os::init()) + .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| { println!("{}, {argv:?}, {cwd}", app.package_info().name); @@ -898,6 +900,8 @@ fn main() { commands::set_pre_release, commands::check_for_updates, commands::try_update, + commands::get_network, + commands::trigger_notification ]) .build(tauri::generate_context!()) .inspect_err( diff --git a/src-tauri/src/notification_manager.rs b/src-tauri/src/notification_manager.rs new file mode 100644 index 000000000..dad3f2d7b --- /dev/null +++ b/src-tauri/src/notification_manager.rs @@ -0,0 +1,69 @@ +use anyhow::Error; +use log::{info, warn}; +use notify_rust::Notification; +use std::sync::LazyLock; + + +use crate::utils::platform_utils::{CurrentOperatingSystem, PlatformUtils}; + +const LOG_TARGET: &str = "tari::universe::notification_manager"; +static INSTANCE: LazyLock = LazyLock::new(NotificationManager::new); + +pub struct NotificationManager {} + +impl NotificationManager { + pub fn new() -> Self { + Self {} + } + + pub fn trigger_notification(&self, summary: &str, body: &str) -> Result<(), Error> { + info!(target: LOG_TARGET, "Triggering notification with summary: {} and body: {}", summary, body); + let notification = self.build_notification(summary, body); + + match PlatformUtils::detect_current_os() { + CurrentOperatingSystem::Linux => { + #[cfg(target_os = "linux")] + { + let handle = notification.show().inspect_err( + |e| warn!(target: LOG_TARGET, "Failed to show notification: {:?}", e), + )?; + handle.on_close(|notification| { + info!(target: LOG_TARGET, "Notification closed: {:?}", notification); + }); + } + + Ok(()) + } + CurrentOperatingSystem::MacOS => { + #[cfg(target_os = "macos")] + notification.show().inspect_err( + |e| warn!(target: LOG_TARGET, "Failed to show notification: {:?}", e), + )?; + Ok(()) + } + CurrentOperatingSystem::Windows => { + #[cfg(target_os = "windows")] + notification.show().inspect_err( + |e| warn!(target: LOG_TARGET, "Failed to show notification: {:?}", e), + )?; + Ok(()) + } + } + } + + fn build_notification(&self, summary: &str, body: &str) -> Notification { + let mut notification = Notification::new().summary(summary).body(body).finalize(); + + match PlatformUtils::detect_current_os() { + CurrentOperatingSystem::Linux => { + notification.auto_icon().appname("Tari Universe").finalize() + } + CurrentOperatingSystem::MacOS => notification.finalize(), + CurrentOperatingSystem::Windows => notification.finalize(), + } + } + + pub fn current() -> &'static NotificationManager { + &INSTANCE + } +} diff --git a/src/containers/main/Dashboard/Dashboard.tsx b/src/containers/main/Dashboard/Dashboard.tsx index ad8445f95..c8947b54b 100644 --- a/src/containers/main/Dashboard/Dashboard.tsx +++ b/src/containers/main/Dashboard/Dashboard.tsx @@ -1,5 +1,7 @@ +import { useNotifcations } from '@app/hooks/useNotifications'; import MiningView from './MiningView/MiningView'; import { DashboardContentContainer } from './styles'; +import { useEffect } from 'react'; export default function Dashboard() { return ( diff --git a/src/containers/main/SideBar/components/MiningButton/MiningButton.tsx b/src/containers/main/SideBar/components/MiningButton/MiningButton.tsx index 4aa174225..ceba1f21f 100644 --- a/src/containers/main/SideBar/components/MiningButton/MiningButton.tsx +++ b/src/containers/main/SideBar/components/MiningButton/MiningButton.tsx @@ -12,6 +12,7 @@ import LoadingSvg from '@app/components/svgs/LoadingSvg.tsx'; import ButtonOrbitAnimation from '../../Miner/components/ButtonOrbitAnimation.tsx'; import { IconWrapper, StyledButton, ButtonWrapper } from './MiningButton.styles.ts'; import { SpinnerIcon } from '@app/components/elements/loaders/SpinnerIcon.tsx'; +import { useNotifcations } from '@app/hooks/useNotifications.ts'; enum MiningButtonStateText { STARTED = 'stop-mining', @@ -19,6 +20,8 @@ enum MiningButtonStateText { } export default function MiningButton() { + const { testNotification } = useNotifcations(); + const { t } = useTranslation('mining-view', { useSuspense: false }); const startMining = useMiningStore((s) => s.startMining); const stopMining = useMiningStore((s) => s.stopMining); @@ -40,6 +43,7 @@ export default function MiningButton() { }, [isMining, isMiningInitiated]); const handleClick = useCallback(async () => { + await testNotification(); if (!isMining) { await startMining(); } else { diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts new file mode 100644 index 000000000..d763631c2 --- /dev/null +++ b/src/hooks/useNotifications.ts @@ -0,0 +1,37 @@ +import { invoke } from '@tauri-apps/api/core'; +import { useCallback } from 'react'; +import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification'; + +export const useNotifcations = () => { + const winNotification = useCallback(async (winAmount: string) => { + await invoke('trigger_notification', { + summary: 'Congratulations !', + body: `You won a block! We are sending you rewards of ${winAmount} tXTM!`, + }); + }, []); + + const testNotification = useCallback(async () => { + console.log('testNotification'); + // Do you have permission to send a notification? + let permissionGranted = await isPermissionGranted(); + + console.log('permissionGranted', permissionGranted); + // If not we need to request it + if (!permissionGranted) { + const permission = await requestPermission(); + permissionGranted = permission === 'granted'; + } + + // Once permission has been granted we can send the notification + if (permissionGranted) { + sendNotification({ title: 'Tauri', body: 'Tauri is awesome!' }); + } + + await invoke('trigger_notification', { + summary: 'Test Notification', + body: 'This is a test notification.', + }); + }, []); + + return { winNotification, testNotification }; +}; diff --git a/src/store/appStateStore.ts b/src/store/appStateStore.ts index 47065e268..42f445041 100644 --- a/src/store/appStateStore.ts +++ b/src/store/appStateStore.ts @@ -33,6 +33,7 @@ interface AppState { updateApplicationsVersions: () => Promise; issueReference?: string; setIssueReference: (value: string) => void; + triggerNotification: (summary: string, body: string) => Promise; } export const useAppStateStore = create()((set, getState) => ({ @@ -116,4 +117,14 @@ export const useAppStateStore = create()((set, getState) => ({ missingExternalDependencies: [], loadExternalDependencies: (externalDependencies: ExternalDependency[]) => set({ externalDependencies }), setIssueReference: (issueReference) => set({ issueReference }), + triggerNotification: async (summary: string, body: string) => { + try { + await invoke('trigger_notification', { + summary, + body, + }); + } catch (error) { + console.error('Error triggering notification', error); + } + }, })); diff --git a/src/types/invoke.ts b/src/types/invoke.ts index 4f3041cb4..7c567fa2a 100644 --- a/src/types/invoke.ts +++ b/src/types/invoke.ts @@ -29,6 +29,7 @@ declare module '@tauri-apps/api/core' { function invoke(param: 'get_external_dependencies'): Promise; function invoke(param: 'get_paper_wallet_details'): Promise; function invoke(param: 'resolve_application_language'): Promise; + function invoke(param: 'trigger_notification', payload: { summary: string; body: string }): Promise; function invoke(param: 'set_mine_on_app_start', payload: { mineOnAppStart: boolean }): Promise; function invoke(param: 'setup_application'): Promise; function invoke(param: 'open_log_dir'): Promise;