From 7ae8a45f0ec5e9b31716e0042901dabbf5c938ef Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 14 Sep 2022 11:30:51 +0200
Subject: [PATCH] Fetch messages in ChatFeature

---
 Examples/xx-messenger/Package.swift           |   3 +
 .../AppFeature/AppEnvironment+Live.swift      |   7 +-
 .../Sources/ChatFeature/ChatFeature.swift     |  75 +++++++++++-
 .../Sources/ChatFeature/ChatView.swift        |  17 ++-
 .../ChatFeatureTests/ChatFeatureTests.swift   | 114 ++++++++++++++++++
 5 files changed, 208 insertions(+), 8 deletions(-)

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 15e3c9a8..c5bcba6a 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 9050fb3e..e307596b 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 1b050a0a..1d34f7f2 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 0d2be0f0..74e08c7b 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 0068e667..ae9e65d9 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
+    }
   }
 }
-- 
GitLab