build()

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");
      }
    }