Files
electron/shell/browser/api/electron_api_notification.cc
Shelley Vohr 4abf4cf868 feat: expose Notification.getHistory() on Windows
Add a new `Notification.getHistory()` that retrieves all
notifications sent by the app from the Windows Action Center using
the `ToastNotificationHistory` API.

The implementation introduces a `ToastHistoryEntry` struct and parses
each toast's XML content to reconstruct notification properties
including title, body, icon, silent, hasReply, timeoutType, actions,
replyPlaceholder, sound, urgency, and closeButtonText. The raw toast
XML is also returned as `toastXml` for cases where the parsed fields
are insufficient.

Currently Windows-only; returns an empty array on other platforms.
2026-03-18 09:49:25 +01:00

437 lines
14 KiB
C++

// Copyright (c) 2014 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#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"
#include "shell/common/gin_converters/image_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/error_thrower.h"
#include "shell/common/gin_helper/handle.h"
#include "shell/common/gin_helper/object_template_builder.h"
#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"
#include "shell/browser/notifications/win/windows_toast_notification.h"
#endif
namespace gin {
template <>
struct Converter<electron::NotificationAction> {
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
electron::NotificationAction* out) {
gin::Dictionary dict(isolate);
if (!ConvertFromV8(isolate, val, &dict))
return false;
if (!dict.Get("type", &(out->type))) {
return false;
}
dict.Get("text", &(out->text));
std::vector<std::u16string> items;
if (dict.Get("items", &items))
out->items = std::move(items);
return true;
}
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
electron::NotificationAction val) {
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);
}
};
#if BUILDFLAG(IS_WIN)
template <>
struct Converter<electron::ToastHistoryEntry> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const electron::ToastHistoryEntry& entry) {
auto dict = gin::Dictionary::CreateEmpty(isolate);
dict.Set("title", entry.title);
dict.Set("body", entry.body);
dict.Set("icon", entry.icon_path);
dict.Set("silent", entry.silent);
dict.Set("hasReply", entry.has_reply);
dict.Set("timeoutType", entry.timeout_type);
dict.Set("replyPlaceholder", entry.reply_placeholder);
dict.Set("sound", entry.sound);
dict.Set("urgency", entry.urgency);
dict.Set("actions", entry.actions);
dict.Set("closeButtonText", entry.close_button_text);
dict.Set("toastXml", entry.toast_xml);
return ConvertToV8(isolate, dict);
}
};
#endif
} // namespace gin
namespace electron::api {
gin::DeprecatedWrapperInfo Notification::kWrapperInfo = {
gin::kEmbedderNativeGin};
Notification::Notification(gin::Arguments* args) {
presenter_ = static_cast<ElectronBrowserClient*>(ElectronBrowserClient::Get())
->GetNotificationPresenter();
gin::Dictionary opts(nullptr);
if (args->GetNext(&opts)) {
opts.Get("id", &id_);
opts.Get("groupId", &group_id_);
opts.Get("title", &title_);
opts.Get("subtitle", &subtitle_);
opts.Get("body", &body_);
opts.Get("icon", &icon_);
opts.Get("silent", &silent_);
opts.Get("replyPlaceholder", &reply_placeholder_);
opts.Get("urgency", &urgency_);
opts.Get("hasReply", &has_reply_);
opts.Get("timeoutType", &timeout_type_);
opts.Get("actions", &actions_);
opts.Get("sound", &sound_);
opts.Get("closeButtonText", &close_button_text_);
opts.Get("toastXml", &toast_xml_);
}
if (id_.empty())
id_ = base::Uuid::GenerateRandomV4().AsLowercaseString();
}
Notification::~Notification() {
if (notification_)
notification_->set_delegate(nullptr);
}
// static
gin_helper::Handle<Notification> Notification::New(
gin_helper::ErrorThrower thrower,
gin::Arguments* args) {
if (!Browser::Get()->is_ready()) {
thrower.ThrowError("Cannot create Notification before app is ready");
return {};
}
return gin_helper::CreateHandle(thrower.isolate(), new Notification(args));
}
// Setters
void Notification::SetTitle(const std::u16string& new_title) {
title_ = new_title;
}
void Notification::SetSubtitle(const std::u16string& new_subtitle) {
subtitle_ = new_subtitle;
}
void Notification::SetBody(const std::u16string& new_body) {
body_ = new_body;
}
void Notification::SetSilent(bool new_silent) {
silent_ = new_silent;
}
void Notification::SetHasReply(bool new_has_reply) {
has_reply_ = new_has_reply;
}
void Notification::SetTimeoutType(const std::u16string& new_timeout_type) {
timeout_type_ = new_timeout_type;
}
void Notification::SetReplyPlaceholder(const std::u16string& new_placeholder) {
reply_placeholder_ = new_placeholder;
}
void Notification::SetSound(const std::u16string& new_sound) {
sound_ = new_sound;
}
void Notification::SetUrgency(const std::u16string& new_urgency) {
urgency_ = new_urgency;
}
void Notification::SetActions(
const std::vector<electron::NotificationAction>& actions) {
actions_ = actions;
}
void Notification::SetCloseButtonText(const std::u16string& text) {
close_button_text_ = text;
}
void Notification::SetToastXml(const std::u16string& new_toast_xml) {
toast_xml_ = new_toast_xml;
}
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() {
Emit("click");
}
void Notification::NotificationReplied(const std::string& 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() {
Emit("show");
}
void Notification::NotificationFailed(const std::string& error) {
Emit("failed", error);
}
void Notification::NotificationDestroyed() {}
void Notification::NotificationClosed(const std::string& reason) {
if (reason.empty()) {
Emit("close");
} else {
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("reason", reason);
EmitWithoutEvent("close", event_object);
}
}
void Notification::Close() {
if (notification_) {
if (notification_->is_dismissed()) {
notification_->Remove();
} else {
notification_->Dismiss();
}
notification_->set_delegate(nullptr);
notification_.reset();
}
}
// Showing notifications
void Notification::Show() {
Close();
if (presenter_) {
notification_ = presenter_->CreateNotification(this, id_);
if (notification_) {
electron::NotificationOptions options;
options.title = title_;
options.subtitle = subtitle_;
options.msg = body_;
options.icon_url = GURL();
options.icon = icon_.AsBitmap();
options.silent = silent_;
options.has_reply = has_reply_;
options.timeout_type = timeout_type_;
options.reply_placeholder = reply_placeholder_;
options.actions = actions_;
options.sound = sound_;
options.close_button_text = close_button_text_;
options.urgency = urgency_;
options.toast_xml = toast_xml_;
options.group_id = group_id_;
notification_->Show(options);
}
}
}
bool Notification::IsSupported() {
return !!static_cast<ElectronBrowserClient*>(ElectronBrowserClient::Get())
->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
#endif
// 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);
});
}
v8::Local<v8::Value> Notification::GetHistory(v8::Isolate* isolate) {
#if BUILDFLAG(IS_WIN)
return gin::ConvertToV8(isolate,
electron::WindowsToastNotification::GetHistory());
#else
return v8::Array::New(isolate);
#endif
}
void Notification::FillObjectTemplate(v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> templ) {
gin::ObjectTemplateBuilder(isolate, GetClassName(), templ)
.SetMethod("show", &Notification::Show)
.SetMethod("close", &Notification::Close)
.SetProperty("id", &Notification::id)
.SetProperty("groupId", &Notification::group_id)
.SetProperty("title", &Notification::title, &Notification::SetTitle)
.SetProperty("subtitle", &Notification::subtitle,
&Notification::SetSubtitle)
.SetProperty("body", &Notification::body, &Notification::SetBody)
.SetProperty("silent", &Notification::is_silent, &Notification::SetSilent)
.SetProperty("hasReply", &Notification::has_reply,
&Notification::SetHasReply)
.SetProperty("timeoutType", &Notification::timeout_type,
&Notification::SetTimeoutType)
.SetProperty("replyPlaceholder", &Notification::reply_placeholder,
&Notification::SetReplyPlaceholder)
.SetProperty("urgency", &Notification::urgency, &Notification::SetUrgency)
.SetProperty("sound", &Notification::sound, &Notification::SetSound)
.SetProperty("actions", &Notification::actions, &Notification::SetActions)
.SetProperty("closeButtonText", &Notification::close_button_text,
&Notification::SetCloseButtonText)
.SetProperty("toastXml", &Notification::toast_xml,
&Notification::SetToastXml)
.Build();
}
const char* Notification::GetTypeName() {
return GetClassName();
}
void Notification::WillBeDestroyed() {
ClearWeak();
}
} // namespace electron::api
namespace {
using electron::api::Notification;
void Initialize(v8::Local<v8::Object> exports,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> context,
void* priv) {
v8::Isolate* const isolate = electron::JavascriptEnvironment::GetIsolate();
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);
dict.SetMethod("getHistory", &Notification::GetHistory);
#endif
}
} // namespace
NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_notification, Initialize)