From 2708f07f4ee6aac961257f3ce1e48b8abc0d0b8e Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Thu, 8 Sep 2022 13:00:35 +0200
Subject: [PATCH 1/5] Add ContactsFeature library

---
 .../xcschemes/ContactsFeature.xcscheme        | 78 +++++++++++++++++++
 Examples/xx-messenger/Package.swift           | 18 +++++
 .../xcschemes/XXMessenger.xcscheme            | 10 +++
 .../ContactsFeature/ContactsFeature.swift     | 28 +++++++
 .../ContactsFeature/ContactsView.swift        | 38 +++++++++
 .../ContactsFeatureTests.swift                | 15 ++++
 6 files changed, 187 insertions(+)
 create mode 100644 Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactsFeature.xcscheme
 create mode 100644 Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift
 create mode 100644 Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift
 create mode 100644 Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift

diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactsFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactsFeature.xcscheme
new file mode 100644
index 00000000..feb1a490
--- /dev/null
+++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactsFeature.xcscheme
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1400"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "ContactsFeature"
+               BuildableName = "ContactsFeature"
+               BlueprintName = "ContactsFeature"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      codeCoverageEnabled = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "ContactsFeatureTests"
+               BuildableName = "ContactsFeatureTests"
+               BlueprintName = "ContactsFeatureTests"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "ContactsFeature"
+            BuildableName = "ContactsFeature"
+            BlueprintName = "ContactsFeature"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 77b97a0f..54f5035c 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -21,6 +21,7 @@ let package = Package(
     .library(name: "AppCore", targets: ["AppCore"]),
     .library(name: "AppFeature", targets: ["AppFeature"]),
     .library(name: "ContactFeature", targets: ["ContactFeature"]),
+    .library(name: "ContactsFeature", targets: ["ContactsFeature"]),
     .library(name: "HomeFeature", targets: ["HomeFeature"]),
     .library(name: "RegisterFeature", targets: ["RegisterFeature"]),
     .library(name: "RestoreFeature", targets: ["RestoreFeature"]),
@@ -111,6 +112,23 @@ let package = Package(
       ],
       swiftSettings: swiftSettings
     ),
