Mercurial > snowl
changeset 248:a7323e1bc175
beginnings of a stream view
author | Myk Melez <myk@mozilla.org> |
---|---|
date | Tue, 19 Aug 2008 18:08:37 -0700 |
parents | ca821839e15b |
children | 6a0e3cc0d866 |
files | content/browser.xul content/river.js content/stream.css content/stream.js content/stream.xul locale/en-US/browser.dtd locale/en-US/stream.dtd |
diffstat | 7 files changed, 543 insertions(+), 7 deletions(-) [+] |
line wrap: on
line diff
--- a/content/browser.xul Tue Aug 19 18:08:20 2008 -0700 +++ b/content/browser.xul Tue Aug 19 18:08:37 2008 -0700 @@ -41,6 +41,10 @@ <overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" id="snowlBrowserOverlay"> + <menupopup id="viewSidebarMenu"> + <menuitem observes="viewSnowlStream"/> + </menupopup> + <menupopup id="menu_viewPopup"> <!-- Since these both get inserted after viewSidebarMenuMenu, they appear - in the reverse of the order below. --> @@ -61,6 +65,14 @@ sidebarurl="chrome://snowl/content/sidebar.xul" sidebartitle="&sidebar.label;" oncommand="toggleSidebar('viewSnowlSidebar')"/> + <broadcaster id="viewSnowlStream" + label="&streamView.label;" + autoCheck="false" + type="checkbox" + group="sidebar" + sidebarurl="chrome://snowl/content/stream.xul" + sidebartitle="&streamSidebar.label;" + oncommand="toggleSidebar('viewSnowlStream')"/> </broadcasterset> </overlay>
--- a/content/river.js Tue Aug 19 18:08:20 2008 -0700 +++ b/content/river.js Tue Aug 19 18:08:37 2008 -0700 @@ -451,15 +451,12 @@ // Safe DOM Manipulation /** - * Use this sandbox to run any dom manipulation code on nodes which - * are already inserted into the content document. + * Use this sandbox to run any DOM manipulation code on nodes + * which are already inserted into the content document. */ - __contentSandbox: null, get _contentSandbox() { - if (!this.__contentSandbox) - this.__contentSandbox = new Cu.Sandbox(this._window); - - return this.__contentSandbox; + delete this._contentSandbox; + return this._contentSandbox = new Cu.Sandbox(this._window); }, // FIXME: use this when setting story title and byline.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/content/stream.css Tue Aug 19 18:08:37 2008 -0700 @@ -0,0 +1,114 @@ +/* ***** 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 ***** */ + +#toolbar { + font: message-box; + -moz-box-align: center; + width: 100%; +} + +/* Don't style the buttons natively because they look ugly on Linux. + * FIXME: test on other platforms and style conditionally as appropriate, + * and figure out how to make them look pretty on Linux. */ +toolbarbutton { + -moz-appearance: none; + min-width: 24px; + min-height: 24px; +} + +/* Hide the button labels, since they would otherwise take up space, + * even if empty, and throw off the centering of the button icons. */ +.toolbarbutton-text { + display: none; +} + +/* Show icons in the sources menulist. */ +#sourceMenu > menupopup > menuitem > .menu-iconic-left { + display: block; +} + +#contentBox { + padding: 7px; + + -moz-user-focus: normal; + -moz-user-select: -moz-all; + + /* Make it look like content rather than the chrome in which it is embedded. */ + color: black; + background-color: white; + font: normal normal normal medium serif; + + /* This gets set programmatically on load, since setting it in CSS doesn't work. */ + height: 0; + + overflow: auto; +} + +/* Make the title and source links look less like links to differentiate + * between the links in the content and these links in the chrome. */ +.source > a, .title > a { + font-family: sans-serif; + text-decoration: none; + color: black; +} +.source > a:visited, .title > a:visited { + color: #555; +} + +h2 { + font-size: larger; + margin-bottom: 0; +} + +.byline { + font-size: smaller; + font-family: sans-serif; +} + +.metadata { + float: left; + width: 20%; + + font: message-box; + font-size: smaller; +} + +/* Don't draw borders around hyperlinked favicons, but do pad them a bit + * between the end of the favicon and the start of the source name. */ +a > img { + border: 0; + -moz-padding-end: 1px; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/content/stream.js Tue Aug 19 18:08:37 2008 -0700 @@ -0,0 +1,353 @@ +/* ***** 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 ***** */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://snowl/modules/log4moz.js"); +Cu.import("resource://snowl/modules/URI.js"); + +Cu.import("resource://snowl/modules/datastore.js"); +Cu.import("resource://snowl/modules/collection.js"); + +const XML_NS = "http://www.w3.org/XML/1998/namespace" +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const HTML_NS = "http://www.w3.org/1999/xhtml"; + +let gBrowserWindow = window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebNavigation). + QueryInterface(Ci.nsIDocShellTreeItem). + rootTreeItem. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindow); + +let gMessageViewWindow = window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebNavigation). + QueryInterface(Ci.nsIDocShellTreeItem). + rootTreeItem. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindow); + +let SnowlMessageView = { + get _log() { + delete this._log; + return this._log = Log4Moz.Service.getLogger("Snowl.Stream"); + }, + + // 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; + }, + + // Favicon Service + get _faviconSvc() { + let faviconSvc = Cc["@mozilla.org/browser/favicon-service;1"]. + getService(Ci.nsIFaviconService); + delete this._faviconSvc; + this._faviconSvc = faviconSvc; + return this._faviconSvc; + }, + + _window: null, + _document: null, + + // The set of messages to display in the view. + _collection: null, + + + //**************************************************************************// + // Initialization & Destruction + + onLoad: function() { + // Explicitly wrap |window| in an XPCNativeWrapper to make sure + // it's a real native object! This will throw an exception if we + // get a non-native object. + this._window = new XPCNativeWrapper(window); + this._document = this._window.document; + + this._collection = new SnowlCollection(); + this._collection.sortOrder = -1; + this.rebuildView(); + }, + + onUnload: function() {}, + + + //**************************************************************************// + // Event Handlers + + + //**************************************************************************// + // Safe DOM Manipulation + + /** + * Use this sandbox to run any DOM manipulation code on nodes + * which are already inserted into the content document. + */ + get _contentSandbox() { + delete this._contentSandbox; + return this._contentSandbox = new Cu.Sandbox(this._window); + }, + + // FIXME: use this when setting story title and byline. + _setContentText: function FW__setContentText(id, text) { + this._contentSandbox.element = this._document.getElementById(id); + this._contentSandbox.textNode = this._document.createTextNode(text); + let codeStr = + "while (element.hasChildNodes()) " + + " element.removeChild(element.firstChild);" + + "element.appendChild(textNode);"; + Cu.evalInSandbox(codeStr, this._contentSandbox); + this._contentSandbox.element = null; + this._contentSandbox.textNode = null; + }, + + // FIXME: use this when linkifying the story title and source. + /** + * Safely sets the href attribute on an anchor tag, providing the URI + * specified can be loaded according to rules. + * + * XXX Renamed from safeSetURIAttribute to unsafeSetURIAttribute to reflect + * that we've commented out the stuff that makes it safe. + * + * FIXME: I don't understand the security implications here, but presumably + * there's a reason this is here, and we should be respecting it, so make this + * work by giving each message in a collection have a reference to its source + * and then use the source's URI to create the principal with which we compare + * the URI. + * + * @param element + * The element to set a URI attribute on + * @param attribute + * The attribute of the element to set the URI to, e.g. href or src + * @param uri + * The URI spec to set as the href + */ + _unsafeSetURIAttribute: + function FW__unsafeSetURIAttribute(element, attribute, uri) { +/* + let secman = Cc["@mozilla.org/scriptsecuritymanager;1"]. + getService(Ci.nsIScriptSecurityManager); + const flags = Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL; + try { + secman.checkLoadURIStrWithPrincipal(this._feedPrincipal, uri, flags); + // checkLoadURIStrWithPrincipal will throw if the link URI should not be + // loaded, either because our feedURI isn't allowed to load it or per + // the rules specified in |flags|, so we'll never "linkify" the link... + } + catch (e) { + // Not allowed to load this link because secman.checkLoadURIStr threw + return; + } +*/ + + this._contentSandbox.element = element; + this._contentSandbox.uri = uri; + let codeStr = "element.setAttribute('" + attribute + "', uri);"; + Cu.evalInSandbox(codeStr, this._contentSandbox); + }, + + + //**************************************************************************// + // Content Generation + + /** + * A JavaScript Strands Future with which we pause the writing of messages + * so as not to hork the UI thread. + */ + _rebuildViewFuture: null, + + /** + * Sleep the specified number of milliseconds before continuing at the point + * in the caller where this function was called. For the most part, this is + * a generic sleep routine like the one provided by JavaScript Strands, + * but we store the Future this function creates in the _rebuildViewFuture + * property so we can interrupt it when writeMessages gets called again + * while it is currently writing messages. + */ + _sleepRebuildView: strand(function(millis) { + this._rebuildViewFuture = new Future(); + setTimeout(this._rebuildViewFuture.fulfill, millis); + yield this._rebuildViewFuture.result(); + }), + + rebuildView: strand(function() { + let begin = new Date(); + + let contentBox = this._document.getElementById("contentBox"); + while (contentBox.hasChildNodes()) + contentBox.removeChild(contentBox.lastChild); + + // Interrupt a strand currently writing messages so we don't both try + // to write messages at the same time. + // FIXME: figure out how to suppress the exception this throws to the error + // console, since this interruption is expected and normal behavior. + if (this._rebuildViewFuture) + this._rebuildViewFuture.interrupt(); + + this._contentSandbox.messages = + this._document.getElementById("contentBox"); + + for (let i = 0; i < this._collection.messages.length; ++i) { + let message = this._collection.messages[i]; + + let messageBox = this._document.createElementNS(HTML_NS, "div"); + messageBox.className = "message"; + messageBox.setAttribute("index", i); + + // Title + let title = this._document.createElementNS(HTML_NS, "div"); + title.className = "title"; + let titleLink = this._document.createElementNS(HTML_NS, "a"); + titleLink.appendChild(this._document.createTextNode(message.subject || "untitled")); + if (message.link) + this._unsafeSetURIAttribute(titleLink, "href", message.link); + title.appendChild(titleLink); + messageBox.appendChild(title); + + // Byline + let bylineBox = this._document.createElementNS(HTML_NS, "div"); + bylineBox.className = "byline"; + messageBox.appendChild(bylineBox); + + // Source + //let source = this._document.createElementNS(HTML_NS, "a"); + //source.className = "source"; + //let sourceIcon = document.createElementNS(HTML_NS, "img"); + //let sourceFaviconURI = message.source.humanURI || URI.get("urn:use-default-icon"); + //sourceIcon.src = this._faviconSvc.getFaviconImageForPage(sourceFaviconURI).spec; + //source.appendChild(sourceIcon); + //source.appendChild(this._document.createTextNode(message.source.name)); + //if (message.source.humanURI) + // this._unsafeSetURIAttribute(source, "href", message.source.humanURI.spec); + //bylineBox.appendChild(source); + + // Author or Source + if (message.author) + bylineBox.appendChild(this._document.createTextNode(message.author)); + else if (message.source) + bylineBox.appendChild(this._document.createTextNode(message.source.name)); + + // Timestamp + let lastUpdated = this._formatTimestamp(new Date(message.timestamp)); + if (lastUpdated) { + let timestamp = this._document.createElementNS(HTML_NS, "span"); + timestamp.className = "timestamp"; + timestamp.appendChild(document.createTextNode(lastUpdated)); + if (bylineBox.hasChildNodes()) + bylineBox.appendChild(this._document.createTextNode(" - ")); + bylineBox.appendChild(timestamp); + } + + this._contentSandbox.messageBox = messageBox; + + let codeStr = "messages.appendChild(messageBox)"; + Cu.evalInSandbox(codeStr, this._contentSandbox); + + // Sleep after every tenth message so we don't hork the UI thread and users + // can immediately start reading messages while we finish writing them. + if (!(i % 10)) + yield this._sleepRebuildView(0); + } + + this._contentSandbox.messages = null; + this._contentSandbox.messageBox = null; + + this._log.info("time spent building view: " + (new Date() - begin) + "ms\n"); + }), + + // FIXME: this also appears in the list and river views; factor it out. + /** + * 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; + } + +};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/content/stream.xul Tue Aug 19 18:08:37 2008 -0700 @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- ***** 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 ***** --> + +<?xml-stylesheet href="chrome://global/skin/" type"text/css"?> +<?xml-stylesheet href="chrome://snowl/content/stream.css" type="text/css"?> + +<!DOCTYPE page SYSTEM "chrome://snowl/locale/stream.dtd"> + +<page id="snowlStreamView" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&page.title;" + onload="SnowlMessageView.onLoad()" + onunload="SnowlMessageView.onUnload()" + onclick="return window.parent.contentAreaClick(event, true);"> + + <script type="application/javascript" src="chrome://snowl/content/strands.js"/> + <script type="application/javascript" src="chrome://snowl/content/stream.js"/> + + <vbox id="contentBox" flex="1"/> + +</page>
--- a/locale/en-US/browser.dtd Tue Aug 19 18:08:20 2008 -0700 +++ b/locale/en-US/browser.dtd Tue Aug 19 18:08:37 2008 -0700 @@ -4,3 +4,6 @@ <!ENTITY listView.accesskey "m"> <!ENTITY sidebar.label "Snowl"> + +<!ENTITY streamView.label "Message Stream"> +<!ENTITY streamSidebar.label "Snowl Message Stream">