From a92492eb914993115751f5de80ce81e63b507bee Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Wed, 6 Sep 2017 13:33:04 -0700 Subject: [PATCH] Make further progress on saving articles from feeds. --- Frameworks/Database/ArticlesTable.swift | 87 ++++++++++-------- Frameworks/Database/Constants.swift | 23 ++++- Frameworks/Database/Database.swift | 1 + .../Extensions/Article+Database.swift | 2 +- ToDo.ooutline | Bin 2719 -> 2970 bytes 5 files changed, 71 insertions(+), 42 deletions(-) diff --git a/Frameworks/Database/ArticlesTable.swift b/Frameworks/Database/ArticlesTable.swift index c41d85b9a..183cfad0d 100644 --- a/Frameworks/Database/ArticlesTable.swift +++ b/Frameworks/Database/ArticlesTable.swift @@ -79,10 +79,10 @@ final class ArticlesTable: DatabaseTable { // MARK: Updating - func update(_ feed: Feed, _ parsedFeed: ParsedFeed, _ completion: @escaping RSVoidCompletionBlock) { + func update(_ feed: Feed, _ parsedFeed: ParsedFeed, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) { if parsedFeed.items.isEmpty { - completion() + completion(nil, nil) return } @@ -90,40 +90,51 @@ final class ArticlesTable: DatabaseTable { // 2. Ignore parsedItems that are userDeleted || (!starred and really old) // 3. Fetch all articles for the feed. // 4. Create Articles with parsedItems. - // 5. + // 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. + let feedID = feed.feedID let parsedItemArticleIDs = Set(parsedFeed.items.map { $0.databaseIdentifierWithFeed(feed) }) let parsedItemsDictionary = parsedFeed.itemsDictionary(with: feed) - statusesTable.ensureStatusesForArticleIDs(parsedItemArticleIDs) { + statusesTable.ensureStatusesForArticleIDs(parsedItemArticleIDs) { // 1 - let filteredParsedItems = self.filterParsedItems(parsedItemsDictionary) + let filteredParsedItems = self.filterParsedItems(parsedItemsDictionary) // 2 if filteredParsedItems.isEmpty { - completion() + completion(nil, nil) return } - queue.fetch{ (database) in - - let fetchedArticles = self.fetchArticlesForFeedID(feedID, withLimits: false, database: database) - - let incomingArticles = Article.articlesWithParsedItems(parsedFeed.items, accountID, feedID) + queue.update{ (database) in + let fetchedArticles = self.fetchArticlesForFeedID(feedID, withLimits: false, database: database) //3 + let fetchedArticlesDictionary = fetchedArticles.dictionary() + let incomingArticles = Article.articlesWithParsedItems(filteredParsedItems, accountID, feedID) //4 + let incomingArticlesDictionary = incomingArticles.dictionary() + let newArticles = Set(incomingArticles.filter { fetchedArticles[$0.articleID] == nil }) //5 + if !newArticles.isEmpty { + saveNewArticles(newArticles, database) + } + + let updatedArticles = incomingArticles.filter{ (incomingArticle) -> Bool in //6 + if let existingArticle = fetchedArticles[incomingArticle.articleID] { + if existingArticle != incomingArticle { + return true + } + } + return false + } + if !updatedArticles.isEmpty { + saveUpdatedArticles(Set(updatedArticles), fetchedArticlesDictionary, database) + } + + DispatchQueue.main.async { + completion(newArticles, updatedArticles) //7 + } } - - - } - - - // 3. For each parsedItem: - // - if userDeleted || (!starred && status.dateArrived < cutoff), then ignore - // - if matches existing article, then update database with changes between the two - // - if new, create article and save in database - - fetchArticlesAsync(feed, withLimits: false) { (articles) in - self.updateArticles(articles.dictionary(), parsedFeed.itemsDictionary(with: feed), feed, completion) } } @@ -182,21 +193,21 @@ private extension ArticlesTable { // MARK: Fetching - func attachRelatedObjects(_ articles: Set
, _ database: FMDatabase) { - - let articleArray = articles.map { $0 as DatabaseObject } - - authorsLookupTable.attachRelatedObjects(to: articleArray, in: database) - attachmentsLookupTable.attachRelatedObjects(to: articleArray, in: database) - tagsLookupTable.attachRelatedObjects(to: articleArray, in: database) - - // In theory, it’s impossible to have a fetched article without a status. - // Let’s handle that impossibility anyway. - // Remember that, if nothing else, the user can edit the SQLite database, - // and thus could delete all their statuses. - - statusesTable.ensureStatusesForArticles(articles, database) - } +// func attachRelatedObjects(_ articles: Set
, _ database: FMDatabase) { +// +// let articleArray = articles.map { $0 as DatabaseObject } +// +// authorsLookupTable.attachRelatedObjects(to: articleArray, in: database) +// attachmentsLookupTable.attachRelatedObjects(to: articleArray, in: database) +// tagsLookupTable.attachRelatedObjects(to: articleArray, in: database) +// +// // In theory, it’s impossible to have a fetched article without a status. +// // Let’s handle that impossibility anyway. +// // Remember that, if nothing else, the user can edit the SQLite database, +// // and thus could delete all their statuses. +// +// statusesTable.ensureStatusesForArticles(articles, database) +// } func articleWithRow(_ row: FMResultSet) -> Article? { diff --git a/Frameworks/Database/Constants.swift b/Frameworks/Database/Constants.swift index a85a78b4a..b3f81cd94 100644 --- a/Frameworks/Database/Constants.swift +++ b/Frameworks/Database/Constants.swift @@ -8,7 +8,24 @@ import Foundation -public struct DatabaseTableName { +// MARK: - Notifications + +public extension Notification.Name { + + public static let ArticlesDidSave = Notification.Name(rawValue: "ArticlesDidSave") +} + +public struct DatabaseNotificationKey { + + // userInfo keys (with Set
values) for ArticlesDidSave. + // One or both will be present. If present, may be empty. + static let newArticles = "newArticles" + static let updatedArticles = "updatedArticles" +} + +// MARK: - Database structure + +struct DatabaseTableName { static let articles = "articles" static let authors = "authors" @@ -19,7 +36,7 @@ public struct DatabaseTableName { static let attachmentsLookup = "attachmentsLookup" } -public struct DatabaseKey { +struct DatabaseKey { // Shared static let databaseID = "databaseID" @@ -65,7 +82,7 @@ public struct DatabaseKey { static let emailAddress = "emailAddress" } -public struct RelationshipName { +struct RelationshipName { static let authors = "authors" static let tags = "tags" diff --git a/Frameworks/Database/Database.swift b/Frameworks/Database/Database.swift index 3ea01febd..f06f61558 100644 --- a/Frameworks/Database/Database.swift +++ b/Frameworks/Database/Database.swift @@ -15,6 +15,7 @@ import Data public typealias ArticleResultBlock = (Set
) -> Void public typealias UnreadCountTable = [String: Int] // feedID: unreadCount public typealias UnreadCountCompletionBlock = (UnreadCountTable) -> Void //feedID: unreadCount +typealias UpdateArticlesWithFeedCompletionBlock = (Set
, Set
) -> Void public final class Database { diff --git a/Frameworks/Database/Extensions/Article+Database.swift b/Frameworks/Database/Extensions/Article+Database.swift index 9191f2cab..a237aa910 100644 --- a/Frameworks/Database/Extensions/Article+Database.swift +++ b/Frameworks/Database/Extensions/Article+Database.swift @@ -73,7 +73,7 @@ extension Article { static func articlesWithParsedItems(_ parsedItems: [ParsedItem], _ accountID: String, _ feedID: String) -> Set
{ - return parsedItems.map{ Article(parsedItem: $0, accountID: accountID, feedID: feedID) } + return Set(parsedItems.map{ Article(parsedItem: $0, accountID: accountID, feedID: feedID) }) } // MARK: Updating with ParsedItem diff --git a/ToDo.ooutline b/ToDo.ooutline index cdd6810350cfef1e84c98a8172211ac5adf5d38b..a095913db673d54d07deea48ca9ce084775c559d 100644 GIT binary patch literal 2970 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 delta 2666 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