From b28a849af6535da355567f465f074040cc068b2a Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 18 Sep 2017 22:00:35 -0700 Subject: [PATCH] Continue march toward non-optional article.status. --- Evergreen.xcodeproj/project.pbxproj | 6 +- .../xcshareddata/WorkspaceSettings.xcsettings | 8 + Evergreen/AppConstants.swift | 28 +++- Evergreen/AppDelegate.swift | 25 ++-- .../MainWindow/MainWindowController.swift | 2 +- .../Sidebar/SidebarViewController.swift | 4 +- .../MainWindow/StatusBar/StatusBarView.swift | 4 +- .../Timeline/TimelineViewController.swift | 6 +- Evergreen/Preferences/Defaults.swift | 21 +-- .../PreferencesWindowController.swift | 25 ++-- Frameworks/Account/Account.swift | 5 + Frameworks/Account/AccountManager.swift | 4 +- .../Account/Extensions/Article+Account.swift | 2 +- .../Account/Extensions/Feed+Account.swift | 2 +- Frameworks/Account/Folder.swift | 2 +- Frameworks/Data/Article.swift | 7 +- Frameworks/Data/DatabaseID.swift | 2 +- Frameworks/Database/ArticlesTable.swift | 71 ++++----- .../Database.xcodeproj/project.pbxproj | 4 + .../Extensions/Article+Database.swift | 14 +- .../Extensions/ParsedArticle+Database.swift | 24 +++ Frameworks/Database/StatusesTable.swift | 138 +++++------------- Frameworks/RSParser/OPML/RSOPMLParser.h | 6 + Frameworks/RSParser/OPML/RSOPMLParser.m | 14 ++ ToDo.opml | 21 ++- 25 files changed, 232 insertions(+), 213 deletions(-) create mode 100644 Evergreen.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 Frameworks/Database/Extensions/ParsedArticle+Database.swift diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index 0768c9672..f37e76a13 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -1147,11 +1147,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 842E45CE1ED8C308000A8B52 /* AppConstants.swift in Sources */, + 849C64641ED37A5D003D8FC0 /* AppDelegate.swift in Sources */, 842E45DD1ED8C54B000A8B52 /* Browser.swift in Sources */, 849A975B1ED9EB0D007D329B /* ArticleUtilities.swift in Sources */, 849A97891ED9ECEF007D329B /* ArticleStyle.swift in Sources */, 849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */, - 849A97541ED9EAC0007D329B /* AddFeedWindowController.swift in Sources */, 849A97791ED9EC04007D329B /* TimelineStringUtilities.swift in Sources */, 849A979F1ED9F130007D329B /* SidebarCell.swift in Sources */, 849A97981ED9EFAA007D329B /* Node-Extensions.swift in Sources */, @@ -1167,12 +1168,11 @@ 849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */, 849A978D1ED9EE4D007D329B /* FeedListWindowController.swift in Sources */, 849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */, - 842E45CE1ED8C308000A8B52 /* AppConstants.swift in Sources */, - 849C64641ED37A5D003D8FC0 /* AppDelegate.swift in Sources */, 849A975C1ED9EB0D007D329B /* DefaultFeedsImporter.swift in Sources */, 849A97781ED9EC04007D329B /* TimelineCellLayout.swift in Sources */, 849A976C1ED9EBC8007D329B /* TimelineTableRowView.swift in Sources */, 849A977B1ED9EC04007D329B /* UnreadIndicatorView.swift in Sources */, + 849A97541ED9EAC0007D329B /* AddFeedWindowController.swift in Sources */, 842E45E51ED8C6B7000A8B52 /* MainWindowSplitView.swift in Sources */, 849A976D1ED9EBC8007D329B /* TimelineTableView.swift in Sources */, 849A97A31ED9F180007D329B /* FolderTreeControllerDelegate.swift in Sources */, diff --git a/Evergreen.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Evergreen.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..3ddf867a1 --- /dev/null +++ b/Evergreen.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + BuildSystemType + Latest + + diff --git a/Evergreen/AppConstants.swift b/Evergreen/AppConstants.swift index 1dc6e464e..cd15a0f40 100644 --- a/Evergreen/AppConstants.swift +++ b/Evergreen/AppConstants.swift @@ -18,12 +18,26 @@ extension Notification.Name { static let AppNavigationKeyPressed = Notification.Name("AppNavigationKeyPressedNotification") } -let viewKey = "view" -let nodeKey = "node" -let objectsKey = "objects" -let articleKey = "article" +struct AppUserInfoKey { + + static let view = "view" + static let node = "node" + static let objects = "objects" + static let article = "article" + static let articles = "articles" + static let articleStatus = "status" + static let appNavigation = "key" +} + +struct AppDefaultsKey { + + static let firstRunDate = "firstRunDate" + + static let sidebarFontSize = "sidebarFontSize" + static let timelineFontSize = "timelineFontSize" + static let detailFontSize = "detailFontSize" + + static let openInBrowserInBackground = "openInBrowserInBackground" +} -let articlesKey = "articles" -let articleStatusKey = "statusKey" -let appNavigationKey = "keyKey" diff --git a/Evergreen/AppDelegate.swift b/Evergreen/AppDelegate.swift index ad1c704fa..43927f000 100644 --- a/Evergreen/AppDelegate.swift +++ b/Evergreen/AppDelegate.swift @@ -41,7 +41,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations { super.init() } - // MARK: NSApplicationDelegate + // MARK: - NSApplicationDelegate func applicationDidFinishLaunching(_ note: Notification) { @@ -51,11 +51,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations { let _ = AccountManager.sharedInstance - let kFirstRunDateKey = "firstRun" var isFirstRun = false - if UserDefaults.standard.object(forKey: kFirstRunDateKey) == nil { + if UserDefaults.standard.object(forKey: AppDefaultsKey.firstRunDate) == nil { isFirstRun = true - UserDefaults.standard.set(Date(), forKey: kFirstRunDateKey) + UserDefaults.standard.set(Date(), forKey: AppDefaultsKey.firstRunDate) } importDefaultFeedsIfNeeded(isFirstRun, account: AccountManager.sharedInstance.localAccount) @@ -224,7 +223,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations { panel.allowsOtherFileTypes = false let result = panel.runModal() - if result == NSFileHandlingPanelOKButton { + if result == NSApplication.ModalResponse.OK { if let url = panel.url { DispatchQueue.main.async { self.parseAndImportOPML(url, AccountManager.sharedInstance.localAccount) @@ -250,15 +249,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations { panel.nameFieldStringValue = "MySubscriptions.opml" let result = panel.runModal() - if result == NSFileHandlingPanelOKButton { + if result.rawValue == NSFileHandlingPanelOKButton { if let url = panel.url { DispatchQueue.main.async { - let opmlString = AccountManager.sharedInstance.localAccount.opmlString(indentLevel: 0) + let opmlString = AccountManager.sharedInstance.localAccount.OPMLString(indentLevel: 0) do { try opmlString.write(to: url, atomically: true, encoding: String.Encoding.utf8) } catch let error as NSError { - NSApplication.shared().presentError(error) + NSApplication.shared.presentError(error) } } } @@ -271,7 +270,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations { let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString")! let urlString = "mailto:support@ranchero.com?subject=I%20need%20help%20with%20\(escapedAppName)%20\(version)&body=I%20ran%20into%20a%20problem:%20" if let url = URL(string: urlString) { - NSWorkspace.shared().open(url) + NSWorkspace.shared.open(url) } } @@ -321,17 +320,17 @@ private extension AppDelegate { return } - let parserData = ParserData(data: opmlData, urlString: url.absoluteString) - RSParseOPML(xmlData) { (opmlDocument, error) in + let parserData = ParserData(url: url.absoluteString, data: opmlData) + RSParseOPML(parserData) { (opmlDocument, error) in if let error = error { - NSApplication.shared().presentError(error) + NSApplication.shared.presentError(error) return } if let opmlDocument = opmlDocument { account.importOPML(opmlDocument) - // account.refreshAll() + account.refreshAll() } } } diff --git a/Evergreen/MainWindow/MainWindowController.swift b/Evergreen/MainWindow/MainWindowController.swift index f0942db8c..ed1f10a84 100644 --- a/Evergreen/MainWindow/MainWindowController.swift +++ b/Evergreen/MainWindow/MainWindowController.swift @@ -40,7 +40,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { @objc func appNavigationKeyPressed(_ note: Notification) { - guard let key = note.userInfo?[appNavigationKey] as? Int else { + guard let key = note.userInfo?[AppKey.appNavigation] as? Int else { return } guard let contentView = window?.contentView, let view = note.object as? NSView, view.isDescendant(of: contentView) else { diff --git a/Evergreen/MainWindow/Sidebar/SidebarViewController.swift b/Evergreen/MainWindow/Sidebar/SidebarViewController.swift index 9e5ea3b5a..3e6ca4e87 100644 --- a/Evergreen/MainWindow/Sidebar/SidebarViewController.swift +++ b/Evergreen/MainWindow/Sidebar/SidebarViewController.swift @@ -177,9 +177,9 @@ private extension SidebarViewController { var userInfo = [AnyHashable: Any]() if let selectedObjects = selectedObjects { - userInfo[objectsKey] = selectedObjects + userInfo[AppKey.objects] = selectedObjects } - userInfo[viewKey] = self.outlineView + userInfo[AppKey.view] = self.outlineView NotificationCenter.default.post(name: .SidebarSelectionDidChange, object: self, userInfo: userInfo) } diff --git a/Evergreen/MainWindow/StatusBar/StatusBarView.swift b/Evergreen/MainWindow/StatusBar/StatusBarView.swift index 61d5e19bc..ffd31a83e 100644 --- a/Evergreen/MainWindow/StatusBar/StatusBarView.swift +++ b/Evergreen/MainWindow/StatusBar/StatusBarView.swift @@ -56,10 +56,10 @@ final class StatusBarView: NSView { @objc dynamic func timelineSelectionDidChange(_ note: Notification) { - let timelineView = note.userInfo?[viewKey] as! NSView + let timelineView = note.userInfo?[AppKey.view] as! NSView if timelineView.window! === self.window { - article = note.userInfo?[articleKey] as? Article + article = note.userInfo?[AppKey.article] as? Article } } diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController.swift b/Evergreen/MainWindow/Timeline/TimelineViewController.swift index c8c30258e..a21146e2e 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController.swift @@ -414,7 +414,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView var fetchedArticles = [Article]() for (accountID, objects) in accountsDictionary { - guard let oneAccount = account(with: accountID) else { + guard let oneAccount = accountWithID(accountID) else { continue } @@ -473,14 +473,14 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { - let rowView: TimelineTableRowView = tableView.make(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "timelineRow"), owner: self) as! TimelineTableRowView + let rowView: TimelineTableRowView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "timelineRow"), owner: self) as! TimelineTableRowView rowView.cellAppearance = cellAppearance return rowView } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { - let cell: TimelineTableCellView = tableView.make(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "timelineCell"), owner: self) as! TimelineTableCellView + let cell: TimelineTableCellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "timelineCell"), owner: self) as! TimelineTableCellView cell.cellAppearance = cellAppearance if let article = articleAtRow(row) { diff --git a/Evergreen/Preferences/Defaults.swift b/Evergreen/Preferences/Defaults.swift index 471bae2b7..5b773b556 100644 --- a/Evergreen/Preferences/Defaults.swift +++ b/Evergreen/Preferences/Defaults.swift @@ -8,15 +8,18 @@ import Foundation -let SidebarFontSizeKey = "sidebarFontSize" -let TimelineFontSizeKey = "timelineFontSize" -let ArticleFontSizeKey = "articleFontSize" +final class AppDefaults { + + + +} -let SidebarFontSizeKVOKey = "values." + SidebarFontSizeKey -let TimelineFontSizeKVOKey = "values." + TimelineFontSizeKey -let ArticleFontSizeKVOKey = "values." + ArticleFontSizeKey - -let OpenInBrowserInBackgroundKey = "openInBrowserInBackground" +extension AppDefaultsKey { + + static let sidebarFontSizeKVO = "values." + sidebarFontSize + static let timelineFontSizeKVO = "values." + timelineFontSize + static let detailFontSizeKVO = "values." + detailFontSize +} enum FontSize: Int { case small = 0 @@ -30,7 +33,7 @@ private let largestFontSizeRawValue = FontSize.veryLarge.rawValue func registerDefaults() { - let defaults = [SidebarFontSizeKey: FontSize.medium.rawValue, TimelineFontSizeKey: FontSize.medium.rawValue, ArticleFontSizeKey: FontSize.medium.rawValue] + let defaults = [AppDefaultsKey.sidebarFontSize: FontSize.medium.rawValue, AppDefaultsKey.timelineFontSize: FontSize.medium.rawValue, AppDefaultsKey.detailFontSize, FontSize.medium.rawValue] UserDefaults.standard.register(defaults: defaults) } diff --git a/Evergreen/Preferences/PreferencesWindowController.swift b/Evergreen/Preferences/PreferencesWindowController.swift index cc54a3fa2..76825e6d3 100644 --- a/Evergreen/Preferences/PreferencesWindowController.swift +++ b/Evergreen/Preferences/PreferencesWindowController.swift @@ -10,9 +10,16 @@ import Cocoa private struct PreferencesToolbarItemSpec { - let identifier: String // Toolbar item identifier and view controller identifier in storyboard + let identifier: NSToolbarItem.Identifier let name: String - let imageName: String + let imageName: NSImage.Name + + init(identifierRawValue: String, name: String, imageName: NSImage.Name) { + + self.identifier = NSToolbarItem.Identifier(rawValue: identifierRawValue) + self.name = name + self.imageName = imageName + } } private let toolbarItemIdentifierGeneral = "General" @@ -23,14 +30,14 @@ class PreferencesWindowController : NSWindowController, NSToolbarDelegate { fileprivate var viewControllers = [String: NSViewController]() fileprivate let toolbarItemSpecs: [PreferencesToolbarItemSpec] = { var specs = [PreferencesToolbarItemSpec]() - specs += [PreferencesToolbarItemSpec(identifier: toolbarItemIdentifierGeneral, name: NSLocalizedString("General", comment: "Preferences"), imageName: NSImageNamePreferencesGeneral)] + specs += [PreferencesToolbarItemSpec(identifierRawValue: toolbarItemIdentifierGeneral, name: NSLocalizedString("General", comment: "Preferences"), imageName: NSImage.Name.preferencesGeneral)] return specs }() override func windowDidLoad() { - let toolbar = NSToolbar(identifier: "PreferencesToolbar") + let toolbar = NSToolbar(identifier: NSToolbar.Identifier(rawValue: "PreferencesToolbar")) toolbar.delegate = self toolbar.autosavesConfiguration = false toolbar.allowsUserCustomization = false @@ -70,18 +77,18 @@ class PreferencesWindowController : NSWindowController, NSToolbarDelegate { return toolbarItem } - func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [String] { + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { return toolbarItemSpecs.map { $0.identifier } } - func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [String] { - + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return toolbarDefaultItemIdentifiers(toolbar) } - func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [String] { - + func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return toolbarDefaultItemIdentifiers(toolbar) } } diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index d85cff6bf..1eeb2c01e 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -89,6 +89,11 @@ public final class Account: DisplayNameProvider, Hashable { return nil //TODO } + public func importOPML(_ opmlDocument: RSOPMLDocument) { + + // TODO + } + // MARK: - Equatable public class func ==(lhs: Account, rhs: Account) -> Bool { diff --git a/Frameworks/Account/AccountManager.swift b/Frameworks/Account/AccountManager.swift index 8916bed12..ed9d095d5 100644 --- a/Frameworks/Account/AccountManager.swift +++ b/Frameworks/Account/AccountManager.swift @@ -98,7 +98,7 @@ public final class AccountManager: UnreadCountProvider { return false } - func anyAccountHasFeedWithURL(_ urlString: String) -> Bool { + public func anyAccountHasFeedWithURL(_ urlString: String) -> Bool { for account in accounts { if let _ = account.existingFeed(withURL: urlString) { @@ -191,7 +191,7 @@ private func accountFilePathWithFolder(_ folderPath: String) -> String { return NSString(string: folderPath).appendingPathComponent(accountDataFileName) } -public func account(with accountID: String) -> Account? { +public func accountWithID(_ accountID: String) -> Account? { // Shortcut. return AccountManager.sharedInstance.existingAccount(with: accountID) diff --git a/Frameworks/Account/Extensions/Article+Account.swift b/Frameworks/Account/Extensions/Article+Account.swift index 4901607d7..796ad7dc4 100644 --- a/Frameworks/Account/Extensions/Article+Account.swift +++ b/Frameworks/Account/Extensions/Article+Account.swift @@ -13,7 +13,7 @@ public extension Article { var account: Account? { get { - return account(with: accountID) + return accountWithID(accountID) } } } diff --git a/Frameworks/Account/Extensions/Feed+Account.swift b/Frameworks/Account/Extensions/Feed+Account.swift index b9cf8a24b..3f032fecc 100644 --- a/Frameworks/Account/Extensions/Feed+Account.swift +++ b/Frameworks/Account/Extensions/Feed+Account.swift @@ -13,7 +13,7 @@ public extension Feed { var account: Account? { get { - return account(with: accountID) + return accountWithID(accountID) } } } diff --git a/Frameworks/Account/Folder.swift b/Frameworks/Account/Folder.swift index c783fa541..d34c98597 100644 --- a/Frameworks/Account/Folder.swift +++ b/Frameworks/Account/Folder.swift @@ -16,7 +16,7 @@ public final class Folder: DisplayNameProvider, UnreadCountProvider { public var account: Account? { get { - return account(with: accountID) + return accountWithID(accountID) } } diff --git a/Frameworks/Data/Article.swift b/Frameworks/Data/Article.swift index 25af7f8bf..c1e983a37 100644 --- a/Frameworks/Data/Article.swift +++ b/Frameworks/Data/Article.swift @@ -56,12 +56,17 @@ public struct Article: Hashable { self.articleID = articleID } else { - self.articleID = databaseIDWithString("\(feedID) \(uniqueID)") + self.articleID = Article.calculatedArticleID(feedID: feedID, uniqueID: uniqueID) } self.hashValue = accountID.hashValue ^ self.articleID.hashValue } + public static func calculatedArticleID(feedID: String, uniqueID: String) -> String { + + return databaseIDWithString("\(feedID) \(uniqueID)") + } + public static func ==(lhs: Article, rhs: Article) -> Bool { return lhs.hashValue == rhs.hashValue && lhs.articleID == rhs.articleID && lhs.accountID == rhs.accountID && lhs.feedID == rhs.feedID && lhs.uniqueID == rhs.uniqueID && lhs.title == rhs.title && lhs.contentHTML == rhs.contentHTML && lhs.url == rhs.url && lhs.externalURL == rhs.externalURL && lhs.summary == rhs.summary && lhs.imageURL == rhs.imageURL && lhs.bannerImageURL == rhs.bannerImageURL && lhs.datePublished == rhs.datePublished && lhs.authors == rhs.authors && lhs.tags == rhs.tags && lhs.attachments == rhs.attachments diff --git a/Frameworks/Data/DatabaseID.swift b/Frameworks/Data/DatabaseID.swift index 8f89eadcf..2729a04b2 100644 --- a/Frameworks/Data/DatabaseID.swift +++ b/Frameworks/Data/DatabaseID.swift @@ -16,7 +16,7 @@ import RSCore private var databaseIDCache = [String: String]() private var databaseIDCacheLock = os_unfair_lock_s() -func databaseIDWithString(_ s: String) -> String { +public func databaseIDWithString(_ s: String) -> String { os_unfair_lock_lock(&databaseIDCacheLock) defer { diff --git a/Frameworks/Database/ArticlesTable.swift b/Frameworks/Database/ArticlesTable.swift index 0747a3408..0f81a0a42 100644 --- a/Frameworks/Database/ArticlesTable.swift +++ b/Frameworks/Database/ArticlesTable.swift @@ -85,8 +85,8 @@ final class ArticlesTable: DatabaseTable { return } - // 1. Create incoming articles with parsedItems. - // 2. Ensure statuses for all the incoming articles. + // 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. @@ -94,19 +94,32 @@ final class ArticlesTable: DatabaseTable { // 7. Call back with new and updated Articles. let feedID = feed.feedID + let articleIDs = Set(parsedFeed.items.map { $0.articleID }) - self.queue.run { (database) in + self.queue.update { (database) in - // This doesn’t hit the database, but it should be done on the database queue. - let allIncomingArticles = Article.articlesWithParsedItems(parsedFeed.items, self.accountID, feedID) //1 + let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, database) //1 + assert(statusesDictionary.count == articleIDs.count) + + let allIncomingArticles = Article.articlesWithParsedItems(parsedFeed.items, self.accountID, feedID, statusesDictionary) //2 if allIncomingArticles.isEmpty { self.callUpdateArticlesCompletionBlock(nil, nil, completion) return } - DispatchQueue.main.async { - self.ensureStatusesAndSaveArticles(allIncomingArticles, feedID, completion) //2-7 + let incomingArticles = self.filterIncomingArticles(allIncomingArticles, statusesDictionary) //3 + if incomingArticles.isEmpty { + self.callUpdateArticlesCompletionBlock(nil, nil, completion) + return } + + let fetchedArticles = self.fetchArticlesForFeedID(feedID, withLimits: false, database: 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 } } @@ -149,9 +162,7 @@ private extension ArticlesTable { // Then fetch the related objects, given the set of articleIDs. // Then create set of Articles *with* related objects and return it. - let (stubArticles, statuses) = stubArticlesAndStatuses(with: resultSet) - - statusesTable.addIfNotCached(statuses) + let stubArticles = makeStubArticles(with: resultSet) if stubArticles.isEmpty { return stubArticles } @@ -173,25 +184,25 @@ private extension ArticlesTable { return articles } - func stubArticlesAndStatuses(with resultSet: FMResultSet) -> (Set
, Set) { + func makeStubArticles(with resultSet: FMResultSet) -> Set
{ var stubArticles = Set
() - var statuses = Set() // Note: the resultSet is a result of a JOIN query with the statuses table, // so we can get the statuses at the same time and avoid additional database lookups. while resultSet.next() { - if let stubArticle = Article(row: resultSet, accountID: accountID) { - stubArticles.insert(stubArticle) + guard let status = statusesTable.statusWithRow(resultSet) else { + assertionFailure("Expected status.") + continue } - if let status = statusesTable.statusWithRow(resultSet) { - statuses.insert(status) + if let stubArticle = Article(row: resultSet, accountID: accountID, status: status) { + stubArticles.insert(stubArticle) } } resultSet.close() - return (stubArticles, statuses) + return stubArticles } func articleWithAttachedRelatedObjects(_ stubArticle: Article, _ authorsMap: RelatedObjectsMap?, _ attachmentsMap: RelatedObjectsMap?, _ tagsMap: RelatedObjectsMap?) -> Article { @@ -263,32 +274,6 @@ private extension ArticlesTable { // MARK: Saving Parsed Items - private func ensureStatusesAndSaveArticles(_ allIncomingArticles: Set
, _ feedID: String, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) { - - statusesTable.ensureStatusesForArticleIDs(allIncomingArticles.articleIDs()) { (statusesDictionary) in // 2 - - self.queue.update{ (database) in - self.saveArticlesWithDatabase(allIncomingArticles, statusesDictionary, feedID, database, completion) - } - } - } - - private func saveArticlesWithDatabase(_ allIncomingArticles: Set
, _ statusesDictionary: [String: ArticleStatus], _ feedID: String, _ database: FMDatabase, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) { // 3-7 - - let incomingArticles = filterIncomingArticles(allIncomingArticles, statusesDictionary) //3 - if incomingArticles.isEmpty { - callUpdateArticlesCompletionBlock(nil, nil, completion) - return - } - - let fetchedArticles = fetchArticlesForFeedID(feedID, withLimits: false, database: database) //4 - let fetchedArticlesDictionary = fetchedArticles.dictionary() - - let newArticles = findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5 - let updatedArticles = findAndSaveUpdatedArticles(incomingArticles, fetchedArticlesDictionary, database) //6 - - callUpdateArticlesCompletionBlock(newArticles, updatedArticles, completion) - } func callUpdateArticlesCompletionBlock(_ newArticles: Set
?, _ updatedArticles: Set
?, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) { diff --git a/Frameworks/Database/Database.xcodeproj/project.pbxproj b/Frameworks/Database/Database.xcodeproj/project.pbxproj index 6ac33d881..88a47764c 100644 --- a/Frameworks/Database/Database.xcodeproj/project.pbxproj +++ b/Frameworks/Database/Database.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840405CE1F1A963700DF0296 /* AttachmentsTable.swift */; }; 84288A001F6A3C4400395871 /* DatabaseObject+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842889FF1F6A3C4400395871 /* DatabaseObject+Database.swift */; }; 84288A021F6A3D8000395871 /* RelatedObjectsMap+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84288A011F6A3D8000395871 /* RelatedObjectsMap+Database.swift */; }; + 843702C31F70D15D00B18807 /* ParsedArticle+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843702C21F70D15D00B18807 /* ParsedArticle+Database.swift */; }; 843CB9961F34174100EE6581 /* Author+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F20F901F1810DD00D8E682 /* Author+Database.swift */; }; 844BEE411F0AB3AB004AB7CD /* Database.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844BEE371F0AB3AA004AB7CD /* Database.framework */; }; 844BEE461F0AB3AB004AB7CD /* DatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */; }; @@ -116,6 +117,7 @@ 840405CE1F1A963700DF0296 /* AttachmentsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsTable.swift; sourceTree = ""; }; 842889FF1F6A3C4400395871 /* DatabaseObject+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseObject+Database.swift"; sourceTree = ""; }; 84288A011F6A3D8000395871 /* RelatedObjectsMap+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelatedObjectsMap+Database.swift"; sourceTree = ""; }; + 843702C21F70D15D00B18807 /* ParsedArticle+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "ParsedArticle+Database.swift"; path = "Extensions/ParsedArticle+Database.swift"; sourceTree = ""; }; 844BEE371F0AB3AA004AB7CD /* Database.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Database.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; @@ -216,6 +218,7 @@ children = ( 846FB36A1F4A937B00EAB81D /* Feed+Database.swift */, 845580751F0AF670003CCFA1 /* Article+Database.swift */, + 843702C21F70D15D00B18807 /* ParsedArticle+Database.swift */, 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */, 84F20F901F1810DD00D8E682 /* Author+Database.swift */, 8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */, @@ -483,6 +486,7 @@ 840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */, 84F20F8F1F180D8700D8E682 /* AuthorsTable.swift in Sources */, 84288A001F6A3C4400395871 /* DatabaseObject+Database.swift in Sources */, + 843702C31F70D15D00B18807 /* ParsedArticle+Database.swift in Sources */, 84E156EC1F0AB80E00F8CC05 /* ArticlesTable.swift in Sources */, 84E156EE1F0AB81400F8CC05 /* StatusesTable.swift in Sources */, 84E156EA1F0AB80500F8CC05 /* Database.swift in Sources */, diff --git a/Frameworks/Database/Extensions/Article+Database.swift b/Frameworks/Database/Extensions/Article+Database.swift index 673a93afc..e4b22298f 100644 --- a/Frameworks/Database/Extensions/Article+Database.swift +++ b/Frameworks/Database/Extensions/Article+Database.swift @@ -13,7 +13,7 @@ import RSParser extension Article { - init?(row: FMResultSet, accountID: String, authors: Set? = nil, attachments: Set? = nil, tags: Set? = nil) { + init?(row: FMResultSet, accountID: String, authors: Set? = nil, attachments: Set? = nil, tags: Set? = nil, status: ArticleStatus) { guard let feedID = row.string(forColumn: DatabaseKey.feedID) else { return nil @@ -35,15 +35,15 @@ extension Article { let dateModified = row.date(forColumn: DatabaseKey.dateModified) let accountInfo: AccountInfo? = nil // TODO - self.init(accountID: accountID, articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments, accountInfo: accountInfo) + self.init(accountID: accountID, articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments, accountInfo: accountInfo, status: status) } - init(parsedItem: ParsedItem, accountID: String, feedID: String) { + init(parsedItem: ParsedItem, accountID: String, feedID: String, status: ArticleStatus) { let authors = Author.authorsWithParsedAuthors(parsedItem.authors) let attachments = Attachment.attachmentsWithParsedAttachments(parsedItem.attachments) - self.init(accountID: accountID, articleID: parsedItem.syncServiceID, feedID: feedID, uniqueID: parsedItem.uniqueID, title: parsedItem.title, contentHTML: parsedItem.contentHTML, contentText: parsedItem.contentText, url: parsedItem.url, externalURL: parsedItem.externalURL, summary: parsedItem.summary, imageURL: parsedItem.imageURL, bannerImageURL: parsedItem.bannerImageURL, datePublished: parsedItem.datePublished, dateModified: parsedItem.dateModified, authors: authors, tags: parsedItem.tags, attachments: attachments, accountInfo: nil) + self.init(accountID: accountID, articleID: parsedItem.syncServiceID, feedID: feedID, uniqueID: parsedItem.uniqueID, title: parsedItem.title, contentHTML: parsedItem.contentHTML, contentText: parsedItem.contentText, url: parsedItem.url, externalURL: parsedItem.externalURL, summary: parsedItem.summary, imageURL: parsedItem.imageURL, bannerImageURL: parsedItem.bannerImageURL, datePublished: parsedItem.datePublished, dateModified: parsedItem.dateModified, authors: authors, tags: parsedItem.tags, attachments: attachments, accountInfo: nil, status: status) } func articleByAttaching(_ authors: Set?, _ attachments: Set?, _ tags: Set?) -> Article { @@ -52,7 +52,7 @@ extension Article { return self } - return Article(accountID: accountID, articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments, accountInfo: accountInfo) + return Article(accountID: accountID, articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments, accountInfo: accountInfo, status: status) } private func addPossibleStringChangeWithKeyPath(_ comparisonKeyPath: KeyPath, _ otherArticle: Article, _ key: String, _ dictionary: NSMutableDictionary) { @@ -108,9 +108,9 @@ extension Article { return d } - static func articlesWithParsedItems(_ parsedItems: Set, _ accountID: String, _ feedID: String) -> Set
{ + static func articlesWithParsedItems(_ parsedItems: Set, _ accountID: String, _ feedID: String, _ statusesDictionary: [String: ArticleStatus]) -> Set
{ - return Set(parsedItems.map{ Article(parsedItem: $0, accountID: accountID, feedID: feedID) }) + return Set(parsedItems.map{ Article(parsedItem: $0, accountID: accountID, feedID: feedID, status: statusesDictionary[$0.articleID]!) }) } } diff --git a/Frameworks/Database/Extensions/ParsedArticle+Database.swift b/Frameworks/Database/Extensions/ParsedArticle+Database.swift new file mode 100644 index 000000000..6069f0899 --- /dev/null +++ b/Frameworks/Database/Extensions/ParsedArticle+Database.swift @@ -0,0 +1,24 @@ +// +// ParsedArticle+Database.swift +// Database +// +// Created by Brent Simmons on 9/18/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import Foundation +import RSParser +import Data + +extension ParsedItem { + + var articleID: String { + get { + if let s = syncServiceID { + return s + } + // Must be same calculation as for Article. + return Article.calculatedArticleID(feedID: feedURL, uniqueID: uniqueID) + } + } +} diff --git a/Frameworks/Database/StatusesTable.swift b/Frameworks/Database/StatusesTable.swift index 54b5f8d46..76e4aeb4a 100644 --- a/Frameworks/Database/StatusesTable.swift +++ b/Frameworks/Database/StatusesTable.swift @@ -15,8 +15,6 @@ import Data // // CREATE TABLE if not EXISTS statuses (articleID TEXT NOT NULL PRIMARY KEY, read BOOL NOT NULL DEFAULT 0, starred BOOL NOT NULL DEFAULT 0, userDeleted BOOL NOT NULL DEFAULT 0, dateArrived DATE NOT NULL DEFAULT 0, accountInfo BLOB); -typealias StatusesCompletionBlock = ([String: ArticleStatus]) -> Void // [articleID: Status] - final class StatusesTable: DatabaseTable { let name = DatabaseTableName.statuses @@ -28,56 +26,26 @@ final class StatusesTable: DatabaseTable { self.queue = queue } - // MARK: Cache - -// func cachedStatus(for articleID: String) -> ArticleStatus? { -// -// assert(Thread.isMainThread) -// assert(cache[articleID] != nil) -// return cache[articleID] -// } -// -// func cachedStatuses(for articleIDs: Set) -> Set { -// -// assert(Thread.isMainThread) -// -// var statuses = Set() -// for articleID in articleIDs { -// if let articleStatus = cache[articleID] { -// statuses.insert(articleStatus) -// } -// } -// -// return statuses -// } - - // MARK: Creating/Updating - func ensureStatusesForArticleIDs(_ articleIDs: Set, _ completion: @escaping StatusesCompletionBlock) { - - // Adds them to the cache if not cached. - - assert(Thread.isMainThread) + func ensureStatusesForArticleIDs(_ articleIDs: Set, _ database: FMDatabase) -> [String: ArticleStatus] { // Check cache. let articleIDsMissingCachedStatus = articleIDsWithNoCachedStatus(articleIDs) if articleIDsMissingCachedStatus.isEmpty { - completion(statusesDictionary(articleIDs)) - return + return statusesDictionary(articleIDs) } // Check database. - fetchAndCacheStatusesForArticleIDs(articleIDsMissingCachedStatus) { + fetchAndCacheStatusesForArticleIDs(articleIDsMissingCachedStatus, database) - let articleIDsNeedingStatus = self.articleIDsWithNoCachedStatus(articleIDs) - if !articleIDsNeedingStatus.isEmpty { - // Create new statuses. - self.createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus) - } - - completion(self.statusesDictionary(articleIDs)) + let articleIDsNeedingStatus = self.articleIDsWithNoCachedStatus(articleIDs) + if !articleIDsNeedingStatus.isEmpty { + // Create new statuses. + self.createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus, database) } + + return statusesDictionary(articleIDs) } // MARK: Marking @@ -100,9 +68,10 @@ final class StatusesTable: DatabaseTable { if updatedStatuses.isEmpty { return } - + let articleIDs = updatedStatuses.articleIDs() + queue.update { (database) in - self.markArticleIDs(updatedStatuses.articleIDs(), statusKey, flag, database) + self.markArticleIDs(articleIDs, statusKey, flag, database) } } @@ -113,11 +82,17 @@ final class StatusesTable: DatabaseTable { guard let articleID = row.string(forColumn: DatabaseKey.articleID) else { return nil } + if let cachedStatus = cache[articleID] { + return cachedStatus + } + guard let dateArrived = row.date(forColumn: DatabaseKey.dateArrived) else { return nil } let articleStatus = ArticleStatus(articleID: articleID, dateArrived: dateArrived, row: row) + cache.addStatusIfNotCached(articleStatus) + return articleStatus } } @@ -130,13 +105,13 @@ private extension StatusesTable { func articleIDsWithNoCachedStatus(_ articleIDs: Set) -> Set { - assert(Thread.isMainThread) + assert(!Thread.isMainThread) return Set(articleIDs.filter { cache[$0] == nil }) } func statusesDictionary(_ articleIDs: Set) -> [String: ArticleStatus] { - assert(Thread.isMainThread) + assert(!Thread.isMainThread) var d = [String: ArticleStatus]() @@ -149,75 +124,31 @@ private extension StatusesTable { return d } - func addToCache(_ statuses: Set) { - - // Replacing any already cached statuses. - if statuses.isEmpty { - return - } - - if Thread.isMainThread { - self.cache.add(statuses) - } - else { - DispatchQueue.main.async { - self.cache.add(statuses) - } - } - } - - func addIfNotCached(_ statuses: Set) { - - if statuses.isEmpty { - return - } - - if Thread.isMainThread { - self.cache.addIfNotCached(statuses) - } - else { - DispatchQueue.main.async { - self.cache.addIfNotCached(statuses) - } - } - } - // MARK: Creating - func saveStatuses(_ statuses: Set) { + func saveStatuses(_ statuses: Set, _ database: FMDatabase) { - queue.update { (database) in - let statusArray = statuses.map { $0.databaseDictionary()! } - self.insertRows(statusArray, insertType: .orIgnore, in: database) - } + let statusArray = statuses.map { $0.databaseDictionary()! } + self.insertRows(statusArray, insertType: .orIgnore, in: database) } - func createAndSaveStatusesForArticleIDs(_ articleIDs: Set) { + func createAndSaveStatusesForArticleIDs(_ articleIDs: Set, _ database: FMDatabase) { - assert(Thread.isMainThread) - let now = Date() let statuses = Set(articleIDs.map { ArticleStatus(articleID: $0, dateArrived: now) }) cache.addIfNotCached(statuses) - saveStatuses(statuses) + saveStatuses(statuses, database) } - func fetchAndCacheStatusesForArticleIDs(_ articleIDs: Set, _ completion: @escaping RSVoidCompletionBlock) { + func fetchAndCacheStatusesForArticleIDs(_ articleIDs: Set, _ database: FMDatabase) { - queue.fetch { (database) in - guard let resultSet = self.selectRowsWhere(key: DatabaseKey.articleID, inValues: Array(articleIDs), in: database) else { - completion() - return - } - - let statuses = resultSet.mapToSet(self.statusWithRow) - - DispatchQueue.main.async { - self.cache.addIfNotCached(statuses) - completion() - } + guard let resultSet = self.selectRowsWhere(key: DatabaseKey.articleID, inValues: Array(articleIDs), in: database) else { + return } + + let statuses = resultSet.mapToSet(self.statusWithRow) + self.cache.addIfNotCached(statuses) } // MARK: Marking @@ -228,6 +159,8 @@ private extension StatusesTable { } } +// MARK: - + private final class StatusCache { // Serial database queue only. @@ -245,6 +178,11 @@ private final class StatusCache { } } + func addStatusIfNotCached(_ status: ArticleStatus) { + + addIfNotCached(Set([status])) + } + func addIfNotCached(_ statuses: Set) { // Does not replace already cached statuses. diff --git a/Frameworks/RSParser/OPML/RSOPMLParser.h b/Frameworks/RSParser/OPML/RSOPMLParser.h index 9f703be71..8db594b03 100755 --- a/Frameworks/RSParser/OPML/RSOPMLParser.h +++ b/Frameworks/RSParser/OPML/RSOPMLParser.h @@ -12,6 +12,12 @@ @class ParserData; @class RSOPMLDocument; +typedef void (^OPMLParserCallback)(RSOPMLDocument *opmlDocument, NSError *error); + +// Parses on background thread; calls back on main thread. +void RSParseOPML(ParserData *parserData, OPMLParserCallback callback); + + @interface RSOPMLParser: NSObject + (RSOPMLDocument *)parseOPMLWithParserData:(ParserData *)parserData error:(NSError **)error; diff --git a/Frameworks/RSParser/OPML/RSOPMLParser.m b/Frameworks/RSParser/OPML/RSOPMLParser.m index 8e1e94d76..12e94a542 100755 --- a/Frameworks/RSParser/OPML/RSOPMLParser.m +++ b/Frameworks/RSParser/OPML/RSOPMLParser.m @@ -24,6 +24,20 @@ @end +void RSParseOPML(ParserData *parserData, OPMLParserCallback callback) { + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + @autoreleasepool { + NSError *error = nil; + RSOPMLDocument *opmlDocument = [RSOPMLParser parseOPMLWithParserData:parserData error:&error]; + + dispatch_async(dispatch_get_main_queue(), ^{ + callback(opmlDocument, error); + }); + } + }); +} @implementation RSOPMLParser diff --git a/ToDo.opml b/ToDo.opml index bf5adfbf2..4d05c524e 100644 --- a/ToDo.opml +++ b/ToDo.opml @@ -6,12 +6,12 @@ --> ToDo Tue, 12 Sep 2017 20:15:17 GMT - 0,18,21,25,30,40,41,43,47,50,52,54,56,65,70 + 0,23,26,30,35,45,46,48,52,55,57,59,61,70,75 0 - 452 - 543 - 1275 - 1211 + 207 + 30 + 762 + 966 @@ -26,10 +26,17 @@ + + + + + + + + + - -