From 1359b80a8e1701cc1df0b251172ecdc52c04acb2 Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Thu, 4 Feb 2021 12:09:05 -0800 Subject: [PATCH] Notification navigation wip --- Localizations/Localizable.strings | 1 + .../NotificationService.swift | 8 ++-- .../Services/UserNotificationService.swift | 22 +++++++++ .../Utilities/UserNotificationClient.swift | 14 ++++-- .../MockUserNotificationClient.swift | 2 + .../View Models/RootViewModel.swift | 47 ++++++++++++++++--- 6 files changed, 81 insertions(+), 13 deletions(-) diff --git a/Localizations/Localizable.strings b/Localizations/Localizable.strings index d7c7ed4..982ff1d 100644 --- a/Localizations/Localizable.strings +++ b/Localizations/Localizable.strings @@ -145,6 +145,7 @@ "main-navigation.notifications" = "Notifications"; "main-navigation.conversations" = "Messages"; "metatext" = "Metatext"; +"notification.signed-in-as-%@" = "Signed in as %@"; "notifications.all" = "All"; "notifications.mentions" = "Mentions"; "ok" = "OK"; diff --git a/Notification Service Extension/NotificationService.swift b/Notification Service Extension/NotificationService.swift index 41337a4..cfc5008 100644 --- a/Notification Service Extension/NotificationService.swift +++ b/Notification Service Extension/NotificationService.swift @@ -55,7 +55,7 @@ final class NotificationService: UNNotificationServiceExtension { } if appPreferences.notificationPictures { - Self.addImage(pushNotification: pushNotification, + Self.addImage(url: pushNotification.icon, bestAttemptContent: bestAttemptContent, contentHandler: contentHandler) } else { @@ -71,14 +71,14 @@ final class NotificationService: UNNotificationServiceExtension { } private extension NotificationService { - static func addImage(pushNotification: PushNotification, + static func addImage(url: URL, bestAttemptContent: UNMutableNotificationContent, contentHandler: @escaping (UNNotificationContent) -> Void) { - let fileName = pushNotification.icon.lastPathComponent + let fileName = url.lastPathComponent let fileURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) .appendingPathComponent(fileName) - KingfisherManager.shared.retrieveImage(with: pushNotification.icon) { + KingfisherManager.shared.retrieveImage(with: url) { switch $0 { case let .success(result): let format: ImageFormat diff --git a/ServiceLayer/Sources/ServiceLayer/Services/UserNotificationService.swift b/ServiceLayer/Sources/ServiceLayer/Services/UserNotificationService.swift index 52e5ef7..e84fca3 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/UserNotificationService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/UserNotificationService.swift @@ -17,6 +17,10 @@ public struct UserNotificationService { public extension UserNotificationService { typealias Event = UserNotificationClient.DelegateEvent + typealias Content = UNNotificationContent + typealias MutableContent = UNMutableNotificationContent + typealias Trigger = UNNotificationTrigger + typealias Request = UNNotificationRequest func isAuthorized(request: Bool) -> AnyPublisher { getNotificationSettings() @@ -32,6 +36,24 @@ public extension UserNotificationService { } .eraseToAnyPublisher() } + + func add(request: Request) -> AnyPublisher { + Future { promise in + userNotificationClient.add(request) { error in + if let error = error { + promise(.failure(error)) + } else { + promise(.success(())) + } + } + } + .ignoreOutput() + .eraseToAnyPublisher() + } + + func removeDeliveredNotifications(withIdentifiers identifiers: [String]) { + userNotificationClient.removeDeliveredNotifications(identifiers) + } } private extension UserNotificationService { diff --git a/ServiceLayer/Sources/ServiceLayer/Utilities/UserNotificationClient.swift b/ServiceLayer/Sources/ServiceLayer/Utilities/UserNotificationClient.swift index fb1dc7e..2a579f8 100644 --- a/ServiceLayer/Sources/ServiceLayer/Utilities/UserNotificationClient.swift +++ b/ServiceLayer/Sources/ServiceLayer/Utilities/UserNotificationClient.swift @@ -10,16 +10,22 @@ public struct UserNotificationClient { case openSettingsForNotification(UNNotification?) } - public var getNotificationSettings: (@escaping (UNNotificationSettings) -> Void) -> Void - public var requestAuthorization: (UNAuthorizationOptions, @escaping (Bool, Error?) -> Void) -> Void - public var delegateEvents: AnyPublisher + public let getNotificationSettings: (@escaping (UNNotificationSettings) -> Void) -> Void + public let requestAuthorization: (UNAuthorizationOptions, @escaping (Bool, Error?) -> Void) -> Void + public let add: (UNNotificationRequest, ((Error?) -> Void)?) -> Void + public let removeDeliveredNotifications: ([String]) -> Void + public let delegateEvents: AnyPublisher public init( getNotificationSettings: @escaping (@escaping (UNNotificationSettings) -> Void) -> Void, requestAuthorization: @escaping (UNAuthorizationOptions, @escaping (Bool, Error?) -> Void) -> Void, + add: @escaping (UNNotificationRequest, ((Error?) -> Void)?) -> Void, + removeDeliveredNotifications: @escaping ([String]) -> Void, delegateEvents: AnyPublisher) { self.getNotificationSettings = getNotificationSettings self.requestAuthorization = requestAuthorization + self.add = add + self.removeDeliveredNotifications = removeDeliveredNotifications self.delegateEvents = delegateEvents } } @@ -59,6 +65,8 @@ extension UserNotificationClient { return UserNotificationClient( getNotificationSettings: userNotificationCenter.getNotificationSettings, requestAuthorization: userNotificationCenter.requestAuthorization, + add: userNotificationCenter.add(_:withCompletionHandler:), + removeDeliveredNotifications: userNotificationCenter.removeDeliveredNotifications(withIdentifiers:), delegateEvents: subject .handleEvents(receiveCancel: { delegate = nil }) .eraseToAnyPublisher()) diff --git a/ServiceLayer/Sources/ServiceLayerMocks/MockUserNotificationClient.swift b/ServiceLayer/Sources/ServiceLayerMocks/MockUserNotificationClient.swift index 953404e..9a6ed8a 100644 --- a/ServiceLayer/Sources/ServiceLayerMocks/MockUserNotificationClient.swift +++ b/ServiceLayer/Sources/ServiceLayerMocks/MockUserNotificationClient.swift @@ -7,5 +7,7 @@ public extension UserNotificationClient { static let mock = UserNotificationClient( getNotificationSettings: { _ in }, requestAuthorization: { _, _ in }, + add: { _, completion in completion?(nil) }, + removeDeliveredNotifications: { _ in }, delegateEvents: Empty(completeImmediately: false).eraseToAnyPublisher()) } diff --git a/ViewModels/Sources/ViewModels/View Models/RootViewModel.swift b/ViewModels/Sources/ViewModels/View Models/RootViewModel.swift index 83df7d7..0e4704c 100644 --- a/ViewModels/Sources/ViewModels/View Models/RootViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/RootViewModel.swift @@ -36,7 +36,7 @@ public final class RootViewModel: ObservableObject { .replaceError(with: nil) .assign(to: &$mostRecentlyUsedIdentityId) - identitySelected(id: mostRecentlyUsedIdentityId, immediate: true) + identitySelected(id: mostRecentlyUsedIdentityId, immediate: true, notify: false) allIdentitiesService.identitiesCreated .sink { [weak self] in self?.identitySelected(id: $0) } @@ -50,7 +50,7 @@ public final class RootViewModel: ObservableObject { public extension RootViewModel { func identitySelected(id: Identity.Id?) { - identitySelected(id: id, immediate: false) + identitySelected(id: id, immediate: false, notify: false) } func deleteIdentity(id: Identity.Id) { @@ -80,7 +80,12 @@ public extension RootViewModel { } private extension RootViewModel { - func identitySelected(id: Identity.Id?, immediate: Bool) { + static let identityChangeNotificationUserInfoKey = + "com.metabolist.metatext.identity-change-notification-user-info-key" + static let removeIdentityChangeNotificationAfter = DispatchTimeInterval.seconds(10) + + // swiftlint:disable:next function_body_length + func identitySelected(id: Identity.Id?, immediate: Bool, notify: Bool) { navigationViewModel?.presentingSecondaryNavigation = false guard @@ -95,7 +100,9 @@ private extension RootViewModel { .catch { [weak self] _ -> Empty in DispatchQueue.main.async { if self?.navigationViewModel?.identityContext.identity.id == id { - self?.identitySelected(id: self?.mostRecentlyUsedIdentityId, immediate: false) + self?.identitySelected(id: self?.mostRecentlyUsedIdentityId, + immediate: false, + notify: true) } } @@ -131,6 +138,10 @@ private extension RootViewModel { .store(in: &self.cancellables) } + if notify { + self.notifyIdentityChange(identityContext: identityContext) + } + return NavigationViewModel(identityContext: identityContext) } .assign(to: &$navigationViewModel) @@ -138,8 +149,15 @@ private extension RootViewModel { func handle(event: UserNotificationService.Event) { switch event { - case let .willPresentNotification(_, completionHandler): + case let .willPresentNotification(notification, completionHandler): completionHandler(.banner) + + if notification.request.content.userInfo[Self.identityChangeNotificationUserInfoKey] as? Bool == true { + DispatchQueue.main.asyncAfter(deadline: .now() + Self.removeIdentityChangeNotificationAfter) { + self.userNotificationService.removeDeliveredNotifications( + withIdentifiers: [notification.request.identifier]) + } + } case let .didReceiveResponse(response, completionHandler): let userInfo = response.notification.request.content.userInfo @@ -157,6 +175,23 @@ private extension RootViewModel { } func handle(pushNotification: PushNotification, identityId: Identity.Id) { - // TODO + if identityId != navigationViewModel?.identityContext.identity.id { + identitySelected(id: identityId, immediate: false, notify: true) + } + } + + func notifyIdentityChange(identityContext: IdentityContext) { + let content = UserNotificationService.MutableContent() + + content.body = String.localizedStringWithFormat( + NSLocalizedString("notification.signed-in-as-%@", comment: ""), + identityContext.identity.handle) + content.userInfo[Self.identityChangeNotificationUserInfoKey] = true + + let request = UserNotificationService.Request(identifier: UUID().uuidString, content: content, trigger: nil) + + userNotificationService.add(request: request) + .sink { _ in } receiveValue: { _ in } + .store(in: &cancellables) } }