mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Added support for CloudKit push notifications (subscriptions).
This commit is contained in:
@@ -377,8 +377,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
grantingType.requestOAuthAccessToken(with: response, transport: transport, completion: completion)
|
||||
}
|
||||
|
||||
public func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
||||
delegate.receiveRemoteNotification(for: self, userInfo: userInfo, completion: completion)
|
||||
}
|
||||
|
||||
public func refreshAll(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
self.delegate.refreshAll(for: self, completion: completion)
|
||||
delegate.refreshAll(for: self, completion: completion)
|
||||
}
|
||||
|
||||
public func syncArticleStatus(completion: ((Result<Void, Error>) -> Void)? = nil) {
|
||||
|
||||
@@ -22,6 +22,8 @@ protocol AccountDelegate {
|
||||
|
||||
var refreshProgress: DownloadProgress { get }
|
||||
|
||||
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void)
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void))
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void))
|
||||
|
||||
@@ -184,7 +184,22 @@ public final class AccountManager: UnreadCountProvider {
|
||||
accounts.forEach { $0.resume() }
|
||||
}
|
||||
|
||||
public func refreshAll(errorHandler: @escaping (Error) -> Void, completion: (() ->Void)? = nil) {
|
||||
public func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: (() -> Void)? = nil) {
|
||||
let group = DispatchGroup()
|
||||
|
||||
activeAccounts.forEach { account in
|
||||
group.enter()
|
||||
account.receiveRemoteNotification(userInfo: userInfo) {
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
public func refreshAll(errorHandler: @escaping (Error) -> Void, completion: (() -> Void)? = nil) {
|
||||
let group = DispatchGroup()
|
||||
|
||||
activeAccounts.forEach { account in
|
||||
@@ -203,7 +218,6 @@ public final class AccountManager: UnreadCountProvider {
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion?()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func syncArticleStatusAll(completion: (() -> Void)? = nil) {
|
||||
|
||||
@@ -30,6 +30,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
return CKContainer(identifier: "iCloud.\(orgID).NetNewsWire")
|
||||
}()
|
||||
|
||||
private lazy var zones = [accountZone]
|
||||
private let accountZone: CloudKitAccountZone
|
||||
|
||||
private let refresher = LocalAccountRefresher()
|
||||
@@ -52,6 +53,21 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
accountZone.refreshProgress = refreshProgress
|
||||
}
|
||||
|
||||
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
||||
let group = DispatchGroup()
|
||||
|
||||
zones.forEach { zone in
|
||||
group.enter()
|
||||
zone.receiveRemoteNotification(userInfo: userInfo) {
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
accountZone.fetchChangesInZone() { result in
|
||||
switch result {
|
||||
@@ -247,11 +263,16 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
|
||||
func accountDidInitialize(_ account: Account) {
|
||||
accountZone.delegate = CloudKitAcountZoneDelegate(account: account, refreshProgress: refreshProgress)
|
||||
accountZone.resumeLongLivedOperationIfPossible()
|
||||
zones.forEach { zone in
|
||||
zone.resumeLongLivedOperationIfPossible()
|
||||
zone.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
func accountWillBeDeleted(_ account: Account) {
|
||||
accountZone.resetChangeToken()
|
||||
zones.forEach { zone in
|
||||
zone.resetChangeToken()
|
||||
}
|
||||
}
|
||||
|
||||
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: (Result<Credentials?, Error>) -> Void) {
|
||||
|
||||
@@ -58,14 +58,43 @@ extension CloudKitZone {
|
||||
}
|
||||
}
|
||||
|
||||
// func startObservingRemoteChanges() {
|
||||
// NotificationCenter.default.addObserver(forName: Notifications.cloudKitDataDidChangeRemotely.name, object: nil, queue: nil, using: { [weak self](_) in
|
||||
// guard let self = self else { return }
|
||||
// DispatchQueue.global(qos: .utility).async {
|
||||
// self.fetchChangesInDatabase(nil)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
func subscribe() {
|
||||
|
||||
let subscription = CKRecordZoneSubscription(zoneID: Self.zoneID)
|
||||
|
||||
let info = CKSubscription.NotificationInfo()
|
||||
info.shouldSendContentAvailable = true
|
||||
subscription.notificationInfo = info
|
||||
|
||||
database?.save(subscription) { _, error in
|
||||
switch CloudKitZoneResult.resolve(error) {
|
||||
case .success:
|
||||
break
|
||||
case .retry(let timeToWait):
|
||||
self.retryOperationIfPossible(retryAfter: timeToWait) {
|
||||
self.subscribe()
|
||||
}
|
||||
default:
|
||||
os_log(.error, log: self.log, "%@ zone fetch changes error: %@.", Self.zoneID.zoneName, error?.localizedDescription ?? "Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
||||
let note = CKRecordZoneNotification(fromRemoteNotificationDictionary: userInfo)
|
||||
guard note?.recordZoneID?.zoneName == Self.zoneID.zoneName else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
fetchChangesInZone() { result in
|
||||
if case .failure(let error) = result {
|
||||
os_log(.error, log: self.log, "%@ zone remote notification fetch error: %@.", Self.zoneID.zoneName, error.localizedDescription)
|
||||
}
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
func save(record: CKRecord, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
modify(recordsToSave: [record], recordIDsToDelete: [], completion: completion)
|
||||
@@ -142,7 +171,7 @@ extension CloudKitZone {
|
||||
let op = CKFetchRecordZoneChangesOperation(recordZoneIDs: [Self.zoneID], configurationsByRecordZoneID: [Self.zoneID: zoneConfig])
|
||||
op.fetchAllChanges = true
|
||||
|
||||
op.recordZoneChangeTokensUpdatedBlock = { [weak self] zoneId, token, _ in
|
||||
op.recordZoneChangeTokensUpdatedBlock = { [weak self] zoneID, token, _ in
|
||||
guard let self = self else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.changeToken = token
|
||||
@@ -156,14 +185,14 @@ extension CloudKitZone {
|
||||
}
|
||||
}
|
||||
|
||||
op.recordWithIDWasDeletedBlock = { [weak self] recordId, recordType in
|
||||
op.recordWithIDWasDeletedBlock = { [weak self] recordID, recordType in
|
||||
guard let self = self else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.cloudKitDidDelete(recordType: recordType, recordID: recordId)
|
||||
self.delegate?.cloudKitDidDelete(recordType: recordType, recordID: recordID)
|
||||
}
|
||||
}
|
||||
|
||||
op.recordZoneFetchCompletionBlock = { [weak self] zoneId ,token, _, _, error in
|
||||
op.recordZoneFetchCompletionBlock = { [weak self] zoneID ,token, _, _, error in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch CloudKitZoneResult.resolve(error) {
|
||||
@@ -176,7 +205,7 @@ extension CloudKitZone {
|
||||
self.fetchChangesInZone(completion: completion)
|
||||
}
|
||||
default:
|
||||
os_log(.error, log: self.log, "%@ zone fetch changes error: %@.", zoneId.zoneName, error?.localizedDescription ?? "Unknown")
|
||||
os_log(.error, log: self.log, "%@ zone fetch changes error: %@.", zoneID.zoneName, error?.localizedDescription ?? "Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,10 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
|
||||
caller.logout() { _ in }
|
||||
}
|
||||
|
||||
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
||||
completion()
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(6)
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
caller.accountMetadata = accountMetadata
|
||||
}
|
||||
}
|
||||
|
||||
var refreshProgress = DownloadProgress(numberOfTasks: 0)
|
||||
|
||||
init(dataFolder: String, transport: Transport?) {
|
||||
|
||||
@@ -71,9 +73,11 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var refreshProgress = DownloadProgress(numberOfTasks: 0)
|
||||
|
||||
|
||||
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
||||
completion()
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(5)
|
||||
|
||||
@@ -95,6 +95,10 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||
|
||||
// MARK: Account API
|
||||
|
||||
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
||||
completion()
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
|
||||
@@ -31,6 +31,10 @@ final class LocalAccountDelegate: AccountDelegate {
|
||||
return refresher.progress
|
||||
}
|
||||
|
||||
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
||||
completion()
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
refresher.refreshFeeds(account.flattenedWebFeeds()) {
|
||||
account.metadata.lastArticleFetchEndTime = Date()
|
||||
|
||||
@@ -57,6 +57,10 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||
database = SyncDatabase(databaseFilePath: dataFolder.appending("/DB.sqlite3"))
|
||||
}
|
||||
|
||||
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
||||
completion()
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
self.refreshProgress.addToNumberOfTasksAndRemaining(5)
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
var refreshProgress = DownloadProgress(numberOfTasks: 0)
|
||||
|
||||
init(dataFolder: String, transport: Transport?) {
|
||||
|
||||
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3")
|
||||
@@ -77,7 +79,9 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
|
||||
}
|
||||
|
||||
var refreshProgress = DownloadProgress(numberOfTasks: 0)
|
||||
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
||||
completion()
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
|
||||
@@ -267,6 +267,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
CrashReporter.check(appName: "NetNewsWire")
|
||||
}
|
||||
#endif
|
||||
|
||||
NSApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
|
||||
func application(_ application: NSApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([NSUserActivityRestoring]) -> Void) -> Bool {
|
||||
@@ -302,6 +304,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
saveState()
|
||||
}
|
||||
|
||||
func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) {
|
||||
AccountManager.shared.receiveRemoteNotification(userInfo: userInfo)
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ notification: Notification) {
|
||||
shuttingDown = true
|
||||
saveState()
|
||||
|
||||
@@ -91,15 +91,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
self.unreadCount = AccountManager.shared.unreadCount
|
||||
}
|
||||
|
||||
UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { (granted, error) in
|
||||
if granted {
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { _, _ in }
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
|
||||
userNotificationManager = UserNotificationManager()
|
||||
|
||||
extensionContainersFile = ExtensionContainersFile()
|
||||
@@ -115,6 +110,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
||||
DispatchQueue.main.async {
|
||||
AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) {
|
||||
completionHandler(.newData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
shuttingDown = true
|
||||
}
|
||||
|
||||
@@ -92,6 +92,8 @@
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>processing</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreenPhone</string>
|
||||
|
||||
Reference in New Issue
Block a user