Skip to content
Snippets Groups Projects
ChatInputView.swift 8.31 KiB
Newer Older
Bruno Muniz's avatar
Bruno Muniz committed
import UIKit
import Shared
import Combine
import CasePaths
import Voxophone
import ComposableArchitecture

public final class ChatInputView: UIToolbar {

    public init(store: Store<ChatInputState, ChatInputAction>) {
        self.store = store
        self.viewStore = ViewStore(store)
        super.init(frame: .zero)

        setup()
        observeStore()
        setupUIActions()
        viewStore.send(.setup)
    }

    required init?(coder: NSCoder) { nil }

    deinit {
        viewStore.send(.destroy)
    }

    public func setMaxHeight(_ function: @escaping () -> CGFloat) {
        text.maxHeight = function
    }

    public func setupReply(message: String, sender: String) {
        viewStore.send(.text(.didTriggerReply(message, sender)))
    }

    let store: Store<ChatInputState, ChatInputAction>
    let viewStore: ViewStore<ChatInputState, ChatInputAction>
    private var cancellables: Set<AnyCancellable> = []

    let stack = UIStackView()
    let text = TextInputView()
    let audio = AudioView()
    let actions = ActionsView()

    private func setup() {
        isTranslucent = false
        translatesAutoresizingMaskIntoConstraints = false
        barTintColor = Asset.neutralWhite.color

        stack.axis = .vertical
        stack.spacing = 8
        stack.addArrangedSubview(text)
        stack.addArrangedSubview(audio)
        stack.addArrangedSubview(actions)

        addSubview(stack)
        stack.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            stack.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 8),
            stack.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 8),
            stack.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -8),
            stack.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -8),
        ])
    }

    private func observeStore() {
        viewStore.publisher
            .map(\.isPresentingActions)
            .combineLatest(viewStore.publisher.map(\.canAddAttachments))
            .sink { [unowned self] isPresentingActions, canAddAttachments in
                if canAddAttachments {
                    text.showActionsButton.isHidden = isPresentingActions
                    text.hideActionsButton.isHidden = !isPresentingActions
                    actions.isHidden = !isPresentingActions
                } else {
                    text.showActionsButton.isHidden = true
                    text.hideActionsButton.isHidden = true
                    actions.isHidden = true
                }
            }
            .store(in: &cancellables)

        viewStore.publisher
            .map(\.reply)
            .sink { [unowned self] reply in
                guard let reply = reply else {
                    text.replyView.isHidden = true
                    return
                }

                text.replyView.isHidden = false
                text.replyView.messageLabel.text = reply.text
                text.replyView.nameLabel.text = reply.name
            }.store(in: &cancellables)

        viewStore.publisher
            .map(\.audio)
            .map { $0 != nil }
            .sink { [unowned self] in
                text.isHidden = $0
                audio.isHidden = !$0
            }
            .store(in: &cancellables)

        viewStore.publisher
            .map(\.text.isEmpty)
            .combineLatest(viewStore.publisher.map(\.canAddAttachments))
            .sink { [unowned self] textIsEmpty, canAddAttachments in
                if canAddAttachments {
                    text.sendButton.isHidden = textIsEmpty
                    text.audioButton.isHidden = !textIsEmpty
                } else {
                    text.sendButton.isHidden = false
                    text.audioButton.isHidden = true
                }

                text.sendButton.isEnabled = !textIsEmpty
                text.placeholderView.isHidden = !textIsEmpty
            }
            .store(in: &cancellables)

        viewStore.publisher
            .map(\.text)
            .sink { [unowned self] in
                if text.textView.markedTextRange == nil {
                    text.textView.text = $0
                } else if $0 == "" {
                    text.textView.text = $0
                }

                text.updateHeight()
            }.store(in: &cancellables)

        let timeFormatter = DateComponentsFormatter()
        timeFormatter.unitsStyle = .positional
        timeFormatter.allowedUnits = [.minute, .second]
        timeFormatter.zeroFormattingBehavior = .pad

        viewStore.publisher
            .map(\.audio)
            .sink { [unowned self] in
                switch $0 {
                case let .idle(_, duration):
                    audio.playButton.isHidden = false
                    audio.stopPlaybackButton.isHidden = true
                    audio.stopRecordingButton.isHidden = true
                    audio.sendButton.isHidden = false
                    audio.timeLabel.text = timeFormatter.string(from: duration)

                case let .recording(_, time):
                    audio.playButton.isHidden = true
                    audio.stopPlaybackButton.isHidden = true
                    audio.stopRecordingButton.isHidden = false
                    audio.sendButton.isHidden = true
                    audio.timeLabel.text = timeFormatter.string(from: time)

                case let .playing(_, _, time):
                    audio.playButton.isHidden = true
                    audio.stopPlaybackButton.isHidden = false
                    audio.stopRecordingButton.isHidden = true
                    audio.sendButton.isHidden = false
                    audio.timeLabel.text = timeFormatter.string(from: time)

                case .none:
                    audio.playButton.isHidden = true
                    audio.stopPlaybackButton.isHidden = true
                    audio.stopRecordingButton.isHidden = true
                    audio.sendButton.isHidden = true
                    audio.timeLabel.text = ""
                }
            }
            .store(in: &cancellables)
    }

    private func setupUIActions() {
        text.textDidChange = { [unowned self] text in viewStore.send(.text(.didUpdate(text))) }

        text.replyView.abortButton.publisher(for: .touchUpInside)
            .sink { [unowned self] in viewStore.send(.text(.didTapAbortReply)) }
            .store(in: &cancellables)

        text.showActionsButton.publisher(for: .touchUpInside)
            .sink { [unowned self] in viewStore.send(.text(.didTapShowActions)) }
            .store(in: &cancellables)

        text.hideActionsButton.publisher(for: .touchUpInside)
            .sink { [unowned self] in viewStore.send(.text(.didTapHideActions)) }
            .store(in: &cancellables)

        text.sendButton.publisher(for: .touchUpInside)
            .sink { [unowned self] in viewStore.send(.text(.didTapSend)) }
            .store(in: &cancellables)

        text.audioButton.publisher(for: .touchUpInside)
            .sink { [unowned self] in viewStore.send(.text(.didTapAudio)) }
            .store(in: &cancellables)

        audio.cancelButton.publisher(for: .touchUpInside)
            .sink { [unowned self] in viewStore.send(.audio(.didTapCancel)) }
            .store(in: &cancellables)

        audio.playButton.publisher(for: .touchUpInside)
            .sink { [unowned self] in viewStore.send(.audio(.didTapPlay)) }
            .store(in: &cancellables)

        audio.stopPlaybackButton.publisher(for: .touchUpInside)
            .sink { [unowned self] in viewStore.send(.audio(.didTapStopPlayback)) }
            .store(in: &cancellables)

        audio.stopRecordingButton.publisher(for: .touchUpInside)
            .sink { [unowned self] in viewStore.send(.audio(.didTapStopRecording)) }
            .store(in: &cancellables)

        audio.sendButton.publisher(for: .touchUpInside)
            .sink { [unowned self] in viewStore.send(.audio(.didTapSend)) }
            .store(in: &cancellables)

        actions.libraryButton.publisher(for: .touchUpInside)
            .sink { [unowned self] in viewStore.send(.actions(.didTapLibrary)) }
            .store(in: &cancellables)

        actions.cameraButton.publisher(for: .touchUpInside)
            .sink { [unowned self] in viewStore.send(.actions(.didTapCamera)) }
            .store(in: &cancellables)
    }
}