changeset 26:3d0ff57dc129

portal view
author Myk Melez <myk@mozilla.org>
date Wed, 16 Apr 2008 22:53:14 -0700
parents d211a3986f8e
children c1e19ca9f364
files extension/content/portal-view.css extension/content/portal-view.html extension/content/portal-view.js
diffstat 3 files changed, 489 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extension/content/portal-view.css	Wed Apr 16 22:53:14 2008 -0700
@@ -0,0 +1,36 @@
+.column {
+  float: left;
+}
+
+.source {
+  border: 1px solid black;
+  max-width: 400px;
+  margin: 1em;
+}
+
+h2 {
+  border-bottom: 1px solid black;
+  font-size: medium;
+  background-color: black;
+  color: white;
+  margin: 0px;
+}
+
+.messages {
+  margin: 0.4em;
+}
+
+a {
+  text-decoration: none;
+  color: black;
+  font-weight: bold;
+}
+
+a:visited {
+  font-weight: normal;
+}
+
+body {
+  background-color: white;
+  color: black;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extension/content/portal-view.html	Wed Apr 16 22:53:14 2008 -0700
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+    <title>Your Messages</title>
+    <script type="application/javascript;version=1.8" src="chrome://snowl/content/portal-view.js"></script>
+    <link rel="stylesheet" href="chrome://snowl/content/portal-view.css" type="text/css">
+</head>
+<body>
+    <div id="view"></div>
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extension/content/portal-view.js	Wed Apr 16 22:53:14 2008 -0700
@@ -0,0 +1,442 @@
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://snowl/service.js");
+Cu.import("resource://snowl/datastore.js");
+Cu.import("resource://snowl/log4moz.js");
+
+let SnowlView = {
+  _log: null,
+
+  // Observer Service
+  get _obsSvc() {
+    let obsSvc = Cc["@mozilla.org/observer-service;1"].
+                 getService(Ci.nsIObserverService);
+    delete this._obsSvc;
+    this._obsSvc = obsSvc;
+    return this._obsSvc;
+  },
+
+  // Date Formatting Service
+  get _dfSvc() {
+    let dfSvc = Cc["@mozilla.org/intl/scriptabledateformat;1"].
+                            getService(Ci.nsIScriptableDateFormat);
+    delete this._dfSvc;
+    this._dfSvc = dfSvc;
+    return this._dfSvc;
+  },
+
+  _rebuildView: function() {
+    this._sourceViews = [];
+    let sources = this._getSources();
+    for (let i = 0; i < sources.length; i++)
+      this._sourceViews.push(this._buildSourceView(sources[i]));
+
+    this._reflowColumns();
+  },
+
+  _sourceViews: null,
+
+  _reflowColumns: function() {
+    let view = document.getElementById("view");
+
+    while (view.hasChildNodes())
+      view.removeChild(view.lastChild);
+
+    // window.innerWidth skews high for HTML in chrome because of bug 429439,
+    // so we have to divide it by the constant of proportionality between the
+    // skewed value and the actual value to get the actual value.
+    let viewportWidth = Math.round(window.innerWidth / 1.2333);
+
+    // We give each column about 400px of space (including margins), enough for
+    // two columns on 800x600 and 1024x768 screens; three columns on 1200x800,
+    // 1400x1050, and 1440x900 screens; and four columns on 1600x1200 screens.
+    let numColumns = Math.ceil(viewportWidth / 400);
+
+    let columns = [];
+    for (let i = 0; i < numColumns; i++) {
+      let column = document.createElement("div");
+      column.setAttribute("class", "column");
+      columns.push(column);
+      view.appendChild(column);
+    }
+
+    // We divide the source views evenly into the columns.
+    for (let i = 0; i < this._sourceViews.length; i++) {
+      let columnIndex = Math.ceil((i+1)/this._sourceViews.length * numColumns) - 1;
+      let column = columns[columnIndex];
+      column.appendChild(this._sourceViews[i]);
+    }
+  },
+
+  _buildSourceView: function(aSource) {
+    let view = document.createElement("div");
+    view.setAttribute("class", "source");
+
+    let header = document.createElement("h2");
+    header.appendChild(document.createTextNode(aSource.title));
+    view.appendChild(header);
+
+    let messageContainer = document.createElement("div");
+    messageContainer.setAttribute("class", "messages");
+    view.appendChild(messageContainer);
+
+    let messages = this._getMessages(aSource.id);
+    for each (let message in messages)
+      messageContainer.appendChild(this._buildMessageView(message));
+
+    return view;
+  },
+
+  _buildMessageView: function(aMessage) {
+    let view = document.createElement("div");
+    view.setAttribute("class", "message");
+
+    let link = document.createElement("a");
+    link.setAttribute("href", aMessage.link);
+    link.appendChild(document.createTextNode(aMessage.subject));
+    view.appendChild(link);
+
+    return view;
+  },
+
+  _getSources: function() {
+    let sources = [];
+
+    let statement = SnowlDatastore.createStatement("SELECT id, title FROM sources");
+    try {
+      while (statement.step())
+        sources.push({ id: statement.row.id, title: statement.row.title });
+    }
+    finally {
+      statement.reset();
+    }
+
+    return sources;
+  },
+
+  _getMessages: function(aSourceID, aMatchWords) {
+    let conditions = [];
+
+    conditions.push("sourceID = :sourceID");
+    conditions.push("messages.current = 1");
+
+    if (aMatchWords)
+      conditions.push("messages.id IN (SELECT messageID FROM parts WHERE content MATCH :matchWords)");
+
+    let statementString = 
+      //"SELECT sources.title AS sourceTitle, subject, author, link, timestamp, content \
+      // FROM sources JOIN messages ON sources.id = messages.sourceID \
+      // LEFT JOIN parts on messages.id = parts.messageID";
+      "SELECT sources.title AS sourceTitle, messages.id AS id, " +
+             "subject, author, link, timestamp, read " +
+      "FROM sources JOIN messages ON sources.id = messages.sourceID " +
+      "";
+
+    if (conditions.length > 0)
+      statementString += " WHERE " + conditions.join(" AND ");
+
+    statementString += " ORDER BY timestamp DESC LIMIT 15";
+
+    //this._log.info("getMessages: statementString = " + statementString);
+
+    let statement = SnowlDatastore.createStatement(statementString);
+
+    if (aMatchWords) {
+      this._log.info("getMessages: aMatchWords = " + aMatchWords);
+      statement.params.matchWords = aMatchWords;
+    }
+
+    statement.params.sourceID = aSourceID;
+
+    let messages = [];
+    try {
+      while (statement.step()) {
+        messages.push({ id: statement.row.id,
+                        sourceTitle: statement.row.sourceTitle,
+                        subject: statement.row.subject,
+                        author: statement.row.author,
+                        link: statement.row.link,
+                        timestamp: statement.row.timestamp,
+                        read: (statement.row.read ? true : false)
+                        //,content: statement.row.content
+                      });
+      }
+    }
+    catch(ex) {
+      this._log.error(statementString + ": " + ex + ": " + SnowlDatastore.dbConnection.lastErrorString + "\n");
+      throw ex;
+    }
+    finally {
+      statement.reset();
+    }
+
+    return messages;
+  },
+
+  init: function() {
+    this._log = Log4Moz.Service.getLogger("Snowl.View");
+
+try {
+    this._obsSvc.addObserver(this, "messages:changed", true);
+}
+catch(ex) {
+  alert(ex);
+}
+    this._rebuildView();
+  },
+  
+  // nsISupports
+  QueryInterface: function(aIID) {
+    if (aIID.equals(Ci.nsIObserver) ||
+        aIID.equals(Ci.nsISupportsWeakReference) ||
+        aIID.equals(Ci.nsISupports))
+      return this;
+    
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+  // nsIObserver
+  observe: function(subject, topic, data) {
+    switch (topic) {
+      case "messages:changed":
+        this._rebuildView();
+        break;
+    }
+  },
+
+  onUpdate: function() {
+    this._rebuildView();
+  },
+
+  onFilter: function() {
+    let filterTextbox = document.getElementById("snowlFilterTextbox");
+    this._rebuildView(filterTextbox.value);
+  },
+
+  // From toolkit/mozapps/update/content/history.js
+
+  /**
+   * Formats a timestamp for human consumption using the date formatting service
+   * for locale-specific formatting along with some additional smarts for more
+   * human-readable representations of recent timestamps.
+   * @param   {Date} the timestamp to format
+   * @returns a human-readable string
+   */
+  _formatTimestamp: function(aTimestamp) {
+    let formattedString;
+
+    let now = new Date();
+
+    let yesterday = new Date(now - 24 * 60 * 60 * 1000);
+    yesterday = new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate());
+
+    let sixDaysAgo = new Date(now - 6 * 24 * 60 * 60 * 1000);
+    sixDaysAgo = new Date(sixDaysAgo.getFullYear(), sixDaysAgo.getMonth(), sixDaysAgo.getDate());
+
+    if (aTimestamp.toLocaleDateString() == now.toLocaleDateString())
+      formattedString = this._dfSvc.FormatTime("",
+                                               this._dfSvc.timeFormatNoSeconds,
+                                               aTimestamp.getHours(),
+                                               aTimestamp.getMinutes(),
+                                               null);
+    else if (aTimestamp > yesterday)
+      formattedString = "Yesterday " + this._dfSvc.FormatTime("",
+                                                              this._dfSvc.timeFormatNoSeconds,
+                                                              aTimestamp.getHours(),
+                                                              aTimestamp.getMinutes(),
+                                                              null);
+    else if (aTimestamp > sixDaysAgo)
+      formattedString = this._dfSvc.FormatDateTime("",
+                                                   this._dfSvc.dateFormatWeekday, 
+                                                   this._dfSvc.timeFormatNoSeconds,
+                                                   aTimestamp.getFullYear(),
+                                                   aTimestamp.getMonth() + 1,
+                                                   aTimestamp.getDate(),
+                                                   aTimestamp.getHours(),
+                                                   aTimestamp.getMinutes(),
+                                                   aTimestamp.getSeconds());
+    else
+      formattedString = this._dfSvc.FormatDateTime("",
+                                                   this._dfSvc.dateFormatShort, 
+                                                   this._dfSvc.timeFormatNoSeconds,
+                                                   aTimestamp.getFullYear(),
+                                                   aTimestamp.getMonth() + 1,
+                                                   aTimestamp.getDate(),
+                                                   aTimestamp.getHours(),
+                                                   aTimestamp.getMinutes(),
+                                                   aTimestamp.getSeconds());
+
+    return formattedString;
+  },
+
+  switchPosition: function() {
+    let container = document.getElementById("snowlViewContainer");
+    let splitter = document.getElementById("snowlViewSplitter");
+    let browser = document.getElementById("browser");
+    let content = document.getElementById("content");
+    let appcontent = document.getElementById("appcontent");
+
+    if (container.parentNode == appcontent) {
+      browser.insertBefore(container, appcontent);
+      browser.insertBefore(splitter, appcontent);
+      splitter.setAttribute("orient", "horizontal");
+    }
+    else {
+      appcontent.insertBefore(container, content);
+      appcontent.insertBefore(splitter, content);
+      splitter.setAttribute("orient", "vertical");
+    }
+  },
+
+  onSelect: function(aEvent) {
+    let tree = document.getElementById("snowlView");
+    if (tree.currentIndex == -1)
+      return;
+
+    // When we support opening multiple links in the background,
+    // perhaps use this code: http://lxr.mozilla.org/mozilla/source/browser/base/content/browser.js#1482
+
+    let children = tree.getElementsByTagName("treechildren")[0];
+    let row = children.childNodes[tree.currentIndex];
+    window.loadURI(row.link, null, null, false);
+
+    this._setRead(row);
+  },
+
+  onKeyPress: function(aEvent) {
+    if (aEvent.altKey || aEvent.metaKey || aEvent.ctrlKey)
+      return;
+
+    // which is either the charCode or the keyCode, depending on which is set.
+    this._log.info("onKeyPress: which = " + aEvent.which);
+
+    if (aEvent.charCode == "r".charCodeAt(0))
+      this._toggleRead();
+    else if (aEvent.charCode == " ".charCodeAt(0))
+      this._onSpacePress(aEvent);
+  },
+
+  // Based on SpaceHit in mailWindowOverlay.js
+  _onSpacePress: function(aEvent) {
+    if (aEvent.shiftKey) {
+      // if at the start of the message, go to the previous one
+      if (gBrowser.contentWindow.scrollY > 0)
+        gBrowser.contentWindow.scrollByPages(-1);
+      else
+        this._goToPreviousUnreadMessage();
+    }
+    else {
+      // if at the end of the message, go to the next one
+      if (gBrowser.contentWindow.scrollY < gBrowser.contentWindow.scrollMaxY)
+        gBrowser.contentWindow.scrollByPages(1);
+      else
+        this._goToNextUnreadMessage();
+    }
+  },
+
+  _goToPreviousUnreadMessage: function() {
+    let tree = document.getElementById("snowlView");
+
+    let children = tree.getElementsByTagName("treechildren")[0];
+
+    let i = tree.currentIndex - 1;
+    while (i != tree.currentIndex) {
+      if (i < 0) {
+        i = tree.view.rowCount - 1;
+        continue;
+      }
+
+      let row = children.childNodes[i];
+      if (row.getElementsByTagName("treecell")[0].hasAttribute("properties")) {
+        tree.view.selection.select(i);
+        tree.treeBoxObject.ensureRowIsVisible(i);
+        break;
+      }
+
+      i--;
+    }
+  },
+
+  _goToNextUnreadMessage: function() {
+    let tree = document.getElementById("snowlView");
+
+    let children = tree.getElementsByTagName("treechildren")[0];
+
+    let i = tree.currentIndex + 1;
+    while (i != tree.currentIndex) {
+      if (i >= tree.view.rowCount) {
+        i = 0;
+        continue;
+      }
+
+      let row = children.childNodes[i];
+      if (row.getElementsByTagName("treecell")[0].hasAttribute("properties")) {
+        tree.view.selection.select(i);
+        tree.treeBoxObject.ensureRowIsVisible(i);
+        break;
+      }
+
+      i++;
+    }
+  },
+
+  _toggleRead: function() {
+this._log.info("_toggleRead");
+    let tree = document.getElementById("snowlView");
+    if (tree.currentIndex == -1)
+      return;
+
+    let children = tree.getElementsByTagName("treechildren")[0];
+    let row = children.childNodes[tree.currentIndex];
+
+    let cell = row.getElementsByTagName("treecell")[0];
+    if (cell.hasAttribute("properties"))
+      this._setRead(row);
+    else
+      this._setUnread(row);
+  },
+
+  _setRead: function(aRow) {
+this._log.info("_setRead");
+    SnowlDatastore.dbConnection.executeSimpleSQL("UPDATE messages SET read = 1 WHERE id = " + aRow.id);
+    // Remove the "unread" property from the treecell.
+    let cells = aRow.getElementsByTagName("treecell");
+    for (let i = 0; i < cells.length; i++)
+      cells[i].removeAttribute("properties");
+  },
+
+  _setUnread: function(aRow) {
+    SnowlDatastore.dbConnection.executeSimpleSQL("UPDATE messages SET read = 0 WHERE id = " + aRow.id);
+    // Remove the "unread" property from the treecell.
+    let cells = aRow.getElementsByTagName("treecell");
+    for (let i = 0; i < cells.length; i++)
+      cells[i].setAttribute("properties", "unread");
+  },
+
+  setSource: function(aSourceID) {
+    this.sourceID = aSourceID;
+    this._rebuildView();
+  },
+
+  toggle: function() {
+    let container = document.getElementById("snowlViewContainer");
+    let splitter = document.getElementById("snowlViewSplitter");
+    if (container.hidden) {
+      container.hidden = false;
+      splitter.hidden = false;
+    }
+    else {
+      container.hidden = true;
+      splitter.hidden = true;
+    }
+  }
+};
+
+window.addEventListener("load", function() { SnowlView.init() }, false);
+
+window.onresize = function() {
+  SnowlView._reflowColumns();
+}