diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index ab6b5f8d4..b28ff6c53 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -573,12 +573,40 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } func update(_ feed: Feed, with parsedFeed: ParsedFeed, _ completion: @escaping (() -> Void)) { + // Used only by an On My Mac account. feed.takeSettings(from: parsedFeed) - update(feed, parsedItems: parsedFeed.items, completion) + let feedIDsAndItems = [feed.feedID: parsedFeed.items] + update(feedIDsAndItems: feedIDsAndItems, defaultRead: false, completion: completion) } - + + func update(feedIDsAndItems: [String: Set], defaultRead: Bool, completion: @escaping (() -> Void)) { + guard !feedIDsAndItems.isEmpty else { + completion() + return + } + database.update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead) { (newArticles, updatedArticles) in + var userInfo = [String: Any]() + let feeds = Set(feedIDsAndItems.compactMap { (key, _) -> Feed? in + self.existingFeed(withFeedID: key) + }) + if let newArticles = newArticles, !newArticles.isEmpty { + self.updateUnreadCounts(for: feeds) + userInfo[UserInfoKey.newArticles] = newArticles + } + if let updatedArticles = updatedArticles, !updatedArticles.isEmpty { + userInfo[UserInfoKey.updatedArticles] = updatedArticles + } + userInfo[UserInfoKey.feeds] = feeds + + completion() + + NotificationCenter.default.post(name: .AccountDidDownloadArticles, object: self, userInfo: userInfo) + } + } + func update(_ feed: Feed, parsedItems: Set, defaultRead: Bool = false, _ completion: @escaping (() -> Void)) { - database.update(feedID: feed.feedID, parsedItems: parsedItems, defaultRead: defaultRead) { (newArticles, updatedArticles) in + let feedIDsAndItems = [feed.feedID: parsedItems] + database.update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead) { (newArticles, updatedArticles) in var userInfo = [String: Any]() if let newArticles = newArticles, !newArticles.isEmpty { self.updateUnreadCounts(for: Set([feed])) diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index 55b939cfe..8a1f60535 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -1054,7 +1054,6 @@ private extension FeedbinAccountDelegate { } func refreshArticles(_ account: Account, page: String?, completion: @escaping (() -> Void)) { - guard let page = page else { completion() return @@ -1074,42 +1073,16 @@ private extension FeedbinAccountDelegate { os_log(.error, log: self.log, "Refresh articles for additional pages failed: %@.", error.localizedDescription) completion() } - } - } func processEntries(account: Account, entries: [FeedbinEntry]?, completion: @escaping (() -> Void)) { - let parsedItems = mapEntriesToParsedItems(entries: entries) - let parsedMap = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ) - - let group = DispatchGroup() - - for (feedID, mapItems) in parsedMap { - - group.enter() - - if let feed = account.existingFeed(withFeedID: feedID) { - DispatchQueue.main.async { - account.update(feed, parsedItems: Set(mapItems), defaultRead: true) { - group.leave() - } - } - } else { - group.leave() - } - - } - - group.notify(queue: DispatchQueue.main) { - completion() - } - + let feedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) } + account.update(feedIDsAndItems: feedIDsAndItems, defaultRead: true, completion: completion) } func mapEntriesToParsedItems(entries: [FeedbinEntry]?) -> Set { - guard let entries = entries else { return Set() } diff --git a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift index b5c4343a1..5aa003d30 100644 --- a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift +++ b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift @@ -18,7 +18,7 @@ import Articles public typealias UnreadCountDictionary = [String: Int] // feedID: unreadCount public typealias UnreadCountCompletionBlock = (UnreadCountDictionary) -> Void -public typealias UpdateArticlesWithFeedCompletionBlock = (Set
?, Set
?) -> Void //newArticles, updatedArticles +public typealias UpdateArticlesCompletionBlock = (Set
?, Set
?) -> Void //newArticles, updatedArticles public final class ArticlesDatabase { @@ -118,10 +118,11 @@ public final class ArticlesDatabase { // MARK: - Saving and Updating Articles - public func update(feedID: String, parsedItems: Set, defaultRead: Bool, completion: @escaping UpdateArticlesWithFeedCompletionBlock) { - return articlesTable.update(feedID, parsedItems, defaultRead, completion) + /// Update articles and save new ones. The key for feedIDsAndItems is feedID. + public func update(feedIDsAndItems: [String: Set], defaultRead: Bool, completion: @escaping UpdateArticlesCompletionBlock) { + articlesTable.update(feedIDsAndItems, defaultRead, completion) } - + public func ensureStatuses(_ articleIDs: Set, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool) { articlesTable.ensureStatuses(articleIDs, defaultRead, statusKey, flag) } diff --git a/Frameworks/ArticlesDatabase/ArticlesTable.swift b/Frameworks/ArticlesDatabase/ArticlesTable.swift index 7d8808339..d9bf36e7c 100644 --- a/Frameworks/ArticlesDatabase/ArticlesTable.swift +++ b/Frameworks/ArticlesDatabase/ArticlesTable.swift @@ -208,9 +208,9 @@ final class ArticlesTable: DatabaseTable { } // MARK: - Updating - - func update(_ feedID: String, _ parsedItems: Set, _ read: Bool, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) { - if parsedItems.isEmpty { + + func update(_ feedIDsAndItems: [String: Set], _ read: Bool, _ completion: @escaping UpdateArticlesCompletionBlock) { + if feedIDsAndItems.isEmpty { completion(nil, nil) return } @@ -224,30 +224,34 @@ final class ArticlesTable: DatabaseTable { // 7. Call back with new and updated Articles. // 8. Update search index. - let articleIDs = Set(parsedItems.map { $0.articleID }) - + var articleIDs = Set() + for (_, parsedItems) in feedIDsAndItems { + articleIDs.formUnion(parsedItems.articleIDs()) + } + self.queue.update { (database) in let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1 assert(statusesDictionary.count == articleIDs.count) - - let allIncomingArticles = Article.articlesWithParsedItems(parsedItems, self.accountID, feedID, statusesDictionary) //2 + + let allIncomingArticles = Article.articlesWithFeedIDsAndItems(feedIDsAndItems, self.accountID, statusesDictionary) //2 if allIncomingArticles.isEmpty { self.callUpdateArticlesCompletionBlock(nil, nil, completion) return } - + let incomingArticles = self.filterIncomingArticles(allIncomingArticles) //3 if incomingArticles.isEmpty { self.callUpdateArticlesCompletionBlock(nil, nil, completion) return } - let fetchedArticles = self.fetchArticlesForFeedID(feedID, withLimits: false, database) //4 + let incomingArticleIDs = incomingArticles.articleIDs() + let fetchedArticles = self.fetchArticles(articleIDs: incomingArticleIDs, database) //4 let fetchedArticlesDictionary = fetchedArticles.dictionary() - + let newArticles = self.findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5 let updatedArticles = self.findAndSaveUpdatedArticles(incomingArticles, fetchedArticlesDictionary, database) //6 - + self.callUpdateArticlesCompletionBlock(newArticles, updatedArticles, completion) //7 // 8. Update search index. @@ -258,16 +262,75 @@ final class ArticlesTable: DatabaseTable { if let updatedArticles = updatedArticles { articlesToIndex.formUnion(updatedArticles) } - let articleIDs = articlesToIndex.articleIDs() - if articleIDs.isEmpty { + let articleIDsToIndex = articlesToIndex.articleIDs() + if articleIDsToIndex.isEmpty { return } DispatchQueue.main.async { - self.searchTable.ensureIndexedArticles(for: articleIDs) + self.searchTable.ensureIndexedArticles(for: articleIDsToIndex) } } } +// func update(_ feedID: String, _ parsedItems: Set, _ read: Bool, _ completion: @escaping UpdateArticlesCompletionBlock) { +// if parsedItems.isEmpty { +// completion(nil, nil) +// return +// } +// +// // 1. Ensure statuses for all the incoming articles. +// // 2. Create incoming articles with parsedItems. +// // 3. Ignore incoming articles that are userDeleted || (!starred and really old) +// // 4. Fetch all articles for the feed. +// // 5. Create array of Articles not in database and save them. +// // 6. Create array of updated Articles and save what’s changed. +// // 7. Call back with new and updated Articles. +// // 8. Update search index. +// +// let articleIDs = Set(parsedItems.map { $0.articleID }) +// +// self.queue.update { (database) in +// let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1 +// assert(statusesDictionary.count == articleIDs.count) +// +// let allIncomingArticles = Article.articlesWithParsedItems(parsedItems, self.accountID, feedID, statusesDictionary) //2 +// if allIncomingArticles.isEmpty { +// self.callUpdateArticlesCompletionBlock(nil, nil, completion) +// return +// } +// +// let incomingArticles = self.filterIncomingArticles(allIncomingArticles) //3 +// if incomingArticles.isEmpty { +// self.callUpdateArticlesCompletionBlock(nil, nil, completion) +// return +// } +// +// let fetchedArticles = self.fetchArticlesForFeedID(feedID, withLimits: false, database) //4 +// let fetchedArticlesDictionary = fetchedArticles.dictionary() +// +// let newArticles = self.findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5 +// let updatedArticles = self.findAndSaveUpdatedArticles(incomingArticles, fetchedArticlesDictionary, database) //6 +// +// self.callUpdateArticlesCompletionBlock(newArticles, updatedArticles, completion) //7 +// +// // 8. Update search index. +// var articlesToIndex = Set
() +// if let newArticles = newArticles { +// articlesToIndex.formUnion(newArticles) +// } +// if let updatedArticles = updatedArticles { +// articlesToIndex.formUnion(updatedArticles) +// } +// let articleIDs = articlesToIndex.articleIDs() +// if articleIDs.isEmpty { +// return +// } +// DispatchQueue.main.async { +// self.searchTable.ensureIndexedArticles(for: articleIDs) +// } +// } +// } + func ensureStatuses(_ articleIDs: Set, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool) { self.queue.update { (database) in let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, defaultRead, database) @@ -596,7 +659,7 @@ private extension ArticlesTable { // MARK: - Saving Parsed Items - func callUpdateArticlesCompletionBlock(_ newArticles: Set
?, _ updatedArticles: Set
?, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) { + func callUpdateArticlesCompletionBlock(_ newArticles: Set
?, _ updatedArticles: Set
?, _ completion: @escaping UpdateArticlesCompletionBlock) { DispatchQueue.main.async { completion(newArticles, updatedArticles) } @@ -728,3 +791,8 @@ private extension ArticlesTable { } } +private extension Set where Element == ParsedItem { + func articleIDs() -> Set { + return Set(map { $0.articleID }) + } +} diff --git a/Frameworks/ArticlesDatabase/Extensions/Article+Database.swift b/Frameworks/ArticlesDatabase/Extensions/Article+Database.swift index 36c5332f1..2fd0d48c8 100644 --- a/Frameworks/ArticlesDatabase/Extensions/Article+Database.swift +++ b/Frameworks/ArticlesDatabase/Extensions/Article+Database.swift @@ -79,11 +79,20 @@ extension Article { return d.count < 1 ? nil : d } - static func articlesWithParsedItems(_ parsedItems: Set, _ accountID: String, _ feedID: String, _ statusesDictionary: [String: ArticleStatus]) -> Set
{ +// static func articlesWithParsedItems(_ parsedItems: Set, _ accountID: String, _ feedID: String, _ statusesDictionary: [String: ArticleStatus]) -> Set
{ +// let maximumDateAllowed = Date().addingTimeInterval(60 * 60 * 24) // Allow dates up to about 24 hours ahead of now +// return Set(parsedItems.map{ Article(parsedItem: $0, maximumDateAllowed: maximumDateAllowed, accountID: accountID, feedID: feedID, status: statusesDictionary[$0.articleID]!) }) +// } + + static func articlesWithFeedIDsAndItems(_ feedIDsAndItems: [String: Set], _ accountID: String, _ statusesDictionary: [String: ArticleStatus]) -> Set
{ let maximumDateAllowed = Date().addingTimeInterval(60 * 60 * 24) // Allow dates up to about 24 hours ahead of now - return Set(parsedItems.map{ Article(parsedItem: $0, maximumDateAllowed: maximumDateAllowed, accountID: accountID, feedID: feedID, status: statusesDictionary[$0.articleID]!) }) + var articles = Set
() + for (feedID, parsedItems) in feedIDsAndItems { + let feedArticles = Set(parsedItems.map{ Article(parsedItem: $0, maximumDateAllowed: maximumDateAllowed, accountID: accountID, feedID: feedID, status: statusesDictionary[$0.articleID]!) }) + articles.formUnion(feedArticles) + } + return articles } - } extension Article: DatabaseObject {