diff --git a/Frameworks/Data/Article.swift b/Frameworks/Data/Article.swift index 7999b905f..b98df64a7 100644 --- a/Frameworks/Data/Article.swift +++ b/Frameworks/Data/Article.swift @@ -11,24 +11,24 @@ import Foundation public final class Article: Hashable { weak var account: Account? - let feedID: String - let articleID: String //Calculated: unique per account + public let feedID: String + public let articleID: String //Calculated: unique per account - var uniqueID: String //guid: unique per feed - var title: String? - var contentHTML: String? - var contentText: String? - var url: String? - var externalURL: String? - var summary: String? - var imageURL: String? - var bannerImageURL: String? - var datePublished: Date? - var dateModified: Date? - var authors: [Author]? - var tags: [String]? - var attachments: [Attachment]? - var status: ArticleStatus? + public var uniqueID: String //guid: unique per feed + public var title: String? + public var contentHTML: String? + public var contentText: String? + public var url: String? + public var externalURL: String? + public var summary: String? + public var imageURL: String? + public var bannerImageURL: String? + public var datePublished: Date? + public var dateModified: Date? + public var authors: [Author]? + public var tags: [String]? + public var attachments: [Attachment]? + public var status: ArticleStatus? public let hashValue: Int public var accountInfo: [String: Any]? //If account needs to store more data diff --git a/Frameworks/Data/ArticleStatus.swift b/Frameworks/Data/ArticleStatus.swift index 7471866df..56bb9f50a 100644 --- a/Frameworks/Data/ArticleStatus.swift +++ b/Frameworks/Data/ArticleStatus.swift @@ -15,13 +15,14 @@ public enum ArticleStatusKey: String { case userDeleted = "userDeleted" } -public final class ArticleStatus { +public final class ArticleStatus: Hashable { public var read = false public var starred = false public var userDeleted = false public var dateArrived: Date var accountInfo: AccountInfo? + public let hashValue: Int init(read: Bool, starred: Bool, userDeleted: Bool, dateArrived: Date, accountInfo: AccountInfo?) { @@ -30,6 +31,7 @@ public final class ArticleStatus { self.userDeleted = userDeleted self.dateArrived = dateArrived self.accountInfo = accountInfo + self.hashValue = dateArrived.hashValue } func boolStatusForKey(_ key: String) -> Bool { @@ -69,4 +71,9 @@ public final class ArticleStatus { accountInfo![key] = status } } + + public class func ==(lhs: ArticleStatus, rhs: ArticleStatus) -> Bool { + + return lhs.dateArrived == rhs.dateArrived && lhs.read == rhs.read && lhs.starred == rhs.starred + } } diff --git a/Frameworks/Data/Protocols/AccountDelegate.swift b/Frameworks/Data/Protocols/AccountDelegate.swift index a2ae5f98c..7798f218b 100644 --- a/Frameworks/Data/Protocols/AccountDelegate.swift +++ b/Frameworks/Data/Protocols/AccountDelegate.swift @@ -8,7 +8,7 @@ import Foundation -public protocol AccountDelegate { +public protocol AccountDelegate: class { func canAddItem(_ item: AnyObject, toContainer: Container) -> Bool diff --git a/Frameworks/Database/ArticleCache.swift b/Frameworks/Database/ArticleCache.swift deleted file mode 100644 index 9d186ba5d..000000000 --- a/Frameworks/Database/ArticleCache.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// LocalArticleCache.swift -// Evergreen -// -// Created by Brent Simmons on 5/9/16. -// Copyright © 2016 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -final class LocalArticleCache { - - private var cachedArticles: NSMapTable = NSMapTable.weakToWeakObjects() -// private var cachedArticles = [String: LocalArticle]() -// fileprivate var articlesByFeedID = [String: Set]() - private let statusesManager: LocalStatusesManager - - init(statusesManager: LocalStatusesManager) { - - self.statusesManager = statusesManager - } - - func uniquedArticles(_ fetchedArticles: Set) -> Set { - - var articles = Set() - - for oneArticle in fetchedArticles { - - assert(oneArticle.status != nil) - - if let existingArticle = cachedArticle(oneArticle.articleID) { - articles.insert(existingArticle) - } - else { - cacheArticle(oneArticle) - articles.insert(oneArticle) - } - } - - statusesManager.attachCachedUniqueStatuses(articles) - - return articles - } - - func cachedArticle(_ articleID: String) -> LocalArticle? { - - return cachedArticles.object(forKey: articleID as NSString) -// return cachedArticles[articleID] - } - - func cacheArticle(_ article: LocalArticle) { - - cachedArticles.setObject(article, forKey: article.articleID as NSString) -// cachedArticles[article.articleID] = article -// addToCachedArticlesForFeedID(Set([article])) - } - - func cacheArticles(_ articles: Set) { - - articles.forEach { cacheArticle($0) } -// addToCachedArticlesForFeedID(articles) - } - -// func cachedArticlesForFeedID(_ feedID: String) -> Set? { -// -// return articlesByFeedID[feedID] -// } -} - -//private extension LocalArticleCache { -// -// func addToCachedArticlesForFeedID(_ feedID: String, _ articles: Set) { -// -// if let cachedArticles = cachedArticlesForFeedID(feedID) { -// replaceCachedArticlesForFeedID(feedID, cachedArticles.union(articles)) -// } -// else { -// replaceCachedArticlesForFeedID(feedID, articles) -// } -// } -// -// func addToCachedArticlesForFeedID(_ articles: Set) { -// -// for oneArticle in articles { -// addToCachedArticlesForFeedID(oneArticle.feedID, Set([oneArticle])) -// } -// } -// -// func replaceCachedArticlesForFeedID(_ feedID: String, _ articles: Set) { -// -// articlesByFeedID[feedID] = articles -// } -// -//} diff --git a/Frameworks/Database/ArticlesManager.swift b/Frameworks/Database/ArticlesManager.swift new file mode 100644 index 000000000..d6636f5f6 --- /dev/null +++ b/Frameworks/Database/ArticlesManager.swift @@ -0,0 +1,52 @@ +// +// ArticlesManager.swift +// Evergreen +// +// Created by Brent Simmons on 5/9/16. +// Copyright © 2016 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import Data + +final class ArticlesManager { + + private let cachedArticles: NSMapTable = NSMapTable.weakToWeakObjects() + + func uniquedArticles(_ fetchedArticles: Set
, statusesManager: StatusesManager) -> Set
{ + + var articles = Set
() + + for oneArticle in fetchedArticles { + + assert(oneArticle.status != nil) + + if let existingArticle = cachedArticle(oneArticle.articleID) { + articles.insert(existingArticle) + } + else { + cacheArticle(oneArticle) + articles.insert(oneArticle) + } + } + + statusesManager.attachCachedStatuses(articles) + + return articles + } + + func cachedArticle(_ articleID: String) -> Article? { + + return cachedArticles.object(forKey: articleID as NSString) + } + + func cacheArticle(_ article: Article) { + + cachedArticles.setObject(article, forKey: article.articleID as NSString) + } + + func cacheArticles(_ articles: Set
) { + + articles.forEach { cacheArticle($0) } + } +} diff --git a/Frameworks/Database/Database.swift b/Frameworks/Database/Database.swift index 4ff703f11..9969a6ed6 100644 --- a/Frameworks/Database/Database.swift +++ b/Frameworks/Database/Database.swift @@ -8,8 +8,8 @@ import Foundation import RSCore -//import RSParser import RSDatabase +import RSParser import Data let sqlLogging = false @@ -20,7 +20,7 @@ func logSQL(_ sql: String) { } } -typealias LocalArticleResultBlock = (Set) -> Void +typealias ArticleResultBlock = (Set
) -> Void private let articlesTableName = "articles" @@ -28,43 +28,34 @@ final class LocalDatabase { fileprivate let queue: RSDatabaseQueue private let databaseFile: String - fileprivate let statusesManager: LocalStatusesManager - fileprivate let articleCache: LocalArticleCache + fileprivate let statusesManager: StatusesManager + fileprivate let articleCache = ArticlesManager() fileprivate var articleArrivalCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 3 * 31)! fileprivate let minimumNumberOfArticles = 10 + fileprivate weak var delegate: AccountDelegate? - var account: LocalAccount! - - init(databaseFile: String) { + init(databaseFile: String, delegate: AccountDelegate) { + self.delegate = delegate self.databaseFile = databaseFile self.queue = RSDatabaseQueue(filepath: databaseFile, excludeFromBackup: false) - self.statusesManager = LocalStatusesManager(queue: self.queue) - self.articleCache = LocalArticleCache(statusesManager: self.statusesManager) + self.statusesManager = StatusesManager(queue: self.queue) - let createStatementsPath = Bundle(for: type(of: self)).path(forResource: "LocalCreateStatements", ofType: "sql")! + let createStatementsPath = Bundle(for: type(of: self)).path(forResource: "CreateStatements", ofType: "sql")! let createStatements = try! NSString(contentsOfFile: createStatementsPath, encoding: String.Encoding.utf8.rawValue) queue.createTables(usingStatements: createStatements as String) queue.vacuumIfNeeded() } - // MARK: API - - func startup() { - - assert(account != nil) -// deleteOldArticles(articleIDsInFeeds) - } - // MARK: Fetching Articles - func fetchArticlesForFeed(_ feed: LocalFeed) -> Set { + func fetchArticlesForFeed(_ feed: Feed) -> Set
{ // if let articles = articleCache.cachedArticlesForFeedID(feed.feedID) { // return articles // } - var fetchedArticles = Set() + var fetchedArticles = Set
() let feedID = feed.feedID queue.fetchSync { (database: FMDatabase!) -> Void in @@ -76,7 +67,7 @@ final class LocalDatabase { return filteredArticles(articles, feedCounts: [feed.feedID: fetchedArticles.count]) } - func fetchArticlesForFeedAsync(_ feed: LocalFeed, _ resultBlock: @escaping LocalArticleResultBlock) { + func fetchArticlesForFeedAsync(_ feed: Feed, _ resultBlock: @escaping ArticleResultBlock) { // if let articles = articleCache.cachedArticlesForFeedID(feed.feedID) { // resultBlock(articles) @@ -138,18 +129,18 @@ final class LocalDatabase { } - func fetchUnreadArticlesForFolder(_ folder: LocalFolder) -> Set { + func fetchUnreadArticlesForFolder(_ folder: Folder) -> Set
{ return fetchUnreadArticlesForFeedIDs(Array(folder.flattenedFeedIDs)) } - func fetchUnreadArticlesForFeedIDs(_ feedIDs: [String]) -> Set { + func fetchUnreadArticlesForFeedIDs(_ feedIDs: [String]) -> Set
{ if feedIDs.isEmpty { return Set() } - var fetchedArticles = Set() + var fetchedArticles = Set
() var counts = [String: Int]() queue.fetchSync { (database: FMDatabase!) -> Void in @@ -191,14 +182,14 @@ final class LocalDatabase { // MARK: Updating Articles - func updateFeedWithParsedFeed(_ feed: LocalFeed, parsedFeed: ParsedFeed, completionHandler: @escaping RSVoidCompletionBlock) { + func updateFeedWithParsedFeed(_ feed: Feed, parsedFeed: ParsedFeed, completionHandler: @escaping RSVoidCompletionBlock) { if parsedFeed.items.isEmpty { completionHandler() return } - let parsedArticlesDictionary = self.articlesDictionary(parsedFeed.articles as NSSet) as! [String: ParsedItem] + let parsedArticlesDictionary = self.articlesDictionary(parsedFeed.items as NSSet) as! [String: ParsedItem] fetchArticlesForFeedAsync(feed) { (articles) -> Void in @@ -211,7 +202,7 @@ final class LocalDatabase { func markArticles(_ articles: NSSet, statusKey: ArticleStatusKey, flag: Bool) { - statusesManager.markArticles(articles as! Set, statusKey: statusKey, flag: flag) + statusesManager.markArticles(articles as! Set
, statusKey: statusKey, flag: flag) } } @@ -221,7 +212,7 @@ private extension LocalDatabase { // MARK: Saving Articles - func saveUpdatedAndNewArticles(_ articleChanges: Set, newArticles: Set) { + func saveUpdatedAndNewArticles(_ articleChanges: Set, newArticles: Set
) { if articleChanges.isEmpty && newArticles.isEmpty { return @@ -259,7 +250,7 @@ private extension LocalDatabase { // MARK: Updating Articles - func updateArticles(_ articles: [String: LocalArticle], parsedArticles: [String: ParsedItem], feed: LocalFeed, completionHandler: @escaping RSVoidCompletionBlock) { + func updateArticles(_ articles: [String: Article], parsedArticles: [String: ParsedItem], feed: Feed, completionHandler: @escaping RSVoidCompletionBlock) { statusesManager.ensureStatusesForParsedArticles(Set(parsedArticles.values)) { @@ -282,7 +273,7 @@ private extension LocalDatabase { return d } - func updateExistingArticles(_ articles: [String: LocalArticle], _ parsedArticles: [String: ParsedItem]) -> Set { + func updateExistingArticles(_ articles: [String: Article], _ parsedArticles: [String: ParsedItem]) -> Set { var articleChanges = Set() @@ -299,14 +290,14 @@ private extension LocalDatabase { // MARK: Creating Articles - func createNewArticlesWithParsedArticles(_ parsedArticles: Set, feedID: String) -> Set { + func createNewArticlesWithParsedArticles(_ parsedArticles: Set, feedID: String) -> Set
{ return Set(parsedArticles.map { LocalArticle(account: account, feedID: feedID, parsedArticle: $0) }) } - func articlesWithParsedArticles(_ parsedArticles: Set, feedID: String) -> Set { + func articlesWithParsedArticles(_ parsedArticles: Set, feedID: String) -> Set
{ - var localArticles = Set() + var localArticles = Set
() for oneParsedArticle in parsedArticles { let oneLocalArticle = LocalArticle(account: self.account, feedID: feedID, parsedArticle: oneParsedArticle) @@ -316,7 +307,7 @@ private extension LocalDatabase { return localArticles } - func createNewArticles(_ existingArticles: [String: LocalArticle], parsedArticles: [String: ParsedItem], feedID: String) -> Set { + func createNewArticles(_ existingArticles: [String: Article], parsedArticles: [String: ParsedItem], feedID: String) -> Set
{ let newParsedArticles = parsedArticlesMinusExistingArticles(parsedArticles, existingArticles: existingArticles) let newArticles = createNewArticlesWithParsedArticles(newParsedArticles, feedID: feedID) @@ -326,7 +317,7 @@ private extension LocalDatabase { return newArticles } - func parsedArticlesMinusExistingArticles(_ parsedArticles: [String: ParsedItem], existingArticles: [String: LocalArticle]) -> Set { + func parsedArticlesMinusExistingArticles(_ parsedArticles: [String: ParsedItem], existingArticles: [String: Article]) -> Set { var result = Set() @@ -343,7 +334,7 @@ private extension LocalDatabase { // MARK: Fetching Articles - func fetchArticlesWithWhereClause(_ database: FMDatabase, whereClause: String, parameters: [AnyObject]?) -> Set { + func fetchArticlesWithWhereClause(_ database: FMDatabase, whereClause: String, parameters: [AnyObject]?) -> Set
{ let sql = "select * from articles natural join statuses where \(whereClause);" logSQL(sql) @@ -355,14 +346,14 @@ private extension LocalDatabase { return Set() } - func articlesWithResultSet(_ resultSet: FMResultSet) -> Set { + func articlesWithResultSet(_ resultSet: FMResultSet) -> Set
{ - var fetchedArticles = Set() + var fetchedArticles = Set
() while (resultSet.next()) { - if let oneArticle = LocalArticle(account: self.account, row: resultSet) { - oneArticle.status = LocalArticleStatus(row: resultSet) + if let oneArticle = Article(account: self.account, row: resultSet) { + oneArticle.status = ArticleStatus(row: resultSet) fetchedArticles.insert(oneArticle) } } @@ -370,7 +361,7 @@ private extension LocalDatabase { return fetchedArticles } - func fetchArticlesForFeedID(_ feedID: String, database: FMDatabase) -> Set { + func fetchArticlesForFeedID(_ feedID: String, database: FMDatabase) -> Set
{ return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [feedID as AnyObject]) } @@ -430,7 +421,7 @@ private extension LocalDatabase { // MARK: Filtering out old articles - func articleIsOlderThanCutoffDate(_ article: LocalArticle) -> Bool { + func articleIsOlderThanCutoffDate(_ article: Article) -> Bool { if let dateArrived = article.status?.dateArrived { return dateArrived < articleArrivalCutoffDate @@ -438,12 +429,12 @@ private extension LocalDatabase { return false } - func articleShouldBeSavedForever(_ article: LocalArticle) -> Bool { + func articleShouldBeSavedForever(_ article: Article) -> Bool { return article.status.starred } - func articleShouldAppearToUser(_ article: LocalArticle, _ numberOfArticlesInFeed: Int) -> Bool { + func articleShouldAppearToUser(_ article: Article, _ numberOfArticlesInFeed: Int) -> Bool { if numberOfArticlesInFeed <= minimumNumberOfArticles { return true @@ -453,9 +444,9 @@ private extension LocalDatabase { private static let minimumNumberOfArticlesInFeed = 10 - func filteredArticles(_ articles: Set, feedCounts: [String: Int]) -> Set { + func filteredArticles(_ articles: Set
, feedCounts: [String: Int]) -> Set
{ - var articlesSet = Set() + var articlesSet = Set
() for oneArticle in articles { if let feedCount = feedCounts[oneArticle.feedID], articleShouldAppearToUser(oneArticle, feedCount) { @@ -469,12 +460,12 @@ private extension LocalDatabase { typealias FeedCountCallback = (Int) -> Void - func feedIDsFromArticles(_ articles: Set) -> Set { + func feedIDsFromArticles(_ articles: Set
) -> Set { return Set(articles.map { $0.feedID }) } - func deletePossibleOldArticles(_ articles: Set) { + func deletePossibleOldArticles(_ articles: Set
) { let feedIDs = feedIDsFromArticles(articles) if feedIDs.isEmpty { @@ -506,7 +497,7 @@ private extension LocalDatabase { } } - func deleteOldArticlesInFeed(_ feed: LocalFeed) { + func deleteOldArticlesInFeed(_ feed: Feed) { numberOfArticlesInFeedID(feed.feedID) { (numberOfArticlesInFeed) in diff --git a/Frameworks/Database/Database.xcodeproj/project.pbxproj b/Frameworks/Database/Database.xcodeproj/project.pbxproj index 8e8cdfa1c..5c7b12dc9 100644 --- a/Frameworks/Database/Database.xcodeproj/project.pbxproj +++ b/Frameworks/Database/Database.xcodeproj/project.pbxproj @@ -9,8 +9,9 @@ /* Begin PBXBuildFile section */ 844BEE411F0AB3AB004AB7CD /* Database.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844BEE371F0AB3AA004AB7CD /* Database.framework */; }; 844BEE461F0AB3AB004AB7CD /* DatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */; }; + 846146271F0ABC7B00870CB3 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 846146241F0ABC7400870CB3 /* RSParser.framework */; }; 84E156EA1F0AB80500F8CC05 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156E91F0AB80500F8CC05 /* Database.swift */; }; - 84E156EC1F0AB80E00F8CC05 /* ArticleCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156EB1F0AB80E00F8CC05 /* ArticleCache.swift */; }; + 84E156EC1F0AB80E00F8CC05 /* ArticlesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156EB1F0AB80E00F8CC05 /* ArticlesManager.swift */; }; 84E156EE1F0AB81400F8CC05 /* StatusesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156ED1F0AB81400F8CC05 /* StatusesManager.swift */; }; 84E156F01F0AB81F00F8CC05 /* CreateStatements.sql in Resources */ = {isa = PBXBuildFile; fileRef = 84E156EF1F0AB81F00F8CC05 /* CreateStatements.sql */; }; 84E156FD1F0AB86100F8CC05 /* Data.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84E156F81F0AB83600F8CC05 /* Data.framework */; }; @@ -26,6 +27,20 @@ remoteGlobalIDString = 844BEE361F0AB3AA004AB7CD; remoteInfo = Database; }; + 846146231F0ABC7400870CB3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 84FF5F841EFA285800C15A01; + remoteInfo = RSParser; + }; + 846146251F0ABC7400870CB3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 84FF5F8D1EFA285800C15A01; + remoteInfo = RSParserTests; + }; 84E156F71F0AB83600F8CC05 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 84E156F11F0AB83600F8CC05 /* Data.xcodeproj */; @@ -68,9 +83,10 @@ 844BEE401F0AB3AB004AB7CD /* DatabaseTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DatabaseTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseTests.swift; sourceTree = ""; }; 844BEE471F0AB3AB004AB7CD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = ""; }; 84E156E81F0AB75600F8CC05 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 84E156E91F0AB80500F8CC05 /* Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = ""; }; - 84E156EB1F0AB80E00F8CC05 /* ArticleCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleCache.swift; sourceTree = ""; }; + 84E156EB1F0AB80E00F8CC05 /* ArticlesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticlesManager.swift; sourceTree = ""; }; 84E156ED1F0AB81400F8CC05 /* StatusesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusesManager.swift; sourceTree = ""; }; 84E156EF1F0AB81F00F8CC05 /* CreateStatements.sql */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CreateStatements.sql; sourceTree = ""; }; 84E156F11F0AB83600F8CC05 /* Data.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Data.xcodeproj; path = ../Data/Data.xcodeproj; sourceTree = ""; }; @@ -83,6 +99,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 846146271F0ABC7B00870CB3 /* RSParser.framework in Frameworks */, 84E1570C1F0AB8A500F8CC05 /* RSDatabase.framework in Frameworks */, 84E156FF1F0AB86700F8CC05 /* RSCore.framework in Frameworks */, 84E156FD1F0AB86100F8CC05 /* Data.framework in Frameworks */, @@ -104,7 +121,7 @@ isa = PBXGroup; children = ( 84E156E91F0AB80500F8CC05 /* Database.swift */, - 84E156EB1F0AB80E00F8CC05 /* ArticleCache.swift */, + 84E156EB1F0AB80E00F8CC05 /* ArticlesManager.swift */, 84E156ED1F0AB81400F8CC05 /* StatusesManager.swift */, 84E156EF1F0AB81F00F8CC05 /* CreateStatements.sql */, 84E156E81F0AB75600F8CC05 /* Info.plist */, @@ -132,6 +149,15 @@ path = DatabaseTests; sourceTree = ""; }; + 8461461F1F0ABC7300870CB3 /* Products */ = { + isa = PBXGroup; + children = ( + 846146241F0ABC7400870CB3 /* RSParser.framework */, + 846146261F0ABC7400870CB3 /* RSParserTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; 84E156F21F0AB83600F8CC05 /* Products */ = { isa = PBXGroup; children = ( @@ -145,6 +171,7 @@ isa = PBXGroup; children = ( 84E156FE1F0AB86700F8CC05 /* RSCore.framework */, + 8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */, 84E157001F0AB89B00F8CC05 /* RSDatabase.xcodeproj */, 84E156F11F0AB83600F8CC05 /* Data.xcodeproj */, ); @@ -252,6 +279,10 @@ ProductGroup = 84E157011F0AB89B00F8CC05 /* Products */; ProjectRef = 84E157001F0AB89B00F8CC05 /* RSDatabase.xcodeproj */; }, + { + ProductGroup = 8461461F1F0ABC7300870CB3 /* Products */; + ProjectRef = 8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */; + }, ); projectRoot = ""; targets = ( @@ -262,6 +293,20 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ + 846146241F0ABC7400870CB3 /* RSParser.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = RSParser.framework; + remoteRef = 846146231F0ABC7400870CB3 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 846146261F0ABC7400870CB3 /* RSParserTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = RSParserTests.xctest; + remoteRef = 846146251F0ABC7400870CB3 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 84E156F81F0AB83600F8CC05 /* Data.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; @@ -323,7 +368,7 @@ buildActionMask = 2147483647; files = ( 84E156EA1F0AB80500F8CC05 /* Database.swift in Sources */, - 84E156EC1F0AB80E00F8CC05 /* ArticleCache.swift in Sources */, + 84E156EC1F0AB80E00F8CC05 /* ArticlesManager.swift in Sources */, 84E156EE1F0AB81400F8CC05 /* StatusesManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Frameworks/Database/StatusesManager.swift b/Frameworks/Database/StatusesManager.swift index 9257fac79..9b863d1c1 100644 --- a/Frameworks/Database/StatusesManager.swift +++ b/Frameworks/Database/StatusesManager.swift @@ -1,5 +1,5 @@ - // -// LocalStatusesManager.swift +// +// StatusesManager.swift // Evergreen // // Created by Brent Simmons on 5/8/16. @@ -9,12 +9,12 @@ import Foundation import RSCore import RSDatabase -//import RSParser +import RSParser import Data -final class LocalStatusesManager { +final class StatusesManager { - var cachedStatuses = [String: LocalArticleStatus]() + var cachedStatuses = [String: ArticleStatus]() let queue: RSDatabaseQueue init(queue: RSDatabaseQueue) { @@ -22,30 +22,29 @@ final class LocalStatusesManager { self.queue = queue } - func markArticles(_ articles: Set, statusKey: ArticleStatusKey, flag: Bool) { + func markArticles(_ articles: Set
, statusKey: ArticleStatusKey, flag: Bool) { assertNoMissingStatuses(articles) - let statusArray = articles.map { $0.status! as! LocalArticleStatus } - let statuses = Set(statusArray) + let statuses = Set(articles.flatMap { $0.status }) markArticleStatuses(statuses, statusKey: statusKey, flag: flag) } - func attachCachedUniqueStatuses(_ articles: Set) { + func attachCachedStatuses(_ articles: Set
) { - articles.forEach { (oneLocalArticle) in + articles.forEach { (oneArticle) in - if let cachedStatus = cachedStatusForArticleID(oneLocalArticle.articleID) { - oneLocalArticle.status = cachedStatus + if let cachedStatus = cachedStatusForArticleID(oneArticle.articleID) { + oneArticle.status = cachedStatus } - else if let oneLocalArticleStatus = oneLocalArticle.status as? LocalArticleStatus { - cacheStatus(oneLocalArticleStatus) + else if let oneArticleStatus = oneArticle.status as? ArticleStatus { + cacheStatus(oneArticleStatus) } } } func ensureStatusesForParsedArticles(_ parsedArticles: [ParsedItem], _ callback: @escaping RSVoidCompletionBlock) { - var articleIDs = Set(parsedArticles.map { $0.databaseID }) + var articleIDs = Set(parsedArticles.map { $0.articleID }) articleIDs = articleIDsMissingStatuses(articleIDs) if articleIDs.isEmpty { callback() @@ -67,7 +66,7 @@ final class LocalStatusesManager { } } - func assertNoMissingStatuses(_ articles: Set) { + func assertNoMissingStatuses(_ articles: Set
) { for oneArticle in articles { if oneArticle.status == nil { @@ -82,11 +81,11 @@ final class LocalStatusesManager { private let statusesTableName = "statuses" -private extension LocalStatusesManager { +private extension StatusesManager { // MARK: Marking - func markArticleStatuses(_ statuses: Set, statusKey: ArticleStatusKey, flag: Bool) { + func markArticleStatuses(_ statuses: Set, statusKey: ArticleStatusKey, flag: Bool) { // Ignore the statuses where status.[statusKey] == flag. Update the remainder and save in database. @@ -107,25 +106,25 @@ private extension LocalStatusesManager { // MARK: Fetching - func fetchStatusesForArticleIDs(_ articleIDs: Set, database: FMDatabase) -> Set { + func fetchStatusesForArticleIDs(_ articleIDs: Set, database: FMDatabase) -> Set { guard !articleIDs.isEmpty else { - return Set() + return Set() } guard let resultSet = database.rs_selectRowsWhereKey(articleIDKey, inValues: Array(articleIDs), tableName: statusesTableName) else { - return Set() + return Set() } - return localArticleStatusesWithResultSet(resultSet) + return articleStatusesWithResultSet(resultSet) } - func localArticleStatusesWithResultSet(_ resultSet: FMResultSet) -> Set { + func articleStatusesWithResultSet(_ resultSet: FMResultSet) -> Set { - var statuses = Set() + var statuses = Set() while(resultSet.next()) { - if let oneArticleStatus = LocalArticleStatus(row: resultSet) { + if let oneArticleStatus = ArticleStatus(row: resultSet) { statuses.insert(oneArticleStatus) } } @@ -135,7 +134,7 @@ private extension LocalStatusesManager { // MARK: Saving - func saveStatuses(_ statuses: Set) { + func saveStatuses(_ statuses: Set) { let statusArray = statuses.map { (oneStatus) -> NSDictionary in return oneStatus.databaseDictionary @@ -164,7 +163,7 @@ private extension LocalStatusesManager { let now = Date() let statuses = articleIDs.map { (oneArticleID) -> LocalArticleStatus in - return LocalArticleStatus(articleID: oneArticleID, read: false, starred: false, userDeleted: false, dateArrived: now) + return ArticleStatus(articleID: oneArticleID, read: false, starred: false, userDeleted: false, dateArrived: now) } cacheStatuses(Set(statuses)) @@ -181,17 +180,17 @@ private extension LocalStatusesManager { // MARK: Cache - func cachedStatusForArticleID(_ articleID: String) -> LocalArticleStatus? { + func cachedStatusForArticleID(_ articleID: String) -> ArticleStatus? { return cachedStatuses[articleID] } - func cacheStatus(_ status: LocalArticleStatus) { + func cacheStatus(_ status: ArticleStatus) { cacheStatuses(Set([status])) } - func cacheStatuses(_ statuses: Set) { + func cacheStatuses(_ statuses: Set) { statuses.forEach { (oneStatus) in if let _ = cachedStatuses[oneStatus.articleID] { diff --git a/Frameworks/RSParser/Feeds/ParsedItem.swift b/Frameworks/RSParser/Feeds/ParsedItem.swift index 7507f753a..8af508ac9 100644 --- a/Frameworks/RSParser/Feeds/ParsedItem.swift +++ b/Frameworks/RSParser/Feeds/ParsedItem.swift @@ -8,7 +8,7 @@ import Foundation -public struct ParsedItem { +public struct ParsedItem: Hashable { public let uniqueID: String public let feedURL: String @@ -25,7 +25,8 @@ public struct ParsedItem { public let authors: [ParsedAuthor]? public let tags: [String]? public let attachments: [ParsedAttachment]? - + public let hashValue: Int + init(uniqueID: String, feedURL: String, url: String?, externalURL: String?, title: String?, contentHTML: String?, contentText: String?, summary: String?, imageURL: String?, bannerImageURL: String?, datePublished: Date?, dateModified: Date?, authors: [ParsedAuthor]?, tags: [String]?, attachments: [ParsedAttachment]?) { self.uniqueID = uniqueID @@ -43,6 +44,12 @@ public struct ParsedItem { self.authors = authors self.tags = tags self.attachments = attachments + self.hashValue = uniqueID.hashValue + } + + public static func ==(lhs: ParsedItem, rhs: ParsedItem) -> Bool { + + return lhs.hashValue == rhs.hashValue && lhs.uniqueID == rhs.uniqueID && lhs.feedURL == rhs.feedURL } }