Mercurial > snowl
view modules/collection.js @ 355:e9d7087abad1
implement tree (list and collection) contextmenu foundation, change XXX Utils to DateUtils, some click handling fixes.
author | alta88 |
---|---|
date | Sun, 02 Nov 2008 10:20:12 -0700 |
parents | 27b518a266e8 |
children |
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 ***** */ let EXPORTED_SYMBOLS = ["SnowlCollection"]; const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; // modules that are generic Cu.import("resource://snowl/modules/URI.js"); Cu.import("resource://snowl/modules/log4moz.js"); // modules that are Snowl-specific Cu.import("resource://snowl/modules/datastore.js"); Cu.import("resource://snowl/modules/message.js"); Cu.import("resource://snowl/modules/utils.js"); // Media type to nsIFeedTextConstruct::type mappings. // FIXME: get this from message.js (or from something that both message.js // and collection.js import). const textConstructTypes = { "text/html": "html", "application/xhtml+xml": "xhtml", "text/plain": "text" }; // FIXME: make SnowlCollection take a hash so it can have named parameters, // since the number of parameters it currently accepts, and the fact that they // are all optional, makes it unwieldy to pass them in the right order. /** * A set of messages. */ function SnowlCollection(id, name, iconURL, constraints, parent, grouped, groupIDColumn, groupNameColumn, groupHomeURLColumn, groupIconURLColumn, filters) { this.id = id; this.name = name; this.iconURL = iconURL; this.constraints = constraints || []; // XXX Does this create a cycle? this.parent = parent; this.grouped = grouped; this.groupIDColumn = groupIDColumn; this.groupNameColumn = groupNameColumn; this.groupHomeURLColumn = groupHomeURLColumn; this.groupIconURLColumn = groupIconURLColumn; this._filters = filters || []; this.sortProperties = ["timestamp"]; } SnowlCollection.prototype = { get _log() { let log = Log4Moz.Service.getLogger("Snowl.Collection"); this.__defineGetter__("_log", function() { return log }); return this._log; }, _filters: null, get filters() { return this._filters; }, set filters(newVal) { this._filters = newVal; this.invalidate(); }, //**************************************************************************// // Grouping // XXX This stuff only matters when the collection is being displayed // in the sidebar. Should we split it out to another class that subclasses // Collection or composes a new class with it? isOpen: false, level: 0, _groups: null, get groups() { if (!this.grouped) return null; if (this._groups) return this._groups; let groups = []; let statement = this._generateGetGroupsStatement(); try { while (statement.step()) { let name = statement.row.name; let iconURL = statement.row.iconURL ? URI.get(statement.row.iconURL) : statement.row.homeURL ? this.getFaviconURL(URI.get(statement.row.homeURL)) : null; if (!iconURL && this.iconURL) iconURL = this.iconURL; // FIXME: fall back to a default collection icon. let constraints = [constraint for each (constraint in this.constraints)]; constraints.push({ expression: this.groupNameColumn + " = :groupValue", parameters: { groupValue: statement.row.name } }); let group = new SnowlCollection(null, name, iconURL, constraints, this); if (this.groupIDColumn) group.groupID = statement.row.groupID; group.level = this.level + 1; groups.push(group); } } finally { statement.reset(); } this._log.info("got " + groups.length + " groups"); return this._groups = groups; }, _generateGetGroupsStatement: function() { let columns = []; if (this.groupIDColumn) { columns.push("DISTINCT(" + this.groupIDColumn + ") AS groupID"); columns.push(this.groupNameColumn + " AS name"); } else columns.push("DISTINCT(" + this.groupNameColumn + ") AS name"); // For some reason, trying to access statement.row.foo dies without throwing // an exception if foo isn't defined as a column in the query, so we have to // define iconURL and homeURL columns even if we don't use them. // FIXME: file a bug on this bizarre behavior. if (this.groupIconURLColumn) columns.push(this.groupIconURLColumn + " AS iconURL"); else columns.push("NULL AS iconURL"); if (this.groupHomeURLColumn) columns.push(this.groupHomeURLColumn + " AS homeURL"); else columns.push("NULL AS homeURL"); // FIXME: allow group queries to make people the primary table. let query = "SELECT " + columns.join(", ") + " " + "FROM sources JOIN messages ON sources.id = messages.sourceID " + "LEFT JOIN people AS authors ON messages.authorID = authors.id"; let conditions = []; for each (let condition in this.constraints) conditions.push(condition.expression); if (conditions.length > 0) query += " WHERE " + conditions.join(" AND "); query += " ORDER BY " + this.groupNameColumn; this._log.info(this.name + " group query: " + query); let statement = SnowlDatastore.createStatement(query); for each (let condition in this.constraints) for (let [name, value] in Iterator(condition.parameters)) statement.params[name] = value; return statement; }, // Favicon Service get _faviconSvc() { let faviconSvc = Cc["@mozilla.org/browser/favicon-service;1"]. getService(Ci.nsIFaviconService); delete this.__proto__._faviconSvc; this.__proto__._faviconSvc = faviconSvc; return this._faviconSvc; }, getFaviconURL: function(homeURL) { try { return this._faviconSvc.getFaviconForPage(homeURL); } catch(ex) { /* no known favicon; use the default */ } return null; }, //**************************************************************************// // Retrieval // sortProperties gets set to its default value in the constructor // since the default is an array, which would be a singleton if defined here. sortProperties: null, sortOrder: 1, _messages: null, get messages() { if (this._messages) return this._messages; this._messages = []; this._messageIndex = {}; let statement = this._generateStatement(); try { while (statement.step()) { let message = new SnowlMessage(statement.row.id, statement.row.subject, statement.row.author, statement.row.link, SnowlDateUtils.julianToJSDate(statement.row.timestamp), (statement.row.read ? true : false), statement.row.authorIcon, SnowlDateUtils.julianToJSDate(statement.row.received)); this._messages.push(message); this._messageIndex[message.id] = message; } } finally { statement.reset(); } this.sort(); // A bug in SQLite breaks relating a virtual table via a LEFT JOIN, so we // can't pull content with our initial query. Instead we do it here. // FIXME: stop doing this once we upgrade to a version of SQLite that does // not have this problem (i.e. 3.5.6+). this._getContent(); this._log.info("Retrieved " + this._messages.length + " messages."); return this._messages; }, invalidate: function() { this._messages = null; }, _getContent: function() { let query = "SELECT messageID, content, mediaType, baseURI, languageCode " + "FROM parts WHERE partType = " + PART_TYPE_CONTENT + " AND messageID IN (" + this._messages.map(function(v) { return v.id }).join(",") + ")"; let statement = SnowlDatastore.createStatement(query); try { while (statement.step()) { let content = Cc["@mozilla.org/feed-textconstruct;1"]. createInstance(Ci.nsIFeedTextConstruct); content.text = statement.row.content; content.type = textConstructTypes[statement.row.mediaType]; content.base = URI.get(statement.row.baseURI); content.lang = statement.row.languageCode; this._messageIndex[statement.row.messageID].content = content; } } finally { statement.reset(); } }, _generateStatement: function() { let columns = ["messages.id", "subject", "authors.name AS author", "link", "timestamp", "read", "authors.iconURL AS authorIcon", "received"]; if (this.groupIDColumn) { columns.push(this.groupIDColumn + " AS groupID"); columns.push(this.groupNameColumn + " AS groupName"); } let query = //"SELECT subject, author, link, timestamp, content \ // FROM sources JOIN messages ON sources.id = messages.sourceID \ // LEFT JOIN parts on messages.id = parts.messageID"; "SELECT " + columns.join(", ") + " " + "FROM sources JOIN messages ON sources.id = messages.sourceID " + "LEFT JOIN people AS authors ON messages.authorID = authors.id"; let conditions = []; for each (let condition in this.constraints) conditions.push(condition.expression); for each (let condition in this.filters) conditions.push(condition.expression); if (conditions.length > 0) query += " WHERE " + conditions.join(" AND "); this._log.info(query); let statement = SnowlDatastore.createStatement(query); for each (let condition in this.constraints) for (let [name, value] in Iterator(condition.parameters)) statement.params[name] = value; for each (let condition in this.filters) for (let [name, value] in Iterator(condition.parameters)) statement.params[name] = value; return statement; }, sort: function() { // Reflect these into local variables that the compare function closure // can access. let properties = this.sortProperties; let order = this.sortOrder; // Fall back on subject. // XXX Should we let callers make this decision? if (properties[properties.length - 1] != "subject") properties.push("subject"); let compare = function(a, b) { for each (let property in properties) { if (prepareObjectForComparison(a[property]) > prepareObjectForComparison(b[property])) return 1 * order; if (prepareObjectForComparison(a[property]) < prepareObjectForComparison(b[property])) return -1 * order; } // Return an inconclusive result. return 0; }; this.messages.sort(compare); } } function prepareObjectForComparison(aObject) { if (typeof aObject == "string") return aObject.toLowerCase(); // Null values are neither greater than nor less than strings, so we // convert them into empty strings, which is how they appear to users. if (aObject == null) return ""; return aObject; }