view content/stream.js @ 263:848e068587c5

factor out date formatting into shared util function
author Myk Melez <myk@mozilla.org>
date Mon, 25 Aug 2008 18:20:52 -0700
parents 3c6bbb2dc4e2
children 196c26539e2c
line wrap: on
line source

/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Snowl.
 *
 * The Initial Developer of the Original Code is Mozilla.
 * Portions created by the Initial Developer are Copyright (C) 2008
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Myk Melez <myk@mozilla.org>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;

Cu.import("resource://snowl/modules/log4moz.js");
Cu.import("resource://snowl/modules/URI.js");

Cu.import("resource://snowl/modules/datastore.js");
Cu.import("resource://snowl/modules/collection.js");
Cu.import("resource://snowl/modules/utils.js");

const XML_NS = "http://www.w3.org/XML/1998/namespace"
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const HTML_NS = "http://www.w3.org/1999/xhtml";

let gBrowserWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).
                     getInterface(Ci.nsIWebNavigation).
                     QueryInterface(Ci.nsIDocShellTreeItem).
                     rootTreeItem.
                     QueryInterface(Ci.nsIInterfaceRequestor).
                     getInterface(Ci.nsIDOMWindow);

let gMessageViewWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).
                         getInterface(Ci.nsIWebNavigation).
                         QueryInterface(Ci.nsIDocShellTreeItem).
                         rootTreeItem.
                         QueryInterface(Ci.nsIInterfaceRequestor).
                         getInterface(Ci.nsIDOMWindow);

