in mail/base/content/widgets/glodaFacet.js [1548:1825]
build() {
const message = this.message;
const subject = this.subject;
// -- eventify
subject.onclick = event => {
FacetContext.showConversationInTab(this, event.button == 1);
};
subject.onkeypress = event => {
if (Event.keyCode == event.DOM_VK_RETURN) {
FacetContext.showConversationInTab(this, event.shiftKey);
}
};
// -- Content Poking
if (message.subject.trim() == "") {
subject.textContent = glodaFacetStrings.GetStringFromName(
"glodaFacetView.result.message.noSubject"
);
} else {
subject.textContent = message.subject;
}
const authorNode = this.author;
authorNode.setAttribute("title", message.from.value);
authorNode.textContent = message.from.contact.name;
const toNode = this.to;
toNode.textContent = glodaFacetStrings.GetStringFromName(
"glodaFacetView.result.message.toLabel"
);
// this.author.textContent = ;
const { makeFriendlyDateAgo } = ChromeUtils.importESModule(
"resource:///modules/TemplateUtils.sys.mjs"
);
this.date.textContent = makeFriendlyDateAgo(message.date);
// - Recipients
try {
const recipientsNode = this.recipients;
if (message.recipients) {
let recipientCount = 0;
const MAX_RECIPIENTS = 3;
const totalRecipientCount = message.recipients.length;
const recipientSeparator = glodaFacetStrings.GetStringFromName(
"glodaFacetView.results.message.recipientSeparator"
);
for (const index in message.recipients) {
const recipNode = document.createElement("span");
recipNode.setAttribute("class", "message-recipient");
recipNode.textContent = message.recipients[index].contact.name;
recipientsNode.appendChild(recipNode);
recipientCount++;
if (recipientCount == MAX_RECIPIENTS) {
break;
}
if (index != totalRecipientCount - 1) {
// add separators (usually commas)
const sepNode = document.createElement("span");
sepNode.setAttribute("class", "message-recipient-separator");
sepNode.textContent = recipientSeparator;
recipientsNode.appendChild(sepNode);
}
}
if (totalRecipientCount > MAX_RECIPIENTS) {
const nOthers = totalRecipientCount - recipientCount;
const andNOthers = document.createElement("span");
andNOthers.setAttribute("class", "message-recipients-andothers");
const andOthersLabel = PluralForm.get(
nOthers,
glodaFacetStrings.GetStringFromName(
"glodaFacetView.results.message.andOthers"
)
).replace("#1", nOthers);
andNOthers.textContent = andOthersLabel;
recipientsNode.appendChild(andNOthers);
}
}
} catch (e) {
console.error(e);
}
// - Starred
const starNode = this.star;
if (message.starred) {
starNode.setAttribute("starred", "true");
}
// - Attachments
if (message.attachmentNames) {
const attachmentsNode = this.attachments;
const imgNode = document.createElement("div");
imgNode.setAttribute("class", "message-attachment-icon");
attachmentsNode.appendChild(imgNode);
for (let attach of message.attachmentNames) {
const attachNode = document.createElement("div");
attachNode.setAttribute("class", "message-attachment");
if (attach.length >= 28) {
attach = attach.substring(0, 24) + "…";
}
attachNode.textContent = attach;
attachmentsNode.appendChild(attachNode);
}
}
// - Tags
const tagsNode = this.tags;
if ("tags" in message && message.tags.length) {
for (const tag of message.tags) {
const tagNode = document.createElement("span");
tagNode.setAttribute("class", "message-tag");
const color = MailServices.tags.getColorForKey(tag.key);
if (color) {
const textColor = !TagUtils.isColorContrastEnough(color)
? "white"
: "black";
tagNode.setAttribute(
"style",
"color: " + textColor + "; background-color: " + color + ";"
);
}
tagNode.textContent = tag.tag;
tagsNode.appendChild(tagNode);
}
}
// - Body
if (message.indexedBodyText) {
let bodyText = message.indexedBodyText;
const matches = [];
if ("stashedColumns" in FacetContext.collection) {
let collection;
if (
"IMCollection" in FacetContext &&
message instanceof Gloda.lookupNounDef("im-conversation").clazz
) {
collection = FacetContext.IMCollection;
} else {
collection = FacetContext.collection;
}
const offsets = collection.stashedColumns[message.id][0];
const offsetNums = offsets.split(" ").map(x => parseInt(x));
for (let i = 0; i < offsetNums.length; i += 4) {
// i is the column index. The indexedBodyText is in the column 0.
// Ignore matches for other columns.
if (offsetNums[i] != 0) {
continue;
}
// i+1 is the term index, indicating which queried term was found.
// We can ignore for now...
// i+2 is the *byte* offset at which the term is in the string.
// i+3 is the term's length.
matches.push([offsetNums[i + 2], offsetNums[i + 3]]);
}
// Sort the matches by index, just to be sure.
// They are probably already sorted, but if they aren't it could
// mess things up at the next step.
matches.sort((a, b) => a[0] - b[0]);
// Convert the byte offsets and lengths into character indexes.
const charCodeToByteCount = c => {
// UTF-8 stores:
// - code points below U+0080 on 1 byte,
// - code points below U+0800 on 2 bytes,
// - code points U+D800 through U+DFFF are UTF-16 surrogate halves
// (they indicate that JS has split a 4 bytes UTF-8 character
// in two halves of 2 bytes each),
// - other code points on 3 bytes.
if (c < 0x80) {
return 1;
}
if (c < 0x800 || (c >= 0xd800 && c <= 0xdfff)) {
return 2;
}
return 3;
};
let byteOffset = 0;
let offset = 0;
for (const match of matches) {
while (byteOffset < match[0]) {
byteOffset += charCodeToByteCount(bodyText.charCodeAt(offset++));
}
match[0] = offset;
for (let i = offset; i < offset + match[1]; ++i) {
const size = charCodeToByteCount(bodyText.charCodeAt(i));
if (size > 1) {
match[1] -= size - 1;
}
}
}
}
// how many lines of context we want before the first match:
const kContextLines = 2;
let startIndex = 0;
if (matches.length > 0) {
// Find where the snippet should begin to show at least the
// first match and kContextLines of context before the match.
startIndex = matches[0][0];
for (let context = kContextLines; context >= 0; --context) {
startIndex = bodyText.lastIndexOf("\n", startIndex - 1);
if (startIndex == -1) {
startIndex = 0;
break;
}
}
}
// start assuming it's just one line that we want to show
let idxNewline = -1;
let ellipses = "…";
let maxLineCount = 5;
if (startIndex != 0) {
// Avoid displaying an ellipses followed by an empty line.
while (bodyText[startIndex + 1] == "\n") {
++startIndex;
}
bodyText = ellipses + bodyText.substring(startIndex);
// The first line will only contain the ellipsis as the character
// at startIndex is always \n, so we show an additional line.
++maxLineCount;
}
for (
let newlineCount = 0;
newlineCount < maxLineCount;
newlineCount++
) {
idxNewline = bodyText.indexOf("\n", idxNewline + 1);
if (idxNewline == -1) {
ellipses = "";
break;
}
}
let snippet = "";
if (idxNewline > -1) {
snippet = bodyText.substring(0, idxNewline);
} else {
snippet = bodyText;
}
if (ellipses) {
snippet = snippet.trimRight() + ellipses;
}
const parent = this.snippet;
let node = document.createTextNode(snippet);
parent.appendChild(node);
let offset = startIndex ? startIndex - 1 : 0; // The ellipsis takes 1 character.
for (const match of matches) {
if (idxNewline > -1 && match[0] > startIndex + idxNewline) {
break;
}
const secondNode = node.splitText(match[0] - offset);
node = secondNode.splitText(match[1]);
offset += match[0] + match[1] - offset;
const span = document.createElement("span");
span.textContent = secondNode.data;
if (!this.firstMatchText) {
this.firstMatchText = secondNode.data;
}
span.setAttribute("class", "message-body-fulltext-match");
parent.replaceChild(span, secondNode);
}
}
// - Misc attributes
if (!message.read) {
this.setAttribute("unread", "true");
}
}