import AppCore import BackupFeature import Combine import ComposableArchitecture import ComposablePresentation import ContactsFeature import Foundation import RegisterFeature import UserSearchFeature import XCTestDynamicOverlay import XXClient import XXMessengerClient import XXModels public struct HomeState: Equatable { public init( failure: String? = nil, isNetworkHealthy: Bool? = nil, networkNodesReport: NodeRegistrationReport? = nil, isDeletingAccount: Bool = false, alert: AlertState<HomeAction>? = nil, register: RegisterState? = nil, contacts: ContactsState? = nil, userSearch: UserSearchState? = nil, backup: BackupState? = nil ) { self.failure = failure self.isNetworkHealthy = isNetworkHealthy self.isDeletingAccount = isDeletingAccount self.alert = alert self.register = register self.contacts = contacts self.userSearch = userSearch self.backup = backup } public var failure: String? public var isNetworkHealthy: Bool? public var networkNodesReport: NodeRegistrationReport? public var isDeletingAccount: Bool public var alert: AlertState<HomeAction>? public var register: RegisterState? public var contacts: ContactsState? public var userSearch: UserSearchState? public var backup: BackupState? } public enum HomeAction: Equatable { public enum Messenger: Equatable { case start case didStartRegistered case didStartUnregistered case failure(NSError) } public enum NetworkMonitor: Equatable { case start case stop case health(Bool) case nodes(NodeRegistrationReport) } public enum DeleteAccount: Equatable { case buttonTapped case confirmed case success case failure(NSError) } case messenger(Messenger) case networkMonitor(NetworkMonitor) case deleteAccount(DeleteAccount) case didDismissAlert case didDismissRegister case userSearchButtonTapped case didDismissUserSearch case contactsButtonTapped case didDismissContacts case backupButtonTapped case didDismissBackup case register(RegisterAction) case contacts(ContactsAction) case userSearch(UserSearchAction) case backup(BackupAction) } public struct HomeEnvironment { public init( messenger: Messenger, dbManager: DBManager, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue>, register: @escaping () -> RegisterEnvironment, contacts: @escaping () -> ContactsEnvironment, userSearch: @escaping () -> UserSearchEnvironment, backup: @escaping () -> BackupEnvironment ) { self.messenger = messenger self.dbManager = dbManager self.mainQueue = mainQueue self.bgQueue = bgQueue self.register = register self.contacts = contacts self.userSearch = userSearch self.backup = backup } public var messenger: Messenger public var dbManager: DBManager public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> public var register: () -> RegisterEnvironment public var contacts: () -> ContactsEnvironment public var userSearch: () -> UserSearchEnvironment public var backup: () -> BackupEnvironment } #if DEBUG extension HomeEnvironment { public static let unimplemented = HomeEnvironment( messenger: .unimplemented, dbManager: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented, register: { .unimplemented }, contacts: { .unimplemented }, userSearch: { .unimplemented }, backup: { .unimplemented } ) } #endif public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> { state, action, env in enum NetworkHealthEffectId {} enum NetworkNodesEffectId {} switch action { case .messenger(.start): return .merge( Effect(value: .networkMonitor(.stop)), Effect.result { do { try env.messenger.start() if env.messenger.isConnected() == false { try env.messenger.connect() } if env.messenger.isListeningForMessages() == false { try env.messenger.listenForMessages() } if env.messenger.isFileTransferRunning() == false { try env.messenger.startFileTransfer() } if env.messenger.isLoggedIn() == false { if try env.messenger.isRegistered() == false { return .success(.messenger(.didStartUnregistered)) } try env.messenger.logIn() } if !env.messenger.isBackupRunning() { try? env.messenger.resumeBackup() } return .success(.messenger(.didStartRegistered)) } catch { return .success(.messenger(.failure(error as NSError))) } } ) .subscribe(on: env.bgQueue) .receive(on: env.mainQueue) .eraseToEffect() case .messenger(.didStartUnregistered): state.register = RegisterState() return .none case .messenger(.didStartRegistered): return Effect(value: .networkMonitor(.start)) case .messenger(.failure(let error)): state.failure = error.localizedDescription return .none case .networkMonitor(.start): return .merge( Effect.run { subscriber in let callback = HealthCallback { isHealthy in subscriber.send(.networkMonitor(.health(isHealthy))) } let cancellable = env.messenger.cMix()?.addHealthCallback(callback) return AnyCancellable { cancellable?.cancel() } } .cancellable(id: NetworkHealthEffectId.self, cancelInFlight: true), Effect.timer( id: NetworkNodesEffectId.self, every: .seconds(2), on: env.bgQueue ) .compactMap { _ in try? env.messenger.cMix()?.getNodeRegistrationStatus() } .map { HomeAction.networkMonitor(.nodes($0)) } .eraseToEffect() ) .subscribe(on: env.bgQueue) .receive(on: env.mainQueue) .eraseToEffect() case .networkMonitor(.stop): state.isNetworkHealthy = nil state.networkNodesReport = nil return .merge( .cancel(id: NetworkHealthEffectId.self), .cancel(id: NetworkNodesEffectId.self) ) case .networkMonitor(.health(let isHealthy)): state.isNetworkHealthy = isHealthy return .none case .networkMonitor(.nodes(let report)): state.networkNodesReport = report return .none case .deleteAccount(.buttonTapped): state.alert = .confirmAccountDeletion() return .none case .deleteAccount(.confirmed): state.isDeletingAccount = true return .result { do { let contactId = try env.messenger.e2e.tryGet().getContact().getId() let contact = try env.dbManager.getDB().fetchContacts(.init(id: [contactId])).first if let username = contact?.username { let ud = try env.messenger.ud.tryGet() try ud.permanentDeleteAccount(username: Fact(type: .username, value: username)) } try env.messenger.destroy() try env.dbManager.removeDB() return .success(.deleteAccount(.success)) } catch { return .success(.deleteAccount(.failure(error as NSError))) } } .subscribe(on: env.bgQueue) .receive(on: env.mainQueue) .eraseToEffect() case .deleteAccount(.success): state.isDeletingAccount = false return .none case .deleteAccount(.failure(let error)): state.isDeletingAccount = false state.alert = .accountDeletionFailed(error) return .none case .didDismissAlert: state.alert = nil return .none case .didDismissRegister: state.register = nil return .none case .userSearchButtonTapped: state.userSearch = UserSearchState() return .none case .didDismissUserSearch: state.userSearch = nil return .none case .contactsButtonTapped: state.contacts = ContactsState() return .none case .didDismissContacts: state.contacts = nil return .none case .register(.finished): state.register = nil return Effect(value: .messenger(.start)) case .backupButtonTapped: state.backup = BackupState() return .none case .didDismissBackup: state.backup = nil return .none case .register(_), .contacts(_), .userSearch(_), .backup(_): return .none } } .presenting( registerReducer, state: .keyPath(\.register), id: .notNil(), action: /HomeAction.register, environment: { $0.register() } ) .presenting( contactsReducer, state: .keyPath(\.contacts), id: .notNil(), action: /HomeAction.contacts, environment: { $0.contacts() } ) .presenting( userSearchReducer, state: .keyPath(\.userSearch), id: .notNil(), action: /HomeAction.userSearch, environment: { $0.userSearch() } ) .presenting( backupReducer, state: .keyPath(\.backup), id: .notNil(), action: /HomeAction.backup, environment: { $0.backup() } )