mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Continue adopting MainActor.
This commit is contained in:
@@ -61,7 +61,7 @@ public enum FetchType {
|
||||
case searchWithArticleIDs(String, Set<String>)
|
||||
}
|
||||
|
||||
public final class Account: DisplayNameProvider, UnreadCountProvider, Container, Hashable {
|
||||
@MainActor public final class Account: DisplayNameProvider, UnreadCountProvider, Container, Hashable {
|
||||
|
||||
public struct UserInfoKey {
|
||||
public static let account = "account" // UserDidAddAccount, UserDidDeleteAccount
|
||||
@@ -1017,13 +1017,13 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
|
||||
// MARK: - Hashable
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
nonisolated public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(accountID)
|
||||
}
|
||||
|
||||
// MARK: - Equatable
|
||||
|
||||
public class func ==(lhs: Account, rhs: Account) -> Bool {
|
||||
nonisolated public class func ==(lhs: Account, rhs: Account) -> Bool {
|
||||
return lhs === rhs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import Articles
|
||||
import RSWeb
|
||||
import Secrets
|
||||
|
||||
protocol AccountDelegate {
|
||||
@MainActor protocol AccountDelegate {
|
||||
|
||||
var behaviors: AccountBehaviors { get }
|
||||
|
||||
|
||||
@@ -9,30 +9,54 @@
|
||||
import Foundation
|
||||
import RSWeb
|
||||
|
||||
public struct WrappedAccountError: LocalizedError {
|
||||
|
||||
public let accountID: String
|
||||
public let underlyingError: Error
|
||||
public let isCredentialsError: Bool
|
||||
|
||||
public var errorTitle: String {
|
||||
NSLocalizedString("error.title.error", bundle: Bundle.module, comment: "Error")
|
||||
}
|
||||
|
||||
public var errorDescription: String? {
|
||||
if isCredentialsError {
|
||||
let localizedText = NSLocalizedString("error.message.credentials-expired.%@", bundle: Bundle.module, comment: "Your ”%@” credentials have expired.")
|
||||
return String(format: localizedText, accountNameForDisplay)
|
||||
}
|
||||
|
||||
let localizedText = NSLocalizedString("An error occurred while processing the “%@” account: %@", comment: "Unknown error")
|
||||
return String(format: localizedText, accountNameForDisplay, underlyingError.localizedDescription)
|
||||
}
|
||||
|
||||
public var recoverySuggestion: String? {
|
||||
if isCredentialsError {
|
||||
return NSLocalizedString("Please update your credentials for this account, or ensure that your account with this service is still valid.", comment: "Expired credentials")
|
||||
}
|
||||
return NSLocalizedString("Please try again later.", comment: "Try later")
|
||||
}
|
||||
|
||||
private let accountNameForDisplay: String
|
||||
|
||||
@MainActor init(account: Account, underlyingError: Error) {
|
||||
self.accountID = account.accountID
|
||||
self.underlyingError = underlyingError
|
||||
self.accountNameForDisplay = account.nameForDisplay
|
||||
|
||||
var isCredentialsError = false
|
||||
if case TransportError.httpError(let status) = underlyingError {
|
||||
isCredentialsError = (status == HTTPResponseCode.unauthorized || status == HTTPResponseCode.forbidden)
|
||||
}
|
||||
self.isCredentialsError = isCredentialsError
|
||||
}
|
||||
}
|
||||
|
||||
public enum AccountError: LocalizedError {
|
||||
|
||||
case createErrorNotFound
|
||||
case createErrorAlreadySubscribed
|
||||
case opmlImportInProgress
|
||||
case wrappedError(error: Error, account: Account)
|
||||
|
||||
public var account: Account? {
|
||||
if case .wrappedError(_, let account) = self {
|
||||
return account
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var isCredentialsError: Bool {
|
||||
if case .wrappedError(let error, _) = self {
|
||||
if case TransportError.httpError(let status) = error {
|
||||
return isCredentialsError(status: status)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
public var errorTitle: String {
|
||||
switch self {
|
||||
case .createErrorNotFound:
|
||||
@@ -41,8 +65,6 @@ public enum AccountError: LocalizedError {
|
||||
return NSLocalizedString("error.title.already-subscribed", bundle: Bundle.module, comment: "Already Subscribed")
|
||||
case .opmlImportInProgress:
|
||||
return NSLocalizedString("error.title.ompl-import-in-progress", bundle: Bundle.module, comment: "OPML Import in Progress")
|
||||
case .wrappedError(_, _):
|
||||
return NSLocalizedString("error.title.error", bundle: Bundle.module, comment: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,18 +76,6 @@ public enum AccountError: LocalizedError {
|
||||
return NSLocalizedString("error.message.feed-already-subscribed", bundle: Bundle.module, comment: "You are already subscribed to this feed and can’t add it again.")
|
||||
case .opmlImportInProgress:
|
||||
return NSLocalizedString("error.message.opml-import-in-progress", bundle: Bundle.module, comment: "An OPML import for this account is already running.")
|
||||
case .wrappedError(let error, let account):
|
||||
switch error {
|
||||
case TransportError.httpError(let status):
|
||||
if isCredentialsError(status: status) {
|
||||
let localizedText = NSLocalizedString("error.message.credentials-expired.%@", bundle: Bundle.module, comment: "Your ”%@” credentials have expired.")
|
||||
return String(format: localizedText, account.nameForDisplay)
|
||||
} else {
|
||||
return unknownError(error, account)
|
||||
}
|
||||
default:
|
||||
return unknownError(error, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,35 +85,8 @@ public enum AccountError: LocalizedError {
|
||||
return nil
|
||||
case .createErrorAlreadySubscribed:
|
||||
return nil
|
||||
case .wrappedError(let error, _):
|
||||
switch error {
|
||||
case TransportError.httpError(let status):
|
||||
if isCredentialsError(status: status) {
|
||||
return NSLocalizedString("Please update your credentials for this account, or ensure that your account with this service is still valid.", comment: "Expired credentials")
|
||||
} else {
|
||||
return NSLocalizedString("Please try again later.", comment: "Try later")
|
||||
}
|
||||
default:
|
||||
return NSLocalizedString("Please try again later.", comment: "Try later")
|
||||
}
|
||||
default:
|
||||
return NSLocalizedString("Please try again later.", comment: "Try later")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private extension AccountError {
|
||||
|
||||
func unknownError(_ error: Error, _ account: Account) -> String {
|
||||
let localizedText = NSLocalizedString("An error occurred while processing the “%@” account: %@", comment: "Unknown error")
|
||||
return NSString.localizedStringWithFormat(localizedText as NSString, account.nameForDisplay, error.localizedDescription) as String
|
||||
}
|
||||
|
||||
func isCredentialsError(status: Int) -> Bool {
|
||||
return status == 401 || status == 403
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import RSDatabase
|
||||
|
||||
// Main thread only.
|
||||
|
||||
public final class AccountManager: UnreadCountProvider {
|
||||
@MainActor public final class AccountManager: UnreadCountProvider {
|
||||
|
||||
public static var shared: AccountManager!
|
||||
public static let netNewsWireNewsURL = "https://netnewswire.blog/feed.xml"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import RSCore
|
||||
|
||||
final class AccountMetadataFile: Logging {
|
||||
@MainActor final class AccountMetadataFile: Logging {
|
||||
|
||||
private let fileURL: URL
|
||||
private let account: Account
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
import Articles
|
||||
import ArticlesDatabase
|
||||
|
||||
public protocol ArticleFetcher {
|
||||
@MainActor public protocol ArticleFetcher {
|
||||
|
||||
func fetchArticles() throws -> Set<Article>
|
||||
func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock)
|
||||
|
||||
@@ -88,7 +88,7 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
}
|
||||
|
||||
/// Persist a web feed record to iCloud and return the external key
|
||||
func createFeed(url: String, name: String?, editedName: String?, homePageURL: String?, container: Container, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
@MainActor func createFeed(url: String, name: String?, editedName: String?, homePageURL: String?, container: Container, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
let recordID = CKRecord.ID(recordName: url.md5String, zoneID: zoneID)
|
||||
let record = CKRecord(recordType: CloudKitFeed.recordType, recordID: recordID)
|
||||
record[CloudKitFeed.Fields.url] = url
|
||||
@@ -138,7 +138,7 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
}
|
||||
|
||||
/// Removes a web feed from a container and optionally deletes it, calling the completion with true if deleted
|
||||
func removeFeed(_ feed: Feed, from: Container, completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
@MainActor func removeFeed(_ feed: Feed, from: Container, completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
guard let fromContainerExternalID = from.externalID else {
|
||||
completion(.failure(CloudKitZoneError.corruptAccount))
|
||||
return
|
||||
@@ -187,7 +187,7 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
}
|
||||
}
|
||||
|
||||
func moveFeed(_ feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
@MainActor func moveFeed(_ feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let fromContainerExternalID = from.externalID, let toContainerExternalID = to.externalID else {
|
||||
completion(.failure(CloudKitZoneError.corruptAccount))
|
||||
return
|
||||
@@ -209,7 +209,7 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
}
|
||||
}
|
||||
|
||||
func addFeed(_ feed: Feed, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
@MainActor func addFeed(_ feed: Feed, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let toContainerExternalID = to.externalID else {
|
||||
completion(.failure(CloudKitZoneError.corruptAccount))
|
||||
return
|
||||
@@ -230,7 +230,7 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
}
|
||||
}
|
||||
|
||||
func findFeedExternalIDs(for folder: Folder, completion: @escaping (Result<[String], Error>) -> Void) {
|
||||
@MainActor func findFeedExternalIDs(for folder: Folder, completion: @escaping (Result<[String], Error>) -> Void) {
|
||||
guard let folderExternalID = folder.externalID else {
|
||||
completion(.failure(CloudKitAccountZoneError.unknown))
|
||||
return
|
||||
@@ -292,7 +292,7 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
createContainer(name: name, isAccount: false, completion: completion)
|
||||
}
|
||||
|
||||
func renameFolder(_ folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
@MainActor func renameFolder(_ folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let externalID = folder.externalID else {
|
||||
completion(.failure(CloudKitZoneError.corruptAccount))
|
||||
return
|
||||
@@ -312,7 +312,7 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
}
|
||||
}
|
||||
|
||||
func removeFolder(_ folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
@MainActor func removeFolder(_ folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delete(externalID: folder.externalID, completion: completion)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
self.articlesZone = articlesZone
|
||||
}
|
||||
|
||||
func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
@MainActor func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
for deletedRecordKey in deleted {
|
||||
switch deletedRecordKey.recordType {
|
||||
case CloudKitAccountZone.CloudKitFeed.recordType:
|
||||
@@ -57,7 +57,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func addOrUpdateFeed(_ record: CKRecord) {
|
||||
@MainActor func addOrUpdateFeed(_ record: CKRecord) {
|
||||
guard let account = account,
|
||||
let urlString = record[CloudKitAccountZone.CloudKitFeed.Fields.url] as? String,
|
||||
let containerExternalIDs = record[CloudKitAccountZone.CloudKitFeed.Fields.containerExternalIDs] as? [String],
|
||||
@@ -82,7 +82,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func removeFeed(_ externalID: String) {
|
||||
@MainActor func removeFeed(_ externalID: String) {
|
||||
if let feed = account?.existingFeed(withExternalID: externalID), let containers = account?.existingContainers(withFeed: feed) {
|
||||
containers.forEach {
|
||||
feed.dropConditionalGetInfo()
|
||||
@@ -91,7 +91,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func addOrUpdateContainer(_ record: CKRecord) {
|
||||
@MainActor func addOrUpdateContainer(_ record: CKRecord) {
|
||||
guard let account = account,
|
||||
let name = record[CloudKitAccountZone.CloudKitContainer.Fields.name] as? String,
|
||||
let isAccount = record[CloudKitAccountZone.CloudKitContainer.Fields.isAccount] as? String,
|
||||
@@ -130,7 +130,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func removeContainer(_ externalID: String) {
|
||||
@MainActor func removeContainer(_ externalID: String) {
|
||||
if let folder = account?.existingFolder(withExternalID: externalID) {
|
||||
account?.removeFolder(folder)
|
||||
}
|
||||
@@ -140,7 +140,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
|
||||
private extension CloudKitAcountZoneDelegate {
|
||||
|
||||
func updateFeed(_ feed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) {
|
||||
@MainActor func updateFeed(_ feed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) {
|
||||
guard let account = account else { return }
|
||||
|
||||
feed.name = name
|
||||
@@ -168,7 +168,7 @@ private extension CloudKitAcountZoneDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func createFeedIfNecessary(url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String, container: Container) {
|
||||
@MainActor func createFeedIfNecessary(url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String, container: Container) {
|
||||
guard let account = account else { return }
|
||||
|
||||
if account.existingFeed(withExternalID: feedExternalID) != nil {
|
||||
|
||||
@@ -62,19 +62,22 @@ final class CloudKitArticlesZone: CloudKitZone {
|
||||
migrateChangeToken()
|
||||
}
|
||||
|
||||
func saveNewArticles(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
guard !articles.isEmpty else {
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
var records = [CKRecord]()
|
||||
|
||||
let saveArticles = articles.filter { $0.status.read == false || $0.status.starred == true }
|
||||
for saveArticle in saveArticles {
|
||||
records.append(makeStatusRecord(saveArticle))
|
||||
records.append(makeArticleRecord(saveArticle))
|
||||
}
|
||||
@MainActor func saveNewArticles(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
guard !articles.isEmpty else {
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
let records: [CKRecord] = {
|
||||
var recordsAccumulator = [CKRecord]()
|
||||
|
||||
let saveArticles = articles.filter { $0.status.read == false || $0.status.starred == true }
|
||||
for saveArticle in saveArticles {
|
||||
recordsAccumulator.append(makeStatusRecord(saveArticle))
|
||||
recordsAccumulator.append(makeArticleRecord(saveArticle))
|
||||
}
|
||||
return recordsAccumulator
|
||||
}()
|
||||
|
||||
compressionQueue.async {
|
||||
let compressedRecords = self.compressArticleRecords(records)
|
||||
@@ -88,7 +91,7 @@ final class CloudKitArticlesZone: CloudKitZone {
|
||||
delete(ckQuery: ckQuery, completion: completion)
|
||||
}
|
||||
|
||||
func modifyArticles(_ statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
@MainActor func modifyArticles(_ statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
guard !statusUpdates.isEmpty else {
|
||||
completion(.success(()))
|
||||
return
|
||||
@@ -114,12 +117,16 @@ final class CloudKitArticlesZone: CloudKitZone {
|
||||
}
|
||||
}
|
||||
|
||||
let modifyRecordsCopy = modifyRecords
|
||||
let newRecordsCopy = newRecords
|
||||
let deleteRecordIDsCopy = deleteRecordIDs
|
||||
|
||||
compressionQueue.async {
|
||||
let compressedModifyRecords = self.compressArticleRecords(modifyRecords)
|
||||
self.modify(recordsToSave: compressedModifyRecords, recordIDsToDelete: deleteRecordIDs) { result in
|
||||
let compressedModifyRecords = self.compressArticleRecords(modifyRecordsCopy)
|
||||
self.modify(recordsToSave: compressedModifyRecords, recordIDsToDelete: deleteRecordIDsCopy) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
let compressedNewRecords = self.compressArticleRecords(newRecords)
|
||||
let compressedNewRecords = self.compressArticleRecords(newRecordsCopy)
|
||||
self.saveIfNew(compressedNewRecords) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -143,12 +150,14 @@ private extension CloudKitArticlesZone {
|
||||
func handleModifyArticlesError(_ error: Error, statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
if case CloudKitZoneError.userDeletedZone = error {
|
||||
self.createZoneRecord() { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.modifyArticles(statusUpdates, completion: completion)
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
Task { @MainActor in
|
||||
switch result {
|
||||
case .success:
|
||||
self.modifyArticles(statusUpdates, completion: completion)
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completion(.failure(error))
|
||||
@@ -163,7 +172,7 @@ private extension CloudKitArticlesZone {
|
||||
return "a|\(id)"
|
||||
}
|
||||
|
||||
func makeStatusRecord(_ article: Article) -> CKRecord {
|
||||
@MainActor func makeStatusRecord(_ article: Article) -> CKRecord {
|
||||
let recordID = CKRecord.ID(recordName: statusID(article.articleID), zoneID: zoneID)
|
||||
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
|
||||
if let feedExternalID = article.feed?.externalID {
|
||||
@@ -174,7 +183,7 @@ private extension CloudKitArticlesZone {
|
||||
return record
|
||||
}
|
||||
|
||||
func makeStatusRecord(_ statusUpdate: CloudKitArticleStatusUpdate) -> CKRecord {
|
||||
@MainActor func makeStatusRecord(_ statusUpdate: CloudKitArticleStatusUpdate) -> CKRecord {
|
||||
let recordID = CKRecord.ID(recordName: statusID(statusUpdate.articleID), zoneID: zoneID)
|
||||
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
|
||||
|
||||
@@ -188,7 +197,7 @@ private extension CloudKitArticlesZone {
|
||||
return record
|
||||
}
|
||||
|
||||
func makeArticleRecord(_ article: Article) -> CKRecord {
|
||||
@MainActor func makeArticleRecord(_ article: Article) -> CKRecord {
|
||||
let recordID = CKRecord.ID(recordName: articleID(article.articleID), zoneID: zoneID)
|
||||
let record = CKRecord(recordType: CloudKitArticle.recordType, recordID: recordID)
|
||||
|
||||
|
||||
@@ -29,38 +29,37 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging {
|
||||
}
|
||||
|
||||
func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
database.selectPendingReadStatusArticleIDs() { result in
|
||||
switch result {
|
||||
case .success(let pendingReadStatusArticleIDs):
|
||||
|
||||
self.database.selectPendingStarredStatusArticleIDs() { result in
|
||||
switch result {
|
||||
case .success(let pendingStarredStatusArticleIDs):
|
||||
self.delete(recordKeys: deleted, pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs) { error in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
} else {
|
||||
self.update(records: updated,
|
||||
pendingReadStatusArticleIDs: pendingReadStatusArticleIDs,
|
||||
pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs,
|
||||
completion: completion)
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
database.selectPendingReadStatusArticleIDs() { result in
|
||||
switch result {
|
||||
case .success(let pendingReadStatusArticleIDs):
|
||||
|
||||
self.database.selectPendingStarredStatusArticleIDs() { result in
|
||||
switch result {
|
||||
case .success(let pendingStarredStatusArticleIDs):
|
||||
self.delete(recordKeys: deleted, pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs) { error in
|
||||
Task { @MainActor in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
} else {
|
||||
self.update(records: updated,
|
||||
pendingReadStatusArticleIDs: pendingReadStatusArticleIDs,
|
||||
pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs,
|
||||
completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
self.logger.error("Error occurred getting pending starred records: \(error.localizedDescription, privacy: .public)")
|
||||
completion(.failure(CloudKitZoneError.unknown))
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(CloudKitZoneError.unknown))
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
self.logger.error("Error occurred getting pending read status records: \(error.localizedDescription, privacy: .public)")
|
||||
completion(.failure(CloudKitZoneError.unknown))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
completion(.failure(CloudKitZoneError.unknown))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CloudKitArticlesZoneDelegate {
|
||||
@@ -76,21 +75,23 @@ private extension CloudKitArticlesZoneDelegate {
|
||||
}
|
||||
|
||||
database.deleteSelectedForProcessing(Array(deletableArticleIDs)) { databaseError in
|
||||
if let databaseError = databaseError {
|
||||
completion(databaseError)
|
||||
} else {
|
||||
self.account?.delete(articleIDs: deletableArticleIDs) { databaseError in
|
||||
if let databaseError = databaseError {
|
||||
completion(databaseError)
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
Task { @MainActor in
|
||||
if let databaseError = databaseError {
|
||||
completion(databaseError)
|
||||
} else {
|
||||
self.account?.delete(articleIDs: deletableArticleIDs) { databaseError in
|
||||
if let databaseError = databaseError {
|
||||
completion(databaseError)
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(records: [CKRecord], pendingReadStatusArticleIDs: Set<String>, pendingStarredStatusArticleIDs: Set<String>, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
@MainActor func update(records: [CKRecord], pendingReadStatusArticleIDs: Set<String>, pendingStarredStatusArticleIDs: Set<String>, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
let receivedUnreadArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.read] == "0" }).map({ stripPrefix($0.externalID) }))
|
||||
let receivedReadArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.read] == "1" }).map({ stripPrefix($0.externalID) }))
|
||||
|
||||
@@ -71,7 +71,7 @@ private extension CloudKitSendStatusOperation {
|
||||
switch result {
|
||||
case .success(let syncStatuses):
|
||||
|
||||
func stopProcessing() {
|
||||
@MainActor func stopProcessing() {
|
||||
if self.showProgress {
|
||||
self.refreshProgress?.completeTask()
|
||||
}
|
||||
@@ -108,7 +108,7 @@ private extension CloudKitSendStatusOperation {
|
||||
let articleIDs = syncStatuses.map({ $0.articleID })
|
||||
account.fetchArticlesAsync(.articleIDs(Set(articleIDs))) { result in
|
||||
|
||||
func processWithArticles(_ articles: Set<Article>) {
|
||||
@MainActor func processWithArticles(_ articles: Set<Article>) {
|
||||
|
||||
let syncStatusesDict = Dictionary(grouping: syncStatuses, by: { $0.articleID })
|
||||
let articlesDict = articles.reduce(into: [String: Article]()) { result, article in
|
||||
@@ -118,7 +118,7 @@ private extension CloudKitSendStatusOperation {
|
||||
return CloudKitArticleStatusUpdate(articleID: key, statuses: value, article: articlesDict[key])
|
||||
}
|
||||
|
||||
func done(_ stop: Bool) {
|
||||
func done(_ stop: Bool) {
|
||||
// Don't clear the last one since we might have had additional ticks added
|
||||
if self.showProgress && self.refreshProgress?.numberRemaining ?? 0 > 1 {
|
||||
self.refreshProgress?.completeTask()
|
||||
|
||||
@@ -18,35 +18,35 @@ extension Notification.Name {
|
||||
|
||||
public protocol Container: AnyObject, ContainerIdentifiable {
|
||||
|
||||
var account: Account? { get }
|
||||
var topLevelFeeds: Set<Feed> { get set }
|
||||
var folders: Set<Folder>? { get set }
|
||||
var externalID: String? { get set }
|
||||
@MainActor var account: Account? { get }
|
||||
@MainActor var topLevelFeeds: Set<Feed> { get set }
|
||||
@MainActor var folders: Set<Folder>? { get set }
|
||||
@MainActor var externalID: String? { get set }
|
||||
|
||||
func hasAtLeastOneFeed() -> Bool
|
||||
func objectIsChild(_ object: AnyObject) -> Bool
|
||||
@MainActor func hasAtLeastOneFeed() -> Bool
|
||||
@MainActor func objectIsChild(_ object: AnyObject) -> Bool
|
||||
|
||||
func hasChildFolder(with: String) -> Bool
|
||||
func childFolder(with: String) -> Folder?
|
||||
@MainActor func hasChildFolder(with: String) -> Bool
|
||||
@MainActor func childFolder(with: String) -> Folder?
|
||||
|
||||
func removeFeed(_ feed: Feed)
|
||||
func addFeed(_ feed: Feed)
|
||||
@MainActor func removeFeed(_ feed: Feed)
|
||||
@MainActor func addFeed(_ feed: Feed)
|
||||
|
||||
//Recursive — checks subfolders
|
||||
func flattenedFeeds() -> Set<Feed>
|
||||
func has(_ feed: Feed) -> Bool
|
||||
func hasFeed(with feedID: String) -> Bool
|
||||
func hasFeed(withURL url: String) -> Bool
|
||||
func existingFeed(withFeedID: String) -> Feed?
|
||||
func existingFeed(withURL url: String) -> Feed?
|
||||
func existingFeed(withExternalID externalID: String) -> Feed?
|
||||
func existingFolder(with name: String) -> Folder?
|
||||
func existingFolder(withID: Int) -> Folder?
|
||||
@MainActor func flattenedFeeds() -> Set<Feed>
|
||||
@MainActor func has(_ feed: Feed) -> Bool
|
||||
@MainActor func hasFeed(with feedID: String) -> Bool
|
||||
@MainActor func hasFeed(withURL url: String) -> Bool
|
||||
@MainActor func existingFeed(withFeedID: String) -> Feed?
|
||||
@MainActor func existingFeed(withURL url: String) -> Feed?
|
||||
@MainActor func existingFeed(withExternalID externalID: String) -> Feed?
|
||||
@MainActor func existingFolder(with name: String) -> Folder?
|
||||
@MainActor func existingFolder(withID: Int) -> Folder?
|
||||
|
||||
func postChildrenDidChangeNotification()
|
||||
@MainActor func postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
public extension Container {
|
||||
@MainActor public extension Container {
|
||||
|
||||
func hasAtLeastOneFeed() -> Bool {
|
||||
return topLevelFeeds.count > 0
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
import Foundation
|
||||
|
||||
public protocol ContainerIdentifiable {
|
||||
var containerID: ContainerIdentifier? { get }
|
||||
@MainActor var containerID: ContainerIdentifier? { get }
|
||||
}
|
||||
|
||||
public enum ContainerIdentifier: Hashable, Equatable {
|
||||
@MainActor public enum ContainerIdentifier: Hashable, Equatable {
|
||||
case smartFeedController
|
||||
case account(String) // accountID
|
||||
case folder(String, String) // accountID, folderName
|
||||
|
||||
@@ -12,7 +12,7 @@ import Foundation
|
||||
// Mainly used with deleting objects and undo/redo.
|
||||
// Especially redo. The idea is to put something back in the right place.
|
||||
|
||||
public struct ContainerPath {
|
||||
@MainActor public struct ContainerPath {
|
||||
|
||||
private weak var account: Account?
|
||||
private let names: [String] // empty if top-level of account
|
||||
|
||||
@@ -48,7 +48,7 @@ extension Feed {
|
||||
|
||||
public extension Article {
|
||||
|
||||
var account: Account? {
|
||||
@MainActor var account: Account? {
|
||||
// The force unwrapped shared instance was crashing Account.framework unit tests.
|
||||
guard let manager = AccountManager.shared else {
|
||||
return nil
|
||||
@@ -56,7 +56,7 @@ public extension Article {
|
||||
return manager.existingAccount(with: accountID)
|
||||
}
|
||||
|
||||
var feed: Feed? {
|
||||
@MainActor var feed: Feed? {
|
||||
return account?.existingFeed(withFeedID: feedID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ public final class Feed: FeedProtocol, Renamable, Hashable, ObservableObject {
|
||||
|
||||
// MARK: - Renamable
|
||||
|
||||
public func rename(to newName: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
@MainActor public func rename(to newName: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let account = account else { return }
|
||||
account.renameFeed(self, to: newName, completion: completion)
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import RSCore
|
||||
|
||||
final class FeedMetadataFile: Logging {
|
||||
@MainActor final class FeedMetadataFile: Logging {
|
||||
|
||||
private let fileURL: URL
|
||||
private let account: Account
|
||||
|
||||
@@ -19,7 +19,7 @@ public enum FeedbinAccountDelegateError: String, Error {
|
||||
case unknown = "An unknown error occurred."
|
||||
}
|
||||
|
||||
final class FeedbinAccountDelegate: AccountDelegate, Logging {
|
||||
@MainActor final class FeedbinAccountDelegate: AccountDelegate, Logging {
|
||||
|
||||
private let database: SyncDatabase
|
||||
|
||||
@@ -92,7 +92,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging {
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
self.refreshProgress.clear()
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging {
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
self.refreshProgress.clear()
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
@@ -134,7 +134,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging {
|
||||
|
||||
database.selectForProcessing { result in
|
||||
|
||||
func processStatuses(_ syncStatuses: [SyncStatus]) {
|
||||
@MainActor func processStatuses(_ syncStatuses: [SyncStatus]) {
|
||||
let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false }
|
||||
let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true }
|
||||
let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true }
|
||||
@@ -280,7 +280,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging {
|
||||
self.refreshProgress.completeTask()
|
||||
self.isOPMLImportInProgress = false
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
@@ -315,7 +315,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging {
|
||||
}
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
@@ -409,7 +409,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging {
|
||||
}
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
@@ -437,7 +437,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging {
|
||||
}
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
@@ -484,7 +484,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging {
|
||||
}
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
@@ -1163,43 +1163,46 @@ private extension FeedbinAccountDelegate {
|
||||
|
||||
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in
|
||||
|
||||
func process(_ fetchedArticleIDs: Set<String>) {
|
||||
@MainActor func process(_ fetchedArticleIDs: Set<String>) {
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
|
||||
let articleIDs = Array(fetchedArticleIDs)
|
||||
let chunkedArticleIDs = articleIDs.chunked(into: 100)
|
||||
|
||||
for chunk in chunkedArticleIDs {
|
||||
group.enter()
|
||||
self.caller.retrieveEntries(articleIDs: chunk) { result in
|
||||
for chunk in chunkedArticleIDs {
|
||||
group.enter()
|
||||
self.caller.retrieveEntries(articleIDs: chunk) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let entries):
|
||||
// Task { @MainActor in
|
||||
switch result {
|
||||
case .success(let entries):
|
||||
self.processEntries(account: account, entries: entries) { error in
|
||||
group.leave()
|
||||
if error != nil {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
self.processEntries(account: account, entries: entries) { error in
|
||||
group.leave()
|
||||
if error != nil {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
errorOccurred = true
|
||||
self.logger.error("Refreshing missing articles failed: \(error.localizedDescription, privacy: .public)")
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
errorOccurred = true
|
||||
self.logger.error("Refreshing missing articles failed: \(error.localizedDescription, privacy: .public)")
|
||||
group.leave()
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
self.refreshProgress.completeTask()
|
||||
self.logger.debug("Done refreshing missing articles.")
|
||||
if errorOccurred {
|
||||
completion(.failure(FeedbinAccountDelegateError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
// Task { @MainActor in
|
||||
self.refreshProgress.completeTask()
|
||||
self.logger.debug("Done refreshing missing articles.")
|
||||
if errorOccurred {
|
||||
completion(.failure(FeedbinAccountDelegateError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1273,7 +1276,7 @@ private extension FeedbinAccountDelegate {
|
||||
|
||||
database.selectPendingReadStatusArticleIDs() { result in
|
||||
|
||||
func process(_ pendingArticleIDs: Set<String>) {
|
||||
@MainActor func process(_ pendingArticleIDs: Set<String>) {
|
||||
|
||||
let feedbinUnreadArticleIDs = Set(articleIDs.map { String($0) } )
|
||||
let updatableFeedbinUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(pendingArticleIDs)
|
||||
@@ -1326,7 +1329,7 @@ private extension FeedbinAccountDelegate {
|
||||
|
||||
database.selectPendingStarredStatusArticleIDs() { result in
|
||||
|
||||
func process(_ pendingArticleIDs: Set<String>) {
|
||||
@MainActor func process(_ pendingArticleIDs: Set<String>) {
|
||||
|
||||
let feedbinStarredArticleIDs = Set(articleIDs.map { String($0) } )
|
||||
let updatableFeedbinStarredArticleIDs = feedbinStarredArticleIDs.subtracting(pendingArticleIDs)
|
||||
@@ -1386,7 +1389,7 @@ private extension FeedbinAccountDelegate {
|
||||
}
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging {
|
||||
self.refreshProgress.completeTask()
|
||||
self.isOPMLImportInProgress = false
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import Foundation
|
||||
struct FeedlyFeedContainerValidator {
|
||||
var container: Container
|
||||
|
||||
func getValidContainer() throws -> (Folder, String) {
|
||||
@MainActor func getValidContainer() throws -> (Folder, String) {
|
||||
guard let folder = container as? Folder else {
|
||||
throw FeedlyAccountDelegateError.addFeedChooseFolder
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public final class Folder: FeedProtocol, Renamable, Container, Hashable {
|
||||
return .read
|
||||
}
|
||||
|
||||
public var containerID: ContainerIdentifier? {
|
||||
@MainActor public var containerID: ContainerIdentifier? {
|
||||
guard let accountID = account?.accountID else {
|
||||
assertionFailure("Expected feed.account, but got nil.")
|
||||
return nil
|
||||
@@ -24,7 +24,7 @@ public final class Folder: FeedProtocol, Renamable, Container, Hashable {
|
||||
return ContainerIdentifier.folder(accountID, nameForDisplay)
|
||||
}
|
||||
|
||||
public var itemID: ItemIdentifier? {
|
||||
@MainActor public var itemID: ItemIdentifier? {
|
||||
guard let accountID = account?.accountID else {
|
||||
assertionFailure("Expected feed.account, but got nil.")
|
||||
return nil
|
||||
@@ -66,7 +66,7 @@ public final class Folder: FeedProtocol, Renamable, Container, Hashable {
|
||||
|
||||
// MARK: - Renamable
|
||||
|
||||
public func rename(to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
@MainActor public func rename(to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let account = account else { return }
|
||||
account.renameFolder(self, to: name, completion: completion)
|
||||
}
|
||||
@@ -122,7 +122,7 @@ public final class Folder: FeedProtocol, Renamable, Container, Hashable {
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
public func addFeeds(_ feeds: Set<Feed>) {
|
||||
@MainActor public func addFeeds(_ feeds: Set<Feed>) {
|
||||
guard !feeds.isEmpty else {
|
||||
return
|
||||
}
|
||||
@@ -135,7 +135,7 @@ public final class Folder: FeedProtocol, Renamable, Container, Hashable {
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
public func removeFeeds(_ feeds: Set<Feed>) {
|
||||
@MainActor public func removeFeeds(_ feeds: Set<Feed>) {
|
||||
guard !feeds.isEmpty else {
|
||||
return
|
||||
}
|
||||
@@ -168,7 +168,7 @@ private extension Folder {
|
||||
unreadCount = updatedUnreadCount
|
||||
}
|
||||
|
||||
func childrenContain(_ feed: Feed) -> Bool {
|
||||
@MainActor func childrenContain(_ feed: Feed) -> Bool {
|
||||
return topLevelFeeds.contains(feed)
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,7 @@ private extension Folder {
|
||||
|
||||
extension Folder: OPMLRepresentable {
|
||||
|
||||
public func OPMLString(indentLevel: Int, allowCustomAttributes: Bool) -> String {
|
||||
@MainActor public func OPMLString(indentLevel: Int, allowCustomAttributes: Bool) -> String {
|
||||
|
||||
let attrExternalID: String = {
|
||||
if allowCustomAttributes, let externalID = externalID {
|
||||
@@ -220,7 +220,7 @@ extension Folder: OPMLRepresentable {
|
||||
|
||||
// MARK: Set
|
||||
|
||||
extension Set where Element == Folder {
|
||||
@MainActor extension Set where Element == Folder {
|
||||
|
||||
func sorted() -> Array<Folder> {
|
||||
return sorted(by: { (folder1, folder2) -> Bool in
|
||||
|
||||
@@ -93,29 +93,30 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
|
||||
|
||||
let parserData = ParserData(url: feed.url, data: data)
|
||||
FeedParser.parse(parserData) { (parsedFeed, error) in
|
||||
|
||||
guard let account = feed.account, let parsedFeed = parsedFeed, error == nil else {
|
||||
completion()
|
||||
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
return
|
||||
}
|
||||
|
||||
account.update(feed, with: parsedFeed) { result in
|
||||
if case .success(let articleChanges) = result {
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse)
|
||||
}
|
||||
feed.contentHash = dataHash
|
||||
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
self.delegate?.localAccountRefresher(self, articleChanges: articleChanges) {
|
||||
completion()
|
||||
}
|
||||
} else {
|
||||
completion()
|
||||
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Task { @MainActor in
|
||||
guard let account = feed.account, let parsedFeed = parsedFeed, error == nil else {
|
||||
completion()
|
||||
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
return
|
||||
}
|
||||
|
||||
account.update(feed, with: parsedFeed) { result in
|
||||
if case .success(let articleChanges) = result {
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse)
|
||||
}
|
||||
feed.contentHash = dataHash
|
||||
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
self.delegate?.localAccountRefresher(self, articleChanges: articleChanges) {
|
||||
completion()
|
||||
}
|
||||
} else {
|
||||
completion()
|
||||
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -323,7 +323,7 @@ extension NewsBlurAccountDelegate {
|
||||
}
|
||||
|
||||
database.selectPendingReadStatusArticleIDs() { result in
|
||||
func process(_ pendingStoryHashes: Set<String>) {
|
||||
@MainActor func process(_ pendingStoryHashes: Set<String>) {
|
||||
|
||||
let newsBlurUnreadStoryHashes = Set(hashes.map { $0.hash } )
|
||||
let updatableNewsBlurUnreadStoryHashes = newsBlurUnreadStoryHashes.subtracting(pendingStoryHashes)
|
||||
@@ -371,7 +371,8 @@ extension NewsBlurAccountDelegate {
|
||||
}
|
||||
|
||||
database.selectPendingStarredStatusArticleIDs() { result in
|
||||
func process(_ pendingStoryHashes: Set<String>) {
|
||||
|
||||
@MainActor func process(_ pendingStoryHashes: Set<String>) {
|
||||
|
||||
let newsBlurStarredStoryHashes = Set(hashes.map { $0.hash } )
|
||||
let updatableNewsBlurUnreadStoryHashes = newsBlurStarredStoryHashes.subtracting(pendingStoryHashes)
|
||||
@@ -550,7 +551,7 @@ extension NewsBlurAccountDelegate {
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
self.refreshProgress.clear()
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
@@ -134,7 +134,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
|
||||
logger.debug("Sending story statuses...")
|
||||
database.selectForProcessing { result in
|
||||
|
||||
func processStatuses(_ syncStatuses: [SyncStatus]) {
|
||||
@MainActor func processStatuses(_ syncStatuses: [SyncStatus]) {
|
||||
let createUnreadStatuses = syncStatuses.filter {
|
||||
$0.key == SyncStatus.Key.read && $0.flag == false
|
||||
}
|
||||
@@ -270,7 +270,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
|
||||
|
||||
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in
|
||||
|
||||
func process(_ fetchedHashes: Set<String>) {
|
||||
@MainActor func process(_ fetchedHashes: Set<String>) {
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
|
||||
@@ -432,7 +432,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
|
||||
self.createFeed(account: account, newsBlurFeed: feed, name: name, container: container, completion: completion)
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
@@ -459,7 +459,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
import RSCore
|
||||
import RSParser
|
||||
|
||||
final class OPMLFile: Logging {
|
||||
@MainActor final class OPMLFile: Logging {
|
||||
|
||||
private let fileURL: URL
|
||||
private let account: Account
|
||||
|
||||
@@ -33,7 +33,7 @@ public enum ReaderAPIAccountDelegateError: LocalizedError {
|
||||
}
|
||||
}
|
||||
|
||||
final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
|
||||
@MainActor final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
|
||||
|
||||
private let variant: ReaderAPIVariant
|
||||
|
||||
@@ -135,7 +135,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
|
||||
DispatchQueue.main.async {
|
||||
self.refreshProgress.clear()
|
||||
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
if wrappedError.isCredentialsError, let basicCredentials = try? account.retrieveCredentials(type: .readerBasic), let endpoint = account.endpointURL {
|
||||
self.caller.credentials = basicCredentials
|
||||
|
||||
@@ -198,7 +198,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
|
||||
|
||||
database.selectForProcessing { result in
|
||||
|
||||
func processStatuses(_ syncStatuses: [SyncStatus]) {
|
||||
@MainActor func processStatuses(_ syncStatuses: [SyncStatus]) {
|
||||
let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false }
|
||||
let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true }
|
||||
let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true }
|
||||
@@ -312,7 +312,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
|
||||
}
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
@@ -422,7 +422,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
|
||||
}
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
@@ -456,7 +456,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
|
||||
}
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
@@ -487,7 +487,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
|
||||
}
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
@@ -537,7 +537,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
|
||||
}
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
@@ -980,7 +980,7 @@ private extension ReaderAPIAccountDelegate {
|
||||
func refreshMissingArticles(_ account: Account, completion: @escaping VoidCompletionBlock) {
|
||||
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { articleIDsResult in
|
||||
|
||||
func process(_ fetchedArticleIDs: Set<String>) {
|
||||
@MainActor func process(_ fetchedArticleIDs: Set<String>) {
|
||||
guard !fetchedArticleIDs.isEmpty else {
|
||||
completion()
|
||||
return
|
||||
@@ -1086,7 +1086,7 @@ private extension ReaderAPIAccountDelegate {
|
||||
|
||||
database.selectPendingReadStatusArticleIDs() { result in
|
||||
|
||||
func process(_ pendingArticleIDs: Set<String>) {
|
||||
@MainActor func process(_ pendingArticleIDs: Set<String>) {
|
||||
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
|
||||
|
||||
account.fetchUnreadArticleIDs { articleIDsResult in
|
||||
@@ -1135,7 +1135,7 @@ private extension ReaderAPIAccountDelegate {
|
||||
|
||||
database.selectPendingStarredStatusArticleIDs() { result in
|
||||
|
||||
func process(_ pendingArticleIDs: Set<String>) {
|
||||
@MainActor func process(_ pendingArticleIDs: Set<String>) {
|
||||
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
|
||||
|
||||
account.fetchStarredArticleIDs { articleIDsResult in
|
||||
|
||||
@@ -255,7 +255,7 @@ final class ReaderAPICaller: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
func deleteTag(folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
@MainActor func deleteTag(folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let baseURL = apiBaseURL else {
|
||||
completion(.failure(CredentialsError.incompleteCredentials))
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user