Finish changes mandated by DatabaseQueue changes.

This commit is contained in:
Brent Simmons 2019-12-16 22:45:59 -08:00
parent 3c8097404f
commit 15184aa3f1
28 changed files with 291 additions and 215 deletions

View File

@ -64,6 +64,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
public static let updatedArticles = "updatedArticles" // AccountDidDownloadArticles
public static let statuses = "statuses" // StatusesDidChange
public static let articles = "articles" // StatusesDidChange
public static let articleIDs = "articleIDs" // StatusesDidChange
public static let webFeeds = "webFeeds" // AccountDidDownloadArticles, StatusesDidChange
}
@ -776,16 +777,41 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
database.ensureStatuses(articleIDs, defaultRead, statusKey, flag, completion: completion)
}
/// Update statuses  set a key and value. This updates the database, and sends a .StatusesDidChange notification.
func update(statuses: Set<ArticleStatus>, statusKey: ArticleStatus.Key, flag: Bool) {
// TODO: https://github.com/brentsimmons/NetNewsWire/issues/1420
/// Mark articleIDs statuses based on statusKey and flag.
/// Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
func mark(articleIDs: Set<String>, statusKey: ArticleStatus.Key, flag: Bool, completion: DatabaseCompletionBlock? = nil) {
guard !articleIDs.isEmpty else {
completion?(nil)
return
}
database.mark(articleIDs: articleIDs, statusKey: statusKey, flag: flag) { error in
if let error = error {
completion?(error)
return
}
self.noteStatusesForArticleIDsDidChange(articleIDs)
completion?(nil)
}
}
/// Update statuses specified by articleIDs  set a key and value.
/// This updates the database, and sends a .StatusesDidChange notification.
/// Any statuses that dont exist will be automatically created.
func mark(articleIDs: Set<String>, statusKey: ArticleStatus.Key, flag: Bool, completion: DatabaseCompletionBlock? = nil) {
// TODO
/// Mark articleIDs as read. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
func markAsRead(_ articleIDs: Set<String>) {
mark(articleIDs: articleIDs, statusKey: .read, flag: true)
}
/// Mark articleIDs as unread. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
func markAsUnread(_ articleIDs: Set<String>) {
mark(articleIDs: articleIDs, statusKey: .read, flag: false)
}
/// Mark articleIDs as starred. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
func markAsStarred(_ articleIDs: Set<String>) {
mark(articleIDs: articleIDs, statusKey: .starred, flag: true)
}
/// Mark articleIDs as unstarred. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
func markAsUnstarred(_ articleIDs: Set<String>) {
mark(articleIDs: articleIDs, statusKey: .starred, flag: false)
}
/// Fetch statuses for the specified articleIDs. The completion handler will get nil if the app is suspended.
@ -1158,14 +1184,20 @@ private extension Account {
func noteStatusesForArticlesDidChange(_ articles: Set<Article>) {
let feeds = Set(articles.compactMap { $0.webFeed })
let statuses = Set(articles.map { $0.status })
let articleIDs = Set(articles.map { $0.articleID })
// .UnreadCountDidChange notification will get sent to Folder and Account objects,
// which will update their own unread counts.
updateUnreadCounts(for: feeds)
NotificationCenter.default.post(name: .StatusesDidChange, object: self, userInfo: [UserInfoKey.statuses: statuses, UserInfoKey.articles: articles, UserInfoKey.webFeeds: feeds])
NotificationCenter.default.post(name: .StatusesDidChange, object: self, userInfo: [UserInfoKey.statuses: statuses, UserInfoKey.articles: articles, UserInfoKey.articleIDs: articleIDs, UserInfoKey.webFeeds: feeds])
}
func noteStatusesForArticleIDsDidChange(_ articleIDs: Set<String>) {
fetchAllUnreadCounts()
NotificationCenter.default.post(name: .StatusesDidChange, object: self, userInfo: [UserInfoKey.articleIDs: articleIDs])
}
func fetchAllUnreadCounts() {
fetchingAllUnreadCounts = true

View File

@ -8,13 +8,14 @@
import Foundation
import Articles
import ArticlesDatabase
public protocol ArticleFetcher {
func fetchArticles() -> Set<Article>
func fetchArticlesAsync(_ completion: @escaping ArticleSetBlock)
func fetchUnreadArticles() -> Set<Article>
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetBlock)
func fetchArticles() throws -> Set<Article>
func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock)
func fetchUnreadArticles() throws -> Set<Article>
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock)
}
extension WebFeed: ArticleFetcher {
@ -23,26 +24,33 @@ extension WebFeed: ArticleFetcher {
return try account?.fetchArticles(.webFeed(self)) ?? Set<Article>()
}
public func fetchArticlesAsync(_ completion: @escaping ArticleSetBlock) {
public func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
guard let account = account else {
assertionFailure("Expected feed.account, but got nil.")
completion(Set<Article>())
completion(.success(Set<Article>()))
return
}
account.fetchArticlesAsync(.webFeed(self), completion)
}
public func fetchUnreadArticles() -> Set<Article> {
return fetchArticles().unreadArticles()
public func fetchUnreadArticles() throws -> Set<Article> {
return try fetchArticles().unreadArticles()
}
public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetBlock) {
public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
guard let account = account else {
assertionFailure("Expected feed.account, but got nil.")
completion(Set<Article>())
completion(.success(Set<Article>()))
return
}
account.fetchArticlesAsync(.webFeed(self)) { completion($0.unreadArticles()) }
account.fetchArticlesAsync(.webFeed(self)) { articleSetResult in
switch articleSetResult {
case .success(let articles):
completion(.success(articles.unreadArticles()))
case .failure(let error):
completion(.failure(error))
}
}
}
}
@ -56,10 +64,10 @@ extension Folder: ArticleFetcher {
return try account.fetchArticles(.folder(self, false))
}
public func fetchArticlesAsync(_ completion: @escaping ArticleSetBlock) {
public func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
guard let account = account else {
assertionFailure("Expected folder.account, but got nil.")
completion(Set<Article>())
completion(.success(Set<Article>()))
return
}
account.fetchArticlesAsync(.folder(self, false), completion)
@ -73,10 +81,10 @@ extension Folder: ArticleFetcher {
return try account.fetchArticles(.folder(self, true))
}
public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetBlock) {
public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
guard let account = account else {
assertionFailure("Expected folder.account, but got nil.")
completion(Set<Article>())
completion(.success(Set<Article>()))
return
}
account.fetchArticlesAsync(.folder(self, true), completion)

View File

@ -165,10 +165,13 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
}
func refreshMissingArticles(for account: Account, completion: @escaping ((Result<Void, Error>)-> Void)) {
guard let fetchedArticleIDs = try? account.fetchArticleIDsForStatusesWithoutArticles() else {
return
}
os_log(.debug, log: log, "Refreshing missing articles...")
let group = DispatchGroup()
let fetchedArticleIDs = account.fetchArticleIDsForStatusesWithoutArticles()
let articleIDs = Array(fetchedArticleIDs)
let chunkedArticleIDs = articleIDs.chunked(into: 100)
@ -428,7 +431,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
}
}
return account.update(articles, statusKey: statusKey, flag: flag)
return try? account.update(articles, statusKey: statusKey, flag: flag)
}
func accountDidInitialize(_ account: Account) {
@ -495,7 +498,7 @@ private extension FeedWranglerAccountDelegate {
}
}
func syncFeedItems(_ account: Account, _ feedItems: [FeedWranglerFeedItem], completion: @escaping (() -> Void)) {
func syncFeedItems(_ account: Account, _ feedItems: [FeedWranglerFeedItem], completion: @escaping VoidCompletionBlock) {
let parsedItems = feedItems.map { (item: FeedWranglerFeedItem) -> ParsedItem in
let itemID = String(item.feedItemID)
// let authors = ...
@ -505,50 +508,35 @@ private extension FeedWranglerAccountDelegate {
}
let feedIDsAndItems = Dictionary(grouping: parsedItems, by: { $0.feedURL }).mapValues { Set($0) }
account.update(webFeedIDsAndItems: feedIDsAndItems, defaultRead: true, completion: completion)
account.update(webFeedIDsAndItems: feedIDsAndItems, defaultRead: true) { _ in
completion()
}
}
func syncArticleReadState(_ account: Account, _ unreadFeedItems: [FeedWranglerFeedItem]) {
let unreadServerItemIDs = Set(unreadFeedItems.map { String($0.feedItemID) })
account.fetchUnreadArticleIDs { unreadLocalItemIDs in
// unread if unread on server
let unreadDiffItemIDs = unreadServerItemIDs.subtracting(unreadLocalItemIDs)
let unreadFoundArticles = account.fetchArticles(.articleIDs(unreadDiffItemIDs))
account.update(unreadFoundArticles, statusKey: .read, flag: false)
let unreadFoundItemIDs = Set(unreadFoundArticles.map { $0.articleID })
let missingArticleIDs = unreadDiffItemIDs.subtracting(unreadFoundItemIDs)
account.ensureStatuses(missingArticleIDs, true, .read, false)
account.fetchUnreadArticleIDs { articleIDsResult in
guard let unreadLocalItemIDs = try? articleIDsResult.get() else {
return
}
account.markAsUnread(unreadServerItemIDs)
let readItemIDs = unreadLocalItemIDs.subtracting(unreadServerItemIDs)
let readArtices = account.fetchArticles(.articleIDs(readItemIDs))
account.update(readArtices, statusKey: .read, flag: true)
let foundReadArticleIDs = Set(readArtices.map { $0.articleID })
let readMissingIDs = readItemIDs.subtracting(foundReadArticleIDs)
account.ensureStatuses(readMissingIDs, true, .read, true)
account.markAsRead(readItemIDs)
}
}
func syncArticleStarredState(_ account: Account, _ unreadFeedItems: [FeedWranglerFeedItem]) {
let unreadServerItemIDs = Set(unreadFeedItems.map { String($0.feedItemID) })
account.fetchUnreadArticleIDs { unreadLocalItemIDs in
// starred if start on server
let unreadDiffItemIDs = unreadServerItemIDs.subtracting(unreadLocalItemIDs)
let unreadFoundArticles = account.fetchArticles(.articleIDs(unreadDiffItemIDs))
account.update(unreadFoundArticles, statusKey: .starred, flag: true)
func syncArticleStarredState(_ account: Account, _ starredFeedItems: [FeedWranglerFeedItem]) {
let starredServerItemIDs = Set(starredFeedItems.map { String($0.feedItemID) })
account.fetchStarredArticleIDs { articleIDsResult in
guard let starredLocalItemIDs = try? articleIDsResult.get() else {
return
}
let unreadFoundItemIDs = Set(unreadFoundArticles.map { $0.articleID })
let missingArticleIDs = unreadDiffItemIDs.subtracting(unreadFoundItemIDs)
account.ensureStatuses(missingArticleIDs, true, .starred, true)
account.markAsStarred(starredServerItemIDs)
let readItemIDs = unreadLocalItemIDs.subtracting(unreadServerItemIDs)
let readArtices = account.fetchArticles(.articleIDs(readItemIDs))
account.update(readArtices, statusKey: .starred, flag: false)
let foundReadArticleIDs = Set(readArtices.map { $0.articleID })
let readMissingIDs = readItemIDs.subtracting(foundReadArticleIDs)
account.ensureStatuses(readMissingIDs, true, .starred, false)
let unstarredItemIDs = starredLocalItemIDs.subtracting(starredServerItemIDs)
account.markAsUnstarred(unstarredItemIDs)
}
}

View File

@ -8,6 +8,7 @@
import Articles
import RSCore
import RSDatabase
import RSParser
import RSWeb
import SyncDatabase
@ -541,7 +542,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
}
return account.update(articles, statusKey: statusKey, flag: flag)
return try? account.update(articles, statusKey: statusKey, flag: flag)
}
func accountDidInitialize(_ account: Account) {
@ -1035,7 +1036,12 @@ private extension FeedbinAccountDelegate {
switch result {
case .success(let (entries, page)):
self.processEntries(account: account, entries: entries) {
self.processEntries(account: account, entries: entries) { error in
if let error = error {
completion(.failure(error))
return
}
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
@ -1080,7 +1086,7 @@ private extension FeedbinAccountDelegate {
}
func refreshArticles(_ account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
func refreshArticles(_ account: Account, completion: @escaping VoidResultCompletionBlock) {
os_log(.debug, log: log, "Refreshing articles...")
@ -1093,9 +1099,15 @@ private extension FeedbinAccountDelegate {
self.refreshProgress.addToNumberOfTasksAndRemaining(last - 1)
}
self.processEntries(account: account, entries: entries) {
self.processEntries(account: account, entries: entries) { error in
self.refreshProgress.completeTask()
if let error = error {
completion(.failure(error))
return
}
self.refreshArticles(account, page: page, updateFetchDate: updateFetchDate) { result in
os_log(.debug, log: self.log, "Done refreshing articles.")
switch result {
@ -1105,23 +1117,29 @@ private extension FeedbinAccountDelegate {
completion(.failure(error))
}
}
}
case .failure(let error):
completion(.failure(error))
}
}
}
func refreshMissingArticles(_ account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
os_log(.debug, log: log, "Refreshing missing articles...")
let group = DispatchGroup()
var errorOccurred = false
var fetchedArticleIDs = Set<String>()
do {
fetchedArticleIDs = try account.fetchArticleIDsForStatusesWithoutArticles()
}
catch(let error) {
self.refreshProgress.completeTask()
completion(.failure(error))
return
}
let fetchedArticleIDs = account.fetchArticleIDsForStatusesWithoutArticles()
let articleIDs = Array(fetchedArticleIDs)
let chunkedArticleIDs = articleIDs.chunked(into: 100)
@ -1132,7 +1150,7 @@ private extension FeedbinAccountDelegate {
switch result {
case .success(let entries):
self.processEntries(account: account, entries: entries) {
self.processEntries(account: account, entries: entries) { _ in
group.leave()
}
@ -1170,7 +1188,7 @@ private extension FeedbinAccountDelegate {
switch result {
case .success(let (entries, nextPage)):
self.processEntries(account: account, entries: entries) {
self.processEntries(account: account, entries: entries) { _ in
self.refreshProgress.completeTask()
self.refreshArticles(account, page: nextPage, updateFetchDate: updateFetchDate, completion: completion)
}
@ -1181,7 +1199,7 @@ private extension FeedbinAccountDelegate {
}
}
func processEntries(account: Account, entries: [FeedbinEntry]?, completion: @escaping (() -> Void)) {
func processEntries(account: Account, entries: [FeedbinEntry]?, completion: @escaping DatabaseCompletionBlock) {
let parsedItems = mapEntriesToParsedItems(entries: entries)
let webFeedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
account.update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: true, completion: completion)
@ -1207,26 +1225,18 @@ private extension FeedbinAccountDelegate {
}
let feedbinUnreadArticleIDs = Set(articleIDs.map { String($0) } )
account.fetchUnreadArticleIDs { currentUnreadArticleIDs in
account.fetchUnreadArticleIDs { articleIDsResult in
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
return
}
// Mark articles as unread
let deltaUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(currentUnreadArticleIDs)
let markUnreadArticles = account.fetchArticles(.articleIDs(deltaUnreadArticleIDs))
account.update(markUnreadArticles, statusKey: .read, flag: false)
// Save any unread statuses for articles we haven't yet received
let markUnreadArticleIDs = Set(markUnreadArticles.map { $0.articleID })
let missingUnreadArticleIDs = deltaUnreadArticleIDs.subtracting(markUnreadArticleIDs)
account.ensureStatuses(missingUnreadArticleIDs, true, .read, false)
account.markAsUnread(deltaUnreadArticleIDs)
// Mark articles as read
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(feedbinUnreadArticleIDs)
let markReadArticles = account.fetchArticles(.articleIDs(deltaReadArticleIDs))
account.update(markReadArticles, statusKey: .read, flag: true)
// Save any read statuses for articles we haven't yet received
let markReadArticleIDs = Set(markReadArticles.map { $0.articleID })
let missingReadArticleIDs = deltaReadArticleIDs.subtracting(markReadArticleIDs)
account.ensureStatuses(missingReadArticleIDs, true, .read, true)
account.markAsRead(deltaReadArticleIDs)
}
}
@ -1236,26 +1246,18 @@ private extension FeedbinAccountDelegate {
}
let feedbinStarredArticleIDs = Set(articleIDs.map { String($0) } )
account.fetchStarredArticleIDs { currentStarredArticleIDs in
account.fetchStarredArticleIDs { articleIDsResult in
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
return
}
// Mark articles as starred
let deltaStarredArticleIDs = feedbinStarredArticleIDs.subtracting(currentStarredArticleIDs)
let markStarredArticles = account.fetchArticles(.articleIDs(deltaStarredArticleIDs))
account.update(markStarredArticles, statusKey: .starred, flag: true)
// Save any starred statuses for articles we haven't yet received
let markStarredArticleIDs = Set(markStarredArticles.map { $0.articleID })
let missingStarredArticleIDs = deltaStarredArticleIDs.subtracting(markStarredArticleIDs)
account.ensureStatuses(missingStarredArticleIDs, true, .starred, true)
account.markAsStarred(deltaStarredArticleIDs)
// Mark articles as unstarred
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(feedbinStarredArticleIDs)
let markUnstarredArticles = account.fetchArticles(.articleIDs(deltaUnstarredArticleIDs))
account.update(markUnstarredArticles, statusKey: .starred, flag: false)
// Save any unstarred statuses for articles we haven't yet received
let markUnstarredArticleIDs = Set(markUnstarredArticles.map { $0.articleID })
let missingUnstarredArticleIDs = deltaUnstarredArticleIDs.subtracting(markUnstarredArticleIDs)
account.ensureStatuses(missingUnstarredArticleIDs, true, .starred, false)
account.markAsUnstarred(deltaUnstarredArticleIDs)
}
}

View File

@ -14,7 +14,7 @@ import SyncDatabase
import os.log
final class FeedlyAccountDelegate: AccountDelegate {
/// Feedly has a sandbox API and a production API.
/// This property is referred to when clients need to know which environment it should be pointing to.
/// The value of this proptery must match any `OAuthAuthorizationClient` used.
@ -476,7 +476,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
}
}
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) throws -> Set<Article>? {
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
let syncStatuses = articles.map { article in
return SyncStatus(articleID: article.articleID, key: statusKey, flag: flag)
@ -491,7 +491,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
}
}
return try account.update(articles, statusKey: statusKey, flag: flag)
return try? account.update(articles, statusKey: statusKey, flag: flag)
}
func accountDidInitialize(_ account: Account) {

View File

@ -52,13 +52,10 @@ private extension FeedlySetUnreadArticlesOperation {
}
let remoteUnreadArticleIDs = allUnreadIdsProvider.entryIds
account.markAsUnread(remoteUnreadArticleIDs)
// Mark articles as unread
account.mark(articleIDs: remoteUnreadArticleIDs, statusKey: .read, flag: false)
// Mark articles as read
let articleIDsToMarkRead = localUnreadArticleIDs.subtracting(remoteUnreadArticleIDs)
account.mark(articleIDs: articleIDsToMarkRead, statusKey: .read, flag: true)
account.markAsRead(articleIDsToMarkRead)
didFinish()
}

View File

@ -118,7 +118,7 @@ final class LocalAccountDelegate: AccountDelegate {
self.refreshProgress.completeTask()
if let parsedFeed = parsedFeed {
account.update(feed, with: parsedFeed, {})
account.update(feed, with: parsedFeed, {_ in})
}
feed.editedName = name
@ -187,7 +187,7 @@ final class LocalAccountDelegate: AccountDelegate {
}
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
return try account.update(articles, statusKey: statusKey, flag: flag)
return try? account.update(articles, statusKey: statusKey, flag: flag)
}
func accountDidInitialize(_ account: Account) {

View File

@ -84,12 +84,14 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
guard let account = feed.account, let parsedFeed = parsedFeed, error == nil else {
return
}
account.update(feed, with: parsedFeed) {
if let httpResponse = response as? HTTPURLResponse {
feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse)
account.update(feed, with: parsedFeed) { error in
if error == nil {
if let httpResponse = response as? HTTPURLResponse {
feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse)
}
feed.contentHash = dataHash
}
feed.contentHash = dataHash
completion()
}
}

View File

@ -417,7 +417,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
}
}
return account.update(articles, statusKey: statusKey, flag: flag)
return try? account.update(articles, statusKey: statusKey, flag: flag)
}
@ -836,12 +836,15 @@ private extension ReaderAPIAccountDelegate {
}
func refreshMissingArticles(_ account: Account, completion: @escaping (() -> Void)) {
func refreshMissingArticles(_ account: Account, completion: @escaping VoidCompletionBlock) {
guard let fetchedArticleIDs = try? account.fetchArticleIDsForStatusesWithoutArticles() else {
self.refreshProgress.completeTask()
return
}
os_log(.debug, log: log, "Refreshing missing articles...")
let group = DispatchGroup()
let fetchedArticleIDs = account.fetchArticleIDsForStatusesWithoutArticles()
let articleIDs = Array(fetchedArticleIDs)
let chunkedArticleIDs = articleIDs.chunked(into: 100)
@ -895,10 +898,12 @@ private extension ReaderAPIAccountDelegate {
}
func processEntries(account: Account, entries: [ReaderAPIEntry]?, completion: @escaping (() -> Void)) {
func processEntries(account: Account, entries: [ReaderAPIEntry]?, completion: @escaping VoidCompletionBlock) {
let parsedItems = mapEntriesToParsedItems(account: account, entries: entries)
let webFeedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
account.update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: true, completion: completion)
account.update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: true) { _ in
completion()
}
}
func mapEntriesToParsedItems(account: Account, entries: [ReaderAPIEntry]?) -> Set<ParsedItem> {
@ -924,26 +929,18 @@ private extension ReaderAPIAccountDelegate {
}
let feedbinUnreadArticleIDs = Set(articleIDs.map { String($0) } )
account.fetchUnreadArticleIDs { currentUnreadArticleIDs in
account.fetchUnreadArticleIDs { articleIDsResult in
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
return
}
// Mark articles as unread
let deltaUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(currentUnreadArticleIDs)
let markUnreadArticles = account.fetchArticles(.articleIDs(deltaUnreadArticleIDs))
account.update(markUnreadArticles, statusKey: .read, flag: false)
// Save any unread statuses for articles we haven't yet received
let markUnreadArticleIDs = Set(markUnreadArticles.map { $0.articleID })
let missingUnreadArticleIDs = deltaUnreadArticleIDs.subtracting(markUnreadArticleIDs)
account.ensureStatuses(missingUnreadArticleIDs, true, .read, false)
account.markAsUnread(deltaUnreadArticleIDs)
// Mark articles as read
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(feedbinUnreadArticleIDs)
let markReadArticles = account.fetchArticles(.articleIDs(deltaReadArticleIDs))
account.update(markReadArticles, statusKey: .read, flag: true)
// Save any read statuses for articles we haven't yet received
let markReadArticleIDs = Set(markReadArticles.map { $0.articleID })
let missingReadArticleIDs = deltaReadArticleIDs.subtracting(markReadArticleIDs)
account.ensureStatuses(missingReadArticleIDs, true, .read, true)
account.markAsRead(deltaReadArticleIDs)
}
}
@ -953,26 +950,18 @@ private extension ReaderAPIAccountDelegate {
}
let feedbinStarredArticleIDs = Set(articleIDs.map { String($0) } )
account.fetchStarredArticleIDs { currentStarredArticleIDs in
account.fetchStarredArticleIDs { articleIDsResult in
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
return
}
// Mark articles as starred
let deltaStarredArticleIDs = feedbinStarredArticleIDs.subtracting(currentStarredArticleIDs)
let markStarredArticles = account.fetchArticles(.articleIDs(deltaStarredArticleIDs))
account.update(markStarredArticles, statusKey: .starred, flag: true)
// Save any starred statuses for articles we haven't yet received
let markStarredArticleIDs = Set(markStarredArticles.map { $0.articleID })
let missingStarredArticleIDs = deltaStarredArticleIDs.subtracting(markStarredArticleIDs)
account.ensureStatuses(missingStarredArticleIDs, true, .starred, true)
account.markAsStarred(deltaStarredArticleIDs)
// Mark articles as unstarred
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(feedbinStarredArticleIDs)
let markUnstarredArticles = account.fetchArticles(.articleIDs(deltaUnstarredArticleIDs))
account.update(markUnstarredArticles, statusKey: .starred, flag: false)
// Save any unstarred statuses for articles we haven't yet received
let markUnstarredArticleIDs = Set(markUnstarredArticles.map { $0.articleID })
let missingUnstarredArticleIDs = deltaUnstarredArticleIDs.subtracting(markUnstarredArticleIDs)
account.ensureStatuses(missingUnstarredArticleIDs, true, .starred, false)
account.markAsUnstarred(deltaUnstarredArticleIDs)
}
}

View File

@ -8,6 +8,7 @@
import Foundation
import Articles
import ArticlesDatabase
public struct SingleArticleFetcher: ArticleFetcher {
@ -19,19 +20,19 @@ public struct SingleArticleFetcher: ArticleFetcher {
self.articleID = articleID
}
public func fetchArticles() -> Set<Article> {
return account.fetchArticles(.articleIDs(Set([articleID])))
public func fetchArticles() throws -> Set<Article> {
return try account.fetchArticles(.articleIDs(Set([articleID])))
}
public func fetchArticlesAsync(_ completion: @escaping ArticleSetBlock) {
public func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
return account.fetchArticlesAsync(.articleIDs(Set([articleID])), completion)
}
public func fetchUnreadArticles() -> Set<Article> {
return account.fetchArticles(.articleIDs(Set([articleID])))
public func fetchUnreadArticles() throws -> Set<Article> {
return try account.fetchArticles(.articleIDs(Set([articleID])))
}
public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetBlock) {
public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
return account.fetchArticlesAsync(.articleIDs(Set([articleID])), completion)
}

View File

@ -188,6 +188,10 @@ public final class ArticlesDatabase {
return try articlesTable.mark(articles, statusKey, flag)
}
public func mark(articleIDs: Set<String>, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping DatabaseCompletionBlock) {
articlesTable.mark(articleIDs, statusKey, flag, completion)
}
public func fetchStatuses(articleIDs: Set<String>, createIfNeeded: Bool, completion: @escaping ArticleStatusesResultBlock) {
articlesTable.fetchStatuses(articleIDs, createIfNeeded, completion)
}

View File

@ -457,6 +457,18 @@ final class ArticlesTable: DatabaseTable {
return statuses
}
func mark(_ articleIDs: Set<String>, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ completion: @escaping DatabaseCompletionBlock) {
queue.runInTransaction { databaseResult in
switch databaseResult {
case .success(let database):
self.statusesTable.mark(articleIDs, statusKey, flag, database)
completion(nil)
case .failure(let databaseError):
completion(databaseError)
}
}
}
// MARK: - Indexing
func indexUnindexedArticles() {

View File

@ -85,6 +85,12 @@ final class StatusesTable: DatabaseTable {
return updatedStatuses
}
func mark(_ articleIDs: Set<String>, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ database: FMDatabase) {
let statusesDictionary = ensureStatusesForArticleIDs(articleIDs, flag, database)
let statuses = Set(statusesDictionary.values)
mark(statuses, statusKey, flag, database)
}
// MARK: - Fetching
func fetchUnreadArticleIDs() throws -> Set<String> {

View File

@ -261,7 +261,9 @@ private extension SidebarViewController {
var articles = Set<Article>()
for object in objects {
if let articleFetcher = object as? ArticleFetcher {
articles.formUnion(articleFetcher.fetchUnreadArticles())
if let unreadArticles = try? articleFetcher.fetchUnreadArticles() {
articles.formUnion(unreadArticles)
}
}
}
return articles

View File

@ -254,12 +254,14 @@ private extension TimelineViewController {
}
func markAllAsReadMenuItem(_ feed: WebFeed) -> NSMenuItem? {
let articles = Array(feed.fetchArticles())
guard let articlesSet = try? feed.fetchArticles() else {
return nil
}
let articles = Array(articlesSet)
guard articles.canMarkAllAsRead() else {
return nil
}
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
let menuText = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String

View File

@ -450,10 +450,10 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
// MARK: - Notifications
@objc func statusesDidChange(_ note: Notification) {
guard let articles = note.userInfo?[Account.UserInfoKey.articles] as? Set<Article> else {
guard let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set<String> else {
return
}
reloadVisibleCells(for: articles)
reloadVisibleCells(for: articleIDs)
updateUnreadCount()
}
@ -1019,9 +1019,13 @@ private extension TimelineViewController {
var fetchedArticles = Set<Article>()
for articleFetcher in articleFetchers {
if articleReadFilterType != ReadFilterType.none {
fetchedArticles.formUnion(articleFetcher.fetchUnreadArticles())
if let articles = try? articleFetcher.fetchUnreadArticles() {
fetchedArticles.formUnion(articles)
}
} else {
fetchedArticles.formUnion(articleFetcher.fetchArticles())
if let articles = try? articleFetcher.fetchArticles() {
fetchedArticles.formUnion(articles)
}
}
}
return fetchedArticles

View File

@ -165,7 +165,7 @@ class ScriptableWebFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
@objc(articles)
var articles:NSArray {
let feedArticles = webFeed.fetchArticles()
let feedArticles = (try? webFeed.fetchArticles()) ?? Set<Article>()
// the articles are a set, use the sorting algorithm from the viewer
let sortedArticles = feedArticles.sorted(by:{
return $0.logicalDatePublished > $1.logicalDatePublished
@ -175,7 +175,7 @@ class ScriptableWebFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
@objc(valueInArticlesWithUniqueID:)
func valueInArticles(withUniqueID id:String) -> ScriptableArticle? {
let articles = webFeed.fetchArticles()
let articles = (try? webFeed.fetchArticles()) ?? Set<Article>()
guard let article = articles.first(where:{$0.uniqueID == id}) else { return nil }
return ScriptableArticle(article, container:self)
}

View File

@ -287,12 +287,12 @@ private extension ActivityManager {
static func identifers(for feed: WebFeed) -> [String] {
var ids = [String]()
ids.append(identifer(for: feed))
for article in feed.fetchArticles() {
ids.append(identifer(for: article))
if let articles = try? feed.fetchArticles() {
for article in articles {
ids.append(identifer(for: article))
}
}
return ids
}
}

View File

@ -10,6 +10,7 @@ import Foundation
import RSCore
import Account
import Articles
import ArticlesDatabase
struct SearchFeedDelegate: SmartFeedDelegate {
@ -31,7 +32,7 @@ struct SearchFeedDelegate: SmartFeedDelegate {
self.fetchType = .search(searchString)
}
func fetchUnreadCount(for: Account, completion: @escaping (Int) -> Void) {
func fetchUnreadCount(for: Account, completion: @escaping SingleUnreadCountCompletionBlock) {
// TODO: after 5.0
}
}

View File

@ -10,6 +10,7 @@ import Foundation
import RSCore
import Account
import Articles
import ArticlesDatabase
struct SearchTimelineFeedDelegate: SmartFeedDelegate {
@ -31,7 +32,7 @@ struct SearchTimelineFeedDelegate: SmartFeedDelegate {
self.fetchType = .searchWithArticleIDs(searchString, articleIDs)
}
func fetchUnreadCount(for: Account, completion: @escaping (Int) -> Void) {
func fetchUnreadCount(for: Account, completion: @escaping SingleUnreadCountCompletionBlock) {
// TODO: after 5.0
}
}

View File

@ -9,6 +9,7 @@
import Foundation
import RSCore
import Articles
import ArticlesDatabase
import Account
final class SmartFeed: PseudoFeed {
@ -80,19 +81,19 @@ final class SmartFeed: PseudoFeed {
extension SmartFeed: ArticleFetcher {
func fetchArticles() -> Set<Article> {
return delegate.fetchArticles()
func fetchArticles() throws -> Set<Article> {
return try delegate.fetchArticles()
}
func fetchArticlesAsync(_ completion: @escaping ArticleSetBlock) {
func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
delegate.fetchArticlesAsync(completion)
}
func fetchUnreadArticles() -> Set<Article> {
return delegate.fetchUnreadArticles()
func fetchUnreadArticles() throws -> Set<Article> {
return try delegate.fetchUnreadArticles()
}
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetBlock) {
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
delegate.fetchUnreadArticlesAsync(completion)
}
}
@ -104,7 +105,10 @@ private extension SmartFeed {
}
func fetchUnreadCount(for account: Account) {
delegate.fetchUnreadCount(for: account) { (accountUnreadCount) in
delegate.fetchUnreadCount(for: account) { singleUnreadCountResult in
guard let accountUnreadCount = try? singleUnreadCountResult.get() else {
return
}
self.unreadCounts[account.accountID] = accountUnreadCount
self.updateUnreadCount()
}

View File

@ -9,28 +9,36 @@
import Foundation
import Account
import Articles
import ArticlesDatabase
import RSCore
protocol SmartFeedDelegate: FeedIdentifiable, DisplayNameProvider, ArticleFetcher, SmallIconProvider {
var fetchType: FetchType { get }
func fetchUnreadCount(for: Account, completion: @escaping (Int) -> Void)
func fetchUnreadCount(for: Account, completion: @escaping SingleUnreadCountCompletionBlock)
}
extension SmartFeedDelegate {
func fetchArticles() -> Set<Article> {
return AccountManager.shared.fetchArticles(fetchType)
func fetchArticles() throws -> Set<Article> {
return try AccountManager.shared.fetchArticles(fetchType)
}
func fetchArticlesAsync(_ completion: @escaping ArticleSetBlock) {
func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
AccountManager.shared.fetchArticlesAsync(fetchType, completion)
}
func fetchUnreadArticles() -> Set<Article> {
return fetchArticles().unreadArticles()
func fetchUnreadArticles() throws -> Set<Article> {
return try fetchArticles().unreadArticles()
}
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetBlock) {
fetchArticlesAsync{ completion($0.unreadArticles()) }
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
fetchArticlesAsync{ articleSetResult in
switch articleSetResult {
case .success(let articles):
completion(.success(articles.unreadArticles()))
case .failure(let error):
completion(.failure(error))
}
}
}
}

View File

@ -9,6 +9,7 @@
import Foundation
import RSCore
import Articles
import ArticlesDatabase
import Account
// Main thread only.
@ -23,7 +24,7 @@ struct StarredFeedDelegate: SmartFeedDelegate {
let fetchType: FetchType = .starred
var smallIcon: IconImage? = AppAssets.starredFeedImage
func fetchUnreadCount(for account: Account, completion: @escaping (Int) -> Void) {
func fetchUnreadCount(for account: Account, completion: @escaping SingleUnreadCountCompletionBlock) {
account.fetchUnreadCountForStarredArticles(completion)
}
}

View File

@ -9,6 +9,7 @@
import Foundation
import RSCore
import Articles
import ArticlesDatabase
import Account
struct TodayFeedDelegate: SmartFeedDelegate {
@ -21,7 +22,7 @@ struct TodayFeedDelegate: SmartFeedDelegate {
let fetchType = FetchType.today
var smallIcon: IconImage? = AppAssets.todayFeedImage
func fetchUnreadCount(for account: Account, completion: @escaping (Int) -> Void) {
func fetchUnreadCount(for account: Account, completion: @escaping SingleUnreadCountCompletionBlock) {
account.fetchUnreadCountForToday(completion)
}
}

View File

@ -14,6 +14,7 @@ import Foundation
import RSCore
import Account
import Articles
import ArticlesDatabase
// This just shows the global unread count, which appDelegate already has. Easy.
@ -61,19 +62,19 @@ final class UnreadFeed: PseudoFeed {
extension UnreadFeed: ArticleFetcher {
func fetchArticles() -> Set<Article> {
return fetchUnreadArticles()
func fetchArticles() throws -> Set<Article> {
return try fetchUnreadArticles()
}
func fetchArticlesAsync(_ completion: @escaping ArticleSetBlock) {
func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
fetchUnreadArticlesAsync(completion)
}
func fetchUnreadArticles() -> Set<Article> {
return AccountManager.shared.fetchArticles(fetchType)
func fetchUnreadArticles() throws -> Set<Article> {
return try AccountManager.shared.fetchArticles(fetchType)
}
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetBlock) {
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
AccountManager.shared.fetchArticlesAsync(fetchType, completion)
}
}

View File

@ -63,7 +63,7 @@ final class FetchRequestOperation {
var fetchersReturned = 0
var fetchedArticles = Set<Article>()
func process(articles: Set<Article>) {
func process(_ articles: Set<Article>) {
precondition(Thread.isMainThread)
guard !self.isCanceled else {
callCompletionIfNeeded()
@ -83,17 +83,18 @@ final class FetchRequestOperation {
for articleFetcher in articleFetchers {
if readFilter {
articleFetcher.fetchUnreadArticlesAsync { (articles) in
process(articles: articles)
articleFetcher.fetchUnreadArticlesAsync { articleSetResult in
let articles = (try? articleSetResult.get()) ?? Set<Article>()
process(articles)
}
} else {
articleFetcher.fetchArticlesAsync { (articles) in
process(articles: articles)
}
else {
articleFetcher.fetchArticlesAsync { articleSetResult in
let articles = (try? articleSetResult.get()) ?? Set<Article>()
process(articles)
}
}
}
}
}

View File

@ -239,10 +239,13 @@ class ArticleViewController: UIViewController {
}
@objc func statusesDidChange(_ note: Notification) {
guard let articles = note.userInfo?[Account.UserInfoKey.articles] as? Set<Article> else {
guard let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set<String> else {
return
}
if articles.count == 1 && articles.first?.articleID == currentArticle?.articleID {
guard let currentArticle = currentArticle else {
return
}
if articleIDs.contains(currentArticle.articleID) {
updateUI()
}
}

View File

@ -340,12 +340,12 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
}
@objc func statusesDidChange(_ note: Notification) {
guard let updatedArticles = note.userInfo?[Account.UserInfoKey.articles] as? Set<Article> else {
guard let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set<String>, !articleIDs.isEmpty else {
return
}
let visibleArticles = tableView.indexPathsForVisibleRows!.compactMap { return dataSource.itemIdentifier(for: $0) }
let visibleUpdatedArticles = visibleArticles.filter { updatedArticles.contains($0) }
let visibleUpdatedArticles = visibleArticles.filter { articleIDs.contains($0.articleID) }
for article in visibleUpdatedArticles {
if let indexPath = dataSource.indexPath(for: article) {
@ -675,8 +675,11 @@ private extension MasterTimelineViewController {
func markAllInFeedAsReadAction(_ article: Article) -> UIAction? {
guard let webFeed = article.webFeed else { return nil }
guard let fetchedArticles = try? webFeed.fetchArticles() else {
return nil
}
let articles = Array(webFeed.fetchArticles())
let articles = Array(fetchedArticles)
guard articles.canMarkAllAsRead() else {
return nil
}
@ -692,8 +695,11 @@ private extension MasterTimelineViewController {
func markAllInFeedAsReadAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let webFeed = article.webFeed else { return nil }
let articles = Array(webFeed.fetchArticles())
guard let fetchedArticles = try? webFeed.fetchArticles() else {
return nil
}
let articles = Array(fetchedArticles)
guard articles.canMarkAllAsRead() else {
return nil
}