Mark many things as MainActor and deal with the fallout.
This commit is contained in:
parent
87db1e3d5f
commit
27d27cbf1a
@ -62,7 +62,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
|
||||
@ -894,12 +894,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
return
|
||||
}
|
||||
database.createStatusesIfNeeded(articleIDs: articleIDs) { error in
|
||||
if let error = error {
|
||||
completion?(error)
|
||||
return
|
||||
|
||||
MainActor.assumeIsolated {
|
||||
if let error = error {
|
||||
completion?(error)
|
||||
return
|
||||
}
|
||||
self.noteStatusesForArticleIDsDidChange(articleIDs)
|
||||
completion?(nil)
|
||||
}
|
||||
self.noteStatusesForArticleIDsDidChange(articleIDs)
|
||||
completion?(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -912,11 +915,13 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
return
|
||||
}
|
||||
database.mark(articleIDs: articleIDs, statusKey: statusKey, flag: flag) { error in
|
||||
if let error {
|
||||
completion?(error)
|
||||
} else {
|
||||
self.noteStatusesForArticleIDsDidChange(articleIDs: articleIDs, statusKey: statusKey, flag: flag)
|
||||
completion?(nil)
|
||||
MainActor.assumeIsolated {
|
||||
if let error {
|
||||
completion?(error)
|
||||
} else {
|
||||
self.noteStatusesForArticleIDsDidChange(articleIDs: articleIDs, statusKey: statusKey, flag: flag)
|
||||
completion?(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import Articles
|
||||
import RSWeb
|
||||
import Secrets
|
||||
|
||||
protocol AccountDelegate {
|
||||
@MainActor protocol AccountDelegate {
|
||||
|
||||
var behaviors: AccountBehaviors { get }
|
||||
|
||||
|
@ -14,18 +14,22 @@ 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
|
||||
case wrappedError(error: Error, accountID: String, accountName: String)
|
||||
|
||||
@MainActor public var account: Account? {
|
||||
if case .wrappedError(_, let accountID, _) = self {
|
||||
return AccountManager.shared.existingAccount(with: accountID)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var isCredentialsError: Bool {
|
||||
if case .wrappedError(let error, _) = self {
|
||||
@MainActor public static func wrappedError(error: Error, account: Account) -> AccountError {
|
||||
wrappedError(error: error, accountID: account.accountID, accountName: account.nameForDisplay)
|
||||
}
|
||||
|
||||
@MainActor public var isCredentialsError: Bool {
|
||||
if case .wrappedError(let error, _, _) = self {
|
||||
if case TransportError.httpError(let status) = error {
|
||||
return isCredentialsError(status: status)
|
||||
}
|
||||
@ -41,17 +45,17 @@ public enum AccountError: LocalizedError {
|
||||
return NSLocalizedString("You are already subscribed to this feed and can’t add it again.", comment: "Already subscribed")
|
||||
case .opmlImportInProgress:
|
||||
return NSLocalizedString("An OPML import for this account is already running.", comment: "Import running")
|
||||
case .wrappedError(let error, let account):
|
||||
case .wrappedError(let error, _, let accountName):
|
||||
switch error {
|
||||
case TransportError.httpError(let status):
|
||||
if isCredentialsError(status: status) {
|
||||
let localizedText = NSLocalizedString("Your “%@” credentials are invalid or expired.", comment: "Invalid or expired")
|
||||
return NSString.localizedStringWithFormat(localizedText as NSString, account.nameForDisplay) as String
|
||||
return NSString.localizedStringWithFormat(localizedText as NSString, accountName) as String
|
||||
} else {
|
||||
return unknownError(error, account)
|
||||
return unknownError(error, accountName)
|
||||
}
|
||||
default:
|
||||
return unknownError(error, account)
|
||||
return unknownError(error, accountName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -62,7 +66,7 @@ public enum AccountError: LocalizedError {
|
||||
return nil
|
||||
case .createErrorAlreadySubscribed:
|
||||
return nil
|
||||
case .wrappedError(let error, _):
|
||||
case .wrappedError(let error, _, _):
|
||||
switch error {
|
||||
case TransportError.httpError(let status):
|
||||
if isCredentialsError(status: status) {
|
||||
@ -84,9 +88,9 @@ public enum AccountError: LocalizedError {
|
||||
|
||||
private extension AccountError {
|
||||
|
||||
func unknownError(_ error: Error, _ account: Account) -> String {
|
||||
func unknownError(_ error: Error, _ accountName: String) -> 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
|
||||
return NSString.localizedStringWithFormat(localizedText as NSString, accountName, error.localizedDescription) as String
|
||||
}
|
||||
|
||||
func isCredentialsError(status: Int) -> Bool {
|
||||
|
@ -15,7 +15,7 @@ import Secrets
|
||||
|
||||
// Main thread only.
|
||||
|
||||
public final class AccountManager: UnreadCountProvider {
|
||||
@MainActor public final class AccountManager: UnreadCountProvider {
|
||||
|
||||
@MainActor public static var shared: AccountManager!
|
||||
public static let netNewsWireNewsURL = "https://netnewswire.blog/feed.xml"
|
||||
|
@ -28,7 +28,7 @@ enum CloudKitAccountDelegateError: LocalizedError {
|
||||
}
|
||||
}
|
||||
|
||||
final class CloudKitAccountDelegate: AccountDelegate {
|
||||
@MainActor final class CloudKitAccountDelegate: AccountDelegate {
|
||||
|
||||
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
|
||||
|
||||
@ -416,10 +416,12 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
|
||||
self.database.insertStatuses(syncStatuses) { _ in
|
||||
self.database.selectPendingCount { result in
|
||||
if let count = try? result.get(), count > 100 {
|
||||
self.sendArticleStatus(for: account, showProgress: false) { _ in }
|
||||
MainActor.assumeIsolated {
|
||||
if let count = try? result.get(), count > 100 {
|
||||
self.sendArticleStatus(for: account, showProgress: false) { _ in }
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
@ -648,35 +650,36 @@ private extension CloudKitAccountDelegate {
|
||||
|
||||
if let parsedFeed = parsedFeed {
|
||||
account.update(feed, with: parsedFeed) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
self.accountZone.createFeed(url: bestFeedSpecifier.urlString,
|
||||
name: parsedFeed.title,
|
||||
editedName: editedName,
|
||||
homePageURL: parsedFeed.homePageURL,
|
||||
container: container) { result in
|
||||
|
||||
self.refreshProgress.completeTask()
|
||||
switch result {
|
||||
case .success(let externalID):
|
||||
feed.externalID = externalID
|
||||
self.sendNewArticlesToTheCloud(account, feed)
|
||||
completion(.success(feed))
|
||||
case .failure(let error):
|
||||
container.removeFeed(feed)
|
||||
self.refreshProgress.completeTasks(2)
|
||||
completion(.failure(error))
|
||||
MainActor.assumeIsolated {
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
self.accountZone.createFeed(url: bestFeedSpecifier.urlString,
|
||||
name: parsedFeed.title,
|
||||
editedName: editedName,
|
||||
homePageURL: parsedFeed.homePageURL,
|
||||
container: container) { result in
|
||||
|
||||
self.refreshProgress.completeTask()
|
||||
switch result {
|
||||
case .success(let externalID):
|
||||
feed.externalID = externalID
|
||||
self.sendNewArticlesToTheCloud(account, feed)
|
||||
completion(.success(feed))
|
||||
case .failure(let error):
|
||||
container.removeFeed(feed)
|
||||
self.refreshProgress.completeTasks(2)
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
container.removeFeed(feed)
|
||||
self.refreshProgress.completeTasks(3)
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
container.removeFeed(feed)
|
||||
self.refreshProgress.completeTasks(3)
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
self.refreshProgress.completeTasks(3)
|
||||
@ -700,9 +703,9 @@ private extension CloudKitAccountDelegate {
|
||||
}
|
||||
|
||||
func sendNewArticlesToTheCloud(_ account: Account, _ feed: Feed) {
|
||||
|
||||
|
||||
Task { @MainActor in
|
||||
|
||||
|
||||
do {
|
||||
let articles = try await account.articles(for: .feed(feed))
|
||||
self.storeArticleChanges(new: articles, updated: Set<Article>(), deleted: Set<Article>()) {
|
||||
@ -716,7 +719,7 @@ private extension CloudKitAccountDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} catch {
|
||||
os_log(.error, log: self.log, "CloudKit Feed send articles error: %@.", error.localizedDescription)
|
||||
}
|
||||
|
@ -20,7 +20,8 @@ enum CloudKitAccountZoneError: LocalizedError {
|
||||
return NSLocalizedString("An unexpected CloudKit error occurred.", comment: "An unexpected CloudKit error occurred.")
|
||||
}
|
||||
}
|
||||
final class CloudKitAccountZone: CloudKitZone {
|
||||
|
||||
@MainActor final class CloudKitAccountZone: CloudKitZone {
|
||||
|
||||
var zoneID: CKRecordZone.ID
|
||||
|
||||
|
@ -13,8 +13,8 @@ import CloudKit
|
||||
import Articles
|
||||
import CloudKitExtras
|
||||
|
||||
class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
|
||||
@MainActor final class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
|
||||
private typealias UnclaimedFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String)
|
||||
private var newUnclaimedFeeds = [String: [UnclaimedFeed]]()
|
||||
private var existingUnclaimedFeeds = [String: [Feed]]()
|
||||
@ -135,7 +135,6 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
account?.removeFolder(folder)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension CloudKitAcountZoneDelegate {
|
||||
|
@ -108,7 +108,7 @@ private extension CloudKitSendStatusOperation {
|
||||
}
|
||||
}
|
||||
|
||||
func processStatuses(_ syncStatuses: [SyncStatus], completion: @escaping (Bool) -> Void) {
|
||||
@MainActor func processStatuses(_ syncStatuses: [SyncStatus], completion: @escaping (Bool) -> Void) {
|
||||
guard let account = account, let articlesZone = articlesZone else {
|
||||
completion(true)
|
||||
return
|
||||
@ -156,9 +156,11 @@ private extension CloudKitSendStatusOperation {
|
||||
}
|
||||
case .failure(let error):
|
||||
self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in
|
||||
self.processAccountError(account, error)
|
||||
os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription)
|
||||
completion(true)
|
||||
MainActor.assumeIsolated {
|
||||
self.processAccountError(account, error)
|
||||
os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription)
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -178,7 +180,7 @@ private extension CloudKitSendStatusOperation {
|
||||
}
|
||||
}
|
||||
|
||||
func processAccountError(_ account: Account, _ error: Error) {
|
||||
@MainActor func processAccountError(_ account: Account, _ error: Error) {
|
||||
if case CloudKitZoneError.userDeletedZone = error {
|
||||
account.removeFeeds(account.topLevelFeeds)
|
||||
for folder in account.folders ?? Set<Folder>() {
|
||||
|
@ -15,20 +15,20 @@ extension Notification.Name {
|
||||
public static let ChildrenDidChange = Notification.Name("ChildrenDidChange")
|
||||
}
|
||||
|
||||
public protocol Container: AnyObject, ContainerIdentifiable {
|
||||
@MainActor 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 }
|
||||
|
||||
|
||||
func hasAtLeastOneFeed() -> Bool
|
||||
func objectIsChild(_ object: AnyObject) -> Bool
|
||||
|
||||
func hasChildFolder(with: String) -> Bool
|
||||
func childFolder(with: String) -> Folder?
|
||||
|
||||
func removeFeed(_ feed: Feed)
|
||||
func removeFeed(_ feed: Feed)
|
||||
func addFeed(_ feed: Feed)
|
||||
|
||||
//Recursive — checks subfolders
|
||||
|
@ -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
|
||||
|
@ -11,7 +11,7 @@ import RSWeb
|
||||
import Articles
|
||||
import Core
|
||||
|
||||
public final class Feed: Renamable, DisplayNameProvider, UnreadCountProvider, Hashable {
|
||||
@MainActor public final class Feed: Renamable, DisplayNameProvider, UnreadCountProvider, Hashable {
|
||||
|
||||
public weak var account: Account?
|
||||
public let url: String
|
||||
@ -294,11 +294,11 @@ extension Feed: OPMLRepresentable {
|
||||
|
||||
extension Set where Element == Feed {
|
||||
|
||||
func feedIDs() -> Set<String> {
|
||||
@MainActor func feedIDs() -> Set<String> {
|
||||
return Set<String>(map { $0.feedID })
|
||||
}
|
||||
|
||||
func sorted() -> Array<Feed> {
|
||||
@MainActor func sorted() -> Array<Feed> {
|
||||
return sorted(by: { (feed1, feed2) -> Bool in
|
||||
if feed1.nameForDisplay.localizedStandardCompare(feed2.nameForDisplay) == .orderedSame {
|
||||
return feed1.url < feed2.url
|
||||
|
@ -136,62 +136,64 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
|
||||
database.selectForProcessing { result in
|
||||
|
||||
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 }
|
||||
let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false }
|
||||
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
|
||||
group.enter()
|
||||
self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
MainActor.assumeIsolated {
|
||||
@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 }
|
||||
let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false }
|
||||
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
|
||||
group.enter()
|
||||
self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendArticleStatuses(createStarredStatuses, apiCall: self.caller.createStarredEntries) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendArticleStatuses(deleteStarredStatuses, apiCall: self.caller.deleteStarredEntries) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
os_log(.debug, log: self.log, "Done sending article statuses.")
|
||||
if errorOccurred {
|
||||
completion(.failure(FeedbinAccountDelegateError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let syncStatuses):
|
||||
processStatuses(syncStatuses)
|
||||
case .failure(let databaseError):
|
||||
completion(.failure(databaseError))
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendArticleStatuses(createStarredStatuses, apiCall: self.caller.createStarredEntries) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendArticleStatuses(deleteStarredStatuses, apiCall: self.caller.deleteStarredEntries) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
os_log(.debug, log: self.log, "Done sending article statuses.")
|
||||
if errorOccurred {
|
||||
completion(.failure(FeedbinAccountDelegateError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let syncStatuses):
|
||||
processStatuses(syncStatuses)
|
||||
case .failure(let databaseError):
|
||||
completion(.failure(databaseError))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -564,10 +566,12 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
|
||||
self.database.insertStatuses(syncStatuses) { _ in
|
||||
self.database.selectPendingCount { result in
|
||||
if let count = try? result.get(), count > 100 {
|
||||
self.sendArticleStatus(for: account) { _ in }
|
||||
MainActor.assumeIsolated {
|
||||
if let count = try? result.get(), count > 100 {
|
||||
self.sendArticleStatus(for: account) { _ in }
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
@ -1082,43 +1086,46 @@ private extension FeedbinAccountDelegate {
|
||||
case .success(let (entries, page)):
|
||||
|
||||
self.processEntries(account: account, entries: entries) { error in
|
||||
|
||||
MainActor.assumeIsolated {
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
self.refreshArticles(account, page: page, updateFetchDate: nil) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
self.refreshProgress.completeTask()
|
||||
self.refreshMissingArticles(account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
self.refreshProgress.completeTask()
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(feed))
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
self.refreshArticles(account, page: page, updateFetchDate: nil) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
self.refreshProgress.completeTask()
|
||||
self.refreshMissingArticles(account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
self.refreshProgress.completeTask()
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(feed))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1145,21 +1152,24 @@ private extension FeedbinAccountDelegate {
|
||||
}
|
||||
|
||||
self.processEntries(account: account, entries: entries) { error in
|
||||
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
MainActor.assumeIsolated {
|
||||
|
||||
self.refreshArticles(account, page: page, updateFetchDate: updateFetchDate) { result in
|
||||
os_log(.debug, log: self.log, "Done refreshing articles.")
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
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 {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1250,16 +1260,18 @@ private extension FeedbinAccountDelegate {
|
||||
case .success(let (entries, nextPage)):
|
||||
|
||||
self.processEntries(account: account, entries: entries) { error in
|
||||
self.refreshProgress.completeTask()
|
||||
MainActor.assumeIsolated {
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
self.refreshArticles(account, page: nextPage, updateFetchDate: updateFetchDate, completion: completion)
|
||||
}
|
||||
|
||||
self.refreshArticles(account, page: nextPage, updateFetchDate: updateFetchDate, completion: completion)
|
||||
}
|
||||
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
@ -1294,47 +1306,50 @@ private extension FeedbinAccountDelegate {
|
||||
|
||||
database.selectPendingReadStatusArticleIDs() { result in
|
||||
|
||||
func process(_ pendingArticleIDs: Set<String>) {
|
||||
|
||||
let feedbinUnreadArticleIDs = Set(articleIDs.map { String($0) } )
|
||||
let updatableFeedbinUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(pendingArticleIDs)
|
||||
|
||||
account.fetchUnreadArticleIDs { articleIDsResult in
|
||||
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
|
||||
return
|
||||
MainActor.assumeIsolated {
|
||||
|
||||
@MainActor func process(_ pendingArticleIDs: Set<String>) {
|
||||
|
||||
let feedbinUnreadArticleIDs = Set(articleIDs.map { String($0) } )
|
||||
let updatableFeedbinUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(pendingArticleIDs)
|
||||
|
||||
account.fetchUnreadArticleIDs { articleIDsResult in
|
||||
MainActor.assumeIsolated {
|
||||
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
|
||||
return
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
// Mark articles as unread
|
||||
let deltaUnreadArticleIDs = updatableFeedbinUnreadArticleIDs.subtracting(currentUnreadArticleIDs)
|
||||
group.enter()
|
||||
account.markAsUnread(deltaUnreadArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
// Mark articles as read
|
||||
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableFeedbinUnreadArticleIDs)
|
||||
group.enter()
|
||||
account.markAsRead(deltaReadArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
// Mark articles as unread
|
||||
let deltaUnreadArticleIDs = updatableFeedbinUnreadArticleIDs.subtracting(currentUnreadArticleIDs)
|
||||
group.enter()
|
||||
account.markAsUnread(deltaUnreadArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
// Mark articles as read
|
||||
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableFeedbinUnreadArticleIDs)
|
||||
group.enter()
|
||||
account.markAsRead(deltaReadArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let pendingArticleIDs):
|
||||
process(pendingArticleIDs)
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Sync Article Read Status failed: %@.", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let pendingArticleIDs):
|
||||
process(pendingArticleIDs)
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Sync Article Read Status failed: %@.", error.localizedDescription)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -1347,46 +1362,50 @@ private extension FeedbinAccountDelegate {
|
||||
|
||||
database.selectPendingStarredStatusArticleIDs() { result in
|
||||
|
||||
func process(_ pendingArticleIDs: Set<String>) {
|
||||
|
||||
let feedbinStarredArticleIDs = Set(articleIDs.map { String($0) } )
|
||||
let updatableFeedbinStarredArticleIDs = feedbinStarredArticleIDs.subtracting(pendingArticleIDs)
|
||||
|
||||
account.fetchStarredArticleIDs { articleIDsResult in
|
||||
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
|
||||
return
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
MainActor.assumeIsolated {
|
||||
@MainActor func process(_ pendingArticleIDs: Set<String>) {
|
||||
|
||||
// Mark articles as starred
|
||||
let deltaStarredArticleIDs = updatableFeedbinStarredArticleIDs.subtracting(currentStarredArticleIDs)
|
||||
group.enter()
|
||||
account.markAsStarred(deltaStarredArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
// Mark articles as unstarred
|
||||
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableFeedbinStarredArticleIDs)
|
||||
group.enter()
|
||||
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion()
|
||||
let feedbinStarredArticleIDs = Set(articleIDs.map { String($0) } )
|
||||
let updatableFeedbinStarredArticleIDs = feedbinStarredArticleIDs.subtracting(pendingArticleIDs)
|
||||
|
||||
account.fetchStarredArticleIDs { articleIDsResult in
|
||||
|
||||
MainActor.assumeIsolated {
|
||||
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
|
||||
return
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
// Mark articles as starred
|
||||
let deltaStarredArticleIDs = updatableFeedbinStarredArticleIDs.subtracting(currentStarredArticleIDs)
|
||||
group.enter()
|
||||
account.markAsStarred(deltaStarredArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
// Mark articles as unstarred
|
||||
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableFeedbinStarredArticleIDs)
|
||||
group.enter()
|
||||
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let pendingArticleIDs):
|
||||
process(pendingArticleIDs)
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Sync Article Starred Status failed: %@.", error.localizedDescription)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let pendingArticleIDs):
|
||||
process(pendingArticleIDs)
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Sync Article Starred Status failed: %@.", error.localizedDescription)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -408,7 +408,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let folder = container as? Folder, let collectionId = folder.externalID else {
|
||||
return DispatchQueue.main.async {
|
||||
completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed)))
|
||||
completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed.nameForDisplay)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -442,7 +442,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||
completion(.success(()))
|
||||
case .failure:
|
||||
from.addFeed(feed)
|
||||
completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed, from, to)))
|
||||
completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed.nameForDisplay, from.nameForDisplay, to.nameForDisplay)))
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
|
@ -14,12 +14,12 @@ enum FeedlyAccountDelegateError: LocalizedError {
|
||||
case unableToAddFolder(String)
|
||||
case unableToRenameFolder(String, String)
|
||||
case unableToRemoveFolder(String)
|
||||
case unableToMoveFeedBetweenFolders(Feed, Folder, Folder)
|
||||
case unableToMoveFeedBetweenFolders(String, String, String)
|
||||
case addFeedChooseFolder
|
||||
case addFeedInvalidFolder(Folder)
|
||||
case addFeedInvalidFolder(String)
|
||||
case unableToRenameFeed(String, String)
|
||||
case unableToRemoveFeed(Feed)
|
||||
|
||||
case unableToRemoveFeed(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .notLoggedIn:
|
||||
@ -41,24 +41,24 @@ enum FeedlyAccountDelegateError: LocalizedError {
|
||||
let template = NSLocalizedString("Could not remove the folder named “%@”.", comment: "Feedly – Could not remove a folder/collection.")
|
||||
return String(format: template, name)
|
||||
|
||||
case .unableToMoveFeedBetweenFolders(let feed, _, let to):
|
||||
case .unableToMoveFeedBetweenFolders(let feedName, _, let destinationFolderName):
|
||||
let template = NSLocalizedString("Could not move “%@” to “%@”.", comment: "Feedly – Could not move a feed between folders/collections.")
|
||||
return String(format: template, feed.nameForDisplay, to.nameForDisplay)
|
||||
|
||||
return String(format: template, feedName, destinationFolderName)
|
||||
|
||||
case .addFeedChooseFolder:
|
||||
return NSLocalizedString("Please choose a folder to contain the feed.", comment: "Feedly – Feed can only be added to folders.")
|
||||
|
||||
case .addFeedInvalidFolder(let invalidFolder):
|
||||
case .addFeedInvalidFolder(let folderName):
|
||||
let template = NSLocalizedString("Feeds cannot be added to the “%@” folder.", comment: "Feedly – Feed can only be added to folders.")
|
||||
return String(format: template, invalidFolder.nameForDisplay)
|
||||
|
||||
return String(format: template, folderName)
|
||||
|
||||
case .unableToRenameFeed(let from, let to):
|
||||
let template = NSLocalizedString("Could not rename “%@” to “%@”.", comment: "Feedly – Could not rename a feed.")
|
||||
return String(format: template, from, to)
|
||||
|
||||
case .unableToRemoveFeed(let feed):
|
||||
case .unableToRemoveFeed(let feedName):
|
||||
let template = NSLocalizedString("Could not remove “%@”.", comment: "Feedly – Could not remove a feed.")
|
||||
return String(format: template, feed.nameForDisplay)
|
||||
return String(format: template, feedName)
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,10 +80,10 @@ enum FeedlyAccountDelegateError: LocalizedError {
|
||||
case .unableToRemoveFolder:
|
||||
return nil
|
||||
|
||||
case .unableToMoveFeedBetweenFolders(let feed, let from, let to):
|
||||
case .unableToMoveFeedBetweenFolders(let feedName, let sourceFolderName, let destinationFolderName):
|
||||
let template = NSLocalizedString("“%@” may be in both “%@” and “%@”.", comment: "Feedly – Could not move a feed between folders/collections.")
|
||||
return String(format: template, feed.nameForDisplay, from.nameForDisplay, to.nameForDisplay)
|
||||
|
||||
return String(format: template, feedName, sourceFolderName, destinationFolderName)
|
||||
|
||||
case .addFeedChooseFolder:
|
||||
return nil
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedlyFeedContainerValidator {
|
||||
@MainActor struct FeedlyFeedContainerValidator {
|
||||
var container: Container
|
||||
|
||||
func getValidContainer() throws -> (Folder, String) {
|
||||
@ -17,7 +17,7 @@ struct FeedlyFeedContainerValidator {
|
||||
}
|
||||
|
||||
guard let collectionId = folder.externalID else {
|
||||
throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder)
|
||||
throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder.nameForDisplay)
|
||||
}
|
||||
|
||||
return (folder, collectionId)
|
||||
|
@ -23,7 +23,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError {
|
||||
return NSLocalizedString("There is already a Feedly account with that username created.", comment: "Duplicate Error")
|
||||
}
|
||||
}
|
||||
@objc public final class OAuthAccountAuthorizationOperation: NSObject, MainThreadOperation, ASWebAuthenticationPresentationContextProviding {
|
||||
@MainActor @objc public final class OAuthAccountAuthorizationOperation: NSObject, MainThreadOperation, ASWebAuthenticationPresentationContextProviding {
|
||||
|
||||
public var isCanceled: Bool = false {
|
||||
didSet {
|
||||
|
@ -10,7 +10,7 @@ import Foundation
|
||||
import Articles
|
||||
import Core
|
||||
|
||||
public final class Folder: Renamable, Container, DisplayNameProvider, UnreadCountProvider, Hashable {
|
||||
@MainActor public final class Folder: Renamable, Container, DisplayNameProvider, UnreadCountProvider, Hashable {
|
||||
|
||||
public var containerID: ContainerIdentifier? {
|
||||
guard let accountID = account?.accountID else {
|
||||
@ -198,7 +198,7 @@ extension Folder: OPMLRepresentable {
|
||||
|
||||
extension Set where Element == Folder {
|
||||
|
||||
func sorted() -> Array<Folder> {
|
||||
@MainActor func sorted() -> Array<Folder> {
|
||||
return sorted(by: { (folder1, folder2) -> Bool in
|
||||
return folder1.nameForDisplay.localizedStandardCompare(folder2.nameForDisplay) == .orderedAscending
|
||||
})
|
||||
|
@ -52,7 +52,7 @@ final class LocalAccountRefresher {
|
||||
|
||||
extension LocalAccountRefresher: DownloadSessionDelegate {
|
||||
|
||||
func downloadSession(_ downloadSession: DownloadSession, requestForRepresentedObject representedObject: AnyObject) -> URLRequest? {
|
||||
@MainActor func downloadSession(_ downloadSession: DownloadSession, requestForRepresentedObject representedObject: AnyObject) -> URLRequest? {
|
||||
guard let feed = representedObject as? Feed else {
|
||||
return nil
|
||||
}
|
||||
@ -68,7 +68,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
|
||||
return request
|
||||
}
|
||||
|
||||
func downloadSession(_ downloadSession: DownloadSession, downloadDidCompleteForRepresentedObject representedObject: AnyObject, response: URLResponse?, data: Data, error: NSError?, completion: @escaping () -> Void) {
|
||||
@MainActor func downloadSession(_ downloadSession: DownloadSession, downloadDidCompleteForRepresentedObject representedObject: AnyObject, response: URLResponse?, data: Data, error: NSError?, completion: @escaping () -> Void) {
|
||||
let feed = representedObject as! Feed
|
||||
|
||||
guard !data.isEmpty, !isSuspended else {
|
||||
@ -101,18 +101,20 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
|
||||
}
|
||||
|
||||
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) {
|
||||
MainActor.assumeIsolated {
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
completion()
|
||||
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -328,45 +328,49 @@ extension NewsBlurAccountDelegate {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
database.selectPendingReadStatusArticleIDs() { result in
|
||||
func process(_ pendingStoryHashes: Set<String>) {
|
||||
|
||||
let newsBlurUnreadStoryHashes = Set(hashes.map { $0.hash } )
|
||||
let updatableNewsBlurUnreadStoryHashes = newsBlurUnreadStoryHashes.subtracting(pendingStoryHashes)
|
||||
|
||||
account.fetchUnreadArticleIDs { articleIDsResult in
|
||||
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
|
||||
return
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
MainActor.assumeIsolated {
|
||||
@MainActor func process(_ pendingStoryHashes: Set<String>) {
|
||||
|
||||
// Mark articles as unread
|
||||
let deltaUnreadArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentUnreadArticleIDs)
|
||||
group.enter()
|
||||
account.markAsUnread(deltaUnreadArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
// Mark articles as read
|
||||
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes)
|
||||
group.enter()
|
||||
account.markAsRead(deltaReadArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
let newsBlurUnreadStoryHashes = Set(hashes.map { $0.hash } )
|
||||
let updatableNewsBlurUnreadStoryHashes = newsBlurUnreadStoryHashes.subtracting(pendingStoryHashes)
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion()
|
||||
account.fetchUnreadArticleIDs { articleIDsResult in
|
||||
MainActor.assumeIsolated {
|
||||
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
|
||||
return
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
// Mark articles as unread
|
||||
let deltaUnreadArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentUnreadArticleIDs)
|
||||
group.enter()
|
||||
account.markAsUnread(deltaUnreadArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
// Mark articles as read
|
||||
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes)
|
||||
group.enter()
|
||||
account.markAsRead(deltaReadArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let pendingArticleIDs):
|
||||
process(pendingArticleIDs)
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Sync Story Read Status failed: %@.", error.localizedDescription)
|
||||
|
||||
switch result {
|
||||
case .success(let pendingArticleIDs):
|
||||
process(pendingArticleIDs)
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Sync Story Read Status failed: %@.", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -378,43 +382,47 @@ extension NewsBlurAccountDelegate {
|
||||
}
|
||||
|
||||
database.selectPendingStarredStatusArticleIDs() { result in
|
||||
func process(_ pendingStoryHashes: Set<String>) {
|
||||
MainActor.assumeIsolated {
|
||||
@MainActor func process(_ pendingStoryHashes: Set<String>) {
|
||||
|
||||
let newsBlurStarredStoryHashes = Set(hashes.map { $0.hash } )
|
||||
let updatableNewsBlurUnreadStoryHashes = newsBlurStarredStoryHashes.subtracting(pendingStoryHashes)
|
||||
let newsBlurStarredStoryHashes = Set(hashes.map { $0.hash } )
|
||||
let updatableNewsBlurUnreadStoryHashes = newsBlurStarredStoryHashes.subtracting(pendingStoryHashes)
|
||||
|
||||
account.fetchStarredArticleIDs { articleIDsResult in
|
||||
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
|
||||
return
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
// Mark articles as starred
|
||||
let deltaStarredArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentStarredArticleIDs)
|
||||
group.enter()
|
||||
account.markAsStarred(deltaStarredArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
// Mark articles as unstarred
|
||||
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes)
|
||||
group.enter()
|
||||
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion()
|
||||
account.fetchStarredArticleIDs { articleIDsResult in
|
||||
MainActor.assumeIsolated {
|
||||
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
|
||||
return
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
// Mark articles as starred
|
||||
let deltaStarredArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentStarredArticleIDs)
|
||||
group.enter()
|
||||
account.markAsStarred(deltaStarredArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
// Mark articles as unstarred
|
||||
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes)
|
||||
group.enter()
|
||||
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let pendingArticleIDs):
|
||||
process(pendingArticleIDs)
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Sync Story Starred Status failed: %@.", error.localizedDescription)
|
||||
switch result {
|
||||
case .success(let pendingArticleIDs):
|
||||
process(pendingArticleIDs)
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Sync Story Starred Status failed: %@.", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import Articles
|
||||
import Database
|
||||
import RSParser
|
||||
@preconcurrency import RSParser
|
||||
import RSWeb
|
||||
import SyncDatabase
|
||||
import os.log
|
||||
@ -136,70 +136,73 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||
|
||||
database.selectForProcessing { result in
|
||||
|
||||
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
|
||||
}
|
||||
let deleteStarredStatuses = syncStatuses.filter {
|
||||
$0.key == SyncStatus.Key.starred && $0.flag == false
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
|
||||
group.enter()
|
||||
self.sendStoryStatuses(createUnreadStatuses, throttle: true, apiCall: self.caller.markAsUnread) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
MainActor.assumeIsolated {
|
||||
|
||||
@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
|
||||
}
|
||||
let deleteStarredStatuses = syncStatuses.filter {
|
||||
$0.key == SyncStatus.Key.starred && $0.flag == false
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
|
||||
group.enter()
|
||||
self.sendStoryStatuses(createUnreadStatuses, throttle: true, apiCall: self.caller.markAsUnread) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendStoryStatuses(deleteUnreadStatuses, throttle: false, apiCall: self.caller.markAsRead) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendStoryStatuses(createStarredStatuses, throttle: true, apiCall: self.caller.star) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendStoryStatuses(deleteStarredStatuses, throttle: true, apiCall: self.caller.unstar) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
os_log(.debug, log: self.log, "Done sending article statuses.")
|
||||
if errorOccurred {
|
||||
completion(.failure(NewsBlurError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendStoryStatuses(deleteUnreadStatuses, throttle: false, apiCall: self.caller.markAsRead) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let syncStatuses):
|
||||
processStatuses(syncStatuses)
|
||||
case .failure(let databaseError):
|
||||
completion(.failure(databaseError))
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendStoryStatuses(createStarredStatuses, throttle: true, apiCall: self.caller.star) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendStoryStatuses(deleteStarredStatuses, throttle: true, apiCall: self.caller.unstar) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
os_log(.debug, log: self.log, "Done sending article statuses.")
|
||||
if errorOccurred {
|
||||
completion(.failure(NewsBlurError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let syncStatuses):
|
||||
processStatuses(syncStatuses)
|
||||
case .failure(let databaseError):
|
||||
completion(.failure(databaseError))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -272,53 +275,55 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||
|
||||
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in
|
||||
|
||||
func process(_ fetchedHashes: Set<String>) {
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
MainActor.assumeIsolated {
|
||||
@MainActor func process(_ fetchedHashes: Set<String>) {
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
|
||||
let storyHashes = Array(fetchedHashes).map {
|
||||
NewsBlurStoryHash(hash: $0, timestamp: Date())
|
||||
}
|
||||
let chunkedStoryHashes = storyHashes.chunked(into: 100)
|
||||
let storyHashes = Array(fetchedHashes).map {
|
||||
NewsBlurStoryHash(hash: $0, timestamp: Date())
|
||||
}
|
||||
let chunkedStoryHashes = storyHashes.chunked(into: 100)
|
||||
|
||||
for chunk in chunkedStoryHashes {
|
||||
group.enter()
|
||||
self.caller.retrieveStories(hashes: chunk) { result in
|
||||
for chunk in chunkedStoryHashes {
|
||||
group.enter()
|
||||
self.caller.retrieveStories(hashes: chunk) { result in
|
||||
|
||||
switch result {
|
||||
case .success((let stories, _)):
|
||||
self.processStories(account: account, stories: stories) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
switch result {
|
||||
case .success((let stories, _)):
|
||||
self.processStories(account: account, stories: stories) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Refresh missing stories failed: %@.", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
case .failure(let error):
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Refresh missing stories failed: %@.", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
self.refreshProgress.completeTask()
|
||||
os_log(.debug, log: self.log, "Done refreshing missing stories.")
|
||||
if errorOccurred {
|
||||
completion(.failure(NewsBlurError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
switch result {
|
||||
case .success(let fetchedArticleIDs):
|
||||
process(fetchedArticleIDs)
|
||||
case .failure(let error):
|
||||
self.refreshProgress.completeTask()
|
||||
os_log(.debug, log: self.log, "Done refreshing missing stories.")
|
||||
if errorOccurred {
|
||||
completion(.failure(NewsBlurError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let fetchedArticleIDs):
|
||||
process(fetchedArticleIDs)
|
||||
case .failure(let error):
|
||||
self.refreshProgress.completeTask()
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -591,10 +596,12 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||
|
||||
self.database.insertStatuses(syncStatuses) { _ in
|
||||
self.database.selectPendingCount { result in
|
||||
if let count = try? result.get(), count > 100 {
|
||||
self.sendArticleStatus(for: account) { _ in }
|
||||
MainActor.assumeIsolated {
|
||||
if let count = try? result.get(), count > 100 {
|
||||
self.sendArticleStatus(for: account) { _ in }
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
|
@ -118,12 +118,14 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
switch result {
|
||||
case .success(let articleIDs):
|
||||
account.markAsRead(Set(articleIDs)) { _ in
|
||||
self.refreshArticleStatus(for: account) { _ in
|
||||
self.refreshProgress.completeTask()
|
||||
self.refreshMissingArticles(account) {
|
||||
self.refreshProgress.clear()
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
MainActor.assumeIsolated {
|
||||
self.refreshArticleStatus(for: account) { _ in
|
||||
self.refreshProgress.completeTask()
|
||||
self.refreshMissingArticles(account) {
|
||||
self.refreshProgress.clear()
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -201,45 +203,48 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
|
||||
database.selectForProcessing { result in
|
||||
|
||||
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 }
|
||||
let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false }
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
group.enter()
|
||||
self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) {
|
||||
group.leave()
|
||||
MainActor.assumeIsolated {
|
||||
|
||||
@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 }
|
||||
let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false }
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
group.enter()
|
||||
self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendArticleStatuses(createStarredStatuses, apiCall: self.caller.createStarredEntries) {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendArticleStatuses(deleteStarredStatuses, apiCall: self.caller.deleteStarredEntries) {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
os_log(.debug, log: self.log, "Done sending article statuses.")
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) {
|
||||
group.leave()
|
||||
|
||||
switch result {
|
||||
case .success(let syncStatuses):
|
||||
processStatuses(syncStatuses)
|
||||
case .failure(let databaseError):
|
||||
completion(.failure(databaseError))
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendArticleStatuses(createStarredStatuses, apiCall: self.caller.createStarredEntries) {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendArticleStatuses(deleteStarredStatuses, apiCall: self.caller.deleteStarredEntries) {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
os_log(.debug, log: self.log, "Done sending article statuses.")
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let syncStatuses):
|
||||
processStatuses(syncStatuses)
|
||||
case .failure(let databaseError):
|
||||
completion(.failure(databaseError))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -617,10 +622,12 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
|
||||
self.database.insertStatuses(syncStatuses) { _ in
|
||||
self.database.selectPendingCount { result in
|
||||
if let count = try? result.get(), count > 100 {
|
||||
self.sendArticleStatus(for: account) { _ in }
|
||||
MainActor.assumeIsolated {
|
||||
if let count = try? result.get(), count > 100 {
|
||||
self.sendArticleStatus(for: account) { _ in }
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
@ -970,18 +977,19 @@ private extension ReaderAPIAccountDelegate {
|
||||
switch result {
|
||||
case .success(let articleIDs):
|
||||
account.markAsRead(Set(articleIDs)) { _ in
|
||||
self.refreshProgress.completeTask()
|
||||
self.refreshArticleStatus(for: account) { _ in
|
||||
MainActor.assumeIsolated {
|
||||
self.refreshProgress.completeTask()
|
||||
self.refreshMissingArticles(account) {
|
||||
self.refreshProgress.clear()
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(feed))
|
||||
self.refreshArticleStatus(for: account) { _ in
|
||||
self.refreshProgress.completeTask()
|
||||
self.refreshMissingArticles(account) {
|
||||
self.refreshProgress.clear()
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(feed))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
@ -994,52 +1002,54 @@ private extension ReaderAPIAccountDelegate {
|
||||
func refreshMissingArticles(_ account: Account, completion: @escaping VoidCompletionBlock) {
|
||||
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { articleIDsResult in
|
||||
|
||||
func process(_ fetchedArticleIDs: Set<String>) {
|
||||
guard !fetchedArticleIDs.isEmpty else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
os_log(.debug, log: self.log, "Refreshing missing articles...")
|
||||
let group = DispatchGroup()
|
||||
MainActor.assumeIsolated {
|
||||
@MainActor func process(_ fetchedArticleIDs: Set<String>) {
|
||||
guard !fetchedArticleIDs.isEmpty else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
let articleIDs = Array(fetchedArticleIDs)
|
||||
let chunkedArticleIDs = articleIDs.chunked(into: 150)
|
||||
os_log(.debug, log: self.log, "Refreshing missing articles...")
|
||||
let group = DispatchGroup()
|
||||
|
||||
self.refreshProgress.addToNumberOfTasksAndRemaining(chunkedArticleIDs.count - 1)
|
||||
let articleIDs = Array(fetchedArticleIDs)
|
||||
let chunkedArticleIDs = articleIDs.chunked(into: 150)
|
||||
|
||||
for chunk in chunkedArticleIDs {
|
||||
group.enter()
|
||||
self.caller.retrieveEntries(articleIDs: chunk) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
self.refreshProgress.addToNumberOfTasksAndRemaining(chunkedArticleIDs.count - 1)
|
||||
|
||||
switch result {
|
||||
case .success(let entries):
|
||||
self.processEntries(account: account, entries: entries) {
|
||||
for chunk in chunkedArticleIDs {
|
||||
group.enter()
|
||||
self.caller.retrieveEntries(articleIDs: chunk) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success(let entries):
|
||||
self.processEntries(account: account, entries: entries) {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Refresh missing articles failed: %@.", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Refresh missing articles failed: %@.", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
self.refreshProgress.completeTask()
|
||||
os_log(.debug, log: self.log, "Done refreshing missing articles.")
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
switch articleIDsResult {
|
||||
case .success(let articleIDs):
|
||||
process(articleIDs)
|
||||
case .failure:
|
||||
self.refreshProgress.completeTask()
|
||||
os_log(.debug, log: self.log, "Done refreshing missing articles.")
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
switch articleIDsResult {
|
||||
case .success(let articleIDs):
|
||||
process(articleIDs)
|
||||
case .failure:
|
||||
self.refreshProgress.completeTask()
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1100,43 +1110,47 @@ private extension ReaderAPIAccountDelegate {
|
||||
|
||||
database.selectPendingReadStatusArticleIDs() { result in
|
||||
|
||||
func process(_ pendingArticleIDs: Set<String>) {
|
||||
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
|
||||
|
||||
account.fetchUnreadArticleIDs { articleIDsResult in
|
||||
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
|
||||
return
|
||||
}
|
||||
MainActor.assumeIsolated {
|
||||
@MainActor func process(_ pendingArticleIDs: Set<String>) {
|
||||
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
// Mark articles as unread
|
||||
let deltaUnreadArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentUnreadArticleIDs)
|
||||
group.enter()
|
||||
account.markAsUnread(deltaUnreadArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
account.fetchUnreadArticleIDs { articleIDsResult in
|
||||
|
||||
// Mark articles as read
|
||||
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableReaderUnreadArticleIDs)
|
||||
group.enter()
|
||||
account.markAsRead(deltaReadArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion()
|
||||
MainActor.assumeIsolated {
|
||||
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
|
||||
return
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
// Mark articles as unread
|
||||
let deltaUnreadArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentUnreadArticleIDs)
|
||||
group.enter()
|
||||
account.markAsUnread(deltaUnreadArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
// Mark articles as read
|
||||
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableReaderUnreadArticleIDs)
|
||||
group.enter()
|
||||
account.markAsRead(deltaReadArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let pendingArticleIDs):
|
||||
process(pendingArticleIDs)
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Sync Article Read Status failed: %@.", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let pendingArticleIDs):
|
||||
process(pendingArticleIDs)
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Sync Article Read Status failed: %@.", error.localizedDescription)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -1149,43 +1163,47 @@ private extension ReaderAPIAccountDelegate {
|
||||
|
||||
database.selectPendingStarredStatusArticleIDs() { result in
|
||||
|
||||
func process(_ pendingArticleIDs: Set<String>) {
|
||||
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
|
||||
MainActor.assumeIsolated {
|
||||
@MainActor func process(_ pendingArticleIDs: Set<String>) {
|
||||
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
|
||||
|
||||
account.fetchStarredArticleIDs { articleIDsResult in
|
||||
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
|
||||
return
|
||||
}
|
||||
account.fetchStarredArticleIDs { articleIDsResult in
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
// Mark articles as starred
|
||||
let deltaStarredArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentStarredArticleIDs)
|
||||
group.enter()
|
||||
account.markAsStarred(deltaStarredArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
MainActor.assumeIsolated {
|
||||
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
|
||||
return
|
||||
}
|
||||
|
||||
// Mark articles as unstarred
|
||||
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableReaderUnreadArticleIDs)
|
||||
group.enter()
|
||||
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
let group = DispatchGroup()
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion()
|
||||
// Mark articles as starred
|
||||
let deltaStarredArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentStarredArticleIDs)
|
||||
group.enter()
|
||||
account.markAsStarred(deltaStarredArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
// Mark articles as unstarred
|
||||
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableReaderUnreadArticleIDs)
|
||||
group.enter()
|
||||
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let pendingArticleIDs):
|
||||
process(pendingArticleIDs)
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Sync Article Starred Status failed: %@.", error.localizedDescription)
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let pendingArticleIDs):
|
||||
process(pendingArticleIDs)
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Sync Article Starred Status failed: %@.", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -257,7 +257,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
|
||||
|
@ -17,12 +17,12 @@ extension Notification.Name {
|
||||
|
||||
public protocol DisplayNameProvider {
|
||||
|
||||
var nameForDisplay: String { get }
|
||||
@MainActor var nameForDisplay: String { get }
|
||||
}
|
||||
|
||||
public extension DisplayNameProvider {
|
||||
|
||||
func postDisplayNameDidChangeNotification() {
|
||||
@MainActor func postDisplayNameDidChangeNotification() {
|
||||
|
||||
NotificationCenter.default.post(name: .DisplayNameDidChange, object: self, userInfo: nil)
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ public protocol MainThreadOperation: AnyObject {
|
||||
///
|
||||
/// The completionBlock is always called on the main thread.
|
||||
/// The queue will clear the completionBlock after calling it.
|
||||
var completionBlock: MainThreadOperationCompletionBlock? { get set }
|
||||
@MainActor var completionBlock: MainThreadOperationCompletionBlock? { get set }
|
||||
|
||||
/// Do the thing this operation does.
|
||||
///
|
||||
|
@ -153,7 +153,7 @@ extension Feed: PasteboardWriterOwner {
|
||||
}
|
||||
}
|
||||
|
||||
@objc final class FeedPasteboardWriter: NSObject, NSPasteboardWriting {
|
||||
@MainActor @objc final class FeedPasteboardWriter: NSObject, NSPasteboardWriting {
|
||||
|
||||
private let feed: Feed
|
||||
static let feedUTI = "com.ranchero.feed"
|
||||
|
@ -12,8 +12,8 @@ import AppKitExtras
|
||||
|
||||
typealias PasteboardFolderDictionary = [String: String]
|
||||
|
||||
struct PasteboardFolder: Hashable {
|
||||
|
||||
@MainActor struct PasteboardFolder: Hashable {
|
||||
|
||||
private struct Key {
|
||||
static let name = "name"
|
||||
// Internal
|
||||
@ -91,7 +91,7 @@ extension Folder: PasteboardWriterOwner {
|
||||
}
|
||||
}
|
||||
|
||||
@objc final class FolderPasteboardWriter: NSObject, NSPasteboardWriting {
|
||||
@MainActor @objc final class FolderPasteboardWriter: NSObject, NSPasteboardWriting {
|
||||
|
||||
private let folder: Folder
|
||||
static let folderUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.folder"
|
||||
|
@ -12,7 +12,7 @@ import Articles
|
||||
import Core
|
||||
|
||||
@objc(ScriptableAccount)
|
||||
class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
|
||||
@MainActor class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
|
||||
|
||||
let account:Account
|
||||
init (_ account:Account) {
|
||||
|
@ -44,7 +44,7 @@ import Articles
|
||||
// I am not sure if account should prefer to be specified by name or by ID
|
||||
// but in either case it seems like the accountID would be used as the keydata, so I chose ID
|
||||
@objc(uniqueId)
|
||||
var scriptingUniqueId:Any {
|
||||
@MainActor var scriptingUniqueId:Any {
|
||||
return feed.feedID
|
||||
}
|
||||
|
||||
@ -71,7 +71,7 @@ import Articles
|
||||
return url
|
||||
}
|
||||
|
||||
class func scriptableFeed(_ feed:Feed, account:Account, folder:Folder?) -> ScriptableFeed {
|
||||
@MainActor class func scriptableFeed(_ feed:Feed, account:Account, folder:Folder?) -> ScriptableFeed {
|
||||
let scriptableAccount = ScriptableAccount(account)
|
||||
if let folder = folder {
|
||||
let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount)
|
||||
@ -120,27 +120,27 @@ import Articles
|
||||
// MARK: --- Scriptable properties ---
|
||||
|
||||
@objc(url)
|
||||
var url:String {
|
||||
@MainActor var url:String {
|
||||
return self.feed.url
|
||||
}
|
||||
|
||||
@objc(name)
|
||||
var name:String {
|
||||
@MainActor var name:String {
|
||||
return self.feed.name ?? ""
|
||||
}
|
||||
|
||||
@objc(homePageURL)
|
||||
var homePageURL:String {
|
||||
@MainActor var homePageURL:String {
|
||||
return self.feed.homePageURL ?? ""
|
||||
}
|
||||
|
||||
@objc(iconURL)
|
||||
var iconURL:String {
|
||||
@MainActor var iconURL:String {
|
||||
return self.feed.iconURL ?? ""
|
||||
}
|
||||
|
||||
@objc(faviconURL)
|
||||
var faviconURL:String {
|
||||
@MainActor var faviconURL:String {
|
||||
return self.feed.faviconURL ?? ""
|
||||
}
|
||||
|
||||
@ -152,13 +152,13 @@ import Articles
|
||||
// MARK: --- scriptable elements ---
|
||||
|
||||
@objc(authors)
|
||||
var authors:NSArray {
|
||||
@MainActor var authors:NSArray {
|
||||
let feedAuthors = feed.authors ?? []
|
||||
return feedAuthors.map { ScriptableAuthor($0, container:self) } as NSArray
|
||||
}
|
||||
|
||||
@objc(valueInAuthorsWithUniqueID:)
|
||||
func valueInAuthors(withUniqueID id:String) -> ScriptableAuthor? {
|
||||
@MainActor func valueInAuthors(withUniqueID id:String) -> ScriptableAuthor? {
|
||||
guard let author = feed.authors?.first(where:{$0.authorID == id}) else { return nil }
|
||||
return ScriptableAuthor(author, container:self)
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import Articles
|
||||
import Core
|
||||
|
||||
@objc(ScriptableFolder)
|
||||
class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
|
||||
@MainActor class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
|
||||
|
||||
let folder:Folder
|
||||
let container:ScriptingObjectContainer
|
||||
|
@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import Account
|
||||
|
||||
struct AddFeedDefaultContainer {
|
||||
@MainActor struct AddFeedDefaultContainer {
|
||||
|
||||
@MainActor static var defaultContainer: Container? {
|
||||
|
||||
|
@ -71,7 +71,7 @@ final class FaviconDownloader {
|
||||
cache = [Feed: IconImage]()
|
||||
}
|
||||
|
||||
func favicon(for feed: Feed) -> IconImage? {
|
||||
@MainActor func favicon(for feed: Feed) -> IconImage? {
|
||||
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
@ -93,7 +93,7 @@ final class FaviconDownloader {
|
||||
return nil
|
||||
}
|
||||
|
||||
func faviconAsIcon(for feed: Feed) -> IconImage? {
|
||||
@MainActor func faviconAsIcon(for feed: Feed) -> IconImage? {
|
||||
|
||||
if let image = cache[feed] {
|
||||
return image
|
||||
|
@ -37,7 +37,7 @@ struct ExtensionContainers: Codable {
|
||||
|
||||
}
|
||||
|
||||
struct ExtensionAccount: ExtensionContainer {
|
||||
@MainActor struct ExtensionAccount: ExtensionContainer {
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
@ -70,7 +70,7 @@ struct ExtensionAccount: ExtensionContainer {
|
||||
|
||||
}
|
||||
|
||||
struct ExtensionFolder: ExtensionContainer {
|
||||
@MainActor struct ExtensionFolder: ExtensionContainer {
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accountName
|
||||
|
@ -9,7 +9,7 @@
|
||||
import AppKit
|
||||
import Account
|
||||
|
||||
@objc final class SmartFeedPasteboardWriter: NSObject, NSPasteboardWriting {
|
||||
@MainActor @objc final class SmartFeedPasteboardWriter: NSObject, NSPasteboardWriting {
|
||||
|
||||
private let smartFeed: PseudoFeed
|
||||
|
||||
|
@ -124,7 +124,7 @@ public extension SyncDatabase {
|
||||
|
||||
nonisolated func insertStatuses(_ statuses: [SyncStatus], completion: @escaping DatabaseCompletionBlock) {
|
||||
|
||||
Task {
|
||||
Task { @MainActor in
|
||||
do {
|
||||
try await self.insertStatuses(statuses)
|
||||
completion(nil)
|
||||
@ -136,7 +136,7 @@ public extension SyncDatabase {
|
||||
|
||||
nonisolated func selectForProcessing(limit: Int? = nil, completion: @escaping SyncStatusesCompletionBlock) {
|
||||
|
||||
Task {
|
||||
Task { @MainActor in
|
||||
do {
|
||||
if let syncStatuses = try await self.selectForProcessing(limit: limit) {
|
||||
completion(.success(Array(syncStatuses)))
|
||||
@ -151,7 +151,7 @@ public extension SyncDatabase {
|
||||
|
||||
nonisolated func selectPendingCount(completion: @escaping DatabaseIntCompletionBlock) {
|
||||
|
||||
Task {
|
||||
Task { @MainActor in
|
||||
do {
|
||||
if let count = try await self.selectPendingCount() {
|
||||
completion(.success(count))
|
||||
@ -167,7 +167,7 @@ public extension SyncDatabase {
|
||||
|
||||
nonisolated func selectPendingReadStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) {
|
||||
|
||||
Task {
|
||||
Task { @MainActor in
|
||||
do {
|
||||
if let articleIDs = try await self.selectPendingReadStatusArticleIDs() {
|
||||
completion(.success(articleIDs))
|
||||
@ -182,7 +182,7 @@ public extension SyncDatabase {
|
||||
|
||||
nonisolated func selectPendingStarredStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) {
|
||||
|
||||
Task {
|
||||
Task { @MainActor in
|
||||
do {
|
||||
if let articleIDs = try await self.selectPendingStarredStatusArticleIDs() {
|
||||
completion(.success(articleIDs))
|
||||
@ -197,7 +197,7 @@ public extension SyncDatabase {
|
||||
|
||||
nonisolated func resetAllSelectedForProcessing(completion: DatabaseCompletionBlock? = nil) {
|
||||
|
||||
Task {
|
||||
Task { @MainActor in
|
||||
do {
|
||||
try await self.resetAllSelectedForProcessing()
|
||||
completion?(nil)
|
||||
@ -209,7 +209,7 @@ public extension SyncDatabase {
|
||||
|
||||
nonisolated func resetSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) {
|
||||
|
||||
Task {
|
||||
Task { @MainActor in
|
||||
do {
|
||||
try await self.resetSelectedForProcessing(articleIDs)
|
||||
completion?(nil)
|
||||
|
Loading…
x
Reference in New Issue
Block a user