Bug 942638 - Clean up the multi-message summary code, part 4: Create a single function for making summary rows; r=mconley
This commit is contained in:
Родитель
c7c7f596bf
Коммит
746c37dd73
|
@ -7,12 +7,7 @@
|
|||
|
||||
@import url("chrome://messenger/content/sharedsummary.css");
|
||||
|
||||
|
||||
#headingwrapper {
|
||||
background-color: #729fcf;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* The heading area */
|
||||
|
||||
#headingwrappertable {
|
||||
position: fixed;
|
||||
|
@ -21,17 +16,9 @@
|
|||
right: 0;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-weight: bold;
|
||||
font-size: medium;
|
||||
margin: 0.5em;
|
||||
border: 1px solid transparent;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-left: 19px;
|
||||
padding: 0.3ex 0px;
|
||||
#headingwrapper {
|
||||
background-color: #729fcf;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#heading {
|
||||
|
@ -44,6 +31,59 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-weight: bold;
|
||||
font-size: medium;
|
||||
margin: 0.5em;
|
||||
border: 1px solid transparent;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.heading.info {
|
||||
font-size: medium;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* The main content area */
|
||||
|
||||
#content {
|
||||
position: absolute;
|
||||
top: 4em;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
padding: 1ex 1em;
|
||||
margin: 0px;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
#messagelist {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
#footer {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
/* The message/thread summary rows */
|
||||
|
||||
#messagelist > li {
|
||||
position: relative;
|
||||
margin-bottom: 1ex;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.thread {
|
||||
border-radius: 4px;
|
||||
border: 2px solid lightgrey;
|
||||
}
|
||||
|
||||
.star {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
@ -56,120 +96,34 @@
|
|||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
#content {
|
||||
position: absolute;
|
||||
top: 4em;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
padding: 1ex 1em;
|
||||
margin: 0px;
|
||||
font-size: small;
|
||||
.item_summary {
|
||||
margin-left: 19px;
|
||||
padding: 0.3ex 0px;
|
||||
}
|
||||
|
||||
.row {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 1ex;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.thread {
|
||||
border-radius: 4px;
|
||||
border: 2px solid lightgrey;
|
||||
}
|
||||
|
||||
.subject {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.sender {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.author {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.count {
|
||||
display: inline;
|
||||
white-space: nowrap;
|
||||
color: InactiveCaptionText;
|
||||
}
|
||||
|
||||
.date {
|
||||
float: right;
|
||||
padding: 0px 1em;
|
||||
}
|
||||
|
||||
.wrappedsender, .wrappedsubject {
|
||||
.item_header {
|
||||
padding-right: 18px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.message.unread .subject,
|
||||
.message.unread .sender {
|
||||
.unread .primary_header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.snippet {
|
||||
color: GrayText;
|
||||
padding-right: 1.5em;
|
||||
padding-bottom: 1ex;
|
||||
}
|
||||
|
||||
.senders {
|
||||
color: GrayText;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.heading.info {
|
||||
font-size: medium;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#column {
|
||||
.right {
|
||||
float: right;
|
||||
right: 1em;
|
||||
top: 1em;
|
||||
}
|
||||
|
||||
#header {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.section_heading {
|
||||
font-weight: bold;
|
||||
padding-bottom: .5em;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.stats {
|
||||
padding: 1em;
|
||||
padding-top: 3em;
|
||||
text-align: center;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.messagecount {
|
||||
display: inline-block;
|
||||
.count {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
color: InactiveCaptionText;
|
||||
-moz-margin-start: 1ch;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: inline;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.snippet {
|
||||
color: #555;
|
||||
-moz-margin-start: 1ch;
|
||||
}
|
||||
|
||||
.tag {
|
||||
|
@ -185,7 +139,8 @@
|
|||
color: #111111;
|
||||
}
|
||||
|
||||
.notice {
|
||||
padding-left: 1em;
|
||||
.snippet {
|
||||
color: #555;
|
||||
padding-right: 1.5em;
|
||||
padding-bottom: 1ex;
|
||||
}
|
||||
|
||||
|
|
|
@ -93,12 +93,6 @@ function _mm_FormatDisplayName(aHeaderParser, aHeaderValue, aContext) {
|
|||
}
|
||||
}
|
||||
|
||||
function _mm_escapeHTML(aUnescaped) {
|
||||
return aUnescaped.replace("&", "&", "g")
|
||||
.replace("<", "<", "g")
|
||||
.replace(">", ">", "g");
|
||||
}
|
||||
|
||||
/**
|
||||
* The MultiMessageSummary class is responsible for populating the message pane
|
||||
* with a reasonable summary of a set of messages that span more than one
|
||||
|
@ -119,7 +113,6 @@ function _mm_escapeHTML(aUnescaped) {
|
|||
function MultiMessageSummary(aMessages, aListener) {
|
||||
this._msgHdrs = aMessages;
|
||||
this._listener = aListener;
|
||||
this._msgTagService = MailServices.tags;
|
||||
this._glodaQueries = [];
|
||||
this._msgNodes = {};
|
||||
|
||||
|
@ -128,47 +121,31 @@ function MultiMessageSummary(aMessages, aListener) {
|
|||
}
|
||||
|
||||
MultiMessageSummary.prototype = {
|
||||
/**
|
||||
* The maximum number of messages to summarize.
|
||||
*/
|
||||
kMaxMessages: 100,
|
||||
|
||||
/**
|
||||
* The length of message snippets to fetch from Gloda.
|
||||
*/
|
||||
kSnippetLength: 300,
|
||||
|
||||
/**
|
||||
* Given a msgHdr, return a list of tag objects. This function just does the
|
||||
* messy work of understanding how tags are stored in nsIMsgDBHdrs. It would
|
||||
* be a good candidate for a utility library.
|
||||
*
|
||||
* @param aMsgHdr The msgHdr whose tags we want.
|
||||
* @return A list of tag objects.
|
||||
* @return An array of nsIMsgTag objects.
|
||||
*/
|
||||
getTagsForMsg: function(aMsgHdr) {
|
||||
let keywords = aMsgHdr.getStringProperty("keywords");
|
||||
let keywordList = keywords.split(" ");
|
||||
let keywordMap = {};
|
||||
for (let iKeyword = 0; iKeyword < keywordList.length; iKeyword++) {
|
||||
let keyword = keywordList[iKeyword];
|
||||
keywordMap[keyword] = true;
|
||||
}
|
||||
let keywords = new Set(aMsgHdr.getStringProperty("keywords").split(" "));
|
||||
let allTags = MailServices.tags.getAllTags({});
|
||||
|
||||
let tagArray = this._msgTagService.getAllTags({});
|
||||
let tags = [];
|
||||
for (let iTag = 0; iTag < tagArray.length; iTag++) {
|
||||
let tag = tagArray[iTag];
|
||||
if (tag.key in keywordMap)
|
||||
tags.push(tag);
|
||||
}
|
||||
return tags;
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a name (as one sees in email headers), strip eventual
|
||||
* leading/trailing quotes (both single and double).
|
||||
*
|
||||
* @param senderName
|
||||
* name which might be quoted
|
||||
* @return
|
||||
* name without quotes
|
||||
**/
|
||||
stripQuotes: function(senderName) {
|
||||
if ((senderName.startsWith("'") && senderName.endsWith("'")) ||
|
||||
(senderName.startsWith('"') && senderName.endsWith('"')))
|
||||
senderName = senderName.slice(1, -1);
|
||||
return senderName;
|
||||
return allTags.filter(function(tag) {
|
||||
return keywords.has(tag.key);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -187,22 +164,22 @@ MultiMessageSummary.prototype = {
|
|||
let archiveBtn = document.getElementById("hdrArchiveButton");
|
||||
archiveBtn.collapsed = !global.gFolderDisplay.canArchiveSelectedMessages;
|
||||
|
||||
function viewThreadId(aMsgHdr) {
|
||||
let thread = global.gFolderDisplay.view.dbView
|
||||
.getThreadContainingMsgHdr(aMsgHdr);
|
||||
return thread.threadKey;
|
||||
}
|
||||
|
||||
// First, we group the messages in threads and count the threads.
|
||||
// First, we group the messages in threads and count the threads. We want
|
||||
// the view's version of threading, not the database's version, in order to
|
||||
// thread together cross-folder messages. XXX: This falls apart for group
|
||||
// by sort; what we really want is a way to specify only the cross-folder
|
||||
// view.
|
||||
let threads = {};
|
||||
let numThreads = 0;
|
||||
for (let [,msgHdr] in Iterator(this._msgHdrs))
|
||||
{
|
||||
if (!threads[viewThreadId(msgHdr)]) {
|
||||
threads[viewThreadId(msgHdr)] = [msgHdr];
|
||||
numThreads += 1;
|
||||
for (let [,msgHdr] in Iterator(this._msgHdrs)) {
|
||||
let viewThreadId = global.gFolderDisplay.view.dbView
|
||||
.getThreadContainingMsgHdr(msgHdr)
|
||||
.threadKey;
|
||||
if (!(viewThreadId in threads)) {
|
||||
threads[viewThreadId] = [msgHdr];
|
||||
numThreads++;
|
||||
} else {
|
||||
threads[viewThreadId(msgHdr)].push(msgHdr);
|
||||
threads[viewThreadId].push(msgHdr);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -216,154 +193,185 @@ MultiMessageSummary.prototype = {
|
|||
|
||||
heading.textContent = messagesTitle;
|
||||
|
||||
const MAX_MESSAGES = 100;
|
||||
const SNIPPET_LENGTH = 300;
|
||||
let count = 0;
|
||||
let maxCountExceeded = false;
|
||||
var parser = new DOMParser();
|
||||
|
||||
for (let [thread, msgs] in Iterator(threads)) {
|
||||
count += msgs.length;
|
||||
if (count > MAX_MESSAGES) {
|
||||
if (count > this.kMaxMessages) {
|
||||
maxCountExceeded = true;
|
||||
break;
|
||||
}
|
||||
let countUnread = 0;
|
||||
let countStarred = 0;
|
||||
let header, countNode;
|
||||
|
||||
// We'll mark the thread unread if any messages in it are unread.
|
||||
for (let [,msgHdr] in Iterator(msgs)) {
|
||||
if (!msgHdr.isRead)
|
||||
countUnread++;
|
||||
if (msgHdr.isFlagged)
|
||||
countStarred++;
|
||||
}
|
||||
|
||||
let numMsgs = msgs.length;
|
||||
let subject = msgs[0].mime2DecodedSubject ||
|
||||
gSelectionSummaryStrings["noSubject"];
|
||||
let author = _mm_FormatDisplayName(
|
||||
MailServices.headerParser, msgs[0].mime2DecodedAuthor, "from"
|
||||
);
|
||||
|
||||
let countstring = "";
|
||||
if (numMsgs > 1) {
|
||||
countstring += "(";
|
||||
countstring += PluralForm.get(
|
||||
numMsgs, gSelectionSummaryStrings["numMsgs"]
|
||||
).replace("#1", numMsgs);
|
||||
|
||||
if (countUnread) {
|
||||
countstring += PluralForm.get(
|
||||
countUnread, gSelectionSummaryStrings["countUnread"]
|
||||
).replace("#1", countUnread);
|
||||
}
|
||||
countstring += ")";
|
||||
}
|
||||
|
||||
let msgContents = '<div class="row">' +
|
||||
' <div class="star"/>' +
|
||||
' <div class="header">' +
|
||||
' <div class="wrappedsubject">' +
|
||||
' <div class="author">' +
|
||||
_mm_escapeHTML(author) + '</div>' +
|
||||
' <div class="subject link">' +
|
||||
_mm_escapeHTML(subject) + '</div>' +
|
||||
' <div class="count">' + countstring + '</div>' +
|
||||
' <div class="tags"></div>' +
|
||||
' </div>' +
|
||||
' <div class="snippet"></div>' +
|
||||
' </div>' +
|
||||
'</div>';
|
||||
|
||||
let msgNode = document.createElement("div");
|
||||
// innerHTML is safe here because all of the data in msgContents is
|
||||
// either generated from integers or escaped to be safe.
|
||||
msgNode.innerHTML = msgContents;
|
||||
|
||||
msgNode.classList.add("message");
|
||||
if (numMsgs > 1)
|
||||
msgNode.classList.add("thread");
|
||||
if (countUnread)
|
||||
msgNode.classList.add("unread");
|
||||
if (countStarred)
|
||||
msgNode.classList.add("starred");
|
||||
|
||||
let msgNode = this._makeSummaryItem(msgs, { showSubject: true });
|
||||
messagesElt.appendChild(msgNode);
|
||||
|
||||
let snippetNode = msgNode.querySelector(".snippet");
|
||||
let authorNode = msgNode.querySelector(".author");
|
||||
try {
|
||||
MsgHdrToMimeMessage(msgs[0], null, function(aMsgHdr, aMimeMsg) {
|
||||
if (aMimeMsg == null) /* shouldn't happen, but sometimes does? */
|
||||
return;
|
||||
|
||||
let [text, meta] = mimeMsgToContentSnippetAndMeta(aMimeMsg,
|
||||
aMsgHdr.folder,
|
||||
SNIPPET_LENGTH);
|
||||
snippetNode.textContent = text;
|
||||
if (meta.author)
|
||||
authorNode.textContent = meta.author;
|
||||
}, false, {saneBodySize: true});
|
||||
} catch (e if e.result == Components.results.NS_ERROR_FAILURE) {
|
||||
// Offline messages generate exceptions, which is unfortunate. When
|
||||
// that's fixed, this code should adapt. XXX
|
||||
snippetNode.textContent = "...";
|
||||
for (let msgHdr of msgs) {
|
||||
let key = msgHdr.messageKey + msgHdr.folder.URI;
|
||||
this._msgNodes[key] = msgNode;
|
||||
}
|
||||
|
||||
// get the subject node.
|
||||
let subjectNode = msgNode.querySelector(".subject");
|
||||
subjectNode.msgs = msgs;
|
||||
subjectNode.addEventListener("click", function() {
|
||||
global.gFolderDisplay.selectMessages(this.msgs);
|
||||
}, true);
|
||||
|
||||
let tagsNode = msgNode.querySelector(".tags");
|
||||
while (tagsNode.hasChildNodes())
|
||||
tagsNode.lastChild.remove();
|
||||
this._addTagNodes(msgs, tagsNode);
|
||||
for (let [,msgHdr] in Iterator(msgs)) {
|
||||
this._msgNodes[msgHdr.messageKey + msgHdr.folder.URI] = msgNode;
|
||||
}
|
||||
messagesElt.appendChild(msgNode);
|
||||
}
|
||||
|
||||
// Stash somewhere so it doesn't get GC'ed.
|
||||
this._glodaQueries.push(Gloda.getMessageCollectionForHeaders(
|
||||
this._msgHdrs, this
|
||||
));
|
||||
this.notifyMaxCountExceeded(this._msgHdrs.length, MAX_MESSAGES);
|
||||
this.notifyMaxCountExceeded(this._msgHdrs.length, this.kMaxMessages);
|
||||
|
||||
this.computeSize();
|
||||
adjustHeadingSize();
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear out the tagsnode, and fill in appropriately for the union of tags in
|
||||
* the specified messags.
|
||||
* Create a summary item for a message or thread.
|
||||
*
|
||||
* @param aMessageOrThread An nsIMsgDBHdr or an array thereof
|
||||
* @param [aOptions] An optional object to customize the output:
|
||||
* currently accepts |showSubject| to show the subject
|
||||
* of the message.
|
||||
* @return A DOM node for the summary item.
|
||||
*/
|
||||
_addTagNodes: function(msgs, tagsNode) {
|
||||
// For tags, stars, and read/unread status, we want to map from all
|
||||
// messages to one node.
|
||||
let tags = {};
|
||||
for (let msgHdr of msgs) {
|
||||
for (let tag of this.getTagsForMsg(msgHdr)) {
|
||||
if (!(tag.key in tags)) {
|
||||
tags[tag.key] = tag;
|
||||
}
|
||||
_makeSummaryItem: function(aMessageOrThread, aOptions) {
|
||||
let message, thread, numUnread, isStarred, tags;
|
||||
if (aMessageOrThread instanceof Components.interfaces.nsIMsgDBHdr) {
|
||||
thread = null;
|
||||
message = aMessageOrThread;
|
||||
|
||||
numUnread = message.isRead ? 0 : 1;
|
||||
isStarred = message.isFlagged;
|
||||
|
||||
tags = this.getTagsForMsg(message);
|
||||
}
|
||||
else {
|
||||
thread = aMessageOrThread;
|
||||
message = thread[0];
|
||||
|
||||
numUnread = thread.reduce(function(x, hdr) {
|
||||
return x + (hdr.isRead ? 0 : 1);
|
||||
}, 0);
|
||||
isStarred = thread.some(function(hdr) { return hdr.isFlagged; });
|
||||
|
||||
tags = new Set();
|
||||
for (let message of thread) {
|
||||
for (let tag of this.getTagsForMsg(message))
|
||||
tags.add(tag);
|
||||
}
|
||||
}
|
||||
for (let [,tag] in Iterator(tags)) {
|
||||
let tagNode = tagsNode.ownerDocument.createElement("span");
|
||||
|
||||
let row = document.createElement("li");
|
||||
row.classList.toggle("thread", thread && thread.length > 1);
|
||||
row.classList.toggle("unread", numUnread > 0);
|
||||
row.classList.toggle("starred", isStarred);
|
||||
row.innerHTML = '<div class="star"/>' +
|
||||
'<div class="item_summary">' +
|
||||
'<div class="item_header"/>' +
|
||||
'<div class="snippet"/>' +
|
||||
'</div>';
|
||||
|
||||
let itemHeaderNode = row.querySelector(".item_header");
|
||||
|
||||
let authorNode = document.createElement("span");
|
||||
authorNode.classList.add("author");
|
||||
authorNode.textContent = _mm_FormatDisplayName(
|
||||
MailServices.headerParser, message.mime2DecodedAuthor, "from"
|
||||
);
|
||||
|
||||
if (aOptions && aOptions.showSubject) {
|
||||
authorNode.classList.add("right");
|
||||
itemHeaderNode.appendChild(authorNode);
|
||||
|
||||
let subjectNode = document.createElement("span");
|
||||
subjectNode.classList.add("subject", "primary_header", "link");
|
||||
subjectNode.textContent = message.mime2DecodedSubject ||
|
||||
gSelectionSummaryStrings["noSubject"];
|
||||
subjectNode.addEventListener("click", function() {
|
||||
global.gFolderDisplay.selectMessages(thread);
|
||||
}, false);
|
||||
itemHeaderNode.appendChild(subjectNode);
|
||||
|
||||
if (thread && thread.length > 1) {
|
||||
let numUnreadStr = "";
|
||||
if (numUnread) {
|
||||
numUnreadStr = PluralForm.get(
|
||||
numUnread, gSelectionSummaryStrings["countUnread"]
|
||||
).replace("#1", numUnread);
|
||||
}
|
||||
let countStr = "(" + PluralForm.get(
|
||||
thread.length, gSelectionSummaryStrings["numMsgs"]
|
||||
).replace("#1", thread.length) + numUnreadStr + ")";
|
||||
|
||||
let countNode = document.createElement("span");
|
||||
countNode.classList.add("count");
|
||||
countNode.textContent = countStr;
|
||||
itemHeaderNode.appendChild(countNode);
|
||||
}
|
||||
}
|
||||
else {
|
||||
let dateNode = document.createElement("span");
|
||||
dateNode.classList.add("date", "right");
|
||||
dateNode.textContent = makeFriendlyDateAgo(new Date(message.date / 1000));
|
||||
itemHeaderNode.appendChild(dateNode);
|
||||
|
||||
authorNode.classList.add("primary_header", "link");
|
||||
authorNode.addEventListener("click", function() {
|
||||
global.gFolderDisplay.selectMessage(message);
|
||||
global.document.getElementById("messagepane").focus();
|
||||
}, false);
|
||||
itemHeaderNode.appendChild(authorNode);
|
||||
}
|
||||
|
||||
let tagNode = document.createElement("span");
|
||||
tagNode.classList.add("tags");
|
||||
this._addTagNodes(tags, tagNode);
|
||||
itemHeaderNode.appendChild(tagNode);
|
||||
|
||||
let snippetNode = row.querySelector(".snippet");
|
||||
try {
|
||||
const kSnippetLength = this.kSnippetLength;
|
||||
MsgHdrToMimeMessage(message, null, function(aMsgHdr, aMimeMsg) {
|
||||
if (aMimeMsg == null) /* shouldn't happen, but sometimes does? */ {
|
||||
return;
|
||||
}
|
||||
let [text, meta] = mimeMsgToContentSnippetAndMeta(
|
||||
aMimeMsg, aMsgHdr.folder, kSnippetLength
|
||||
);
|
||||
snippetNode.textContent = text;
|
||||
if (meta.author)
|
||||
authorNode.textContent = meta.author;
|
||||
}, false, {saneBodySize: true});
|
||||
} catch (e if e.result == Components.results.NS_ERROR_FAILURE) {
|
||||
// Offline messages generate exceptions, which is unfortunate. When
|
||||
// that's fixed, this code should adapt. XXX
|
||||
snippetNode.textContent = "...";
|
||||
}
|
||||
|
||||
return row;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a list of tags to a DOM node.
|
||||
*
|
||||
* @param aTags An array (or any iterable) of nsIMsgTag objects.
|
||||
* @param aTagsNode The DOM node to contain the list of tags.
|
||||
*/
|
||||
_addTagNodes: function(aTags, aTagsNode) {
|
||||
// Make sure the tags are sorted in their natural order.
|
||||
let sortedTags = [...aTags];
|
||||
sortedTags.sort(function(a, b) {
|
||||
return a.key.localeCompare(b.key) ||
|
||||
a.ordinal.localeCompare(b.ordinal);
|
||||
});
|
||||
|
||||
for (let tag of sortedTags) {
|
||||
let tagNode = document.createElement("span");
|
||||
// See tagColors.css.
|
||||
let colorClass = "blc-" + this._msgTagService
|
||||
.getColorForKey(tag.key)
|
||||
.substr(1);
|
||||
let color = MailServices.tags.getColorForKey(tag.key);
|
||||
let colorClass = "blc-" + color.substr(1);
|
||||
|
||||
tagNode.classList.add("tag", colorClass);
|
||||
tagNode.dataset.tag = tag.tag;
|
||||
tagNode.textContent = tag.tag;
|
||||
tagsNode.appendChild(tagNode);
|
||||
aTagsNode.appendChild(tagNode);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -418,41 +426,46 @@ MultiMessageSummary.prototype = {
|
|||
* @param aItems Contents of a gloda collection.
|
||||
*/
|
||||
processItems: function(aItems) {
|
||||
let knownMessageNodes = [];
|
||||
let knownMessageNodes = new Map();
|
||||
|
||||
for (let [,glodaMsg] in Iterator(aItems)) {
|
||||
let messageKey = glodaMsg.messageKey;
|
||||
let domkey = messageKey + glodaMsg.folder.uri;
|
||||
|
||||
// Unread and starred will get set if any of the messages in a collapsed
|
||||
// thread qualify. The trick here is that we may get multiple items
|
||||
// corresponding to the same thread (and hence DOM node), so we need to
|
||||
// detect when we get the first item for a particular DOM node, stash the
|
||||
// preexisting status of that DOM node, an only do transitions if the
|
||||
// items warrant it.
|
||||
let headerNode = this._msgNodes[domkey];
|
||||
if (! headerNode.flags) {
|
||||
headerNode.flags = {};
|
||||
knownMessageNodes.push(headerNode);
|
||||
let key = glodaMsg.messageKey + glodaMsg.folder.uri;
|
||||
let headerNode = this._msgNodes[key];
|
||||
if (!knownMessageNodes.has(headerNode)) {
|
||||
knownMessageNodes.set(headerNode, {
|
||||
read: true,
|
||||
starred: false,
|
||||
tags: new Set()
|
||||
});
|
||||
}
|
||||
|
||||
headerNode.flags["unread"] = !glodaMsg.read;
|
||||
headerNode.flags["starred"] = glodaMsg.starred;
|
||||
let flags = knownMessageNodes.get(headerNode);
|
||||
|
||||
// For tags, there's a minor problem in that if _some_ of the items in a
|
||||
// thread got modified.
|
||||
let key = messageKey + glodaMsg.folder.uri;
|
||||
// Count as read if *all* the messages are read.
|
||||
flags.read &= glodaMsg.read;
|
||||
// Count as starred if *any* of the messages are starred.
|
||||
flags.starred |= glodaMsg.starred;
|
||||
// Count as tagged with a tag if *any* of the messages have that tag.
|
||||
for (let tag of this.getTagsForMsg(glodaMsg.folderMessage))
|
||||
flags.tags.add(tag);
|
||||
}
|
||||
|
||||
for (let [headerNode, flags] of knownMessageNodes) {
|
||||
headerNode.classList.toggle("unread", !flags.read);
|
||||
headerNode.classList.toggle("starred", flags.starred);
|
||||
|
||||
// Clear out all the tags and start fresh, just to make sure we don't get
|
||||
// out of sync.
|
||||
let tagsNode = headerNode.querySelector(".tags");
|
||||
while (tagsNode.hasChildNodes())
|
||||
tagsNode.lastChild.remove();
|
||||
this._addTagNodes([msg.folderMessage for ([,msg] in Iterator(aItems))],
|
||||
tagsNode);
|
||||
}
|
||||
|
||||
for (let headerNode of knownMessageNodes) {
|
||||
headerNode.classList.toggle("unread", headerNode.flags["unread"]);
|
||||
headerNode.classList.toggle("starred", headerNode.flags["starred"]);
|
||||
headerNode.flags = null;
|
||||
this._addTagNodes(flags.tags, tagsNode);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -486,7 +499,6 @@ MultiMessageSummary.prototype = {
|
|||
function ThreadSummary(aMessages, aListener) {
|
||||
this._msgHdrs = aMessages;
|
||||
this._listener = aListener;
|
||||
this._msgTagService = MailServices.tags;
|
||||
this._glodaQueries = [];
|
||||
this._msgNodes = {};
|
||||
|
||||
|
@ -518,8 +530,6 @@ ThreadSummary.prototype = {
|
|||
|
||||
let count = 0;
|
||||
let ignoredCount = 0;
|
||||
const MAX_THREADS = 100;
|
||||
const SNIPPET_LENGTH = 300;
|
||||
let maxCountExceeded = false;
|
||||
for (let i = 0; i < numMsgs; ++i) {
|
||||
let msgHdr = this._msgHdrs[i];
|
||||
|
@ -530,94 +540,16 @@ ThreadSummary.prototype = {
|
|||
}
|
||||
|
||||
count++;
|
||||
if (count > MAX_THREADS) {
|
||||
if (count > this.kMaxMessages) {
|
||||
maxCountExceeded = true;
|
||||
break;
|
||||
}
|
||||
|
||||
let senderName = _mm_FormatDisplayName(
|
||||
MailServices.headerParser, msgHdr.mime2DecodedAuthor, "from"
|
||||
);
|
||||
let date = makeFriendlyDateAgo(new Date(msgHdr.date/1000));
|
||||
|
||||
let msgContents = '<div class="row">' +
|
||||
' <div class="star"/>' +
|
||||
' <div class="header">' +
|
||||
' <div class="wrappedsender">' +
|
||||
' <div class="sender link">' +
|
||||
_mm_escapeHTML(senderName) + '</div>' +
|
||||
' <div class="date">' + date + '</div>' +
|
||||
' <div class="tags"></div>' +
|
||||
' </div>' +
|
||||
' <div class="snippet"></div>' +
|
||||
' </div>' +
|
||||
'</div>';
|
||||
|
||||
let msgNode = document.createElement("div");
|
||||
// innerHTML is safe here because all of the data in msgContents is
|
||||
// either generated from integers or escaped to be safe.
|
||||
msgNode.innerHTML = msgContents;
|
||||
|
||||
msgNode.classList.add("message");
|
||||
if (!msgHdr.isRead)
|
||||
msgNode.classList.add("unread");
|
||||
if (msgHdr.isFlagged)
|
||||
msgNode.classList.add("starred");
|
||||
|
||||
let msgNode = this._makeSummaryItem(msgHdr);
|
||||
messagesElt.appendChild(msgNode);
|
||||
|
||||
let key = msgHdr.messageKey + msgHdr.folder.URI;
|
||||
let snippetNode = msgNode.querySelector(".snippet");
|
||||
let senderNode = msgNode.querySelector(".sender");
|
||||
try {
|
||||
MsgHdrToMimeMessage(msgHdr, null, function(aMsgHdr, aMimeMsg) {
|
||||
if (aMimeMsg == null) /* shouldn't happen, but sometimes does? */ {
|
||||
return;
|
||||
}
|
||||
let [text, meta] = mimeMsgToContentSnippetAndMeta(aMimeMsg,
|
||||
aMsgHdr.folder,
|
||||
SNIPPET_LENGTH);
|
||||
snippetNode.textContent = text;
|
||||
if (meta.author)
|
||||
senderNode.textContent = meta.author;
|
||||
}, false, {saneBodySize: true});
|
||||
} catch (e if e.result == Components.results.NS_ERROR_FAILURE) {
|
||||
// Offline messages generate exceptions, which is unfortunate. When
|
||||
// that's fixed, this code should adapt. XXX
|
||||
snippetNode.textContent = "...";
|
||||
}
|
||||
|
||||
let tagsNode = msgNode.querySelector(".tags");
|
||||
for (let tag of this.getTagsForMsg(msgHdr)) {
|
||||
let tagNode = tagsNode.ownerDocument.createElement("span");
|
||||
// See tagColors.css.
|
||||
let colorClass = "blc-" + this._msgTagService
|
||||
.getColorForKey(tag.key)
|
||||
.substr(1);
|
||||
tagNode.classList.add("tag", colorClass);
|
||||
tagNode.dataset.tag = tag.tag;
|
||||
tagNode.textContent = tag.tag;
|
||||
tagsNode.appendChild(tagNode);
|
||||
}
|
||||
|
||||
let sender = msgNode.querySelector(".sender");
|
||||
sender.msgHdr = msgHdr;
|
||||
sender.addEventListener("click", function(e) {
|
||||
// If the msg is the first message in a collapsed thread, we need to
|
||||
// uncollapse it.
|
||||
let dbView = global.gFolderDisplay.view.dbView;
|
||||
let origRowCount = dbView.rowCount;
|
||||
let viewIndex = dbView.findIndexOfMsgHdr(e.target.msgHdr, true);
|
||||
dbView.selectFolderMsgByKey(this.folder, this.msgKey);
|
||||
if (dbView.rowCount != origRowCount)
|
||||
dbView.selectionChanged();
|
||||
}, true);
|
||||
sender.folder = msgHdr.folder;
|
||||
sender.msgKey = msgHdr.messageKey;
|
||||
|
||||
this._msgNodes[key] = msgNode;
|
||||
|
||||
messagesElt.appendChild(msgNode);
|
||||
}
|
||||
|
||||
let countInfo = PluralForm.get(
|
||||
|
@ -640,7 +572,7 @@ ThreadSummary.prototype = {
|
|||
this._glodaQueries.push(Gloda.getMessageCollectionForHeaders(
|
||||
this._msgHdrs, this
|
||||
));
|
||||
this.notifyMaxCountExceeded(numMsgs, MAX_THREADS);
|
||||
this.notifyMaxCountExceeded(numMsgs, this.kMaxMessages);
|
||||
|
||||
this.computeSize();
|
||||
adjustHeadingSize();
|
||||
|
|
|
@ -58,8 +58,8 @@
|
|||
</div>
|
||||
</div>
|
||||
<div id="content">
|
||||
<div id="messagelist"/>
|
||||
<div class="notice">
|
||||
<ul id="messagelist"/>
|
||||
<div id="footer">
|
||||
<span class="info" id="size"/> <span class="info" id="notice"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -10,7 +10,10 @@
|
|||
* showing messages.
|
||||
*/
|
||||
function summarizeSelection(aMessageDisplay) {
|
||||
// Figure out if we're looking at one thread or more than one thread.
|
||||
// Figure out if we're looking at one thread or more than one thread. We want
|
||||
// the view's version of threading, not the database's version, in order to
|
||||
// thread together cross-folder messages. XXX: This falls apart for group by
|
||||
// sort; what we really want is a way to specify only the cross-folder view.
|
||||
let folderDisplay = aMessageDisplay.folderDisplay;
|
||||
let selectedIndices = folderDisplay.selectedIndices;
|
||||
let dbView = folderDisplay.view.dbView;
|
||||
|
|
|
@ -177,17 +177,17 @@ function test_summarization_thread_detection() {
|
|||
toggle_thread_row(0);
|
||||
assert_messages_summarized(mc, messages);
|
||||
// count the number of messages represented
|
||||
assert_summary_contains_N_divs('wrappedsender', 10);
|
||||
assert_summary_contains_N_elts('#messagelist > li', 10);
|
||||
select_shift_click_row(1);
|
||||
// this should have shifted to the multi-message view
|
||||
assert_summary_contains_N_divs('wrappedsender', 0);
|
||||
assert_summary_contains_N_divs('wrappedsubject', 2);
|
||||
assert_summary_contains_N_elts('.item_header > .date', 0);
|
||||
assert_summary_contains_N_elts('.item_header > .subject', 2);
|
||||
select_none();
|
||||
assert_nothing_selected();
|
||||
select_click_row(1); // select a single message
|
||||
select_shift_click_row(2); // add a thread
|
||||
assert_summary_contains_N_divs('wrappedsender', 0);
|
||||
assert_summary_contains_N_divs('wrappedsubject', 2);
|
||||
assert_summary_contains_N_elts('.item_header > .date', 0);
|
||||
assert_summary_contains_N_elts('.item_header > .subject', 2);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -291,10 +291,9 @@ function test_summary_when_multiple_identities() {
|
|||
// Assertions
|
||||
select_click_row(0);
|
||||
assert_messages_summarized(mc, mc.folderDisplay.selectedMessages);
|
||||
// Thread summary uses class wrappedsender, while multimessage summary uses
|
||||
// class author.
|
||||
assert_summary_contains_N_divs('author', 0);
|
||||
assert_summary_contains_N_divs('wrappedsender', 2);
|
||||
// Thread summary shows a date, while multimessage summary shows a subject.
|
||||
assert_summary_contains_N_elts('.item_header > .subject', 0);
|
||||
assert_summary_contains_N_elts('.item_header > .date', 2);
|
||||
|
||||
// Second half of the test, makes sure MultiMessageSummary groups messages
|
||||
// according to their view thread id
|
||||
|
@ -303,7 +302,7 @@ function test_summary_when_multiple_identities() {
|
|||
be_in_folder(folderVirtual);
|
||||
select_shift_click_row(1);
|
||||
|
||||
assert_summary_contains_N_divs('author', 2);
|
||||
assert_summary_contains_N_elts('.item_header > .subject', 2);
|
||||
}
|
||||
|
||||
function extract_first_address(thread)
|
||||
|
@ -320,7 +319,7 @@ function extract_first_address(thread)
|
|||
|
||||
function check_address_name(name) {
|
||||
let htmlframe = mc.e('multimessage');
|
||||
let match = htmlframe.contentDocument.querySelector('.sender');
|
||||
let match = htmlframe.contentDocument.querySelector('.author');
|
||||
if (match.textContent != name)
|
||||
throw new Error("Expected to find sender named '" + name + "', found '" +
|
||||
match.textContent + "'");
|
||||
|
|
|
@ -2724,18 +2724,21 @@ function reset_close_message_on_delete() {
|
|||
|
||||
/**
|
||||
* assert that the multimessage/thread summary view contains
|
||||
* the specified number of elements of the specified class.
|
||||
* the specified number of elements of the specified selector.
|
||||
*
|
||||
* @param aClassName: the class to use to select
|
||||
* @param aSelector: the CSS selector to use to select
|
||||
* @param aNumElts: the number of expected elements that have that class
|
||||
*/
|
||||
|
||||
function assert_summary_contains_N_divs(aClassName, aNumElts) {
|
||||
function assert_summary_contains_N_elts(aSelector, aNumElts) {
|
||||
let htmlframe = mc.e('multimessage');
|
||||
let matches = htmlframe.contentDocument.getElementsByClassName(aClassName);
|
||||
if (matches.length != aNumElts)
|
||||
throw new Error("Expected to find " + aNumElts + " elements with class " +
|
||||
aClassName + ", found: " + matches.length);
|
||||
let matches = htmlframe.contentDocument.querySelectorAll(aSelector);
|
||||
if (matches.length != aNumElts) {
|
||||
throw new Error(
|
||||
"Expected to find " + aNumElts + " elements with selector '" +
|
||||
aSelector + "', found: " + matches.length
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче