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

Merge branch 'feature/messenger-example-group-chat' into 'development'

[Messenger example] group chat

See merge request elixxir/elixxir-dapps-sdk-swift!152
parents a869acdb 02f25d6b
No related branches found
No related tags found
2 merge requests!153Release 1.1.0,!152[Messenger example] group chat
Showing
with 726 additions and 32 deletions
......@@ -267,6 +267,7 @@ let package = Package(
name: "GroupFeature",
dependencies: [
.target(name: "AppCore"),
.target(name: "ChatFeature"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "ComposablePresentation", package: "swift-composable-presentation"),
.product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
......
......@@ -12,6 +12,7 @@ public struct AppDependencies {
public var bgQueue: AnySchedulerOf<DispatchQueue>
public var now: () -> Date
public var sendMessage: SendMessage
public var sendGroupMessage: SendGroupMessage
public var sendImage: SendImage
public var messageListener: MessageListenerHandler
public var receiveFileHandler: ReceiveFileHandler
......@@ -46,6 +47,11 @@ extension AppDependencies {
db: dbManager.getDB,
now: now
),
sendGroupMessage: .live(
messenger: messenger,
db: dbManager.getDB,
now: now
),
sendImage: .live(
messenger: messenger,
db: dbManager.getDB,
......@@ -85,6 +91,7 @@ extension AppDependencies {
placeholder: Date(timeIntervalSince1970: 0)
),
sendMessage: .unimplemented,
sendGroupMessage: .unimplemented,
sendImage: .unimplemented,
messageListener: .unimplemented,
receiveFileHandler: .unimplemented,
......
import Foundation
import XCTestDynamicOverlay
import XXClient
import XXMessengerClient
import XXModels
public struct SendGroupMessage {
public typealias OnError = (Error) -> Void
public typealias Completion = () -> Void
public var run: (String, Data, @escaping OnError, @escaping Completion) -> Void
public func callAsFunction(
text: String,
to groupId: Data,
onError: @escaping OnError,
completion: @escaping Completion
) {
run(text, groupId, onError, completion)
}
}
extension SendGroupMessage {
public static func live(
messenger: Messenger,
db: DBManagerGetDB,
now: @escaping () -> Date
) -> SendGroupMessage {
SendGroupMessage { text, groupId, onError, completion in
do {
let chat = try messenger.groupChat.tryGet()
let myContactId = try messenger.e2e.tryGet().getContact().getId()
var message = try db().saveMessage(.init(
senderId: myContactId,
recipientId: nil,
groupId: groupId,
date: now(),
status: .sending,
isUnread: false,
text: text
))
let payload = MessagePayload(text: message.text)
let report = try chat.send(
groupId: groupId,
message: try payload.encode()
)
message.networkId = report.messageId
message.roundURL = report.roundURL
message = try db().saveMessage(message)
try messenger.cMix.tryGet().waitForRoundResult(
roundList: try report.encode(),
timeoutMS: 30_000,
callback: .init { result in
let status: XXModels.Message.Status
switch result {
case .delivered(_):
status = .sent
case .notDelivered(let timedOut):
status = timedOut ? .sendingTimedOut : .sendingFailed
}
do {
try db().bulkUpdateMessages(
.init(id: [message.id]),
.init(status: status)
)
} catch {
onError(error)
}
completion()
}
)
} catch {
onError(error)
completion()
}
}
}
}
extension SendGroupMessage {
public static let unimplemented = SendGroupMessage(
run: XCTUnimplemented("\(Self.self)")
)
}
......@@ -10,7 +10,8 @@ import XXModels
public struct ChatComponent: ReducerProtocol {
public struct State: Equatable, Identifiable {
public enum ID: Equatable, Hashable {
case contact(Data)
case contact(XXModels.Contact.ID)
case group(XXModels.Group.ID)
}
public struct Message: Equatable, Identifiable {
......@@ -18,6 +19,7 @@ public struct ChatComponent: ReducerProtocol {
id: Int64,
date: Date,
senderId: Data,
senderName: String?,
text: String,
status: XXModels.Message.Status,
fileTransfer: XXModels.FileTransfer? = nil
......@@ -25,6 +27,7 @@ public struct ChatComponent: ReducerProtocol {
self.id = id
self.date = date
self.senderId = senderId
self.senderName = senderName
self.text = text
self.status = status
self.fileTransfer = fileTransfer
......@@ -33,6 +36,7 @@ public struct ChatComponent: ReducerProtocol {
public var id: Int64
public var date: Date
public var senderId: Data
public var senderName: String?
public var text: String
public var status: XXModels.Message.Status
public var fileTransfer: XXModels.FileTransfer?
......@@ -77,6 +81,7 @@ public struct ChatComponent: ReducerProtocol {
@Dependency(\.app.messenger) var messenger: Messenger
@Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB
@Dependency(\.app.sendMessage) var sendMessage: SendMessage
@Dependency(\.app.sendGroupMessage) var sendGroupMessage: SendGroupMessage
@Dependency(\.app.sendImage) var sendImage: SendImage
@Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue>
@Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue>
......@@ -93,37 +98,46 @@ public struct ChatComponent: ReducerProtocol {
let myContactId = try messenger.e2e.tryGet().getContact().getId()
state.myContactId = myContactId
let queryChat: XXModels.Message.Query.Chat
let receivedFileTransfersQuery: XXModels.FileTransfer.Query
let sentFileTransfersQuery: XXModels.FileTransfer.Query
let receivedFileTransfersPublisher: AnyPublisher<[XXModels.FileTransfer], Error>
let sentFileTransfersPublisher: AnyPublisher<[XXModels.FileTransfer], Error>
switch state.id {
case .contact(let contactId):
queryChat = .direct(myContactId, contactId)
receivedFileTransfersQuery = .init(
receivedFileTransfersPublisher = try db().fetchFileTransfersPublisher(.init(
contactId: contactId,
isIncoming: true
)
sentFileTransfersQuery = .init(
))
sentFileTransfersPublisher = try db().fetchFileTransfersPublisher(.init(
contactId: myContactId,
isIncoming: false
)
))
case .group(let groupId):
queryChat = .group(groupId)
receivedFileTransfersPublisher = Just([])
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
sentFileTransfersPublisher = Just([])
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
let messagesQuery = XXModels.Message.Query(chat: queryChat)
return Publishers.CombineLatest3(
try db().fetchMessagesPublisher(messagesQuery),
try db().fetchFileTransfersPublisher(receivedFileTransfersQuery),
try db().fetchFileTransfersPublisher(sentFileTransfersQuery)
try db().fetchContactsPublisher(.init()),
Publishers.CombineLatest(
receivedFileTransfersPublisher,
sentFileTransfersPublisher
).map(+)
)
.map { messages, receivedFileTransfers, sentFileTransfers in
(messages, receivedFileTransfers + sentFileTransfers)
}
.assertNoFailure()
.map { messages, fileTransfers in
messages.compactMap { message in
.map { messages, contacts, fileTransfers -> [State.Message] in
messages.compactMap { message -> State.Message? in
guard let id = message.id else { return nil }
return State.Message(
id: id,
date: message.date,
senderId: message.senderId,
senderName: contacts.first { $0.id == message.senderId }?.username,
text: message.text,
status: message.status,
fileTransfer: fileTransfers.first { $0.id == message.fileTransferId }
......@@ -163,6 +177,17 @@ public struct ChatComponent: ReducerProtocol {
subscriber.send(completion: .finished)
}
)
case .group(let groupId):
sendGroupMessage(
text: text,
to: groupId,
onError: { error in
subscriber.send(.sendFailed(error.localizedDescription))
},
completion: {
subscriber.send(completion: .finished)
}
)
}
return AnyCancellable {}
}
......@@ -175,10 +200,8 @@ public struct ChatComponent: ReducerProtocol {
return .none
case .imagePicked(let data):
let chatId = state.id
guard case .contact(let recipientId) = state.id else { return .none }
return Effect.run { subscriber in
switch chatId {
case .contact(let recipientId):
sendImage(
data,
to: recipientId,
......@@ -189,7 +212,6 @@ public struct ChatComponent: ReducerProtocol {
subscriber.send(completion: .finished)
}
)
}
return AnyCancellable {}
}
.subscribe(on: bgQueue)
......
......@@ -16,6 +16,7 @@ public struct ChatView: View {
var failure: String?
var sendFailure: String?
var text: String
var disableImagePicker: Bool
init(state: ChatComponent.State) {
myContactId = state.myContactId
......@@ -23,6 +24,12 @@ public struct ChatView: View {
failure = state.failure
sendFailure = state.sendFailure
text = state.text
switch state.id {
case .contact(_):
disableImagePicker = false
case .group(_):
disableImagePicker = true
}
}
}
......@@ -109,6 +116,7 @@ public struct ChatView: View {
}
}
}
.disabled(viewStore.disableImagePicker)
}
}
.padding()
......@@ -139,6 +147,13 @@ public struct ChatView: View {
var body: some View {
VStack {
if let sender = message.senderName {
Text(sender)
.foregroundColor(.secondary)
.font(.footnote)
.frame(maxWidth: .infinity, alignment: alignment)
}
Text("\(message.date.formatted()), \(statusText)")
.foregroundColor(.secondary)
.font(.footnote)
......@@ -208,6 +223,7 @@ public struct ChatView_Previews: PreviewProvider {
id: 1,
date: Date(),
senderId: "contact-id".data(using: .utf8)!,
senderName: "Contact",
text: "Hello!",
status: .received
),
......@@ -215,6 +231,7 @@ public struct ChatView_Previews: PreviewProvider {
id: 2,
date: Date(),
senderId: "my-contact-id".data(using: .utf8)!,
senderName: "Me",
text: "Hi!",
status: .sent
),
......@@ -222,6 +239,7 @@ public struct ChatView_Previews: PreviewProvider {
id: 3,
date: Date(),
senderId: "contact-id".data(using: .utf8)!,
senderName: "Contact",
text: "",
status: .received,
fileTransfer: .init(
......@@ -237,6 +255,7 @@ public struct ChatView_Previews: PreviewProvider {
id: 4,
date: Date(),
senderId: "my-contact-id".data(using: .utf8)!,
senderName: "Me",
text: "",
status: .sent,
fileTransfer: .init(
......
import AppCore
import ChatFeature
import ComposableArchitecture
import ComposablePresentation
import Foundation
import XXMessengerClient
import XXModels
......@@ -10,18 +12,21 @@ public struct GroupComponent: ReducerProtocol {
groupId: XXModels.Group.ID,
groupInfo: XXModels.GroupInfo? = nil,
isJoining: Bool = false,
joinFailure: String? = nil
joinFailure: String? = nil,
chat: ChatComponent.State? = nil
) {
self.groupId = groupId
self.groupInfo = groupInfo
self.isJoining = isJoining
self.joinFailure = joinFailure
self.chat = chat
}
public var groupId: XXModels.Group.ID
public var groupInfo: XXModels.GroupInfo?
public var isJoining: Bool
public var joinFailure: String?
public var chat: ChatComponent.State?
}
public enum Action: Equatable {
......@@ -30,6 +35,9 @@ public struct GroupComponent: ReducerProtocol {
case joinButtonTapped
case didJoin
case didFailToJoin(String)
case chatButtonTapped
case didDismissChat
case chat(ChatComponent.Action)
}
public init() {}
......@@ -88,7 +96,24 @@ public struct GroupComponent: ReducerProtocol {
state.isJoining = false
state.joinFailure = failure
return .none
case .chatButtonTapped:
state.chat = ChatComponent.State(id: .group(state.groupId))
return .none
case .didDismissChat:
state.chat = nil
return .none
case .chat(_):
return .none
}
}
.presenting(
state: .keyPath(\.chat),
id: .notNil(),
action: /Action.chat,
presented: { ChatComponent() }
)
}
}
import AppCore
import ChatFeature
import ComposableArchitecture
import ComposablePresentation
import SwiftUI
import XXModels
......@@ -68,8 +70,28 @@ public struct GroupView: View {
}
}
}
Section {
Button {
viewStore.send(.chatButtonTapped)
} label: {
HStack {
Text("Chat")
Spacer()
Image(systemName: "chevron.forward")
}
}
}
}
.navigationTitle("Group")
.background(NavigationLinkWithStore(
store.scope(
state: \.chat,
action: Component.Action.chat
),
onDeactivate: { viewStore.send(.didDismissChat) },
destination: ChatView.init(store:)
))
.task { viewStore.send(.start) }
}
}
......
import CustomDump
import XCTest
import XCTestDynamicOverlay
import XXClient
import XXMessengerClient
import XXModels
@testable import AppCore
final class SendGroupMessageTests: XCTestCase {
enum Action: Equatable {
case didReceiveError(String)
case didComplete
case didSaveMessage(XXModels.Message)
case didSend(groupId: Data, message: Data, tag: String?)
case didWaitForRoundResults(roundList: Data, timeoutMS: Int)
case didUpdateMessage(
query: XXModels.Message.Query,
assignments: XXModels.Message.Assignments
)
}
var actions: [Action]!
override func setUp() {
actions = []
}
override func tearDown() {
actions = nil
}
func testSend() {
let text = "Hello!"
let groupId = "group-id".data(using: .utf8)!
let myContactId = "my-contact-id".data(using: .utf8)!
let messageId: Int64 = 321
let sendReport = GroupSendReport(
rounds: [],
roundURL: "round-url",
timestamp: 1234,
messageId: "message-id".data(using: .utf8)!
)
var messageDeliveryCallback: MessageDeliveryCallback?
var messenger: Messenger = .unimplemented
messenger.groupChat.get = {
var groupChat: GroupChat = .unimplemented
groupChat.send.run = { groupId, message, tag in
self.actions.append(.didSend(groupId: groupId, message: message, tag: tag))
return sendReport
}
return groupChat
}
messenger.e2e.get = {
var e2e: E2E = .unimplemented
e2e.getContact.run = {
var contact: XXClient.Contact = .unimplemented(Data())
contact.getIdFromContact.run = { _ in myContactId }
return contact
}
return e2e
}
messenger.cMix.get = {
var cMix: CMix = .unimplemented
cMix.waitForRoundResult.run = { roundList, timeoutMS, callback in
self.actions.append(.didWaitForRoundResults(roundList: roundList, timeoutMS: timeoutMS))
messageDeliveryCallback = callback
}
return cMix
}
var db: DBManagerGetDB = .unimplemented
db.run = {
var db: Database = .unimplemented
db.saveMessage.run = { message in
self.actions.append(.didSaveMessage(message))
var message = message
message.id = messageId
return message
}
db.bulkUpdateMessages.run = { query, assignments in
self.actions.append(.didUpdateMessage(query: query, assignments: assignments))
return 1
}
return db
}
let now = Date()
let send: SendGroupMessage = .live(
messenger: messenger,
db: db,
now: { now }
)
send(
text: text,
to: groupId,
onError: { error in
self.actions.append(.didReceiveError(error.localizedDescription))
},
completion: {
self.actions.append(.didComplete)
}
)
XCTAssertNoDifference(actions, [
.didSaveMessage(.init(
senderId: myContactId,
recipientId: nil,
groupId: groupId,
date: now,
status: .sending,
isUnread: false,
text: text
)),
.didSend(
groupId: groupId,
message: try! MessagePayload(text: text).encode(),
tag: nil
),
.didSaveMessage(.init(
id: messageId,
networkId: sendReport.messageId,
senderId: myContactId,
recipientId: nil,
groupId: groupId,
date: now,
status: .sending,
isUnread: false,
text: text,
roundURL: sendReport.roundURL
)),
.didWaitForRoundResults(
roundList: try! sendReport.encode(),
timeoutMS: 30_000
),
])
actions = []
messageDeliveryCallback?.handle(.delivered(roundResults: []))
XCTAssertNoDifference(actions, [
.didUpdateMessage(
query: .init(id: [messageId]),
assignments: .init(status: .sent)
),
.didComplete,
])
actions = []
messageDeliveryCallback?.handle(.notDelivered(timedOut: true))
XCTAssertNoDifference(actions, [
.didUpdateMessage(
query: .init(id: [messageId]),
assignments: .init(status: .sendingTimedOut)
),
.didComplete,
])
actions = []
messageDeliveryCallback?.handle(.notDelivered(timedOut: false))
XCTAssertNoDifference(actions, [
.didUpdateMessage(
query: .init(id: [messageId]),
assignments: .init(status: .sendingFailed)
),
.didComplete,
])
}
func testSendDatabaseFailure() {
struct Failure: Error, Equatable {}
let failure = Failure()
var messenger: Messenger = .unimplemented
messenger.e2e.get = {
var e2e: E2E = .unimplemented
e2e.getContact.run = {
var contact: XXClient.Contact = .unimplemented(Data())
contact.getIdFromContact.run = { _ in Data() }
return contact
}
return e2e
}
messenger.groupChat.get = { .unimplemented }
var db: DBManagerGetDB = .unimplemented
db.run = { throw failure }
let send: SendGroupMessage = .live(
messenger: messenger,
db: db,
now: XCTestDynamicOverlay.unimplemented("now", placeholder: Date())
)
send(
text: "Hello",
to: "group-id".data(using: .utf8)!,
onError: { error in
self.actions.append(.didReceiveError(error.localizedDescription))
},
completion: {
self.actions.append(.didComplete)
}
)
XCTAssertNoDifference(actions, [
.didReceiveError(failure.localizedDescription),
.didComplete
])
}
func testBulkUpdateOnDeliveryFailure() {
struct Failure: Error, Equatable {}
let failure = Failure()
var messageDeliveryCallback: MessageDeliveryCallback?
var messenger: Messenger = .unimplemented
messenger.groupChat.get = {
var groupChat: GroupChat = .unimplemented
groupChat.send.run = { _, _, _ in
GroupSendReport(
rounds: [],
roundURL: "",
timestamp: 0,
messageId: Data()
)
}
return groupChat
}
messenger.e2e.get = {
var e2e: E2E = .unimplemented
e2e.getContact.run = {
var contact: XXClient.Contact = .unimplemented(Data())
contact.getIdFromContact.run = { _ in Data() }
return contact
}
return e2e
}
messenger.cMix.get = {
var cMix: CMix = .unimplemented
cMix.waitForRoundResult.run = { _, _, callback in
messageDeliveryCallback = callback
}
return cMix
}
var db: DBManagerGetDB = .unimplemented
db.run = {
var db: Database = .unimplemented
db.saveMessage.run = { message in message }
db.bulkUpdateMessages.run = { _, _ in throw failure }
return db
}
let now = Date()
let send: SendGroupMessage = .live(
messenger: messenger,
db: db,
now: { now }
)
send(
text: "Hello",
to: Data(),
onError: { error in
self.actions.append(.didReceiveError(error.localizedDescription))
},
completion: {
self.actions.append(.didComplete)
}
)
messageDeliveryCallback?.handle(.delivered(roundResults: []))
XCTAssertNoDifference(actions, [
.didReceiveError(failure.localizedDescription),
.didComplete,
])
}
}
......@@ -9,7 +9,7 @@ import XXModels
@testable import ChatFeature
final class ChatComponentTests: XCTestCase {
func testStart() {
func testStartDirectChat() {
let contactId = "contact-id".data(using: .utf8)!
let myContactId = "my-contact-id".data(using: .utf8)!
......@@ -22,6 +22,8 @@ final class ChatComponentTests: XCTestCase {
let messagesPublisher = PassthroughSubject<[XXModels.Message], Error>()
var didFetchFileTransfersWithQuery: [XXModels.FileTransfer.Query] = []
let fileTransfersPublisher = PassthroughSubject<[XXModels.FileTransfer], Error>()
var didFetchContactsWithQuery: [XXModels.Contact.Query] = []
let contactsPublisher = PassthroughSubject<[XXModels.Contact], Error>()
store.dependencies.app.mainQueue = .immediate
store.dependencies.app.bgQueue = .immediate
......@@ -40,6 +42,10 @@ final class ChatComponentTests: XCTestCase {
didFetchMessagesWithQuery.append(query)
return messagesPublisher.eraseToAnyPublisher()
}
db.fetchContactsPublisher.run = { query in
didFetchContactsWithQuery.append(query)
return contactsPublisher.eraseToAnyPublisher()
}
db.fetchFileTransfersPublisher.run = { query in
didFetchFileTransfersWithQuery.append(query)
return fileTransfersPublisher.eraseToAnyPublisher()
......@@ -58,6 +64,9 @@ final class ChatComponentTests: XCTestCase {
.init(contactId: contactId, isIncoming: true),
.init(contactId: myContactId, isIncoming: false),
])
XCTAssertNoDifference(didFetchContactsWithQuery, [
.init(),
])
let receivedFileTransfer = FileTransfer(
id: "file-transfer-1-id".data(using: .utf8)!,
......@@ -111,12 +120,17 @@ final class ChatComponentTests: XCTestCase {
receivedFileTransfer,
sentFileTransfer,
])
contactsPublisher.send([
.init(id: myContactId, username: "My username"),
.init(id: contactId, username: "Contact username"),
])
let expectedMessages = IdentifiedArrayOf<ChatComponent.State.Message>(uniqueElements: [
.init(
id: 1,
date: Date(timeIntervalSince1970: 1),
senderId: contactId,
senderName: "Contact username",
text: "Message 1",
status: .received,
fileTransfer: receivedFileTransfer
......@@ -125,6 +139,7 @@ final class ChatComponentTests: XCTestCase {
id: 2,
date: Date(timeIntervalSince1970: 2),
senderId: myContactId,
senderName: "My username",
text: "Message 2",
status: .sent,
fileTransfer: sentFileTransfer
......@@ -137,6 +152,131 @@ final class ChatComponentTests: XCTestCase {
messagesPublisher.send(completion: .finished)
fileTransfersPublisher.send(completion: .finished)
contactsPublisher.send(completion: .finished)
}
func testStartGroupChat() {
let groupId = "group-id".data(using: .utf8)!
let myContactId = "my-contact-id".data(using: .utf8)!
let firstMemberId = "member-1-id".data(using: .utf8)!
let secondMemberId = "member-2-id".data(using: .utf8)!
let store = TestStore(
initialState: ChatComponent.State(id: .group(groupId)),
reducer: ChatComponent()
)
var didFetchMessagesWithQuery: [XXModels.Message.Query] = []
let messagesPublisher = PassthroughSubject<[XXModels.Message], Error>()
var didFetchContactsWithQuery: [XXModels.Contact.Query] = []
let contactsPublisher = PassthroughSubject<[XXModels.Contact], Error>()
store.dependencies.app.mainQueue = .immediate
store.dependencies.app.bgQueue = .immediate
store.dependencies.app.messenger.e2e.get = {
var e2e: E2E = .unimplemented
e2e.getContact.run = {
var contact: XXClient.Contact = .unimplemented(Data())
contact.getIdFromContact.run = { _ in myContactId }
return contact
}
return e2e
}
store.dependencies.app.dbManager.getDB.run = {
var db: Database = .unimplemented
db.fetchMessagesPublisher.run = { query in
didFetchMessagesWithQuery.append(query)
return messagesPublisher.eraseToAnyPublisher()
}
db.fetchContactsPublisher.run = { query in
didFetchContactsWithQuery.append(query)
return contactsPublisher.eraseToAnyPublisher()
}
return db
}
store.send(.start) {
$0.myContactId = myContactId
}
XCTAssertNoDifference(didFetchMessagesWithQuery, [
.init(chat: .group(groupId))
])
XCTAssertNoDifference(didFetchContactsWithQuery, [
.init(),
])
messagesPublisher.send([
.init(
id: 0,
senderId: myContactId,
recipientId: nil,
groupId: groupId,
date: Date(timeIntervalSince1970: 0),
status: .sent,
isUnread: false,
text: "Message 0"
),
.init(
id: 1,
senderId: firstMemberId,
recipientId: nil,
groupId: groupId,
date: Date(timeIntervalSince1970: 1),
status: .received,
isUnread: false,
text: "Message 1"
),
.init(
id: 2,
senderId: secondMemberId,
recipientId: nil,
groupId: groupId,
date: Date(timeIntervalSince1970: 2),
status: .received,
isUnread: false,
text: "Message 2"
),
])
contactsPublisher.send([
.init(id: myContactId, username: "My username"),
.init(id: firstMemberId, username: "First username"),
.init(id: secondMemberId, username: "Second username"),
])
let expectedMessages = IdentifiedArrayOf<ChatComponent.State.Message>(uniqueElements: [
.init(
id: 0,
date: Date(timeIntervalSince1970: 0),
senderId: myContactId,
senderName: "My username",
text: "Message 0",
status: .sent
),
.init(
id: 1,
date: Date(timeIntervalSince1970: 1),
senderId: firstMemberId,
senderName: "First username",
text: "Message 1",
status: .received
),
.init(
id: 2,
date: Date(timeIntervalSince1970: 2),
senderId: secondMemberId,
senderName: "Second username",
text: "Message 2",
status: .received
),
])
store.receive(.didFetchMessages(expectedMessages)) {
$0.messages = expectedMessages
}
messagesPublisher.send(completion: .finished)
contactsPublisher.send(completion: .finished)
}
func testStartFailure() {
......@@ -165,7 +305,7 @@ final class ChatComponentTests: XCTestCase {
}
}
func testSend() {
func testSendDirectMessage() {
struct SendMessageParams: Equatable {
var text: String
var recipientId: Data
......@@ -200,7 +340,7 @@ final class ChatComponentTests: XCTestCase {
sendMessageCompletion?()
}
func testSendFailure() {
func testSendDirectMessageFailure() {
var sendMessageOnError: SendMessage.OnError?
var sendMessageCompletion: SendMessage.Completion?
......@@ -237,6 +377,80 @@ final class ChatComponentTests: XCTestCase {
}
}
func testSendGroupMessage() {
let groupId = "group-id".data(using: .utf8)!
let text = "Hello"
struct SendGroupMessageParams: Equatable {
var text: String
var groupId: Data
}
var didSendGroupMessageWithParams: [SendGroupMessageParams] = []
var sendGroupMessageCompletion: SendGroupMessage.Completion?
let store = TestStore(
initialState: ChatComponent.State(id: .group(groupId)),
reducer: ChatComponent()
)
store.dependencies.app.mainQueue = .immediate
store.dependencies.app.bgQueue = .immediate
store.dependencies.app.sendGroupMessage.run = { text, groupId, _, completion in
didSendGroupMessageWithParams.append(.init(text: text, groupId: groupId))
sendGroupMessageCompletion = completion
}
store.send(.set(\.$text, text)) {
$0.text = text
}
store.send(.sendTapped) {
$0.text = ""
}
XCTAssertNoDifference(didSendGroupMessageWithParams, [
.init(text: text, groupId: groupId)
])
sendGroupMessageCompletion?()
}
func testSendGroupMessageFailure() {
var sendGroupMessageOnError: SendGroupMessage.OnError?
var sendGroupMessageCompletion: SendGroupMessage.Completion?
let store = TestStore(
initialState: ChatComponent.State(
id: .group("group-id".data(using: .utf8)!),
text: "Hello"
),
reducer: ChatComponent()
)
store.dependencies.app.mainQueue = .immediate
store.dependencies.app.bgQueue = .immediate
store.dependencies.app.sendGroupMessage.run = { _, _, onError, completion in
sendGroupMessageOnError = onError
sendGroupMessageCompletion = completion
}
store.send(.sendTapped) {
$0.text = ""
}
let error = NSError(domain: "test", code: 123)
sendGroupMessageOnError?(error)
store.receive(.sendFailed(error.localizedDescription)) {
$0.sendFailure = error.localizedDescription
}
sendGroupMessageCompletion?()
store.send(.dismissSendFailureTapped) {
$0.sendFailure = nil
}
}
func testSendImage() {
struct SendImageParams: Equatable {
var image: Data
......
import ChatFeature
import Combine
import ComposableArchitecture
import CustomDump
......@@ -140,6 +141,26 @@ final class GroupComponentTests: XCTestCase {
$0.joinFailure = failure.localizedDescription
}
}
func testPresentChat() {
let groupInfo = GroupInfo.stub()
let store = TestStore(
initialState: GroupComponent.State(
groupId: groupInfo.group.id,
groupInfo: groupInfo
),
reducer: GroupComponent()
)
store.send(.chatButtonTapped) {
$0.chat = ChatComponent.State(id: .group(groupInfo.id))
}
store.send(.didDismissChat) {
$0.chat = nil
}
}
}
private extension XXModels.GroupInfo {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment