From ebd30d1c9d76f5178bfeb25390abbd85868a46b4 Mon Sep 17 00:00:00 2001 From: Bruno Muniz Azevedo Filho <bruno@elixxir.io> Date: Wed, 23 Nov 2022 00:31:08 -0300 Subject: [PATCH] trying to setup pulse --- .../NotificationService.swift | 92 +++++++- Package.swift | 23 +- Sources/AppFeature/AppDelegate.swift | 219 ++++++++++++++++- ...cyRegistrator.swift => Dependencies.swift} | 15 ++ .../AppFeature/PushNotificationRouter.swift | 57 +++++ Sources/AppFeature/PushRouter.swift | 57 ----- Sources/LaunchFeature/LaunchController.swift | 13 +- Sources/LaunchFeature/LaunchViewModel.swift | 45 ++-- .../PushNotificationRouter.swift} | 10 +- Sources/PushFeature/ContentsBuilder.swift | 23 -- Sources/PushFeature/MockPushHandler.swift | 39 ---- Sources/PushFeature/Push.swift | 7 - Sources/PushFeature/PushExtractor.swift | 42 ---- Sources/PushFeature/PushHandler.swift | 220 ------------------ Sources/PushFeature/PushHandling.swift | 69 ------ .../xcshareddata/swiftpm/Package.resolved | 9 + 16 files changed, 435 insertions(+), 505 deletions(-) rename Sources/AppFeature/{DependencyRegistrator.swift => Dependencies.swift} (87%) create mode 100644 Sources/AppFeature/PushNotificationRouter.swift delete mode 100644 Sources/AppFeature/PushRouter.swift rename Sources/{PushFeature/PushRouter.swift => LaunchFeature/PushNotificationRouter.swift} (62%) delete mode 100644 Sources/PushFeature/ContentsBuilder.swift delete mode 100644 Sources/PushFeature/MockPushHandler.swift delete mode 100644 Sources/PushFeature/Push.swift delete mode 100644 Sources/PushFeature/PushExtractor.swift delete mode 100644 Sources/PushFeature/PushHandler.swift delete mode 100644 Sources/PushFeature/PushHandling.swift diff --git a/App/NotificationExtension/NotificationService.swift b/App/NotificationExtension/NotificationService.swift index 4444b66a..d9657fb8 100644 --- a/App/NotificationExtension/NotificationService.swift +++ b/App/NotificationExtension/NotificationService.swift @@ -1,13 +1,97 @@ -import PushFeature +import XXModels +import XXClient +import XXDatabase +import ReportingFeature +import XXMessengerClient import UserNotifications final class NotificationService: UNNotificationServiceExtension { - private let pushHandler = PushHandler() - override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { - pushHandler.handlePush(request, contentHandler) + guard let defaults = UserDefaults(suiteName: "group.elixxir.messenger") else { return } + + var environment = MessengerEnvironment.live() + environment.serviceList = .userDefaults(key: "preImage", userDefaults: defaults) + let messenger = Messenger.live(environment) + let userInfo = request.content.userInfo + let dbPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! + .appendingPathComponent("xxm_databasse") + .appendingPathExtension("sqlite").path + + guard let csv = userInfo["notificationData"] as? String, + let reports = try? messenger.getNotificationReports(notificationCSV: csv) else { return } + reports + .filter { $0.forMe } + .filter { $0.type != .silent } + .filter { $0.type != .default } + .compactMap { + let content = UNMutableNotificationContent() + content.badge = 1 + content.sound = .default + content.threadIdentifier = "new_message_identifier" + content.userInfo["type"] = $0.type.rawValue + content.userInfo["source"] = $0.source + content.body = getBodyForUnknownWith(type: $0.type) + + guard let db = try? Database.onDisk(path: dbPath), + let contact = try? db.fetchContacts(.init(id: [$0.source])).first else { + return content + } + if ReportingStatus.live().isEnabled(), (contact.isBlocked || contact.isBanned) { + return nil + } + if let showSender = defaults.value(forKey: "isShowingUsernames") as? Bool, showSender == true { + let name = (contact.nickname ?? contact.username) ?? "" + content.body = getBodyFor(name: name, with: $0.type) + } + return content + }.forEach { + contentHandler($0) + } + } + + private func getBodyForUnknownWith(type: NotificationReport.ReportType) -> String { + switch type { + case .`default`, .silent: + fatalError() + case .request: + return "Request received" + case .reset: + return "One of your contacts has restored their account" + case .confirm: + return "Request accepted" + case .e2e: + return "New private message" + case .group: + return "New group message" + case .endFT: + return "New media received" + case .groupRQ: + return "Group request received" + } + } + + private func getBodyFor(name: String, with type: NotificationReport.ReportType) -> String { + switch type { + case .silent, .`default`: + fatalError() + case .e2e: + return String(format: "%@ sent you a private message", name) + case .reset: + return String(format: "%@ restored their account", name) + case .endFT: + return String(format: "%@ sent you a file", name) + case .group: + return String(format: "%@ sent you a group message", name) + case .groupRQ: + return String(format: "%@ sent you a group request", name) + case .confirm: + return String(format: "%@ confirmed your contact request", name) + case .request: + return String(format: "%@ sent you a contact request", name) + } } } diff --git a/Package.swift b/Package.swift index 7bf71c7d..155b1dd4 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,6 @@ let package = Package( .library(name: "ScanFeature", targets: ["ScanFeature"]), .library(name: "MenuFeature", targets: ["MenuFeature"]), .library(name: "ChatFeature", targets: ["ChatFeature"]), - .library(name: "PushFeature", targets: ["PushFeature"]), .library(name: "CrashReport", targets: ["CrashReport"]), .library(name: "UpdateErrors", targets: ["UpdateErrors"]), .library(name: "CheckVersion", targets: ["CheckVersion"]), @@ -121,6 +120,10 @@ let package = Package( url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.4.4") ), + .package( + url: "https://github.com/kean/Pulse.git", + .upToNextMajor(from: "2.1.3") + ), .package( url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git", .upToNextMajor(from: "0.3.3") @@ -136,7 +139,6 @@ let package = Package( .target(name: "ChatFeature"), .target(name: "MenuFeature"), .target(name: "CrashReport"), - .target(name: "PushFeature"), .target(name: "TermsFeature"), .target(name: "BackupFeature"), .target(name: "SearchFeature"), @@ -154,6 +156,8 @@ let package = Package( .target(name: "CreateGroupFeature"), .target(name: "ContactListFeature"), .target(name: "RequestPermissionFeature"), + .product(name: "PulseUI", package: "Pulse"), // TO REMOVE + .product(name: "PulseLogHandler", package: "Pulse"), // TO REMOVE ] ), .testTarget( @@ -270,17 +274,6 @@ let package = Package( ), ] ), - .target( - name: "PushFeature", - dependencies: [ - .target(name: "AppCore"), - .target(name: "Defaults"), - .target(name: "ReportingFeature"), - .product(name: "XXDatabase", package: "client-ios-db"), - .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), - .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), - ] - ), .target( name: "Keychain", dependencies: [ @@ -392,7 +385,6 @@ let package = Package( name: "SearchFeature", dependencies: [ .target(name: "Shared"), - .target(name: "PushFeature"), .target(name: "ContactFeature"), .target(name: "CountryListFeature"), .product(name: "Retry", package: "Retry"), @@ -404,7 +396,6 @@ let package = Package( dependencies: [ .target(name: "Shared"), .target(name: "Defaults"), - .target(name: "PushFeature"), .target(name: "UpdateErrors"), .target(name: "CheckVersion"), .target(name: "BackupFeature"), @@ -525,7 +516,6 @@ let package = Package( .target(name: "Defaults"), .target(name: "Keychain"), .target(name: "InputField"), - .target(name: "PushFeature"), .target(name: "DrawerFeature"), .target(name: "AppNavigation"), .target(name: "CountryListFeature"), @@ -611,7 +601,6 @@ let package = Package( .target(name: "Defaults"), .target(name: "Keychain"), .target(name: "InputField"), - .target(name: "PushFeature"), .target(name: "MenuFeature"), .target(name: "CrashReport"), .target(name: "DrawerFeature"), diff --git a/Sources/AppFeature/AppDelegate.swift b/Sources/AppFeature/AppDelegate.swift index cb3dce57..3f9cc3ec 100644 --- a/Sources/AppFeature/AppDelegate.swift +++ b/Sources/AppFeature/AppDelegate.swift @@ -1,23 +1,64 @@ import UIKit import AppCore import Defaults +import XXClient +import Dependencies import LaunchFeature +import XXMessengerClient + +// MARK: - TO REMOVE FROM PRODUCTION: +import Logging +import PulseUI +import AppNavigation +import PulseLogHandler +// MARK: - public class AppDelegate: UIResponder, UIApplicationDelegate { - public var coverView: UIView? public var window: UIWindow? + private var coverView: UIView? + private var backgroundTimer: Timer? + private var backgroundTask: UIBackgroundTaskIdentifier? + + @Dependency(\.app.log) var logger + @Dependency(\.navigator) var navigator + @Dependency(\.app.messenger) var messenger + @Dependency(\.pushNotificationRouter) var pushNotificationRouter @KeyObject(.hideAppList, defaultValue: false) var shouldHideAppInAppList + @KeyObject(.pushNotifications, defaultValue: false) var isPushNotificationsEnabled public func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + UNUserNotificationCenter.current().delegate = self let navController = UINavigationController(rootViewController: LaunchController()) window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = RootViewController(navController) window?.makeKeyAndVisible() + + pushNotificationRouter.set(.live(navigationController: navController)) + + // MARK: - TO REMOVE FROM PRODUCTION: + LoggingSystem.bootstrap(PersistentLogHandler.init) + + NotificationCenter.default.addObserver( + forName: UIApplication.userDidTakeScreenshotNotification, + object: nil, + queue: OperationQueue.main + ) { [weak self] _ in + guard let self else { return } + let pulseViewController = PulseUI.MainViewController(store: .shared) + self.navigator.perform( + PresentModal( + pulseViewController, + from: navController.topViewController! + ) + ) + } + // MARK: - + return true } @@ -34,4 +75,180 @@ public class AppDelegate: UIResponder, UIApplicationDelegate { application.applicationIconBadgeNumber = 0 coverView?.removeFromSuperview() } + + public func applicationWillEnterForeground(_ application: UIApplication) { + resumeMessenger(application) + } + + public func applicationDidEnterBackground(_ application: UIApplication) { + stopMessenger(application) + } + + public func application( + application: UIApplication, + shouldAllowExtensionPointIdentifier identifier: String + ) -> Bool { + if identifier == UIApplication.ExtensionPointIdentifier.keyboard.rawValue { + return false /// Disable custom keyboards + } + return true + } + + public func application( + _ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void + ) -> Bool { + guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, + let incomingURL = userActivity.webpageURL, + let username = getUsernameFromInvitationDeepLink(incomingURL), + let router = pushNotificationRouter.get() else { + return false + } + + router.navigateTo(.search(username: username), {}) + return true + } +} + +extension AppDelegate: UNUserNotificationCenterDelegate { + public func application( + _ application: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data + ) { + if messenger.isConnected() { + do { + try messenger.registerForNotifications(token: deviceToken) + isPushNotificationsEnabled = true + } catch { + isPushNotificationsEnabled = false + logger(.error(error as NSError)) + print(error.localizedDescription) + } + } + } + + public func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + let userInfo = response.notification.request.content.userInfo + guard let string = userInfo["type"] as? String, + let type = NotificationReport.ReportType(rawValue: string) else { + completionHandler() + return + } + var route: PushNotificationRouter.Route? + switch type { + case .e2e, .group: + guard let source = userInfo["source"] as? Data else { + completionHandler() + return + } + if type == .e2e { + route = .contactChat(id: source) + } else { + route = .groupChat(id: source) + } + default: + break + } + + if let route, let router = pushNotificationRouter.get() { + router.navigateTo(route, completionHandler) + } + } + + public func application( + _ application: UIApplication, + didReceiveRemoteNotification notification: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { + if application.applicationState == .background, + let csv = notification["notificationData"] as? String, + let reports = try? messenger.getNotificationReports(notificationCSV: csv) { + reports + .filter { $0.forMe } + .filter { $0.type != .silent } + .filter { $0.type != .default } + .map { + let content = UNMutableNotificationContent() + content.badge = 1 + content.body = "" + content.sound = .default + content.userInfo["source"] = $0.source + content.userInfo["type"] = $0.type.rawValue + content.threadIdentifier = "new_message_identifier" + return content + }.map { + UNNotificationRequest( + identifier: Bundle.main.bundleIdentifier!, + content: $0, + trigger: UNTimeIntervalNotificationTrigger( + timeInterval: 1, + repeats: false + ) + ) + }.forEach { + UNUserNotificationCenter.current().add($0) { error in + error == nil ? completionHandler(.newData) : completionHandler(.failed) + } + } + } else { + completionHandler(.noData) + } + } +} + +extension AppDelegate { + private func resumeMessenger(_ application: UIApplication) { + backgroundTimer?.invalidate() + backgroundTimer = nil + if let backgroundTask { + application.endBackgroundTask(backgroundTask) + } + do { + if messenger.isLoaded() { + try messenger.start() + } + } catch { + logger(.error(error as NSError)) + print(error.localizedDescription) + } + } + + private func stopMessenger(_ application: UIApplication) { + guard messenger.isLoaded() else { return } + + backgroundTask = application.beginBackgroundTask(withName: "STOPPING_NETWORK") + backgroundTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in + guard let self else { return } + + if application.backgroundTimeRemaining <= 5 { + do { + self.backgroundTimer?.invalidate() + try self.messenger.stop() + } catch { + self.logger(.error(error as NSError)) + print(error.localizedDescription) + } + if let backgroundTask = self.backgroundTask { + application.endBackgroundTask(backgroundTask) + } + } + } + } +} + +func getUsernameFromInvitationDeepLink(_ url: URL) -> String? { + if let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + components.scheme == "https", + components.host == "elixxir.io", + components.path == "/connect", + let queryItem = components.queryItems?.first(where: { $0.name == "username" }), + let username = queryItem.value { + return username + } + return nil } diff --git a/Sources/AppFeature/DependencyRegistrator.swift b/Sources/AppFeature/Dependencies.swift similarity index 87% rename from Sources/AppFeature/DependencyRegistrator.swift rename to Sources/AppFeature/Dependencies.swift index 3504173f..16f2bf78 100644 --- a/Sources/AppFeature/DependencyRegistrator.swift +++ b/Sources/AppFeature/Dependencies.swift @@ -131,3 +131,18 @@ extension NavigatorKey: DependencyKey { ) ) } + +import LaunchFeature +import XXMessengerClient + +private enum PushNotificationRouterKey: DependencyKey { + static var liveValue = Stored<PushNotificationRouter?>.inMemory() + static var testValue = Stored<PushNotificationRouter?>.unimplemented() +} + +extension DependencyValues { + public var pushNotificationRouter: Stored<PushNotificationRouter?> { + get { self[PushNotificationRouterKey.self] } + set { self[PushNotificationRouterKey.self] = newValue } + } +} diff --git a/Sources/AppFeature/PushNotificationRouter.swift b/Sources/AppFeature/PushNotificationRouter.swift new file mode 100644 index 00000000..0ff84204 --- /dev/null +++ b/Sources/AppFeature/PushNotificationRouter.swift @@ -0,0 +1,57 @@ +import UIKit +import Dependencies +import AppNavigation + +import ChatFeature +import LaunchFeature +import SearchFeature +import ChatListFeature +import RequestsFeature + +extension PushNotificationRouter { + public static func live(navigationController: UINavigationController) -> PushNotificationRouter { + PushNotificationRouter { route, completion in + @Dependency(\.navigator) var navigator + @Dependency(\.app.dbManager) var dbManager + + if let launchController = navigationController.viewControllers.last as? LaunchController { + launchController.pendingPushNotificationRoute = route + } else { + switch route { + case .requests: + if !(navigationController.viewControllers.last is RequestsContainerController) { + navigator.perform(PresentRequests(on: navigationController)) + } + + case .search(username: let username): + if !(navigationController.viewControllers.last is SearchContainerController) { + navigator.perform(PresentSearch( + searching: username, + fromOnboarding: true, + on: navigationController, + animated: true + )) + } else { + (navigationController.viewControllers.last as? SearchContainerController)? + .startSearchingFor(username) + } + + case .contactChat(id: let id): + if let contact = try? dbManager.getDB().fetchContacts(.init(id: [id])).first { + navigator.perform(SetStack([ + ChatListController(), SingleChatController(contact) + ], on: navigationController)) + } + + case .groupChat(id: let id): + if let groupInfo = try? dbManager.getDB().fetchGroupInfos(.init(groupId: id)).first { + navigator.perform(SetStack([ + ChatListController(), GroupChatController(groupInfo) + ], on: navigationController)) + } + } + } + completion() + } + } +} diff --git a/Sources/AppFeature/PushRouter.swift b/Sources/AppFeature/PushRouter.swift deleted file mode 100644 index f36d1e8a..00000000 --- a/Sources/AppFeature/PushRouter.swift +++ /dev/null @@ -1,57 +0,0 @@ -import UIKit -import XXModels -import PushFeature -import ChatFeature -import SearchFeature -import LaunchFeature -import ChatListFeature -import RequestsFeature -import XXMessengerClient - -extension PushRouter { - static func live(navigationController: UINavigationController) -> PushRouter { - fatalError() -// PushRouter { route, completion in -// if let launchController = navigationController.viewControllers.last as? LaunchController { -// launchController.pendingPushRoute = route -// } else { -// switch route { -// case .requests: -// if !(navigationController.viewControllers.last is RequestsContainerController) { -// navigationController.setViewControllers([RequestsContainerController()], animated: true) -// } -// case .search(username: let username): -// if let messenger = try? DI.Container.shared.resolve() as Messenger, -// let _ = try? messenger.ud.get()?.getContact() { -// if !(navigationController.viewControllers.last is SearchContainerController) { -// navigationController.setViewControllers([ -// ChatListController(), -// SearchContainerController(username) -// ], animated: true) -// } else { -// (navigationController.viewControllers.last as? SearchContainerController)?.startSearchingFor(username) -// } -// } -// case .contactChat(id: let id): -// if let database: Database = try? DI.Container.shared.resolve(), -// let contact = try? dbManager.getDB().fetchContacts(.init(id: [id])).first { -// navigationController.setViewControllers([ -// ChatListController(), -// SingleChatController(contact) -// ], animated: true) -// } -// case .groupChat(id: let id): -// if let database: Database = try? DI.Container.shared.resolve(), -// let info = try? dbManager.getDB().fetchGroupInfos(.init(groupId: id)).first { -// navigationController.setViewControllers([ -// ChatListController(), -// GroupChatController(info) -// ], animated: true) -// } -// } -// } -// -// completion() -// } - } -} diff --git a/Sources/LaunchFeature/LaunchController.swift b/Sources/LaunchFeature/LaunchController.swift index 32e9b9b6..d6d1780b 100644 --- a/Sources/LaunchFeature/LaunchController.swift +++ b/Sources/LaunchFeature/LaunchController.swift @@ -1,7 +1,6 @@ import UIKit import Shared import Combine -import PushFeature import Dependencies import AppResources import DrawerFeature @@ -15,6 +14,8 @@ public final class LaunchController: UIViewController { private var cancellables = Set<AnyCancellable>() private var drawerCancellables = Set<AnyCancellable>() + public var pendingPushNotificationRoute: PushNotificationRouter.Route? + public override func loadView() { view = screenView } @@ -31,10 +32,10 @@ public final class LaunchController: UIViewController { navigator.perform(PresentTermsAndConditions(replacing: true, on: navigationController!)) return } -// if let route = pendingPushRoute { -// hasPendingPushRoute(route) -// return -// } + if let route = pendingPushNotificationRoute { + hasPendingPushRoute(route) + return + } navigator.perform(PresentChatList(on: navigationController!)) return } @@ -50,7 +51,7 @@ public final class LaunchController: UIViewController { viewModel.startLaunch() } - private func hasPendingPushRoute(_ route: PushRouter.Route) { + private func hasPendingPushRoute(_ route: PushNotificationRouter.Route) { switch route { case .requests: navigator.perform(PresentRequests(on: navigationController!)) diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift index 68587689..bbb68e5d 100644 --- a/Sources/LaunchFeature/LaunchViewModel.swift +++ b/Sources/LaunchFeature/LaunchViewModel.swift @@ -26,6 +26,8 @@ import XXLegacyDatabaseMigrator import class XXClient.Cancellable +import PulseLogHandler + final class LaunchViewModel { struct UpdateModel { let content: String @@ -42,16 +44,15 @@ final class LaunchViewModel { var shouldPushOnboarding = false } + @Dependency(\.app.log) var log @Dependency(\.app.bgQueue) var bgQueue @Dependency(\.permissions) var permissions @Dependency(\.app.messenger) var messenger @Dependency(\.app.dbManager) var dbManager - @Dependency(\.keychain) var keychainManager @Dependency(\.updateErrors) var updateErrors @Dependency(\.app.hudManager) var hudManager @Dependency(\.checkVersion) var checkVersion @Dependency(\.dummyTraffic) var dummyTraffic - @Dependency(\.backupService) var backupService @Dependency(\.app.toastManager) var toastManager @Dependency(\.fetchBannedList) var fetchBannedList @Dependency(\.reportingStatus) var reportingStatus @@ -166,17 +167,20 @@ final class LaunchViewModel { extension LaunchViewModel { func setupMessenger() throws { - authHandlerCancellable = authHandler { - print($0.localizedDescription) + _ = try messenger.setLogLevel(.trace) + messenger.startLogging() + + authHandlerCancellable = authHandler { [weak self] in + self?.log(.error($0 as NSError)) } - backupHandlerCancellable = backupHandler { - print($0.localizedDescription) + backupHandlerCancellable = backupHandler { [weak self] in + self?.log(.error($0 as NSError)) } - receiveFileHandlerCancellable = receiveFileHandler { - print($0.localizedDescription) + receiveFileHandlerCancellable = receiveFileHandler { [weak self] in + self?.log(.error($0 as NSError)) } - messageListenerHandlerCancellable = messageListener { - print($0.localizedDescription) + messageListenerHandlerCancellable = messageListener { [weak self] in + self?.log(.error($0 as NSError)) } if messenger.isLoaded() == false { @@ -217,18 +221,18 @@ extension LaunchViewModel { try? messenger.resumeBackup() } - groupRequestCancellable = groupRequest { - print($0) + groupRequestCancellable = groupRequest { [weak self] in + self?.log(.error($0 as NSError)) } - groupMessageHandlerCancellable = groupMessageHandler { - print($0) + groupMessageHandlerCancellable = groupMessageHandler { [weak self] in + self?.log(.error($0 as NSError)) } try messenger.startGroupChat() - try messenger.trackServices { - print($0.localizedDescription) + try messenger.trackServices { [weak self] in + self?.log(.error($0 as NSError)) } try messenger.startFileTransfer() @@ -239,10 +243,19 @@ extension LaunchViewModel { self.networkMonitor.update($0) } ) + + try failPendingProcessesFromLastSession() } } extension LaunchViewModel { + func failPendingProcessesFromLastSession() throws { + try dbManager.getDB().bulkUpdateMessages( + .init(status: [.sending]), + .init(status: .sendingFailed) + ) + } + func updateBannedList(completion: @escaping () -> Void) { fetchBannedList { result in switch result { diff --git a/Sources/PushFeature/PushRouter.swift b/Sources/LaunchFeature/PushNotificationRouter.swift similarity index 62% rename from Sources/PushFeature/PushRouter.swift rename to Sources/LaunchFeature/PushNotificationRouter.swift index 942f747e..45b7850a 100644 --- a/Sources/PushFeature/PushRouter.swift +++ b/Sources/LaunchFeature/PushNotificationRouter.swift @@ -1,6 +1,7 @@ import Foundation +import XCTestDynamicOverlay -public struct PushRouter { +public struct PushNotificationRouter { public typealias NavigateTo = (Route, @escaping () -> Void) -> Void public enum Route { @@ -17,7 +18,8 @@ public struct PushRouter { } } -public extension PushRouter { - static let noop = PushRouter { _, _ in } +public extension PushNotificationRouter { + static let unimplemented = PushNotificationRouter( + navigateTo: XCTUnimplemented("\(Self.self)") + ) } - diff --git a/Sources/PushFeature/ContentsBuilder.swift b/Sources/PushFeature/ContentsBuilder.swift deleted file mode 100644 index 9aa75041..00000000 --- a/Sources/PushFeature/ContentsBuilder.swift +++ /dev/null @@ -1,23 +0,0 @@ -import UserNotifications - -public struct ContentsBuilder { - enum Constants { - static let threadIdentifier = "new_message_identifier" - } - - public var build: (String, Push) -> UNMutableNotificationContent -} - -public extension ContentsBuilder { - static let live = ContentsBuilder { title, push in - let content = UNMutableNotificationContent() - content.badge = 1 - content.body = title - content.title = title - content.sound = .default - content.userInfo["source"] = push.source - content.userInfo["type"] = push.type.rawValue - content.threadIdentifier = Constants.threadIdentifier - return content - } -} diff --git a/Sources/PushFeature/MockPushHandler.swift b/Sources/PushFeature/MockPushHandler.swift deleted file mode 100644 index 385175c3..00000000 --- a/Sources/PushFeature/MockPushHandler.swift +++ /dev/null @@ -1,39 +0,0 @@ -import UIKit - -public struct MockPushHandler: PushHandling { - public init() {} - - public func registerToken(_ token: Data) { - // TODO - } - - public func requestAuthorization( - _ completion: @escaping (Result<Bool, Error>) -> Void - ) { - completion(.success(true)) - } - - public func handlePush( - _ notification: [AnyHashable : Any], - _ completion: @escaping (UIBackgroundFetchResult) -> Void - ) { - completion(.noData) - } - - public func handlePush( - _ request: UNNotificationRequest, - _ completion: @escaping (UNNotificationContent) -> Void - ) { - let content = UNMutableNotificationContent() - content.title = String(describing: Self.self) - completion(content) - } - - public func handleAction( - _ router: PushRouter, - _ userInfo: [AnyHashable : Any], - _ completion: @escaping () -> Void - ) { - completion() - } -} diff --git a/Sources/PushFeature/Push.swift b/Sources/PushFeature/Push.swift deleted file mode 100644 index a5ce5151..00000000 --- a/Sources/PushFeature/Push.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XXClient -import Foundation - -public struct Push { - public let type: NotificationReport.ReportType - public let source: Data -} diff --git a/Sources/PushFeature/PushExtractor.swift b/Sources/PushFeature/PushExtractor.swift deleted file mode 100644 index ee584586..00000000 --- a/Sources/PushFeature/PushExtractor.swift +++ /dev/null @@ -1,42 +0,0 @@ -import XXModels -import XXClient -import Foundation -import XXMessengerClient - -public struct PushExtractor { - enum Constants { - static let preImage = "preImage" - static let appGroup = "group.elixxir.messenger" - static let notificationData = "notificationData" - } - - public var extractFrom: ([AnyHashable: Any]) -> Result<[Push]?, Error> -} - -public extension PushExtractor { - static let live = PushExtractor { dictionary in - var environment: MessengerEnvironment = .live() - environment.ndfEnvironment = .mainnet - environment.serviceList = .userDefaults( - key: "preImage", - userDefaults: UserDefaults(suiteName: "group.elixxir.messenger")! - ) - let messenger = Messenger.live(environment) - guard let csv = dictionary[Constants.notificationData] as? String, - let defaults = UserDefaults(suiteName: Constants.appGroup) else { - return .success(nil) - } - do { - let reports = try messenger.getNotificationReports(notificationCSV: csv) - return .success( - reports - .filter { $0.forMe } - .filter { $0.type != .silent } - .filter { $0.type != .default } - .map { Push(type: $0.type, source: $0.source) } - ) - } catch { - return .failure(error) - } - } -} diff --git a/Sources/PushFeature/PushHandler.swift b/Sources/PushFeature/PushHandler.swift deleted file mode 100644 index 86bb7a29..00000000 --- a/Sources/PushFeature/PushHandler.swift +++ /dev/null @@ -1,220 +0,0 @@ -import UIKit -import AppCore -import Defaults -import XXClient -import XXModels -import XXDatabase -import ReportingFeature -import XXMessengerClient -import ComposableArchitecture - -public final class PushHandler: PushHandling { - private enum Constants { - static let appGroup = "group.elixxir.messenger" - static let usernamesSetting = "isShowingUsernames" - } - - @Dependency(\.app.messenger) var messenger: Messenger - - @KeyObject(.pushNotifications, defaultValue: false) var isPushEnabled: Bool - - let requestAuth: RequestAuth - public static let defaultRequestAuth = UNUserNotificationCenter.current().requestAuthorization - public typealias RequestAuth = (UNAuthorizationOptions, @escaping (Bool, Error?) -> Void) -> Void - - public var pushExtractor: PushExtractor - public var contentsBuilder: ContentsBuilder - public var applicationState: () -> UIApplication.State - - public init( - requestAuth: @escaping RequestAuth = defaultRequestAuth, - pushExtractor: PushExtractor = .live, - contentsBuilder: ContentsBuilder = .live, - applicationState: @escaping () -> UIApplication.State = { UIApplication.shared.applicationState } - ) { - self.requestAuth = requestAuth - self.pushExtractor = pushExtractor - self.contentsBuilder = contentsBuilder - self.applicationState = applicationState - } - - public func registerToken(_ token: Data) { - do { - try RegisterForNotifications.live( - e2eId: messenger.e2e.get()!.getId(), - token: token.map { String(format: "%02hhx", $0) }.joined() - ) - } catch { - print(error.localizedDescription) - isPushEnabled = false - } - } - - public func requestAuthorization( - _ completion: @escaping (Result<Bool, Error>) -> Void - ) { - let options: UNAuthorizationOptions = [.alert, .sound, .badge] - - requestAuth(options) { granted, error in - guard let error = error else { - completion(.success(granted)) - return - } - - completion(.failure(error)) - } - } - - public func handlePush( - _ userInfo: [AnyHashable: Any], - _ completion: @escaping (UIBackgroundFetchResult) -> Void - ) { - do { - guard - let pushes = try pushExtractor.extractFrom(userInfo).get(), - applicationState() == .background, - pushes.isEmpty == false - else { - completion(.noData) - return - } - - let content = contentsBuilder.build("New Messages Available", pushes.first!) - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) - let request = UNNotificationRequest(identifier: Bundle.main.bundleIdentifier!, content: content, trigger: trigger) - - UNUserNotificationCenter.current().add(request) { error in - if error == nil { - completion(.newData) - } else { - completion(.failed) - } - } - } catch { - completion(.failed) - } - } - - public func handlePush( - _ request: UNNotificationRequest, - _ completion: @escaping (UNNotificationContent) -> Void - ) { - guard let pushes = try? pushExtractor.extractFrom(request.content.userInfo).get(), !pushes.isEmpty, - let defaults = UserDefaults(suiteName: Constants.appGroup) else { return } - - let dbPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! - .appendingPathComponent("xxm_database") - .appendingPathExtension("sqlite").path - - let tuples: [(String, Push)] = pushes.compactMap { - guard let dbManager = try? Database.onDisk(path: dbPath), - let contact = try? dbManager.fetchContacts(.init(id: [$0.source])).first else { - return (getStringForUnknown(type: $0.type), $0) - } - - if ReportingStatus.live().isEnabled(), (contact.isBlocked || contact.isBanned) { - return nil - } - - if let showSender = defaults.value(forKey: Constants.usernamesSetting) as? Bool, showSender == true { - let name = (contact.nickname ?? contact.username) ?? "" - return (getStringForKnown(name: name, type: $0.type), $0) - } else { - return (getStringForUnknown(type: $0.type), $0) - } - } - - tuples - .map(contentsBuilder.build) - .forEach { completion($0) } - } - - public func handleAction( - _ router: PushRouter, - _ userInfo: [AnyHashable : Any], - _ completion: @escaping () -> Void - ) { - guard let typeString = userInfo["type"] as? String, - let type = NotificationReport.ReportType.init(rawValue: typeString) else { - completion() - return - } - - let route: PushRouter.Route - - switch type { - case .e2e: - guard let source = userInfo["source"] as? Data else { - completion() - return - } - - route = .contactChat(id: source) - - case .group: - guard let source = userInfo["source"] as? Data else { - completion() - return - } - - route = .groupChat(id: source) - - case .request, .groupRQ: - route = .requests - - case .silent, .`default`: - fatalError("Silent/Default push types should be filtered at this point") - - case .reset, .endFT, .confirm: - route = .requests - } - - router.navigateTo(route, completion) - } -} - -private func getStringForUnknown(type: NotificationReport.ReportType) -> String { - switch type { - case .`default`, .silent: - return "" - case .request: - return "Request received" - case .reset: - return "One of your contacts has restored their account" - case .confirm: - return "Request accepted" - case .e2e: - return "New private message" - case .group: - return "New group message" - case .endFT: - return "New media received" - case .groupRQ: - return "Group request received" - } -} - -private func getStringForKnown( - name: String, - type: NotificationReport.ReportType -) -> String { - switch type { - case .silent, .`default`: - return "" - case .e2e: - return String(format: "%@ sent you a private message", name) - case .reset: - return String(format: "%@ restored their account", name) - case .endFT: - return String(format: "%@ sent you a file", name) - case .group: - return String(format: "%@ sent you a group message", name) - case .groupRQ: - return String(format: "%@ sent you a group request", name) - case .confirm: - return String(format: "%@ confirmed your contact request", name) - case .request: - return String(format: "%@ sent you a contact request", name) - } -} diff --git a/Sources/PushFeature/PushHandling.swift b/Sources/PushFeature/PushHandling.swift deleted file mode 100644 index 1fe3a41b..00000000 --- a/Sources/PushFeature/PushHandling.swift +++ /dev/null @@ -1,69 +0,0 @@ -import UIKit - -public protocol PushHandling { - /// Submits the APNS token to a 3rd-party service. - /// This should be called whenever the user accepts - /// receiving remote push notifications. - /// - /// - Parameters: - /// - token: The APNS provided token - /// - func registerToken( - _ token: Data - ) - - /// Prompts a system alert to the user requesting - /// permission for receiving remote push notifications - /// - /// - Parameters: - /// - completion: Async result closure containing the user reponse - /// - func requestAuthorization( - _ completion: @escaping (Result<Bool, Error>) -> Void - ) - - /// Evaluates if the notification should be displayed or not - /// and if yes, how should it look like. - /// - /// - Note: This function should be called by the main app target - /// - Warning: The notifications should only appear if the app is in background - /// - /// - Parameters: - /// - userInfo: Dictionary contaning the payload of the remote push - /// - completion: Async closure containing the operation chosed - /// - func handlePush( - _ userInfo: [AnyHashable: Any], - _ completion: @escaping (UIBackgroundFetchResult) -> Void - ) - - /// Evaluates if the notification should be displayed or not - /// and if yes, how it should look like and who is it from - /// - /// - Note: This function should be called by the `NotificationExtension` - /// - /// - Parameters: - /// - request: The notification request that arrived for the `NotificationExtension` - /// - completion: Async closure containing the operation chosed - /// - func handlePush( - _ request: UNNotificationRequest, - _ completion: @escaping (UNNotificationContent) -> Void - ) - - /// Deeplinks to any UI flow set within the notification. - /// It can get called either when the user starts the app - /// from a notification or when the user has the app in - /// background and resumes the app by tapping on a push - /// - /// - Parameters: - /// - router: Router instance that will decide the correct UI flow - /// - userInfo: Dictionary contaning the payload of the notification - /// - completion: Async empty closure - /// - func handleAction( - _ router: PushRouter, - _ userInfo: [AnyHashable: Any], - _ completion: @escaping () -> Void - ) -} diff --git a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved index a54280f2..97e0e358 100644 --- a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -243,6 +243,15 @@ "version" : "2.1.1" } }, + { + "identity" : "pulse", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kean/Pulse.git", + "state" : { + "revision" : "6b682c529d98a38e6fdffee2a8bfa40c8de30821", + "version" : "2.1.3" + } + }, { "identity" : "quick", "kind" : "remoteSourceControl", -- GitLab