Select Git revision
version_vars.go
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() }
)