mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
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.
437 lines
14 KiB
C++
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)
|