feat: implements cold COM activation (#49919)

* fix: implements cold COM activation

* fix: code review feedack
This commit is contained in:
Jan Hannemann
2026-03-05 14:30:04 -08:00
committed by GitHub
parent d6fc627ba5
commit ddefb54c8f
8 changed files with 296 additions and 8 deletions

View File

@@ -36,6 +36,46 @@ The `Notification` class has the following static methods:
Returns `boolean` - Whether or not desktop notifications are supported on the current system
#### `Notification.handleActivation(callback)` _Windows_
* `callback` Function
* `details` [ActivationArguments](structures/activation-arguments.md) - Details about the notification activation.
Registers a callback to handle all notification activations. The callback is invoked whenever a
notification is clicked, replied to, or has an action button pressed - regardless of whether
the original `Notification` object is still in memory.
This method handles timing automatically:
* If an activation already occurred before calling this method, the callback is invoked immediately
with those details.
* For all subsequent activations, the callback is invoked when they occur.
The callback remains registered until replaced by another call to `handleActivation`.
This provides a centralized way to handle notification interactions that works in all scenarios:
* Cold start (app launched from notification click)
* Notifications persisted in AC that have no in-memory representation after app re-start
* Notification object was garbage collected
* Notification object is still in memory (callback is invoked in addition to instance events)
```js
const { Notification, app } = require('electron')
app.whenReady().then(() => {
// Register handler for all notification activations
Notification.handleActivation((details) => {
console.log('Notification activated:', details.type)
if (details.type === 'reply') {
console.log('User reply:', details.reply)
} else if (details.type === 'action') {
console.log('Action index:', details.actionIndex)
}
})
})
```
### `new Notification([options])`
* `options` Object (optional)

View File

@@ -0,0 +1,9 @@
# ActivationArguments Object
> Used on Windows only.
* `type` string - The type of activation that launched the app: `'click'`, `'action'`, or `'reply'`.
* `arguments` string - The raw activation arguments string from Windows.
* `actionIndex` number (optional) - For `'action'` type, the index of the button that was clicked.
* `reply` string (optional) - For `'reply'` type, the text the user entered in the reply field.
* `userInputs` Record\<string, string\> (optional) - A dictionary of all user inputs from the notification.

View File

@@ -1,8 +1,10 @@
const {
Notification: ElectronNotification,
isSupported
} = process._linkedBinding('electron_browser_notification');
const binding = process._linkedBinding('electron_browser_notification');
ElectronNotification.isSupported = isSupported;
const ElectronNotification = binding.Notification;
ElectronNotification.isSupported = binding.isSupported;
if (process.platform === 'win32' && binding.handleActivation) {
ElectronNotification.handleActivation = binding.handleActivation;
}
export default ElectronNotification;

View File

@@ -4,7 +4,11 @@
#include "shell/browser/api/electron_api_notification.h"
#include "base/functional/bind.h"
#include "base/uuid.h"
#include "build/build_config.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "shell/browser/api/electron_api_menu.h"
#include "shell/browser/browser.h"
#include "shell/browser/electron_browser_client.h"
@@ -16,6 +20,14 @@
#include "shell/common/node_includes.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_WIN)
#include <windows.h>
#include "base/no_destructor.h"
#include "shell/browser/javascript_environment.h"
#include "shell/browser/notifications/win/windows_toast_activator.h"
#endif
namespace gin {
template <>
@@ -252,6 +264,79 @@ bool Notification::IsSupported() {
->GetNotificationPresenter();
}
#if BUILDFLAG(IS_WIN)
namespace {
// Helper to convert ActivationArguments to JS object
v8::Local<v8::Value> ActivationArgumentsToV8(
v8::Isolate* isolate,
const electron::ActivationArguments& details) {
gin_helper::Dictionary dict = gin_helper::Dictionary::CreateEmpty(isolate);
dict.Set("type", details.type);
dict.Set("arguments", details.arguments);
if (details.type == "action") {
dict.Set("actionIndex", details.action_index);
} else if (details.type == "reply") {
dict.Set("reply", details.reply);
}
if (!details.user_inputs.empty()) {
gin_helper::Dictionary inputs =
gin_helper::Dictionary::CreateEmpty(isolate);
for (const auto& [key, value] : details.user_inputs) {
inputs.Set(key, value);
}
dict.Set("userInputs", inputs);
}
return dict.GetHandle();
}
// Storage for the JavaScript callback (persistent so it survives GC).
// Uses base::NoDestructor to avoid exit-time destructor issues with globals.
// v8::Global supports Reset() for reassignment.
base::NoDestructor<v8::Global<v8::Function>> g_js_launch_callback;
void InvokeJsCallback(const electron::ActivationArguments& details) {
if (g_js_launch_callback->IsEmpty())
return;
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = isolate->GetCurrentContext();
if (context.IsEmpty())
return;
v8::Context::Scope context_scope(context);
v8::Local<v8::Function> callback = g_js_launch_callback->Get(isolate);
v8::Local<v8::Value> argv[] = {ActivationArgumentsToV8(isolate, details)};
v8::TryCatch try_catch(isolate);
callback->Call(context, v8::Undefined(isolate), 1, argv)
.FromMaybe(v8::Local<v8::Value>());
// Callback stays registered for future activations
}
} // namespace
// static
void Notification::HandleActivation(v8::Isolate* isolate,
v8::Local<v8::Function> callback) {
// Replace any previous callback using Reset (v8::Global supports this)
g_js_launch_callback->Reset(isolate, callback);
// Register the C++ callback that invokes the JS callback.
// - If activation details already exist, callback is invoked immediately.
// - Callback remains registered for all future activations.
electron::SetActivationHandler(
[](const electron::ActivationArguments& details) {
InvokeJsCallback(details);
});
}
#endif
void Notification::FillObjectTemplate(v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> templ) {
gin::ObjectTemplateBuilder(isolate, GetClassName(), templ)
@@ -300,6 +385,9 @@ void Initialize(v8::Local<v8::Object> exports,
gin_helper::Dictionary dict{isolate, exports};
dict.Set("Notification", Notification::GetConstructor(isolate, context));
dict.SetMethod("isSupported", &Notification::IsSupported);
#if BUILDFLAG(IS_WIN)
dict.SetMethod("handleActivation", &Notification::HandleActivation);
#endif
}
} // namespace

View File

@@ -9,6 +9,7 @@
#include <vector>
#include "base/memory/raw_ptr.h"
#include "build/build_config.h"
#include "shell/browser/event_emitter_mixin.h"
#include "shell/browser/notifications/notification.h"
#include "shell/browser/notifications/notification_delegate.h"
@@ -38,6 +39,16 @@ class Notification final : public gin_helper::DeprecatedWrappable<Notification>,
public:
static bool IsSupported();
#if BUILDFLAG(IS_WIN)
// Register a callback to handle all notification activations.
// The callback is invoked for every activation (click, reply, action)
// regardless of whether the Notification object is still in memory.
// If an activation already occurred, callback is invoked immediately.
// Callback remains registered until replaced by another call.
static void HandleActivation(v8::Isolate* isolate,
v8::Local<v8::Function> callback);
#endif
// gin_helper::Constructible
static gin_helper::Handle<Notification> New(gin_helper::ErrorThrower thrower,
gin::Arguments* args);

View File

@@ -15,6 +15,7 @@
#undef StrCat
#endif
#include <memory>
#include <string>
#include <vector>
@@ -26,6 +27,7 @@
#include "base/hash/hash.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_number_conversions_win.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
@@ -45,8 +47,43 @@
namespace electron {
ActivationArguments::ActivationArguments() = default;
ActivationArguments::~ActivationArguments() = default;
ActivationArguments::ActivationArguments(const ActivationArguments&) = default;
ActivationArguments& ActivationArguments::operator=(
const ActivationArguments&) = default;
// Use NoDestructor to avoid exit-time destructor issues with globals.
// unique_ptr provides automatic memory management.
base::NoDestructor<std::unique_ptr<ActivationArguments>> g_activation_arguments;
base::NoDestructor<std::unique_ptr<ActivationCallback>> g_launch_callback;
void SetActivationHandler(ActivationCallback callback) {
*g_launch_callback =
std::make_unique<ActivationCallback>(std::move(callback));
// If we already have stored details (late subscription), invoke immediately
if (*g_activation_arguments) {
(**g_launch_callback)(**g_activation_arguments);
// Clear the details after handling
g_activation_arguments->reset();
}
}
namespace {
ActivationArguments& GetOrCreateActivationArguments() {
if (!*g_activation_arguments)
*g_activation_arguments = std::make_unique<ActivationArguments>();
return **g_activation_arguments;
}
void DebugLog(std::string_view log_msg) {
if (electron::debug_notifications) {
LOG(INFO) << log_msg;
}
}
class NotificationActivatorFactory final : public IClassFactory {
public:
NotificationActivatorFactory() : ref_count_(1) {}
@@ -323,6 +360,11 @@ IFACEMETHODIMP NotificationActivator::Activate(
std::wstring args = invoked_args ? invoked_args : L"";
std::wstring aumid = app_user_model_id ? app_user_model_id : L"";
DebugLog("=== NotificationActivator::Activate CALLED ===");
DebugLog(" AUMID: " + base::WideToUTF8(aumid));
DebugLog(" Args: " + base::WideToUTF8(args));
DebugLog(" Data count: " + base::NumberToString(data_count));
std::vector<ActivationUserInput> copied_inputs;
if (data && data_count) {
std::vector<NOTIFICATION_USER_INPUT_DATA> temp;
@@ -347,6 +389,10 @@ IFACEMETHODIMP NotificationActivator::Activate(
void HandleToastActivation(const std::wstring& invoked_args,
std::vector<ActivationUserInput> inputs) {
DebugLog("=== HandleToastActivation CALLED ===");
DebugLog(" invoked_args: " + base::WideToUTF8(invoked_args));
DebugLog(" inputs count: " + base::NumberToString(inputs.size()));
// Expected invoked_args format:
// type=<click|action|reply>&action=<index>&tag=<hash> Parse simple key=value
// pairs separated by '&'.
@@ -382,14 +428,68 @@ void HandleToastActivation(const std::wstring& invoked_args,
}
}
auto build_activation_args = [&]() -> ActivationArguments {
ActivationArguments args;
args.arguments = base::WideToUTF8(invoked_args);
if (type == L"action") {
args.type = "action";
args.action_index = action_index;
} else if (type == L"reply" || !reply_text.empty()) {
args.type = "reply";
args.reply = reply_text;
} else {
args.type = "click";
}
// Store all user inputs
for (const auto& entry : inputs) {
args.user_inputs[base::WideToUTF8(entry.key)] =
base::WideToUTF8(entry.value);
}
return args;
};
// Helper to invoke or store callback
auto handle_callback = [&](const ActivationArguments& args) {
if (*g_launch_callback) {
// Callback registered - invoke it (callback stays registered for future)
DebugLog("Invoking registered activation callback");
(**g_launch_callback)(args);
// Clear any stored details (callback handled it)
g_activation_arguments->reset();
} else {
// No callback yet - store details for late subscription
DebugLog("Storing activation details (no callback registered yet)");
auto& details = GetOrCreateActivationArguments();
details = args;
}
};
auto* browser_client =
static_cast<ElectronBrowserClient*>(ElectronBrowserClient::Get());
if (!browser_client)
DebugLog(std::string("browser_client = ") +
(browser_client ? "valid" : "NULL"));
if (!browser_client) {
// App not fully initialized - store for later retrieval
DebugLog("App not initialized - storing details");
handle_callback(build_activation_args());
return;
}
NotificationPresenter* presenter = browser_client->GetNotificationPresenter();
if (!presenter)
DebugLog(std::string("presenter = ") + (presenter ? "valid" : "NULL"));
if (!presenter) {
// Presenter not ready - store for later retrieval
DebugLog("Presenter not ready - storing details");
handle_callback(build_activation_args());
return;
}
ActivationArguments activation_args = build_activation_args();
DebugLog("Activation: type=" + activation_args.type);
handle_callback(activation_args);
Notification* target = nullptr;
for (auto* n : presenter->notifications()) {
@@ -401,9 +501,12 @@ void HandleToastActivation(const std::wstring& invoked_args,
}
}
if (!target)
if (!target) {
DebugLog("No matching Notification object found");
return;
}
DebugLog("Dispatching to Notification object delegate");
if (type == L"action" && target->delegate()) {
int selection_index = -1;
for (const auto& entry : inputs) {

View File

@@ -8,6 +8,8 @@
#include <NotificationActivationCallback.h>
#include <windows.h>
#include <wrl/implements.h>
#include <functional>
#include <map>
#include <string>
#include <vector>
@@ -41,9 +43,32 @@ struct ActivationUserInput {
std::wstring value;
};
// Arguments from a notification activation when the app was launched cold
// (no existing notification object to receive the event)
struct ActivationArguments {
ActivationArguments();
~ActivationArguments();
ActivationArguments(const ActivationArguments&);
ActivationArguments& operator=(const ActivationArguments&);
std::string type; // "click", "action", or "reply"
int action_index = -1; // For action type, the button index
std::string reply; // For reply type, the user's reply text
std::string arguments; // Raw activation arguments
std::map<std::string, std::string> user_inputs; // All user inputs
};
void HandleToastActivation(const std::wstring& invoked_args,
std::vector<ActivationUserInput> inputs);
// Callback type for launch activation handler
using ActivationCallback = std::function<void(const ActivationArguments&)>;
// Set a callback to handle notification activation.
// If details already exist, callback is invoked immediately.
// Callback remains registered for all future activations.
void SetActivationHandler(ActivationCallback callback);
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_NOTIFICATIONS_WIN_WINDOWS_TOAST_ACTIVATOR_H_

View File

@@ -108,9 +108,19 @@ declare namespace NodeJS {
resolveHost(host: string, options?: Electron.ResolveHostOptions): Promise<Electron.ResolvedHost>;
}
interface ActivationArgumentsInternal {
type: string;
arguments: string;
actionIndex?: number;
reply?: string;
userInputs?: Record<string, string>;
}
interface NotificationBinding {
isSupported(): boolean;
Notification: typeof Electron.Notification;
// Windows-only callback for cold-start notification activation
handleActivation?: (callback: (details: ActivationArgumentsInternal) => void) => void;
}
interface PowerMonitorBinding extends Electron.PowerMonitor {