diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index 15e3c9a8cc2f1192ea6ca0a0ec8db731bdef92bc..c5bcba6a09c4d828a7506a531d429f9d9ab8a92d 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -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 ), diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 9050fb3ec79c8a45e11cb4a10640c2c590051e6d..e307596b1edf9b23aeaa81ff3794d704d62b3f6a 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -67,7 +67,12 @@ extension AppEnvironment { ) }, chat: { - ChatEnvironment() + ChatEnvironment( + messenger: messenger, + db: dbManager.getDB, + mainQueue: mainQueue, + bgQueue: bgQueue + ) } ) diff --git a/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift b/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift index 1b050a0a2e093671bbff13c157bf814ae93a3eb4..1d34f7f2f54fb623cbaaf1478c13975de6faa934 100644 --- a/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift +++ b/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift @@ -1,6 +1,10 @@ +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 } } diff --git a/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift b/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift index 0d2be0f07ae59c589cd3fd09f9d1516e15f306e7..74e08c7b6371f86fafbd570020214dedb9e93df7 100644 --- a/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift +++ b/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift @@ -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!" diff --git a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift index 0068e667cc3a3dc84a0605c8d13e7843ef874eb8..ae9e65d9d8cba972cb779e72d4248f6be62caf76 100644 --- a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift @@ -1,10 +1,16 @@ +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 + } } }