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..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]@ %[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,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/Resources/Localizable.xcstrings b/Vienna/Resources/Localizable.xcstrings index 19a89d25a5..384bb147d7 100644 --- a/Vienna/Resources/Localizable.xcstrings +++ b/Vienna/Resources/Localizable.xcstrings @@ -6522,6 +6522,7 @@ }, "Date" : { "comment" : "Data field name visible in menu/article list/smart folder definition", + "extractionState" : "stale", "localizations" : { "cs" : { "stringUnit" : { @@ -6645,6 +6646,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" : { @@ -12888,6 +12892,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..8653ceba5b 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 @@ -172,8 +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_Date]]]; + NSPredicateEditorRowTemplate *dateCompareTemplate = [[VNADateWithUnitPredicateEditorRowTemplate alloc] + initWithLeftExpressions:dateLeftExpressions]; [rowTemplates addObject:dateCompareTemplate]; // date = / < / <= today / yesterday / lastWeek @@ -186,7 +192,7 @@ - (void)prepareTemplates @(NSLessThanOrEqualToPredicateOperatorType), ]; NSPredicateEditorRowTemplate *todayTemplate = [[NSPredicateEditorRowTemplate alloc] - initWithLeftExpressions:@[[NSExpression expressionForConstantValue:@"Date"]] + initWithLeftExpressions:dateLeftExpressions rightExpressions:todayRightExpressions modifier:NSDirectPredicateModifier operators:todayOperators @@ -204,11 +210,11 @@ - (void)prepareTemplates @(NSGreaterThanOrEqualToPredicateOperatorType) ]; NSPredicateEditorRowTemplate *yesterdayTemplate = [[NSPredicateEditorRowTemplate alloc] - initWithLeftExpressions:@[[NSExpression expressionForConstantValue:@"Date"]] - rightExpressions:yesterdayRightExpressions - modifier:NSDirectPredicateModifier - operators:yesterdayOperators - options:0]; + initWithLeftExpressions:dateLeftExpressions + rightExpressions:yesterdayRightExpressions + modifier:NSDirectPredicateModifier + operators:yesterdayOperators + options:0]; [rowTemplates addObject:yesterdayTemplate]; // date = / > / >= / < / <= last week @@ -223,7 +229,7 @@ - (void)prepareTemplates @(NSLessThanOrEqualToPredicateOperatorType) ]; NSPredicateEditorRowTemplate *dateTemplate = [[NSPredicateEditorRowTemplate alloc] - initWithLeftExpressions:@[[NSExpression expressionForConstantValue:@"Date"]] + initWithLeftExpressions:dateLeftExpressions rightExpressions:weekRightExpressions modifier:NSDirectPredicateModifier operators:weekOperators @@ -234,10 +240,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 +262,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/Application/AppController.m b/Vienna/Sources/Application/AppController.m index c44e0a3017..2c3efb20fb 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; @@ -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/Criteria/Criteria+NSPredicate.swift b/Vienna/Sources/Criteria/Criteria+NSPredicate.swift index 96298d5b15..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_Date: + 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_Date, 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_Date, comparisonOperator: operatorType, count: count, unit: unit) + return DatePredicateWithUnit(field: field, comparisonOperator: operatorType, count: count, unit: unit) } else { return buildComparisonPredicate(field, value, operatorType) } @@ -291,9 +292,10 @@ 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 || 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: "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 diff --git a/Vienna/Sources/Database/Database.m b/Vienna/Sources/Database/Database.m index 7bf3f3436c..691bf4face 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) { @@ -328,22 +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_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_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]; + [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"); @@ -353,7 +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_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"); @@ -1506,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 * articleDate = article.date; NSString * articleLink = article.link.vna_trimmed; NSString * userName = article.author.vna_trimmed; NSString * articleEnclosure = article.enclosure.vna_trimmed; @@ -1518,13 +1519,29 @@ -(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]; + NSDate *currentDate = [NSDate date]; - // Set some defaults - if (articleDate == nil) { - articleDate = [NSDate date]; + // 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; } + + // Dates are stored as time intervals + NSTimeInterval lastUpdateIntervalSince1970 = lastUpdate.timeIntervalSince1970; + NSTimeInterval publicationIntervalSince1970 = publicationDate.timeIntervalSince1970; + + // Set some defaults if (userName == nil) { userName = @""; } @@ -1534,10 +1551,6 @@ -(BOOL)addArticle:(Article *)article toFolder:(NSInteger)folderID articleTitle = [NSString vna_stringByRemovingHTML:articleBody].vna_firstNonBlankLine; } - // Dates are stored as time intervals - NSTimeInterval interval = articleDate.timeIntervalSince1970; - NSTimeInterval createdInterval = article.createdDate.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) " @@ -1547,8 +1560,8 @@ -(BOOL)addArticle:(Article *)article toFolder:(NSInteger)folderID @(folderID), userName, articleLink, - @(interval), - @(createdInterval), + @(lastUpdateIntervalSince1970), + @(publicationIntervalSince1970), @(read_flag), @(marked_flag), @(deleted_flag), @@ -1591,17 +1604,28 @@ -(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; 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; - // Set some defaults - if (articleDate == nil) { - articleDate = existingArticle.date; + // 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) { userName = @""; } @@ -1611,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 interval = articleDate.timeIntervalSince1970; - // The article is revised if either the title or the body has changed. NSString * existingTitle = existingArticle.title; @@ -1646,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, - @(interval), + @(lastUpdateIntervalSince1970), + @(publicationIntervalSince1970), articleTitle, articleBody, @(revised_flag), @@ -2226,8 +2248,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..aa5e085667 100644 --- a/Vienna/Sources/Fetching/OpenReader.m +++ b/Vienna/Sources/Fetching/OpenReader.m @@ -568,7 +568,7 @@ -(void)feedRequestDone:(NSMutableURLRequest *)request response:(NSURLResponse *) NSMutableArray *articleArray = [NSMutableArray array]; for (NSDictionary *newsItem in (NSArray *)subscriptionsDict[@"items"]) { - NSDate *articleDate = [NSDate dateWithTimeIntervalSince1970:[newsItem[@"published"] doubleValue]]; + NSString *articleGuid = newsItem[@"id"]; Article *article = [[Article alloc] initWithGuid:articleGuid]; article.folderId = refreshedFolder.itemId; @@ -611,7 +611,15 @@ -(void)feedRequestDone:(NSMutableURLRequest *)request response:(NSURLResponse *) article.link = refreshedFolder.feedURL; } - article.date = articleDate; + NSString *publishedField = newsItem[@"published"]; + if (publishedField) { + article.publicationDate = [NSDate dateWithTimeIntervalSince1970:[publishedField doubleValue]]; + } + + NSString * updatedField = newsItem[@"updated"]; + 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 627c06aabd..5c97ac6ab4 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,8 @@ -(void)finalizeFolderRefresh:(NSDictionary *)parameters // Parse off items. for (id newsItem in newFeed.items) { - NSDate * articleDate = newsItem.modifiedDate; + NSDate * articleDate = newsItem.publicationDate; + NSDate * modificationDate = newsItem.modificationDate; NSString * articleGuid = newsItem.guid; @@ -795,7 +796,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 +838,8 @@ -(void)finalizeFolderRefresh:(NSDictionary *)parameters articleLink = feedLink; } article.link = articleLink; - article.date = articleDate; + 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/Main window/ArticleController.m b/Vienna/Sources/Main window/ArticleController.m index 1d6bbf48da..c849d5d5cc 100644 --- a/Vienna/Sources/Main window/ArticleController.m +++ b/Vienna/Sources/Main window/ArticleController.m @@ -93,8 +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_PublicationDate: @{ + @"key": [@"articleData." stringByAppendingString:MA_Field_PublicationDate], @"selector": NSStringFromSelector(@selector(compare:)) }, MA_Field_Author: @{ @@ -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"])]; + // 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]; [descriptors removeObjectAtIndex:index]; @@ -1014,19 +1021,16 @@ - (BOOL)filterArticle:(Article *)article usingMode:(NSInteger)filterMode { case VNAFilterUnread: return !article.read; case VNAFilterLastRefresh: { - NSDate *date = article.createdDate; - 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.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 755107cd7e..fdc5abbc5f 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); @@ -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) { @@ -593,7 +593,11 @@ -(void)updateArticleListRowHeight -(void)showSortDirection { NSString * sortColumnIdentifier = self.controller.articleController.sortColumnIdentifier; - + + 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 +994,7 @@ -(void)refreshFolder:(NSInteger)refreshFlag break; case VNARefreshSortAndRedraw: [self.controller.articleController sortArticles]; + [self showSortDirection]; break; } @@ -1287,8 +1292,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) { @@ -1307,8 +1312,10 @@ -(id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColum } NSString * cellString; - if ([identifier isEqualToString:MA_Field_Date]) { - cellString = [NSDateFormatter vna_relativeDateStringFromDate:theArticle.date]; + 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 88bceabde4..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,34 +38,33 @@ 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; 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, + VNAArticleFieldTagLastUpdate, + VNAArticleFieldTagParent, + VNAArticleFieldTagRead, + VNAArticleFieldTagFlagged, + VNAArticleFieldTagText, + VNAArticleFieldTagFolder, + VNAArticleFieldTagLink, /* 410 was previously used */ - ArticleFieldIDHeadlines = 411, - ArticleFieldIDDeleted, - ArticleFieldIDSummary, - /* 414 was previously used */ - ArticleFieldIDEnclosure = 415, - ArticleFieldIDEnclosureDownloaded, - ArticleFieldIDHasEnclosure -}; + VNAArticleFieldTagHeadlines = 411, + VNAArticleFieldTagDeleted, + VNAArticleFieldTagSummary, + VNAArticleFieldTagPublicationDate, + VNAArticleFieldTagEnclosure, + VNAArticleFieldTagEnclosureDownloaded, + VNAArticleFieldTagHasEnclosure +} NS_SWIFT_NAME(Article.FieldTag); typedef NS_ENUM(NSInteger, ArticleStatus) { ArticleStatusEmpty = 0, @@ -85,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 19437473c8..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,8 +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; + 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]) { @@ -243,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]; } @@ -354,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..c350bdcb29 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,21 @@ - (BOOL)initAtomFeed:(NSXMLElement *)atomElement } // Parse item date - if (isArticleElementAtomType && [articleItemTag isEqualToString:@"modified"]) { - 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:@"created"]) { + if (isArticleElementAtomType && ([articleItemTag isEqualToString:@"modified"] || [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:@"updated"]) { + if (isArticleElementAtomType && ([articleItemTag isEqualToString:@"created"] || [articleItemTag isEqualToString:@"published"])) { 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]) { + 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..8a843a78b4 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; } @@ -259,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.modifiedDate = [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; } 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..8ff3534264 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; @@ -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";