Mercurial > snowl
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"/> <!--