mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Continue adoptContinue converting ArticlesDatabase to async/await.
This commit is contained in:
@@ -765,7 +765,14 @@ public enum FetchType {
|
||||
}
|
||||
|
||||
public func fetchUnreadCountForStarredArticles(_ completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
database.fetchStarredAndUnreadCount(for: flattenedFeedIDs(), completion: completion)
|
||||
Task { @MainActor in
|
||||
do {
|
||||
let unreadCount = try await database.unreadCountForStarredArticlesForFeedIDs(flattenedFeedIDs())
|
||||
completion(.success(unreadCount))
|
||||
} catch {
|
||||
completion(.failure(error as! DatabaseError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchCountForStarredArticles() throws -> Int {
|
||||
@@ -820,14 +827,14 @@ public enum FetchType {
|
||||
// Used only by an On My Mac or iCloud account.
|
||||
precondition(Thread.isMainThread)
|
||||
precondition(type == .onMyMac || type == .cloudKit)
|
||||
|
||||
database.update(with: parsedItems, feedID: feedID, deleteOlder: deleteOlder) { updateArticlesResult in
|
||||
switch updateArticlesResult {
|
||||
case .success(let articleChanges):
|
||||
|
||||
Task { @MainActor in
|
||||
do {
|
||||
let articleChanges = try await database.update(with: parsedItems, feedID: feedID, deleteOlder: deleteOlder)
|
||||
self.sendNotificationAbout(articleChanges)
|
||||
completion(.success(articleChanges))
|
||||
case .failure(let databaseError):
|
||||
completion(.failure(databaseError))
|
||||
} catch {
|
||||
completion(.failure(error as! DatabaseError))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -841,13 +848,13 @@ public enum FetchType {
|
||||
return
|
||||
}
|
||||
|
||||
database.update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead) { updateArticlesResult in
|
||||
switch updateArticlesResult {
|
||||
case .success(let newAndUpdatedArticles):
|
||||
self.sendNotificationAbout(newAndUpdatedArticles)
|
||||
Task { @MainActor in
|
||||
do {
|
||||
let articleChanges = try await database.update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead)
|
||||
self.sendNotificationAbout(articleChanges)
|
||||
completion(nil)
|
||||
case .failure(let databaseError):
|
||||
completion(databaseError)
|
||||
} catch {
|
||||
completion(error as? DatabaseError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -975,13 +982,6 @@ public enum FetchType {
|
||||
}
|
||||
|
||||
// Delete the articles associated with the given set of articleIDs
|
||||
func delete(articleIDs: Set<String>, completion: DatabaseCompletionBlock? = nil) {
|
||||
guard !articleIDs.isEmpty else {
|
||||
completion?(nil)
|
||||
return
|
||||
}
|
||||
database.delete(articleIDs: articleIDs, completion: completion)
|
||||
}
|
||||
|
||||
/// Empty caches that can reasonably be emptied. Call when the app goes in the background, for instance.
|
||||
func emptyCaches() {
|
||||
@@ -1501,9 +1501,9 @@ private extension Account {
|
||||
fetchingAllUnreadCounts = true
|
||||
database.fetchAllUnreadCounts { result in
|
||||
Task { @MainActor in
|
||||
guard let unreadCountDictionary = try? result.get() else {
|
||||
return
|
||||
}
|
||||
guard let unreadCountDictionary = try? result.get() else {
|
||||
return
|
||||
}
|
||||
self.processUnreadCounts(unreadCountDictionary: unreadCountDictionary, feeds: self.flattenedFeeds())
|
||||
|
||||
self.fetchingAllUnreadCounts = false
|
||||
|
||||
@@ -25,10 +25,13 @@ public typealias SingleUnreadCountResult = Result<Int, DatabaseError>
|
||||
public typealias SingleUnreadCountCompletionBlock = (SingleUnreadCountResult) -> Void
|
||||
|
||||
public struct ArticleChanges {
|
||||
|
||||
public let newArticles: Set<Article>?
|
||||
public let updatedArticles: Set<Article>?
|
||||
public let deletedArticles: Set<Article>?
|
||||
|
||||
static let empty = ArticleChanges(newArticles: nil, updatedArticles: nil, deletedArticles: nil)
|
||||
|
||||
public init() {
|
||||
self.newArticles = Set<Article>()
|
||||
self.updatedArticles = Set<Article>()
|
||||
@@ -197,34 +200,28 @@ public typealias ArticleStatusesResultBlock = (ArticleStatusesResult) -> Void
|
||||
try await articlesTable.unreadCountForFeedIDsSince(feedIDs, since)
|
||||
}
|
||||
|
||||
public func fetchStarredAndUnreadCount(for feedIDs: Set<String>, completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
articlesTable.fetchStarredAndUnreadCount(feedIDs, completion)
|
||||
public func unreadCountForStarredArticlesForFeedIDs(_ feedIDs: Set<String>) async throws -> Int {
|
||||
try await articlesTable.unreadCountForStarredArticlesForFeedIDs(feedIDs)
|
||||
}
|
||||
|
||||
// MARK: - Saving, Updating, and Deleting Articles
|
||||
|
||||
/// Update articles and save new ones — for feed-based systems (local and iCloud).
|
||||
public func update(with parsedItems: Set<ParsedItem>, feedID: String, deleteOlder: Bool, completion: @escaping UpdateArticlesCompletionBlock) {
|
||||
public func update(with parsedItems: Set<ParsedItem>, feedID: String, deleteOlder: Bool) async throws -> ArticleChanges {
|
||||
precondition(retentionStyle == .feedBased)
|
||||
articlesTable.update(parsedItems, feedID, deleteOlder, completion)
|
||||
return try await articlesTable.update(parsedItems, feedID, deleteOlder)
|
||||
}
|
||||
|
||||
/// Update articles and save new ones — for sync systems (Feedbin, Feedly, etc.).
|
||||
public func update(feedIDsAndItems: [String: Set<ParsedItem>], defaultRead: Bool, completion: @escaping UpdateArticlesCompletionBlock) {
|
||||
public func update(feedIDsAndItems: [String: Set<ParsedItem>], defaultRead: Bool) async throws -> ArticleChanges {
|
||||
precondition(retentionStyle == .syncSystem)
|
||||
articlesTable.update(feedIDsAndItems, defaultRead, completion)
|
||||
return try await articlesTable.update(feedIDsAndItems, defaultRead)
|
||||
}
|
||||
|
||||
/// Delete articles
|
||||
public func deleteArticleIDs(_ articleIDs: Set<String>) async throws {
|
||||
try await articlesTable.deleteArticleIDs(articleIDs)
|
||||
}
|
||||
|
||||
/// Delete articles
|
||||
public func delete(articleIDs: Set<String>, completion: DatabaseCompletionBlock?) {
|
||||
articlesTable.delete(articleIDs: articleIDs, completion: completion)
|
||||
}
|
||||
|
||||
// MARK: - Status
|
||||
|
||||
/// Fetch the articleIDs of unread articles.
|
||||
|
||||
@@ -203,11 +203,10 @@ final class ArticlesTable: DatabaseTable {
|
||||
|
||||
// MARK: - Updating and Deleting
|
||||
|
||||
func update(_ parsedItems: Set<ParsedItem>, _ feedID: String, _ deleteOlder: Bool, _ completion: @escaping UpdateArticlesCompletionBlock) {
|
||||
func update(_ parsedItems: Set<ParsedItem>, _ feedID: String, _ deleteOlder: Bool) async throws -> ArticleChanges {
|
||||
precondition(retentionStyle == .feedBased)
|
||||
if parsedItems.isEmpty {
|
||||
callUpdateArticlesCompletionBlock(nil, nil, nil, completion)
|
||||
return
|
||||
return ArticleChanges.empty
|
||||
}
|
||||
|
||||
// 1. Ensure statuses for all the incoming articles.
|
||||
@@ -220,74 +219,74 @@ final class ArticlesTable: DatabaseTable {
|
||||
// 8. Delete Articles in database no longer present in the feed.
|
||||
// 9. Update search index.
|
||||
|
||||
self.queue.runInTransaction { (databaseResult) in
|
||||
|
||||
func makeDatabaseCalls(_ database: FMDatabase) {
|
||||
let articleIDs = parsedItems.articleIDs()
|
||||
|
||||
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, false, database) //1
|
||||
assert(statusesDictionary.count == articleIDs.count)
|
||||
|
||||
let incomingArticles = Article.articlesWithParsedItems(parsedItems, feedID, self.accountID, statusesDictionary) //2
|
||||
if incomingArticles.isEmpty {
|
||||
self.callUpdateArticlesCompletionBlock(nil, nil, nil, completion)
|
||||
return
|
||||
}
|
||||
|
||||
let fetchedArticles = self.fetchArticlesForFeedID(feedID, database) //4
|
||||
let fetchedArticlesDictionary = fetchedArticles.dictionary()
|
||||
|
||||
let newArticles = self.findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5
|
||||
let updatedArticles = self.findAndSaveUpdatedArticles(incomingArticles, fetchedArticlesDictionary, database) //6
|
||||
|
||||
// Articles to delete are 1) not starred and 2) older than 30 days and 3) no longer in feed.
|
||||
let articlesToDelete: Set<Article>
|
||||
if deleteOlder {
|
||||
let cutoffDate = Date().bySubtracting(days: 30)
|
||||
articlesToDelete = fetchedArticles.filter { (article) -> Bool in
|
||||
return !article.status.starred && article.status.dateArrived < cutoffDate && !articleIDs.contains(article.articleID)
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
self.queue.runInTransaction { (databaseResult) in
|
||||
|
||||
func updateArticlesInDatabase(_ database: FMDatabase) -> ArticleChanges {
|
||||
let articleIDs = parsedItems.articleIDs()
|
||||
|
||||
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, false, database) //1
|
||||
assert(statusesDictionary.count == articleIDs.count)
|
||||
|
||||
let incomingArticles = Article.articlesWithParsedItems(parsedItems, feedID, self.accountID, statusesDictionary) //2
|
||||
if incomingArticles.isEmpty {
|
||||
return ArticleChanges.empty
|
||||
}
|
||||
} else {
|
||||
articlesToDelete = Set<Article>()
|
||||
|
||||
let fetchedArticles = self.fetchArticlesForFeedID(feedID, database) //4
|
||||
let fetchedArticlesDictionary = fetchedArticles.dictionary()
|
||||
|
||||
let newArticles = self.findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5
|
||||
let updatedArticles = self.findAndSaveUpdatedArticles(incomingArticles, fetchedArticlesDictionary, database) //6
|
||||
|
||||
// Articles to delete are 1) not starred and 2) older than 30 days and 3) no longer in feed.
|
||||
let articlesToDelete: Set<Article>
|
||||
if deleteOlder {
|
||||
let cutoffDate = Date().bySubtracting(days: 30)
|
||||
articlesToDelete = fetchedArticles.filter { (article) -> Bool in
|
||||
return !article.status.starred && article.status.dateArrived < cutoffDate && !articleIDs.contains(article.articleID)
|
||||
}
|
||||
} else {
|
||||
articlesToDelete = Set<Article>()
|
||||
}
|
||||
|
||||
self.addArticlesToCache(newArticles)
|
||||
self.addArticlesToCache(updatedArticles)
|
||||
|
||||
// 8. Delete articles no longer in feed.
|
||||
let articleIDsToDelete = articlesToDelete.articleIDs()
|
||||
if !articleIDsToDelete.isEmpty {
|
||||
self.removeArticles(articleIDsToDelete, database)
|
||||
self.removeArticleIDsFromCache(articleIDsToDelete)
|
||||
}
|
||||
|
||||
// 9. Update search index.
|
||||
if let newArticles = newArticles {
|
||||
self.searchTable.indexNewArticles(newArticles, database)
|
||||
}
|
||||
if let updatedArticles = updatedArticles {
|
||||
self.searchTable.indexUpdatedArticles(updatedArticles, database)
|
||||
}
|
||||
|
||||
let articleChanges = ArticleChanges(newArticles: newArticles, updatedArticles: updatedArticles, deletedArticles: articlesToDelete)
|
||||
return articleChanges
|
||||
}
|
||||
|
||||
self.callUpdateArticlesCompletionBlock(newArticles, updatedArticles, articlesToDelete, completion) //7
|
||||
|
||||
self.addArticlesToCache(newArticles)
|
||||
self.addArticlesToCache(updatedArticles)
|
||||
|
||||
// 8. Delete articles no longer in feed.
|
||||
let articleIDsToDelete = articlesToDelete.articleIDs()
|
||||
if !articleIDsToDelete.isEmpty {
|
||||
self.removeArticles(articleIDsToDelete, database)
|
||||
self.removeArticleIDsFromCache(articleIDsToDelete)
|
||||
}
|
||||
|
||||
// 9. Update search index.
|
||||
if let newArticles = newArticles {
|
||||
self.searchTable.indexNewArticles(newArticles, database)
|
||||
}
|
||||
if let updatedArticles = updatedArticles {
|
||||
self.searchTable.indexUpdatedArticles(updatedArticles, database)
|
||||
}
|
||||
}
|
||||
|
||||
switch databaseResult {
|
||||
case .success(let database):
|
||||
makeDatabaseCalls(database)
|
||||
case .failure(let databaseError):
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(databaseError))
|
||||
|
||||
switch databaseResult {
|
||||
case .success(let database):
|
||||
let articleChanges = updateArticlesInDatabase(database)
|
||||
continuation.resume(returning: articleChanges)
|
||||
case .failure(let databaseError):
|
||||
continuation.resume(throwing: databaseError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(_ feedIDsAndItems: [String: Set<ParsedItem>], _ read: Bool, _ completion: @escaping UpdateArticlesCompletionBlock) {
|
||||
func update(_ feedIDsAndItems: [String: Set<ParsedItem>], _ read: Bool) async throws -> ArticleChanges {
|
||||
precondition(retentionStyle == .syncSystem)
|
||||
if feedIDsAndItems.isEmpty {
|
||||
callUpdateArticlesCompletionBlock(nil, nil, nil, completion)
|
||||
return
|
||||
return ArticleChanges.empty
|
||||
}
|
||||
|
||||
// 1. Ensure statuses for all the incoming articles.
|
||||
@@ -299,56 +298,56 @@ final class ArticlesTable: DatabaseTable {
|
||||
// 7. Call back with new and updated Articles.
|
||||
// 8. Update search index.
|
||||
|
||||
self.queue.runInTransaction { (databaseResult) in
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
self.queue.runInTransaction { (databaseResult) in
|
||||
|
||||
func makeDatabaseCalls(_ database: FMDatabase) {
|
||||
var articleIDs = Set<String>()
|
||||
for (_, parsedItems) in feedIDsAndItems {
|
||||
articleIDs.formUnion(parsedItems.articleIDs())
|
||||
func updateArticlesInDatabase(_ database: FMDatabase) -> ArticleChanges {
|
||||
var articleIDs = Set<String>()
|
||||
for (_, parsedItems) in feedIDsAndItems {
|
||||
articleIDs.formUnion(parsedItems.articleIDs())
|
||||
}
|
||||
|
||||
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1
|
||||
assert(statusesDictionary.count == articleIDs.count)
|
||||
|
||||
let allIncomingArticles = Article.articlesWithFeedIDsAndItems(feedIDsAndItems, self.accountID, statusesDictionary) //2
|
||||
if allIncomingArticles.isEmpty {
|
||||
return ArticleChanges.empty
|
||||
}
|
||||
|
||||
let incomingArticles = self.filterIncomingArticles(allIncomingArticles) //3
|
||||
if incomingArticles.isEmpty {
|
||||
return ArticleChanges.empty
|
||||
}
|
||||
|
||||
let incomingArticleIDs = incomingArticles.articleIDs()
|
||||
let fetchedArticles = self.fetchArticlesForArticleIDs(incomingArticleIDs, database) //4
|
||||
let fetchedArticlesDictionary = fetchedArticles.dictionary()
|
||||
|
||||
let newArticles = self.findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5
|
||||
let updatedArticles = self.findAndSaveUpdatedArticles(incomingArticles, fetchedArticlesDictionary, database) //6
|
||||
|
||||
self.addArticlesToCache(newArticles)
|
||||
self.addArticlesToCache(updatedArticles)
|
||||
|
||||
// 8. Update search index.
|
||||
if let newArticles = newArticles {
|
||||
self.searchTable.indexNewArticles(newArticles, database)
|
||||
}
|
||||
if let updatedArticles = updatedArticles {
|
||||
self.searchTable.indexUpdatedArticles(updatedArticles, database)
|
||||
}
|
||||
|
||||
let articleChanges = ArticleChanges(newArticles: newArticles, updatedArticles: updatedArticles, deletedArticles: nil)
|
||||
return articleChanges
|
||||
}
|
||||
|
||||
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1
|
||||
assert(statusesDictionary.count == articleIDs.count)
|
||||
|
||||
let allIncomingArticles = Article.articlesWithFeedIDsAndItems(feedIDsAndItems, self.accountID, statusesDictionary) //2
|
||||
if allIncomingArticles.isEmpty {
|
||||
self.callUpdateArticlesCompletionBlock(nil, nil, nil, completion)
|
||||
return
|
||||
}
|
||||
|
||||
let incomingArticles = self.filterIncomingArticles(allIncomingArticles) //3
|
||||
if incomingArticles.isEmpty {
|
||||
self.callUpdateArticlesCompletionBlock(nil, nil, nil, completion)
|
||||
return
|
||||
}
|
||||
|
||||
let incomingArticleIDs = incomingArticles.articleIDs()
|
||||
let fetchedArticles = self.fetchArticlesForArticleIDs(incomingArticleIDs, database) //4
|
||||
let fetchedArticlesDictionary = fetchedArticles.dictionary()
|
||||
|
||||
let newArticles = self.findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5
|
||||
let updatedArticles = self.findAndSaveUpdatedArticles(incomingArticles, fetchedArticlesDictionary, database) //6
|
||||
|
||||
self.callUpdateArticlesCompletionBlock(newArticles, updatedArticles, nil, completion) //7
|
||||
|
||||
self.addArticlesToCache(newArticles)
|
||||
self.addArticlesToCache(updatedArticles)
|
||||
|
||||
// 8. Update search index.
|
||||
if let newArticles = newArticles {
|
||||
self.searchTable.indexNewArticles(newArticles, database)
|
||||
}
|
||||
if let updatedArticles = updatedArticles {
|
||||
self.searchTable.indexUpdatedArticles(updatedArticles, database)
|
||||
}
|
||||
}
|
||||
|
||||
switch databaseResult {
|
||||
case .success(let database):
|
||||
makeDatabaseCalls(database)
|
||||
case .failure(let databaseError):
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(databaseError))
|
||||
switch databaseResult {
|
||||
case .success(let database):
|
||||
let articleChanges = updateArticlesInDatabase(database)
|
||||
continuation.resume(returning: articleChanges)
|
||||
case .failure(let databaseError):
|
||||
continuation.resume(throwing: databaseError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -356,44 +355,19 @@ final class ArticlesTable: DatabaseTable {
|
||||
|
||||
public func deleteArticleIDs(_ articleIDs: Set<String>) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
Task { @MainActor in
|
||||
queue.runInTransaction { databaseResult in
|
||||
switch databaseResult {
|
||||
case .success(let database):
|
||||
self.removeArticles(articleIDs, database)
|
||||
continuation.resume()
|
||||
case .failure(let databaseError):
|
||||
continuation.resume(throwing: databaseError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func delete(articleIDs: Set<String>, completion: DatabaseCompletionBlock?) {
|
||||
self.queue.runInTransaction { (databaseResult) in
|
||||
|
||||
func makeDatabaseCalls(_ database: FMDatabase) {
|
||||
self.removeArticles(articleIDs, database)
|
||||
DispatchQueue.main.async {
|
||||
completion?(nil)
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
queue.runInTransaction { databaseResult in
|
||||
switch databaseResult {
|
||||
case .success(let database):
|
||||
self.removeArticles(articleIDs, database)
|
||||
continuation.resume()
|
||||
case .failure(let databaseError):
|
||||
continuation.resume(throwing: databaseError)
|
||||
}
|
||||
}
|
||||
|
||||
switch databaseResult {
|
||||
case .success(let database):
|
||||
makeDatabaseCalls(database)
|
||||
case .failure(let databaseError):
|
||||
DispatchQueue.main.async {
|
||||
completion?(databaseError)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Unread Counts
|
||||
|
||||
func unreadCountForFeedID(_ feedID: String) async throws -> Int {
|
||||
@@ -491,6 +465,34 @@ final class ArticlesTable: DatabaseTable {
|
||||
}
|
||||
}
|
||||
|
||||
func unreadCountForStarredArticlesForFeedIDs(_ feedIDs: Set<String>) async throws -> Int {
|
||||
if feedIDs.isEmpty {
|
||||
return 0
|
||||
}
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
queue.runInDatabase { databaseResult in
|
||||
|
||||
func fetchUnreadCount(_ database: FMDatabase) -> Int {
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
|
||||
let sql = "select count(*) from articles natural join statuses where feedID in \(placeholders) and read=0 and starred=1;"
|
||||
let parameters = Array(feedIDs) as [Any]
|
||||
|
||||
let unreadCount = self.numberWithSQLAndParameters(sql, parameters, in: database)
|
||||
return unreadCount
|
||||
}
|
||||
|
||||
switch databaseResult {
|
||||
case .success(let database):
|
||||
let unreadCount = fetchUnreadCount(database)
|
||||
continuation.resume(returning: unreadCount)
|
||||
case .failure(let databaseError):
|
||||
continuation.resume(throwing: databaseError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchStarredAndUnreadCount(_ feedIDs: Set<String>, _ completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
if feedIDs.isEmpty {
|
||||
completion(.success(0))
|
||||
@@ -1073,15 +1075,6 @@ private extension ArticlesTable {
|
||||
return articles.filter{ articleIDs.contains($0.articleID) }
|
||||
}
|
||||
|
||||
// MARK: - Saving Parsed Items
|
||||
|
||||
func callUpdateArticlesCompletionBlock(_ newArticles: Set<Article>?, _ updatedArticles: Set<Article>?, _ deletedArticles: Set<Article>?, _ completion: @escaping UpdateArticlesCompletionBlock) {
|
||||
let articleChanges = ArticleChanges(newArticles: newArticles, updatedArticles: updatedArticles, deletedArticles: deletedArticles)
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(articleChanges))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Saving New Articles
|
||||
|
||||
func findNewArticles(_ incomingArticles: Set<Article>, _ fetchedArticlesDictionary: [String: Article]) -> Set<Article>? {
|
||||
|
||||
Reference in New Issue
Block a user