Skip to content
Snippets Groups Projects
Commit 8def5f9d authored by Dariusz Rybicki's avatar Dariusz Rybicki
Browse files

Implement creating new group

parent 172ddcad
Branches
Tags
2 merge requests!153Release 1.1.0,!149[Messenger example] create new group
...@@ -8,31 +8,43 @@ public struct NewGroupComponent: ReducerProtocol { ...@@ -8,31 +8,43 @@ public struct NewGroupComponent: ReducerProtocol {
public struct State: Equatable { public struct State: Equatable {
public enum Field: String, Hashable { public enum Field: String, Hashable {
case name case name
case message
} }
public init( public init(
contacts: IdentifiedArrayOf<XXModels.Contact> = [], contacts: IdentifiedArrayOf<XXModels.Contact> = [],
members: IdentifiedArrayOf<XXModels.Contact> = [], members: IdentifiedArrayOf<XXModels.Contact> = [],
name: String = "", name: String = "",
focusedField: Field? = nil message: String = "",
focusedField: Field? = nil,
isCreating: Bool = false,
failure: String? = nil
) { ) {
self.contacts = contacts self.contacts = contacts
self.members = members self.members = members
self.name = name self.name = name
self.message = message
self.focusedField = focusedField self.focusedField = focusedField
self.isCreating = isCreating
self.failure = failure
} }
public var contacts: IdentifiedArrayOf<XXModels.Contact> public var contacts: IdentifiedArrayOf<XXModels.Contact>
public var members: IdentifiedArrayOf<XXModels.Contact> public var members: IdentifiedArrayOf<XXModels.Contact>
@BindableState public var name: String @BindableState public var name: String
@BindableState public var message: String
@BindableState public var focusedField: Field? @BindableState public var focusedField: Field?
public var isCreating: Bool
public var failure: String?
} }
public enum Action: Equatable, BindableAction { public enum Action: Equatable, BindableAction {
case start case start
case didFetchContacts([XXModels.Contact]) case didFetchContacts([XXModels.Contact])
case didSelectContact(XXModels.Contact) case didSelectContact(XXModels.Contact)
case createButtonTapped
case didFinish case didFinish
case didFail(String)
case binding(BindingAction<State>) case binding(BindingAction<State>)
} }
...@@ -42,6 +54,7 @@ public struct NewGroupComponent: ReducerProtocol { ...@@ -42,6 +54,7 @@ public struct NewGroupComponent: ReducerProtocol {
@Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB
@Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue>
@Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue>
@Dependency(\.date) var date: DateGenerator
public var body: some ReducerProtocol<State, Action> { public var body: some ReducerProtocol<State, Action> {
BindingReducer() BindingReducer()
...@@ -71,7 +84,61 @@ public struct NewGroupComponent: ReducerProtocol { ...@@ -71,7 +84,61 @@ public struct NewGroupComponent: ReducerProtocol {
} }
return .none 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: case .didFinish:
state.isCreating = false
state.failure = nil
return .none
case .didFail(let failure):
state.isCreating = false
state.failure = failure
return .none return .none
case .binding(_): case .binding(_):
......
...@@ -19,13 +19,19 @@ public struct NewGroupView: View { ...@@ -19,13 +19,19 @@ public struct NewGroupView: View {
contacts = state.contacts contacts = state.contacts
members = state.members members = state.members
name = state.name name = state.name
message = state.message
focusedField = state.focusedField focusedField = state.focusedField
isCreating = state.isCreating
failure = state.failure
} }
var contacts: IdentifiedArrayOf<XXModels.Contact> var contacts: IdentifiedArrayOf<XXModels.Contact>
var members: IdentifiedArrayOf<XXModels.Contact> var members: IdentifiedArrayOf<XXModels.Contact>
var name: String var name: String
var message: String
var focusedField: Component.State.Field? var focusedField: Component.State.Field?
var isCreating: Bool
var failure: String?
} }
public var body: some View { public var body: some View {
...@@ -34,6 +40,13 @@ public struct NewGroupView: View { ...@@ -34,6 +40,13 @@ public struct NewGroupView: View {
Section { Section {
membersView(viewStore) membersView(viewStore)
nameView(viewStore) nameView(viewStore)
messageView(viewStore)
}
Section {
createButton(viewStore)
if let failure = viewStore.failure {
Text(failure)
}
} }
} }
.navigationTitle("New Group") .navigationTitle("New Group")
...@@ -61,6 +74,7 @@ public struct NewGroupView: View { ...@@ -61,6 +74,7 @@ public struct NewGroupView: View {
} }
} }
} }
.disabled(viewStore.isCreating)
} }
func nameView(_ viewStore: ViewStore) -> some View { func nameView(_ viewStore: ViewStore) -> some View {
...@@ -69,6 +83,33 @@ public struct NewGroupView: View { ...@@ -69,6 +83,33 @@ public struct NewGroupView: View {
send: { .set(\.$name, $0) } send: { .set(\.$name, $0) }
)) ))
.focused($focusedField, equals: .name) .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)
} }
} }
......
...@@ -10,6 +10,10 @@ import XXModels ...@@ -10,6 +10,10 @@ import XXModels
final class NewGroupComponentTests: XCTestCase { final class NewGroupComponentTests: XCTestCase {
enum Action: Equatable { enum Action: Equatable {
case didFetchContacts(XXModels.Contact.Query) 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]! var actions: [Action]!
...@@ -116,6 +120,165 @@ final class NewGroupComponentTests: XCTestCase { ...@@ -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() { func testFinish() {
let store = TestStore( let store = TestStore(
initialState: NewGroupComponent.State(), initialState: NewGroupComponent.State(),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment