implement the first cut of Places for the collections tree. this patch also fixes an endless throbber on a particular error in subscribe, and cleans up message headers.

This commit is contained in:
alta88 2009-01-29 13:19:56 -07:00
Родитель a9956004de
Коммит a45b5b26c2
30 изменённых файлов: 1184 добавлений и 673 удалений

Просмотреть файл

@ -151,14 +151,19 @@ let Snowl = {
}
}
// Hierarchy init
let hmenuitems = document.getElementsByAttribute("name", "snowlHierarchyMenuitemGroup");
let isHierarchical = this._prefs.get("collection.hierarchicalView");
// Flat/Grouped init
let isFlatList;
let sidebarDoc = document.getElementById("sidebar").contentDocument;
let sourcesView = sidebarDoc.getElementById("sourcesView");
if (sourcesView)
isFlatList = sourcesView.getAttribute("flat") == "true";
let hmenuitems = document.getElementsByAttribute("name", "snowlFlatListMenuitemGroup");
let rivertab = this._snowlRiverTab();
if (hmenuitems) {
for (var i = 0; i < hmenuitems.length; i++) {
hmenuitems[i].setAttribute("disabled", !lchecked && !(rivertab));
if (i == isHierarchical)
if (i == isFlatList)
hmenuitems[i].setAttribute("checked", true);
}
}
@ -352,27 +357,34 @@ let Snowl = {
return headerDeck;
},
// Collections hierarchy toggle
kHierarchyOff: 0,
kHierarchyOn: 1,
// Collections flat/grouped toggle, menu disabled if not in List view
kFlatListOff: 0,
kFlatListOn: 1,
_toggleHierarchy: function(val) {
_toggleFlatList: function(val) {
let sidebarDoc = document.getElementById("sidebar").contentWindow;
let lchecked = document.getElementById("viewSnowlList").hasAttribute("checked");
if (lchecked) {
sidebarDoc.CollectionsView.isHierarchical = val;
sidebarDoc.CollectionsView._buildCollectionTree();
sidebarDoc.CollectionsView._tree.setAttribute("flat", val ? true : false);
sidebarDoc.CollectionsView._tree.place = val ?
SnowlPlaces.queryFlat : SnowlPlaces.queryGrouped;
// Ensure collection selection maintained, if in List sidebar
if (document.getElementById("snowlSidebar") && SnowlUtils.gListViewCollectionItemId) {
sidebarDoc.CollectionsView._tree.
selectItems([SnowlUtils.gListViewCollectionItemId]);
sidebarDoc.CollectionsView._tree.boxObject.
ensureRowIsVisible(sidebarDoc.CollectionsView._tree.currentIndex);
}
}
let rivertab = this._snowlRiverTab();
if (rivertab) {
let tabWindowDoc = gBrowser.getBrowserAtIndex(rivertab._tPos).contentWindow;
let tabDoc = new XPCNativeWrapper(tabWindowDoc).wrappedJSObject;
tabDoc.CollectionsView.isHierarchical = val;
tabDoc.CollectionsView._buildCollectionTree();
tabDoc.CollectionsView._tree.setAttribute("flat", val ? true : false);
tabDoc.CollectionsView._tree.place = val ?
SnowlPlaces.queryFlat : SnowlPlaces.queryGrouped;
}
this._prefs.set("collection.hierarchicalView", val);
},
// Need to init onLoad due to xul structure, toolbar exists in list and stream

Просмотреть файл

@ -161,18 +161,18 @@
headerType="Snowl.kFullHeader"
oncommand="Snowl._toggleHeader(event)"/>
<menuseparator/>
<menuitem id="snowlHierarchyOffMenuitem"
label="&hierarchyOff.label;"
<menuitem id="snowlGroupedOnMenuitem"
label="&groupedOn.label;"
type="radio"
accesskey="&hierarchyOff.accesskey;"
name="snowlHierarchyMenuitemGroup"
oncommand="Snowl._toggleHierarchy(Snowl.kHierarchyOff)"/>
<menuitem id="snowlHierarchyOnMenuitem"
label="&hierarchyOn.label;"
accesskey="&groupedOn.accesskey;"
name="snowlFlatListMenuitemGroup"
oncommand="Snowl._toggleFlatList(Snowl.kFlatListOff)"/>
<menuitem id="snowlGroupedOffMenuitem"
label="&groupedOff.label;"
type="radio"
accesskey="&hierarchyOn.accesskey;"
name="snowlHierarchyMenuitemGroup"
oncommand="Snowl._toggleHierarchy(Snowl.kHierarchyOn)"/>
accesskey="&groupedOff.accesskey;"
name="snowlFlatListMenuitemGroup"
oncommand="Snowl._toggleFlatList(Snowl.kFlatListOn)"/>
<menuseparator/>
<menuitem id="snowlToolbarMenuitem"
label="&toolbar.label;"

Просмотреть файл

@ -35,6 +35,7 @@
* ***** END LICENSE BLOCK ***** */
#sourcesView {
-moz-binding: url(chrome://snowl/content/snowlTree.xml#tree);
margin-left: 0;
margin-right: 0;
}

Просмотреть файл

@ -49,13 +49,7 @@ Cu.import("resource://snowl/modules/feed.js");
Cu.import("resource://snowl/modules/identity.js");
Cu.import("resource://snowl/modules/collection.js");
Cu.import("resource://snowl/modules/opml.js");
let gBrowserWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIWebNavigation).
QueryInterface(Ci.nsIDocShellTreeItem).
rootTreeItem.
QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIDOMWindow);
//Cu.import("resource://snowl/components/components.js");
let CollectionsView = {
_log: null,
@ -67,185 +61,316 @@ let CollectionsView = {
get _children() {
delete this._children;
return this._children = this._tree.getElementsByTagName("treechildren")[0];
return this._children = document.getElementById("sourcesViewChildren");
},
isHierarchical: gBrowserWindow.Snowl._prefs.get("collection.hierarchicalView"),
//**************************************************************************//
// Initialization & Destruction
init: function() {
this._log = Log4Moz.repository.getLogger("Snowl.Sidebar");
Observers.add("snowl:sources:changed", this.onSourcesChanged, this);
Observers.add("snowl:messages:changed", this.onMessagesChanged, this);
this._getCollections();
this._buildCollectionTree();
Observers.add("snowl:source:added", this.onSourceAdded, this);
Observers.add("snowl:message:added", this.onMessageAdded, this);
Observers.add("snowl:messages:changed", this.onMessagesComplete, this);
// Intialize places
SnowlPlaces.init();
if (!this._tree.hasAttribute("flat"))
this._tree.setAttribute("flat", true);
// Get collections and convert to places tree - one time upgrade
if (!SnowlPlaces.convertedToPlaces) {
this._getCollections();
this._buildCollectionTree();
}
let query = this._tree.getAttribute("flat") == "true" ?
SnowlPlaces.queryFlat : SnowlPlaces.queryGrouped;
this._tree.place = query;
// Ensure collection selection maintained, if in List sidebar
if (document.getElementById("snowlSidebar"))
this._tree.view.selection.select(SnowlUtils.gListViewCollectionIndex);
},
//**************************************************************************//
// nsITreeView
selection: null,
get rowCount() {
return this._rows.length;
},
// FIXME: consolidate these two references.
_treebox: null,
setTree: function(treeBox) {
this._treeBox = treeBox;
},
getCellText : function(row, column) {
return this._rows[row].name;
},
isContainer: function(row) {
//this._log.info("isContainer: " + (this._rows[row].groups ? true : false));
return (this._rows[row].groups ? true : false);
},
isContainerOpen: function(row) {
//this._log.info("isContainerOpen: " + this._rows[row].isOpen);
return this._rows[row].isOpen;
},
isContainerEmpty: function(row) {
//this._log.info("isContainerEmpty: " + row + " " + this._rows[row].groups.length + " " + (this._rows[row].groups.length == 0));
return (this._rows[row].groups.length == 0);
},
isSeparator: function(row) { return false },
isSorted: function() { return false },
// FIXME: make this return true for collection names that are editable,
// and then implement name editing on the new architecture.
isEditable: function(row, column) { return false },
getParentIndex: function(row) {
//this._log.info("getParentIndex: " + row);
let thisLevel = this.getLevel(row);
if (thisLevel == 0)
return -1;
for (let t = row - 1; t >= 0; t--)
if (this.getLevel(t) < thisLevel)
return t;
throw "getParentIndex: couldn't figure out parent index for row " + row;
},
getLevel: function(row) {
//this._log.info("getLevel: " + row);
if (!this.isHierarchical)
return 0;
return this._rows[row].level;
},
hasNextSibling: function(idx, after) {
//this._log.info("hasNextSibling: " + idx + " " + after);
let thisLevel = this.getLevel(idx);
for (let t = idx + 1; t < this._rows.length; t++) {
let nextLevel = this.getLevel(t);
if (nextLevel == thisLevel)
return true;
if (nextLevel < thisLevel)
return false;
}
return false;
},
getImageSrc: function(row, column) {
if (column.id == "nameCol") {
let iconURL = this._rows[row].iconURL;
if (iconURL)
return iconURL.spec;
}
return null;
},
toggleOpenState: function(idx) {
//this._log.info("toggleOpenState: " + idx);
let item = this._rows[idx];
if (!item.groups)
return;
if (item.isOpen) {
item.isOpen = false;
let thisLevel = this.getLevel(idx);
let numToDelete = 0;
for (let t = idx + 1; t < this._rows.length; t++) {
if (this.getLevel(t) > thisLevel)
numToDelete++;
else
break;
}
if (numToDelete) {
this._rows.splice(idx + 1, numToDelete);
this._treeBox.rowCountChanged(idx + 1, -numToDelete);
}
}
else {
item.isOpen = true;
let groups = this._rows[idx].groups;
for (let i = 0; i < groups.length; i++)
this._rows.splice(idx + 1 + i, 0, groups[i]);
this._treeBox.rowCountChanged(idx + 1, groups.length);
}
},
getRowProperties: function (row, properties) {},
getCellProperties: function (row, column, properties) {},
getColumnProperties: function(columnID, column, properties) {},
setCellText: function(aRow, aCol, aValue) {
let statement = SnowlDatastore.createStatement(
"UPDATE sources SET name = :name WHERE id = :id");
statement.params.name = this._rows[aRow].name = aValue;
statement.params.id = this._rows[aRow].id;
try {
statement.execute();
}
finally {
statement.reset();
}
if (document.getElementById("snowlSidebar") && SnowlUtils.gListViewCollectionItemId)
this._tree.selectItems([SnowlUtils.gListViewCollectionItemId]);
},
//**************************************************************************//
// Event & Notification Handlers
onSourcesChanged: function() {
this._getCollections();
// Rebuild the view to reflect the new collection of messages.
// Since the number of rows might have changed, we do this by reinitializing
// the view instead of merely invalidating the box object (which doesn't
// expect changes to the number of rows).
this._buildCollectionTree();
onSourceAdded: function() {
// Newly subscribed source has been added to places, select the inserted row.
// The tree may not be ready, so use a timeout. The effect of selecting here
// is that onMessageAdded will trigger a list view refresh for each message,
// so messages pop into the list as added.
this._tree.view.selection.select(-1);
this._tree.currentSelectedIndex = this._tree.currentIndex;
setTimeout(function() {
SnowlUtils.gListViewDeleteMoveInsert = true;
SnowlUtils.RestoreSelection(CollectionsView._tree, SnowlUtils.gListViewCollectionItemId);
}, 300)
},
onMessagesChanged: function() {
// When messages change, the list of users we display might also change,
// so we rebuild the view from scratch.
this._getCollections();
this._buildCollectionTree();
onMessageAdded: function(aMessageObj) {
// Determine if source or author of new message is currently selected in the
// collections list; if so refresh list view.
let query, uri, rangeFirst = { }, rangeLast = { }, refreshFlag = false;
let numRanges = this._tree.view.selection.getRangeCount();
for (let i = 0; i < numRanges && !refreshFlag; i++) {
this._tree.view.selection.getRangeAt(i, rangeFirst, rangeLast);
for (let index = rangeFirst.value; index <= rangeLast.value; index++) {
uri = this._tree.view.nodeForTreeIndex(index).uri;
query = new SnowlQuery(uri);
//this._log.info("onMessageAdded: queryId:aMsgObj = " +
//query.queryID + " : " + aMessageObj.toSource());
if ((query.queryGroupIDColumn == "sources.id" &&
query.queryID == aMessageObj.sourceID) ||
(query.queryGroupIDColumn == "author.id" &&
query.queryID == aMessageObj.authorID))
refreshFlag = true;
}
}
// Refresh list view if found updating source matches at least one selection
// in the tree.
if (refreshFlag) {
this._log.info("onMessageAdded: REFRESH queryId:aMsgObj = " +
query.queryID + " : " + aMessageObj.toSource());
gMessageViewWindow.SnowlMessageView._collection.invalidate();
gMessageViewWindow.SnowlMessageView._rebuildView();
}
},
onMessagesComplete: function(aSourceId) {
// Finished downloading all messages. Scroll the collection tree intelligently.
// SnowlUtils.scrollPlacement(this._tree, this._tree.currentIndex);
},
onSelect: function(aEvent) {
// We want to only select onClick (more precisely, mouseup) for mouse events
// but need onSelect for key events (arrow keys). Since onSelect events do
// not have info on whether mouse or key, we track it ourselves.
if (this._tree.currentIndex == -1 || SnowlUtils.gMouseEvent)
return;
this.onClick(aEvent);
},
onClick: function(aEvent) {
let row = { }, col = { }, obj = { };
let constraints = [];
let uri, rangeFirst = { }, rangeLast = { }, stop = false;
let modKey = aEvent.metaKey || aEvent.ctrlKey || aEvent.shiftKey;
this._log.info("onClick start: curIndex:curSelectedIndex = "+
this._tree.currentIndex+" : "+this._tree.currentSelectedIndex);
this._log.info("onClick start: selNodeViewIndex:viewSelcurIndex = "+
(this._tree.selectedNode ?
this._tree.selectedNode.viewIndex+" : "+this._tree.view.selection.currentIndex :
"NULL selectedNode"));
this._log.info("onClick start - gMouseEvent:gRtbutton:modKey = "+
SnowlUtils.gMouseEvent+" : "+SnowlUtils.gRightMouseButtonDown+" : "+modKey);
this._log.info("onClick: selectionCount = "+this._tree.view.selection.count);
// Don't run query on right click, or already selected row (unless deselecting).
if (SnowlUtils.gRightMouseButtonDown || this._tree.currentIndex == -1 ||
(this._tree.view.selection.count == 1 && !modKey &&
this._tree.currentIndex == this._tree.currentSelectedIndex))
return;
// On unsubscribe, RestoreSelection() attempts to select the last selected
// row, which may have been removed as a result of source unsubscribe, in
// which case there should be a null selectedNode. Shift-left click will
// also deselect a row. Notify list view to clear the message list.
let numRanges = this._tree.view.selection.getRangeCount();
if (this._tree.view.selection.count == 0) {
gMessageViewWindow.SnowlMessageView._collection.clear();
gMessageViewWindow.SnowlMessageView._rebuildView();
this._tree.currentSelectedIndex = -1;
// SnowlUtils.gMouseEvent = null;
return;
}
// XXX: file behavior bug - closing container with selected child selects
// container, does not remember selected child on open. Restoring original
// selection by traversing the tree for itemID is too expensive here.
this._tree.boxObject.getCellAt(aEvent.clientX, aEvent.clientY, row, col, obj);
if (obj.value == "twisty") {
// SnowlUtils.RestoreSelection(CollectionsView._tree);
return;
}
// Get selected row(s) and construct a query.
for (let i = 0; i < numRanges && !stop; i++) {
this._tree.view.selection.getRangeAt(i, rangeFirst, rangeLast);
for (let index = rangeFirst.value; index <= rangeLast.value; index++) {
uri = this._tree.view.nodeForTreeIndex(index).uri;
let query = new SnowlQuery(uri);
if (query.queryProtocol == "place:") {
// Currently, any place: protocol is a collection that returns all
// records; for any such selection, break with no constraints. There
// may be other such 'system' collections but more likely collections
// will be rows which are user defined snowl: queries. Folders TBD.
constraints = null;
stop = true;
break;
}
else {
// Construct the contraint to be passed to the collection object for
// the db query.
let constraint = { };
constraint.expression = query.queryGroupIDColumn +
" = :groupValue" + index;
constraint.parameters = { };
eval("constraint.parameters.groupValue" + index + " = " + query.queryID);
constraint.operator = "OR";
constraints.push(constraint);
// constraints.push(eval('{expression: "' + SnowlPlaces.queryGroupIDColumn +
// ' = :groupValue' + index + '", ' +
// 'parameters:{groupValue' + index + ':' +
// SnowlPlaces.queryID + '}}'));
this._log.info("onClick: constraints = " + constraints.toSource());
}
}
}
let collection = new SnowlCollection(null, name, null, constraints, null);
this._tree.currentSelectedIndex = this._tree.currentIndex;
// If multiselection, no selectedNode.
if (this._tree.selectedNode)
SnowlUtils.gListViewCollectionItemId = this._tree.selectedNode.itemId;
//this._log.info("onSelect collection Obj - " + collection.toSource());
gMessageViewWindow.SnowlMessageView.setCollection(collection);
SnowlUtils.gMouseEvent = null;
},
onCollectionsTreeMouseDown: function(aEvent) {
SnowlUtils.onTreeMouseDown(aEvent, this._tree);
},
onTreeContextPopupHidden: function() {
SnowlUtils.RestoreSelection(this._tree, SnowlUtils.gListViewCollectionItemId);
},
onSubscribe: function() {
SnowlService.gBrowserWindow.Snowl.onSubscribe();
},
onUnsubscribe: function() {
this.unsubscribe();
},
onRefresh: function() {
SnowlService.refreshAllSources();
},
refreshSource: function() {
let selectedSources = [];
// XXX: Multiselection?
let selectedSource =
this._tree.view.nodeForTreeIndex(this._tree.currentSelectedIndex);
// Create places query object from tree item uri
let query = new SnowlQuery(selectedSource.uri);
if (query.queryGroupIDColumn != "sources.id")
return;
selectedSources.push(SnowlService.sourcesByID[query.queryID]);
SnowlService.refreshAllSources(selectedSources);
},
unsubscribe: function() {
let selectedSourceNodeID = [];
let selectedSourceNodesIDs = [];
let unsubCurSel = false;
// XXX: Multiselection? since only a source type may be unsubscribed and
// the tree contains mixed types of items, this needs some thought. Single
// selection only for now.
// XXX: fix contextmenu
let selectedSource =
this._tree.view.nodeForTreeIndex(this._tree.currentSelectedIndex);
// No selection or unsubscribing current selection?
if (selectedSource.viewIndex == this._tree.currentIndex)
unsubCurSel = true;
// Create places query object from tree item uri
let query = new SnowlQuery(selectedSource.uri);
if (query.queryGroupIDColumn != "sources.id")
return;
this._log.info("unsubscribe: source - " + query.queryName + " : " + selectedSource.itemId);
selectedSourceNodeID = [selectedSource, query.queryID];
selectedSourceNodesIDs.push(selectedSourceNodeID);
// Delete loop here, if multiple selections..
for (let i = 0; i < selectedSourceNodesIDs.length; ++i) {
sourceNode = selectedSourceNodesIDs[i][0];
sourceID = selectedSourceNodesIDs[i][1];
SnowlDatastore.dbConnection.beginTransaction();
try {
// Delete messages
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM metadata " +
"WHERE messageID IN " +
"(SELECT id FROM messages WHERE sourceID = " + sourceID + ")");
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM partsText " +
"WHERE docid IN " +
"(SELECT id FROM parts WHERE messageID IN " +
"(SELECT id FROM messages WHERE sourceID = " + sourceID + "))");
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM parts " +
"WHERE messageID IN " +
"(SELECT id FROM messages WHERE sourceID = " + sourceID + ")");
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM messages " +
"WHERE sourceID = " + sourceID);
// Delete people/identities
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM people " +
"WHERE id IN " +
"(SELECT personId FROM identities WHERE sourceID = " + sourceID + ")");
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM identities " +
"WHERE sourceID = " + sourceID);
// Delete the source
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM sources " +
"WHERE id = " + sourceID);
// Finally, clean up the places tree
// Authors XXX: a more efficient way would be much better here..
let anno = SnowlPlaces.SNOWL_COLLECTIONS_GROUPEDFOLDER_ANNO + "Authors";
let pages = PlacesUtils.annotations.getPagesWithAnnotation(anno, { });
for (let i = 0; i < pages.length; ++i) {
let annoVal = PlacesUtils.annotations.getPageAnnotation(pages[i], anno);
if (annoVal == "snowl:sourceID=" + sourceID) {
let bookmarkIds = PlacesUtils.bookmarks.getBookmarkIdsForURI(pages[i], {});
for (let j=0; j < bookmarkIds.length; j++) {
PlacesUtils.bookmarks.removeItem(bookmarkIds[j]);
}
}
}
// Source
PlacesUtils.bookmarks.removeItem(sourceNode.itemId);
SnowlDatastore.dbConnection.commitTransaction();
}
catch(ex) {
SnowlDatastore.dbConnection.rollbackTransaction();
throw ex;
}
}
if (unsubCurSel) {
this._tree.currentSelectedIndex = -1;
}
SnowlUtils.gListViewDeleteMoveInsert = true;
Observers.notify("snowl:source:removed");
},
//**************************************************************************//
// Places conversion
// Create the source/authors collection from the db to convert to places.
_collections: null,
_getCollections: function() {
this._collections = [];
@ -277,135 +402,97 @@ let CollectionsView = {
}
},
// Build the list of rows in the tree. By default, all containers
// are closed, so this is the same as the list of collections, although
// in the future we might persist and restore the open state.
// Convert the list of rows in the tree to places.
_buildCollectionTree: function() {
// XXX: add in proper scrollling/row selection code
this._tree.view.selection.select(-1);
if (this.isHierarchical) {
this._rows = [collection for each (collection in this._collections)];
}
else {
this._rows = [];
for each (let collection in this._collections) {
if (collection.grouped)
for each (let group in collection.groups)
this._rows.push(group);
else
this._rows.push(collection);
for each (let collection in this._collections) {
if (collection.grouped) {
let table, value, sourceID, personID;
switch (collection.groupIDColumn) {
case "sources.id":
table = "sources";
break;
case "authors.id":
table = "identities";
break;
default:
table = null;
break;
}
for each (let group in collection.groups) {
this._log.info(table+" group.name:group.groupID - " + group.name + " : " + group.groupID);
if (table == "sources")
value = group.groupID;
else if (table == "identities") {
if (!group.groupID)
// Skip null authors
continue;
// Get the sourceID that the author belongs to
value = SnowlDatastore.selectIdentitiesSourceID(group.groupID);
}
placesID = SnowlPlaces.persistPlace(table,
group.groupID,
group.name,
null, //machineURI.spec,
null, //username,
group.iconURL,
value); // aSourceID
this._log.info("Converted to places - " + group.name);
}
}
}
this._tree.view = this;
},
onSelect: function(aEvent) {
if (this._tree.currentIndex == -1 || SnowlUtils.gRightMouseButtonDown)
return;
let collection = this._rows[this._tree.currentIndex];
SnowlUtils.gListViewCollectionIndex = this._tree.currentIndex;
gMessageViewWindow.SnowlMessageView.setCollection(collection);
},
onCollectionsTreeMouseDown: function(aEvent) {
SnowlUtils.onTreeMouseDown(aEvent, this._tree);
},
onTreeContextPopupHidden: function() {
if (!SnowlUtils.gSelectOnRtClick)
SnowlUtils.RestoreSelectionWithoutContentLoad(this._tree);
},
onSubscribe: function() {
gBrowserWindow.Snowl.onSubscribe();
},
onUnsubscribe: function() {
this.unsubscribe();
},
onRefresh: function() {
SnowlService.refreshAllSources();
},
refreshSource: function() {
let selectedSourceIDs = [];
// XXX: put in a loop for multiselected collections?
let selectedSource = this._rows[SnowlUtils.gListViewCollectionIndex];
if (!selectedSource.parent || selectedSource.parent.groupIDColumn != "sources.id")
return;
//this._log.info("refreshing selected source ID: "+selectedSource.groupID);
selectedSourceIDs.push(selectedSource.groupID);
let selectedSources = SnowlService.sources.
filter(function(source) selectedSourceIDs.indexOf(source.id) != -1);
SnowlService.refreshAllSources(selectedSources);
},
unsubscribe: function() {
let selectedSourceIDs = [];
let currentSourceID = this._rows[this._tree.currentIndex] ?
this._rows[this._tree.currentIndex].groupID : null;
let notifyID = null;
// XXX: put in a loop for multiselected collections?
let selectedSource = this._rows[SnowlUtils.gListViewCollectionIndex];
if (!selectedSource.parent || selectedSource.parent.groupIDColumn != "sources.id")
return;
this._log.info("unsubscribing source: "+selectedSource.name);
selectedSourceIDs.push(selectedSource.groupID);
// Delete loop here, if multiple selections..
for (let i = 0; i < selectedSourceIDs.length; ++i) {
sourceID = selectedSourceIDs[i];
SnowlDatastore.dbConnection.beginTransaction();
try {
// Delete messages
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM metadata " +
"WHERE messageID IN " +
"(SELECT id FROM messages WHERE sourceID = " + sourceID + ")");
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM partsText " +
"WHERE docid IN " +
"(SELECT id FROM parts WHERE messageID IN " +
"(SELECT id FROM messages WHERE sourceID = " + sourceID + "))");
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM parts " +
"WHERE messageID IN " +
"(SELECT id FROM messages WHERE sourceID = " + sourceID + ")");
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM messages " +
"WHERE sourceID = " + sourceID);
// Delete people/identities
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM people " +
"WHERE id IN " +
"(SELECT personId FROM identities WHERE sourceID = " + sourceID + ")");
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM identities " +
"WHERE sourceID = " + sourceID);
// Finally, delete the source
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM sources " +
"WHERE id = " + sourceID);
SnowlDatastore.dbConnection.commitTransaction();
}
catch(ex) {
SnowlDatastore.dbConnection.rollbackTransaction();
throw ex;
}
if (sourceID == currentSourceID)
notifyID = sourceID;
}
Observers.notify("snowl:sources:changed");
// If the current selection is unsubscribed, pass its id on to list view
Observers.notify("snowl:messages:changed", notifyID);
Observers.notify("snowl:source:removed");
}
};
/**
* A single collection list view tree row.
*
* @aNode (nsINavHistoryResultNode) collection row node
function lvCollectionNode(aNode) {
this._node = aNode;
}
lvCollectionNode.prototype = {
get uri() {
delete this._uri;
return this._uri = this._node ? _node.uri : null;
},
get itemId() {
delete this._itemId;
return this._itemId = this._node ? _node.itemId : null;
},
get viewIndex() {
delete this.viewIndex;
return this.viewIndex = this._node ? _node.viewIndex : -1;
}
};
*/
/**
* PlacesTreeView overrides here.
*
*/
PlacesTreeView.prototype._drop = PlacesTreeView.prototype.drop;
PlacesTreeView.prototype.drop = SnowlTreeViewDrop;
function SnowlTreeViewDrop(aRow, aOrientation) {
this._drop(aRow, aOrientation);
SnowlUtils.gListViewDeleteMoveInsert = true;
SnowlUtils.RestoreSelection(CollectionsView._tree,
SnowlUtils.gListViewCollectionItemId);
};
// Not using this yet..
//function PlacesController(aView) {
// this._view = aView;
//}
//PlacesController.prototype = {
/**
* The places view.
*/
// _view: null
//};
window.addEventListener("load", function() { CollectionsView.init() }, true);

Просмотреть файл

@ -48,9 +48,20 @@
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/javascript" src="chrome://snowl/content/collections.js"/>
<script type="application/x-javascript"
src="chrome://snowl/content/collections.js"/>
<vbox id="collectionsViewBox" flex="1">
<!-- Places oncommandupdater events = null for now, override list and river
overlays of placesOverlay.xul -->
<commandset id="placesCommands" events=""/>
<commandset id="editMenuCommands">
<commandset id="editMenuCommandSetAll" events=""/>
<commandset id="editMenuCommandSetUndo" events=""/>
<commandset id="editMenuCommandSetPaste" events=""/>
</commandset>
<vbox id="collectionsViewBox"
flex="1">
<!-- Collection context -->
<popup id="snowlCollectionContext"
@ -70,13 +81,32 @@
oncommand="CollectionsView.unsubscribe();"/>
</popup>
<tree id="sourcesView" flex="1" editable="true"
onselect="CollectionsView.onSelect(event)">
<!-- onclick="SidebarUtils.handleTreeClick(this, event, true);" -->
<tree id="sourcesView"
flex="1"
editable="true"
class="sidebar-placesTree"
type="places"
persist="flat"
hidecolumnpicker="true"
onkeypress="SidebarUtils.handleTreeKeyPress(event);"
onmousemove="SidebarUtils.handleTreeMouseMove(event);"
onmouseout="SidebarUtils.clearURLFromStatusBar();"
onselect="CollectionsView.onSelect(event)"
onclick="CollectionsView.onClick(event)">
<treecols>
<treecol id="nameCol" label="&nameCol.label;" primary="true" flex="1"/>
<treecol id="title"
label="&nameCol.label;"
primary="true"
flex="1"
hideheader="true"/>
</treecols>
<treechildren flex="1" context="snowlCollectionContext"
<treechildren id="sourcesViewChildren"
view="sourcesView"
class="sidebar-placesTreechildren"
flex="1"
context="snowlCollectionContext"
onmousedown="CollectionsView.onCollectionsTreeMouseDown(event)"/>
</tree>

