feat: improve Windows Toast actions support (#49786)

* feat: improve Windows Toast actions support

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* fix: ensure MSIX compatibility

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* test: add bad clsid format test

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
This commit is contained in:
trop[bot]
2026-02-18 13:22:10 -05:00
committed by GitHub
parent c7a033dd06
commit 3d475716f4
20 changed files with 1004 additions and 60 deletions

View File

@@ -88,6 +88,7 @@
#if BUILDFLAG(IS_WIN)
#include "base/strings/utf_string_conversions.h"
#include "shell/browser/notifications/win/windows_toast_activator.h"
#include "shell/browser/ui/win/jump_list.h"
#endif
@@ -1840,6 +1841,10 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) {
#if BUILDFLAG(IS_WIN)
.SetMethod("setAppUserModelId",
base::BindRepeating(&Browser::SetAppUserModelID, browser))
.SetMethod("setToastActivatorCLSID",
base::BindRepeating(&App::SetToastActivatorCLSID,
base::Unretained(this)))
.SetProperty("toastActivatorCLSID", &App::GetToastActivatorCLSID)
#endif
.SetMethod(
"isDefaultProtocolClient",
@@ -1967,6 +1972,34 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) {
.SetMethod("resolveProxy", &App::ResolveProxy);
}
#if BUILDFLAG(IS_WIN)
void App::SetToastActivatorCLSID(gin_helper::ErrorThrower thrower,
const std::string& id) {
std::wstring wide = base::UTF8ToWide(id);
CLSID parsed;
if (FAILED(::CLSIDFromString(wide.c_str(), &parsed))) {
if (!wide.empty() && wide.front() != L'{') {
std::wstring with_braces = L"{" + wide + L"}";
if (FAILED(::CLSIDFromString(with_braces.c_str(), &parsed))) {
thrower.ThrowError("Invalid CLSID format");
return;
}
wide = std::move(with_braces);
} else {
thrower.ThrowError("Invalid CLSID format");
return;
}
}
SetAppToastActivatorCLSID(wide);
}
v8::Local<v8::Value> App::GetToastActivatorCLSID(v8::Isolate* isolate) {
return gin::ConvertToV8(isolate,
base::WideToUTF8(GetAppToastActivatorCLSID()));
}
#endif
const char* App::GetHumanReadableName() const {
return "Electron / App";
}

View File

@@ -265,6 +265,12 @@ class App final : public gin::Wrappable<App>,
// Set or remove a custom Jump List for the application.
JumpListResult SetJumpList(v8::Isolate* isolate, v8::Local<v8::Value> val);
// Set the toast activator CLSID.
void SetToastActivatorCLSID(gin_helper::ErrorThrower thrower,
const std::string& id);
// Get the toast activator CLSID.
v8::Local<v8::Value> GetToastActivatorCLSID(v8::Isolate* isolate);
#endif // BUILDFLAG(IS_WIN)
std::unique_ptr<ProcessSingleton> process_singleton_;

View File

@@ -31,6 +31,9 @@ struct Converter<electron::NotificationAction> {
return false;
}
dict.Get("text", &(out->text));
std::vector<std::u16string> items;
if (dict.Get("items", &items))
out->items = std::move(items);
return true;
}
@@ -39,6 +42,9 @@ struct Converter<electron::NotificationAction> {
auto dict = gin::Dictionary::CreateEmpty(isolate);
dict.Set("text", val.text);
dict.Set("type", val.type);
if (!val.items.empty()) {
dict.Set("items", val.items);
}
return ConvertToV8(isolate, dict);
}
};
@@ -138,8 +144,20 @@ void Notification::SetToastXml(const std::u16string& new_toast_xml) {
toast_xml_ = new_toast_xml;
}
void Notification::NotificationAction(int index) {
Emit("action", index);
void Notification::NotificationAction(int action_index, int selection_index) {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
gin_helper::internal::Event* event =
gin_helper::internal::Event::New(isolate);
v8::Local<v8::Object> event_object =
event->GetWrapper(isolate).ToLocalChecked();
gin_helper::Dictionary dict(isolate, event_object);
dict.Set("selectionIndex", selection_index);
dict.Set("actionIndex", action_index);
EmitWithoutEvent("action", event_object, action_index, selection_index);
}
void Notification::NotificationClick() {
@@ -147,7 +165,18 @@ void Notification::NotificationClick() {
}
void Notification::NotificationReplied(const std::string& reply) {
Emit("reply", reply);
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
gin_helper::internal::Event* event =
gin_helper::internal::Event::New(isolate);
v8::Local<v8::Object> event_object =
event->GetWrapper(isolate).ToLocalChecked();
gin_helper::Dictionary dict(isolate, event_object);
dict.Set("reply", reply);
EmitWithoutEvent("reply", event_object, reply);
}
void Notification::NotificationDisplayed() {

View File

@@ -45,7 +45,7 @@ class Notification final : public gin_helper::DeprecatedWrappable<Notification>,
static const char* GetClassName() { return "Notification"; }
// NotificationDelegate:
void NotificationAction(int index) override;
void NotificationAction(int action_index, int selection_index) override;
void NotificationClick() override;
void NotificationReplied(const std::string& reply) override;
void NotificationDisplayed() override;

View File

@@ -139,7 +139,7 @@ void CocoaNotification::NotificationReplied(const std::string& reply) {
void CocoaNotification::NotificationActivated() {
if (delegate())
delegate()->NotificationAction(action_index_);
delegate()->NotificationAction(action_index_, -1);
this->LogAction("button clicked");
}
@@ -156,7 +156,7 @@ void CocoaNotification::NotificationActivated(
}
}
delegate()->NotificationAction(index);
delegate()->NotificationAction(index, -1);
}
this->LogAction("button clicked");

View File

@@ -22,6 +22,15 @@ NotificationOptions& NotificationOptions::operator=(NotificationOptions&&) =
default;
NotificationOptions::~NotificationOptions() = default;
NotificationAction::NotificationAction() = default;
NotificationAction::~NotificationAction() = default;
NotificationAction::NotificationAction(const NotificationAction&) = default;
NotificationAction& NotificationAction::operator=(const NotificationAction&) =
default;
NotificationAction::NotificationAction(NotificationAction&&) noexcept = default;
NotificationAction& NotificationAction::operator=(
NotificationAction&&) noexcept = default;
Notification::Notification(NotificationDelegate* delegate,
NotificationPresenter* presenter)
: delegate_(delegate), presenter_(presenter) {}

View File

@@ -23,6 +23,14 @@ class NotificationPresenter;
struct NotificationAction {
std::u16string type;
std::u16string text;
std::vector<std::u16string> items;
NotificationAction();
~NotificationAction();
NotificationAction(const NotificationAction&);
NotificationAction& operator=(const NotificationAction&);
NotificationAction(NotificationAction&&) noexcept;
NotificationAction& operator=(NotificationAction&&) noexcept;
};
struct NotificationOptions {

View File

@@ -19,7 +19,9 @@ class NotificationDelegate {
// Notification was replied to
virtual void NotificationReplied(const std::string& reply) {}
virtual void NotificationAction(int index) {}
// |selection_index| is >= 0 only for selection actions (Windows), otherwise
// -1.
virtual void NotificationAction(int action_index, int selection_index = -1) {}
virtual void NotificationClick() {}
virtual void NotificationClosed() {}

View File

@@ -16,6 +16,7 @@
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "shell/browser/notifications/win/windows_toast_activator.h"
#include "shell/browser/notifications/win/windows_toast_notification.h"
#include "shell/common/thread_restrictions.h"
#include "third_party/skia/include/core/SkBitmap.h"
@@ -45,6 +46,9 @@ std::unique_ptr<NotificationPresenter> NotificationPresenter::Create() {
if (!presenter->Init())
return {};
// Ensure COM toast activator is registered once the presenter is ready.
NotificationActivator::RegisterActivator();
if (electron::debug_notifications)
LOG(INFO) << "Successfully created Windows notifications presenter";

View File

@@ -0,0 +1,429 @@
// Copyright (c) 2025 Microsoft, GmbH
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/notifications/win/windows_toast_activator.h"
#include <propkey.h>
#include <propvarutil.h>
#include <shlobj.h>
#include <shobjidl.h>
#include <wrl/module.h>
#ifdef StrCat
// Undefine Windows shlwapi.h StrCat macro to avoid conflict with
// base/strings/strcat.h which deliberately defines a StrCat macro sentinel.
#undef StrCat
#endif
#include <string>
#include <vector>
#include <algorithm>
#include <atomic>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/hash/hash.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/strings/string_number_conversions_win.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/registry.h"
#include "base/win/scoped_propvariant.h"
#include "base/win/win_util.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "shell/browser/api/electron_api_notification.h" // nogncheck - for delegate events
#include "shell/browser/electron_browser_client.h"
#include "shell/browser/notifications/notification.h"
#include "shell/browser/notifications/notification_delegate.h"
#include "shell/browser/notifications/notification_presenter.h"
#include "shell/browser/notifications/win/notification_presenter_win.h"
#include "shell/common/application_info.h"
namespace electron {
namespace {
class NotificationActivatorFactory final : public IClassFactory {
public:
NotificationActivatorFactory() : ref_count_(1) {}
~NotificationActivatorFactory() = default;
IFACEMETHODIMP QueryInterface(REFIID riid, void** ppv) override {
if (!ppv)
return E_POINTER;
if (riid == IID_IUnknown || riid == IID_IClassFactory) {
*ppv = static_cast<IClassFactory*>(this);
} else {
*ppv = nullptr;
return E_NOINTERFACE;
}
AddRef();
return S_OK;
}
IFACEMETHODIMP_(ULONG) AddRef() override { return ++ref_count_; }
IFACEMETHODIMP_(ULONG) Release() override {
ULONG res = --ref_count_;
if (res == 0)
delete this;
return res;
}
IFACEMETHODIMP CreateInstance(IUnknown* outer,
REFIID riid,
void** ppv) override {
if (outer)
return CLASS_E_NOAGGREGATION;
auto activator = Microsoft::WRL::Make<NotificationActivator>();
return activator.CopyTo(riid, ppv);
}
IFACEMETHODIMP LockServer(BOOL) override { return S_OK; }
private:
std::atomic<ULONG> ref_count_;
};
NotificationActivatorFactory* g_factory_instance = nullptr;
bool g_registration_in_progress = false;
std::wstring GetExecutablePath() {
wchar_t path[MAX_PATH];
DWORD len = ::GetModuleFileNameW(nullptr, path, std::size(path));
if (len == 0 || len == std::size(path))
return L"";
return std::wstring(path, len);
}
void EnsureCLSIDRegistry() {
std::wstring exe = GetExecutablePath();
if (exe.empty())
return;
std::wstring clsid = GetAppToastActivatorCLSID();
std::wstring key_path = L"Software\\Classes\\CLSID\\" + clsid;
base::win::RegKey key(HKEY_CURRENT_USER, key_path.c_str(), KEY_SET_VALUE);
if (!key.Valid())
return;
key.WriteValue(nullptr, L"Electron Notification Activator");
key.WriteValue(L"CustomActivator", 1U);
std::wstring local_server = key_path + L"\\LocalServer32";
base::win::RegKey server_key(HKEY_CURRENT_USER, local_server.c_str(),
KEY_SET_VALUE);
if (!server_key.Valid())
return;
server_key.WriteValue(nullptr, exe.c_str());
}
bool ExistingShortcutValid(const base::FilePath& lnk_path, PCWSTR aumid) {
if (!base::PathExists(lnk_path))
return false;
Microsoft::WRL::ComPtr<IShellLink> existing;
if (FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&existing))))
return false;
Microsoft::WRL::ComPtr<IPersistFile> pf;
if (FAILED(existing.As(&pf)) ||
FAILED(pf->Load(lnk_path.value().c_str(), STGM_READ))) {
return false;
}
Microsoft::WRL::ComPtr<IPropertyStore> store;
if (FAILED(existing.As(&store)))
return false;
base::win::ScopedPropVariant pv_id;
base::win::ScopedPropVariant pv_clsid;
if (FAILED(store->GetValue(PKEY_AppUserModel_ID, pv_id.Receive())) ||
FAILED(store->GetValue(PKEY_AppUserModel_ToastActivatorCLSID,
pv_clsid.Receive()))) {
return false;
}
if (pv_id.get().vt == VT_LPWSTR && pv_clsid.get().vt == VT_CLSID &&
pv_id.get().pwszVal && pv_clsid.get().puuid) {
CLSID desired;
if (SUCCEEDED(::CLSIDFromString(GetAppToastActivatorCLSID(), &desired))) {
return _wcsicmp(pv_id.get().pwszVal, aumid) == 0 &&
IsEqualGUID(*pv_clsid.get().puuid, desired);
}
}
return false;
}
void EnsureShortcut() {
PCWSTR aumid = GetRawAppUserModelID();
if (!aumid || !*aumid)
return;
std::wstring exe = GetExecutablePath();
if (exe.empty())
return;
PWSTR programs_path = nullptr;
if (FAILED(
SHGetKnownFolderPath(FOLDERID_Programs, 0, nullptr, &programs_path)))
return;
base::FilePath programs(programs_path);
CoTaskMemFree(programs_path);
std::wstring product_name = base::UTF8ToWide(GetApplicationName());
if (product_name.empty())
product_name = L"ElectronApp";
base::CreateDirectory(programs);
base::FilePath lnk_path = programs.Append(product_name + L".lnk");
if (base::PathExists(lnk_path)) {
Microsoft::WRL::ComPtr<IShellLink> existing;
if (SUCCEEDED(CoCreateInstance(CLSID_ShellLink, nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&existing)))) {
Microsoft::WRL::ComPtr<IPersistFile> pf;
if (SUCCEEDED(existing.As(&pf)) &&
SUCCEEDED(pf->Load(lnk_path.value().c_str(), STGM_READ))) {
Microsoft::WRL::ComPtr<IPropertyStore> store;
if (SUCCEEDED(existing.As(&store))) {
base::win::ScopedPropVariant pv_clsid;
if (SUCCEEDED(store->GetValue(PKEY_AppUserModel_ToastActivatorCLSID,
pv_clsid.Receive())) &&
pv_clsid.get().vt == VT_CLSID && pv_clsid.get().puuid) {
wchar_t buf[64] = {0};
if (StringFromGUID2(*pv_clsid.get().puuid, buf, std::size(buf)) >
0) {
SetAppToastActivatorCLSID(buf);
}
}
}
}
}
}
if (ExistingShortcutValid(lnk_path, aumid))
return;
Microsoft::WRL::ComPtr<IShellLink> shell_link;
if (FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&shell_link))))
return;
shell_link->SetPath(exe.c_str());
shell_link->SetArguments(L"");
shell_link->SetDescription(product_name.c_str());
shell_link->SetWorkingDirectory(
base::FilePath(exe).DirName().value().c_str());
Microsoft::WRL::ComPtr<IPropertyStore> prop_store;
if (SUCCEEDED(shell_link.As(&prop_store))) {
PROPVARIANT pv_id;
PropVariantInit(&pv_id);
if (SUCCEEDED(InitPropVariantFromString(aumid, &pv_id))) {
prop_store->SetValue(PKEY_AppUserModel_ID, pv_id);
PropVariantClear(&pv_id);
}
PROPVARIANT pv_clsid;
PropVariantInit(&pv_clsid);
GUID clsid;
if (SUCCEEDED(::CLSIDFromString(GetAppToastActivatorCLSID(), &clsid)) &&
SUCCEEDED(InitPropVariantFromCLSID(clsid, &pv_clsid))) {
prop_store->SetValue(PKEY_AppUserModel_ToastActivatorCLSID, pv_clsid);
PropVariantClear(&pv_clsid);
}
prop_store->Commit();
}
Microsoft::WRL::ComPtr<IPersistFile> persist_file;
if (SUCCEEDED(shell_link.As(&persist_file))) {
persist_file->Save(lnk_path.value().c_str(), TRUE);
}
}
} // namespace
DWORD NotificationActivator::g_cookie_ = 0;
bool NotificationActivator::g_registered_ = false;
NotificationActivator::NotificationActivator() = default;
NotificationActivator::~NotificationActivator() = default;
// static
// static
void NotificationActivator::RegisterActivator() {
if (g_registered_ || g_registration_in_progress)
return;
g_registration_in_progress = true;
// For packaged (MSIX) apps, COM server registration is handled via the app
// manifest (com:Extension and desktop:ToastNotificationActivation), so we
// skip the registry and shortcut creation. We still need to call
// CoRegisterClassObject to handle activations at runtime.
//
// For unpackaged apps, we need to create the Start Menu shortcut with
// AUMID/CLSID properties and register the COM server in the registry.
bool is_packaged = IsRunningInDesktopBridge();
// Perform all blocking filesystem / registry work off the UI thread.
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(
[](bool is_packaged) {
// Skip shortcut and registry setup for packaged apps - these are
// declared in the app manifest instead.
if (!is_packaged) {
EnsureShortcut();
EnsureCLSIDRegistry();
}
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce([]() {
g_registration_in_progress = false;
if (g_registered_)
return;
CLSID clsid;
if (FAILED(::CLSIDFromString(GetAppToastActivatorCLSID(),
&clsid))) {
LOG(ERROR) << "Invalid toast activator CLSID";
return;
}
if (!g_factory_instance)
g_factory_instance =
new (std::nothrow) NotificationActivatorFactory();
if (!g_factory_instance)
return;
DWORD cookie = 0;
HRESULT hr = ::CoRegisterClassObject(
clsid, static_cast<IClassFactory*>(g_factory_instance),
CLSCTX_LOCAL_SERVER,
REGCLS_MULTIPLEUSE | REGCLS_SUSPENDED, &cookie);
if (FAILED(hr))
return;
hr = ::CoResumeClassObjects();
if (FAILED(hr)) {
::CoRevokeClassObject(cookie);
return;
}
g_cookie_ = cookie;
g_registered_ = true;
}));
},
is_packaged));
}
// static
void NotificationActivator::UnregisterActivator() {
if (!g_registered_)
return;
::CoRevokeClassObject(g_cookie_);
g_cookie_ = 0;
g_registered_ = false;
// Factory instance lifetime intentionally leaked after revoke to avoid
// race; could be deleted here if ensured no pending COM calls.
}
IFACEMETHODIMP NotificationActivator::Activate(
LPCWSTR app_user_model_id,
LPCWSTR invoked_args,
const NOTIFICATION_USER_INPUT_DATA* data,
ULONG data_count) {
std::wstring args = invoked_args ? invoked_args : L"";
std::wstring aumid = app_user_model_id ? app_user_model_id : L"";
std::vector<ActivationUserInput> copied_inputs;
if (data && data_count) {
std::vector<NOTIFICATION_USER_INPUT_DATA> temp;
temp.resize(static_cast<size_t>(data_count));
std::copy_n(data, static_cast<size_t>(data_count), temp.begin());
copied_inputs.reserve(temp.size());
for (const auto& entry : temp) {
ActivationUserInput ui;
if (entry.Key)
ui.key = entry.Key;
if (entry.Value)
ui.value = entry.Value;
copied_inputs.push_back(std::move(ui));
}
}
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&HandleToastActivation, std::move(args),
std::move(copied_inputs)));
return S_OK;
}
void HandleToastActivation(const std::wstring& invoked_args,
std::vector<ActivationUserInput> inputs) {
// Expected invoked_args format:
// type=<click|action|reply>&action=<index>&tag=<hash> Parse simple key=value
// pairs separated by '&'.
std::wstring args = invoked_args;
std::wstring type;
std::wstring action_index_str;
std::wstring tag_str;
for (const auto& token : base::SplitString(args, L"&", base::KEEP_WHITESPACE,
base::SPLIT_WANT_NONEMPTY)) {
auto kv = base::SplitString(token, L"=", base::KEEP_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
if (kv.size() != 2)
continue;
if (kv[0] == L"type")
type = kv[1];
else if (kv[0] == L"action")
action_index_str = kv[1];
else if (kv[0] == L"tag")
tag_str = kv[1];
}
int action_index = -1;
if (!action_index_str.empty()) {
action_index = std::stoi(action_index_str);
}
std::string reply_text;
for (const auto& entry : inputs) {
std::wstring_view key_view(entry.key);
if (key_view == L"reply") {
reply_text = base::WideToUTF8(entry.value);
}
}
auto* browser_client =
static_cast<ElectronBrowserClient*>(ElectronBrowserClient::Get());
if (!browser_client)
return;
NotificationPresenter* presenter = browser_client->GetNotificationPresenter();
if (!presenter)
return;
Notification* target = nullptr;
for (auto* n : presenter->notifications()) {
std::wstring tag_hash =
base::NumberToWString(base::FastHash(n->notification_id()));
if (tag_hash == tag_str) {
target = n;
break;
}
}
if (!target)
return;
if (type == L"action" && target->delegate()) {
int selection_index = -1;
for (const auto& entry : inputs) {
std::wstring_view key_view(entry.key);
if (base::StartsWith(key_view, L"selection",
base::CompareCase::SENSITIVE)) {
int parsed = -1;
if (base::StringToInt(entry.value, &parsed) && parsed >= 0)
selection_index = parsed;
break;
}
}
if (action_index >= 0)
target->delegate()->NotificationAction(action_index, selection_index);
} else if ((type == L"reply" || (!reply_text.empty() && type.empty())) &&
target->delegate()) {
target->delegate()->NotificationReplied(reply_text);
} else if ((type == L"click" || type.empty()) && target->delegate()) {
target->NotificationClicked();
}
}
} // namespace electron

