From 50276233d1b1e09d256850d783dde529d2b5cde1 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 6 Oct 2019 12:37:46 -0700 Subject: [PATCH 1/4] Add feeds all in one go when processing Feedbin subscriptions. This is a performance enhancement. --- .../Feedbin/FeedbinAccountDelegate.swift | 19 +++++++++++++------ .../Account/Feedbin/FeedbinSubscription.swift | 5 ++++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index bbd185238..55b939cfe 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -722,21 +722,28 @@ private extension FeedbinAccountDelegate { } // Add any feeds we don't have and update any we do + var subscriptionsToAdd = Set() subscriptions.forEach { subscription in - + let subFeedId = String(subscription.feedID) - + if let feed = account.existingFeed(withFeedID: subFeedId) { feed.name = subscription.name // If the name has been changed on the server remove the locally edited name feed.editedName = nil feed.homePageURL = subscription.homePageURL feed.subscriptionID = String(subscription.subscriptionID) - } else { - let feed = account.createFeed(with: subscription.name, url: subscription.url, feedID: subFeedId, homePageURL: subscription.homePageURL) - feed.subscriptionID = String(subscription.subscriptionID) - account.addFeed(feed) } + else { + subscriptionsToAdd.insert(subscription) + } + } + + // Actually add subscriptions all in one go, so we don’t trigger various rebuilding things that Account does. + subscriptionsToAdd.forEach { subscription in + let feed = account.createFeed(with: subscription.name, url: subscription.url, feedID: String(subscription.feedID), homePageURL: subscription.homePageURL) + feed.subscriptionID = String(subscription.subscriptionID) + account.addFeed(feed) } } diff --git a/Frameworks/Account/Feedbin/FeedbinSubscription.swift b/Frameworks/Account/Feedbin/FeedbinSubscription.swift index 8d3ca1109..feac0e7dd 100644 --- a/Frameworks/Account/Feedbin/FeedbinSubscription.swift +++ b/Frameworks/Account/Feedbin/FeedbinSubscription.swift @@ -10,7 +10,7 @@ import Foundation import RSCore import RSParser -struct FeedbinSubscription: Codable { +struct FeedbinSubscription: Hashable, Codable { let subscriptionID: Int let feedID: Int @@ -26,6 +26,9 @@ struct FeedbinSubscription: Codable { case homePageURL = "site_url" } + public func hash(into hasher: inout Hasher) { + hasher.combine(subscriptionID) + } } struct FeedbinCreateSubscription: Codable { From be89df884ddddadf5cada0ccaf2c0fb4bd74a2b9 Mon Sep 17 00:00:00 2001 From: Jim Correia Date: Sun, 6 Oct 2019 17:56:53 -0700 Subject: [PATCH 2/4] The Debug menu can now be enabled in release builds. `defaults write com.ranchero.NetNewsWire-Evergreen ShowDebugMenu -bool YES` Toggling the Web Inspector uses SPI, so it's always excluded from the Debug menu when building for the Mac App Store. --- Mac/AppDefaults.swift | 22 +++++++++++++++++++++- Mac/AppDelegate.swift | 23 ++++++++++++++++------- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/Mac/AppDefaults.swift b/Mac/AppDefaults.swift index 9df0e52c2..2eaade629 100644 --- a/Mac/AppDefaults.swift +++ b/Mac/AppDefaults.swift @@ -33,6 +33,7 @@ struct AppDefaults { static let exportOPMLAccountID = "exportOPMLAccountID" // Hidden prefs + static let showDebugMenu = "ShowDebugMenu" static let timelineShowsSeparators = "CorreiaSeparators" static let showTitleOnMainWindow = "KafasisTitleMode" static let hideDockUnreadCount = "JustinMillerHideDockUnreadCount" @@ -142,6 +143,10 @@ struct AppDefaults { static var hideDockUnreadCount: Bool { return bool(for: Key.hideDockUnreadCount) } + + static var showDebugMenu: Bool { + return bool(for: Key.showDebugMenu) + } #if !MAC_APP_STORE static var webInspectorEnabled: Bool { @@ -196,7 +201,22 @@ struct AppDefaults { } static func registerDefaults() { - let defaults: [String : Any] = [Key.lastImageCacheFlushDate: Date(), Key.sidebarFontSize: FontSize.medium.rawValue, Key.timelineFontSize: FontSize.medium.rawValue, Key.detailFontSize: FontSize.medium.rawValue, Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue, "NSScrollViewShouldScrollUnderTitlebar": false, Key.refreshInterval: RefreshInterval.everyHour.rawValue] + #if DEBUG + let showDebugMenu = true + #else + let showDebugMenu = false + #endif + + let defaults: [String : Any] = [ + Key.lastImageCacheFlushDate: Date(), + Key.sidebarFontSize: FontSize.medium.rawValue, + Key.timelineFontSize: FontSize.medium.rawValue, + Key.detailFontSize: FontSize.medium.rawValue, + Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue, + "NSScrollViewShouldScrollUnderTitlebar": false, + Key.refreshInterval: RefreshInterval.everyHour.rawValue, + Key.showDebugMenu: showDebugMenu, + ] UserDefaults.standard.register(defaults: defaults) diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 581126f56..81fbddd7d 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -175,18 +175,25 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, refreshTimer = AccountRefreshTimer() syncTimer = ArticleStatusSyncTimer() - #if RELEASE + if AppDefaults.showDebugMenu { + refreshTimer!.update() + syncTimer!.update() + + // The Web Inspector uses SPI and can never appear in a MAC_APP_STORE build. + #if MAC_APP_STORE + let debugMenu = debugMenuItem.submenu! + let toggleWebInspectorItemIndex = debugMenu.indexOfItem(withTarget: self, andAction: #selector(toggleWebInspectorEnabled(_:))) + if toggleWebInspectorItemIndex != -1 { + debugMenu.removeItem(at: toggleWebInspectorItemIndex) + } + #endif + } else { debugMenuItem.menu?.removeItem(debugMenuItem) DispatchQueue.main.async { self.refreshTimer!.timedRefresh(nil) self.syncTimer!.timedRefresh(nil) } - #endif - - #if DEBUG - refreshTimer!.update() - syncTimer!.update() - #endif + } #if !MAC_APP_STORE DispatchQueue.main.async { @@ -309,11 +316,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, if item.action == #selector(showAddFeedWindow(_:)) || item.action == #selector(showAddFolderWindow(_:)) { return !isDisplayingSheet && !AccountManager.shared.activeAccounts.isEmpty } + #if !MAC_APP_STORE if item.action == #selector(toggleWebInspectorEnabled(_:)) { (item as! NSMenuItem).state = AppDefaults.webInspectorEnabled ? .on : .off } #endif + return true } From 099172d9d2fee7c7cab593783bfadb2ef374a399 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 12 Oct 2019 15:06:21 -0700 Subject: [PATCH 3/4] Empty the database articles cache when NetNewsWire moves to the background. This helps prevent the cache from growing forever. --- Frameworks/Account/Account.swift | 5 +++++ Frameworks/Account/AccountManager.swift | 9 +++++++++ Frameworks/ArticlesDatabase/ArticlesDatabase.swift | 8 ++++++++ Frameworks/ArticlesDatabase/ArticlesTable.swift | 8 ++++++++ Mac/AppDelegate.swift | 3 +-- 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index f6c069760..ab6b5f8d4 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -615,6 +615,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } } + /// Empty caches that can reasonably be emptied. Call when the app goes in the background, for instance. + func emptyCaches() { + database.emptyCaches() + } + // MARK: - Container public func flattenedFeeds() -> Set { diff --git a/Frameworks/Account/AccountManager.swift b/Frameworks/Account/AccountManager.swift index 0b5e71b20..a56336673 100644 --- a/Frameworks/Account/AccountManager.swift +++ b/Frameworks/Account/AccountManager.swift @@ -223,6 +223,15 @@ public final class AccountManager: UnreadCountProvider { } } + // MARK: - Caches + + /// Empty caches that can reasonably be emptied — when the app moves to the background, for instance. + public func emptyCaches() { + for account in accounts { + account.emptyCaches() + } + } + // MARK: - Notifications @objc dynamic func unreadCountDidChange(_ notification: Notification) { diff --git a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift index d3e455e36..b5c4343a1 100644 --- a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift +++ b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift @@ -143,6 +143,14 @@ public final class ArticlesDatabase { public func mark(_ articles: Set
, statusKey: ArticleStatus.Key, flag: Bool) -> Set? { return articlesTable.mark(articles, statusKey, flag) } + + // MARK: - Caches + + /// Call to free up some memory. Should be done when the app is backgrounded, for instance. + /// This does not empty *all* caches — just the ones that are empty-able. + public func emptyCaches() { + articlesTable.emptyCaches() + } } // MARK: - Private diff --git a/Frameworks/ArticlesDatabase/ArticlesTable.swift b/Frameworks/ArticlesDatabase/ArticlesTable.swift index 8f06afdc8..7d8808339 100644 --- a/Frameworks/ArticlesDatabase/ArticlesTable.swift +++ b/Frameworks/ArticlesDatabase/ArticlesTable.swift @@ -411,6 +411,14 @@ final class ArticlesTable: DatabaseTable { } } } + + // MARK: - Caches + + func emptyCaches() { + queue.run { _ in + self.databaseArticlesCache = [String: DatabaseArticle]() + } + } } // MARK: - Private diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 81fbddd7d..4270d4899 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -224,9 +224,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } func applicationDidResignActive(_ notification: Notification) { - TimelineStringFormatter.emptyCaches() - + AccountManager.shared.emptyCaches() saveState() } From 1d0cacd5fcb5169a7520b6bf0a775fa114370f9c Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 13 Oct 2019 19:02:56 -0700 Subject: [PATCH 4/4] =?UTF-8?q?When=20syncing,=20update=20the=20database?= =?UTF-8?q?=20by=20articleIDs=20rather=20than=20by=20feeds=20=E2=80=94=20t?= =?UTF-8?q?his=20means=20*far*=20fewer=20fetches=20and=20much=20less=20dat?= =?UTF-8?q?a=20pulled=20from=20the=20database.=20It=20should=20help=20app?= =?UTF-8?q?=20responsiveness=20dramatically=20during=20sync,=20and=20espec?= =?UTF-8?q?ially=20during=20an=20initial=20sync.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frameworks/Account/Account.swift | 34 ++++++- .../Feedbin/FeedbinAccountDelegate.swift | 31 +----- .../ArticlesDatabase/ArticlesDatabase.swift | 9 +- .../ArticlesDatabase/ArticlesTable.swift | 98 ++++++++++++++++--- .../Extensions/Article+Database.swift | 15 ++- 5 files changed, 133 insertions(+), 54 deletions(-) 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 {