Bug 1359733 - Move menu notification state to jsm draft
authorDoug Thayer <dothayer@mozilla.com>
Thu, 18 May 2017 13:22:27 -0700
changeset 583045 a32b82659a8103ad39709dae888006bf618fcdcf
parent 583025 6dfa56094f0cc291945dd3c24d0a4c2682d80ec7
child 583046 b15c9b889898d6e7e3e1ec5c87b481e073549e55
push id60284
push userbmo:dothayer@mozilla.com
push dateTue, 23 May 2017 17:20:55 +0000
bugs1359733
milestone55.0a1
Bug 1359733 - Move menu notification state to jsm Right now, app menu doorhangers/badges have their state managed directly inside panelUI.js. This is problematic because these doorhangers and badges usually have to do with Firefox itself, and not the specific window that's showing them. Accordingly, the simplest solution was to move panelUI.js's notification state out into a jsm file, which will fire notifications that all panelUI instances can listen to. MozReview-Commit-ID: 7b8w1WsQ29p
browser/components/customizableui/content/panelUI.js
browser/components/customizableui/test/browser.ini
browser/components/customizableui/test/browser_panelUINotifications.js
browser/components/customizableui/test/browser_panelUINotifications_fullscreen.js
browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js
toolkit/modules/AppMenuNotifications.jsm
toolkit/modules/moz.build
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -5,16 +5,18 @@
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
                                   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ScrollbarSampler",
                                   "resource:///modules/ScrollbarSampler.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
                                   "resource://gre/modules/ShortcutUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppMenuNotifications",
+                                  "resource://gre/modules/AppMenuNotifications.jsm");
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "gPhotonStructure",
   "browser.photon.structure.enabled", false);
 
 /**
  * Maintains the state and dispatches events for the main menu panel.
  */
 
@@ -41,38 +43,39 @@ const PanelUI = {
 
       overflowFixedList: gPhotonStructure ? "widget-overflow-fixed-list" : "",
       overflowPanel: gPhotonStructure ? "widget-overflow" : "",
       navbar: "nav-bar",
     };
   },
 
   _initialized: false,
+  _notifications: null,
+
   init() {
     this._initElements();
 
-    this.notifications = [];
     this.menuButton.addEventListener("mousedown", this);
     this.menuButton.addEventListener("keypress", this);
     this._overlayScrollListenerBoundFn = this._overlayScrollListener.bind(this);
 
     Services.obs.addObserver(this, "fullscreen-nav-toolbox");
-    Services.obs.addObserver(this, "panelUI-notification-main-action");
-    Services.obs.addObserver(this, "panelUI-notification-dismissed");
+    Services.obs.addObserver(this, "appMenu-notifications");
 
     window.addEventListener("fullscreen", this);
     window.addEventListener("activate", this);
     window.matchMedia("(-moz-overlay-scrollbars)").addListener(this._overlayScrollListenerBoundFn);
     CustomizableUI.addListener(this);
 
     for (let event of this.kEvents) {
       this.notificationPanel.addEventListener(event, this);
     }
 
     this._initPhotonPanel();
+    Services.obs.notifyObservers(null, "appMenu-notifications-request", "refresh");
 
     this._initialized = true;
   },
 
   reinit() {
     this._removeEventListeners();
     // If the Photon pref changes, we need to re-init our element references.
     this._initElements();
@@ -137,18 +140,17 @@ const PanelUI = {
 
   uninit() {
     this._removeEventListeners();
     for (let event of this.kEvents) {
       this.notificationPanel.removeEventListener(event, this);
     }
 
     Services.obs.removeObserver(this, "fullscreen-nav-toolbox");
-    Services.obs.removeObserver(this, "panelUI-notification-main-action");
-    Services.obs.removeObserver(this, "panelUI-notification-dismissed");
+    Services.obs.removeObserver(this, "appMenu-notifications");
 
     window.removeEventListener("fullscreen", this);
     window.removeEventListener("activate", this);
     this.menuButton.removeEventListener("mousedown", this);
     this.menuButton.removeEventListener("keypress", this);
     window.matchMedia("(-moz-overlay-scrollbars)").removeListener(this._overlayScrollListenerBoundFn);
     CustomizableUI.removeListener(this);
     this._overlayScrollListenerBoundFn = null;
@@ -227,104 +229,41 @@ const PanelUI = {
         anchor = this._getPanelAnchor(anchor);
         this.panel.openPopup(anchor);
       }, (reason) => {
         console.error("Error showing the PanelUI menu", reason);
       });
     });
   },
 
