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

Fetch messages in ChatFeature

parent b7b68d7a
No related branches found
No related tags found
2 merge requests!102Release 1.0.0,!87Messenger example - chat
......@@ -108,6 +108,9 @@ let package = Package(
dependencies: [
.target(name: "AppCore"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
.product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
.product(name: "XXModels", package: "client-ios-db"),
],
swiftSettings: swiftSettings
),
......
......@@ -67,7 +67,12 @@ extension AppEnvironment {
)
},
chat: {
ChatEnvironment()
ChatEnvironment(
messenger: messenger,
db: dbManager.getDB,
mainQueue: mainQueue,
bgQueue: bgQueue
)
}
)
......
import AppCore
import ComposableArchitecture
import Foundation
import XCTestDynamicOverlay
import XXClient
import XXMessengerClient
import XXModels
public struct ChatState: Equatable, Identifiable {
public enum ID: Equatable, Hashable {
......@@ -9,7 +13,7 @@ public struct ChatState: Equatable, Identifiable {
public struct Message: Equatable, Identifiable {
public init(
id: Data,
id: Int64,
date: Date,
senderId: Data,
text: String
......@@ -20,7 +24,7 @@ public struct ChatState: Equatable, Identifiable {
self.text = text
}
public var id: Data
public var id: Int64
public var date: Date
public var senderId: Data
public var text: String
......@@ -29,36 +33,97 @@ public struct ChatState: Equatable, Identifiable {
public init(
id: ID,
myContactId: Data? = nil,
messages: IdentifiedArrayOf<Message> = []
messages: IdentifiedArrayOf<Message> = [],
failure: String? = nil
) {
self.id = id
self.myContactId = myContactId
self.messages = messages
self.failure = failure
}
public var id: ID
public var myContactId: Data?
public var messages: IdentifiedArrayOf<Message>
public var failure: String?
}
public enum ChatAction: Equatable {
case start
case didFetchMessages(IdentifiedArrayOf<ChatState.Message>)
}
public struct ChatEnvironment {
public init() {}
public init(
messenger: Messenger,
db: DBManagerGetDB,
mainQueue: AnySchedulerOf<DispatchQueue>,
bgQueue: AnySchedulerOf<DispatchQueue>
) {
self.messenger = messenger
self.db = db
self.mainQueue = mainQueue
self.bgQueue = bgQueue
}
public var messenger: Messenger
public var db: DBManagerGetDB
public var mainQueue: AnySchedulerOf<DispatchQueue>
public var bgQueue: AnySchedulerOf<DispatchQueue>
}
#if DEBUG
extension ChatEnvironment {
public static let unimplemented = ChatEnvironment()
public static let unimplemented = ChatEnvironment(
messenger: .unimplemented,
db: .unimplemented,
mainQueue: .unimplemented,
bgQueue: .unimplemented
)
}
#endif
public let chatReducer = Reducer<ChatState, ChatAction, ChatEnvironment>
{ state, action, env in
enum FetchEffectId {}
switch action {
case .start:
state.failure = nil
do {
let myContactId = try env.messenger.e2e.tryGet().getContact().getId()
let queryChat: XXModels.Message.Query.Chat
switch state.id {
case .contact(let contactId):
queryChat = .direct(myContactId, contactId)
}
let query = XXModels.Message.Query(chat: queryChat)
return try env.db().fetchMessagesPublisher(query)
.assertNoFailure()
.map { messages in
messages.compactMap { message in
guard let id = message.id else { return nil }
return ChatState.Message(
id: id,
date: message.date,
senderId: message.senderId,
text: message.text
)
}
}
.map { IdentifiedArrayOf<ChatState.Message>(uniqueElements: $0) }
.map(ChatAction.didFetchMessages)
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
.cancellable(id: FetchEffectId.self, cancelInFlight: true)
} catch {
state.failure = error.localizedDescription
return .none
}
case .didFetchMessages(let messages):
state.messages = messages
return .none
}
}
......@@ -12,10 +12,12 @@ public struct ChatView: View {
struct ViewState: Equatable {
var myContactId: Data?
var messages: IdentifiedArrayOf<ChatState.Message>
var failure: String?
init(state: ChatState) {
myContactId = state.myContactId
messages = state.messages
failure = state.failure
}
}
......@@ -23,6 +25,17 @@ public struct ChatView: View {
WithViewStore(store, observe: ViewState.init) { viewStore in
ScrollView {
LazyVStack {
if let failure = viewStore.failure {
Text(failure)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
Button {
viewStore.send(.start)
} label: {
Text("Retry")
}
.padding()
}
ForEach(viewStore.messages) { message in
MessageView(
message: message,
......@@ -107,13 +120,13 @@ public struct ChatView_Previews: PreviewProvider {
myContactId: "my-contact-id".data(using: .utf8)!,
messages: [
.init(
id: "message-1-id".data(using: .utf8)!,
id: 1,
date: Date(),
senderId: "contact-id".data(using: .utf8)!,
text: "Hello!"
),
.init(
id: "message-2-id".data(using: .utf8)!,
id: 2,
date: Date(),
senderId: "my-contact-id".data(using: .utf8)!,
text: "Hi!"
......
import Combine
import ComposableArchitecture
import CustomDump
import XCTest
import XXClient
import XXMessengerClient
import XXModels
@testable import ChatFeature
final class ChatFeatureTests: XCTestCase {
func testStart() {
let contactId = "contact-id".data(using: .utf8)!
let myContactId = "my-contact-id".data(using: .utf8)!
let store = TestStore(
initialState: ChatState(id: .contact(contactId)),
......@@ -12,6 +18,114 @@ final class ChatFeatureTests: XCTestCase {
environment: .unimplemented
)
var didFetchMessagesWithQuery: [XXModels.Message.Query] = []
let messagesPublisher = PassthroughSubject<[XXModels.Message], Error>()
store.environment.mainQueue = .immediate
store.environment.bgQueue = .immediate
store.environment.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.environment.db.run = {
var db: Database = .failing
db.fetchMessagesPublisher.run = { query in
didFetchMessagesWithQuery.append(query)
return messagesPublisher.eraseToAnyPublisher()
}
return db
}
store.send(.start)
XCTAssertNoDifference(didFetchMessagesWithQuery, [
.init(chat: .direct(myContactId, contactId))
])
messagesPublisher.send([
.init(
id: nil,
senderId: contactId,
recipientId: myContactId,
groupId: nil,
date: Date(timeIntervalSince1970: 0),
status: .received,
isUnread: false,
text: "Message 0"
),
.init(
id: 1,
senderId: contactId,
recipientId: myContactId,
groupId: nil,
date: Date(timeIntervalSince1970: 1),
status: .received,
isUnread: false,
text: "Message 1"
),
.init(
id: 2,
senderId: myContactId,
recipientId: contactId,
groupId: nil,
date: Date(timeIntervalSince1970: 2),
status: .sent,
isUnread: false,
text: "Message 2"
),
])
let expectedMessages = IdentifiedArrayOf<ChatState.Message>(uniqueElements: [
.init(
id: 1,
date: Date(timeIntervalSince1970: 1),
senderId: contactId,
text: "Message 1"
),
.init(
id: 2,
date: Date(timeIntervalSince1970: 2),
senderId: myContactId,
text: "Message 2"
),
])
store.receive(.didFetchMessages(expectedMessages)) {
$0.messages = expectedMessages
}
messagesPublisher.send(completion: .finished)
}
func testStartFailure() {
let store = TestStore(
initialState: ChatState(id: .contact("contact-id".data(using: .utf8)!)),
reducer: chatReducer,
environment: .unimplemented
)
struct Failure: Error {}
let error = Failure()
store.environment.mainQueue = .immediate
store.environment.bgQueue = .immediate
store.environment.messenger.e2e.get = {
var e2e: E2E = .unimplemented
e2e.getContact.run = {
var contact: XXClient.Contact = .unimplemented(Data())
contact.getIdFromContact.run = { _ in throw error }
return contact
}
return e2e
}
store.send(.start) {
$0.failure = error.localizedDescription
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment