Mercurial > snowl
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(); +}