Bug 1715713 - Prevent showing multiple newmailalert.xhtml notification windows. r=mkmelin
authorPing Chen <remotenonsense@gmail.com>
Mon, 14 Jun 2021 13:29:26 +0300
changeset 32814 81f4842d2dd7e44befef8edc478a014ffff14f19
parent 32813 6c9fb9bfd4c76de7579a9859e3fd9de8314a1b9c
child 32815 64bbd687d6d14f81f4917029570b9253f0eb7a23
push id18873
push usermkmelin@iki.fi
push dateMon, 14 Jun 2021 10:30:36 +0000
treeherdercomm-central@64bbd687d6d1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs1715713
Bug 1715713 - Prevent showing multiple newmailalert.xhtml notification windows. r=mkmelin This patch makes two changes: 1. If a newmailalert.xhtml window is already shown, save the folder and show a new notification only after the current notification is closed. 2. Pass new message keys to newmailalert.js. Previously, newmailalert.js receives a root folder and scans all subfolders for NEW messages, which is unnecessary and may incorrectly include old messages. Differential Revision: https://phabricator.services.mozilla.com/D117617
mail/base/content/foldersummary.js
mailnews/base/content/newmailalert.js
mailnews/base/src/MailNotificationManager.jsm
--- a/mail/base/content/foldersummary.js
+++ b/mail/base/content/foldersummary.js
@@ -42,55 +42,55 @@
         "chrome://messenger/locale/messenger.properties"
       );
     }
 
     hasMessages() {
       return this.lastElementChild;
     }
 
+    static createFolderSummaryMessage() {
+      let vbox = document.createXULElement("vbox");
+      vbox.setAttribute("class", "folderSummaryMessage");
+
+      let hbox = document.createXULElement("hbox");
+      hbox.setAttribute("class", "folderSummary-message-row");
+
+      let subject = document.createXULElement("label");
+      subject.setAttribute("class", "folderSummary-subject");
+
+      let sender = document.createXULElement("label");
+      sender.setAttribute("class", "folderSummary-sender");
+      sender.setAttribute("crop", "right");
+
+      hbox.appendChild(subject);
+      hbox.appendChild(sender);
+
+      let preview = document.createXULElement("description");
+      preview.setAttribute(
+        "class",
+        "folderSummary-message-row folderSummary-previewText"
+      );
+      preview.setAttribute("crop", "right");
+
+      vbox.appendChild(hbox);
+      vbox.appendChild(preview);
+      return vbox;
+    }
+
     /**
      * Check the given folder for NEW messages.
      * @param {nsIMsgFolder} folder - The folder to examine.
      * @param {nsIUrlListener} urlListener - Listener to notify if we run urls
      *   to fetch msgs.
      * @param Object outAsync - Object with value property set to true if there
      *   are async fetches pending (a message preview will be available later).
      * @returns true if the folder knows about messages that should be shown.
      */
     parseFolder(folder, urlListener, outAsync) {
-      function createFolderSummaryMessage() {
-        let vbox = document.createXULElement("vbox");
-        vbox.setAttribute("class", "folderSummaryMessage");
-
-        let hbox = document.createXULElement("hbox");
-        hbox.setAttribute("class", "folderSummary-message-row");
-
-        let subject = document.createXULElement("label");
-        subject.setAttribute("class", "folderSummary-subject");
-
-        let sender = document.createXULElement("label");
-        sender.setAttribute("class", "folderSummary-sender");
-        sender.setAttribute("crop", "right");
-
-        hbox.appendChild(subject);
-        hbox.appendChild(sender);
-
-        let preview = document.createXULElement("description");
-        preview.setAttribute(
-          "class",
-          "folderSummary-message-row folderSummary-previewText"
-        );
-        preview.setAttribute("crop", "right");
-
-        vbox.appendChild(hbox);
-        vbox.appendChild(preview);
-        return vbox;
-      }
-
       // Skip servers, Trash, Junk folders and newsgroups.
       if (
         !folder ||
         folder.isServer ||
         !folder.hasNewMessages ||
         folder.getFlag(Ci.nsMsgFolderFlags.Junk) ||
         folder.getFlag(Ci.nsMsgFolderFlags.Trash) ||
         folder.server instanceof Ci.nsINntpIncomingServer
@@ -185,17 +185,17 @@
           return false;
         }
 
         for (
           let i = 0;
           i + curHdrsInPopup < this.maxMsgHdrsInPopup && i < msgKeys.length;
           i++
         ) {
-          let msgBox = createFolderSummaryMessage();
+          let msgBox = MozFolderSummary.createFolderSummaryMessage();
           let msgHdr = msgDatabase.GetMsgHdrForKey(msgKeys[i]);
           msgBox.addEventListener("click", event => {
             if (event.button !== 0) {
               return;
             }
             MailUtils.displayMessageInFolderTab(msgHdr);
           });
 
@@ -239,16 +239,76 @@
             );
           }
           this.appendChild(msgBox);
           haveMsgsToShow = true;
         }
       }
       return haveMsgsToShow;
     }
