diff options
Diffstat (limited to 'toolkit/modules/AppMenuNotifications.sys.mjs')
-rw-r--r-- | toolkit/modules/AppMenuNotifications.sys.mjs | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/toolkit/modules/AppMenuNotifications.sys.mjs b/toolkit/modules/AppMenuNotifications.sys.mjs new file mode 100644 index 0000000000..77487437ac --- /dev/null +++ b/toolkit/modules/AppMenuNotifications.sys.mjs @@ -0,0 +1,184 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function AppMenuNotification(id, mainAction, secondaryAction, options = {}) { + this.id = id; + this.mainAction = mainAction; + this.secondaryAction = secondaryAction; + this.options = options; + this.dismissed = this.options.dismissed || false; +} + +export var AppMenuNotifications = { + _notifications: [], + _hasInitialized: false, + + get notifications() { + return Array.from(this._notifications); + }, + + _lazyInit() { + if (!this._hasInitialized) { + Services.obs.addObserver(this, "xpcom-shutdown"); + Services.obs.addObserver(this, "appMenu-notifications-request"); + } + }, + + uninit() { + Services.obs.removeObserver(this, "xpcom-shutdown"); + Services.obs.removeObserver(this, "appMenu-notifications-request"); + }, + + observe(subject, topic, status) { + switch (topic) { + case "xpcom-shutdown": + this.uninit(); + break; + case "appMenu-notifications-request": + if (this._notifications.length) { + Services.obs.notifyObservers(null, "appMenu-notifications", "init"); + } + break; + } + }, + + get activeNotification() { + if (this._notifications.length) { + const doorhanger = this._notifications.find( + n => !n.dismissed && !n.options.badgeOnly + ); + return doorhanger || this._notifications[0]; + } + + return null; + }, + + showNotification(id, mainAction, secondaryAction, options = {}) { + let notification = new AppMenuNotification( + id, + mainAction, + secondaryAction, + options + ); + let existingIndex = this._notifications.findIndex(n => n.id == id); + if (existingIndex != -1) { + this._notifications.splice(existingIndex, 1); + } + + // We don't want to clobber doorhanger notifications just to show a badge, + // so don't dismiss any of them and the badge will show once the doorhanger + // gets resolved. + if (!options.badgeOnly && !options.dismissed) { + this._notifications.forEach(n => { + n.dismissed = true; + }); + } + + // Since notifications are generally somewhat pressing, the ideal case is that + // we never have two notifications at once. However, in the event that we do, + // it's more likely that the older notification has been sitting around for a + // bit, and so we don't want to hide the new notification behind it. Thus, + // we want our notifications to behave like a stack instead of a queue. + this._notifications.unshift(notification); + + this._lazyInit(); + this._updateNotifications(); + return notification; + }, + + showBadgeOnlyNotification(id) { + return this.showNotification(id, null, null, { badgeOnly: true }); + }, + + removeNotification(id) { + let notifications; + if (typeof id == "string") { + notifications = this._notifications.filter(n => n.id == id); + } else { + // If it's not a string, assume RegExp + notifications = this._notifications.filter(n => id.test(n.id)); + } + // _updateNotifications can be expensive if it forces attachment of XBL + // bindings that haven't been used yet, so return early if we haven't found + // any notification to remove, as callers may expect this removeNotification + // method to be a no-op for non-existent notifications. + if (!notifications.length) { + return; + } + + notifications.forEach(n => { + this._removeNotification(n); + }); + this._updateNotifications(); + }, + + dismissNotification(id) { + let notifications; + if (typeof id == "string") { + notifications = this._notifications.filter(n => n.id == id); + } else { + // If it's not a string, assume RegExp + notifications = this._notifications.filter(n => id.test(n.id)); + } + + notifications.forEach(n => { + n.dismissed = true; + if (n.options.onDismissed) { + n.options.onDismissed(); + } + }); + this._updateNotifications(); + }, + + callMainAction(win, notification, fromDoorhanger) { + let action = notification.mainAction; + this._callAction(win, notification, action, fromDoorhanger); + }, + + callSecondaryAction(win, notification) { + let action = notification.secondaryAction; + this._callAction(win, notification, action, true); + }, + + _callAction(win, notification, action, fromDoorhanger) { + let dismiss = true; + if (action) { + try { + action.callback(win, fromDoorhanger); + } catch (error) { + console.error(error); + } + + dismiss = action.dismiss; + } + + if (dismiss) { + notification.dismissed = true; + } else { + this._removeNotification(notification); + } + + this._updateNotifications(); + }, + + _removeNotification(notification) { + // This notification may already be removed, in which case let's just ignore. + let notifications = this._notifications; + if (!notifications) { + return; + } + + var index = notifications.indexOf(notification); + if (index == -1) { + return; + } + + // Remove the notification + notifications.splice(index, 1); + }, + + _updateNotifications() { + Services.obs.notifyObservers(null, "appMenu-notifications", "update"); + }, +}; |