From 2d46e3d59b5efa81ff582723a4530f5c556cb565 Mon Sep 17 00:00:00 2001 From: Aurimas Vinckevicius Date: Tue, 3 Feb 2015 11:57:32 -0600 Subject: [PATCH] Various feeds changes --- .../content/zotero/xpcom/collectionTreeRow.js | 4 +- .../zotero/xpcom/collectionTreeView.js | 13 ++- chrome/content/zotero/xpcom/data/feed.js | 108 ++++++++++++------ chrome/content/zotero/xpcom/data/feedItem.js | 33 ------ chrome/content/zotero/xpcom/data/feedItems.js | 17 ++- chrome/content/zotero/xpcom/data/feeds.js | 6 +- chrome/content/zotero/xpcom/data/items.js | 8 +- chrome/content/zotero/xpcom/feedReader.js | 48 ++++---- chrome/content/zotero/xpcom/uri.js | 26 ++++- chrome/content/zotero/zoteroPane.js | 86 ++++++-------- .../default/zotero/treesource-feed-error.png | Bin 0 -> 3463 bytes .../zotero/treesource-feed-updating.png | Bin 0 -> 3541 bytes 12 files changed, 189 insertions(+), 160 deletions(-) create mode 100644 chrome/skin/default/zotero/treesource-feed-error.png create mode 100644 chrome/skin/default/zotero/treesource-feed-updating.png diff --git a/chrome/content/zotero/xpcom/collectionTreeRow.js b/chrome/content/zotero/xpcom/collectionTreeRow.js index 80b192851..9dbed7bd6 100644 --- a/chrome/content/zotero/xpcom/collectionTreeRow.js +++ b/chrome/content/zotero/xpcom/collectionTreeRow.js @@ -149,14 +149,14 @@ Zotero.CollectionTreeRow.prototype.isWithinEditableGroup = function () { } Zotero.CollectionTreeRow.prototype.__defineGetter__('editable', function () { - if (this.isTrash() || this.isShare() || this.isBucket() || this.isFeed()) { + if (this.isTrash() || this.isShare() || this.isBucket()) { return false; } if (!this.isWithinGroup() || this.isPublications()) { return true; } var libraryID = this.ref.libraryID; - if (this.isGroup()) { + if (this.isGroup() || this.isFeed()) { return this.ref.editable; } if (this.isCollection() || this.isSearch() || this.isDuplicates() || this.isUnfiled()) { diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js index 8ab70b8aa..ce9d06d48 100644 --- a/chrome/content/zotero/xpcom/collectionTreeView.js +++ b/chrome/content/zotero/xpcom/collectionTreeView.js @@ -48,6 +48,7 @@ Zotero.CollectionTreeView = function() 'collection', 'search', 'publications', + 'feed', 'share', 'group', 'feedItem', @@ -315,7 +316,7 @@ 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') { + if (type == 'feed' && (action == 'unreadCountUpdated' || action == 'statusChanged')) { for (let i=0; i "F." + c + " AS " + Zotero.Feed._colToProp(c)).join(", ") - + ", (SELECT COUNT(*) FROM items I JOIN feedItems FeI USING (itemID)" - + " WHERE I.libraryID=F.libraryID AND FeI.readTime IS NULL) AS feedUnreadCount" + + ", " + Zotero.Feed._unreadCountSQL }); Zotero.defineProperty(Zotero.Feed, '_rowSQL', { @@ -89,6 +99,17 @@ Zotero.defineProperty(Zotero.Feed.prototype, 'isFeed', { Zotero.defineProperty(Zotero.Feed.prototype, 'libraryTypes', { value: Object.freeze(Zotero.Feed._super.prototype.libraryTypes.concat(['feed'])) }); +Zotero.defineProperty(Zotero.Feed.prototype, 'unreadCount', { + get: function() this._feedUnreadCount +}); +Zotero.defineProperty(Zotero.Feed.prototype, 'updating', { + get: function() this._updating, + set: function(v) { + if (!v == !this._updating) return; // Unchanged + this._updating = !!v; + Zotero.Notifier.trigger('statusChanged', 'feed', this.id); + } +}); (function() { // Create accessors @@ -185,7 +206,7 @@ Zotero.Feed.prototype._loadDataFromRow = function(row) { this._feedLastUpdate = row._feedLastUpdate || null; this._feedCleanupAfter = parseInt(row._feedCleanupAfter) || null; this._feedRefreshInterval = parseInt(row._feedRefreshInterval) || null; - this._feedUnreadCount = parseInt(row.feedUnreadCount); + this._feedUnreadCount = parseInt(row._feedUnreadCount); } Zotero.Feed.prototype._reloadFromDB = Zotero.Promise.coroutine(function* () { @@ -218,7 +239,7 @@ Zotero.Feed.prototype._initSave = Zotero.Promise.coroutine(function* (env) { Zotero.Feed.prototype._saveData = Zotero.Promise.coroutine(function* (env) { yield Zotero.Feed._super.prototype._saveData.apply(this, arguments); - Zotero.debug("Saving feed data for collection " + this.id); + Zotero.debug("Saving feed data for library " + this.id); let changedCols = [], params = []; for (let i=0; i 0"; - let expiredIDs = yield Zotero.DB.queryAsync(sql, [{int: this.cleanupAfter}]); + let expiredIDs = yield Zotero.DB.queryAsync(sql, [this.id, {int: this.cleanupAfter}]); return expiredIDs.map(row => row.id); }); -Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () { - let errorMessage = ''; +Zotero.Feed.prototype.clearExpiredItems = Zotero.Promise.coroutine(function* () { try { // Clear expired items if (this.cleanupAfter) { @@ -299,10 +316,16 @@ Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () { Zotero.debug("Error clearing expired feed items."); Zotero.debug(e); } +}); + +Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () { + this.updating = true; + this.lastCheckError = null; + yield this.clearExpiredItems(); try { let fr = new Zotero.FeedReader(this.url); - let itemIterator = fr.itemIterator; + let itemIterator = new fr.ItemIterator(); let item, toAdd = [], processedGUIDs = []; while (item = yield itemIterator.next().value) { if (item.dateModified && this.lastUpdate @@ -331,6 +354,7 @@ Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () { feedItem.libraryID = this.id; } else { Zotero.debug("Feed item " + item.guid + " already in library."); + if (item.dateModified && feedItem.dateModified && feedItem.dateModified == item.dateModified ) { @@ -353,19 +377,23 @@ Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () { // Save in reverse order let savePromises = new Array(toAdd.length); for (let i=toAdd.length-1; i>=0; i--) { - yield toAdd[i].save({skipEditCheck: true, setDateModified: true}); + // Saving currently has to happen sequentially so as not to violate the + // unique constraints in dataValues (FIXME) + yield toAdd[i].save({skipEditCheck: true}); } this.lastUpdate = Zotero.Date.dateToSQL(new Date(), true); - } catch(e) { - Zotero.debug("Error processing feed from " + this.url); - Zotero.debug(e); - errorMessage = e.message || 'Error processing feed'; } - + catch (e) { + if (e.message) { + Zotero.debug("Error processing feed from " + this.url); + Zotero.debug(e); + } + this.lastCheckError = e.message || 'Error processing feed'; + } this.lastCheck = Zotero.Date.dateToSQL(new Date(), true); - this.lastCheckError = errorMessage || null; yield this.saveTx({skipEditCheck: true}); + this.updating = false; }); Zotero.Feed.prototype.updateFeed = function() { @@ -375,17 +403,27 @@ Zotero.Feed.prototype.updateFeed = function() { }); } -Zotero.Feed.prototype.erase = Zotero.Promise.coroutine(function* () { - yield this.loadChildItems(); - let childItemIDs = this.getChildItems(true, true); +Zotero.Feed.prototype._finalizeErase = Zotero.Promise.coroutine(function* (){ + let notifierData = {}; + notifierData[this.libraryID] = { + libraryID: this.libraryID + }; + Zotero.Notifier.trigger('delete', 'feed', this.id, notifierData); + Zotero.Feeds.unregister(this.libraryID); + return Zotero.Feed._super.prototype._finalizeErase.call(this); +}); + +Zotero.Feed.prototype.erase = Zotero.Promise.coroutine(function* (deleteItems) { + let childItemIDs = yield Zotero.FeedItems.getAll(this.id, false, false, true); 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 -}) + + yield Zotero.Feed._super.prototype.erase.call(this); +}); Zotero.Feed.prototype.updateUnreadCount = Zotero.Promise.coroutine(function* () { - let sql = "SELECT " + this._ObjectsClass._primaryDataSQLParts.feedUnreadCount - + this._ObjectsClass.primaryDataSQLFrom - + " AND O.libraryID=?"; + let sql = "SELECT " + Zotero.Feed._unreadCountSQL + + " FROM feeds F JOIN libraries L USING (libraryID)" + + " WHERE L.libraryID=?"; let newCount = yield Zotero.DB.valueQueryAsync(sql, [this.id]); if (newCount != this._feedUnreadCount) { diff --git a/chrome/content/zotero/xpcom/data/feedItem.js b/chrome/content/zotero/xpcom/data/feedItem.js index f50de3260..d68f50aa7 100644 --- a/chrome/content/zotero/xpcom/data/feedItem.js +++ b/chrome/content/zotero/xpcom/data/feedItem.js @@ -132,38 +132,6 @@ 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 Zotero.Libraries.get(id)); } - this.get = Zotero.Libraries.get; + this.get = Zotero.Libraries.get.bind(Zotero.Libraries); this.haveFeeds = function() { if (!this._cache) throw new Error("Zotero.Feeds cache is not initialized"); @@ -154,8 +154,8 @@ Zotero.Feeds = new 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); + + "OR (julianday(lastCheck, 'utc') + (refreshInterval/1440.0) - 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 = []; diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js index 5b4548e7b..638c3aa78 100644 --- a/chrome/content/zotero/xpcom/data/items.js +++ b/chrome/content/zotero/xpcom/data/items.js @@ -133,9 +133,10 @@ Zotero.Items = function() { * @param {Integer} libraryID * @param {Boolean} [onlyTopLevel=false] If true, don't include child items * @param {Boolean} [includeDeleted=false] If true, include deleted items - * @return {Promise>} + * @param {Boolean} [onlyIDs=false] If true, resolves only with IDs + * @return {Promise>} */ - this.getAll = Zotero.Promise.coroutine(function* (libraryID, onlyTopLevel, includeDeleted) { + this.getAll = Zotero.Promise.coroutine(function* (libraryID, onlyTopLevel, includeDeleted, onlyIDs=false) { var sql = 'SELECT A.itemID FROM items A'; if (onlyTopLevel) { sql += ' LEFT JOIN itemNotes B USING (itemID) ' @@ -150,6 +151,9 @@ Zotero.Items = function() { } sql += " AND libraryID=?"; var ids = yield Zotero.DB.columnQueryAsync(sql, libraryID); + if (onlyIDs) { + return ids; + } return this.getAsync(ids); }); diff --git a/chrome/content/zotero/xpcom/feedReader.js b/chrome/content/zotero/xpcom/feedReader.js index c6cf624f1..d0a1b0903 100644 --- a/chrome/content/zotero/xpcom/feedReader.js +++ b/chrome/content/zotero/xpcom/feedReader.js @@ -45,7 +45,7 @@ * * @property {Zotero.Promise} feedProperties An object * representing feed properties - * @property {Zotero.Promise*} itemIterator Returns an iterator + * @property {Zotero.Promise*} ItemIterator Returns an iterator * for feed items. The iterator returns FeedItem promises that have to be * resolved before requesting the next promise. When all items are exhausted. * the promise resolves to null. @@ -170,17 +170,10 @@ Zotero.FeedReader = new function() { } /* - * Format JS date as SQL date + time zone offset + * Format JS date as SQL date */ function formatDate(date) { - let offset = (date.getTimezoneOffset() / 60) * -1; - let absOffset = Math.abs(offset); - offset = offset - ? ' ' + (offset < 0 ? '-' : '+') - + Zotero.Utilities.lpad(Math.floor(absOffset), '0', 2) - + ('' + ( (absOffset - Math.floor(absOffset)) || '' )).substr(1) // Get ".5" fraction or "" otherwise - : ''; - return Zotero.Date.dateToSQL(date, false) + offset; + return Zotero.Date.dateToSQL(date, true); } /* @@ -268,7 +261,6 @@ Zotero.FeedReader = new function() { if (!item.dateModified) { // When there's no reliable modification date, we can assume that item doesn't get updated Zotero.debug("FeedReader: Feed item missing a modification date (" + item.guid + ")"); - item.dateModified = null; } if (!item.date && item.dateModified) { @@ -470,7 +462,9 @@ Zotero.FeedReader = new function() { Zotero.debug("FeedReader: Fetching feed from " + feedUrl.spec); - this._channel = ios.newChannelFromURI(feedUrl); + this._channel = ios.newChannelFromURI2(feedUrl, null, + Services.scriptSecurityManager.getSystemPrincipal(), null, + Ci.nsILoadInfo.SEC_NORMAL, Ci.nsIContentPolicy.TYPE_OTHER); this._channel.asyncOpen(feedProcessor, null); // Sends an HTTP request } @@ -486,21 +480,25 @@ Zotero.FeedReader = new function() { * is terminated ahead of time, in which case it will be rejected with the reason * for termination. */ - Zotero.defineProperty(FeedReader.prototype, 'itemIterator', { + Zotero.defineProperty(FeedReader.prototype, 'ItemIterator', { get: function() { let items = this._feedItems; - return new function() { - let i = 0; - this.next = function() { - let item = items[i++]; - return { - value: item ? item.promise : null, - done: i >= items.length - }; + + let iterator = function() { + this.index = 0; + }; + + iterator.prototype.next = function() { + let item = items[this.index++]; + return { + value: item ? item.promise : null, + done: this.index >= items.length }; - } + }; + + return iterator; } - }); + }, {lazy: true}); /* * Terminate feed processing at any given time @@ -521,8 +519,8 @@ Zotero.FeedReader = new function() { } // Close feed connection - if (channel.isPending) { - channel.cancel(Components.results.NS_BINDING_ABORTED); + if (this._channel.isPending) { + this._channel.cancel(Components.results.NS_BINDING_ABORTED); } }; diff --git a/chrome/content/zotero/xpcom/uri.js b/chrome/content/zotero/xpcom/uri.js index 5cf8856b9..7cc2e9bc6 100644 --- a/chrome/content/zotero/xpcom/uri.js +++ b/chrome/content/zotero/xpcom/uri.js @@ -133,6 +133,14 @@ Zotero.URI = new function () { } + this.getFeedItemURI = function(feedItem) { + return this.getItemURI(feedItem); + } + + this.getFeedItemPath = function(feedItem) { + return this.getItemPath(feedItem); + } + /** * Return URI of collection, which might be a local URI if user hasn't synced */ @@ -148,6 +156,14 @@ Zotero.URI = new function () { return this._getObjectPath(collection); } + this.getFeedURI = function(feed) { + return this.getLibraryURI(feed); + } + + this.getFeedPath = function(feed) { + return this.getLibraryPath(feed); + } + this.getGroupsURL = function () { return ZOTERO_CONFIG.WWW_BASE_URL + "groups"; @@ -172,7 +188,7 @@ Zotero.URI = new function () { return path; } - if (obj instanceof Zotero.Item) { + if (obj instanceof Zotero.Item || obj instanceof Zotero.Feed) { return path + '/items/' + obj.key; } @@ -208,6 +224,9 @@ Zotero.URI = new function () { return this._getURIObject(itemURI, 'item'); } + this.getURIFeedItem = function (feedItemURI) { + return this._getURIObject(feedItemURI, 'feedItem'); + } /** * @param {String} itemURI @@ -264,6 +283,11 @@ Zotero.URI = new function () { let library = this._getURIObjectLibrary(libraryURI); return library ? library.id : false; } + + + this.getURIFeed = function (feedURI) { + return this._getURIObjectLibrary(feedURI, 'feed'); + } /** diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index 388a41b30..b473774eb 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -522,22 +522,7 @@ var ZoteroPane = new function() } 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; 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 literal 0 HcmV?d00001 diff --git a/chrome/skin/default/zotero/treesource-feed-updating.png b/chrome/skin/default/zotero/treesource-feed-updating.png new file mode 100644 index 0000000000000000000000000000000000000000..0634484e229a85ff2ceb64aa1db2c1f4a4895506 GIT binary patch literal 3541 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 P00000NkvXXu0mjf9xR%b literal 0 HcmV?d00001