+
+    /**
+     * Render NEW messages in a folder.
+     * @param {nsIMsgFolder} folder - A real folder containing new messages.
+     * @param {number[]} msgKeys - The keys of new messages.
+     */
+    render(folder, msgKeys) {
+      let msgDatabase = folder.msgDatabase;
+      for (let msgKey of msgKeys) {
+        let msgBox = MozFolderSummary.createFolderSummaryMessage();
+        let msgHdr = msgDatabase.GetMsgHdrForKey(msgKey);
+        msgBox.addEventListener("click", event => {
+          if (event.button !== 0) {
+            return;
+          }
+          MailUtils.displayMessageInFolderTab(msgHdr);
+        });
+
+        if (this.showSubject) {
+          let msgSubject = msgHdr.mime2DecodedSubject;
+          const kMsgFlagHasRe = 0x0010; // MSG_FLAG_HAS_RE
+          if (msgHdr.flags & kMsgFlagHasRe) {
+            msgSubject = msgSubject ? "Re: " + msgSubject : "Re: ";
+          }
+          msgBox.querySelector(
+            ".folderSummary-subject"
+          ).textContent = msgSubject;
+        }
+
+        if (this.showSender) {
+          let addrs = MailServices.headerParser.parseEncodedHeader(
+            msgHdr.author,
+            msgHdr.effectiveCharset,
+            false
+          );
+          let folderSummarySender = msgBox.querySelector(
+            ".folderSummary-sender"
+          );
+          // Set the label value instead of textContent to avoid wrapping.
+          folderSummarySender.value =
+            addrs.length > 0 ? addrs[0].name || addrs[0].email : "";
+          if (addrs.length > 1) {
+            let andOthersStr = this.messengerBundle.GetStringFromName(
+              "andOthers"
+            );
+            folderSummarySender.value += " " + andOthersStr;
+          }
+        }
+
+        if (this.showPreview) {
+          // Get the preview text as a UTF-8 encoded string.
+          msgBox.querySelector(
+            ".folderSummary-previewText"
+          ).textContent = decodeURIComponent(
+            escape(msgHdr.getStringProperty("preview") || "")
+          );
+        }
+        this.appendChild(msgBox);
+      }
+    }
   }
   customElements.define("folder-summary", MozFolderSummary);
 
   /**
    * MozFolderTooltip displays a tooltip summarizing the folder status:
    *  - if there are NEW messages, display a summary of them
    *  - if the folder name is cropped, include the name and more details
    *  - a summary of the unread count in this folder and its subfolders
--- a/mailnews/base/content/newmailalert.js
+++ b/mailnews/base/content/newmailalert.js
@@ -9,88 +9,47 @@ var { PluralForm } = ChromeUtils.import(
 
 // Copied from nsILookAndFeel.h, see comments on eIntID_AlertNotificationOrigin.
 var NS_ALERT_LEFT = 2;
 var NS_ALERT_TOP = 4;
 
 var gNumNewMsgsToShowInAlert = 6;
 var gOpenTime = 4000; // total time the alert should stay up once we are done animating.
 
-var gPendingPreviewFetchRequests = 0;
+var gAlertListener = null;
 var gOrigin = 0; // Default value: alert from bottom right.
 var gDragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
   Ci.nsIDragService
 );
 
 function prefillAlertInfo() {
   // unwrap all the args....
-  // arguments[0] --> The nsIMsgFolder with new mail
-  var rootFolder = window.arguments[0];
+  // arguments[0] --> The real nsIMsgFolder with new mail.
+  // arguments[1] --> The keys of new messages.
+  // arguments[2] --> The nsIObserver to receive window closed event.
+  let [folder, newMsgKeys, listener] = window.arguments;
+  newMsgKeys = newMsgKeys.wrappedJSObject;
+  gAlertListener = listener.QueryInterface(Ci.nsIObserver);
 
   // Generate an account label string based on the root folder.
   var label = document.getElementById("alertTitle");
-  var totalNumNewMessages = rootFolder.getNumNewMessages(true);
+  var totalNumNewMessages = newMsgKeys.length;
   let message = document
     .getElementById("bundle_messenger")
     .getString("newMailAlert_message");
   label.value = PluralForm.get(totalNumNewMessages, message)
-    .replace("#1", rootFolder.prettyName)
+    .replace("#1", folder.server.rootFolder.prettyName)
     .replace("#2", totalNumNewMessages);
 
-  // This is really the root folder and we have to walk through the list to
-  // find the real folder that has new mail in it...:(
+  // <folder-summary> handles rendering of new messages.
   var folderSummaryInfoEl = document.getElementById("folderSummaryInfo");
   folderSummaryInfoEl.maxMsgHdrsInPopup = gNumNewMsgsToShowInAlert;
-  for (let folder of rootFolder.descendants) {
-    if (folder.hasNewMessages) {
-      let notify =
-        // Any folder which is an inbox or ...
-        folder.getFlag(Ci.nsMsgFolderFlags.Inbox) ||
-        // any non-special or non-virtual folder. In other words, we don't
-        // notify for Drafts|Trash|SentMail|Templates|Junk|Archive|Queue or virtual.
-        !(
-          folder.flags &
-          (Ci.nsMsgFolderFlags.SpecialUse | Ci.nsMsgFolderFlags.Virtual)
-        );
-
-      if (notify) {
-        var asyncFetch = {};
-        folderSummaryInfoEl.parseFolder(
-          folder,
-          new urlListener(folder),
-          asyncFetch
-        );
-        if (asyncFetch.value) {
-          gPendingPreviewFetchRequests++;
-        }
-      }
-    }
-  }
+  folderSummaryInfoEl.render(folder, newMsgKeys);
 }
 
-function urlListener(aFolder) {
-  this.mFolder = aFolder;
-}
-
-urlListener.prototype = {
-  OnStartRunningUrl(aUrl) {},
-
-  OnStopRunningUrl(aUrl, aExitCode) {
-    let folderSummaryInfoEl = document.getElementById("folderSummaryInfo");
-    folderSummaryInfoEl.parseFolder(this.mFolder, null, {});
-    gPendingPreviewFetchRequests--;
-
-    // when we are done running all of our urls for fetching the preview text,
-    // start the alert.
-    if (!gPendingPreviewFetchRequests) {
-      showAlert();
-    }
-  },
-};
-
 function onAlertLoad() {
   let dragSession = gDragService.getCurrentSession();
   if (dragSession && dragSession.sourceNode) {
     // If a drag session is active, adjusting this window's dimensions causes
     // the drag session to be abruptly terminated. To avoid interrupting the
     // user, wait until the drag is finished and then set up and show the alert.
     dragSession.sourceNode.addEventListener("dragend", () => doOnAlertLoad());
   } else {
@@ -101,23 +60,19 @@ function onAlertLoad() {
 function doOnAlertLoad() {
   prefillAlertInfo();
 
   gOpenTime = Services.prefs.getIntPref("alerts.totalOpenTime");
 
   // bogus call to make sure the window is moved offscreen until we are ready for it.
   resizeAlert(true);
 
-  // if we aren't waiting to fetch preview text, then go ahead and
-  // start showing the alert.
-  if (!gPendingPreviewFetchRequests) {
-    // Let the JS thread unwind, to give layout
-    // a chance to recompute the styles and widths for our alert text.
-    setTimeout(showAlert, 0);
-  }
+  // Let the JS thread unwind, to give layout
+  // a chance to recompute the styles and widths for our alert text.
+  setTimeout(showAlert, 0);
 }
 
 // If the user initiated the alert, show it right away, otherwise start opening the alert with
 // the fade effect.
 function showAlert() {
   if (!document.getElementById("folderSummaryInfo").hasMessages()) {
     closeAlert(); // no mail, so don't bother showing the alert...
     return;
@@ -181,9 +136,10 @@ function fadeOutAlert() {
       closeAlert();
     }
   });
   alertContainer.setAttribute("fade-out", true);
 }
 
 function closeAlert() {
   window.close();
+  gAlertListener.observe(null, "newmailalert-closed", "");
 }
--- a/mailnews/base/src/MailNotificationManager.jsm
+++ b/mailnews/base/src/MailNotificationManager.jsm
@@ -32,16 +32,20 @@ class MailNotificationManager {
     "nsIFolderListener",
     "mozINewMailListener",
   ]);
 
   constructor() {
     this._systemAlertAvailable = true;
     this._unreadChatCount = 0;
     this._unreadMailCount = 0;
+    // @type {Map<nsIMsgFolder, number>} - A map of folder and its last biff time.
+    this._folderBiffTime = new Map();
+    // @type {Set<nsIMsgFolder>} - A set of folders to show alert for.
+    this._pendingFolders = new Set();
 
     this._logger = console.createInstance({
       prefix: "mail.notification",
       maxLogLevel: "Warn",
       maxLogLevelPref: "mail.notification.loglevel",
     });
     this._bundle = Services.strings.createBundle(
       "chrome://messenger/locale/messenger.properties"
@@ -71,32 +75,44 @@ class MailNotificationManager {
       Services.obs.addObserver(this, "new-directed-incoming-message");
     }
     if (AppConstants.platform == "win") {
       Services.obs.addObserver(this, "window-restored-from-tray");
     }
   }
 
   observe(subject, topic, data) {
-    if (topic == "alertclickcallback") {
-      // Display the associated message when an alert is clicked.
-      let msgHdr = Cc["@mozilla.org/messenger;1"]
-        .getService(Ci.nsIMessenger)
-        .msgHdrFromURI(data);
-      MailUtils.displayMessageInFolderTab(msgHdr);
-    } else if (topic == "unread-im-count-changed") {
-      this._logger.log(`Unread chat count changed to ${this._unreadChatCount}`);
-      this._unreadChatCount = parseInt(data, 10) || 0;
-      this._updateUnreadCount();
-    } else if (topic == "new-directed-incoming-messenger") {
-      this._animateDockIcon();
-    } else if (topic == "window-restored-from-tray") {
-      this._updateUnreadCount();
-    } else if (topic == "profile-before-change") {
-      this._osIntegration?.onExit();
+    switch (topic) {
+      case "alertclickcallback":
+        // Display the associated message when an alert is clicked.
+        let msgHdr = Cc["@mozilla.org/messenger;1"]
+          .getService(Ci.nsIMessenger)
+          .msgHdrFromURI(data);
+        MailUtils.displayMessageInFolderTab(msgHdr);
+        return;
+      case "unread-im-count-changed":
+        this._logger.log(
+          `Unread chat count changed to ${this._unreadChatCount}`
+        );
+        this._unreadChatCount = parseInt(data, 10) || 0;
+        this._updateUnreadCount();
+        return;
+      case "new-directed-incoming-messenger":
+        this._animateDockIcon();
+        return;
+      case "window-restored-from-tray":
+        this._updateUnreadCount();
+        return;
+      case "profile-before-change":
+        this._osIntegration?.onExit();
+        return;
+      case "newmailalert-closed":
+        // newmailalert.xhtml is closed, try to show the next queued folder.
+        this._customizedAlertShown = false;
+        this._showCustomizedAlert();
     }
   }
 
   /**
    * Following are nsIFolderListener interfaces. Do nothing about them.
    */
   OnItemAdded() {}
 