Просмотреть файл

@ -34,9 +34,6 @@
*
* ***** END LICENSE BLOCK ***** */
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
// modules that are generic
@ -46,19 +43,9 @@ Cu.import("resource://snowl/modules/Observers.js");
Cu.import("resource://snowl/modules/service.js");
Cu.import("resource://snowl/modules/utils.js");
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 gBrowserWindow = SnowlService.gBrowserWindow;
// Defined differently in River
let gMessageViewWindow = SnowlService.gBrowserWindow;
let ListSidebar = {
@ -82,12 +69,14 @@ let ListSidebar = {
onLoad: function() {
gBrowserWindow.SnowlMessageView.show();
this._updateWriteButton();
Observers.add("snowl:sources:changed", this.onSourcesChanged, this);
Observers.add("snowl:source:added", this.onSourcesChanged, this);
Observers.add("snowl:source:removed", this.onSourcesChanged, this);
},
onUnload: function() {
gBrowserWindow.SnowlMessageView.hide();
Observers.remove("snowl:sources:changed", this.onSourcesChanged, this);
Observers.remove("snowl:source:added", this.onSourcesChanged, this);
Observers.remove("snowl:source:removed", this.onSourcesChanged, this);
},
onToggleWrite: function(event) {

Просмотреть файл

@ -36,25 +36,41 @@
-
- ***** END LICENSE BLOCK ***** -->
<!-- Places overlays need to be done here so that overrides may be done
- in any code defined in collections.xul -->
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
<?xul-overlay href="chrome://snowl/content/write.xul"?>
<!-- toolbar.dtd gives us writeButton.tooltip. -->
<!DOCTYPE page [
<!ENTITY % placesDTD SYSTEM "chrome://browser/locale/places/places.dtd">
%placesDTD;
<!ENTITY % listDTD SYSTEM "chrome://snowl/locale/list.dtd">
%listDTD;
<!ENTITY % toolbarDTD SYSTEM "chrome://snowl/locale/toolbar.dtd">
%toolbarDTD;
]>
<?xul-overlay href="chrome://snowl/content/write.xul"?>
<page id="snowlSidebar"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="&page.title;"
onload="ListSidebar.onLoad()"
onunload="ListSidebar.onUnload()">
<script type="application/javascript" src="chrome://snowl/content/list-sidebar.js"/>
<script type="application/x-javascript"
src="chrome://browser/content/bookmarks/sidebarUtils.js"/>
<script type="application/javascript"
src="chrome://snowl/content/list-sidebar.js"/>
<commandset id="placesCommands"/>
<commandset id="editMenuCommands"/>
<popup id="placesContext"/>
<!-- This is the overlay point for the collections view overlay. -->
<vbox id="collectionsViewBox"/>

Просмотреть файл

@ -114,7 +114,7 @@ let SnowlMessageView = {
// nsITreeView
get rowCount() {
this._log.info("get rowCount: " + this._collection.messages.length);
//this._log.info("get rowCount: " + this._collection.messages.length);
return this._collection.messages.length;
},
@ -189,14 +189,17 @@ this._log.info("get rowCount: " + this._collection.messages.length);
let layoutIndex = Snowl.layoutName.indexOf(layout) < 0 ?
this.kClassicLayout : Snowl.layoutName.indexOf(layout);
this.layout(layoutIndex);
// Init list with empty collection.
this._collection = new SnowlCollection();
this._tree.view = this;
},
show: function() {
Observers.add("snowl:messages:changed", this.onMessagesChanged, this);
this._collection = new SnowlCollection();
this._sort();
this._tree.view = this;
// Refresh list on each new message.
// Observers.add("snowl:message:added", this.onMessageAdded, this);
// Refresh list at end of all message downloads.
// Observers.add("snowl:messages:changed", this.onMessagesChanged, this);
this._snowlViewContainer.hidden = false;
this._snowlViewSplitter.hidden = false;
@ -211,33 +214,14 @@ this._log.info("get rowCount: " + this._collection.messages.length);
// XXX Should we somehow destroy the view here (f.e. by setting
// this._tree.view to null)?
Observers.remove("snowl:messages:changed", this.onMessagesChanged, this);
// Observers.remove("snowl:message:added", this.onMessageAdded, this);
// Observers.remove("snowl:messages:changed", this.onMessagesChanged, this);
},
//**************************************************************************//
// Event & Notification Handling
onMessagesChanged: function(sourceID) {
// Don't update the list view if the source whose messages have changed
// is not the one currently being displayed in the view.
if (this._collection.groupID && this._collection.groupID != sourceID)
return;
// FIXME: make the collection listen for message changes and invalidate
// itself, then rebuild the view in a timeout to give the collection time
// to do so.
this._collection.invalidate();
// Don't rebuild the view if the list view hasn't been made visible yet
// (in which case the tree won't yet have a view property).
// XXX problem: if some non viewed source updates, we loose our selection
// which is not good. not good even if our viewed source updates
// (additions).. need to rebuild for unsubscribe though (blank out view).
if (this._tree.view)
this._rebuildView();
},
onFilter: function() {
this._applyFilters();
},
@ -268,6 +252,7 @@ this._log.info("get rowCount: " + this._collection.messages.length);
setCollection: function(collection) {
this._collection = collection;
this._collection.invalidate();
this._rebuildView();
},
@ -283,8 +268,11 @@ this._log.info("get rowCount: " + this._collection.messages.length);
// this._tree.view = this; <- doesn't work for all DOM moves..
this._tree.boxObject.QueryInterface(Ci.nsITreeBoxObject).view = this;
this._sort();
// Scroll back to the top of the tree.
this._tree.boxObject.scrollToRow(this._tree.boxObject.getFirstVisibleRow());
// XXX: need to preserve selection.
// this._tree.boxObject.scrollToRow(this._tree.boxObject.getFirstVisibleRow());
},
switchLayout: function(layout) {
@ -380,7 +368,7 @@ this._log.info("get rowCount: " + this._collection.messages.length);
},
onSelect: function(aEvent) {
//this._log.info("onSelect - start: event.target.id = "+aEvent.target.id);
//this._log.info("onSelect - start: currentIndex = "+this._tree.currentIndex);
if (this._tree.currentIndex == -1 || SnowlUtils.gRightMouseButtonDown)
return;
@ -395,7 +383,9 @@ this._log.info("get rowCount: " + this._collection.messages.length);
let url = "chrome://snowl/content/message.xul?id=" + message.id;
window.loadURI(url, null, null, false);
SnowlUtils.gListViewListIndex = row;
// On conversion of list tree to places, this will be stored in
// currentSelectedIndex as for collections tree..
// SnowlUtils.gListViewListIndex = row;
this._setRead(true);
// If new message selected, reset for toggle
SnowlUtils.gMessagePosition.pageIndex = null;
@ -601,12 +591,11 @@ this._log.info("_toggleRead: all? " + aAll);
},
onListTreeMouseDown: function(aEvent) {
SnowlUtils.onTreeMouseDown(aEvent, this._tree);
// SnowlUtils.onTreeMouseDown(aEvent, this._tree);
},
onTreeContextPopupHidden: function(aEvent) {
if (!SnowlUtils.gSelectOnRtClick)
SnowlUtils.RestoreSelectionWithoutContentLoad(this._tree);
// SnowlUtils.RestoreSelection(this._tree);
},
};

Просмотреть файл

@ -38,20 +38,25 @@
border-bottom: 1px solid black;
}
.labelBox {
-moz-box-pack: end;
}
.label {
font-weight: bold;
}
.briefHeaderBox {
.headerBox {
-moz-box-align: baseline;
}
#briefSubject[href] .textbox-input {
cursor: pointer;
#briefHeader > columns {
-moz-box-align: baseline;
}
#fullHeader .label {
width: 4.5em;
text-align: right;
}
label.text-link {
margin-left: 0px;
}
#body {

Просмотреть файл

@ -46,14 +46,10 @@ Cu.import("resource://snowl/modules/StringBundle.js");
// modules that are Snowl-specific
Cu.import("resource://snowl/modules/constants.js");
Cu.import("resource://snowl/modules/message.js");
Cu.import("resource://snowl/modules/service.js");
Cu.import("resource://snowl/modules/utils.js");
let gBrowserWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIWebNavigation).
QueryInterface(Ci.nsIDocShellTreeItem).
rootTreeItem.
QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIDOMWindow);
let gBrowserWindow = SnowlService.gBrowserWindow;
// Parse URL parameters
let params = {};

Просмотреть файл

@ -52,36 +52,37 @@
<grid id="briefHeader">
<columns>
<column id="briefSubjectColumn" flex="1">
<hbox class="briefHeaderBox">
<hbox class="headerBox">
<label class="label"
value="&subject.label;"/>
<textbox id="briefSubject"
class="text-link plain"
readonly="true"
<label id="briefSubject"
class="text-link"
crop="end"
flex="1"/>
</hbox>
</column>
<column id="briefAuthorColumn" flex="1">
<hbox class="briefHeaderBox">
<hbox class="headerBox">
<label class="label"
value="&author.label;"/>
<textbox id="briefAuthor"
class="plain"
readonly="true"
clickSelectsAll="true"
crop="end"
flex="1"/>
flex="0"/>
</hbox>
</column>
<column id="briefTimestampColumn">
<hbox class="briefHeaderBox">
<hbox class="headerBox">
<label class="label"
value="&timestamp.label;"/>
<textbox id="briefTimestamp"
class="plain"
readonly="true"
clickSelectsAll="true"
crop="end"
flex="1"/>
flex="0"/>
</hbox>
</column>
</columns>
@ -90,38 +91,47 @@
</rows>
</grid>
<grid id="fullHeader">
<columns>
<column id="labelColumn"/>
<column id="valueColumn" flex="1"/>
</columns>
<rows>
<row id="authorRow">
<hbox class="labelBox">
<label class="label" value="&author.label;"/>
</hbox>
<label id="author" crop="end"/>
</row>
<row id="subjectRow">
<hbox class="labelBox">
<label class="label" value="&subject.label;"/>
</hbox>
<label id="subject" crop="end"/>
</row>
<row id="timestampRow">
<hbox class="labelBox">
<label class="label" value="&timestamp.label;"/>
</hbox>
<label id="timestamp" crop="end"/>
</row>
<row id="linkRow">
<hbox class="labelBox">
<label class="label" value="&link.label;"/>
</hbox>
<label id="link" class="text-link" crop="end"/>
</row>
</rows>
</grid>
<vbox id="fullHeader">
<hbox id="authorRow"
class="headerBox">
<label class="label"
value="&author.label;"/>
<textbox id="author"
flex="1"
class="plain"
readonly="true"
clickSelectsAll="true"
crop="end"/>
</hbox>
<hbox id="subjectRow"
class="headerBox">
<label class="label" value="&subject.label;"/>
<textbox id="subject"
flex="1"
class="plain"
readonly="true"
clickSelectsAll="true"
crop="end"/>
</hbox>
<hbox id="timestampRow"
class="headerBox">
<label class="label" value="&timestamp.label;"/>
<textbox id="timestamp"
flex="1"
class="plain"
readonly="true"
clickSelectsAll="true"
crop="end"/>
</hbox>
<hbox id="linkRow"
class="headerBox">
<label class="label" value="&link.label;"/>
<label id="link"
flex="1"
class="text-link"
crop="end"/>
</hbox>
</vbox>
</deck>

Просмотреть файл

