Compare commits
3 Commits
d5887f1f02
...
a5506aeab6
Author | SHA1 | Date |
---|---|---|
Shadowfacts | a5506aeab6 | |
Shadowfacts | 23b76a7276 | |
Shadowfacts | d8f503351b |
|
@ -315,11 +315,12 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Search
|
// MARK: - Search
|
||||||
public static func search(query: String, types: [SearchResultType]? = nil, resolve: Bool? = nil, limit: Int? = nil) -> Request<SearchResults> {
|
public static func search(query: String, types: [SearchResultType]? = nil, resolve: Bool? = nil, limit: Int? = nil, following: Bool? = nil) -> Request<SearchResults> {
|
||||||
return Request<SearchResults>(method: .get, path: "/api/v2/search", queryParameters: [
|
return Request<SearchResults>(method: .get, path: "/api/v2/search", queryParameters: [
|
||||||
"q" => query,
|
"q" => query,
|
||||||
"resolve" => resolve,
|
"resolve" => resolve,
|
||||||
"limit" => limit,
|
"limit" => limit,
|
||||||
|
"following" => following,
|
||||||
] + "types" => types?.map { $0.rawValue })
|
] + "types" => types?.map { $0.rawValue })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -315,8 +315,6 @@
|
||||||
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; };
|
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; };
|
||||||
D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */; };
|
D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */; };
|
||||||
D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */; };
|
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 */; };
|
D6F6A550291F058600F496A8 /* CreateListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A54F291F058600F496A8 /* CreateListService.swift */; };
|
||||||
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A551291F098700F496A8 /* RenameListService.swift */; };
|
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A551291F098700F496A8 /* RenameListService.swift */; };
|
||||||
D6F6A554291F0D9600F496A8 /* DeleteListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A553291F0D9600F496A8 /* DeleteListService.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 = "<group>"; };
|
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarViewController.swift; sourceTree = "<group>"; };
|
||||||
D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueReporterViewController.swift; sourceTree = "<group>"; };
|
D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueReporterViewController.swift; sourceTree = "<group>"; };
|
||||||
D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = IssueReporterViewController.xib; sourceTree = "<group>"; };
|
D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = IssueReporterViewController.xib; sourceTree = "<group>"; };
|
||||||
D6F6A54B291EF6FE00F496A8 /* EditListSearchResultsContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListSearchResultsContainerViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D6F6A54D291EF7E100F496A8 /* EditListSearchFollowingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListSearchFollowingViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D6F6A54F291F058600F496A8 /* CreateListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateListService.swift; sourceTree = "<group>"; };
|
D6F6A54F291F058600F496A8 /* CreateListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateListService.swift; sourceTree = "<group>"; };
|
||||||
D6F6A551291F098700F496A8 /* RenameListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenameListService.swift; sourceTree = "<group>"; };
|
D6F6A551291F098700F496A8 /* RenameListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenameListService.swift; sourceTree = "<group>"; };
|
||||||
D6F6A553291F0D9600F496A8 /* DeleteListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteListService.swift; sourceTree = "<group>"; };
|
D6F6A553291F0D9600F496A8 /* DeleteListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteListService.swift; sourceTree = "<group>"; };
|
||||||
|
@ -857,8 +853,6 @@
|
||||||
children = (
|
children = (
|
||||||
D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */,
|
D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */,
|
||||||
D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */,
|
D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */,
|
||||||
D6F6A54B291EF6FE00F496A8 /* EditListSearchResultsContainerViewController.swift */,
|
|
||||||
D6F6A54D291EF7E100F496A8 /* EditListSearchFollowingViewController.swift */,
|
|
||||||
);
|
);
|
||||||
path = Lists;
|
path = Lists;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1796,7 +1790,6 @@
|
||||||
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
|
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
|
||||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
||||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
||||||
D6F6A54C291EF6FE00F496A8 /* EditListSearchResultsContainerViewController.swift in Sources */,
|
|
||||||
D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */,
|
D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */,
|
||||||
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
||||||
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */,
|
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */,
|
||||||
|
@ -2035,7 +2028,6 @@
|
||||||
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
||||||
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
||||||
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */,
|
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */,
|
||||||
D6F6A54E291EF7E100F496A8 /* EditListSearchFollowingViewController.swift in Sources */,
|
|
||||||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */,
|
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */,
|
||||||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */,
|
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */,
|
||||||
D626493F23C101C500612E6E /* AlbumAssetCollectionViewController.swift in Sources */,
|
D626493F23C101C500612E6E /* AlbumAssetCollectionViewController.swift in Sources */,
|
||||||
|
|
|
@ -20,7 +20,7 @@ class EditListAccountsViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
var nextRange: RequestRange?
|
var nextRange: RequestRange?
|
||||||
|
|
||||||
var searchResultsController: EditListSearchResultsContainerViewController!
|
var searchResultsController: SearchResultsViewController!
|
||||||
var searchController: UISearchController!
|
var searchController: UISearchController!
|
||||||
|
|
||||||
private var listRenamedCancellable: AnyCancellable?
|
private var listRenamedCancellable: AnyCancellable?
|
||||||
|
@ -64,23 +64,15 @@ class EditListAccountsViewController: EnhancedTableViewController {
|
||||||
})
|
})
|
||||||
dataSource.editListAccountsController = self
|
dataSource.editListAccountsController = self
|
||||||
|
|
||||||
searchResultsController = EditListSearchResultsContainerViewController(mastodonController: mastodonController) { [unowned self] accountID in
|
searchResultsController = SearchResultsViewController(mastodonController: mastodonController, resultTypes: [.accounts])
|
||||||
Task {
|
searchResultsController.following = true
|
||||||
await self.addAccount(id: accountID)
|
searchResultsController.delegate = self
|
||||||
}
|
|
||||||
}
|
|
||||||
searchController = UISearchController(searchResultsController: searchResultsController)
|
searchController = UISearchController(searchResultsController: searchResultsController)
|
||||||
searchController.hidesNavigationBarDuringPresentation = false
|
searchController.hidesNavigationBarDuringPresentation = false
|
||||||
searchController.searchResultsUpdater = searchResultsController
|
searchController.searchResultsUpdater = searchResultsController
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
searchController.scopeBarActivation = .onSearchActivation
|
|
||||||
} else {
|
|
||||||
searchController.automaticallyShowsScopeBar = true
|
|
||||||
}
|
|
||||||
searchController.searchBar.autocapitalizationType = .none
|
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.delegate = searchResultsController
|
||||||
searchController.searchBar.scopeButtonTitles = ["Everyone", "People You Follow"]
|
|
||||||
definesPresentationContext = true
|
definesPresentationContext = true
|
||||||
|
|
||||||
navigationItem.searchController = searchController
|
navigationItem.searchController = searchController
|
||||||
|
@ -206,3 +198,11 @@ extension EditListAccountsViewController: ToastableViewController {
|
||||||
|
|
||||||
extension EditListAccountsViewController: MenuActionProvider {
|
extension EditListAccountsViewController: MenuActionProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension EditListAccountsViewController: SearchResultsViewControllerDelegate {
|
||||||
|
func selectedSearchResult(account accountID: String) {
|
||||||
|
Task {
|
||||||
|
await addAccount(id: accountID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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<Section, String>!
|
|
||||||
|
|
||||||
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<Section, String>()
|
|
||||||
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<Section, String>()
|
|
||||||
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 {
|
|
||||||
}
|
|
|
@ -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<String?, Never>()
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -238,7 +238,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
|
|
||||||
case .discoverHeader, .listsHeader, .addList, .savedHashtagsHeader, .addSavedHashtag, .savedInstancesHeader, .addSavedInstance:
|
case .discoverHeader, .listsHeader, .addList, .savedHashtagsHeader, .addSavedHashtag, .savedInstancesHeader, .addSavedInstance:
|
||||||
// These items are not selectable in the sidebar collection view, so this code is unreachable.
|
// These items are not selectable in the sidebar collection view, so this code is unreachable.
|
||||||
fatalError("unreachable")
|
fatalError("unexpected selected sidebar item: \(sidebar.selectedItem!)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,7 +313,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
case is ProfileDirectoryViewController:
|
case is ProfileDirectoryViewController:
|
||||||
exploreItem = .profileDirectory
|
exploreItem = .profileDirectory
|
||||||
default:
|
default:
|
||||||
fatalError("unhandled second-level explore screen")
|
fatalError("unhandled second-level explore screen: \(tabNavigationStack[1])")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
transferNavigationStack(from: tabNavController, to: exploreItem!, skipFirst: 1, prepend: toPrepend)
|
transferNavigationStack(from: tabNavController, to: exploreItem!, skipFirst: 1, prepend: toPrepend)
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
import Sentry
|
||||||
|
|
||||||
class NotificationsTableViewController: DiffableTimelineLikeTableViewController<NotificationsTableViewController.Section, NotificationsTableViewController.Item> {
|
class NotificationsTableViewController: DiffableTimelineLikeTableViewController<NotificationsTableViewController.Section, NotificationsTableViewController.Item> {
|
||||||
|
|
||||||
|
@ -71,7 +72,18 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
cell.updateUI(statusID: notification.status!.id, state: group.statusState!)
|
guard let status = notification.status else {
|
||||||
|
let crumb = Breadcrumb(level: .fatal, category: "notifications")
|
||||||
|
crumb.data = [
|
||||||
|
"id": notification.id,
|
||||||
|
"type": notification.kind.rawValue,
|
||||||
|
"created_at": notification.createdAt.formatted(.iso8601),
|
||||||
|
"account": notification.account.id,
|
||||||
|
]
|
||||||
|
SentrySDK.addBreadcrumb(crumb: crumb)
|
||||||
|
fatalError("missing status for mention notification")
|
||||||
|
}
|
||||||
|
cell.updateUI(statusID: status.id, state: group.statusState!)
|
||||||
return cell
|
return cell
|
||||||
|
|
||||||
case .favourite, .reblog:
|
case .favourite, .reblog:
|
||||||
|
@ -114,6 +126,19 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func validateNotifications(_ notifications: [Pachyderm.Notification]) {
|
||||||
|
for notif in notifications where notif.status == nil && (notif.kind == .mention || notif.kind == .reblog || notif.kind == .favourite) {
|
||||||
|
let crumb = Breadcrumb(level: .fatal, category: "notifications")
|
||||||
|
crumb.data = [
|
||||||
|
"id": notif.id,
|
||||||
|
"type": notif.kind.rawValue,
|
||||||
|
"created_at": notif.createdAt.formatted(.iso8601),
|
||||||
|
"account": notif.account.id,
|
||||||
|
]
|
||||||
|
SentrySDK.addBreadcrumb(crumb: crumb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func loadInitialItems(completion: @escaping (LoadResult) -> Void) {
|
override func loadInitialItems(completion: @escaping (LoadResult) -> Void) {
|
||||||
let request = Client.getNotifications(excludeTypes: excludedTypes)
|
let request = Client.getNotifications(excludeTypes: excludedTypes)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
|
@ -122,6 +147,7 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
||||||
completion(.failure(.client(error)))
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
case let .success(notifications, _):
|
case let .success(notifications, _):
|
||||||
|
self.validateNotifications(notifications)
|
||||||
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
|
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
|
||||||
|
|
||||||
if !notifications.isEmpty {
|
if !notifications.isEmpty {
|
||||||
|
@ -152,6 +178,7 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
||||||
completion(.failure(.client(error)))
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
case let .success(newNotifications, _):
|
case let .success(newNotifications, _):
|
||||||
|
self.validateNotifications(newNotifications)
|
||||||
if !newNotifications.isEmpty {
|
if !newNotifications.isEmpty {
|
||||||
self.older = .before(id: newNotifications.last!.id, count: nil)
|
self.older = .before(id: newNotifications.last!.id, count: nil)
|
||||||
}
|
}
|
||||||
|
@ -183,6 +210,7 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
||||||
completion(.failure(.client(error)))
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
case let .success(newNotifications, _):
|
case let .success(newNotifications, _):
|
||||||
|
self.validateNotifications(newNotifications)
|
||||||
guard !newNotifications.isEmpty else {
|
guard !newNotifications.isEmpty else {
|
||||||
completion(.failure(.allCaughtUp))
|
completion(.failure(.allCaughtUp))
|
||||||
return
|
return
|
||||||
|
|
|
@ -40,6 +40,8 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
/// Types of results to search for. `nil` means all results will be included.
|
/// Types of results to search for. `nil` means all results will be included.
|
||||||
var resultTypes: [SearchResultType]? = nil
|
var resultTypes: [SearchResultType]? = nil
|
||||||
|
/// Whether to limit results to accounts the users is following.
|
||||||
|
var following: Bool? = nil
|
||||||
|
|
||||||
let searchSubject = PassthroughSubject<String?, Never>()
|
let searchSubject = PassthroughSubject<String?, Never>()
|
||||||
var currentQuery: String?
|
var currentQuery: String?
|
||||||
|
@ -77,7 +79,6 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
tableView.trailingAnchor.constraint(equalToSystemSpacingAfter: errorLabel.trailingAnchor, multiplier: 1),
|
tableView.trailingAnchor.constraint(equalToSystemSpacingAfter: errorLabel.trailingAnchor, multiplier: 1),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: accountCell)
|
tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: accountCell)
|
||||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell)
|
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell)
|
||||||
tableView.register(UINib(nibName: "HashtagTableViewCell", bundle: .main), forCellReuseIdentifier: hashtagCell)
|
tableView.register(UINib(nibName: "HashtagTableViewCell", bundle: .main), forCellReuseIdentifier: hashtagCell)
|
||||||
|
@ -150,7 +151,7 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
activityIndicator.startAnimating()
|
activityIndicator.startAnimating()
|
||||||
errorLabel.isHidden = true
|
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
|
mastodonController.run(request) { (response) in
|
||||||
switch response {
|
switch response {
|
||||||
case let .success(results, _):
|
case let .success(results, _):
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import SwiftSoup
|
import SwiftSoup
|
||||||
|
import Sentry
|
||||||
|
|
||||||
class ActionNotificationGroupTableViewCell: UITableViewCell {
|
class ActionNotificationGroupTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
@ -66,7 +67,17 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
||||||
self.group = group
|
self.group = group
|
||||||
|
|
||||||
guard let firstNotification = group.notifications.first else { fatalError() }
|
guard let firstNotification = group.notifications.first else { fatalError() }
|
||||||
let status = firstNotification.status!
|
guard let status = firstNotification.status else {
|
||||||
|
let crumb = Breadcrumb(level: .fatal, category: "notifications")
|
||||||
|
crumb.data = [
|
||||||
|
"id": firstNotification.id,
|
||||||
|
"type": firstNotification.kind.rawValue,
|
||||||
|
"created_at": firstNotification.createdAt.formatted(.iso8601),
|
||||||
|
"account": firstNotification.account.id,
|
||||||
|
]
|
||||||
|
SentrySDK.addBreadcrumb(crumb: crumb)
|
||||||
|
fatalError("missing status for favorite/reblog notification")
|
||||||
|
}
|
||||||
self.statusID = status.id
|
self.statusID = status.id
|
||||||
|
|
||||||
updateUIForPreferences()
|
updateUIForPreferences()
|
||||||
|
|
Loading…
Reference in New Issue