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( ...@@ -108,6 +108,9 @@ let package = Package(
dependencies: [ dependencies: [
.target(name: "AppCore"), .target(name: "AppCore"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .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 swiftSettings: swiftSettings
), ),
......
...@@ -67,7 +67,12 @@ extension AppEnvironment { ...@@ -67,7 +67,12 @@ extension AppEnvironment {
) )
}, },
chat: { chat: {
ChatEnvironment() ChatEnvironment(
messenger: messenger,
db: dbManager.getDB,
mainQueue: mainQueue,
bgQueue: bgQueue
)
} }
) )
......
import AppCore
import ComposableArchitecture import ComposableArchitecture
import Foundation import Foundation
import XCTestDynamicOverlay import XCTestDynamicOverlay
import XXClient
import XXMessengerClient
import XXModels
public struct ChatState: Equatable, Identifiable { public struct ChatState: Equatable, Identifiable {
public enum ID: Equatable, Hashable { public enum ID: Equatable, Hashable {
...@@ -9,7 +13,7 @@ public struct ChatState: Equatable, Identifiable { ...@@ -9,7 +13,7 @@ public struct ChatState: Equatable, Identifiable {
public struct Message: Equatable, Identifiable { public struct Message: Equatable, Identifiable {
public init( public init(
id: Data, id: Int64,
date: Date, date: Date,
senderId: Data, senderId: Data,
text: String text: String
...@@ -20,7 +24,7 @@ public struct ChatState: Equatable, Identifiable { ...@@ -20,7 +24,7 @@ public struct ChatState: Equatable, Identifiable {
self.text = text self.text = text
} }
public var id: Data public var id: Int64
public var date: Date public var date: Date
public var senderId: Data public var senderId: Data
public var text: String public var text: String
...@@ -29,36 +33,97 @@ public struct ChatState: Equatable, Identifiable { ...@@ -29,36 +33,97 @@ public struct ChatState: Equatable, Identifiable {
public init( public init(
id: ID, id: ID,
myContactId: Data? = nil, myContactId: Data? = nil,
messages: IdentifiedArrayOf<Message> = [] messages: IdentifiedArrayOf<Message> = [],
failure: String? = nil
) { ) {
self.id = id self.id = id
self.myContactId = myContactId self.myContactId = myContactId
self.messages = messages self.messages = messages
self.failure = failure
} }
public var id: ID public var id: ID
public var myContactId: Data? public var myContactId: Data?
public var messages: IdentifiedArrayOf<Message> public var messages: IdentifiedArrayOf<Message>
public var failure: String?
} }
public enum ChatAction: Equatable { public enum ChatAction: Equatable {
case start case start
case didFetchMessages(IdentifiedArrayOf<ChatState.Message>)
} }
public struct ChatEnvironment { 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 #if DEBUG
extension ChatEnvironment { extension ChatEnvironment {
public static let unimplemented = ChatEnvironment() public static let unimplemented = ChatEnvironment(
messenger: .unimplemented,
db: .unimplemented,
mainQueue: .unimplemented,
bgQueue: .unimplemented
)
} }
#endif #endif
public let chatReducer = Reducer<ChatState, ChatAction, ChatEnvironment> public let chatReducer = Reducer<ChatState, ChatAction, ChatEnvironment>
{ state, action, env in { state, action, env in
enum FetchEffectId {}
switch action { switch action {
case .start: 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 return .none
} }
} }
...@@ -12,10 +12,12 @@ public struct ChatView: View { ...@@ -12,10 +12,12 @@ public struct ChatView: View {
struct ViewState: Equatable { struct ViewState: Equatable {
var myContactId: Data? var myContactId: Data?
var messages: IdentifiedArrayOf<ChatState.Message> var messages: IdentifiedArrayOf<ChatState.Message>
var failure: String?
init(state: ChatState) { init(state: ChatState) {
myContactId = state.myContactId myContactId = state.myContactId
messages = state.messages messages = state.messages
failure = state.failure
} }
} }
...@@ -23,6 +25,17 @@ public struct ChatView: View { ...@@ -23,6 +25,17 @@ public struct ChatView: View {
WithViewStore(store, observe: ViewState.init) { viewStore in WithViewStore(store, observe: ViewState.init) { viewStore in
ScrollView { ScrollView {
LazyVStack { 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 ForEach(viewStore.messages) { message in
MessageView( MessageView(
message: message, message: message,
...@@ -107,13 +120,13 @@ public struct ChatView_Previews: PreviewProvider { ...@@ -107,13 +120,13 @@ public struct ChatView_Previews: PreviewProvider {
myContactId: "my-contact-id".data(using: .utf8)!, myContactId: "my-contact-id".data(using: .utf8)!,
messages: [ messages: [
.init( .init(
id: "message-1-id".data(using: .utf8)!, id: 1,
date: Date(), date: Date(),
senderId: "contact-id".data(using: .utf8)!, senderId: "contact-id".data(using: .utf8)!,
text: "Hello!" text: "Hello!"
), ),
.init( .init(
id: "message-2-id".data(using: .utf8)!, id: 2,
date: Date(), date: Date(),
senderId: "my-contact-id".data(using: .utf8)!, senderId: "my-contact-id".data(using: .utf8)!,
text: "Hi!" text: "Hi!"
......
import Combine
import ComposableArchitecture import ComposableArchitecture
import CustomDump
import XCTest import XCTest
import XXClient
import XXMessengerClient
import XXModels
@testable import ChatFeature @testable import ChatFeature
final class ChatFeatureTests: XCTestCase { final class ChatFeatureTests: XCTestCase {
func testStart() { func testStart() {
let contactId = "contact-id".data(using: .utf8)! let contactId = "contact-id".data(using: .utf8)!
let myContactId = "my-contact-id".data(using: .utf8)!
let store = TestStore( let store = TestStore(
initialState: ChatState(id: .contact(contactId)), initialState: ChatState(id: .contact(contactId)),
...@@ -12,6 +18,114 @@ final class ChatFeatureTests: XCTestCase { ...@@ -12,6 +18,114 @@ final class ChatFeatureTests: XCTestCase {
environment: .unimplemented 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) 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