feat(native): register AUMID so Windows rich toasts work (D6)
Build Lotus Chat Desktop / prepare (push) Successful in 3s
Build Lotus Chat Desktop / build-windows (push) Failing after 25m37s
Build Lotus Chat Desktop / build-linux (push) Successful in 25m29s
Build Lotus Chat Desktop / update-manifest (push) Has been skipped

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:
2026-07-02 22:32:08 -04:00
parent 3a48771588
commit f9ed3d7116
3 changed files with 120 additions and 2 deletions
+111
View File
@@ -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) {}
+4
View File
@@ -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)?;
+5 -2
View File
@@ -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(())