www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

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:
Mchrome/content/zotero/xpcom/collectionTreeView.js | 18++++++++++++++----
Mchrome/content/zotero/xpcom/data/feed.js | 29+++++++++++++++++++++--------
Mchrome/content/zotero/xpcom/data/feedItem.js | 45+++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/data/feedItems.js | 13+++++++++++++
Mchrome/content/zotero/xpcom/data/feeds.js | 25++++++++++++++++++++++++-
Mchrome/content/zotero/xpcom/data/item.js | 3+++
Mchrome/content/zotero/xpcom/itemTreeView.js | 72+++++++++++++++++++++++-------------------------------------------------
Mchrome/content/zotero/xpcom/notifier.js | 5+++--
Mchrome/content/zotero/zoteroPane.js | 86++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mchrome/skin/default/zotero/overlay.css | 6++++++
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;