diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 912d5c8..e719a72 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -466,7 +466,20 @@ fn flash_window(window: tauri::Window) -> Result<(), String> { .map_err(|e| e.to_string()) } +/// Bring the main window to the foreground. Invoked from the web side when a +/// notification is clicked — the service-worker/page path can't raise the native +/// OS window on its own (a WebView2 `client.focus()` only focuses the document). +#[tauri::command] +fn focus_main_window(app: tauri::AppHandle) { + show_main(&app); +} + pub fn run() { + // Advertise the process AUMID BEFORE the main window is built (in `.setup`, + // below) so the window's taskbar button groups under the same identity as the + // pinned installer shortcut. Must precede the WebviewWindowBuilder. + native::aumid::set_process_aumid(); + let port: u16 = 44548; let context = tauri::generate_context!(); @@ -492,6 +505,7 @@ pub fn run() { set_tray_unread, get_tray_dnd, flash_window, + focus_main_window, send_notification, check_for_update, install_update, diff --git a/src-tauri/src/native/aumid.rs b/src-tauri/src/native/aumid.rs index 5494f72..3dc3794 100644 --- a/src-tauri/src/native/aumid.rs +++ b/src-tauri/src/native/aumid.rs @@ -7,9 +7,12 @@ //! (our NSIS build) has none by default, so `Show()` errored and the rich toast //! (reply box + click-to-open-room) silently fell back to the plain plugin toast. //! -//! On startup we (1) advertise the AUMID for this process and (2) install/refresh -//! a Start-Menu `.lnk` (same name → overwrites the installer's, no duplicate) -//! carrying the AUMID. Reuses the `IShellLinkW` + `IPropertyStore` + `PROPVARIANT` +//! Two pieces: (1) `set_process_aumid` advertises the AUMID for this process — +//! called at the very top of `run()` so it precedes the main window's taskbar +//! button (which otherwise groups under a mismatched implicit AUMID and shows a +//! second taskbar icon); (2) `ensure_app_user_model_id` installs/refreshes a +//! Start-Menu `.lnk` (same name → overwrites the installer's, no duplicate) +//! carrying the AUMID, reusing the `IShellLinkW` + `IPropertyStore` + `PROPVARIANT` //! pattern proven in `jumplist.rs`. Best-effort: any failure is logged and //! swallowed (the toast just keeps falling back, as before — never crash boot). //! @@ -19,12 +22,35 @@ use tauri::AppHandle; /// The AUMID this process advertises and that the Start-Menu shortcut carries. /// `toast.rs` binds the toast notifier to it via `CreateToastNotifierWithId`. -pub const APP_USER_MODEL_ID: &str = "LotusGuild.LotusChat"; +/// +/// This MUST equal the AUMID Tauri's NSIS installer stamps on its Desktop / +/// Start-Menu shortcuts (`System.AppUserModel.ID` = the bundle `identifier`), +/// otherwise the running window and a pinned shortcut group under different +/// identities and Windows shows two taskbar icons. +pub const APP_USER_MODEL_ID: &str = "org.lotusguild.lotus-chat"; + +/// Advertise this process's AUMID. MUST run before the main window is built so +/// the window's taskbar button groups under the same AUMID as the pinned +/// installer shortcut. Plain shell32 export — no COM init required, so this is +/// deliberately separate from the `.lnk` install (which needs its own STA COM). +#[cfg(target_os = "windows")] +pub fn set_process_aumid() { + use windows::core::HSTRING; + use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID; + if let Err(e) = + unsafe { SetCurrentProcessExplicitAppUserModelID(&HSTRING::from(APP_USER_MODEL_ID)) } + { + eprintln!("aumid: SetCurrentProcessExplicitAppUserModelID failed: {e}"); + } +} + +#[cfg(not(target_os = "windows"))] +pub fn set_process_aumid() {} #[cfg(target_os = "windows")] pub fn ensure_app_user_model_id(_app: &AppHandle) { use std::os::windows::ffi::OsStrExt; - use windows::core::{Interface, HSTRING, PCWSTR}; + use windows::core::{Interface, PCWSTR}; // PKEY_AppUserModel_ID lives in EnhancedStorage (same module as jumplist's // PKEY_Title), NOT PropertiesSystem — use the ready-made constant rather than // hand-rolling the PROPERTYKEY. @@ -34,20 +60,14 @@ pub fn ensure_app_user_model_id(_app: &AppHandle) { StructuredStorage::PROPVARIANT, CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED, }; use windows::Win32::UI::Shell::{ - PropertiesSystem::IPropertyStore, IShellLinkW, SetCurrentProcessExplicitAppUserModelID, - ShellLink, + PropertiesSystem::IPropertyStore, IShellLinkW, ShellLink, }; - // 1. Advertise the AUMID for this process (must happen before any toast fires). - if let Err(e) = - unsafe { SetCurrentProcessExplicitAppUserModelID(&HSTRING::from(APP_USER_MODEL_ID)) } - { - eprintln!("aumid: SetCurrentProcessExplicitAppUserModelID failed: {e}"); - } - - // 2. Install/refresh the Start-Menu shortcut carrying the AUMID so Action - // Center attributes toasts to "Lotus Chat". Path via %APPDATA% (avoids the - // SHGetKnownFolderPath free-mem dance); dir already exists for installed apps. + // The process AUMID is advertised earlier (see `set_process_aumid`, called at + // the top of `run()` before the window is built). Here we only install/refresh + // the Start-Menu shortcut carrying the AUMID so Action Center attributes toasts + // to "Lotus Chat". Path via %APPDATA% (avoids the SHGetKnownFolderPath free-mem + // dance); dir already exists for installed apps. let appdata = match std::env::var_os("APPDATA") { Some(v) => v, None => return, diff --git a/src-tauri/src/native/toast.rs b/src-tauri/src/native/toast.rs index c0b4802..9c6887f 100644 --- a/src-tauri/src/native/toast.rs +++ b/src-tauri/src/native/toast.rs @@ -186,7 +186,13 @@ fn show_windows_toast( .to_string(); super::emit_to_web(&app_activated, "lotus-notification-reply", &payload); } else { - // Plain body click: forward the launch path so the web routes to it. + // Plain body click: raise the window to the foreground (the + // "foreground" activationType is unreliable for an unpackaged app, + // so do it explicitly), then forward the launch path so the web + // routes to the room. `show_main` is the shared tray/deep-link + // helper. Not done for the reply branch — an inline quick-reply + // shouldn't yank the window forward. + crate::show_main(&app_activated); let payload = serde_json::json!({ "path": path_owned.as_deref(), })