@ -36,14 +36,11 @@
*
* ***** 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");
//Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const Cu = Components.utils;
// modules that are generic
Cu.import("resource://snowl/modules/log4moz.js");
@ -56,20 +53,16 @@ Cu.import("resource://snowl/modules/datastore.js");
Cu.import("resource://snowl/modules/service.js");
Cu.import("resource://snowl/modules/utils.js");
const XML_NS = "http://www.w3.org/XML/1998/namespace"
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 gBrowserWindow = SnowlService.gBrowserWindow;
// Defined differently in List View
let gMessageViewWindow = window;
let SnowlMessageView = {
get _log() {
delete this._log;
return this._log = Log4Moz.repository.getLogger("Snowl.River");
@ -869,4 +862,42 @@ let splitterDragObserver = {
document.getElementById("columnResizeSplitter").left = width;
this._timeout = window.setTimeout(this.callback, 500, width);
}
};
// From browser.js for Places sidebar
var XULBrowserWindow = {
// Stored Status, Link and Loading values
overLink: "",
statusText: "",
get statusTextField () {
delete this.statusTextField;
return this.statusTextField = gBrowserWindow.
document.getElementById("statusbar-display");
},
destroy: function () {
// XXXjag to avoid leaks :-/, see bug 60729
delete this.statusTextField;
delete this.statusText;
},
setOverLink: function (link, b) {
// Encode bidirectional formatting characters.
// (RFC 3987 sections 3.2 and 4.1 paragraph 6)
this.overLink = link.replace(/[\u200e\u200f\u202a\u202b\u202c\u202d\u202e]/g,
encodeURIComponent);
this.updateStatusField();
},
updateStatusField: function () {
var text = this.overLink;
// check the current value so we don't trigger an attribute change
// and cause needless (slow!) UI updates
if (this.statusText != text) {
this.statusTextField.label = text;
this.statusText = text;
}
}
}

Просмотреть файл

@ -36,10 +36,24 @@
-
- ***** END LICENSE BLOCK ***** -->
<!-- Places overlays need to be done here so that overrides may be done
- in any code defined in collections.xul -->
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
<?xml-stylesheet href="chrome://snowl/content/river.css" type="text/css"?>
<?xml-stylesheet href="chrome://snowl/content/riverContent.css" type="text/css"?>
<!-- We load overlays via xul-overlay processing instructions rather than
- chrome.manifest instructions because manifest instructions require us
- to specify the exact URL to overlay, and our URL changes based on
- query parameters that get set in response to user searches. -->
<?xul-overlay href="chrome://snowl/content/collections.xul"?>
<?xul-overlay href="chrome://snowl/content/write.xul"?>
<!-- toolbar.dtd gives us writeButton.tooltip -->
<!DOCTYPE page [
<!ENTITY % riverDTD SYSTEM "chrome://snowl/locale/river.dtd">
@ -50,18 +64,20 @@
%filterTextboxDTD;
]>
<!-- We load overlays via xul-overlay processing instructions rather than
- chrome.manifest instructions because manifest instructions require us
- to specify the exact URL to overlay, and our URL changes based on
- query parameters that get set in response to user searches. -->
<?xul-overlay href="chrome://snowl/content/collections.xul"?>
<?xul-overlay href="chrome://snowl/content/write.xul"?>
<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
title="&page.title;"
onload="SnowlMessageView.onLoad()">
<!-- Note: this is for implementation of Places functions in the collections
- view within a page -->
<script type="application/x-javascript"
src="chrome://browser/content/bookmarks/sidebarUtils.js"/>
<commandset id="placesCommands"/>
<commandset id="editMenuCommands"/>
<popup id="placesContext"/>
<!-- Note: the page intentionally has no onunload handler, as onunload
- would suppress the bfcache, which would cause Firefox to reload the view
- every time the user goes back to it after following a link away from it,

Просмотреть файл

@ -1,80 +0,0 @@
<?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/preferences.css" type="text/css"?>
<!DOCTYPE page [
<!ENTITY % subscribeDTD SYSTEM "chrome://snowl/locale/sources.dtd">
%subscribeDTD;
<!ENTITY % loginDTD SYSTEM "chrome://snowl/locale/login.dtd">
%loginDTD;
]>
<page title="&page.title;"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
pack="center"
align="center">
<script type="application/javascript" src="chrome://snowl/content/strands.js"/>
<script type="application/javascript" src="chrome://snowl/content/subscribe.js"/>
<vbox id="content">
<label flex="1" value="&page.title;" class="header"/>
<separator class="groove-thin"/>
<separator class="thin" orient="horizontal"/>
<tree id="sourcesView" flex="1" context="sourcesContextMenu" editable="true"
onselect="CollectionsView.onSelect(event)">
<treecols>
<treecol id="nameCol" label="&nameCol.label;" primary="true" flex="1"/>
</treecols>
<treechildren flex="1"/>
</tree>
<separator class="thin" orient="horizontal"/>
<hbox>
<spacer flex="1"/>
<button label="&closeButton.label;" oncommand="window.close()"/>
</hbox>
</vbox>
</page>

Просмотреть файл

@ -56,20 +56,6 @@ Cu.import("resource://snowl/modules/utils.js");
Cu.import("resource://snowl/modules/twitter.js");
Cu.import("resource://snowl/modules/service.js");
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;
@ -116,7 +102,7 @@ let SnowlMessageView = {
onLoad: function() {
Observers.add("snowl:message:added", this.onMessageAdded, this);
Observers.add("snowl:sources:changed", this.onSourcesChanged, this);
Observers.add("snowl:source:added", this.onSourcesChanged, this);
Observers.add("snowl:source:removed", this.onSourceRemoved, this);
this.onResize();
@ -161,7 +147,7 @@ let SnowlMessageView = {
onunLoad: function() {
Observers.remove("snowl:message:added", this.onMessageAdded, this);
Observers.remove("snowl:sources:changed", this.onSourcesChanged, this);
Observers.remove("snowl:source:added", this.onSourcesChanged, this);
Observers.remove("snowl:source:removed", this.onSourceRemoved, this);
},

Просмотреть файл

@ -49,7 +49,7 @@ let SubscriptionListener = {
if (subject != source)
return;
let code, message;
let code, message, errorMsg;
// If blank, fine
let identity = source.name;
let stringBundle = document.getElementById("snowlStringBundle");
@ -79,6 +79,11 @@ let SubscriptionListener = {
if (data == 401)
message = stringBundle.getString("messagePassword");
}
else if (data.split(":", 1)[0] == "error") {
code = "error";
errorMsg = data.split("error:")[1];
message = stringBundle.getFormattedString("messageGenericError", [errorMsg]);
}
else {
// Under most circumstances, this message will be replaced immediately
// by the "getting messages" message.

Просмотреть файл

@ -90,7 +90,8 @@ let WriteForm = {
// Event & Notification Handlers
onLoad: function() {
Observers.add("snowl:sources:changed", this.onSourcesChanged, this);
Observers.add("snowl:source:added", this.onSourcesChanged, this);
Observers.add("snowl:source:removed", this.onSourcesChanged, this);
this._rebuildTargetsMenu();
this._updateFormState();
},

Просмотреть файл

@ -4,9 +4,6 @@ pref("extensions.snowl@mozilla.org.description", "chrome://snowl/locale/about.pr
// Header view pref: 0 = none, 1 = brief, 2 = full
pref("extensions.snowl.message.headerView", 1);
// Collections hierarchical view pref: 0 = flat, 1 = hierarchical
pref("extensions.snowl.collection.hierarchicalView", 0);
pref("extensions.snowl.log.logger.root.level", "All");
pref("extensions.snowl.log.appender.console.level", "Warn");
pref("extensions.snowl.log.appender.dump.level", "Debug");

Просмотреть файл

@ -68,10 +68,10 @@
<!ENTITY toolbar.accesskey "d">
<!ENTITY viewtoolbar.label "List Toolbar">
<!ENTITY viewtoolbar.accesskey "i">
<!ENTITY hierarchyOff.label "Flat Collections">
<!ENTITY hierarchyOff.accesskey "a">
<!ENTITY hierarchyOn.label "Hierarchical Collections">
<!ENTITY hierarchyOn.accesskey "H">
<!ENTITY groupedOff.label "Flat Collections">
<!ENTITY groupedOff.accesskey "a">
<!ENTITY groupedOn.label "Grouped Collections">
<!ENTITY groupedOn.accesskey "G">
<!-- These labels and access keys are for toolbar buttons -->

Просмотреть файл

@ -1,6 +1,6 @@
# These are the default collections that Snowl displays in the collections pane.
# They get inserted into the database when it is first created after the user
# installs Snowl.
allCollectionName = All
sourcesCollectionName = Sources
authorsCollectionName = Authors
allCollectionName = All Messages
sourcesCollectionName = All Sources
authorsCollectionName = All Authors

Просмотреть файл

@ -8,6 +8,7 @@ messagePassword = Your credentials were not accepted. Please check your
messageConnected = Connected.
messageGettingMessages = Getting messages...
messageSuccess = You have successfully subscribed to this message source.
messageGenericError = There was an error completing the subscription to this message source. Error: %1$S.
title = Snowl Preferences
titleWindows = Snowl Options

Просмотреть файл

@ -137,7 +137,7 @@ SnowlCollection.prototype = {
parameters: { groupValue: statement.row.name } });
let group = new SnowlCollection(null, name, iconURL, constraints, this);
//this._log.info("got group name: " + group.name);
this._log.info("got group name: " + group.name);
if (this.groupIDColumn)
group.groupID = statement.row.groupID;
@ -237,7 +237,8 @@ this._log.info("got " + groups.length + " groups");
sortProperties: null,
sortOrder: 1,
_messages: null,
// No messages loaded initially, invalidate and rebuild on setCollection().
_messages: [],
get messages() {
if (this._messages)
@ -265,6 +266,7 @@ this._log.info("got " + groups.length + " groups");
sourceID: statement.row.sourceID,
subject: statement.row.subject,
author: statement.row.author,
authorID: statement.row.authorID,
link: statement.row.link,
timestamp: SnowlDateUtils.julianToJSDate(statement.row.timestamp),
_read: (statement.row.read ? true : false),
@ -290,10 +292,16 @@ this._log.info("got " + groups.length + " groups");
this._messages = null;
},
clear: function() {
this._messages = [];
this._messageIndex = {};
},
_generateStatement: function() {
let columns = [
"messages.id AS messageID",
"messages.sourceID",
"messages.authorID",
"messages.subject",
"messages.link",
"messages.timestamp",
@ -325,16 +333,28 @@ this._log.info("got " + groups.length + " groups");
// all messages whether or not they have a content part.
"AND parts.partType = " + PART_TYPE_CONTENT;
let conditions = [];
let conditions = [], operator;
for each (let condition in this.constraints)
for each (let condition in this.constraints) {
operator = condition.operator ? condition.operator : "AND";
if (conditions.length == 0)
conditions.push(" WHERE");
else
conditions.push(operator);
conditions.push(condition.expression);
}
for each (let condition in this.filters)
for each (let condition in this.filters) {
operator = condition.operator ? condition.operator : "AND";
if (conditions.length == 0)
conditions.push(" WHERE");
else
conditions.push(operator);
conditions.push(condition.expression);
}
if (conditions.length > 0)
query += " WHERE " + conditions.join(" AND ");
query += conditions.join(" ");
if (this.order)
query += " ORDER BY " + this.order;

Просмотреть файл

@ -34,7 +34,7 @@
*
* ***** END LICENSE BLOCK ***** */
let EXPORTED_SYMBOLS = ["SnowlDatastore"];
let EXPORTED_SYMBOLS = ["SnowlDatastore", "SnowlPlaces", "SnowlQuery"];
const Cc = Components.classes;
const Ci = Components.interfaces;
@ -42,11 +42,13 @@ const Cr = Components.results;
const Cu = Components.utils;
// modules that come with Firefox
Cu.import("resource://gre/modules/utils.js"); // Places
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
// modules that are generic
Cu.import("resource://snowl/modules/log4moz.js");
Cu.import("resource://snowl/modules/StringBundle.js");
Cu.import("resource://snowl/modules/URI.js");
// modules that are Snowl-specific
Cu.import("resource://snowl/modules/constants.js");
@ -875,17 +877,308 @@ let SnowlDatastore = {
this._insertMetadatumStatement.params.messageID = aMessageID;
this._insertMetadatumStatement.params.attributeID = aAttributeID;
try {
this._insertMetadatumStatement.params.value = aValue;
}
catch(ex) {
//dump(ex + " with attribute ID: " + aAttributeID + " and value: " + aValue + "\n");
throw ex;
}
try {
this._insertMetadatumStatement.params.value = aValue;
}
catch(ex) {
//dump(ex + " with attribute ID: " + aAttributeID + " and value: " + aValue + "\n");
throw ex;
}
this._insertMetadatumStatement.execute();
return this.dbConnection.lastInsertRowID;
},
get _selectIdentitiesSourceIDStatement() {
let statement = this.createStatement(
"SELECT sourceID FROM identities WHERE personID = :id"
);
this.__defineGetter__("_selectIdentitiesSourceIDStatement",
function() { return statement });
return this._selectIdentitiesSourceIDStatement;
},
/**
* Get sourceID for a people table entry from identities table.
*
* @param aID {integer} the record ID of the people entry, which should be
* tested against the peopleID value in identities
*
* @returns {integer} the sourceID of the people record
*/
selectIdentitiesSourceID: function(aID) {
let sourceID;
try {
this._selectIdentitiesSourceIDStatement.params.id = aID;
if (this._selectIdentitiesSourceIDStatement.step()) {
sourceID = this._selectIdentitiesSourceIDStatement.row["sourceID"];
}
}
finally {
this._selectIdentitiesSourceIDStatement.reset();
}
return sourceID;
}
};
/**
* Parsed query uri associated with a Places collection row.
*
* @param aUri (string) - query string contained in the places item's uri.
*/
function SnowlQuery(aUri) {
this.queryUri = decodeURI(aUri);
if (this.queryUri) {
if (this.queryUri.indexOf("place:") != -1)
this.queryProtocol = "place:";
else if (this.queryUri.indexOf("snowl:") != -1) {
this.queryProtocol = "snowl:";
this.queryID = this.queryUri.split(".id=")[1].split("&")[0];
this.queryName = this.queryUri.split("name=")[1].split("&")[0];
this.queryGroupIDColumn = this.queryUri.split("snowl:")[1].split("=")[0];
}
}
}
SnowlQuery.prototype = {
queryUri: null,
queryProtocol: null,
queryID: null,
queryName: null,
queryGroupIDColumn: null,
};
/**
* Places functions for Snowl
*/
let SnowlPlaces = {
get _log() {
let logger = Log4Moz.repository.getLogger("Snowl.SnowlPlaces");
this.__defineGetter__("_log", function() logger);
return this._log;
},
SNOWL_ROOT_ANNO: "Snowl",
SNOWL_COLLECTIONS_FLAT_ANNO: "Snowl/CollectionsFlat",
SNOWL_COLLECTIONS_GROUPED_ANNO: "Snowl/CollectionsGrouped",
SNOWL_COLLECTIONS_GROUPEDFOLDER_ANNO: "Snowl/CollectionsGrouped/Folder/",
// SMART_BOOKMARKS_ANNO: "Places/SmartBookmark",
ORGANIZER_QUERY_ANNO: "PlacesOrganizer/OrganizerQuery",
snowlRootID: null,
collectionsFlatID: null,
collectionsGroupedID: null,
collectionsGroupedFolderID: null,
convertedToPlaces: null,
get queryFlat() {
delete this._queryFlat;
return this._queryFlat = "place:queryType=1&expandQueries=0&folder=" +
this.collectionsFlatID;
},
get queryGrouped() {
delete this._queryGrouped;
return this._queryGrouped = "place:queryType=1&expandQueries=1&folder=" +
this.collectionsGroupedID;
},
persistPlace: function(aTable, aId, aName, aMachineURI, aUsername, aIconURI, aSourceId) {
let uri, iconUri, annoType;
if (aTable == "sources") {
uri = URI("snowl:sources.id=" + aId +
"&name=" + aName +
// "&machineURI=" + aMachineURI +
// "&username=" + aUsername +
// "&groupIDColumn=sources.id" +
"&");
annoType = "Sources";
}
else if (aTable == "identities") {
uri = URI("snowl:authors.id=" + aId +
"&name=" + aName +
// "&externalID=" + aUsername +
// "&sourceID=" + aSourceId +
// "&groupIDColumn=authors.id" +
"&");
annoType = "Authors";
}
else
return null;
let placesID = PlacesUtils.bookmarks.
insertBookmark(SnowlPlaces.collectionsFlatID,
uri,
PlacesUtils.bookmarks.DEFAULT_INDEX,
aName);
let anno = SnowlPlaces.SNOWL_COLLECTIONS_GROUPEDFOLDER_ANNO + annoType;
PlacesUtils.annotations.
setPageAnnotation(uri,
anno,
"snowl:sourceID=" + aSourceId,
0,
PlacesUtils.annotations.EXPIRE_NEVER);
//this._log.info(annoType + " iconURI.spec - " + (aIconURI ? aIconURI.spec : "null"));
PlacesUtils.favicons.
// setFaviconUrlForPage(uri,
setAndLoadFaviconForPage(uri,
aIconURI,
false);
//this._log.info(aType + " name:placesID - " + aName + " : " + id);
return placesID;
},
// Check for our places structure and create if not found
init: function() {
let itemID, items;
items = PlacesUtils.annotations.
getItemsWithAnnotation(this.SNOWL_ROOT_ANNO, {});
if (items.length != 0 && items[0] != -1) {
// Have our root..
this.snowlRootID = items[0];
// Get flat collection root
items = PlacesUtils.annotations.
getItemsWithAnnotation(this.SNOWL_COLLECTIONS_FLAT_ANNO, {});
this.collectionsFlatID = items[0];
// Get grouped collection root
items = PlacesUtils.annotations.
getItemsWithAnnotation(this.SNOWL_COLLECTIONS_GROUPED_ANNO, {});
this.collectionsGroupedID = items[0];
// Get grouped folder root
items = PlacesUtils.annotations.
getItemsWithAnnotation(this.SNOWL_COLLECTIONS_GROUPEDFOLDER_ANNO, {});
this.collectionsGroupedFolderID = items[0];
this.convertedToPlaces = true;
}
else {
// Create places stucture
itemID = PlacesUtils.bookmarks.
createFolder(PlacesUtils.placesRootId,
"snowlRoot",
-1);
// Ensure immediate children can't be removed
PlacesUtils.bookmarks.setFolderReadonly(itemID, true);
// Create annotation
PlacesUtils.annotations.
setItemAnnotation(itemID,
this.SNOWL_ROOT_ANNO,
"snowl:root",
0,
PlacesUtils.annotations.EXPIRE_NEVER);
this.snowlRootID = itemID;
// Create flat collections root
itemID = PlacesUtils.bookmarks.
createFolder(this.snowlRootID,
"snowlCollectionsFlat",
PlacesUtils.bookmarks.DEFAULT_INDEX);
PlacesUtils.annotations.
setItemAnnotation(itemID,
this.SNOWL_COLLECTIONS_FLAT_ANNO,
"snowl:collectionsFlat",
0,
PlacesUtils.annotations.EXPIRE_NEVER);
this.collectionsFlatID = itemID;
// Create grouped collections root
itemID = PlacesUtils.bookmarks.
createFolder(this.snowlRootID,
"snowlCollectionsGrouped",
PlacesUtils.bookmarks.DEFAULT_INDEX);
PlacesUtils.annotations.
setItemAnnotation(itemID,
this.SNOWL_COLLECTIONS_GROUPED_ANNO,
"snowl:collectionsGrouped",
0,
PlacesUtils.annotations.EXPIRE_NEVER);
this.collectionsGroupedID = itemID;
// Ensure immediate child can't be removed
PlacesUtils.bookmarks.setFolderReadonly(itemID, true);
// Create grouped collections folder
itemID = PlacesUtils.bookmarks.
createFolder(this.collectionsGroupedID,
"All Messages",
PlacesUtils.bookmarks.DEFAULT_INDEX);
PlacesUtils.annotations.
setItemAnnotation(itemID,
this.SNOWL_COLLECTIONS_GROUPEDFOLDER_ANNO,
"snowl:collectionsGroupedFolder",
0,
PlacesUtils.annotations.EXPIRE_NEVER);
this.collectionsGroupedFolderID = itemID;
// Default collections
let collections = [];
// All
coll = {queryId: "snowl:AllMessages",
itemId: null,
title: strings.get("allCollectionName"),
uri: URI("place:folder=" + this.collectionsFlatID +
"&OR" +
"&expandQueries=0" +
"&annotation=" +
this.SNOWL_COLLECTIONS_FLAT_ANNO),
parent: this.collectionsFlatID,
position: PlacesUtils.bookmarks.DEFAULT_INDEX};
collections.push(coll);
// Sources
coll = {queryId: "snowl:AllSources",
itemId: null,
title: strings.get("sourcesCollectionName"),
uri: URI("place:folder=" + this.collectionsGroupedFolderID +
"&OR" +
"&annotation=" +
this.SNOWL_COLLECTIONS_GROUPEDFOLDER_ANNO + "Sources" +
"&expandQueries=1" +
"&queryType=" +
Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS +
"&sort=" +
Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING),
parent: this.collectionsGroupedFolderID,
position: PlacesUtils.bookmarks.DEFAULT_INDEX};
collections.push(coll);
// Authors
coll = {queryId: "snowl:AllAuthors",
itemId: null,
title: strings.get("authorsCollectionName"),
uri: URI("place:folder=" + this.collectionsGroupedFolderID +
"&OR" +
"&annotation=" +
this.SNOWL_COLLECTIONS_GROUPEDFOLDER_ANNO + "Authors" +
"&expandQueries=1" +
"&queryType=" +
Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS +
"&sort=" +
Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING),
parent: this.collectionsGroupedFolderID,
position: PlacesUtils.bookmarks.DEFAULT_INDEX};
collections.push(coll);
// Add the collections
for each(let coll in collections) {
coll.itemId = PlacesUtils.bookmarks.insertBookmark(coll.parent,
coll.uri,
coll.position,
coll.title);
PlacesUtils.annotations.
// setPageAnnotation(coll.itemId,
setItemAnnotation(coll.itemId,
this.ORGANIZER_QUERY_ANNO,
coll.queryId,
0,
PlacesUtils.annotations.EXPIRE_NEVER);
};
this.convertedToPlaces = false;
}
}
};
// FIXME: don't wrap statements in this wrapper for stable releases.

Просмотреть файл

@ -329,21 +329,20 @@ SnowlFeed.prototype = {
},
_processRefresh: strand(function(aResult, refreshTime) {
// FIXME: figure out why aResult.doc is sometimes null (its content isn't
// a valid feed?) and report a more descriptive error message.
if (aResult.doc == null) {
this._log.error("_processRefresh: aResult.doc is null");
// Observers.notify("snowl:subscribe:get:end", this);
return;
}
// FIXME: Make this be "snowl:refresh:start" or move it into the subscribing
// caller so it makes sense that it's called "snowl:subscribe:get:start",
// since this method also gets called during periodically on feeds to which
// the user is already subscribed.
Observers.notify("snowl:subscribe:get:start", this);
// FIXME: figure out why aResult.doc is sometimes null (its content isn't
// a valid feed?) and report a more descriptive error message.
// FIXME: don't notify get:start and get:end if aResult.doc == null.
if (aResult.doc == null) {
this._log.error("_processRefresh: aResult.doc is null");
Observers.notify("snowl:subscribe:get:end", this);
return;
}
let feed = aResult.doc.QueryInterface(Components.interfaces.nsIFeed);
let currentMessageIDs = [];
@ -409,6 +408,8 @@ SnowlFeed.prototype = {
" THEN 1 ELSE 0 END) WHERE sourceID = " + this.id
);
// Notify list and collections views on completion of messages download, list
// also notified of each message addition.
if (messagesChanged)
Observers.notify("snowl:messages:changed", this.id);
@ -676,8 +677,9 @@ SnowlFeed.prototype = {
},
onSubscribeResult: strand(function(aResult) {
let feed;
try {
let feed = aResult.doc.QueryInterface(Components.interfaces.nsIFeed);
feed = aResult.doc.QueryInterface(Components.interfaces.nsIFeed);
// Extract the name (if we don't already have one) and human URI from the feed.
if (!this.name)
@ -686,7 +688,7 @@ SnowlFeed.prototype = {
this.persist();
Observers.notify("snowl:sources:changed");
// Observers.notify("snowl:sources:changed");
// Refresh the feed to import all its items.
// FIXME: use a date provided by the subscriber so refresh times are the same
@ -694,7 +696,9 @@ SnowlFeed.prototype = {
yield this._processRefresh(aResult, new Date());
}
catch(ex) {
this._log.error("error on subscribe result: " + feed.toSource());
this._log.error("error on subscribe result: " + ex);
Observers.notify("snowl:subscribe:connect:end", this, "error:" + ex);
}
finally {
if (this._subscribeCallback)

Просмотреть файл

@ -84,6 +84,10 @@ SnowlIdentity.create = function(sourceID, externalID, name, homeURL, iconURL) {
"INSERT INTO identities (sourceID, externalID, personID) " +
"VALUES (:sourceID, :externalID, :personID)"
);
// let identityStatement = SnowlDatastore.createStatement(
// "INSERT INTO identities (sourceID, externalID, personID, placesID) " +
// "VALUES (:sourceID, :externalID, :personID, :placesID)"
// );
try {
personStatement.params.name = name;
@ -92,9 +96,26 @@ SnowlIdentity.create = function(sourceID, externalID, name, homeURL, iconURL) {
personStatement.step();
let personID = SnowlDatastore.dbConnection.lastInsertRowID;
// XXX lookup favicon in collections table rather than hardcoding
let iconURI =
iconURL ? URI.get(iconURL) :
homeURL ? SnowlSource.faviconSvc.getFaviconForPage(homeURL) :
URI.get("chrome://snowl/skin/person-16.png");
// Create places record, placesID stored into people table record.
//SnowlPlaces._log.info("Author name:iconURI.spec - " + name + " : " + iconURI.spec);
let placesID = SnowlPlaces.persistPlace("identities",
personID,
name,
null, // homeURL,
null, // externalID,
iconURI,
sourceID);
identityStatement.params.sourceID = sourceID;
identityStatement.params.externalID = externalID;
identityStatement.params.personID = personID;
// identityStatement.params.placesID = placesID;
identityStatement.step();
let identityID = SnowlDatastore.dbConnection.lastInsertRowID;

Просмотреть файл

@ -67,7 +67,7 @@ SnowlMessage.get = function(id) {
let statement = SnowlDatastore.createStatement(
"SELECT sourceID, subject, authors.name AS author, link, timestamp, read, " +
" authors.iconURL AS authorIcon, received " +
" authors.iconURL AS authorIcon, received, authorID " +
"FROM messages LEFT JOIN people AS authors ON messages.authorID = authors.id " +
"WHERE messages.id = :id"
);
@ -80,6 +80,7 @@ SnowlMessage.get = function(id) {
sourceID: statement.row.sourceID,
subject: statement.row.subject,
author: statement.row.author,
authorID: statement.row.authorID,
link: statement.row.link,
timestamp: SnowlDateUtils.julianToJSDate(statement.row.timestamp),
_read: (statement.row.read ? true : false),

Просмотреть файл

@ -67,6 +67,13 @@ const SNOWL_HANDLER_TITLE = "Snowl";
const REFRESH_CHECK_INTERVAL = 60 * 1000; // 60 seconds
let SnowlService = {
get gBrowserWindow() {
let wm = Cc["@mozilla.org/appshell/window-mediator;1"].
getService(Ci.nsIWindowMediator);
delete this._gBrowserWindow;
return this._gBrowserWindow = wm.getMostRecentWindow("navigator:browser");
},
get _prefs() {
delete this._prefs;
return this._prefs = new Preferences("extensions.snowl.");
@ -102,7 +109,8 @@ let SnowlService = {
this._registerFeedHandler();
this._initTimer();
Observers.add("snowl:sources:changed", this.onSourcesChanged, this);
Observers.add("snowl:source:added", this.onSourcesChanged, this);
Observers.add("snowl:source:removed", this.onSourcesChanged, this);
// FIXME: refresh stale sources on startup in a way that doesn't hang
// the UI thread.

Просмотреть файл

@ -42,6 +42,7 @@ const Cr = Components.results;
const Cu = Components.utils;
// modules that are generic
Cu.import("resource://snowl/modules/Observers.js");
Cu.import("resource://snowl/modules/URI.js");
// modules that are Snowl-specific
@ -205,18 +206,33 @@ let SnowlSource = {
return this.faviconSvc;
},
// XXX: If a favicon is not in cache, getFaviconForPage throws, but we do
// not want to try getFaviconImageForPage as that returns a default moz image.
// Perhaps overkill to try to get a data uri for the favicon via additional
// favicon methods. So we will try the former, and use the below for first
// time visits for sources we have so far, til this can be fixed properly.
get faviconURI() {
if (this.humanURI) {
try {
// If the page has been visited and the icon is in cache
return this.faviconSvc.getFaviconForPage(this.humanURI);
}
catch(ex) { /* no known favicon */ }
catch(ex) {
// Try to get the image, returns moz default if not found
// return this.faviconSvc.getFaviconImageForPage(this.humanURI);
// return this.faviconSvc.getFaviconLinkForIcon(this.humanURI);
}
}
// The default favicon for feed sources.
// FIXME: once we support other types of sources, override this
// with a type-specific icon.
//return URI.get("chrome://snowl/skin/livemarkFolder-16.png");
// FIXME: get icon from collections table instead of hardcoding
if (this.constructor.name == "SnowlFeed")
return URI.get("chrome://snowl/skin/livemarkFolder-16.png");
// The default favicon for twitter.
// FIXME: get icon from collections table instead of hardcoding
if (this.constructor.name == "SnowlTwitter")
return URI.get("http://static.twitter.com/images/favicon.ico");
return null;
},
@ -239,12 +255,14 @@ let SnowlSource = {
/**
* Insert a record for this source into the database, or update an existing
* record.
* record; store placesID back into sources table.
*
* FIXME: move this to a SnowlAccount interface.
* XXX need to make this one commitable transaction (with place db store)
* to maintain strict integrity..
*/
persist: function() {
let statement;
let statement, placesID;
if (this.id) {
statement = SnowlDatastore.createStatement(
"UPDATE sources " +
@ -263,6 +281,7 @@ let SnowlSource = {
);
}
SnowlDatastore.dbConnection.beginTransaction();
try {
statement.params.name = this.name;
statement.params.type = this.constructor.name;
@ -272,14 +291,39 @@ let SnowlSource = {
if (this.id)
statement.params.id = this.id;
statement.step();
if (!this.id) {
// Extract the ID of the source from the newly-created database record.
this.id = SnowlDatastore.dbConnection.lastInsertRowID;
// Create places record
placesID = SnowlPlaces.persistPlace("sources",
this.id,
this.name,
null, // this.machineURI.spec,
null, // this.username,
this.faviconURI,
this.id); // aSourceID
// Store placedID back into messages for db integrity
// XXX uncomment once field is added..
// SnowlDatastore.dbConnection.executeSimpleSQL(
// "UPDATE sources " +
// "SET placesID = " + placesID +
// "WHERE id = " + this.id);
SnowlUtils.gListViewCollectionItemId = placesID;
this._log.info("persist newItemId - " + SnowlUtils.gListViewCollectionItemId);
// Use 'added' here for collections observer for more specificity
Observers.notify("snowl:source:added");
}
SnowlDatastore.dbConnection.commitTransaction();
}
catch(ex) {
SnowlDatastore.dbConnection.rollbackTransaction();
throw ex;
}
finally {
statement.reset();
}
// Extract the ID of the source from the newly-created database record.
if (!this.id)
this.id = SnowlDatastore.dbConnection.lastInsertRowID;
},
get _stmtGetInternalIDForExternalID() {

Просмотреть файл

@ -379,7 +379,7 @@ SnowlTwitter.prototype = {
// Save the source to the database.
this.persist();
Observers.notify("snowl:sources:changed");
// Observers.notify("snowl:sources:changed");
// FIXME: use a date provided by the subscriber so refresh times are the same
// for all accounts subscribed at the same time (f.e. in an OPML import).
@ -555,7 +555,7 @@ SnowlTwitter.prototype = {
this.username = this._authInfo.username;
this.name = NAME + " - " + this._authInfo.username;
this.persist();
Observers.notify("snowl:sources:changed");
// Observers.notify("snowl:sources:changed");
}
this._saveLogin(this._authInfo);
@ -628,6 +628,8 @@ SnowlTwitter.prototype = {
" THEN 1 ELSE 0 END) WHERE sourceID = " + this.id
);
// Notify list and collections views on completion of messages download, list
// also notified of each message addition.
if (messagesChanged)
Observers.notify("snowl:messages:changed", this.id);

Просмотреть файл

@ -307,111 +307,137 @@ let SnowlUtils = {
return this._log;
},
// Always maintain selected listitem within a session
//**************************************************************************//
// Utilities to track tree selections within a session
// XXX store on document for restore on restart??
gListViewListIndex: -1,
gListViewCollectionIndex: -1,
gListViewDeleteMoveInsert: false,
// Current collections tree itemId
// FIXME: figure out where to store this (make array too) across sidebar loads.
gListViewCollectionItemId: null,
// Position of current page in tabs and history
gMessagePosition: {tabIndex: null, pageIndex: null},
// From Tb: Detect right mouse click and change the highlight to the row
// where the click happened without loading the message headers in
// the Folder or Thread Pane.
gRightMouseButtonDown: false,
gSelectOnRtClick: false,
// Track mouse and right mouse click for tree row onSelect, contextmenu, and
// dnd handling without running a query resulting in content load.
gRightMouseButtonDown: null,
gMouseEvent: null,
onTreeMouseDown: function(aEvent, tree) {
if (aEvent.button == 2 && !this.gSelectOnRtClick) {
this.gMouseEvent = true;
if (aEvent.button == 2)
this.gRightMouseButtonDown = true;
this.ChangeSelectionWithoutContentLoad(aEvent, aEvent.target.parentNode);
}
else {
// Add a capturing click listener to the tree so we can find out if the user
// clicked on a row that is already selected (in which case we let them edit
// the collection name).
// FIXME: disable this for names that can't be changed.
// this._tree.addEventListener("mousedown", function(aEvent) {
// CollectionsView.onClick(aEvent) }, true);
let row = {}, col = {}, child = {};
tree.treeBoxObject.getCellAt(aEvent.clientX, aEvent.clientY, row, col, child);
if (tree.view.selection.isSelected(row.value))
this._log.info("row: "+ row.value + " is selected");
else {
this._log.info("row: "+ row.value + " is not selected");
}
this.gRightMouseButtonDown = false;
}
this.ChangeSelectionWithoutContentLoad(aEvent, aEvent.target.parentNode);
},
// From Tb: Function to change the highlighted row to where the mouse was
// clicked without loading the contents of the selected row.
// It will also keep the outline/dotted line in the original row.
// Change the highlighted tree row to where the mouse was clicked (right
// button for contextmenu or left button for mousedown dnd) without loading
// the contents of the selected row. The original row is indicated by the
// dotted border (row at currentIndex). Current active selected row (via
// right or left click) is stored in new tree property currentSelectedIndex.
ChangeSelectionWithoutContentLoad: function(aEvent, tree) {
//this._log.info("change selection right click: tree.id = "+tree.id);
//this._log.info("ChangeSelection");
let treeBoxObj = tree.treeBoxObject;
let treeSelection = treeBoxObj.view.selection;
let modKey = aEvent.metaKey || aEvent.ctrlKey || aEvent.shiftKey;
let row = { }, col = { }, obj = { };
let row = treeBoxObj.getRowAt(aEvent.clientX, aEvent.clientY);
treeBoxObj.getCellAt(aEvent.clientX, aEvent.clientY, row, col, obj);
// Make sure that row.value is valid so that it doesn't mess up
// the call to ensureRowIsVisible().
if((row >= 0) && !treeSelection.isSelected(row)) {
// Not for twisty click or multiselection
if (obj.value == "twisty" || modKey)
return;
//this._log.info("ChangeSelection: currentSelIndex = "+tree.currentSelectedIndex);
//this._log.info("ChangeSelection: currentIndex = "+treeSelection.currentIndex);
// Make sure that row.value is valid for the call to ensureRowIsVisible().
if((row.value >= 0) && !treeSelection.isSelected(row.value)) {
let saveCurrentIndex = treeSelection.currentIndex;
treeSelection.selectEventsSuppressed = true;
treeSelection.select(row);
treeSelection.select(row.value);
treeSelection.currentIndex = saveCurrentIndex;
treeBoxObj.ensureRowIsVisible(row);
treeBoxObj.ensureRowIsVisible(row.value);
treeSelection.selectEventsSuppressed = false;
// Keep track of which row in the tree is currently selected.
if(tree.id == "snowlView")
this.gListViewListIndex = row;
if(tree.id == "sourcesView")
this.gListViewCollectionIndex = row;
// Keep track of which row in the tree is currently selected via rt click,
// onClick handler will update currentSelectedIndex for left click.
if (this.gRightMouseButtonDown)
tree.currentSelectedIndex = row.value;
//this._log.info("ChangeSelection: currentSelIndex = "+tree.currentSelectedIndex);
//this._log.info("ChangeSelection: currentIndex = "+treeSelection.currentIndex);
}
// This will not stop the onSelect event, need to test in the handler..
// aEvent.stopPropagation();
aEvent.stopPropagation();
},
// From Tb: Function to change the highlighted row back to the row that
// is currently outline/dotted without loading the contents of either rows.
// This is triggered when the context menu for a given row is hidden/closed
// (onpopuphidden for the context <popup>).
RestoreSelectionWithoutContentLoad: function(tree) {
// All purpose function to make sure the right row is selected. Restore the
// original row currently indicated by dotted border without loading its query,
// unless rows have been deleted/moved/inserted. This is triggered when the
// context menu for the row is hidden/closed (onpopuphidden event) or mouseup
// for dnd. Also called from onSourceAdded for insertions.
RestoreSelection: function(tree, itemId) {
//this._log.info("RestoreSelection");
let treeSelection = tree.view.selection;
// Make sure that currentIndex is valid so that we don't try to restore
// a selection of an invalid row.
if((!treeSelection.isSelected(treeSelection.currentIndex)) &&
(treeSelection.currentIndex >= 0)) {
treeSelection.selectEventsSuppressed = true;
treeSelection.select(treeSelection.currentIndex);
treeSelection.selectEventsSuppressed = false;
// Reset mouse state to enable key navigation.
this.gMouseEvent = null;
this.gRightMouseButtonDown = null;
//this._log.info("RestoreSelection: START currentSelIndex = "+tree.currentSelectedIndex);
//this._log.info("RestoreSelection: START currentIndex = "+treeSelection.currentIndex);
// Reset which row in the tree is currently selected.
if(tree.id == "snowlView")
this.gListViewListIndex = treeSelection.currentIndex;
if(tree.id == "sourcesView")
this.gListViewCollectionIndex = treeSelection.currentIndex;
// If tree rows removed, need to get new index of originally selected row,
// unless original row is removed, then deselect.
if (this.gListViewDeleteMoveInsert) {
//this._log.info("RestoreSelection DelMoveIns itemId - " + itemId);
// If selectItems gets no such itemId, the row was removed, currentIndex = -1
tree.selectItems([itemId]);
// If the itemId is selected, now need to make it the current selection for
// the onselect event to run the query. Make sure row shows.
// XXX don't run db query 1st time on default next (if none) selection row.
if (tree.currentIndex != -1) {
tree.currentSelectedIndex = tree.currentIndex;
tree.boxObject.ensureRowIsVisible(tree.currentIndex);
}
this.gListViewDeleteMoveInsert = false;
}
else if(treeSelection.currentIndex < 0)
// Clear the selection in the case of when a folder has just been
// loaded where the message pane does not have a message loaded yet.
// When right-clicking a message in this case and dismissing the
// popup menu (by either executing a menu command or clicking
// somewhere else), the selection needs to be cleared.
// However, if the 'Delete Message' or 'Move To' menu item has been
// selected, DO NOT clear the selection, else it will prevent the
// tree view from refreshing.
treeSelection.clearSelection();
// Need to reset gRightMouseButtonDown to false here because
// TreeOnMouseDown() is only called on a mousedown, not on a key down.
// So resetting it here allows the loading of messages in the messagepane
// when navigating via the keyboard or the toolbar buttons *after*
// the context menu has been dismissed.
this.gRightMouseButtonDown = false;
else {
tree.currentSelectedIndex = treeSelection.currentIndex;
// Make sure that currentIndex is valid so that we don't try to restore
// a selection of an invalid row.
if((!treeSelection.isSelected(treeSelection.currentIndex)) &&
(treeSelection.currentIndex >= 0)) {
treeSelection.selectEventsSuppressed = true;
treeSelection.select(treeSelection.currentIndex);
treeSelection.selectEventsSuppressed = false;
}
else if(treeSelection.currentIndex < 0) {
// Clear the selection and border outline index.
treeSelection.clearSelection();
tree.currentSelectedIndex = -1;
}
}
//this._log.info("RestoreSelection: END currentSelIndex = "+tree.currentSelectedIndex);
//this._log.info("RestoreSelection: END currentIndex = "+treeSelection.currentIndex);
},
// Scroll tree to proper position.
scrollPlacement: function(aTree, aRowIndex) {
if (aTree.view.rowCount <= aTree.boxObject.getPageLength() ||
(aRowIndex >= aTree.boxObject.getFirstVisibleRow() &&
aRowIndex <= aTree.boxObject.getLastVisibleRow()) ||
aRowIndex == -1)
return;
let excessRows = aTree.view.rowCount - aTree.view.selection.currentIndex;
if (excessRows > aTree.boxObject.getPageLength())
aTree.boxObject.scrollToRow(aRowIndex);
else
aTree.boxObject.scrollByPages(1);
},
// FIXME: put the following function into a generic SnowlMessageView
// pure virtual class (i.e. an object rather than a function with a prototype)
// from which SnowlMessageView instances inherit this functionality.