+    .target(
+      name: "ContactsFeature",
+      dependencies: [
+        .target(name: "AppCore"),
+        .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
+        .product(name: "ComposablePresentation", package: "swift-composable-presentation"),
+        .product(name: "XXModels", package: "client-ios-db"),
+      ],
+      swiftSettings: swiftSettings
+    ),
+    .testTarget(
+      name: "ContactsFeatureTests",
+      dependencies: [
+        .target(name: "ContactsFeature"),
+      ],
+      swiftSettings: swiftSettings
+    ),
     .target(
       name: "HomeFeature",
       dependencies: [
diff --git a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
index 0d7cb106..9d31f6f7 100644
--- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
+++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
@@ -59,6 +59,16 @@
                ReferencedContainer = "container:..">
             </BuildableReference>
          </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "ContactsFeatureTests"
+               BuildableName = "ContactsFeatureTests"
+               BlueprintName = "ContactsFeatureTests"
+               ReferencedContainer = "container:..">
+            </BuildableReference>
+         </TestableReference>
          <TestableReference
             skipped = "NO">
             <BuildableReference
diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift
new file mode 100644
index 00000000..e7f98fe7
--- /dev/null
+++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift
@@ -0,0 +1,28 @@
+import ComposableArchitecture
+import XCTestDynamicOverlay
+
+public struct ContactsState: Equatable {
+  public init() {}
+}
+
+public enum ContactsAction: Equatable {
+  case start
+}
+
+public struct ContactsEnvironment {
+  public init() {}
+}
+
+#if DEBUG
+extension ContactsEnvironment {
+  public static let unimplemented = ContactsEnvironment()
+}
+#endif
+
+public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvironment>
+{ state, action, env in
+  switch action {
+  case .start:
+    return .none
+  }
+}
diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift
new file mode 100644
index 00000000..870889f2
--- /dev/null
+++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift
@@ -0,0 +1,38 @@
+import ComposableArchitecture
+import SwiftUI
+
+public struct ContactsView: View {
+  public init(store: Store<ContactsState, ContactsAction>) {
+    self.store = store
+  }
+
+  let store: Store<ContactsState, ContactsAction>
+
+  struct ViewState: Equatable {
+    init(state: ContactsState) {}
+  }
+
+  public var body: some View {
+    WithViewStore(store.scope(state: ViewState.init)) { viewStore in
+      Form {
+
+      }
+      .navigationTitle("Contacts")
+      .task { viewStore.send(.start) }
+    }
+  }
+}
+
+#if DEBUG
+public struct ContactsView_Previews: PreviewProvider {
+  public static var previews: some View {
+    NavigationView {
+      ContactsView(store: Store(
+        initialState: ContactsState(),
+        reducer: .empty,
+        environment: ()
+      ))
+    }
+  }
+}
+#endif
diff --git a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift
new file mode 100644
index 00000000..3b521768
--- /dev/null
+++ b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift
@@ -0,0 +1,15 @@
+import ComposableArchitecture
+import XCTest
+@testable import ContactsFeature
+
+final class ContactsFeatureTests: XCTestCase {
+  func testStart() {
+    let store = TestStore(
+      initialState: ContactsState(),
+      reducer: contactsReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.start)
+  }
+}
-- 
GitLab


From 42d12de2743b12d44a9843a59f515269b21812ed Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Thu, 8 Sep 2022 13:10:08 +0200
Subject: [PATCH 2/5] Present Contacts from Home

---
 Examples/xx-messenger/Package.swift           |  2 ++
 .../AppFeature/AppEnvironment+Live.swift      |  4 +++
 .../Sources/HomeFeature/HomeFeature.swift     | 28 ++++++++++++++++++-
 .../Sources/HomeFeature/HomeView.swift        | 21 ++++++++++++++
 .../HomeFeatureTests/HomeFeatureTests.swift   | 27 ++++++++++++++++++
 5 files changed, 81 insertions(+), 1 deletion(-)

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 54f5035c..25e6d083 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -73,6 +73,7 @@ let package = Package(
       dependencies: [
         .target(name: "AppCore"),
         .target(name: "ContactFeature"),
+        .target(name: "ContactsFeature"),
         .target(name: "HomeFeature"),
         .target(name: "RegisterFeature"),
         .target(name: "RestoreFeature"),
@@ -133,6 +134,7 @@ let package = Package(
       name: "HomeFeature",
       dependencies: [
         .target(name: "AppCore"),
+        .target(name: "ContactsFeature"),
         .target(name: "RegisterFeature"),
         .target(name: "UserSearchFeature"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index 01e31519..2caa4f0f 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -1,5 +1,6 @@
 import AppCore
 import ContactFeature
+import ContactsFeature
 import Foundation
 import HomeFeature
 import RegisterFeature
@@ -48,6 +49,9 @@ extension AppEnvironment {
               bgQueue: bgQueue
             )
           },
+          contacts: {
+            ContactsEnvironment()
+          },
           userSearch: {
             UserSearchEnvironment(
               messenger: messenger,
diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift
index 9df6d745..faa5757c 100644
--- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift
+++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift
@@ -2,6 +2,7 @@ import AppCore
 import Combine
 import ComposableArchitecture
 import ComposablePresentation
+import ContactsFeature
 import Foundation
 import RegisterFeature
 import UserSearchFeature
@@ -16,6 +17,7 @@ public struct HomeState: Equatable {
     isDeletingAccount: Bool = false,
     alert: AlertState<HomeAction>? = nil,
     register: RegisterState? = nil,
+    contacts: ContactsState? = nil,
     userSearch: UserSearchState? = nil
   ) {
     self.failure = failure
@@ -23,6 +25,7 @@ public struct HomeState: Equatable {
     self.isDeletingAccount = isDeletingAccount
     self.alert = alert
     self.register = register
+    self.contacts = contacts
     self.userSearch = userSearch
   }
 
@@ -32,6 +35,7 @@ public struct HomeState: Equatable {
   public var isDeletingAccount: Bool
   public var alert: AlertState<HomeAction>?
   public var register: RegisterState?
+  public var contacts: ContactsState?
   public var userSearch: UserSearchState?
 }
 
@@ -64,7 +68,10 @@ public enum HomeAction: Equatable {
   case didDismissRegister
   case userSearchButtonTapped
   case didDismissUserSearch
+  case contactsButtonTapped
+  case didDismissContacts
   case register(RegisterAction)
+  case contacts(ContactsAction)
   case userSearch(UserSearchAction)
 }
 
@@ -75,6 +82,7 @@ public struct HomeEnvironment {
     mainQueue: AnySchedulerOf<DispatchQueue>,
     bgQueue: AnySchedulerOf<DispatchQueue>,
     register: @escaping () -> RegisterEnvironment,
+    contacts: @escaping () -> ContactsEnvironment,
     userSearch: @escaping () -> UserSearchEnvironment
   ) {
     self.messenger = messenger
@@ -82,6 +90,7 @@ public struct HomeEnvironment {
     self.mainQueue = mainQueue
     self.bgQueue = bgQueue
     self.register = register
+    self.contacts = contacts
     self.userSearch = userSearch
   }
 
@@ -90,6 +99,7 @@ public struct HomeEnvironment {
   public var mainQueue: AnySchedulerOf<DispatchQueue>
   public var bgQueue: AnySchedulerOf<DispatchQueue>
   public var register: () -> RegisterEnvironment
+  public var contacts: () -> ContactsEnvironment
   public var userSearch: () -> UserSearchEnvironment
 }
 
@@ -100,6 +110,7 @@ extension HomeEnvironment {
     mainQueue: .unimplemented,
     bgQueue: .unimplemented,
     register: { .unimplemented },
+    contacts: { .unimplemented },
     userSearch: { .unimplemented }
   )
 }
@@ -238,11 +249,19 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment>
     state.userSearch = nil
     return .none
 
+  case .contactsButtonTapped:
+    state.contacts = ContactsState()
+    return .none
+
+  case .didDismissContacts:
+    state.contacts = nil
+    return .none
+
   case .register(.finished):
     state.register = nil
     return Effect(value: .messenger(.start))
 
-  case .register(_), .userSearch(_):
+  case .register(_), .contacts(_), .userSearch(_):
     return .none
   }
 }
@@ -253,6 +272,13 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment>
   action: /HomeAction.register,
   environment: { $0.register() }
 )
+.presenting(
+  contactsReducer,
+  state: .keyPath(\.contacts),
+  id: .notNil(),
+  action: /HomeAction.contacts,
+  environment: { $0.contacts() }
+)
 .presenting(
   userSearchReducer,
   state: .keyPath(\.userSearch),
diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift
index e52e8d1b..414bf928 100644
--- a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift
+++ b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift
@@ -1,5 +1,6 @@
 import ComposableArchitecture
 import ComposablePresentation
+import ContactsFeature
 import RegisterFeature
 import SwiftUI
 import UserSearchFeature
@@ -88,6 +89,16 @@ public struct HomeView: View {
           }
 
           Section {
+            Button {
+              viewStore.send(.contactsButtonTapped)
+            } label: {
+              HStack {
+                Text("Contacts")
+                Spacer()
+                Image(systemName: "chevron.forward")
+              }
+            }
+
             Button {
               viewStore.send(.userSearchButtonTapped)
             } label: {
@@ -123,6 +134,16 @@ public struct HomeView: View {
           store.scope(state: \.alert),
           dismiss: HomeAction.didDismissAlert
         )
+        .background(NavigationLinkWithStore(
+          store.scope(
+            state: \.contacts,
+            action: HomeAction.contacts
+          ),
+          onDeactivate: {
+            viewStore.send(.didDismissContacts)
+          },
+          destination: ContactsView.init(store:)
+        ))
         .background(NavigationLinkWithStore(
           store.scope(
             state: \.userSearch,
diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
index 2d7f6daf..89aef4c9 100644
--- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
@@ -1,4 +1,5 @@
 import ComposableArchitecture
+import ContactsFeature
 import RegisterFeature
 import UserSearchFeature
 import XCTest
@@ -464,4 +465,30 @@ final class HomeFeatureTests: XCTestCase {
       $0.userSearch = nil
     }
   }
+
+  func testContactsButtonTapped() {
+    let store = TestStore(
+      initialState: HomeState(),
+      reducer: homeReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.contactsButtonTapped) {
+      $0.contacts = ContactsState()
+    }
+  }
+
+  func testDidDismissContacts() {
+    let store = TestStore(
+      initialState: HomeState(
+        contacts: ContactsState()
+      ),
+      reducer: homeReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.didDismissContacts) {
+      $0.contacts = nil
+    }
+  }
 }
-- 
GitLab


From 300a762ac5ae9885e7969ed3287798547f949c32 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Thu, 8 Sep 2022 13:36:06 +0200
Subject: [PATCH 3/5] Fetch db contacts

---
 .../AppFeature/AppEnvironment+Live.swift      |  6 ++-
 .../ContactsFeature/ContactsFeature.swift     | 43 ++++++++++++++--
 .../ContactsFeature/ContactsView.swift        | 49 +++++++++++++++++--
 .../ContactsFeatureTests.swift                | 32 ++++++++++++
 4 files changed, 123 insertions(+), 7 deletions(-)

diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index 2caa4f0f..8ac88751 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -50,7 +50,11 @@ extension AppEnvironment {
             )
           },
           contacts: {
-            ContactsEnvironment()
+            ContactsEnvironment(
+              db: dbManager.getDB,
+              mainQueue: mainQueue,
+              bgQueue: bgQueue
+            )
           },
           userSearch: {
             UserSearchEnvironment(
diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift
index e7f98fe7..c5780594 100644
--- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift
@@ -1,21 +1,47 @@
+import AppCore
 import ComposableArchitecture
+import Foundation
 import XCTestDynamicOverlay
+import XXModels
 
 public struct ContactsState: Equatable {
-  public init() {}
+  public init(
+    contacts: IdentifiedArrayOf<Contact> = []
+  ) {
+    self.contacts = contacts
+  }
+
+  public var contacts: IdentifiedArrayOf<XXModels.Contact>
 }
 
 public enum ContactsAction: Equatable {
   case start
+  case didFetchContacts([XXModels.Contact])
 }
 
 public struct ContactsEnvironment {
-  public init() {}
+  public init(
+    db: DBManagerGetDB,
+    mainQueue: AnySchedulerOf<DispatchQueue>,
+    bgQueue: AnySchedulerOf<DispatchQueue>
+  ) {
+    self.db = db
+    self.mainQueue = mainQueue
+    self.bgQueue = bgQueue
+  }
+
+  public var db: DBManagerGetDB
+  public var mainQueue: AnySchedulerOf<DispatchQueue>
+  public var bgQueue: AnySchedulerOf<DispatchQueue>
 }
 
 #if DEBUG
 extension ContactsEnvironment {
-  public static let unimplemented = ContactsEnvironment()
+  public static let unimplemented = ContactsEnvironment(
+    db: .unimplemented,
+    mainQueue: .unimplemented,
+    bgQueue: .unimplemented
+  )
 }
 #endif
 
@@ -23,6 +49,17 @@ public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvi
 { state, action, env in
   switch action {
   case .start:
+    return Effect
+      .catching { try env.db() }
+      .flatMap { $0.fetchContactsPublisher(.init()) }
+      .assertNoFailure()
+      .map(ContactsAction.didFetchContacts)
+      .subscribe(on: env.bgQueue)
+      .receive(on: env.mainQueue)
+      .eraseToEffect()
+
+  case .didFetchContacts(let contacts):
+    state.contacts = IdentifiedArray(uniqueElements: contacts)
     return .none
   }
 }
diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift
index 870889f2..ad5d1a37 100644
--- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift
+++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift
@@ -1,5 +1,7 @@
+import AppCore
 import ComposableArchitecture
 import SwiftUI
+import XXModels
 
 public struct ContactsView: View {
   public init(store: Store<ContactsState, ContactsAction>) {
@@ -9,13 +11,36 @@ public struct ContactsView: View {
   let store: Store<ContactsState, ContactsAction>
 
   struct ViewState: Equatable {
-    init(state: ContactsState) {}
+    var contacts: IdentifiedArrayOf<XXModels.Contact>
+
+    init(state: ContactsState) {
+      contacts = state.contacts
+    }
   }
 
   public var body: some View {
     WithViewStore(store.scope(state: ViewState.init)) { viewStore in
       Form {
-
+        ForEach(viewStore.contacts) { contact in
+          Section {
+            Button {
+              // TODO:
+            } label: {
+              HStack {
+                VStack(alignment: .leading, spacing: 8) {
+                  Label(contact.username ?? "", systemImage: "person")
+                  Label(contact.email ?? "", systemImage: "envelope")
+                  Label(contact.phone ?? "", systemImage: "phone")
+                }
+                .font(.callout)
+                .tint(Color.primary)
+                Spacer()
+                Image(systemName: "chevron.forward")
+              }
+            }
+            ContactAuthStatusView(contact.authStatus)
+          }
+        }
       }
       .navigationTitle("Contacts")
       .task { viewStore.send(.start) }
@@ -28,7 +53,25 @@ public struct ContactsView_Previews: PreviewProvider {
   public static var previews: some View {
     NavigationView {
       ContactsView(store: Store(
-        initialState: ContactsState(),
+        initialState: ContactsState(
+          contacts: [
+            .init(
+              id: "1".data(using: .utf8)!,
+              username: "John Doe",
+              email: "john@doe.com",
+              phone: "+1234567890",
+              authStatus: .friend
+            ),
+            .init(
+              id: "2".data(using: .utf8)!,
+              username: "Alice Unknown",
+              authStatus: .requested
+            ),
+            .init(
+              id: "3".data(using: .utf8)!
+            ),
+          ]
+        ),
         reducer: .empty,
         environment: ()
       ))
diff --git a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift
index 3b521768..04533b34 100644
--- a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift
@@ -1,5 +1,8 @@
+import Combine
 import ComposableArchitecture
+import CustomDump
 import XCTest
+import XXModels
 @testable import ContactsFeature
 
 final class ContactsFeatureTests: XCTestCase {
@@ -10,6 +13,35 @@ final class ContactsFeatureTests: XCTestCase {
       environment: .unimplemented
     )
 
+    var didFetchContacts: [XXModels.Contact.Query] = []
+    let contactsPublisher = PassthroughSubject<[XXModels.Contact], Error>()
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.db.run = {
+      var db: Database = .failing
+      db.fetchContactsPublisher.run = { query in
+        didFetchContacts.append(query)
+        return contactsPublisher.eraseToAnyPublisher()
+      }
+      return db
+    }
+
     store.send(.start)
+
+    XCTAssertNoDifference(didFetchContacts, [XXModels.Contact.Query()])
+
+    let contacts: [XXModels.Contact] = [
+      .init(id: "1".data(using: .utf8)!),
+      .init(id: "2".data(using: .utf8)!),
+      .init(id: "3".data(using: .utf8)!),
+    ]
+    contactsPublisher.send(contacts)
+
+    store.receive(.didFetchContacts(contacts)) {
+      $0.contacts = IdentifiedArray(uniqueElements: contacts)
+    }
+
+    contactsPublisher.send(completion: .finished)
   }
 }
-- 
GitLab


From d0f0e6e06c8bc1182c07ed2b76f5043c9e52577e Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Thu, 8 Sep 2022 13:46:46 +0200
Subject: [PATCH 4/5] Present Contact from Contacts

---
 Examples/xx-messenger/Package.swift           |  1 +
 .../AppFeature/AppEnvironment+Live.swift      | 35 +++++++++---------
 .../ContactsFeature/ContactsFeature.swift     | 36 +++++++++++++++++--
 .../ContactsFeature/ContactsView.swift        | 12 ++++++-
 .../ContactsFeatureTests.swift                | 32 +++++++++++++++++
 5 files changed, 95 insertions(+), 21 deletions(-)

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 25e6d083..db3097ee 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -117,6 +117,7 @@ let package = Package(
       name: "ContactsFeature",
       dependencies: [
         .target(name: "AppCore"),
+        .target(name: "ContactFeature"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
         .product(name: "ComposablePresentation", package: "swift-composable-presentation"),
         .product(name: "XXModels", package: "client-ios-db"),
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index 8ac88751..f57003f0 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -19,6 +19,21 @@ extension AppEnvironment {
     let mainQueue = DispatchQueue.main.eraseToAnyScheduler()
     let bgQueue = DispatchQueue.global(qos: .background).eraseToAnyScheduler()
 
+    let contactEnvironment = ContactEnvironment(
+      messenger: messenger,
+      db: dbManager.getDB,
+      mainQueue: mainQueue,
+      bgQueue: bgQueue,
+      sendRequest: {
+        SendRequestEnvironment(
+          messenger: messenger,
+          db: dbManager.getDB,
+          mainQueue: mainQueue,
+          bgQueue: bgQueue
+        )
+      }
+    )
+
     return AppEnvironment(
       dbManager: dbManager,
       messenger: messenger,
@@ -53,7 +68,8 @@ extension AppEnvironment {
             ContactsEnvironment(
               db: dbManager.getDB,
               mainQueue: mainQueue,
-              bgQueue: bgQueue
+              bgQueue: bgQueue,
+              contact: { contactEnvironment }
             )
           },
           userSearch: {
@@ -61,22 +77,7 @@ extension AppEnvironment {
               messenger: messenger,
               mainQueue: mainQueue,
               bgQueue: bgQueue,
-              contact: {
-                ContactEnvironment(
-                  messenger: messenger,
-                  db: dbManager.getDB,
-                  mainQueue: mainQueue,
-                  bgQueue: bgQueue,
-                  sendRequest: {
-                    SendRequestEnvironment(
-                      messenger: messenger,
-                      db: dbManager.getDB,
-                      mainQueue: mainQueue,
-                      bgQueue: bgQueue
-                    )
-                  }
-                )
-              }
+              contact: { contactEnvironment }
             )
           }
         )
diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift
index c5780594..79690934 100644
--- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift
@@ -1,38 +1,49 @@
 import AppCore
 import ComposableArchitecture
+import ComposablePresentation
+import ContactFeature
 import Foundation
 import XCTestDynamicOverlay
 import XXModels
 
 public struct ContactsState: Equatable {
   public init(
-    contacts: IdentifiedArrayOf<Contact> = []
+    contacts: IdentifiedArrayOf<Contact> = [],
+    contact: ContactState? = nil
   ) {
     self.contacts = contacts
+    self.contact = contact
   }
 
   public var contacts: IdentifiedArrayOf<XXModels.Contact>
+  public var contact: ContactState?
 }
 
 public enum ContactsAction: Equatable {
   case start
   case didFetchContacts([XXModels.Contact])
+  case contactSelected(XXModels.Contact)
+  case contactDismissed
+  case contact(ContactAction)
 }
 
 public struct ContactsEnvironment {
   public init(
     db: DBManagerGetDB,
     mainQueue: AnySchedulerOf<DispatchQueue>,
-    bgQueue: AnySchedulerOf<DispatchQueue>
+    bgQueue: AnySchedulerOf<DispatchQueue>,
+    contact: @escaping () -> ContactEnvironment
   ) {
     self.db = db
     self.mainQueue = mainQueue
     self.bgQueue = bgQueue
+    self.contact = contact
   }
 
   public var db: DBManagerGetDB
   public var mainQueue: AnySchedulerOf<DispatchQueue>
   public var bgQueue: AnySchedulerOf<DispatchQueue>
+  public var contact: () -> ContactEnvironment
 }
 
 #if DEBUG
@@ -40,7 +51,8 @@ extension ContactsEnvironment {
   public static let unimplemented = ContactsEnvironment(
     db: .unimplemented,
     mainQueue: .unimplemented,
-    bgQueue: .unimplemented
+    bgQueue: .unimplemented,
+    contact: { .unimplemented }
   )
 }
 #endif
@@ -61,5 +73,23 @@ public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvi
   case .didFetchContacts(let contacts):
     state.contacts = IdentifiedArray(uniqueElements: contacts)
     return .none
+
+  case .contactSelected(let contact):
+    state.contact = ContactState(id: contact.id, dbContact: contact)
+    return .none
+
+  case .contactDismissed:
+    state.contact = nil
+    return .none
+
+  case .contact(_):
+    return .none
   }
 }
+.presenting(
+  contactReducer,
+  state: .keyPath(\.contact),
+  id: .keyPath(\.?.id),
+  action: /ContactsAction.contact,
+  environment: { $0.contact() }
+)
diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift
index ad5d1a37..7b02efd1 100644
--- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift
+++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift
@@ -1,5 +1,7 @@
 import AppCore
 import ComposableArchitecture
+import ComposablePresentation
+import ContactFeature
 import SwiftUI
 import XXModels
 
@@ -24,7 +26,7 @@ public struct ContactsView: View {
         ForEach(viewStore.contacts) { contact in
           Section {
             Button {
-              // TODO:
+              viewStore.send(.contactSelected(contact))
             } label: {
               HStack {
                 VStack(alignment: .leading, spacing: 8) {
@@ -44,6 +46,14 @@ public struct ContactsView: View {
       }
       .navigationTitle("Contacts")
       .task { viewStore.send(.start) }
+      .background(NavigationLinkWithStore(
+        store.scope(
+          state: \.contact,
+          action: ContactsAction.contact
+        ),
+        onDeactivate: { viewStore.send(.contactDismissed) },
+        destination: ContactView.init(store:)
+      ))
     }
   }
 }
diff --git a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift
index 04533b34..6c68f406 100644
--- a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift
@@ -1,5 +1,6 @@
 import Combine
 import ComposableArchitecture
+import ContactFeature
 import CustomDump
 import XCTest
 import XXModels
@@ -44,4 +45,35 @@ final class ContactsFeatureTests: XCTestCase {
 
     contactsPublisher.send(completion: .finished)
   }
+
+  func testSelectContact() {
+    let store = TestStore(
+      initialState: ContactsState(),
+      reducer: contactsReducer,
+      environment: .unimplemented
+    )
+
+    let contact = XXModels.Contact(id: "id".data(using: .utf8)!)
+
+    store.send(.contactSelected(contact)) {
+      $0.contact = ContactState(id: contact.id, dbContact: contact)
+    }
+  }
+
+  func testDismissContact() {
+    let store = TestStore(
+      initialState: ContactsState(
+        contact: ContactState(
+          id: "id".data(using: .utf8)!,
+          dbContact: Contact(id: "id".data(using: .utf8)!)
+        )
+      ),
+      reducer: contactsReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.contactDismissed) {
+      $0.contact = nil
+    }
+  }
 }
-- 
GitLab


From 844924bdcba1503cd0fba77e7a04cd765132129c Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Thu, 8 Sep 2022 14:03:17 +0200
Subject: [PATCH 5/5] Highlight my contact on the list

---
 Examples/xx-messenger/Package.swift           |  2 +
 .../AppFeature/AppEnvironment+Live.swift      |  1 +
 .../ContactsFeature/ContactsFeature.swift     | 18 +++++++-
 .../ContactsFeature/ContactsView.swift        | 44 +++++++++++++------
 .../ContactsFeatureTests.swift                | 22 +++++++++-
 5 files changed, 69 insertions(+), 18 deletions(-)

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index db3097ee..5df8eb39 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -120,6 +120,8 @@ let package = Package(
         .target(name: "ContactFeature"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
         .product(name: "ComposablePresentation", package: "swift-composable-presentation"),
+        .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 f57003f0..61bfba1f 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -66,6 +66,7 @@ extension AppEnvironment {
           },
           contacts: {
             ContactsEnvironment(
+              messenger: messenger,
               db: dbManager.getDB,
               mainQueue: mainQueue,
               bgQueue: bgQueue,
diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift
index 79690934..1ded89de 100644
--- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift
@@ -4,17 +4,22 @@ import ComposablePresentation
 import ContactFeature
 import Foundation
 import XCTestDynamicOverlay
+import XXClient
+import XXMessengerClient
 import XXModels
 
 public struct ContactsState: Equatable {
   public init(
-    contacts: IdentifiedArrayOf<Contact> = [],
+    myId: Data? = nil,
+    contacts: IdentifiedArrayOf<XXModels.Contact> = [],
     contact: ContactState? = nil
   ) {
+    self.myId = myId
     self.contacts = contacts
     self.contact = contact
   }
 
+  public var myId: Data?
   public var contacts: IdentifiedArrayOf<XXModels.Contact>
   public var contact: ContactState?
 }
@@ -29,17 +34,20 @@ public enum ContactsAction: Equatable {
 
 public struct ContactsEnvironment {
   public init(
+    messenger: Messenger,
     db: DBManagerGetDB,
     mainQueue: AnySchedulerOf<DispatchQueue>,
     bgQueue: AnySchedulerOf<DispatchQueue>,
     contact: @escaping () -> ContactEnvironment
   ) {
+    self.messenger = messenger
     self.db = db
     self.mainQueue = mainQueue
     self.bgQueue = bgQueue
     self.contact = contact
   }
 
+  public var messenger: Messenger
   public var db: DBManagerGetDB
   public var mainQueue: AnySchedulerOf<DispatchQueue>
   public var bgQueue: AnySchedulerOf<DispatchQueue>
@@ -49,6 +57,7 @@ public struct ContactsEnvironment {
 #if DEBUG
 extension ContactsEnvironment {
   public static let unimplemented = ContactsEnvironment(
+    messenger: .unimplemented,
     db: .unimplemented,
     mainQueue: .unimplemented,
     bgQueue: .unimplemented,
@@ -61,6 +70,7 @@ public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvi
 { state, action, env in
   switch action {
   case .start:
+    state.myId = try? env.messenger.e2e.tryGet().getContact().getId()
     return Effect
       .catching { try env.db() }
       .flatMap { $0.fetchContactsPublisher(.init()) }
@@ -70,7 +80,11 @@ public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvi
       .receive(on: env.mainQueue)
       .eraseToEffect()
 
-  case .didFetchContacts(let contacts):
+  case .didFetchContacts(var contacts):
+    if let myId = state.myId,
+       let myIndex = contacts.firstIndex(where: { $0.id == myId }) {
+      contacts.move(fromOffsets: [myIndex], toOffset: contacts.startIndex)
+    }
     state.contacts = IdentifiedArray(uniqueElements: contacts)
     return .none
 
diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift
index 7b02efd1..b7798685 100644
--- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift
+++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift
@@ -13,9 +13,11 @@ public struct ContactsView: View {
   let store: Store<ContactsState, ContactsAction>
 
   struct ViewState: Equatable {
+    var myId: Data?
     var contacts: IdentifiedArrayOf<XXModels.Contact>
 
     init(state: ContactsState) {
+      myId = state.myId
       contacts = state.contacts
     }
   }
@@ -24,23 +26,37 @@ public struct ContactsView: View {
     WithViewStore(store.scope(state: ViewState.init)) { viewStore in
       Form {
         ForEach(viewStore.contacts) { contact in
-          Section {
-            Button {
-              viewStore.send(.contactSelected(contact))
-            } label: {
-              HStack {
-                VStack(alignment: .leading, spacing: 8) {
-                  Label(contact.username ?? "", systemImage: "person")
-                  Label(contact.email ?? "", systemImage: "envelope")
-                  Label(contact.phone ?? "", systemImage: "phone")
+          if contact.id == viewStore.myId {
+            Section {
+              VStack(alignment: .leading, spacing: 8) {
+                Label(contact.username ?? "", systemImage: "person")
+                Label(contact.email ?? "", systemImage: "envelope")
+                Label(contact.phone ?? "", systemImage: "phone")
+              }
+              .font(.callout)
+              .tint(Color.primary)
+            } header: {
+              Text("My contact")
+            }
+          } else {
+            Section {
+              Button {
+                viewStore.send(.contactSelected(contact))
+              } label: {
+                HStack {
+                  VStack(alignment: .leading, spacing: 8) {
+                    Label(contact.username ?? "", systemImage: "person")
+                    Label(contact.email ?? "", systemImage: "envelope")
+                    Label(contact.phone ?? "", systemImage: "phone")
+                  }
+                  .font(.callout)
+                  .tint(Color.primary)
+                  Spacer()
+                  Image(systemName: "chevron.forward")
                 }
-                .font(.callout)
-                .tint(Color.primary)
-                Spacer()
-                Image(systemName: "chevron.forward")
               }
+              ContactAuthStatusView(contact.authStatus)
             }
-            ContactAuthStatusView(contact.authStatus)
           }
         }
       }
diff --git a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift
index 6c68f406..a0c0291e 100644
--- a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift
@@ -3,6 +3,8 @@ import ComposableArchitecture
 import ContactFeature
 import CustomDump
 import XCTest
+import XXClient
+import XXMessengerClient
 import XXModels
 @testable import ContactsFeature
 
@@ -14,11 +16,21 @@ final class ContactsFeatureTests: XCTestCase {
       environment: .unimplemented
     )
 
+    let myId = "2".data(using: .utf8)!
     var didFetchContacts: [XXModels.Contact.Query] = []
     let contactsPublisher = PassthroughSubject<[XXModels.Contact], 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 myId }
+        return contact
+      }
+      return e2e
+    }
     store.environment.db.run = {
       var db: Database = .failing
       db.fetchContactsPublisher.run = { query in
@@ -28,7 +40,9 @@ final class ContactsFeatureTests: XCTestCase {
       return db
     }
 
-    store.send(.start)
+    store.send(.start) {
+      $0.myId = myId
+    }
 
     XCTAssertNoDifference(didFetchContacts, [XXModels.Contact.Query()])
 
@@ -40,7 +54,11 @@ final class ContactsFeatureTests: XCTestCase {
     contactsPublisher.send(contacts)
 
     store.receive(.didFetchContacts(contacts)) {
-      $0.contacts = IdentifiedArray(uniqueElements: contacts)
+      $0.contacts = IdentifiedArray(uniqueElements: [
+        contacts[1],
+        contacts[0],
+        contacts[2],
+      ])
     }
 
     contactsPublisher.send(completion: .finished)
-- 
GitLab