diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 742f66bf7..858a866bf 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2167,6 +2167,7 @@ dependencies = [ "tauri-build", "tauri-runtime", "tauri-utils", + "tauri-winrt-notification", "thiserror", "tokio", "tracing", @@ -3657,19 +3658,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" -[[package]] -name = "mac-notification-sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51fca4d74ff9dbaac16a01b924bc3693fa2bba0862c2c633abc73f9a8ea21f64" -dependencies = [ - "cc", - "dirs-next", - "objc-foundation", - "objc_id", - "time", -] - [[package]] name = "mach2" version = "0.4.2" @@ -4053,19 +4041,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "notify-rust" -version = "4.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "827c5edfa80235ded4ab3fe8e9dc619b4f866ef16fe9b1c6b8a7f8692c0f2226" -dependencies = [ - "log", - "mac-notification-sys", - "serde", - "tauri-winrt-notification", - "zbus", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -6539,7 +6514,6 @@ dependencies = [ "heck 0.4.1", "http 0.2.11", "ignore", - "notify-rust", "objc", "once_cell", "open", diff --git a/rust/connlib/shared/src/windows.rs b/rust/connlib/shared/src/windows.rs index ffda0d100..ea688658f 100644 --- a/rust/connlib/shared/src/windows.rs +++ b/rust/connlib/shared/src/windows.rs @@ -13,6 +13,10 @@ use std::path::PathBuf; /// This should be identical to the `tauri.bundle.identifier` over in `tauri.conf.json`, /// but sometimes I need to use this before Tauri has booted up, or in a place where /// getting the Tauri app handle would be awkward. +/// +/// Luckily this is also the AppUserModelId that Windows uses to label notifications, +/// so if your dev system has Firezone installed by MSI, the notifications will look right. +/// pub const BUNDLE_ID: &str = "dev.firezone.client"; /// Returns e.g. `C:/Users/User/AppData/Local/dev.firezone.client diff --git a/rust/windows-client/src-tauri/Cargo.toml b/rust/windows-client/src-tauri/Cargo.toml index 7eae824a4..e83df766c 100644 --- a/rust/windows-client/src-tauri/Cargo.toml +++ b/rust/windows-client/src-tauri/Cargo.toml @@ -52,9 +52,10 @@ native-dialog = "0.7.0" [target.'cfg(windows)'.dependencies] # Tauri works fine on Linux, but it requires a lot of build-time deps like glib and gdk, so I've blocked it out for now. -tauri = { version = "1.5", features = [ "dialog", "notification", "shell-open-api", "system-tray" ] } +tauri = { version = "1.5", features = [ "dialog", "shell-open-api", "system-tray" ] } tauri-runtime = "0.14.2" tauri-utils = "1.5.1" +tauri-winrt-notification = "0.1.3" winreg = "0.52.0" wintun = "0.4.0" diff --git a/rust/windows-client/src-tauri/src/client.rs b/rust/windows-client/src-tauri/src/client.rs index 11c88e4b0..6b434cb7d 100644 --- a/rust/windows-client/src-tauri/src/client.rs +++ b/rust/windows-client/src-tauri/src/client.rs @@ -124,15 +124,23 @@ fn run_gui(cli: Cli) -> Result<()> { Ok(result?) } +/// The debug / test flags like `crash_on_purpose` and `test_update_notification` +/// don't propagate when we use `RunAs` to elevate ourselves. So those must be run +/// from an admin terminal, or with "Run as administrator" in the right-click menu. #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Cli { #[command(subcommand)] command: Option, + /// If true, purposely crash the program to test the crash handler #[arg(long, hide = true)] crash_on_purpose: bool, + /// If true, slow down I/O operations to test how the GUI handles slow I/O #[arg(long, hide = true)] inject_faults: bool, + /// If true, show a fake update notification that opens the Firezone release page when clicked + #[arg(long, hide = true)] + test_update_notification: bool, } #[derive(clap::Subcommand)] diff --git a/rust/windows-client/src-tauri/src/client/gui.rs b/rust/windows-client/src-tauri/src/client/gui.rs index d7f7ecad7..d1b705f03 100644 --- a/rust/windows-client/src-tauri/src/client/gui.rs +++ b/rust/windows-client/src-tauri/src/client/gui.rs @@ -14,7 +14,7 @@ use connlib_shared::{messages::ResourceId, windows::BUNDLE_ID}; use secrecy::{ExposeSecret, SecretString}; use std::{net::IpAddr, path::PathBuf, str::FromStr, sync::Arc, time::Duration}; use system_tray_menu::Event as TrayMenuEvent; -use tauri::{api::notification::Notification, Manager, SystemTray, SystemTrayEvent}; +use tauri::{Manager, SystemTray, SystemTrayEvent}; use tokio::sync::{mpsc, oneshot, Notify}; use ControllerRequest as Req; @@ -53,12 +53,16 @@ impl Managed { #[derive(Debug, thiserror::Error)] pub(crate) enum Error { + #[error(r#"Couldn't show clickable notification titled "{0}""#)] + ClickableNotification(String), #[error("Deep-link module error: {0}")] DeepLink(#[from] deep_link::Error), #[error("Can't show log filter error dialog: {0}")] LogFilterErrorDialog(native_dialog::Error), #[error("Logging module error: {0}")] Logging(#[from] logging::Error), + #[error(r#"Couldn't show notification titled "{0}""#)] + Notification(String), #[error(transparent)] Tauri(#[from] tauri::Error), #[error("tokio::runtime::Runtime::new failed: {0}")] @@ -117,6 +121,9 @@ pub(crate) fn run(cli: client::Cli) -> Result<(), Error> { let rt = tokio::runtime::Runtime::new().map_err(Error::TokioRuntimeNew)?; let _guard = rt.enter(); + let (ctlr_tx, ctlr_rx) = mpsc::channel(5); + let notify_controller = Arc::new(Notify::new()); + if cli.crash_on_purpose { tokio::spawn(async { let delay = 10; @@ -129,8 +136,17 @@ pub(crate) fn run(cli: client::Cli) -> Result<(), Error> { }); } - let (ctlr_tx, ctlr_rx) = mpsc::channel(5); - let notify_controller = Arc::new(Notify::new()); + if cli.test_update_notification { + // TODO: Clicking doesn't work if the notification times out and hides first. + // See docs for `show_clickable_notification`. + + show_clickable_notification( + "Firezone update", + "Click here to open the release page.", + ctlr_tx.clone(), + Req::NotificationClicked, + )?; + } // Make sure we're single-instance // We register our deep links to call the `open-deep-link` subcommand, @@ -278,6 +294,7 @@ pub(crate) enum ControllerRequest { DisconnectedTokenExpired, ExportLogs { path: PathBuf, stem: PathBuf }, GetAdvancedSettings(oneshot::Sender), + NotificationClicked, SchemeRequest(url::Url), SystemTrayMenu(TrayMenuEvent), TunnelReady, @@ -597,6 +614,14 @@ async fn run_controller( Req::GetAdvancedSettings(tx) => { tx.send(controller.advanced_settings.clone()).ok(); } + Req::NotificationClicked => { + tracing::info!("NotificationClicked in run_controller!"); + tauri::api::shell::open( + &app.shell_scope(), + "https://example.com/notification_clicked", + None, + )?; + } Req::SchemeRequest(url) => if let Err(e) = controller.handle_deep_link(&url).await { tracing::error!("couldn't handle deep link: {e:#?}"); } @@ -644,10 +669,57 @@ async fn run_controller( /// /// May say "Windows Powershell" and have the wrong icon in dev mode /// See -fn show_notification(title: &str, body: &str) -> Result<()> { - Notification::new(BUNDLE_ID) +fn show_notification(title: &str, body: &str) -> Result<(), Error> { + tauri_winrt_notification::Toast::new(BUNDLE_ID) .title(title) - .body(body) - .show()?; + .text1(body) + .show() + .map_err(|_| Error::Notification(title.to_string()))?; + + Ok(()) +} + +/// Show a notification that signals `Controller` when clicked +/// +/// May say "Windows Powershell" and have the wrong icon in dev mode +/// See +/// +/// Known issue: If the notification times out and goes into the notification center +/// (the little thing that pops up when you click the bell icon), then we may not get the +/// click signal. +/// +/// I've seen this reported by people using Powershell, C#, etc., so I think it might +/// be a Windows bug? +/// - +/// - +/// - +/// +/// Firefox doesn't have this problem. Maybe they're using a different API. +fn show_clickable_notification( + title: &str, + body: &str, + tx: CtlrTx, + req: ControllerRequest, +) -> Result<(), Error> { + // For some reason `on_activated` is FnMut + let mut req = Some(req); + + tauri_winrt_notification::Toast::new(BUNDLE_ID) + .title(title) + .text1(body) + .scenario(tauri_winrt_notification::Scenario::Reminder) + .on_activated(move || { + if let Some(req) = req.take() { + if let Err(error) = tx.blocking_send(req) { + tracing::error!( + ?error, + "User clicked on notification, but we couldn't tell `Controller`" + ); + } + } + Ok(()) + }) + .show() + .map_err(|_| Error::ClickableNotification(title.to_string()))?; Ok(()) } diff --git a/rust/windows-client/src-tauri/src/client/gui/system_tray_menu.rs b/rust/windows-client/src-tauri/src/client/gui/system_tray_menu.rs index cab587d4e..4c323a51c 100644 --- a/rust/windows-client/src-tauri/src/client/gui/system_tray_menu.rs +++ b/rust/windows-client/src-tauri/src/client/gui/system_tray_menu.rs @@ -1,3 +1,8 @@ +//! Code for the Windows notification area +//! +//! "Notification Area" is Microsoft's official name instead of "System tray": +//! + use connlib_client_shared::ResourceDescription; use std::str::FromStr; use tauri::{CustomMenuItem, SystemTrayMenu, SystemTrayMenuItem, SystemTraySubmenu};