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 */; };
|
D8099078294BC8A30050219F /* PrivacyTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099077294BC8A30050219F /* PrivacyTableViewController.swift */; };
|
||||||
D809907A294BC9390050219F /* PrivacyTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099079294BC9390050219F /* PrivacyTableViewCell.swift */; };
|
D809907A294BC9390050219F /* PrivacyTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099079294BC9390050219F /* PrivacyTableViewCell.swift */; };
|
||||||
D809907C294D25510050219F /* PrivacyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D809907B294D25510050219F /* PrivacyViewModel.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 */; };
|
D8363B1629469CE200A74079 /* OnboardingNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8363B1529469CE200A74079 /* OnboardingNextView.swift */; };
|
||||||
D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; };
|
D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; };
|
||||||
D87BFC8D291EB81200FEE264 /* MastodonLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D82463542A52B47B00A3DBDD /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = be; path = be.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
|
@ -1791,6 +1797,24 @@
|
||||||
path = Privacy;
|
path = Privacy;
|
||||||
sourceTree = "<group>";
|
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 */ = {
|
D8A6AB68291C50F3003AB663 /* Login */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -2898,6 +2922,7 @@
|
||||||
DBF1D24F269DAF6100C1C08A /* SearchDetail */ = {
|
DBF1D24F269DAF6100C1C08A /* SearchDetail */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D81A22732AB4641F00905D71 /* Search Results Overview */,
|
||||||
DB4F0964269ED06700D62E92 /* SearchResult */,
|
DB4F0964269ED06700D62E92 /* SearchResult */,
|
||||||
DBF1D252269DB01700C1C08A /* SearchHistory */,
|
DBF1D252269DB01700C1C08A /* SearchHistory */,
|
||||||
DBF1D24D269DAF5D00C1C08A /* SearchDetailViewController.swift */,
|
DBF1D24D269DAF5D00C1C08A /* SearchDetailViewController.swift */,
|
||||||
|
@ -3538,6 +3563,7 @@
|
||||||
DB3E6FE02806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift in Sources */,
|
DB3E6FE02806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift in Sources */,
|
||||||
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||||
DB0FCB7427956939006C02E2 /* DataSourceFacade+Status.swift in Sources */,
|
DB0FCB7427956939006C02E2 /* DataSourceFacade+Status.swift in Sources */,
|
||||||
|
D81A22782AB4782400905D71 /* SearchResultOverviewSection.swift in Sources */,
|
||||||
DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */,
|
DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */,
|
||||||
DB63F75A279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift in Sources */,
|
DB63F75A279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift in Sources */,
|
||||||
DB023D26279FFB0A005AC798 /* ShareActivityProvider.swift in Sources */,
|
DB023D26279FFB0A005AC798 /* ShareActivityProvider.swift in Sources */,
|
||||||
|
@ -3692,6 +3718,7 @@
|
||||||
6213AF5A28939C8400BCADB6 /* BookmarkViewModel.swift in Sources */,
|
6213AF5A28939C8400BCADB6 /* BookmarkViewModel.swift in Sources */,
|
||||||
5B24BBDB262DB14800A9381B /* ReportStatusViewModel+Diffable.swift in Sources */,
|
5B24BBDB262DB14800A9381B /* ReportStatusViewModel+Diffable.swift in Sources */,
|
||||||
5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */,
|
5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */,
|
||||||
|
D81A22752AB4643200905D71 /* SearchResultsOverviewTableViewController.swift in Sources */,
|
||||||
2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */,
|
2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */,
|
||||||
DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */,
|
DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */,
|
||||||
DB852D1926FAEB6B00FC9D81 /* SidebarViewController.swift in Sources */,
|
DB852D1926FAEB6B00FC9D81 /* SidebarViewController.swift in Sources */,
|
||||||
|
@ -3790,6 +3817,7 @@
|
||||||
2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */,
|
2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */,
|
||||||
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */,
|
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */,
|
||||||
2A3D9B7E29A8F33A00F30313 /* StatusHistoryView.swift in Sources */,
|
2A3D9B7E29A8F33A00F30313 /* StatusHistoryView.swift in Sources */,
|
||||||
|
D81A227B2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift in Sources */,
|
||||||
6213AF5E2893A8B200BCADB6 /* DataSourceFacade+Bookmark.swift in Sources */,
|
6213AF5E2893A8B200BCADB6 /* DataSourceFacade+Bookmark.swift in Sources */,
|
||||||
DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */,
|
DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */,
|
||||||
DBF156E22702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m in Sources */,
|
DBF156E22702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m in Sources */,
|
||||||
|
|
|
@ -41,13 +41,8 @@ extension HomeTimelineViewModel {
|
||||||
.sink { [weak self] records in
|
.sink { [weak self] records in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard let diffableDataSource = self.diffableDataSource 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
|
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()
|
let oldSnapshot = diffableDataSource.snapshot()
|
||||||
var newSnapshot: NSDiffableDataSourceSnapshot<StatusSection, StatusItem> = {
|
var newSnapshot: NSDiffableDataSourceSnapshot<StatusSection, StatusItem> = {
|
||||||
let newItems = records.map { record in
|
let newItems = records.map { record in
|
||||||
|
|
|
@ -21,9 +21,6 @@ final class HeightFixedSearchBar: UISearchBar {
|
||||||
}
|
}
|
||||||
|
|
||||||
final class SearchViewController: UIViewController, NeedsDependency {
|
final class SearchViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
let logger = Logger(subsystem: "SearchViewController", category: "ViewController")
|
|
||||||
|
|
||||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
@ -37,16 +34,6 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
||||||
let titleViewContainer = UIView()
|
let titleViewContainer = UIView()
|
||||||
let searchBar = HeightFixedSearchBar()
|
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
|
// value is the initial search text to set
|
||||||
let searchBarTapPublisher = PassthroughSubject<String, Never>()
|
let searchBarTapPublisher = PassthroughSubject<String, Never>()
|
||||||
|
|
||||||
|
@ -62,11 +49,6 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
||||||
)
|
)
|
||||||
return viewController
|
return viewController
|
||||||
}()
|
}()
|
||||||
|
|
||||||
deinit {
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchViewController {
|
extension SearchViewController {
|
||||||
|
@ -85,30 +67,12 @@ extension SearchViewController {
|
||||||
title = L10n.Scene.Search.title
|
title = L10n.Scene.Search.title
|
||||||
|
|
||||||
setupSearchBar()
|
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 }
|
guard let discoveryViewController = self.discoveryViewController else { return }
|
||||||
|
|
||||||
addChild(discoveryViewController)
|
addChild(discoveryViewController)
|
||||||
discoveryViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
discoveryViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(discoveryViewController.view)
|
view.addSubview(discoveryViewController.view)
|
||||||
discoveryViewController.view.pinToParent()
|
discoveryViewController.view.pinToParent()
|
||||||
|
|
||||||
// discoveryViewController.view.isHidden = true
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
@ -183,12 +147,8 @@ extension SearchViewController: UISearchBarDelegate {
|
||||||
// MARK: - UISearchControllerDelegate
|
// MARK: - UISearchControllerDelegate
|
||||||
extension SearchViewController: UISearchControllerDelegate {
|
extension SearchViewController: UISearchControllerDelegate {
|
||||||
func willDismissSearchController(_ searchController: UISearchController) {
|
func willDismissSearchController(_ searchController: UISearchController) {
|
||||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
|
||||||
searchController.isActive = true
|
searchController.isActive = true
|
||||||
}
|
}
|
||||||
func didPresentSearchController(_ searchController: UISearchController) {
|
|
||||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - ScrollViewContainer
|
// MARK: - ScrollViewContainer
|
||||||
|
@ -200,23 +160,3 @@ extension SearchViewController: ScrollViewContainer {
|
||||||
discoveryViewController?.scrollToTop(animated: animated)
|
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.context = context
|
||||||
self.authContext = authContext
|
self.authContext = authContext
|
||||||
super.init()
|
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 os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import Pageboy
|
|
||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
import MastodonCore
|
import MastodonCore
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
|
@ -23,10 +22,7 @@ final class CustomSearchController: UISearchController {
|
||||||
|
|
||||||
// Fake search bar not works on iPad with UISplitViewController
|
// Fake search bar not works on iPad with UISplitViewController
|
||||||
// check device and fallback to standard UISearchController
|
// check device and fallback to standard UISearchController
|
||||||
final class SearchDetailViewController: PageboyViewController, NeedsDependency {
|
final class SearchDetailViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
let logger = Logger(subsystem: "SearchDetail", category: "UI")
|
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
var observations = Set<NSKeyValueObservation>()
|
var observations = Set<NSKeyValueObservation>()
|
||||||
|
|
||||||
|
@ -38,7 +34,6 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var viewModel: SearchDetailViewModel!
|
var viewModel: SearchDetailViewModel!
|
||||||
var viewControllers: [SearchResultViewController]!
|
|
||||||
|
|
||||||
let navigationBarVisualEffectBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
|
let navigationBarVisualEffectBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
|
||||||
let navigationBarBackgroundView = UIView()
|
let navigationBarBackgroundView = UIView()
|
||||||
|
@ -73,9 +68,7 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency {
|
||||||
searchController.searchBar.setShowsScope(true, animated: false)
|
searchController.searchBar.setShowsScope(true, animated: false)
|
||||||
}
|
}
|
||||||
searchBar.placeholder = L10n.Scene.Search.SearchBar.placeholder
|
searchBar.placeholder = L10n.Scene.Search.SearchBar.placeholder
|
||||||
searchBar.scopeButtonTitles = SearchDetailViewModel.SearchScope.allCases.map { $0.segmentedControlTitle }
|
|
||||||
searchBar.sizeToFit()
|
searchBar.sizeToFit()
|
||||||
searchBar.scopeBarBackgroundImage = UIImage()
|
|
||||||
return searchBar
|
return searchBar
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -86,9 +79,11 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency {
|
||||||
searchHistoryViewController.viewModel = SearchHistoryViewModel(context: context, authContext: viewModel.authContext)
|
searchHistoryViewController.viewModel = SearchHistoryViewModel(context: context, authContext: viewModel.authContext)
|
||||||
return searchHistoryViewController
|
return searchHistoryViewController
|
||||||
}()
|
}()
|
||||||
}
|
|
||||||
|
|
||||||
extension SearchDetailViewController {
|
private(set) lazy var searchResultsOverviewViewController: SearchResultsOverviewTableViewController = {
|
||||||
|
let searchResultsOverviewViewController = SearchResultsOverviewTableViewController(appContext: context, authContext: viewModel.authContext)
|
||||||
|
return searchResultsOverviewViewController
|
||||||
|
}()
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
@ -119,81 +114,43 @@ extension SearchDetailViewController {
|
||||||
searchHistoryViewController.view.pinToParent()
|
searchHistoryViewController.view.pinToParent()
|
||||||
}
|
}
|
||||||
|
|
||||||
transition = Transition(style: .fade, duration: 0.1)
|
searchResultsOverviewViewController.delegate = self
|
||||||
isScrollEnabled = false
|
|
||||||
|
|
||||||
viewControllers = viewModel.searchScopes.map { scope in
|
addChild(searchResultsOverviewViewController)
|
||||||
let searchResultViewController = SearchResultViewController()
|
searchResultsOverviewViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
searchResultViewController.context = context
|
view.addSubview(searchResultsOverviewViewController.view)
|
||||||
searchResultViewController.coordinator = coordinator
|
searchResultsOverviewViewController.didMove(toParent: self)
|
||||||
searchResultViewController.viewModel = SearchResultViewModel(context: context, authContext: viewModel.authContext, searchScope: scope)
|
if isPhoneDevice {
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
// bind searchText
|
searchResultsOverviewViewController.view.topAnchor.constraint(equalTo: navigationBarBackgroundView.bottomAnchor),
|
||||||
viewModel.searchText
|
searchResultsOverviewViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
.assign(to: \.value, on: searchResultViewController.viewModel.searchText)
|
searchResultsOverviewViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
.store(in: &searchResultViewController.disposeBag)
|
searchResultsOverviewViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
// bind navigationBarFrame
|
} else {
|
||||||
viewModel.navigationBarFrame
|
searchResultsOverviewViewController.view.pinToParent()
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.assign(to: \.value, on: searchResultViewController.viewModel.navigationBarFrame)
|
|
||||||
.store(in: &searchResultViewController.disposeBag)
|
|
||||||
return searchResultViewController
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// set initial items from "all" search scope for non-appeared lists
|
// bind search trigger
|
||||||
if let allSearchScopeViewController = viewControllers.first(where: { $0.viewModel.searchScope == .all }) {
|
// "local" search
|
||||||
allSearchScopeViewController.viewModel.$items
|
viewModel.searchText
|
||||||
.receive(on: DispatchQueue.main)
|
.removeDuplicates()
|
||||||
.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)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] searchScope in
|
.sink { [weak self] searchText in
|
||||||
guard let self = self else { return }
|
guard let self else { return }
|
||||||
if let index = self.viewModel.searchScopes.firstIndex(of: searchScope) {
|
|
||||||
self.searchBar.selectedScopeButtonIndex = index
|
self.searchResultsOverviewViewController.showStandardSearch(for: searchText)
|
||||||
self.scrollToPage(.at(index: index), animated: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// bind search trigger
|
// delayed search on server
|
||||||
viewModel.searchText
|
viewModel.searchText
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
|
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
|
||||||
.sink { [weak self] searchText in
|
.sink { [weak self] searchText in
|
||||||
guard let self = self else { return }
|
guard let self else { return }
|
||||||
guard let searchResultViewController = self.currentViewController as? SearchResultViewController else {
|
|
||||||
return
|
self.searchResultsOverviewViewController.searchForSuggestions(for: searchText)
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
@ -203,7 +160,9 @@ extension SearchDetailViewController {
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] searchText in
|
.sink { [weak self] searchText in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
self.searchHistoryViewController.view.isHidden = !searchText.isEmpty
|
self.searchHistoryViewController.view.isHidden = !searchText.isEmpty
|
||||||
|
self.searchResultsOverviewViewController.view.isHidden = searchText.isEmpty
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
@ -253,7 +212,6 @@ extension SearchDetailViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchDetailViewController {
|
extension SearchDetailViewController {
|
||||||
|
@ -292,7 +250,6 @@ extension SearchDetailViewController {
|
||||||
searchController.searchBar.sizeToFit()
|
searchController.searchBar.sizeToFit()
|
||||||
}
|
}
|
||||||
|
|
||||||
searchBar.text = viewModel.searchText.value
|
|
||||||
searchBar.delegate = self
|
searchBar.delegate = self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,12 +262,7 @@ extension SearchDetailViewController {
|
||||||
// MARK: - UISearchBarDelegate
|
// MARK: - UISearchBarDelegate
|
||||||
extension SearchDetailViewController: UISearchBarDelegate {
|
extension SearchDetailViewController: UISearchBarDelegate {
|
||||||
|
|
||||||
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
|
||||||
viewModel.selectedSearchScope.value = viewModel.searchScopes[selectedScope]
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
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)
|
viewModel.searchText.value = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,77 +274,23 @@ extension SearchDetailViewController: UISearchBarDelegate {
|
||||||
navigationController?.popViewController(animated: false)
|
navigationController?.popViewController(animated: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - PageboyViewControllerDataSource
|
//MARK: SearchResultsOverviewViewControllerDelegate
|
||||||
extension SearchDetailViewController: PageboyViewControllerDataSource {
|
extension SearchDetailViewController: SearchResultsOverviewTableViewControllerDeleagte {
|
||||||
|
func showPosts(_ viewController: UIViewController) {
|
||||||
func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int {
|
//TODO: Implement
|
||||||
return viewControllers.count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? {
|
func showPeople(_ viewController: UIViewController) {
|
||||||
guard index < viewControllers.count else { return nil }
|
//TODO: Implement
|
||||||
return viewControllers[index]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? {
|
func showProfile(_ viewController: UIViewController) {
|
||||||
return .first
|
//TODO: Implement
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
func openLink(_ 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
// Created by MainasuK Cirno on 2021-7-13.
|
// Created by MainasuK Cirno on 2021-7-13.
|
||||||
//
|
//
|
||||||
|
|
||||||
import os.log
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreGraphics
|
import CoreGraphics
|
||||||
import Combine
|
import Combine
|
||||||
|
@ -15,48 +14,37 @@ import MastodonAsset
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
|
|
||||||
final class SearchDetailViewModel {
|
final class SearchDetailViewModel {
|
||||||
|
|
||||||
// input
|
// input
|
||||||
let authContext: AuthContext
|
let authContext: AuthContext
|
||||||
var needsBecomeFirstResponder = false
|
var needsBecomeFirstResponder = false
|
||||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||||
let navigationBarFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
let navigationBarFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
||||||
|
|
||||||
// output
|
// output
|
||||||
let searchScopes = SearchScope.allCases
|
let searchScopes = SearchScope.allCases
|
||||||
let selectedSearchScope = CurrentValueSubject<SearchScope, Never>(.all)
|
let selectedSearchScope = CurrentValueSubject<SearchScope, Never>(.all)
|
||||||
let searchText: CurrentValueSubject<String, Never>
|
let searchText: CurrentValueSubject<String, Never>
|
||||||
let searchActionPublisher = PassthroughSubject<Void, Never>()
|
let searchActionPublisher = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
init(authContext: AuthContext, initialSearchText: String = "") {
|
init(authContext: AuthContext, initialSearchText: String = "") {
|
||||||
self.authContext = authContext
|
self.authContext = authContext
|
||||||
self.searchText = CurrentValueSubject(initialSearchText)
|
self.searchText = CurrentValueSubject(initialSearchText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchDetailViewModel {
|
enum SearchScope: CaseIterable {
|
||||||
enum SearchScope: CaseIterable {
|
case all
|
||||||
case all
|
case people
|
||||||
case people
|
case hashtags
|
||||||
case hashtags
|
case posts
|
||||||
case posts
|
|
||||||
|
var searchType: Mastodon.API.V2.Search.SearchType {
|
||||||
var segmentedControlTitle: String {
|
switch self {
|
||||||
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
|
case .all: return .default
|
||||||
case .people: return .accounts
|
case .people: return .accounts
|
||||||
case .hashtags: return .hashtags
|
case .hashtags: return .hashtags
|
||||||
case .posts: return .statuses
|
case .posts: return .statuses
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import UIKit
|
||||||
import MetaTextKit
|
import MetaTextKit
|
||||||
|
|
||||||
final class HashtagTableViewCell: UITableViewCell {
|
final class HashtagTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
static let reuseIdentifier = "HashtagTableViewCell"
|
||||||
|
|
||||||
let primaryLabel = MetaLabel(style: .statusName)
|
let primaryLabel = MetaLabel(style: .statusName)
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,6 @@ import MastodonUI
|
||||||
|
|
||||||
final class SearchResultViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
final class SearchResultViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||||
|
|
||||||
let logger = Logger(subsystem: "SearchResultViewController", category: "ViewController")
|
|
||||||
|
|
||||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
// Created by MainasuK Cirno on 2021-7-14.
|
// Created by MainasuK Cirno on 2021-7-14.
|
||||||
//
|
//
|
||||||
|
|
||||||
import os.log
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import GameplayKit
|
import GameplayKit
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
|
@ -20,7 +20,7 @@ final class SearchResultViewModel {
|
||||||
// input
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let authContext: AuthContext
|
let authContext: AuthContext
|
||||||
let searchScope: SearchDetailViewModel.SearchScope
|
let searchScope: SearchScope
|
||||||
let searchText = CurrentValueSubject<String, Never>("")
|
let searchText = CurrentValueSubject<String, Never>("")
|
||||||
@Published var hashtags: [Mastodon.Entity.Tag] = []
|
@Published var hashtags: [Mastodon.Entity.Tag] = []
|
||||||
let userFetchedResultsController: UserFetchedResultsController
|
let userFetchedResultsController: UserFetchedResultsController
|
||||||
|
@ -48,7 +48,7 @@ final class SearchResultViewModel {
|
||||||
}()
|
}()
|
||||||
let didDataSourceUpdate = PassthroughSubject<Void, Never>()
|
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.context = context
|
||||||
self.authContext = authContext
|
self.authContext = authContext
|
||||||
self.searchScope = searchScope
|
self.searchScope = searchScope
|
||||||
|
|
|
@ -13,6 +13,8 @@ import MastodonLocalization
|
||||||
import MastodonUI
|
import MastodonUI
|
||||||
|
|
||||||
final class StatusTableViewCell: UITableViewCell {
|
final class StatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
static let reuseIdentifier = "StatusTableViewCell"
|
||||||
|
|
||||||
static let marginForRegularHorizontalSizeClass: CGFloat = 64
|
static let marginForRegularHorizontalSizeClass: CGFloat = 64
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,8 @@ import MastodonSDK
|
||||||
protocol UserTableViewCellDelegate: UserViewDelegate, AnyObject { }
|
protocol UserTableViewCellDelegate: UserViewDelegate, AnyObject { }
|
||||||
|
|
||||||
final class UserTableViewCell: UITableViewCell {
|
final class UserTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
static let reuseIdentifier = "UserTableViewCell"
|
||||||
weak var delegate: UserTableViewCellDelegate?
|
weak var delegate: UserTableViewCellDelegate?
|
||||||
|
|
||||||
let userView = UserView()
|
let userView = UserView()
|
||||||
|
|
|
@ -97,7 +97,6 @@ extension UserFetchedResultsController {
|
||||||
// MARK: - NSFetchedResultsControllerDelegate
|
// MARK: - NSFetchedResultsControllerDelegate
|
||||||
extension UserFetchedResultsController: NSFetchedResultsControllerDelegate {
|
extension UserFetchedResultsController: NSFetchedResultsControllerDelegate {
|
||||||
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
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 indexes = userIDs
|
||||||
let objects = fetchedResultsController.fetchedObjects ?? []
|
let objects = fetchedResultsController.fetchedObjects ?? []
|
||||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
||||||
/// 2021/1/28
|
/// 2021/1/28
|
||||||
/// # Reference
|
/// # Reference
|
||||||
/// [Document](https://docs.joinmastodon.org/entities/history/)
|
/// [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
|
/// UNIX timestamp on midnight of the given day
|
||||||
public let day: Date
|
public let day: Date
|
||||||
public let uses: String
|
public let uses: String
|
||||||
|
|
|
@ -361,8 +361,6 @@ extension StatusView.ViewModel {
|
||||||
statusView.statusCardControl.alpha = isContentReveal ? 1 : 0
|
statusView.statusCardControl.alpha = isContentReveal ? 1 : 0
|
||||||
|
|
||||||
statusView.setSpoilerOverlayViewHidden(isHidden: isContentReveal)
|
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)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
@ -400,7 +398,6 @@ extension StatusView.ViewModel {
|
||||||
$mediaViewConfigurations
|
$mediaViewConfigurations
|
||||||
.sink { [weak self] configurations in
|
.sink { [weak self] configurations in
|
||||||
guard let self = self else { return }
|
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()
|
statusView.mediaGridContainerView.prepareForReuse()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue