diff --git a/Examples/xx-messenger/Sources/GroupFeature/GroupComponent.swift b/Examples/xx-messenger/Sources/GroupFeature/GroupComponent.swift index 122fe918d1a2178ae726f0dd9a703bd2f1fd931c..d9b7d23306b64b6924d5ce7ff3351728667476a3 100644 --- a/Examples/xx-messenger/Sources/GroupFeature/GroupComponent.swift +++ b/Examples/xx-messenger/Sources/GroupFeature/GroupComponent.swift @@ -1,29 +1,40 @@ import AppCore import ComposableArchitecture import Foundation +import XXMessengerClient import XXModels public struct GroupComponent: ReducerProtocol { public struct State: Equatable { public init( groupId: XXModels.Group.ID, - groupInfo: XXModels.GroupInfo? = nil + groupInfo: XXModels.GroupInfo? = nil, + isJoining: Bool = false, + joinFailure: String? = nil ) { self.groupId = groupId self.groupInfo = groupInfo + self.isJoining = isJoining + self.joinFailure = joinFailure } public var groupId: XXModels.Group.ID public var groupInfo: XXModels.GroupInfo? + public var isJoining: Bool + public var joinFailure: String? } public enum Action: Equatable { case start case didFetchGroupInfo(XXModels.GroupInfo?) + case joinButtonTapped + case didJoin + case didFailToJoin(String) } public init() {} + @Dependency(\.app.messenger) var messenger: Messenger @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> @@ -47,6 +58,36 @@ public struct GroupComponent: ReducerProtocol { case .didFetchGroupInfo(let groupInfo): state.groupInfo = groupInfo return .none + + case .joinButtonTapped: + guard let info = state.groupInfo else { return .none } + state.isJoining = true + state.joinFailure = nil + return Effect.result { + do { + let groupChat = try messenger.groupChat.tryGet() + try groupChat.joinGroup(serializedGroupData: info.group.serialized) + var group = info.group + group.authStatus = .participating + try db().saveGroup(group) + return .success(.didJoin) + } catch { + return .success(.didFailToJoin(error.localizedDescription)) + } + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .didJoin: + state.isJoining = false + state.joinFailure = nil + return .none + + case .didFailToJoin(let failure): + state.isJoining = false + state.joinFailure = failure + return .none } } } diff --git a/Examples/xx-messenger/Sources/GroupFeature/GroupView.swift b/Examples/xx-messenger/Sources/GroupFeature/GroupView.swift index 2cd55078a2afada5489e144c75668e996398796d..67b880f4b1ad3f8e790f51f1202624f195d0d60a 100644 --- a/Examples/xx-messenger/Sources/GroupFeature/GroupView.swift +++ b/Examples/xx-messenger/Sources/GroupFeature/GroupView.swift @@ -16,9 +16,13 @@ public struct GroupView: View { struct ViewState: Equatable { init(state: Component.State) { info = state.groupInfo + isJoining = state.isJoining + joinFailure = state.joinFailure } var info: XXModels.GroupInfo? + var isJoining: Bool + var joinFailure: String? } public var body: some View { @@ -41,6 +45,27 @@ public struct GroupView: View { Section("Status") { GroupAuthStatusView(info.group.authStatus) + + if case .pending = info.group.authStatus { + Button { + viewStore.send(.joinButtonTapped) + } label: { + HStack { + Text("Join") + Spacer() + if viewStore.isJoining { + ProgressView() + } else { + Image(systemName: "play.fill") + } + } + } + .disabled(viewStore.isJoining) + } + + if let failure = viewStore.joinFailure { + Text(failure) + } } } } diff --git a/Examples/xx-messenger/Tests/GroupFeatureTests/GroupComponentTests.swift b/Examples/xx-messenger/Tests/GroupFeatureTests/GroupComponentTests.swift index 3522ab7cae63566db7062ea1df601418ddc1552e..3c7193fc4ebea418f97800fae37e782d982d497f 100644 --- a/Examples/xx-messenger/Tests/GroupFeatureTests/GroupComponentTests.swift +++ b/Examples/xx-messenger/Tests/GroupFeatureTests/GroupComponentTests.swift @@ -2,12 +2,16 @@ import Combine import ComposableArchitecture import CustomDump import XCTest +import XXClient +import XXMessengerClient import XXModels @testable import GroupFeature final class GroupComponentTests: XCTestCase { enum Action: Equatable { case didFetchGroupInfos(GroupInfo.Query) + case didJoinGroup(Data) + case didSaveGroup(XXModels.Group) } var actions: [Action]! @@ -57,6 +61,85 @@ final class GroupComponentTests: XCTestCase { groupInfosSubject.send(completion: .finished) } + + func testJoinGroup() { + var groupInfo = GroupInfo.stub() + groupInfo.group.authStatus = .pending + + let store = TestStore( + initialState: GroupComponent.State( + groupId: groupInfo.group.id, + groupInfo: groupInfo + ), + reducer: GroupComponent() + ) + + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.groupChat.get = { + var groupChat: GroupChat = .unimplemented + groupChat.joinGroup.run = { serializedGroupData in + self.actions.append(.didJoinGroup(serializedGroupData)) + } + return groupChat + } + store.dependencies.app.dbManager.getDB.run = { + var db: Database = .unimplemented + db.saveGroup.run = { group in + self.actions.append(.didSaveGroup(group)) + return group + } + return db + } + + store.send(.joinButtonTapped) { + $0.isJoining = true + } + + XCTAssertNoDifference(actions, [ + .didJoinGroup(groupInfo.group.serialized), + .didSaveGroup({ + var group = groupInfo.group + group.authStatus = .participating + return group + }()) + ]) + + store.receive(.didJoin) { + $0.isJoining = false + } + } + + func testJoinGroupFailure() { + let groupInfo = GroupInfo.stub() + struct Failure: Error {} + let failure = Failure() + + let store = TestStore( + initialState: GroupComponent.State( + groupId: groupInfo.group.id, + groupInfo: groupInfo + ), + reducer: GroupComponent() + ) + + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.groupChat.get = { + var groupChat: GroupChat = .unimplemented + groupChat.joinGroup.run = { _ in throw failure } + return groupChat + } + + store.send(.joinButtonTapped) { + $0.isJoining = true + } + + store.receive(.didFailToJoin(failure.localizedDescription)) { + $0.isJoining = false + $0.joinFailure = failure.localizedDescription + } + } } private extension XXModels.GroupInfo {