Files
cinny-desktop/src-tauri/src/native/aumid.rs
T
jaredandClaude Opus 4.8 33b1a45edd
Build Lotus Chat Desktop / prepare (push) Successful in 3s
Build Lotus Chat Desktop / build-linux (push) Successful in 24m50s
Build Lotus Chat Desktop / build-windows (push) Successful in 32m26s
Build Lotus Chat Desktop / update-manifest (push) Successful in 4s
fix(windows): merge taskbar icon (AUMID) + focus window on notification click
Taskbar showed two icons because the AppUserModelID was (a) set too late — after
the main window was built, so its taskbar button grouped under a mismatched
implicit AUMID — and (b) valued 'LotusGuild.LotusChat', which differs from the
bundle identifier 'org.lotusguild.lotus-chat' that Tauri's NSIS installer stamps
on its shortcuts. Fix both: set the AUMID at the top of run() before the window
is built (new set_process_aumid, split out of ensure_app_user_model_id), and
align the constant to the bundle identifier so the running window and a pinned
installer shortcut group together.

Notification click didn't raise the window: add a focus_main_window command
(reuses show_main) for the web/service-worker path, and call show_main directly
in the rich-toast Activated body-click handler.

Note: existing users who pinned the old mis-grouped icon may need to re-pin once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 23:40:57 -04:00

128 lines
5.5 KiB
Rust

//! 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.
//!
//! 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).
//!
//! 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`.
///
/// 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, 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.
use windows::Win32::Storage::EnhancedStorage::PKEY_AppUserModel_ID;
use windows::Win32::System::Com::{
CoCreateInstance, CoInitializeEx, CoUninitialize, IPersistFile,
StructuredStorage::PROPVARIANT, CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED,
};
use windows::Win32::UI::Shell::{
PropertiesSystem::IPropertyStore, IShellLinkW, ShellLink,
};
// 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,
};
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();
// 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_AppUserModel_ID, &value)?;
store.Commit()?;
// Persist the .lnk to the Start-Menu Programs folder.
let persist: IPersistFile = link.cast()?;
persist.Save(PCWSTR(lnk_wide.as_ptr()), true)?;
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) {}