mirror of
https://github.com/mastodon/mastodon-ios
synced 2025-04-11 22:58:02 +02:00
Remove push notification subscription management responsibility from SettingService
The SettingService used to be created very soon after launch, so it would catch the new registration token. It is now only created when needed by the Settings view controller, which makes it a bad place to keep the listener responsible for updating the current subscriptions with the new device token. Also, update all subscriptions on launch, rather than each account’s subscriptions only when they become the active account. Contributes to IOS-384
This commit is contained in:
parent
bf2a1ce790
commit
d7d565879f
@ -218,35 +218,16 @@ extension SettingsCoordinator: NotificationSettingsViewControllerDelegate {
|
||||
|
||||
guard let subscription = setting.activeSubscription,
|
||||
setting.domain == authenticationBox.domain,
|
||||
setting.userID == authenticationBox.userID,
|
||||
let legacyViewModel = NotificationService.shared.dequeueNotificationViewModel(mastodonAuthenticationBox: authenticationBox), let deviceToken = NotificationService.shared.deviceToken.value else { return }
|
||||
setting.userID == authenticationBox.userID else { return }
|
||||
|
||||
let queryData = Mastodon.API.Subscriptions.QueryData(
|
||||
policy: viewModel.selectedPolicy.subscriptionPolicy,
|
||||
alerts: Mastodon.API.Subscriptions.QueryData.Alerts(
|
||||
favourite: viewModel.notifyFavorites,
|
||||
follow: viewModel.notifyNewFollowers,
|
||||
reblog: viewModel.notifyBoosts,
|
||||
mention: viewModel.notifyMentions,
|
||||
poll: subscription.alert.poll
|
||||
)
|
||||
NotificationService.shared.updatePushNotificationSubscription(subscription.objectID, for: authenticationBox, policy: viewModel.selectedPolicy.subscriptionPolicy, alerts: Mastodon.API.Subscriptions.QueryData.Alerts(
|
||||
favourite: viewModel.notifyFavorites,
|
||||
follow: viewModel.notifyNewFollowers,
|
||||
reblog: viewModel.notifyBoosts,
|
||||
mention: viewModel.notifyMentions,
|
||||
poll: subscription.alert.poll
|
||||
)
|
||||
let query = legacyViewModel.createSubscribeQuery(
|
||||
deviceToken: deviceToken,
|
||||
queryData: queryData,
|
||||
mastodonAuthenticationBox: authenticationBox
|
||||
)
|
||||
|
||||
APIService.shared.createSubscription(
|
||||
subscriptionObjectID: subscription.objectID,
|
||||
query: query,
|
||||
mastodonAuthenticationBox: authenticationBox
|
||||
).sink(receiveCompletion: { completion in
|
||||
print(completion)
|
||||
}, receiveValue: { output in
|
||||
print(output)
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
func showNotificationSettings(_ viewController: UIViewController) {
|
||||
|
@ -64,7 +64,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
extension AppDelegate {
|
||||
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||
NotificationService.shared.deviceToken.value = deviceToken
|
||||
NotificationService.shared.registrationStatus.send(.registrationTokenReceived(deviceToken))
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error) {
|
||||
NotificationService.shared.registrationStatus.send(.error(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,43 +13,39 @@ import MastodonSDK
|
||||
|
||||
extension APIService {
|
||||
|
||||
public func createSubscription(
|
||||
public func subscribeToPushNotifications(
|
||||
subscriptionObjectID: NSManagedObjectID,
|
||||
query: Mastodon.API.Subscriptions.CreateSubscriptionQuery,
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> {
|
||||
) async throws -> Mastodon.Entity.Subscription {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
|
||||
return Mastodon.API.Subscriptions.createSubscription(
|
||||
let responseContent = try await Mastodon.API.Subscriptions.createSubscription(
|
||||
session: session,
|
||||
domain: domain,
|
||||
authorization: authorization,
|
||||
query: query
|
||||
)
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> in
|
||||
let managedObjectContext = self.backgroundManagedObjectContext
|
||||
return managedObjectContext.performChanges {
|
||||
guard let subscription = managedObjectContext.object(with: subscriptionObjectID) as? NotificationSubscription else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
subscription.alert.update(favourite: response.value.alerts.favourite)
|
||||
subscription.alert.update(reblog: response.value.alerts.reblog)
|
||||
subscription.alert.update(follow: response.value.alerts.follow)
|
||||
subscription.alert.update(mention: response.value.alerts.mention)
|
||||
|
||||
subscription.endpoint = response.value.endpoint
|
||||
subscription.serverKey = response.value.serverKey
|
||||
subscription.userToken = authorization.accessToken
|
||||
subscription.didUpdate(at: response.networkDate)
|
||||
|
||||
let newSubscription = responseContent.value
|
||||
let managedObjectContext = self.backgroundManagedObjectContext
|
||||
try await managedObjectContext.performChanges {
|
||||
guard let subscription = managedObjectContext.object(with: subscriptionObjectID) as? NotificationSubscription else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
.setFailureType(to: Error.self)
|
||||
.map { _ in return response }
|
||||
.eraseToAnyPublisher()
|
||||
subscription.alert.update(favourite: newSubscription.alerts.favourite)
|
||||
subscription.alert.update(reblog: newSubscription.alerts.reblog)
|
||||
subscription.alert.update(follow: newSubscription.alerts.follow)
|
||||
subscription.alert.update(mention: newSubscription.alerts.mention)
|
||||
|
||||
subscription.endpoint = newSubscription.endpoint
|
||||
subscription.serverKey = newSubscription.serverKey
|
||||
subscription.userToken = authorization.accessToken
|
||||
subscription.didUpdate(at: responseContent.networkDate)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
return newSubscription
|
||||
}
|
||||
|
||||
func cancelSubscription(
|
||||
|
@ -16,6 +16,12 @@ import MastodonLocalization
|
||||
@MainActor
|
||||
public final class NotificationService {
|
||||
|
||||
public enum PushNotificationRegistrationStatus {
|
||||
case registering
|
||||
case error(Error)
|
||||
case registrationTokenReceived(Data)
|
||||
}
|
||||
|
||||
public static let shared = { NotificationService() }()
|
||||
|
||||
public static let unreadShortcutItemIdentifier = "org.joinmastodon.app.NotificationService.unread-shortcut"
|
||||
@ -25,8 +31,8 @@ public final class NotificationService {
|
||||
let workingQueue = DispatchQueue(label: "org.joinmastodon.app.NotificationService.working-queue")
|
||||
|
||||
// input
|
||||
public let registrationStatus = CurrentValueSubject<PushNotificationRegistrationStatus, Never>(.registering)
|
||||
public let isNotificationPermissionGranted = CurrentValueSubject<Bool, Never>(false)
|
||||
public let deviceToken = CurrentValueSubject<Data?, Never>(nil)
|
||||
public let applicationIconBadgeNeedsUpdate = CurrentValueSubject<Void, Never>(Void())
|
||||
|
||||
// output
|
||||
@ -36,13 +42,22 @@ public final class NotificationService {
|
||||
public let requestRevealNotificationPublisher = PassthroughSubject<MastodonPushNotification, Never>()
|
||||
|
||||
private init() {
|
||||
AuthenticationServiceProvider.shared.currentActiveUser
|
||||
.sink(receiveValue: { [weak self] auth in
|
||||
Publishers.CombineLatest(
|
||||
AuthenticationServiceProvider.shared.currentActiveUser,
|
||||
registrationStatus
|
||||
)
|
||||
.sink(receiveValue: { [weak self] auth, registrationStatus in
|
||||
guard let self = self else { return }
|
||||
|
||||
// request permission when sign-in
|
||||
guard auth != nil else { return }
|
||||
self.requestNotificationPermission()
|
||||
switch (auth, registrationStatus) {
|
||||
case (nil, _):
|
||||
break
|
||||
case (_, .registrationTokenReceived):
|
||||
self.requestNotificationPermissionAndUpdateSubscriptions()
|
||||
case (_, .registering), (_, .error):
|
||||
break
|
||||
}
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
@ -73,18 +88,22 @@ public final class NotificationService {
|
||||
}
|
||||
|
||||
extension NotificationService {
|
||||
private func requestNotificationPermission() {
|
||||
private func requestNotificationPermissionAndUpdateSubscriptions() {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, error in
|
||||
guard let self = self else { return }
|
||||
|
||||
guard self.isNotificationPermissionGranted.value != granted else { return }
|
||||
self.isNotificationPermissionGranted.value = granted
|
||||
|
||||
if let _ = error {
|
||||
// Handle the error here.
|
||||
switch (granted, registrationStatus.value) {
|
||||
case (true, .registrationTokenReceived(let token)):
|
||||
Task {
|
||||
await self.updatePushNotificationSubscriptions(deviceToken: token)
|
||||
}
|
||||
case (true, .error), (true, .registering):
|
||||
break
|
||||
case (false, _):
|
||||
break
|
||||
}
|
||||
|
||||
// Enable or disable features based on the authorization.
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -169,7 +188,7 @@ extension NotificationService {
|
||||
private func fetchLatestNotifications(
|
||||
pushNotification: MastodonPushNotification
|
||||
) async throws {
|
||||
guard let authenticationBox = try await authenticationBox(for: pushNotification) else { return }
|
||||
guard let authenticationBox = authenticationBox(for: pushNotification) else { return }
|
||||
|
||||
_ = try await APIService.shared.notifications(
|
||||
olderThan: nil,
|
||||
@ -231,6 +250,62 @@ extension NotificationService {
|
||||
|
||||
}
|
||||
|
||||
extension NotificationService {
|
||||
private func updatePushNotificationSubscriptions(deviceToken: Data) async {
|
||||
do {
|
||||
for userAuthBox in AuthenticationServiceProvider.shared.mastodonAuthenticationBoxes {
|
||||
guard let setting = SettingService.shared.setting(for: userAuthBox) else { continue }
|
||||
guard let subscription = setting.activeSubscription else { continue }
|
||||
|
||||
let queryData = Mastodon.API.Subscriptions.QueryData(
|
||||
policy: subscription.policy,
|
||||
alerts: Mastodon.API.Subscriptions.QueryData.Alerts(
|
||||
favourite: subscription.alert.favourite,
|
||||
follow: subscription.alert.follow,
|
||||
reblog: subscription.alert.reblog,
|
||||
mention: subscription.alert.mention,
|
||||
poll: subscription.alert.poll
|
||||
)
|
||||
)
|
||||
let query = NotificationService.createSubscribeQuery(
|
||||
deviceToken: deviceToken,
|
||||
queryData: queryData,
|
||||
mastodonAuthenticationBox: userAuthBox
|
||||
)
|
||||
|
||||
let createSubscriptionTask = Task {
|
||||
try await APIService.shared.subscribeToPushNotifications(
|
||||
subscriptionObjectID: subscription.objectID,
|
||||
query: query,
|
||||
mastodonAuthenticationBox: userAuthBox
|
||||
)
|
||||
}
|
||||
let _ = try await createSubscriptionTask.value
|
||||
}
|
||||
} catch {
|
||||
assertionFailure("error creating push notification subscription")
|
||||
}
|
||||
}
|
||||
|
||||
public func updatePushNotificationSubscription(_ subscriptionObjectID: NSManagedObjectID, for userAuthBox: MastodonAuthenticationBox, policy: Mastodon.API.Subscriptions.QueryData.Policy, alerts: Mastodon.API.Subscriptions.QueryData.Alerts) {
|
||||
guard case let .registrationTokenReceived(deviceToken) = registrationStatus.value else { return }
|
||||
let queryData = Mastodon.API.Subscriptions.QueryData(policy: policy, alerts: alerts)
|
||||
let query = NotificationService.createSubscribeQuery(
|
||||
deviceToken: deviceToken,
|
||||
queryData: queryData,
|
||||
mastodonAuthenticationBox: userAuthBox
|
||||
)
|
||||
|
||||
Task {
|
||||
let _ = try await APIService.shared.subscribeToPushNotifications(
|
||||
subscriptionObjectID: subscriptionObjectID,
|
||||
query: query,
|
||||
mastodonAuthenticationBox: userAuthBox
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NotificationViewModel
|
||||
|
||||
extension NotificationService {
|
||||
@ -251,8 +326,8 @@ extension NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationService.NotificationViewModel {
|
||||
public func createSubscribeQuery(
|
||||
extension NotificationService {
|
||||
public static func createSubscribeQuery(
|
||||
deviceToken: Data,
|
||||
queryData: Mastodon.API.Subscriptions.QueryData,
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
|
@ -66,62 +66,11 @@ public final class SettingService {
|
||||
.sink { [weak self] mastodonAuthenticationBoxes, settings in
|
||||
guard let self = self else { return }
|
||||
guard let activeMastodonAuthenticationBox = mastodonAuthenticationBoxes.first else { return }
|
||||
let currentSetting = settings.first(where: { setting in
|
||||
return setting.domain == activeMastodonAuthenticationBox.domain
|
||||
&& setting.userID == activeMastodonAuthenticationBox.userID
|
||||
})
|
||||
let currentSetting = setting(for: activeMastodonAuthenticationBox)
|
||||
self.currentSetting.value = currentSetting
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest3(
|
||||
notificationService.deviceToken,
|
||||
currentSetting.eraseToAnyPublisher(),
|
||||
AuthenticationServiceProvider.shared.$mastodonAuthenticationBoxes
|
||||
)
|
||||
.compactMap { [weak self] deviceToken, setting, mastodonAuthenticationBoxes -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error>? in
|
||||
guard let self = self else { return nil }
|
||||
guard let deviceToken = deviceToken else { return nil }
|
||||
guard let setting = setting else { return nil }
|
||||
guard let authenticationBox = mastodonAuthenticationBoxes.first else { return nil }
|
||||
|
||||
guard let subscription = setting.activeSubscription else { return nil }
|
||||
|
||||
guard setting.domain == authenticationBox.domain,
|
||||
setting.userID == authenticationBox.userID else { return nil }
|
||||
|
||||
let _viewModel = notificationService.dequeueNotificationViewModel(
|
||||
mastodonAuthenticationBox: authenticationBox
|
||||
)
|
||||
guard let viewModel = _viewModel else { return nil }
|
||||
let queryData = Mastodon.API.Subscriptions.QueryData(
|
||||
policy: subscription.policy,
|
||||
alerts: Mastodon.API.Subscriptions.QueryData.Alerts(
|
||||
favourite: subscription.alert.favourite,
|
||||
follow: subscription.alert.follow,
|
||||
reblog: subscription.alert.reblog,
|
||||
mention: subscription.alert.mention,
|
||||
poll: subscription.alert.poll
|
||||
)
|
||||
)
|
||||
let query = viewModel.createSubscribeQuery(
|
||||
deviceToken: deviceToken,
|
||||
queryData: queryData,
|
||||
mastodonAuthenticationBox: authenticationBox
|
||||
)
|
||||
|
||||
return apiService.createSubscription(
|
||||
subscriptionObjectID: subscription.objectID,
|
||||
query: query,
|
||||
mastodonAuthenticationBox: authenticationBox
|
||||
)
|
||||
}
|
||||
.debounce(for: .seconds(3), scheduler: DispatchQueue.main) // limit subscribe request emit time interval
|
||||
.sink(receiveValue: { _ in
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SettingService {
|
||||
@ -138,3 +87,12 @@ extension SettingService {
|
||||
return alertController
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingService {
|
||||
public func setting(for userAuthBox: MastodonAuthenticationBox) -> Setting? {
|
||||
return settingFetchedResultController.settings.value.first(where: { setting in
|
||||
return setting.domain == userAuthBox.domain
|
||||
&& setting.userID == userAuthBox.userID
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -61,24 +61,21 @@ extension Mastodon.API.Subscriptions {
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - authorization: User token. Could be nil if status is public
|
||||
/// - Returns: `AnyPublisher` contains `Subscription` nested in the response
|
||||
/// - Returns: `Subscription`
|
||||
public static func createSubscription(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
authorization: Mastodon.API.OAuth.Authorization,
|
||||
query: CreateSubscriptionQuery
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> {
|
||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Subscription> {
|
||||
let request = Mastodon.API.post(
|
||||
url: pushEndpointURL(domain: domain),
|
||||
query: query,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Subscription.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
let (data, response) = try await session.data(for: request)
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Subscription.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
|
||||
/// Change types of notifications
|
||||
|
Loading…
x
Reference in New Issue
Block a user