mirror of
https://github.com/electron/electron.git
synced 2026-02-19 03:14:51 -05:00
feat: improve Windows Toast actions support (#48132)
* feat: improve Windows Toast actions support * fix: ensure MSIX compatibility * test: add bad clsid format test
This commit is contained in:
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
429
shell/browser/notifications/win/windows_toast_activator.cc
Normal file
429
shell/browser/notifications/win/windows_toast_activator.cc
Normal 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
|
||||
49
shell/browser/notifications/win/windows_toast_activator.h
Normal file
49
shell/browser/notifications/win/windows_toast_activator.h
Normal 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_
|
||||
@@ -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_));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user