feat(native): register AUMID so Windows rich toasts work (D6)
The WinRT rich toast (reply box P5-41, click-to-open-room P5-35) was inert on
Windows: CreateToastNotifier needs the process under an AppUserModelID mapped to
a Start-Menu shortcut, and none was registered — so it errored and silently fell
back to the plain plugin toast.
New native/aumid.rs (Windows-only; no-op elsewhere), called first in
native::setup: (1) SetCurrentProcessExplicitAppUserModelID("LotusGuild.LotusChat"),
(2) install/refresh a Start-Menu "Lotus Chat.lnk" carrying PKEY_AppUserModel_ID,
reusing jumplist.rs's IShellLinkW + IPropertyStore + PROPVARIANT + IPersistFile
pattern (best-effort; failures logged + swallowed). toast.rs now binds the
notifier via CreateToastNotifierWithId(AUMID).
CI-compile-only (windows runner); runtime needs a Windows build to confirm the
toast shows a reply box + opens the room. windows-crate 0.61 symbol assumptions
(IPersistFile, SetCurrentProcessExplicitAppUserModelID, PROPERTYKEY,
GUID::from_u128, CreateToastNotifierWithId) validated by CI — all mirror existing
jumplist.rs usage where possible.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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<u16> = exe
|
||||||
|
.as_os_str()
|
||||||
|
.encode_wide()
|
||||||
|
.chain(std::iter::once(0))
|
||||||
|
.collect();
|
||||||
|
let lnk_wide: Vec<u16> = 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<bool>.
|
||||||
|
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) {}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
|
pub mod aumid;
|
||||||
pub mod chrome;
|
pub mod chrome;
|
||||||
pub mod focus_assist;
|
pub mod focus_assist;
|
||||||
pub mod jumplist;
|
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
|
/// listeners or managed state get initialized here. (jumplist/chrome are
|
||||||
/// command-only and need no setup.)
|
/// command-only and need no setup.)
|
||||||
pub fn setup(app: &AppHandle) -> tauri::Result<()> {
|
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)?;
|
power::setup(app)?;
|
||||||
thumbbar::setup(app)?;
|
thumbbar::setup(app)?;
|
||||||
smtc::setup(app)?;
|
smtc::setup(app)?;
|
||||||
|
|||||||
@@ -224,8 +224,11 @@ fn show_windows_toast(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No AUMID argument: relies on the process AppUserModelID (see module note).
|
// Bind the notifier to our registered AUMID (native::aumid) so it resolves to
|
||||||
let notifier = ToastNotificationManager::CreateToastNotifier()?;
|
// 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)?;
|
notifier.Show(&toast)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Reference in New Issue
Block a user