Skip to content
Snippets Groups Projects
UserSearchFeature.swift 3.85 KiB
Newer Older
import ComposableArchitecture
import ComposablePresentation
import ContactFeature
Dariusz Rybicki's avatar
Dariusz Rybicki committed
import Foundation
import XCTestDynamicOverlay
Dariusz Rybicki's avatar
Dariusz Rybicki committed
import XXClient
import XXMessengerClient

public struct UserSearchState: Equatable {
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  public enum Field: String, Hashable {
    case username
    case email
    case phone
  }

  public init(
    focusedField: Field? = nil,
    query: MessengerSearchUsers.Query = .init(),
    isSearching: Bool = false,
    failure: String? = nil,
    results: IdentifiedArrayOf<UserSearchResultState> = [],
    contact: ContactState? = nil
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  ) {
    self.focusedField = focusedField
    self.query = query
    self.isSearching = isSearching
    self.failure = failure
    self.results = results
    self.contact = contact
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  }

  @BindableState public var focusedField: Field?
  @BindableState public var query: MessengerSearchUsers.Query
  public var isSearching: Bool
  public var failure: String?
  public var results: IdentifiedArrayOf<UserSearchResultState>
  public var contact: ContactState?
Dariusz Rybicki's avatar
Dariusz Rybicki committed
public enum UserSearchAction: Equatable, BindableAction {
  case searchTapped
  case didFail(String)
  case didSucceed([Contact])
  case didDismissContact
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  case binding(BindingAction<UserSearchState>)
  case result(id: UserSearchResultState.ID, action: UserSearchResultAction)
  case contact(ContactAction)

public struct UserSearchEnvironment {
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  public init(
    messenger: Messenger,
    mainQueue: AnySchedulerOf<DispatchQueue>,
    bgQueue: AnySchedulerOf<DispatchQueue>,
    result: @escaping () -> UserSearchResultEnvironment,
    contact: @escaping () -> ContactEnvironment
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  ) {
    self.messenger = messenger
    self.mainQueue = mainQueue
    self.bgQueue = bgQueue
    self.contact = contact
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  }

  public var messenger: Messenger
  public var mainQueue: AnySchedulerOf<DispatchQueue>
  public var bgQueue: AnySchedulerOf<DispatchQueue>
  public var result: () -> UserSearchResultEnvironment
  public var contact: () -> ContactEnvironment
}

#if DEBUG
extension UserSearchEnvironment {
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  public static let unimplemented = UserSearchEnvironment(
    messenger: .unimplemented,
    mainQueue: .unimplemented,
    bgQueue: .unimplemented,
    result: { .unimplemented },
    contact: { .unimplemented }
Dariusz Rybicki's avatar
Dariusz Rybicki committed
public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSearchEnvironment>
{ state, action, env in
  switch action {
  case .searchTapped:
    state.focusedField = nil
    state.isSearching = true
    state.results = []
    state.failure = nil
    return .result { [query = state.query] in
      do {
        return .success(.didSucceed(try env.messenger.searchUsers(query: query)))
      } catch {
        return .success(.didFail(error.localizedDescription))
      }
    }
    .subscribe(on: env.bgQueue)
    .receive(on: env.mainQueue)
    .eraseToEffect()

  case .didSucceed(let contacts):
    state.isSearching = false
    state.failure = nil
    state.results = IdentifiedArray(uniqueElements: contacts.compactMap { contact in
      guard let id = try? contact.getId() else { return nil }
Dariusz Rybicki's avatar
Dariusz Rybicki committed
      return UserSearchResultState(id: id, xxContact: contact)
Dariusz Rybicki's avatar
Dariusz Rybicki committed
    })
    return .none

  case .didFail(let failure):
    state.isSearching = false
    state.failure = failure
    state.results = []
    return .none

  case .didDismissContact:
    state.contact = nil
    return .none

  case .result(let id, action: .tapped):
Dariusz Rybicki's avatar
Dariusz Rybicki committed
    state.contact = ContactState(
      id: id,
      xxContact: state.results[id: id]?.xxContact
    )
    return .none

  case .binding(_), .result(_, _), .contact(_):
Dariusz Rybicki's avatar
Dariusz Rybicki committed
    return .none
  }
}
.binding()
.presenting(
  forEach: userSearchResultReducer,
  state: \.results,
  action: /UserSearchAction.result(id:action:),
  environment: { $0.result() }
)
.presenting(
  contactReducer,
  state: .keyPath(\.contact),
  id: .notNil(), // TODO: use Contact.ID
  action: /UserSearchAction.contact,
  environment: { $0.contact() }
)