From 55ed3317db980e9f4118ae03f35d39fe9bbd4b95 Mon Sep 17 00:00:00 2001 From: Panos Astithas Date: Sun, 20 Nov 2016 18:40:04 +0100 Subject: [PATCH] Bug 1004061 - Make most doorhanger notifications persistent. r=florian MozReview-Commit-ID: IEPkDRnYpiM --HG-- extra : rebase_source : 59ba5ffcd41d80ad5e4a0e3b9cd05f5b3a4308a3 --- browser/base/content/browser-addons.js | 9 +- browser/base/content/browser-plugins.js | 1 + browser/base/content/browser.js | 61 +----- .../content/test/general/browser_bug553455.js | 8 +- .../test/popupNotifications/browser.ini | 2 + .../browser_popupNotification_4.js | 63 ------ .../browser_popupNotification_5.js | 205 ++++++++++++++++++ .../test/social/browser_social_activation.js | 24 +- browser/components/nsBrowserGlue.js | 1 + browser/modules/PermissionUI.jsm | 1 + browser/modules/SocialService.jsm | 1 + browser/modules/webrtcUI.jsm | 1 + .../passwordmgr/nsLoginManagerPrompter.js | 2 +- toolkit/modules/PopupNotifications.jsm | 30 ++- 14 files changed, 265 insertions(+), 144 deletions(-) create mode 100644 browser/base/content/test/popupNotifications/browser_popupNotification_5.js diff --git a/browser/base/content/browser-addons.js b/browser/base/content/browser-addons.js index f636873746ed..77ca857d3b23 100644 --- a/browser/base/content/browser-addons.js +++ b/browser/base/content/browser-addons.js @@ -88,10 +88,10 @@ const gXPInstallObserver = { const anchorID = "addons-notification-icon"; - // Make notifications persist a minimum of 30 seconds + // Make notifications persistent var options = { displayURI: installInfo.originatingURI, - timeout: Date.now() + 30000, + persistent: true, }; let cancelInstallation = () => { @@ -230,9 +230,10 @@ const gXPInstallObserver = { var brandShortName = brandBundle.getString("brandShortName"); var notificationID = aTopic; - // Make notifications persist a minimum of 30 seconds + // Make notifications persistent var options = { displayURI: installInfo.originatingURI, + persistent: true, timeout: Date.now() + 30000, }; @@ -567,7 +568,7 @@ var LightWeightThemeWebInstaller = { }; let options = { - timeout: Date.now() + 30000 + persistent: true }; PopupNotifications.show(gBrowser.selectedBrowser, "addon-theme-change", diff --git a/browser/base/content/browser-plugins.js b/browser/base/content/browser-plugins.js index bd4ddbbb1e16..fb68d262c65e 100644 --- a/browser/base/content/browser-plugins.js +++ b/browser/base/content/browser-plugins.js @@ -278,6 +278,7 @@ var gPluginHandler = { let options = { dismissed: !showNow, + persistent: showNow, eventCallback: this._clickToPlayNotificationEventCallback, primaryPlugin: primaryPluginPermission, pluginData: pluginData, diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 2619f181c7f1..ec26361dcbe9 100755 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -5971,7 +5971,7 @@ var OfflineApps = { let anchorID = "indexedDB-notification-icon"; PopupNotifications.show(browser, "offline-app-usage", message, - anchorID, mainAction); + anchorID, mainAction, null, { persistent: true }); // Now that we've warned once, prevent the warning from showing up // again. @@ -6051,6 +6051,7 @@ var OfflineApps = { [host]); let anchorID = "indexedDB-notification-icon"; let options = { + persistent: true, controlledItems : [[Cu.getWeakReference(browser), docId, uri]] }; notification = PopupNotifications.show(browser, notificationID, message, @@ -6142,18 +6143,12 @@ var IndexedDBPromptHelper = { responseTopic = this._permissionsResponse; } - const hiddenTimeoutDuration = 30000; // 30 seconds - const firstTimeoutDuration = 300000; // 5 minutes - - var timeoutId; - var observer = requestor.getInterface(Ci.nsIObserver); var mainAction = { label: gNavigatorBundle.getString("offlineApps.allow"), accessKey: gNavigatorBundle.getString("offlineApps.allowAccessKey"), callback: function() { - clearTimeout(timeoutId); observer.observe(null, responseTopic, Ci.nsIPermissionManager.ALLOW_ACTION); } @@ -6164,61 +6159,15 @@ var IndexedDBPromptHelper = { label: gNavigatorBundle.getString("offlineApps.never"), accessKey: gNavigatorBundle.getString("offlineApps.neverAccessKey"), callback: function() { - clearTimeout(timeoutId); observer.observe(null, responseTopic, Ci.nsIPermissionManager.DENY_ACTION); } } ]; - // This will be set to the result of PopupNotifications.show(). - var notification; - - function timeoutNotification() { - // Remove the notification. - if (notification) { - notification.remove(); - } - - // Clear all of our timeout stuff. We may be called directly, not just - // when the timeout actually elapses. - clearTimeout(timeoutId); - - // And tell the page that the popup timed out. - observer.observe(null, responseTopic, - Ci.nsIPermissionManager.UNKNOWN_ACTION); - } - - var options = { - eventCallback: function(state) { - // Don't do anything if the timeout has not been set yet. - if (!timeoutId) { - return; - } - - // If the popup is being dismissed start the short timeout. - if (state == "dismissed") { - clearTimeout(timeoutId); - timeoutId = setTimeout(timeoutNotification, hiddenTimeoutDuration); - return; - } - - // If the popup is being re-shown then clear the timeout allowing - // unlimited waiting. - if (state == "shown") { - clearTimeout(timeoutId); - } - } - }; - - notification = PopupNotifications.show(browser, topic, message, - this._notificationIcon, mainAction, - secondaryActions, options); - - // Set the timeoutId after the popup has been created, and use the long - // timeout value. If the user doesn't notice the popup after this amount of - // time then it is most likely not visible and we want to alert the page. - timeoutId = setTimeout(timeoutNotification, firstTimeoutDuration); + PopupNotifications.show(browser, topic, message, + this._notificationIcon, mainAction, + secondaryActions, { persistent: true }); } }; diff --git a/browser/base/content/test/general/browser_bug553455.js b/browser/base/content/test/general/browser_bug553455.js index 4f5d2b0e8903..f81282c63c46 100644 --- a/browser/base/content/test/general/browser_bug553455.js +++ b/browser/base/content/test/general/browser_bug553455.js @@ -116,13 +116,13 @@ function waitForNotification(aId, aExpectedCount = 1) { yield observerPromise; yield panelEventPromise; - info("Saw a notification"); + info("Saw a " + aId + " notification"); ok(PopupNotifications.isPanelOpen, "Panel should be open"); is(PopupNotifications.panel.childNodes.length, aExpectedCount, "Should be the right number of notifications"); if (PopupNotifications.panel.childNodes.length) { let nodes = Array.from(PopupNotifications.panel.childNodes); let notification = nodes.find(n => n.id == aId + "-notification"); - ok(notification, `Should have seen the right notification`); + ok(notification, "Should have seen the " + aId + " notification"); } return PopupNotifications.panel; @@ -512,7 +512,9 @@ function test_multiple() { function test_sequential() { return Task.spawn(function* () { // This test is only relevant if using the new doorhanger UI - if (!Preferences.get("xpinstall.customConfirmationUI", false)) { + // TODO: this subtest is disabled until multiple notification prompts are + // reworked in bug 1188152 + if (true || !Preferences.get("xpinstall.customConfirmationUI", false)) { return; } let pm = Services.perms; diff --git a/browser/base/content/test/popupNotifications/browser.ini b/browser/base/content/test/popupNotifications/browser.ini index 83bb7c5174b9..3faf8defe670 100644 --- a/browser/base/content/test/popupNotifications/browser.ini +++ b/browser/base/content/test/popupNotifications/browser.ini @@ -12,6 +12,8 @@ skip-if = (os == "linux" && (debug || asan)) skip-if = (os == "linux" && (debug || asan)) [browser_popupNotification_4.js] skip-if = (os == "linux" && (debug || asan)) +[browser_popupNotification_5.js] +skip-if = (os == "linux" && (debug || asan)) [browser_popupNotification_checkbox.js] skip-if = (os == "linux" && (debug || asan)) [browser_reshow_in_background.js] diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_4.js b/browser/base/content/test/popupNotifications/browser_popupNotification_4.js index a3d0098ff950..e22cfa326f3f 100644 --- a/browser/base/content/test/popupNotifications/browser_popupNotification_4.js +++ b/browser/base/content/test/popupNotifications/browser_popupNotification_4.js @@ -227,68 +227,5 @@ var tests = [ notification.remove(); goNext(); } - }, - // panel updates should fire the showing and shown callbacks again. - { id: "Test#11", - run: function() { - this.notifyObj = new BasicNotification(this.id); - this.notification = showNotification(this.notifyObj); - }, - onShown: function(popup) { - checkPopup(popup, this.notifyObj); - - this.notifyObj.showingCallbackTriggered = false; - this.notifyObj.shownCallbackTriggered = false; - - // Force an update of the panel. This is typically called - // automatically when receiving 'activate' or 'TabSelect' events, - // but from a setTimeout, which is inconvenient for the test. - PopupNotifications._update(); - - checkPopup(popup, this.notifyObj); - - this.notification.remove(); - }, - onHidden: function() { } - }, - // A first dismissed notification shouldn't stop _update from showing a second notification - { id: "Test#12", - run: function() { - this.notifyObj1 = new BasicNotification(this.id); - this.notifyObj1.id += "_1"; - this.notifyObj1.anchorID = "default-notification-icon"; - this.notifyObj1.options.dismissed = true; - this.notification1 = showNotification(this.notifyObj1); - - this.notifyObj2 = new BasicNotification(this.id); - this.notifyObj2.id += "_2"; - this.notifyObj2.anchorID = "geo-notification-icon"; - this.notifyObj2.options.dismissed = true; - this.notification2 = showNotification(this.notifyObj2); - - this.notification2.dismissed = false; - PopupNotifications._update(); - }, - onShown: function(popup) { - checkPopup(popup, this.notifyObj2); - this.notification1.remove(); - this.notification2.remove(); - }, - onHidden: function(popup) { } - }, - // The anchor icon should be shown for notifications in background windows. - { id: "Test#13", - run: function() { - let notifyObj = new BasicNotification(this.id); - notifyObj.options.dismissed = true; - let win = gBrowser.replaceTabWithWindow(gBrowser.addTab("about:blank")); - whenDelayedStartupFinished(win, function() { - showNotification(notifyObj); - let anchor = document.getElementById("default-notification-icon"); - is(anchor.getAttribute("showing"), "true", "the anchor is shown"); - win.close(); - goNext(); - }); - } } ]; diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_5.js b/browser/base/content/test/popupNotifications/browser_popupNotification_5.js new file mode 100644 index 000000000000..e84c7b2899dd --- /dev/null +++ b/browser/base/content/test/popupNotifications/browser_popupNotification_5.js @@ -0,0 +1,205 @@ +/* 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 test() { + waitForExplicitFinish(); + + ok(PopupNotifications, "PopupNotifications object exists"); + ok(PopupNotifications.panel, "PopupNotifications panel exists"); + + setup(); + goNext(); +} + +var gNotification; + +var tests = [ + // panel updates should fire the showing and shown callbacks again. + { id: "Test#1", + run: function() { + this.notifyObj = new BasicNotification(this.id); + this.notification = showNotification(this.notifyObj); + }, + onShown: function(popup) { + checkPopup(popup, this.notifyObj); + + this.notifyObj.showingCallbackTriggered = false; + this.notifyObj.shownCallbackTriggered = false; + + // Force an update of the panel. This is typically called + // automatically when receiving 'activate' or 'TabSelect' events, + // but from a setTimeout, which is inconvenient for the test. + PopupNotifications._update(); + + checkPopup(popup, this.notifyObj); + + this.notification.remove(); + }, + onHidden: function() { } + }, + // A first dismissed notification shouldn't stop _update from showing a second notification + { id: "Test#2", + run: function() { + this.notifyObj1 = new BasicNotification(this.id); + this.notifyObj1.id += "_1"; + this.notifyObj1.anchorID = "default-notification-icon"; + this.notifyObj1.options.dismissed = true; + this.notification1 = showNotification(this.notifyObj1); + + this.notifyObj2 = new BasicNotification(this.id); + this.notifyObj2.id += "_2"; + this.notifyObj2.anchorID = "geo-notification-icon"; + this.notifyObj2.options.dismissed = true; + this.notification2 = showNotification(this.notifyObj2); + + this.notification2.dismissed = false; + PopupNotifications._update(); + }, + onShown: function(popup) { + checkPopup(popup, this.notifyObj2); + this.notification1.remove(); + this.notification2.remove(); + }, + onHidden: function(popup) { } + }, + // The anchor icon should be shown for notifications in background windows. + { id: "Test#3", + run: function() { + let notifyObj = new BasicNotification(this.id); + notifyObj.options.dismissed = true; + let win = gBrowser.replaceTabWithWindow(gBrowser.addTab("about:blank")); + whenDelayedStartupFinished(win, function() { + showNotification(notifyObj); + let anchor = document.getElementById("default-notification-icon"); + is(anchor.getAttribute("showing"), "true", "the anchor is shown"); + win.close(); + goNext(); + }); + } + }, + // Test that persistent doesn't allow the notification to persist after + // navigation. + { id: "Test#4", + run: function* () { + this.oldSelectedTab = gBrowser.selectedTab; + gBrowser.selectedTab = gBrowser.addTab("about:blank"); + yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/"); + this.notifyObj = new BasicNotification(this.id); + this.notifyObj.addOptions({ + persistent: true + }); + this.notification = showNotification(this.notifyObj); + }, + onShown: function* (popup) { + this.complete = false; + + yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/"); + yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/"); + + // This code should not be executed. + ok(false, "Should have removed the notification after navigation"); + // Properly dismiss and cleanup in case the unthinkable happens. + this.complete = true; + triggerSecondaryCommand(popup, 1); + }, + onHidden: function(popup) { + ok(!this.complete, "Should have hidden the notification after navigation"); + this.notification.remove(); + gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.selectedTab = this.oldSelectedTab; + } + }, + // Test that persistent allows the notification to persist until explicitly + // dismissed. + { id: "Test#5", + run: function* () { + this.oldSelectedTab = gBrowser.selectedTab; + gBrowser.selectedTab = gBrowser.addTab("about:blank"); + yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/"); + this.notifyObj = new BasicNotification(this.id); + this.notifyObj.addOptions({ + persistent: true + }); + this.notification = showNotification(this.notifyObj); + }, + onShown: function* (popup) { + this.complete = false; + + // Notification should persist after attempt to dismiss by clicking on the + // content area. + let browser = gBrowser.selectedBrowser; + yield BrowserTestUtils.synthesizeMouseAtCenter("body", {}, browser) + + // Notification should be hidden after dismissal via Not Now. + this.complete = true; + triggerSecondaryCommand(popup, 1); + }, + onHidden: function(popup) { + ok(this.complete, "Should have hidden the notification after clicking Not Now"); + this.notification.remove(); + gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.selectedTab = this.oldSelectedTab; + } + }, + // Test that persistent panels are still open after switching to another tab + // and back. + { id: "Test#6a", + run: function* () { + this.notifyObj = new BasicNotification(this.id); + this.notifyObj.options.persistent = true; + gNotification = showNotification(this.notifyObj); + }, + onShown: function* (popup) { + this.oldSelectedTab = gBrowser.selectedTab; + gBrowser.selectedTab = gBrowser.addTab("about:blank"); + info("Waiting for the new tab to load."); + yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/"); + }, + onHidden: function(popup) { + ok(true, "Should have hidden the notification after tab switch"); + gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.selectedTab = this.oldSelectedTab; + } + }, + // Second part of the previous test that compensates for the limitation in + // runNextTest that expects a single onShown/onHidden invocation per test. + { id: "Test#6b", + run: function* () { + let id = PopupNotifications.panel.firstChild.getAttribute("popupid"); + ok(id.endsWith("Test#6a"), "Should have found the notification from Test6a"); + ok(PopupNotifications.isPanelOpen, "Should have shown the popup again after getting back to the tab"); + gNotification.remove(); + gNotification = null; + goNext(); + } + }, + // Test that persistent panels are still open after switching to another + // window and back. + { id: "Test#7", + run: function* () { + this.oldSelectedTab = gBrowser.selectedTab; + gBrowser.selectedTab = gBrowser.addTab("about:blank"); + let notifyObj = new BasicNotification(this.id); + notifyObj.options.persistent = true; + this.notification = showNotification(notifyObj); + let win = gBrowser.replaceTabWithWindow(gBrowser.addTab("about:blank")); + whenDelayedStartupFinished(win, () => { + ok(notifyObj.shownCallbackTriggered, "Should have triggered the shown callback"); + let anchor = win.document.getElementById("default-notification-icon"); + win.PopupNotifications._reshowNotifications(anchor); + ok(win.PopupNotifications.panel.childNodes.length == 0, + "no notification displayed in new window"); + ok(PopupNotifications.isPanelOpen, "Should be still showing the popup in the first window"); + win.close(); + let id = PopupNotifications.panel.firstChild.getAttribute("popupid"); + ok(id.endsWith("Test#7"), "Should have found the notification from Test7"); + ok(PopupNotifications.isPanelOpen, "Should have shown the popup again after getting back to the window"); + this.notification.remove(); + gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.selectedTab = this.oldSelectedTab; + goNext(); + }); + } + } +]; diff --git a/browser/base/content/test/social/browser_social_activation.js b/browser/base/content/test/social/browser_social_activation.js index 33b7f3e4d8b6..74f8c59b8c7f 100644 --- a/browser/base/content/test/social/browser_social_activation.js +++ b/browser/base/content/test/social/browser_social_activation.js @@ -115,23 +115,23 @@ function activateOneProvider(manifest, finishActivation, aCallback) { let panel = document.getElementById("servicesInstall-notification"); BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => { ok(!panel.hidden, "servicesInstall-notification panel opened"); + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden").then(() => { + ok(panel.hidden, "servicesInstall-notification panel hidden"); + if (!finishActivation) { + ok(panel.hidden, "activation panel is not showing"); + executeSoon(aCallback); + } else { + waitForProviderLoad(manifest.origin).then(() => { + checkSocialUI(); + executeSoon(aCallback); + }); + } + }); if (finishActivation) panel.button.click(); else panel.closebutton.click(); }); - BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden").then(() => { - ok(panel.hidden, "servicesInstall-notification panel hidden"); - if (!finishActivation) { - ok(panel.hidden, "activation panel is not showing"); - executeSoon(aCallback); - } else { - waitForProviderLoad(manifest.origin).then(() => { - checkSocialUI(); - executeSoon(aCallback); - }); - } - }); // the test will continue as the popup events fire... activateProvider(manifest.origin, function() { diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js index 3b910b45ecd7..e07cdacd613a 100644 --- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -2854,6 +2854,7 @@ var E10SAccessibilityCheck = { let options = { popupIconURL: "chrome://browser/skin/e10s-64@2x.png", learnMoreURL: Services.urlFormatter.formatURLPref("app.support.e10sAccessibilityUrl"), + persistent: true, persistWhileVisible: true, hideNotNow: true, }; diff --git a/browser/modules/PermissionUI.jsm b/browser/modules/PermissionUI.jsm index 5fa0f9f062b6..ecb6c746d250 100644 --- a/browser/modules/PermissionUI.jsm +++ b/browser/modules/PermissionUI.jsm @@ -337,6 +337,7 @@ this.PermissionPromptPrototype = { if (!options.hasOwnProperty('displayURI') || options.displayURI) { options.displayURI = this.principal.URI; } + options.persistent = true; this.onBeforeShow(); chromeWin.PopupNotifications.show(this.browser, diff --git a/browser/modules/SocialService.jsm b/browser/modules/SocialService.jsm index 6eb99753fc94..2cc4255d1f00 100644 --- a/browser/modules/SocialService.jsm +++ b/browser/modules/SocialService.jsm @@ -567,6 +567,7 @@ this.SocialService = { let options = { learnMoreURL: Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api", + persistent: true, }; let anchor = "servicesInstall-notification-icon"; let notificationid = "servicesInstall"; diff --git a/browser/modules/webrtcUI.jsm b/browser/modules/webrtcUI.jsm index 149f0e745d58..75b3d61f5fb8 100644 --- a/browser/modules/webrtcUI.jsm +++ b/browser/modules/webrtcUI.jsm @@ -356,6 +356,7 @@ function prompt(aBrowser, aRequest) { } let options = { + persistent: true, eventCallback: function(aTopic, aNewBrowser) { if (aTopic == "swapping") return true; diff --git a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js index 2fd54bec5b5a..a6b3ff982b88 100644 --- a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js +++ b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js @@ -957,9 +957,9 @@ LoginManagerPrompter.prototype = { mainAction, secondaryActions, { - timeout: Date.now() + 10000, displayURI: Services.io.newURI(login.hostname, null, null), persistWhileVisible: true, + persistent: true, passwordNotificationType: type, eventCallback: function(topic) { switch (topic) { diff --git a/toolkit/modules/PopupNotifications.jsm b/toolkit/modules/PopupNotifications.jsm index 61880075e456..e2046d08d6df 100644 --- a/toolkit/modules/PopupNotifications.jsm +++ b/toolkit/modules/PopupNotifications.jsm @@ -309,6 +309,11 @@ PopupNotifications.prototype = { * persistWhileVisible: * A boolean. If true, a visible notification will always * persist across location changes. + * persistent: A boolean. If true, the notification will always + * persist even across tab and app changes (but not across + * location changes), until the user accepts or rejects + * the request. The notification will never be implicitly + * dismissed. * dismissed: Whether the notification should be added as a dismissed * notification. Dismissed notifications can be activated * by clicking on their anchorElement. @@ -475,7 +480,7 @@ PopupNotifications.prototype = { if (notification.options.persistWhileVisible && this.isPanelOpen) { if ("persistence" in notification.options && - notification.options.persistence) + notification.options.persistence) notification.options.persistence--; return true; } @@ -589,6 +594,14 @@ PopupNotifications.prototype = { this.nextDismissReason = telemetryReason; } + // An explicitly dismissed persistent notification effectively becomes + // non-persistent. + if (this.panel.firstChild && + (telemetryReason == TELEMETRY_STAT_DISMISSAL_CLOSE_BUTTON || + telemetryReason == TELEMETRY_STAT_DISMISSAL_NOT_NOW)) { + this.panel.firstChild.notification.options.persistent = false; + } + let browser = this.panel.firstChild && this.panel.firstChild.notification.browser; this.panel.hidePopup(); @@ -831,7 +844,7 @@ PopupNotifications.prototype = { // If the panel is already open but we're changing anchors, we need to hide // it first. Otherwise it can appear in the wrong spot. (_hidePanel is // safe to call even if the panel is already hidden.) - let promise = this._hidePanel().then(() => { + this._hidePanel().then(() => { // If the anchor element is hidden or null, use the tab as the anchor. We // only ever show notifications for the current browser, so we can just use // the current tab. @@ -846,6 +859,12 @@ PopupNotifications.prototype = { this._currentAnchorElement = anchorElement; + if (notificationsToShow.some(n => n.options.persistent)) { + this.panel.setAttribute("noautohide", "true"); + } else { + this.panel.removeAttribute("noautohide"); + } + // On OS X and Linux we need a different panel arrow color for // click-to-play plugins, so copy the popupid and use css. this.panel.setAttribute("popupid", this.panel.firstChild.getAttribute("popupid")); @@ -931,9 +950,10 @@ PopupNotifications.prototype = { } } - // Filter out notifications that have been dismissed. + // Filter out notifications that have been dismissed, unless they are + // persistent. let notificationsToShow = notifications.filter(function(n) { - return !n.dismissed && !n.options.neverShow; + return (!n.dismissed || n.options.persistent) && !n.options.neverShow; }); if (useIconBox) { @@ -1111,7 +1131,7 @@ PopupNotifications.prototype = { } // Ensure we move focus into the panel because it's opened through user interaction: - this.panel.removeAttribute("noautofocus", "true"); + this.panel.removeAttribute("noautofocus"); this._reshowNotifications(anchor); },