Implement read/unread functionality in feeds

This commit is contained in:
Aurimas Vinckevicius 2014-12-14 23:07:37 -06:00 committed by Dan Stillman
parent 9686758c7d
commit 2c3eb205ab
10 changed files with 204 additions and 98 deletions

View File

@ -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(" ");

View File

@ -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);
}
});

View File

@ -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();
}
});

View File

@ -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({}),

View File

@ -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()
});
});
}

View File

@ -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++) {

View File

@ -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(" ");
}

View File

@ -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:

View File

@ -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);
});
}

View File

@ -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;