mail/base/content/glodaFacetBindings.xml
author Jorg K <jorgk@jorgk.com>
Tue, 22 Jan 2019 09:53:30 +0100
changeset 34269 f00434eba47a01e79c2ad037578b089a3b017572
parent 34262 a0a854d99ee07836775ef135aab693f53ce3ccc2
child 34301 0c2445d753759cf438e37127b79de0dfabb5f5c0
permissions -rw-r--r--
Bug 1521671 - Port bug 1514340: Implement nsIWebProgressListener.onContentBlockingEvent(), JS part. rs=bustage-fix

<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
  - License, v. 2.0. If a copy of the MPL was not distributed with this
  - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->

<!-- import-globals-from glodaFacetView.js -->

<bindings id="glodaFacetBindings"
          xmlns="http://www.mozilla.org/xbl"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
          xmlns:html="http://www.w3.org/1999/xhtml"
          xmlns:xbl="http://www.mozilla.org/xbl"
          xmlns:svg="http://www.w3.org/2000/svg">

<!-- ===== Results ===== -->

<binding id="results-message">
  <content>
    <html:div class="results-message-header">
      <html:h2 class="results-message-count" anonid="count"></html:h2>
      <html:div class="results-message-showall">
        <html:span class="results-message-showall-button"
            tabindex="0" anonid="showall" role="button"
            onclick="FacetContext.showActiveSetInTab()"></html:span>
      </html:div>
      <html:div anonid="sort" class="results-message-sort-bar">
        <html:span anonid="sort-label" class="results-message-sort-label"/>
        <html:span anonid="sort-relevance" class="results-message-sort-value"
            tabindex="0" role="button"/>
        <html:span anonid="sort-date" class="results-message-sort-value"
            tabindex="0" role="button"/>
      </html:div>
    </html:div>
    <html:div class="messages" anonid="messages">
    </html:div>
  </content>
  <implementation>
    <method name="setMessages">
      <parameter name="aMessages" />
      <body><![CDATA[
        // -- Count
        let countNode = document.getAnonymousElementByAttribute(
                          this, "anonid", "count");
        let topMessagesPluralFormat = glodaFacetStrings.get(
          "glodaFacetView.results.header.countLabel.NMessages");
        let outOfPluralFormat = glodaFacetStrings.get(
          "glodaFacetView.results.header.countLabel.ofN");
        let groupingFormat = glodaFacetStrings.get(
          "glodaFacetView.results.header.countLabel.grouping");

        let displayCount = aMessages.length;
        let totalCount = FacetContext.activeSet.length;

        // set the count so CSS selectors can know what the results look like
        this.setAttribute("state", (totalCount <= 0) ? "empty" : "some");

        let topMessagesStr = PluralForm.get(displayCount,
                                            topMessagesPluralFormat)
                                       .replace("#1",
                                                displayCount.toLocaleString());
        let outOfStr = PluralForm.get(totalCount,
                                      outOfPluralFormat)
                                 .replace("#1", totalCount.toLocaleString());
        countNode.textContent = groupingFormat.replace("#1", topMessagesStr)
                                              .replace("#2", outOfStr);

        // -- Show All
        let showNode = document.getAnonymousElementByAttribute(
                          this, "anonid", "showall");
        const GlodaMessage = Gloda.lookupNounDef("message").clazz;
        let visible = aMessages.some(m => m instanceof GlodaMessage);
        showNode.style.display = visible ? "inline" : "none";
        showNode.textContent = glodaFacetStrings.get(
          "glodaFacetView.results.message.openEmailAsList.label");
        showNode.setAttribute("title", glodaFacetStrings.get(
          "glodaFacetView.results.message.openEmailAsList.tooltip"));
        showNode.onkeypress = function(event) {
          if (event.charCode == KeyEvent.DOM_VK_SPACE) {
            FacetContext.showActiveSetInTab();
            event.preventDefault();
          }
        };

        let sortLabelNode = document.getAnonymousElementByAttribute(
                          this, "anonid", "sort-label");
        sortLabelNode.textContent = glodaFacetStrings.get(
          "glodaFacetView.results.message.sort.label");

        let sortRelevanceNode = document.getAnonymousElementByAttribute(
                          this, "anonid", "sort-relevance");
        sortRelevanceNode.textContent = glodaFacetStrings.get(
          "glodaFacetView.results.message.sort.relevance");

        let dis = this;
        sortRelevanceNode.onclick = function() {
          FacetContext.sortBy = "-dascore";
          dis.updateSortLabels();
        };
        sortRelevanceNode.onkeypress = function(event) {
          if (event.charCode == KeyEvent.DOM_VK_SPACE) {
            FacetContext.sortBy = "-dascore";
            dis.updateSortLabels();
            event.preventDefault();
          }
        };

        let sortDateNode = document.getAnonymousElementByAttribute(
                          this, "anonid", "sort-date");
        sortDateNode.textContent = glodaFacetStrings.get(
          "glodaFacetView.results.message.sort.date");
        sortDateNode.onclick = function() {
          FacetContext.sortBy = "-date";
          dis.updateSortLabels();
        };
        sortDateNode.onkeypress = function(event) {
          if (event.charCode == KeyEvent.DOM_VK_SPACE) {
            FacetContext.sortBy = "-date";
            dis.updateSortLabels();
            event.preventDefault();
          }
        };

        this.updateSortLabels(FacetContext.sortBy);

        let messagesNode = document.getAnonymousElementByAttribute(
                             this, "anonid", "messages");
        while (messagesNode.hasChildNodes())
          messagesNode.lastChild.remove();
      try {
        // -- Messages
        for (let message of aMessages) {
          let msgNode = document.createElement("message");
          msgNode.message = message;
          msgNode.setAttribute("class", "message");
          messagesNode.appendChild(msgNode);
        }
      } catch (e) {
        logException(e);
      }
      ]]></body>
    </method>
    <method name="updateSortLabels">
      <body><![CDATA[
      try {
        let sortBy = FacetContext.sortBy;
        let sortRelevanceNode = document.getAnonymousElementByAttribute(
                          this, "anonid", "sort-relevance");
        let sortDateNode = document.getAnonymousElementByAttribute(
                          this, "anonid", "sort-date");

        if (sortBy == "-dascore") {
          sortRelevanceNode.setAttribute("selected", "true");
          sortDateNode.removeAttribute("selected");
        } else if (sortBy == "-date") {
          sortRelevanceNode.removeAttribute("selected");
          sortDateNode.setAttribute("selected", "true");
        }
      } catch (e) {
        logException(e);
      }
      ]]></body>
    </method>
  </implementation>
