mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
* refactor: replace deprecated NSUserNotifications with User Notifications
Removes deprecated NSUserNotification API, now using User Notifications
It replaces API calls for generating, scheduling, and receiving native
macOS notifications with equivalent API calls from the new framework,
or functionally equivalent implementations.
To preserve the existing Notification module API, special handling was
required in certain cases:
- Dynamically declared notification actions
Typically, notification actions should be declared at app launch time
when using the User Notifications framework. However, this isn’t
compatible with Electron’s architecture. Instead, we dynamically
declare new notifications actions when necessary and carefully manage
the existing actions registered at runtime.
- Localizations for ‘Reply’ and ‘Show’ labels
New translation files are added and processed through GRIT to add
localizations for “Reply” and “Show” button labels which were
initially supplied by the NSUserNotification framework.
* Use NotificationImageRetainer pattern from //chrome
* build: fix lint
* build: update config to handle --translate-gender for pak files
* test: also sign on arm64
* fix: add error handling for scheduling notification
* docs: add details to breaking changes
* docs: clarify breaking change details
* docs: add details for notifications tutorial and API documentation
---------
Co-authored-by: Keeley Hammond <khammond@slack-corp.com>
285 lines
11 KiB
Plaintext
285 lines
11 KiB
Plaintext
// Copyright (c) 2015 GitHub, Inc.
|
|
// Use of this source code is governed by the MIT license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "shell/browser/notifications/mac/cocoa_notification.h"
|
|
|
|
#include <string>
|
|
#include <utility>
|
|
|
|
#include "base/apple/foundation_util.h"
|
|
#include "base/logging.h"
|
|
#include "base/mac/mac_util.h"
|
|
#include "base/strings/sys_string_conversions.h"
|
|
#include "base/task/sequenced_task_runner.h"
|
|
#include "chrome/common/notifications/notification_image_retainer.h"
|
|
#include "grit/electron_resources.h"
|
|
#include "shell/browser/notifications/mac/notification_presenter_mac.h"
|
|
#include "shell/browser/notifications/notification_delegate.h"
|
|
#include "shell/browser/notifications/notification_presenter.h"
|
|
#include "skia/ext/skia_utils_mac.h"
|
|
#include "ui/base/l10n/l10n_util_mac.h"
|
|
#include "ui/gfx/image/image.h"
|
|
|
|
#import <AppKit/AppKit.h>
|
|
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
|
|
|
namespace electron {
|
|
|
|
CocoaNotification::CocoaNotification(NotificationDelegate* delegate,
|
|
NotificationPresenter* presenter)
|
|
: Notification(delegate, presenter) {}
|
|
|
|
CocoaNotification::~CocoaNotification() {
|
|
if (notification_request_)
|
|
[[UNUserNotificationCenter currentNotificationCenter]
|
|
removeDeliveredNotificationsWithIdentifiers:@[
|
|
notification_request_.identifier
|
|
]];
|
|
}
|
|
|
|
void CocoaNotification::Show(const NotificationOptions& options) {
|
|
UNMutableNotificationContent* content =
|
|
[[UNMutableNotificationContent alloc] init];
|
|
|
|
content.title = base::SysUTF16ToNSString(options.title);
|
|
content.subtitle = base::SysUTF16ToNSString(options.subtitle);
|
|
content.body = base::SysUTF16ToNSString(options.msg);
|
|
|
|
if (options.silent) {
|
|
content.sound = nil;
|
|
} else if (options.sound.empty()) {
|
|
content.sound = [UNNotificationSound defaultSound];
|
|
} else {
|
|
content.sound = [UNNotificationSound
|
|
soundNamed:base::SysUTF16ToNSString(options.sound)];
|
|
}
|
|
|
|
if (options.has_reply || options.actions.size() > 0 ||
|
|
!options.close_button_text.empty()) {
|
|
NSMutableArray* actions = [NSMutableArray array];
|
|
NSMutableString* actionString = [NSMutableString string];
|
|
|
|
if (options.has_reply) {
|
|
NSString* replyTitle =
|
|
l10n_util::GetNSString(IDS_MAC_NOTIFICATION_INLINE_REPLY_BUTTON);
|
|
NSString* replyPlaceholder =
|
|
base::SysUTF16ToNSString(options.reply_placeholder);
|
|
UNNotificationAction* replyAction = [UNTextInputNotificationAction
|
|
actionWithIdentifier:@"REPLY_ACTION"
|
|
title:replyTitle
|
|
options:UNNotificationActionOptionNone
|
|
textInputButtonTitle:replyTitle
|
|
textInputPlaceholder:replyPlaceholder];
|
|
[actionString appendFormat:@"REPLY_%@_%@", replyTitle, replyPlaceholder];
|
|
[actions addObject:replyAction];
|
|
}
|
|
|
|
int i = 0;
|
|
for (const auto& action : options.actions) {
|
|
NSString* showText =
|
|
l10n_util::GetNSString(IDS_MAC_NOTIFICATION_SHOW_BUTTON);
|
|
NSString* actionText = action.text.empty()
|
|
? showText
|
|
: base::SysUTF16ToNSString(action.text);
|
|
// Action indicies are stored in the action identifier
|
|
UNNotificationAction* notificationAction = [UNNotificationAction
|
|
actionWithIdentifier:[NSString stringWithFormat:@"ACTION_%d", i]
|
|
title:actionText
|
|
options:UNNotificationActionOptionNone];
|
|
[actionString appendFormat:@"ACTION_%d_%@", i, actionText];
|
|
[actions addObject:notificationAction];
|
|
i++;
|
|
}
|
|
|
|
if (!options.close_button_text.empty()) {
|
|
NSString* closeButtonText =
|
|
base::SysUTF16ToNSString(options.close_button_text);
|
|
UNNotificationAction* closeAction = [UNNotificationAction
|
|
actionWithIdentifier:@"CLOSE_ACTION"
|
|
title:closeButtonText
|
|
options:UNNotificationActionOptionNone];
|
|
[actionString appendFormat:@"CLOSE_%@", closeButtonText];
|
|
[actions addObject:closeAction];
|
|
i++;
|
|
}
|
|
|
|
// Categories are unique based on the actionString
|
|
NSString* categoryIdentifier =
|
|
[NSString stringWithFormat:@"CATEGORY_%lu", [actionString hash]];
|
|
|
|
// UNNotificationCategoryOptionCustomDismissAction enables the notification
|
|
// delegate to receive dismiss actions for handling
|
|
UNNotificationCategory* category = [UNNotificationCategory
|
|
categoryWithIdentifier:categoryIdentifier
|
|
actions:actions
|
|
intentIdentifiers:@[]
|
|
options:UNNotificationCategoryOptionCustomDismissAction];
|
|
|
|
UNUserNotificationCenter* center =
|
|
[UNUserNotificationCenter currentNotificationCenter];
|
|
[center getNotificationCategoriesWithCompletionHandler:^(
|
|
NSSet<UNNotificationCategory*>* _Nonnull existingCategories) {
|
|
if (![existingCategories containsObject:category]) {
|
|
NSMutableSet* updatedCategories = [existingCategories mutableCopy];
|
|
[updatedCategories addObject:category];
|
|
[center setNotificationCategories:updatedCategories];
|
|
}
|
|
}];
|
|
content.categoryIdentifier = categoryIdentifier;
|
|
}
|
|
|
|
if (!options.icon.drawsNothing()) {
|
|
gfx::Image icon = gfx::Image::CreateFrom1xBitmap(options.icon);
|
|
auto* mac_presenter = static_cast<NotificationPresenterMac*>(presenter());
|
|
mac_presenter->image_task_runner()->PostTaskAndReplyWithResult(
|
|
FROM_HERE,
|
|
base::BindOnce(
|
|
[](NotificationPresenterMac* mac_presenter,
|
|
gfx::Image icon) -> UNNotificationAttachment* {
|
|
base::FilePath path =
|
|
mac_presenter->image_retainer()->RegisterTemporaryImage(icon);
|
|
if (path.empty()) {
|
|
return nil;
|
|
}
|
|
NSURL* url = base::apple::FilePathToNSURL(path);
|
|
NSDictionary* attachment_options = @{
|
|
UNNotificationAttachmentOptionsTypeHintKey :
|
|
UTTypePNG.identifier
|
|
};
|
|
NSError* error = nil;
|
|
UNNotificationAttachment* attachment = [UNNotificationAttachment
|
|
attachmentWithIdentifier:[[NSUUID UUID] UUIDString]
|
|
URL:url
|
|
options:attachment_options
|
|
error:&error];
|
|
return (attachment && !error) ? attachment : nil;
|
|
},
|
|
mac_presenter, icon),
|
|
base::BindOnce(
|
|
[](base::WeakPtr<Notification> weak_self,
|
|
UNMutableNotificationContent* content,
|
|
UNNotificationAttachment* attachment) {
|
|
if (auto* notification = weak_self.get()) {
|
|
content.attachments = @[ attachment ];
|
|
auto* self = static_cast<CocoaNotification*>(notification);
|
|
self->ScheduleNotification(content);
|
|
}
|
|
},
|
|
GetWeakPtr(), content));
|
|
} else {
|
|
ScheduleNotification(content);
|
|
}
|
|
}
|
|
|
|
void CocoaNotification::ScheduleNotification(
|
|
UNMutableNotificationContent* content) {
|
|
NSString* identifier =
|
|
[NSString stringWithFormat:@"%@:notification:%@",
|
|
[[NSBundle mainBundle] bundleIdentifier],
|
|
[[NSUUID UUID] UUIDString]];
|
|
|
|
UNNotificationRequest* request =
|
|
[UNNotificationRequest requestWithIdentifier:identifier
|
|
content:content
|
|
trigger:nil];
|
|
|
|
notification_request_ = request;
|
|
if (electron::debug_notifications) {
|
|
LOG(INFO) << "Notification created (" << [identifier UTF8String] << ")";
|
|
}
|
|
|
|
scoped_refptr<base::SequencedTaskRunner> task_runner =
|
|
base::SequencedTaskRunner::GetCurrentDefault();
|
|
auto weak_self = GetWeakPtr();
|
|
|
|
[[UNUserNotificationCenter currentNotificationCenter]
|
|
addNotificationRequest:request
|
|
withCompletionHandler:^(NSError* _Nullable error) {
|
|
if (error) {
|
|
if (electron::debug_notifications) {
|
|
LOG(INFO) << "Error scheduling notification ("
|
|
<< [identifier UTF8String] << ") "
|
|
<< [error.localizedDescription UTF8String];
|
|
}
|
|
std::string error_description =
|
|
[error.localizedDescription UTF8String];
|
|
task_runner->PostTask(
|
|
FROM_HERE, base::BindOnce(
|
|
[](base::WeakPtr<Notification> weak_self,
|
|
std::string error_description) {
|
|
if (Notification* self = weak_self.get()) {
|
|
self->NotificationFailed(error_description);
|
|
}
|
|
},
|
|
weak_self, std::move(error_description)));
|
|
} else {
|
|
task_runner->PostTask(
|
|
FROM_HERE, base::BindOnce(
|
|
[](base::WeakPtr<Notification> weak_self) {
|
|
if (Notification* self = weak_self.get()) {
|
|
CocoaNotification* un_self =
|
|
static_cast<CocoaNotification*>(self);
|
|
un_self->NotificationDisplayed();
|
|
}
|
|
},
|
|
weak_self));
|
|
if (electron::debug_notifications) {
|
|
LOG(INFO) << "Notification scheduled (" << [identifier UTF8String]
|
|
<< ")";
|
|
}
|
|
}
|
|
}];
|
|
}
|
|
|
|
void CocoaNotification::Dismiss() {
|
|
if (notification_request_)
|
|
[[UNUserNotificationCenter currentNotificationCenter]
|
|
removeDeliveredNotificationsWithIdentifiers:@[
|
|
notification_request_.identifier
|
|
]];
|
|
|
|
NotificationDismissed();
|
|
|
|
notification_request_ = nil;
|
|
}
|
|
|
|
void CocoaNotification::NotificationDisplayed() {
|
|
if (delegate())
|
|
delegate()->NotificationDisplayed();
|
|
|
|
this->LogAction("displayed");
|
|
}
|
|
|
|
void CocoaNotification::NotificationReplied(const std::string& reply) {
|
|
if (delegate())
|
|
delegate()->NotificationReplied(reply);
|
|
|
|
this->LogAction("replied to");
|
|
}
|
|
|
|
void CocoaNotification::NotificationActivated(int actionIndex) {
|
|
if (delegate())
|
|
delegate()->NotificationAction(actionIndex);
|
|
|
|
this->LogAction("button clicked");
|
|
}
|
|
|
|
void CocoaNotification::NotificationDismissed() {
|
|
if (delegate())
|
|
delegate()->NotificationClosed();
|
|
|
|
this->LogAction("dismissed");
|
|
}
|
|
|
|
void CocoaNotification::LogAction(const char* action) {
|
|
if (electron::debug_notifications && notification_request_) {
|
|
NSString* identifier = [notification_request_ valueForKey:@"identifier"];
|
|
DCHECK(identifier);
|
|
LOG(INFO) << "Notification " << action << " (" << [identifier UTF8String]
|
|
<< ")";
|
|
}
|
|
}
|
|
|
|
} // namespace electron
|