diff --git a/chrome/content/zotero/xpcom/collectionTreeRow.js b/chrome/content/zotero/xpcom/collectionTreeRow.js index 80b192851a..9dbed7bd68 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 8ab70b8aa0..ce9d06d480 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 f50de3260c..d68f50aa7c 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 5b4548e7b6..638c3aa78e 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 c6cf624f17..d0a1b09035 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 5cf8856b91..7cc2e9bc63 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 388a41b302..b473774ebf 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; i