diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index c34f3c19a..a50582199 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -621,12 +621,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])) @@ -663,6 +691,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 b6eee62d8..29b7e9095 100644 --- a/Frameworks/Account/AccountManager.swift +++ b/Frameworks/Account/AccountManager.swift @@ -248,6 +248,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/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index d831a3a99..c8f7da41b 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -730,9 +730,9 @@ 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 @@ -1060,7 +1060,6 @@ private extension FeedbinAccountDelegate { } func refreshArticles(_ account: Account, page: String?, completion: @escaping (() -> Void)) { - guard let page = page else { completion() return @@ -1080,42 +1079,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 312a02bee..555ff05b7 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 { @@ -126,10 +126,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) } @@ -151,6 +152,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 894642799..e3d5455bb 100644 --- a/Frameworks/ArticlesDatabase/ArticlesTable.swift +++ b/Frameworks/ArticlesDatabase/ArticlesTable.swift @@ -215,9 +215,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 } @@ -231,30 +231,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. @@ -265,16 +269,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) @@ -418,6 +481,14 @@ final class ArticlesTable: DatabaseTable { } } } + + // MARK: - Caches + + func emptyCaches() { + queue.run { _ in + self.databaseArticlesCache = [String: DatabaseArticle]() + } + } } // MARK: - Private @@ -595,7 +666,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) } @@ -727,3 +798,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 { diff --git a/Mac/AppDefaults.swift b/Mac/AppDefaults.swift index 0f23b0071..6ac0938ea 100644 --- a/Mac/AppDefaults.swift +++ b/Mac/AppDefaults.swift @@ -5,7 +5,6 @@ // Created by Brent Simmons on 9/22/17. // Copyright © 2017 Ranchero Software. All rights reserved. // - import AppKit enum FontSize: Int { @@ -228,7 +227,6 @@ struct AppDefaults { // an issue, this could be changed to proactively look for whether the default has been // set _by the user_ to false, and respect that default if it is so-set. // UserDefaults.standard.set(true, forKey: "NSQuitAlwaysKeepsWindows") - // TODO: revisit the above when coming back to state restoration issues. } @@ -326,7 +324,6 @@ private extension AppDefaults { } // MARK: - - extension UserDefaults { /// This property exists so that it can conveniently be observed via KVO @objc var CorreiaSeparators: Bool { @@ -338,4 +335,3 @@ extension UserDefaults { } } } - diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index b0a823249..a4b4f5c08 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -5,7 +5,6 @@ // Created by Brent Simmons on 7/11/15. // Copyright © 2015 Ranchero Software, LLC. All rights reserved. // - import AppKit import UserNotifications import Articles @@ -84,7 +83,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } // MARK: - API - func logMessage(_ message: String, type: LogItem.ItemType) { #if DEBUG @@ -115,7 +113,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } // MARK: - NSApplicationDelegate - func applicationWillFinishLaunching(_ notification: Notification) { installAppleEventHandlers() #if TEST @@ -271,7 +268,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } // MARK: Notifications - @objc func unreadCountDidChange(_ note: Notification) { if note.object is AccountManager { @@ -305,7 +301,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } // MARK: Main Window - func windowControllerWithName(_ storyboardName: String) -> NSWindowController { let storyboard = NSStoryboard(name: NSStoryboard.Name(storyboardName), bundle: nil) @@ -322,7 +317,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } // MARK: NSUserInterfaceValidations - func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { if shuttingDown { return false @@ -362,7 +356,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } // MARK: Add Feed - func addFeed(_ urlString: String?, name: String? = nil, account: Account? = nil, folder: Folder? = nil) { createAndShowMainWindow() @@ -374,14 +367,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } // MARK: - Dock Badge - @objc func updateDockBadge() { let label = unreadCount > 0 && !AppDefaults.hideDockUnreadCount ? "\(unreadCount)" : "" NSApplication.shared.dockTile.badgeLabel = label } // MARK: - Actions - @IBAction func showPreferences(_ sender: Any?) { if preferencesWindowController == nil { @@ -588,7 +579,6 @@ extension AppDelegate { // An attached inspector can display incorrectly on certain setups (like mine); default to displaying in a separate window, // and reset the default to a separate window when the preference is toggled off and on again in case the inspector is // accidentally reattached. - AppDefaults.webInspectorStartsAttached = false NotificationCenter.default.post(name: .WebInspectorEnabledDidChange, object: newValue) #endif @@ -649,4 +639,4 @@ extension AppDelegate : ScriptingAppDelegate { internal var scriptingSelectedArticles: [Article] { return self.scriptingMainWindowController?.scriptingSelectedArticles ?? [] } -} +} \ No newline at end of file