mirror of
https://github.com/metabolist/metatext.git
synced 2024-12-21 21:26:26 +00:00
Finish autocomplete
This commit is contained in:
parent
588154b050
commit
8e6793fbf5
4 changed files with 144 additions and 19 deletions
|
@ -81,6 +81,12 @@ final class AutocompleteDataSource: UICollectionViewDiffableDataSource<Autocompl
|
|||
}
|
||||
}
|
||||
|
||||
extension AutocompleteDataSource {
|
||||
func updateUse(emoji: PickerEmoji) {
|
||||
emojiPickerViewModel.updateUse(emoji: emoji)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AutocompleteDataSource {
|
||||
static func combine(query: String, searchViewModel: SearchViewModel, emojiPickerViewModel: EmojiPickerViewModel) {
|
||||
if query.starts(with: ":") {
|
||||
|
|
|
@ -8,6 +8,7 @@ import ViewModels
|
|||
|
||||
final class CompositionInputAccessoryView: UIView {
|
||||
let tagForInputView = UUID().hashValue
|
||||
let autocompleteSelections: AnyPublisher<String, Never>
|
||||
|
||||
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<String, Never>()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue