Finish autocomplete

This commit is contained in:
Justin Mazzocchi 2021-02-15 13:52:28 -08:00
parent 588154b050
commit 8e6793fbf5
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
4 changed files with 144 additions and 19 deletions

View file

@ -81,6 +81,12 @@ final class AutocompleteDataSource: UICollectionViewDiffableDataSource<Autocompl
} }
} }
extension AutocompleteDataSource {
func updateUse(emoji: PickerEmoji) {
emojiPickerViewModel.updateUse(emoji: emoji)
}
}
private extension AutocompleteDataSource { private extension AutocompleteDataSource {
static func combine(query: String, searchViewModel: SearchViewModel, emojiPickerViewModel: EmojiPickerViewModel) { static func combine(query: String, searchViewModel: SearchViewModel, emojiPickerViewModel: EmojiPickerViewModel) {
if query.starts(with: ":") { if query.starts(with: ":") {

View file

@ -8,6 +8,7 @@ import ViewModels
final class CompositionInputAccessoryView: UIView { final class CompositionInputAccessoryView: UIView {
let tagForInputView = UUID().hashValue let tagForInputView = UUID().hashValue
let autocompleteSelections: AnyPublisher<String, Never>
private let viewModel: CompositionViewModel private let viewModel: CompositionViewModel
private let parentViewModel: NewStatusViewModel private let parentViewModel: NewStatusViewModel
@ -17,6 +18,7 @@ final class CompositionInputAccessoryView: UIView {
collectionViewLayout: CompositionInputAccessoryView.autocompleteLayout()) collectionViewLayout: CompositionInputAccessoryView.autocompleteLayout())
private let autocompleteDataSource: AutocompleteDataSource private let autocompleteDataSource: AutocompleteDataSource
private let autocompleteCollectionViewHeightConstraint: NSLayoutConstraint private let autocompleteCollectionViewHeightConstraint: NSLayoutConstraint
private let autocompleteSelectionsSubject = PassthroughSubject<String, Never>()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(viewModel: CompositionViewModel, init(viewModel: CompositionViewModel,
@ -29,7 +31,8 @@ final class CompositionInputAccessoryView: UIView {
queryPublisher: autocompleteQueryPublisher, queryPublisher: autocompleteQueryPublisher,
parentViewModel: parentViewModel) parentViewModel: parentViewModel)
autocompleteCollectionViewHeightConstraint = autocompleteCollectionViewHeightConstraint =
autocompleteCollectionView.heightAnchor.constraint(equalToConstant: .minimumButtonDimension) autocompleteCollectionView.heightAnchor.constraint(equalToConstant: .hairline)
autocompleteSelections = autocompleteSelectionsSubject.eraseToAnyPublisher()
super.init( super.init(
frame: .init( frame: .init(
@ -43,6 +46,12 @@ final class CompositionInputAccessoryView: UIView {
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func didMoveToSuperview() {
super.didMoveToSuperview()
layoutIfNeeded()
}
} }
private extension CompositionInputAccessoryView { private extension CompositionInputAccessoryView {
@ -241,6 +250,46 @@ private extension CompositionInputAccessoryView {
extension CompositionInputAccessoryView: UICollectionViewDelegate { extension CompositionInputAccessoryView: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: true) 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)
}
})
}
} }
} }

View file

@ -51,15 +51,7 @@ private extension CompositionPollOptionView {
textField.inputAccessoryView = textInputAccessoryView textField.inputAccessoryView = textInputAccessoryView
textField.tag = textInputAccessoryView.tagForInputView textField.tag = textInputAccessoryView.tagForInputView
textField.addAction( textField.addAction(
UIAction { [weak self] _ in UIAction { [weak self] _ in self?.textFieldEditingChanged() },
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
}
},
for: .editingChanged) for: .editingChanged)
textField.text = option.text textField.text = option.text
@ -96,5 +88,37 @@ private extension CompositionPollOptionView {
remainingCharactersLabel.textColor = $0 < 0 ? .systemRed : .label remainingCharactersLabel.textColor = $0 < 0 ? .systemRed : .label
} }
.store(in: &cancellables) .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()
} }
} }

View file

@ -96,15 +96,7 @@ private extension CompositionView {
spoilerTextField.inputAccessoryView = spoilerTextinputAccessoryView spoilerTextField.inputAccessoryView = spoilerTextinputAccessoryView
spoilerTextField.tag = spoilerTextinputAccessoryView.tagForInputView spoilerTextField.tag = spoilerTextinputAccessoryView.tagForInputView
spoilerTextField.addAction( spoilerTextField.addAction(
UIAction { [weak self] _ in UIAction { [weak self] _ in self?.spoilerTextFieldEditingChanged() },
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
}
},
for: .editingChanged) for: .editingChanged)
let textViewFont = UIFont.preferredFont(forTextStyle: .body) let textViewFont = UIFont.preferredFont(forTextStyle: .body)
@ -257,6 +249,14 @@ private extension CompositionView {
} }
.store(in: &cancellables) .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 guide = UIDevice.current.userInterfaceIdiom == .pad ? readableContentGuide : layoutMarginsGuide
let constraints = [ let constraints = [
avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension), 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()
}
} }