commit 2c3eb205ab35239dcd23e041dafd21bb5800cc9b
parent 9686758c7d9a70fc94e3a215ea82430a4d74b580
Author: Aurimas Vinckevicius <aurimas.dev@gmail.com>
Date: Sun, 14 Dec 2014 23:07:37 -0600
Implement read/unread functionality in feeds
Diffstat:
10 files changed, 204 insertions(+), 98 deletions(-)
diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js
@@ -50,6 +50,7 @@ Zotero.CollectionTreeView = function()
'publications',
'share',
'group',
+ 'feedItem',
'trash',
'bucket'
],
@@ -225,10 +226,10 @@ Zotero.CollectionTreeView.prototype.refresh = Zotero.Promise.coroutine(function*
}, 0),
added++
);
- for (let i = 0, len = groups.length; i < len; i++) {
+ for (let feed of feeds) {
this._addRowToArray(
newRows,
- new Zotero.CollectionTreeRow('feed', feeds[i]),
+ new Zotero.CollectionTreeRow('feed', feed),
added++
);
}
@@ -251,10 +252,10 @@ Zotero.CollectionTreeView.prototype.refresh = Zotero.Promise.coroutine(function*
}, 0),
added++
);
- for (let i = 0, len = groups.length; i < len; i++) {
+ for (let group of groups) {
this._addRowToArray(
newRows,
- new Zotero.CollectionTreeRow('group', groups[i]),
+ new Zotero.CollectionTreeRow('group', group),
added++
);
}
@@ -314,6 +315,13 @@ Zotero.CollectionTreeView.prototype.selectWait = Zotero.Promise.method(function
* Called by Zotero.Notifier on any changes to collections in the data layer
*/
Zotero.CollectionTreeView.prototype.notify = Zotero.Promise.coroutine(function* (action, type, ids, extraData) {
+ if (type == 'feed' && action == 'unreadCountUpdated') {
+ for (let i=0; i<ids.length; i++) {
+ this._treebox.invalidateRow(this._rowMap['L' + ids[i]]);
+ }
+ return;
+ }
+
if ((!ids || ids.length == 0) && action != 'refresh' && action != 'redraw') {
return;
}
@@ -2143,6 +2151,8 @@ Zotero.CollectionTreeView.prototype.getCellProperties = function(row, col, prop)
}
else if (treeRow.isPublications()) {
props.push("notwisty");
+ } else if (treeRow.ref && treeRow.ref.unreadCount) {
+ props.push('unread');
}
return props.join(" ");
diff --git a/chrome/content/zotero/xpcom/data/feed.js b/chrome/content/zotero/xpcom/data/feed.js
@@ -178,6 +178,7 @@ Zotero.Feed.prototype._set = function (prop, val) {
Zotero.Feed.prototype._loadDataFromRow = function(row) {
Zotero.Feed._super.prototype._loadDataFromRow.call(this, row);
+ this._feedName = row._feedName;
this._feedUrl = row._feedUrl;
this._feedLastCheckError = row._feedLastCheckError || null;
this._feedLastCheck = row._feedLastCheck || null;
@@ -274,8 +275,8 @@ Zotero.Feed.prototype._finalizeErase = Zotero.Promise.method(function(env) {
Zotero.Feed.prototype.getExpiredFeedItemIDs = Zotero.Promise.coroutine(function* () {
let sql = "SELECT itemID AS id FROM feedItems "
- + "WHERE readTimestamp IS NOT NULL "
- + "AND (julianday(readTimestamp, 'utc') + (?) - julianday('now', 'utc')) > 0";
+ + "WHERE readTime IS NOT NULL "
+ + "AND (julianday(readTime, 'utc') + (?) - julianday('now', 'utc')) > 0";
let expiredIDs = yield Zotero.DB.queryAsync(sql, [{int: this.cleanupAfter}]);
return expiredIDs.map(row => row.id);
});
@@ -289,7 +290,7 @@ Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () {
Zotero.debug("Cleaning up read feed items...");
if (expiredItems.length) {
Zotero.debug(expiredItems.join(', '));
- yield Zotero.FeedItems.erase(expiredItems);
+ yield Zotero.FeedItems.eraseTx(expiredItems);
} else {
Zotero.debug("No expired feed items");
}
@@ -301,7 +302,7 @@ Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () {
try {
let fr = new Zotero.FeedReader(this.url);
- let itemIterator = fr.createItemIterator();
+ let itemIterator = fr.itemIterator;
let item, toAdd = [], processedGUIDs = [];
while (item = yield itemIterator.next().value) {
if (item.dateModified && this.lastUpdate
@@ -320,14 +321,14 @@ Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () {
}
processedGUIDs.push(item.guid);
- Zotero.debug("New feed item retrieved:");
- Zotero.debug(item);
+ Zotero.debug("New feed item retrieved:", 5);
+ Zotero.debug(item, 5);
let feedItem = yield Zotero.FeedItems.getAsyncByGUID(item.guid);
if (!feedItem) {
feedItem = new Zotero.FeedItem();
feedItem.guid = item.guid;
- feedItem.setCollections([this.id]);
+ feedItem.libraryID = this.id;
} else {
Zotero.debug("Feed item " + item.guid + " already in library.");
if (item.dateModified && feedItem.dateModified
@@ -364,7 +365,7 @@ Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () {
this.lastCheck = Zotero.Date.dateToSQL(new Date(), true);
this.lastCheckError = errorMessage || null;
- yield this.save({skipEditCheck: true});
+ yield this.saveTx({skipEditCheck: true});
});
Zotero.Feed.prototype.updateFeed = function() {
@@ -380,3 +381,15 @@ Zotero.Feed.prototype.erase = Zotero.Promise.coroutine(function* () {
yield Zotero.FeedItems.erase(childItemIDs);
return Zotero.Feed._super.prototype.erase.call(this); // Don't tell it to delete child items. They're already gone
})
+
+Zotero.Feed.prototype.updateUnreadCount = Zotero.Promise.coroutine(function* () {
+ let sql = "SELECT " + this._ObjectsClass._primaryDataSQLParts.feedUnreadCount
+ + this._ObjectsClass.primaryDataSQLFrom
+ + " AND O.libraryID=?";
+ let newCount = yield Zotero.DB.valueQueryAsync(sql, [this.id]);
+
+ if (newCount != this._feedUnreadCount) {
+ this._feedUnreadCount = newCount;
+ Zotero.Notifier.trigger('unreadCountUpdated', 'feed', this.id);
+ }
+});
diff --git a/chrome/content/zotero/xpcom/data/feedItem.js b/chrome/content/zotero/xpcom/data/feedItem.js
@@ -132,6 +132,51 @@ Zotero.FeedItem.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
yield Zotero.DB.queryAsync(sql, [env.id, this.guid, this._feedItemReadTime]);
this._clearChanged('feedItemData');
+
+/* let itemID;
+ if (env.isNew) {
+ // For new items, run this first so we get an item ID
+ yield Zotero.FeedItem._super.prototype._saveData.apply(this, arguments);
+ itemID = env.id;
+ } else {
+ itemID = this.id;
+ }
+
+ }
+
+ if (!env.isNew) {
+ if (this.hasChanged()) {
+ yield Zotero.FeedItem._super.prototype._saveData.apply(this, arguments);
+ } else {
+ env.skipPrimaryDataReload = true;
+ }
+ Zotero.Notifier.trigger('modify', 'feedItem', itemID);
+ } else {
+ Zotero.Notifier.trigger('add', 'feedItem', itemID);
+ }
+
+ if (env.collectionsAdded || env.collectionsRemoved) {
+ let affectedCollections = (env.collectionsAdded || [])
+ .concat(env.collectionsRemoved || []);
+ if (affectedCollections.length) {
+ let feeds = yield Zotero.Feeds.getAsync(affectedCollections);
+ for (let i=0; i<feeds.length; i++) {
+ feeds[i].updateUnreadCount();
+ }
+ }*/
+ }
+});
+
+Zotero.FeedItem.prototype.toggleRead = Zotero.Promise.coroutine(function* (state) {
+ state = state !== undefined ? !!state : !this.isRead;
+ let changed = this.isRead != state;
+ this.isRead = state;
+ if (changed) {
+ yield this.save({skipEditCheck: true, skipDateModifiedUpdate: true});
+
+ yield this.loadCollections();
+ let feed = Zotero.Feeds.get(this.libraryID);
+ feed.updateUnreadCount();
}
});
diff --git a/chrome/content/zotero/xpcom/data/feedItems.js b/chrome/content/zotero/xpcom/data/feedItems.js
@@ -94,6 +94,19 @@ Zotero.FeedItems = new Proxy(function() {
return this.getAsync(id);
});
+ this.toggleReadById = Zotero.Promise.coroutine(function* (ids, state) {
+ if (!Array.isArray(ids)) {
+ if (typeof ids != 'string') throw new Error('ids must be a string or array in Zotero.FeedItems.toggleReadById');
+
+ ids = [ids];
+ }
+
+ let items = yield this.getAsync(ids);
+ for (let i=0; i<items.length; i++) {
+ items[i].toggleRead(state);
+ }
+ });
+
return this;
}.call({}),
diff --git a/chrome/content/zotero/xpcom/data/feeds.js b/chrome/content/zotero/xpcom/data/feeds.js
@@ -23,7 +23,7 @@
***** END LICENSE BLOCK *****
*/
-// Add some feed methods, but otherwise proxy to Zotero.Collections
+// Mimics Zotero.Libraries
Zotero.Feeds = new function() {
this._cache = null;
@@ -105,12 +105,15 @@ Zotero.Feeds = new function() {
.map(id => Zotero.Libraries.get(id));
}
+ this.get = Zotero.Libraries.get;
+
this.haveFeeds = function() {
if (!this._cache) throw new Error("Zotero.Feeds cache is not initialized");
return !!Object.keys(this._cache.urlByLibraryID).length
}
+ let globalFeedCheckDelay = Zotero.Promise.resolve();
this.scheduleNextFeedCheck = Zotero.Promise.coroutine(function* () {
Zotero.debug("Scheduling next feed update.");
let sql = "SELECT ( CASE "
@@ -146,4 +149,24 @@ Zotero.Feeds = new function() {
Zotero.debug("No feeds with auto-update.");
}
});
+
+ this.updateFeeds = Zotero.Promise.coroutine(function* () {
+ let sql = "SELECT libraryID AS id FROM feeds "
+ + "WHERE refreshInterval IS NOT NULL "
+ + "AND ( lastCheck IS NULL "
+ + "OR (julianday(lastCheck, 'utc') + (refreshInterval/1440) - julianday('now', 'utc')) <= 0 )";
+ let needUpdate = yield Zotero.DB.queryAsync(sql).map(row => row.id);
+ Zotero.debug("Running update for feeds: " + needUpdate.join(', '));
+ let feeds = Zotero.Libraries.get(needUpdate);
+ let updatePromises = [];
+ for (let i=0; i<feeds.length; i++) {
+ updatePromises.push(feeds[i]._updateFeed());
+ }
+
+ return Zotero.Promise.settle(updatePromises)
+ .then(() => {
+ Zotero.debug("All feed updates done.");
+ this.scheduleNextFeedCheck()
+ });
+ });
}
diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js
@@ -1660,6 +1660,9 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
let toAdd = Zotero.Utilities.arrayDiff(newCollections, oldCollections);
let toRemove = Zotero.Utilities.arrayDiff(oldCollections, newCollections);
+
+ env.collectionsAdded = toAdd;
+ env.collectionsRemoved = toRemove;
if (toAdd.length) {
for (let i=0; i<toAdd.length; i++) {
diff --git a/chrome/content/zotero/xpcom/itemTreeView.js b/chrome/content/zotero/xpcom/itemTreeView.js
@@ -53,9 +53,9 @@ Zotero.ItemTreeView = function (collectionTreeRow, sourcesOnly) {
this._refreshPromise = Zotero.Promise.resolve();
- this._unregisterID = Zotero.Notifier.registerObserver(
+ this._unregisterID = Zotero.Notifier.registerObserver(
this,
- ['item', 'collection-item', 'item-tag', 'share-items', 'bucket'],
+ ['item', 'collection-item', 'item-tag', 'share-items', 'bucket', 'feedItem'],
'itemTreeView',
50
);
@@ -391,6 +391,14 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
return;
}
+ // FeedItem may have changed read/unread state
+ if (type == 'feedItem' && action == 'modify') {
+ for (let i=0; i<ids.length; i++) {
+ this._treebox.invalidateRow(this._itemRowMap[ids[i]]);
+ }
+ return;
+ }
+
// Clear item type icon and tag colors when a tag is added to or removed from an item
if (type == 'item-tag') {
// TODO: Only update if colored tag changed?
@@ -526,12 +534,9 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
// Since a remove involves shifting of rows, we have to do it in order,
// so sort the ids by row
var rows = [];
+ let push = action == 'delete' || action == 'trash';
for (var i=0, len=ids.length; i<len; i++) {
- let push = false;
- if (action == 'delete' || action == 'trash') {
- push = true;
- }
- else {
+ if (!push) {
push = !collectionTreeRow.ref.hasItem(ids[i]);
}
// Row might already be gone (e.g. if this is a child and
@@ -567,7 +572,7 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
}
}
}
- else if (action == 'modify')
+ else if (type == 'item' && action == 'modify')
{
// Clear row caches
var items = yield Zotero.Items.getAsync(ids);
@@ -685,7 +690,7 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
}
}
}
- else if(action == 'add')
+ else if(type == 'item' && action == 'add')
{
let items = yield Zotero.Items.getAsync(ids);
@@ -3054,56 +3059,25 @@ Zotero.ItemTreeView.prototype.getCellProperties = function(row, col, prop) {
// Mark items not matching search as context rows, displayed in gray
if (this._searchMode && !this._searchItemIDs[itemID]) {
- // <=Fx21
- if (prop) {
- var aServ = Components.classes["@mozilla.org/atom-service;1"].
- getService(Components.interfaces.nsIAtomService);
- prop.AppendElement(aServ.getAtom("contextRow"));
- }
- // Fx22+
- else {
- props.push("contextRow");
- }
+ props.push("contextRow");
}
// Mark hasAttachment column, which needs special image handling
if (col.id == 'zotero-items-column-hasAttachment') {
- // <=Fx21
- if (prop) {
- var aServ = Components.classes["@mozilla.org/atom-service;1"].
- getService(Components.interfaces.nsIAtomService);
- prop.AppendElement(aServ.getAtom("hasAttachment"));
- }
- // Fx22+
- else {
- props.push("hasAttachment");
- }
+ props.push("hasAttachment");
// Don't show pie for open parent items, since we show it for the
// child item
- if (this.isContainer(row) && this.isContainerOpen(row)) {
- return props.join(" ");
- }
-
- var num = Zotero.Sync.Storage.getItemDownloadImageNumber(treeRow.ref);
- //var num = Math.round(new Date().getTime() % 10000 / 10000 * 64);
- if (num !== false) {
- // <=Fx21
- if (prop) {
- if (!aServ) {
- var aServ = Components.classes["@mozilla.org/atom-service;1"].
- getService(Components.interfaces.nsIAtomService);
- }
- prop.AppendElement(aServ.getAtom("pie"));
- prop.AppendElement(aServ.getAtom("pie" + num));
- }
- // Fx22+
- else {
- props.push("pie", "pie" + num);
- }
+ if (!this.isContainer(row) || !this.isContainerOpen(row)) {
+ var num = Zotero.Sync.Storage.getItemDownloadImageNumber(treeRow.ref);
+ //var num = Math.round(new Date().getTime() % 10000 / 10000 * 64);
+ if (num !== false) props.push("pie", "pie" + num);
}
}
+ // Style unread items in feeds
+ if (treeRow.ref.isFeedItem && !treeRow.ref.isRead) props.push('unread');
+
return props.join(" ");
}
diff --git a/chrome/content/zotero/xpcom/notifier.js b/chrome/content/zotero/xpcom/notifier.js
@@ -95,8 +95,9 @@ Zotero.Notifier = new function(){
* Possible values:
*
* event: 'add', 'modify', 'delete', 'move' ('c', for changing parent),
- * 'remove' (ci, it), 'refresh', 'redraw', 'trash'
- * type - 'collection', 'search', 'item', 'collection-item', 'item-tag', 'tag', 'group', 'relation'
+ * 'remove' (ci, it), 'refresh', 'redraw', 'trash', 'unreadCountUpdated'
+ * type - 'collection', 'search', 'item', 'collection-item', 'item-tag', 'tag',
+ * 'group', 'relation', 'feed', 'feedItem'
* ids - single id or array of ids
*
* Notes:
diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js
@@ -501,9 +501,9 @@ var ZoteroPane = new function()
}
}
-
- function handleKeyUp(event, from) {
- if (from == 'zotero-pane') {
+ function handleKeyUp(event) {
+ var from = event.originalTarget.id;
+ if (from == 'zotero-items-tree') {
if ((Zotero.isWin && event.keyCode == 17) ||
(!Zotero.isWin && event.keyCode == 18)) {
if (this.highlightTimer) {
@@ -511,6 +511,33 @@ var ZoteroPane = new function()
this.highlightTimer = null;
}
ZoteroPane_Local.collectionsView.setHighlightedRows();
+ return;
+ } else if (event.keyCode == event.DOM_VK_BACK_QUOTE) {
+ // Toggle read/unread
+ let row = this.collectionsView.getRow(this.collectionsView.selection.currentIndex);
+ if (!row || !row.isFeed()) return;
+ if(itemReadTimeout) {
+ itemReadTimeout.cancel();
+ itemReadTimeout = null;
+ }
+
+ let itemIDs = this.getSelectedItems(true);
+ Zotero.FeedItems.getAsync(itemIDs)
+ .then(function(feedItems) {
+ // Determine what most items are set to;
+ let allUnread = true;
+ for (let item of feedItems) {
+ if (item.isRead) {
+ allUnread = false;
+ break;
+ }
+ }
+
+ // If something is unread, toggle all read by default
+ for (let i=0; i<feedItems.length; i++) {
+ feedItems[i].toggleRead(!allUnread);
+ }
+ });
}
}
}
@@ -594,21 +621,6 @@ var ZoteroPane = new function()
//event.preventDefault();
//event.stopPropagation();
return;
- } else if (event.keyCode == event.DOM_VK_BACK_QUOTE) {
- // Toggle read/unread
- if (!this.collectionsView.selection.currentIndex) return;
- let row = this.collectionsView.getRow(this.collectionsView.selection.currentIndex);
- if (!row || !row.isFeed()) return;
-
- if(itemReadTimeout) {
- itemReadTimeout.cancel();
- itemReadTimeout = null;
- }
-
- let itemIDs = this.getSelectedItems(true);
- for (var i=0; i<itemIDs; i++) {
- this.markItemRead(itemIDs[i]);
- }
}
}
@@ -1377,15 +1389,11 @@ var ZoteroPane = new function()
tabs.selectedIndex = document.getElementById('zotero-view-item').selectedIndex;
}
- if (collectionTreeRow.isFeed()) {
- // Fire timer for read item
- let feedItem = yield Zotero.FeedItems.getAsync(item.id);
- if (feedItem) {
- this.startItemReadTimeout(feedItem.id);
+ if (item.isFeedItem) {
+ this.startItemReadTimeout(item.id);
}
}
}
- }
// Zero or multiple items selected
else {
var count = this.itemsView.selection.count;
@@ -4290,14 +4298,6 @@ var ZoteroPane = new function()
});
- this.markItemRead = Zotero.Promise.coroutine(function* (feedItemID, toggle) {
- let feedItem = yield Zotero.FeedItems.getAsync(feedItemID);
- if (!feedItem) return;
-
- feedItem.isRead = toggle !== undefined ? !!toggle : !feedItem.isRead;
- yield feedItem.save({skipEditCheck: true, skipDateModifiedUpdate: true});
- })
-
let itemReadTimeout;
this.startItemReadTimeout = function(feedItemID) {
if (itemReadTimeout) {
@@ -4305,8 +4305,18 @@ var ZoteroPane = new function()
itemReadTimeout = null;
}
- itemReadTimeout = Zotero.Promise.delay(3000)
+ let feedItem;
+ itemReadTimeout = Zotero.FeedItems.getAsync(feedItemID)
.cancellable()
+ .then(function(newFeedItem) {
+ if (!newFeedItem) {
+ throw new Zotero.Promise.CancellationError('Not a FeedItem');
+ } else if(newFeedItem.isRead) {
+ throw new Zotero.Promise.CancellationError('FeedItem already read.');
+ }
+ feedItem = newFeedItem;
+ })
+ .delay(3000)
.then(() => {
itemReadTimeout = null;
// Check to make sure we're still on the same item
@@ -4315,7 +4325,15 @@ var ZoteroPane = new function()
let row = this.itemsView.getRow(this.itemsView.selection.currentIndex);
if (!row || !row.ref || !row.ref.id == feedItemID) return;
- return this.markItemRead(feedItemID, true);
+ return feedItem.toggleRead(true);
+ })
+ .catch(function(e) {
+ if (e instanceof Zotero.Promise.CancellationError) {
+ Zotero.debug(e.message);
+ return;
+ }
+
+ Zotero.debug(e, 1);
});
}
diff --git a/chrome/skin/default/zotero/overlay.css b/chrome/skin/default/zotero/overlay.css
@@ -215,6 +215,12 @@
color: inherit;
}
+/* Style unread items/collections in bold */
+#zotero-items-tree treechildren::-moz-tree-cell-text(unread),
+#zotero-collections-tree treechildren::-moz-tree-cell-text(unread) {
+ font-weight: bold;
+}
+
#zotero-items-pane
{
min-width: 290px;