зеркало из https://github.com/mozilla/snowl.git
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:
Родитель
a9956004de
Коммит
a45b5b26c2
|
@ -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="×tamp.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="×tamp.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="×tamp.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;
|
||||
|
@ -181,7 +181,7 @@ this._log.info("got " + groups.length + " groups");
|
|||
|
||||
// FIXME: allow group queries to make people the primary table.
|
||||
|
||||
let query =
|
||||
let query =
|
||||
"SELECT " + columns.join(", ") + " " +
|
||||
"FROM sources LEFT JOIN messages ON sources.id = messages.sourceID " +
|
||||
"LEFT JOIN people AS authors ON messages.authorID = authors.id";
|
||||
|
@ -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);
|
||||
|
||||
|
|
180
modules/utils.js
180
modules/utils.js
|
@ -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.
|
||||
|
|
Загрузка…
Ссылка в новой задаче