diff --git a/src-tauri/src/native/aumid.rs b/src-tauri/src/native/aumid.rs new file mode 100644 index 0000000..e00ad4b --- /dev/null +++ b/src-tauri/src/native/aumid.rs @@ -0,0 +1,111 @@ +//! P5-41 / P5-35 — Register an AppUserModelID (AUMID) so the WinRT rich toasts in +//! `toast.rs` actually work on Windows. +//! +//! `ToastNotificationManager::CreateToastNotifierWithId` (and the ambient +//! `CreateToastNotifier`) require the process to run under an AUMID that maps to a +//! Start-Menu shortcut carrying `System.AppUserModel.ID`. An unpackaged Win32 app +//! (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` +//! pattern proven in `jumplist.rs`. Best-effort: any failure is logged and +//! swallowed (the toast just keeps falling back, as before — never crash boot). +//! +//! Non-Windows: a no-op. + +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"; + +#[cfg(target_os = "windows")] +pub fn ensure_app_user_model_id(_app: &AppHandle) { + use std::os::windows::ffi::OsStrExt; + use windows::core::{Interface, GUID, HSTRING, PCWSTR}; + use windows::Win32::System::Com::{ + CoCreateInstance, CoInitializeEx, CoUninitialize, IPersistFile, + StructuredStorage::PROPVARIANT, CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED, + }; + use windows::Win32::UI::Shell::{ + PropertiesSystem::{IPropertyStore, PROPERTYKEY}, + IShellLinkW, SetCurrentProcessExplicitAppUserModelID, 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. + let appdata = match std::env::var_os("APPDATA") { + Some(v) => v, + None => return, + }; + let mut lnk = std::path::PathBuf::from(appdata); + lnk.push(r"Microsoft\Windows\Start Menu\Programs"); + let _ = std::fs::create_dir_all(&lnk); + lnk.push("Lotus Chat.lnk"); + + let exe = match std::env::current_exe() { + Ok(p) => p, + Err(_) => return, + }; + let exe_wide: Vec = exe + .as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + let lnk_wide: Vec = lnk + .as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + + // PKEY_AppUserModel_ID = {9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}, pid 5. + // Constructed inline so we don't depend on the const's crate module path. + const PKEY_APP_USER_MODEL_ID: PROPERTYKEY = PROPERTYKEY { + fmtid: GUID::from_u128(0x9F4C2855_9F79_4B39_A8D0_E1D42DE1D5F3), + pid: 5, + }; + + // STA apartment for the shell link objects, mirroring jumplist.rs. All COM + // interfaces are dropped before CoUninitialize. + let hr = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }; + let result = (|| -> windows::core::Result<()> { + unsafe { + let link: IShellLinkW = CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)?; + link.SetPath(PCWSTR(exe_wide.as_ptr()))?; + link.SetIconLocation(PCWSTR(exe_wide.as_ptr()), 0)?; + + // Stamp the AUMID onto the link's property store (VT_LPWSTR, exactly + // like PKEY_Title in jumplist.rs). + let store: IPropertyStore = link.cast()?; + let value = PROPVARIANT::from(APP_USER_MODEL_ID); + store.SetValue(&PKEY_APP_USER_MODEL_ID, &value)?; + store.Commit()?; + + // Persist the .lnk to the Start-Menu Programs folder. + let persist: IPersistFile = link.cast()?; + // fremember is a Win32 BOOL (not bool); `.into()` uses From. + persist.Save(PCWSTR(lnk_wide.as_ptr()), true.into())?; + Ok(()) + } + })(); + + if hr.is_ok() { + unsafe { CoUninitialize() }; + } + if let Err(e) = result { + eprintln!("aumid: failed to install Start-Menu shortcut: {e}"); + } +} + +#[cfg(not(target_os = "windows"))] +pub fn ensure_app_user_model_id(_app: &AppHandle) {} diff --git a/src-tauri/src/native/mod.rs b/src-tauri/src/native/mod.rs index d2e62c6..efe1258 100644 --- a/src-tauri/src/native/mod.rs +++ b/src-tauri/src/native/mod.rs @@ -8,6 +8,7 @@ use tauri::{AppHandle, Manager}; +pub mod aumid; pub mod chrome; pub mod focus_assist; pub mod jumplist; @@ -34,6 +35,9 @@ pub fn emit_to_web(app: &AppHandle, event: &str, detail_json: &str) { /// listeners or managed state get initialized here. (jumplist/chrome are /// command-only and need no setup.) pub fn setup(app: &AppHandle) -> tauri::Result<()> { + // Register the AUMID + Start-Menu shortcut FIRST so the WinRT rich toast can + // create its notifier (before any notification path fires). Best-effort. + aumid::ensure_app_user_model_id(app); power::setup(app)?; thumbbar::setup(app)?; smtc::setup(app)?; diff --git a/src-tauri/src/native/toast.rs b/src-tauri/src/native/toast.rs index 296d9aa..c0b4802 100644 --- a/src-tauri/src/native/toast.rs +++ b/src-tauri/src/native/toast.rs @@ -224,8 +224,11 @@ fn show_windows_toast( } } - // No AUMID argument: relies on the process AppUserModelID (see module note). - let notifier = ToastNotificationManager::CreateToastNotifier()?; + // Bind the notifier to our registered AUMID (native::aumid) so it resolves to + // the "Lotus Chat" Start-Menu shortcut rather than an ambient/absent default. + let notifier = ToastNotificationManager::CreateToastNotifierWithId(&HSTRING::from( + crate::native::aumid::APP_USER_MODEL_ID, + ))?; notifier.Show(&toast)?; Ok(())