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>)
|
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 struct UserInfoKey {
|
||||||
public static let account = "account" // UserDidAddAccount, UserDidDeleteAccount
|
public static let account = "account" // UserDidAddAccount, UserDidDeleteAccount
|
||||||
@ -894,12 +894,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
database.createStatusesIfNeeded(articleIDs: articleIDs) { error in
|
database.createStatusesIfNeeded(articleIDs: articleIDs) { error in
|
||||||
if let error = error {
|
|
||||||
completion?(error)
|
MainActor.assumeIsolated {
|
||||||
return
|
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
|
return
|
||||||
}
|
}
|
||||||
database.mark(articleIDs: articleIDs, statusKey: statusKey, flag: flag) { error in
|
database.mark(articleIDs: articleIDs, statusKey: statusKey, flag: flag) { error in
|
||||||
if let error {
|
MainActor.assumeIsolated {
|
||||||
completion?(error)
|
if let error {
|
||||||
} else {
|
completion?(error)
|
||||||
self.noteStatusesForArticleIDsDidChange(articleIDs: articleIDs, statusKey: statusKey, flag: flag)
|
} else {
|
||||||
completion?(nil)
|
self.noteStatusesForArticleIDsDidChange(articleIDs: articleIDs, statusKey: statusKey, flag: flag)
|
||||||
|
completion?(nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import Articles
|
|||||||
import RSWeb
|
import RSWeb
|
||||||
import Secrets
|
import Secrets
|
||||||
|
|
||||||
protocol AccountDelegate {
|
@MainActor protocol AccountDelegate {
|
||||||
|
|
||||||
var behaviors: AccountBehaviors { get }
|
var behaviors: AccountBehaviors { get }
|
||||||
|
|
||||||
|
@ -14,18 +14,22 @@ public enum AccountError: LocalizedError {
|
|||||||
case createErrorNotFound
|
case createErrorNotFound
|
||||||
case createErrorAlreadySubscribed
|
case createErrorAlreadySubscribed
|
||||||
case opmlImportInProgress
|
case opmlImportInProgress
|
||||||
case wrappedError(error: Error, account: Account)
|
case wrappedError(error: Error, accountID: String, accountName: String)
|
||||||
|
|
||||||
public var account: Account? {
|
@MainActor public var account: Account? {
|
||||||
if case .wrappedError(_, let account) = self {
|
if case .wrappedError(_, let accountID, _) = self {
|
||||||
return account
|
return AccountManager.shared.existingAccount(with: accountID)
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var isCredentialsError: Bool {
|
@MainActor public static func wrappedError(error: Error, account: Account) -> AccountError {
|
||||||
if case .wrappedError(let error, _) = self {
|
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 {
|
if case TransportError.httpError(let status) = error {
|
||||||
return isCredentialsError(status: status)
|
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")
|
return NSLocalizedString("You are already subscribed to this feed and can’t add it again.", comment: "Already subscribed")
|
||||||
case .opmlImportInProgress:
|
case .opmlImportInProgress:
|
||||||
return NSLocalizedString("An OPML import for this account is already running.", comment: "Import running")
|
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 {
|
switch error {
|
||||||
case TransportError.httpError(let status):
|
case TransportError.httpError(let status):
|
||||||
if isCredentialsError(status: status) {
|
if isCredentialsError(status: status) {
|
||||||
let localizedText = NSLocalizedString("Your “%@” credentials are invalid or expired.", comment: "Invalid or expired")
|
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 {
|
} else {
|
||||||
return unknownError(error, account)
|
return unknownError(error, accountName)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return unknownError(error, account)
|
return unknownError(error, accountName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,7 +66,7 @@ public enum AccountError: LocalizedError {
|
|||||||
return nil
|
return nil
|
||||||
case .createErrorAlreadySubscribed:
|
case .createErrorAlreadySubscribed:
|
||||||
return nil
|
return nil
|
||||||
case .wrappedError(let error, _):
|
case .wrappedError(let error, _, _):
|
||||||
switch error {
|
switch error {
|
||||||
case TransportError.httpError(let status):
|
case TransportError.httpError(let status):
|
||||||
if isCredentialsError(status: status) {
|
if isCredentialsError(status: status) {
|
||||||
@ -84,9 +88,9 @@ public enum AccountError: LocalizedError {
|
|||||||
|
|
||||||
private extension AccountError {
|
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")
|
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 {
|
func isCredentialsError(status: Int) -> Bool {
|
||||||
|
@ -15,7 +15,7 @@ import Secrets
|
|||||||
|
|
||||||
// Main thread only.
|
// Main thread only.
|
||||||
|
|
||||||
public final class AccountManager: UnreadCountProvider {
|
@MainActor public final class AccountManager: UnreadCountProvider {
|
||||||
|
|
||||||
@MainActor public static var shared: AccountManager!
|
@MainActor public static var shared: AccountManager!
|
||||||
public static let netNewsWireNewsURL = "https://netnewswire.blog/feed.xml"
|
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")
|
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.insertStatuses(syncStatuses) { _ in
|
||||||
self.database.selectPendingCount { result in
|
self.database.selectPendingCount { result in
|
||||||
if let count = try? result.get(), count > 100 {
|
MainActor.assumeIsolated {
|
||||||
self.sendArticleStatus(for: account, showProgress: false) { _ in }
|
if let count = try? result.get(), count > 100 {
|
||||||
|
self.sendArticleStatus(for: account, showProgress: false) { _ in }
|
||||||
|
}
|
||||||
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
completion(.success(()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
@ -648,35 +650,36 @@ private extension CloudKitAccountDelegate {
|
|||||||
|
|
||||||
if let parsedFeed = parsedFeed {
|
if let parsedFeed = parsedFeed {
|
||||||
account.update(feed, with: parsedFeed) { result in
|
account.update(feed, with: parsedFeed) { result in
|
||||||
switch result {
|
MainActor.assumeIsolated {
|
||||||
case .success:
|
switch result {
|
||||||
|
case .success:
|
||||||
self.accountZone.createFeed(url: bestFeedSpecifier.urlString,
|
|
||||||
name: parsedFeed.title,
|
self.accountZone.createFeed(url: bestFeedSpecifier.urlString,
|
||||||
editedName: editedName,
|
name: parsedFeed.title,
|
||||||
homePageURL: parsedFeed.homePageURL,
|
editedName: editedName,
|
||||||
container: container) { result in
|
homePageURL: parsedFeed.homePageURL,
|
||||||
|
container: container) { result in
|
||||||
self.refreshProgress.completeTask()
|
|
||||||
switch result {
|
self.refreshProgress.completeTask()
|
||||||
case .success(let externalID):
|
switch result {
|
||||||
feed.externalID = externalID
|
case .success(let externalID):
|
||||||
self.sendNewArticlesToTheCloud(account, feed)
|
feed.externalID = externalID
|
||||||
completion(.success(feed))
|
self.sendNewArticlesToTheCloud(account, feed)
|
||||||
case .failure(let error):
|
completion(.success(feed))
|
||||||
container.removeFeed(feed)
|
case .failure(let error):
|
||||||
self.refreshProgress.completeTasks(2)
|
container.removeFeed(feed)
|
||||||
completion(.failure(error))
|
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 {
|
} else {
|
||||||
self.refreshProgress.completeTasks(3)
|
self.refreshProgress.completeTasks(3)
|
||||||
@ -700,9 +703,9 @@ private extension CloudKitAccountDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendNewArticlesToTheCloud(_ account: Account, _ feed: Feed) {
|
func sendNewArticlesToTheCloud(_ account: Account, _ feed: Feed) {
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let articles = try await account.articles(for: .feed(feed))
|
let articles = try await account.articles(for: .feed(feed))
|
||||||
self.storeArticleChanges(new: articles, updated: Set<Article>(), deleted: Set<Article>()) {
|
self.storeArticleChanges(new: articles, updated: Set<Article>(), deleted: Set<Article>()) {
|
||||||
@ -716,7 +719,7 @@ private extension CloudKitAccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
os_log(.error, log: self.log, "CloudKit Feed send articles error: %@.", error.localizedDescription)
|
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.")
|
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
|
var zoneID: CKRecordZone.ID
|
||||||
|
|
||||||
|
@ -13,8 +13,8 @@ import CloudKit
|
|||||||
import Articles
|
import Articles
|
||||||
import CloudKitExtras
|
import CloudKitExtras
|
||||||
|
|
||||||
class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
@MainActor final class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||||
|
|
||||||
private typealias UnclaimedFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String)
|
private typealias UnclaimedFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String)
|
||||||
private var newUnclaimedFeeds = [String: [UnclaimedFeed]]()
|
private var newUnclaimedFeeds = [String: [UnclaimedFeed]]()
|
||||||
private var existingUnclaimedFeeds = [String: [Feed]]()
|
private var existingUnclaimedFeeds = [String: [Feed]]()
|
||||||
@ -135,7 +135,6 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
|||||||
account?.removeFolder(folder)
|
account?.removeFolder(folder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension CloudKitAcountZoneDelegate {
|
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 {
|
guard let account = account, let articlesZone = articlesZone else {
|
||||||
completion(true)
|
completion(true)
|
||||||
return
|
return
|
||||||
@ -156,9 +156,11 @@ private extension CloudKitSendStatusOperation {
|
|||||||
}
|
}
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in
|
self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in
|
||||||
self.processAccountError(account, error)
|
MainActor.assumeIsolated {
|
||||||
os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription)
|
self.processAccountError(account, error)
|
||||||
completion(true)
|
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 {
|
if case CloudKitZoneError.userDeletedZone = error {
|
||||||
account.removeFeeds(account.topLevelFeeds)
|
account.removeFeeds(account.topLevelFeeds)
|
||||||
for folder in account.folders ?? Set<Folder>() {
|
for folder in account.folders ?? Set<Folder>() {
|
||||||
|
@ -15,20 +15,20 @@ extension Notification.Name {
|
|||||||
public static let ChildrenDidChange = Notification.Name("ChildrenDidChange")
|
public static let ChildrenDidChange = Notification.Name("ChildrenDidChange")
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol Container: AnyObject, ContainerIdentifiable {
|
@MainActor public protocol Container: AnyObject, ContainerIdentifiable {
|
||||||
|
|
||||||
var account: Account? { get }
|
var account: Account? { get }
|
||||||
var topLevelFeeds: Set<Feed> { get set }
|
var topLevelFeeds: Set<Feed> { get set }
|
||||||
var folders: Set<Folder>? { get set }
|
var folders: Set<Folder>? { get set }
|
||||||
var externalID: String? { get set }
|
var externalID: String? { get set }
|
||||||
|
|
||||||
func hasAtLeastOneFeed() -> Bool
|
func hasAtLeastOneFeed() -> Bool
|
||||||
func objectIsChild(_ object: AnyObject) -> Bool
|
func objectIsChild(_ object: AnyObject) -> Bool
|
||||||
|
|
||||||
func hasChildFolder(with: String) -> Bool
|
func hasChildFolder(with: String) -> Bool
|
||||||
func childFolder(with: String) -> Folder?
|
func childFolder(with: String) -> Folder?
|
||||||
|
|
||||||
func removeFeed(_ feed: Feed)
|
func removeFeed(_ feed: Feed)
|
||||||
func addFeed(_ feed: Feed)
|
func addFeed(_ feed: Feed)
|
||||||
|
|
||||||
//Recursive — checks subfolders
|
//Recursive — checks subfolders
|
||||||
|
@ -12,7 +12,7 @@ import Foundation
|
|||||||
// Mainly used with deleting objects and undo/redo.
|
// Mainly used with deleting objects and undo/redo.
|
||||||
// Especially redo. The idea is to put something back in the right place.
|
// 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 weak var account: Account?
|
||||||
private let names: [String] // empty if top-level of account
|
private let names: [String] // empty if top-level of account
|
||||||
|
@ -11,7 +11,7 @@ import RSWeb
|
|||||||
import Articles
|
import Articles
|
||||||
import Core
|
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 weak var account: Account?
|
||||||
public let url: String
|
public let url: String
|
||||||
@ -294,11 +294,11 @@ extension Feed: OPMLRepresentable {
|
|||||||
|
|
||||||
extension Set where Element == Feed {
|
extension Set where Element == Feed {
|
||||||
|
|
||||||
func feedIDs() -> Set<String> {
|
@MainActor func feedIDs() -> Set<String> {
|
||||||
return Set<String>(map { $0.feedID })
|
return Set<String>(map { $0.feedID })
|
||||||
}
|
}
|
||||||
|
|
||||||
func sorted() -> Array<Feed> {
|
@MainActor func sorted() -> Array<Feed> {
|
||||||
return sorted(by: { (feed1, feed2) -> Bool in
|
return sorted(by: { (feed1, feed2) -> Bool in
|
||||||
if feed1.nameForDisplay.localizedStandardCompare(feed2.nameForDisplay) == .orderedSame {
|
if feed1.nameForDisplay.localizedStandardCompare(feed2.nameForDisplay) == .orderedSame {
|
||||||
return feed1.url < feed2.url
|
return feed1.url < feed2.url
|
||||||
|
@ -136,62 +136,64 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||||||
|
|
||||||
database.selectForProcessing { result in
|
database.selectForProcessing { result in
|
||||||
|
|
||||||
func processStatuses(_ syncStatuses: [SyncStatus]) {
|
MainActor.assumeIsolated {
|
||||||
let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false }
|
@MainActor func processStatuses(_ syncStatuses: [SyncStatus]) {
|
||||||
let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true }
|
let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false }
|
||||||
let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true }
|
let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true }
|
||||||
let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false }
|
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
|
let group = DispatchGroup()
|
||||||
|
var errorOccurred = false
|
||||||
group.enter()
|
|
||||||
self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) { result in
|
group.enter()
|
||||||
group.leave()
|
self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) { result in
|
||||||
if case .failure = result {
|
group.leave()
|
||||||
errorOccurred = true
|
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()
|
switch result {
|
||||||
self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) { result in
|
case .success(let syncStatuses):
|
||||||
group.leave()
|
processStatuses(syncStatuses)
|
||||||
if case .failure = result {
|
case .failure(let databaseError):
|
||||||
errorOccurred = true
|
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.insertStatuses(syncStatuses) { _ in
|
||||||
self.database.selectPendingCount { result in
|
self.database.selectPendingCount { result in
|
||||||
if let count = try? result.get(), count > 100 {
|
MainActor.assumeIsolated {
|
||||||
self.sendArticleStatus(for: account) { _ in }
|
if let count = try? result.get(), count > 100 {
|
||||||
|
self.sendArticleStatus(for: account) { _ in }
|
||||||
|
}
|
||||||
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
completion(.success(()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
@ -1082,43 +1086,46 @@ private extension FeedbinAccountDelegate {
|
|||||||
case .success(let (entries, page)):
|
case .success(let (entries, page)):
|
||||||
|
|
||||||
self.processEntries(account: account, entries: entries) { error in
|
self.processEntries(account: account, entries: entries) { error in
|
||||||
|
|
||||||
|
MainActor.assumeIsolated {
|
||||||
if let error = error {
|
if let error = error {
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.refreshArticleStatus(for: account) { result in
|
self.refreshArticleStatus(for: account) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
|
|
||||||
self.refreshArticles(account, page: page, updateFetchDate: nil) { result in
|
self.refreshArticles(account, page: page, updateFetchDate: nil) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
|
|
||||||
self.refreshProgress.completeTask()
|
self.refreshProgress.completeTask()
|
||||||
self.refreshMissingArticles(account) { result in
|
self.refreshMissingArticles(account) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
|
|
||||||
self.refreshProgress.completeTask()
|
self.refreshProgress.completeTask()
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
completion(.success(feed))
|
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.processEntries(account: account, entries: entries) { error in
|
||||||
|
|
||||||
self.refreshProgress.completeTask()
|
|
||||||
|
|
||||||
if let error = error {
|
MainActor.assumeIsolated {
|
||||||
completion(.failure(error))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.refreshArticles(account, page: page, updateFetchDate: updateFetchDate) { result in
|
self.refreshProgress.completeTask()
|
||||||
os_log(.debug, log: self.log, "Done refreshing articles.")
|
|
||||||
switch result {
|
if let error = error {
|
||||||
case .success:
|
|
||||||
completion(.success(()))
|
|
||||||
case .failure(let error):
|
|
||||||
completion(.failure(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)):
|
case .success(let (entries, nextPage)):
|
||||||
|
|
||||||
self.processEntries(account: account, entries: entries) { error in
|
self.processEntries(account: account, entries: entries) { error in
|
||||||
self.refreshProgress.completeTask()
|
MainActor.assumeIsolated {
|
||||||
|
self.refreshProgress.completeTask()
|
||||||
|
|
||||||
if let error = error {
|
if let error = error {
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.refreshArticles(account, page: nextPage, updateFetchDate: updateFetchDate, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.refreshArticles(account, page: nextPage, updateFetchDate: updateFetchDate, completion: completion)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
@ -1294,47 +1306,50 @@ private extension FeedbinAccountDelegate {
|
|||||||
|
|
||||||
database.selectPendingReadStatusArticleIDs() { result in
|
database.selectPendingReadStatusArticleIDs() { result in
|
||||||
|
|
||||||
func process(_ pendingArticleIDs: Set<String>) {
|
MainActor.assumeIsolated {
|
||||||
|
|
||||||
let feedbinUnreadArticleIDs = Set(articleIDs.map { String($0) } )
|
@MainActor func process(_ pendingArticleIDs: Set<String>) {
|
||||||
let updatableFeedbinUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(pendingArticleIDs)
|
|
||||||
|
let feedbinUnreadArticleIDs = Set(articleIDs.map { String($0) } )
|
||||||
account.fetchUnreadArticleIDs { articleIDsResult in
|
let updatableFeedbinUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(pendingArticleIDs)
|
||||||
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
|
|
||||||
return
|
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
|
database.selectPendingStarredStatusArticleIDs() { result in
|
||||||
|
|
||||||
func process(_ pendingArticleIDs: Set<String>) {
|
MainActor.assumeIsolated {
|
||||||
|
@MainActor 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()
|
|
||||||
|
|
||||||
// Mark articles as starred
|
let feedbinStarredArticleIDs = Set(articleIDs.map { String($0) } )
|
||||||
let deltaStarredArticleIDs = updatableFeedbinStarredArticleIDs.subtracting(currentStarredArticleIDs)
|
let updatableFeedbinStarredArticleIDs = feedbinStarredArticleIDs.subtracting(pendingArticleIDs)
|
||||||
group.enter()
|
|
||||||
account.markAsStarred(deltaStarredArticleIDs) { _ in
|
account.fetchStarredArticleIDs { articleIDsResult in
|
||||||
group.leave()
|
|
||||||
}
|
MainActor.assumeIsolated {
|
||||||
|
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
|
||||||
// Mark articles as unstarred
|
return
|
||||||
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableFeedbinStarredArticleIDs)
|
}
|
||||||
group.enter()
|
|
||||||
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
|
let group = DispatchGroup()
|
||||||
group.leave()
|
|
||||||
}
|
// Mark articles as starred
|
||||||
|
let deltaStarredArticleIDs = updatableFeedbinStarredArticleIDs.subtracting(currentStarredArticleIDs)
|
||||||
group.notify(queue: DispatchQueue.main) {
|
group.enter()
|
||||||
completion()
|
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) {
|
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 {
|
guard let folder = container as? Folder, let collectionId = folder.externalID else {
|
||||||
return DispatchQueue.main.async {
|
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(()))
|
completion(.success(()))
|
||||||
case .failure:
|
case .failure:
|
||||||
from.addFeed(feed)
|
from.addFeed(feed)
|
||||||
completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed, from, to)))
|
completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed.nameForDisplay, from.nameForDisplay, to.nameForDisplay)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
|
@ -14,12 +14,12 @@ enum FeedlyAccountDelegateError: LocalizedError {
|
|||||||
case unableToAddFolder(String)
|
case unableToAddFolder(String)
|
||||||
case unableToRenameFolder(String, String)
|
case unableToRenameFolder(String, String)
|
||||||
case unableToRemoveFolder(String)
|
case unableToRemoveFolder(String)
|
||||||
case unableToMoveFeedBetweenFolders(Feed, Folder, Folder)
|
case unableToMoveFeedBetweenFolders(String, String, String)
|
||||||
case addFeedChooseFolder
|
case addFeedChooseFolder
|
||||||
case addFeedInvalidFolder(Folder)
|
case addFeedInvalidFolder(String)
|
||||||
case unableToRenameFeed(String, String)
|
case unableToRenameFeed(String, String)
|
||||||
case unableToRemoveFeed(Feed)
|
case unableToRemoveFeed(String)
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .notLoggedIn:
|
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.")
|
let template = NSLocalizedString("Could not remove the folder named “%@”.", comment: "Feedly – Could not remove a folder/collection.")
|
||||||
return String(format: template, name)
|
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.")
|
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:
|
case .addFeedChooseFolder:
|
||||||
return NSLocalizedString("Please choose a folder to contain the feed.", comment: "Feedly – Feed can only be added to folders.")
|
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.")
|
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):
|
case .unableToRenameFeed(let from, let to):
|
||||||
let template = NSLocalizedString("Could not rename “%@” to “%@”.", comment: "Feedly – Could not rename a feed.")
|
let template = NSLocalizedString("Could not rename “%@” to “%@”.", comment: "Feedly – Could not rename a feed.")
|
||||||
return String(format: template, from, to)
|
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.")
|
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:
|
case .unableToRemoveFolder:
|
||||||
return nil
|
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.")
|
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:
|
case .addFeedChooseFolder:
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct FeedlyFeedContainerValidator {
|
@MainActor struct FeedlyFeedContainerValidator {
|
||||||
var container: Container
|
var container: Container
|
||||||
|
|
||||||
func getValidContainer() throws -> (Folder, String) {
|
func getValidContainer() throws -> (Folder, String) {
|
||||||
@ -17,7 +17,7 @@ struct FeedlyFeedContainerValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard let collectionId = folder.externalID else {
|
guard let collectionId = folder.externalID else {
|
||||||
throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder)
|
throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder.nameForDisplay)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (folder, collectionId)
|
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")
|
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 {
|
public var isCanceled: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
|
@ -10,7 +10,7 @@ import Foundation
|
|||||||
import Articles
|
import Articles
|
||||||
import Core
|
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? {
|
public var containerID: ContainerIdentifier? {
|
||||||
guard let accountID = account?.accountID else {
|
guard let accountID = account?.accountID else {
|
||||||
@ -198,7 +198,7 @@ extension Folder: OPMLRepresentable {
|
|||||||
|
|
||||||
extension Set where Element == Folder {
|
extension Set where Element == Folder {
|
||||||
|
|
||||||
func sorted() -> Array<Folder> {
|
@MainActor func sorted() -> Array<Folder> {
|
||||||
return sorted(by: { (folder1, folder2) -> Bool in
|
return sorted(by: { (folder1, folder2) -> Bool in
|
||||||
return folder1.nameForDisplay.localizedStandardCompare(folder2.nameForDisplay) == .orderedAscending
|
return folder1.nameForDisplay.localizedStandardCompare(folder2.nameForDisplay) == .orderedAscending
|
||||||
})
|
})
|
||||||
|
@ -52,7 +52,7 @@ final class LocalAccountRefresher {
|
|||||||
|
|
||||||
extension LocalAccountRefresher: DownloadSessionDelegate {
|
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 {
|
guard let feed = representedObject as? Feed else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -68,7 +68,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
|
|||||||
return request
|
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
|
let feed = representedObject as! Feed
|
||||||
|
|
||||||
guard !data.isEmpty, !isSuspended else {
|
guard !data.isEmpty, !isSuspended else {
|
||||||
@ -101,18 +101,20 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
account.update(feed, with: parsedFeed) { result in
|
account.update(feed, with: parsedFeed) { result in
|
||||||
if case .success(let articleChanges) = result {
|
MainActor.assumeIsolated {
|
||||||
if let httpResponse = response as? HTTPURLResponse {
|
if case .success(let articleChanges) = result {
|
||||||
feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse)
|
if let httpResponse = response as? HTTPURLResponse {
|
||||||
}
|
feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse)
|
||||||
feed.contentHash = dataHash
|
}
|
||||||
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
feed.contentHash = dataHash
|
||||||
self.delegate?.localAccountRefresher(self, articleChanges: articleChanges) {
|
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||||
|
self.delegate?.localAccountRefresher(self, articleChanges: articleChanges) {
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
completion()
|
completion()
|
||||||
|
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
completion()
|
|
||||||
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -328,45 +328,49 @@ extension NewsBlurAccountDelegate {
|
|||||||
completion()
|
completion()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
database.selectPendingReadStatusArticleIDs() { result in
|
database.selectPendingReadStatusArticleIDs() { result in
|
||||||
func process(_ pendingStoryHashes: Set<String>) {
|
MainActor.assumeIsolated {
|
||||||
|
@MainActor 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()
|
|
||||||
|
|
||||||
// Mark articles as unread
|
let newsBlurUnreadStoryHashes = Set(hashes.map { $0.hash } )
|
||||||
let deltaUnreadArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentUnreadArticleIDs)
|
let updatableNewsBlurUnreadStoryHashes = newsBlurUnreadStoryHashes.subtracting(pendingStoryHashes)
|
||||||
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) {
|
account.fetchUnreadArticleIDs { articleIDsResult in
|
||||||
completion()
|
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 {
|
||||||
switch result {
|
case .success(let pendingArticleIDs):
|
||||||
case .success(let pendingArticleIDs):
|
process(pendingArticleIDs)
|
||||||
process(pendingArticleIDs)
|
case .failure(let error):
|
||||||
case .failure(let error):
|
os_log(.error, log: self.log, "Sync Story Read Status failed: %@.", error.localizedDescription)
|
||||||
os_log(.error, log: self.log, "Sync Story Read Status failed: %@.", error.localizedDescription)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -378,43 +382,47 @@ extension NewsBlurAccountDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
database.selectPendingStarredStatusArticleIDs() { result in
|
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 newsBlurStarredStoryHashes = Set(hashes.map { $0.hash } )
|
||||||
let updatableNewsBlurUnreadStoryHashes = newsBlurStarredStoryHashes.subtracting(pendingStoryHashes)
|
let updatableNewsBlurUnreadStoryHashes = newsBlurStarredStoryHashes.subtracting(pendingStoryHashes)
|
||||||
|
|
||||||
account.fetchStarredArticleIDs { articleIDsResult in
|
account.fetchStarredArticleIDs { articleIDsResult in
|
||||||
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
|
MainActor.assumeIsolated {
|
||||||
return
|
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
|
||||||
}
|
return
|
||||||
|
}
|
||||||
let group = DispatchGroup()
|
|
||||||
|
let group = DispatchGroup()
|
||||||
// Mark articles as starred
|
|
||||||
let deltaStarredArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentStarredArticleIDs)
|
// Mark articles as starred
|
||||||
group.enter()
|
let deltaStarredArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentStarredArticleIDs)
|
||||||
account.markAsStarred(deltaStarredArticleIDs) { _ in
|
group.enter()
|
||||||
group.leave()
|
account.markAsStarred(deltaStarredArticleIDs) { _ in
|
||||||
}
|
group.leave()
|
||||||
|
}
|
||||||
// Mark articles as unstarred
|
|
||||||
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes)
|
// Mark articles as unstarred
|
||||||
group.enter()
|
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes)
|
||||||
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
|
group.enter()
|
||||||
group.leave()
|
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
|
||||||
}
|
group.leave()
|
||||||
|
}
|
||||||
group.notify(queue: DispatchQueue.main) {
|
|
||||||
completion()
|
group.notify(queue: DispatchQueue.main) {
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let pendingArticleIDs):
|
case .success(let pendingArticleIDs):
|
||||||
process(pendingArticleIDs)
|
process(pendingArticleIDs)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
os_log(.error, log: self.log, "Sync Story Starred Status failed: %@.", error.localizedDescription)
|
os_log(.error, log: self.log, "Sync Story Starred Status failed: %@.", error.localizedDescription)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import Articles
|
import Articles
|
||||||
import Database
|
import Database
|
||||||
import RSParser
|
@preconcurrency import RSParser
|
||||||
import RSWeb
|
import RSWeb
|
||||||
import SyncDatabase
|
import SyncDatabase
|
||||||
import os.log
|
import os.log
|
||||||
@ -136,70 +136,73 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
|||||||
|
|
||||||
database.selectForProcessing { result in
|
database.selectForProcessing { result in
|
||||||
|
|
||||||
func processStatuses(_ syncStatuses: [SyncStatus]) {
|
MainActor.assumeIsolated {
|
||||||
let createUnreadStatuses = syncStatuses.filter {
|
|
||||||
$0.key == SyncStatus.Key.read && $0.flag == false
|
@MainActor func processStatuses(_ syncStatuses: [SyncStatus]) {
|
||||||
}
|
let createUnreadStatuses = syncStatuses.filter {
|
||||||
let deleteUnreadStatuses = syncStatuses.filter {
|
$0.key == SyncStatus.Key.read && $0.flag == false
|
||||||
$0.key == SyncStatus.Key.read && $0.flag == true
|
}
|
||||||
}
|
let deleteUnreadStatuses = syncStatuses.filter {
|
||||||
let createStarredStatuses = syncStatuses.filter {
|
$0.key == SyncStatus.Key.read && $0.flag == true
|
||||||
$0.key == SyncStatus.Key.starred && $0.flag == true
|
}
|
||||||
}
|
let createStarredStatuses = syncStatuses.filter {
|
||||||
let deleteStarredStatuses = syncStatuses.filter {
|
$0.key == SyncStatus.Key.starred && $0.flag == true
|
||||||
$0.key == SyncStatus.Key.starred && $0.flag == false
|
}
|
||||||
}
|
let deleteStarredStatuses = syncStatuses.filter {
|
||||||
|
$0.key == SyncStatus.Key.starred && $0.flag == false
|
||||||
let group = DispatchGroup()
|
}
|
||||||
var errorOccurred = false
|
|
||||||
|
let group = DispatchGroup()
|
||||||
group.enter()
|
var errorOccurred = false
|
||||||
self.sendStoryStatuses(createUnreadStatuses, throttle: true, apiCall: self.caller.markAsUnread) { result in
|
|
||||||
group.leave()
|
group.enter()
|
||||||
if case .failure = result {
|
self.sendStoryStatuses(createUnreadStatuses, throttle: true, apiCall: self.caller.markAsUnread) { result in
|
||||||
errorOccurred = true
|
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()
|
switch result {
|
||||||
self.sendStoryStatuses(deleteUnreadStatuses, throttle: false, apiCall: self.caller.markAsRead) { result in
|
case .success(let syncStatuses):
|
||||||
group.leave()
|
processStatuses(syncStatuses)
|
||||||
if case .failure = result {
|
case .failure(let databaseError):
|
||||||
errorOccurred = true
|
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
|
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in
|
||||||
|
|
||||||
func process(_ fetchedHashes: Set<String>) {
|
MainActor.assumeIsolated {
|
||||||
let group = DispatchGroup()
|
@MainActor func process(_ fetchedHashes: Set<String>) {
|
||||||
var errorOccurred = false
|
let group = DispatchGroup()
|
||||||
|
var errorOccurred = false
|
||||||
|
|
||||||
let storyHashes = Array(fetchedHashes).map {
|
let storyHashes = Array(fetchedHashes).map {
|
||||||
NewsBlurStoryHash(hash: $0, timestamp: Date())
|
NewsBlurStoryHash(hash: $0, timestamp: Date())
|
||||||
}
|
}
|
||||||
let chunkedStoryHashes = storyHashes.chunked(into: 100)
|
let chunkedStoryHashes = storyHashes.chunked(into: 100)
|
||||||
|
|
||||||
for chunk in chunkedStoryHashes {
|
for chunk in chunkedStoryHashes {
|
||||||
group.enter()
|
group.enter()
|
||||||
self.caller.retrieveStories(hashes: chunk) { result in
|
self.caller.retrieveStories(hashes: chunk) { result in
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success((let stories, _)):
|
case .success((let stories, _)):
|
||||||
self.processStories(account: account, stories: stories) { result in
|
self.processStories(account: account, stories: stories) { result in
|
||||||
group.leave()
|
group.leave()
|
||||||
if case .failure = result {
|
if case .failure = result {
|
||||||
errorOccurred = true
|
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()
|
self.refreshProgress.completeTask()
|
||||||
os_log(.debug, log: self.log, "Done refreshing missing stories.")
|
completion(.failure(error))
|
||||||
if errorOccurred {
|
|
||||||
completion(.failure(NewsBlurError.unknown))
|
|
||||||
} else {
|
|
||||||
completion(.success(()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.insertStatuses(syncStatuses) { _ in
|
||||||
self.database.selectPendingCount { result in
|
self.database.selectPendingCount { result in
|
||||||
if let count = try? result.get(), count > 100 {
|
MainActor.assumeIsolated {
|
||||||
self.sendArticleStatus(for: account) { _ in }
|
if let count = try? result.get(), count > 100 {
|
||||||
|
self.sendArticleStatus(for: account) { _ in }
|
||||||
|
}
|
||||||
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
completion(.success(()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
|
@ -118,12 +118,14 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
|||||||
switch result {
|
switch result {
|
||||||
case .success(let articleIDs):
|
case .success(let articleIDs):
|
||||||
account.markAsRead(Set(articleIDs)) { _ in
|
account.markAsRead(Set(articleIDs)) { _ in
|
||||||
self.refreshArticleStatus(for: account) { _ in
|
MainActor.assumeIsolated {
|
||||||
self.refreshProgress.completeTask()
|
self.refreshArticleStatus(for: account) { _ in
|
||||||
self.refreshMissingArticles(account) {
|
self.refreshProgress.completeTask()
|
||||||
self.refreshProgress.clear()
|
self.refreshMissingArticles(account) {
|
||||||
DispatchQueue.main.async {
|
self.refreshProgress.clear()
|
||||||
completion(.success(()))
|
DispatchQueue.main.async {
|
||||||
|
completion(.success(()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -201,45 +203,48 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
|||||||
|
|
||||||
database.selectForProcessing { result in
|
database.selectForProcessing { result in
|
||||||
|
|
||||||
func processStatuses(_ syncStatuses: [SyncStatus]) {
|
MainActor.assumeIsolated {
|
||||||
let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false }
|
|
||||||
let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true }
|
@MainActor func processStatuses(_ syncStatuses: [SyncStatus]) {
|
||||||
let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true }
|
let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false }
|
||||||
let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $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 group = DispatchGroup()
|
let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false }
|
||||||
|
|
||||||
group.enter()
|
let group = DispatchGroup()
|
||||||
self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) {
|
|
||||||
group.leave()
|
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()
|
switch result {
|
||||||
self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) {
|
case .success(let syncStatuses):
|
||||||
group.leave()
|
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.insertStatuses(syncStatuses) { _ in
|
||||||
self.database.selectPendingCount { result in
|
self.database.selectPendingCount { result in
|
||||||
if let count = try? result.get(), count > 100 {
|
MainActor.assumeIsolated {
|
||||||
self.sendArticleStatus(for: account) { _ in }
|
if let count = try? result.get(), count > 100 {
|
||||||
|
self.sendArticleStatus(for: account) { _ in }
|
||||||
|
}
|
||||||
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
completion(.success(()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
@ -970,18 +977,19 @@ private extension ReaderAPIAccountDelegate {
|
|||||||
switch result {
|
switch result {
|
||||||
case .success(let articleIDs):
|
case .success(let articleIDs):
|
||||||
account.markAsRead(Set(articleIDs)) { _ in
|
account.markAsRead(Set(articleIDs)) { _ in
|
||||||
self.refreshProgress.completeTask()
|
MainActor.assumeIsolated {
|
||||||
self.refreshArticleStatus(for: account) { _ in
|
|
||||||
self.refreshProgress.completeTask()
|
self.refreshProgress.completeTask()
|
||||||
self.refreshMissingArticles(account) {
|
self.refreshArticleStatus(for: account) { _ in
|
||||||
self.refreshProgress.clear()
|
self.refreshProgress.completeTask()
|
||||||
DispatchQueue.main.async {
|
self.refreshMissingArticles(account) {
|
||||||
completion(.success(feed))
|
self.refreshProgress.clear()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(.success(feed))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
@ -994,52 +1002,54 @@ private extension ReaderAPIAccountDelegate {
|
|||||||
func refreshMissingArticles(_ account: Account, completion: @escaping VoidCompletionBlock) {
|
func refreshMissingArticles(_ account: Account, completion: @escaping VoidCompletionBlock) {
|
||||||
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { articleIDsResult in
|
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { articleIDsResult in
|
||||||
|
|
||||||
func process(_ fetchedArticleIDs: Set<String>) {
|
MainActor.assumeIsolated {
|
||||||
guard !fetchedArticleIDs.isEmpty else {
|
@MainActor func process(_ fetchedArticleIDs: Set<String>) {
|
||||||
completion()
|
guard !fetchedArticleIDs.isEmpty else {
|
||||||
return
|
completion()
|
||||||
}
|
return
|
||||||
|
}
|
||||||
os_log(.debug, log: self.log, "Refreshing missing articles...")
|
|
||||||
let group = DispatchGroup()
|
|
||||||
|
|
||||||
let articleIDs = Array(fetchedArticleIDs)
|
os_log(.debug, log: self.log, "Refreshing missing articles...")
|
||||||
let chunkedArticleIDs = articleIDs.chunked(into: 150)
|
let group = DispatchGroup()
|
||||||
|
|
||||||
self.refreshProgress.addToNumberOfTasksAndRemaining(chunkedArticleIDs.count - 1)
|
let articleIDs = Array(fetchedArticleIDs)
|
||||||
|
let chunkedArticleIDs = articleIDs.chunked(into: 150)
|
||||||
|
|
||||||
for chunk in chunkedArticleIDs {
|
self.refreshProgress.addToNumberOfTasksAndRemaining(chunkedArticleIDs.count - 1)
|
||||||
group.enter()
|
|
||||||
self.caller.retrieveEntries(articleIDs: chunk) { result in
|
|
||||||
self.refreshProgress.completeTask()
|
|
||||||
|
|
||||||
switch result {
|
for chunk in chunkedArticleIDs {
|
||||||
case .success(let entries):
|
group.enter()
|
||||||
self.processEntries(account: account, entries: entries) {
|
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()
|
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()
|
self.refreshProgress.completeTask()
|
||||||
os_log(.debug, log: self.log, "Done refreshing missing articles.")
|
|
||||||
completion()
|
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
|
database.selectPendingReadStatusArticleIDs() { result in
|
||||||
|
|
||||||
func process(_ pendingArticleIDs: Set<String>) {
|
MainActor.assumeIsolated {
|
||||||
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
|
@MainActor func process(_ pendingArticleIDs: Set<String>) {
|
||||||
|
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
|
||||||
account.fetchUnreadArticleIDs { articleIDsResult in
|
|
||||||
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let group = DispatchGroup()
|
account.fetchUnreadArticleIDs { articleIDsResult in
|
||||||
|
|
||||||
// Mark articles as unread
|
|
||||||
let deltaUnreadArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentUnreadArticleIDs)
|
|
||||||
group.enter()
|
|
||||||
account.markAsUnread(deltaUnreadArticleIDs) { _ in
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark articles as read
|
MainActor.assumeIsolated {
|
||||||
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableReaderUnreadArticleIDs)
|
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
|
||||||
group.enter()
|
return
|
||||||
account.markAsRead(deltaReadArticleIDs) { _ in
|
}
|
||||||
group.leave()
|
|
||||||
}
|
let group = DispatchGroup()
|
||||||
|
|
||||||
group.notify(queue: DispatchQueue.main) {
|
// Mark articles as unread
|
||||||
completion()
|
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
|
database.selectPendingStarredStatusArticleIDs() { result in
|
||||||
|
|
||||||
func process(_ pendingArticleIDs: Set<String>) {
|
MainActor.assumeIsolated {
|
||||||
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
|
@MainActor func process(_ pendingArticleIDs: Set<String>) {
|
||||||
|
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
|
||||||
|
|
||||||
account.fetchStarredArticleIDs { articleIDsResult in
|
account.fetchStarredArticleIDs { articleIDsResult in
|
||||||
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let group = DispatchGroup()
|
MainActor.assumeIsolated {
|
||||||
|
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
|
||||||
// Mark articles as starred
|
return
|
||||||
let deltaStarredArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentStarredArticleIDs)
|
}
|
||||||
group.enter()
|
|
||||||
account.markAsStarred(deltaStarredArticleIDs) { _ in
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark articles as unstarred
|
let group = DispatchGroup()
|
||||||
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableReaderUnreadArticleIDs)
|
|
||||||
group.enter()
|
|
||||||
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
|
|
||||||
group.notify(queue: DispatchQueue.main) {
|
// Mark articles as starred
|
||||||
completion()
|
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 {
|
guard let baseURL = apiBaseURL else {
|
||||||
completion(.failure(CredentialsError.incompleteCredentials))
|
completion(.failure(CredentialsError.incompleteCredentials))
|
||||||
return
|
return
|
||||||
|
@ -17,12 +17,12 @@ extension Notification.Name {
|
|||||||
|
|
||||||
public protocol DisplayNameProvider {
|
public protocol DisplayNameProvider {
|
||||||
|
|
||||||
var nameForDisplay: String { get }
|
@MainActor var nameForDisplay: String { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension DisplayNameProvider {
|
public extension DisplayNameProvider {
|
||||||
|
|
||||||
func postDisplayNameDidChangeNotification() {
|
@MainActor func postDisplayNameDidChangeNotification() {
|
||||||
|
|
||||||
NotificationCenter.default.post(name: .DisplayNameDidChange, object: self, userInfo: nil)
|
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 completionBlock is always called on the main thread.
|
||||||
/// The queue will clear the completionBlock after calling it.
|
/// 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.
|
/// 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
|
private let feed: Feed
|
||||||
static let feedUTI = "com.ranchero.feed"
|
static let feedUTI = "com.ranchero.feed"
|
||||||
|
@ -12,8 +12,8 @@ import AppKitExtras
|
|||||||
|
|
||||||
typealias PasteboardFolderDictionary = [String: String]
|
typealias PasteboardFolderDictionary = [String: String]
|
||||||
|
|
||||||
struct PasteboardFolder: Hashable {
|
@MainActor struct PasteboardFolder: Hashable {
|
||||||
|
|
||||||
private struct Key {
|
private struct Key {
|
||||||
static let name = "name"
|
static let name = "name"
|
||||||
// Internal
|
// 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
|
private let folder: Folder
|
||||||
static let folderUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.folder"
|
static let folderUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.folder"
|
||||||
|
@ -12,7 +12,7 @@ import Articles
|
|||||||
import Core
|
import Core
|
||||||
|
|
||||||
@objc(ScriptableAccount)
|
@objc(ScriptableAccount)
|
||||||
class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
|
@MainActor class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
|
||||||
|
|
||||||
let account:Account
|
let account:Account
|
||||||
init (_ 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
|
// 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
|
// but in either case it seems like the accountID would be used as the keydata, so I chose ID
|
||||||
@objc(uniqueId)
|
@objc(uniqueId)
|
||||||
var scriptingUniqueId:Any {
|
@MainActor var scriptingUniqueId:Any {
|
||||||
return feed.feedID
|
return feed.feedID
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ import Articles
|
|||||||
return url
|
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)
|
let scriptableAccount = ScriptableAccount(account)
|
||||||
if let folder = folder {
|
if let folder = folder {
|
||||||
let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount)
|
let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount)
|
||||||
@ -120,27 +120,27 @@ import Articles
|
|||||||
// MARK: --- Scriptable properties ---
|
// MARK: --- Scriptable properties ---
|
||||||
|
|
||||||
@objc(url)
|
@objc(url)
|
||||||
var url:String {
|
@MainActor var url:String {
|
||||||
return self.feed.url
|
return self.feed.url
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc(name)
|
@objc(name)
|
||||||
var name:String {
|
@MainActor var name:String {
|
||||||
return self.feed.name ?? ""
|
return self.feed.name ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc(homePageURL)
|
@objc(homePageURL)
|
||||||
var homePageURL:String {
|
@MainActor var homePageURL:String {
|
||||||
return self.feed.homePageURL ?? ""
|
return self.feed.homePageURL ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc(iconURL)
|
@objc(iconURL)
|
||||||
var iconURL:String {
|
@MainActor var iconURL:String {
|
||||||
return self.feed.iconURL ?? ""
|
return self.feed.iconURL ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc(faviconURL)
|
@objc(faviconURL)
|
||||||
var faviconURL:String {
|
@MainActor var faviconURL:String {
|
||||||
return self.feed.faviconURL ?? ""
|
return self.feed.faviconURL ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,13 +152,13 @@ import Articles
|
|||||||
// MARK: --- scriptable elements ---
|
// MARK: --- scriptable elements ---
|
||||||
|
|
||||||
@objc(authors)
|
@objc(authors)
|
||||||
var authors:NSArray {
|
@MainActor var authors:NSArray {
|
||||||
let feedAuthors = feed.authors ?? []
|
let feedAuthors = feed.authors ?? []
|
||||||
return feedAuthors.map { ScriptableAuthor($0, container:self) } as NSArray
|
return feedAuthors.map { ScriptableAuthor($0, container:self) } as NSArray
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc(valueInAuthorsWithUniqueID:)
|
@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 }
|
guard let author = feed.authors?.first(where:{$0.authorID == id}) else { return nil }
|
||||||
return ScriptableAuthor(author, container:self)
|
return ScriptableAuthor(author, container:self)
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import Articles
|
|||||||
import Core
|
import Core
|
||||||
|
|
||||||
@objc(ScriptableFolder)
|
@objc(ScriptableFolder)
|
||||||
class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
|
@MainActor class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
|
||||||
|
|
||||||
let folder:Folder
|
let folder:Folder
|
||||||
let container:ScriptingObjectContainer
|
let container:ScriptingObjectContainer
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Account
|
import Account
|
||||||
|
|
||||||
struct AddFeedDefaultContainer {
|
@MainActor struct AddFeedDefaultContainer {
|
||||||
|
|
||||||
@MainActor static var defaultContainer: Container? {
|
@MainActor static var defaultContainer: Container? {
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ final class FaviconDownloader {
|
|||||||
cache = [Feed: IconImage]()
|
cache = [Feed: IconImage]()
|
||||||
}
|
}
|
||||||
|
|
||||||
func favicon(for feed: Feed) -> IconImage? {
|
@MainActor func favicon(for feed: Feed) -> IconImage? {
|
||||||
|
|
||||||
assert(Thread.isMainThread)
|
assert(Thread.isMainThread)
|
||||||
|
|
||||||
@ -93,7 +93,7 @@ final class FaviconDownloader {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func faviconAsIcon(for feed: Feed) -> IconImage? {
|
@MainActor func faviconAsIcon(for feed: Feed) -> IconImage? {
|
||||||
|
|
||||||
if let image = cache[feed] {
|
if let image = cache[feed] {
|
||||||
return image
|
return image
|
||||||
|
@ -37,7 +37,7 @@ struct ExtensionContainers: Codable {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ExtensionAccount: ExtensionContainer {
|
@MainActor struct ExtensionAccount: ExtensionContainer {
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case name
|
case name
|
||||||
@ -70,7 +70,7 @@ struct ExtensionAccount: ExtensionContainer {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ExtensionFolder: ExtensionContainer {
|
@MainActor struct ExtensionFolder: ExtensionContainer {
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case accountName
|
case accountName
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
import AppKit
|
import AppKit
|
||||||
import Account
|
import Account
|
||||||
|
|
||||||
@objc final class SmartFeedPasteboardWriter: NSObject, NSPasteboardWriting {
|
@MainActor @objc final class SmartFeedPasteboardWriter: NSObject, NSPasteboardWriting {
|
||||||
|
|
||||||
private let smartFeed: PseudoFeed
|
private let smartFeed: PseudoFeed
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ public extension SyncDatabase {
|
|||||||
|
|
||||||
nonisolated func insertStatuses(_ statuses: [SyncStatus], completion: @escaping DatabaseCompletionBlock) {
|
nonisolated func insertStatuses(_ statuses: [SyncStatus], completion: @escaping DatabaseCompletionBlock) {
|
||||||
|
|
||||||
Task {
|
Task { @MainActor in
|
||||||
do {
|
do {
|
||||||
try await self.insertStatuses(statuses)
|
try await self.insertStatuses(statuses)
|
||||||
completion(nil)
|
completion(nil)
|
||||||
@ -136,7 +136,7 @@ public extension SyncDatabase {
|
|||||||
|
|
||||||
nonisolated func selectForProcessing(limit: Int? = nil, completion: @escaping SyncStatusesCompletionBlock) {
|
nonisolated func selectForProcessing(limit: Int? = nil, completion: @escaping SyncStatusesCompletionBlock) {
|
||||||
|
|
||||||
Task {
|
Task { @MainActor in
|
||||||
do {
|
do {
|
||||||
if let syncStatuses = try await self.selectForProcessing(limit: limit) {
|
if let syncStatuses = try await self.selectForProcessing(limit: limit) {
|
||||||
completion(.success(Array(syncStatuses)))
|
completion(.success(Array(syncStatuses)))
|
||||||
@ -151,7 +151,7 @@ public extension SyncDatabase {
|
|||||||
|
|
||||||
nonisolated func selectPendingCount(completion: @escaping DatabaseIntCompletionBlock) {
|
nonisolated func selectPendingCount(completion: @escaping DatabaseIntCompletionBlock) {
|
||||||
|
|
||||||
Task {
|
Task { @MainActor in
|
||||||
do {
|
do {
|
||||||
if let count = try await self.selectPendingCount() {
|
if let count = try await self.selectPendingCount() {
|
||||||
completion(.success(count))
|
completion(.success(count))
|
||||||
@ -167,7 +167,7 @@ public extension SyncDatabase {
|
|||||||
|
|
||||||
nonisolated func selectPendingReadStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) {
|
nonisolated func selectPendingReadStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) {
|
||||||
|
|
||||||
Task {
|
Task { @MainActor in
|
||||||
do {
|
do {
|
||||||
if let articleIDs = try await self.selectPendingReadStatusArticleIDs() {
|
if let articleIDs = try await self.selectPendingReadStatusArticleIDs() {
|
||||||
completion(.success(articleIDs))
|
completion(.success(articleIDs))
|
||||||
@ -182,7 +182,7 @@ public extension SyncDatabase {
|
|||||||
|
|
||||||
nonisolated func selectPendingStarredStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) {
|
nonisolated func selectPendingStarredStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) {
|
||||||
|
|
||||||
Task {
|
Task { @MainActor in
|
||||||
do {
|
do {
|
||||||
if let articleIDs = try await self.selectPendingStarredStatusArticleIDs() {
|
if let articleIDs = try await self.selectPendingStarredStatusArticleIDs() {
|
||||||
completion(.success(articleIDs))
|
completion(.success(articleIDs))
|
||||||
@ -197,7 +197,7 @@ public extension SyncDatabase {
|
|||||||
|
|
||||||
nonisolated func resetAllSelectedForProcessing(completion: DatabaseCompletionBlock? = nil) {
|
nonisolated func resetAllSelectedForProcessing(completion: DatabaseCompletionBlock? = nil) {
|
||||||
|
|
||||||
Task {
|
Task { @MainActor in
|
||||||
do {
|
do {
|
||||||
try await self.resetAllSelectedForProcessing()
|
try await self.resetAllSelectedForProcessing()
|
||||||
completion?(nil)
|
completion?(nil)
|
||||||
@ -209,7 +209,7 @@ public extension SyncDatabase {
|
|||||||
|
|
||||||
nonisolated func resetSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) {
|
nonisolated func resetSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) {
|
||||||
|
|
||||||
Task {
|
Task { @MainActor in
|
||||||
do {
|
do {
|
||||||
try await self.resetSelectedForProcessing(articleIDs)
|
try await self.resetSelectedForProcessing(articleIDs)
|
||||||
completion?(nil)
|
completion?(nil)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user