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