WIP: Add some search-implementation and clean stuff (IOS-141)
Shame on me for such a big commit. I'm new to iOS-development, sorry :nerd:
This commit is contained in:
parent
e8509a063d
commit
2e384f3cb5
|
@ -139,6 +139,9 @@
|
|||
D8099078294BC8A30050219F /* PrivacyTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099077294BC8A30050219F /* PrivacyTableViewController.swift */; };
|
||||
D809907A294BC9390050219F /* PrivacyTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099079294BC9390050219F /* PrivacyTableViewCell.swift */; };
|
||||
D809907C294D25510050219F /* PrivacyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D809907B294D25510050219F /* PrivacyViewModel.swift */; };
|
||||
D81A22752AB4643200905D71 /* SearchResultsOverviewTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81A22742AB4643200905D71 /* SearchResultsOverviewTableViewController.swift */; };
|
||||
D81A22782AB4782400905D71 /* SearchResultOverviewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81A22772AB4782400905D71 /* SearchResultOverviewSection.swift */; };
|
||||
D81A227B2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81A227A2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift */; };
|
||||
D8363B1629469CE200A74079 /* OnboardingNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8363B1529469CE200A74079 /* OnboardingNextView.swift */; };
|
||||
D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; };
|
||||
D87BFC8D291EB81200FEE264 /* MastodonLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */; };
|
||||
|
@ -774,6 +777,9 @@
|
|||
D8099077294BC8A30050219F /* PrivacyTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyTableViewController.swift; sourceTree = "<group>"; };
|
||||
D8099079294BC9390050219F /* PrivacyTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D809907B294D25510050219F /* PrivacyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyViewModel.swift; sourceTree = "<group>"; };
|
||||
D81A22742AB4643200905D71 /* SearchResultsOverviewTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsOverviewTableViewController.swift; sourceTree = "<group>"; };
|
||||
D81A22772AB4782400905D71 /* SearchResultOverviewSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultOverviewSection.swift; sourceTree = "<group>"; };
|
||||
D81A227A2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultDefaultSectionTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D82463522A52B47B00A3DBDD /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = be; path = be.lproj/Intents.strings; sourceTree = "<group>"; };
|
||||
D82463532A52B47B00A3DBDD /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = be; path = be.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
|
||||
D82463542A52B47B00A3DBDD /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = be; path = be.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
|
@ -1791,6 +1797,24 @@
|
|||
path = Privacy;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D81A22732AB4641F00905D71 /* Search Results Overview */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D81A22792AB47B8400905D71 /* Cells */,
|
||||
D81A22742AB4643200905D71 /* SearchResultsOverviewTableViewController.swift */,
|
||||
D81A22772AB4782400905D71 /* SearchResultOverviewSection.swift */,
|
||||
);
|
||||
path = "Search Results Overview";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D81A22792AB47B8400905D71 /* Cells */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D81A227A2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift */,
|
||||
);
|
||||
path = Cells;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8A6AB68291C50F3003AB663 /* Login */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2898,6 +2922,7 @@
|
|||
DBF1D24F269DAF6100C1C08A /* SearchDetail */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D81A22732AB4641F00905D71 /* Search Results Overview */,
|
||||
DB4F0964269ED06700D62E92 /* SearchResult */,
|
||||
DBF1D252269DB01700C1C08A /* SearchHistory */,
|
||||
DBF1D24D269DAF5D00C1C08A /* SearchDetailViewController.swift */,
|
||||
|
@ -3538,6 +3563,7 @@
|
|||
DB3E6FE02806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift in Sources */,
|
||||
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||
DB0FCB7427956939006C02E2 /* DataSourceFacade+Status.swift in Sources */,
|
||||
D81A22782AB4782400905D71 /* SearchResultOverviewSection.swift in Sources */,
|
||||
DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */,
|
||||
DB63F75A279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift in Sources */,
|
||||
DB023D26279FFB0A005AC798 /* ShareActivityProvider.swift in Sources */,
|
||||
|
@ -3692,6 +3718,7 @@
|
|||
6213AF5A28939C8400BCADB6 /* BookmarkViewModel.swift in Sources */,
|
||||
5B24BBDB262DB14800A9381B /* ReportStatusViewModel+Diffable.swift in Sources */,
|
||||
5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */,
|
||||
D81A22752AB4643200905D71 /* SearchResultsOverviewTableViewController.swift in Sources */,
|
||||
2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */,
|
||||
DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */,
|
||||
DB852D1926FAEB6B00FC9D81 /* SidebarViewController.swift in Sources */,
|
||||
|
@ -3790,6 +3817,7 @@
|
|||
2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */,
|
||||
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */,
|
||||
2A3D9B7E29A8F33A00F30313 /* StatusHistoryView.swift in Sources */,
|
||||
D81A227B2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift in Sources */,
|
||||
6213AF5E2893A8B200BCADB6 /* DataSourceFacade+Bookmark.swift in Sources */,
|
||||
DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */,
|
||||
DBF156E22702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m in Sources */,
|
||||
|
|
|
@ -41,13 +41,8 @@ extension HomeTimelineViewModel {
|
|||
.sink { [weak self] records in
|
||||
guard let self = self else { return }
|
||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): incoming \(records.count) objects")
|
||||
|
||||
Task { @MainActor in
|
||||
let start = CACurrentMediaTime()
|
||||
defer {
|
||||
let end = CACurrentMediaTime()
|
||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): cost \(end - start, format: .fixed(precision: 4))s to process \(records.count) feeds")
|
||||
}
|
||||
let oldSnapshot = diffableDataSource.snapshot()
|
||||
var newSnapshot: NSDiffableDataSourceSnapshot<StatusSection, StatusItem> = {
|
||||
let newItems = records.map { record in
|
||||
|
|
|
@ -21,9 +21,6 @@ final class HeightFixedSearchBar: UISearchBar {
|
|||
}
|
||||
|
||||
final class SearchViewController: UIViewController, NeedsDependency {
|
||||
|
||||
let logger = Logger(subsystem: "SearchViewController", category: "ViewController")
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
|
@ -37,16 +34,6 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
|||
let titleViewContainer = UIView()
|
||||
let searchBar = HeightFixedSearchBar()
|
||||
|
||||
// let collectionView: UICollectionView = {
|
||||
// var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
|
||||
// configuration.backgroundColor = .clear
|
||||
// configuration.headerMode = .supplementary
|
||||
// let layout = UICollectionViewCompositionalLayout.list(using: configuration)
|
||||
// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
// collectionView.backgroundColor = .clear
|
||||
// return collectionView
|
||||
// }()
|
||||
|
||||
// value is the initial search text to set
|
||||
let searchBarTapPublisher = PassthroughSubject<String, Never>()
|
||||
|
||||
|
@ -62,11 +49,6 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
|||
)
|
||||
return viewController
|
||||
}()
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SearchViewController {
|
||||
|
@ -85,30 +67,12 @@ extension SearchViewController {
|
|||
title = L10n.Scene.Search.title
|
||||
|
||||
setupSearchBar()
|
||||
|
||||
// collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// view.addSubview(collectionView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
// collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
// collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
// collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
// ])
|
||||
//
|
||||
// collectionView.delegate = self
|
||||
// viewModel.setupDiffableDataSource(
|
||||
// collectionView: collectionView
|
||||
// )
|
||||
|
||||
guard let discoveryViewController = self.discoveryViewController else { return }
|
||||
|
||||
addChild(discoveryViewController)
|
||||
discoveryViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(discoveryViewController.view)
|
||||
discoveryViewController.view.pinToParent()
|
||||
|
||||
// discoveryViewController.view.isHidden = true
|
||||
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
|
@ -183,12 +147,8 @@ extension SearchViewController: UISearchBarDelegate {
|
|||
// MARK: - UISearchControllerDelegate
|
||||
extension SearchViewController: UISearchControllerDelegate {
|
||||
func willDismissSearchController(_ searchController: UISearchController) {
|
||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
searchController.isActive = true
|
||||
}
|
||||
func didPresentSearchController(_ searchController: UISearchController) {
|
||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ScrollViewContainer
|
||||
|
@ -200,23 +160,3 @@ extension SearchViewController: ScrollViewContainer {
|
|||
discoveryViewController?.scrollToTop(animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDelegate
|
||||
//extension SearchViewController: UICollectionViewDelegate {
|
||||
// func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): select item at: \(indexPath.debugDescription)")
|
||||
//
|
||||
// defer {
|
||||
// collectionView.deselectItem(at: indexPath, animated: true)
|
||||
// }
|
||||
//
|
||||
// guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
//
|
||||
// switch item {
|
||||
// case .trend(let hashtag):
|
||||
// let viewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag.name)
|
||||
// coordinator.present(scene: .hashtagTimeline(viewModel: viewModel), from: self, transition: .show)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
|
|
@ -31,32 +31,5 @@ final class SearchViewModel: NSObject {
|
|||
self.context = context
|
||||
self.authContext = authContext
|
||||
super.init()
|
||||
|
||||
// Publishers.CombineLatest(
|
||||
// context.authenticationService.activeMastodonAuthenticationBox,
|
||||
// viewDidAppeared
|
||||
// )
|
||||
// .compactMap { authenticationBox, _ -> MastodonAuthenticationBox? in
|
||||
// return authenticationBox
|
||||
// }
|
||||
// .throttle(for: 3, scheduler: DispatchQueue.main, latest: true)
|
||||
// .asyncMap { authenticationBox in
|
||||
// try await context.apiService.trendHashtags(domain: authenticationBox.domain, query: nil)
|
||||
// }
|
||||
// .retry(3)
|
||||
// .map { response in Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { response } }
|
||||
// .catch { error in Just(Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { throw error }) }
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self] result in
|
||||
// guard let self = self else { return }
|
||||
// switch result {
|
||||
// case .success(let response):
|
||||
// self.hashtags = response.value
|
||||
// case .failure:
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
|
||||
class SearchResultDefaultSectionTableViewCell: UITableViewCell {
|
||||
static let reuseIdentifier = "SearchResultDefaultSectionTableViewCell"
|
||||
|
||||
func configure(item: SearchResultOverviewItem.DefaultSectionEntry) {
|
||||
var content = UIListContentConfiguration.cell()
|
||||
content.image = item.icon
|
||||
content.text = item.title
|
||||
content.imageProperties.tintColor = Asset.Colors.Brand.blurple.color
|
||||
|
||||
self.contentConfiguration = content
|
||||
}
|
||||
|
||||
func configure(item: SearchResultOverviewItem.SuggestionSectionEntry) {
|
||||
var content = UIListContentConfiguration.cell()
|
||||
content.image = item.icon
|
||||
content.text = item.title
|
||||
content.imageProperties.tintColor = Asset.Colors.Brand.blurple.color
|
||||
|
||||
self.contentConfiguration = content
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import CoreDataStack
|
||||
|
||||
enum SearchResultOverviewSection: Hashable {
|
||||
case `default`
|
||||
case suggestions
|
||||
}
|
||||
|
||||
enum SearchResultOverviewItem: Hashable {
|
||||
case `default`(DefaultSectionEntry)
|
||||
case suggestion(SuggestionSectionEntry)
|
||||
|
||||
enum DefaultSectionEntry: Hashable {
|
||||
case posts(String)
|
||||
case people(String)
|
||||
case profile(String, String)
|
||||
case openLink(String)
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
//TODO: Add localization
|
||||
case .posts(let text):
|
||||
return "Posts with \(text)"
|
||||
case .people(let username):
|
||||
return "People with \(username)"
|
||||
case .profile(let username, let instanceName):
|
||||
return "Go to @\(username)@\(instanceName)"
|
||||
case .openLink(_):
|
||||
return "Open Link"
|
||||
}
|
||||
}
|
||||
|
||||
var icon: UIImage? {
|
||||
switch self {
|
||||
case .posts(_):
|
||||
return UIImage(systemName: "number")
|
||||
case .people(_):
|
||||
return UIImage(systemName: "person.2")
|
||||
case .profile(_, _):
|
||||
return UIImage(systemName: "person.crop.circle")
|
||||
case .openLink(_):
|
||||
return UIImage(systemName: "link")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SuggestionSectionEntry: Hashable {
|
||||
//TODO: Use User instead
|
||||
case hashtag(tag: Mastodon.Entity.Tag)
|
||||
case profile(ManagedObjectRecord<MastodonUser>)
|
||||
|
||||
var title: String? {
|
||||
if case let .hashtag(tag) = self {
|
||||
return tag.name
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var icon: UIImage? {
|
||||
if case let .hashtag(tag) = self {
|
||||
return UIImage(systemName: "number")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
protocol SearchResultsOverviewTableViewControllerDeleagte: AnyObject {
|
||||
func showPosts(_ viewController: UIViewController)
|
||||
func showPeople(_ viewController: UIViewController)
|
||||
func showProfile(_ viewController: UIViewController)
|
||||
func openLink(_ viewController: UIViewController)
|
||||
}
|
||||
|
||||
// we could move lots of this stuff to a coordinator, it's too much for work a viewcontroller
|
||||
class SearchResultsOverviewTableViewController: UIViewController {
|
||||
// similar to the other search results view controller but without the whole statemachine bullshit
|
||||
// with scope all
|
||||
|
||||
let appContext: AppContext
|
||||
let authContext: AuthContext
|
||||
|
||||
private let tableView: UITableView
|
||||
var dataSource: UITableViewDiffableDataSource<SearchResultOverviewSection, SearchResultOverviewItem>?
|
||||
|
||||
weak var delegate: SearchResultsOverviewTableViewControllerDeleagte?
|
||||
|
||||
init(appContext: AppContext, authContext: AuthContext) {
|
||||
|
||||
self.appContext = appContext
|
||||
self.authContext = authContext
|
||||
|
||||
tableView = UITableView(frame: .zero, style: .insetGrouped)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tableView.backgroundColor = .systemGroupedBackground
|
||||
tableView.register(SearchResultDefaultSectionTableViewCell.self, forCellReuseIdentifier: SearchResultDefaultSectionTableViewCell.reuseIdentifier)
|
||||
tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: StatusTableViewCell.reuseIdentifier)
|
||||
tableView.register(HashtagTableViewCell.self, forCellReuseIdentifier: HashtagTableViewCell.reuseIdentifier)
|
||||
tableView.register(UserTableViewCell.self, forCellReuseIdentifier: UserTableViewCell.reuseIdentifier)
|
||||
|
||||
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: UserTableViewCell.reuseIdentifier, for: indexPath) as? UserTableViewCell else { fatalError() }
|
||||
|
||||
// cell.configure(me: <#T##MastodonUser?#>, tableView: <#T##UITableView#>, viewModel: <#T##UserTableViewCell.ViewModel#>, delegate: <#T##UserTableViewCellDelegate?#>)
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
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") }
|
||||
|
||||
func showStandardSearch(for searchText: String) {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<SearchResultOverviewSection, SearchResultOverviewItem>()
|
||||
snapshot.appendSections([.default, .suggestions])
|
||||
snapshot.appendItems([.default(.posts(searchText)),
|
||||
.default(.people(searchText)),
|
||||
.default(.profile(searchText, authContext.mastodonAuthenticationBox.domain))], toSection: .default)
|
||||
|
||||
if URL(string: searchText) != nil {
|
||||
//TODO: Check if Mastodon-URL
|
||||
snapshot.appendItems([.default(.openLink(searchText))], toSection: .default)
|
||||
}
|
||||
dataSource?.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
func searchForSuggestions(for searchText: String) {
|
||||
|
||||
let query = Mastodon.API.V2.Search.Query(
|
||||
q: searchText,
|
||||
type: .default,
|
||||
resolve: true
|
||||
)
|
||||
|
||||
Task {
|
||||
do {
|
||||
let searchResult = try await appContext.apiService.search(
|
||||
query: query,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
).value
|
||||
|
||||
let firstThreeHashtags = searchResult.hashtags.prefix(3)
|
||||
let firstThreeUsers = searchResult.accounts.prefix(3)
|
||||
|
||||
guard var snapshot = dataSource?.snapshot() else { return }
|
||||
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .suggestions))
|
||||
snapshot.appendItems(firstThreeHashtags.map { .suggestion(.hashtag(tag: $0)) }, toSection: .suggestions )
|
||||
// snapshot.appendItems(firstThreeUsers.map { .suggestion(.profile($0.displayName)) }, toSection: .suggestions )
|
||||
|
||||
await MainActor.run {
|
||||
dataSource?.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
|
||||
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: UITableViewDelegate
|
||||
extension SearchResultsOverviewTableViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
|
||||
//TODO: Implement properly!
|
||||
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 string):
|
||||
delegate?.showPosts(self)
|
||||
case .people(let string):
|
||||
delegate?.showPeople(self)
|
||||
case .profile(let profile, let instanceName):
|
||||
delegate?.showProfile(self)
|
||||
case .openLink(let string):
|
||||
delegate?.openLink(self)
|
||||
}
|
||||
case .suggestion(let suggestionSectionEntry):
|
||||
switch suggestionSectionEntry {
|
||||
|
||||
case .hashtag(_):
|
||||
delegate?.showPosts(self)
|
||||
case .profile(_):
|
||||
delegate?.showProfile(self)
|
||||
}
|
||||
}
|
||||
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import Pageboy
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
|
@ -23,10 +22,7 @@ final class CustomSearchController: UISearchController {
|
|||
|
||||
// Fake search bar not works on iPad with UISplitViewController
|
||||
// check device and fallback to standard UISearchController
|
||||
final class SearchDetailViewController: PageboyViewController, NeedsDependency {
|
||||
|
||||
let logger = Logger(subsystem: "SearchDetail", category: "UI")
|
||||
|
||||
final class SearchDetailViewController: UIViewController, NeedsDependency {
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
|
@ -38,7 +34,6 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency {
|
|||
}()
|
||||
|
||||
var viewModel: SearchDetailViewModel!
|
||||
var viewControllers: [SearchResultViewController]!
|
||||
|
||||
let navigationBarVisualEffectBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
|
||||
let navigationBarBackgroundView = UIView()
|
||||
|
@ -73,9 +68,7 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency {
|
|||
searchController.searchBar.setShowsScope(true, animated: false)
|
||||
}
|
||||
searchBar.placeholder = L10n.Scene.Search.SearchBar.placeholder
|
||||
searchBar.scopeButtonTitles = SearchDetailViewModel.SearchScope.allCases.map { $0.segmentedControlTitle }
|
||||
searchBar.sizeToFit()
|
||||
searchBar.scopeBarBackgroundImage = UIImage()
|
||||
return searchBar
|
||||
}()
|
||||
|
||||
|
@ -86,9 +79,11 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency {
|
|||
searchHistoryViewController.viewModel = SearchHistoryViewModel(context: context, authContext: viewModel.authContext)
|
||||
return searchHistoryViewController
|
||||
}()
|
||||
}
|
||||
|
||||
extension SearchDetailViewController {
|
||||
private(set) lazy var searchResultsOverviewViewController: SearchResultsOverviewTableViewController = {
|
||||
let searchResultsOverviewViewController = SearchResultsOverviewTableViewController(appContext: context, authContext: viewModel.authContext)
|
||||
return searchResultsOverviewViewController
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
@ -119,81 +114,43 @@ extension SearchDetailViewController {
|
|||
searchHistoryViewController.view.pinToParent()
|
||||
}
|
||||
|
||||
transition = Transition(style: .fade, duration: 0.1)
|
||||
isScrollEnabled = false
|
||||
searchResultsOverviewViewController.delegate = self
|
||||
|
||||
viewControllers = viewModel.searchScopes.map { scope in
|
||||
let searchResultViewController = SearchResultViewController()
|
||||
searchResultViewController.context = context
|
||||
searchResultViewController.coordinator = coordinator
|
||||
searchResultViewController.viewModel = SearchResultViewModel(context: context, authContext: viewModel.authContext, searchScope: scope)
|
||||
addChild(searchResultsOverviewViewController)
|
||||
searchResultsOverviewViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(searchResultsOverviewViewController.view)
|
||||
searchResultsOverviewViewController.didMove(toParent: self)
|
||||
if isPhoneDevice {
|
||||
NSLayoutConstraint.activate([
|
||||
searchResultsOverviewViewController.view.topAnchor.constraint(equalTo: navigationBarBackgroundView.bottomAnchor),
|
||||
searchResultsOverviewViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
searchResultsOverviewViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
searchResultsOverviewViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
} else {
|
||||
searchResultsOverviewViewController.view.pinToParent()
|
||||
}
|
||||
|
||||
// bind searchText
|
||||
// bind search trigger
|
||||
// "local" search
|
||||
viewModel.searchText
|
||||
.assign(to: \.value, on: searchResultViewController.viewModel.searchText)
|
||||
.store(in: &searchResultViewController.disposeBag)
|
||||
|
||||
// bind navigationBarFrame
|
||||
viewModel.navigationBarFrame
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.value, on: searchResultViewController.viewModel.navigationBarFrame)
|
||||
.store(in: &searchResultViewController.disposeBag)
|
||||
return searchResultViewController
|
||||
}
|
||||
.sink { [weak self] searchText in
|
||||
guard let self else { return }
|
||||
|
||||
// set initial items from "all" search scope for non-appeared lists
|
||||
if let allSearchScopeViewController = viewControllers.first(where: { $0.viewModel.searchScope == .all }) {
|
||||
allSearchScopeViewController.viewModel.$items
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] items in
|
||||
guard let self = self else { return }
|
||||
guard self.currentViewController === allSearchScopeViewController else { return }
|
||||
for viewController in self.viewControllers where viewController != allSearchScopeViewController {
|
||||
// do not change appeared list
|
||||
guard !viewController.viewModel.viewDidAppear.value else { continue }
|
||||
// set initial items
|
||||
switch viewController.viewModel.searchScope {
|
||||
case .all:
|
||||
assertionFailure()
|
||||
break
|
||||
case .people:
|
||||
viewController.viewModel.userFetchedResultsController.userIDs = allSearchScopeViewController.viewModel.userFetchedResultsController.userIDs
|
||||
case .hashtags:
|
||||
viewController.viewModel.hashtags = allSearchScopeViewController.viewModel.hashtags
|
||||
case .posts:
|
||||
viewController.viewModel.statusFetchedResultsController.statusIDs = allSearchScopeViewController.viewModel.statusFetchedResultsController.statusIDs
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &allSearchScopeViewController.disposeBag)
|
||||
}
|
||||
|
||||
dataSource = self
|
||||
delegate = self
|
||||
|
||||
// bind search bar scope
|
||||
viewModel.selectedSearchScope
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] searchScope in
|
||||
guard let self = self else { return }
|
||||
if let index = self.viewModel.searchScopes.firstIndex(of: searchScope) {
|
||||
self.searchBar.selectedScopeButtonIndex = index
|
||||
self.scrollToPage(.at(index: index), animated: true)
|
||||
}
|
||||
self.searchResultsOverviewViewController.showStandardSearch(for: searchText)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind search trigger
|
||||
// delayed search on server
|
||||
viewModel.searchText
|
||||
.removeDuplicates()
|
||||
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
|
||||
.sink { [weak self] searchText in
|
||||
guard let self = self else { return }
|
||||
guard let searchResultViewController = self.currentViewController as? SearchResultViewController else {
|
||||
return
|
||||
}
|
||||
self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): trigger search \(searchText)")
|
||||
searchResultViewController.viewModel.stateMachine.enter(SearchResultViewModel.State.Loading.self)
|
||||
guard let self else { return }
|
||||
|
||||
self.searchResultsOverviewViewController.searchForSuggestions(for: searchText)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
@ -203,7 +160,9 @@ extension SearchDetailViewController {
|
|||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] searchText in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.searchHistoryViewController.view.isHidden = !searchText.isEmpty
|
||||
self.searchResultsOverviewViewController.view.isHidden = searchText.isEmpty
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
@ -253,7 +212,6 @@ extension SearchDetailViewController {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SearchDetailViewController {
|
||||
|
@ -292,7 +250,6 @@ extension SearchDetailViewController {
|
|||
searchController.searchBar.sizeToFit()
|
||||
}
|
||||
|
||||
searchBar.text = viewModel.searchText.value
|
||||
searchBar.delegate = self
|
||||
}
|
||||
|
||||
|
@ -305,12 +262,7 @@ extension SearchDetailViewController {
|
|||
// MARK: - UISearchBarDelegate
|
||||
extension SearchDetailViewController: UISearchBarDelegate {
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
||||
viewModel.selectedSearchScope.value = viewModel.searchScopes[selectedScope]
|
||||
}
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): searchTest \(searchText)")
|
||||
viewModel.searchText.value = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
|
@ -322,77 +274,23 @@ extension SearchDetailViewController: UISearchBarDelegate {
|
|||
navigationController?.popViewController(animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - PageboyViewControllerDataSource
|
||||
extension SearchDetailViewController: PageboyViewControllerDataSource {
|
||||
|
||||
func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int {
|
||||
return viewControllers.count
|
||||
//MARK: SearchResultsOverviewViewControllerDelegate
|
||||
extension SearchDetailViewController: SearchResultsOverviewTableViewControllerDeleagte {
|
||||
func showPosts(_ viewController: UIViewController) {
|
||||
//TODO: Implement
|
||||
}
|
||||
|
||||
func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? {
|
||||
guard index < viewControllers.count else { return nil }
|
||||
return viewControllers[index]
|
||||
func showPeople(_ viewController: UIViewController) {
|
||||
//TODO: Implement
|
||||
}
|
||||
|
||||
func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? {
|
||||
return .first
|
||||
func showProfile(_ viewController: UIViewController) {
|
||||
//TODO: Implement
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - PageboyViewControllerDelegate
|
||||
extension SearchDetailViewController: PageboyViewControllerDelegate {
|
||||
|
||||
func pageboyViewController(
|
||||
_ pageboyViewController: PageboyViewController,
|
||||
willScrollToPageAt index: PageboyViewController.PageIndex,
|
||||
direction: PageboyViewController.NavigationDirection,
|
||||
animated: Bool
|
||||
) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func pageboyViewController(
|
||||
_ pageboyViewController: PageboyViewController,
|
||||
didScrollTo position: CGPoint,
|
||||
direction: PageboyViewController.NavigationDirection,
|
||||
animated: Bool
|
||||
) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func pageboyViewController(
|
||||
_ pageboyViewController: PageboyViewController,
|
||||
didCancelScrollToPageAt index: PageboyViewController.PageIndex,
|
||||
returnToPageAt previousIndex: PageboyViewController.PageIndex
|
||||
) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func pageboyViewController(
|
||||
_ pageboyViewController: PageboyViewController,
|
||||
didScrollToPageAt index: PageboyViewController.PageIndex,
|
||||
direction: PageboyViewController.NavigationDirection,
|
||||
animated: Bool
|
||||
) {
|
||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): index \(index)")
|
||||
|
||||
let searchResultViewController = viewControllers[index]
|
||||
viewModel.selectedSearchScope.value = searchResultViewController.viewModel.searchScope
|
||||
|
||||
// trigger fetch
|
||||
searchResultViewController.viewModel.stateMachine.enter(SearchResultViewModel.State.Loading.self)
|
||||
}
|
||||
|
||||
|
||||
func pageboyViewController(
|
||||
_ pageboyViewController: PageboyViewController,
|
||||
didReloadWith currentViewController: UIViewController,
|
||||
currentPageIndex: PageboyViewController.PageIndex
|
||||
) {
|
||||
// do nothing
|
||||
func openLink(_ viewController: UIViewController) {
|
||||
//TODO: Implement
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
// Created by MainasuK Cirno on 2021-7-13.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
import Combine
|
||||
|
@ -34,22 +33,12 @@ final class SearchDetailViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
extension SearchDetailViewModel {
|
||||
enum SearchScope: CaseIterable {
|
||||
case all
|
||||
case people
|
||||
case hashtags
|
||||
case posts
|
||||
|
||||
var segmentedControlTitle: String {
|
||||
switch self {
|
||||
case .all: return L10n.Scene.Search.Searching.Segment.all
|
||||
case .people: return L10n.Scene.Search.Searching.Segment.people
|
||||
case .hashtags: return L10n.Scene.Search.Searching.Segment.hashtags
|
||||
case .posts: return L10n.Scene.Search.Searching.Segment.posts
|
||||
}
|
||||
}
|
||||
|
||||
var searchType: Mastodon.API.V2.Search.SearchType {
|
||||
switch self {
|
||||
case .all: return .default
|
||||
|
@ -59,4 +48,3 @@ extension SearchDetailViewModel {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import MetaTextKit
|
|||
|
||||
final class HashtagTableViewCell: UITableViewCell {
|
||||
|
||||
static let reuseIdentifier = "HashtagTableViewCell"
|
||||
|
||||
let primaryLabel = MetaLabel(style: .statusName)
|
||||
|
||||
let separatorLine = UIView.separatorLine
|
||||
|
|
|
@ -14,8 +14,6 @@ import MastodonUI
|
|||
|
||||
final class SearchResultViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
|
||||
let logger = Logger(subsystem: "SearchResultViewController", category: "ViewController")
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
// Created by MainasuK Cirno on 2021-7-14.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
|
|
|
@ -20,7 +20,7 @@ final class SearchResultViewModel {
|
|||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let searchScope: SearchDetailViewModel.SearchScope
|
||||
let searchScope: SearchScope
|
||||
let searchText = CurrentValueSubject<String, Never>("")
|
||||
@Published var hashtags: [Mastodon.Entity.Tag] = []
|
||||
let userFetchedResultsController: UserFetchedResultsController
|
||||
|
@ -48,7 +48,7 @@ final class SearchResultViewModel {
|
|||
}()
|
||||
let didDataSourceUpdate = PassthroughSubject<Void, Never>()
|
||||
|
||||
init(context: AppContext, authContext: AuthContext, searchScope: SearchDetailViewModel.SearchScope) {
|
||||
init(context: AppContext, authContext: AuthContext, searchScope: SearchScope = .all) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.searchScope = searchScope
|
||||
|
|
|
@ -14,6 +14,8 @@ import MastodonUI
|
|||
|
||||
final class StatusTableViewCell: UITableViewCell {
|
||||
|
||||
static let reuseIdentifier = "StatusTableViewCell"
|
||||
|
||||
static let marginForRegularHorizontalSizeClass: CGFloat = 64
|
||||
|
||||
weak var delegate: StatusTableViewCellDelegate?
|
||||
|
|
|
@ -17,6 +17,7 @@ protocol UserTableViewCellDelegate: UserViewDelegate, AnyObject { }
|
|||
|
||||
final class UserTableViewCell: UITableViewCell {
|
||||
|
||||
static let reuseIdentifier = "UserTableViewCell"
|
||||
weak var delegate: UserTableViewCellDelegate?
|
||||
|
||||
let userView = UserView()
|
||||
|
|
|
@ -97,7 +97,6 @@ extension UserFetchedResultsController {
|
|||
// MARK: - NSFetchedResultsControllerDelegate
|
||||
extension UserFetchedResultsController: NSFetchedResultsControllerDelegate {
|
||||
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
let indexes = userIDs
|
||||
let objects = fetchedResultsController.fetchedObjects ?? []
|
||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
|||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/history/)
|
||||
public struct History: Codable, Sendable {
|
||||
public struct History: Hashable, Codable, Sendable {
|
||||
/// UNIX timestamp on midnight of the given day
|
||||
public let day: Date
|
||||
public let uses: String
|
||||
|
|
|
@ -361,8 +361,6 @@ extension StatusView.ViewModel {
|
|||
statusView.statusCardControl.alpha = isContentReveal ? 1 : 0
|
||||
|
||||
statusView.setSpoilerOverlayViewHidden(isHidden: isContentReveal)
|
||||
|
||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): isContentReveal: \(isContentReveal)")
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
@ -400,7 +398,6 @@ extension StatusView.ViewModel {
|
|||
$mediaViewConfigurations
|
||||
.sink { [weak self] configurations in
|
||||
guard let self = self else { return }
|
||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): configure media")
|
||||
|
||||
statusView.mediaGridContainerView.prepareForReuse()
|
||||
|
||||
|
|
Loading…
Reference in New Issue