</binding>

<binding id="result-message">
  <content>
    <html:div class="message-header">
      <html:div class="message-line">
        <html:div class="message-meta">
          <html:div anonid="addresses-group" class="message-addresses-group">
            <html:div anonid="author-group" class="message-author-group">
              <html:span anonid="author" class="message-author"></html:span>
              <html:div anonid="date" class="message-date"></html:div>
            </html:div>
          </html:div>
        </html:div>
        <html:div class="message-subject-group">
          <html:span anonid="star" class="message-star"></html:span>
          <html:span anonid="subject" class="message-subject" tabindex="0"
              role="link"></html:span>
          <html:span anonid="tags" class="message-tags"></html:span>
          <html:div anonid="recipients-group" class="message-recipients-group">
            <html:span anonid="to" class="message-to-label"></html:span>
            <html:div anonid="recipients" class="message-recipients"/>
          </html:div>
        </html:div>
      </html:div>
    </html:div>
    <html:pre anonid="snippet" class="message-body"></html:pre>
      <html:div anonid="attachments" class="message-attachments"></html:div>
  </content>
  <implementation>
    <constructor><![CDATA[
      ChromeUtils.import("resource:///modules/MailServices.jsm");
      this.build();
    ]]></constructor>
    <method name="build">
      <body><![CDATA[
        let message = this.message;
        let dis = this;
        function anonElem(aAnonId) {
          return document.getAnonymousElementByAttribute(dis, "anonid",
                                                         aAnonId);
        }
        let subject = anonElem("subject");
        // -- eventify
        subject.onclick = function(aEvent) {
          FacetContext.showConversationInTab(this,
                                             aEvent.button == 1);
        }.bind(this);
        subject.onkeypress = function(aEvent) {
          if (aEvent.keyCode == aEvent.DOM_VK_RETURN)
            FacetContext.showConversationInTab(this,
                                               aEvent.shiftKey);
        }.bind(this);

        // -- Content Poking
        if (message.subject.trim() == "")
          subject.textContent = glodaFacetStrings.get("glodaFacetView.result.message.noSubject");
        else
          subject.textContent = message.subject;
        let authorNode = anonElem("author");
        authorNode.setAttribute("title", message.from.value);
        authorNode.textContent = message.from.contact.name;
        let toNode = anonElem("to");
        toNode.textContent = glodaFacetStrings.get("glodaFacetView.result.message.toLabel");

        // anonElem("author").textContent = ;
        let {
          makeFriendlyDateAgo,
        } = ChromeUtils.import("resource:///modules/templateUtils.js", null);
        anonElem("date").textContent = makeFriendlyDateAgo(message.date);

        // - Recipients
      try {
        let recipientsNode = anonElem("recipients");
        if (message.recipients) {
          let recipientCount = 0;
          const MAX_RECIPIENTS = 3;
          let totalRecipientCount = message.recipients.length;
          let recipientSeparator = glodaFacetStrings.get(
            "glodaFacetView.results.message.recipientSeparator");
          for (let index in message.recipients) {
            let 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)
              let sepNode = document.createElement("span");
              sepNode.setAttribute("class", "message-recipient-separator");
              sepNode.textContent = recipientSeparator;
              recipientsNode.appendChild(sepNode);
            }
          }
          if (totalRecipientCount > MAX_RECIPIENTS) {
            let nOthers = totalRecipientCount - recipientCount;
            let andNOthers = document.createElement("span");
            andNOthers.setAttribute("class", "message-recipients-andothers");

            let andOthersLabel = PluralForm.get(
              nOthers, glodaFacetStrings.get("glodaFacetView.results.message.andOthers")
            ).replace("#1", nOthers);

            andNOthers.textContent = andOthersLabel;
            recipientsNode.appendChild(andNOthers);
          }
        }
      } catch (e) {
        logException(e);
      }

        // - Starred
        let starNode = anonElem("star");
        if (message.starred) {
          starNode.setAttribute("starred", "true");
        }

        // - Attachments
        if (message.attachmentNames) {
          let attachmentsNode = anonElem("attachments");
          let imgNode = document.createElement("div");
          imgNode.setAttribute("class", "message-attachment-icon");
          attachmentsNode.appendChild(imgNode);
          for (let attach of message.attachmentNames) {
            let 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
        let tagsNode = anonElem("tags");
        if ("tags" in message && message.tags.length) {
          for (let tag of message.tags) {
            let tagNode = document.createElement("span");
            let colorClass = "blc-" + MailServices.tags.getColorForKey(tag.key).substr(1);
            tagNode.setAttribute("class", "message-tag tag " + colorClass);
            tagNode.textContent = tag.tag;
            tagsNode.appendChild(tagNode);
          }
        }

        // - Body
        if (message.indexedBodyText) {
          let bodyText = message.indexedBodyText;

          let 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;
            let offsets = collection.stashedColumns[message.id][0];
            let 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.
            let charCodeToByteCount = function(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 (let match of matches) {
              while (byteOffset < match[0])
                byteOffset += charCodeToByteCount(bodyText.charCodeAt(offset++));
              match[0] = offset;
              for (let i = offset; i < offset + match[1]; ++i) {
                let 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;

          let parent = anonElem("snippet");
          let node = document.createTextNode(snippet);
          parent.appendChild(node);

          let offset = startIndex ? startIndex - 1 : 0; // The ellipsis takes 1 character.
          for (let match of matches) {
            if (idxNewline > -1 && match[0] > startIndex + idxNewline)
              break;
            let secondNode = node.splitText(match[0] - offset);
            node = secondNode.splitText(match[1]);
            offset += match[0] + match[1] - offset;
            let 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");
      ]]></body>
    </method>
  </implementation>
  <handlers>
    <handler event="mouseover"><![CDATA[
      FacetContext.hoverFacet(FacetContext.fakeResultFaceter,
                              FacetContext.fakeResultAttr,
                              this.message, [this.message]);
    ]]></handler>
    <handler event="mouseout"><![CDATA[
      FacetContext.unhoverFacet(FacetContext.fakeResultFaceter,
                                FacetContext.fakeResultAttr,
                                this.message, [this.message]);
    ]]></handler>
  </handlers>
</binding>

</bindings>