-  showNotification(id, mainAction, secondaryActions = [], options = {}) {
-    let notification = new PanelUINotification(id, mainAction, secondaryActions, 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._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);
-    this._updateNotifications();
-  },
-
   /**
    * If the menu panel is being shown, hide it.
    */
   hide() {
     if (document.documentElement.hasAttribute("customizing")) {
       return;
     }
 
     this.panel.hidePopup();
   },
 
   observe(subject, topic, status) {
     switch (topic) {
       case "fullscreen-nav-toolbox":
-        this._updateNotifications();
-        break;
-      case "panelUI-notification-main-action":
-        if (subject != window) {
-          this.removeNotification(status);
+        if (this._notifications) {
+          this._updateNotifications(false);
         }
         break;
-      case "panelUI-notification-dismissed":
-        if (subject != window) {
-          this.dismissNotification(status);
+      case "appMenu-notifications":
+        // Don't initialize twice.
+        if (status == "init" && this._notifications) {
+          break;
         }
+        this._notifications = AppMenuNotifications.notifications;
+        this._updateNotifications(true);
         break;
     }
   },
 
   handleEvent(aEvent) {
     // Ignore context menus and menu button menus showing and hiding:
     if (aEvent.type.startsWith("popup") &&
         aEvent.target != this.panel) {
@@ -371,26 +310,16 @@ const PanelUI = {
   },
 
   get isNotificationPanelOpen() {
     let panelState = this.notificationPanel.state;
 
     return panelState == "showing" || panelState == "open";
   },
 
-  get activeNotification() {
-    if (this.notifications.length > 0) {
-      const doorhanger =
-        this.notifications.find(n => !n.dismissed && !n.options.badgeOnly);
-      return doorhanger || this.notifications[0];
-    }
-
-    return null;
-  },
-
   /**
    * Registering the menu panel is done lazily for performance reasons. This
    * method is exposed so that CustomizationMode can force panel-readyness in the
    * event that customization mode is started before the panel has been opened
    * by the user.
    *
    * @param aCustomizing (optional) set to true if this was called while entering
    *        customization mode. If that's the case, we trust that customization
@@ -744,54 +673,57 @@ const PanelUI = {
   },
 
   _hidePopup() {
     if (this.isNotificationPanelOpen) {
       this.notificationPanel.hidePopup();
     }
   },
 
-  _updateNotifications() {
-    if (!this.notifications.length) {
-      this._clearAllNotifications();
-      this._hidePopup();
+  _updateNotifications(notificationsChanged) {
+    let notifications = this._notifications;
+    if (!notifications || !notifications.length) {
+      if (notificationsChanged) {
+        this._clearAllNotifications();
+        this._hidePopup();
+      }
       return;
     }
 
     if (window.fullScreen && FullScreen.navToolboxHidden) {
       this._hidePopup();
       return;
     }
 
     let doorhangers =
-      this.notifications.filter(n => !n.dismissed && !n.options.badgeOnly);
+      notifications.filter(n => !n.dismissed && !n.options.badgeOnly);
 
     if (this.panel.state == "showing" || this.panel.state == "open") {
       // If the menu is already showing, then we need to dismiss all notifications
       // since we don't want their doorhangers competing for attention
       doorhangers.forEach(n => { n.dismissed = true; })
       this._hidePopup();
       this._clearBadge();
-      if (!this.notifications[0].options.badgeOnly) {
-        this._showBannerItem(this.notifications[0]);
+      if (!notifications[0].options.badgeOnly) {
+        this._showBannerItem(notifications[0]);
       }
     } else if (doorhangers.length > 0) {
       // Only show the doorhanger if the window is focused and not fullscreen
       if (window.fullScreen || Services.focus.activeWindow !== window) {
         this._hidePopup();
         this._showBadge(doorhangers[0]);
         this._showBannerItem(doorhangers[0]);
       } else {
         this._clearBadge();
         this._showNotificationPanel(doorhangers[0]);
       }
     } else {
       this._hidePopup();
-      this._showBadge(this.notifications[0]);
-      this._showBannerItem(this.notifications[0]);
+      this._showBadge(notifications[0]);
+      this._showBannerItem(notifications[0]);
     }
   },
 
   _showNotificationPanel(notification) {
     this._refreshNotificationPanel(notification);
 
     if (this.isNotificationPanelOpen) {
       return;
@@ -859,89 +791,41 @@ const PanelUI = {
 
   _clearBannerItem() {
     if (this._panelBannerItem) {
       this._panelBannerItem.notification = null;
       this._panelBannerItem.hidden = true;
     }
   },
 
-  _removeNotification(notification) {
-    // This notification may already be removed, in which case let's just fail
-    // silently.
-    let notifications = this.notifications;
-    if (!notifications)
-      return;
-
-    var index = notifications.indexOf(notification);
-    if (index == -1)
-      return;
-
-    // Remove the notification
-    notifications.splice(index, 1);
-  },
-
   _onNotificationButtonEvent(event, type) {
     let notificationEl = getNotificationFromElement(event.originalTarget);
 
     if (!notificationEl)
       throw "PanelUI._onNotificationButtonEvent: couldn't find notification element";
 
     if (!notificationEl.notification)
       throw "PanelUI._onNotificationButtonEvent: couldn't find notification";
 
     let notification = notificationEl.notification;
 
-    let action = notification.mainAction;
-
     if (type == "secondarybuttoncommand") {
-      action = notification.secondaryActions[0];
+      AppMenuNotifications.callSecondaryAction(window, notification);
+    } else {
+      AppMenuNotifications.callMainAction(window, notification, true);
     }
-
-    let dismiss = true;
-    if (action) {
-      try {
-        if (action === notification.mainAction) {
-          action.callback(true);
-          this._notify(notification.id, "main-action");
-        } else {
-          action.callback();
-        }
-      } catch (error) {
-        Cu.reportError(error);
-      }
-
-      dismiss = action.dismiss;
-    }
-
-    if (dismiss) {
-      notification.dismissed = true;
-      this._notify(notification.id, "dismissed");
-    } else {
-      this._removeNotification(notification);
-    }
-    this._updateNotifications();
   },
 
   _onBannerItemSelected(event) {
     let target = event.originalTarget;
     if (!target.notification)
       throw "menucommand target has no associated action/notification";
 
     event.stopPropagation();
-
-    try {
-      target.notification.mainAction.callback(false);
-      this._notify(target.notification.id, "main-action");
-    } catch (error) {
-      Cu.reportError(error);
-    }
-
-    this._removeNotification(target.notification);
-    this._updateNotifications();
+    AppMenuNotifications.callMainAction(window, target.notification, false);
   },
 
   _getPopupId(notification) { return "appMenu-" + notification.id + "-notification"; },
 
   _getBadgeStatus(notification) { return notification.id; },
 
   _getPanelAnchor(candidate) {
     let iconAnchor =
@@ -960,40 +844,28 @@ const PanelUI = {
       let keyId = button.getAttribute("key");
       let key = document.getElementById(keyId);
       if (!key) {
         continue;
       }
       button.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(key));
     }
   },
-
-  _notify(status, topic) {
-    Services.obs.notifyObservers(window, "panelUI-notification-" + topic, status);
-  }
 };
 
 XPCOMUtils.defineConstant(this, "PanelUI", PanelUI);
 
 /**
  * Gets the currently selected locale for display.
  * @return  the selected locale
  */
 function getLocale() {
   return Services.locale.getAppLocaleAsLangTag();
 }
 
-function PanelUINotification(id, mainAction, secondaryActions = [], options = {}) {
-  this.id = id;
-  this.mainAction = mainAction;
-  this.secondaryActions = secondaryActions;
-  this.options = options;
-  this.dismissed = this.options.dismissed || false;
-}
-
 function getNotificationFromElement(aElement) {
   // Need to find the associated notification object, which is a bit tricky
   // since it isn't associated with the element directly - this is kind of
   // gross and very dependent on the structure of the popupnotification
   // binding's content.
   let notificationEl;
   let parent = aElement;
   while (parent && (parent = aElement.ownerDocument.getBindingParent(parent))) {
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -146,14 +146,16 @@ skip-if = os == "mac"
 [browser_1096763_seen_widgets_post_reset.js]
 [browser_1161838_inserted_new_default_buttons.js]
 [browser_bootstrapped_custom_toolbar.js]
 [browser_customizemode_contextmenu_menubuttonstate.js]
 [browser_exit_background_customize_mode.js]
 [browser_overflow_use_subviews.js]
 [browser_panel_toggle.js]
 [browser_panelUINotifications.js]
+[browser_panelUINotifications_fullscreen.js]
+[browser_panelUINotifications_multiWindow.js]
 [browser_switch_to_customize_mode.js]
 [browser_synced_tabs_menu.js]
 [browser_check_tooltips_in_navbar.js]
 [browser_editcontrols_update.js]
 subsuite = clipboard
 [browser_remote_tabs_button.js]
--- a/browser/components/customizableui/test/browser_panelUINotifications.js
+++ b/browser/components/customizableui/test/browser_panelUINotifications.js
@@ -1,10 +1,12 @@
 "use strict";
 
+Cu.import("resource://gre/modules/AppMenuNotifications.jsm");
+
 /**
  * Tests that when we click on the main call-to-action of the doorhanger, the provided
  * action is called, and the doorhanger removed.
  */
 add_task(async function testMainActionCalled() {
   let options = {
     gBrowser: window.gBrowser,
     url: "about:blank"
@@ -13,17 +15,17 @@ add_task(async function testMainActionCa
   await BrowserTestUtils.withNewTab(options, function(browser) {
     let doc = browser.ownerDocument;
 
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
     let mainActionCalled = false;
     let mainAction = {
       callback: () => { mainActionCalled = true; }
     };
-    PanelUI.showNotification("update-manual", mainAction);
+    AppMenuNotifications.showNotification("update-manual", mainAction);
 
     isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
     let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     let doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
 
     let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
@@ -31,118 +33,16 @@ add_task(async function testMainActionCa
 
     ok(mainActionCalled, "Main action callback was called");
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
     is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
   });
 });
 
 /**
- * Tests that when we try to show a notification in a background window, it
- * does not display until the window comes back into the foreground. However,
- * it should display a badge.
- */
-add_task(async function testDoesNotShowDoorhangerForBackgroundWindow() {
-  let options = {
-    gBrowser: window.gBrowser,
-    url: "about:blank"
-  };
-
-  await BrowserTestUtils.withNewTab(options, async function(browser) {
-    let doc = browser.ownerDocument;
-
-    let win = await BrowserTestUtils.openNewBrowserWindow();
-    let mainActionCalled = false;
-    let mainAction = {
-      callback: () => { mainActionCalled = true; }
-    };
-    PanelUI.showNotification("update-manual", mainAction);
-    is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
-    is(PanelUI.menuButton.hasAttribute("badge-status"), true, "The background window has a badge.");
-
-    await BrowserTestUtils.closeWindow(win);
-    await SimpleTest.promiseFocus(window);
-    isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
-    let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
-    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
-    let doorhanger = notifications[0];
-    is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
-
-    let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
-    button.click();
-
-    ok(mainActionCalled, "Main action callback was called");
-    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
-    is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
-  });
-});
-
-/**
- * Tests that when we try to show a notification in a background window and in
- * a foreground window, if the foreground window's main action is called, the
- * background window's doorhanger will be removed.
- */
-add_task(async function testBackgroundWindowNotificationsAreRemovedByForeground() {
-  let options = {
-    gBrowser: window.gBrowser,
-    url: "about:blank"
-  };
-
-  await BrowserTestUtils.withNewTab(options, async function(browser) {
-    let win = await BrowserTestUtils.openNewBrowserWindow();
-    PanelUI.showNotification("update-manual", {callback() {}});
-    win.PanelUI.showNotification("update-manual", {callback() {}});
-    let doc = win.gBrowser.ownerDocument;
-    let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
-    let doorhanger = notifications[0];
-    let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
-    button.click();
-
-    await BrowserTestUtils.closeWindow(win);
-    await SimpleTest.promiseFocus(window);
-
-    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
-    is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
-  });
-});
-
-/**
- * Tests that when we try to show a notification in a background window and in
- * a foreground window, if the foreground window's doorhanger is dismissed,
- * the background window's doorhanger will also be dismissed once the window
- * regains focus.
- */
-add_task(async function testBackgroundWindowNotificationsAreDismissedByForeground() {
-  let options = {
-    gBrowser: window.gBrowser,
-    url: "about:blank"
-  };
-
-  await BrowserTestUtils.withNewTab(options, async function(browser) {
-    let win = await BrowserTestUtils.openNewBrowserWindow();
-    PanelUI.showNotification("update-manual", {callback() {}});
-    win.PanelUI.showNotification("update-manual", {callback() {}});
-    let doc = win.gBrowser.ownerDocument;
-    let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
-    let doorhanger = notifications[0];
-    let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
-    button.click();
-
-    await BrowserTestUtils.closeWindow(win);
-    await SimpleTest.promiseFocus(window);
-
-    is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
-    is(PanelUI.menuButton.hasAttribute("badge-status"), true,
-       "The dismissed notification should still have a badge status");
-
-    PanelUI.removeNotification(/.*/);
-  });
-});
-
-/**
  * This tests that when we click the secondary action for a notification,
  * it will display the badge for that notification on the PanelUI menu button.
  * Once we click on this button, we should see an item in the menu which will
  * call our main action.
  */
 add_task(async function testSecondaryActionWorkflow() {
   let options = {
     gBrowser: window.gBrowser,
@@ -153,17 +53,17 @@ add_task(async function testSecondaryAct
     let doc = browser.ownerDocument;
 
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
 
     let mainActionCalled = false;
     let mainAction = {
       callback: () => { mainActionCalled = true; },
     };
-    PanelUI.showNotification("update-manual", mainAction);
+    AppMenuNotifications.showNotification("update-manual", mainAction);
 
     isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
     let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     let doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
 
     let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
@@ -181,39 +81,39 @@ add_task(async function testSecondaryAct
 
     await PanelUI.hide();
     is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is shown on PanelUI button.");
 
     await PanelUI.show();
     menuItem.click();
     ok(mainActionCalled, "Main action callback was called");
 
-    PanelUI.removeNotification(/.*/);
+    AppMenuNotifications.removeNotification(/.*/);
   });
 });
 
 /**
  * We want to ensure a few things with this:
  * - Adding a doorhanger will make a badge disappear
  * - once the notification for the doorhanger is resolved (removed, not just dismissed),
  *   then we display any other badges that are remaining.
  */
 add_task(async function testInteractionWithBadges() {
   await BrowserTestUtils.withNewTab("about:blank", async function(browser) {
     let doc = browser.ownerDocument;
 
-    PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+    AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
     is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
 
     let mainActionCalled = false;
     let mainAction = {
       callback: () => { mainActionCalled = true; },
     };
-    PanelUI.showNotification("update-manual", mainAction);
+    AppMenuNotifications.showNotification("update-manual", mainAction);
 
     isnot(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is hidden on PanelUI button.");
     isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
     let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     let doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
 
@@ -229,96 +129,96 @@ add_task(async function testInteractionW
     let menuItem = PanelUI.mainView.querySelector(".panel-banner-item");
     is(menuItem.label, menuItem.getAttribute("label-update-manual"), "Showing correct label");
     is(menuItem.hidden, false, "update-manual menu item is showing.");
 
     menuItem.click();
     ok(mainActionCalled, "Main action callback was called");
 
     is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
-    PanelUI.removeNotification(/.*/);
+    AppMenuNotifications.removeNotification(/.*/);
     is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
   });
 });
 
 /**
  * This tests that adding a badge will not dismiss any existing doorhangers.
  */
 add_task(async function testAddingBadgeWhileDoorhangerIsShowing() {
   await BrowserTestUtils.withNewTab("about:blank", function(browser) {
     let doc = browser.ownerDocument;
 
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
     let mainActionCalled = false;
     let mainAction = {
       callback: () => { mainActionCalled = true; }
     };
-    PanelUI.showNotification("update-manual", mainAction);
-    PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+    AppMenuNotifications.showNotification("update-manual", mainAction);
+    AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
 
     isnot(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is hidden on PanelUI button.");
     isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
     let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     let doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
 
     let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
     mainActionButton.click();
 
     ok(mainActionCalled, "Main action callback was called");
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
     is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
-    PanelUI.removeNotification(/.*/);
+    AppMenuNotifications.removeNotification(/.*/);
     is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
   });
 });
 
 /**
  * Tests that badges operate like a stack.
  */
 add_task(async function testMultipleBadges() {
   await BrowserTestUtils.withNewTab("about:blank", async function(browser) {
     let doc = browser.ownerDocument;
     let menuButton = doc.getElementById("PanelUI-menu-button");
 
     is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
     is(menuButton.hasAttribute("badge"), false, "Should not have the badge attribute set");
 
-    PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+    AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
     is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
 
-    PanelUI.showBadgeOnlyNotification("update-succeeded");
+    AppMenuNotifications.showBadgeOnlyNotification("update-succeeded");
     is(menuButton.getAttribute("badge-status"), "update-succeeded", "Should have update-succeeded badge status (update > fxa)");
 
-    PanelUI.showBadgeOnlyNotification("update-failed");
+    AppMenuNotifications.showBadgeOnlyNotification("update-failed");
     is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
 
-    PanelUI.showBadgeOnlyNotification("download-severe");
+    AppMenuNotifications.showBadgeOnlyNotification("download-severe");
     is(menuButton.getAttribute("badge-status"), "download-severe", "Should have download-severe badge status");
 
-    PanelUI.showBadgeOnlyNotification("download-warning");
+    AppMenuNotifications.showBadgeOnlyNotification("download-warning");
     is(menuButton.getAttribute("badge-status"), "download-warning", "Should have download-warning badge status");
 
-    PanelUI.removeNotification(/^download-/);
+    AppMenuNotifications.removeNotification(/^download-/);
     is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
 
-    PanelUI.removeNotification(/^update-/);
+    AppMenuNotifications.removeNotification(/^update-/);
     is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
 
-    PanelUI.removeNotification(/^fxa-/);
+    AppMenuNotifications.removeNotification(/^fxa-/);
     is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
 
     await PanelUI.show();
     is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status (Hamburger menu opened)");
     PanelUI.hide();
 
-    PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
-    PanelUI.showBadgeOnlyNotification("update-succeeded");
-    PanelUI.removeNotification(/.*/);
+    AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
+    AppMenuNotifications.showBadgeOnlyNotification("update-succeeded");
+    AppMenuNotifications.removeNotification(/.*/);
     is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
   });
 });
 
 /**
  * Tests that non-badges also operate like a stack.
  */
 add_task(async function testMultipleNonBadges() {
@@ -331,28 +231,28 @@ add_task(async function testMultipleNonB
         called: false,
         callback: () => { updateManualAction.called = true; },
     };
     let updateRestartAction = {
         called: false,
         callback: () => { updateRestartAction.called = true; },
     };
 
-    PanelUI.showNotification("update-manual", updateManualAction);
+    AppMenuNotifications.showNotification("update-manual", updateManualAction);
 
     let notifications;
     let doorhanger;
 
     isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing.");
     notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
 
-    PanelUI.showNotification("update-restart", updateRestartAction);
+    AppMenuNotifications.showNotification("update-restart", updateRestartAction);
 
     isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing.");
     notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-restart-notification", "PanelUI is displaying the update-restart notification.");
 
     let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
@@ -377,47 +277,8 @@ add_task(async function testMultipleNonB
     isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "update-manual badge is hidden on PanelUI button.");
     is(menuItem.label, menuItem.getAttribute("label-update-manual"), "Showing correct label");
     is(menuItem.hidden, false, "update-manual menu item is showing.");
 
     menuItem.click();
     ok(updateManualAction.called, "update-manual main action callback was called");
   });
 });
-
-add_task(async function testFullscreen() {
-  let doc = document;
-
-  is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
-  let mainActionCalled = false;
-  let mainAction = {
-    callback: () => { mainActionCalled = true; }
-  };
-  PanelUI.showNotification("update-manual", mainAction);
-
-  isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
-  let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
-  is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
-  let doorhanger = notifications[0];
-  is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
-
-  let popuphiddenPromise = BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popuphidden");
-  EventUtils.synthesizeKey("VK_F11", {});
-  await popuphiddenPromise;
-  await new Promise(executeSoon);
-  is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
-
-  window.FullScreen.showNavToolbox();
-  is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
-
-  let popupshownPromise = BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popupshown");
-  EventUtils.synthesizeKey("VK_F11", {});
-  await popupshownPromise;
-  await new Promise(executeSoon);
-  isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
-  isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is not displaying on PanelUI button.");
-
-  let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
-  mainActionButton.click();
-  ok(mainActionCalled, "Main action callback was called");
-  is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
-  is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
-});
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_panelUINotifications_fullscreen.js
@@ -0,0 +1,42 @@
+"use strict";
+
+Cu.import("resource://gre/modules/AppMenuNotifications.jsm");
+
+add_task(async function testFullscreen() {
+  let doc = document;
+
+  is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+  let mainActionCalled = false;
+  let mainAction = {
+    callback: () => { mainActionCalled = true; }
+  };
+  AppMenuNotifications.showNotification("update-manual", mainAction);
+
+  isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+  let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
+  is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+  let doorhanger = notifications[0];
+  is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+  let popuphiddenPromise = BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popuphidden");
+  EventUtils.synthesizeKey("VK_F11", {});
+  await popuphiddenPromise;
+  await new Promise(executeSoon);
+  is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+  FullScreen.showNavToolbox();
+  is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
+
+  let popupshownPromise = BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popupshown");
+  EventUtils.synthesizeKey("VK_F11", {});
+  await popupshownPromise;
+  await new Promise(executeSoon);
+  isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+  isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is not displaying on PanelUI button.");
+
+  let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+  mainActionButton.click();
+  ok(mainActionCalled, "Main action callback was called");
+  is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+  is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js
@@ -0,0 +1,130 @@
+"use strict";
+
+Cu.import("resource://gre/modules/AppMenuNotifications.jsm");
+
+/**
+ * Tests that when we try to show a notification in a background window, it
+ * does not display until the window comes back into the foreground. However,
+ * it should display a badge.
+ */
+add_task(async function testDoesNotShowDoorhangerForBackgroundWindow() {
+  let options = {
+    gBrowser: window.gBrowser,
+    url: "about:blank"
+  };
+
+  await BrowserTestUtils.withNewTab(options, async function(browser) {
+    let doc = browser.ownerDocument;
+
+    let win = await BrowserTestUtils.openNewBrowserWindow();
+    let mainActionCalled = false;
+    let mainAction = {
+      callback: () => { mainActionCalled = true; }
+    };
+    AppMenuNotifications.showNotification("update-manual", mainAction);
+    is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
+    is(PanelUI.menuButton.hasAttribute("badge-status"), true, "The background window has a badge.");
+
+    await BrowserTestUtils.closeWindow(win);
+    await SimpleTest.promiseFocus(window);
+    isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+    let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    let doorhanger = notifications[0];
+    is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+    let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+    button.click();
+
+    ok(mainActionCalled, "Main action callback was called");
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+    is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+  });
+});
+
+/**
+ * Tests that when we try to show a notification in a background window and in
+ * a foreground window, if the foreground window's main action is called, the
+ * background window's doorhanger will be removed.
+ */
+add_task(async function testBackgroundWindowNotificationsAreRemovedByForeground() {
+  let options = {
+    gBrowser: window.gBrowser,
+    url: "about:blank"
+  };
+
+  await BrowserTestUtils.withNewTab(options, async function(browser) {
+    let win = await BrowserTestUtils.openNewBrowserWindow();
+    AppMenuNotifications.showNotification("update-manual", {callback() {}});
+    let doc = win.gBrowser.ownerDocument;
+    let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    let doorhanger = notifications[0];
+    let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+    button.click();
+
+    await BrowserTestUtils.closeWindow(win);
+    await SimpleTest.promiseFocus(window);
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+    is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+  });
+});
+
+/**
+ * Tests that when we try to show a notification in a background window and in
+ * a foreground window, if the foreground window's doorhanger is dismissed,
+ * the background window's doorhanger will also be dismissed once the window
+ * regains focus.
+ */
+add_task(async function testBackgroundWindowNotificationsAreDismissedByForeground() {
+  let options = {
+    gBrowser: window.gBrowser,
+    url: "about:blank"
+  };
+
+  await BrowserTestUtils.withNewTab(options, async function(browser) {
+    let win = await BrowserTestUtils.openNewBrowserWindow();
+    AppMenuNotifications.showNotification("update-manual", {callback() {}});
+    let doc = win.gBrowser.ownerDocument;
+    let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    let doorhanger = notifications[0];
+    let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+    button.click();
+
+    await BrowserTestUtils.closeWindow(win);
+    await SimpleTest.promiseFocus(window);
+
+    is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
+    is(PanelUI.menuButton.hasAttribute("badge-status"), true,
+       "The dismissed notification should still have a badge status");
+
+    AppMenuNotifications.removeNotification(/.*/);
+  });
+});
+
+/**
+ * Tests that when we open a new window while a notification is showing, the
+ * notification also shows on the new window.
+ */
+add_task(async function testOpenWindowAfterShowingNotification() {
+  AppMenuNotifications.showNotification("update-manual", {callback() {}});
+
+  let win = await BrowserTestUtils.openNewBrowserWindow();
+  let doc = win.gBrowser.ownerDocument;
+  let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
+  is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+  let doorhanger = notifications[0];
+  let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+  button.click();
+
+  await BrowserTestUtils.closeWindow(win);
+  await SimpleTest.promiseFocus(window);
+
+  is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
+  is(PanelUI.menuButton.hasAttribute("badge-status"), true,
+     "The dismissed notification should still have a badge status");
+
+  AppMenuNotifications.removeNotification(/.*/);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/AppMenuNotifications.jsm
@@ -0,0 +1,179 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["AppMenuNotifications"];
+
+const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+function AppMenuNotification(id, mainAction, secondaryAction, options = {}) {
+  this.id = id;
+  this.mainAction = mainAction;
+  this.secondaryAction = secondaryAction;
+  this.options = options;
+  this.dismissed = this.options.dismissed || false;
+}
+
+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 != 0) {
+          Services.obs.notifyObservers(null, "appMenu-notifications", "init");
+        }
+        break;
+    }
+  },
+
+  get activeNotification() {
+    if (this._notifications.length > 0) {
+      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;
+    });
+    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) {
+        Cu.reportError(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");
+  },
+};
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -168,16 +168,17 @@ EXTRA_JS_MODULES += [
     'addons/MatchPattern.jsm',
     'addons/WebNavigation.jsm',
     'addons/WebNavigationContent.js',
     'addons/WebNavigationFrames.jsm',
     'addons/WebRequest.jsm',
     'addons/WebRequestCommon.jsm',
     'addons/WebRequestContent.js',
     'addons/WebRequestUpload.jsm',
+    'AppMenuNotifications.jsm',
     'AsyncPrefs.jsm',
     'Battery.jsm',
     'BinarySearch.jsm',
     'BrowserUtils.jsm',
     'CanonicalJSON.jsm',
     'CertUtils.jsm',
     'CharsetMenu.jsm',
     'ClientID.jsm',