diff --git a/App/NotificationExtension/NotificationService.swift b/App/NotificationExtension/NotificationService.swift index 495958b9ab1e9b26981c9996f088b17b5fa1053d..4444b66a596ebbc62040280bd2c675f2c9744515 100644 --- a/App/NotificationExtension/NotificationService.swift +++ b/App/NotificationExtension/NotificationService.swift @@ -2,12 +2,12 @@ import PushFeature import UserNotifications final class NotificationService: UNNotificationServiceExtension { - private let pushHandler = PushHandler() + private let pushHandler = PushHandler() - override func didReceive( - _ request: UNNotificationRequest, - withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void - ) { - pushHandler.handlePush(request, contentHandler) - } + override func didReceive( + _ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void + ) { + pushHandler.handlePush(request, contentHandler) + } } diff --git a/App/client-ios.xcodeproj/project.pbxproj b/App/client-ios.xcodeproj/project.pbxproj index 87ff542bdc1cb222888508d0c6737164e264baf4..b173e088056b9a96da598954f6a23b4d0024d8d0 100644 --- a/App/client-ios.xcodeproj/project.pbxproj +++ b/App/client-ios.xcodeproj/project.pbxproj @@ -448,7 +448,7 @@ CODE_SIGN_ENTITLEMENTS = "client-ios/Resources/client-ios.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 289; + CURRENT_PROJECT_VERSION = 292; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; @@ -487,7 +487,7 @@ CODE_SIGN_ENTITLEMENTS = "client-ios/Resources/client-ios.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 289; + CURRENT_PROJECT_VERSION = 292; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; @@ -522,7 +522,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 289; + CURRENT_PROJECT_VERSION = 292; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -553,7 +553,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 289; + CURRENT_PROJECT_VERSION = 292; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift index 25a3041a36827376d2fc3dc0a60e41bd82522b48..ec35ad28c85991a2255727da59a5dcaee95429dc 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/App/AppDelegate.swift @@ -21,6 +21,7 @@ import CloudFilesDropbox public class AppDelegate: UIResponder, UIApplicationDelegate { @Dependency var navigator: Navigator + @Dependency var pushRouter: PushRouter @Dependency var pushHandler: PushHandling @Dependency var crashReporter: CrashReporter @@ -52,6 +53,10 @@ public class AppDelegate: UIResponder, UIApplicationDelegate { window?.makeKeyAndVisible() DependencyRegistrator.registerNavigators(navController) + + DependencyInjection.Container.shared.register( + PushRouter.live(navigationController: navController) + ) return true } @@ -86,12 +91,10 @@ public class AppDelegate: UIResponder, UIApplicationDelegate { guard UIApplication.shared.backgroundTimeRemaining > 9 else { if !self.forceFailedPendingMessages { self.forceFailedPendingMessages = true - let query = Message.Query(status: [.sending]) let assignment = Message.Assignments(status: .sendingFailed) _ = try? database.bulkUpdateMessages(query, assignment) } - return } }) @@ -166,7 +169,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { withCompletionHandler completionHandler: @escaping () -> Void ) { let userInfo = response.notification.request.content.userInfo - //pushHandler.handleAction(pushRouter, userInfo, completionHandler) + pushHandler.handleAction(pushRouter, userInfo, completionHandler) } public func application( diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift index 795e5d7d5b106e3f2de2061d9b4a241c046ad253..dfa6014673d4c9c4a9054092e7e252478ab0471e 100644 --- a/Sources/App/DependencyRegistrator.swift +++ b/Sources/App/DependencyRegistrator.swift @@ -243,38 +243,13 @@ struct DependencyRegistrator { ) as Navigator) } - // container.register( - // ProfileCoordinator( - // imagePickerFactory: UIImagePickerController.init, - // permissionFactory: RequestPermissionController.init, - // countriesFactory: CountryListController.init(_:) - // //codeFactory: ProfileCodeController.init(_:_:) - // ) as ProfileCoordinating) - - // container.register( - // SearchCoordinator( - // contactsFactory: ContactListController.init, - // requestsFactory: RequestsContainerController.init, - // contactFactory: ContactController.init(_:), - // countriesFactory: CountryListController.init(_:) - // ) as SearchCoordinating) - - // container.register( - // ContactListCoordinator( - // scanFactory: ScanContainerController.init, - // searchFactory: SearchContainerController.init, - // newGroupFactory: CreateGroupController.init, - // requestsFactory: RequestsContainerController.init, - // contactFactory: ContactController.init(_:), - // singleChatFactory: SingleChatController.init(_:), - // groupChatFactory: GroupChatController.init(_:), - // sideMenuFactory: MenuController.init(_:_:), - // groupDrawerFactory: CreateDrawerController.init(_:_:) - // ) as ContactListCoordinating) - static private func registerCommonDependencies() { var environment: MessengerEnvironment = .live() environment.ndfEnvironment = .mainnet + environment.serviceList = .userDefaults( + key: "preImage", + userDefaults: UserDefaults(suiteName: "group.elixxir.messenger")! + ) environment.udEnvironment = .init( address: AlternativeUDConstants.address, cert: AlternativeUDConstants.cert.data(using: .utf8)!, diff --git a/Sources/LaunchFeature/LaunchViewModel+Messenger.swift b/Sources/LaunchFeature/LaunchViewModel+Messenger.swift index 364be5cf10c6295580e83fc02ec1ccd06ad36651..0fa99f35a9c81debbae2c2cddcaefaa5caab3739 100644 --- a/Sources/LaunchFeature/LaunchViewModel+Messenger.swift +++ b/Sources/LaunchFeature/LaunchViewModel+Messenger.swift @@ -79,8 +79,10 @@ extension LaunchViewModel { } func setupLogWriter() { - _ = try! SetLogLevel.live(.fatal) - RegisterLogWriter.live(.init(handle: { XXLogger.live().debug($0) })) + _ = try! SetLogLevel.live(.error) + RegisterLogWriter.live(.init(handle: { + XXLogger.live().debug($0) + })) } func listenToNetworkUpdates() { @@ -439,6 +441,11 @@ extension LaunchViewModel { if !messenger.isBackupRunning() { try? messenger.resumeBackup() } + + try messenger.trackServices { + print(">>> Error on track services callback: \($0.localizedDescription)") + } + // TODO: Biometric auth } } diff --git a/Sources/PushFeature/Push.swift b/Sources/PushFeature/Push.swift index 51c25bd52f513816ca850930e07a4c0a72204798..a5ce515126a20ce0fd6e01671eba919d338472c9 100644 --- a/Sources/PushFeature/Push.swift +++ b/Sources/PushFeature/Push.swift @@ -1,15 +1,7 @@ +import XXClient import Foundation public struct Push { - public let type: PushType - public let source: Data? - - public init?(type: String, source: Data?) { - guard let pushType = PushType(rawValue: type) else { - return nil - } - - self.type = pushType - self.source = source - } + public let type: NotificationReport.ReportType + public let source: Data } diff --git a/Sources/PushFeature/PushExtractor.swift b/Sources/PushFeature/PushExtractor.swift index 6d080841ae0e87120bb57391d09cd41e73bf39d4..dd00bcbe3a9b3d488a43f7ec5975cd6c188fb1ab 100644 --- a/Sources/PushFeature/PushExtractor.swift +++ b/Sources/PushFeature/PushExtractor.swift @@ -1,57 +1,46 @@ import XXModels import Foundation +import XXClient +import XXMessengerClient import DependencyInjection 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> + 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 - NSLog("EXTRACTING PUSH A...") - - guard let database = try? DependencyInjection.Container.shared.resolve() as Database, - let anyone = try? database.fetchContacts(.init()).first else { - NSLog("EXTRACTING PUSH B...") - return .success(nil) - } - - NSLog("EXTRACTING PUSH C...") - - return .success([.init(type: PushType.request.rawValue, source: anyone.id)!]) - -// guard let messenger = try? DependencyInjection.Container.shared.resolve() as Messenger, -// let data = dictionary[Constants.notificationData] as? String else { -// return .success(nil) -// } -// -// do { -// let reportFunctor = GetNotificationsReport.live() -// let report = try reportFunctor( -// e2eId: messenger.e2e.get()!.getId(), -// notificationCSV: data, -// marshaledServices: Data() // <--- ??? -// ) -// -// guard report.forMe, -// report.type != .silent, -// report.type != .default -// else { -// return .success(nil) -// } -// -// return .success([Push( -// type: report.type.rawValue, -// source: report.source)! -// ]) -// } catch { -// return .failure(error) -// } + 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 report = try messenger.getNotificationReport(notificationCSV: csv) + guard report.forMe, + report.type != .silent, + report.type != .default + else { + return .success(nil) + } + return .success([Push( + type: report.type, + source: report.source + )]) + } catch { + return .failure(error) } + } } diff --git a/Sources/PushFeature/PushHandler.swift b/Sources/PushFeature/PushHandler.swift index b43a575d275a44c3c6e63979ecf37c5d1cf3fdbd..c22b66dd5b1bf6398dad00faa905fbda525bb3b8 100644 --- a/Sources/PushFeature/PushHandler.swift +++ b/Sources/PushFeature/PushHandler.swift @@ -8,171 +8,212 @@ import ReportingFeature import DependencyInjection public final class PushHandler: PushHandling { - private enum Constants { - static let appGroup = "group.elixxir.messenger" - static let usernamesSetting = "isShowingUsernames" + private enum Constants { + static let appGroup = "group.elixxir.messenger" + static let usernamesSetting = "isShowingUsernames" + } + + @Dependency 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 } + } - @Dependency var messenger: Messenger - @Dependency var reportingStatus: ReportingStatus - - @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] + 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 - } + requestAuth(options) { granted, error in + guard let error = error else { + completion(.success(granted)) + return + } - completion(.failure(error)) - } + 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( + _ 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) + } } - 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 userId = $0.source, - let dbManager = try? Database.onDisk(path: dbPath), - let contact = try? dbManager.fetchContacts(.init(id: [userId])).first else { - return ($0.type.unknownSenderContent!, $0) - } - - if reportingStatus.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 ($0.type.knownSenderContent(name)!, $0) - } else { - return ($0.type.unknownSenderContent!, $0) - } - } - - tuples - .map(contentsBuilder.build) - .forEach { completion($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 } - public func handleAction( - _ router: PushRouter, - _ userInfo: [AnyHashable : Any], - _ completion: @escaping () -> Void - ) { - guard let typeString = userInfo["type"] as? String, - let type = PushType(rawValue: typeString) else { - completion() - return - } + let route: PushRouter.Route - let route: PushRouter.Route + switch type { + case .e2e: + guard let source = userInfo["source"] as? Data else { + completion() + return + } - switch type { - case .e2e: - guard let source = userInfo["source"] as? Data else { - completion() - return - } + route = .contactChat(id: source) - route = .contactChat(id: source) + case .group: + guard let source = userInfo["source"] as? Data else { + completion() + return + } - case .group: - guard let source = userInfo["source"] as? Data else { - completion() - return - } + route = .groupChat(id: source) - route = .groupChat(id: source) + case .request, .groupRQ: + route = .requests - case .request, .groupRq: - route = .requests + case .silent, .`default`: + fatalError("Silent/Default push types should be filtered at this point") - case .silent, .`default`: - fatalError("Silent/Default push types should be filtered at this point") + case .reset, .endFT, .confirm: + route = .requests + } - case .reset, .endFT, .confirm: - route = .requests - } + router.navigateTo(route, completion) + } +} - 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/PushType.swift b/Sources/PushFeature/PushType.swift deleted file mode 100644 index 7cd2fefefcbce0968a7d4478aa4e5cc01d771f98..0000000000000000000000000000000000000000 --- a/Sources/PushFeature/PushType.swift +++ /dev/null @@ -1,53 +0,0 @@ -public enum PushType: String { - case e2e - case reset - case endFT - case group - case silent - case groupRq - case confirm - case request - case `default` - - var unknownSenderContent: String? { - switch self { - case .silent, .`default`: - return nil - case .endFT: - return "New media received" - case .group: - return "New group message" - case .groupRq: - return "Group request received" - case .e2e: - return "New private message" - case .reset: - return "One of your contacts has restored their account" - case .request: - return "Request received" - case .confirm: - return "Request accepted" - } - } - - var knownSenderContent: (String) -> String? { - switch self { - case .silent, .`default`: - return { _ in nil } - case .e2e: - return { String(format: "%@ sent you a private message", $0) } - case .reset: - return { String(format: "%@ restored their account", $0) } - case .endFT: - return { String(format: "%@ sent you a file", $0) } - case .group: - return { String(format: "%@ sent you a group message", $0) } - case .groupRq: - return { String(format: "%@ sent you a group request", $0) } - case .confirm: - return { String(format: "%@ confirmed your contact request", $0) } - case .request: - return { String(format: "%@ sent you a contact request", $0) } - } - } -} diff --git a/Sources/ReportingFeature/ReportingStatus.swift b/Sources/ReportingFeature/ReportingStatus.swift index 1bb72983430de28a6d46c47cd7ec2a3a21612659..f6346ce1350caa9bca8ead86515724a5edf7171d 100644 --- a/Sources/ReportingFeature/ReportingStatus.swift +++ b/Sources/ReportingFeature/ReportingStatus.swift @@ -1,51 +1,51 @@ import Combine public struct ReportingStatus { - public var isOptional: () -> Bool - public var isEnabled: () -> Bool - public var isEnabledPublisher: () -> AnyPublisher<Bool, Never> - public var enable: (Bool) -> Void + public var isOptional: () -> Bool + public var isEnabled: () -> Bool + public var isEnabledPublisher: () -> AnyPublisher<Bool, Never> + public var enable: (Bool) -> Void } extension ReportingStatus { - public static func live( - isOptional: ReportingStatusIsOptional = .live(), - isEnabled: ReportingStatusIsEnabled = .live() - ) -> ReportingStatus { - ReportingStatus( - isOptional: { - isOptional.get() - }, - isEnabled: { - if isOptional.get() == false { - return true - } + public static func live( + isOptional: ReportingStatusIsOptional = .live(), + isEnabled: ReportingStatusIsEnabled = .live() + ) -> ReportingStatus { + ReportingStatus( + isOptional: { + isOptional.get() + }, + isEnabled: { + if isOptional.get() == false { + return true + } - return isEnabled.get() - }, - isEnabledPublisher: { - if isOptional.get() == false { - return Just(true).eraseToAnyPublisher() - } + return isEnabled.get() + }, + isEnabledPublisher: { + if isOptional.get() == false { + return Just(true).eraseToAnyPublisher() + } - return isEnabled.publisher() - }, - enable: { enabled in - isEnabled.set(enabled) - } - ) - } + return isEnabled.publisher() + }, + enable: { enabled in + isEnabled.set(enabled) + } + ) + } - public static func mock( - isEnabled: Bool = false, - isOptional: Bool = true - ) -> ReportingStatus { - let isEnabledSubject = CurrentValueSubject<Bool, Never>(isEnabled) - return ReportingStatus( - isOptional: { isOptional }, - isEnabled: { isEnabledSubject.value }, - isEnabledPublisher: { isEnabledSubject.eraseToAnyPublisher() }, - enable: { isEnabledSubject.send($0) } - ) - } + public static func mock( + isEnabled: Bool = false, + isOptional: Bool = true + ) -> ReportingStatus { + let isEnabledSubject = CurrentValueSubject<Bool, Never>(isEnabled) + return ReportingStatus( + isOptional: { isOptional }, + isEnabled: { isEnabledSubject.value }, + isEnabledPublisher: { isEnabledSubject.eraseToAnyPublisher() }, + enable: { isEnabledSubject.send($0) } + ) + } } diff --git a/Sources/SearchFeature/Controllers/SearchLeftController.swift b/Sources/SearchFeature/Controllers/SearchLeftController.swift index 8370152285392b1d722c41013a687dee5c254502..3db6a1dc74fed08a9856943d2506dd6a54a40195 100644 --- a/Sources/SearchFeature/Controllers/SearchLeftController.swift +++ b/Sources/SearchFeature/Controllers/SearchLeftController.swift @@ -108,24 +108,6 @@ final class SearchLeftController: UIViewController { } private func setupBindings() { - // viewModel.hudPublisher - // .removeDuplicates() - // .receive(on: DispatchQueue.main) - // .sink { [unowned self] in - // hud.update(with: $0) - // - // if case .onAction = $0, let hudBtn = hud.actionButton { - // hudBtn.publisher(for: .touchUpInside) - // .receive(on: DispatchQueue.main) - // .sink { [unowned self] in viewModel.didTapCancelSearch() } - // .store(in: &self.hudCancellables) - // } else { - // hudCancellables.forEach { $0.cancel() } - // hudCancellables.removeAll() - // } - // } - // .store(in: &cancellables) - viewModel .statePublisher .map(\.item) diff --git a/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift b/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift index 9306052b15cd56eb80c7d649635936fab0676334..011442d5025d3df89d5b23dd2e9aabe015b0ea3f 100644 --- a/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift @@ -1,8 +1,8 @@ import UIKit import Combine import Defaults -import PushFeature import XXClient +import PushFeature import DependencyInjection final class SearchContainerViewModel { diff --git a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift index 8f21ff47a2f5d6fab2517d555ec355b623afc529..93fe554a846e084811fbc9f4474821037c7ed0be 100644 --- a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift @@ -62,11 +62,19 @@ final class SearchLeftViewModel { if let pendingInvitation = invitation { invitation = nil stateSubject.value.input = pendingInvitation - hudController.show(.init(actionTitle: Localized.Ud.Search.cancel)) + hudController.show(.init( + actionTitle: Localized.Ud.Search.cancel, + hasDotAnimation: true, + onTapClosure: { [weak self] in + guard let self else { return } + self.didTapCancelSearch() + } + )) networkCancellable.removeAll() - networkMonitor.statusPublisher + networkMonitor + .statusPublisher .first { $0 == .available } .eraseToAnyPublisher() .flatMap { _ in @@ -102,7 +110,14 @@ final class SearchLeftViewModel { func didStartSearching() { guard stateSubject.value.input.isEmpty == false else { return } - hudController.show(.init(actionTitle: Localized.Ud.Search.cancel)) + hudController.show(.init( + actionTitle: Localized.Ud.Search.cancel, + hasDotAnimation: true, + onTapClosure: { [weak self] in + guard let self else { return } + self.didTapCancelSearch() + } + )) var content = stateSubject.value.input diff --git a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift index 4337264c1fcd97117547e4b8f61d971446e195cc..63d3713b68c07fe9cf968581314e2767bc850714 100644 --- a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift +++ b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift @@ -118,9 +118,12 @@ final class SettingsViewModel { case .success(let granted): self.pushNotifications = granted self.stateRelay.value.isPushNotification = granted - if granted { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() }} + if granted { + DispatchQueue.main.async { + UIApplication.shared.registerForRemoteNotifications() + } + } self.hudController.dismiss() - case .failure(let error): self.hudController.show(.init(error: error)) self.pushNotifications = false @@ -130,19 +133,15 @@ final class SettingsViewModel { } else { backgroundScheduler.schedule { [weak self] in guard let self else { return } - do { try UnregisterForNotifications.live( e2eId: self.messenger.e2e.get()!.getId() ) - self.hudController.dismiss() } catch { let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) self.hudController.show(.init(content: xxError)) - } - self.pushNotifications = false self.stateRelay.value.isPushNotification = false } diff --git a/Sources/Shared/Controllers/HUDController.swift b/Sources/Shared/Controllers/HUDController.swift index fc48d6c7dd643aebcbf21aa24a8e6e71732a2ede..7606cd7fd85f4255d6990c804baeed7b8b20d3b0 100644 --- a/Sources/Shared/Controllers/HUDController.swift +++ b/Sources/Shared/Controllers/HUDController.swift @@ -1,6 +1,9 @@ import Combine +import Foundation public final class HUDController { + private var timer: Timer? + var modelPublisher: AnyPublisher<HUDModel?, Never> { modelSubject.eraseToAnyPublisher() } @@ -19,6 +22,13 @@ public final class HUDController { return } + if model.isAutoDismissable { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in + guard let self else { return } + self.modelSubject.send(nil) + } + } + modelSubject.send(model) } } diff --git a/Sources/Shared/Models/HUDModel.swift b/Sources/Shared/Models/HUDModel.swift index ddc8fb5c2cd3ae20e8733147c937e66c59411b96..c77d5607aca8b33031373e17d47d087d36aedce8 100644 --- a/Sources/Shared/Models/HUDModel.swift +++ b/Sources/Shared/Models/HUDModel.swift @@ -13,7 +13,6 @@ public struct HUDModel { content: String? = nil, actionTitle: String? = nil, hasDotAnimation: Bool = false, - isAutoDismissable: Bool = false, onTapClosure: (() -> Void)? = nil ) { self.title = title @@ -21,7 +20,7 @@ public struct HUDModel { self.actionTitle = actionTitle self.onTapClosure = onTapClosure self.hasDotAnimation = hasDotAnimation - self.isAutoDismissable = isAutoDismissable + self.isAutoDismissable = onTapClosure == nil && !hasDotAnimation } public init(