summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/AppMenuNotifications.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/AppMenuNotifications.sys.mjs')
-rw-r--r--toolkit/modules/AppMenuNotifications.sys.mjs184
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");
+ },
+};