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) } }