зеркало из https://github.com/mozilla/snowl.git
368 строки
14 KiB
JavaScript
368 строки
14 KiB
JavaScript
/* ***** 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;
|
|
|
|
// modules that come with Firefox
|
|
// FIXME: remove this import of XPCOMUtils, as it is no longer being used.
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
// modules that are generic
|
|
Cu.import("resource://snowl/modules/log4moz.js");
|
|
Cu.import("resource://snowl/modules/Observers.js");
|
|
Cu.import("resource://snowl/modules/Sync.js");
|
|
Cu.import("resource://snowl/modules/URI.js");
|
|
|
|
// modules that are Snowl-specific
|
|
Cu.import("resource://snowl/modules/datastore.js");
|
|
Cu.import("resource://snowl/modules/collection2.js");
|
|
Cu.import("resource://snowl/modules/constants.js");
|
|
Cu.import("resource://snowl/modules/utils.js");
|
|
Cu.import("resource://snowl/modules/twitter.js");
|
|
Cu.import("resource://snowl/modules/service.js");
|
|
|
|
let SnowlMessageView = {
|
|
get _log() {
|
|
delete this._log;
|
|
return this._log = Log4Moz.repository.getLogger("Snowl.Stream");
|
|
},
|
|
|
|
get _writeButton() {
|
|
delete this._writeButton;
|
|
return this._writeButton = document.getElementById("writeButton");
|
|
},
|
|
|
|
get _writeForm() {
|
|
delete this._writeForm;
|
|
return this._writeForm = document.getElementById("writeForm");
|
|
},
|
|
|
|
get _refreshButton() {
|
|
delete this._refreshButton;
|
|
return this._refreshButton = document.getElementById("snowlRefreshButton");
|
|
},
|
|
|
|
_window: null,
|
|
_document: null,
|
|
|
|
// The set of messages to display in the view.
|
|
_collection: null,
|
|
|
|
// the width (narrower dimension) of the vertical and horizontal scrollbars;
|
|
// useful for calculating the viewable size of the viewport, since window.
|
|
// innerWidth and innerHeight include the area taken up by the scrollbars
|
|
// XXX Is this value correct, and does it vary by platform?
|
|
scrollbarWidth: 15,
|
|
|
|
get stylesheet() {
|
|
for (let i = 0; i < document.styleSheets.length; i++)
|
|
if (document.styleSheets[i].href == "chrome://snowl/content/stream.css")
|
|
return document.styleSheets[i];
|
|
return null;
|
|
},
|
|
|
|
_updateRule: function(position, newValue) {
|
|
this.stylesheet.deleteRule(position);
|
|
this.stylesheet.insertRule(newValue, position);
|
|
},
|
|
|
|
|
|
//**************************************************************************//
|
|
// Initialization & Destruction
|
|
|
|
onLoad: function() {
|
|
Observers.add("snowl:message:added", this.onMessageAdded, this);
|
|
Observers.add("snowl:source:added", this.onSourcesChanged, this);
|
|
Observers.add("snowl:source:unstored", this.onSourceRemoved, this);
|
|
Observers.add("snowl:messages:completed", this.onMessagesCompleted, this);
|
|
|
|
this.onResize();
|
|
|
|
// 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;
|
|
|
|
// We sort in descending order by ID (the default sort in StorageCollection)
|
|
// in order to do an implicit sort on received time (so that we show
|
|
// messages in the order they are received) while making sure that we always
|
|
// show messages in the same order even when their received times are
|
|
// the same.
|
|
//
|
|
// We could instead sort by received and timestamp, to show messages
|
|
// as they are received, with messages received at the same time being
|
|
// sorted by timestamp; but since we add messages to the view as they
|
|
// are received, regardless of their timestamp, doing that would cause
|
|
// there to be a difference between what the user sees when they leave
|
|
// the view open (and messages accumulate in it over time) versus what
|
|
// they see when they open it anew.
|
|
|
|
// We show only the last couple hundred messages.
|
|
// We used to show all messages within a certain time period, like the last
|
|
// week or the last day, but the purpose of the stream view is to let users
|
|
// glance at recent activity as it scrolls by, not browse messages over long
|
|
// periods of time, and a week's worth of messages is too many to usefully
|
|
// browse in the view. And a day's worth of messages means that if you
|
|
// start your browser after not having used it for a day, you'll see nothing
|
|
// in the view when you first open it, which is confusing and unexpected.
|
|
|
|
this._collection = new StorageCollection({ limit: 250 });
|
|
|
|
this._initWriteForm();
|
|
this._updateWriteButton();
|
|
|
|
this._rebuildView();
|
|
},
|
|
|
|
onunLoad: function() {
|
|
Observers.remove("snowl:message:added", this.onMessageAdded, this);
|
|
Observers.remove("snowl:source:added", this.onSourcesChanged, this);
|
|
Observers.remove("snowl:source:unstored", this.onSourceRemoved, this);
|
|
Observers.remove("snowl:messages:completed", this.onMessagesCompleted, this);
|
|
},
|
|
|
|
_initWriteForm: function() {
|
|
// For some reason setting hidden="true" in the XUL file prevents us
|
|
// from showing the box later via writeForm.hidden = false, so we set it
|
|
// here instead.
|
|
// FIXME: file a bug on this abnormality.
|
|
// XXX Note: setting hidden="true" and then showing the box later
|
|
// via writeForm.hidden = false works fine in the list sidebar, so I'm
|
|
// not sure why it isn't working here.
|
|
this._writeForm.hidden = true;
|
|
},
|
|
|
|
// Selectively enable/disable the button for writing a message depending on
|
|
// whether or not the user has an account that supports writing.
|
|
_updateWriteButton: function() {
|
|
this._writeButton.disabled = (SnowlService.targetsByID.length == 0);
|
|
},
|
|
|
|
|
|
//**************************************************************************//
|
|
// Event & Notification Handlers
|
|
|
|
/**
|
|
* Resize the content in the middle column based on the width of the viewport.
|
|
* FIXME: file a bug on the problem that necessitates this hack.
|
|
*/
|
|
onResize: function() {
|
|
const LEFT_COLUMN_WIDTH = 24 + 4; // 24px width + 4px right margin
|
|
const RIGHT_COLUMN_WIDTH = 16 + 2; // 16px width + 2px left margin
|
|
|
|
// Calculate the width of the middle column and set it (along with some
|
|
// of its contents to that width). See the comments in stream.css
|
|
// for more info on why we set each of these rules.
|
|
|
|
// We anticipate that there will be a scrollbar, so we include it
|
|
// in the calculation. Perhaps we should instead wait to include it
|
|
// until the content actually overflows.
|
|
|
|
// window.innerWidth == document.documentElement.boxObject.width == document.documentElement.clientWidth,
|
|
// and I know of no reason to prefer one over the other, except that
|
|
// clientWidth only works in Firefox 3.1+, and we support Firefox 3.0,
|
|
// so one of the others is better.
|
|
|
|
let width = window.innerWidth - this.scrollbarWidth - LEFT_COLUMN_WIDTH - RIGHT_COLUMN_WIDTH;
|
|
this._updateRule(0, ".body { min-width: " + width + "px; max-width: " + width + "px }");
|
|
this._updateRule(1, ".body > div { min-width: " + width + "px; max-width: " + width + "px }");
|
|
this._updateRule(2, ".centerColumn { min-width: " + width + "px; max-width: " + width + "px }");
|
|
},
|
|
|
|
onMessageAdded: function(message) {
|
|
let messages = this._document.getElementById("contentBox");
|
|
let messageBox = this._buildMessageView(message);
|
|
messages.insertBefore(messageBox, messages.firstChild);
|
|
},
|
|
|
|
onSourcesChanged: function() {
|
|
this._updateWriteButton();
|
|
},
|
|
|
|
onSourceRemoved: function() {
|
|
// We don't currently have a way to remove just the messages
|
|
// from the removed source, so rebuild the entire view.
|
|
this._rebuildView();
|
|
},
|
|
|
|
onMessagesCompleted: function(aSourceId) {
|
|
// Enable refresh button.
|
|
if (SnowlService.refreshingCount == 0)
|
|
this._refreshButton.removeAttribute("disabled");
|
|
// Disable refresh button.
|
|
if (aSourceId == "refresh")
|
|
this._refreshButton.setAttribute("disabled", true);
|
|
},
|
|
|
|
onToggleGroup: function(event) {
|
|
event.target.nextSibling.style.display = event.target.checked ? "block" : "none";
|
|
},
|
|
|
|
onRefresh: function() {
|
|
SnowlService.refreshAllSources();
|
|
},
|
|
|
|
onToggleWrite: function(event) {
|
|
this._writeForm.hidden = !event.target.checked;
|
|
},
|
|
|
|
|
|
//**************************************************************************//
|
|
// Content Generation
|
|
|
|
_rebuildView: function() {
|
|
let begin = new Date();
|
|
|
|
let contentBox = this._document.getElementById("contentBox");
|
|
while (contentBox.hasChildNodes())
|
|
contentBox.removeChild(contentBox.lastChild);
|
|
|
|
for each (let message in this._collection) {
|
|
let messageBox = this._buildMessageView(message);
|
|
contentBox.appendChild(messageBox);
|
|
|
|
// Sleep a bit after every message so we don't hork the UI thread and users
|
|
// can immediately start reading messages while we finish writing them.
|
|
Sync.sleep(0);
|
|
}
|
|
|
|
this._log.info("time spent building view: " + (new Date() - begin) + "ms\n");
|
|
|
|
//let serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
|
|
// createInstance(Ci.nsIDOMSerializer);
|
|
//this._log.info(serializer.serializeToString(document.getElementById("contentBox")));
|
|
},
|
|
|
|
_buildMessageView: function(message) {
|
|
let messageBox = this._document.createElementNS(XUL_NS, "hbox");
|
|
messageBox.className = "message";
|
|
|
|
// left column
|
|
let leftColumn = this._document.createElementNS(XUL_NS, "vbox");
|
|
leftColumn.className = "leftColumn";
|
|
let icon = document.createElementNS(XUL_NS, "image");
|
|
icon.className = "icon";
|
|
if (message.author && message.author.person.iconURL)
|
|
icon.setAttribute("src", message.author.person.iconURL);
|
|
else if (message.source.faviconURI)
|
|
icon.setAttribute("src", message.source.faviconURI.spec)
|
|
else
|
|
icon.setAttribute("src", "chrome://snowl/skin/livemarkItem-16.png");
|
|
|
|
leftColumn.appendChild(icon);
|
|
messageBox.appendChild(leftColumn);
|
|
|
|
// center column
|
|
let centerColumn = this._document.createElementNS(XUL_NS, "vbox");
|
|
centerColumn.className = "centerColumn";
|
|
messageBox.appendChild(centerColumn);
|
|
|
|
// Author or Source
|
|
if (message.author || message.source) {
|
|
let desc = this._document.createElementNS(XUL_NS, "description");
|
|
let value = message.author && message.author.person.name ?
|
|
message.author.person.name :
|
|
message.source.name;
|
|
desc.className = "author";
|
|
desc.setAttribute("crop", "end");
|
|
desc.setAttribute("value", value);
|
|
centerColumn.appendChild(desc);
|
|
}
|
|
|
|
// Timestamp
|
|
// Commented out because the timestamp isn't that useful when we order
|
|
// by time received. Instead, we're going to group by time period
|
|
// received (this morning, yesterday, last week, etc.) to give users
|
|
// useful chronographic info.
|
|
//let lastUpdated = SnowlDateUtils._formatDate(message.timestamp);
|
|
//if (lastUpdated) {
|
|
// let timestamp = this._document.createElementNS(XUL_NS, "description");
|
|
// timestamp.className = "timestamp";
|
|
// timestamp.setAttribute("crop", "end");
|
|
// timestamp.setAttribute("value", lastUpdated);
|
|
// centerColumn.appendChild(timestamp);
|
|
//}
|
|
|
|
// Content (subject or excerpt)
|
|
let body = this._document.createElementNS(XUL_NS, "description");
|
|
body.className = "body";
|
|
let div = this._document.createElementNS(HTML_NS, "div");
|
|
|
|
let content = message.subject || message.excerpt;
|
|
|
|
if (message.link) {
|
|
let a = this._document.createElementNS(HTML_NS, "a");
|
|
SnowlUtils.safelySetURIAttribute(a,
|
|
"href",
|
|
message.link.spec,
|
|
message.source.principal);
|
|
body.className += " text-link";
|
|
a.appendChild(this._document.createTextNode(content));
|
|
div.appendChild(a);
|
|
}
|
|
else if (content) {
|
|
SnowlUtils.linkifyText(content, div, message.source.principal);
|
|
}
|
|
|
|
body.appendChild(div);
|
|
centerColumn.appendChild(body);
|
|
|
|
// 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)
|
|
// SnowlUtils.safelySetURIAttribute(source, "href", message.source.humanURI.spec, message.source.principal);
|
|
//centerColumn.appendChild(source);
|
|
|
|
// right column
|
|
let rightColumn = this._document.createElementNS(XUL_NS, "vbox");
|
|
rightColumn.className = "rightColumn";
|
|
messageBox.appendChild(rightColumn);
|
|
|
|
return messageBox;
|
|
}
|
|
|
|
};
|