View File

@@ -0,0 +1,49 @@
// Copyright (c) 2025 Microsoft, GmbH
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_NOTIFICATIONS_WIN_WINDOWS_TOAST_ACTIVATOR_H_
#define ELECTRON_SHELL_BROWSER_NOTIFICATIONS_WIN_WINDOWS_TOAST_ACTIVATOR_H_
#include <NotificationActivationCallback.h>
#include <windows.h>
#include <wrl/implements.h>
#include <string>
#include <vector>
namespace electron {
class NotificationPresenterWin;
class NotificationActivator
: public Microsoft::WRL::RuntimeClass<
Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
INotificationActivationCallback> {
public:
NotificationActivator();
~NotificationActivator() override;
IFACEMETHODIMP Activate(LPCWSTR app_user_model_id,
LPCWSTR invoked_args,
const NOTIFICATION_USER_INPUT_DATA* data,
ULONG data_count) override;
static void RegisterActivator();
static void UnregisterActivator();
private:
static DWORD g_cookie_;
static bool g_registered_;
};
struct ActivationUserInput {
std::wstring key;
std::wstring value;
};
void HandleToastActivation(const std::wstring& invoked_args,
std::vector<ActivationUserInput> inputs);
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_NOTIFICATIONS_WIN_WINDOWS_TOAST_ACTIVATOR_H_

View File

@@ -111,43 +111,41 @@ const std::string FailureResultToString(HRESULT failure_reason) {
return hresult_str;
}
constexpr char kPlaceholderContent[] = "placeHolderContent";
constexpr char kContent[] = "content";
constexpr char kToast[] = "toast";
constexpr char kVisual[] = "visual";
constexpr char kBinding[] = "binding";
constexpr char kTemplate[] = "template";
constexpr char kToastText01[] = "ToastText01";
constexpr char kToastText02[] = "ToastText02";
constexpr char kToastImageAndText01[] = "ToastImageAndText01";
constexpr char kToastImageAndText02[] = "ToastImageAndText02";
constexpr char kToastTemplate[] = "ToastGeneric";
constexpr char kText[] = "text";
constexpr char kImage[] = "image";
constexpr char kPlacement[] = "placement";
constexpr char kAppLogoOverride[] = "appLogoOverride";
constexpr char kHintCrop[] = "hint-crop";
constexpr char kHintInputId[] = "hint-inputId";
constexpr char kHintCropNone[] = "none";
constexpr char kSrc[] = "src";
constexpr char kAudio[] = "audio";
constexpr char kSilent[] = "silent";
constexpr char kReply[] = "reply";
constexpr char kTrue[] = "true";
constexpr char kID[] = "id";
constexpr char kInput[] = "input";
constexpr char kType[] = "type";
constexpr char kSelection[] = "selection";
constexpr char kScenario[] = "scenario";
constexpr char kReminder[] = "reminder";
constexpr char kActions[] = "actions";
constexpr char kAction[] = "action";
constexpr char kActivationType[] = "activationType";
constexpr char kSystem[] = "system";
constexpr char kActivationTypeForeground[] = "foreground";
constexpr char kActivationTypeSystem[] = "system";
constexpr char kArguments[] = "arguments";
constexpr char kDismiss[] = "dismiss";
// The XML version header that has to be stripped from the output.
constexpr char kXmlVersionHeader[] = "<?xml version=\"1.0\"?>\n";
const char* GetTemplateType(bool two_lines, bool has_icon) {
if (has_icon) {
return two_lines ? kToastImageAndText02 : kToastImageAndText01;
}
return two_lines ? kToastText02 : kToastText01;
}
} // namespace
// static
@@ -171,8 +169,9 @@ WindowsToastNotification::GetToastTaskRunner() {
}
bool WindowsToastNotification::Initialize() {
// Just initialize, don't care if it fails or already initialized.
Windows::Foundation::Initialize(RO_INIT_MULTITHREADED);
HRESULT hr = Windows::Foundation::Initialize(RO_INIT_SINGLETHREADED);
if (FAILED(hr))
Windows::Foundation::Initialize(RO_INIT_MULTITHREADED);
ScopedHString toast_manager_str(
RuntimeClass_Windows_UI_Notifications_ToastNotificationManager);
@@ -273,8 +272,8 @@ void WindowsToastNotification::CreateToastNotificationOnBackgroundThread(
DebugLog("CreateToastXmlOnBackgroundThread called");
ComPtr<IXmlDocument> toast_xml;
if (!CreateToastXmlDocument(options, presenter, weak_notification,
ui_task_runner, &toast_xml)) {
if (!CreateToastXmlDocument(options, presenter, notification_id,
weak_notification, ui_task_runner, &toast_xml)) {
return; // Error already posted to UI thread
}
@@ -300,13 +299,14 @@ void WindowsToastNotification::CreateToastNotificationOnBackgroundThread(
bool WindowsToastNotification::CreateToastXmlDocument(
const NotificationOptions& options,
NotificationPresenter* presenter,
const std::string& notification_id,
base::WeakPtr<Notification> weak_notification,
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
ComPtr<IXmlDocument>* toast_xml) {
// The custom xml takes priority over the preset template.
if (!options.toast_xml.empty()) {
DebugLog(base::StrCat({"Processing custom toast_xml, length: ",
base::NumberToString(options.toast_xml.length())}));
DebugLog("Toast XML (custom) id=" + notification_id + ": " +
base::UTF16ToUTF8(options.toast_xml));
HRESULT hr = XmlDocumentFromString(base::as_wcstr(options.toast_xml),
toast_xml->GetAddressOf());
DebugLog(base::StrCat({"XmlDocumentFromString returned HRESULT: ",
@@ -316,7 +316,6 @@ bool WindowsToastNotification::CreateToastXmlDocument(
base::StrCat({"XML: Invalid XML, ERROR ", FailureResultToString(hr)});
DebugLog(base::StrCat({"XML parsing failed, posting error: ", err}));
PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
DebugLog("PostNotificationFailedToUIThread called");
return false;
}
DebugLog("XML parsing succeeded");
@@ -325,8 +324,13 @@ bool WindowsToastNotification::CreateToastXmlDocument(
std::wstring icon_path =
presenter_win->SaveIconToFilesystem(options.icon, options.icon_url);
std::u16string toast_xml_str =
GetToastXml(options.title, options.msg, icon_path, options.timeout_type,
options.silent);
GetToastXml(notification_id, options.title, options.msg, icon_path,
options.timeout_type, options.silent, options.actions,
options.has_reply, options.reply_placeholder);
DebugLog("Toast XML (generated) id=" + notification_id + ": " +
base::UTF16ToUTF8(toast_xml_str));
HRESULT hr = XmlDocumentFromString(base::as_wcstr(toast_xml_str),
toast_xml->GetAddressOf());
if (FAILED(hr)) {
@@ -523,11 +527,15 @@ void WindowsToastNotification::Dismiss() {
}
std::u16string WindowsToastNotification::GetToastXml(
const std::string& notification_id,
const std::u16string& title,
const std::u16string& msg,
const std::wstring& icon_path,
const std::u16string& timeout_type,
bool silent) {
bool silent,
const std::vector<NotificationAction>& actions,
bool has_reply,
const std::u16string& reply_placeholder) {
XmlWriter xml_writer;
xml_writer.StartWriting();
@@ -543,36 +551,33 @@ std::u16string WindowsToastNotification::GetToastXml(
xml_writer.StartElement(kVisual);
// <binding template="<template>">
xml_writer.StartElement(kBinding);
const bool two_lines = (!title.empty() && !msg.empty());
xml_writer.AddAttribute(kTemplate,
GetTemplateType(two_lines, !icon_path.empty()));
xml_writer.AddAttribute(kTemplate, kToastTemplate);
// Add text nodes.
std::u16string line1;
std::u16string line2;
if (title.empty() || msg.empty()) {
line1 = title.empty() ? msg : title;
if (line1.empty())
line1 = u"[no message]";
// <text>
xml_writer.StartElement(kText);
xml_writer.AddAttribute(kID, "1");
xml_writer.AppendElementContent(base::UTF16ToUTF8(line1));
xml_writer.EndElement(); // </text>
} else {
line1 = title;
line2 = msg;
// <text>
xml_writer.StartElement(kText);
xml_writer.AddAttribute(kID, "1");
xml_writer.AppendElementContent(base::UTF16ToUTF8(line1));
xml_writer.EndElement();
xml_writer.EndElement(); // </text>
// <text>
xml_writer.StartElement(kText);
xml_writer.AddAttribute(kID, "2");
xml_writer.AppendElementContent(base::UTF16ToUTF8(line2));
xml_writer.EndElement();
xml_writer.EndElement(); // </text>
}
// Optional icon as app logo override (small icon).
if (!icon_path.empty()) {
// <image>
xml_writer.StartElement(kImage);
xml_writer.AddAttribute(kID, "1");
xml_writer.AddAttribute(kPlacement, kAppLogoOverride);
@@ -584,15 +589,85 @@ std::u16string WindowsToastNotification::GetToastXml(
xml_writer.EndElement(); // </binding>
xml_writer.EndElement(); // </visual>
// <actions> (only to ensure reminder has a dismiss button).
if (is_reminder) {
if (is_reminder || has_reply || !actions.empty()) {
// <actions>
xml_writer.StartElement(kActions);
xml_writer.StartElement(kAction);
xml_writer.AddAttribute(kActivationType, kSystem);
xml_writer.AddAttribute(kArguments, kDismiss);
xml_writer.AddAttribute(
"content", base::WideToUTF8(l10n_util::GetWideString(IDS_APP_CLOSE)));
xml_writer.EndElement(); // </action>
if (is_reminder) {
xml_writer.StartElement(kAction);
xml_writer.AddAttribute(kActivationType, kActivationTypeSystem);
xml_writer.AddAttribute(kArguments, kDismiss);
xml_writer.AddAttribute(
kContent, base::WideToUTF8(l10n_util::GetWideString(IDS_APP_CLOSE)));
xml_writer.EndElement(); // </action>
}
if (has_reply) {
// <input>
xml_writer.StartElement(kInput);
xml_writer.AddAttribute(kID, kReply);
xml_writer.AddAttribute(kType, kText);
if (!reply_placeholder.empty()) {
xml_writer.AddAttribute(kPlaceholderContent,
base::UTF16ToUTF8(reply_placeholder));
}
xml_writer.EndElement(); // </input>
}
for (size_t i = 0; i < actions.size(); ++i) {
const auto& act = actions[i];
if (act.type == u"button" || act.type.empty()) {
// <action>
xml_writer.StartElement(kAction);
xml_writer.AddAttribute(kActivationType, kActivationTypeForeground);
std::string args = base::StrCat(
{"type=action&action=", base::NumberToString(i),
"&tag=", base::NumberToString(base::FastHash(notification_id))});
xml_writer.AddAttribute(kArguments, args.c_str());
xml_writer.AddAttribute(kContent, base::UTF16ToUTF8(act.text));
xml_writer.EndElement(); // <action>
} else if (act.type == u"selection") {
std::string input_id =
base::StrCat({kSelection, base::NumberToString(i)});
xml_writer.StartElement(kInput); // <input>
xml_writer.AddAttribute(kID, input_id.c_str());
xml_writer.AddAttribute(kType, kSelection);
for (size_t opt_i = 0; opt_i < act.items.size(); ++opt_i) {
xml_writer.StartElement(kSelection); // <selection>
xml_writer.AddAttribute(kID, base::NumberToString(opt_i).c_str());
xml_writer.AddAttribute(kContent,
base::UTF16ToUTF8(act.items[opt_i]));
xml_writer.EndElement(); // </selection>
}
xml_writer.EndElement(); // </input>
// The button that submits the selection.
xml_writer.StartElement(kAction);
xml_writer.AddAttribute(kActivationType, kActivationTypeForeground);
std::string args = base::StrCat(
{"type=action&action=", base::NumberToString(i),
"&tag=", base::NumberToString(base::FastHash(notification_id))});
xml_writer.AddAttribute(kArguments, args.c_str());
xml_writer.AddAttribute(
kContent,
base::UTF16ToUTF8(act.text.empty() ? u"Select" : act.text));
xml_writer.AddAttribute(kHintInputId, input_id.c_str());
xml_writer.EndElement(); // </action>
}
}
if (has_reply) {
// <action>
xml_writer.StartElement(kAction);
xml_writer.AddAttribute(kActivationType, kActivationTypeForeground);
std::string args =
base::StrCat({"type=reply&tag=",
base::NumberToString(base::FastHash(notification_id))});
xml_writer.AddAttribute(kArguments, args.c_str());
// TODO(codebytere): we should localize this.
xml_writer.AddAttribute(kContent, "Reply");
xml_writer.AddAttribute(kHintInputId, kReply);
xml_writer.EndElement(); // <action>
}
xml_writer.EndElement(); // </actions>
}
@@ -663,6 +738,57 @@ ToastEventHandler::~ToastEventHandler() = default;
IFACEMETHODIMP ToastEventHandler::Invoke(
winui::Notifications::IToastNotification* sender,
IInspectable* args) {
std::wstring arguments_w;
std::wstring tag_w;
std::wstring group_w;
if (args) {
Microsoft::WRL::ComPtr<winui::Notifications::IToastActivatedEventArgs>
activated_args;
if (SUCCEEDED(args->QueryInterface(IID_PPV_ARGS(&activated_args)))) {
HSTRING args_hs = nullptr;
if (SUCCEEDED(activated_args->get_Arguments(&args_hs)) && args_hs) {
UINT32 len = 0;
const wchar_t* raw = WindowsGetStringRawBuffer(args_hs, &len);
if (raw && len)
arguments_w.assign(raw, len);
}
}
}
if (sender) {
Microsoft::WRL::ComPtr<winui::Notifications::IToastNotification2> toast2;
if (SUCCEEDED(sender->QueryInterface(IID_PPV_ARGS(&toast2)))) {
HSTRING tag_hs = nullptr;
if (SUCCEEDED(toast2->get_Tag(&tag_hs)) && tag_hs) {
UINT32 len = 0;
const wchar_t* raw = WindowsGetStringRawBuffer(tag_hs, &len);
if (raw && len)
tag_w.assign(raw, len);
}
HSTRING group_hs = nullptr;
if (SUCCEEDED(toast2->get_Group(&group_hs)) && group_hs) {
UINT32 len = 0;
const wchar_t* raw = WindowsGetStringRawBuffer(group_hs, &len);
if (raw && len)
group_w.assign(raw, len);
}
}
}
std::string notif_id;
std::string notif_hash;
if (notification_) {
notif_id = notification_->notification_id();
notif_hash = base::NumberToString(base::FastHash(notif_id));
}
bool structured = arguments_w.find(L"&tag=") != std::wstring::npos ||
arguments_w.find(L"type=action") != std::wstring::npos ||
arguments_w.find(L"type=reply") != std::wstring::npos;
if (structured)
return S_OK;
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&Notification::NotificationClicked, notification_));

View File

@@ -60,11 +60,16 @@ class WindowsToastNotification : public Notification {
friend class ToastEventHandler;
HRESULT ShowInternal(const NotificationOptions& options);
static std::u16string GetToastXml(const std::u16string& title,
const std::u16string& msg,
const std::wstring& icon_path,
const std::u16string& timeout_type,
const bool silent);
static std::u16string GetToastXml(
const std::string& notification_id,
const std::u16string& title,
const std::u16string& msg,
const std::wstring& icon_path,
const std::u16string& timeout_type,
const bool silent,
const std::vector<NotificationAction>& actions,
bool has_reply,
const std::u16string& reply_placeholder);
static HRESULT XmlDocumentFromString(
const wchar_t* xmlString,
ABI::Windows::Data::Xml::Dom::IXmlDocument** doc);
@@ -77,6 +82,7 @@ class WindowsToastNotification : public Notification {
static bool CreateToastXmlDocument(
const NotificationOptions& options,
NotificationPresenter* presenter,
const std::string& notification_id,
base::WeakPtr<Notification> weak_notification,
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlDocument>* toast_xml);

View File

@@ -32,6 +32,8 @@ PCWSTR GetRawAppUserModelID();
bool GetAppUserModelID(ScopedHString* app_id);
void SetAppUserModelID(const std::wstring& name);
bool IsRunningInDesktopBridge();
PCWSTR GetAppToastActivatorCLSID();
void SetAppToastActivatorCLSID(const std::wstring& clsid);
#endif
} // namespace electron

View File

@@ -28,6 +28,11 @@ std::wstring& GetAppUserModelId() {
return *g_app_user_model_id;
}
std::wstring& GetToastActivatorCLSID() {
static base::NoDestructor<std::wstring> g_toast_activator_clsid;
return *g_toast_activator_clsid;
}
std::string GetApplicationName() {
auto* module = GetModuleHandle(nullptr);
std::unique_ptr<FileVersionInfo> info(
@@ -82,4 +87,37 @@ bool IsRunningInDesktopBridge() {
return result;
}
PCWSTR GetAppToastActivatorCLSID() {
if (GetToastActivatorCLSID().empty()) {
GUID guid;
if (SUCCEEDED(::CoCreateGuid(&guid))) {
wchar_t buf[64] = {0};
if (StringFromGUID2(guid, buf, std::size(buf)) > 0)
GetToastActivatorCLSID() = buf;
}
}
return GetToastActivatorCLSID().c_str();
}
void SetAppToastActivatorCLSID(const std::wstring& clsid) {
CLSID parsed;
if (SUCCEEDED(::CLSIDFromString(clsid.c_str(), &parsed))) {
// Normalize formatting.
wchar_t buf[64] = {0};
if (StringFromGUID2(parsed, buf, std::size(buf)) > 0)
GetToastActivatorCLSID() = buf;
} else {
// Try adding braces if user omitted them.
if (!clsid.empty() && clsid.front() != L'{') {
std::wstring with_braces = L"{" + clsid + L"}";
if (SUCCEEDED(::CLSIDFromString(with_braces.c_str(), &parsed))) {
wchar_t buf[64] = {0};
if (StringFromGUID2(parsed, buf, std::size(buf)) > 0)
GetToastActivatorCLSID() = buf;
}
}
}
}
} // namespace electron