Compare commits

...

1 Commits

Author SHA1 Message Date
Shelley Vohr
6decef5b5b feat: expose Notification.getHistory() 2026-02-12 16:04:20 +01:00
7 changed files with 351 additions and 1 deletions

View File

@@ -30,6 +30,12 @@ The `Notification` class has the following static methods:
Returns `boolean` - Whether or not desktop notifications are supported on the current system
#### `Notification.getHistory()` _Windows_
Returns `Notification[]` - the notification history, for all notifications sent by this app, from Action Center.
See [`ToastNotificationHistory.GetHistory`](https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toastnotificationhistory.gethistory?view=winrt-26100#windows-ui-notifications-toastnotificationhistory-gethistory) for more information
### `new Notification([options])`
* `options` Object (optional)

View File

@@ -1,8 +1,10 @@
const {
Notification: ElectronNotification,
isSupported
isSupported,
getHistory
} = process._linkedBinding('electron_browser_notification');
ElectronNotification.isSupported = isSupported;
ElectronNotification.getHistory = getHistory;
export default ElectronNotification;

View File

@@ -16,6 +16,10 @@
#include "shell/common/node_includes.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_WIN)
#include "shell/browser/notifications/win/windows_toast_notification.h"
#endif
namespace gin {
template <>
@@ -43,6 +47,29 @@ struct Converter<electron::NotificationAction> {
}
};
#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 {
@@ -208,6 +235,15 @@ bool Notification::IsSupported() {
->GetNotificationPresenter();
}
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)
@@ -256,6 +292,7 @@ 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);
dict.SetMethod("getHistory", &Notification::GetHistory);
}
} // namespace

View File

@@ -37,6 +37,7 @@ class Notification final : public gin_helper::DeprecatedWrappable<Notification>,
public NotificationDelegate {
public:
static bool IsSupported();
static v8::Local<v8::Value> GetHistory(v8::Isolate* isolate);
// gin_helper::Constructible
static gin_helper::Handle<Notification> New(gin_helper::ErrorThrower thrower,

View File

@@ -9,8 +9,12 @@
#include "shell/browser/notifications/win/windows_toast_notification.h"
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <shlobj.h>
#include <windows.data.xml.dom.h>
#include <windows.foundation.collections.h>
#include <wrl\wrappers\corewrappers.h>
#include "base/containers/fixed_flat_map.h"
@@ -59,6 +63,13 @@ namespace winui = ABI::Windows::UI;
namespace electron {
ToastHistoryEntry::ToastHistoryEntry() = default;
ToastHistoryEntry::~ToastHistoryEntry() = default;
ToastHistoryEntry::ToastHistoryEntry(const ToastHistoryEntry&) = default;
ToastHistoryEntry& ToastHistoryEntry::operator=(const ToastHistoryEntry&) = default;
ToastHistoryEntry::ToastHistoryEntry(ToastHistoryEntry&&) = default;
ToastHistoryEntry& ToastHistoryEntry::operator=(ToastHistoryEntry&&) = default;
namespace {
// This string needs to be max 16 characters to work on Windows 10 prior to
@@ -148,6 +159,61 @@ const char* GetTemplateType(bool two_lines, bool has_icon) {
return two_lines ? kToastText02 : kToastText01;
}
std::u16string HstringToU16(HSTRING value) {
if (!value)
return {};
unsigned int length = 0;
const wchar_t* buffer = WindowsGetStringRawBuffer(value, &length);
return std::u16string(reinterpret_cast<const char16_t*>(buffer), length);
}
std::u16string GetAttributeValue(
const ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNode>& node,
const wchar_t* name) {
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlElement> element;
if (FAILED(node.As(&element)))
return {};
HSTRING value = nullptr;
if (FAILED(element->GetAttribute(HStringReference(name).Get(), &value)) ||
!value) {
return {};
}
auto result = HstringToU16(value);
WindowsDeleteString(value);
return result;
}
std::u16string GetInnerText(
const ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNode>& node) {
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNodeSerializer> serializer;
if (FAILED(node.As(&serializer)))
return {};
HSTRING value = nullptr;
if (FAILED(serializer->get_InnerText(&value)) || !value)
return {};
auto result = HstringToU16(value);
WindowsDeleteString(value);
return result;
}
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNodeList> SelectNodes(
const ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNode>& node,
const wchar_t* xpath) {
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNodeSelector> selector;
if (FAILED(node.As(&selector)))
return nullptr;
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNodeList> list;
if (FAILED(selector->SelectNodes(HStringReference(xpath).Get(), &list)))
return nullptr;
return list;
}
} // namespace
// static
@@ -210,6 +276,219 @@ bool WindowsToastNotification::Initialize() {
}
}
std::vector<ToastHistoryEntry> WindowsToastNotification::GetHistory() {
std::vector<ToastHistoryEntry> history;
if (!Initialize())
return history;
ComPtr<winui::Notifications::IToastNotificationManagerStatics2>
toast_manager2;
if (FAILED(toast_manager_->As(&toast_manager2)))
return history;
ComPtr<winui::Notifications::IToastNotificationHistory> notification_history;
if (FAILED(toast_manager2->get_History(&notification_history)))
return history;
ComPtr<winui::Notifications::IToastNotificationHistory2> notification_history2;
if (FAILED(notification_history.As(&notification_history2)))
return history;
ComPtr<ABI::Windows::Foundation::Collections::IVectorView<
winui::Notifications::ToastNotification*>>
toast_history;
ScopedHString app_id;
if (!GetAppUserModelID(&app_id))
return history;
HRESULT hr = notification_history2->GetHistoryWithId(app_id, &toast_history);
if (FAILED(hr) || !toast_history)
return history;
unsigned int size = 0;
if (FAILED(toast_history->get_Size(&size)))
return history;
history.reserve(size);
for (unsigned int i = 0; i < size; ++i) {
ComPtr<winui::Notifications::IToastNotification> toast;
if (FAILED(toast_history->GetAt(i, &toast)))
continue;
ToastHistoryEntry entry;
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlDocument> xml_content;
if (FAILED(toast->get_Content(&xml_content)))
continue;
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNodeSerializer> serializer;
if (SUCCEEDED(xml_content.As(&serializer))) {
HSTRING xml = nullptr;
if (SUCCEEDED(serializer->GetXml(&xml)) && xml) {
entry.toast_xml = HstringToU16(xml);
WindowsDeleteString(xml);
}
}
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNode> content_node;
if (FAILED(xml_content.As(&content_node)))
continue;
if (auto toast_nodes = SelectNodes(content_node, L"/toast")) {
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNode> toast_node;
if (SUCCEEDED(toast_nodes->Item(0, &toast_node)) && toast_node) {
auto scenario = GetAttributeValue(toast_node, L"scenario");
if (scenario == u"reminder")
entry.timeout_type = u"never";
}
}
if (entry.timeout_type.empty())
entry.timeout_type = u"default";
if (auto text_nodes =
SelectNodes(content_node, L"/toast/visual/binding/text")) {
unsigned int text_size = 0;
if (SUCCEEDED(text_nodes->get_Length(&text_size)) && text_size > 0) {
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNode> text_node;
if (SUCCEEDED(text_nodes->Item(0, &text_node)) && text_node) {
entry.title = GetInnerText(text_node);
}
if (text_size > 1 && SUCCEEDED(text_nodes->Item(1, &text_node)) &&
text_node) {
entry.body = GetInnerText(text_node);
}
}
}
if (auto image_nodes =
SelectNodes(content_node, L"/toast/visual/binding/image")) {
unsigned int image_size = 0;
if (SUCCEEDED(image_nodes->get_Length(&image_size))) {
for (unsigned int idx = 0; idx < image_size; ++idx) {
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNode> image_node;
if (FAILED(image_nodes->Item(idx, &image_node)) || !image_node)
continue;
auto placement = GetAttributeValue(image_node, L"placement");
auto src = GetAttributeValue(image_node, L"src");
if (!src.empty() &&
(placement.empty() || placement == u"appLogoOverride")) {
entry.icon_path = src;
break;
}
}
}
}
if (auto audio_nodes = SelectNodes(content_node, L"/toast/audio")) {
unsigned int audio_size = 0;
if (SUCCEEDED(audio_nodes->get_Length(&audio_size)) && audio_size > 0) {
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNode> audio_node;
if (SUCCEEDED(audio_nodes->Item(0, &audio_node)) && audio_node) {
auto silent = GetAttributeValue(audio_node, L"silent");
entry.silent = (silent == u"true");
auto src = GetAttributeValue(audio_node, L"src");
if (!src.empty())
entry.sound = src;
}
}
}
std::unordered_map<std::u16string, std::vector<std::u16string>>
selection_inputs;
if (auto input_nodes = SelectNodes(content_node, L"/toast/actions/input")) {
unsigned int input_size = 0;
if (SUCCEEDED(input_nodes->get_Length(&input_size))) {
for (unsigned int idx = 0; idx < input_size; ++idx) {
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNode> input_node;
if (FAILED(input_nodes->Item(idx, &input_node)) || !input_node)
continue;
auto type = GetAttributeValue(input_node, L"type");
auto id = GetAttributeValue(input_node, L"id");
if (type == u"text" && id == u"reply") {
entry.has_reply = true;
entry.reply_placeholder =
GetAttributeValue(input_node, L"placeHolderContent");
continue;
}
if (type == u"selection" && !id.empty()) {
std::vector<std::u16string> items;
if (auto selection_nodes = SelectNodes(input_node, L"selection")) {
unsigned int sel_size = 0;
if (SUCCEEDED(selection_nodes->get_Length(&sel_size))) {
for (unsigned int sel_i = 0; sel_i < sel_size; ++sel_i) {
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNode> sel_node;
if (FAILED(selection_nodes->Item(sel_i, &sel_node)) ||
!sel_node) {
continue;
}
auto sel_content = GetAttributeValue(sel_node, L"content");
if (!sel_content.empty())
items.push_back(sel_content);
}
}
}
selection_inputs.emplace(id, std::move(items));
}
}
}
}
std::unordered_set<std::u16string> used_selection_ids;
if (auto action_nodes =
SelectNodes(content_node, L"/toast/actions/action")) {
unsigned int action_size = 0;
if (SUCCEEDED(action_nodes->get_Length(&action_size))) {
for (unsigned int idx = 0; idx < action_size; ++idx) {
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNode> action_node;
if (FAILED(action_nodes->Item(idx, &action_node)) || !action_node)
continue;
auto activation = GetAttributeValue(action_node, L"activationType");
auto arguments = GetAttributeValue(action_node, L"arguments");
auto action_content = GetAttributeValue(action_node, L"content");
auto hint_input_id = GetAttributeValue(action_node, L"hint-inputId");
if (activation == u"system" && arguments == u"dismiss") {
entry.close_button_text = action_content;
continue;
}
if (hint_input_id == u"reply") {
entry.has_reply = true;
continue;
}
if (!hint_input_id.empty()) {
auto it = selection_inputs.find(hint_input_id);
if (it != selection_inputs.end() &&
!used_selection_ids.contains(hint_input_id)) {
NotificationAction action;
action.type = u"selection";
action.text = action_content;
// Note: selection items not stored in NotificationAction
entry.actions.push_back(std::move(action));
used_selection_ids.insert(hint_input_id);
continue;
}
}
if (!action_content.empty()) {
NotificationAction action;
action.type = u"button";
action.text = action_content;
entry.actions.push_back(std::move(action));
}
}
}
}
history.push_back(std::move(entry));
}
return history;
}
WindowsToastNotification::WindowsToastNotification(
NotificationDelegate* delegate,
NotificationPresenter* presenter)

View File

@@ -13,6 +13,7 @@
#include <windows.ui.notifications.h>
#include <wrl/implements.h>
#include <string>
#include <vector>
#include "base/memory/scoped_refptr.h"
#include "base/task/single_thread_task_runner.h"
@@ -28,6 +29,28 @@ namespace electron {
class ScopedHString;
struct ToastHistoryEntry {
ToastHistoryEntry();
~ToastHistoryEntry();
ToastHistoryEntry(const ToastHistoryEntry&);
ToastHistoryEntry& operator=(const ToastHistoryEntry&);
ToastHistoryEntry(ToastHistoryEntry&&);
ToastHistoryEntry& operator=(ToastHistoryEntry&&);
std::u16string toast_xml;
std::u16string title;
std::u16string body;
std::u16string icon_path;
std::u16string timeout_type;
std::u16string reply_placeholder;
std::u16string sound;
std::u16string urgency;
std::u16string close_button_text;
bool silent = false;
bool has_reply = false;
std::vector<NotificationAction> actions;
};
using DesktopToastActivatedEventHandler =
ABI::Windows::Foundation::ITypedEventHandler<
ABI::Windows::UI::Notifications::ToastNotification*,
@@ -45,6 +68,7 @@ class WindowsToastNotification : public Notification {
public:
// Should only be called by NotificationPresenterWin.
static bool Initialize();
static std::vector<ToastHistoryEntry> GetHistory();
WindowsToastNotification(NotificationDelegate* delegate,
NotificationPresenter* presenter);

View File

@@ -110,6 +110,7 @@ declare namespace NodeJS {
interface NotificationBinding {
isSupported(): boolean;
getHistory(): Electron.Notification[];
Notification: typeof Electron.Notification;
}