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
--- 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 = "";