mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
feat: implements cold COM activation (#49919)
* fix: implements cold COM activation * fix: code review feedack
This commit is contained in:
@@ -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)
|
||||
|
||||
9
docs/api/structures/activation-arguments.md
Normal file
9
docs/api/structures/activation-arguments.md
Normal 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.
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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_
|
||||
|
||||
10
typings/internal-ambient.d.ts
vendored
10
typings/internal-ambient.d.ts
vendored
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user