diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index dfaa183d0..42dd6ee20 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -543,8 +543,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, return database.fetchStarredArticleIDs() } - public func fetchArticleIDsForStatusesWithoutArticles() -> Set { - return database.fetchArticleIDsForStatusesWithoutArticles() + public func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate() -> Set { + return database.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate() } public func opmlDocument() -> String { @@ -593,33 +593,70 @@ 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. + precondition(Thread.isMainThread) + precondition(type == .onMyMac) // TODO: allow iCloud feed.takeSettings(from: parsedFeed) - let feedIDsAndItems = [feed.feedID: parsedFeed.items] - update(feedIDsAndItems: feedIDsAndItems, defaultRead: false, completion: completion) + let parsedItems = parsedFeed.items + guard !parsedItems.isEmpty else { + completion() + return + } + + database.update(with: parsedItems, feedID: feed.feedID) { articleChanges in + self.sendNotificationAbout(articleChanges) + completion() + } } func update(feedIDsAndItems: [String: Set], defaultRead: Bool, completion: @escaping (() -> Void)) { - assert(Thread.isMainThread) + // Used only by syncing systems. + precondition(Thread.isMainThread) + precondition(type != .onMyMac) // TODO: also make sure type != iCloud 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 - + database.update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead) { articleChanges in + self.sendNotificationAbout(articleChanges) completion() + } + } + func sendNotificationAbout(_ articleChanges: ArticleChanges) { + var webFeeds = Set() + + if let newArticles = articleChanges.newArticles { + webFeeds.formUnion(Set(newArticles.compactMap { $0.feed })) + } + if let updatedArticles = articleChanges.updatedArticles { + webFeeds.formUnion(Set(updatedArticles.compactMap { $0.feed })) + } + + var shouldSendNotification = false + var shouldUpdateUnreadCounts = false + var userInfo = [String: Any]() + + if let newArticles = articleChanges.newArticles, !newArticles.isEmpty { + shouldSendNotification = true + shouldUpdateUnreadCounts = true + userInfo[UserInfoKey.newArticles] = newArticles + } + + if let updatedArticles = articleChanges.updatedArticles, !updatedArticles.isEmpty { + shouldSendNotification = true + userInfo[UserInfoKey.updatedArticles] = updatedArticles + } + + if let deletedArticles = articleChanges.deletedArticles, !deletedArticles.isEmpty { + shouldUpdateUnreadCounts = true + } + + if shouldUpdateUnreadCounts { + self.updateUnreadCounts(for: webFeeds) + } + + if shouldSendNotification { + userInfo[UserInfoKey.feeds] = webFeeds NotificationCenter.default.post(name: .AccountDidDownloadArticles, object: self, userInfo: userInfo) } } diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index f09a50f98..9dbf9d539 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -996,7 +996,7 @@ private extension FeedbinAccountDelegate { os_log(.debug, log: log, "Refreshing missing articles...") let group = DispatchGroup() - let fetchedArticleIDs = account.fetchArticleIDsForStatusesWithoutArticles() + let fetchedArticleIDs = account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate() let articleIDs = Array(fetchedArticleIDs) let chunkedArticleIDs = articleIDs.chunked(into: 100) diff --git a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift index 0b1a3355e..fc5ee980e 100644 --- a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift +++ b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift @@ -139,8 +139,15 @@ public final class ArticlesDatabase { // MARK: - Saving and Updating Articles - /// Update articles and save new ones. The key for feedIDsAndItems is feedID. + /// Update articles and save new ones — for feed-based systems (local and iCloud). + public func update(with parsedItems: Set, feedID: String, completion: @escaping UpdateArticlesCompletionBlock) { + precondition(retentionStyle == .feedBased) + articlesTable.update(parsedItems, feedID, completion) + } + + /// Update articles and save new ones — for sync systems (Feedbin, Feedly, etc.). public func update(feedIDsAndItems: [String: Set], defaultRead: Bool, completion: @escaping UpdateArticlesCompletionBlock) { + precondition(retentionStyle == .syncSystem) articlesTable.update(feedIDsAndItems, defaultRead, completion) } @@ -158,8 +165,8 @@ public final class ArticlesDatabase { return articlesTable.fetchStarredArticleIDs() } - public func fetchArticleIDsForStatusesWithoutArticles() -> Set { - return articlesTable.fetchArticleIDsForStatusesWithoutArticles() + public func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate() -> Set { + return articlesTable.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate() } public func mark(_ articles: Set
, statusKey: ArticleStatus.Key, flag: Bool) -> Set? { diff --git a/Frameworks/ArticlesDatabase/ArticlesTable.swift b/Frameworks/ArticlesDatabase/ArticlesTable.swift index 98290d149..06a453ce9 100644 --- a/Frameworks/ArticlesDatabase/ArticlesTable.swift +++ b/Frameworks/ArticlesDatabase/ArticlesTable.swift @@ -393,8 +393,8 @@ final class ArticlesTable: DatabaseTable { return statusesTable.fetchStarredArticleIDs() } - func fetchArticleIDsForStatusesWithoutArticles() -> Set { - return statusesTable.fetchArticleIDsForStatusesWithoutArticles() + func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate() -> Set { + return statusesTable.fetchArticleIDsForStatusesWithoutArticlesNewerThan(articleCutoffDate) } func mark(_ articles: Set
, _ statusKey: ArticleStatus.Key, _ flag: Bool) -> Set? { diff --git a/Frameworks/ArticlesDatabase/StatusesTable.swift b/Frameworks/ArticlesDatabase/StatusesTable.swift index 4d207e224..2a44a9692 100644 --- a/Frameworks/ArticlesDatabase/StatusesTable.swift +++ b/Frameworks/ArticlesDatabase/StatusesTable.swift @@ -100,11 +100,18 @@ final class StatusesTable: DatabaseTable { func fetchStarredArticleIDs() -> Set { return fetchArticleIDs("select articleID from statuses where starred=1;") } - - func fetchArticleIDsForStatusesWithoutArticles() -> Set { - return fetchArticleIDs("select articleID from statuses s where (read=0 or starred=1) and not exists (select 1 from articles a where a.articleID = s.articleID);") + + func fetchArticleIDsForStatusesWithoutArticlesNewerThan(_ cutoffDate: Date) -> Set { + var articleIDs = Set() + queue.runInDatabaseSync { database in + let sql = "select articleID from statuses s where (starred=1 or dateArrived>?) and not exists (select 1 from articles a where a.articleID = s.articleID);" + if let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate]) { + articleIDs = resultSet.mapToSet(self.articleIDWithRow) + } + } + return articleIDs } - + func fetchArticleIDs(_ sql: String) -> Set { var articleIDs = Set() queue.runInDatabaseSync { (database) in diff --git a/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift b/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift index 0362803ae..83378d420 100644 --- a/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift +++ b/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift @@ -149,7 +149,6 @@ private extension ArticlePasteboardWriter { d[Key.externalURL] = article.externalURL ?? nil d[Key.summary] = article.summary ?? nil d[Key.imageURL] = article.imageURL ?? nil - d[Key.bannerImageURL] = article.bannerImageURL ?? nil d[Key.datePublished] = article.datePublished ?? nil d[Key.dateModified] = article.dateModified ?? nil d[Key.dateArrived] = article.status.dateArrived diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index b8e90f57f..d2346f9d1 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -549,7 +549,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr let longTitle = "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?" let prototypeID = "prototype" let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, dateArrived: Date()) - let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, feedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, bannerImageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status) + let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, feedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status) let prototypeCellData = TimelineCellData(article: prototypeArticle, showFeedName: showingFeedNames, feedName: "Prototype Feed Name", avatar: nil, showAvatar: false, featuredImage: nil) let height = TimelineCellLayout.height(for: 100, cellData: prototypeCellData, appearance: cellAppearance)