changeset 5:23b470a52ff1

a million changes: the first chrome view, logging, bug fixes
author Myk Melez <myk@mozilla.org>
date Mon, 18 Feb 2008 21:03:25 -0800
parents ff671aec51c5
children c93fa04dabcb
files extension/content/feedclient.js extension/content/snowl.js extension/content/snowl.xul extension/content/view.js extension/modules/datastore.js extension/modules/service.js website/riverOfNewsView.js website/riverOfNewsView.xul
diffstat 8 files changed, 484 insertions(+), 70 deletions(-) [+]
line wrap: on
line diff
--- a/extension/content/feedclient.js	Tue Feb 05 01:26:06 2008 -0800
+++ b/extension/content/feedclient.js	Mon Feb 18 21:03:25 2008 -0800
@@ -1,3 +1,5 @@
+// FIXME: rename this file feed-source.js
+
 var SnowlFeedClient = {
   // XXX Make this take a feed ID once it stores the list of subscribed feeds
   // in the datastore.
@@ -31,6 +33,9 @@
   }
 };
 
+// XXX: rename this SnowlFeedSource?
+// XXX: make this inherit from a SnowlMessageSource interface?
+
 function SnowlFeed(aID, aURL, aTitle) {
   this.id = aID;
   this.url = aURL;
@@ -70,6 +75,10 @@
   // nsIFeedResultListener
 
   handleResult: function(result) {
+    // Now that we know we successfully downloaded the feed and obtained
+    // a result from it, update the "last refreshed" timestamp.
+    Snowl.resetLastRefreshed(this);
+
     let feed = result.doc.QueryInterface(Components.interfaces.nsIFeed);
 
     for (let i = 0; i < feed.items.length; i++) {
@@ -150,9 +159,10 @@
     // Convert the content type specified by nsIFeedTextConstruct, which is
     // either "html", "xhtml", or "text", into an Internet media type.
     let contentType = aEntry.content ? this.contentTypes[aEntry.content.type] : null;
+    let contentText = aEntry.content ? aEntry.content.text : null;
     let messageID = Snowl.addSimpleMessage(this.id, aUniversalID, aEntry.title.text,
                                            author, timestamp, aEntry.link,
-                                           aEntry.content.text, contentType);
+                                           contentText, contentType);
 
     // Add metadata.
     let fields = aEntry.QueryInterface(Ci.nsIFeedContainer).
--- a/extension/content/snowl.js	Tue Feb 05 01:26:06 2008 -0800
+++ b/extension/content/snowl.js	Mon Feb 18 21:03:25 2008 -0800
@@ -1,31 +1,20 @@
-if (typeof Cc == "undefined") Cc = Components.classes;
-if (typeof Ci == "undefined") Ci = Components.interfaces;
-if (typeof Cr == "undefined") Cr = Components.results;
-if (typeof Cu == "undefined") Cu = Components.utils;
+var Snowl = {
+  log: null,
+
+  init: function() {
+    this._service = new SnowlService();
 
-var Snowl = {
-  init: function() {
     this._initModules();
+
+    this.log = Log4Moz.Service.getLogger("Snowl.Controller");
+    this.log.warn("foo");
+
     //SnowlFeedClient.refresh("http://www.melez.com/mykzilla/atom.xml");
+    
+    SnowlView.onLoad();
   },
 
   _initModules: function() {
-    let ioSvc = Cc["@mozilla.org/network/io-service;1"].
-                getService(Ci.nsIIOService);
-    
-    let resProt = ioSvc.getProtocolHandler("resource").
-                  QueryInterface(Ci.nsIResProtocolHandler);
-    
-    if (!resProt.hasSubstitution("snowl")) {
-      let extMgr = Cc["@mozilla.org/extensions/manager;1"].
-                   getService(Ci.nsIExtensionManager);
-      let loc = extMgr.getInstallLocation("snowl@mozilla.org");
-      let extD = loc.getItemLocation("snowl@mozilla.org");
-      extD.append("modules");
-      resProt.setSubstitution("snowl", ioSvc.newFileURI(extD));
-    }
-    
-    Cu.import("resource://snowl/datastore.js");
   },
 
   getNewMessages: function() {
@@ -45,6 +34,19 @@
     return sources;
   },
 
+  toggleView: 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;
+    }
+  },
+
   /**
    * Determine whether or not the datastore contains the message with the given ID.
    *
@@ -101,6 +103,21 @@
     let attributeID = SnowlDatastore.selectAttributeID(aAttributeName)
                       || SnowlDatastore.insertAttribute(aAttributeName);
     SnowlDatastore.insertMetadatum(aMessageID, attributeID, aValue);
+  },
+
+  /**
+   * Reset the last refreshed time for the given source to the current time.
+   *
+   * XXX should this be setLastRefreshed and take a time parameter
+   * to set the last refreshed time to?
+   *
+   * aSource {SnowlMessageSource} the source for which to set the time
+   */
+  resetLastRefreshed: function(aSource) {
+    let stmt = SnowlDatastore.createStatement("UPDATE sources SET lastRefreshed = :lastRefreshed WHERE id = :id");
+    stmt.params.lastRefreshed = new Date().getTime();
+    stmt.params.id = aSource.id;
+    stmt.execute();
   }
 };
 
--- a/extension/content/snowl.xul	Tue Feb 05 01:26:06 2008 -0800
+++ b/extension/content/snowl.xul	Mon Feb 18 21:03:25 2008 -0800
@@ -1,10 +1,45 @@
 <?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+
 <overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
          id="snowlOverlay">
 
-<!--  <script type="application/x-javascript" src="datastore.js"/> -->
+  <!-- <script type="application/x-javascript" src="datastore.js"/> -->
+  <script type="application/x-javascript" src="init.js"/>
   <script type="application/x-javascript" src="feedclient.js"/>
+  <script type="application/x-javascript" src="view.js"/>
   <script type="application/x-javascript" src="snowl.js"/>
 
-  <button oncommand="Snowl.getNewMessages()" label="Get New Messages"/>
+  <menupopup id="menu_viewPopup">
+    <menuitem insertafter="viewSidebarMenuMenu" label="Messages" type="checkbox"
+              checked="true" oncommand="Snowl.toggleView()" accesskey="M"/>
+  </menupopup>
+
+  <vbox id="appcontent">
+    <vbox id="snowlViewContainer" insertbefore="content">
+      <toolbar id="snowlViewToolbar" pack="end">
+        <button oncommand="Snowl.getNewMessages()" label="G"/>
+        <button oncommand="SnowlView.switchPosition()" label="S"/>
+        <textbox id="snowlFilterTextbox" type="timed" timeout="200" oncommand="SnowlView.onFilter()"/>
+      </toolbar>
+      <listbox id="snowlView" flex="1">
+        <listcols>
+          <listcol flex="1"/>
+          <splitter class="tree-splitter"/>
+          <listcol flex="3"/>
+          <splitter class="tree-splitter"/>
+          <listcol flex="1"/>
+        </listcols>
+        <listhead>
+          <listheader label="Who"/>
+          <listheader label="What"/>
+          <listheader label="When"/>
+        </listhead>
+      </listbox>
+    </vbox>
+    <splitter id="snowlViewSplitter" insertbefore="content"/>
+  </vbox>
+
 </overlay>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extension/content/view.js	Mon Feb 18 21:03:25 2008 -0800
@@ -0,0 +1,112 @@
+let SnowlView = {
+  _getMessages: function(aMatchWords) {
+    let conditions = [];
+
+    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 \
+       JOIN parts on messages.id = parts.messageID";
+
+    if (conditions.length > 0)
+      statementString += " WHERE " + conditions.join(" AND ");
+
+    statementString += " ORDER BY timestamp DESC";
+
+    let statement = SnowlDatastore.createStatement(statementString);
+
+    if (aMatchWords)
+      statement.params.matchWords = aMatchWords;
+
+    let messages = [];
+    try {
+      while (statement.step()) {
+        let row = statement.row;
+        messages.push({ sourceTitle: row.sourceTitle,
+                        subject: row.subject,
+                        author: row.author,
+                        link: row.link,
+                        timestamp: new Date(row.timestamp).toLocaleString(),
+                        content: row.content });
+      }
+    }
+    catch(ex) {
+      dump(statementString + ": " + ex + ": " + SnowlDatastore.dbConnection.lastErrorString + "\n");
+      throw ex;
+    }
+    finally {
+      statement.reset();
+    }
+
+    return messages;
+  },
+
+  onLoad: function() {
+    this._rebuildView();
+  },
+  
+  onUpdate: function() {
+    this._rebuildView();
+  },
+
+  onFilter: function() {
+    let filterTextbox = document.getElementById("filterTextbox");
+    this._rebuildView(filterTextbox.value);
+  },
+
+  _rebuildView: function(aMatchWords) {
+    let rootNode = document.getElementById("snowlView");
+
+    // Empty the view.
+    let oldItems = rootNode.getElementsByTagName("listitem");
+    for (let i = 0; i < oldItems.length; i++)
+      oldItems[i].parentNode.removeChild(oldItems[i]);
+
+    // Get the list of messages.
+    let messages = this._getMessages(aMatchWords);
+
+    for each (let message in messages) {
+      let item = document.createElement("listitem");
+      item.setAttribute("flex", "1");
+
+      let authorCell = document.createElement("listcell");
+      authorCell.setAttribute("label", message.author);
+      authorCell.setAttribute("crop", "center");
+
+      let subjectCell = document.createElement("listcell");
+      subjectCell.setAttribute("label", message.subject);
+      subjectCell.setAttribute("crop", "center");
+      
+      let timestampCell = document.createElement("listcell");
+      timestampCell.setAttribute("label", message.timestamp);
+      timestampCell.setAttribute("crop", "center");
+
+      item.appendChild(authorCell);
+      item.appendChild(subjectCell);
+      item.appendChild(timestampCell);
+      rootNode.appendChild(item);
+    }
+  },
+
+  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");
+    }
+  }
+
+};
--- a/extension/modules/datastore.js	Tue Feb 05 01:26:06 2008 -0800
+++ b/extension/modules/datastore.js	Mon Feb 18 21:03:25 2008 -0800
@@ -5,6 +5,9 @@
 
 EXPORTED_SYMBOLS = ["SnowlDatastore"];
 
