diff --git a/Pachyderm/Sources/Pachyderm/Client.swift b/Pachyderm/Sources/Pachyderm/Client.swift index 396b575e..45b54ae5 100644 --- a/Pachyderm/Sources/Pachyderm/Client.swift +++ b/Pachyderm/Sources/Pachyderm/Client.swift @@ -315,11 +315,12 @@ public class Client { } // MARK: - Search - public static func search(query: String, types: [SearchResultType]? = nil, resolve: Bool? = nil, limit: Int? = nil) -> Request { + public static func search(query: String, types: [SearchResultType]? = nil, resolve: Bool? = nil, limit: Int? = nil, following: Bool? = nil) -> Request { return Request(method: .get, path: "/api/v2/search", queryParameters: [ "q" => query, "resolve" => resolve, "limit" => limit, + "following" => following, ] + "types" => types?.map { $0.rawValue }) } diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 6c064e94..f56bdcd2 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -315,8 +315,6 @@ D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; }; D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */; }; D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */; }; - D6F6A54C291EF6FE00F496A8 /* EditListSearchResultsContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A54B291EF6FE00F496A8 /* EditListSearchResultsContainerViewController.swift */; }; - D6F6A54E291EF7E100F496A8 /* EditListSearchFollowingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A54D291EF7E100F496A8 /* EditListSearchFollowingViewController.swift */; }; D6F6A550291F058600F496A8 /* CreateListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A54F291F058600F496A8 /* CreateListService.swift */; }; D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A551291F098700F496A8 /* RenameListService.swift */; }; D6F6A554291F0D9600F496A8 /* DeleteListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A553291F0D9600F496A8 /* DeleteListService.swift */; }; @@ -692,8 +690,6 @@ D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarViewController.swift; sourceTree = ""; }; D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueReporterViewController.swift; sourceTree = ""; }; D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = IssueReporterViewController.xib; sourceTree = ""; }; - D6F6A54B291EF6FE00F496A8 /* EditListSearchResultsContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListSearchResultsContainerViewController.swift; sourceTree = ""; }; - D6F6A54D291EF7E100F496A8 /* EditListSearchFollowingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListSearchFollowingViewController.swift; sourceTree = ""; }; D6F6A54F291F058600F496A8 /* CreateListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateListService.swift; sourceTree = ""; }; D6F6A551291F098700F496A8 /* RenameListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenameListService.swift; sourceTree = ""; }; D6F6A553291F0D9600F496A8 /* DeleteListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteListService.swift; sourceTree = ""; }; @@ -857,8 +853,6 @@ children = ( D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */, D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */, - D6F6A54B291EF6FE00F496A8 /* EditListSearchResultsContainerViewController.swift */, - D6F6A54D291EF7E100F496A8 /* EditListSearchFollowingViewController.swift */, ); path = Lists; sourceTree = ""; @@ -1796,7 +1790,6 @@ D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */, D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */, 0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */, - D6F6A54C291EF6FE00F496A8 /* EditListSearchResultsContainerViewController.swift in Sources */, D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */, D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */, D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */, @@ -2035,7 +2028,6 @@ D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */, D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */, D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */, - D6F6A54E291EF7E100F496A8 /* EditListSearchFollowingViewController.swift in Sources */, D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */, D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */, D626493F23C101C500612E6E /* AlbumAssetCollectionViewController.swift in Sources */, diff --git a/Tusker/Screens/Lists/EditListAccountsViewController.swift b/Tusker/Screens/Lists/EditListAccountsViewController.swift index ff1055c9..dd6aa7bb 100644 --- a/Tusker/Screens/Lists/EditListAccountsViewController.swift +++ b/Tusker/Screens/Lists/EditListAccountsViewController.swift @@ -20,7 +20,7 @@ class EditListAccountsViewController: EnhancedTableViewController { var nextRange: RequestRange? - var searchResultsController: EditListSearchResultsContainerViewController! + var searchResultsController: SearchResultsViewController! var searchController: UISearchController! private var listRenamedCancellable: AnyCancellable? @@ -64,23 +64,15 @@ class EditListAccountsViewController: EnhancedTableViewController { }) dataSource.editListAccountsController = self - searchResultsController = EditListSearchResultsContainerViewController(mastodonController: mastodonController) { [unowned self] accountID in - Task { - await self.addAccount(id: accountID) - } - } + searchResultsController = SearchResultsViewController(mastodonController: mastodonController, resultTypes: [.accounts]) + searchResultsController.following = true + searchResultsController.delegate = self searchController = UISearchController(searchResultsController: searchResultsController) searchController.hidesNavigationBarDuringPresentation = false searchController.searchResultsUpdater = searchResultsController - if #available(iOS 16.0, *) { - searchController.scopeBarActivation = .onSearchActivation - } else { - searchController.automaticallyShowsScopeBar = true - } searchController.searchBar.autocapitalizationType = .none - searchController.searchBar.placeholder = NSLocalizedString("Search for accounts to add", comment: "edit list search field placeholder") + searchController.searchBar.placeholder = NSLocalizedString("Search accounts you follow", comment: "edit list search field placeholder") searchController.searchBar.delegate = searchResultsController - searchController.searchBar.scopeButtonTitles = ["Everyone", "People You Follow"] definesPresentationContext = true navigationItem.searchController = searchController @@ -206,3 +198,11 @@ extension EditListAccountsViewController: ToastableViewController { extension EditListAccountsViewController: MenuActionProvider { } + +extension EditListAccountsViewController: SearchResultsViewControllerDelegate { + func selectedSearchResult(account accountID: String) { + Task { + await addAccount(id: accountID) + } + } +} diff --git a/Tusker/Screens/Lists/EditListSearchFollowingViewController.swift b/Tusker/Screens/Lists/EditListSearchFollowingViewController.swift deleted file mode 100644 index 4b68848a..00000000 --- a/Tusker/Screens/Lists/EditListSearchFollowingViewController.swift +++ /dev/null @@ -1,178 +0,0 @@ -// -// EditListSearchFollowingViewController.swift -// Tusker -// -// Created by Shadowfacts on 11/11/22. -// Copyright © 2022 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm - -class EditListSearchFollowingViewController: EnhancedTableViewController { - - private let mastodonController: MastodonController - private let didSelectAccount: (String) -> Void - - private var dataSource: UITableViewDiffableDataSource! - - private var query: String? - private var accountIDs: [String] = [] - private var nextRange: RequestRange? - - init(mastodonController: MastodonController, didSelectAccount: @escaping (String) -> Void) { - self.mastodonController = mastodonController - self.didSelectAccount = didSelectAccount - - super.init(style: .grouped) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: "accountCell") - dataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, itemIdentifier in - let cell = tableView.dequeueReusableCell(withIdentifier: "accountCell", for: indexPath) as! AccountTableViewCell - cell.delegate = self - cell.updateUI(accountID: itemIdentifier) - return cell - } - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - if dataSource.snapshot().numberOfItems == 0 { - Task { - await load() - } - } - } - - override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - print("will display: \(indexPath)") - if indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 { - Task { - await load() - } - } - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let id = dataSource.itemIdentifier(for: indexPath) else { - return - } - didSelectAccount(id) - } - - private func load() async { - do { - let ownAccount = try await mastodonController.getOwnAccount() - let req = Account.getFollowing(ownAccount.id, range: nextRange ?? .default) - let (following, pagination) = try await mastodonController.run(req) - await withCheckedContinuation { continuation in - mastodonController.persistentContainer.addAll(accounts: following) { - continuation.resume() - } - } - accountIDs.append(contentsOf: following.lazy.map(\.id)) - nextRange = pagination?.older - updateDataSource(appending: following.map(\.id)) - } catch { - let config = ToastConfiguration(from: error, with: "Error Loading Following", in: self) { toast in - toast.dismissToast(animated: true) - await self.load() - } - self.showToast(configuration: config, animated: true) - } - } - - private func updateDataSourceForQueryChanged() { - guard let query, !query.isEmpty else { - let snapshot = NSDiffableDataSourceSnapshot() - dataSource.apply(snapshot, animatingDifferences: true) - return - } - - let ids = filterAccounts(ids: accountIDs, with: query) - - var snapshot = dataSource.snapshot() - if snapshot.indexOfSection(.accounts) == nil { - snapshot.appendSections([.accounts]) - } else { - snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .accounts)) - } - snapshot.appendItems(ids) - dataSource.apply(snapshot, animatingDifferences: true) - - // if there aren't any results for the current query, try to load more - if ids.isEmpty { - Task { - await load() - } - } - } - - private func updateDataSource(appending ids: [String]) { - guard let query, !query.isEmpty else { - let snapshot = NSDiffableDataSourceSnapshot() - dataSource.apply(snapshot, animatingDifferences: true) - return - } - - let ids = filterAccounts(ids: ids, with: query) - - var snapshot = dataSource.snapshot() - if snapshot.indexOfSection(.accounts) == nil { - snapshot.appendSections([.accounts]) - } - let existing = snapshot.itemIdentifiers(inSection: .accounts) - snapshot.appendItems(ids.filter { !existing.contains($0) }) - dataSource.apply(snapshot, animatingDifferences: true) - - // if there aren't any results for the current query, try to load more - if ids.isEmpty { - Task { - await load() - } - } - } - - private func filterAccounts(ids: [String], with query: String) -> [String] { - let req = AccountMO.fetchRequest() - req.predicate = NSPredicate(format: "id in %@", ids) - let accounts = try! mastodonController.persistentContainer.viewContext.fetch(req) - - return accounts - .map { (account) -> (AccountMO, Bool) in - let displayNameMatch = FuzzyMatcher.match(pattern: query, str: account.displayNameWithoutCustomEmoji) - let usernameMatch = FuzzyMatcher.match(pattern: query, str: account.acct) - return (account, displayNameMatch.matched || usernameMatch.matched) - } - .filter(\.1) - .map(\.0.id) - } - - func updateQuery(_ query: String) { - self.query = query - updateDataSourceForQueryChanged() - } - -} - -extension EditListSearchFollowingViewController { - enum Section { - case accounts - } -} - -extension EditListSearchFollowingViewController: TuskerNavigationDelegate { - var apiController: MastodonController! { mastodonController } -} - -extension EditListSearchFollowingViewController: MenuActionProvider { -} diff --git a/Tusker/Screens/Lists/EditListSearchResultsContainerViewController.swift b/Tusker/Screens/Lists/EditListSearchResultsContainerViewController.swift deleted file mode 100644 index 769f1be9..00000000 --- a/Tusker/Screens/Lists/EditListSearchResultsContainerViewController.swift +++ /dev/null @@ -1,115 +0,0 @@ -// -// EditListSearchResultsContainerViewController.swift -// Tusker -// -// Created by Shadowfacts on 11/11/22. -// Copyright © 2022 Shadowfacts. All rights reserved. -// - -import UIKit -import Combine - -class EditListSearchResultsContainerViewController: UIViewController { - - private let mastodonController: MastodonController - private let didSelectAccount: (String) -> Void - - private let searchResultsController: SearchResultsViewController - private let searchFollowingController: EditListSearchFollowingViewController - - var mode = Mode.search { - willSet { - currentViewController.removeViewAndController() - } - didSet { - embedChild(currentViewController) - } - } - var currentViewController: UIViewController { - switch mode { - case .search: - return searchResultsController - case .following: - return searchFollowingController - } - } - - private var currentQuery: String? - private var searchSubject = PassthroughSubject() - private var cancellables = Set() - - init(mastodonController: MastodonController, didSelectAccount: @escaping (String) -> Void) { - self.mastodonController = mastodonController - self.didSelectAccount = didSelectAccount - - self.searchResultsController = SearchResultsViewController(mastodonController: mastodonController, resultTypes: [.accounts]) - self.searchFollowingController = EditListSearchFollowingViewController(mastodonController: mastodonController, didSelectAccount: didSelectAccount) - - super.init(nibName: nil, bundle: nil) - - self.searchResultsController.delegate = self - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - embedChild(currentViewController) - - searchSubject - .debounce(for: .milliseconds(500), scheduler: RunLoop.main) - .sink { [unowned self] in self.performSearch(query: $0) } - .store(in: &cancellables) - } - - func performSearch(query: String?) { - guard var query = query?.trimmingCharacters(in: .whitespacesAndNewlines) else { - return - } - if query.starts(with: "@") { - query = String(query.dropFirst()) - } - guard query != self.currentQuery else { - return - } - self.currentQuery = query - - switch mode { - case .search: - searchResultsController.performSearch(query: query) - case .following: - searchFollowingController.updateQuery(query) - } - } - - enum Mode: Equatable { - case search, following - } - -} - -extension EditListSearchResultsContainerViewController: UISearchResultsUpdating { - func updateSearchResults(for searchController: UISearchController) { - searchSubject.send(searchController.searchBar.text) - } -} - -extension EditListSearchResultsContainerViewController: UISearchBarDelegate { - func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { - performSearch(query: searchBar.text) - } - - func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) { - mode = selectedScope == 0 ? .search : .following - performSearch(query: searchBar.text) - } -} - -extension EditListSearchResultsContainerViewController: SearchResultsViewControllerDelegate { - func selectedSearchResult(account accountID: String) { - didSelectAccount(accountID) - } -} diff --git a/Tusker/Screens/Search/SearchResultsViewController.swift b/Tusker/Screens/Search/SearchResultsViewController.swift index 1cb9c8e8..b42f6016 100644 --- a/Tusker/Screens/Search/SearchResultsViewController.swift +++ b/Tusker/Screens/Search/SearchResultsViewController.swift @@ -40,6 +40,8 @@ class SearchResultsViewController: EnhancedTableViewController { /// Types of results to search for. `nil` means all results will be included. var resultTypes: [SearchResultType]? = nil + /// Whether to limit results to accounts the users is following. + var following: Bool? = nil let searchSubject = PassthroughSubject() var currentQuery: String? @@ -77,7 +79,6 @@ class SearchResultsViewController: EnhancedTableViewController { tableView.trailingAnchor.constraint(equalToSystemSpacingAfter: errorLabel.trailingAnchor, multiplier: 1), ]) - tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: accountCell) tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell) tableView.register(UINib(nibName: "HashtagTableViewCell", bundle: .main), forCellReuseIdentifier: hashtagCell) @@ -150,7 +151,7 @@ class SearchResultsViewController: EnhancedTableViewController { activityIndicator.startAnimating() errorLabel.isHidden = true - let request = Client.search(query: query, types: resultTypes, resolve: true, limit: 10) + let request = Client.search(query: query, types: resultTypes, resolve: true, limit: 10, following: following) mastodonController.run(request) { (response) in switch response { case let .success(results, _):