mirror of
https://github.com/zotero/zotero.git
synced 2026-08-28 05:25:31 +00:00
Track attachment last-read time, add Recently Read virtual collection (#2854)
Track when attachments are last opened or read, storing a `lastRead` Unix timestamp on the attachment. For user library items, `lastRead` syncs as an attachment property in item JSON. For group library items, it syncs via a per-user synced setting (like `lastPageIndex`). - Add `lastRead` column to `itemAttachments` - Add `AttachmentReadObserver` to update `lastRead` on file open and page change (throttled to 5 min for page changes) - Add "Recently Read" virtual collection (items read in last 14 days, sorted by `lastRead` descending) - Add `lastRead` search condition with date operators - Add `lastRead` item tree column with new `dependsOnChildren` property for parent item aggregation - Add `getItemLastRead()` to return max `lastRead` across child attachments Also: - Generalize collection tree SCSS to support universal (context-fill) icons alongside themed icons --------- Co-authored-by: Dan Stillman <dstillman@zotero.org>
This commit is contained in:
parent
0ce5cd5bac
commit
34039991f8
30 changed files with 713 additions and 64 deletions
|
|
@ -83,6 +83,8 @@ var ZoteroAdvancedSearch = new function () {
|
|||
isFeed: () => false,
|
||||
isFeeds: () => false,
|
||||
isFeedsOrFeed: () => false,
|
||||
isRecentlyRead: () => false,
|
||||
isSortable: () => true,
|
||||
isShare: () => false,
|
||||
isTrash: () => false,
|
||||
isSearch: () => true
|
||||
|
|
@ -101,7 +103,7 @@ var ZoteroAdvancedSearch = new function () {
|
|||
_searchBox.updateSearch();
|
||||
_searchBox.active = true;
|
||||
|
||||
return this.itemsView.changeCollectionTreeRow({
|
||||
var collectionTreeRow = {
|
||||
ref: _searchBox.search,
|
||||
visibilityGroup: 'default',
|
||||
isSearchMode: () => true,
|
||||
|
|
@ -113,8 +115,21 @@ var ZoteroAdvancedSearch = new function () {
|
|||
search.libraryID = _libraryID;
|
||||
var ids = await search.search();
|
||||
return Zotero.Items.get(ids);
|
||||
}
|
||||
});
|
||||
},
|
||||
isLibrary: () => false,
|
||||
isCollection: () => false,
|
||||
isPublications: () => false,
|
||||
isDuplicates: () => false,
|
||||
isFeed: () => false,
|
||||
isFeeds: () => false,
|
||||
isFeedsOrFeed: () => false,
|
||||
isRecentlyRead: () => false,
|
||||
isSortable: () => true,
|
||||
isShare: () => false,
|
||||
isTrash: () => false
|
||||
};
|
||||
|
||||
return this.itemsView.changeCollectionTreeRow(collectionTreeRow);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -513,6 +513,8 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
}
|
||||
this._virtualCollectionLibraries.unfiled =
|
||||
Zotero.Prefs.getVirtualCollectionState('unfiled');
|
||||
this._virtualCollectionLibraries.recentlyRead =
|
||||
Zotero.Prefs.getVirtualCollectionState('recentlyRead');
|
||||
this._virtualCollectionLibraries.retracted =
|
||||
Zotero.Prefs.getVirtualCollectionState('retracted');
|
||||
this._virtualCollectionLibraries.publications = Zotero.Prefs.getVirtualCollectionState('publications');
|
||||
|
|
@ -1258,7 +1260,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
}
|
||||
|
||||
/**
|
||||
* Toggle virtual collection (duplicates/unfiled) visibility
|
||||
* Toggle virtual collection (duplicates/unfiled/recently read/retracted) visibility
|
||||
*
|
||||
* @param libraryID {Number}
|
||||
* @param type {String}
|
||||
|
|
@ -1269,6 +1271,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
const types = {
|
||||
duplicates: 'D',
|
||||
unfiled: 'U',
|
||||
recentlyRead: 'Y',
|
||||
retracted: 'R',
|
||||
publications: 'P'
|
||||
};
|
||||
|
|
@ -1427,6 +1430,10 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
case 'feeds':
|
||||
icon = 'feed-library';
|
||||
break;
|
||||
|
||||
case 'recentlyRead':
|
||||
icon = 'recent';
|
||||
break;
|
||||
|
||||
case 'header':
|
||||
if (treeRow.ref.id == 'group-libraries-header') {
|
||||
|
|
@ -2971,6 +2978,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
&& this._virtualCollectionLibraries.duplicates[libraryID] !== false;
|
||||
var showUnfiled = this.props.hideSources.indexOf('unfiled') == -1
|
||||
&& this._virtualCollectionLibraries.unfiled?.[libraryID] !== false;
|
||||
var showRecentlyRead = this._virtualCollectionLibraries.recentlyRead?.[libraryID] !== false;
|
||||
var showRetracted = this.props.hideSources.indexOf('retracted') == -1
|
||||
&& this._virtualCollectionLibraries.retracted?.[libraryID] !== false
|
||||
&& Zotero.Retractions.libraryHasRetractedItems(libraryID);
|
||||
|
|
@ -2983,6 +2991,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
var savedSearches = [];
|
||||
var showDuplicates = false;
|
||||
var showUnfiled = false;
|
||||
var showRecentlyRead = false;
|
||||
var showRetracted = false;
|
||||
var showPublications = false;
|
||||
var showTrash = false;
|
||||
|
|
@ -2997,7 +3006,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
return 0;
|
||||
}
|
||||
|
||||
var startOpen = !!(collections.length || savedSearches.length || showDuplicates || showUnfiled || showRetracted || showTrash);
|
||||
var startOpen = !!(collections.length || savedSearches.length || showDuplicates || showUnfiled || showRecentlyRead || showRetracted || showTrash);
|
||||
|
||||
// If this isn't a manual open, set the initial state depending on whether
|
||||
// there are child nodes
|
||||
|
|
@ -3011,6 +3020,18 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
|
||||
var newRows = 0;
|
||||
|
||||
// Recently Read
|
||||
if (showRecentlyRead) {
|
||||
let s = new Zotero.Search();
|
||||
s.libraryID = libraryID;
|
||||
s.name = Zotero.getString('pane.collections.recentlyRead');
|
||||
s.addCondition('libraryID', 'is', libraryID);
|
||||
s.addCondition('lastRead', 'isInTheLast', '14 days');
|
||||
rows.splice(row + 1 + newRows, 0,
|
||||
new Zotero.CollectionTreeRow(this, 'recentlyRead', s, level + 1));
|
||||
newRows++;
|
||||
}
|
||||
|
||||
// Add collections
|
||||
for (var i = 0, len = collections.length; i < len; i++) {
|
||||
// Skip collections in trash
|
||||
|
|
@ -3146,13 +3167,19 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
else {
|
||||
// Get all collections at the same level that don't have a different parent
|
||||
startRow++;
|
||||
// Skip past virtual collections (e.g., Recently Read) that come
|
||||
// before collections in the tree
|
||||
while (startRow < this._rows.length
|
||||
&& this.getLevel(startRow) == level
|
||||
&& this.getRow(startRow).isRecentlyRead()) {
|
||||
startRow++;
|
||||
}
|
||||
loop:
|
||||
for (let i = startRow; i < this._rows.length; i++) {
|
||||
let treeRow = this.getRow(i);
|
||||
beforeRow = i;
|
||||
|
||||
// Since collections come first, if we reach something that's not a collection,
|
||||
// stop
|
||||
|
||||
// If we reach something that's not a collection, stop
|
||||
if (!treeRow.isCollection()) {
|
||||
break;
|
||||
}
|
||||
|
|
@ -3215,7 +3242,11 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
for (let i = startRow; i < this._rows.length; i++) {
|
||||
let treeRow = this.getRow(i);
|
||||
beforeRow = i;
|
||||
|
||||
|
||||
// Skip forward to first collection
|
||||
if (treeRow.isRecentlyRead()) {
|
||||
continue;
|
||||
}
|
||||
// If we've reached something other than collections, stop
|
||||
if (treeRow.isSearch()) {
|
||||
// If current search sorts after, stop
|
||||
|
|
|
|||
|
|
@ -360,6 +360,7 @@
|
|||
case 'date':
|
||||
case 'dateAdded':
|
||||
case 'dateModified':
|
||||
case 'lastRead':
|
||||
case 'itemType':
|
||||
case 'fileTypeID':
|
||||
case 'publicationTitle':
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ const STUB_COLLECTION_TREE_ROW = {
|
|||
isFeed: () => false,
|
||||
isFeeds: () => false,
|
||||
isFeedsOrFeed: () => false,
|
||||
isRecentlyRead: () => false,
|
||||
isSortable: () => true,
|
||||
isShare: () => false,
|
||||
isTrash: () => false
|
||||
};
|
||||
|
|
@ -261,8 +263,12 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
this._itemsPaneMessage = null;
|
||||
return shouldRerender && new Promise(resolve => this.forceUpdate(resolve));
|
||||
}
|
||||
|
||||
refresh = Zotero.serial(async function (skipExpandMatchParents) {
|
||||
|
||||
/**
|
||||
* @param {Boolean} [options.forceSortAll] Sort all items instead of only added items
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
refresh = Zotero.serial(async function (options = {}) {
|
||||
Zotero.debug('Refreshing items list for ' + this.id);
|
||||
|
||||
var resolve, reject;
|
||||
|
|
@ -393,7 +399,7 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
// This still results in a lot of extra work (e.g., when clearing a quick search, we have to
|
||||
// re-sort all items that didn't match the search), so as a further optimization we could keep
|
||||
// a sorted list of items for a given column configuration and restore items from that.
|
||||
await this.sort([...addedItemIDs]);
|
||||
await this.sort(options.forceSortAll ? [...allItemIDs] : [...addedItemIDs]);
|
||||
|
||||
// Update search results before collapse/expand of containers so that
|
||||
// if hideContextAnnotationRows pref is true, child rows appear/disappear properly
|
||||
|
|
@ -771,6 +777,21 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
await this.toggleOpenState(parentItemRowIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.parentItemID && this._getColumns().some(col => !col.hidden && col.dependsOnChildren)) {
|
||||
delete this._rowCache[item.parentItemID];
|
||||
if (this._rowMap[item.parentItemID] !== undefined) {
|
||||
this.tree.invalidateRow(this._rowMap[item.parentItemID]);
|
||||
// If we're sorting by a dependsOnChildren column, also re-sort
|
||||
// (We don't look at secondary sort here because no dependsOnParent columns can be secondary sorts.
|
||||
// If that changes, we need to be more thorough here.)
|
||||
let sortField = this.getSortField();
|
||||
if (this._getColumns().find(col => col.dataKey == sortField)?.dependsOnChildren) {
|
||||
madeChanges = true;
|
||||
sort = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sort && ids.length != 1) {
|
||||
|
|
@ -794,6 +815,7 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
|| collectionTreeRow.isPublications()
|
||||
|| collectionTreeRow.isTrash()
|
||||
|| collectionTreeRow.isUnfiled()
|
||||
|| collectionTreeRow.isRecentlyRead()
|
||||
|| hasQuickSearch) {
|
||||
if (hasQuickSearch) {
|
||||
// For item adds, clear the quick search, unless all the new items have
|
||||
|
|
@ -1125,7 +1147,7 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
showHeader: true,
|
||||
columns: this._getColumns(),
|
||||
onColumnPickerMenu: this._displayColumnPickerMenu,
|
||||
onColumnSort: this.collectionTreeRow.isFeedsOrFeed() ? null : this._handleColumnSort,
|
||||
onColumnSort: this.collectionTreeRow.isSortable() ? this._handleColumnSort : null,
|
||||
getColumnPrefs: this._getColumnPrefs,
|
||||
storeColumnPrefs: this._storeColumnPrefs,
|
||||
getDefaultColumnOrder: this._getDefaultColumnOrder,
|
||||
|
|
@ -1189,7 +1211,8 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
if (collectionTreeRow.visibilityGroup) {
|
||||
newId += "-" + collectionTreeRow.visibilityGroup;
|
||||
}
|
||||
if (this.id != newId && this.props.persistColumns) {
|
||||
let idChanged = this.id != newId;
|
||||
if (idChanged && this.props.persistColumns) {
|
||||
await this._writeColumnPrefsToFile(true);
|
||||
this.id = newId;
|
||||
await this._loadColumnPrefsFromFile();
|
||||
|
|
@ -1203,7 +1226,7 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
|
||||
this.selection.clearSelection();
|
||||
this.selection.focused = 0;
|
||||
await this.refresh();
|
||||
await this.refresh({ forceSortAll: idChanged });
|
||||
if (Zotero.CollectionTreeCache.error) {
|
||||
return this.setItemsPaneMessage(Zotero.getString('pane.items.loadError'));
|
||||
}
|
||||
|
|
@ -1487,6 +1510,9 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
|
||||
case 'feed':
|
||||
return (row.ref.isFeedItem && Zotero.Feeds.get(row.ref.libraryID).name) || "";
|
||||
|
||||
case 'lastRead':
|
||||
return item.getItemLastRead();
|
||||
|
||||
default:
|
||||
let extraField = this.props.getExtraField(row.ref, field);
|
||||
|
|
@ -1976,6 +2002,7 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
else if (collectionTreeRow.isLibrary(true)
|
||||
|| collectionTreeRow.isSearch()
|
||||
|| collectionTreeRow.isUnfiled()
|
||||
|| collectionTreeRow.isRecentlyRead()
|
||||
|| collectionTreeRow.isRetracted()
|
||||
|| collectionTreeRow.isDuplicates()
|
||||
|| force) {
|
||||
|
|
@ -2046,10 +2073,13 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
* @return {Number} - -1 for descending, 1 for ascending
|
||||
*/
|
||||
getSortDirection(sortFields) {
|
||||
sortFields = sortFields || this.getSortFields();
|
||||
if (this.collectionTreeRow.isFeedsOrFeed()) {
|
||||
return Zotero.Prefs.get('feeds.sortAscending') ? 1 : -1;
|
||||
}
|
||||
if (this.collectionTreeRow.isRecentlyRead()) {
|
||||
return -1;
|
||||
}
|
||||
sortFields = sortFields || this.getSortFields();
|
||||
const columns = this._getColumns();
|
||||
for (const field of sortFields) {
|
||||
const col = columns.find(c => c.dataKey == field);
|
||||
|
|
@ -2064,6 +2094,9 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
if (this.collectionTreeRow.isFeedsOrFeed()) {
|
||||
return 'id';
|
||||
}
|
||||
if (this.collectionTreeRow.isRecentlyRead()) {
|
||||
return 'lastRead';
|
||||
}
|
||||
var column = this._sortedColumn;
|
||||
if (!column) {
|
||||
column = this._getColumns().find(col => !col.hidden);
|
||||
|
|
@ -2789,7 +2822,7 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
//
|
||||
// Secondary Sort menu
|
||||
//
|
||||
if (!this.collectionTreeRow.isFeedsOrFeed()) {
|
||||
if (this.collectionTreeRow.isSortable()) {
|
||||
try {
|
||||
const id = prefix + 'sort-menu';
|
||||
const primaryField = this.getSortField();
|
||||
|
|
@ -3445,7 +3478,8 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
}
|
||||
row.numNotes = treeRow.numNotes() || "";
|
||||
row.feed = (treeRow.ref.isFeedItem && Zotero.Feeds.get(treeRow.ref.libraryID).name) || "";
|
||||
|
||||
row.lastRead = row.isItem ? treeRow.ref.getItemLastRead() : "";
|
||||
|
||||
if (treeRow.ref.isFileAttachment()
|
||||
// TODO: Adjust this if we localize "Snapshot"
|
||||
&& !(treeRow.ref.isSnapshotAttachment() && /snapshot/i.test(treeRow.ref.getField('title')))
|
||||
|
|
@ -3503,6 +3537,13 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
val = '';
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'lastRead':
|
||||
if (val) {
|
||||
let date = new Date(val * 1000);
|
||||
val = date.toLocaleString();
|
||||
}
|
||||
break;
|
||||
}
|
||||
row[key] = val;
|
||||
}
|
||||
|
|
@ -3671,6 +3712,15 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
}
|
||||
}
|
||||
|
||||
// Force sort indicator for views with a fixed sort order
|
||||
if (this.collectionTreeRow?.isRecentlyRead()) {
|
||||
let col = this._columns.find(c => c.dataKey === 'lastRead');
|
||||
if (col) {
|
||||
col.sortDirection = -1;
|
||||
this._sortedColumn = col;
|
||||
}
|
||||
}
|
||||
|
||||
return this._columns.sort((a, b) => a.ordinal - b.ordinal);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ const Icons = require('components/icons');
|
|||
* @property {string[]} [enabledTreeIDs=[]] - Which tree ids the column should be enabled in. If undefined, enabled in main tree. If ["*"], enabled in all trees.
|
||||
* @property {string[]} [defaultIn] - Will be deprecated. Types of trees the column is default in. Can be [default, feed];
|
||||
* @property {string[]} [disabledIn] - Will be deprecated. Types of trees where the column is not available
|
||||
* @property {boolean} [dependsOnChildren=false] - Set to true if the column depends on child item data (e.g. numNotes, lastRead)
|
||||
* @property {boolean} [sortReverse=false] - Default: false. Set to true to reverse the sort order
|
||||
* @property {number} [flex=1] - Default: 1. When the column is added to the tree how much space it should occupy as a flex ratio
|
||||
* @property {string} [width] - A column width instead of flex ratio. See above.
|
||||
|
|
@ -60,7 +61,7 @@ const COLUMNS = [
|
|||
{
|
||||
dataKey: "title",
|
||||
primary: true,
|
||||
defaultIn: ["default", "feeds", "feed"],
|
||||
defaultIn: ["default", "feeds", "feed", "recentlyRead"],
|
||||
label: "itemFields.title",
|
||||
showInColumnPicker: false,
|
||||
flex: 4,
|
||||
|
|
@ -68,7 +69,7 @@ const COLUMNS = [
|
|||
},
|
||||
{
|
||||
dataKey: "firstCreator",
|
||||
defaultIn: ["default", "feeds", "feed"],
|
||||
defaultIn: ["default", "feeds", "feed", "recentlyRead"],
|
||||
label: "zotero.items.creator_column",
|
||||
showInColumnPicker: true,
|
||||
flex: 1,
|
||||
|
|
@ -185,6 +186,16 @@ const COLUMNS = [
|
|||
flex: 1,
|
||||
zoteroPersist: ["width", "hidden", "sortDirection"]
|
||||
},
|
||||
{
|
||||
dataKey: "lastRead",
|
||||
defaultSort: -1,
|
||||
defaultIn: ["recentlyRead"],
|
||||
disabledIn: ["feeds", "feed"],
|
||||
dependsOnChildren: true,
|
||||
label: "pane.items.columns.lastRead",
|
||||
flex: 2,
|
||||
zoteroPersist: ["width", "hidden", "sortDirection"]
|
||||
},
|
||||
{
|
||||
dataKey: "archive",
|
||||
disabledIn: ["feeds", "feed"],
|
||||
|
|
@ -342,6 +353,7 @@ const COLUMNS = [
|
|||
defaultIn: ["default"],
|
||||
disabledIn: ["feeds", "feed"],
|
||||
showInColumnPicker: true,
|
||||
dependsOnChildren: true,
|
||||
label: "zotero.tabs.attachments.label",
|
||||
iconLabel: <Icons.IconAttachSmall />,
|
||||
fixedWidth: true,
|
||||
|
|
@ -352,6 +364,7 @@ const COLUMNS = [
|
|||
dataKey: "numNotes",
|
||||
disabledIn: ["feeds", "feed"],
|
||||
showInColumnPicker: true,
|
||||
dependsOnChildren: true,
|
||||
label: "zotero.tabs.notes.label",
|
||||
iconLabel: <Icons.IconTreeitemNoteSmall />,
|
||||
width: "26",
|
||||
|
|
|
|||
113
chrome/content/zotero/xpcom/attachmentReadObserver.js
Normal file
113
chrome/content/zotero/xpcom/attachmentReadObserver.js
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2022 Corporation for Digital Scholarship
|
||||
Vienna, Virginia, USA
|
||||
http://digitalscholar.org/
|
||||
|
||||
This file is part of Zotero.
|
||||
|
||||
Zotero is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Zotero is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
Zotero.AttachmentReadObserver = {
|
||||
init() {
|
||||
this._observerID = Zotero.Notifier.registerObserver(this, ['file', 'setting'], 'attachmentReadObserver');
|
||||
},
|
||||
|
||||
unregister() {
|
||||
if (this._observerID) {
|
||||
Zotero.Notifier.unregisterObserver(this._observerID);
|
||||
this._observerID = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* To make the date mockable in tests
|
||||
* @return {Date}
|
||||
*/
|
||||
_getCurrentDate() {
|
||||
return new Date();
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Zotero.Item} item
|
||||
*/
|
||||
async updateAttachmentLastRead(item) {
|
||||
// Limit to My Library and groups
|
||||
if (item.libraryID != Zotero.Libraries.userLibraryID && !item.library.isGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.attachmentLastRead = Math.round(this._getCurrentDate().getTime() / 1000);
|
||||
await item.saveTx({ skipDateModifiedUpdate: true });
|
||||
},
|
||||
|
||||
async notify(action, type, ids, extraData) {
|
||||
if (type == 'file') {
|
||||
if (!['pageChange', 'open'].includes(action)) {
|
||||
return;
|
||||
}
|
||||
let items = await Zotero.Items.getAsync(ids);
|
||||
switch (action) {
|
||||
case 'open':
|
||||
for (let item of items) {
|
||||
await this.updateAttachmentLastRead(item);
|
||||
}
|
||||
break;
|
||||
case 'pageChange': {
|
||||
let fiveMinutesAgo = this._getCurrentDate();
|
||||
fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 5);
|
||||
for (let item of items) {
|
||||
if (item.library.lastReadItemInSession !== item.id
|
||||
|| new Date(item.attachmentLastRead * 1000) < fiveMinutesAgo) {
|
||||
await this.updateAttachmentLastRead(item);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type == 'setting') {
|
||||
for (let id of ids) {
|
||||
let [settingLibraryID, settingKey] = id.split('/');
|
||||
settingLibraryID = parseInt(settingLibraryID);
|
||||
if (settingLibraryID != Zotero.Libraries.userLibraryID) {
|
||||
continue;
|
||||
}
|
||||
if (settingKey.startsWith('lastRead_')) {
|
||||
let [, librarySlug, itemKey] = settingKey.split('_');
|
||||
let libraryID;
|
||||
if (librarySlug == 'u') {
|
||||
continue; // lastRead_ synced settings are only used for group items
|
||||
}
|
||||
else if (librarySlug.startsWith('g')) {
|
||||
libraryID = Zotero.Groups.getLibraryIDFromGroupID(parseInt(librarySlug.substring(1)));
|
||||
}
|
||||
else {
|
||||
Zotero.debug('Invalid library slug in key: ' + settingKey);
|
||||
continue;
|
||||
}
|
||||
let item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, itemKey);
|
||||
if (item.isAttachment()) {
|
||||
item.attachmentLastRead = Zotero.SyncedSettings.get(settingLibraryID, settingKey);
|
||||
await item.saveTx({ skipDateModifiedUpdate: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -56,6 +56,9 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('id', function () {
|
|||
case 'unfiled':
|
||||
return 'U' + this.ref.libraryID;
|
||||
|
||||
case 'recentlyRead':
|
||||
return 'Y' + this.ref.libraryID;
|
||||
|
||||
case 'retracted':
|
||||
return 'R' + this.ref.libraryID;
|
||||
|
||||
|
|
@ -109,6 +112,10 @@ Zotero.CollectionTreeRow.prototype.isUnfiled = function () {
|
|||
return this.type == 'unfiled';
|
||||
}
|
||||
|
||||
Zotero.CollectionTreeRow.prototype.isRecentlyRead = function () {
|
||||
return this.type == 'recentlyRead';
|
||||
}
|
||||
|
||||
Zotero.CollectionTreeRow.prototype.isRetracted = function () {
|
||||
return this.type == 'retracted';
|
||||
}
|
||||
|
|
@ -187,7 +194,7 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('editable', function () {
|
|||
return true;
|
||||
}
|
||||
var libraryID = this.ref.libraryID;
|
||||
if (this.isCollection() || this.isSearch() || this.isDuplicates() || this.isUnfiled() || this.isRetracted()) {
|
||||
if (this.isCollection() || this.isSearch() || this.isDuplicates() || this.isUnfiled() || this.isRecentlyRead() || this.isRetracted()) {
|
||||
var type = Zotero.Libraries.get(libraryID).libraryType;
|
||||
if (type == 'group') {
|
||||
var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
|
||||
|
|
@ -210,7 +217,7 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('filesEditable', function ()
|
|||
if (this.isGroup()) {
|
||||
return this.ref.editable && this.ref.filesEditable;
|
||||
}
|
||||
if (this.isCollection() || this.isSearch() || this.isDuplicates() || this.isUnfiled() || this.isRetracted()) {
|
||||
if (this.isCollection() || this.isSearch() || this.isDuplicates() || this.isUnfiled() || this.isRecentlyRead() || this.isRetracted()) {
|
||||
var type = Zotero.Libraries.get(libraryID).libraryType;
|
||||
if (type == 'group') {
|
||||
var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
|
||||
|
|
@ -223,7 +230,7 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('filesEditable', function ()
|
|||
});
|
||||
|
||||
|
||||
Zotero.CollectionTreeRow.visibilityGroups = {'feed': 'feed', 'feeds': 'feeds'};
|
||||
Zotero.CollectionTreeRow.visibilityGroups = {'feed': 'feed', 'feeds': 'feeds', 'recentlyRead': 'recentlyRead'};
|
||||
|
||||
|
||||
Zotero.CollectionTreeRow.prototype.__defineGetter__('visibilityGroup', function () {
|
||||
|
|
@ -503,6 +510,10 @@ Zotero.CollectionTreeRow.prototype.isSearchMode = function () {
|
|||
}
|
||||
}
|
||||
|
||||
Zotero.CollectionTreeRow.prototype.isSortable = function () {
|
||||
return !this.isFeedsOrFeed() && !this.isRecentlyRead();
|
||||
}
|
||||
|
||||
Zotero.CollectionTreeCache = {
|
||||
"lastTreeRow":null,
|
||||
"lastTempTable":null,
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ Zotero.Item = function (itemTypeOrID) {
|
|||
this._attachmentSyncedModificationTime = null;
|
||||
this._attachmentSyncedHash = null;
|
||||
this._attachmentLastProcessedModificationTime = null;
|
||||
this._attachmentLastRead = null;
|
||||
|
||||
// loadCreators
|
||||
this._creators = [];
|
||||
|
|
@ -363,6 +364,7 @@ Zotero.Item.prototype._parseRowData = function (row) {
|
|||
case 'attachmentSyncedModificationTime':
|
||||
case 'attachmentSyncedHash':
|
||||
case 'attachmentLastProcessedModificationTime':
|
||||
case 'attachmentLastRead':
|
||||
case 'createdByUserID':
|
||||
case 'lastModifiedByUserID':
|
||||
break;
|
||||
|
|
@ -1938,13 +1940,13 @@ Zotero.Item.prototype._saveData = async function (env) {
|
|||
let sql = "";
|
||||
let cols = [
|
||||
'parentItemID', 'linkMode', 'contentType', 'charsetID', 'path', 'syncState',
|
||||
'storageModTime', 'storageHash', 'lastProcessedModificationTime'
|
||||
'storageModTime', 'storageHash', 'lastProcessedModificationTime', 'lastRead'
|
||||
];
|
||||
// TODO: Replace with UPSERT after SQLite 3.24.0
|
||||
if (isNew) {
|
||||
sql = "INSERT INTO itemAttachments "
|
||||
+ "(itemID, " + cols.join(", ") + ") "
|
||||
+ "VALUES (?,?,?,?,?,?,?,?,?,?)";
|
||||
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?)";
|
||||
}
|
||||
else {
|
||||
sql = "UPDATE itemAttachments SET " + cols.join("=?, ") + "=? WHERE itemID=?";
|
||||
|
|
@ -1959,6 +1961,7 @@ Zotero.Item.prototype._saveData = async function (env) {
|
|||
let storageModTime = this.attachmentSyncedModificationTime;
|
||||
let storageHash = this.attachmentSyncedHash;
|
||||
let lastProcessedModificationTime = this.attachmentLastProcessedModificationTime;
|
||||
let lastRead = this.attachmentLastRead;
|
||||
|
||||
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE && libraryType != 'user') {
|
||||
throw new Error("Linked files can only be added to user library");
|
||||
|
|
@ -1974,6 +1977,7 @@ Zotero.Item.prototype._saveData = async function (env) {
|
|||
storageModTime !== undefined ? storageModTime : null,
|
||||
storageHash || null,
|
||||
lastProcessedModificationTime || null,
|
||||
lastRead || null,
|
||||
];
|
||||
if (isNew) {
|
||||
params.unshift(itemID);
|
||||
|
|
@ -1987,6 +1991,17 @@ Zotero.Item.prototype._saveData = async function (env) {
|
|||
if (!isNew && parentItemID) {
|
||||
reloadParentChildItems[parentItemID] = true;
|
||||
}
|
||||
|
||||
// Save attachmentLastRead to a synced setting if this is a group item
|
||||
if (libraryType == 'group' && lastRead !== undefined) {
|
||||
let id = this._getLastReadSettingKey();
|
||||
if (lastRead === null) {
|
||||
await Zotero.SyncedSettings.clear(Zotero.Libraries.userLibraryID, id);
|
||||
}
|
||||
else {
|
||||
await Zotero.SyncedSettings.set(Zotero.Libraries.userLibraryID, id, lastRead);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
|
@ -3547,6 +3562,49 @@ Zotero.defineProperty(Zotero.Item.prototype, 'attachmentSyncedHash', {
|
|||
});
|
||||
|
||||
|
||||
Zotero.defineProperty(Zotero.Item.prototype, 'attachmentLastRead', {
|
||||
get() {
|
||||
if (!this.isAttachment()) {
|
||||
return undefined;
|
||||
}
|
||||
return this._attachmentLastRead;
|
||||
},
|
||||
|
||||
set(val) {
|
||||
if (!this.isAttachment()) {
|
||||
throw new Error('attachmentLastRead can only be set for attachment items');
|
||||
}
|
||||
if (!this.libraryID) {
|
||||
throw new Error('Item not in library');
|
||||
}
|
||||
if (this.libraryID != Zotero.Libraries.userLibraryID && !this.library.isGroup) {
|
||||
throw new Error('attachmentLastRead can only be set on items in My Library and groups');
|
||||
}
|
||||
|
||||
if (val !== null && typeof val != 'number') {
|
||||
throw new Error('attachmentLastRead must be a number');
|
||||
}
|
||||
if (val != parseInt(val)) {
|
||||
throw new Error('attachmentLastRead must be an integer timestamp in seconds');
|
||||
}
|
||||
|
||||
let lastReadItem = Zotero.Items.get(this.library.lastReadItemInSession);
|
||||
if (!lastReadItem || lastReadItem.attachmentLastRead < val) {
|
||||
this.library.lastReadItemInSession = this.id;
|
||||
}
|
||||
|
||||
if (val == this._attachmentLastRead) {
|
||||
return;
|
||||
}
|
||||
if (!this._changed.attachmentData) {
|
||||
this._changed.attachmentData = {};
|
||||
}
|
||||
this._changed.attachmentData.lastRead = true;
|
||||
this._attachmentLastRead = val;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
//
|
||||
// PDF attachment properties
|
||||
//
|
||||
|
|
@ -3627,20 +3685,22 @@ Zotero.Item.prototype.setAttachmentLastPageIndex = async function (val) {
|
|||
|
||||
var id = this._getLastPageIndexSettingKey();
|
||||
if (val === null) {
|
||||
return Zotero.SyncedSettings.clear(id);
|
||||
return Zotero.SyncedSettings.clear(Zotero.Libraries.userLibraryID, id);
|
||||
}
|
||||
return Zotero.SyncedSettings.set(Zotero.Libraries.userLibraryID, id, val);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the key for the item's pageIndex synced setting
|
||||
* Get the key for a synced setting related to this item
|
||||
*
|
||||
* E.g., 'lastPageIndex_u_ABCD2345' or 'lastPageIndex_g123_ABCD2345'
|
||||
* @param {String} prefix
|
||||
* @param {Boolean} [ignoreInvalid=false]
|
||||
* @return {String | false}
|
||||
*/
|
||||
Zotero.Item.prototype._getLastPageIndexSettingKey = function (ignoreInvalid) {
|
||||
var library = Zotero.Libraries.get(this.libraryID);
|
||||
var id = 'lastPageIndex_';
|
||||
Zotero.Item.prototype._getSettingKey = function (prefix, ignoreInvalid = false) {
|
||||
var library = this.library;
|
||||
var id = prefix + '_';
|
||||
switch (library.libraryType) {
|
||||
case 'user':
|
||||
id += 'u';
|
||||
|
|
@ -3651,7 +3711,7 @@ Zotero.Item.prototype._getLastPageIndexSettingKey = function (ignoreInvalid) {
|
|||
break;
|
||||
|
||||
default:
|
||||
var msg = `Can't get last page index key for ${library.libraryType} item`;
|
||||
var msg = `Can't get ${prefix} key for ${library.libraryType} item`;
|
||||
if (ignoreInvalid) {
|
||||
Zotero.logError(msg);
|
||||
return false;
|
||||
|
|
@ -3663,6 +3723,42 @@ Zotero.Item.prototype._getLastPageIndexSettingKey = function (ignoreInvalid) {
|
|||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the key for the item's lastPageIndex synced setting
|
||||
*
|
||||
* E.g., 'lastPageIndex_u_ABCD2345' or 'lastPageIndex_g123_ABCD2345'
|
||||
*
|
||||
* @param {Boolean} [ignoreInvalid=false]
|
||||
* @return {String | false}
|
||||
*/
|
||||
Zotero.Item.prototype._getLastPageIndexSettingKey = function (ignoreInvalid = false) {
|
||||
return this._getSettingKey('lastPageIndex', ignoreInvalid);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the key for the item's lastRead synced setting
|
||||
*
|
||||
* E.g., 'lastRead_g123_ABCD2345' in a group library.
|
||||
* If this item is in a non-group library and ignoreInvalid isn't true, throws.
|
||||
*
|
||||
* @param {Boolean} [ignoreInvalid=false]
|
||||
* @return {String | false}
|
||||
*/
|
||||
Zotero.Item.prototype._getLastReadSettingKey = function (ignoreInvalid = false) {
|
||||
let library = this.library;
|
||||
if (!library.isGroup) {
|
||||
let msg = `Can't get lastRead key for ${library.libraryType} item`;
|
||||
if (ignoreInvalid) {
|
||||
Zotero.logError(msg);
|
||||
return false;
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
return this._getSettingKey('lastRead', ignoreInvalid);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Modification time of an attachment file
|
||||
*
|
||||
|
|
@ -4050,6 +4146,22 @@ Zotero.Item.prototype.setAutoAttachmentTitle = function ({ forceFirstOfType } =
|
|||
};
|
||||
|
||||
|
||||
Zotero.Item.prototype.getItemLastRead = function () {
|
||||
if (this.isAttachment()) {
|
||||
return this.attachmentLastRead;
|
||||
}
|
||||
else {
|
||||
let max = null;
|
||||
for (let attachment of Zotero.Items.get(this.getAttachments(false))) {
|
||||
if (!max || attachment.attachmentLastRead > max) {
|
||||
max = attachment.attachmentLastRead;
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
//
|
||||
//
|
||||
|
|
@ -5142,6 +5254,13 @@ Zotero.Item.prototype._eraseData = async function (env) {
|
|||
if (id) {
|
||||
await Zotero.SyncedSettings.clear(Zotero.Libraries.userLibraryID, id);
|
||||
}
|
||||
|
||||
// Delete last read synced setting for group items
|
||||
if (this.library.isGroup) {
|
||||
await Zotero.SyncedSettings.clear(
|
||||
Zotero.Libraries.userLibraryID, this._getLastReadSettingKey()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Zotero.Sync.EventListeners.ChangeListener needs to know if this was a storage file
|
||||
|
|
@ -5306,6 +5425,22 @@ Zotero.Item.prototype.fromJSON = function (json, options = {}) {
|
|||
this[field] = val;
|
||||
break;
|
||||
|
||||
case 'lastRead':
|
||||
if (this.libraryID != Zotero.Libraries.userLibraryID) {
|
||||
Zotero.logError(`Discarding invalid ${field} '${val}' for item ${this.libraryKey} (not in user library)`);
|
||||
continue;
|
||||
}
|
||||
if (val) {
|
||||
let i = parseInt(val);
|
||||
if (!Number.isInteger(i)) {
|
||||
Zotero.logError(`Discarding invalid ${field} '${val}' for item ${this.libraryKey}`);
|
||||
continue;
|
||||
}
|
||||
val = i;
|
||||
}
|
||||
this.attachmentLastRead = val;
|
||||
break;
|
||||
|
||||
case 'creators':
|
||||
//this.setCreators(json.creators.concat(extraCreators), options);
|
||||
this.setCreators(json.creators, options);
|
||||
|
|
@ -5585,6 +5720,10 @@ Zotero.Item.prototype.toJSON = function (options = {}) {
|
|||
obj.filename = this.attachmentFilename;
|
||||
}
|
||||
|
||||
if (this.libraryID == Zotero.Libraries.userLibraryID && this.attachmentLastRead) {
|
||||
obj.lastRead = this.attachmentLastRead;
|
||||
}
|
||||
|
||||
if (this.isStoredFileAttachment() && !options.skipStorageProperties) {
|
||||
if (options.syncedStorageProperties) {
|
||||
let mtime = this.attachmentSyncedModificationTime;
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ Zotero.Items = function () {
|
|||
attachmentSyncedModificationTime: "IA.storageModTime AS attachmentSyncedModificationTime",
|
||||
attachmentSyncedHash: "IA.storageHash AS attachmentSyncedHash",
|
||||
attachmentLastProcessedModificationTime: "IA.lastProcessedModificationTime AS attachmentLastProcessedModificationTime",
|
||||
attachmentLastRead: "IA.lastRead AS attachmentLastRead",
|
||||
};
|
||||
}
|
||||
}, {lazy: true});
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ Zotero.Library = function (params = {}) {
|
|||
this._hasSearches = null;
|
||||
this._storageDownloadNeeded = false;
|
||||
|
||||
this._lastReadItemInSession = null;
|
||||
|
||||
Zotero.Utilities.Internal.assignProps(
|
||||
this,
|
||||
params,
|
||||
|
|
@ -126,6 +128,11 @@ Zotero.defineProperty(Zotero.Library.prototype, 'libraryType', {
|
|||
set: function (v) { return this._set('_libraryType', v); }
|
||||
});
|
||||
|
||||
Zotero.defineProperty(Zotero.Library.prototype, 'lastReadItemInSession', {
|
||||
get() { return this._lastReadItemInSession; },
|
||||
set(val) { this._lastReadItemInSession = val; }
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the library-type-specific id for the library (e.g., userID for user library,
|
||||
* groupID for group library)
|
||||
|
|
|
|||
|
|
@ -1469,22 +1469,23 @@ Zotero.Search.prototype._buildQuery = async function () {
|
|||
break;
|
||||
}
|
||||
|
||||
if (!skipOperators){
|
||||
if (!skipOperators) {
|
||||
// Special handling for date fields
|
||||
//
|
||||
// Note: We assume full datetimes are already UTC and don't
|
||||
// need to be handled specially
|
||||
if ((condition['name']=='dateAdded' ||
|
||||
condition['name']=='dateModified' ||
|
||||
condition['name']=='datefield') &&
|
||||
!Zotero.Date.isSQLDateTime(condition['value'])){
|
||||
if ((condition.name == 'dateAdded'
|
||||
|| condition.name == 'dateModified'
|
||||
|| condition.name == 'lastRead'
|
||||
|| condition.name == 'datefield')
|
||||
&& !Zotero.Date.isSQLDateTime(condition.value)) {
|
||||
|
||||
// TODO: document these flags
|
||||
var parseDate = null;
|
||||
var alt = null;
|
||||
var useFreeform = null;
|
||||
|
||||
switch (condition['operator']){
|
||||
switch (condition.operator) {
|
||||
case 'is':
|
||||
case 'isNot':
|
||||
var parseDate = true;
|
||||
|
|
@ -1511,7 +1512,7 @@ Zotero.Search.prototype._buildQuery = async function () {
|
|||
break;
|
||||
|
||||
default:
|
||||
throw ('Invalid date field operator in search');
|
||||
throw new Error('Invalid date field operator in search');
|
||||
}
|
||||
|
||||
// Convert stored UTC dates to localtime
|
||||
|
|
@ -1519,14 +1520,20 @@ Zotero.Search.prototype._buildQuery = async function () {
|
|||
// It'd be nice not to deal with time zones here at all,
|
||||
// but otherwise searching for the date part of a field
|
||||
// stored as UTC that wraps midnight would be unsuccessful
|
||||
if (condition['name']=='dateAdded' ||
|
||||
condition['name']=='dateModified' ||
|
||||
condition['alias']=='accessDate'){
|
||||
condSQL += "DATE(" + condition['field'] + ", 'localtime')";
|
||||
//
|
||||
// lastRead is a UNIX timestamp in seconds, so we need to
|
||||
// explicitly pass 'unixepoch'
|
||||
if (condition.name == 'lastRead') {
|
||||
condSQL += "DATE(" + condition.field + ", 'unixepoch', 'localtime')";
|
||||
}
|
||||
else if (condition.name == 'dateAdded'
|
||||
|| condition.name == 'dateModified'
|
||||
|| condition.alias == 'accessDate') {
|
||||
condSQL += "DATE(" + condition.field + ", 'localtime')";
|
||||
}
|
||||
// Only use first (SQL) part of multipart dates
|
||||
else {
|
||||
condSQL += "SUBSTR(" + condition['field'] + ", 1, 10)";
|
||||
condSQL += "SUBSTR(" + condition.field + ", 1, 10)";
|
||||
}
|
||||
|
||||
if (parseDate){
|
||||
|
|
|
|||
|
|
@ -311,6 +311,19 @@ Zotero.SearchConditions = new function () {
|
|||
field: 'dateModified'
|
||||
},
|
||||
|
||||
{
|
||||
name: 'lastRead',
|
||||
operators: {
|
||||
is: true,
|
||||
isNot: true,
|
||||
isBefore: true,
|
||||
isAfter: true,
|
||||
isInTheLast: true
|
||||
},
|
||||
table: 'itemAttachments',
|
||||
field: 'lastRead'
|
||||
},
|
||||
|
||||
// Deprecated
|
||||
{
|
||||
name: 'itemTypeID',
|
||||
|
|
|
|||
|
|
@ -116,9 +116,10 @@ Zotero.Notifier = new function () {
|
|||
* Possible values:
|
||||
*
|
||||
* event: 'add', 'modify', 'delete', 'move' ('c', for changing parent),
|
||||
* 'remove' (ci, it), 'refresh', 'redraw', 'trash', 'unreadCountUpdated', 'index'
|
||||
* 'remove' (ci, it), 'refresh', 'redraw', 'trash', 'unreadCountUpdated', 'index',
|
||||
* 'pageChange' (file)
|
||||
* type - 'collection', 'search', 'item', 'collection-item', 'item-tag', 'tag',
|
||||
* 'group', 'relation', 'feed', 'feedItem'
|
||||
* 'group', 'relation', 'feed', 'feedItem', 'file'
|
||||
* ids - single id or array of ids
|
||||
*
|
||||
* Notes:
|
||||
|
|
|
|||
|
|
@ -504,8 +504,9 @@ Zotero.Prefs = new function () {
|
|||
const prefKeys = {
|
||||
duplicates: 'duplicateLibraries',
|
||||
unfiled: 'unfiledLibraries',
|
||||
recentlyRead: 'recentlyReadLibraries',
|
||||
retracted: 'retractedLibraries',
|
||||
publications: 'publications',
|
||||
publications: 'publications'
|
||||
};
|
||||
let prefKey = prefKeys[type];
|
||||
if (!prefKey) {
|
||||
|
|
@ -538,6 +539,7 @@ Zotero.Prefs = new function () {
|
|||
const prefKeys = {
|
||||
duplicates: 'duplicateLibraries',
|
||||
unfiled: 'unfiledLibraries',
|
||||
recentlyRead: 'recentlyReadLibraries',
|
||||
retracted: 'retractedLibraries',
|
||||
publications: 'publications'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -970,15 +970,21 @@ class ReaderInstance {
|
|||
async _setState(state) {
|
||||
let item = Zotero.Items.get(this._item.id);
|
||||
if (item) {
|
||||
let lastPageIndex;
|
||||
if (this._type === 'pdf') {
|
||||
item.setAttachmentLastPageIndex(state.pageIndex);
|
||||
lastPageIndex = state.pageIndex;
|
||||
}
|
||||
else if (this._type === 'epub') {
|
||||
item.setAttachmentLastPageIndex(state.cfi);
|
||||
lastPageIndex = state.cfi;
|
||||
}
|
||||
else if (this._type === 'snapshot') {
|
||||
item.setAttachmentLastPageIndex(state.scrollYPercent);
|
||||
lastPageIndex = state.scrollYPercent;
|
||||
}
|
||||
else {
|
||||
throw new Error('Unknown reader type: ' + this._type);
|
||||
}
|
||||
let pageChanged = item.getAttachmentLastPageIndex() != lastPageIndex;
|
||||
item.setAttachmentLastPageIndex(lastPageIndex);
|
||||
let file = Zotero.Attachments.getStorageDirectory(item);
|
||||
if (!(await OS.File.exists(file.path))) {
|
||||
await Zotero.Attachments.createDirectoryForItem(item);
|
||||
|
|
@ -1006,6 +1012,10 @@ class ReaderInstance {
|
|||
await IOUtils.writeJSON(path, state);
|
||||
};
|
||||
this._pendingWriteStateTimeout = setTimeout(this._pendingWriteStateFunction, 5000);
|
||||
|
||||
if (pageChanged) {
|
||||
Zotero.Notifier.trigger('pageChange', 'file', item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3516,6 +3516,11 @@ Zotero.Schema = new function () {
|
|||
else if (i == 123) {
|
||||
await Zotero.DB.queryAsync("CREATE INDEX itemData_valueID ON itemData(valueID)");
|
||||
}
|
||||
|
||||
else if (i == 124) {
|
||||
await Zotero.DB.queryAsync("ALTER TABLE itemAttachments ADD COLUMN lastRead INT");
|
||||
await Zotero.DB.queryAsync("CREATE INDEX itemAttachments_lastRead ON itemAttachments(lastRead)");
|
||||
}
|
||||
|
||||
// If breaking compatibility or doing anything dangerous, clear minorUpdateFrom
|
||||
}
|
||||
|
|
|
|||
|
|
@ -298,6 +298,10 @@ Zotero.SyncedSettings = (function () {
|
|||
var currentValue = this.get(libraryID, setting);
|
||||
var hasCurrentValue = currentValue !== null;
|
||||
|
||||
if (!hasCurrentValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var id = libraryID + '/' + setting;
|
||||
|
||||
var extraData = {};
|
||||
|
|
|
|||
|
|
@ -735,6 +735,7 @@ const { CommandLineOptions } = ChromeUtils.importESModule("chrome://zotero/conte
|
|||
await Zotero.Retractions.init();
|
||||
await Zotero.Dictionaries.init();
|
||||
Zotero.Reader.init();
|
||||
Zotero.AttachmentReadObserver.init();
|
||||
|
||||
// Load all library data except for items, which are loaded when libraries are first
|
||||
// clicked on or if otherwise necessary
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ const xpcomFilesLocal = [
|
|||
'annotations',
|
||||
'api',
|
||||
'attachments',
|
||||
'attachmentReadObserver',
|
||||
'browserDownload',
|
||||
'cite',
|
||||
'citeprocRsBridge',
|
||||
|
|
|
|||
|
|
@ -2302,6 +2302,7 @@ var ZoteroPane = new function () {
|
|||
else if (collectionTreeRow.isLibrary(true)
|
||||
|| collectionTreeRow.isSearch()
|
||||
|| collectionTreeRow.isUnfiled()
|
||||
|| collectionTreeRow.isRecentlyRead()
|
||||
|| collectionTreeRow.isRetracted()
|
||||
|| collectionTreeRow.isDuplicates()) {
|
||||
// In library, don't prompt if meta key was pressed
|
||||
|
|
@ -2408,6 +2409,11 @@ var ZoteroPane = new function () {
|
|||
this.setVirtual(collectionTreeRow.ref.libraryID, 'unfiled', false);
|
||||
return;
|
||||
}
|
||||
// Remove virtual recently read collection
|
||||
else if (collectionTreeRow.isRecentlyRead()) {
|
||||
this.setVirtual(collectionTreeRow.ref.libraryID, 'recentlyRead', false);
|
||||
return;
|
||||
}
|
||||
// Remove virtual retracted collection
|
||||
else if (collectionTreeRow.isRetracted()) {
|
||||
this.setVirtual(collectionTreeRow.ref.libraryID, 'retracted', false);
|
||||
|
|
@ -3336,6 +3342,12 @@ var ZoteroPane = new function () {
|
|||
this.setVirtual(this.getSelectedLibraryID(), 'unfiled', true, true);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "showRecentlyRead",
|
||||
oncommand: () => {
|
||||
this.setVirtual(this.getSelectedLibraryID(), 'recentlyRead', true, true);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "showRetracted",
|
||||
oncommand: () => {
|
||||
|
|
@ -3569,7 +3581,8 @@ var ZoteroPane = new function () {
|
|||
else if (collectionTreeRow.isTrash()) {
|
||||
show = ['emptyTrash'];
|
||||
}
|
||||
else if (collectionTreeRow.isDuplicates() || collectionTreeRow.isUnfiled() || collectionTreeRow.isRetracted()) {
|
||||
else if (collectionTreeRow.isDuplicates() || collectionTreeRow.isUnfiled() || collectionTreeRow.isRecentlyRead()
|
||||
|| collectionTreeRow.isRetracted()) {
|
||||
show = ['deleteCollection'];
|
||||
|
||||
m.deleteCollection.setAttribute('label', Zotero.getString('general.hide'));
|
||||
|
|
@ -3601,13 +3614,16 @@ var ZoteroPane = new function () {
|
|||
let unfiled = Zotero.Prefs.getVirtualCollectionStateForLibrary(
|
||||
libraryID, 'unfiled'
|
||||
);
|
||||
let recentlyRead = Zotero.Prefs.getVirtualCollectionStateForLibrary(
|
||||
libraryID, 'recentlyRead'
|
||||
);
|
||||
let retracted = Zotero.Prefs.getVirtualCollectionStateForLibrary(
|
||||
libraryID, 'retracted'
|
||||
);
|
||||
let publications = Zotero.Prefs.getVirtualCollectionStateForLibrary(
|
||||
libraryID, 'publications'
|
||||
);
|
||||
if (!duplicates || !unfiled || !retracted || !publications) {
|
||||
if (!duplicates || !unfiled || !recentlyRead || !retracted || !publications) {
|
||||
if (!library.archived) {
|
||||
show.push('sep2');
|
||||
}
|
||||
|
|
@ -3617,6 +3633,9 @@ var ZoteroPane = new function () {
|
|||
if (!unfiled) {
|
||||
show.push('showUnfiled');
|
||||
}
|
||||
if (!recentlyRead) {
|
||||
show.push('showRecentlyRead');
|
||||
}
|
||||
if (!retracted) {
|
||||
show.push('showRetracted');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -939,6 +939,7 @@
|
|||
<menuseparator/>
|
||||
<menuitem class="zotero-menuitem-show-duplicates" label="&zotero.toolbar.duplicate.label;"/>
|
||||
<menuitem class="zotero-menuitem-show-unfiled" label="&zotero.collections.showUnfiledItems;"/>
|
||||
<menuitem class="zotero-menuitem-show-recently-read" data-l10n-id="collections-menu-show-recently-read"/>
|
||||
<menuitem class="zotero-menuitem-show-retracted" label="&zotero.collections.showRetractedItems;"/>
|
||||
<menuitem class="zotero-menuitem-show-publications" data-l10n-id="show-publications-menuitem" />
|
||||
<menuitem class="zotero-menuitem-edit-collection"/>
|
||||
|
|
|
|||
|
|
@ -179,6 +179,8 @@ zotero-toolbar-tabs-scroll-backwards =
|
|||
toolbar-add-attachment =
|
||||
.tooltiptext = { add-attachment }
|
||||
|
||||
collections-menu-show-recently-read =
|
||||
.label = Show Recently Read
|
||||
collections-menu-rename-collection =
|
||||
.label = Rename Collection
|
||||
collections-menu-edit-saved-search =
|
||||
|
|
|
|||
|
|
@ -273,6 +273,7 @@ pane.collections.feedLibraries = Feeds
|
|||
pane.collections.trash = Trash
|
||||
pane.collections.untitled = Untitled
|
||||
pane.collections.unfiled = Unfiled Items
|
||||
pane.collections.recentlyRead = Recently Read
|
||||
pane.collections.retracted = Retracted Items
|
||||
pane.collections.duplicate = Duplicate Items
|
||||
pane.collections.removeLibrary = Remove Library
|
||||
|
|
@ -374,6 +375,7 @@ pane.items.menu.duplicateAndConvert.toBookSection = Create Book Section
|
|||
pane.items.menu.duplicateAndConvert.toBook = Create Book from Book Section
|
||||
pane.items.menu.showInFeed = Show in Feed
|
||||
pane.items.showItemInLibrary = Show Item in Library
|
||||
pane.items.columns.lastRead = Last Read
|
||||
|
||||
pane.items.letter.oneParticipant = Letter to %S
|
||||
pane.items.letter.twoParticipants = Letter to %S and %S
|
||||
|
|
@ -838,6 +840,7 @@ searchConditions.dateModified = Date Modified
|
|||
searchConditions.fulltextContent = Attachment Content
|
||||
searchConditions.programmingLanguage = Programming Language
|
||||
searchConditions.fileTypeID = Attachment File Type
|
||||
searchConditions.lastRead = Attachment Last Read
|
||||
searchConditions.annotationText = Annotation Text
|
||||
searchConditions.annotationComment = Annotation Comment
|
||||
searchConditions.anyField = Any Field
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2289_29)">
|
||||
<path d="M7.9996 0C12.4179 0 15.9996 3.58172 15.9996 8C15.9996 12.4183 12.4179 16 7.9996 16C3.58151 15.9998 -0.000396729 12.4181 -0.000396729 8C-0.000396729 3.58185 3.58151 0.000214404 7.9996 0ZM7.9996 1C4.13379 1.00021 0.999603 4.13414 0.999603 8C0.999603 11.8659 4.13379 14.9998 7.9996 15C11.8656 15 14.9996 11.866 14.9996 8C14.9996 4.13401 11.8656 1 7.9996 1ZM7.9996 8H11.9996V9H6.9996V3H7.9996V8Z" fill="context-fill"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2289_29">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 669 B |
|
|
@ -1,4 +1,4 @@
|
|||
-- 123
|
||||
-- 124
|
||||
|
||||
-- Copyright (c) 2009 Center for History and New Media
|
||||
-- George Mason University, Fairfax, Virginia, USA
|
||||
|
|
@ -211,6 +211,7 @@ CREATE TABLE itemAttachments (
|
|||
storageModTime INT,
|
||||
storageHash TEXT,
|
||||
lastProcessedModificationTime INT,
|
||||
lastRead INT,
|
||||
FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (parentItemID) REFERENCES items(itemID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (charsetID) REFERENCES charsets(charsetID) ON DELETE SET NULL
|
||||
|
|
@ -220,6 +221,7 @@ CREATE INDEX itemAttachments_charsetID ON itemAttachments(charsetID);
|
|||
CREATE INDEX itemAttachments_contentType ON itemAttachments(contentType);
|
||||
CREATE INDEX itemAttachments_syncState ON itemAttachments(syncState);
|
||||
CREATE INDEX itemAttachments_lastProcessedModificationTime ON itemAttachments(lastProcessedModificationTime);
|
||||
CREATE INDEX itemAttachments_lastRead ON itemAttachments(lastRead);
|
||||
|
||||
CREATE TABLE itemAnnotations (
|
||||
itemID INTEGER PRIMARY KEY,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
$icons: (
|
||||
// Themed icons -- separate SVGs per color in dark/, light/, white/
|
||||
$themed-icons: (
|
||||
collection, duplicates, feed, feed-error, feed-library, feed-updating, groups, library,
|
||||
library-group, publications, trash-full, trash, unfiled, retracted, search,
|
||||
);
|
||||
|
||||
@mixin -icon-collection-type-rules {
|
||||
@each $icon in $icons {
|
||||
// Universal icons -- single SVG with context-fill, colored via CSS
|
||||
// Map of icon name to fill color variable
|
||||
$universal-icons: (
|
||||
recent: --tag-purple,
|
||||
);
|
||||
|
||||
@mixin -themed-icon-rules {
|
||||
@each $icon in $themed-icons {
|
||||
.icon-css.icon-#{$icon} {
|
||||
@include focus-states using ($color) {
|
||||
@include svgicon($icon, $color, "16", "collection-tree");
|
||||
|
|
@ -13,15 +20,37 @@ $icons: (
|
|||
}
|
||||
}
|
||||
|
||||
// Universal icon rules
|
||||
@include focus-states using ($color) {
|
||||
@include -icon-collection-type-rules;
|
||||
@mixin -universal-icon-rules {
|
||||
@each $icon, $fill in $universal-icons {
|
||||
.icon-css.icon-#{$icon} {
|
||||
@include svgicon($icon, "universal", "16", "collection-tree");
|
||||
@include focus-states using ($color) {
|
||||
@if $color == "white" {
|
||||
fill: white;
|
||||
}
|
||||
@else {
|
||||
fill: var(#{$fill});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include focus-states using ($color) {
|
||||
@include -themed-icon-rules;
|
||||
}
|
||||
|
||||
// Selection and focus aware icon rules
|
||||
.focus-states-target {
|
||||
.row {
|
||||
@include -icon-collection-type-rules;
|
||||
@include -themed-icon-rules;
|
||||
}
|
||||
}
|
||||
|
||||
@include -universal-icon-rules;
|
||||
|
||||
.focus-states-target {
|
||||
.row {
|
||||
@include -universal-icon-rules;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +89,7 @@ $icons: (
|
|||
height: 16px;
|
||||
}
|
||||
|
||||
@each $icon in $icons {
|
||||
@each $icon in $themed-icons {
|
||||
.icon-css.icon-#{$icon} {
|
||||
@include focus-states(
|
||||
".row.selected",
|
||||
|
|
|
|||
73
test/tests/attachmentReadObserverTest.js
Normal file
73
test/tests/attachmentReadObserverTest.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
"use strict";
|
||||
|
||||
describe("Zotero.AttachmentReadObserver", function () {
|
||||
describe("file events", function () {
|
||||
beforeEach(function () {
|
||||
Zotero.Libraries.userLibrary.lastReadItemInSession = null;
|
||||
});
|
||||
|
||||
it("should update an attachment's attachmentLastRead every time it is opened", async function () {
|
||||
let attachment = await importPDFAttachment(null);
|
||||
|
||||
// We open the attachment at midnight on January 1
|
||||
let stub = sinon.stub(Zotero.AttachmentReadObserver, '_getCurrentDate')
|
||||
.callsFake(() => new Date(2023, 1, 1, 0, 0, 0));
|
||||
await Zotero.Notifier.trigger('open', 'file', [attachment.id]);
|
||||
let initialLastRead = attachment.attachmentLastRead;
|
||||
assert.isNumber(initialLastRead);
|
||||
|
||||
// We open it again, only five seconds later
|
||||
stub.callsFake(() => new Date(2023, 1, 1, 0, 0, 5));
|
||||
await Zotero.Notifier.trigger('open', 'file', [attachment.id]);
|
||||
assert.isAbove(attachment.attachmentLastRead, initialLastRead);
|
||||
|
||||
stub.restore();
|
||||
});
|
||||
|
||||
it("should update an attachment's attachmentLastRead every five minutes when the page changes", async function () {
|
||||
let attachment = await importPDFAttachment(null);
|
||||
|
||||
// We open the attachment at midnight on January 1
|
||||
let stub = sinon.stub(Zotero.AttachmentReadObserver, '_getCurrentDate')
|
||||
.callsFake(() => new Date(2023, 1, 1, 0, 0, 0));
|
||||
await Zotero.Notifier.trigger('open', 'file', [attachment.id]);
|
||||
let initialLastRead = attachment.attachmentLastRead;
|
||||
assert.isNumber(initialLastRead);
|
||||
|
||||
// We change pages, only five seconds later
|
||||
stub.callsFake(() => new Date(2023, 1, 1, 0, 0, 5));
|
||||
await Zotero.Notifier.trigger('pageChange', 'file', [attachment.id]);
|
||||
assert.equal(attachment.attachmentLastRead, initialLastRead);
|
||||
|
||||
// We change pages again, five minutes later
|
||||
stub.callsFake(() => new Date(2023, 1, 1, 0, 5, 5));
|
||||
await Zotero.Notifier.trigger('pageChange', 'file', [attachment.id]);
|
||||
let updatedLastRead = attachment.attachmentLastRead;
|
||||
assert.isAbove(updatedLastRead, initialLastRead);
|
||||
|
||||
// We change pages again, a minute after that
|
||||
stub.callsFake(() => new Date(2023, 1, 1, 0, 6, 5));
|
||||
await Zotero.Notifier.trigger('pageChange', 'file', [attachment.id]);
|
||||
assert.equal(attachment.attachmentLastRead, updatedLastRead);
|
||||
|
||||
stub.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setting events", function () {
|
||||
it("should update a group attachment's attachmentLastRead when the associated synced setting changes", async function () {
|
||||
let group = await createGroup();
|
||||
let item = await createDataObject('item', { libraryID: group.libraryID });
|
||||
let attachment = await importPDFAttachment(item);
|
||||
let key = attachment._getLastReadSettingKey();
|
||||
|
||||
let firstValue = 1674668000;
|
||||
await Zotero.SyncedSettings.set(Zotero.Libraries.userLibraryID, key, firstValue);
|
||||
assert.equal(attachment.attachmentLastRead, firstValue);
|
||||
|
||||
let secondValue = 1674668123;
|
||||
await Zotero.SyncedSettings.set(Zotero.Libraries.userLibraryID, key, secondValue);
|
||||
assert.equal(attachment.attachmentLastRead, secondValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -604,8 +604,8 @@ describe("Zotero.CollectionTree", function () {
|
|||
await createDataObject('collection', { libraryID: group.libraryID });
|
||||
await select(win, c2);
|
||||
|
||||
// Group, collections, Duplicates, Unfiled, and trash
|
||||
assert.equal(cv._rows.length, originalRowCount + 9);
|
||||
// Group, collections, Recently Read, Duplicates, Unfiled, and trash
|
||||
assert.equal(cv._rows.length, originalRowCount + 10);
|
||||
|
||||
// Select group
|
||||
await cv.selectLibrary(group.libraryID);
|
||||
|
|
|
|||
|
|
@ -2159,6 +2159,20 @@ describe("Zotero.Item", function () {
|
|||
|
||||
assert.equal(Zotero.Users.getCurrentName(), username);
|
||||
});
|
||||
|
||||
it("should save a group attachment's attachmentLastRead to the database", async function () {
|
||||
let group = await createGroup();
|
||||
let libraryID = group.libraryID;
|
||||
let item = await createDataObject('item', { libraryID });
|
||||
let attachment = await importPDFAttachment(item);
|
||||
attachment.attachmentLastRead = 1674668111;
|
||||
await attachment.saveTx();
|
||||
|
||||
let dbVal = await Zotero.DB.valueQueryAsync(
|
||||
"SELECT lastRead FROM itemAttachments WHERE itemID=?", attachment.id
|
||||
);
|
||||
assert.equal(dbVal, attachment.attachmentLastRead);
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
|
|
@ -2457,6 +2471,23 @@ describe("Zotero.Item", function () {
|
|||
assert.notProperty(json, 'charset');
|
||||
assert.notProperty(json, 'path');
|
||||
});
|
||||
|
||||
it("should output lastRead for a user library item", async function () {
|
||||
let attachment = await createDataObject('item', { itemType: 'attachment' });
|
||||
attachment.attachmentLastRead = 123450000;
|
||||
let json = attachment.toJSON();
|
||||
assert.equal(json.lastRead, 123450000);
|
||||
});
|
||||
|
||||
it("shouldn't output lastRead for a group item", async function () {
|
||||
let group = await createGroup();
|
||||
let attachment = await createDataObject('item', { itemType: 'attachment', libraryID: group.libraryID });
|
||||
attachment.attachmentLastRead = 123450000;
|
||||
assert.equal(attachment.attachmentLastRead, 123450000);
|
||||
let json = attachment.toJSON();
|
||||
assert.notProperty(json, 'lastRead');
|
||||
await group.eraseTx();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Annotations", function () {
|
||||
|
|
|
|||
|
|
@ -1005,6 +1005,60 @@ describe("Zotero.ItemTree", function () {
|
|||
});
|
||||
});
|
||||
|
||||
it("should re-sort by Last Read when child attachmentLastRead is updated in the user library", async function () {
|
||||
let userLibraryID = Zotero.Libraries.userLibraryID;
|
||||
let item1 = await createDataObject('item', { libraryID: userLibraryID });
|
||||
let attachment1 = await importPDFAttachment(item1);
|
||||
let item2 = await createDataObject('item', { libraryID: userLibraryID });
|
||||
let attachment2 = await importPDFAttachment(item2);
|
||||
assert.notOk(item1.getItemLastRead());
|
||||
assert.notOk(item2.getItemLastRead());
|
||||
|
||||
// attachment2 is more recently opened
|
||||
attachment1.attachmentLastRead = Math.round(Date.now() / 1000) - 5;
|
||||
attachment2.attachmentLastRead = Math.round(Date.now() / 1000);
|
||||
await attachment1.saveTx();
|
||||
await attachment2.saveTx();
|
||||
|
||||
await zp.setVirtual(userLibraryID, 'recentlyRead', true, true);
|
||||
assert.equal(zp.getCollectionTreeRow().id, 'Y' + userLibraryID);
|
||||
await waitForItemsLoad(win);
|
||||
assert.isAbove(zp.itemsView.getRowIndexByID(item1.id), zp.itemsView.getRowIndexByID(item2.id));
|
||||
|
||||
// Now make attachment2 much less recently opened
|
||||
attachment2.attachmentLastRead = Math.round(Date.now() / 1000) - 60;
|
||||
await attachment2.saveTx();
|
||||
|
||||
assert.isBelow(zp.itemsView.getRowIndexByID(item1.id), zp.itemsView.getRowIndexByID(item2.id));
|
||||
});
|
||||
|
||||
it("should re-sort by Last Read when child attachmentLastRead is updated in a group library", async function () {
|
||||
let groupLibraryID = (await createGroup()).libraryID;
|
||||
let item1 = await createDataObject('item', { libraryID: groupLibraryID });
|
||||
let attachment1 = await importPDFAttachment(item1);
|
||||
let item2 = await createDataObject('item', { libraryID: groupLibraryID });
|
||||
let attachment2 = await importPDFAttachment(item2);
|
||||
assert.notOk(item1.getItemLastRead());
|
||||
assert.notOk(item2.getItemLastRead());
|
||||
|
||||
// attachment2 is more recently opened
|
||||
attachment1.attachmentLastRead = Math.round(Date.now() / 1000) - 5;
|
||||
attachment2.attachmentLastRead = Math.round(Date.now() / 1000);
|
||||
await attachment1.saveTx();
|
||||
await attachment2.saveTx();
|
||||
|
||||
await zp.setVirtual(groupLibraryID, 'recentlyRead', true, true);
|
||||
assert.equal(zp.getCollectionTreeRow().id, 'Y' + groupLibraryID);
|
||||
await waitForItemsLoad(win);
|
||||
assert.isAbove(zp.itemsView.getRowIndexByID(item1.id), zp.itemsView.getRowIndexByID(item2.id));
|
||||
|
||||
// Now make attachment2 much less recently opened
|
||||
attachment2.attachmentLastRead = Math.round(Date.now() / 1000) - 60;
|
||||
await attachment2.saveTx();
|
||||
|
||||
assert.isBelow(zp.itemsView.getRowIndexByID(item1.id), zp.itemsView.getRowIndexByID(item2.id));
|
||||
});
|
||||
|
||||
describe("Trash", function () {
|
||||
it("should remove untrashed parent item when last trashed child is deleted", async function () {
|
||||
var item = await createDataObject('item');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue