Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Windows Toast notification dismissal from Action Center #40244

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/api/notification.md
Expand Up @@ -85,6 +85,8 @@ Emitted when the notification is closed by manual intervention from the user.
This event is not guaranteed to be emitted in all cases where the notification
is closed.

On Windows, the `close` event can be emitted in one of three ways: programmatic dismissal with `notification.close()`, by the user closing the notification, or via system timeout. If a notification is in the Action Center after the initial `close` event is emitted, a call to `notification.close()` will remove the notification from the action center but the `close` event will not be emitted again.

#### Event: 'reply' _macOS_

Returns:
Expand Down Expand Up @@ -127,6 +129,8 @@ shown notification and create a new one with identical properties.

Dismisses the notification.

On Windows, calling `notification.close()` while the notification is visible on screen will dismiss the notification and remove it from the Action Center. If `notification.close()` is called after the notification is no longer visible on screen, calling `notification.close()` will try remove it from the Action Center.

### Instance Properties

#### `notification.title`
Expand Down
6 changes: 5 additions & 1 deletion shell/browser/api/electron_api_notification.cc
Expand Up @@ -216,7 +216,11 @@ void Notification::NotificationClosed() {

void Notification::Close() {
if (notification_) {
notification_->Dismiss();
if (notification_->is_dismissed()) {
notification_->Remove();
} else {
notification_->Dismiss();
}
notification_->set_delegate(nullptr);
notification_.reset();
}
Expand Down
10 changes: 8 additions & 2 deletions shell/browser/notifications/notification.cc
Expand Up @@ -27,10 +27,14 @@ void Notification::NotificationClicked() {
Destroy();
}

void Notification::NotificationDismissed() {
void Notification::NotificationDismissed(bool should_destroy) {
if (delegate())
delegate()->NotificationClosed();
Destroy();

set_is_dismissed(true);

if (should_destroy)
Destroy();
}

void Notification::NotificationFailed(const std::string& error) {
Expand All @@ -39,6 +43,8 @@ void Notification::NotificationFailed(const std::string& error) {
Destroy();
}

void Notification::Remove() {}

void Notification::Destroy() {
presenter()->RemoveNotification(this);
}
Expand Down
15 changes: 12 additions & 3 deletions shell/browser/notifications/notification.h
Expand Up @@ -50,13 +50,19 @@ class Notification {

// Shows the notification.
virtual void Show(const NotificationOptions& options) = 0;
// Closes the notification, this instance will be destroyed after the
// notification gets closed.

// Dismisses the notification. On some platforms this will result in full
// removal and destruction of the notification, but if the initial dismissal
// does not fully get rid of the notification it will be destroyed in Remove.
virtual void Dismiss() = 0;

// Removes the notification if it was not fully removed during dismissal,
// as can happen on some platforms including Windows.
virtual void Remove();

// Should be called by derived classes.
void NotificationClicked();
void NotificationDismissed();
void NotificationDismissed(bool should_destroy = true);
void NotificationFailed(const std::string& error = "");

// delete this.
Expand All @@ -68,10 +74,12 @@ class Notification {

void set_delegate(NotificationDelegate* delegate) { delegate_ = delegate; }
void set_notification_id(const std::string& id) { notification_id_ = id; }
void set_is_dismissed(bool dismissed) { is_dismissed_ = dismissed; }

NotificationDelegate* delegate() const { return delegate_; }
NotificationPresenter* presenter() const { return presenter_; }
const std::string& notification_id() const { return notification_id_; }
bool is_dismissed() const { return is_dismissed_; }

// disable copy
Notification(const Notification&) = delete;
Expand All @@ -85,6 +93,7 @@ class Notification {
raw_ptr<NotificationDelegate> delegate_;
raw_ptr<NotificationPresenter> presenter_;
std::string notification_id_;
bool is_dismissed_ = false;

base::WeakPtrFactory<Notification> weak_factory_{this};
};
Expand Down
108 changes: 71 additions & 37 deletions shell/browser/notifications/win/windows_toast_notification.cc
Expand Up @@ -8,6 +8,8 @@

#include "shell/browser/notifications/win/windows_toast_notification.h"

#include <string_view>

#include <shlobj.h>
#include <wrl\wrappers\corewrappers.h>

Expand All @@ -34,6 +36,8 @@ using ABI::Windows::Data::Xml::Dom::IXmlNodeList;
using ABI::Windows::Data::Xml::Dom::IXmlText;
using Microsoft::WRL::Wrappers::HStringReference;

namespace winui = ABI::Windows::UI;

#define RETURN_IF_FAILED(hr) \
do { \
HRESULT _hrTemp = hr; \
Expand All @@ -47,8 +51,7 @@ using Microsoft::WRL::Wrappers::HStringReference;
std::string _msgTemp = msg; \
if (FAILED(_hrTemp)) { \
std::string _err = _msgTemp + ",ERROR " + std::to_string(_hrTemp); \
if (IsDebuggingNotifications()) \
LOG(INFO) << _err; \
DebugLog(_err); \
Notification::NotificationFailed(_err); \
return _hrTemp; \
} \
Expand All @@ -58,17 +61,23 @@ namespace electron {

namespace {

bool IsDebuggingNotifications() {
return base::Environment::Create()->HasVar("ELECTRON_DEBUG_NOTIFICATIONS");
// This string needs to be max 16 characters to work on Windows 10 prior to
// applying Creators Update (build 15063).
constexpr wchar_t kGroup[] = L"Notifications";

void DebugLog(std::string_view log_msg) {
if (base::Environment::Create()->HasVar("ELECTRON_DEBUG_NOTIFICATIONS"))
LOG(INFO) << log_msg;
}

} // namespace

// static
ComPtr<ABI::Windows::UI::Notifications::IToastNotificationManagerStatics>
ComPtr<winui::Notifications::IToastNotificationManagerStatics>
WindowsToastNotification::toast_manager_;

// static
ComPtr<ABI::Windows::UI::Notifications::IToastNotifier>
ComPtr<winui::Notifications::IToastNotifier>
WindowsToastNotification::toast_notifier_;

// static
Expand Down Expand Up @@ -112,17 +121,37 @@ WindowsToastNotification::~WindowsToastNotification() {

void WindowsToastNotification::Show(const NotificationOptions& options) {
if (SUCCEEDED(ShowInternal(options))) {
if (IsDebuggingNotifications())
LOG(INFO) << "Notification created";
DebugLog("Notification created");

if (delegate())
delegate()->NotificationDisplayed();
}
}

void WindowsToastNotification::Remove() {
DebugLog("Removing notification from action center");

ComPtr<winui::Notifications::IToastNotificationManagerStatics2>
toast_manager2;
if (FAILED(toast_manager_.As(&toast_manager2)))
return;

ComPtr<winui::Notifications::IToastNotificationHistory> notification_history;
if (FAILED(toast_manager2->get_History(&notification_history)))
return;

ScopedHString app_id;
if (!GetAppUserModelID(&app_id))
return;

ScopedHString group(kGroup);
ScopedHString tag(base::as_wcstr(base::UTF8ToUTF16(notification_id())));
notification_history->RemoveGroupedTagWithId(tag, group, app_id);
}

void WindowsToastNotification::Dismiss() {
if (IsDebuggingNotifications())
LOG(INFO) << "Hiding notification";
DebugLog("Hiding notification");

toast_notifier_->Hide(toast_notification_.Get());
}

Expand Down Expand Up @@ -151,8 +180,7 @@ HRESULT WindowsToastNotification::ShowInternal(
return E_FAIL;
}

ComPtr<ABI::Windows::UI::Notifications::IToastNotificationFactory>
toast_factory;
ComPtr<winui::Notifications::IToastNotificationFactory> toast_factory;
REPORT_AND_RETURN_IF_FAILED(
Windows::Foundation::GetActivationFactory(toast_str, &toast_factory),
"WinAPI: GetActivationFactory failed");
Expand All @@ -161,6 +189,19 @@ HRESULT WindowsToastNotification::ShowInternal(
toast_xml.Get(), &toast_notification_),
"WinAPI: CreateToastNotification failed");

ComPtr<winui::Notifications::IToastNotification2> toast2;
REPORT_AND_RETURN_IF_FAILED(
toast_notification_->QueryInterface(IID_PPV_ARGS(&toast2)),
"WinAPI: Getting Notification interface failed");

ScopedHString group(kGroup);
REPORT_AND_RETURN_IF_FAILED(toast2->put_Group(group),
"WinAPI: Setting group failed");

ScopedHString tag(base::as_wcstr(base::UTF8ToUTF16(notification_id())));
REPORT_AND_RETURN_IF_FAILED(toast2->put_Tag(tag),
"WinAPI: Setting tag failed");

REPORT_AND_RETURN_IF_FAILED(SetupCallbacks(toast_notification_.Get()),
"WinAPI: SetupCallbacks failed");

Expand All @@ -170,22 +211,20 @@ HRESULT WindowsToastNotification::ShowInternal(
}

HRESULT WindowsToastNotification::GetToastXml(
ABI::Windows::UI::Notifications::IToastNotificationManagerStatics*
toastManager,
winui::Notifications::IToastNotificationManagerStatics* toastManager,
const std::u16string& title,
const std::u16string& msg,
const std::wstring& icon_path,
const std::u16string& timeout_type,
bool silent,
IXmlDocument** toast_xml) {
ABI::Windows::UI::Notifications::ToastTemplateType template_type;
winui::Notifications::ToastTemplateType template_type;
if (title.empty() || msg.empty()) {
// Single line toast.
template_type =
icon_path.empty()
? ABI::Windows::UI::Notifications::ToastTemplateType_ToastText01
: ABI::Windows::UI::Notifications::
ToastTemplateType_ToastImageAndText01;
? winui::Notifications::ToastTemplateType_ToastText01
: winui::Notifications::ToastTemplateType_ToastImageAndText01;
REPORT_AND_RETURN_IF_FAILED(
toast_manager_->GetTemplateContent(template_type, toast_xml),
"XML: Fetching XML ToastImageAndText01 template failed");
Expand All @@ -199,9 +238,8 @@ HRESULT WindowsToastNotification::GetToastXml(
// Title and body toast.
template_type =
icon_path.empty()
? ABI::Windows::UI::Notifications::ToastTemplateType_ToastText02
: ABI::Windows::UI::Notifications::
ToastTemplateType_ToastImageAndText02;
? winui::Notifications::ToastTemplateType_ToastText02
: winui::Notifications::ToastTemplateType_ToastImageAndText02;
REPORT_AND_RETURN_IF_FAILED(
toastManager->GetTemplateContent(template_type, toast_xml),
"XML: Fetching XML ToastImageAndText02 template failed");
Expand Down Expand Up @@ -567,7 +605,7 @@ HRESULT WindowsToastNotification::XmlDocumentFromString(
}

HRESULT WindowsToastNotification::SetupCallbacks(
ABI::Windows::UI::Notifications::IToastNotification* toast) {
winui::Notifications::IToastNotification* toast) {
event_handler_ = Make<ToastEventHandler>(this);
RETURN_IF_FAILED(
toast->add_Activated(event_handler_.Get(), &activated_token_));
Expand All @@ -578,7 +616,7 @@ HRESULT WindowsToastNotification::SetupCallbacks(
}

bool WindowsToastNotification::RemoveCallbacks(
ABI::Windows::UI::Notifications::IToastNotification* toast) {
winui::Notifications::IToastNotification* toast) {
if (FAILED(toast->remove_Activated(activated_token_)))
return false;

Expand All @@ -597,41 +635,37 @@ ToastEventHandler::ToastEventHandler(Notification* notification)
ToastEventHandler::~ToastEventHandler() = default;

IFACEMETHODIMP ToastEventHandler::Invoke(
ABI::Windows::UI::Notifications::IToastNotification* sender,
winui::Notifications::IToastNotification* sender,
IInspectable* args) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&Notification::NotificationClicked, notification_));
if (IsDebuggingNotifications())
LOG(INFO) << "Notification clicked";
DebugLog("Notification clicked");

return S_OK;
}

IFACEMETHODIMP ToastEventHandler::Invoke(
ABI::Windows::UI::Notifications::IToastNotification* sender,
ABI::Windows::UI::Notifications::IToastDismissedEventArgs* e) {
winui::Notifications::IToastNotification* sender,
winui::Notifications::IToastDismissedEventArgs* e) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&Notification::NotificationDismissed, notification_));
if (IsDebuggingNotifications())
LOG(INFO) << "Notification dismissed";

FROM_HERE, base::BindOnce(&Notification::NotificationDismissed,
notification_, false));
DebugLog("Notification dismissed");
return S_OK;
}

IFACEMETHODIMP ToastEventHandler::Invoke(
ABI::Windows::UI::Notifications::IToastNotification* sender,
ABI::Windows::UI::Notifications::IToastFailedEventArgs* e) {
winui::Notifications::IToastNotification* sender,
winui::Notifications::IToastFailedEventArgs* e) {
HRESULT error;
e->get_ErrorCode(&error);
std::string errorMessage =
"Notification failed. HRESULT:" + std::to_string(error);
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&Notification::NotificationFailed,
notification_, errorMessage));
if (IsDebuggingNotifications())
LOG(INFO) << errorMessage;
DebugLog(errorMessage);

return S_OK;
}
Expand Down
Expand Up @@ -52,6 +52,7 @@ class WindowsToastNotification : public Notification {
// Notification:
void Show(const NotificationOptions& options) override;
void Dismiss() override;
void Remove() override;

private:
friend class ToastEventHandler;
Expand Down