From 8def5f9dd062667740494073962a119cfc60e14e Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 1 Dec 2022 12:40:38 +0100 Subject: [PATCH] Implement creating new group --- .../NewGroupFeature/NewGroupComponent.swift | 69 +++++++- .../NewGroupFeature/NewGroupView.swift | 41 +++++ .../NewGroupComponentTests.swift | 163 ++++++++++++++++++ 3 files changed, 272 insertions(+), 1 deletion(-) diff --git a/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupComponent.swift b/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupComponent.swift index c3ceb282..fd5e36c6 100644 --- a/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupComponent.swift +++ b/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupComponent.swift @@ -8,31 +8,43 @@ public struct NewGroupComponent: ReducerProtocol { public struct State: Equatable { public enum Field: String, Hashable { case name + case message } public init( contacts: IdentifiedArrayOf<XXModels.Contact> = [], members: IdentifiedArrayOf<XXModels.Contact> = [], name: String = "", - focusedField: Field? = nil + message: String = "", + focusedField: Field? = nil, + isCreating: Bool = false, + failure: String? = nil ) { self.contacts = contacts self.members = members self.name = name + self.message = message self.focusedField = focusedField + self.isCreating = isCreating + self.failure = failure } public var contacts: IdentifiedArrayOf<XXModels.Contact> public var members: IdentifiedArrayOf<XXModels.Contact> @BindableState public var name: String + @BindableState public var message: String @BindableState public var focusedField: Field? + public var isCreating: Bool + public var failure: String? } public enum Action: Equatable, BindableAction { case start case didFetchContacts([XXModels.Contact]) case didSelectContact(XXModels.Contact) + case createButtonTapped case didFinish + case didFail(String) case binding(BindingAction<State>) } @@ -42,6 +54,7 @@ public struct NewGroupComponent: ReducerProtocol { @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.date) var date: DateGenerator public var body: some ReducerProtocol<State, Action> { BindingReducer() @@ -71,7 +84,61 @@ public struct NewGroupComponent: ReducerProtocol { } return .none + case .createButtonTapped: + state.focusedField = nil + state.isCreating = true + state.failure = nil + return Effect.result { [state] in + do { + let groupChat = try messenger.groupChat.tryGet() + let report = try groupChat.makeGroup( + membership: state.members.map(\.id), + message: state.message.data(using: .utf8)!, + name: state.name.data(using: .utf8)! + ) + let myContactId = try messenger.e2e.tryGet().getContact().getId() + let group = XXModels.Group( + id: report.id, + name: state.name, + leaderId: myContactId, + createdAt: date(), + authStatus: .participating, + serialized: try report.encode() + ) + try db().saveGroup(group) + if state.message.isEmpty == false { + try db().saveMessage(.init( + senderId: myContactId, + recipientId: nil, + groupId: group.id, + date: group.createdAt, + status: .sent, + isUnread: false, + text: state.message + )) + } + try state.members.map { + GroupMember(groupId: group.id, contactId: $0.id) + }.forEach { + try db().saveGroupMember($0) + } + return .success(.didFinish) + } catch { + return .success(.didFail(error.localizedDescription)) + } + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + case .didFinish: + state.isCreating = false + state.failure = nil + return .none + + case .didFail(let failure): + state.isCreating = false + state.failure = failure return .none case .binding(_): diff --git a/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupView.swift b/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupView.swift index 697fdd17..2536dc12 100644 --- a/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupView.swift +++ b/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupView.swift @@ -19,13 +19,19 @@ public struct NewGroupView: View { contacts = state.contacts members = state.members name = state.name + message = state.message focusedField = state.focusedField + isCreating = state.isCreating + failure = state.failure } var contacts: IdentifiedArrayOf<XXModels.Contact> var members: IdentifiedArrayOf<XXModels.Contact> var name: String + var message: String var focusedField: Component.State.Field? + var isCreating: Bool + var failure: String? } public var body: some View { @@ -34,6 +40,13 @@ public struct NewGroupView: View { Section { membersView(viewStore) nameView(viewStore) + messageView(viewStore) + } + Section { + createButton(viewStore) + if let failure = viewStore.failure { + Text(failure) + } } } .navigationTitle("New Group") @@ -61,6 +74,7 @@ public struct NewGroupView: View { } } } + .disabled(viewStore.isCreating) } func nameView(_ viewStore: ViewStore) -> some View { @@ -69,6 +83,33 @@ public struct NewGroupView: View { send: { .set(\.$name, $0) } )) .focused($focusedField, equals: .name) + .disabled(viewStore.isCreating) + } + + func messageView(_ viewStore: ViewStore) -> some View { + TextField("Initial message", text: viewStore.binding( + get: \.message, + send: { .set(\.$message, $0) } + )) + .focused($focusedField, equals: .message) + .disabled(viewStore.isCreating) + } + + func createButton(_ viewStore: ViewStore) -> some View { + Button { + viewStore.send(.createButtonTapped) + } label: { + HStack { + Text("Create group") + Spacer() + if viewStore.isCreating { + ProgressView() + } else { + Image(systemName: "play.fill") + } + } + } + .disabled(viewStore.isCreating) } } diff --git a/Examples/xx-messenger/Tests/NewGroupFeatureTests/NewGroupComponentTests.swift b/Examples/xx-messenger/Tests/NewGroupFeatureTests/NewGroupComponentTests.swift index d8d742ba..3207ed8c 100644 --- a/Examples/xx-messenger/Tests/NewGroupFeatureTests/NewGroupComponentTests.swift +++ b/Examples/xx-messenger/Tests/NewGroupFeatureTests/NewGroupComponentTests.swift @@ -10,6 +10,10 @@ import XXModels final class NewGroupComponentTests: XCTestCase { enum Action: Equatable { case didFetchContacts(XXModels.Contact.Query) + case didMakeGroup(membership: [Data], message: Data?, name: Data?) + case didSaveGroup(XXModels.Group) + case didSaveMessage(XXModels.Message) + case didSaveGroupMember(XXModels.GroupMember) } var actions: [Action]! @@ -116,6 +120,165 @@ final class NewGroupComponentTests: XCTestCase { } } + func testEnterInitialMessage() { + let store = TestStore( + initialState: NewGroupComponent.State(), + reducer: NewGroupComponent() + ) + + store.send(.binding(.set(\.$focusedField, .message))) { + $0.focusedField = .message + } + + store.send(.binding(.set(\.$message, "Welcome message"))) { + $0.message = "Welcome message" + } + + store.send(.binding(.set(\.$focusedField, nil))) { + $0.focusedField = nil + } + } + + func testCreateGroup() { + let members: [XXModels.Contact] = [ + .init(id: "member-contact-1".data(using: .utf8)!), + .init(id: "member-contact-2".data(using: .utf8)!), + .init(id: "member-contact-3".data(using: .utf8)!), + ] + let name = "New group" + let message = "Welcome message" + let groupReport = GroupReport( + id: "new-group-id".data(using: .utf8)!, + rounds: [], + roundURL: "", + status: 0 + ) + let myContactId = "my-contact-id".data(using: .utf8)! + let currentDate = Date(timeIntervalSince1970: 123) + + let store = TestStore( + initialState: NewGroupComponent.State( + members: IdentifiedArray(uniqueElements: members), + name: name, + message: message + ), + reducer: NewGroupComponent() + ) + + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.groupChat.get = { + var groupChat: GroupChat = .unimplemented + groupChat.makeGroup.run = { membership, message, name in + self.actions.append(.didMakeGroup( + membership: membership, + message: message, + name: name + )) + return groupReport + } + return groupChat + } + store.dependencies.app.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { + var contact = XXClient.Contact.unimplemented("my-contact-data".data(using: .utf8)!) + contact.getIdFromContact.run = { _ in myContactId } + return contact + } + return e2e + } + store.dependencies.date = .constant(currentDate) + store.dependencies.app.dbManager.getDB.run = { + var db: Database = .unimplemented + db.saveGroup.run = { group in + self.actions.append(.didSaveGroup(group)) + return group + } + db.saveMessage.run = { message in + self.actions.append(.didSaveMessage(message)) + return message + } + db.saveGroupMember.run = { groupMember in + self.actions.append(.didSaveGroupMember(groupMember)) + return groupMember + } + return db + } + + store.send(.createButtonTapped) { + $0.isCreating = true + } + + XCTAssertNoDifference(actions, [ + .didMakeGroup( + membership: members.map(\.id), + message: message.data(using: .utf8)!, + name: name.data(using: .utf8)! + ), + .didSaveGroup(.init( + id: groupReport.id, + name: name, + leaderId: myContactId, + createdAt: currentDate, + authStatus: .participating, + serialized: try! groupReport.encode() + )), + .didSaveMessage(.init( + senderId: myContactId, + recipientId: nil, + groupId: groupReport.id, + date: currentDate, + status: .sent, + isUnread: false, + text: message + )), + .didSaveGroupMember(.init( + groupId: groupReport.id, + contactId: members[0].id + )), + .didSaveGroupMember(.init( + groupId: groupReport.id, + contactId: members[1].id + )), + .didSaveGroupMember(.init( + groupId: groupReport.id, + contactId: members[2].id + )), + ]) + + store.receive(.didFinish) { + $0.isCreating = false + } + } + + func testCreateGroupFailure() { + struct Failure: Error, Equatable {} + let failure = Failure() + + let store = TestStore( + initialState: NewGroupComponent.State(), + reducer: NewGroupComponent() + ) + + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.groupChat.get = { + var groupChat: GroupChat = .unimplemented + groupChat.makeGroup.run = { _, _, _ in throw failure } + return groupChat + } + + store.send(.createButtonTapped) { + $0.isCreating = true + } + + store.receive(.didFail(failure.localizedDescription)) { + $0.isCreating = false + $0.failure = failure.localizedDescription + } + } + func testFinish() { let store = TestStore( initialState: NewGroupComponent.State(), -- GitLab