diff --git a/Data Sources/AutocompleteDataSource.swift b/Data Sources/AutocompleteDataSource.swift index 00f4c85..888f2d5 100644 --- a/Data Sources/AutocompleteDataSource.swift +++ b/Data Sources/AutocompleteDataSource.swift @@ -81,6 +81,12 @@ final class AutocompleteDataSource: UICollectionViewDiffableDataSource private let viewModel: CompositionViewModel private let parentViewModel: NewStatusViewModel @@ -17,6 +18,7 @@ final class CompositionInputAccessoryView: UIView { collectionViewLayout: CompositionInputAccessoryView.autocompleteLayout()) private let autocompleteDataSource: AutocompleteDataSource private let autocompleteCollectionViewHeightConstraint: NSLayoutConstraint + private let autocompleteSelectionsSubject = PassthroughSubject() private var cancellables = Set() init(viewModel: CompositionViewModel, @@ -29,7 +31,8 @@ final class CompositionInputAccessoryView: UIView { queryPublisher: autocompleteQueryPublisher, parentViewModel: parentViewModel) autocompleteCollectionViewHeightConstraint = - autocompleteCollectionView.heightAnchor.constraint(equalToConstant: .minimumButtonDimension) + autocompleteCollectionView.heightAnchor.constraint(equalToConstant: .hairline) + autocompleteSelections = autocompleteSelectionsSubject.eraseToAnyPublisher() super.init( frame: .init( @@ -43,6 +46,12 @@ final class CompositionInputAccessoryView: UIView { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func didMoveToSuperview() { + super.didMoveToSuperview() + + layoutIfNeeded() + } } private extension CompositionInputAccessoryView { @@ -241,6 +250,46 @@ private extension CompositionInputAccessoryView { extension CompositionInputAccessoryView: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { collectionView.deselectItem(at: indexPath, animated: true) + + guard let item = autocompleteDataSource.itemIdentifier(for: indexPath) else { return } + + switch item { + case let .account(account): + autocompleteSelectionsSubject.send("@".appending(account.acct)) + case let .tag(tag): + autocompleteSelectionsSubject.send("#".appending(tag.name)) + case let .emoji(emoji): + let escaped = emoji.applyingDefaultSkinTone(identityContext: parentViewModel.identityContext).escaped + + autocompleteSelectionsSubject.send(escaped) + autocompleteDataSource.updateUse(emoji: emoji) + } + + UISelectionFeedbackGenerator().selectionChanged() + + // To dismiss without waiting for the throttle + UIView.animate(withDuration: .zeroIfReduceMotion(.shortAnimationDuration)) { + self.setAutocompleteCollectionViewHeight(.hairline) + } + } + + func collectionView(_ collectionView: UICollectionView, + contextMenuConfigurationForItemAt indexPath: IndexPath, + point: CGPoint) -> UIContextMenuConfiguration? { + guard let item = autocompleteDataSource.itemIdentifier(for: indexPath), + case let .emoji(emojiItem) = item, + case let .system(emoji, _) = emojiItem, + !emoji.skinToneVariations.isEmpty + else { return nil } + + return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in + UIMenu(children: ([emoji] + emoji.skinToneVariations).map { skinToneVariation in + UIAction(title: skinToneVariation.emoji) { [weak self] _ in + self?.autocompleteSelectionsSubject.send(skinToneVariation.emoji) + self?.autocompleteDataSource.updateUse(emoji: emojiItem) + } + }) + } } } diff --git a/Views/UIKit/CompositionPollOptionView.swift b/Views/UIKit/CompositionPollOptionView.swift index 2553a02..65d4e95 100644 --- a/Views/UIKit/CompositionPollOptionView.swift +++ b/Views/UIKit/CompositionPollOptionView.swift @@ -51,15 +51,7 @@ private extension CompositionPollOptionView { textField.inputAccessoryView = textInputAccessoryView textField.tag = textInputAccessoryView.tagForInputView textField.addAction( - UIAction { [weak self] _ in - guard let self = self, let text = self.textField.text else { return } - - self.option.text = text - - if let textToSelectedRange = self.textField.textToSelectedRange { - self.option.textToSelectedRange = textToSelectedRange - } - }, + UIAction { [weak self] _ in self?.textFieldEditingChanged() }, for: .editingChanged) textField.text = option.text @@ -96,5 +88,37 @@ private extension CompositionPollOptionView { remainingCharactersLabel.textColor = $0 < 0 ? .systemRed : .label } .store(in: &cancellables) + + textInputAccessoryView.autocompleteSelections + .sink { [weak self] in self?.autocompleteSelected($0) } + .store(in: &cancellables) + } + + func textFieldEditingChanged() { + guard let text = textField.text else { return } + + option.text = text + + if let textToSelectedRange = textField.textToSelectedRange { + option.textToSelectedRange = textToSelectedRange + } + } + + func autocompleteSelected(_ autocompleteText: String) { + guard let autocompleteQuery = option.autocompleteQuery, + let queryRange = option.textToSelectedRange.range(of: autocompleteQuery, options: .backwards), + let textToSelectedRangeRange = option.text.range(of: option.textToSelectedRange) + else { return } + + let replaced = option.textToSelectedRange.replacingOccurrences( + of: autocompleteQuery, + with: autocompleteText.appending(" "), + range: queryRange) + + textField.text = option.text.replacingOccurrences( + of: option.textToSelectedRange, + with: replaced, + range: textToSelectedRangeRange) + textFieldEditingChanged() } } diff --git a/Views/UIKit/CompositionView.swift b/Views/UIKit/CompositionView.swift index 1996943..03e3579 100644 --- a/Views/UIKit/CompositionView.swift +++ b/Views/UIKit/CompositionView.swift @@ -96,15 +96,7 @@ private extension CompositionView { spoilerTextField.inputAccessoryView = spoilerTextinputAccessoryView spoilerTextField.tag = spoilerTextinputAccessoryView.tagForInputView spoilerTextField.addAction( - UIAction { [weak self] _ in - guard let self = self, let text = self.spoilerTextField.text else { return } - - self.viewModel.contentWarning = text - - if let textToSelectedRange = self.spoilerTextField.textToSelectedRange { - self.viewModel.contentWarningTextToSelectedRange = textToSelectedRange - } - }, + UIAction { [weak self] _ in self?.spoilerTextFieldEditingChanged() }, for: .editingChanged) let textViewFont = UIFont.preferredFont(forTextStyle: .body) @@ -257,6 +249,14 @@ private extension CompositionView { } .store(in: &cancellables) + textInputAccessoryView.autocompleteSelections + .sink { [weak self] in self?.autocompleteSelected($0) } + .store(in: &cancellables) + + spoilerTextinputAccessoryView.autocompleteSelections + .sink { [weak self] in self?.spoilerTextAutocompleteSelected($0) } + .store(in: &cancellables) + let guide = UIDevice.current.userInterfaceIdiom == .pad ? readableContentGuide : layoutMarginsGuide let constraints = [ avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension), @@ -314,4 +314,50 @@ private extension CompositionView { } }) } + + func spoilerTextFieldEditingChanged() { + guard let text = spoilerTextField.text else { return } + + viewModel.contentWarning = text + + if let textToSelectedRange = spoilerTextField.textToSelectedRange { + viewModel.contentWarningTextToSelectedRange = textToSelectedRange + } + } + + func autocompleteSelected(_ autocompleteText: String) { + guard let autocompleteQuery = viewModel.autocompleteQuery, + let queryRange = viewModel.textToSelectedRange.range(of: autocompleteQuery, options: .backwards), + let textToSelectedRangeRange = viewModel.text.range(of: viewModel.textToSelectedRange) + else { return } + + let replaced = viewModel.textToSelectedRange.replacingOccurrences( + of: autocompleteQuery, + with: autocompleteText.appending(" "), + range: queryRange) + + textView.text = viewModel.text.replacingOccurrences( + of: viewModel.textToSelectedRange, + with: replaced, + range: textToSelectedRangeRange) + textViewDidChange(textView) + } + + func spoilerTextAutocompleteSelected(_ autocompleteText: String) { + guard let autocompleteQuery = viewModel.contentWarningAutocompleteQuery, + let queryRange = viewModel.contentWarningTextToSelectedRange.range(of: autocompleteQuery, options: .backwards), + let textToSelectedRangeRange = viewModel.contentWarning.range(of: viewModel.contentWarningTextToSelectedRange) + else { return } + + let replaced = viewModel.contentWarningTextToSelectedRange.replacingOccurrences( + of: autocompleteQuery, + with: autocompleteText.appending(" "), + range: queryRange) + + spoilerTextField.text = viewModel.contentWarning.replacingOccurrences( + of: viewModel.contentWarningTextToSelectedRange, + with: replaced, + range: textToSelectedRangeRange) + spoilerTextFieldEditingChanged() + } }