Skip to content
Snippets Groups Projects
UserSearchFeature.swift 3.39 KiB
Newer Older
import ComposableArchitecture
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 struct Result: Equatable, Identifiable {
    public init(
      id: Data,
      contact: Contact,
      username: String? = nil,
      email: String? = nil,
      phone: String? = nil
    ) {
      self.id = id
      self.contact = contact
      self.username = username
      self.email = email
      self.phone = phone
    }

    public var id: Data
    public var contact: XXClient.Contact
    public var username: String?
    public var email: String?
    public var phone: String?
  }

  public init(
    focusedField: Field? = nil,
    query: MessengerSearchUsers.Query = .init(),
    isSearching: Bool = false,
    failure: String? = nil,
    results: IdentifiedArrayOf<Result> = []
  ) {
    self.focusedField = focusedField
    self.query = query
    self.isSearching = isSearching
    self.failure = failure
    self.results = results
  }

  @BindableState public var focusedField: Field?
  @BindableState public var query: MessengerSearchUsers.Query
  public var isSearching: Bool
  public var failure: String?
  public var results: IdentifiedArrayOf<Result>
Dariusz Rybicki's avatar
Dariusz Rybicki committed
public enum UserSearchAction: Equatable, BindableAction {
  case searchTapped
  case didFail(String)
  case didSucceed([Contact])
  case binding(BindingAction<UserSearchState>)
}

public struct UserSearchEnvironment {
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  public init(
    messenger: Messenger,
    mainQueue: AnySchedulerOf<DispatchQueue>,
    bgQueue: AnySchedulerOf<DispatchQueue>
  ) {
    self.messenger = messenger
    self.mainQueue = mainQueue
    self.bgQueue = bgQueue
  }

  public var messenger: Messenger
  public var mainQueue: AnySchedulerOf<DispatchQueue>
  public var bgQueue: AnySchedulerOf<DispatchQueue>
}

#if DEBUG
extension UserSearchEnvironment {
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  public static let unimplemented = UserSearchEnvironment(
    messenger: .unimplemented,
    mainQueue: .unimplemented,
    bgQueue: .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 }
      let facts = (try? contact.getFacts()) ?? []
      return UserSearchState.Result(
        id: id,
        contact: contact,
        username: facts.first(where: { $0.type == 0 })?.fact,
        email: facts.first(where: { $0.type == 1 })?.fact,
        phone: facts.first(where: { $0.type == 2 })?.fact
      )
    })
    return .none

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

  case .binding(_):
    return .none
  }
}
.binding()