diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift index e054e6ac8bf07bd9ade9d0837d1c6e0408362348..0a019179b2d45f23a41e54ecb084ac7ec835fe2d 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift @@ -11,6 +11,7 @@ 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 @@ -24,6 +25,7 @@ public struct HomeState: Equatable { 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? @@ -41,6 +43,7 @@ public enum HomeAction: Equatable { case start case stop case health(Bool) + case nodes(NodeRegistrationReport) } public enum DeleteAccount: Equatable { @@ -93,6 +96,7 @@ extension HomeEnvironment { public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> { state, action, env in enum NetworkHealthEffectId {} + enum NetworkNodesEffectId {} switch action { case .messenger(.start): @@ -135,26 +139,44 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> return .none case .networkMonitor(.start): - return .run { subscriber in - let callback = HealthCallback { isHealthy in - subscriber.send(.networkMonitor(.health(isHealthy))) + 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() } } - let cancellable = env.messenger.cMix()?.addHealthCallback(callback) - return AnyCancellable { cancellable?.cancel() } - } - .cancellable(id: NetworkHealthEffectId.self, cancelInFlight: true) + .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 - return .cancel(id: NetworkHealthEffectId.self) + 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 diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift index d105e0bd6c400c18217d17cb6b791d1ab391359f..ce8aac42ab93d9578770f7cb606b7090a6fb6082 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift @@ -2,6 +2,7 @@ import ComposableArchitecture import ComposablePresentation import RegisterFeature import SwiftUI +import XXClient public struct HomeView: View { public init(store: Store<HomeState, HomeAction>) { @@ -13,12 +14,14 @@ public struct HomeView: View { struct ViewState: Equatable { var failure: String? var isNetworkHealthy: Bool? + var networkNodesReport: NodeRegistrationReport? var isDeletingAccount: Bool init(state: HomeState) { failure = state.failure isNetworkHealthy = state.isNetworkHealthy isDeletingAccount = state.isDeletingAccount + networkNodesReport = state.networkNodesReport } } @@ -26,15 +29,17 @@ public struct HomeView: View { WithViewStore(store.scope(state: ViewState.init)) { viewStore in NavigationView { Form { - if let failure = viewStore.failure { - Section { + Section { + if let failure = viewStore.failure { Text(failure) Button { viewStore.send(.messenger(.start)) } label: { Text("Retry") } - } header: { + } + } header: { + if viewStore.failure != nil { Text("Error") } } @@ -57,6 +62,26 @@ public struct HomeView: View { .foregroundColor(.gray) } } + + ProgressView( + value: viewStore.networkNodesReport?.ratio ?? 0, + label: { + Text("Node registration") + }, + currentValueLabel: { + if let report = viewStore.networkNodesReport { + HStack { + Text("\(Int((report.ratio * 100).rounded(.down)))%") + Spacer() + Text("\(report.registered) / \(report.total)") + } + } else { + Text("Unknown") + } + } + ) + .tint((viewStore.networkNodesReport?.ratio ?? 0) >= 0.8 ? .green : .orange) + .animation(.default, value: viewStore.networkNodesReport?.ratio) } header: { Text("Network") } diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift index 97ac8900fff0624421ed26fee9232fce36f939b9..c7168d5b4537564a69ac82145b433c6ab22c65ff 100644 --- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift @@ -58,6 +58,10 @@ final class HomeFeatureTests: XCTestCase { store.environment.messenger.cMix.get = { var cMix: CMix = .unimplemented cMix.addHealthCallback.run = { _ in Cancellable {} } + cMix.getNodeRegistrationStatus.run = { + struct Unimplemented: Error {} + throw Unimplemented() + } return cMix } @@ -96,6 +100,10 @@ final class HomeFeatureTests: XCTestCase { store.environment.messenger.cMix.get = { var cMix: CMix = .unimplemented cMix.addHealthCallback.run = { _ in Cancellable {} } + cMix.getNodeRegistrationStatus.run = { + struct Unimplemented: Error {} + throw Unimplemented() + } return cMix } @@ -219,41 +227,82 @@ final class HomeFeatureTests: XCTestCase { environment: .unimplemented ) + let bgQueue = DispatchQueue.test + let mainQueue = DispatchQueue.test + var cMixDidAddHealthCallback: [HealthCallback] = [] var healthCallbackDidCancel = 0 - - store.environment.bgQueue = .immediate - store.environment.mainQueue = .immediate + var nodeRegistrationStatusIndex = 0 + let nodeRegistrationStatus: [NodeRegistrationReport] = [ + .init(registered: 0, total: 10), + .init(registered: 1, total: 11), + .init(registered: 2, total: 12), + ] + + store.environment.bgQueue = bgQueue.eraseToAnyScheduler() + store.environment.mainQueue = mainQueue.eraseToAnyScheduler() store.environment.messenger.cMix.get = { var cMix: CMix = .unimplemented cMix.addHealthCallback.run = { callback in cMixDidAddHealthCallback.append(callback) return Cancellable { healthCallbackDidCancel += 1 } } + cMix.getNodeRegistrationStatus.run = { + defer { nodeRegistrationStatusIndex += 1 } + return nodeRegistrationStatus[nodeRegistrationStatusIndex] + } return cMix } store.send(.networkMonitor(.start)) + bgQueue.advance() + XCTAssertNoDifference(cMixDidAddHealthCallback.count, 1) cMixDidAddHealthCallback.first?.handle(true) + mainQueue.advance() store.receive(.networkMonitor(.health(true))) { $0.isNetworkHealthy = true } cMixDidAddHealthCallback.first?.handle(false) + mainQueue.advance() store.receive(.networkMonitor(.health(false))) { $0.isNetworkHealthy = false } + bgQueue.advance(by: 2) + mainQueue.advance() + + store.receive(.networkMonitor(.nodes(nodeRegistrationStatus[0]))) { + $0.networkNodesReport = nodeRegistrationStatus[0] + } + + bgQueue.advance(by: 2) + mainQueue.advance() + + store.receive(.networkMonitor(.nodes(nodeRegistrationStatus[1]))) { + $0.networkNodesReport = nodeRegistrationStatus[1] + } + + bgQueue.advance(by: 2) + mainQueue.advance() + + store.receive(.networkMonitor(.nodes(nodeRegistrationStatus[2]))) { + $0.networkNodesReport = nodeRegistrationStatus[2] + } + store.send(.networkMonitor(.stop)) { $0.isNetworkHealthy = nil + $0.networkNodesReport = nil } XCTAssertNoDifference(healthCallbackDidCancel, 1) + + mainQueue.advance() } func testAccountDeletion() {