diff --git a/App/client-ios.xcodeproj/project.pbxproj b/App/client-ios.xcodeproj/project.pbxproj index a3615f3201af5b3a10ab768fcd6283c37af38c1f..bddd3873d8a672f63620f6732ff81d1cc9826096 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 = 259; + CURRENT_PROJECT_VERSION = 266; 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 = 259; + CURRENT_PROJECT_VERSION = 266; 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 = 259; + CURRENT_PROJECT_VERSION = 266; 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 = 259; + CURRENT_PROJECT_VERSION = 266; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/App/client-ios.xcodeproj/xcshareddata/xcschemes/Release.xcscheme b/App/client-ios.xcodeproj/xcshareddata/xcschemes/Release.xcscheme index 800c200df2b4ad94d75e5144e7951bdcd91611f2..b47f627ae04ed4310d32bd7d0d0f1e5176aa6b0b 100644 --- a/App/client-ios.xcodeproj/xcshareddata/xcschemes/Release.xcscheme +++ b/App/client-ios.xcodeproj/xcshareddata/xcschemes/Release.xcscheme @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <Scheme LastUpgradeVersion = "1200" - version = "1.7"> + version = "1.8"> <BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "YES"> diff --git a/App/client-ios/Resources/Info.plist b/App/client-ios/Resources/Info.plist index d8cb845b40392068f453251c9c0aa72cd00979db..ee4d813d2e0ccc94a932d66a36070d80705cab5b 100644 --- a/App/client-ios/Resources/Info.plist +++ b/App/client-ios/Resources/Info.plist @@ -105,6 +105,6 @@ <key>UIViewControllerBasedStatusBarAppearance</key> <true/> <key>isReportingOptional</key> - <false/> + <true/> </dict> </plist> diff --git a/Package.swift b/Package.swift index c7012b91b7ea7166428d28193ca4e0c1d2e12ff6..8d4ef5559502d9c460fc78f7ef983fd72e941b4e 100644 --- a/Package.swift +++ b/Package.swift @@ -115,9 +115,9 @@ let package = Package( .upToNextMajor(from: "1.6.0") ), .package( -// path: "../elixxir-dapps-sdk-swift" - url: "https://git.xx.network/elixxir/elixxir-dapps-sdk-swift", - branch: "development" + path: "../elixxir-dapps-sdk-swift" +// url: "https://git.xx.network/elixxir/elixxir-dapps-sdk-swift", +// branch: "development" ), .package( url: "https://git.xx.network/elixxir/client-ios-db.git", @@ -414,6 +414,7 @@ let package = Package( .target(name: "DropboxFeature"), .target(name: "GoogleDriveFeature"), .target(name: "DependencyInjection"), + .product(name: "XXDatabase", package: "client-ios-db"), .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), ] ), @@ -446,6 +447,7 @@ let package = Package( .target(name: "Defaults"), .target(name: "Keychain"), .target(name: "Voxophone"), + .target(name: "Models"), .target(name: "Permissions"), .target(name: "Presentation"), .target(name: "DrawerFeature"), @@ -501,6 +503,7 @@ let package = Package( .target(name: "Defaults"), .target(name: "PushFeature"), .target(name: "Permissions"), + .target(name: "BackupFeature"), .target(name: "DropboxFeature"), .target(name: "VersionChecking"), .target(name: "ReportingFeature"), diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift index 4ea40cf53608c20392ba77811f139963e8300e07..26b76e3ede2160f5bcce6ed1e2f1018adfa1d98f 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/App/AppDelegate.swift @@ -17,197 +17,196 @@ import XXClient import XXMessengerClient public class AppDelegate: UIResponder, UIApplicationDelegate { - @Dependency private var pushRouter: PushRouter - @Dependency private var pushHandler: PushHandling - @Dependency private var crashReporter: CrashReporter - @Dependency private var dropboxService: DropboxInterface - - @KeyObject(.hideAppList, defaultValue: false) var hideAppList: Bool - @KeyObject(.recordingLogs, defaultValue: true) var recordingLogs: Bool - @KeyObject(.crashReporting, defaultValue: true) var isCrashReportingEnabled: Bool - - var calledStopNetwork = false - var forceFailedPendingMessages = false - - var coverView: UIView? - var backgroundTimer: Timer? - public var window: UIWindow? - - public func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - #if DEBUG - DependencyRegistrator.registerForMock() - #else - DependencyRegistrator.registerForLive() - #endif - - if recordingLogs { - XXLogger.start() - } - - crashReporter.configure() - crashReporter.setEnabled(isCrashReportingEnabled) - - UNUserNotificationCenter.current().delegate = self - - let window = Window() - let navController = UINavigationController(rootViewController: LaunchController()) - window.rootViewController = StatusBarViewController(ToastViewController(navController)) - window.backgroundColor = UIColor.white - window.makeKeyAndVisible() - self.window = window - - DependencyInjection.Container.shared.register( - PushRouter.live(navigationController: navController) - ) - - return true + @Dependency private var pushRouter: PushRouter + @Dependency private var pushHandler: PushHandling + @Dependency private var crashReporter: CrashReporter + @Dependency private var dropboxService: DropboxInterface + + @KeyObject(.hideAppList, defaultValue: false) var hideAppList: Bool + @KeyObject(.recordingLogs, defaultValue: true) var recordingLogs: Bool + @KeyObject(.crashReporting, defaultValue: true) var isCrashReportingEnabled: Bool + + var calledStopNetwork = false + var forceFailedPendingMessages = false + + var coverView: UIView? + var backgroundTimer: Timer? + public var window: UIWindow? + + public func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { +#if DEBUG + DependencyRegistrator.registerForMock() +#else + DependencyRegistrator.registerForLive() +#endif + + if recordingLogs { + XXLogger.start() } - public func application(application: UIApplication, shouldAllowExtensionPointIdentifier: String) -> Bool { - false - } - - public func applicationDidEnterBackground(_ application: UIApplication) { - if let messenger = try? DependencyInjection.Container.shared.resolve() as Messenger, - let database = try? DependencyInjection.Container.shared.resolve() as Database, - let cMix = messenger.cMix.get() { - let backgroundTask = application.beginBackgroundTask(withName: "xx.stop.network") {} - - backgroundTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { timer in - print(">>> .backgroundTimeRemaining: \(UIApplication.shared.backgroundTimeRemaining)") - - guard UIApplication.shared.backgroundTimeRemaining > 8 else { - if !self.calledStopNetwork { - self.calledStopNetwork = true - try! cMix.stopNetworkFollower() - print(">>> Called stopNetworkFollower") - } else { - if cMix.hasRunningProcesses() == false { - application.endBackgroundTask(backgroundTask) - timer.invalidate() - } - } - - return - } - - 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 - } - }) + crashReporter.configure() + crashReporter.setEnabled(isCrashReportingEnabled) + + UNUserNotificationCenter.current().delegate = self + + let window = Window() + let navController = UINavigationController(rootViewController: LaunchController()) + window.rootViewController = StatusBarViewController(ToastViewController(navController)) + window.backgroundColor = UIColor.white + window.makeKeyAndVisible() + self.window = window + + DependencyInjection.Container.shared.register( + PushRouter.live(navigationController: navController) + ) + + return true + } + + public func application(application: UIApplication, shouldAllowExtensionPointIdentifier: String) -> Bool { + false + } + + public func applicationDidEnterBackground(_ application: UIApplication) { + if let messenger = try? DependencyInjection.Container.shared.resolve() as Messenger, + let database = try? DependencyInjection.Container.shared.resolve() as Database, + let cMix = try? messenger.cMix.tryGet() { + let backgroundTask = application.beginBackgroundTask(withName: "xx.stop.network") {} + + backgroundTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { timer in + print(">>> .backgroundTimeRemaining: \(UIApplication.shared.backgroundTimeRemaining)") + + guard UIApplication.shared.backgroundTimeRemaining > 8 else { + if !self.calledStopNetwork { + self.calledStopNetwork = true + try! messenger.stop() + print(">>> Called stopNetworkFollower") + } else { + if cMix.hasRunningProcesses() == false { + application.endBackgroundTask(backgroundTask) + timer.invalidate() + } + } + + return } - } - public func applicationWillResignActive(_ application: UIApplication) { - if hideAppList { - coverView?.removeFromSuperview() - coverView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) - coverView?.frame = window?.bounds ?? .zero - window?.addSubview(coverView!) - } - } - - public func applicationWillTerminate(_ application: UIApplication) { - if let messenger = try? DependencyInjection.Container.shared.resolve() as Messenger, - let cMix = messenger.cMix.get() { - try? cMix.stopNetworkFollower() - } - } + guard UIApplication.shared.backgroundTimeRemaining > 9 else { + if !self.forceFailedPendingMessages { + self.forceFailedPendingMessages = true - public func applicationWillEnterForeground(_ application: UIApplication) { - if backgroundTimer != nil { - backgroundTimer?.invalidate() - backgroundTimer = nil - print(">>> Invalidated background timer") - } + let query = Message.Query(status: [.sending]) + let assignment = Message.Assignments(status: .sendingFailed) + _ = try? database.bulkUpdateMessages(query, assignment) + } - if let messenger = try? DependencyInjection.Container.shared.resolve() as Messenger, - let cMix = messenger.cMix.get() { - guard self.calledStopNetwork == true else { return } - try? cMix.startNetworkFollower(timeoutMS: 10_000) - print(">>> Called startNetworkFollower") - self.calledStopNetwork = false + return } + }) } - - public func applicationDidBecomeActive(_ application: UIApplication) { - application.applicationIconBadgeNumber = 0 - coverView?.removeFromSuperview() + } + + public func applicationWillResignActive(_ application: UIApplication) { + if hideAppList { + coverView?.removeFromSuperview() + coverView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + coverView?.frame = window?.bounds ?? .zero + window?.addSubview(coverView!) } + } - public func application( - _ app: UIApplication, - open url: URL, - options: [UIApplication.OpenURLOptionsKey : Any] = [:] - ) -> Bool { - dropboxService.handleOpenUrl(url) + public func applicationWillTerminate(_ application: UIApplication) { + if let messenger = try? DependencyInjection.Container.shared.resolve() as Messenger { + try? messenger.stop() } + } - 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 = try? DependencyInjection.Container.shared.resolve() as PushRouter else { - return false - } + public func applicationWillEnterForeground(_ application: UIApplication) { + if backgroundTimer != nil { + backgroundTimer?.invalidate() + backgroundTimer = nil + print(">>> Invalidated background timer") + } - router.navigateTo(.search(username: username), {}) - return true + if let messenger = try? DependencyInjection.Container.shared.resolve() as Messenger, + let cMix = messenger.cMix.get() { + guard self.calledStopNetwork == true else { return } + try? cMix.startNetworkFollower(timeoutMS: 10_000) + print(">>> Called startNetworkFollower") + self.calledStopNetwork = false } + } + + public func applicationDidBecomeActive(_ application: UIApplication) { + application.applicationIconBadgeNumber = 0 + coverView?.removeFromSuperview() + } + + public func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey : Any] = [:] + ) -> Bool { + dropboxService.handleOpenUrl(url) + } + + 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 = try? DependencyInjection.Container.shared.resolve() as PushRouter else { + return false + } + + router.navigateTo(.search(username: username), {}) + return true + } } 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 + 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 } // MARK: Notifications extension AppDelegate: UNUserNotificationCenterDelegate { - public func userNotificationCenter( - _ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void - ) { - let userInfo = response.notification.request.content.userInfo - pushHandler.handleAction(pushRouter, userInfo, completionHandler) - } - - public func application( - _ application: UIApplication, - didReceiveRemoteNotification notification: [AnyHashable: Any], - fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void - ) { - pushHandler.handlePush(notification, completionHandler) - } - - public func application( - _: UIApplication, - didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data - ) { - pushHandler.registerToken(deviceToken) - } + public func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + let userInfo = response.notification.request.content.userInfo + pushHandler.handleAction(pushRouter, userInfo, completionHandler) + } + + public func application( + _ application: UIApplication, + didReceiveRemoteNotification notification: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { + pushHandler.handlePush(notification, completionHandler) + } + + public func application( + _: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data + ) { + pushHandler.registerToken(deviceToken) + } } diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift index f1d31d5d61f2b5569afde8542631ed9128d252f0..4f2c9a0b5fdb0c0a588920024053a113b5001f3e 100644 --- a/Sources/App/DependencyRegistrator.swift +++ b/Sources/App/DependencyRegistrator.swift @@ -192,7 +192,7 @@ struct DependencyRegistrator { successFactory: RestoreSuccessController.init, chatListFactory: ChatListController.init, restoreFactory: RestoreController.init(_:), - passphraseFactory: RestorePassphraseController.init(_:) + passphraseFactory: RestorePassphraseController.init(_:_:) ) as RestoreCoordinating) container.register( diff --git a/Sources/BackupFeature/Controllers/BackupPassphraseController.swift b/Sources/BackupFeature/Controllers/BackupPassphraseController.swift index 97c23d21b497a643a7850f3b9147723053dd1e9e..5021b438717d18a72e144d2a13e8372bd21073a8 100644 --- a/Sources/BackupFeature/Controllers/BackupPassphraseController.swift +++ b/Sources/BackupFeature/Controllers/BackupPassphraseController.swift @@ -4,70 +4,70 @@ import Combine import InputField public final class BackupPassphraseController: UIViewController { - lazy private var screenView = BackupPassphraseView() + lazy private var screenView = BackupPassphraseView() - private var passphrase = "" { - didSet { - switch Validator.backupPassphrase.validate(passphrase) { - case .success: - screenView.continueButton.isEnabled = true - case .failure: - screenView.continueButton.isEnabled = false - } - } + private var passphrase = "" { + didSet { + switch Validator.backupPassphrase.validate(passphrase) { + case .success: + screenView.continueButton.isEnabled = true + case .failure: + screenView.continueButton.isEnabled = false + } } + } - private let cancelClosure: EmptyClosure - private let stringClosure: StringClosure - private var cancellables = Set<AnyCancellable>() + private let cancelClosure: EmptyClosure + private let stringClosure: StringClosure + private var cancellables = Set<AnyCancellable>() - public init( - _ cancelClosure: @escaping EmptyClosure, - _ stringClosure: @escaping StringClosure - ) { - self.stringClosure = stringClosure - self.cancelClosure = cancelClosure - super.init(nibName: nil, bundle: nil) - } + public init( + _ cancelClosure: @escaping EmptyClosure, + _ stringClosure: @escaping StringClosure + ) { + self.stringClosure = stringClosure + self.cancelClosure = cancelClosure + super.init(nibName: nil, bundle: nil) + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } - public override func loadView() { - view = screenView - } + public override func loadView() { + view = screenView + } - public override func viewDidLoad() { - super.viewDidLoad() - setupBindings() - } + public override func viewDidLoad() { + super.viewDidLoad() + setupBindings() + } - private func setupBindings() { - screenView - .inputField - .returnPublisher - .sink { [unowned self] in - screenView.inputField.endEditing(true) - }.store(in: &cancellables) + private func setupBindings() { + screenView + .inputField + .returnPublisher + .sink { [unowned self] in + screenView.inputField.endEditing(true) + }.store(in: &cancellables) - screenView - .inputField - .textPublisher - .sink { [unowned self] in - passphrase = $0.trimmingCharacters(in: .whitespacesAndNewlines) - }.store(in: &cancellables) + screenView + .inputField + .textPublisher + .sink { [unowned self] in + passphrase = $0.trimmingCharacters(in: .whitespacesAndNewlines) + }.store(in: &cancellables) - screenView - .continueButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - dismiss(animated: true) { self.stringClosure(self.passphrase) } - }.store(in: &cancellables) + screenView + .continueButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { self.stringClosure(self.passphrase) } + }.store(in: &cancellables) - screenView - .cancelButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - dismiss(animated: true) { self.cancelClosure() } - }.store(in: &cancellables) - } + screenView + .cancelButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { self.cancelClosure() } + }.store(in: &cancellables) + } } diff --git a/Sources/BackupFeature/Service/BackupService.swift b/Sources/BackupFeature/Service/BackupService.swift index 788fd7d454ff95f4614ce7e49d4a381af4761596..1d4857924384d0cc263f947f46a87fc0bd8bc86f 100644 --- a/Sources/BackupFeature/Service/BackupService.swift +++ b/Sources/BackupFeature/Service/BackupService.swift @@ -13,368 +13,355 @@ import XXClient import XXMessengerClient public final class BackupService { - @Dependency var messenger: Messenger - @Dependency var sftpService: SFTPService - @Dependency var icloudService: iCloudInterface - @Dependency var dropboxService: DropboxInterface - @Dependency var networkManager: NetworkMonitoring - @Dependency var keychainHandler: KeychainHandling - @Dependency var driveService: GoogleDriveInterface - - @KeyObject(.email, defaultValue: nil) var email: String? - @KeyObject(.phone, defaultValue: nil) var phone: String? - @KeyObject(.username, defaultValue: nil) var username: String? - - var manager: XXClient.Backup? - - @KeyObject(.backupSettings, defaultValue: Data()) private var storedSettings: Data - - public var settingsPublisher: AnyPublisher<BackupSettings, Never> { - settings.handleEvents(receiveSubscription: { [weak self] _ in - guard let self = self else { return } - - let lastRefreshDate = self.settingsLastRefreshedDate ?? Date.distantPast - - if Date().timeIntervalSince(lastRefreshDate) < 10 { return } - - self.settingsLastRefreshedDate = Date() - self.refreshConnections() - self.refreshBackups() - }).eraseToAnyPublisher() - } - - private var connType: ConnectionType = .wifi - private var settingsLastRefreshedDate: Date? - private var cancellables = Set<AnyCancellable>() - private lazy var settings = CurrentValueSubject<BackupSettings, Never>(.init(fromData: storedSettings)) - - public init() { - settings - .dropFirst() - .removeDuplicates() - .sink { [unowned self] in storedSettings = $0.toData() } - .store(in: &cancellables) - - networkManager.connType - .receive(on: DispatchQueue.main) - .sink { [unowned self] in connType = $0 } - .store(in: &cancellables) - } + @Dependency var messenger: Messenger + @Dependency var sftpService: SFTPService + @Dependency var icloudService: iCloudInterface + @Dependency var dropboxService: DropboxInterface + @Dependency var networkManager: NetworkMonitoring + @Dependency var keychainHandler: KeychainHandling + @Dependency var driveService: GoogleDriveInterface + + @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.backupSettings, defaultValue: Data()) private var storedSettings: Data + + public var settingsPublisher: AnyPublisher<BackupSettings, Never> { + settings.handleEvents(receiveSubscription: { [weak self] _ in + guard let self = self else { return } + + let lastRefreshDate = self.settingsLastRefreshedDate ?? Date.distantPast + + if Date().timeIntervalSince(lastRefreshDate) < 10 { return } + + self.settingsLastRefreshedDate = Date() + self.refreshConnections() + self.refreshBackups() + }).eraseToAnyPublisher() + } + + private var connType: ConnectionType = .wifi + private var settingsLastRefreshedDate: Date? + private var cancellables = Set<AnyCancellable>() + private lazy var settings = CurrentValueSubject<BackupSettings, Never>(.init(fromData: storedSettings)) + + public init() { + settings + .dropFirst() + .removeDuplicates() + .sink { [unowned self] in storedSettings = $0.toData() } + .store(in: &cancellables) + + networkManager.connType + .receive(on: DispatchQueue.main) + .sink { [unowned self] in connType = $0 } + .store(in: &cancellables) + } } extension BackupService { - public func initializeBackup(passphrase: String) { - manager = try! InitializeBackup.live( - e2eId: messenger.e2e.get()!.getId(), - udId: messenger.ud.get()!.getId(), - password: passphrase, - callback: .init(handle: { [weak self] backupData in - self?.updateBackup(data: backupData) - }) - ) + public func stopBackups() { + print(">>> [AccountBackup] Requested to stop backup mechanism") - didUpdateFacts() + if messenger.isBackupRunning() == true { + print(">>> [AccountBackup] messenger.isBackupRunning() == true") + try! messenger.stopBackup() + + print(">>> [AccountBackup] Stopped backup mechanism") } + } - public func didUpdateFacts() { - if let manager = manager { - let currentFacts = try! JSONEncoder().encode( - BackupParams( - username: username!, - email: email, - phone: phone - ) - ) - - print(">>> Will addJSON: \(String(data: currentFacts, encoding: .utf8)!)") - manager.addJSON(String(data: currentFacts, encoding: .utf8)!) - } + public func initializeBackup(passphrase: String) { + try! messenger.startBackup( + password: passphrase, + params: .init(username: username!) + ) - performBackup() - } + print(">>> [AccountBackup] Initialized backup mechanism") + } - public func performBackupIfAutomaticIsEnabled() { - guard settings.value.automaticBackups == true else { return } - performBackup() - } + public func performBackupIfAutomaticIsEnabled() { + print(">>> [AccountBackup] Requested backup if automatic is enabled") - public func performBackup() { - print(">>> Did call performBackup()") + guard settings.value.automaticBackups == true else { return } + performBackup() + } - guard let directoryUrl = try? FileManager.default.url( - for: .applicationSupportDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: true - ) else { fatalError("Couldn't generate the URL to persist the backup") } + public func performBackup() { + print(">>> [AccountBackup] Requested backup without explicitly passing data") - let fileUrl = directoryUrl - .appendingPathComponent("backup") - .appendingPathExtension("xxm") + guard let directoryUrl = try? FileManager.default.url( + for: .applicationSupportDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) else { fatalError("Couldn't generate the URL to persist the backup") } - guard let data = try? Data(contentsOf: fileUrl) else { - print(">>> Tried to backup arbitrarily but there was nothing to be backed up. Aborting...") - return - } + let fileUrl = directoryUrl + .appendingPathComponent("backup") + .appendingPathExtension("xxm") - performBackup(data: data) + guard let data = try? Data(contentsOf: fileUrl) else { + print(">>> [AccountBackup] Tried to backup arbitrarily but there was nothing to be backed up. Aborting...") + return } - public func updateBackup(data: Data) { - print(">>> Did call updateBackup(data)") + performBackup(data: data) + } - guard let directoryUrl = try? FileManager.default.url( - for: .applicationSupportDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: true - ) else { fatalError("Couldn't generate the URL to persist the backup") } + public func updateBackup(data: Data) { + print(">>> [AccountBackup] Requested to update backup passing data") - let fileUrl = directoryUrl - .appendingPathComponent("backup") - .appendingPathExtension("xxm") + guard let directoryUrl = try? FileManager.default.url( + for: .applicationSupportDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) else { fatalError("Couldn't generate the URL to persist the backup") } - do { - try data.write(to: fileUrl) - } catch { - fatalError("Couldn't write backup to fileurl") - } + let fileUrl = directoryUrl + .appendingPathComponent("backup") + .appendingPathExtension("xxm") - let isWifiOnly = settings.value.wifiOnlyBackup - let isAutomaticEnabled = settings.value.automaticBackups - let hasEnabledService = settings.value.enabledService != nil + do { + try data.write(to: fileUrl) + } catch { + fatalError("Couldn't write backup to fileurl") + } - if isWifiOnly { - guard connType == .wifi else { return } - } else { - guard connType != .unknown else { return } - } + let isWifiOnly = settings.value.wifiOnlyBackup + let isAutomaticEnabled = settings.value.automaticBackups + let hasEnabledService = settings.value.enabledService != nil - if isAutomaticEnabled && hasEnabledService { - performBackup() - } + if isWifiOnly { + guard connType == .wifi else { return } + } else { + guard connType != .unknown else { return } } - public func setBackupOnlyOnWifi(_ enabled: Bool) { - settings.value.wifiOnlyBackup = enabled + if isAutomaticEnabled && hasEnabledService { + performBackup() + } + } + + public func setBackupOnlyOnWifi(_ enabled: Bool) { + settings.value.wifiOnlyBackup = enabled + } + + public func setBackupAutomatically(_ enabled: Bool) { + settings.value.automaticBackups = enabled + + guard enabled else { return } + performBackup() + } + + public func toggle(service: CloudService, enabling: Bool) { + settings.value.enabledService = enabling ? service : nil + } + + public func authorize(service: CloudService, presenting screen: UIViewController) { + switch service { + case .drive: + driveService.authorize(presenting: screen) { [weak self] _ in + guard let self = self else { return } + self.refreshConnections() + self.refreshBackups() + } + case .icloud: + if !icloudService.isAuthorized() { + icloudService.openSettings() + } else { + refreshConnections() + refreshBackups() + } + case .dropbox: + if !dropboxService.isAuthorized() { + dropboxService.authorize(presenting: screen) + .sink { [weak self] _ in + guard let self = self else { return } + self.refreshConnections() + self.refreshBackups() + }.store(in: &cancellables) + } + case .sftp: + if !sftpService.isAuthorized() { + sftpService.authorizeFlow((screen, { [weak self] in + guard let self = self else { return } + screen.navigationController?.popViewController(animated: true) + self.refreshConnections() + self.refreshBackups() + })) + } } + } +} - public func setBackupAutomatically(_ enabled: Bool) { - settings.value.automaticBackups = enabled +extension BackupService { + private func refreshConnections() { + if icloudService.isAuthorized() && !settings.value.connectedServices.contains(.icloud) { + settings.value.connectedServices.insert(.icloud) + } else if !icloudService.isAuthorized() && settings.value.connectedServices.contains(.icloud) { + settings.value.connectedServices.remove(.icloud) + } - guard enabled else { return } - performBackup() + if dropboxService.isAuthorized() && !settings.value.connectedServices.contains(.dropbox) { + settings.value.connectedServices.insert(.dropbox) + } else if !dropboxService.isAuthorized() && settings.value.connectedServices.contains(.dropbox) { + settings.value.connectedServices.remove(.dropbox) } - public func toggle(service: CloudService, enabling: Bool) { - settings.value.enabledService = enabling ? service : nil + if sftpService.isAuthorized() && !settings.value.connectedServices.contains(.sftp) { + settings.value.connectedServices.insert(.sftp) + } else if !sftpService.isAuthorized() && settings.value.connectedServices.contains(.sftp) { + settings.value.connectedServices.remove(.sftp) } - public func authorize(service: CloudService, presenting screen: UIViewController) { - switch service { - case .drive: - driveService.authorize(presenting: screen) { [weak self] _ in - guard let self = self else { return } - self.refreshConnections() - self.refreshBackups() - } - case .icloud: - if !icloudService.isAuthorized() { - icloudService.openSettings() - } else { - refreshConnections() - refreshBackups() - } - case .dropbox: - if !dropboxService.isAuthorized() { - dropboxService.authorize(presenting: screen) - .sink { [weak self] _ in - guard let self = self else { return } - self.refreshConnections() - self.refreshBackups() - }.store(in: &cancellables) - } - case .sftp: - if !sftpService.isAuthorized() { - sftpService.authorizeFlow((screen, { [weak self] in - guard let self = self else { return } - screen.navigationController?.popViewController(animated: true) - self.refreshConnections() - self.refreshBackups() - })) - } - } + driveService.isAuthorized { [weak settings] isAuthorized in + guard let settings = settings else { return } + + if isAuthorized && !settings.value.connectedServices.contains(.drive) { + settings.value.connectedServices.insert(.drive) + } else if !isAuthorized && settings.value.connectedServices.contains(.drive) { + settings.value.connectedServices.remove(.drive) + } } -} + } -extension BackupService { - private func refreshConnections() { - if icloudService.isAuthorized() && !settings.value.connectedServices.contains(.icloud) { - settings.value.connectedServices.insert(.icloud) - } else if !icloudService.isAuthorized() && settings.value.connectedServices.contains(.icloud) { - settings.value.connectedServices.remove(.icloud) - } + private func refreshBackups() { + if icloudService.isAuthorized() { + icloudService.downloadMetadata { [weak settings] in + guard let settings = settings else { return } - if dropboxService.isAuthorized() && !settings.value.connectedServices.contains(.dropbox) { - settings.value.connectedServices.insert(.dropbox) - } else if !dropboxService.isAuthorized() && settings.value.connectedServices.contains(.dropbox) { - settings.value.connectedServices.remove(.dropbox) + guard let metadata = try? $0.get() else { + settings.value.backups[.icloud] = nil + return } - if sftpService.isAuthorized() && !settings.value.connectedServices.contains(.sftp) { - settings.value.connectedServices.insert(.sftp) - } else if !sftpService.isAuthorized() && settings.value.connectedServices.contains(.sftp) { - settings.value.connectedServices.remove(.sftp) - } + settings.value.backups[.icloud] = BackupModel( + id: metadata.path, + date: metadata.modifiedDate, + size: metadata.size + ) + } + } - driveService.isAuthorized { [weak settings] isAuthorized in - guard let settings = settings else { return } + if sftpService.isAuthorized() { + sftpService.fetchMetadata { [weak settings] in + guard let settings = settings else { return } - if isAuthorized && !settings.value.connectedServices.contains(.drive) { - settings.value.connectedServices.insert(.drive) - } else if !isAuthorized && settings.value.connectedServices.contains(.drive) { - settings.value.connectedServices.remove(.drive) - } + guard let metadata = try? $0.get()?.backup else { + settings.value.backups[.sftp] = nil + return } + + settings.value.backups[.sftp] = BackupModel( + id: metadata.id, + date: metadata.date, + size: metadata.size + ) + } } - private func refreshBackups() { - if icloudService.isAuthorized() { - icloudService.downloadMetadata { [weak settings] in - guard let settings = settings else { return } - - guard let metadata = try? $0.get() else { - settings.value.backups[.icloud] = nil - return - } - - settings.value.backups[.icloud] = BackupModel( - id: metadata.path, - date: metadata.modifiedDate, - size: metadata.size - ) - } - } + if dropboxService.isAuthorized() { + dropboxService.downloadMetadata { [weak settings] in + guard let settings = settings else { return } - if sftpService.isAuthorized() { - sftpService.fetchMetadata { [weak settings] in - guard let settings = settings else { return } - - guard let metadata = try? $0.get()?.backup else { - settings.value.backups[.sftp] = nil - return - } - - settings.value.backups[.sftp] = BackupModel( - id: metadata.id, - date: metadata.date, - size: metadata.size - ) - } + guard let metadata = try? $0.get() else { + settings.value.backups[.dropbox] = nil + return } - if dropboxService.isAuthorized() { - dropboxService.downloadMetadata { [weak settings] in - guard let settings = settings else { return } - - guard let metadata = try? $0.get() else { - settings.value.backups[.dropbox] = nil - return - } - - settings.value.backups[.dropbox] = BackupModel( - id: metadata.path, - date: metadata.modifiedDate, - size: metadata.size - ) - } - } + settings.value.backups[.dropbox] = BackupModel( + id: metadata.path, + date: metadata.modifiedDate, + size: metadata.size + ) + } + } - driveService.isAuthorized { [weak settings] isAuthorized in - guard let settings = settings else { return } - - if isAuthorized { - self.driveService.downloadMetadata { - guard let metadata = try? $0.get() else { return } - - settings.value.backups[.drive] = BackupModel( - id: metadata.identifier, - date: metadata.modifiedDate, - size: metadata.size - ) - } - } else { - settings.value.backups[.drive] = nil - } + driveService.isAuthorized { [weak settings] isAuthorized in + guard let settings = settings else { return } + + if isAuthorized { + self.driveService.downloadMetadata { + guard let metadata = try? $0.get() else { return } + + settings.value.backups[.drive] = BackupModel( + id: metadata.identifier, + date: metadata.modifiedDate, + size: metadata.size + ) } + } else { + settings.value.backups[.drive] = nil + } } + } - private func performBackup(data: Data) { - print(">>> Did call performBackup(data)") + private func performBackup(data: Data) { + print(">>> Did call performBackup(data)") - guard let enabledService = settings.value.enabledService else { - fatalError("Trying to backup but nothing is enabled") - } + guard let enabledService = settings.value.enabledService else { + fatalError("Trying to backup but nothing is enabled") + } - let url = URL(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent(UUID().uuidString) + let url = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent(UUID().uuidString) - do { - try data.write(to: url, options: .atomic) - } catch { - print(">>> Couldn't write to temp: \(error.localizedDescription)") - return - } + do { + try data.write(to: url, options: .atomic) + } catch { + print(">>> Couldn't write to temp: \(error.localizedDescription)") + return + } - switch enabledService { - case .drive: - driveService.uploadBackup(url) { - switch $0 { - case .success(let metadata): - self.settings.value.backups[.drive] = .init( - id: metadata.identifier, - date: metadata.modifiedDate, - size: metadata.size - ) - case .failure(let error): - print(error.localizedDescription) - } - } - case .icloud: - icloudService.uploadBackup(url) { - switch $0 { - case .success(let metadata): - self.settings.value.backups[.icloud] = .init( - id: metadata.path, - date: metadata.modifiedDate, - size: metadata.size - ) - case .failure(let error): - print(error.localizedDescription) - } - } - case .dropbox: - dropboxService.uploadBackup(url) { - switch $0 { - case .success(let metadata): - self.settings.value.backups[.dropbox] = .init( - id: metadata.path, - date: metadata.modifiedDate, - size: metadata.size - ) - case .failure(let error): - print(error.localizedDescription) - } - } - case .sftp: - sftpService.uploadBackup(url: url) { - switch $0 { - case .success(let backup): - self.settings.value.backups[.sftp] = backup - case .failure(let error): - print(error.localizedDescription) - } - } + switch enabledService { + case .drive: + driveService.uploadBackup(url) { + switch $0 { + case .success(let metadata): + self.settings.value.backups[.drive] = .init( + id: metadata.identifier, + date: metadata.modifiedDate, + size: metadata.size + ) + case .failure(let error): + print(error.localizedDescription) + } + } + case .icloud: + icloudService.uploadBackup(url) { + switch $0 { + case .success(let metadata): + self.settings.value.backups[.icloud] = .init( + id: metadata.path, + date: metadata.modifiedDate, + size: metadata.size + ) + case .failure(let error): + print(error.localizedDescription) + } + } + case .dropbox: + dropboxService.uploadBackup(url) { + switch $0 { + case .success(let metadata): + self.settings.value.backups[.dropbox] = .init( + id: metadata.path, + date: metadata.modifiedDate, + size: metadata.size + ) + case .failure(let error): + print(error.localizedDescription) + } + } + case .sftp: + sftpService.uploadBackup(url: url) { + switch $0 { + case .success(let backup): + self.settings.value.backups[.sftp] = backup + case .failure(let error): + print(error.localizedDescription) } + } } + } } diff --git a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift index 4b398f9028bf284019a1155ef4e1f0011dab165a..74ce209fbf6b2d71a81bb3e93a88fa9838150ea3 100644 --- a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift +++ b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift @@ -10,104 +10,97 @@ import Foundation import DependencyInjection enum BackupActionState { - case backupFinished - case backupAllowed(Bool) - case backupInProgress(Float, Float) + case backupFinished + case backupAllowed(Bool) + case backupInProgress(Float, Float) } struct BackupConfigViewModel { - var didTapBackupNow: () -> Void - var didChooseWifiOnly: (Bool) -> Void - var didChooseAutomatic: (Bool) -> Void - var didToggleService: (UIViewController, CloudService, Bool) -> Void - var didTapService: (CloudService, UIViewController) -> Void + var didTapBackupNow: () -> Void + var didChooseWifiOnly: (Bool) -> Void + var didChooseAutomatic: (Bool) -> Void + var didToggleService: (UIViewController, CloudService, Bool) -> Void + var didTapService: (CloudService, UIViewController) -> Void - var wifiOnly: () -> AnyPublisher<Bool, Never> - var automatic: () -> AnyPublisher<Bool, Never> - var lastBackup: () -> AnyPublisher<BackupModel?, Never> - var actionState: () -> AnyPublisher<BackupActionState, Never> - var enabledService: () -> AnyPublisher<CloudService?, Never> - var connectedServices: () -> AnyPublisher<Set<CloudService>, Never> + var wifiOnly: () -> AnyPublisher<Bool, Never> + var automatic: () -> AnyPublisher<Bool, Never> + var lastBackup: () -> AnyPublisher<BackupModel?, Never> + var actionState: () -> AnyPublisher<BackupActionState, Never> + var enabledService: () -> AnyPublisher<CloudService?, Never> + var connectedServices: () -> AnyPublisher<Set<CloudService>, Never> } extension BackupConfigViewModel { - static func live() -> Self { - class Context { - @Dependency var hud: HUD - @Dependency var service: BackupService - @Dependency var coordinator: BackupCoordinating - } - - let context = Context() - - return .init( - didTapBackupNow: { - context.service.performBackup() - context.hud.update(with: .on) - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { - context.hud.update(with: .none) - } - }, - didChooseWifiOnly: context.service.setBackupOnlyOnWifi(_:), - didChooseAutomatic: context.service.setBackupAutomatically(_:), - didToggleService: { controller, service, enabling in - guard enabling == true else { - context.service.toggle(service: service, enabling: false) - if let manager = context.service.manager { - if manager.isRunning() { - try! manager.stop() - } + static func live() -> Self { + class Context { + @Dependency var hud: HUD + @Dependency var service: BackupService + @Dependency var coordinator: BackupCoordinating + } - context.service.manager = nil - } + let context = Context() - return - } + return .init( + didTapBackupNow: { + context.service.performBackup() + context.hud.update(with: .on) + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + context.hud.update(with: .none) + } + }, + didChooseWifiOnly: context.service.setBackupOnlyOnWifi(_:), + didChooseAutomatic: context.service.setBackupAutomatically(_:), + didToggleService: { controller, service, enabling in + guard enabling == true else { + context.service.toggle(service: service, enabling: false) + context.service.stopBackups() + return + } - context.coordinator.toPassphrase(from: controller, cancelClosure: { - context.service.toggle(service: service, enabling: false) - }, passphraseClosure: { passphrase in - context.hud.update(with: .onTitle("Initializing and securing your backup file will take few seconds, please keep the app open.")) - context.service.toggle(service: service, enabling: enabling) - context.service.initializeBackup(passphrase: passphrase) - context.hud.update(with: .none) - }) - }, - didTapService: context.service.authorize, - wifiOnly: { - context.service.settingsPublisher - .map(\.wifiOnlyBackup) - .eraseToAnyPublisher() - }, - automatic: { - context.service.settingsPublisher - .map(\.automaticBackups) - .eraseToAnyPublisher() - }, - lastBackup: { - context.service.settingsPublisher - .map { - guard let enabledService = $0.enabledService else { return nil } - return $0.backups[enabledService] - }.eraseToAnyPublisher() - }, - actionState: { - context.service.settingsPublisher - .map(\.enabledService) - .map { BackupActionState.backupAllowed($0 != nil) } - .eraseToAnyPublisher() - }, - enabledService: { - context.service.settingsPublisher - .map(\.enabledService) - .eraseToAnyPublisher() - }, - connectedServices: { - context.service.settingsPublisher - .map(\.connectedServices) - .removeDuplicates() - .eraseToAnyPublisher() - } - ) - } + context.coordinator.toPassphrase(from: controller, cancelClosure: { + context.service.toggle(service: service, enabling: false) + }, passphraseClosure: { passphrase in + context.hud.update(with: .onTitle("Initializing and securing your backup file will take few seconds, please keep the app open.")) + context.service.toggle(service: service, enabling: enabling) + context.service.initializeBackup(passphrase: passphrase) + context.hud.update(with: .none) + }) + }, + didTapService: context.service.authorize, + wifiOnly: { + context.service.settingsPublisher + .map(\.wifiOnlyBackup) + .eraseToAnyPublisher() + }, + automatic: { + context.service.settingsPublisher + .map(\.automaticBackups) + .eraseToAnyPublisher() + }, + lastBackup: { + context.service.settingsPublisher + .map { + guard let enabledService = $0.enabledService else { return nil } + return $0.backups[enabledService] + }.eraseToAnyPublisher() + }, + actionState: { + context.service.settingsPublisher + .map(\.enabledService) + .map { BackupActionState.backupAllowed($0 != nil) } + .eraseToAnyPublisher() + }, + enabledService: { + context.service.settingsPublisher + .map(\.enabledService) + .eraseToAnyPublisher() + }, + connectedServices: { + context.service.settingsPublisher + .map(\.connectedServices) + .removeDuplicates() + .eraseToAnyPublisher() + } + ) + } } diff --git a/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift b/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift index 1329609d408d1fb1f262aa4cc4883b7cf1d5b9bd..7e5bc8ea026b1f68378a3935594616a9a16ebee8 100644 --- a/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift @@ -60,7 +60,7 @@ final class GroupChatViewModel { var messages: AnyPublisher<[ArraySection<ChatSection, Message>], Never> { database.fetchMessagesPublisher(.init(chat: .group(info.group.id))) - .assertNoFailure() + .replaceError(with: []) .map { messages -> [ArraySection<ChatSection, Message>] in let groupedByDate = Dictionary(grouping: messages) { domainModel -> Date in let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date) diff --git a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift index 0b348894d743841ce4c52ac192723e0211986503..d06d88f7eb5c2e6917a891dab8d062cb2723bc02 100644 --- a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift @@ -105,13 +105,13 @@ final class SingleChatViewModel: NSObject { updateRecentState(contact) database.fetchContactsPublisher(Contact.Query(id: [contact.id])) - .assertNoFailure() + .replaceError(with: []) .compactMap { $0.first } .sink { [unowned self] in contactSubject.send($0) } .store(in: &cancellables) database.fetchMessagesPublisher(.init(chat: .direct(myId, contact.id))) - .assertNoFailure() + .replaceError(with: []) .map { let groupedByDate = Dictionary(grouping: $0) { domainModel -> Date in let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date) diff --git a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift index ac787dd2ea58eeaaa584114991a83f0ca8c88dca..32b7e99a945568919a6b5bbde947515944490f87 100644 --- a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift +++ b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift @@ -57,7 +57,7 @@ final class ChatListViewModel { ) return database.fetchContactsPublisher(query) - .assertNoFailure() + .replaceError(with: []) .map { let section = SectionId() var snapshot = RecentsSnapshot() @@ -75,7 +75,7 @@ final class ChatListViewModel { return Publishers.CombineLatest3( database.fetchContactsPublisher(contactsQuery) - .assertNoFailure() + .replaceError(with: []) .map { $0.filter { $0.id != self.myId }}, chatsPublisher, searchSubject @@ -140,8 +140,8 @@ final class ChatListViewModel { ) return Publishers.CombineLatest( - database.fetchContactsPublisher(contactsQuery).assertNoFailure(), - database.fetchGroupsPublisher(groupQuery).assertNoFailure() + database.fetchContactsPublisher(contactsQuery).replaceError(with: []), + database.fetchGroupsPublisher(groupQuery).replaceError(with: []) ) .map { $0.0.count + $0.1.count } .eraseToAnyPublisher() @@ -170,7 +170,7 @@ final class ChatListViewModel { authStatus: [.participating] ) )) - .assertNoFailure() + .replaceError(with: []) .sink { [unowned self] in chatsSubject.send($0) } .store(in: &cancellables) } diff --git a/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift b/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift index 548cc89bcdb37202d318b75d9ff82d8042170d01..2f1364c5306724b14a08ed3592d4af6b094efd86 100644 --- a/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift +++ b/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift @@ -26,7 +26,7 @@ final class ContactListViewModel { ) return database.fetchContactsPublisher(query) - .assertNoFailure() + .replaceError(with: []) .map { $0.filter { $0.id != self.myId }} .eraseToAnyPublisher() } @@ -51,8 +51,10 @@ final class ContactListViewModel { ) return Publishers.CombineLatest( - database.fetchContactsPublisher(contactsQuery).assertNoFailure(), - database.fetchGroupsPublisher(groupQuery).assertNoFailure() + database.fetchContactsPublisher(contactsQuery) + .replaceError(with: []), + database.fetchGroupsPublisher(groupQuery) + .replaceError(with: []) ) .map { $0.0.count + $0.1.count } .eraseToAnyPublisher() diff --git a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift b/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift index f292c7cb7ed4589007e86a565cd0ad5100b91ff4..08314f15ff9c16729e88e4ada4270ceef6ad3277 100644 --- a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift +++ b/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift @@ -62,7 +62,7 @@ final class CreateGroupViewModel { ) database.fetchContactsPublisher(query) - .assertNoFailure() + .replaceError(with: []) .map { $0.filter { $0.id != self.myId }} .map { $0.sorted(by: { $0.username! < $1.username! })} .sink { [unowned self] in diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift index fb6f56c3c9aa472a6fa12b1d7867b0f30728be85..79fafead16257f18d3310e00891ad76b14cb59d9 100644 --- a/Sources/LaunchFeature/LaunchViewModel.swift +++ b/Sources/LaunchFeature/LaunchViewModel.swift @@ -9,6 +9,7 @@ import Keychain import Foundation import Permissions import ToastFeature +import BackupFeature import DropboxFeature import VersionChecking import ReportingFeature @@ -25,692 +26,695 @@ import XXMessengerClient import NetworkMonitor struct Update { - let content: String - let urlString: String - let positiveActionTitle: String - let negativeActionTitle: String? - let actionStyle: CapsuleButtonStyle + let content: String + let urlString: String + let positiveActionTitle: String + let negativeActionTitle: String? + let actionStyle: CapsuleButtonStyle } enum LaunchRoute { - case chats - case update(Update) - case onboarding + case chats + case update(Update) + case onboarding } final class LaunchViewModel { - @Dependency var database: Database - @Dependency var versionChecker: VersionChecker - @Dependency var dropboxService: DropboxInterface - @Dependency var fetchBannedList: FetchBannedList - @Dependency var reportingStatus: ReportingStatus - @Dependency var toastController: ToastController - @Dependency var keychainHandler: KeychainHandling - @Dependency var networkMonitor: NetworkMonitoring - @Dependency var processBannedList: ProcessBannedList - @Dependency var permissionHandler: PermissionHandling - - @KeyObject(.username, defaultValue: nil) var username: String? - @KeyObject(.biometrics, defaultValue: false) var isBiometricsOn: Bool - @KeyObject(.dummyTrafficOn, defaultValue: false) var dummyTrafficOn: Bool - - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } - - var authCallbacksCancellable: Cancellable? - var networkCallbacksCancellable: Cancellable? - var messageListenerCallbacksCancellable: Cancellable? - - var routePublisher: AnyPublisher<LaunchRoute, Never> { - routeSubject.eraseToAnyPublisher() - } - - var mainScheduler: AnySchedulerOf<DispatchQueue> = { - DispatchQueue.main.eraseToAnyScheduler() - }() - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = { - DispatchQueue.global().eraseToAnyScheduler() - }() - - private var cancellables = Set<AnyCancellable>() - private let routeSubject = PassthroughSubject<LaunchRoute, Never>() - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) - - func viewDidAppear() { - mainScheduler.schedule(after: .init(.now() + 1)) { [weak self] in - guard let self = self else { return } - - self.hudSubject.send(.on) - - self.versionChecker().sink { [unowned self] in - switch $0 { - case .upToDate: - self.updateBannedList { - self.updateErrors { - self.continueWithInitialization() - } - } - case .failure(let error): - self.versionFailed(error: error) - case .updateRequired(let info): - self.versionUpdateRequired(info) - case .updateRecommended(let info): - self.versionUpdateRecommended(info) - } - }.store(in: &self.cancellables) + @Dependency var database: Database + @Dependency var backupService: BackupService + @Dependency var versionChecker: VersionChecker + @Dependency var dropboxService: DropboxInterface + @Dependency var fetchBannedList: FetchBannedList + @Dependency var reportingStatus: ReportingStatus + @Dependency var toastController: ToastController + @Dependency var keychainHandler: KeychainHandling + @Dependency var networkMonitor: NetworkMonitoring + @Dependency var processBannedList: ProcessBannedList + @Dependency var permissionHandler: PermissionHandling + + @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.biometrics, defaultValue: false) var isBiometricsOn: Bool + @KeyObject(.dummyTrafficOn, defaultValue: false) var dummyTrafficOn: Bool + + var hudPublisher: AnyPublisher<HUDStatus, Never> { + hudSubject.eraseToAnyPublisher() + } + + var authCallbacksCancellable: Cancellable? + var backupCallbackCancellable: Cancellable? + var networkCallbacksCancellable: Cancellable? + var messageListenerCallbacksCancellable: Cancellable? + + var routePublisher: AnyPublisher<LaunchRoute, Never> { + routeSubject.eraseToAnyPublisher() + } + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = { + DispatchQueue.global().eraseToAnyScheduler() + }() + + private var cancellables = Set<AnyCancellable>() + private let routeSubject = PassthroughSubject<LaunchRoute, Never>() + private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) + + func viewDidAppear() { + backgroundScheduler.schedule(after: .init(.now() + 1)) { [weak self] in + guard let self = self else { return } + + self.hudSubject.send(.on) + + self.versionChecker().sink { [unowned self] in + switch $0 { + case .upToDate: + self.updateBannedList { + self.updateErrors { + self.continueWithInitialization() + } + } + case .failure(let error): + self.versionFailed(error: error) + case .updateRequired(let info): + self.versionUpdateRequired(info) + case .updateRecommended(let info): + self.versionUpdateRecommended(info) } + }.store(in: &self.cancellables) } + } - func continueWithInitialization() { - do { - try self.setupDatabase() + func continueWithInitialization() { + do { + try self.setupDatabase() - let messenger = makeMessenger() - DependencyInjection.Container.shared.register(messenger) + let messenger = makeMessenger() + DependencyInjection.Container.shared.register(messenger) - setupLogWriter() - setupAuthCallback(messenger) - setupMessageCallback(messenger) + setupLogWriter() + setupAuthCallback(messenger) + setupBackupCallback(messenger) + setupMessageCallback(messenger) - if messenger.isLoaded() == false { - if messenger.isCreated() == false { - try messenger.create() - } - - try messenger.load() - } + if messenger.isLoaded() == false { + if messenger.isCreated() == false { + try messenger.create() + } - try messenger.start() + try messenger.load() + } - if messenger.isConnected() == false { - try messenger.connect() - try messenger.listenForMessages() - } + try messenger.start() - try generateGroupManager(messenger) - try generateTrafficManager(messenger) - try generateTransferManager(messenger) - listenToNetworkUpdates(messenger) - - if messenger.isLoggedIn() == false { - if try messenger.isRegistered() { - try messenger.logIn() - hudSubject.send(.none) - routeSubject.send(.chats) - } else { - dropboxService.unlink() - hudSubject.send(.none) - routeSubject.send(.onboarding) - } - } else { - hudSubject.send(.none) - routeSubject.send(.chats) - } + if messenger.isConnected() == false { + try messenger.connect() + try messenger.listenForMessages() + } - // TODO: Biometric auth + try generateGroupManager(messenger) + try generateTrafficManager(messenger) + try generateTransferManager(messenger) + listenToNetworkUpdates(messenger) - } catch { - let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) - hudSubject.send(.error(.init(content: xxError))) + if messenger.isLoggedIn() == false { + if try messenger.isRegistered() { + try messenger.logIn() + hudSubject.send(.none) + routeSubject.send(.chats) + } else { + dropboxService.unlink() + hudSubject.send(.none) + routeSubject.send(.onboarding) } - } - - private func cleanUp() { -// try? cMixManager.remove() -// try? keychainHandler.clear() - } - - private func presentOnboardingFlow() { + } else { hudSubject.send(.none) - routeSubject.send(.onboarding) - } + routeSubject.send(.chats) + } - private func setupDatabase() throws { - let legacyOldPath = NSSearchPathForDirectoriesInDomains( - .documentDirectory, .userDomainMask, true - )[0].appending("/xxmessenger.sqlite") + if !messenger.isBackupRunning() { + try? messenger.resumeBackup() + } - let legacyPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! - .appendingPathComponent("database") - .appendingPathExtension("sqlite").path + // TODO: Biometric auth - let dbExistsInLegacyOldPath = FileManager.default.fileExists(atPath: legacyOldPath) - let dbExistsInLegacyPath = FileManager.default.fileExists(atPath: legacyPath) - - if dbExistsInLegacyOldPath && !dbExistsInLegacyPath { - try? FileManager.default.moveItem(atPath: legacyOldPath, toPath: legacyPath) - } + } catch { + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + hudSubject.send(.error(.init(content: xxError))) + } + } - let dbPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! - .appendingPathComponent("xxm_database") - .appendingPathExtension("sqlite").path + private func cleanUp() { + // try? cMixManager.remove() + // try? keychainHandler.clear() + } - let database = try Database.onDisk(path: dbPath) + private func presentOnboardingFlow() { + hudSubject.send(.none) + routeSubject.send(.onboarding) + } - if dbExistsInLegacyPath { - try Migrator.live()( - try .init(path: legacyPath), - to: database, - myContactId: Data(), //client.bindings.myId, - meMarshaled: Data() //client.bindings.meMarshalled - ) + private func setupDatabase() throws { + let legacyOldPath = NSSearchPathForDirectoriesInDomains( + .documentDirectory, .userDomainMask, true + )[0].appending("/xxmessenger.sqlite") - try FileManager.default.moveItem(atPath: legacyPath, toPath: legacyPath.appending("-backup")) - } + let legacyPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! + .appendingPathComponent("database") + .appendingPathExtension("sqlite").path - DependencyInjection.Container.shared.register(database) + let dbExistsInLegacyOldPath = FileManager.default.fileExists(atPath: legacyOldPath) + let dbExistsInLegacyPath = FileManager.default.fileExists(atPath: legacyPath) - _ = try? database.bulkUpdateContacts(.init(authStatus: [.requesting]), .init(authStatus: .requestFailed)) - _ = try? database.bulkUpdateContacts(.init(authStatus: [.confirming]), .init(authStatus: .confirmationFailed)) - _ = try? database.bulkUpdateContacts(.init(authStatus: [.verificationInProgress]), .init(authStatus: .verificationFailed)) + if dbExistsInLegacyOldPath && !dbExistsInLegacyPath { + try? FileManager.default.moveItem(atPath: legacyOldPath, toPath: legacyPath) } - func getContactWith(userId: Data) -> XXModels.Contact? { - let query = Contact.Query( - id: [userId], - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil - ) - - guard let database: Database = try? DependencyInjection.Container.shared.resolve(), - let contact = try? database.fetchContacts(query).first else { - return nil - } - - return contact - } + let dbPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! + .appendingPathComponent("xxm_database") + .appendingPathExtension("sqlite").path - func getGroupInfoWith(groupId: Data) -> GroupInfo? { - let query = GroupInfo.Query(groupId: groupId) + let database = try Database.onDisk(path: dbPath) - guard let database: Database = try? DependencyInjection.Container.shared.resolve(), - let info = try? database.fetchGroupInfos(query).first else { - return nil - } + if dbExistsInLegacyPath { + try Migrator.live()( + try .init(path: legacyPath), + to: database, + myContactId: Data(), //client.bindings.myId, + meMarshaled: Data() //client.bindings.meMarshalled + ) - return info + try FileManager.default.moveItem(atPath: legacyPath, toPath: legacyPath.appending("-backup")) } - private func versionFailed(error: Error) { - let title = Localized.Launch.Version.failed - let content = error.localizedDescription - let hudError = HUDError(content: content, title: title, dismissable: false) + DependencyInjection.Container.shared.register(database) - hudSubject.send(.error(hudError)) - } - - private func versionUpdateRequired(_ info: DappVersionInformation) { - hudSubject.send(.none) + _ = try? database.bulkUpdateContacts(.init(authStatus: [.requesting]), .init(authStatus: .requestFailed)) + _ = try? database.bulkUpdateContacts(.init(authStatus: [.confirming]), .init(authStatus: .confirmationFailed)) + _ = try? database.bulkUpdateContacts(.init(authStatus: [.verificationInProgress]), .init(authStatus: .verificationFailed)) + } - let model = Update( - content: info.minimumMessage, - urlString: info.appUrl, - positiveActionTitle: Localized.Launch.Version.Required.positive, - negativeActionTitle: nil, - actionStyle: .brandColored - ) + func getContactWith(userId: Data) -> XXModels.Contact? { + let query = Contact.Query( + id: [userId], + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ) - routeSubject.send(.update(model)) + guard let database: Database = try? DependencyInjection.Container.shared.resolve(), + let contact = try? database.fetchContacts(query).first else { + return nil } - private func versionUpdateRecommended(_ info: DappVersionInformation) { - hudSubject.send(.none) + return contact + } - let model = Update( - content: Localized.Launch.Version.Recommended.title, - urlString: info.appUrl, - positiveActionTitle: Localized.Launch.Version.Recommended.positive, - negativeActionTitle: Localized.Launch.Version.Recommended.negative, - actionStyle: .simplestColoredRed - ) + func getGroupInfoWith(groupId: Data) -> GroupInfo? { + let query = GroupInfo.Query(groupId: groupId) - routeSubject.send(.update(model)) + guard let database: Database = try? DependencyInjection.Container.shared.resolve(), + let info = try? database.fetchGroupInfos(query).first else { + return nil } - private func checkBiometrics(completion: @escaping (Result<Bool, Error>) -> Void) { - if permissionHandler.isBiometricsAvailable && isBiometricsOn { - permissionHandler.requestBiometrics { - switch $0 { - case .success(let granted): - completion(.success(granted)) - - case .failure(let error): - completion(.failure(error)) - } - } - } else { - completion(.success(true)) + return info + } + + private func versionFailed(error: Error) { + let title = Localized.Launch.Version.failed + let content = error.localizedDescription + let hudError = HUDError(content: content, title: title, dismissable: false) + + hudSubject.send(.error(hudError)) + } + + private func versionUpdateRequired(_ info: DappVersionInformation) { + hudSubject.send(.none) + + let model = Update( + content: info.minimumMessage, + urlString: info.appUrl, + positiveActionTitle: Localized.Launch.Version.Required.positive, + negativeActionTitle: nil, + actionStyle: .brandColored + ) + + routeSubject.send(.update(model)) + } + + private func versionUpdateRecommended(_ info: DappVersionInformation) { + hudSubject.send(.none) + + let model = Update( + content: Localized.Launch.Version.Recommended.title, + urlString: info.appUrl, + positiveActionTitle: Localized.Launch.Version.Recommended.positive, + negativeActionTitle: Localized.Launch.Version.Recommended.negative, + actionStyle: .simplestColoredRed + ) + + routeSubject.send(.update(model)) + } + + private func checkBiometrics(completion: @escaping (Result<Bool, Error>) -> Void) { + if permissionHandler.isBiometricsAvailable && isBiometricsOn { + permissionHandler.requestBiometrics { + switch $0 { + case .success(let granted): + completion(.success(granted)) + + case .failure(let error): + completion(.failure(error)) } + } + } else { + completion(.success(true)) } - - private func updateErrors(completion: @escaping () -> Void) { - let errorsURLString = "https://git.xx.network/elixxir/client-error-database/-/raw/main/clientErrors.json" - - URLSession.shared.dataTask(with: URL(string: errorsURLString)!) { [weak self] data, _, error in - guard let self = self else { return } - - guard error == nil else { - print(">>> Issue when trying to download errors json: \(error!.localizedDescription)") - self.updateErrors(completion: completion) - return - } - - guard let data = data, let json = String(data: data, encoding: .utf8) else { - print(">>> Issue when trying to unwrap errors json") - return - } - - do { - try UpdateCommonErrors.live(jsonFile: json) - completion() - } catch { - print(">>> Issue when trying to update common errors: \(error.localizedDescription)") - } - }.resume() - } - - private func updateBannedList(completion: @escaping () -> Void) { - fetchBannedList { result in - switch result { - case .failure(_): - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - self.updateBannedList(completion: completion) - } - case .success(let data): - self.processBannedList(data, completion: completion) - } + } + + private func updateErrors(completion: @escaping () -> Void) { + let errorsURLString = "https://git.xx.network/elixxir/client-error-database/-/raw/main/clientErrors.json" + + URLSession.shared.dataTask(with: URL(string: errorsURLString)!) { [weak self] data, _, error in + guard let self = self else { return } + + guard error == nil else { + print(">>> Issue when trying to download errors json: \(error!.localizedDescription)") + self.updateErrors(completion: completion) + return + } + + guard let data = data, let json = String(data: data, encoding: .utf8) else { + print(">>> Issue when trying to unwrap errors json") + return + } + + do { + try UpdateCommonErrors.live(jsonFile: json) + completion() + } catch { + print(">>> Issue when trying to update common errors: \(error.localizedDescription)") + } + }.resume() + } + + private func updateBannedList(completion: @escaping () -> Void) { + fetchBannedList { result in + switch result { + case .failure(_): + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.updateBannedList(completion: completion) } + case .success(let data): + self.processBannedList(data, completion: completion) + } } - - private func processBannedList(_ data: Data, completion: @escaping () -> Void) { - processBannedList( - data: data, - forEach: { result in - switch result { - case .success(let userId): - let query = Contact.Query(id: [userId]) - if var contact = try! database.fetchContacts(query).first { - if contact.isBanned == false { - contact.isBanned = true - try! database.saveContact(contact) - self.enqueueBanWarning(contact: contact) - } - } else { - try! database.saveContact(.init(id: userId, isBanned: true)) - } - - case .failure(_): - break - } - }, - completion: { result in - switch result { - case .failure(_): - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - self.updateBannedList(completion: completion) - } - - case .success(_): - completion() - } + } + + private func processBannedList(_ data: Data, completion: @escaping () -> Void) { + processBannedList( + data: data, + forEach: { result in + switch result { + case .success(let userId): + let query = Contact.Query(id: [userId]) + if var contact = try! database.fetchContacts(query).first { + if contact.isBanned == false { + contact.isBanned = true + try! database.saveContact(contact) + self.enqueueBanWarning(contact: contact) } - ) - } + } else { + try! database.saveContact(.init(id: userId, isBanned: true)) + } - private func enqueueBanWarning(contact: XXModels.Contact) { - let name = (contact.nickname ?? contact.username) ?? "One of your contacts" - toastController.enqueueToast(model: .init( - title: "\(name) has been banned for offensive content.", - leftImage: Asset.requestSentToaster.image - )) - } + case .failure(_): + break + } + }, + completion: { result in + switch result { + case .failure(_): + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.updateBannedList(completion: completion) + } + + case .success(_): + completion() + } + } + ) + } + + private func enqueueBanWarning(contact: XXModels.Contact) { + let name = (contact.nickname ?? contact.username) ?? "One of your contacts" + toastController.enqueueToast(model: .init( + title: "\(name) has been banned for offensive content.", + leftImage: Asset.requestSentToaster.image + )) + } } extension LaunchViewModel { - private func generateGroupManager(_ messenger: Messenger) throws { - let manager = try NewGroupChat.live( - e2eId: messenger.e2e()!.getId(), - groupRequest: .init(handle: { [weak self] group in - guard let self = self else { return } - self.handleGroupRequest(from: group, messenger: messenger) - }), - groupChatProcessor: .init(handle: { result in - switch result { - case .success(let cb): - - print("Incoming GroupMessage:") - print("- groupId: \(cb.decryptedMessage.groupId.base64EncodedString().prefix(10))...") - print("- senderId: \(cb.decryptedMessage.senderId.base64EncodedString().prefix(10))...") - print("- messageId: \(cb.decryptedMessage.messageId.base64EncodedString().prefix(10))...") - - if let payload = try? Payload(with: cb.decryptedMessage.payload) { - print("- payload.text: \(payload.text)") - - if let reply = payload.reply { - print("- payload.reply.senderId: \(reply.senderId.base64EncodedString().prefix(10))...") - print("- payload.reply.messageId: \(reply.messageId.base64EncodedString().prefix(10))...") - } else { - print("- payload.reply: ∅") - } - } - print("") - - guard let payload = try? Payload(with: cb.decryptedMessage.payload) else { - fatalError("Couldn't decode payload: \(String(data: cb.decryptedMessage.payload, encoding: .utf8) ?? "nil")") - } - - let msg = Message( - networkId: cb.decryptedMessage.messageId, - senderId: cb.decryptedMessage.senderId, - recipientId: nil, - groupId: cb.decryptedMessage.groupId, - date: Date.fromTimestamp(Int(cb.decryptedMessage.timestamp)), - status: .received, - isUnread: true, - text: payload.text, - replyMessageId: payload.reply?.messageId, - roundURL: "https://google.com.br", - fileTransferId: nil - ) - - _ = try? self.database.saveMessage(msg) - - case .failure(let error): - break - } - }) - ) - - DependencyInjection.Container.shared.register(manager) - } - - private func generateTransferManager(_ messenger: Messenger) throws { - let manager = try InitFileTransfer.live( - e2eId: messenger.e2e()!.getId(), - callback: .init(handle: { [weak self] in - guard let self = self else { return } - - switch $0 { - case .success(let receivedFile): - self.handleIncomingTransfer(receivedFile, messenger: messenger) - case .failure(let error): - print(error.localizedDescription) - } - }) - ) + private func generateGroupManager(_ messenger: Messenger) throws { + let manager = try NewGroupChat.live( + e2eId: messenger.e2e()!.getId(), + groupRequest: .init(handle: { [weak self] group in + guard let self = self else { return } + self.handleGroupRequest(from: group, messenger: messenger) + }), + groupChatProcessor: .init(handle: { result in + switch result { + case .success(let cb): + + print("Incoming GroupMessage:") + print("- groupId: \(cb.decryptedMessage.groupId.base64EncodedString().prefix(10))...") + print("- senderId: \(cb.decryptedMessage.senderId.base64EncodedString().prefix(10))...") + print("- messageId: \(cb.decryptedMessage.messageId.base64EncodedString().prefix(10))...") + + if let payload = try? Payload(with: cb.decryptedMessage.payload) { + print("- payload.text: \(payload.text)") + + if let reply = payload.reply { + print("- payload.reply.senderId: \(reply.senderId.base64EncodedString().prefix(10))...") + print("- payload.reply.messageId: \(reply.messageId.base64EncodedString().prefix(10))...") + } else { + print("- payload.reply: ∅") + } + } + print("") + + guard let payload = try? Payload(with: cb.decryptedMessage.payload) else { + fatalError("Couldn't decode payload: \(String(data: cb.decryptedMessage.payload, encoding: .utf8) ?? "nil")") + } + + let msg = Message( + networkId: cb.decryptedMessage.messageId, + senderId: cb.decryptedMessage.senderId, + recipientId: nil, + groupId: cb.decryptedMessage.groupId, + date: Date.fromTimestamp(Int(cb.decryptedMessage.timestamp)), + status: .received, + isUnread: true, + text: payload.text, + replyMessageId: payload.reply?.messageId, + roundURL: "https://google.com.br", + fileTransferId: nil + ) + + _ = try? self.database.saveMessage(msg) + + case .failure(let error): + break + } + }) + ) + + DependencyInjection.Container.shared.register(manager) + } + + private func generateTransferManager(_ messenger: Messenger) throws { + let manager = try InitFileTransfer.live( + e2eId: messenger.e2e()!.getId(), + callback: .init(handle: { [weak self] in + guard let self = self else { return } + + switch $0 { + case .success(let receivedFile): + self.handleIncomingTransfer(receivedFile, messenger: messenger) + case .failure(let error): + print(error.localizedDescription) + } + }) + ) - DependencyInjection.Container.shared.register(manager) - } + DependencyInjection.Container.shared.register(manager) + } - private func generateTrafficManager(_ messenger: Messenger) throws { - let manager = try NewDummyTrafficManager.live( - cMixId: messenger.e2e()!.getId() - ) + private func generateTrafficManager(_ messenger: Messenger) throws { + let manager = try NewDummyTrafficManager.live( + cMixId: messenger.e2e()!.getId() + ) - DependencyInjection.Container.shared.register(manager) - try! manager.setStatus(dummyTrafficOn) - } + DependencyInjection.Container.shared.register(manager) + try! manager.setStatus(dummyTrafficOn) + } } extension LaunchViewModel { - private func handleDirectRequest(from contact: XXClient.Contact) { - guard let id = try? contact.getId() else { - fatalError("Couldn't extract ID from contact request arrived.") - } - - if let _ = try? database.fetchContacts(.init(id: [id])).first { - print(">>> Tried to handle request from pre-existing contact.") - return - } - - let facts = try? contact.getFacts() - let email = facts?.first(where: { $0.type == .email })?.value - let phone = facts?.first(where: { $0.type == .phone })?.value - let username = facts?.first(where: { $0.type == .username })?.value - - var model = try! database.saveContact(.init( - id: id, - marshaled: contact.data, - username: username, - email: email, - phone: phone, - nickname: nil, - photo: nil, - authStatus: .verificationInProgress, - isRecent: true, - createdAt: Date() - )) + private func handleDirectRequest(from contact: XXClient.Contact) { + guard let id = try? contact.getId() else { + fatalError("Couldn't extract ID from contact request arrived.") + } - do { - let messenger: Messenger = try DependencyInjection.Container.shared.resolve() - try messenger.waitForNetwork() + if let _ = try? database.fetchContacts(.init(id: [id])).first { + print(">>> Tried to handle request from pre-existing contact.") + return + } - if try messenger.verifyContact(contact) { - print(">>> [messenger.verifyContact \(#file):\(#line)]") + let facts = try? contact.getFacts() + let email = facts?.first(where: { $0.type == .email })?.value + let phone = facts?.first(where: { $0.type == .phone })?.value + let username = facts?.first(where: { $0.type == .username })?.value + + var model = try! database.saveContact(.init( + id: id, + marshaled: contact.data, + username: username, + email: email, + phone: phone, + nickname: nil, + photo: nil, + authStatus: .verificationInProgress, + isRecent: true, + createdAt: Date() + )) + + do { + let messenger: Messenger = try DependencyInjection.Container.shared.resolve() + try messenger.waitForNetwork() + + if try messenger.verifyContact(contact) { + print(">>> [messenger.verifyContact \(#file):\(#line)]") + + model.authStatus = .verified + model = try database.saveContact(model) + } else { + print(">>> [messenger.verifyContact \(#file):\(#line)]") + try database.deleteContact(model) + } + } catch { + print(">>> [messenger.verifyContact] thrown an exception: \(error.localizedDescription)") + + model.authStatus = .verificationFailed + model = try! database.saveContact(model) + } + } - model.authStatus = .verified - model = try database.saveContact(model) - } else { - print(">>> [messenger.verifyContact \(#file):\(#line)]") - try database.deleteContact(model) - } - } catch { - print(">>> [messenger.verifyContact] thrown an exception: \(error.localizedDescription)") + private func handleConfirm(from contact: XXClient.Contact) { + guard let id = try? contact.getId() else { + fatalError("Couldn't extract ID from contact confirmation arrived.") + } - model.authStatus = .verificationFailed - model = try! database.saveContact(model) - } + guard var existentContact = try? database.fetchContacts(.init(id: [id])).first else { + print(">>> Tried to handle a confirmation from someone that is not a contact yet") + return } - private func handleConfirm(from contact: XXClient.Contact) { - guard let id = try? contact.getId() else { - fatalError("Couldn't extract ID from contact confirmation arrived.") - } + existentContact.isRecent = true + existentContact.authStatus = .friend + try! database.saveContact(existentContact) + } - guard var existentContact = try? database.fetchContacts(.init(id: [id])).first else { - print(">>> Tried to handle a confirmation from someone that is not a contact yet") - return - } + private func handleReset(from user: XXClient.Contact) { + if var contact = try? database.fetchContacts(.init(id: [user.getId()])).first { + contact.authStatus = .friend + _ = try? database.saveContact(contact) + } + } - existentContact.isRecent = true - existentContact.authStatus = .friend - try! database.saveContact(existentContact) + private func handleGroupRequest(from group: XXClient.Group, messenger: Messenger) { + if let _ = try? database.fetchGroups(.init(id: [group.getId()])).first { + print(">>> Tried to handle a group request that is already handled") + return } - private func handleReset(from user: XXClient.Contact) { - if var contact = try? database.fetchContacts(.init(id: [user.getId()])).first { - contact.authStatus = .friend - _ = try? database.saveContact(contact) - } + guard var members = try? group.getMembership(), let leader = members.first else { + fatalError("Failed to get group membership/leader") } - private func handleGroupRequest(from group: XXClient.Group, messenger: Messenger) { - if let _ = try? database.fetchGroups(.init(id: [group.getId()])).first { - print(">>> Tried to handle a group request that is already handled") - return - } + try! database.saveGroup(.init( + id: group.getId(), + name: String(data: group.getName(), encoding: .utf8)!, + leaderId: leader.id, + createdAt: Date.fromMSTimestamp(group.getCreatedMS()), + authStatus: .pending, + serialized: group.serialize() + )) + + if let initMessageData = group.getInitMessage(), + let initMessage = String(data: initMessageData, encoding: .utf8) { + try! database.saveMessage(.init( + senderId: leader.id, + recipientId: nil, + groupId: group.getId(), + date: Date.fromMSTimestamp(group.getCreatedMS()), + status: .received, + isUnread: true, + text: initMessage + )) + } - guard var members = try? group.getMembership(), let leader = members.first else { - fatalError("Failed to get group membership/leader") - } + print(">>> All members in the arrived group request:") + members.forEach { print(">>> \($0.id.base64EncodedString().prefix(10))...") } + print(">>> My ud.id is: \(try! messenger.ud.get()!.getContact().getId().base64EncodedString().prefix(10))...") + print(">>> My e2e.id is: \(try! messenger.e2e.get()!.getContact().getId().base64EncodedString().prefix(10))...") + + let friends = try! database.fetchContacts(.init( + id: Set(members.map(\.id)), + authStatus: [ + .friend, + .hidden, + .requesting, + .confirming, + .verificationInProgress, + .verified, + .requested, + .requestFailed, + .verificationFailed, + .confirmationFailed + ] + )) + + print(">>> These people I already know:") + friends.forEach { + print(">>> Username: \($0.username), authStatus: \($0.authStatus.rawValue), id: \($0.id.base64EncodedString().prefix(10))...") + } - try! database.saveGroup(.init( - id: group.getId(), - name: String(data: group.getName(), encoding: .utf8)!, - leaderId: leader.id, - createdAt: Date.fromMSTimestamp(group.getCreatedMS()), - authStatus: .pending, - serialized: group.serialize() + let strangers = Set(members.map(\.id)).subtracting(Set(friends.map(\.id))) + + strangers.forEach { + if let stranger = try? database.fetchContacts(.init(id: [$0])).first { + print(">>> This is a stranger, but I already knew about his/her existance: \(stranger.id.base64EncodedString().prefix(10))...") + } else { + print(">>> This is a complete stranger. Storing on the db: \($0.base64EncodedString().prefix(10))...") + + try! database.saveContact(.init( + id: $0, + marshaled: nil, + username: "Fetching...", + email: nil, + phone: nil, + nickname: nil, + photo: nil, + authStatus: .stranger, + isRecent: false, + isBlocked: false, + isBanned: false, + createdAt: Date.fromMSTimestamp(group.getCreatedMS()) )) + } + } - if let initMessageData = group.getInitMessage(), - let initMessage = String(data: initMessageData, encoding: .utf8) { - try! database.saveMessage(.init( - senderId: leader.id, - recipientId: nil, - groupId: group.getId(), - date: Date.fromMSTimestamp(group.getCreatedMS()), - status: .received, - isUnread: true, - text: initMessage - )) - } + members.forEach { + let model = XXModels.GroupMember(groupId: group.getId(), contactId: $0.id) + _ = try? database.saveGroupMember(model) + } - print(">>> All members in the arrived group request:") - members.forEach { print(">>> \($0.id.base64EncodedString().prefix(10))...") } - print(">>> My ud.id is: \(try! messenger.ud.get()!.getContact().getId().base64EncodedString().prefix(10))...") - print(">>> My e2e.id is: \(try! messenger.e2e.get()!.getContact().getId().base64EncodedString().prefix(10))...") - - let friends = try! database.fetchContacts(.init( - id: Set(members.map(\.id)), - authStatus: [ - .friend, - .hidden, - .requesting, - .confirming, - .verificationInProgress, - .verified, - .requested, - .requestFailed, - .verificationFailed, - .confirmationFailed - ] - )) + print(">>> Performing a multi-lookup for group strangers:") - print(">>> These people I already know:") - friends.forEach { - print(">>> Username: \($0.username), authStatus: \($0.authStatus.rawValue), id: \($0.id.base64EncodedString().prefix(10))...") - } + do { + let multiLookup = try messenger.lookupContacts(ids: strangers.map { $0 }) - let strangers = Set(members.map(\.id)).subtracting(Set(friends.map(\.id))) + for user in multiLookup.contacts { + print(">>> Found stranger w/ id: \(try! user.getId().base64EncodedString().prefix(10))...") - strangers.forEach { - if let stranger = try? database.fetchContacts(.init(id: [$0])).first { - print(">>> This is a stranger, but I already knew about his/her existance: \(stranger.id.base64EncodedString().prefix(10))...") - } else { - print(">>> This is a complete stranger. Storing on the db: \($0.base64EncodedString().prefix(10))...") - - try! database.saveContact(.init( - id: $0, - marshaled: nil, - username: "Fetching...", - email: nil, - phone: nil, - nickname: nil, - photo: nil, - authStatus: .stranger, - isRecent: false, - isBlocked: false, - isBanned: false, - createdAt: Date.fromMSTimestamp(group.getCreatedMS()) - )) - } - } - - members.forEach { - let model = XXModels.GroupMember(groupId: group.getId(), contactId: $0.id) - _ = try? database.saveGroupMember(model) + if var foo = try? self.database.fetchContacts(.init(id: [user.getId()])).first, + let username = try? user.getFact(.username)?.value { + foo.username = username + print(">>> Set username: \(username) for \(try! user.getId().base64EncodedString().prefix(10))...") + _ = try? self.database.saveContact(foo) } + } - print(">>> Performing a multi-lookup for group strangers:") - - do { - let multiLookup = try messenger.lookupContacts(ids: strangers.map { $0 }) - - for user in multiLookup.contacts { - print(">>> Found stranger w/ id: \(try! user.getId().base64EncodedString().prefix(10))...") - - if var foo = try? self.database.fetchContacts(.init(id: [user.getId()])).first, - let username = try? user.getFact(.username)?.value { - foo.username = username - print(">>> Set username: \(username) for \(try! user.getId().base64EncodedString().prefix(10))...") - _ = try? self.database.saveContact(foo) - } - } - - for error in multiLookup.errors { - print(">>> Failure on Multilookup: \(error.localizedDescription)") - } + for error in multiLookup.errors { + print(">>> Failure on Multilookup: \(error.localizedDescription)") + } - for failedId in multiLookup.failedIds { - print(">>> Failed id: \(failedId.base64EncodedString().prefix(10))...") - } - } catch { - print(">>> Exception on multilookup: \(error.localizedDescription)") - } + for failedId in multiLookup.failedIds { + print(">>> Failed id: \(failedId.base64EncodedString().prefix(10))...") + } + } catch { + print(">>> Exception on multilookup: \(error.localizedDescription)") } + } } extension LaunchViewModel { - private func handleIncomingTransfer(_ receivedFile: ReceivedFile, messenger: Messenger) { - if var model = try? database.saveFileTransfer(.init( - id: receivedFile.transferId, - contactId: receivedFile.senderId, - name: receivedFile.name, - type: receivedFile.type, - data: nil, - progress: 0.0, - isIncoming: true, - createdAt: Date() - )) { - try! database.saveMessage(.init( - networkId: nil, - senderId: receivedFile.senderId, - recipientId: messenger.e2e.get()!.getContact().getId(), - groupId: nil, - date: Date(), - status: .receiving, - isUnread: false, - text: "", - replyMessageId: nil, - roundURL: nil, - fileTransferId: model.id - )) - - if let manager: XXClient.FileTransfer = try? DependencyInjection.Container.shared.resolve() { - print(">>> registerReceivedProgressCallback") - - try! manager.registerReceivedProgressCallback( - transferId: receivedFile.transferId, - period: 1_000, - callback: .init(handle: { [weak self] in - guard let self = self else { return } - switch $0 { - case .success(let cb): - if cb.progress.completed { - model.progress = 100 - model.data = try! manager.receive(transferId: receivedFile.transferId) - } else { - model.progress = Float(cb.progress.transmitted/cb.progress.total) - } - - model = try! self.database.saveFileTransfer(model) - - case .failure(let error): - print(error.localizedDescription) - } - }) - ) - } else { -// print(DependencyInjection.Container.shared.dependencies) + private func handleIncomingTransfer(_ receivedFile: ReceivedFile, messenger: Messenger) { + if var model = try? database.saveFileTransfer(.init( + id: receivedFile.transferId, + contactId: receivedFile.senderId, + name: receivedFile.name, + type: receivedFile.type, + data: nil, + progress: 0.0, + isIncoming: true, + createdAt: Date() + )) { + try! database.saveMessage(.init( + networkId: nil, + senderId: receivedFile.senderId, + recipientId: messenger.e2e.get()!.getContact().getId(), + groupId: nil, + date: Date(), + status: .receiving, + isUnread: false, + text: "", + replyMessageId: nil, + roundURL: nil, + fileTransferId: model.id + )) + + if let manager: XXClient.FileTransfer = try? DependencyInjection.Container.shared.resolve() { + print(">>> registerReceivedProgressCallback") + + try! manager.registerReceivedProgressCallback( + transferId: receivedFile.transferId, + period: 1_000, + callback: .init(handle: { [weak self] in + guard let self = self else { return } + switch $0 { + case .success(let cb): + if cb.progress.completed { + model.progress = 100 + model.data = try! manager.receive(transferId: receivedFile.transferId) + } else { + model.progress = Float(cb.progress.transmitted/cb.progress.total) + } + + model = try! self.database.saveFileTransfer(model) + + case .failure(let error): + print(error.localizedDescription) } - } - } - - private func setupLogWriter() { - _ = try! SetLogLevel.live(.debug) - RegisterLogWriter.live(.init(handle: { XXLogger.live().debug($0) })) + }) + ) + } else { + // print(DependencyInjection.Container.shared.dependencies) + } } - - private func makeMessenger() -> Messenger { - var environment: MessengerEnvironment = .live() - environment.ndfEnvironment = .mainnet - environment.udAddress = "46.101.98.49:18001" - environment.udCert = """ + } + + private func setupLogWriter() { + _ = try! SetLogLevel.live(.debug) + RegisterLogWriter.live(.init(handle: { XXLogger.live().debug($0) })) + } + + private func makeMessenger() -> Messenger { + var environment: MessengerEnvironment = .live() + environment.ndfEnvironment = .mainnet + environment.udAddress = "46.101.98.49:18001" + environment.udCert = """ -----BEGIN CERTIFICATE----- MIIDbDCCAlSgAwIBAgIJAOUNtZneIYECMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx @@ -733,60 +737,67 @@ extension LaunchViewModel { 6m52PyzMNV+2N21IPppKwA== -----END CERTIFICATE----- """.data(using: .utf8)! - environment.udContact = """ + environment.udContact = """ <xxc(2)7mbKFLE201WzH4SGxAOpHjjehwztIV+KGifi5L/PYPcDkAZiB9kZo+Dl3Vc7dD2SdZCFMOJVgwqGzfYRDkjc8RGEllBqNxq2sRRX09iQVef0kJQUgJCHNCOcvm6Ki0JJwvjLceyFh36iwK8oLbhLgqEZY86UScdACTyBCzBIab3ob5mBthYc3mheV88yq5PGF2DQ+dEvueUm+QhOSfwzppAJA/rpW9Wq9xzYcQzaqc3ztAGYfm2BBAHS7HVmkCbvZ/K07Xrl4EBPGHJYq12tWAN/C3mcbbBYUOQXyEzbSl/mO7sL3ORr0B4FMuqCi8EdlD6RO52pVhY+Cg6roRH1t5Ng1JxPt8Mv1yyjbifPhZ5fLKwxBz8UiFORfk0/jnhwgm25LRHqtNRRUlYXLvhv0HhqyYTUt17WNtCLATSVbqLrFGdy2EGadn8mP+kQNHp93f27d/uHgBNNe7LpuYCJMdWpoG6bOqmHEftxt0/MIQA8fTtTm3jJzv+7/QjZJDvQIv0SNdp8HFogpuwde+GuS4BcY7v5xz+ArGWcRR63ct2z83MqQEn9ODr1/gAAAgA7szRpDDQIdFUQo9mkWg8xBA==xxc> """.data(using: .utf8) - return Messenger.live(environment) - } - - private func setupAuthCallback(_ messenger: Messenger) { - authCallbacksCancellable = messenger.registerAuthCallbacks( - AuthCallbacks(handle: { - switch $0 { - case .confirm(contact: let contact, receptionId: _, ephemeralId: _, roundId: _): - self.handleConfirm(from: contact) - case .request(contact: let contact, receptionId: _, ephemeralId: _, roundId: _): - self.handleDirectRequest(from: contact) - case .reset(contact: let contact, receptionId: _, ephemeralId: _, roundId: _): - self.handleReset(from: contact) - } - }) - ) - } - - private func setupMessageCallback(_ messenger: Messenger) { - messageListenerCallbacksCancellable = messenger.registerMessageListener(.init(handle: { - guard let payload = try? Payload(with: $0.payload) else { - fatalError("Couldn't decode payload: \(String(data: $0.payload, encoding: .utf8) ?? "nil")") - } - - try! self.database.saveMessage(.init( - networkId: $0.id, - senderId: $0.sender, - recipientId: messenger.e2e.get()!.getContact().getId(), - groupId: nil, - date: Date.fromTimestamp($0.timestamp), - status: .received, - isUnread: true, - text: payload.text, - replyMessageId: payload.reply?.messageId, - roundURL: $0.roundURL, - fileTransferId: nil - )) - - if var contact = try? self.database.fetchContacts(.init(id: [$0.sender])).first { - contact.isRecent = false - try! self.database.saveContact(contact) - } - })) - } - - private func listenToNetworkUpdates(_ messenger: Messenger) { - networkMonitor.start() - networkCallbacksCancellable = messenger.cMix.get()!.addHealthCallback(.init(handle: { [weak self] in - guard let self = self else { return } - self.networkMonitor.update($0) - })) - } + return Messenger.live(environment) + } + + private func setupAuthCallback(_ messenger: Messenger) { + authCallbacksCancellable = messenger.registerAuthCallbacks( + AuthCallbacks(handle: { + switch $0 { + case .confirm(contact: let contact, receptionId: _, ephemeralId: _, roundId: _): + self.handleConfirm(from: contact) + case .request(contact: let contact, receptionId: _, ephemeralId: _, roundId: _): + self.handleDirectRequest(from: contact) + case .reset(contact: let contact, receptionId: _, ephemeralId: _, roundId: _): + self.handleReset(from: contact) + } + }) + ) + } + + private func setupBackupCallback(_ messenger: Messenger) { + backupCallbackCancellable = messenger.registerBackupCallback(.init(handle: { [weak self] in + print(">>> Backup callback from bindings got called") + self?.backupService.updateBackup(data: $0) + })) + } + + private func setupMessageCallback(_ messenger: Messenger) { + messageListenerCallbacksCancellable = messenger.registerMessageListener(.init(handle: { + guard let payload = try? Payload(with: $0.payload) else { + fatalError("Couldn't decode payload: \(String(data: $0.payload, encoding: .utf8) ?? "nil")") + } + + try! self.database.saveMessage(.init( + networkId: $0.id, + senderId: $0.sender, + recipientId: messenger.e2e.get()!.getContact().getId(), + groupId: nil, + date: Date.fromTimestamp($0.timestamp), + status: .received, + isUnread: true, + text: payload.text, + replyMessageId: payload.reply?.messageId, + roundURL: $0.roundURL, + fileTransferId: nil + )) + + if var contact = try? self.database.fetchContacts(.init(id: [$0.sender])).first { + contact.isRecent = false + try! self.database.saveContact(contact) + } + })) + } + + private func listenToNetworkUpdates(_ messenger: Messenger) { + networkMonitor.start() + networkCallbacksCancellable = messenger.cMix.get()!.addHealthCallback(.init(handle: { [weak self] in + guard let self = self else { return } + self.networkMonitor.update($0) + })) + } } diff --git a/Sources/MenuFeature/ViewModels/MenuViewModel.swift b/Sources/MenuFeature/ViewModels/MenuViewModel.swift index ce8161fe4e1c248a0a841f8560751cba05be0213..9d56f10abf0cf3bde167b10c96f54e8663fa3a2b 100644 --- a/Sources/MenuFeature/ViewModels/MenuViewModel.swift +++ b/Sources/MenuFeature/ViewModels/MenuViewModel.swift @@ -33,8 +33,10 @@ final class MenuViewModel { ) return Publishers.CombineLatest( - database.fetchContactsPublisher(contactsQuery).assertNoFailure(), - database.fetchGroupsPublisher(groupQuery).assertNoFailure() + database.fetchContactsPublisher(contactsQuery) + .replaceError(with: []), + database.fetchGroupsPublisher(groupQuery) + .replaceError(with: []) ) .map { $0.0.count + $0.1.count } .eraseToAnyPublisher() diff --git a/Sources/Models/BackupSettings.swift b/Sources/Models/BackupSettings.swift index 89194c24e359664a54701e51b46799c3659604b9..d52cb1f0e33149b70a22337f56c23d6814d7f7be 100644 --- a/Sources/Models/BackupSettings.swift +++ b/Sources/Models/BackupSettings.swift @@ -29,7 +29,7 @@ public struct BackupSettings: Equatable, Codable { let settings = try? PropertyListDecoder().decode(BackupSettings.self, from: data) self.init( wifiOnlyBackup: settings?.wifiOnlyBackup ?? false, - automaticBackups: settings?.automaticBackups ?? false, + automaticBackups: settings?.automaticBackups ?? true, enabledService: settings?.enabledService, connectedServices: settings?.connectedServices ?? [], backups: settings?.backups ?? [:] diff --git a/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift index 7e6f3751cedcceb2ee077fa2c12b60a4c466c744..d7eb5db970117df0d24c0fe33a464bef841d141e 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift @@ -7,9 +7,8 @@ import XXClient import Foundation import InputField import CombineSchedulers -import DependencyInjection import XXMessengerClient -import BackupFeature +import DependencyInjection struct ProfileCodeViewState: Equatable { var input: String = "" @@ -19,7 +18,6 @@ struct ProfileCodeViewState: Equatable { final class ProfileCodeViewModel { @Dependency var messenger: Messenger - @Dependency var backupService: BackupService @KeyObject(.email, defaultValue: nil) var email: String? @KeyObject(.phone, defaultValue: nil) var phone: String? @@ -82,8 +80,6 @@ final class ProfileCodeViewModel { self.phone = self.confirmation.content } - self.backupService.didUpdateFacts() - self.timer?.invalidate() self.hudRelay.send(.none) self.completionRelay.send(self.confirmation) diff --git a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift index 252acd4974aa48cefed3a71b390d14fc62208d9c..1d01467d8e20b16aff653bb67e5a441292b802d6 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift @@ -4,114 +4,114 @@ import Shared import Models import Combine import Defaults +import XXClient import Countries import Foundation import Permissions -import XXClient import CombineSchedulers import DependencyInjection import XXMessengerClient enum ProfileNavigationRoutes { - case none - case library - case libraryPermission + case none + case library + case libraryPermission } struct ProfileViewState: Equatable { - var email: String? - var phone: String? - var photo: UIImage? + var email: String? + var phone: String? + var photo: UIImage? } final class ProfileViewModel { - @KeyObject(.avatar, defaultValue: nil) var avatar: Data? - @KeyObject(.email, defaultValue: nil) var emailStored: String? - @KeyObject(.phone, defaultValue: nil) var phoneStored: String? - @KeyObject(.username, defaultValue: nil) var username: String? - @KeyObject(.sharingEmail, defaultValue: false) var isEmailSharing: Bool - @KeyObject(.sharingPhone, defaultValue: false) var isPhoneSharing: Bool + @KeyObject(.avatar, defaultValue: nil) var avatar: Data? + @KeyObject(.email, defaultValue: nil) var emailStored: String? + @KeyObject(.phone, defaultValue: nil) var phoneStored: String? + @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.sharingEmail, defaultValue: false) var isEmailSharing: Bool + @KeyObject(.sharingPhone, defaultValue: false) var isPhoneSharing: Bool - @Dependency var messenger: Messenger - @Dependency private var permissions: PermissionHandling + @Dependency var messenger: Messenger + @Dependency private var permissions: PermissionHandling - var name: String { username! } + var name: String { username! } - var state: AnyPublisher<ProfileViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<ProfileViewState, Never>(.init()) + var state: AnyPublisher<ProfileViewState, Never> { stateRelay.eraseToAnyPublisher() } + private let stateRelay = CurrentValueSubject<ProfileViewState, Never>(.init()) - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) + var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } + private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - var navigation: AnyPublisher<ProfileNavigationRoutes, Never> { navigationRoutes.eraseToAnyPublisher() } - private let navigationRoutes = PassthroughSubject<ProfileNavigationRoutes, Never>() + var navigation: AnyPublisher<ProfileNavigationRoutes, Never> { navigationRoutes.eraseToAnyPublisher() } + private let navigationRoutes = PassthroughSubject<ProfileNavigationRoutes, Never>() - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - init() { - refresh() - } + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - func refresh() { - var cleanPhone = phoneStored + init() { + refresh() + } - if let phone = cleanPhone { - let country = Country.findFrom(phone) - cleanPhone = "\(country.prefix)\(phone.dropLast(2))" - } + func refresh() { + var cleanPhone = phoneStored - stateRelay.value = .init( - email: emailStored, - phone: cleanPhone, - photo: avatar != nil ? UIImage(data: avatar!) : nil - ) + if let phone = cleanPhone { + let country = Country.findFrom(phone) + cleanPhone = "\(country.prefix)\(phone.dropLast(2))" } - func didRequestLibraryAccess() { - if permissions.isPhotosAllowed { - navigationRoutes.send(.library) - } else { - navigationRoutes.send(.libraryPermission) - } + stateRelay.value = .init( + email: emailStored, + phone: cleanPhone, + photo: avatar != nil ? UIImage(data: avatar!) : nil + ) + } + + func didRequestLibraryAccess() { + if permissions.isPhotosAllowed { + navigationRoutes.send(.library) + } else { + navigationRoutes.send(.libraryPermission) } + } - func didNavigateSomewhere() { - navigationRoutes.send(.none) - } + func didNavigateSomewhere() { + navigationRoutes.send(.none) + } - func didChoosePhoto(_ photo: UIImage) { - stateRelay.value.photo = photo - avatar = photo.jpegData(compressionQuality: 0.0) - } + func didChoosePhoto(_ photo: UIImage) { + stateRelay.value.photo = photo + avatar = photo.jpegData(compressionQuality: 0.0) + } + + func didTapDelete(isEmail: Bool) { + hudRelay.send(.on) + + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } - func didTapDelete(isEmail: Bool) { - hudRelay.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.messenger.ud.get()!.removeFact( - .init( - type: isEmail ? .email : .phone, - value: isEmail ? self.emailStored! : self.phoneStored! - ) - ) - - if isEmail { - self.emailStored = nil - self.isEmailSharing = false - } else { - self.phoneStored = nil - self.isPhoneSharing = false - } - - self.hudRelay.send(.none) - self.refresh() - } catch { - let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) - self.hudRelay.send(.error(.init(content: xxError))) - } + do { + try self.messenger.ud.get()!.removeFact( + .init( + type: isEmail ? .email : .phone, + value: isEmail ? self.emailStored! : self.phoneStored! + ) + ) + + if isEmail { + self.emailStored = nil + self.isEmailSharing = false + } else { + self.phoneStored = nil + self.isPhoneSharing = false } + + self.hudRelay.send(.none) + self.refresh() + } catch { + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudRelay.send(.error(.init(content: xxError))) + } } + } } diff --git a/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift index 1a3fb575a3978b2f2ab889ade23cc846b24fd8b1..e570e286c30e54f136873acf4feba2923f792748 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift @@ -33,7 +33,7 @@ final class RequestsFailedViewModel { init() { database.fetchContactsPublisher(.init(authStatus: [.requestFailed, .confirmationFailed])) - .assertNoFailure() + .replaceError(with: []) .map { data -> NSDiffableDataSourceSnapshot<Section, Request> in var snapshot = NSDiffableDataSourceSnapshot<Section, Request>() snapshot.appendSections([.appearing]) diff --git a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift index 7eacdc19544448e73ddcd75ce64f90e0f78364c4..fd1cc9b51dbf39625d22e709fb17c815586e131b 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift @@ -80,8 +80,13 @@ final class RequestsReceivedViewModel { isBanned: reportingStatus.isEnabled() ? false : nil ) - let groupStream = database.fetchGroupsPublisher(groupsQuery).assertNoFailure() - let contactsStream = database.fetchContactsPublisher(contactsQuery).assertNoFailure() + let groupStream = database + .fetchGroupsPublisher(groupsQuery) + .replaceError(with: []) + + let contactsStream = database + .fetchContactsPublisher(contactsQuery) + .replaceError(with: []) Publishers.CombineLatest3( groupStream, @@ -212,7 +217,7 @@ final class RequestsReceivedViewModel { ) { if let info = try? database.fetchGroupInfos(.init(groupId: group.id)).first { database.fetchContactsPublisher(.init(id: Set(info.members.map(\.id)))) - .assertNoFailure() + .replaceError(with: []) .sink { members in let withUsername = members .filter { $0.username != nil } diff --git a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift index 6d9457b562e85d780f0abeec4ae227581a90ef8f..e23720943c3e900c1c322c2d717a4884a97c6d27 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift @@ -53,7 +53,7 @@ final class RequestsSentViewModel { ) database.fetchContactsPublisher(query) - .assertNoFailure() + .replaceError(with: []) .removeDuplicates() .map { data -> NSDiffableDataSourceSnapshot<Section, RequestSent> in var snapshot = NSDiffableDataSourceSnapshot<Section, RequestSent>() diff --git a/Sources/RestoreFeature/Controllers/RestoreController.swift b/Sources/RestoreFeature/Controllers/RestoreController.swift index 5ba058c634a7e790927794f6a981a91368acee05..83af547638a6b8ec1808ec234f76adb801fd44ba 100644 --- a/Sources/RestoreFeature/Controllers/RestoreController.swift +++ b/Sources/RestoreFeature/Controllers/RestoreController.swift @@ -1,8 +1,8 @@ import UIKit import Models import Shared -import DrawerFeature import Combine +import DrawerFeature import DependencyInjection public final class RestoreController: UIViewController { @@ -56,9 +56,13 @@ public final class RestoreController: UIViewController { screenView.updateFor(step: $0) if $0 == .wrongPass { - coordinator.toPassphrase(from: self) { pass in - self.viewModel.retryWith(passphrase: pass) + coordinator.toPassphrase( + from: self, + cancelClosure: { self.dismiss(animated: true) }, + passphraseClosure: { pwd in + self.viewModel.retryWith(passphrase: pwd) } + ) return } @@ -81,9 +85,13 @@ public final class RestoreController: UIViewController { screenView.restoreButton .publisher(for: .touchUpInside) .sink { [unowned self] in - coordinator.toPassphrase(from: self) { passphrase in - self.viewModel.didTapRestore(passphrase: passphrase) + coordinator.toPassphrase( + from: self, + cancelClosure: { self.dismiss(animated: true) }, + passphraseClosure: { pwd in + self.viewModel.didTapRestore(passphrase: pwd) } + ) }.store(in: &cancellables) } diff --git a/Sources/RestoreFeature/Controllers/RestorePassphraseController.swift b/Sources/RestoreFeature/Controllers/RestorePassphraseController.swift index 4d19e356729508ea5b688bb8435e09c602b5c8a6..67dd450b68baef63bccc07257c652705288d97d0 100644 --- a/Sources/RestoreFeature/Controllers/RestorePassphraseController.swift +++ b/Sources/RestoreFeature/Controllers/RestorePassphraseController.swift @@ -2,92 +2,71 @@ import UIKit import Shared import Combine import InputField -import ScrollViewController public final class RestorePassphraseController: UIViewController { - lazy private var screenView = RestorePassphraseView() - - private var passphrase = "" { - didSet { - switch Validator.backupPassphrase.validate(passphrase) { - case .success: - screenView.continueButton.isEnabled = true - case .failure: - screenView.continueButton.isEnabled = false - } - } - } - - private let completion: StringClosure - private var cancellables = Set<AnyCancellable>() - private let keyboardListener = KeyboardFrameChangeListener(notificationCenter: .default) - - public init(_ completion: @escaping StringClosure) { - self.completion = completion - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override func loadView() { - let view = UIView() - view.addSubview(screenView) - - screenView.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalToSuperview().offset(0) - } - - self.view = view - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupKeyboard() - setupBindings() - + lazy private var screenView = RestorePassphraseView() + + private var passphrase = "" { + didSet { + switch Validator.backupPassphrase.validate(passphrase) { + case .success: + screenView.continueButton.isEnabled = true + case .failure: screenView.continueButton.isEnabled = false + } } - - private func setupKeyboard() { - keyboardListener.keyboardFrameWillChange = { [weak self] keyboard in - guard let self = self else { return } - - let inset = self.view.frame.height - self.view.convert(keyboard.frame, from: nil).minY - - self.screenView.snp.updateConstraints { - $0.bottom.equalToSuperview().offset(-inset) - } - - self.view.setNeedsLayout() - - UIView.animate(withDuration: keyboard.animationDuration) { - self.view.layoutIfNeeded() - } + } + + private let cancelClosure: EmptyClosure + private let stringClosure: StringClosure + private var cancellables = Set<AnyCancellable>() + + public init( + _ cancelClosure: @escaping EmptyClosure, + _ stringClosure: @escaping StringClosure + ) { + self.stringClosure = stringClosure + self.cancelClosure = cancelClosure + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func loadView() { + view = screenView + } + + public override func viewDidLoad() { + super.viewDidLoad() + + screenView + .inputField + .returnPublisher + .sink { [unowned self] in + screenView.inputField.endEditing(true) + }.store(in: &cancellables) + + screenView + .inputField + .textPublisher + .sink { [unowned self] in + passphrase = $0.trimmingCharacters(in: .whitespacesAndNewlines) + }.store(in: &cancellables) + + screenView + .continueButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { + self.stringClosure(self.passphrase) } - } - - private func setupBindings() { - screenView.inputField.returnPublisher - .sink { [unowned self] in screenView.inputField.endEditing(true) } - .store(in: &cancellables) - - screenView.cancelButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in dismiss(animated: true) } - .store(in: &cancellables) - - screenView.inputField - .textPublisher - .sink { [unowned self] in passphrase = $0.trimmingCharacters(in: .whitespacesAndNewlines) } - .store(in: &cancellables) - - screenView.continueButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - dismiss(animated: true, completion: { self.completion(self.passphrase) }) - }.store(in: &cancellables) - } + }.store(in: &cancellables) + + screenView + .cancelButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) + }.store(in: &cancellables) + } } diff --git a/Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift b/Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift index b5f5d05716247e1fa164a935c8bc72f25f5b5969..83c698fb30595098474fcf5e324f3723eee4cf59 100644 --- a/Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift +++ b/Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift @@ -2,66 +2,96 @@ import UIKit import Models import Shared import Presentation +import ScrollViewController public protocol RestoreCoordinating { - func toChats(from: UIViewController) - func toSuccess(from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toPassphrase(from: UIViewController, _: @escaping StringClosure) - func toRestore(with: RestoreSettings, from: UIViewController) + func toChats(from: UIViewController) + func toSuccess(from: UIViewController) + func toDrawer(_: UIViewController, from: UIViewController) + func toRestore(with: RestoreSettings, from: UIViewController) + + func toPassphrase( + from: UIViewController, + cancelClosure: @escaping EmptyClosure, + passphraseClosure: @escaping StringClosure + ) } public struct RestoreCoordinator: RestoreCoordinating { - var pushPresenter: Presenting = PushPresenter() - var bottomPresenter: Presenting = BottomPresenter() - var replacePresenter: Presenting = ReplacePresenter() + var pushPresenter: Presenting = PushPresenter() + var bottomPresenter: Presenting = BottomPresenter() + var replacePresenter: Presenting = ReplacePresenter() + var fullscreenPresenter: Presenting = FullscreenPresenter() + + var successFactory: () -> UIViewController + var chatListFactory: () -> UIViewController + var restoreFactory: (RestoreSettings) -> UIViewController - var successFactory: () -> UIViewController - var chatListFactory: () -> UIViewController - var restoreFactory: (RestoreSettings) -> UIViewController - var passphraseFactory: (@escaping StringClosure) -> UIViewController + var passphraseFactory: ( + @escaping EmptyClosure, + @escaping StringClosure + ) -> UIViewController - public init( - successFactory: @escaping () -> UIViewController, - chatListFactory: @escaping () -> UIViewController, - restoreFactory: @escaping (RestoreSettings) -> UIViewController, - passphraseFactory: @escaping (@escaping StringClosure) -> UIViewController - ) { - self.successFactory = successFactory - self.restoreFactory = restoreFactory - self.chatListFactory = chatListFactory - self.passphraseFactory = passphraseFactory - } + public init( + successFactory: @escaping () -> UIViewController, + chatListFactory: @escaping () -> UIViewController, + restoreFactory: @escaping (RestoreSettings) -> UIViewController, + passphraseFactory: @escaping ( + @escaping EmptyClosure, + @escaping StringClosure + ) -> UIViewController + ) { + self.successFactory = successFactory + self.restoreFactory = restoreFactory + self.chatListFactory = chatListFactory + self.passphraseFactory = passphraseFactory + } } public extension RestoreCoordinator { - func toRestore( - with settings: RestoreSettings, - from parent: UIViewController - ) { - let screen = restoreFactory(settings) - pushPresenter.present(screen, from: parent) - } + func toRestore( + with settings: RestoreSettings, + from parent: UIViewController + ) { + let screen = restoreFactory(settings) + pushPresenter.present(screen, from: parent) + } + + func toChats(from parent: UIViewController) { + let screen = chatListFactory() + replacePresenter.present(screen, from: parent) + } - func toChats(from parent: UIViewController) { - let screen = chatListFactory() - replacePresenter.present(screen, from: parent) - } + func toSuccess(from parent: UIViewController) { + let screen = successFactory() + replacePresenter.present(screen, from: parent) + } - func toSuccess(from parent: UIViewController) { - let screen = successFactory() - replacePresenter.present(screen, from: parent) - } + func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { + bottomPresenter.present(drawer, from: parent) + } + + func toPassphrase( + from parent: UIViewController, + cancelClosure: @escaping EmptyClosure, + passphraseClosure: @escaping StringClosure + ) { + let screen = passphraseFactory(cancelClosure, passphraseClosure) + let target = ScrollViewController.embedding(screen) + fullscreenPresenter.present(target, from: parent) + } +} - func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } +extension ScrollViewController { + static func embedding(_ viewController: UIViewController) -> ScrollViewController { + let scrollViewController = ScrollViewController() + scrollViewController.addChild(viewController) + scrollViewController.contentView = viewController.view + scrollViewController.wrapperView.handlesTouchesOutsideContent = false + scrollViewController.wrapperView.alignContentToBottom = true + scrollViewController.scrollView.bounces = false - func toPassphrase( - from parent: UIViewController, - _ completion: @escaping StringClosure - ) { - let screen = passphraseFactory(completion) - bottomPresenter.present(screen, from: parent) - } + viewController.didMove(toParent: scrollViewController) + return scrollViewController + } } diff --git a/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift index ec1895dc2721ab6d695d76d859f36033ebaf1fcd..bbf4daa2d9b4c714c763ffa7f8ab981139110125 100644 --- a/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift +++ b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift @@ -12,171 +12,213 @@ import iCloudFeature import DropboxFeature import GoogleDriveFeature +import XXModels +import XXDatabase + import XXClient import XXMessengerClient enum RestorationStep { - case idle(CloudService, BackupModel?) - case downloading(Float, Float) - case failDownload(Error) - case wrongPass - case parsingData - case done + case idle(CloudService, BackupModel?) + case downloading(Float, Float) + case failDownload(Error) + case wrongPass + case parsingData + case done } extension RestorationStep: Equatable { - static func ==(lhs: RestorationStep, rhs: RestorationStep) -> Bool { - switch (lhs, rhs) { - case (.done, .done), (.wrongPass, .wrongPass): - return true - case let (.failDownload(a), .failDownload(b)): - return a.localizedDescription == b.localizedDescription - case let (.downloading(a, b), .downloading(c, d)): - return a == c && b == d - case (.idle, _), (.downloading, _), (.parsingData, _), - (.done, _), (.failDownload, _), (.wrongPass, _): - return false - } + static func ==(lhs: RestorationStep, rhs: RestorationStep) -> Bool { + switch (lhs, rhs) { + case (.done, .done), (.wrongPass, .wrongPass): + return true + case let (.failDownload(a), .failDownload(b)): + return a.localizedDescription == b.localizedDescription + case let (.downloading(a, b), .downloading(c, d)): + return a == c && b == d + case (.idle, _), (.downloading, _), (.parsingData, _), + (.done, _), (.failDownload, _), (.wrongPass, _): + return false } + } } final class RestoreViewModel { - @Dependency var messenger: Messenger - @Dependency private var sftpService: SFTPService - @Dependency private var iCloudService: iCloudInterface - @Dependency private var dropboxService: DropboxInterface - @Dependency private var googleService: GoogleDriveInterface - - @KeyObject(.username, defaultValue: nil) var username: String? - @KeyObject(.phone, defaultValue: nil) var phone: String? - @KeyObject(.email, defaultValue: nil) var email: String? - - var step: AnyPublisher<RestorationStep, Never> { - stepRelay.eraseToAnyPublisher() + @Dependency var database: Database + @Dependency var messenger: Messenger + @Dependency var sftpService: SFTPService + @Dependency var iCloudService: iCloudInterface + @Dependency var dropboxService: DropboxInterface + @Dependency var googleService: GoogleDriveInterface + + @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.phone, defaultValue: nil) var phone: String? + @KeyObject(.email, defaultValue: nil) var email: String? + + var step: AnyPublisher<RestorationStep, Never> { + stepRelay.eraseToAnyPublisher() + } + + // TO REFACTOR: + // + private var pendingData: Data? + + private var passphrase: String! + private let settings: RestoreSettings + private let stepRelay: CurrentValueSubject<RestorationStep, Never> + + init(settings: RestoreSettings) { + self.settings = settings + self.stepRelay = .init(.idle(settings.cloudService, settings.backup)) + } + + func retryWith(passphrase: String) { + self.passphrase = passphrase + continueRestoring(data: pendingData!) + } + + func didTapRestore(passphrase: String) { + self.passphrase = passphrase + + guard let backup = settings.backup else { fatalError() } + + stepRelay.send(.downloading(0.0, backup.size)) + + switch settings.cloudService { + case .drive: + downloadBackupForDrive(backup) + case .dropbox: + downloadBackupForDropbox(backup) + case .icloud: + downloadBackupForiCloud(backup) + case .sftp: + downloadBackupForSFTP(backup) } - - // TO REFACTOR: - // - private var pendingData: Data? - - private var passphrase: String! - private let settings: RestoreSettings - private let stepRelay: CurrentValueSubject<RestorationStep, Never> - - init(settings: RestoreSettings) { - self.settings = settings - self.stepRelay = .init(.idle(settings.cloudService, settings.backup)) + } + + private func downloadBackupForSFTP(_ backup: BackupModel) { + sftpService.downloadBackup(path: backup.id) { [weak self] in + guard let self = self else { return } + self.stepRelay.send(.downloading(backup.size, backup.size)) + + switch $0 { + case .success(let data): + self.continueRestoring(data: data) + case .failure(let error): + self.stepRelay.send(.failDownload(error)) + } } - - func retryWith(passphrase: String) { - self.passphrase = passphrase - continueRestoring(data: pendingData!) + } + + private func downloadBackupForDropbox(_ backup: BackupModel) { + dropboxService.downloadBackup(backup.id) { [weak self] in + guard let self = self else { return } + self.stepRelay.send(.downloading(backup.size, backup.size)) + + switch $0 { + case .success(let data): + self.continueRestoring(data: data) + case .failure(let error): + self.stepRelay.send(.failDownload(error)) + } } - - func didTapRestore(passphrase: String) { - self.passphrase = passphrase - - guard let backup = settings.backup else { fatalError() } - - stepRelay.send(.downloading(0.0, backup.size)) - - switch settings.cloudService { - case .drive: - downloadBackupForDrive(backup) - case .dropbox: - downloadBackupForDropbox(backup) - case .icloud: - downloadBackupForiCloud(backup) - case .sftp: - downloadBackupForSFTP(backup) - } - } - - private func downloadBackupForSFTP(_ backup: BackupModel) { - sftpService.downloadBackup(path: backup.id) { [weak self] in - guard let self = self else { return } - self.stepRelay.send(.downloading(backup.size, backup.size)) - - switch $0 { - case .success(let data): - self.continueRestoring(data: data) - case .failure(let error): - self.stepRelay.send(.failDownload(error)) - } - } + } + + private func downloadBackupForiCloud(_ backup: BackupModel) { + iCloudService.downloadBackup(backup.id) { [weak self] in + guard let self = self else { return } + self.stepRelay.send(.downloading(backup.size, backup.size)) + + switch $0 { + case .success(let data): + self.continueRestoring(data: data) + case .failure(let error): + self.stepRelay.send(.failDownload(error)) + } } - - private func downloadBackupForDropbox(_ backup: BackupModel) { - dropboxService.downloadBackup(backup.id) { [weak self] in - guard let self = self else { return } - self.stepRelay.send(.downloading(backup.size, backup.size)) - - switch $0 { - case .success(let data): - self.continueRestoring(data: data) - case .failure(let error): - self.stepRelay.send(.failDownload(error)) - } - } + } + + private func downloadBackupForDrive(_ backup: BackupModel) { + googleService.downloadBackup(backup.id) { [weak self] in + if let stepRelay = self?.stepRelay { + stepRelay.send(.downloading($0, backup.size)) + } + } _: { [weak self] in + guard let self = self else { return } + + switch $0 { + case .success(let data): + self.continueRestoring(data: data) + case .failure(let error): + self.stepRelay.send(.failDownload(error)) + } } - - private func downloadBackupForiCloud(_ backup: BackupModel) { - iCloudService.downloadBackup(backup.id) { [weak self] in - guard let self = self else { return } - self.stepRelay.send(.downloading(backup.size, backup.size)) - - switch $0 { - case .success(let data): - self.continueRestoring(data: data) - case .failure(let error): - self.stepRelay.send(.failDownload(error)) - } + } + + private func continueRestoring(data: Data) { + stepRelay.send(.parsingData) + + DispatchQueue.global().async { [weak self] in + guard let self = self else { return } + + do { + print(">>> Calling messenger destroy") + try self.messenger.destroy() + + print(">>> Calling restore backup") + let result = try self.messenger.restoreBackup( + backupData: data, + backupPassphrase: self.passphrase + ) + + self.username = result.restoredParams.username + let facts = try self.messenger.ud.tryGet().getFacts() + self.email = facts.get(.email)?.value + self.phone = facts.get(.phone)?.value + + print(">>> Calling wait for network") + try self.messenger.waitForNetwork() + + print(">>> Calling waitForNodes") + try self.messenger.waitForNodes( + targetRatio: 0.5, + sleepInterval: 3, + retries: 15, + onProgress: { print(">>> \($0)") } + ) + + print(">>> Calling multilookup") + let multilookup = try self.messenger.lookupContacts(ids: result.restoredContacts) + + multilookup.contacts.forEach { + print(">>> Found \(try! $0.getFact(.username)?.value)") + + try! self.database.saveContact(.init( + id: try $0.getId(), + marshaled: $0.data, + username: try? $0.getFact(.username)?.value, + email: nil, + phone: nil, + nickname: try? $0.getFact(.username)?.value, + photo: nil, + authStatus: .friend, + isRecent: false, + isBlocked: false, + isBanned: false, + createdAt: Date() + )) } - } - private func downloadBackupForDrive(_ backup: BackupModel) { - googleService.downloadBackup(backup.id) { [weak self] in - if let stepRelay = self?.stepRelay { - stepRelay.send(.downloading($0, backup.size)) - } - } _: { [weak self] in - guard let self = self else { return } - - switch $0 { - case .success(let data): - self.continueRestoring(data: data) - case .failure(let error): - self.stepRelay.send(.failDownload(error)) - } + multilookup.errors.forEach { + print(">>> Error: \($0.localizedDescription)") } - } - - private func continueRestoring(data: Data) { - stepRelay.send(.parsingData) - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - - do { - let result = try self.messenger.restoreBackup( - backupData: data, - backupPassphrase: self.passphrase - ) - - print(">>> Finished restoring on bindings") - - self.username = result.restoredParams.username - self.email = result.restoredParams.email - self.phone = result.restoredParams.phone - - //let restoreContacts = result.restoredContacts - - self.stepRelay.send(.done) - } catch { - print(">>> Error on restoration: \(error.localizedDescription)") - self.pendingData = data - self.stepRelay.send(.wrongPass) - } - } + self.stepRelay.send(.done) + } catch { + print(">>> Error on restoration: \(error.localizedDescription)") + self.pendingData = data + self.stepRelay.send(.wrongPass) + } } + } } diff --git a/Sources/RestoreFeature/Views/RestorePassphraseView.swift b/Sources/RestoreFeature/Views/RestorePassphraseView.swift index d54fbd4a7577fe1f13d1813cf2047f3956eb8832..6dfaa4d716ae2a354ab9afa4a0d9563df97cc3f8 100644 --- a/Sources/RestoreFeature/Views/RestorePassphraseView.swift +++ b/Sources/RestoreFeature/Views/RestorePassphraseView.swift @@ -3,65 +3,78 @@ import Shared import InputField final class RestorePassphraseView: UIView { - let titleLabel = UILabel() - let subtitleLabel = UILabel() - let inputField = InputField() - let stackView = UIStackView() - let continueButton = CapsuleButton() - let cancelButton = CapsuleButton() + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let inputField = InputField() + let stackView = UIStackView() + let continueButton = CapsuleButton() + let cancelButton = CapsuleButton() - init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } + init() { + super.init(frame: .zero) + layer.cornerRadius = 40 + backgroundColor = Asset.neutralWhite.color + layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - private func setup() { - layer.cornerRadius = 40 - backgroundColor = Asset.neutralWhite.color - layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + setupInput() + setupLabels() + setupButtons() + setupStackView() + } - subtitleLabel.numberOfLines = 0 - titleLabel.textColor = Asset.neutralActive.color - subtitleLabel.textColor = Asset.neutralActive.color + required init?(coder: NSCoder) { nil } - inputField.setup( - style: .regular, - title: "Passphrase", - placeholder: "* * * * * *", - subtitleColor: Asset.neutralDisabled.color - ) + private func setupInput() { + inputField.setup( + style: .regular, + title: Localized.Backup.Passphrase.Input.title, + placeholder: Localized.Backup.Passphrase.Input.placeholder, + rightView: .toggleSecureEntry, + subtitleColor: Asset.neutralDisabled.color, + allowsEmptySpace: false, + autocapitalization: .none, + contentType: .password + ) + } - titleLabel.text = "Backup password" - titleLabel.textAlignment = .left - titleLabel.font = Fonts.Mulish.bold.font(size: 26.0) + private func setupLabels() { + titleLabel.textAlignment = .left + titleLabel.textColor = Asset.neutralActive.color + titleLabel.font = Fonts.Mulish.bold.font(size: 26.0) + titleLabel.text = Localized.Backup.Restore.Passphrase.title - subtitleLabel.text = "Please enter your backup password that you used when you did the backup setup" - subtitleLabel.textAlignment = .left - subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) + subtitleLabel.numberOfLines = 0 + subtitleLabel.textAlignment = .left + subtitleLabel.textColor = Asset.neutralActive.color + subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) + subtitleLabel.text = Localized.Backup.Restore.Passphrase.subtitle + } - continueButton.setStyle(.brandColored) - continueButton.setTitle("Continue", for: .normal) + private func setupButtons() { + cancelButton.setStyle(.seeThrough) + cancelButton.setTitle(Localized.Backup.Passphrase.cancel, for: .normal) - cancelButton.setStyle(.seeThrough) - cancelButton.setTitle("Cancel", for: .normal) + continueButton.isEnabled = false + continueButton.setStyle(.brandColored) + continueButton.setTitle(Localized.Backup.Passphrase.continue, for: .normal) + } - stackView.spacing = 20 - stackView.axis = .vertical - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(subtitleLabel) - stackView.addArrangedSubview(inputField) - stackView.addArrangedSubview(continueButton) - stackView.addArrangedSubview(cancelButton) + private func setupStackView() { + stackView.spacing = 20 + stackView.axis = .vertical + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(subtitleLabel) + stackView.addArrangedSubview(inputField) + stackView.addArrangedSubview(continueButton) + stackView.addArrangedSubview(cancelButton) - addSubview(stackView) + addSubview(stackView) - stackView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(60) - make.left.equalToSuperview().offset(50) - make.right.equalToSuperview().offset(-50) - make.bottom.equalToSuperview().offset(-70) - } + stackView.snp.makeConstraints { + $0.top.equalToSuperview().offset(60) + $0.left.equalToSuperview().offset(50) + $0.right.equalToSuperview().offset(-50) + $0.bottom.equalToSuperview().offset(-70) } + } } diff --git a/Sources/Shared/AutoGenerated/Strings.swift b/Sources/Shared/AutoGenerated/Strings.swift index 207d794348897f39bcbbf7f0d0278ad933a8ca75..4ced950dd511ef22ad7f25f4ddb1c1192587ed7e 100644 --- a/Sources/Shared/AutoGenerated/Strings.swift +++ b/Sources/Shared/AutoGenerated/Strings.swift @@ -290,6 +290,14 @@ public enum Localized { public static let title = Localized.tr("Localizable", "backup.passphrase.input.title") } } + public enum Restore { + public enum Passphrase { + /// Please enter your backup password that you used when you did the backup setup + public static let subtitle = Localized.tr("Localizable", "backup.restore.passphrase.subtitle") + /// Backup password + public static let title = Localized.tr("Localizable", "backup.restore.passphrase.title") + } + } public enum Setup { /// Setup your #backup service#. public static let title = Localized.tr("Localizable", "backup.setup.title") diff --git a/Sources/Shared/Resources/en.lproj/Localizable.strings b/Sources/Shared/Resources/en.lproj/Localizable.strings index 0f0ebae69110ac84168b49e9579bc87d02073dc5..3dfbb4a622ecde1973cc79fb0e692d78add2cefa 100644 --- a/Sources/Shared/Resources/en.lproj/Localizable.strings +++ b/Sources/Shared/Resources/en.lproj/Localizable.strings @@ -668,6 +668,10 @@ = "Set password and continue"; "backup.passphrase.cancel" = "Cancel"; +"backup.restore.passphrase.title" += "Backup password"; +"backup.restore.passphrase.subtitle" += "Please enter your backup password that you used when you did the backup setup"; "backup.iCloud" = "iCloud"; diff --git a/Sources/iCloudFeature/iCloudService.swift b/Sources/iCloudFeature/iCloudService.swift index 0c766b9f963972246982d5e396aa476a0467cb20..9c804211e759b7901af2f2917e8d071d9de2ffa2 100644 --- a/Sources/iCloudFeature/iCloudService.swift +++ b/Sources/iCloudFeature/iCloudService.swift @@ -18,8 +18,7 @@ public struct iCloudService: iCloudInterface { public func downloadMetadata(_ completion: @escaping (Result<iCloudMetadata?, Error>) -> Void) { guard let documentsProvider = documentsProvider else { - // TODO: Use some generic error - fatalError() + fatalError("ICloud wasn't set properly, force crashed due to lack of fallback") } documentsProvider.contentsOfDirectory(path: "/", completionHandler: { contents, error in diff --git a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8e2d54ed77fa92338542461e10b1aeb094c77987..b1639bc2fd1a68e6e0ac6a9ed6ba62404e56b029 100644 --- a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -99,15 +99,6 @@ "version" : "1.3.0" } }, - { - "identity" : "elixxir-dapps-sdk-swift", - "kind" : "remoteSourceControl", - "location" : "https://git.xx.network/elixxir/elixxir-dapps-sdk-swift", - "state" : { - "branch" : "development", - "revision" : "d011281416542b38e302f3ac59b6c658d6438caa" - } - }, { "identity" : "fileprovider", "kind" : "remoteSourceControl",