231 lines
10 KiB
Swift
231 lines
10 KiB
Swift
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
|
|
|
import UIKit
|
|
import MastodonCore
|
|
import MastodonSDK
|
|
import MastodonLocalization
|
|
import MastodonUI
|
|
|
|
protocol SearchResultsOverviewTableViewControllerDelegate: AnyObject {
|
|
func goTo(_ viewController: SearchResultsOverviewTableViewController, urlString: String)
|
|
func showPosts(_ viewController: SearchResultsOverviewTableViewController, tag: Mastodon.Entity.Tag)
|
|
func searchForPosts(_ viewController: SearchResultsOverviewTableViewController, withSearchText searchText: String)
|
|
func searchForPeople(_ viewController: SearchResultsOverviewTableViewController, withName searchText: String)
|
|
func showProfile(_ viewController: SearchResultsOverviewTableViewController, for account: Mastodon.Entity.Account)
|
|
func searchForPerson(_ viewController: SearchResultsOverviewTableViewController, username: String, domain: String)
|
|
}
|
|
|
|
class SearchResultsOverviewTableViewController: UIViewController, NeedsDependency, AuthContextProvider {
|
|
let authContext: AuthContext
|
|
var context: AppContext!
|
|
var coordinator: SceneCoordinator!
|
|
|
|
private let tableView: UITableView
|
|
var dataSource: UITableViewDiffableDataSource<SearchResultOverviewSection, SearchResultOverviewItem>?
|
|
|
|
weak var delegate: SearchResultsOverviewTableViewControllerDelegate?
|
|
|
|
var activeTask: Task<Void, Never>?
|
|
|
|
init(appContext: AppContext, authContext: AuthContext, sceneCoordinator: SceneCoordinator) {
|
|
|
|
self.authContext = authContext
|
|
self.context = appContext
|
|
self.coordinator = sceneCoordinator
|
|
|
|
tableView = UITableView(frame: .zero, style: .insetGrouped)
|
|
tableView.keyboardDismissMode = .onDrag
|
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
|
tableView.backgroundColor = .systemGroupedBackground
|
|
tableView.separatorInset.left = 62
|
|
tableView.register(SearchResultDefaultSectionTableViewCell.self, forCellReuseIdentifier: SearchResultDefaultSectionTableViewCell.reuseIdentifier)
|
|
tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: StatusTableViewCell.reuseIdentifier)
|
|
tableView.register(HashtagTableViewCell.self, forCellReuseIdentifier: HashtagTableViewCell.reuseIdentifier)
|
|
tableView.register(SearchResultsProfileTableViewCell.self, forCellReuseIdentifier: SearchResultsProfileTableViewCell.reuseIdentifier)
|
|
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
|
|
let dataSource = UITableViewDiffableDataSource<SearchResultOverviewSection, SearchResultOverviewItem>(tableView: tableView) { tableView, indexPath, itemIdentifier in
|
|
|
|
switch itemIdentifier {
|
|
case .default(let item):
|
|
guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultDefaultSectionTableViewCell.reuseIdentifier, for: indexPath) as? SearchResultDefaultSectionTableViewCell else { fatalError() }
|
|
|
|
cell.configure(item: item)
|
|
|
|
return cell
|
|
|
|
case .suggestion(let suggestion):
|
|
switch suggestion {
|
|
case .hashtag(let hashtag):
|
|
guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultDefaultSectionTableViewCell.reuseIdentifier, for: indexPath) as? SearchResultDefaultSectionTableViewCell else { fatalError() }
|
|
|
|
cell.configure(item: .hashtag(tag: hashtag))
|
|
return cell
|
|
|
|
case .profile(let profile):
|
|
guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultsProfileTableViewCell.reuseIdentifier, for: indexPath) as? SearchResultsProfileTableViewCell else { fatalError() }
|
|
|
|
cell.condensedUserView.configure(with: profile)
|
|
|
|
return cell
|
|
}
|
|
}
|
|
}
|
|
|
|
tableView.dataSource = dataSource
|
|
tableView.delegate = self
|
|
self.dataSource = dataSource
|
|
|
|
view.addSubview(tableView)
|
|
tableView.pinToParent()
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
var snapshot = NSDiffableDataSourceSnapshot<SearchResultOverviewSection, SearchResultOverviewItem>()
|
|
snapshot.appendSections([.default, .suggestions])
|
|
dataSource?.apply(snapshot, animatingDifferences: false)
|
|
}
|
|
|
|
func showStandardSearch(for searchText: String) {
|
|
guard let dataSource else { return }
|
|
|
|
var snapshot = dataSource.snapshot()
|
|
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .default))
|
|
|
|
if searchText.lowercased().starts(with: "https://") && (searchText.contains(" ") == false) {
|
|
if URL(string: searchText)?.isValidURL() ?? false {
|
|
snapshot.appendItems([.default(.openLink(searchText))], toSection: .default)
|
|
}
|
|
}
|
|
|
|
if searchText.starts(with: "#") && searchText.length > 1 {
|
|
snapshot.appendItems([.default(.showHashtag(hashtag: searchText.replacingOccurrences(of: "#", with: "")))],
|
|
toSection: .default)
|
|
} else if searchText.length > 1, let hashtagRegex = try? NSRegularExpression(pattern: MastodonRegex.Search.hashtag, options: .caseInsensitive), hashtagRegex.firstMatch(in: searchText, range: NSRange(location: 0, length: searchText.length-1)) != nil {
|
|
snapshot.appendItems([.default(.showHashtag(hashtag: searchText.replacingOccurrences(of: "#", with: "")))],
|
|
toSection: .default)
|
|
}
|
|
|
|
if searchText.length > 1,
|
|
let usernameRegex = try? NSRegularExpression(pattern: MastodonRegex.Search.username, options: .caseInsensitive),
|
|
usernameRegex.firstMatch(in: searchText, range: NSRange(location: 0, length: searchText.length-1)) != nil {
|
|
let components = searchText.split(separator: "@")
|
|
if components.count == 2 {
|
|
let username = String(components[0]).replacingOccurrences(of: "@", with: "")
|
|
|
|
let domain = String(components[1])
|
|
if domain.split(separator: ".").count >= 2 {
|
|
snapshot.appendItems([.default(.showProfile(username: username, domain: domain))], toSection: .default)
|
|
} else {
|
|
snapshot.appendItems([.default(.showProfile(username: username, domain: authContext.mastodonAuthenticationBox.domain))], toSection: .default)
|
|
}
|
|
} else {
|
|
snapshot.appendItems([.default(.showProfile(username: searchText, domain: authContext.mastodonAuthenticationBox.domain))], toSection: .default)
|
|
}
|
|
}
|
|
|
|
snapshot.appendItems([.default(.posts(matching: searchText)),
|
|
.default(.people(matching: searchText))], toSection: .default)
|
|
|
|
dataSource.apply(snapshot, animatingDifferences: false)
|
|
}
|
|
|
|
func searchForSuggestions(for searchText: String) {
|
|
|
|
activeTask?.cancel()
|
|
|
|
guard let dataSource else { return }
|
|
|
|
var snapshot = dataSource.snapshot()
|
|
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .suggestions))
|
|
dataSource.apply(snapshot, animatingDifferences: false)
|
|
|
|
guard searchText.isNotEmpty else { return }
|
|
|
|
let query = Mastodon.API.V2.Search.Query(
|
|
q: searchText,
|
|
type: .default,
|
|
resolve: true
|
|
)
|
|
|
|
let searchTask = Task {
|
|
do {
|
|
let searchResult = try await context.apiService.search(
|
|
query: query,
|
|
authenticationBox: authContext.mastodonAuthenticationBox
|
|
).value
|
|
|
|
let firstThreeHashtags = searchResult.hashtags.prefix(3)
|
|
let firstThreeUsers = searchResult.accounts.prefix(3)
|
|
|
|
var snapshot = dataSource.snapshot()
|
|
|
|
if firstThreeHashtags.isNotEmpty {
|
|
snapshot.appendItems(firstThreeHashtags.map { .suggestion(.hashtag(tag: $0)) }, toSection: .suggestions )
|
|
}
|
|
|
|
if firstThreeUsers.isNotEmpty {
|
|
snapshot.appendItems(firstThreeUsers.map { .suggestion(.profile(user: $0)) }, toSection: .suggestions )
|
|
}
|
|
|
|
guard Task.isCancelled == false else { return }
|
|
|
|
await MainActor.run {
|
|
dataSource.apply(snapshot, animatingDifferences: false)
|
|
}
|
|
|
|
} catch {
|
|
// do nothing
|
|
print(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
activeTask = searchTask
|
|
}
|
|
}
|
|
|
|
//MARK: UITableViewDelegate
|
|
extension SearchResultsOverviewTableViewController: UITableViewDelegate {
|
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
|
|
guard let snapshot = dataSource?.snapshot() else { return }
|
|
let section = snapshot.sectionIdentifiers[indexPath.section]
|
|
let item = snapshot.itemIdentifiers(inSection: section)[indexPath.row]
|
|
|
|
switch item {
|
|
case .default(let defaultSectionEntry):
|
|
switch defaultSectionEntry {
|
|
case .posts(let searchText):
|
|
delegate?.searchForPosts(self, withSearchText: searchText)
|
|
case .people(let searchText):
|
|
delegate?.searchForPeople(self, withName: searchText)
|
|
case .showProfile(let username, let domain):
|
|
delegate?.searchForPerson(self, username: username, domain: domain)
|
|
case .openLink(let urlString):
|
|
delegate?.goTo(self, urlString: urlString)
|
|
case .showHashtag(let hashtagText):
|
|
let tag = Mastodon.Entity.Tag(name: hashtagText, url: "")
|
|
delegate?.showPosts(self, tag: tag)
|
|
}
|
|
case .suggestion(let suggestionSectionEntry):
|
|
switch suggestionSectionEntry {
|
|
|
|
case .hashtag(let tag):
|
|
delegate?.showPosts(self, tag: tag)
|
|
case .profile(let account):
|
|
delegate?.showProfile(self, for: account)
|
|
}
|
|
}
|
|
|
|
tableView.deselectRow(at: indexPath, animated: true)
|
|
}
|
|
}
|
|
|
|
extension SearchResultsOverviewTableViewController: UserTableViewCellDelegate {}
|