+const TABLE_TYPE_NORMAL = 0;
+const TABLE_TYPE_FULLTEXT = 1;
+
 let SnowlDatastore = {
   get _storage() {
     var storage = Cc["@mozilla.org/storage/service;1"].
@@ -16,7 +19,7 @@
   //**************************************************************************//
   // Database Creation & Access
 
-  _dbVersion: 1,
+  _dbVersion: 2,
 
   _dbSchema: {
     tables: {
@@ -59,30 +62,56 @@
     // FIXME: support labeling the subject as HTML or another content type.
 
     tables: {
-      sources:    "id INTEGER PRIMARY KEY, \
-                   url TEXT NOT NULL, \
-                   title TEXT NOT NULL",
-  
-      messages:   "id INTEGER PRIMARY KEY, \
-                   sourceID INTEGER NOT NULL REFERENCES sources(id), \
-                   universalID TEXT, \
-                   subject TEXT, \
-                   author TEXT, \
-                   timestamp INTEGER, \
-                   link TEXT",
-  
-      parts:      "id INTEGER PRIMARY KEY, \
-                   messageID INTEGER NOT NULL REFERENCES messages(id), \
-                   content BLOB NOT NULL, \
-                   contentType TEXT NOT NULL",
+      sources: {
+        type: TABLE_TYPE_NORMAL,
+        columns: [
+          "id INTEGER PRIMARY KEY",
+          "url TEXT NOT NULL",
+          "title TEXT NOT NULL",
+          "lastRefreshed INTEGER",
+        ]
+      },
+
+      messages: {
+        type: TABLE_TYPE_NORMAL,
+        columns: [
+          "id INTEGER PRIMARY KEY",
+          "sourceID INTEGER NOT NULL REFERENCES sources(id)",
+          "universalID TEXT",
+          "subject TEXT",
+          "author TEXT",
+          "timestamp INTEGER",
+          "link TEXT"
+        ]
+      },
 
-      attributes: "id INTEGER PRIMARY KEY, \
-                   namespace TEXT, \
-                   name TEXT NOT NULL",
-  
-      metadata:   "messageID INTEGER REFERENCES messages(id), \
-                   attributeID INTEGER REFERENCES attributes(id), \
-                   value BLOB"
+      parts: {
+        type: TABLE_TYPE_FULLTEXT,
+        columns: [
+          "messageID INTEGER NOT NULL REFERENCES messages(id)",
+          "contentType",
+          "content"
+        ]
+      },
+
+      attributes: {
+        type: TABLE_TYPE_NORMAL,
+        columns: [
+          "id INTEGER PRIMARY KEY",
+          "namespace TEXT",
+          "name TEXT NOT NULL"
+        ]
+      },
+
+      metadata: {
+        type: TABLE_TYPE_NORMAL,
+        columns: [
+          "messageID INTEGER REFERENCES messages(id)",
+          "attributeID INTEGER REFERENCES attributes(id)",
+          "value BLOB"
+        ]
+      }
+
     },
 
     fulltextTables: {
@@ -117,10 +146,10 @@
     return wrappedStatement;
   },
 
-  // _dbInit and the methods it calls (_dbCreate, _dbMigrate, and version-
-  // specific migration methods) must be careful not to call any method
-  // of the service that assumes the database connection has already been
-  // initialized, since it won't be initialized until at the end of _dbInit.
+  // _dbInit, the methods it calls (_dbCreateTables, _dbMigrate), and methods
+  // those methods call must be careful not to call any method of the service
+  // that assumes the database connection has already been initialized,
+  // since it won't be initialized until this function returns.
 
   _dbInit: function() {
     var dirService = Cc["@mozilla.org/file/directory_service;1"].
@@ -133,11 +162,12 @@
 
     var dbConnection;
 
-    if (!dbFile.exists())
+    if (!dbFile.exists()) {
       dbConnection = this._dbCreate(dbService, dbFile);
+    }
     else {
       try {
-        dbConnection = dbService.openDatabase(dbFile);
+        dbConnection = dbService.openUnsharedDatabase(dbFile);
 
         // Get the version of the database in the file.
         var version = dbConnection.schemaVersion;
@@ -169,11 +199,43 @@
   },
 
   _dbCreate: function(aDBService, aDBFile) {
-      var dbConnection = aDBService.openDatabase(aDBFile);
-      for (var tableName in this._dbSchema.tables)
-        dbConnection.createTable(tableName, this._dbSchema.tables[tableName]);
-      dbConnection.schemaVersion = this._dbVersion;
-      return dbConnection;
+    var dbConnection = aDBService.openUnsharedDatabase(aDBFile);
+
+    dbConnection.beginTransaction();
+    try {
+      this._dbCreateTables(dbConnection);
+      dbConnection.commitTransaction();
+    }
+    catch(ex) {
+      dbConnection.rollbackTransaction();
+      throw ex;
+    }
+
+    return dbConnection;
+  },
+
+  _dbCreateTables: function(aDBConnection) {
+    for (var tableName in this._dbSchema.tables) {
+      var table = this._dbSchema.tables[tableName];
+      switch (table.type) {
+        case TABLE_TYPE_FULLTEXT:
+          this._dbCreateFulltextTable(aDBConnection, tableName, table.columns);
+          break;
+        case TABLE_TYPE_NORMAL:
+        default:
+          aDBConnection.createTable(tableName, table.columns.join(", "));
+          break;
+      }
+    }
+
+    aDBConnection.schemaVersion = this._dbVersion;
+  },
+
+  _dbCreateFulltextTable: function(aDBConnection, aTableName, aColumns) {
+    aDBConnection.executeSimpleSQL(
+      "CREATE VIRTUAL TABLE " + aTableName +
+      " USING fts3(" + aColumns.join(", ") + ")"
+    );
   },
 
   _dbMigrate: function(aDBConnection, aOldVersion, aNewVersion) {
@@ -194,9 +256,15 @@
             " to v" + aNewVersion + ": no migrator function");
   },
 
-  _dbMigrate0To1: function(aDBConnection) {
-    for (var tableName in this._dbSchema.tables)
-      dbConnection.createTable(tableName, this._dbSchema.tables[tableName]);
+  /**
+   * Migrate the database schema from version 0 to version 1.  We never create
+   * a database with version 0, so the database can only have that version
+   * if the database file was created without the schema being constructed.
+   * Thus migrating the database is as simple as constructing the schema as if
+   * from scratch.
+   */
+  _dbMigrate0To2: function(aDBConnection) {
+    this._dbCreateTables(aDBConnection);
   },
 
   get _selectSourcesStatement() {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extension/modules/service.js	Mon Feb 18 21:03:25 2008 -0800
@@ -0,0 +1,139 @@
+const EXPORTED_SYMBOLS = ["SnowlService"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://snowl/log4moz.js");
+
+const PERMS_FILE      = 0644;
+const PERMS_DIRECTORY = 0755;
+
+const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed";
+const PREF_CONTENTHANDLERS_BRANCH = "browser.contentHandlers.types.";
+const SNOWL_HANDLER_URI = "chrome://snowl/content/subscribe.xul?feed=%s";
+const SNOWL_HANDLER_TITLE = "Snowl";
+
+function SnowlService() {
+  this._init();
+}
+SnowlService.prototype = {
+  // Preferences Service
+  get _prefSvc() {
+    let prefSvc = Cc["@mozilla.org/preferences-service;1"].
+                  getService(Ci.nsIPrefService).
+                  QueryInterface(Ci.nsIPrefBranch).
+                  QueryInterface(Ci.nsIPrefBranch2);
+    this.__defineGetter__("_prefSvc", function() { return prefSvc });
+    return this._prefSvc;
+  },
+
+  get _dirSvc() {
+    let dirSvc = Cc["@mozilla.org/file/directory_service;1"].
+                 getService(Ci.nsIProperties);
+    this.__defineGetter__("_dirSvc", function() { return dirSvc });
+    return this._dirSvc;
+  },
+
+  get _converterSvc() {
+    let converterSvc =
+      Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"].
+      getService(Ci.nsIWebContentConverterService);
+    this.__defineGetter__("_converterSvc", function() { return converterSvc });
+    return this._converterSvc;
+  },
+
+  _log: null,
+
+  _init: function() {
+    this._initLogs();
+    this._registerFeedHandler();
+  },
+
+  _initLogs: function() {
+    this._log = Log4Moz.Service.getLogger("Service.Main");
+
+    let formatter = Log4Moz.Service.newFormatter("basic");
+    let root = Log4Moz.Service.rootLogger;
+    root.level = Log4Moz.Level.Debug;
+
+    let capp = Log4Moz.Service.newAppender("console", formatter);
+    capp.level = Log4Moz.Level.Warn;
+    root.addAppender(capp);
+
+    let dapp = Log4Moz.Service.newAppender("dump", formatter);
+    dapp.level = Log4Moz.Level.All;
+    root.addAppender(dapp);
+
+    let logFile = this._dirSvc.get("ProfD", Ci.nsIFile);
+
+    let brief = this._dirSvc.get("ProfD", Ci.nsIFile);
+    brief.QueryInterface(Ci.nsILocalFile);
+
+    brief.append("snowl");
+    if (!brief.exists())
+      brief.create(brief.DIRECTORY_TYPE, PERMS_DIRECTORY);
+
+    brief.append("logs");
+    if (!brief.exists())
+      brief.create(brief.DIRECTORY_TYPE, PERMS_DIRECTORY);
+
+    brief.append("brief-log.txt");
+    if (!brief.exists())
+      brief.create(brief.NORMAL_FILE_TYPE, PERMS_FILE);
+
+    let verbose = brief.parent.clone();
+    verbose.append("verbose-log.txt");
+    if (!verbose.exists())
+      verbose.create(verbose.NORMAL_FILE_TYPE, PERMS_FILE);
+
+    let fapp = Log4Moz.Service.newFileAppender("rotating", brief, formatter);
+    fapp.level = Log4Moz.Level.Info;
+    root.addAppender(fapp);
+    let vapp = Log4Moz.Service.newFileAppender("rotating", verbose, formatter);
+    vapp.level = Log4Moz.Level.Debug;
+    root.addAppender(vapp);
+  },
+
+  _registerFeedHandler: function() {
+    if (this._converterSvc.getWebContentHandlerByURI(TYPE_MAYBE_FEED, SNOWL_HANDLER_URI))
+      return;
+
+    try {
+      this._converterSvc.registerContentHandler(TYPE_MAYBE_FEED,
+                                                SNOWL_HANDLER_URI,
+                                                SNOWL_HANDLER_TITLE,
+                                                null);
+    }
+    catch(ex) {
+      // Bug 415732 hasn't been fixed yet, so work around the bug by writing
+      // preferences directly, although the handler won't be available until
+      // the user restarts the browser.
+      // Based on code in browser/components/feeds/src/WebContentConverter.js.
+      let i = 0;
+      let typeBranch = null;
+      while (true) {
+        typeBranch = this._prefSvc.getBranch(PREF_CONTENTHANDLERS_BRANCH + i + ".");
+        try {
+          let type = typeBranch.getCharPref("type");
+          let uri = typeBranch.getCharPref("uri");
+          if (type == TYPE_MAYBE_FEED && uri == SNOWL_HANDLER_URI)
+            return;
+          ++i;
+        }
+        catch (e) {
+          // No more handlers
+          break;
+        }
+      }
+      if (typeBranch) {
+        typeBranch.setCharPref("type", TYPE_MAYBE_FEED);
+        typeBranch.setCharPref("uri", SNOWL_HANDLER_URI);
+        typeBranch.setCharPref("title", SNOWL_HANDLER_TITLE);
+        this._prefSvc.savePrefFile(null);
+      }
+    }
+  }
+};
--- a/website/riverOfNewsView.js	Tue Feb 05 01:26:06 2008 -0800
+++ b/website/riverOfNewsView.js	Mon Feb 18 21:03:25 2008 -0800
@@ -38,15 +38,28 @@
 }
 
 let RiverOfNews = {
-  _selectMessages: function() {
+  _selectMessages: function(aMatchWords) {
     netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
 
-    let statement = SnowlDatastore.createStatement(
+    let conditions = [];
+
+    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 \
-       JOIN parts ON messages.id = parts.messageID \
-       ORDER BY timestamp DESC"
-    );
+       JOIN parts on messages.id = parts.messageID";
+
+    if (conditions.length > 0)
+      statementString += " WHERE " + conditions.join(" AND ");
+
+    statementString += " ORDER BY timestamp DESC";
+
+    let statement = SnowlDatastore.createStatement(statementString);
+
+    if (aMatchWords)
+      statement.params.matchWords = aMatchWords;
 
     let messages = [];
     try {
@@ -60,6 +73,10 @@
                         content: row.content });
       }
     }
+    catch(ex) {
+      dump(statementString + ": " + ex + ": " + SnowlDatastore.dbConnection.lastErrorString + "\n");
+      throw ex;
+    }
     finally {
       statement.reset();
     }
@@ -68,7 +85,20 @@
   },
 
   onLoad: function() {
-    let messages = this._selectMessages();
+    this._rebuildView();
+  },
+  
+  onFilter: function() {
+    let filterTextbox = document.getElementById("filterTextbox");
+    this._rebuildView(filterTextbox.value);
+  },
+
+  _rebuildView: function(aMatchWords) {
+    let rootNode = document.getElementById("content");
+    while (rootNode.hasChildNodes())
+      rootNode.removeChild(rootNode.lastChild);
+
+    let messages = this._selectMessages(aMatchWords);
 
     for each (let message in messages) {
       let entry = new String(ENTRY_TEMPLATE);
@@ -85,7 +115,7 @@
       }
 
       let container = document.createElementNS(HTML_NS, "div");
-      document.getElementById("content").appendChild(container);
+      rootNode.appendChild(container);
       container.innerHTML = entry;
     }
   }
--- a/website/riverOfNewsView.xul	Tue Feb 05 01:26:06 2008 -0800
+++ b/website/riverOfNewsView.xul	Mon Feb 18 21:03:25 2008 -0800
@@ -10,6 +10,9 @@
   <script type="application/x-javascript" src="template.js"/>
   <script type="application/x-javascript" src="riverOfNewsView.js"/>
 
+  <hbox pack="end">
+    <textbox id="filterTextbox" type="timed" timeout="200" oncommand="RiverOfNews.onFilter()"/>
+  </hbox>
   <vbox id="content" style="overflow: auto; -moz-user-focus: normal;" flex="1"/>
 
 <!--