Select Git revision
ConnectionGetId.swift
HomeFeature.swift 10.16 KiB
import AppCore
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,
authFailure: String? = nil,
messageListenerFailure: String? = nil,
isNetworkHealthy: Bool? = nil,
networkNodesReport: NodeRegistrationReport? = nil,
isDeletingAccount: Bool = false,
alert: AlertState<HomeAction>? = nil,
register: RegisterState? = nil,
contacts: ContactsState? = nil,
userSearch: UserSearchState? = nil
) {
self.failure = failure
self.authFailure = authFailure
self.messageListenerFailure = messageListenerFailure
self.isNetworkHealthy = isNetworkHealthy
self.isDeletingAccount = isDeletingAccount
self.alert = alert
self.register = register
self.contacts = contacts
self.userSearch = userSearch
}
public var failure: String?
public var authFailure: String?
public var messageListenerFailure: 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 enum HomeAction: Equatable {
public enum Messenger: Equatable {
case start
case didStartRegistered
case didStartUnregistered
case failure(NSError)
}
public enum AuthHandler: Equatable {
case start
case stop
case failure(NSError)
case failureDismissed
}
public enum MessageListener: Equatable {
case start
case stop
case failure(NSError)
case failureDismissed
}
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 authHandler(AuthHandler)
case messageListener(MessageListener)
case networkMonitor(NetworkMonitor)
case deleteAccount(DeleteAccount)
case didDismissAlert
case didDismissRegister
case userSearchButtonTapped
case didDismissUserSearch
case contactsButtonTapped
case didDismissContacts
case register(RegisterAction)
case contacts(ContactsAction)
case userSearch(UserSearchAction)
}
public struct HomeEnvironment {
public init(
messenger: Messenger,
dbManager: DBManager,
authHandler: AuthCallbackHandler,
messageListener: MessageListenerHandler,
mainQueue: AnySchedulerOf<DispatchQueue>,
bgQueue: AnySchedulerOf<DispatchQueue>,
register: @escaping () -> RegisterEnvironment,
contacts: @escaping () -> ContactsEnvironment,
userSearch: @escaping () -> UserSearchEnvironment
) {
self.messenger = messenger
self.dbManager = dbManager
self.authHandler = authHandler
self.messageListener = messageListener
self.mainQueue = mainQueue
self.bgQueue = bgQueue
self.register = register
self.contacts = contacts
self.userSearch = userSearch
}
public var messenger: Messenger
public var dbManager: DBManager
public var authHandler: AuthCallbackHandler
public var messageListener: MessageListenerHandler
public var mainQueue: AnySchedulerOf<DispatchQueue>
public var bgQueue: AnySchedulerOf<DispatchQueue>
public var register: () -> RegisterEnvironment
public var contacts: () -> ContactsEnvironment
public var userSearch: () -> UserSearchEnvironment
}
extension HomeEnvironment {
public static let unimplemented = HomeEnvironment(
messenger: .unimplemented,
dbManager: .unimplemented,
authHandler: .unimplemented,
messageListener: .unimplemented,
mainQueue: .unimplemented,
bgQueue: .unimplemented,
register: { .unimplemented },
contacts: { .unimplemented },
userSearch: { .unimplemented }
)
}
public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment>
{ state, action, env in
enum NetworkHealthEffectId {}
enum NetworkNodesEffectId {}
enum AuthCallbacksEffectId {}
enum MessageListenerEffectId {}
switch action {
case .messenger(.start):
return .merge(
Effect(value: .authHandler(.start)),
Effect(value: .messageListener(.start)),
Effect(value: .networkMonitor(.stop)),
Effect.result {
do {
try env.messenger.start()
if env.messenger.isConnected() == false {
try env.messenger.connect()
try env.messenger.listenForMessages()
}
if env.messenger.isLoggedIn() == false {
if try env.messenger.isRegistered() == false {
return .success(.messenger(.didStartUnregistered))
}
try env.messenger.logIn()
}
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 .authHandler(.start):
return Effect.run { subscriber in
let cancellable = env.authHandler(onError: { error in
subscriber.send(.authHandler(.failure(error as NSError)))
})
return AnyCancellable { cancellable.cancel() }
}
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
.cancellable(id: AuthCallbacksEffectId.self, cancelInFlight: true)
case .authHandler(.stop):
return .cancel(id: AuthCallbacksEffectId.self)
case .authHandler(.failure(let error)):
state.authFailure = error.localizedDescription
return .none
case .authHandler(.failureDismissed):
state.authFailure = nil
return .none
case .messageListener(.start):
return Effect.run { subscriber in
let cancellable = env.messageListener(onError: { error in
subscriber.send(.messageListener(.failure(error as NSError)))
})
return AnyCancellable { cancellable.cancel() }
}
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
.cancellable(id: MessageListenerEffectId.self, cancelInFlight: true)
case .messageListener(.stop):
return .cancel(id: MessageListenerEffectId.self)
case .messageListener(.failure(let error)):
state.messageListenerFailure = error.localizedDescription
return .none
case .messageListener(.failureDismissed):
state.messageListenerFailure = nil
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 .register(_), .contacts(_), .userSearch(_):
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() }
)