@@ -120,25 +136,27 @@ class MailNotificationManager {
     if (!Services.prefs.getBoolPref("mail.biff.show_alert")) {
       return;
     }
 
     this._logger.debug(
       `OnItemIntPropertyChanged; property=${property}: ${oldValue} => ${newValue}, folder.URI=${folder.URI}`
     );
 
-    if (
-      property == "BiffState" &&
-      newValue == Ci.nsIMsgFolder.nsMsgBiffState_NewMail
-    ) {
-      // The folder argument is a root folder.
-      this._fillAlertInfo(folder);
-    } else if (property == "NewMailReceived") {
-      // The folder argument is a real folder.
-      this._fillAlertInfo(folder);
+    switch (property) {
+      case "BiffState":
+        if (newValue == Ci.nsIMsgFolder.nsMsgBiffState_NewMail) {
+          // The folder argument is a root folder.
+          this._fillAlertInfo(folder);
+        }
+        break;
+      case "NewMailReceived":
+        // The folder argument is a real folder.
+        this._fillAlertInfo(folder);
+        break;
     }
   }
 
   /**
    * @see mozINewMailNotificationService
    */
   onCountChanged(count) {
     this._logger.log(`Unread mail count changed to ${count}`);
@@ -320,23 +338,77 @@ class MailNotificationManager {
       } catch (e) {
         this._logger.error(e);
         this._systemAlertAvailable = false;
       }
     }
 
     // The use_system_alert pref is false or showAlert somehow failed, use the
     // customized alert window.
+    this._showCustomizedAlert(folder);
+  }
+
+  /**
+   * Show a customized alert window (newmailalert.xhtml), if there is already
+   * one showing, do not show another one, because the newer one will block the
+   * older one. Instead, save the folder and newMsgKeys to this._pendingFolders.
+   * @param {nsIMsgFolder} [folder] - The folder containing new messages.
+   */
+  _showCustomizedAlert(folder) {
+    let newMsgKeys;
+    if (folder) {
+      // Show this folder or queue it.
+      newMsgKeys = folder.msgDatabase
+        .getNewList()
+        .slice(-folder.getNumNewMessages(false));
+      if (this._customizedAlertShown) {
+        this._pendingFolders.add(folder);
+        return;
+      }
+    } else {
+      // Get the next folder from the queue.
+      folder = this._pendingFolders.keys().next().value;
+      if (!folder) {
+        return;
+      }
+      let msgDb = folder.msgDatabase;
+      let lastBiffTime = this._folderBiffTime.get(folder) || 0;
+      newMsgKeys = msgDb.getNewList().filter(key => {
+        // It's possible that after we queued the folder, user has opened the
+        // folder and read some new messages. We compare the timestamp of each
+        // NEW message with the last biff time to make sure only real NEW
+        // messages are alerted.
+        let msgHdr = msgDb.GetMsgHdrForKey(key);
+        return msgHdr.dateInSeconds * 1000 > lastBiffTime;
+      });
+
+      this._pendingFolders.delete(folder);
+    }
+    if (newMsgKeys.length == 0) {
+      // No NEW message in the current folder, try the next queued folder.
+      this._showCustomizedAlert();
+      return;
+    }
+
+    this._folderBiffTime.set(folder, Date.now());
+
+    let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+    args.appendElement(folder);
+    args.appendElement({
+      wrappedJSObject: newMsgKeys,
+    });
+    args.appendElement(this);
     Services.ww.openWindow(
       null,
       "chrome://messenger/content/newmailalert.xhtml",
       "_blank",
       "chrome,dialog=yes,titlebar=no,popup=yes",
-      folder.server.rootFolder
+      args
     );
+    this._customizedAlertShown = true;
   }
 
   async _updateUnreadCount() {
     this._logger.debug(
       `Update unreadMailCount=${this._unreadMailCount}, unreadChatCount=${this._unreadChatCount}`
     );
     let count = this._unreadMailCount + this._unreadChatCount;
     let tooltip = "";