mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 17:50:59 +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 {
|
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: ":") {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue