From 672bfab200dea52b407cc92d766e8c2582bc5d26 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 18 May 2021 15:06:00 +0800 Subject: [PATCH] feat: add auto complete replace input action --- .../Diffiable/Item/AutoCompleteItem.swift | 1 + .../Section/AutoCompleteSection.swift | 1 + .../AutoCompleteViewController.swift | 11 ++++++ .../AutoCompleteViewModel+State.swift | 4 +-- .../Scene/Compose/ComposeViewController.swift | 36 +++++++++++++++++++ 5 files changed, 51 insertions(+), 2 deletions(-) diff --git a/Mastodon/Diffiable/Item/AutoCompleteItem.swift b/Mastodon/Diffiable/Item/AutoCompleteItem.swift index 5fac45f4b..ee296ba71 100644 --- a/Mastodon/Diffiable/Item/AutoCompleteItem.swift +++ b/Mastodon/Diffiable/Item/AutoCompleteItem.swift @@ -47,6 +47,7 @@ extension AutoCompleteItem: Hashable { hasher.combine(account.id) case .emoji(let emoji): hasher.combine(emoji.shortcode) + hasher.combine(emoji.url) case .bottomLoader: hasher.combine(String(describing: AutoCompleteItem.bottomLoader.self)) } diff --git a/Mastodon/Diffiable/Section/AutoCompleteSection.swift b/Mastodon/Diffiable/Section/AutoCompleteSection.swift index 0d5caf872..39aa6e9cc 100644 --- a/Mastodon/Diffiable/Section/AutoCompleteSection.swift +++ b/Mastodon/Diffiable/Section/AutoCompleteSection.swift @@ -37,6 +37,7 @@ extension AutoCompleteSection { return cell case .bottomLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell + cell.startAnimating() return cell } } diff --git a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewController.swift b/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewController.swift index 4d6e3a031..acd7396fd 100644 --- a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewController.swift +++ b/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewController.swift @@ -9,12 +9,18 @@ import os.log import UIKit import Combine +protocol AutoCompleteViewControllerDelegate: AnyObject { + func autoCompleteViewController(_ viewController: AutoCompleteViewController, didSelectItem item: AutoCompleteItem) +} + final class AutoCompleteViewController: UIViewController { static let chevronViewHeight: CGFloat = 24 var viewModel: AutoCompleteViewModel! var disposeBag = Set() + + weak var delegate: AutoCompleteViewControllerDelegate? let chevronView = AutoCompleteTopChevronView() let containerBackgroundView: UIView = { @@ -26,6 +32,7 @@ final class AutoCompleteViewController: UIViewController { let tableView: UITableView = { let tableView = ControlContainableTableView() tableView.register(AutoCompleteTableViewCell.self, forCellReuseIdentifier: String(describing: AutoCompleteTableViewCell.self)) + tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none tableView.backgroundColor = .clear @@ -96,6 +103,10 @@ extension AutoCompleteViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath: %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription) tableView.deselectRow(at: indexPath, animated: true) + + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + delegate?.autoCompleteViewController(self, didSelectItem: item) } func tableView(_ tableView: UITableView, didHighlightRowAt indexPath: IndexPath) { diff --git a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift b/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift index f57328f70..4e59ce082 100644 --- a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift +++ b/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift @@ -99,9 +99,9 @@ extension AutoCompleteViewModel.State { let searchPattern = ArraySlice(String(searchText.dropFirst())) let passthroughs = emojiTrie.passthrough(searchPattern) let matchingEmojis = passthroughs - .map { $0.values } // [Set] + .map { $0.values } // [Set] .map { set in set.compactMap { $0 as? Mastodon.Entity.Emoji } } // [[Emoji]] - .flatMap { $0 } // [Emoji] + .flatMap { $0 } // [Emoji] let items: [AutoCompleteItem] = matchingEmojis.map { emoji in AutoCompleteItem.emoji(emoji: emoji) } diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index c895566a8..b0d445d72 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -98,6 +98,7 @@ final class ComposeViewController: UIViewController, NeedsDependency { private(set) lazy var autoCompleteViewController: AutoCompleteViewController = { let viewController = AutoCompleteViewController() viewController.viewModel = AutoCompleteViewModel(context: context) + viewController.delegate = self viewModel.customEmojiViewModel .assign(to: \.value, on: viewController.viewModel.customEmojiViewModel) .store(in: &disposeBag) @@ -1221,3 +1222,38 @@ extension ComposeViewController: ComposeStatusPollExpiresOptionCollectionViewCel viewModel.pollExpiresOptionAttribute.expiresOption.value = expiresOption } } + +// MARK: - AutoCompleteViewControllerDelegate +extension ComposeViewController: AutoCompleteViewControllerDelegate { + func autoCompleteViewController(_ viewController: AutoCompleteViewController, didSelectItem item: AutoCompleteItem) { + guard let info = viewModel.autoCompleteInfo.value else { return } + let _replacedText: String? = { + var text: String + switch item { + case .hashtag(let hashtag): + text = "#" + hashtag.name + case .hashtagV1(let hashtagName): + text = "#" + hashtagName + case .account(let account): + text = "@" + account.acct + case .emoji(let emoji): + text = ":" + emoji.shortcode + ":" + case .bottomLoader: + return nil + } + text.append(" ") + return text + }() + guard let replacedText = _replacedText else { return } + + guard let textEditorView = textEditorView() else { return } + let text = textEditorView.text + + do { + try textEditorView.updateByReplacing(range: NSRange(info.toHighlightEndRange, in: text), with: replacedText) + viewModel.autoCompleteInfo.value = nil + } catch { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete fail %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + } + } +}