mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
feat: expose Notification.getHistory()
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
const {
|
||||
Notification: ElectronNotification,
|
||||
isSupported
|
||||
isSupported,
|
||||
getHistory
|
||||
} = process._linkedBinding('electron_browser_notification');
|
||||
|
||||
ElectronNotification.isSupported = isSupported;
|
||||
ElectronNotification.getHistory = getHistory;
|
||||
|
||||
export default ElectronNotification;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(¬ification_history)))
|
||||
return history;
|
||||
|
||||
ComPtr<winui::Notifications::IToastNotificationHistory2> notification_history2;
|
||||
if (FAILED(notification_history.As(¬ification_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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
1
typings/internal-ambient.d.ts
vendored
1
typings/internal-ambient.d.ts
vendored
@@ -110,6 +110,7 @@ declare namespace NodeJS {
|
||||
|
||||
interface NotificationBinding {
|
||||
isSupported(): boolean;
|
||||
getHistory(): Electron.Notification[];
|
||||
Notification: typeof Electron.Notification;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user