Skip to content
Snippets Groups Projects
ChatFeature.swift 4.22 KiB
Newer Older
import AppCore
import Combine
import ComposableArchitecture
import Foundation
import XCTestDynamicOverlay
import XXClient
import XXMessengerClient
import XXModels

public struct ChatState: Equatable, Identifiable {
  public enum ID: Equatable, Hashable {
    case contact(Data)
  }

  public struct Message: Equatable, Identifiable {
    public init(
      id: Int64,
      date: Date,
      senderId: Data,
      text: String,
      status: XXModels.Message.Status
    ) {
      self.id = id
      self.date = date
      self.senderId = senderId
      self.text = text
      self.status = status
    public var id: Int64
    public var date: Date
    public var senderId: Data
    public var text: String
    public var status: XXModels.Message.Status
  }

  public init(
    id: ID,
    myContactId: Data? = nil,
    messages: IdentifiedArrayOf<Message> = [],
    failure: String? = nil,
    text: String = ""
    self.id = id
    self.myContactId = myContactId
    self.messages = messages
    self.failure = failure
    self.text = text
  }

  public var id: ID
  public var myContactId: Data?
  public var messages: IdentifiedArrayOf<Message>
  public var failure: String?
  @BindableState public var text: String
public enum ChatAction: Equatable, BindableAction {
  case start
  case didFetchMessages(IdentifiedArrayOf<ChatState.Message>)
  case sendTapped
  case binding(BindingAction<ChatState>)
}

public struct ChatEnvironment {
  public init(
    messenger: Messenger,
    db: DBManagerGetDB,
    sendMessage: SendMessage,
    mainQueue: AnySchedulerOf<DispatchQueue>,
    bgQueue: AnySchedulerOf<DispatchQueue>
  ) {
    self.messenger = messenger
    self.db = db
    self.sendMessage = sendMessage
    self.mainQueue = mainQueue
    self.bgQueue = bgQueue
  }

  public var messenger: Messenger
  public var db: DBManagerGetDB
  public var sendMessage: SendMessage
  public var mainQueue: AnySchedulerOf<DispatchQueue>
  public var bgQueue: AnySchedulerOf<DispatchQueue>
}

#if DEBUG
extension ChatEnvironment {
  public static let unimplemented = ChatEnvironment(
    messenger: .unimplemented,
    db: .unimplemented,
    sendMessage: .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()
      state.myContactId = myContactId
      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,
              status: message.status
            )
          }
        }
        .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

  case .sendTapped:
    let text = state.text
    let chatId = state.id
    state.text = ""
    return Effect.run { subscriber in
      switch chatId {
      case .contact(let recipientId):
        env.sendMessage(
          text: text,
          to: recipientId,
          onError: { error in
            // TODO: handle error
            print("^^^ ERROR: \(error)")
          }
        )
      }
      subscriber.send(completion: .finished)
      return AnyCancellable {}
    }
    .subscribe(on: env.bgQueue)
    .receive(on: env.mainQueue)
    .eraseToEffect()

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