From 14173f5a64c92eadd734384d5f38dfffd37f64ad Mon Sep 17 00:00:00 2001 From: Eitot Date: Thu, 18 Jul 2024 20:49:04 +0200 Subject: [PATCH 01/14] Restore ArticleFieldIDCreatedDate enum case This partially reverts dbae95034af27391f76df5cffa591017ca2e1779. With the ArticleFieldIDCreatedDate case restored, the explicit value of 415 for ArticleFieldIDEnclosure is no longer needed. --- Vienna/Sources/Models/Article.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Vienna/Sources/Models/Article.h b/Vienna/Sources/Models/Article.h index 88bceabde4..9026f2a4d1 100644 --- a/Vienna/Sources/Models/Article.h +++ b/Vienna/Sources/Models/Article.h @@ -61,8 +61,8 @@ typedef NS_ENUM(NSInteger, ArticleFieldID) { ArticleFieldIDHeadlines = 411, ArticleFieldIDDeleted, ArticleFieldIDSummary, - /* 414 was previously used */ - ArticleFieldIDEnclosure = 415, + ArticleFieldIDCreatedDate, + ArticleFieldIDEnclosure, ArticleFieldIDEnclosureDownloaded, ArticleFieldIDHasEnclosure }; From e8b4922cc887ae4bdfe79255048d0a05a03d8b77 Mon Sep 17 00:00:00 2001 From: Eitot Date: Thu, 18 Jul 2024 20:56:23 +0200 Subject: [PATCH 02/14] Add article sorting and a table column for the createdDate SQL field --- Vienna/Sources/Database/Database.m | 2 ++ Vienna/Sources/Main window/ArticleController.m | 9 ++++++++- Vienna/Sources/Main window/ArticleListView.m | 2 ++ Vienna/Sources/Models/Article.m | 2 ++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Vienna/Sources/Database/Database.m b/Vienna/Sources/Database/Database.m index 7bf3f3436c..fa26389dd2 100644 --- a/Vienna/Sources/Database/Database.m +++ b/Vienna/Sources/Database/Database.m @@ -336,6 +336,7 @@ -(void)initaliseFields { [self addField:MA_Field_Subject type:VNAFieldTypeString tag:ArticleFieldIDSubject sqlField:@"title" visible:YES width:472]; [self addField:MA_Field_Folder type:VNAFieldTypeFolder tag:ArticleFieldIDFolder sqlField:@"folder_id" visible:NO width:130]; [self addField:MA_Field_Date type:VNAFieldTypeDate tag:ArticleFieldIDDate sqlField:@"date" visible:YES width:152]; + [self addField:MA_Field_CreatedDate type:VNAFieldTypeDate tag:ArticleFieldIDCreatedDate sqlField:@"createddate" visible:NO width:152]; [self addField:MA_Field_Parent type:VNAFieldTypeInteger tag:ArticleFieldIDParent sqlField:@"parent_id" visible:NO width:72]; [self addField:MA_Field_Author type:VNAFieldTypeString tag:ArticleFieldIDAuthor sqlField:@"sender" visible:YES width:138]; [self addField:MA_Field_Link type:VNAFieldTypeString tag:ArticleFieldIDLink sqlField:@"link" visible:NO width:138]; @@ -354,6 +355,7 @@ -(void)initaliseFields { [self fieldByName:MA_Field_Subject].displayName = NSLocalizedString(@"Subject", @"Data field name visible in menu/article list/smart folder definition"); [self fieldByName:MA_Field_Folder].displayName = NSLocalizedString(@"Folder", @"Data field name visible in menu/article list/smart folder definition"); [self fieldByName:MA_Field_Date].displayName = NSLocalizedString(@"Date", @"Data field name visible in menu/article list/smart folder definition"); + [self fieldByName:MA_Field_CreatedDate].displayName = NSLocalizedString(@"Date Added", @"Data field name visible in menu/article list/smart folder definition"); [self fieldByName:MA_Field_Author].displayName = NSLocalizedString(@"Author", @"Data field name visible in menu/article list/smart folder definition"); [self fieldByName:MA_Field_Text].displayName = NSLocalizedString(@"Text", @"Data field name visible in smart folder definition"); [self fieldByName:MA_Field_Summary].displayName = NSLocalizedString(@"Summary", @"Pseudo field name visible in menu/article list"); diff --git a/Vienna/Sources/Main window/ArticleController.m b/Vienna/Sources/Main window/ArticleController.m index 1d6bbf48da..8a44cdfbd8 100644 --- a/Vienna/Sources/Main window/ArticleController.m +++ b/Vienna/Sources/Main window/ArticleController.m @@ -97,6 +97,10 @@ -(instancetype)init @"key": [@"articleData." stringByAppendingString:MA_Field_Date], @"selector": NSStringFromSelector(@selector(compare:)) }, + MA_Field_CreatedDate: @{ + @"key": [@"articleData." stringByAppendingString:MA_Field_CreatedDate], + @"selector": NSStringFromSelector(@selector(compare:)) + }, MA_Field_Author: @{ @"key": [@"articleData." stringByAppendingString:MA_Field_Author], @"selector": NSStringFromSelector(@selector(caseInsensitiveCompare:)) @@ -288,7 +292,10 @@ -(void)sortByIdentifier:(NSString *)columnName NSUInteger index = [[descriptors valueForKey:@"key"] indexOfObject:[specifier valueForKey:@"key"]]; if (index == NSNotFound) { - sortDescriptor = [[NSSortDescriptor alloc] initWithKey:[specifier valueForKey:@"key"] ascending:YES selector:NSSelectorFromString([specifier valueForKey:@"selector"])]; + // Date should be sorted in descending order. + // TODO: Add a key to articleSortSpecifiers for a default sort order + BOOL ascending = [columnName isEqualToString:MA_Field_CreatedDate] ? NO : YES; + sortDescriptor = [[NSSortDescriptor alloc] initWithKey:[specifier valueForKey:@"key"] ascending:ascending selector:NSSelectorFromString([specifier valueForKey:@"selector"])]; } else { sortDescriptor = descriptors[index]; [descriptors removeObjectAtIndex:index]; diff --git a/Vienna/Sources/Main window/ArticleListView.m b/Vienna/Sources/Main window/ArticleListView.m index 755107cd7e..159ac380b3 100644 --- a/Vienna/Sources/Main window/ArticleListView.m +++ b/Vienna/Sources/Main window/ArticleListView.m @@ -1309,6 +1309,8 @@ -(id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColum NSString * cellString; if ([identifier isEqualToString:MA_Field_Date]) { cellString = [NSDateFormatter vna_relativeDateStringFromDate:theArticle.date]; + } else if ([identifier isEqualToString:MA_Field_CreatedDate]) { + cellString = [NSDateFormatter vna_relativeDateStringFromDate:theArticle.createdDate]; } else if ([identifier isEqualToString:MA_Field_Folder]) { Folder * folder = [db folderFromID:theArticle.folderId]; cellString = folder.name; diff --git a/Vienna/Sources/Models/Article.m b/Vienna/Sources/Models/Article.m index 19437473c8..f85538ab39 100644 --- a/Vienna/Sources/Models/Article.m +++ b/Vienna/Sources/Models/Article.m @@ -200,6 +200,8 @@ -(id)valueForKeyPath:(NSString *)keyPath NSString * key = [keyPath substringFromIndex:(@"articleData.").length]; if ([key isEqualToString:MA_Field_Date]) { return self.date; + } else if ([key isEqualToString:MA_Field_CreatedDate]) { + return self.createdDate; } else if ([key isEqualToString:MA_Field_Author]) { return self.author; } else if ([key isEqualToString:MA_Field_Subject]) { From a0de3e03a2461acb864d851545cb01d04bb19510 Mon Sep 17 00:00:00 2001 From: Eitot Date: Thu, 18 Jul 2024 21:01:20 +0200 Subject: [PATCH 03/14] Rename ArticleFieldID enum to VNAArticleFieldTag --- Vienna/Sources/Application/AppController.m | 30 +++++++-------- Vienna/Sources/Database/Database.m | 34 ++++++++--------- Vienna/Sources/Main window/ArticleListView.m | 8 ++-- Vienna/Sources/Models/Article.h | 39 ++++++++++---------- 4 files changed, 55 insertions(+), 56 deletions(-) diff --git a/Vienna/Sources/Application/AppController.m b/Vienna/Sources/Application/AppController.m index c44e0a3017..3563722474 100644 --- a/Vienna/Sources/Application/AppController.m +++ b/Vienna/Sources/Application/AppController.m @@ -1143,15 +1143,15 @@ -(void)initSortMenu for (Field * field in [db arrayOfFields]) { // Filter out columns we don't sort on. Later we should have an attribute in the // field object itself based on which columns we can sort on. - if (field.tag != ArticleFieldIDParent && - field.tag != ArticleFieldIDGUID && - field.tag != ArticleFieldIDDeleted && - field.tag != ArticleFieldIDHeadlines && - field.tag != ArticleFieldIDSummary && - field.tag != ArticleFieldIDLink && - field.tag != ArticleFieldIDText && - field.tag != ArticleFieldIDEnclosureDownloaded && - field.tag != ArticleFieldIDEnclosure) + if (field.tag != VNAArticleFieldTagParent && + field.tag != VNAArticleFieldTagGUID && + field.tag != VNAArticleFieldTagDeleted && + field.tag != VNAArticleFieldTagHeadlines && + field.tag != VNAArticleFieldTagSummary && + field.tag != VNAArticleFieldTagLink && + field.tag != VNAArticleFieldTagText && + field.tag != VNAArticleFieldTagEnclosureDownloaded && + field.tag != VNAArticleFieldTagEnclosure) { NSMenuItem * menuItem = [[NSMenuItem alloc] initWithTitle:field.displayName action:@selector(doSortColumn:) keyEquivalent:@""]; menuItem.representedObject = field; @@ -1184,12 +1184,12 @@ -(void)initColumnsMenu for (Field * field in [db arrayOfFields]) { // Filter out columns we don't view in the article list. Later we should have an attribute in the // field object based on which columns are visible in the tableview. - if (field.tag != ArticleFieldIDText && - field.tag != ArticleFieldIDGUID && - field.tag != ArticleFieldIDDeleted && - field.tag != ArticleFieldIDParent && - field.tag != ArticleFieldIDHeadlines && - field.tag != ArticleFieldIDEnclosureDownloaded) + if (field.tag != VNAArticleFieldTagText && + field.tag != VNAArticleFieldTagGUID && + field.tag != VNAArticleFieldTagDeleted && + field.tag != VNAArticleFieldTagParent && + field.tag != VNAArticleFieldTagHeadlines && + field.tag != VNAArticleFieldTagEnclosureDownloaded) { NSMenuItem * menuItem = [[NSMenuItem alloc] initWithTitle:field.displayName action:@selector(doViewColumn:) keyEquivalent:@""]; menuItem.representedObject = field; diff --git a/Vienna/Sources/Database/Database.m b/Vienna/Sources/Database/Database.m index fa26389dd2..a1c0d5560d 100644 --- a/Vienna/Sources/Database/Database.m +++ b/Vienna/Sources/Database/Database.m @@ -328,23 +328,23 @@ -(void)initaliseFields { self.fieldsByName = [[NSMutableDictionary alloc] init]; self.fieldsOrdered = [[NSMutableArray alloc] init]; - [self addField:MA_Field_Read type:VNAFieldTypeFlag tag:ArticleFieldIDRead sqlField:@"read_flag" visible:YES width:17]; - [self addField:MA_Field_Flagged type:VNAFieldTypeFlag tag:ArticleFieldIDFlagged sqlField:@"marked_flag" visible:YES width:17]; - [self addField:MA_Field_HasEnclosure type:VNAFieldTypeFlag tag:ArticleFieldIDHasEnclosure sqlField:@"hasenclosure_flag" visible:YES width:17]; - [self addField:MA_Field_Deleted type:VNAFieldTypeFlag tag:ArticleFieldIDDeleted sqlField:@"deleted_flag" visible:NO width:15]; - [self addField:MA_Field_GUID type:VNAFieldTypeInteger tag:ArticleFieldIDGUID sqlField:@"message_id" visible:NO width:72]; - [self addField:MA_Field_Subject type:VNAFieldTypeString tag:ArticleFieldIDSubject sqlField:@"title" visible:YES width:472]; - [self addField:MA_Field_Folder type:VNAFieldTypeFolder tag:ArticleFieldIDFolder sqlField:@"folder_id" visible:NO width:130]; - [self addField:MA_Field_Date type:VNAFieldTypeDate tag:ArticleFieldIDDate sqlField:@"date" visible:YES width:152]; - [self addField:MA_Field_CreatedDate type:VNAFieldTypeDate tag:ArticleFieldIDCreatedDate sqlField:@"createddate" visible:NO width:152]; - [self addField:MA_Field_Parent type:VNAFieldTypeInteger tag:ArticleFieldIDParent sqlField:@"parent_id" visible:NO width:72]; - [self addField:MA_Field_Author type:VNAFieldTypeString tag:ArticleFieldIDAuthor sqlField:@"sender" visible:YES width:138]; - [self addField:MA_Field_Link type:VNAFieldTypeString tag:ArticleFieldIDLink sqlField:@"link" visible:NO width:138]; - [self addField:MA_Field_Text type:VNAFieldTypeString tag:ArticleFieldIDText sqlField:@"text" visible:NO width:152]; - [self addField:MA_Field_Summary type:VNAFieldTypeString tag:ArticleFieldIDSummary sqlField:@"summary" visible:NO width:152]; - [self addField:MA_Field_Headlines type:VNAFieldTypeString tag:ArticleFieldIDHeadlines sqlField:@"" visible:NO width:100]; - [self addField:MA_Field_Enclosure type:VNAFieldTypeString tag:ArticleFieldIDEnclosure sqlField:@"enclosure" visible:NO width:100]; - [self addField:MA_Field_EnclosureDownloaded type:VNAFieldTypeFlag tag:ArticleFieldIDEnclosureDownloaded sqlField:@"enclosuredownloaded_flag" visible:NO width:100]; + [self addField:MA_Field_Read type:VNAFieldTypeFlag tag:VNAArticleFieldTagRead sqlField:@"read_flag" visible:YES width:17]; + [self addField:MA_Field_Flagged type:VNAFieldTypeFlag tag:VNAArticleFieldTagFlagged sqlField:@"marked_flag" visible:YES width:17]; + [self addField:MA_Field_HasEnclosure type:VNAFieldTypeFlag tag:VNAArticleFieldTagHasEnclosure sqlField:@"hasenclosure_flag" visible:YES width:17]; + [self addField:MA_Field_Deleted type:VNAFieldTypeFlag tag:VNAArticleFieldTagDeleted sqlField:@"deleted_flag" visible:NO width:15]; + [self addField:MA_Field_GUID type:VNAFieldTypeInteger tag:VNAArticleFieldTagGUID sqlField:@"message_id" visible:NO width:72]; + [self addField:MA_Field_Subject type:VNAFieldTypeString tag:VNAArticleFieldTagSubject sqlField:@"title" visible:YES width:472]; + [self addField:MA_Field_Folder type:VNAFieldTypeFolder tag:VNAArticleFieldTagFolder sqlField:@"folder_id" visible:NO width:130]; + [self addField:MA_Field_Date type:VNAFieldTypeDate tag:VNAArticleFieldTagDate sqlField:@"date" visible:YES width:152]; + [self addField:MA_Field_CreatedDate type:VNAFieldTypeDate tag:VNAArticleFieldTagCreatedDate sqlField:@"createddate" visible:NO width:152]; + [self addField:MA_Field_Parent type:VNAFieldTypeInteger tag:VNAArticleFieldTagParent sqlField:@"parent_id" visible:NO width:72]; + [self addField:MA_Field_Author type:VNAFieldTypeString tag:VNAArticleFieldTagAuthor sqlField:@"sender" visible:YES width:138]; + [self addField:MA_Field_Link type:VNAFieldTypeString tag:VNAArticleFieldTagLink sqlField:@"link" visible:NO width:138]; + [self addField:MA_Field_Text type:VNAFieldTypeString tag:VNAArticleFieldTagText sqlField:@"text" visible:NO width:152]; + [self addField:MA_Field_Summary type:VNAFieldTypeString tag:VNAArticleFieldTagSummary sqlField:@"summary" visible:NO width:152]; + [self addField:MA_Field_Headlines type:VNAFieldTypeString tag:VNAArticleFieldTagHeadlines sqlField:@"" visible:NO width:100]; + [self addField:MA_Field_Enclosure type:VNAFieldTypeString tag:VNAArticleFieldTagEnclosure sqlField:@"enclosure" visible:NO width:100]; + [self addField:MA_Field_EnclosureDownloaded type:VNAFieldTypeFlag tag:VNAArticleFieldTagEnclosureDownloaded sqlField:@"enclosuredownloaded_flag" visible:NO width:100]; //set user friendly and localizable names for some fields [self fieldByName:MA_Field_Read].displayName = NSLocalizedString(@"Read", @"Data field name visible in menu/smart folder definition"); diff --git a/Vienna/Sources/Main window/ArticleListView.m b/Vienna/Sources/Main window/ArticleListView.m index 159ac380b3..968671c295 100644 --- a/Vienna/Sources/Main window/ArticleListView.m +++ b/Vienna/Sources/Main window/ArticleListView.m @@ -386,13 +386,13 @@ -(void)updateVisibleColumns // Handle which fields can be visible in the condensed (vertical) layout // versus the report (horizontal) layout if (tableLayout == VNALayoutReport) { - showField = field.visible && tag != ArticleFieldIDHeadlines; + showField = field.visible && tag != VNAArticleFieldTagHeadlines; } else { showField = NO; - if (tag == ArticleFieldIDRead || tag == ArticleFieldIDFlagged || tag == ArticleFieldIDHasEnclosure) { + if (tag == VNAArticleFieldTagRead || tag == VNAArticleFieldTagFlagged || tag == VNAArticleFieldTagHasEnclosure) { showField = field.visible; } - if (tag == ArticleFieldIDHeadlines) { + if (tag == VNAArticleFieldTagHeadlines) { showField = YES; } } @@ -430,7 +430,7 @@ -(void)updateVisibleColumns column.dataCell = cell; } - BOOL isResizable = (tag != ArticleFieldIDRead && tag != ArticleFieldIDFlagged && tag != ArticleFieldIDHasEnclosure); + BOOL isResizable = (tag != VNAArticleFieldTagRead && tag != VNAArticleFieldTagFlagged && tag != VNAArticleFieldTagHasEnclosure); column.resizingMask = (isResizable ? NSTableColumnUserResizingMask : NSTableColumnNoResizing); // the headline column is auto-resizable column.resizingMask = column.resizingMask | ([column.identifier isEqualToString:MA_Field_Headlines] ? NSTableColumnAutoresizingMask : 0); diff --git a/Vienna/Sources/Models/Article.h b/Vienna/Sources/Models/Article.h index 9026f2a4d1..51a7309489 100644 --- a/Vienna/Sources/Models/Article.h +++ b/Vienna/Sources/Models/Article.h @@ -45,27 +45,26 @@ extern NSString * const MA_Field_HasEnclosure; NS_ASSUME_NONNULL_END -// Article field IDs -typedef NS_ENUM(NSInteger, ArticleFieldID) { - ArticleFieldIDGUID = 400, - ArticleFieldIDSubject, - ArticleFieldIDAuthor, - ArticleFieldIDDate, - ArticleFieldIDParent, - ArticleFieldIDRead, - ArticleFieldIDFlagged, - ArticleFieldIDText, - ArticleFieldIDFolder, - ArticleFieldIDLink, +typedef NS_ENUM(NSInteger, VNAArticleFieldTag) { + VNAArticleFieldTagGUID = 400, + VNAArticleFieldTagSubject, + VNAArticleFieldTagAuthor, + VNAArticleFieldTagDate, + VNAArticleFieldTagParent, + VNAArticleFieldTagRead, + VNAArticleFieldTagFlagged, + VNAArticleFieldTagText, + VNAArticleFieldTagFolder, + VNAArticleFieldTagLink, /* 410 was previously used */ - ArticleFieldIDHeadlines = 411, - ArticleFieldIDDeleted, - ArticleFieldIDSummary, - ArticleFieldIDCreatedDate, - ArticleFieldIDEnclosure, - ArticleFieldIDEnclosureDownloaded, - ArticleFieldIDHasEnclosure -}; + VNAArticleFieldTagHeadlines = 411, + VNAArticleFieldTagDeleted, + VNAArticleFieldTagSummary, + VNAArticleFieldTagCreatedDate, + VNAArticleFieldTagEnclosure, + VNAArticleFieldTagEnclosureDownloaded, + VNAArticleFieldTagHasEnclosure +} NS_SWIFT_NAME(Article.FieldTag); typedef NS_ENUM(NSInteger, ArticleStatus) { ArticleStatusEmpty = 0, From 48603258eafdef901fdbb1d32b960b3232d5e8e8 Mon Sep 17 00:00:00 2001 From: Eitot Date: Fri, 19 Jul 2024 08:23:31 +0200 Subject: [PATCH 04/14] Fix sort indicators in ArticleListView --- Vienna/Sources/Main window/ArticleListView.m | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Vienna/Sources/Main window/ArticleListView.m b/Vienna/Sources/Main window/ArticleListView.m index 968671c295..e03eb6efaf 100644 --- a/Vienna/Sources/Main window/ArticleListView.m +++ b/Vienna/Sources/Main window/ArticleListView.m @@ -593,7 +593,12 @@ -(void)updateArticleListRowHeight -(void)showSortDirection { NSString * sortColumnIdentifier = self.controller.articleController.sortColumnIdentifier; - + + // FIXME: Sort out the order of initialization + if (!sortColumnIdentifier) { + sortColumnIdentifier = [Preferences.standardPreferences stringForKey:MAPref_SortColumn]; + } + for (NSTableColumn * column in articleList.tableColumns) { if ([column.identifier isEqualToString:sortColumnIdentifier]) { // These NSImage names are available in AppKit, but not as constants. @@ -990,6 +995,7 @@ -(void)refreshFolder:(NSInteger)refreshFlag break; case VNARefreshSortAndRedraw: [self.controller.articleController sortArticles]; + [self showSortDirection]; break; } From 5bd2c015f8e4879901c3685a38ac5351c5667ff5 Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Sat, 20 Jul 2024 16:52:12 +0200 Subject: [PATCH 05/14] distinguish between publication and update date Co-authored-by: Eitot --- Vienna Tests/ArticleTests.swift | 16 +++---- Vienna Tests/VNAArticleTests.m | 16 +++---- .../Resources/Base.lproj/Predicates.strings | 8 ++-- Vienna/Resources/Localizable.xcstrings | 7 +++ Vienna/Sources/Alerts/SmartFolder.m | 2 +- .../Criteria/Criteria+NSPredicate.swift | 8 ++-- Vienna/Sources/Database/Database.m | 45 ++++++++++--------- Vienna/Sources/Fetching/OpenReader.m | 6 ++- Vienna/Sources/Fetching/RefreshManager.m | 8 ++-- .../Sources/Main window/ArticleController.m | 16 +++---- Vienna/Sources/Main window/ArticleListView.m | 14 +++--- Vienna/Sources/Models/Article.h | 12 ++--- Vienna/Sources/Models/Article.m | 30 ++++++------- Vienna/Sources/Models/Folder.h | 2 +- Vienna/Sources/Models/Folder.m | 2 +- Vienna/Sources/Parsing/AtomFeed.m | 40 ++++------------- Vienna/Sources/Parsing/Feed.h | 2 +- Vienna/Sources/Parsing/FeedItem.h | 3 +- Vienna/Sources/Parsing/JSONFeed.swift | 6 +-- Vienna/Sources/Parsing/JSONFeedItem.swift | 17 ++++--- Vienna/Sources/Parsing/RSSFeed.m | 5 ++- Vienna/Sources/Parsing/XMLFeed.h | 2 +- Vienna/Sources/Parsing/XMLFeedItem.swift | 3 +- .../Sources/Preferences window/Preferences.m | 4 +- 24 files changed, 134 insertions(+), 140 deletions(-) diff --git a/Vienna Tests/ArticleTests.swift b/Vienna Tests/ArticleTests.swift index 656ef87549..a2d3ca6bcf 100644 --- a/Vienna Tests/ArticleTests.swift +++ b/Vienna Tests/ArticleTests.swift @@ -81,20 +81,20 @@ class ArticleTests: XCTestCase { XCTAssertEqual(self.article.link, link) } - func testDate() { + func testLastUpdate() { let date = Date() - self.article.date = date + self.article.lastUpdate = date - XCTAssertEqual(self.article.date, date) + XCTAssertEqual(self.article.lastUpdate, date) } - func testDateCreated() { + func testPublicationDate() { let date = Date() - self.article.createdDate = date + self.article.publicationDate = date - XCTAssertEqual(self.article.createdDate, date) + XCTAssertEqual(self.article.publicationDate, date) } func testBody() { @@ -193,9 +193,9 @@ class ArticleTests: XCTestCase { func testCompatibilityDate() { let date = Date() - let dateKeyPath = "articleData." + MA_Field_Date + let dateKeyPath = "articleData." + MA_Field_LastUpdate - self.article.date = date + self.article.lastUpdate = date XCTAssertEqual(self.article.value(forKeyPath: dateKeyPath) as? Date, date) } diff --git a/Vienna Tests/VNAArticleTests.m b/Vienna Tests/VNAArticleTests.m index fa44ec5a37..bc2784b480 100644 --- a/Vienna Tests/VNAArticleTests.m +++ b/Vienna Tests/VNAArticleTests.m @@ -73,22 +73,22 @@ - (void)testLink XCTAssertEqualObjects(self.article.link, Link); } -- (void)testDate +- (void)testLastUpdate { NSDate *date = [NSDate date]; - self.article.date = date; + self.article.lastUpdate = date; - XCTAssertEqualObjects(self.article.date, date); + XCTAssertEqualObjects(self.article.lastUpdate, date); } -- (void)testDateCreated +- (void)testPublicationDate { NSDate *date = [NSDate date]; - self.article.createdDate = date; + self.article.publicationDate = date; - XCTAssertEqualObjects(self.article.createdDate, date); + XCTAssertEqualObjects(self.article.publicationDate, date); } - (void)testBody @@ -201,9 +201,9 @@ - (void)testMarkEnclosureDowloaded - (void)testCompatibilityDate { NSDate *date = [NSDate date]; - NSString *dateKeyPath = [@"articleData." stringByAppendingString:MA_Field_Date]; + NSString *dateKeyPath = [@"articleData." stringByAppendingString:MA_Field_LastUpdate]; - self.article.date = date; + self.article.lastUpdate = date; XCTAssertEqualObjects([self.article valueForKeyPath:dateKeyPath], date); } diff --git a/Vienna/Resources/Base.lproj/Predicates.strings b/Vienna/Resources/Base.lproj/Predicates.strings index e14362f0d3..662c0c698a 100644 --- a/Vienna/Resources/Base.lproj/Predicates.strings +++ b/Vienna/Resources/Base.lproj/Predicates.strings @@ -1,8 +1,8 @@ "%[All,Any,None]@ of the following are true" = "Article matches %1$[all,any,none]@ of the following conditions"; "%[Author,Folder,Subject,Text]@ %[is,is not]@ %@" = "%1$[Author,Folder,Subject,Content]@ %2$[is,is not]@ %3$@"; "%[Author,Subject,Text]@ %[contains,does not contain]@ %@" = "%1$[Author,Subject,Content]@ %2$[contains,does not contain]@ %3$@"; -"%[Date]@ %[is,is greater than or equal to,is less than,is less than or equal to]@ %[yesterday]@" = "%1$[Date]@ %2$[is,is after or is,is before,is before or is]@ %3$[yesterday]@"; -"%[Date]@ %[is,is greater than,is greater than or equal to,is less than,is less than or equal to]@ %[last week]@" = "%1$[Date]@ %2$[is,is after,is after or is,is before,is before or is]@ %3$[last week]@"; -"%[Date]@ %[is,is less than,is less than or equal to]@ %[today]@" = "%1$[Date]@ %2$[is,is before,is before or is]@ %3$[today]@"; -"%[Date]@ %[less than ago,more than ago]@ %[days,hours,minutes,months,weeks,years]@ %@" = "%1$[Date]@ %2$[is in the last,is not in the last]@ %4$@ %3$[days,hours,minutes,months,weeks,years]@"; +"%[Date]@ %[is,is greater than or equal to,is less than,is less than or equal to]@ %[yesterday]@" = "%1$[Last update]@ %2$[is,is after or is,is before,is before or is]@ %3$[yesterday]@"; +"%[Date]@ %[is,is greater than,is greater than or equal to,is less than,is less than or equal to]@ %[last week]@" = "%1$[Last update]@ %2$[is,is after,is after or is,is before,is before or is]@ %3$[last week]@"; +"%[Date]@ %[is,is less than,is less than or equal to]@ %[today]@" = "%1$[Last update]@ %2$[is,is before,is before or is]@ %3$[today]@"; +"%[Date]@ %[less than ago,more than ago]@ %[days,hours,minutes,months,weeks,years]@ %@" = "%1$[Last update]@ %2$[is in the last,is not in the last]@ %4$@ %3$[days,hours,minutes,months,weeks,years]@"; "%[Deleted,Flagged,HasEnclosure,Read]@ is %[No,Yes]@" = "%1$[Is deleted,Is flagged,Has enclosure,Is read]@ %2$[No,Yes]@"; diff --git a/Vienna/Resources/Localizable.xcstrings b/Vienna/Resources/Localizable.xcstrings index bbccfd4107..06231f3c7d 100644 --- a/Vienna/Resources/Localizable.xcstrings +++ b/Vienna/Resources/Localizable.xcstrings @@ -6498,6 +6498,7 @@ }, "Date" : { "comment" : "Data field name visible in menu/article list/smart folder definition", + "extractionState" : "stale", "localizations" : { "cs" : { "stringUnit" : { @@ -6621,6 +6622,9 @@ } } }, + "Date Published" : { + "comment" : "Data field name visible in menu/article list/smart folder definition" + }, "Delete" : { "comment" : "Title of a button on an alert\n Title of a menu item", "localizations" : { @@ -12852,6 +12856,9 @@ } } }, + "Last Update" : { + "comment" : "Data field name visible in menu/article list/smart folder definition" + }, "Link" : { "comment" : "Data field name visible in menu/article list", "localizations" : { diff --git a/Vienna/Sources/Alerts/SmartFolder.m b/Vienna/Sources/Alerts/SmartFolder.m index 3ce6949389..cab0c332f5 100644 --- a/Vienna/Sources/Alerts/SmartFolder.m +++ b/Vienna/Sources/Alerts/SmartFolder.m @@ -173,7 +173,7 @@ - (void)prepareTemplates [rowTemplates addObject:[VNASeparatorPredicateEditorRowTemplate new]]; // date < / > days / weeks / months / years old - NSPredicateEditorRowTemplate *dateCompareTemplate = [[VNADateWithUnitPredicateEditorRowTemplate alloc] initWithLeftExpressions:@[[NSExpression expressionForConstantValue:MA_Field_Date]]]; + NSPredicateEditorRowTemplate *dateCompareTemplate = [[VNADateWithUnitPredicateEditorRowTemplate alloc] initWithLeftExpressions:@[[NSExpression expressionForConstantValue:MA_Field_LastUpdate]]]; [rowTemplates addObject:dateCompareTemplate]; // date = / < / <= today / yesterday / lastWeek diff --git a/Vienna/Sources/Criteria/Criteria+NSPredicate.swift b/Vienna/Sources/Criteria/Criteria+NSPredicate.swift index 96298d5b15..edec521410 100644 --- a/Vienna/Sources/Criteria/Criteria+NSPredicate.swift +++ b/Vienna/Sources/Criteria/Criteria+NSPredicate.swift @@ -177,7 +177,7 @@ extension Criteria: PredicateConvertible { fallback = true criteriaOperator = .equalTo } - case MA_Field_Date: + case MA_Field_LastUpdate: switch predicate.predicateOperatorType { case .lessThan: criteriaOperator = .before @@ -231,14 +231,14 @@ extension Criteria: PredicateConvertible { let value = self.value let operatorType = self.operatorType - if field == MA_Field_Date, let unit = DateUnit.allCases.first(where: { value.hasSuffix($0.rawValue) }) { + if field == MA_Field_LastUpdate, let unit = DateUnit.allCases.first(where: { value.hasSuffix($0.rawValue) }) { let countString = value .replacingOccurrences(of: unit.rawValue, with: "") .trimmingCharacters(in: CharacterSet.whitespaces) guard let count = UInt(countString) else { fatalError("malformed criteria value \(value)") } - return DatePredicateWithUnit(field: MA_Field_Date, comparisonOperator: operatorType, count: count, unit: unit) + return DatePredicateWithUnit(field: MA_Field_LastUpdate, comparisonOperator: operatorType, count: count, unit: unit) } else { return buildComparisonPredicate(field, value, operatorType) } @@ -291,7 +291,7 @@ extension Criteria: PredicateConvertible { // TODO: constants for fixed criteria values also for Criteria+SQL, // e.g. YES, NO, yesterday, today, last week, ... - if field == MA_Field_Date && operatorType == .after && value == "yesterday" { + if field == MA_Field_LastUpdate && operatorType == .after && value == "yesterday" { // Use canonical "is today" instead of "is after yesterday" comparisonPredicate = NSComparisonPredicate(leftExpression: left, rightExpression: NSExpression(forConstantValue: "today"), modifier: .direct, type: .equalTo) } else if operatorType == .notEqualTo && (value == "No" || value == "Yes") { diff --git a/Vienna/Sources/Database/Database.m b/Vienna/Sources/Database/Database.m index a1c0d5560d..bd5b3d5050 100644 --- a/Vienna/Sources/Database/Database.m +++ b/Vienna/Sources/Database/Database.m @@ -241,7 +241,7 @@ - (BOOL)setupInitialDatabase { [self createInitialSmartFolder:NSLocalizedString(@"Unread Articles", nil) withCriteria:unreadCriteria]; // Create a criteria to show all articles received today - Criteria * todayCriteria = [[Criteria alloc] initWithField:MA_Field_Date operatorType:VNACriteriaOperatorEqualTo value:@"today"]; + Criteria * todayCriteria = [[Criteria alloc] initWithField:MA_Field_LastUpdate operatorType:VNACriteriaOperatorEqualTo value:@"today"]; [self createInitialSmartFolder:NSLocalizedString(@"Today's Articles", nil) withCriteria:todayCriteria]; [self.databaseQueue inDatabase:^(FMDatabase *db) { @@ -335,8 +335,8 @@ -(void)initaliseFields { [self addField:MA_Field_GUID type:VNAFieldTypeInteger tag:VNAArticleFieldTagGUID sqlField:@"message_id" visible:NO width:72]; [self addField:MA_Field_Subject type:VNAFieldTypeString tag:VNAArticleFieldTagSubject sqlField:@"title" visible:YES width:472]; [self addField:MA_Field_Folder type:VNAFieldTypeFolder tag:VNAArticleFieldTagFolder sqlField:@"folder_id" visible:NO width:130]; - [self addField:MA_Field_Date type:VNAFieldTypeDate tag:VNAArticleFieldTagDate sqlField:@"date" visible:YES width:152]; - [self addField:MA_Field_CreatedDate type:VNAFieldTypeDate tag:VNAArticleFieldTagCreatedDate sqlField:@"createddate" visible:NO width:152]; + [self addField:MA_Field_LastUpdate type:VNAFieldTypeDate tag:VNAArticleFieldTagLastUpdate sqlField:@"date" visible:YES width:152]; + [self addField:MA_Field_PublicationDate type:VNAFieldTypeDate tag:VNAArticleFieldTagPublicationDate sqlField:@"createddate" visible:NO width:152]; [self addField:MA_Field_Parent type:VNAFieldTypeInteger tag:VNAArticleFieldTagParent sqlField:@"parent_id" visible:NO width:72]; [self addField:MA_Field_Author type:VNAFieldTypeString tag:VNAArticleFieldTagAuthor sqlField:@"sender" visible:YES width:138]; [self addField:MA_Field_Link type:VNAFieldTypeString tag:VNAArticleFieldTagLink sqlField:@"link" visible:NO width:138]; @@ -354,8 +354,8 @@ -(void)initaliseFields { [self fieldByName:MA_Field_Deleted].displayName = NSLocalizedString(@"Deleted", @"Data field name visible in smart folder definition"); [self fieldByName:MA_Field_Subject].displayName = NSLocalizedString(@"Subject", @"Data field name visible in menu/article list/smart folder definition"); [self fieldByName:MA_Field_Folder].displayName = NSLocalizedString(@"Folder", @"Data field name visible in menu/article list/smart folder definition"); - [self fieldByName:MA_Field_Date].displayName = NSLocalizedString(@"Date", @"Data field name visible in menu/article list/smart folder definition"); - [self fieldByName:MA_Field_CreatedDate].displayName = NSLocalizedString(@"Date Added", @"Data field name visible in menu/article list/smart folder definition"); + [self fieldByName:MA_Field_LastUpdate].displayName = NSLocalizedString(@"Last Update", @"Data field name visible in menu/article list/smart folder definition"); + [self fieldByName:MA_Field_PublicationDate].displayName = NSLocalizedString(@"Date Published", @"Data field name visible in menu/article list/smart folder definition"); [self fieldByName:MA_Field_Author].displayName = NSLocalizedString(@"Author", @"Data field name visible in menu/article list/smart folder definition"); [self fieldByName:MA_Field_Text].displayName = NSLocalizedString(@"Text", @"Data field name visible in smart folder definition"); [self fieldByName:MA_Field_Summary].displayName = NSLocalizedString(@"Summary", @"Pseudo field name visible in menu/article list"); @@ -1508,7 +1508,7 @@ -(BOOL)addArticle:(Article *)article toFolder:(NSInteger)folderID // Extract the article data from the dictionary. NSString * articleBody = article.body; NSString * articleTitle = article.title; - NSDate * articleDate = article.date; + NSDate * lastUpdate = article.lastUpdate; NSString * articleLink = article.link.vna_trimmed; NSString * userName = article.author.vna_trimmed; NSString * articleEnclosure = article.enclosure.vna_trimmed; @@ -1520,12 +1520,12 @@ -(BOOL)addArticle:(Article *)article toFolder:(NSInteger)folderID BOOL deleted_flag = article.deleted; BOOL hasenclosure_flag = article.hasEnclosure; - // We always set the created date ourselves - article.createdDate = [NSDate date]; + // We set the publication date ourselves if it is not contained in the feed, and only once when the article is created + article.publicationDate = article.publicationDate == nil ? [NSDate date] : article.publicationDate; // Set some defaults - if (articleDate == nil) { - articleDate = [NSDate date]; + if (lastUpdate == nil) { + lastUpdate = [NSDate date]; } if (userName == nil) { userName = @""; @@ -1537,9 +1537,10 @@ -(BOOL)addArticle:(Article *)article toFolder:(NSInteger)folderID } // Dates are stored as time intervals - NSTimeInterval interval = articleDate.timeIntervalSince1970; - NSTimeInterval createdInterval = article.createdDate.timeIntervalSince1970; - + NSTimeInterval lastUpdateIntervalSince1970 = lastUpdate.timeIntervalSince1970; + NSTimeInterval publicationIntervalSince1970 = + (article.publicationDate == nil ? [NSDate date] : article.publicationDate).timeIntervalSince1970; + __block BOOL success; [queue inTransaction:^(FMDatabase *db, BOOL *rollback) { success = [db executeUpdate:@"INSERT INTO messages (message_id, parent_id, folder_id, sender, link, date, createddate, read_flag, marked_flag, deleted_flag, title, text, revised_flag, enclosure, hasenclosure_flag) " @@ -1549,8 +1550,8 @@ -(BOOL)addArticle:(Article *)article toFolder:(NSInteger)folderID @(folderID), userName, articleLink, - @(interval), - @(createdInterval), + @(lastUpdateIntervalSince1970), + @(publicationIntervalSince1970), @(read_flag), @(marked_flag), @(deleted_flag), @@ -1593,7 +1594,7 @@ -(BOOL)updateArticle:(Article *)existingArticle ofFolder:(NSInteger)folderID wit // Extract the data from the new state of article NSString * articleBody = article.body; NSString * articleTitle = article.title; - NSDate * articleDate = article.date; + NSDate * lastUpdate = article.lastUpdate; NSString * articleLink = article.link.vna_trimmed; NSString * userName = article.author.vna_trimmed; NSString * articleGuid = article.guid; @@ -1601,8 +1602,8 @@ -(BOOL)updateArticle:(Article *)existingArticle ofFolder:(NSInteger)folderID wit BOOL revised_flag = article.revised; // Set some defaults - if (articleDate == nil) { - articleDate = existingArticle.date; + if (lastUpdate == nil) { + lastUpdate = existingArticle.lastUpdate; } if (userName == nil) { userName = @""; @@ -1614,7 +1615,7 @@ -(BOOL)updateArticle:(Article *)existingArticle ofFolder:(NSInteger)folderID wit } // Dates are stored as time intervals - NSTimeInterval interval = articleDate.timeIntervalSince1970; + NSTimeInterval lastUpdateIntervalSince1970 = lastUpdate.timeIntervalSince1970; // The article is revised if either the title or the body has changed. @@ -1653,7 +1654,7 @@ -(BOOL)updateArticle:(Article *)existingArticle ofFolder:(NSInteger)folderID wit @(parentId), userName, articleLink, - @(interval), + @(lastUpdateIntervalSince1970), articleTitle, articleBody, @(revised_flag), @@ -2228,8 +2229,8 @@ -(NSArray *)arrayOfArticles:(NSInteger)folderId filterString:(NSString *)filterS article.title = [results stringForColumnIndex:6]; article.author = [results stringForColumnIndex:7]; article.link = [results stringForColumnIndex:8]; - article.createdDate = [NSDate dateWithTimeIntervalSince1970:[results stringForColumnIndex:9].doubleValue]; - article.date = [NSDate dateWithTimeIntervalSince1970:[results stringForColumnIndex:10].doubleValue]; + article.publicationDate = [NSDate dateWithTimeIntervalSince1970:[results stringForColumnIndex:9].doubleValue]; + article.lastUpdate = [NSDate dateWithTimeIntervalSince1970:[results stringForColumnIndex:10].doubleValue]; NSString * text = [results stringForColumnIndex:11]; article.body = text; [article markRevised:[results intForColumnIndex:12]]; diff --git a/Vienna/Sources/Fetching/OpenReader.m b/Vienna/Sources/Fetching/OpenReader.m index 451f43248b..8d9c19c4e7 100644 --- a/Vienna/Sources/Fetching/OpenReader.m +++ b/Vienna/Sources/Fetching/OpenReader.m @@ -568,7 +568,8 @@ -(void)feedRequestDone:(NSMutableURLRequest *)request response:(NSURLResponse *) NSMutableArray *articleArray = [NSMutableArray array]; for (NSDictionary *newsItem in (NSArray *)subscriptionsDict[@"items"]) { - NSDate *articleDate = [NSDate dateWithTimeIntervalSince1970:[newsItem[@"published"] doubleValue]]; + NSDate *publicationDate = [NSDate dateWithTimeIntervalSince1970:[newsItem[@"published"] doubleValue]]; + NSDate *lastUpdate = [NSDate dateWithTimeIntervalSince1970:[newsItem[@"updated"] doubleValue]]; NSString *articleGuid = newsItem[@"id"]; Article *article = [[Article alloc] initWithGuid:articleGuid]; article.folderId = refreshedFolder.itemId; @@ -611,7 +612,8 @@ -(void)feedRequestDone:(NSMutableURLRequest *)request response:(NSURLResponse *) article.link = refreshedFolder.feedURL; } - article.date = articleDate; + article.publicationDate = publicationDate == nil ? lastUpdate : publicationDate; + article.lastUpdate = lastUpdate == nil ? publicationDate : lastUpdate; if ([newsItem[@"enclosure"] count] != 0) { article.enclosure = newsItem[@"enclosure"][0][@"href"]; diff --git a/Vienna/Sources/Fetching/RefreshManager.m b/Vienna/Sources/Fetching/RefreshManager.m index 627c06aabd..e749ec9d98 100644 --- a/Vienna/Sources/Fetching/RefreshManager.m +++ b/Vienna/Sources/Fetching/RefreshManager.m @@ -755,7 +755,7 @@ -(void)finalizeFolderRefresh:(NSDictionary *)parameters NSMutableArray *articleArray = [NSMutableArray array]; NSMutableArray *articleGuidArray = [NSMutableArray array]; - NSDate *itemAlternativeDate = newFeed.modifiedDate; + NSDate *itemAlternativeDate = newFeed.modificationDate; if (itemAlternativeDate == nil) { itemAlternativeDate = [NSDate date]; } @@ -763,7 +763,7 @@ -(void)finalizeFolderRefresh:(NSDictionary *)parameters // Parse off items. for (id newsItem in newFeed.items) { - NSDate * articleDate = newsItem.modifiedDate; + NSDate * articleDate = newsItem.modificationDate; NSString * articleGuid = newsItem.guid; @@ -795,7 +795,7 @@ -(void)finalizeFolderRefresh:(NSDictionary *)parameters // first, hack the initial article (which is probably the first loaded / most recent one) NSString * firstFoundArticleNewGuid = [NSString stringWithFormat:@"%ld-%@-%@-%@", (long)folderId, - [NSString stringWithFormat:@"%1.3f", firstFoundArticle.date.timeIntervalSince1970], firstFoundArticle.link, + [NSString stringWithFormat:@"%1.3f", firstFoundArticle.lastUpdate.timeIntervalSince1970], firstFoundArticle.link, firstFoundArticle.title]; firstFoundArticle.guid = firstFoundArticleNewGuid; articleGuidArray[articleIndex] = firstFoundArticleNewGuid; @@ -837,7 +837,7 @@ -(void)finalizeFolderRefresh:(NSDictionary *)parameters articleLink = feedLink; } article.link = articleLink; - article.date = articleDate; + article.lastUpdate = articleDate; NSString * enclosureLink = newsItem.enclosure; if ([enclosureLink isNotEqualTo:@""] && ![enclosureLink hasPrefix:@"http:"] && ![enclosureLink hasPrefix:@"https:"]) { enclosureLink = [NSURL URLWithString:enclosureLink relativeToURL:url].absoluteString; diff --git a/Vienna/Sources/Main window/ArticleController.m b/Vienna/Sources/Main window/ArticleController.m index 8a44cdfbd8..774320aee5 100644 --- a/Vienna/Sources/Main window/ArticleController.m +++ b/Vienna/Sources/Main window/ArticleController.m @@ -93,12 +93,12 @@ -(instancetype)init @"key": @"isFlagged", @"selector": NSStringFromSelector(@selector(compare:)) }, - MA_Field_Date: @{ - @"key": [@"articleData." stringByAppendingString:MA_Field_Date], + MA_Field_LastUpdate: @{ + @"key": [@"articleData." stringByAppendingString:MA_Field_LastUpdate], @"selector": NSStringFromSelector(@selector(compare:)) }, - MA_Field_CreatedDate: @{ - @"key": [@"articleData." stringByAppendingString:MA_Field_CreatedDate], + MA_Field_PublicationDate: @{ + @"key": [@"articleData." stringByAppendingString:MA_Field_PublicationDate], @"selector": NSStringFromSelector(@selector(compare:)) }, MA_Field_Author: @{ @@ -294,7 +294,7 @@ -(void)sortByIdentifier:(NSString *)columnName if (index == NSNotFound) { // Date should be sorted in descending order. // TODO: Add a key to articleSortSpecifiers for a default sort order - BOOL ascending = [columnName isEqualToString:MA_Field_CreatedDate] ? NO : YES; + BOOL ascending = [columnName isEqualToString:MA_Field_PublicationDate] ? NO : YES; sortDescriptor = [[NSSortDescriptor alloc] initWithKey:[specifier valueForKey:@"key"] ascending:ascending selector:NSSelectorFromString([specifier valueForKey:@"selector"])]; } else { sortDescriptor = descriptors[index]; @@ -1021,19 +1021,19 @@ - (BOOL)filterArticle:(Article *)article usingMode:(NSInteger)filterMode { case VNAFilterUnread: return !article.read; case VNAFilterLastRefresh: { - NSDate *date = article.createdDate; + NSDate *date = article.publicationDate; Preferences *prefs = [Preferences standardPreferences]; NSComparisonResult result = [date compare:[prefs objectForKey:MAPref_LastRefreshDate]]; return result != NSOrderedAscending; } case VNAFilterToday: - return [NSCalendar.currentCalendar isDateInToday:article.date]; + return [NSCalendar.currentCalendar isDateInToday:article.lastUpdate]; case VNAFilterTime48h: { NSDate *twoDaysAgo = [NSCalendar.currentCalendar dateByAddingUnit:NSCalendarUnitDay value:-2 toDate:[NSDate date] options:0]; - return [article.date compare:twoDaysAgo] != NSOrderedAscending; + return [article.lastUpdate compare:twoDaysAgo] != NSOrderedAscending; } case VNAFilterFlagged: return article.flagged; diff --git a/Vienna/Sources/Main window/ArticleListView.m b/Vienna/Sources/Main window/ArticleListView.m index e03eb6efaf..22d5c442a1 100644 --- a/Vienna/Sources/Main window/ArticleListView.m +++ b/Vienna/Sources/Main window/ArticleListView.m @@ -571,7 +571,7 @@ -(void)updateArticleListRowHeight if ([db fieldByName:MA_Field_Subject].visible) { ++numberOfRowsInCell; } - if ([db fieldByName:MA_Field_Folder].visible || [db fieldByName:MA_Field_Date].visible || [db fieldByName:MA_Field_Author].visible) { + if ([db fieldByName:MA_Field_Folder].visible || [db fieldByName:MA_Field_LastUpdate].visible || [db fieldByName:MA_Field_Author].visible) { ++numberOfRowsInCell; } if ([db fieldByName:MA_Field_Link].visible) { @@ -1293,8 +1293,8 @@ -(id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColum [summaryString appendFormat:@"%@", folder.name]; delimiter = @" - "; } - if ([db fieldByName:MA_Field_Date].visible) { - [summaryString appendFormat:@"%@%@", delimiter, [NSDateFormatter vna_relativeDateStringFromDate:theArticle.date]]; + if ([db fieldByName:MA_Field_LastUpdate].visible) { + [summaryString appendFormat:@"%@%@", delimiter, [NSDateFormatter vna_relativeDateStringFromDate:theArticle.lastUpdate]]; delimiter = @" - "; } if ([db fieldByName:MA_Field_Author].visible) { @@ -1313,10 +1313,10 @@ -(id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColum } NSString * cellString; - if ([identifier isEqualToString:MA_Field_Date]) { - cellString = [NSDateFormatter vna_relativeDateStringFromDate:theArticle.date]; - } else if ([identifier isEqualToString:MA_Field_CreatedDate]) { - cellString = [NSDateFormatter vna_relativeDateStringFromDate:theArticle.createdDate]; + if ([identifier isEqualToString:MA_Field_LastUpdate]) { + cellString = [NSDateFormatter vna_relativeDateStringFromDate:theArticle.lastUpdate]; + } else if ([identifier isEqualToString:MA_Field_PublicationDate]) { + cellString = [NSDateFormatter vna_relativeDateStringFromDate:theArticle.publicationDate]; } else if ([identifier isEqualToString:MA_Field_Folder]) { Folder * folder = [db folderFromID:theArticle.folderId]; cellString = folder.name; diff --git a/Vienna/Sources/Models/Article.h b/Vienna/Sources/Models/Article.h index 51a7309489..a5aef227fe 100644 --- a/Vienna/Sources/Models/Article.h +++ b/Vienna/Sources/Models/Article.h @@ -29,7 +29,7 @@ extern NSString * const MA_Field_GUID; extern NSString * const MA_Field_Subject; extern NSString * const MA_Field_Author; extern NSString * const MA_Field_Link; -extern NSString * const MA_Field_Date; +extern NSString * const MA_Field_LastUpdate; extern NSString * const MA_Field_Read; extern NSString * const MA_Field_Flagged; extern NSString * const MA_Field_Deleted; @@ -38,7 +38,7 @@ extern NSString * const MA_Field_Folder; extern NSString * const MA_Field_Parent; extern NSString * const MA_Field_Headlines; extern NSString * const MA_Field_Summary; -extern NSString * const MA_Field_CreatedDate; +extern NSString * const MA_Field_PublicationDate; extern NSString * const MA_Field_Enclosure; extern NSString * const MA_Field_EnclosureDownloaded; extern NSString * const MA_Field_HasEnclosure; @@ -49,7 +49,7 @@ typedef NS_ENUM(NSInteger, VNAArticleFieldTag) { VNAArticleFieldTagGUID = 400, VNAArticleFieldTagSubject, VNAArticleFieldTagAuthor, - VNAArticleFieldTagDate, + VNAArticleFieldTagLastUpdate, VNAArticleFieldTagParent, VNAArticleFieldTagRead, VNAArticleFieldTagFlagged, @@ -60,7 +60,7 @@ typedef NS_ENUM(NSInteger, VNAArticleFieldTag) { VNAArticleFieldTagHeadlines = 411, VNAArticleFieldTagDeleted, VNAArticleFieldTagSummary, - VNAArticleFieldTagCreatedDate, + VNAArticleFieldTagPublicationDate, VNAArticleFieldTagEnclosure, VNAArticleFieldTagEnclosureDownloaded, VNAArticleFieldTagHasEnclosure @@ -84,8 +84,8 @@ typedef NS_ENUM(NSInteger, ArticleStatus) { @property (nullable, nonatomic, copy) NSString *link; @property (readonly, nullable, nonatomic) NSString *summary; @property (nullable, nonatomic, copy) NSString *enclosure; -@property (nullable, nonatomic, copy) NSDate *date; -@property (nullable, nonatomic, copy) NSDate *createdDate; +@property (nullable, nonatomic) NSDate *lastUpdate; +@property (nullable, nonatomic) NSDate *publicationDate; @property (nullable, nonatomic, readonly) Folder *containingFolder; @property (nonatomic) NSInteger folderId; @property (nonatomic, getter=isRead, readonly) BOOL read; diff --git a/Vienna/Sources/Models/Article.m b/Vienna/Sources/Models/Article.m index f85538ab39..aa6b74ecb9 100644 --- a/Vienna/Sources/Models/Article.m +++ b/Vienna/Sources/Models/Article.m @@ -31,7 +31,7 @@ NSString * const MA_Field_Subject = @"Subject"; NSString * const MA_Field_Author = @"Author"; NSString * const MA_Field_Link = @"Link"; -NSString * const MA_Field_Date = @"Date"; +NSString * const MA_Field_LastUpdate = @"Date"; NSString * const MA_Field_Read = @"Read"; NSString * const MA_Field_Flagged = @"Flagged"; NSString * const MA_Field_Deleted = @"Deleted"; @@ -40,7 +40,7 @@ NSString * const MA_Field_Parent = @"Parent"; NSString * const MA_Field_Headlines = @"Headlines"; NSString * const MA_Field_Summary = @"Summary"; -NSString * const MA_Field_CreatedDate = @"CreatedDate"; +NSString * const MA_Field_PublicationDate = @"PublicationDate"; NSString * const MA_Field_Enclosure = @"Enclosure"; NSString * const MA_Field_EnclosureDownloaded = @"EnclosureDownloaded"; NSString * const MA_Field_HasEnclosure = @"HasEnclosure"; @@ -108,17 +108,17 @@ -(void)setLink:(NSString *)newLink /* setDate * Sets the date when the article was published. */ --(void)setDate:(NSDate *)newDate +-(void)setLastUpdate:(NSDate *)lastUpdate { - articleData[MA_Field_Date] = [newDate copy]; + articleData[MA_Field_LastUpdate] = lastUpdate; } -/* setCreatedDate - * Sets the date when the article was first created in the database. +/* + * Sets the date when the article was first published or added to the database. */ --(void)setCreatedDate:(NSDate *)newCreatedDate +- (void)setPublicationDate:(NSDate *)publicationDate { - articleData[MA_Field_CreatedDate] = [newCreatedDate copy]; + articleData[MA_Field_PublicationDate] = publicationDate; } /* setBody @@ -198,10 +198,10 @@ -(id)valueForKeyPath:(NSString *)keyPath { if ([keyPath hasPrefix:@"articleData."]) { NSString * key = [keyPath substringFromIndex:(@"articleData.").length]; - if ([key isEqualToString:MA_Field_Date]) { - return self.date; - } else if ([key isEqualToString:MA_Field_CreatedDate]) { - return self.createdDate; + if ([key isEqualToString:MA_Field_LastUpdate]) { + return self.lastUpdate; + } else if ([key isEqualToString:MA_Field_PublicationDate]) { + return self.publicationDate; } else if ([key isEqualToString:MA_Field_Author]) { return self.author; } else if ([key isEqualToString:MA_Field_Subject]) { @@ -245,8 +245,8 @@ -(NSString *)summary } return summary; } --(NSDate *)date { return articleData[MA_Field_Date]; } --(NSDate *)createdDate { return articleData[MA_Field_CreatedDate]; } +-(NSDate *)lastUpdate { return articleData[MA_Field_LastUpdate]; } +-(NSDate *)publicationDate { return articleData[MA_Field_PublicationDate]; } -(NSString *)body { return articleData[MA_Field_Text]; } -(NSString *)enclosure { return articleData[MA_Field_Enclosure]; } @@ -356,7 +356,7 @@ -(NSString *)tagArticleAuthor */ -(NSString *)tagArticleDate { - return [NSDateFormatter vna_relativeDateStringFromDate:self.date]; + return [NSDateFormatter vna_relativeDateStringFromDate:self.lastUpdate]; } /* tagArticleEnclosureLink diff --git a/Vienna/Sources/Models/Folder.h b/Vienna/Sources/Models/Folder.h index 724f7a9638..9bf458af2d 100644 --- a/Vienna/Sources/Models/Folder.h +++ b/Vienna/Sources/Models/Folder.h @@ -76,7 +76,7 @@ typedef NS_OPTIONS(NSUInteger, VNAFolderFlag) { @property (nonatomic, copy) NSString *feedDescription; @property (nonatomic, copy) NSString *homePage; @property (nonatomic, copy) NSString *feedURL; -@property (nonatomic, copy) NSDate *lastUpdate; +@property (nonatomic) NSDate *lastUpdate; @property (nonatomic, copy) NSString *lastUpdateString; @property (nonatomic, copy) NSString *username; @property (nonatomic, copy) NSString *password; diff --git a/Vienna/Sources/Models/Folder.m b/Vienna/Sources/Models/Folder.m index 584aea2639..3cb1481a18 100644 --- a/Vienna/Sources/Models/Folder.m +++ b/Vienna/Sources/Models/Folder.m @@ -585,7 +585,7 @@ -(void)restoreArticleToCache:(Article *)article [self.cachedArticles setObject:article forKey:[NSString stringWithString:guid]]; [self.cachedGuids addObject:guid]; // note if article has incomplete data - if (article.createdDate == nil) { + if (article.publicationDate == nil) { self.containsBodies = NO; } } diff --git a/Vienna/Sources/Parsing/AtomFeed.m b/Vienna/Sources/Parsing/AtomFeed.m index 3990ea5728..0a66ff44e7 100644 --- a/Vienna/Sources/Parsing/AtomFeed.m +++ b/Vienna/Sources/Parsing/AtomFeed.m @@ -121,17 +121,9 @@ - (BOOL)initAtomFeed:(NSXMLElement *)atomElement } // Parse the date when this feed was last updated - if (isAtomElement && [elementTag isEqualToString:@"updated"]) { + if (isAtomElement && ([elementTag isEqualToString:@"updated"] || [elementTag isEqualToString:@"modified"])) { NSString *dateString = atomChildElement.stringValue; - self.modifiedDate = [self dateWithXMLString:dateString]; - success = YES; - continue; - } - - // Parse the date when this feed was last updated - if (isAtomElement && [elementTag isEqualToString:@"modified"]) { - NSString *dateString = atomChildElement.stringValue; - self.modifiedDate = [self dateWithXMLString:dateString]; + self.modificationDate = [self dateWithXMLString:dateString]; success = YES; continue; } @@ -261,31 +253,15 @@ - (BOOL)initAtomFeed:(NSXMLElement *)atomElement } // Parse item date - if (isArticleElementAtomType && [articleItemTag isEqualToString:@"modified"]) { + if (isArticleElementAtomType && ([articleItemTag isEqualToString:@"modified"] || [articleItemTag isEqualToString:@"created"] || [articleItemTag isEqualToString:@"updated"])) { NSString *dateString = itemChildElement.stringValue; NSDate *newDate = [self dateWithXMLString:dateString]; - if (newFeedItem.modifiedDate == nil || [newDate isGreaterThan:newFeedItem.modifiedDate]) { - newFeedItem.modifiedDate = newDate; + if (newFeedItem.modificationDate == nil || [newDate isGreaterThan:newFeedItem.modificationDate]) { + newFeedItem.modificationDate = newDate; } - continue; - } - - // Parse item date - if (isArticleElementAtomType && [articleItemTag isEqualToString:@"created"]) { - NSString *dateString = itemChildElement.stringValue; - NSDate *newDate = [self dateWithXMLString:dateString]; - if (newFeedItem.modifiedDate == nil || [newDate isGreaterThan:newFeedItem.modifiedDate]) { - newFeedItem.modifiedDate = newDate; - } - continue; - } - - // Parse item date - if (isArticleElementAtomType && [articleItemTag isEqualToString:@"updated"]) { - NSString *dateString = itemChildElement.stringValue; - NSDate *newDate = [self dateWithXMLString:dateString]; - if (newFeedItem.modifiedDate == nil || [newDate isGreaterThan:newFeedItem.modifiedDate]) { - newFeedItem.modifiedDate = newDate; + if (newFeedItem.publicationDate == nil || [newDate isLessThan:newFeedItem.publicationDate]) { + //publication date will only be registered once for each article, so later updates don´t matter + newFeedItem.publicationDate = newDate; } continue; } diff --git a/Vienna/Sources/Parsing/Feed.h b/Vienna/Sources/Parsing/Feed.h index a59e433a7f..2652b71e37 100644 --- a/Vienna/Sources/Parsing/Feed.h +++ b/Vienna/Sources/Parsing/Feed.h @@ -29,7 +29,7 @@ NS_SWIFT_NAME(Feed) @property (copy, nonatomic) NSString *title; @property (nullable, copy, nonatomic) NSString *feedDescription; @property (nullable, copy, nonatomic) NSString *homePageURL; -@property (nullable, nonatomic) NSDate *modifiedDate; +@property (nullable, nonatomic) NSDate *modificationDate; @property (copy, nonatomic) NSArray> *items; @end diff --git a/Vienna/Sources/Parsing/FeedItem.h b/Vienna/Sources/Parsing/FeedItem.h index 7a58c01636..3b12f79dff 100644 --- a/Vienna/Sources/Parsing/FeedItem.h +++ b/Vienna/Sources/Parsing/FeedItem.h @@ -28,7 +28,8 @@ NS_SWIFT_NAME(FeedItem) @property (nullable, copy, nonatomic) NSString *title; @property (nullable, copy, nonatomic) NSString *authors; @property (copy, nonatomic) NSString *content; -@property (nullable, nonatomic) NSDate *modifiedDate; +@property (nullable, nonatomic) NSDate *publicationDate; +@property (nullable, nonatomic) NSDate *modificationDate; @property (nullable, copy, nonatomic) NSString *url; @property (nullable, copy, nonatomic) NSString *enclosure; diff --git a/Vienna/Sources/Parsing/JSONFeed.swift b/Vienna/Sources/Parsing/JSONFeed.swift index fb5c880347..46a58b053c 100644 --- a/Vienna/Sources/Parsing/JSONFeed.swift +++ b/Vienna/Sources/Parsing/JSONFeed.swift @@ -32,7 +32,7 @@ class JSONFeed: NSObject, Feed, Decodable { var homePageURL: String? // JSON Feed has no key for this at the feed level. - var modifiedDate: Date? + var modificationDate: Date? // The `items` key is required (but the array may be empty). var items: [any FeedItem] @@ -43,8 +43,8 @@ class JSONFeed: NSObject, Feed, Decodable { case title case feedDescription = "description" case homePageURL = "home_page_url" - case modifiedDate = "date_modified" - case publishedDate = "date_published" + case publicationDate = "date_published" + case modificationDate = "date_modified" case items // These keys are only used by JSONFeedItem diff --git a/Vienna/Sources/Parsing/JSONFeedItem.swift b/Vienna/Sources/Parsing/JSONFeedItem.swift index 2a9e22bfe5..aef2a0eec0 100644 --- a/Vienna/Sources/Parsing/JSONFeedItem.swift +++ b/Vienna/Sources/Parsing/JSONFeedItem.swift @@ -39,9 +39,13 @@ class JSONFeedItem: NSObject, FeedItem, Decodable { // are mutually exclusive. At least one key must be present. var content: String - // JSON Feed has `date_published` and `date_modified` keys, neither is - // required. If present, they should be date strings in RFC 3339 format. - var modifiedDate: Date? + // JSON Feed has the `date_published` key, which is not required. If + // present, it should be a date string in RFC 3339 format. + var publicationDate: Date? + + // JSON Feed has the `date_modified` key, which is not required. If present, + // it should be a date string in RFC 3339 format. + var modificationDate: Date? // The `url` key is optional. The `id` key might be a URL too, so it could // be a fallback. @@ -59,8 +63,8 @@ class JSONFeedItem: NSObject, FeedItem, Decodable { case author case contentHTML = "content_html" case contentText = "content_text" - case modifiedDate = "date_modified" - case publishedDate = "date_published" + case publicationDate = "date_published" + case modificationDate = "date_modified" case url case attachments } @@ -103,7 +107,8 @@ class JSONFeedItem: NSObject, FeedItem, Decodable { content = try container.decode(String.self, forKey: .contentText) } - modifiedDate = try container.decodeIfPresent(Date.self, forKey: .modifiedDate) + publicationDate = try container.decodeIfPresent(Date.self, forKey: .publicationDate) + modificationDate = try container.decodeIfPresent(Date.self, forKey: .modificationDate) do { url = try container.decode(URL.self, forKey: .url).absoluteString diff --git a/Vienna/Sources/Parsing/RSSFeed.m b/Vienna/Sources/Parsing/RSSFeed.m index 000593b552..29e8131ee7 100644 --- a/Vienna/Sources/Parsing/RSSFeed.m +++ b/Vienna/Sources/Parsing/RSSFeed.m @@ -137,7 +137,8 @@ - (BOOL)initRSSFeedHeaderWithElement:(NSXMLElement *)channelElement (isRSSElement && [channelItemTag isEqualToString:@"pubDate"]) || ([element.prefix isEqualToString:self.dcPrefix] && [channelItemTag isEqualToString:@"date"])) { NSString *dateString = element.stringValue; - self.modifiedDate = [self dateWithXMLString:dateString]; + //publication date will be set to the current date in a later step, so we don´t set it here + self.modificationDate = [self dateWithXMLString:dateString]; success = YES; continue; } @@ -260,7 +261,7 @@ - (BOOL)initRSSFeedItems:(NSXMLElement *)startElement // Parse item date if ((isRSSElement && [articleItemTag isEqualToString:@"pubDate"]) || ([itemChildElement.prefix isEqualToString:self.dcPrefix] && [articleItemTag isEqualToString:@"date"])) { NSString *dateString = itemChildElement.stringValue; - newFeedItem.modifiedDate = [self dateWithXMLString:dateString]; + newFeedItem.modificationDate = [self dateWithXMLString:dateString]; continue; } diff --git a/Vienna/Sources/Parsing/XMLFeed.h b/Vienna/Sources/Parsing/XMLFeed.h index 9c3830cdaf..8109c6a3a4 100644 --- a/Vienna/Sources/Parsing/XMLFeed.h +++ b/Vienna/Sources/Parsing/XMLFeed.h @@ -31,7 +31,7 @@ NS_SWIFT_NAME(XMLFeed) @property (copy, nonatomic) NSString *title; @property (nullable, copy, nonatomic) NSString *feedDescription; @property (nullable, copy, nonatomic) NSString *homePageURL; -@property (nonatomic) NSDate *modifiedDate; +@property (nonatomic) NSDate *modificationDate; @property (copy, nonatomic) NSArray> *items; // MARK: Prefix handling diff --git a/Vienna/Sources/Parsing/XMLFeedItem.swift b/Vienna/Sources/Parsing/XMLFeedItem.swift index 43d4d25384..a363d0cf47 100644 --- a/Vienna/Sources/Parsing/XMLFeedItem.swift +++ b/Vienna/Sources/Parsing/XMLFeedItem.swift @@ -26,7 +26,8 @@ class XMLFeedItem: NSObject, FeedItem { @objc var title: String? @objc var authors: String? @objc var content = "" - @objc var modifiedDate: Date? + @objc var publicationDate: Date? + @objc var modificationDate: Date? @objc var url: String? @objc var enclosure: String? diff --git a/Vienna/Sources/Preferences window/Preferences.m b/Vienna/Sources/Preferences window/Preferences.m index ee2338175c..2b91363ed4 100644 --- a/Vienna/Sources/Preferences window/Preferences.m +++ b/Vienna/Sources/Preferences window/Preferences.m @@ -210,7 +210,7 @@ -(NSDictionary *)allocFactoryDefaults NSFileManager *fileManager = NSFileManager.defaultManager; NSString *appSupportPath = fileManager.vna_applicationSupportDirectory.path; - NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:[@"articleData." stringByAppendingString:MA_Field_Date] + NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:[@"articleData." stringByAppendingString:MA_Field_LastUpdate] ascending:YES]; defaultValues[MAPref_DefaultDatabase] = [appSupportPath stringByAppendingPathComponent:MA_Database_Name]; @@ -218,7 +218,7 @@ -(NSDictionary *)allocFactoryDefaults defaultValues[MAPref_ShowUnreadArticlesInBold] = boolYes; defaultValues[MAPref_CheckForNewArticlesOnStartup] = boolYes; defaultValues[MAPref_CachedFolderID] = @1; - defaultValues[MAPref_SortColumn] = MA_Field_Date; + defaultValues[MAPref_SortColumn] = MA_Field_LastUpdate; defaultValues[MAPref_CheckFrequency] = @(MA_Default_Check_Frequency); defaultValues[MAPref_MarkReadInterval] = @((float)MA_Default_Read_Interval); defaultValues[MAPref_ActiveStyleName] = MA_DefaultStyleName; From a8eee7f12009fd5457aa6b0ce4e8ca5d56d73e3a Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Mon, 29 Jul 2024 00:26:37 +0200 Subject: [PATCH 06/14] add publication date as intelligent folder criterion --- Vienna/Sources/Alerts/SmartFolder.m | 41 +++++++++++-------- .../Criteria/Criteria+NSPredicate.swift | 4 +- Vienna/Sources/Criteria/Criteria+SQL.swift | 12 +++--- Vienna/Sources/Criteria/Criteria.swift | 30 +++++++------- .../Criteria/DatePredicateWithUnit.swift | 4 ++ 5 files changed, 51 insertions(+), 40 deletions(-) diff --git a/Vienna/Sources/Alerts/SmartFolder.m b/Vienna/Sources/Alerts/SmartFolder.m index cab0c332f5..03bc911086 100644 --- a/Vienna/Sources/Alerts/SmartFolder.m +++ b/Vienna/Sources/Alerts/SmartFolder.m @@ -147,9 +147,9 @@ - (void)prepareTemplates // subject / author / text contains / = / != NSArray *textLeftExpressions = @[ - [NSExpression expressionForConstantValue:@"Text"], - [NSExpression expressionForConstantValue:@"Author"], - [NSExpression expressionForConstantValue:@"Subject"] + [NSExpression expressionForConstantValue:MA_Field_Text], + [NSExpression expressionForConstantValue:MA_Field_Author], + [NSExpression expressionForConstantValue:MA_Field_Subject] ]; NSPredicateEditorRowTemplate *textTemplate = [[NSPredicateEditorRowTemplate alloc] initWithLeftExpressions:textLeftExpressions @@ -173,7 +173,10 @@ - (void)prepareTemplates [rowTemplates addObject:[VNASeparatorPredicateEditorRowTemplate new]]; // date < / > days / weeks / months / years old - NSPredicateEditorRowTemplate *dateCompareTemplate = [[VNADateWithUnitPredicateEditorRowTemplate alloc] initWithLeftExpressions:@[[NSExpression expressionForConstantValue:MA_Field_LastUpdate]]]; + NSPredicateEditorRowTemplate *dateCompareTemplate = [[VNADateWithUnitPredicateEditorRowTemplate alloc] + initWithLeftExpressions:@[ + [NSExpression expressionForConstantValue:MA_Field_LastUpdate], + [NSExpression expressionForConstantValue:MA_Field_PublicationDate]]]; [rowTemplates addObject:dateCompareTemplate]; // date = / < / <= today / yesterday / lastWeek @@ -186,7 +189,9 @@ - (void)prepareTemplates @(NSLessThanOrEqualToPredicateOperatorType), ]; NSPredicateEditorRowTemplate *todayTemplate = [[NSPredicateEditorRowTemplate alloc] - initWithLeftExpressions:@[[NSExpression expressionForConstantValue:@"Date"]] + initWithLeftExpressions:@[ + [NSExpression expressionForConstantValue:MA_Field_LastUpdate], + [NSExpression expressionForConstantValue:MA_Field_PublicationDate]] rightExpressions:todayRightExpressions modifier:NSDirectPredicateModifier operators:todayOperators @@ -204,11 +209,13 @@ - (void)prepareTemplates @(NSGreaterThanOrEqualToPredicateOperatorType) ]; NSPredicateEditorRowTemplate *yesterdayTemplate = [[NSPredicateEditorRowTemplate alloc] - initWithLeftExpressions:@[[NSExpression expressionForConstantValue:@"Date"]] - rightExpressions:yesterdayRightExpressions - modifier:NSDirectPredicateModifier - operators:yesterdayOperators - options:0]; + initWithLeftExpressions:@[ + [NSExpression expressionForConstantValue:MA_Field_LastUpdate], + [NSExpression expressionForConstantValue:MA_Field_PublicationDate]] + rightExpressions:yesterdayRightExpressions + modifier:NSDirectPredicateModifier + operators:yesterdayOperators + options:0]; [rowTemplates addObject:yesterdayTemplate]; // date = / > / >= / < / <= last week @@ -223,7 +230,9 @@ - (void)prepareTemplates @(NSLessThanOrEqualToPredicateOperatorType) ]; NSPredicateEditorRowTemplate *dateTemplate = [[NSPredicateEditorRowTemplate alloc] - initWithLeftExpressions:@[[NSExpression expressionForConstantValue:@"Date"]] + initWithLeftExpressions:@[ + [NSExpression expressionForConstantValue:MA_Field_LastUpdate], + [NSExpression expressionForConstantValue:MA_Field_PublicationDate]] rightExpressions:weekRightExpressions modifier:NSDirectPredicateModifier operators:weekOperators @@ -234,10 +243,10 @@ - (void)prepareTemplates // read / flagged / deleted / has_enclosure = YES / NO NSArray *booleanLeftExpressions = @[ - [NSExpression expressionForConstantValue:@"Read"], - [NSExpression expressionForConstantValue:@"Flagged"], - [NSExpression expressionForConstantValue:@"Deleted"], - [NSExpression expressionForConstantValue:@"HasEnclosure"] + [NSExpression expressionForConstantValue:MA_Field_Read], + [NSExpression expressionForConstantValue:MA_Field_Flagged], + [NSExpression expressionForConstantValue:MA_Field_Deleted], + [NSExpression expressionForConstantValue:MA_Field_HasEnclosure] ]; NSArray *booleanRightExpressions = @[ [NSExpression expressionForConstantValue:@"Yes"], @@ -256,7 +265,7 @@ - (void)prepareTemplates // folder is / is not NSArray *folders = [self fillFolderValueField:VNAFolderTypeRoot atIndent:0]; NSPredicateEditorRowTemplate *folderTemplate = [[NSPredicateEditorRowTemplate alloc] - initWithLeftExpressions:@[[NSExpression expressionForConstantValue:@"Folder"]] + initWithLeftExpressions:@[[NSExpression expressionForConstantValue:MA_Field_Folder]] rightExpressions:folders modifier:NSDirectPredicateModifier operators:@[@(NSEqualToPredicateOperatorType), @(NSNotEqualToPredicateOperatorType)] diff --git a/Vienna/Sources/Criteria/Criteria+NSPredicate.swift b/Vienna/Sources/Criteria/Criteria+NSPredicate.swift index edec521410..6cba63ea7b 100644 --- a/Vienna/Sources/Criteria/Criteria+NSPredicate.swift +++ b/Vienna/Sources/Criteria/Criteria+NSPredicate.swift @@ -291,9 +291,9 @@ extension Criteria: PredicateConvertible { // TODO: constants for fixed criteria values also for Criteria+SQL, // e.g. YES, NO, yesterday, today, last week, ... - if field == MA_Field_LastUpdate && operatorType == .after && value == "yesterday" { + if field == MA_Field_LastUpdate && operatorType == .after && value == DateOffset.yesterday.rawValue { // Use canonical "is today" instead of "is after yesterday" - comparisonPredicate = NSComparisonPredicate(leftExpression: left, rightExpression: NSExpression(forConstantValue: "today"), modifier: .direct, type: .equalTo) + comparisonPredicate = NSComparisonPredicate(leftExpression: left, rightExpression: NSExpression(forConstantValue: DateOffset.today), modifier: .direct, type: .equalTo) } else if operatorType == .notEqualTo && (value == "No" || value == "Yes") { // Use canonical "is yes / is no" representation instead of allowing // ambiguous "is not yes - is no / is not no - is yes" diff --git a/Vienna/Sources/Criteria/Criteria+SQL.swift b/Vienna/Sources/Criteria/Criteria+SQL.swift index 36c8085c16..889c755467 100644 --- a/Vienna/Sources/Criteria/Criteria+SQL.swift +++ b/Vienna/Sources/Criteria/Criteria+SQL.swift @@ -89,11 +89,11 @@ extension Criteria: SQLConversion { let startOfToday = Calendar.current.startOfDay(for: Date()) let startDate: Date? - if value == "today" { + if value == DateOffset.today.rawValue { startDate = startOfToday - } else if value == "yesterday" { + } else if value == DateOffset.yesterday.rawValue { startDate = Calendar.current.date(byAdding: .day, value: -1, to: startOfToday) - } else if value == "last week" { + } else if value == DateOffset.lastWeek.rawValue { startDate = Calendar.current.date(byAdding: .weekOfYear, value: -1, to: startOfToday) } else { // Check for the pattern for date with unit criteria @@ -142,10 +142,10 @@ extension Criteria: SQLConversion { guard operatorType == .equalTo || operatorType == .notEqualTo else { fatalError("Operator type \(operatorType) not applicable to flag field \(sqlFieldName)") } - let val: String - if value == "Yes" { val = "1" } else { val = "0" } + let sqlValue: String + if value == "Yes" { sqlValue = "1" } else { sqlValue = "0" } let sqlOperator = standardSqlOperator() - return "\(sqlFieldName) \(sqlOperator) \(val)" + return "\(sqlFieldName) \(sqlOperator) \(sqlValue)" } func folderSqlString(sqlFieldName: String, database: Database) -> String { diff --git a/Vienna/Sources/Criteria/Criteria.swift b/Vienna/Sources/Criteria/Criteria.swift index f965fe76b5..97495e88cc 100644 --- a/Vienna/Sources/Criteria/Criteria.swift +++ b/Vienna/Sources/Criteria/Criteria.swift @@ -45,40 +45,38 @@ enum CriteriaOperator: Int { // Workaround as long as this enum needs to be exposed to Objective-C and cannot have a string as raw value init?(rawValue: String) { - let criteriaOperator: CriteriaOperator switch rawValue { case "\(CriteriaOperator.equalTo)": - criteriaOperator = CriteriaOperator.equalTo + self = CriteriaOperator.equalTo case "\(CriteriaOperator.notEqualTo)": - criteriaOperator = CriteriaOperator.notEqualTo + self = CriteriaOperator.notEqualTo case "\(CriteriaOperator.lessThan)": - criteriaOperator = CriteriaOperator.lessThan + self = CriteriaOperator.lessThan case "\(CriteriaOperator.greaterThan)": - criteriaOperator = CriteriaOperator.greaterThan + self = CriteriaOperator.greaterThan case "\(CriteriaOperator.lessThanOrEqualTo)": - criteriaOperator = CriteriaOperator.lessThanOrEqualTo + self = CriteriaOperator.lessThanOrEqualTo case "\(CriteriaOperator.greaterThanOrEqualTo)": - criteriaOperator = CriteriaOperator.greaterThanOrEqualTo + self = CriteriaOperator.greaterThanOrEqualTo case "\(CriteriaOperator.contains)": - criteriaOperator = CriteriaOperator.contains + self = CriteriaOperator.contains case "\(CriteriaOperator.containsNot)": - criteriaOperator = CriteriaOperator.containsNot + self = CriteriaOperator.containsNot case "\(CriteriaOperator.before)": - criteriaOperator = CriteriaOperator.before + self = CriteriaOperator.before case "\(CriteriaOperator.after)": - criteriaOperator = CriteriaOperator.after + self = CriteriaOperator.after case "\(CriteriaOperator.onOrBefore)": - criteriaOperator = CriteriaOperator.onOrBefore + self = CriteriaOperator.onOrBefore case "\(CriteriaOperator.onOrAfter)": - criteriaOperator = CriteriaOperator.onOrAfter + self = CriteriaOperator.onOrAfter case "\(CriteriaOperator.under)": - criteriaOperator = CriteriaOperator.under + self = CriteriaOperator.under case "\(CriteriaOperator.notUnder)": - criteriaOperator = CriteriaOperator.notUnder + self = CriteriaOperator.notUnder default: return nil } - self.init(rawValue: criteriaOperator.rawValue) } var intValue: Int { diff --git a/Vienna/Sources/Criteria/DatePredicateWithUnit.swift b/Vienna/Sources/Criteria/DatePredicateWithUnit.swift index 85bdc411ce..f7e8b66d25 100644 --- a/Vienna/Sources/Criteria/DatePredicateWithUnit.swift +++ b/Vienna/Sources/Criteria/DatePredicateWithUnit.swift @@ -19,6 +19,10 @@ import Foundation +enum DateOffset: String { + case today, yesterday, lastWeek = "last week" +} + enum DateUnit: String, CaseIterable { case minutes case hours From 2a482597e43540cccb1338951c95eb7f84fe2958 Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Mon, 29 Jul 2024 00:36:53 +0200 Subject: [PATCH 07/14] localize new predicates in english --- Vienna/Resources/Base.lproj/Predicates.strings | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Vienna/Resources/Base.lproj/Predicates.strings b/Vienna/Resources/Base.lproj/Predicates.strings index 662c0c698a..b66c7f539b 100644 --- a/Vienna/Resources/Base.lproj/Predicates.strings +++ b/Vienna/Resources/Base.lproj/Predicates.strings @@ -1,8 +1,8 @@ "%[All,Any,None]@ of the following are true" = "Article matches %1$[all,any,none]@ of the following conditions"; "%[Author,Folder,Subject,Text]@ %[is,is not]@ %@" = "%1$[Author,Folder,Subject,Content]@ %2$[is,is not]@ %3$@"; "%[Author,Subject,Text]@ %[contains,does not contain]@ %@" = "%1$[Author,Subject,Content]@ %2$[contains,does not contain]@ %3$@"; -"%[Date]@ %[is,is greater than or equal to,is less than,is less than or equal to]@ %[yesterday]@" = "%1$[Last update]@ %2$[is,is after or is,is before,is before or is]@ %3$[yesterday]@"; -"%[Date]@ %[is,is greater than,is greater than or equal to,is less than,is less than or equal to]@ %[last week]@" = "%1$[Last update]@ %2$[is,is after,is after or is,is before,is before or is]@ %3$[last week]@"; -"%[Date]@ %[is,is less than,is less than or equal to]@ %[today]@" = "%1$[Last update]@ %2$[is,is before,is before or is]@ %3$[today]@"; -"%[Date]@ %[less than ago,more than ago]@ %[days,hours,minutes,months,weeks,years]@ %@" = "%1$[Last update]@ %2$[is in the last,is not in the last]@ %4$@ %3$[days,hours,minutes,months,weeks,years]@"; +"%[Date, PublicationDate]@ %[is,is greater than or equal to,is less than,is less than or equal to]@ %[yesterday]@" = "%1$[Last update, Date Published]@ %2$[is,is after or is,is before,is before or is]@ %3$[yesterday]@"; +"%[Date, PublicationDate]@ %[is,is greater than,is greater than or equal to,is less than,is less than or equal to]@ %[last week]@" = "%1$[Last update, Date Published]@ %2$[is,is after,is after or is,is before,is before or is]@ %3$[last week]@"; +"%[Date, PublicationDate]@ %[is,is less than,is less than or equal to]@ %[today]@" = "%1$[Last update, Date Published]@ %2$[is,is before,is before or is]@ %3$[today]@"; +"%[Date, PublicationDate]@ %[less than ago,more than ago]@ %[days,hours,minutes,months,weeks,years]@ %@" = "%1$[Last update, Date Published]@ %2$[is in the last,is not in the last]@ %4$@ %3$[days,hours,minutes,months,weeks,years]@"; "%[Deleted,Flagged,HasEnclosure,Read]@ is %[No,Yes]@" = "%1$[Is deleted,Is flagged,Has enclosure,Is read]@ %2$[No,Yes]@"; From 36b61129a578d2b0aa6b39e13270c8fe58ded79a Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Sun, 18 Aug 2024 17:03:26 +0200 Subject: [PATCH 08/14] check and fallback for empty date fields in open reader --- Vienna/Sources/Fetching/OpenReader.m | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Vienna/Sources/Fetching/OpenReader.m b/Vienna/Sources/Fetching/OpenReader.m index 8d9c19c4e7..210f16a002 100644 --- a/Vienna/Sources/Fetching/OpenReader.m +++ b/Vienna/Sources/Fetching/OpenReader.m @@ -568,8 +568,7 @@ -(void)feedRequestDone:(NSMutableURLRequest *)request response:(NSURLResponse *) NSMutableArray *articleArray = [NSMutableArray array]; for (NSDictionary *newsItem in (NSArray *)subscriptionsDict[@"items"]) { - NSDate *publicationDate = [NSDate dateWithTimeIntervalSince1970:[newsItem[@"published"] doubleValue]]; - NSDate *lastUpdate = [NSDate dateWithTimeIntervalSince1970:[newsItem[@"updated"] doubleValue]]; + NSString *articleGuid = newsItem[@"id"]; Article *article = [[Article alloc] initWithGuid:articleGuid]; article.folderId = refreshedFolder.itemId; @@ -612,8 +611,21 @@ -(void)feedRequestDone:(NSMutableURLRequest *)request response:(NSURLResponse *) article.link = refreshedFolder.feedURL; } - article.publicationDate = publicationDate == nil ? lastUpdate : publicationDate; - article.lastUpdate = lastUpdate == nil ? publicationDate : lastUpdate; + NSDate *currentDate = [NSDate date]; + + NSString *publishedField = newsItem[@"published"]; + double publishedDate = [publishedField doubleValue]; + NSDate *publicationDate = [NSDate dateWithTimeIntervalSince1970:publishedDate]; + + NSString * updatedField = newsItem[@"updated"]; + //if we get no update date, use the publication date, if that does not exist either, use the current date. + NSDate *lastUpdate = updatedField + ? [NSDate dateWithTimeIntervalSince1970:[updatedField doubleValue]] + : (publicationDate ? publicationDate : [NSDate date]); + + //fallback to currentDate for publicationDate; this will not be stored in the db in case of an update + article.publicationDate = publicationDate ? publicationDate : currentDate; + article.lastUpdate = lastUpdate; if ([newsItem[@"enclosure"] count] != 0) { article.enclosure = newsItem[@"enclosure"][0][@"href"]; From 2575ff6257f79555bda4fa4c532eb33f33ff2014 Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Sun, 18 Aug 2024 17:23:06 +0200 Subject: [PATCH 09/14] make date logic when storing articles more explicit --- Vienna/Sources/Database/Database.m | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Vienna/Sources/Database/Database.m b/Vienna/Sources/Database/Database.m index bd5b3d5050..a15dcfcd2e 100644 --- a/Vienna/Sources/Database/Database.m +++ b/Vienna/Sources/Database/Database.m @@ -1508,7 +1508,6 @@ -(BOOL)addArticle:(Article *)article toFolder:(NSInteger)folderID // Extract the article data from the dictionary. NSString * articleBody = article.body; NSString * articleTitle = article.title; - NSDate * lastUpdate = article.lastUpdate; NSString * articleLink = article.link.vna_trimmed; NSString * userName = article.author.vna_trimmed; NSString * articleEnclosure = article.enclosure.vna_trimmed; @@ -1520,13 +1519,15 @@ -(BOOL)addArticle:(Article *)article toFolder:(NSInteger)folderID BOOL deleted_flag = article.deleted; BOOL hasenclosure_flag = article.hasEnclosure; + NSDate *currentDate = [NSDate date]; + // We set the publication date ourselves if it is not contained in the feed, and only once when the article is created - article.publicationDate = article.publicationDate == nil ? [NSDate date] : article.publicationDate; + NSDate *publicationDate = article.publicationDate ? article.publicationDate : currentDate; + article.publicationDate = publicationDate; + + NSDate * lastUpdate = article.lastUpdate ? article.lastUpdate : currentDate; // Set some defaults - if (lastUpdate == nil) { - lastUpdate = [NSDate date]; - } if (userName == nil) { userName = @""; } @@ -1538,8 +1539,7 @@ -(BOOL)addArticle:(Article *)article toFolder:(NSInteger)folderID // Dates are stored as time intervals NSTimeInterval lastUpdateIntervalSince1970 = lastUpdate.timeIntervalSince1970; - NSTimeInterval publicationIntervalSince1970 = - (article.publicationDate == nil ? [NSDate date] : article.publicationDate).timeIntervalSince1970; + NSTimeInterval publicationIntervalSince1970 = publicationDate.timeIntervalSince1970; __block BOOL success; [queue inTransaction:^(FMDatabase *db, BOOL *rollback) { @@ -1594,17 +1594,17 @@ -(BOOL)updateArticle:(Article *)existingArticle ofFolder:(NSInteger)folderID wit // Extract the data from the new state of article NSString * articleBody = article.body; NSString * articleTitle = article.title; - NSDate * lastUpdate = article.lastUpdate; NSString * articleLink = article.link.vna_trimmed; NSString * userName = article.author.vna_trimmed; NSString * articleGuid = article.guid; NSInteger parentId = article.parentId; BOOL revised_flag = article.revised; + //keep last update date the same if not set in the current version of the article + NSDate * lastUpdate = article.lastUpdate ? article.lastUpdate : existingArticle.lastUpdate; + //we do not update the publication date ever after inserting the article into the DB + // Set some defaults - if (lastUpdate == nil) { - lastUpdate = existingArticle.lastUpdate; - } if (userName == nil) { userName = @""; } From 2f86ec61b97adfbc739191bf0dd5823cefa2b799 Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Sun, 18 Aug 2024 17:40:25 +0200 Subject: [PATCH 10/14] Use earliest date available for publication date Only when there is a provided publication date on creation, use that one without questioning it Fix publication date not being transmitted by RefreshManager.m Most dates interpretations/manipulations are now in Database.m During fetching from feeds, we just retrieve the infos and store them in relevant Article fields. This makes the logic more apprehensible and easier to maintain. Solves issue #1749 Co-authored-by: Barijaona Ramaholimihaso --- Vienna/Sources/Database/Database.m | 49 ++++++++++++++++-------- Vienna/Sources/Fetching/OpenReader.m | 18 +++------ Vienna/Sources/Fetching/RefreshManager.m | 2 + Vienna/Sources/Parsing/AtomFeed.m | 10 ++++- 4 files changed, 50 insertions(+), 29 deletions(-) diff --git a/Vienna/Sources/Database/Database.m b/Vienna/Sources/Database/Database.m index a15dcfcd2e..691bf4face 100644 --- a/Vienna/Sources/Database/Database.m +++ b/Vienna/Sources/Database/Database.m @@ -1521,11 +1521,25 @@ -(BOOL)addArticle:(Article *)article toFolder:(NSInteger)folderID NSDate *currentDate = [NSDate date]; - // We set the publication date ourselves if it is not contained in the feed, and only once when the article is created - NSDate *publicationDate = article.publicationDate ? article.publicationDate : currentDate; - article.publicationDate = publicationDate; + // use the given publication date if it is contained in the feed, or the earliest date available + NSDate * publicationDate = article.publicationDate; + if (!publicationDate) { + publicationDate = article.lastUpdate && [article.lastUpdate isLessThan:currentDate] + ? article.lastUpdate + : currentDate; + article.publicationDate = publicationDate; + } + + // if a last update date is not provided, use our publication date + NSDate * lastUpdate = article.lastUpdate; + if (!lastUpdate) { + lastUpdate = publicationDate; + article.lastUpdate = lastUpdate; + } - NSDate * lastUpdate = article.lastUpdate ? article.lastUpdate : currentDate; + // Dates are stored as time intervals + NSTimeInterval lastUpdateIntervalSince1970 = lastUpdate.timeIntervalSince1970; + NSTimeInterval publicationIntervalSince1970 = publicationDate.timeIntervalSince1970; // Set some defaults if (userName == nil) { @@ -1537,10 +1551,6 @@ -(BOOL)addArticle:(Article *)article toFolder:(NSInteger)folderID articleTitle = [NSString vna_stringByRemovingHTML:articleBody].vna_firstNonBlankLine; } - // Dates are stored as time intervals - NSTimeInterval lastUpdateIntervalSince1970 = lastUpdate.timeIntervalSince1970; - NSTimeInterval publicationIntervalSince1970 = publicationDate.timeIntervalSince1970; - __block BOOL success; [queue inTransaction:^(FMDatabase *db, BOOL *rollback) { success = [db executeUpdate:@"INSERT INTO messages (message_id, parent_id, folder_id, sender, link, date, createddate, read_flag, marked_flag, deleted_flag, title, text, revised_flag, enclosure, hasenclosure_flag) " @@ -1600,9 +1610,20 @@ -(BOOL)updateArticle:(Article *)existingArticle ofFolder:(NSInteger)folderID wit NSInteger parentId = article.parentId; BOOL revised_flag = article.revised; - //keep last update date the same if not set in the current version of the article - NSDate * lastUpdate = article.lastUpdate ? article.lastUpdate : existingArticle.lastUpdate; - //we do not update the publication date ever after inserting the article into the DB + // keep last update date the same if not set in the current version of the article + NSDate * lastUpdate = article.lastUpdate && [article.lastUpdate isGreaterThan:existingArticle.lastUpdate] + ? article.lastUpdate + : existingArticle.lastUpdate; + + // we never change the publication date, unless the date provided in the feed is prior to it + NSDate * publicationDate = existingArticle.publicationDate; + if (article.publicationDate && [article.publicationDate isLessThan:existingArticle.publicationDate]) { + publicationDate = article.publicationDate; + } + + // Dates are stored as time intervals + NSTimeInterval lastUpdateIntervalSince1970 = lastUpdate.timeIntervalSince1970; + NSTimeInterval publicationIntervalSince1970 = publicationDate.timeIntervalSince1970; // Set some defaults if (userName == nil) { @@ -1614,9 +1635,6 @@ -(BOOL)updateArticle:(Article *)existingArticle ofFolder:(NSInteger)folderID wit articleTitle = [NSString vna_stringByRemovingHTML:articleBody].vna_firstNonBlankLine; } - // Dates are stored as time intervals - NSTimeInterval lastUpdateIntervalSince1970 = lastUpdate.timeIntervalSince1970; - // The article is revised if either the title or the body has changed. NSString * existingTitle = existingArticle.title; @@ -1649,12 +1667,13 @@ -(BOOL)updateArticle:(Article *)existingArticle ofFolder:(NSInteger)folderID wit __block BOOL success; [queue inDatabase:^(FMDatabase *db) { - success = [db executeUpdate:@"UPDATE messages SET parent_id=?, sender=?, link=?, date=?, " + success = [db executeUpdate:@"UPDATE messages SET parent_id=?, sender=?, link=?, date=?, createddate=?, " @"read_flag=0, title=?, text=?, revised_flag=? WHERE folder_id=? AND message_id=?", @(parentId), userName, articleLink, @(lastUpdateIntervalSince1970), + @(publicationIntervalSince1970), articleTitle, articleBody, @(revised_flag), diff --git a/Vienna/Sources/Fetching/OpenReader.m b/Vienna/Sources/Fetching/OpenReader.m index 210f16a002..aa5e085667 100644 --- a/Vienna/Sources/Fetching/OpenReader.m +++ b/Vienna/Sources/Fetching/OpenReader.m @@ -611,21 +611,15 @@ -(void)feedRequestDone:(NSMutableURLRequest *)request response:(NSURLResponse *) article.link = refreshedFolder.feedURL; } - NSDate *currentDate = [NSDate date]; - NSString *publishedField = newsItem[@"published"]; - double publishedDate = [publishedField doubleValue]; - NSDate *publicationDate = [NSDate dateWithTimeIntervalSince1970:publishedDate]; + if (publishedField) { + article.publicationDate = [NSDate dateWithTimeIntervalSince1970:[publishedField doubleValue]]; + } NSString * updatedField = newsItem[@"updated"]; - //if we get no update date, use the publication date, if that does not exist either, use the current date. - NSDate *lastUpdate = updatedField - ? [NSDate dateWithTimeIntervalSince1970:[updatedField doubleValue]] - : (publicationDate ? publicationDate : [NSDate date]); - - //fallback to currentDate for publicationDate; this will not be stored in the db in case of an update - article.publicationDate = publicationDate ? publicationDate : currentDate; - article.lastUpdate = lastUpdate; + if (updatedField) { + article.lastUpdate = [NSDate dateWithTimeIntervalSince1970:[updatedField doubleValue]]; + } if ([newsItem[@"enclosure"] count] != 0) { article.enclosure = newsItem[@"enclosure"][0][@"href"]; diff --git a/Vienna/Sources/Fetching/RefreshManager.m b/Vienna/Sources/Fetching/RefreshManager.m index e749ec9d98..b2e7006695 100644 --- a/Vienna/Sources/Fetching/RefreshManager.m +++ b/Vienna/Sources/Fetching/RefreshManager.m @@ -764,6 +764,7 @@ -(void)finalizeFolderRefresh:(NSDictionary *)parameters for (id newsItem in newFeed.items) { NSDate * articleDate = newsItem.modificationDate; + NSDate * publicationDate = newsItem.publicationDate; NSString * articleGuid = newsItem.guid; @@ -838,6 +839,7 @@ -(void)finalizeFolderRefresh:(NSDictionary *)parameters } article.link = articleLink; article.lastUpdate = articleDate; + article.publicationDate = publicationDate; NSString * enclosureLink = newsItem.enclosure; if ([enclosureLink isNotEqualTo:@""] && ![enclosureLink hasPrefix:@"http:"] && ![enclosureLink hasPrefix:@"https:"]) { enclosureLink = [NSURL URLWithString:enclosureLink relativeToURL:url].absoluteString; diff --git a/Vienna/Sources/Parsing/AtomFeed.m b/Vienna/Sources/Parsing/AtomFeed.m index 0a66ff44e7..c350bdcb29 100644 --- a/Vienna/Sources/Parsing/AtomFeed.m +++ b/Vienna/Sources/Parsing/AtomFeed.m @@ -253,14 +253,20 @@ - (BOOL)initAtomFeed:(NSXMLElement *)atomElement } // Parse item date - if (isArticleElementAtomType && ([articleItemTag isEqualToString:@"modified"] || [articleItemTag isEqualToString:@"created"] || [articleItemTag isEqualToString:@"updated"])) { + if (isArticleElementAtomType && ([articleItemTag isEqualToString:@"modified"] || [articleItemTag isEqualToString:@"updated"])) { NSString *dateString = itemChildElement.stringValue; NSDate *newDate = [self dateWithXMLString:dateString]; if (newFeedItem.modificationDate == nil || [newDate isGreaterThan:newFeedItem.modificationDate]) { newFeedItem.modificationDate = newDate; } + continue; + } + + // Parse item date + if (isArticleElementAtomType && ([articleItemTag isEqualToString:@"created"] || [articleItemTag isEqualToString:@"published"])) { + NSString *dateString = itemChildElement.stringValue; + NSDate *newDate = [self dateWithXMLString:dateString]; if (newFeedItem.publicationDate == nil || [newDate isLessThan:newFeedItem.publicationDate]) { - //publication date will only be registered once for each article, so later updates don´t matter newFeedItem.publicationDate = newDate; } continue; From 80d9d2940514ad1b10adec32c36f6c33c61f33a0 Mon Sep 17 00:00:00 2001 From: Barijaona Ramaholimihaso Date: Sat, 24 Aug 2024 17:52:40 +0300 Subject: [PATCH 11/14] Handle dc:modified elements in RSS feeds Therefore, regarding dates, adopt in RSSFeed the same logic than in AtomFeed. Adapt RefreshManager to this unified logic. --- Vienna/Sources/Fetching/RefreshManager.m | 8 ++++---- Vienna/Sources/Parsing/RSSFeed.m | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Vienna/Sources/Fetching/RefreshManager.m b/Vienna/Sources/Fetching/RefreshManager.m index b2e7006695..5c97ac6ab4 100644 --- a/Vienna/Sources/Fetching/RefreshManager.m +++ b/Vienna/Sources/Fetching/RefreshManager.m @@ -763,8 +763,8 @@ -(void)finalizeFolderRefresh:(NSDictionary *)parameters // Parse off items. for (id newsItem in newFeed.items) { - NSDate * articleDate = newsItem.modificationDate; - NSDate * publicationDate = newsItem.publicationDate; + NSDate * articleDate = newsItem.publicationDate; + NSDate * modificationDate = newsItem.modificationDate; NSString * articleGuid = newsItem.guid; @@ -838,8 +838,8 @@ -(void)finalizeFolderRefresh:(NSDictionary *)parameters articleLink = feedLink; } article.link = articleLink; - article.lastUpdate = articleDate; - article.publicationDate = publicationDate; + article.publicationDate = articleDate; + article.lastUpdate = modificationDate; NSString * enclosureLink = newsItem.enclosure; if ([enclosureLink isNotEqualTo:@""] && ![enclosureLink hasPrefix:@"http:"] && ![enclosureLink hasPrefix:@"https:"]) { enclosureLink = [NSURL URLWithString:enclosureLink relativeToURL:url].absoluteString; diff --git a/Vienna/Sources/Parsing/RSSFeed.m b/Vienna/Sources/Parsing/RSSFeed.m index 29e8131ee7..8a843a78b4 100644 --- a/Vienna/Sources/Parsing/RSSFeed.m +++ b/Vienna/Sources/Parsing/RSSFeed.m @@ -260,8 +260,19 @@ - (BOOL)initRSSFeedItems:(NSXMLElement *)startElement // Parse item date if ((isRSSElement && [articleItemTag isEqualToString:@"pubDate"]) || ([itemChildElement.prefix isEqualToString:self.dcPrefix] && [articleItemTag isEqualToString:@"date"])) { - NSString *dateString = itemChildElement.stringValue; - newFeedItem.modificationDate = [self dateWithXMLString:dateString]; + NSDate *newDate = [self dateWithXMLString:itemChildElement.stringValue]; + if (newFeedItem.publicationDate == nil || [newDate isLessThan:newFeedItem.publicationDate]) { + newFeedItem.publicationDate = newDate; + } + continue; + } + + // Parse item modification date + if ([itemChildElement.prefix isEqualToString:self.dcPrefix] && [articleItemTag isEqualToString:@"modified"]) { + NSDate *newDate = [self dateWithXMLString:itemChildElement.stringValue]; + if (newFeedItem.modificationDate == nil || [newDate isGreaterThan:newFeedItem.modificationDate]) { + newFeedItem.modificationDate = newDate; + } continue; } From 0b979ef4f4090b05d2adf0b95de7da4f76af84b6 Mon Sep 17 00:00:00 2001 From: Barijaona Ramaholimihaso Date: Sat, 24 Aug 2024 14:54:30 +0300 Subject: [PATCH 12/14] Make treatment uniform for the 2 date columns --- Vienna/Sources/Main window/ArticleController.m | 6 +++--- Vienna/Sources/Main window/ArticleListView.m | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Vienna/Sources/Main window/ArticleController.m b/Vienna/Sources/Main window/ArticleController.m index 774320aee5..3a0b5c6958 100644 --- a/Vienna/Sources/Main window/ArticleController.m +++ b/Vienna/Sources/Main window/ArticleController.m @@ -292,9 +292,9 @@ -(void)sortByIdentifier:(NSString *)columnName NSUInteger index = [[descriptors valueForKey:@"key"] indexOfObject:[specifier valueForKey:@"key"]]; if (index == NSNotFound) { - // Date should be sorted in descending order. - // TODO: Add a key to articleSortSpecifiers for a default sort order - BOOL ascending = [columnName isEqualToString:MA_Field_PublicationDate] ? NO : YES; + // Dates should be sorted initially in descending order + // MIGHT DO : Add a key to articleSortSpecifiers for a default sort order + BOOL ascending = [columnName isEqualToString:MA_Field_PublicationDate] || [columnName isEqualToString:MA_Field_LastUpdate] ? NO : YES; sortDescriptor = [[NSSortDescriptor alloc] initWithKey:[specifier valueForKey:@"key"] ascending:ascending selector:NSSelectorFromString([specifier valueForKey:@"selector"])]; } else { sortDescriptor = descriptors[index]; diff --git a/Vienna/Sources/Main window/ArticleListView.m b/Vienna/Sources/Main window/ArticleListView.m index 22d5c442a1..fdc5abbc5f 100644 --- a/Vienna/Sources/Main window/ArticleListView.m +++ b/Vienna/Sources/Main window/ArticleListView.m @@ -594,7 +594,6 @@ -(void)showSortDirection { NSString * sortColumnIdentifier = self.controller.articleController.sortColumnIdentifier; - // FIXME: Sort out the order of initialization if (!sortColumnIdentifier) { sortColumnIdentifier = [Preferences.standardPreferences stringForKey:MAPref_SortColumn]; } From bb067607e7348369a05476836c4cee130f5d7564 Mon Sep 17 00:00:00 2001 From: Barijaona Ramaholimihaso Date: Sun, 25 Aug 2024 03:44:09 +0300 Subject: [PATCH 13/14] Adapt the "Last Refresh" filter As the meaning of `createddate` has changed, we have to adopt another approach which provides similar (but not identical) results. --- Vienna/Sources/Application/AppController.m | 4 ---- Vienna/Sources/Main window/ArticleController.m | 5 +---- Vienna/Sources/Preferences window/Preferences.m | 1 - Vienna/Sources/Shared/Constants.h | 1 - Vienna/Sources/Shared/Constants.m | 1 - 5 files changed, 1 insertion(+), 11 deletions(-) diff --git a/Vienna/Sources/Application/AppController.m b/Vienna/Sources/Application/AppController.m index 3563722474..2c3efb20fb 100644 --- a/Vienna/Sources/Application/AppController.m +++ b/Vienna/Sources/Application/AppController.m @@ -1764,10 +1764,6 @@ -(void)handleFolderNameChange:(NSNotification *)nc -(void)handleRefreshStatusChange:(NSNotification *)nc { if (self.connecting) { - // Save the date/time of this refresh so we do the right thing when - // we apply the filter. - [[Preferences standardPreferences] setObject:[NSDate date] forKey:MAPref_LastRefreshDate]; - // Toggle the refresh button NSToolbarItem *item = [self toolbarItemWithIdentifier:@"Refresh"]; item.action = @selector(cancelAllRefreshesToolbar:); diff --git a/Vienna/Sources/Main window/ArticleController.m b/Vienna/Sources/Main window/ArticleController.m index 3a0b5c6958..c849d5d5cc 100644 --- a/Vienna/Sources/Main window/ArticleController.m +++ b/Vienna/Sources/Main window/ArticleController.m @@ -1021,10 +1021,7 @@ - (BOOL)filterArticle:(Article *)article usingMode:(NSInteger)filterMode { case VNAFilterUnread: return !article.read; case VNAFilterLastRefresh: { - NSDate *date = article.publicationDate; - Preferences *prefs = [Preferences standardPreferences]; - NSComparisonResult result = [date compare:[prefs objectForKey:MAPref_LastRefreshDate]]; - return result != NSOrderedAscending; + return article.status == ArticleStatusNew || article.status == ArticleStatusUpdated; } case VNAFilterToday: return [NSCalendar.currentCalendar isDateInToday:article.lastUpdate]; diff --git a/Vienna/Sources/Preferences window/Preferences.m b/Vienna/Sources/Preferences window/Preferences.m index 2b91363ed4..8ff3534264 100644 --- a/Vienna/Sources/Preferences window/Preferences.m +++ b/Vienna/Sources/Preferences window/Preferences.m @@ -236,7 +236,6 @@ -(NSDictionary *)allocFactoryDefaults defaultValues[MAPref_FilterMode] = [NSNumber numberWithInt:VNAFilterAll]; defaultValues[MAPref_MinimumFontSize] = @(MA_Default_MinimumFontSize); defaultValues[MAPref_AutoExpireDuration] = @(MA_Default_AutoExpireDuration); - defaultValues[MAPref_LastRefreshDate] = [NSDate distantPast]; defaultValues[MAPref_Layout] = [NSNumber numberWithInt:VNALayoutReport]; defaultValues[MAPref_NewArticlesNotification] = [NSNumber numberWithInt:0]; defaultValues[MAPref_EmptyTrashNotification] = [NSNumber numberWithInt:VNAEmptyTrashWithWarning]; diff --git a/Vienna/Sources/Shared/Constants.h b/Vienna/Sources/Shared/Constants.h index a297567c55..e00ed36e87 100644 --- a/Vienna/Sources/Shared/Constants.h +++ b/Vienna/Sources/Shared/Constants.h @@ -87,7 +87,6 @@ extern NSString * const MAPref_UseJavaScript; extern NSString * const MAPref_CachedArticleGUID; extern NSString * const MAPref_ArticleListSortOrders; extern NSString * const MAPref_FilterMode; -extern NSString * const MAPref_LastRefreshDate; extern NSString * const MAPref_TabList; extern NSString * const MAPref_TabTitleDictionary; extern NSString * const MAPref_Layout; diff --git a/Vienna/Sources/Shared/Constants.m b/Vienna/Sources/Shared/Constants.m index 34e38f8530..d7b1b97c15 100644 --- a/Vienna/Sources/Shared/Constants.m +++ b/Vienna/Sources/Shared/Constants.m @@ -87,7 +87,6 @@ NSString * const MAPref_CachedArticleGUID = @"CachedArticleGUID"; NSString * const MAPref_ArticleListSortOrders = @"ArticleListSortOrders"; NSString * const MAPref_FilterMode = @"FilterMode"; -NSString * const MAPref_LastRefreshDate = @"LastRefreshDate"; NSString * const MAPref_TabList = @"TabList"; NSString * const MAPref_TabTitleDictionary = @"TabTitleDict"; NSString * const MAPref_Layout = @"Layout"; From 2930d7e7144094ba8528d81272d86520bb167dd0 Mon Sep 17 00:00:00 2001 From: Tassilo Karge Date: Sun, 25 Aug 2024 15:26:23 +0200 Subject: [PATCH 14/14] add publication date in Criteria+NSPredicate.swift this allows the predicate editor to correctly re-load "date published" predicates. Also, make the SmartFolder code better by extracting the repeated array of Date expressions and remove superfluous space in predicate translation strings --- .../Resources/Base.lproj/Predicates.strings | 8 +++---- Vienna/Sources/Alerts/SmartFolder.m | 21 ++++++++----------- .../Criteria/Criteria+NSPredicate.swift | 10 +++++---- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/Vienna/Resources/Base.lproj/Predicates.strings b/Vienna/Resources/Base.lproj/Predicates.strings index b66c7f539b..9a37fa63ae 100644 --- a/Vienna/Resources/Base.lproj/Predicates.strings +++ b/Vienna/Resources/Base.lproj/Predicates.strings @@ -1,8 +1,8 @@ "%[All,Any,None]@ of the following are true" = "Article matches %1$[all,any,none]@ of the following conditions"; "%[Author,Folder,Subject,Text]@ %[is,is not]@ %@" = "%1$[Author,Folder,Subject,Content]@ %2$[is,is not]@ %3$@"; "%[Author,Subject,Text]@ %[contains,does not contain]@ %@" = "%1$[Author,Subject,Content]@ %2$[contains,does not contain]@ %3$@"; -"%[Date, PublicationDate]@ %[is,is greater than or equal to,is less than,is less than or equal to]@ %[yesterday]@" = "%1$[Last update, Date Published]@ %2$[is,is after or is,is before,is before or is]@ %3$[yesterday]@"; -"%[Date, PublicationDate]@ %[is,is greater than,is greater than or equal to,is less than,is less than or equal to]@ %[last week]@" = "%1$[Last update, Date Published]@ %2$[is,is after,is after or is,is before,is before or is]@ %3$[last week]@"; -"%[Date, PublicationDate]@ %[is,is less than,is less than or equal to]@ %[today]@" = "%1$[Last update, Date Published]@ %2$[is,is before,is before or is]@ %3$[today]@"; -"%[Date, PublicationDate]@ %[less than ago,more than ago]@ %[days,hours,minutes,months,weeks,years]@ %@" = "%1$[Last update, Date Published]@ %2$[is in the last,is not in the last]@ %4$@ %3$[days,hours,minutes,months,weeks,years]@"; +"%[Date,PublicationDate]@ %[is,is greater than or equal to,is less than,is less than or equal to]@ %[yesterday]@" = "%1$[Last update, Date Published]@ %2$[is,is after or is,is before,is before or is]@ %3$[yesterday]@"; +"%[Date,PublicationDate]@ %[is,is greater than,is greater than or equal to,is less than,is less than or equal to]@ %[last week]@" = "%1$[Last update,Date Published]@ %2$[is,is after,is after or is,is before,is before or is]@ %3$[last week]@"; +"%[Date,PublicationDate]@ %[is,is less than,is less than or equal to]@ %[today]@" = "%1$[Last update,Date Published]@ %2$[is,is before,is before or is]@ %3$[today]@"; +"%[Date,PublicationDate]@ %[less than ago,more than ago]@ %[days,hours,minutes,months,weeks,years]@ %@" = "%1$[Last update,Date Published]@ %2$[is in the last,is not in the last]@ %4$@ %3$[days,hours,minutes,months,weeks,years]@"; "%[Deleted,Flagged,HasEnclosure,Read]@ is %[No,Yes]@" = "%1$[Is deleted,Is flagged,Has enclosure,Is read]@ %2$[No,Yes]@"; diff --git a/Vienna/Sources/Alerts/SmartFolder.m b/Vienna/Sources/Alerts/SmartFolder.m index 03bc911086..8653ceba5b 100644 --- a/Vienna/Sources/Alerts/SmartFolder.m +++ b/Vienna/Sources/Alerts/SmartFolder.m @@ -172,11 +172,14 @@ - (void)prepareTemplates [rowTemplates addObject:[VNASeparatorPredicateEditorRowTemplate new]]; + + NSArray *dateLeftExpressions = @[ + [NSExpression expressionForConstantValue:MA_Field_LastUpdate], + [NSExpression expressionForConstantValue:MA_Field_PublicationDate]]; + // date < / > days / weeks / months / years old NSPredicateEditorRowTemplate *dateCompareTemplate = [[VNADateWithUnitPredicateEditorRowTemplate alloc] - initWithLeftExpressions:@[ - [NSExpression expressionForConstantValue:MA_Field_LastUpdate], - [NSExpression expressionForConstantValue:MA_Field_PublicationDate]]]; + initWithLeftExpressions:dateLeftExpressions]; [rowTemplates addObject:dateCompareTemplate]; // date = / < / <= today / yesterday / lastWeek @@ -189,9 +192,7 @@ - (void)prepareTemplates @(NSLessThanOrEqualToPredicateOperatorType), ]; NSPredicateEditorRowTemplate *todayTemplate = [[NSPredicateEditorRowTemplate alloc] - initWithLeftExpressions:@[ - [NSExpression expressionForConstantValue:MA_Field_LastUpdate], - [NSExpression expressionForConstantValue:MA_Field_PublicationDate]] + initWithLeftExpressions:dateLeftExpressions rightExpressions:todayRightExpressions modifier:NSDirectPredicateModifier operators:todayOperators @@ -209,9 +210,7 @@ - (void)prepareTemplates @(NSGreaterThanOrEqualToPredicateOperatorType) ]; NSPredicateEditorRowTemplate *yesterdayTemplate = [[NSPredicateEditorRowTemplate alloc] - initWithLeftExpressions:@[ - [NSExpression expressionForConstantValue:MA_Field_LastUpdate], - [NSExpression expressionForConstantValue:MA_Field_PublicationDate]] + initWithLeftExpressions:dateLeftExpressions rightExpressions:yesterdayRightExpressions modifier:NSDirectPredicateModifier operators:yesterdayOperators @@ -230,9 +229,7 @@ - (void)prepareTemplates @(NSLessThanOrEqualToPredicateOperatorType) ]; NSPredicateEditorRowTemplate *dateTemplate = [[NSPredicateEditorRowTemplate alloc] - initWithLeftExpressions:@[ - [NSExpression expressionForConstantValue:MA_Field_LastUpdate], - [NSExpression expressionForConstantValue:MA_Field_PublicationDate]] + initWithLeftExpressions:dateLeftExpressions rightExpressions:weekRightExpressions modifier:NSDirectPredicateModifier operators:weekOperators diff --git a/Vienna/Sources/Criteria/Criteria+NSPredicate.swift b/Vienna/Sources/Criteria/Criteria+NSPredicate.swift index 6cba63ea7b..052bd1d813 100644 --- a/Vienna/Sources/Criteria/Criteria+NSPredicate.swift +++ b/Vienna/Sources/Criteria/Criteria+NSPredicate.swift @@ -177,7 +177,7 @@ extension Criteria: PredicateConvertible { fallback = true criteriaOperator = .equalTo } - case MA_Field_LastUpdate: + case MA_Field_LastUpdate, MA_Field_PublicationDate: switch predicate.predicateOperatorType { case .lessThan: criteriaOperator = .before @@ -231,14 +231,15 @@ extension Criteria: PredicateConvertible { let value = self.value let operatorType = self.operatorType - if field == MA_Field_LastUpdate, let unit = DateUnit.allCases.first(where: { value.hasSuffix($0.rawValue) }) { + if field == MA_Field_LastUpdate || field == MA_Field_PublicationDate, + let unit = DateUnit.allCases.first(where: { value.hasSuffix($0.rawValue) }) { let countString = value .replacingOccurrences(of: unit.rawValue, with: "") .trimmingCharacters(in: CharacterSet.whitespaces) guard let count = UInt(countString) else { fatalError("malformed criteria value \(value)") } - return DatePredicateWithUnit(field: MA_Field_LastUpdate, comparisonOperator: operatorType, count: count, unit: unit) + return DatePredicateWithUnit(field: field, comparisonOperator: operatorType, count: count, unit: unit) } else { return buildComparisonPredicate(field, value, operatorType) } @@ -291,7 +292,8 @@ extension Criteria: PredicateConvertible { // TODO: constants for fixed criteria values also for Criteria+SQL, // e.g. YES, NO, yesterday, today, last week, ... - if field == MA_Field_LastUpdate && operatorType == .after && value == DateOffset.yesterday.rawValue { + if (field == MA_Field_LastUpdate || field == MA_Field_PublicationDate) + && operatorType == .after && value == DateOffset.yesterday.rawValue { // Use canonical "is today" instead of "is after yesterday" comparisonPredicate = NSComparisonPredicate(leftExpression: left, rightExpression: NSExpression(forConstantValue: DateOffset.today), modifier: .direct, type: .equalTo) } else if operatorType == .notEqualTo && (value == "No" || value == "Yes") {