let SnowlMessageView = {
  get _log() {
    delete this._log;
    return this._log = Log4Moz.Service.getLogger("Snowl.Stream");
  },

  // Favicon Service
  get _faviconSvc() {
    let faviconSvc = Cc["@mozilla.org/browser/favicon-service;1"].
                     getService(Ci.nsIFaviconService);
    delete this._faviconSvc;
    this._faviconSvc = faviconSvc;
    return this._faviconSvc;
  },

  _window: null,
  _document: null,

  // The set of messages to display in the view.
  _collection: null,

  // the width (narrower dimension) of the vertical and horizontal scrollbars;
  // useful for calculating the viewable size of the viewport, since window.
  // innerWidth and innerHeight include the area taken up by the scrollbars
  // XXX Is this value correct, and does it vary by platform?
  scrollbarWidth: 15,

  get stylesheet() {
    for (let i = 0; i < document.styleSheets.length; i++)
      if (document.styleSheets[i].href == "chrome://snowl/content/stream.css")
        return document.styleSheets[i];
    return null;
  },

  _updateRule: function(position, newValue) {
    this.stylesheet.deleteRule(position);
    this.stylesheet.insertRule(newValue, position);
  },


  // This regex is designed to match URLs in plain text.  It correctly
  // excludes puncuation at the end of the URL, so in "See http://example.com."
  // it matches "http://example.com", not "http://example.com.".
  // Based on http://www.perl.com/doc/FMTEYEWTK/regexps.html
  get _linkifyRegex() {
    let protocols = "(?:" + ["http", "https", "ftp"].join("|") + ")";
    let ltrs = '\\w';
    let gunk = '/#~:.?+=&%@!\\-';
    let punc = '.:?\\-';
    let any  = ltrs + gunk + punc;

    let regex = new RegExp(
      "\\b(" + protocols + ":[" + any + "]+?)(?=[" + punc + "]*[^" + any + "]|$)",
      "gi"
    );

    delete this._linkifyRegex;
    return this._linkifyRegex = regex;
  },


  //**************************************************************************//
  // Initialization & Destruction

  onLoad: function() {
    this.onResize();

    // Explicitly wrap |window| in an XPCNativeWrapper to make sure
    // it's a real native object! This will throw an exception if we
    // get a non-native object.
    this._window = new XPCNativeWrapper(window);
    this._document = this._window.document;

    this._collection = new SnowlCollection();
    this._collection.sortProperties = ["received"];
    this._collection.sortOrder = -1;
    this._collection.sort();
    this.rebuildView();
  },


  //**************************************************************************//
  // Event Handlers

  /**
   * Resize the content in the middle column based on the width of the viewport.
   * FIXME: file a bug on the problem that necessitates this hack.
   */
  onResize: function() {
    // We anticipate that there will be a scrollbar, so we include it
    // in the calculation.  Perhaps we should instead wait to include
    // the scrollbar until the content actually overflows.
    // XXX Why do we have to subtract *double* the width of the scrollbar???
    let width = window.innerWidth - (this.scrollbarWidth * 2) - 24 - 16;
    this._updateRule(0, ".body > * { width: " + width + "px }");
  },


  //**************************************************************************//
  // Safe DOM Manipulation

  /**
   * Use this sandbox to run any DOM manipulation code on nodes
   * which are already inserted into the content document.
   */
  get _contentSandbox() {
    delete this._contentSandbox;
    return this._contentSandbox = new Cu.Sandbox(this._window);
  },

  // FIXME: use this when setting story title and byline.
  _setContentText: function FW__setContentText(id, text) {
    this._contentSandbox.element = this._document.getElementById(id);
    this._contentSandbox.textNode = this._document.createTextNode(text);
    let codeStr =
      "while (element.hasChildNodes()) " +
      "  element.removeChild(element.firstChild);" +
      "element.appendChild(textNode);";
    Cu.evalInSandbox(codeStr, this._contentSandbox);
    this._contentSandbox.element = null;
    this._contentSandbox.textNode = null;
  },

  // FIXME: use this when linkifying the story title and source.
  /**
   * Safely sets the href attribute on an anchor tag, providing the URI 
   * specified can be loaded according to rules.
   *
   * XXX Renamed from safeSetURIAttribute to unsafeSetURIAttribute to reflect
   * that we've commented out the stuff that makes it safe.
   *
   * FIXME: I don't understand the security implications here, but presumably
   * there's a reason this is here, and we should be respecting it, so make this
   * work by giving each message in a collection have a reference to its source
   * and then use the source's URI to create the principal with which we compare
   * the URI.
   * 
   * @param   element
   *          The element to set a URI attribute on
   * @param   attribute
   *          The attribute of the element to set the URI to, e.g. href or src
   * @param   uri
   *          The URI spec to set as the href
   */
  _unsafeSetURIAttribute: 
  function FW__unsafeSetURIAttribute(element, attribute, uri) {
/*
    let secman = Cc["@mozilla.org/scriptsecuritymanager;1"].
                 getService(Ci.nsIScriptSecurityManager);    
    const flags = Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL;
    try {
      secman.checkLoadURIStrWithPrincipal(this._feedPrincipal, uri, flags);
      // checkLoadURIStrWithPrincipal will throw if the link URI should not be
      // loaded, either because our feedURI isn't allowed to load it or per
      // the rules specified in |flags|, so we'll never "linkify" the link...
    }
    catch (e) {
      // Not allowed to load this link because secman.checkLoadURIStr threw
      return;
    }
*/

    this._contentSandbox.element = element;
    this._contentSandbox.uri = uri;
    let codeStr = "element.setAttribute('" + attribute + "', uri);";
    Cu.evalInSandbox(codeStr, this._contentSandbox);
  },


  //**************************************************************************//
  // Content Generation

  /**
   * A JavaScript Strands Future with which we pause the writing of messages
   * so as not to hork the UI thread.
   */
  _rebuildViewFuture: null,

  /**
   * Sleep the specified number of milliseconds before continuing at the point
   * in the caller where this function was called.  For the most part, this is
   * a generic sleep routine like the one provided by JavaScript Strands,
   * but we store the Future this function creates in the _rebuildViewFuture
   * property so we can interrupt it when writeMessages gets called again
   * while it is currently writing messages.
   */
  _sleepRebuildView: strand(function(millis) {
    this._rebuildViewFuture = new Future();
    setTimeout(this._rebuildViewFuture.fulfill, millis);
    yield this._rebuildViewFuture.result();
  }),

  rebuildView: strand(function() {
    let begin = new Date();

    let contentBox = this._document.getElementById("contentBox");
    while (contentBox.hasChildNodes())
      contentBox.removeChild(contentBox.lastChild);

    // Interrupt a strand currently writing messages so we don't both try
    // to write messages at the same time.
    // FIXME: figure out how to suppress the exception this throws to the error
    // console, since this interruption is expected and normal behavior.
    if (this._rebuildViewFuture)
      this._rebuildViewFuture.interrupt();

    this._contentSandbox.messages =
      this._document.getElementById("contentBox");

    for (let i = 0; i < this._collection.messages.length; ++i) {
      let message = this._collection.messages[i];

      let messageBox = this._document.createElementNS(XUL_NS, "hbox");
      messageBox.className = "message";
      messageBox.setAttribute("index", i);

      // left column
      let leftColumn = this._document.createElementNS(XUL_NS, "vbox");
      leftColumn.className = "leftColumn";
      let icon = document.createElementNS(XUL_NS, "image");
      icon.className = "icon";
      if (message.authorIcon) {
        icon.setAttribute("src", message.authorIcon);
      }
      else {
        let sourceFaviconURI = message.source.humanURI || URI.get("urn:use-default-icon");
        icon.setAttribute("src", this._faviconSvc.getFaviconImageForPage(sourceFaviconURI).spec);
      }
      leftColumn.appendChild(icon);
      messageBox.appendChild(leftColumn);

      // center column
      let centerColumn = this._document.createElementNS(XUL_NS, "vbox");
      centerColumn.className = "centerColumn";
      messageBox.appendChild(centerColumn);

      // Author or Source
      if (message.author || message.source) {
        let desc = this._document.createElementNS(XUL_NS, "description");
        desc.className = "author";
        desc.setAttribute("crop", "end");
        desc.setAttribute("value", message.author || message.source.name);
        centerColumn.appendChild(desc);
      }

      // Timestamp
      let lastUpdated = SnowlUtils._formatDate(new Date(message.timestamp));
      if (lastUpdated) {
        let timestamp = this._document.createElementNS(XUL_NS, "description");
        timestamp.className = "timestamp";
        timestamp.setAttribute("crop", "end");
        timestamp.setAttribute("value", lastUpdated);
        centerColumn.appendChild(timestamp);
      }

      // content (title or short message)
      let body = this._document.createElementNS(XUL_NS, "description");
      body.className = "body";
      let div = this._document.createElementNS(HTML_NS, "div");
      let a = this._document.createElementNS(HTML_NS, "a");
      // FIXME: make this localizable.
      let subject = message.subject || "empty message";

      if (message.link) {
        let a = this._document.createElementNS(HTML_NS, "a");
        this._unsafeSetURIAttribute(a, "href", message.link);
        body.className += " text-link";
        a.appendChild(this._document.createTextNode(subject));
        div.appendChild(a);
      }
      else {
        // Split the text into its plaintext and URL parts, which alternate
        // in the array of results, with the first part always being plaintext.
        let parts = subject.split(this._linkifyRegex);
        for (let i = 0; i < parts.length; i++) {
          if (i % 2 == 0)
            div.appendChild(this._document.createTextNode(parts[i]));
          else {
            let a = this._document.createElementNS(HTML_NS, "a");
            this._unsafeSetURIAttribute(a, "href", parts[i]);
            a.appendChild(this._document.createTextNode(parts[i]));
            div.appendChild(a);
          }
        }
      }

      body.appendChild(div);
      centerColumn.appendChild(body);

      // Source
      //let source = this._document.createElementNS(HTML_NS, "a");
      //source.className = "source";
      //let sourceIcon = document.createElementNS(HTML_NS, "img");
      //let sourceFaviconURI = message.source.humanURI || URI.get("urn:use-default-icon");
      //sourceIcon.src = this._faviconSvc.getFaviconImageForPage(sourceFaviconURI).spec;
      //source.appendChild(sourceIcon);
      //source.appendChild(this._document.createTextNode(message.source.name));
      //if (message.source.humanURI)
      //  this._unsafeSetURIAttribute(source, "href", message.source.humanURI.spec);
      //centerColumn.appendChild(source);

      // right column
      let rightColumn = this._document.createElementNS(XUL_NS, "vbox");
      rightColumn.className = "rightColumn";
      messageBox.appendChild(rightColumn);

      this._contentSandbox.messageBox = messageBox;

      let codeStr = "messages.appendChild(messageBox)";
      Cu.evalInSandbox(codeStr, this._contentSandbox);

      // Sleep after every tenth message so we don't hork the UI thread and users
      // can immediately start reading messages while we finish writing them.
      if (!(i % 10))
        yield this._sleepRebuildView(0);
    }

    this._contentSandbox.messages = null;
    this._contentSandbox.messageBox = null;

    this._log.info("time spent building view: " + (new Date() - begin) + "ms\n");

    //let serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
    //                 createInstance(Ci.nsIDOMSerializer);
    //this._log.info(serializer.serializeToString(document.getElementById("contentBox")));
  })

};