mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
feat: add id and groupId options to macOS notifications (#50097)
* feat: add custom `id` property to Notification API (macOS only) * feat: add `groupId` property to Notification API (macOS). Notifications with the same groupId will be visually grouped together in Notification Center * fix: move validation to construction time, add empty string check, remove setters * docs: clarify id/group id properties, make instance properties read-only * test: update tests to reflect read-only properties
This commit is contained in:
@@ -79,6 +79,8 @@ app.whenReady().then(() => {
|
||||
### `new Notification([options])`
|
||||
|
||||
* `options` Object (optional)
|
||||
* `id` string (optional) _macOS_ - A unique identifier for the notification, mapping to `UNNotificationRequest`'s [`identifier`](https://developer.apple.com/documentation/usernotifications/unnotificationrequest/identifier) property. Defaults to a random UUID if not provided or if an empty string is passed. This can be used to remove or update previously delivered notifications.
|
||||
* `groupId` string (optional) _macOS_ - A string identifier used to visually group notifications together in Notification Center. Maps to `UNNotificationContent`'s [`threadIdentifier`](https://developer.apple.com/documentation/usernotifications/unnotificationcontent/threadidentifier) property.
|
||||
* `title` string (optional) - A title for the notification, which will be displayed at the top of the notification window when it is shown.
|
||||
* `subtitle` string (optional) _macOS_ - A subtitle for the notification, which will be displayed below the title.
|
||||
* `body` string (optional) - The body text of the notification, which will be displayed below the title or subtitle.
|
||||
@@ -323,6 +325,14 @@ app.whenReady().then(() => {
|
||||
|
||||
### Instance Properties
|
||||
|
||||
#### `notification.id` _macOS_ _Readonly_
|
||||
|
||||
A `string` property representing the unique identifier of the notification. This is set at construction time — either from the `id` option or as a generated UUID if none was provided.
|
||||
|
||||
#### `notification.groupId` _macOS_ _Readonly_
|
||||
|
||||
A `string` property representing the group identifier of the notification. Notifications with the same `groupId` will be visually grouped together in Notification Center.
|
||||
|
||||
#### `notification.title`
|
||||
|
||||
A `string` property representing the title of the notification.
|
||||
|
||||
@@ -74,6 +74,8 @@ Notification::Notification(gin::Arguments* args) {
|
||||
|
||||
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_);
|
||||
@@ -88,6 +90,9 @@ Notification::Notification(gin::Arguments* args) {
|
||||
opts.Get("closeButtonText", &close_button_text_);
|
||||
opts.Get("toastXml", &toast_xml_);
|
||||
}
|
||||
|
||||
if (id_.empty())
|
||||
id_ = base::Uuid::GenerateRandomV4().AsLowercaseString();
|
||||
}
|
||||
|
||||
Notification::~Notification() {
|
||||
@@ -236,8 +241,7 @@ void Notification::Close() {
|
||||
void Notification::Show() {
|
||||
Close();
|
||||
if (presenter_) {
|
||||
notification_ = presenter_->CreateNotification(
|
||||
this, base::Uuid::GenerateRandomV4().AsLowercaseString());
|
||||
notification_ = presenter_->CreateNotification(this, id_);
|
||||
if (notification_) {
|
||||
electron::NotificationOptions options;
|
||||
options.title = title_;
|
||||
@@ -254,6 +258,7 @@ void Notification::Show() {
|
||||
options.close_button_text = close_button_text_;
|
||||
options.urgency = urgency_;
|
||||
options.toast_xml = toast_xml_;
|
||||
options.group_id = group_id_;
|
||||
notification_->Show(options);
|
||||
}
|
||||
}
|
||||
@@ -342,6 +347,8 @@ void Notification::FillObjectTemplate(v8::Isolate* isolate,
|
||||
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)
|
||||
|
||||
@@ -83,6 +83,8 @@ class Notification final : public gin_helper::DeprecatedWrappable<Notification>,
|
||||
void Close();
|
||||
|
||||
// Prop Getters
|
||||
const std::string& id() const { return id_; }
|
||||
const std::string& group_id() const { return group_id_; }
|
||||
const std::u16string& title() const { return title_; }
|
||||
const std::u16string& subtitle() const { return subtitle_; }
|
||||
const std::u16string& body() const { return body_; }
|
||||
@@ -113,6 +115,8 @@ class Notification final : public gin_helper::DeprecatedWrappable<Notification>,
|
||||
void SetToastXml(const std::u16string& new_toast_xml);
|
||||
|
||||
private:
|
||||
std::string id_;
|
||||
std::string group_id_;
|
||||
std::u16string title_;
|
||||
std::u16string subtitle_;
|
||||
std::u16string body_;
|
||||
|
||||
@@ -46,6 +46,10 @@ void CocoaNotification::Show(const NotificationOptions& options) {
|
||||
content.subtitle = base::SysUTF16ToNSString(options.subtitle);
|
||||
content.body = base::SysUTF16ToNSString(options.msg);
|
||||
|
||||
if (!options.group_id.empty()) {
|
||||
content.threadIdentifier = base::SysUTF8ToNSString(options.group_id);
|
||||
}
|
||||
|
||||
if (options.silent) {
|
||||
content.sound = nil;
|
||||
} else if (options.sound.empty()) {
|
||||
@@ -174,10 +178,7 @@ void CocoaNotification::Show(const NotificationOptions& options) {
|
||||
|
||||
void CocoaNotification::ScheduleNotification(
|
||||
UNMutableNotificationContent* content) {
|
||||
NSString* identifier =
|
||||
[NSString stringWithFormat:@"%@:notification:%@",
|
||||
[[NSBundle mainBundle] bundleIdentifier],
|
||||
[[NSUUID UUID] UUIDString]];
|
||||
NSString* identifier = base::SysUTF8ToNSString(notification_id());
|
||||
|
||||
UNNotificationRequest* request =
|
||||
[UNNotificationRequest requestWithIdentifier:identifier
|
||||
|
||||
@@ -49,6 +49,7 @@ struct NotificationOptions {
|
||||
std::vector<NotificationAction> actions;
|
||||
std::u16string close_button_text;
|
||||
std::u16string toast_xml;
|
||||
std::string group_id;
|
||||
|
||||
NotificationOptions();
|
||||
NotificationOptions(const NotificationOptions&);
|
||||
|
||||
@@ -15,6 +15,75 @@ describe('Notification module', () => {
|
||||
expect(Notification.isSupported()).to.be.a('boolean');
|
||||
});
|
||||
|
||||
ifit(process.platform === 'darwin')('inits and gets id property', () => {
|
||||
const n = new Notification({
|
||||
id: 'my-custom-id',
|
||||
title: 'title',
|
||||
body: 'body'
|
||||
});
|
||||
|
||||
expect(n.id).to.equal('my-custom-id');
|
||||
});
|
||||
|
||||
ifit(process.platform === 'darwin')('id is read-only', () => {
|
||||
const n = new Notification({
|
||||
id: 'my-custom-id',
|
||||
title: 'title',
|
||||
body: 'body'
|
||||
});
|
||||
|
||||
expect(() => { (n as any).id = 'new-id'; }).to.throw();
|
||||
});
|
||||
|
||||
ifit(process.platform === 'darwin')('defaults id to a UUID when not provided', () => {
|
||||
const n = new Notification({
|
||||
title: 'title',
|
||||
body: 'body'
|
||||
});
|
||||
|
||||
expect(n.id).to.be.a('string').and.not.be.empty();
|
||||
expect(n.id).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
||||
});
|
||||
|
||||
ifit(process.platform === 'darwin')('defaults id to a UUID when empty string is provided', () => {
|
||||
const n = new Notification({
|
||||
id: '',
|
||||
title: 'title',
|
||||
body: 'body'
|
||||
});
|
||||
|
||||
expect(n.id).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
||||
});
|
||||
|
||||
ifit(process.platform === 'darwin')('inits and gets groupId property', () => {
|
||||
const n = new Notification({
|
||||
title: 'title',
|
||||
body: 'body',
|
||||
groupId: 'E017VKL2N8H|C07RBMNS9EK'
|
||||
});
|
||||
|
||||
expect(n.groupId).to.equal('E017VKL2N8H|C07RBMNS9EK');
|
||||
});
|
||||
|
||||
ifit(process.platform === 'darwin')('groupId is read-only', () => {
|
||||
const n = new Notification({
|
||||
title: 'title',
|
||||
body: 'body',
|
||||
groupId: 'E017VKL2N8H|C07RBMNS9EK'
|
||||
});
|
||||
|
||||
expect(() => { (n as any).groupId = 'new-group'; }).to.throw();
|
||||
});
|
||||
|
||||
ifit(process.platform === 'darwin')('defaults groupId to empty string when not provided', () => {
|
||||
const n = new Notification({
|
||||
title: 'title',
|
||||
body: 'body'
|
||||
});
|
||||
|
||||
expect(n.groupId).to.equal('');
|
||||
});
|
||||
|
||||
it('inits, gets and sets basic string properties correctly', () => {
|
||||
const n = new Notification({
|
||||
title: 'title',
|
||||
@@ -141,6 +210,45 @@ describe('Notification module', () => {
|
||||
}
|
||||
});
|
||||
|
||||
ifit(process.platform === 'darwin')('emits show and close events with custom id', async () => {
|
||||
const n = new Notification({
|
||||
id: 'test-custom-id',
|
||||
title: 'test notification',
|
||||
body: 'test body',
|
||||
silent: true
|
||||
});
|
||||
{
|
||||
const e = once(n, 'show');
|
||||
n.show();
|
||||
await e;
|
||||
}
|
||||
{
|
||||
const e = once(n, 'close');
|
||||
n.close();
|
||||
await e;
|
||||
}
|
||||
});
|
||||
|
||||
ifit(process.platform === 'darwin')('emits show and close events with custom id and groupId', async () => {
|
||||
const n = new Notification({
|
||||
id: 'E017VKL2N8H|C07RBMNS9EK|1772656675.039',
|
||||
groupId: 'E017VKL2N8H|C07RBMNS9EK',
|
||||
title: 'test notification',
|
||||
body: 'test body',
|
||||
silent: true
|
||||
});
|
||||
{
|
||||
const e = once(n, 'show');
|
||||
n.show();
|
||||
await e;
|
||||
}
|
||||
{
|
||||
const e = once(n, 'close');
|
||||
n.close();
|
||||
await e;
|
||||
}
|
||||
});
|
||||
|
||||
ifit(process.platform === 'win32')('emits failed event', async () => {
|
||||
const n = new Notification({
|
||||
toastXml: 'not xml'
|
||||
|
||||
Reference in New Issue
Block a user