From 0fa75fc1d30c6ee3bab3a672869652da2216b197 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 14 Sep 2023 16:51:39 +0200 Subject: [PATCH 01/34] Remove some logs while figuring out how search works (IOS-141) --- ...urceProvider+StatusTableViewCellDelegate.swift | 6 ------ .../DataSourceProvider+UITableViewDelegate.swift | 3 --- .../Protocol/Provider/DataSourceProvider.swift | 1 - .../SearchDetail/SearchDetailViewModel.swift | 5 ----- ...hResultViewController+DataSourceProvider.swift | 1 - .../SearchResultViewModel+State.swift | 15 --------------- 6 files changed, 31 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index dc6a9fda5..85718c94a 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -352,10 +352,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte choices: [choice], authenticationBox: authContext.mastodonAuthenticationBox ) - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): vote poll for \(choice) success") } catch { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): vote poll fail: \(error.localizedDescription)") - // restore voting state try await managedObjectContext.performChanges { guard @@ -411,10 +408,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte choices: choices, authenticationBox: authContext.mastodonAuthenticationBox ) - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): vote poll for \(choices) success") } catch { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): vote poll fail: \(error.localizedDescription)") - // restore voting state try await managedObjectContext.performChanges { guard let poll = poll.object(in: managedObjectContext) else { return } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index 6ced42601..299951ce2 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -15,7 +15,6 @@ import MastodonLocalization extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvider { func aspectTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): indexPath: \(indexPath.debugDescription)") Task { let source = DataSourceItem.Source(tableViewCell: nil, indexPath: indexPath) guard let item = await item(from: source) else { @@ -77,7 +76,6 @@ extension UITableViewDelegate where Self: DataSourceProvider & MediaPreviewableV contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint ) -> UIContextMenuConfiguration? { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") guard let cell = tableView.cellForRow(at: indexPath) as? StatusViewContainerTableViewCell else { return nil } @@ -238,7 +236,6 @@ extension UITableViewDelegate where Self: DataSourceProvider & MediaPreviewableV willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating ) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") guard let configuration = configuration as? TimelineTableViewCellContextMenuConfiguration else { return } guard let indexPath = configuration.indexPath, let index = configuration.index else { return } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider.swift b/Mastodon/Protocol/Provider/DataSourceProvider.swift index 425e40417..d7c86f56d 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider.swift @@ -45,6 +45,5 @@ extension DataSourceItem { } protocol DataSourceProvider: NeedsDependency & UIViewController { - var logger: Logger { get } func item(from source: DataSourceItem.Source) async -> DataSourceItem? } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewModel.swift index 779aaa2dc..425722e37 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewModel.swift @@ -32,11 +32,6 @@ final class SearchDetailViewModel { self.authContext = authContext self.searchText = CurrentValueSubject(initialSearchText) } - - deinit { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - } - } extension SearchDetailViewModel { diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift index 71ac81ef6..a2f41aefb 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift @@ -40,7 +40,6 @@ extension SearchResultViewController: DataSourceProvider { extension SearchResultViewController { func aspectTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): indexPath: \(indexPath.debugDescription)") Task { let source = DataSourceItem.Source(tableViewCell: nil, indexPath: indexPath) guard let item = await item(from: source) else { diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift index 9b12e1af0..3fdf65436 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift @@ -14,8 +14,6 @@ import MastodonCore extension SearchResultViewModel { class State: GKState { - let logger = Logger(subsystem: "SearchResultViewModel.State", category: "StateMachine") - let id = UUID() weak var viewModel: SearchResultViewModel? @@ -24,22 +22,10 @@ extension SearchResultViewModel { self.viewModel = viewModel } - override func didEnter(from previousState: GKState?) { - super.didEnter(from: previousState) - - let from = previousState.flatMap { String(describing: $0) } ?? "nil" - let to = String(describing: self) - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") - } - @MainActor func enter(state: State.Type) { stateMachine?.enter(state) } - - deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") - } } } @@ -165,7 +151,6 @@ extension SearchResultViewModel.State { viewModel.hashtags = hashtags } catch { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): search \(searchText) fail: \(error.localizedDescription)") await enter(state: Fail.self) } } // end Task From c89203409371c2b88abed7c9e294688ed83e703c Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 15 Sep 2023 10:49:32 +0200 Subject: [PATCH 02/34] More cleanup (IOS-141) --- Mastodon.xcodeproj/project.pbxproj | 4 - .../Search/SearchViewModel+Diffable.swift | 42 ------ ...ySectionHeaderCollectionReusableView.swift | 5 +- .../SearchHistoryViewController.swift | 4 - .../SearchHistoryViewModel.swift | 68 --------- .../SearchResultViewController.swift | 73 ---------- .../SearchResult/SearchResultViewModel.swift | 133 ------------------ 7 files changed, 1 insertion(+), 328 deletions(-) delete mode 100644 Mastodon/Scene/Search/Search/SearchViewModel+Diffable.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index c46ea5ec7..e8c01abdd 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -194,7 +194,6 @@ DB0FCB842796B2A2006C02E2 /* FavoriteViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB832796B2A2006C02E2 /* FavoriteViewController+DataSourceProvider.swift */; }; DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB852796BDA1006C02E2 /* SearchSection.swift */; }; DB0FCB882796BDA9006C02E2 /* SearchItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB872796BDA9006C02E2 /* SearchItem.swift */; }; - DB0FCB8C2796BF8D006C02E2 /* SearchViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB8B2796BF8D006C02E2 /* SearchViewModel+Diffable.swift */; }; DB0FCB8E2796C0B7006C02E2 /* TrendCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB8D2796C0B7006C02E2 /* TrendCollectionViewCell.swift */; }; DB0FCB922796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB912796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift */; }; DB0FCB942797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB932797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift */; }; @@ -847,7 +846,6 @@ DB0FCB832796B2A2006C02E2 /* FavoriteViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoriteViewController+DataSourceProvider.swift"; sourceTree = ""; }; DB0FCB852796BDA1006C02E2 /* SearchSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSection.swift; sourceTree = ""; }; DB0FCB872796BDA9006C02E2 /* SearchItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchItem.swift; sourceTree = ""; }; - DB0FCB8B2796BF8D006C02E2 /* SearchViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewModel+Diffable.swift"; sourceTree = ""; }; DB0FCB8D2796C0B7006C02E2 /* TrendCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendCollectionViewCell.swift; sourceTree = ""; }; DB0FCB912796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendSectionHeaderCollectionReusableView.swift; sourceTree = ""; }; DB0FCB932797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewModel+Diffable.swift"; sourceTree = ""; }; @@ -2933,7 +2931,6 @@ 2DE0FAC62615F5D200CDF649 /* View */, DB9D6BE825E4F5340051B173 /* SearchViewController.swift */, 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */, - DB0FCB8B2796BF8D006C02E2 /* SearchViewModel+Diffable.swift */, ); path = Search; sourceTree = ""; @@ -3593,7 +3590,6 @@ DB0FCB9C27980AB6006C02E2 /* HashtagTimelineViewController+DataSourceProvider.swift in Sources */, DB63F76F279A7D1100455B82 /* NotificationTableViewCell.swift in Sources */, 2A1BF99529F7E68400FA1BA5 /* DataSourceFacade+UserView.swift in Sources */, - DB0FCB8C2796BF8D006C02E2 /* SearchViewModel+Diffable.swift in Sources */, DBEFCD76282A143F00C0ABEA /* ReportStatusViewController.swift in Sources */, DBDFF1952805561700557A48 /* DiscoveryPostsViewModel+Diffable.swift in Sources */, DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */, diff --git a/Mastodon/Scene/Search/Search/SearchViewModel+Diffable.swift b/Mastodon/Scene/Search/Search/SearchViewModel+Diffable.swift deleted file mode 100644 index 3f448289a..000000000 --- a/Mastodon/Scene/Search/Search/SearchViewModel+Diffable.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// SearchViewModel+Diffable.swift -// Mastodon -// -// Created by MainasuK on 2022-1-18. -// - -import UIKit -import MastodonSDK - -//extension SearchViewModel { -// -// func setupDiffableDataSource( -// collectionView: UICollectionView -// ) { -// diffableDataSource = SearchSection.diffableDataSource( -// collectionView: collectionView, -// context: context -// ) -// -// var snapshot = NSDiffableDataSourceSnapshot() -// snapshot.appendSections([.trend]) -// diffableDataSource?.apply(snapshot) -// -// $hashtags -// .receive(on: DispatchQueue.main) -// .sink { [weak self] hashtags in -// guard let self = self else { return } -// guard let diffableDataSource = self.diffableDataSource else { return } -// -// var snapshot = NSDiffableDataSourceSnapshot() -// snapshot.appendSections([.trend]) -// -// let trendItems = hashtags.map { SearchItem.trend($0) } -// snapshot.appendItems(trendItems, toSection: .trend) -// -// diffableDataSource.apply(snapshot) -// } -// .store(in: &disposeBag) -// } -// -//} diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistorySectionHeaderCollectionReusableView.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistorySectionHeaderCollectionReusableView.swift index b6f30f94a..170f59934 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistorySectionHeaderCollectionReusableView.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistorySectionHeaderCollectionReusableView.swift @@ -16,9 +16,7 @@ protocol SearchHistorySectionHeaderCollectionReusableViewDelegate: AnyObject, Us } final class SearchHistorySectionHeaderCollectionReusableView: UICollectionReusableView { - - let logger = Logger(subsystem: "SearchHistorySectionHeaderCollectionReusableView", category: "View") - + weak var delegate: SearchHistorySectionHeaderCollectionReusableViewDelegate? let primaryLabel: UILabel = { @@ -78,7 +76,6 @@ extension SearchHistorySectionHeaderCollectionReusableView { extension SearchHistorySectionHeaderCollectionReusableView { @objc private func clearButtonDidPressed(_ sender: UIButton) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") delegate?.searchHistorySectionHeaderCollectionReusableView(self, clearButtonDidPressed: sender) } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift index ff15b08ed..2ef2c8440 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift @@ -68,8 +68,6 @@ extension SearchHistoryViewController { extension SearchHistoryViewController: 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) } @@ -116,8 +114,6 @@ extension SearchHistoryViewController: SearchHistorySectionHeaderCollectionReusa _ searchHistorySectionHeaderCollectionReusableView: SearchHistorySectionHeaderCollectionReusableView, clearButtonDidPressed button: UIButton ) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - Task { try await DataSourceFacade.responseToDeleteSearchHistory( provider: self diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift index d20f2d495..5078895c0 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift @@ -32,71 +32,3 @@ final class SearchHistoryViewModel { } } - -//extension SearchHistoryViewModel { -// func persistSearchHistory(for item: SearchHistoryItem) { -// guard let box = context.authenticationService.activeMastodonAuthenticationBox.value else { return } -// let property = SearchHistory.Property(domain: box.domain, userID: box.userID) -// -// switch item { -// case .account(let objectID): -// let managedObjectContext = context.backgroundManagedObjectContext -// managedObjectContext.performChanges { -// guard let user = try? managedObjectContext.existingObject(with: objectID) as? MastodonUser else { return } -// if let searchHistory = user.findSearchHistory(domain: box.domain, userID: box.userID) { -// searchHistory.update(updatedAt: Date()) -// } else { -// SearchHistory.insert(into: managedObjectContext, property: property, account: user) -// } -// } -// .sink { result in -// switch result { -// case .failure(let error): -// assertionFailure(error.localizedDescription) -// case .success: -// break -// } -// } -// .store(in: &context.disposeBag) -// -// case .hashtag(let objectID): -// let managedObjectContext = context.backgroundManagedObjectContext -// managedObjectContext.performChanges { -// guard let hashtag = try? managedObjectContext.existingObject(with: objectID) as? Tag else { return } -// if let searchHistory = hashtag.findSearchHistory(domain: box.domain, userID: box.userID) { -// searchHistory.update(updatedAt: Date()) -// } else { -// _ = SearchHistory.insert(into: managedObjectContext, property: property, hashtag: hashtag) -// } -// } -// .sink { result in -// switch result { -// case .failure(let error): -// assertionFailure(error.localizedDescription) -// case .success: -// break -// } -// } -// .store(in: &context.disposeBag) -// -// case .status: -// // FIXME: -// break -// } -// } -// -// func clearSearchHistory() { -// let managedObjectContext = context.backgroundManagedObjectContext -// managedObjectContext.performChanges { -// let request = SearchHistory.sortedFetchRequest -// let searchHistories = managedObjectContext.safeFetch(request) -// for searchHistory in searchHistories { -// managedObjectContext.delete(searchHistory) -// } -// } -// .sink { result in -// // do nothing -// } -// .store(in: &context.disposeBag) -// } -//} diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift index e94d3033a..093e0f971 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift @@ -53,7 +53,6 @@ extension SearchResultViewController { tableView.pinToParent() tableView.delegate = self -// tableView.prefetchDataSource = self viewModel.setupDiffableDataSource( tableView: tableView, statusTableViewCellDelegate: self, @@ -180,81 +179,9 @@ extension SearchResultViewController: UITableViewDelegate, AutoGenerateTableView func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator) } - // sourcery:end - -// func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { -// aspectTableView(tableView, estimatedHeightForRowAt: indexPath) -// } -// -// func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { -// aspectTableView(tableView, willDisplay: cell, forRowAt: indexPath) -// } -// -// func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { -// aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath) -// } -// -// func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { -// guard let diffableDataSource = viewModel.diffableDataSource else { return } -// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } -// -// viewModel.persistSearchHistory(for: item) -// -// switch item { -// case .account(let account): -// let profileViewModel = RemoteProfileViewModel(context: context, userID: account.id) -// coordinator.present(scene: .profile(viewModel: profileViewModel), from: self, transition: .show) -// case .hashtag(let hashtag): -// let hashtagViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag.name) -// coordinator.present(scene: .hashtagTimeline(viewModel: hashtagViewModel), from: self, transition: .show) -// case .status: -// aspectTableView(tableView, didSelectRowAt: indexPath) -// case .bottomLoader: -// break -// } -// } -// -// func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { -// aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point) -// } -// -// func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { -// aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration) -// } -// -// func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { -// aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration) -// } -// -// func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { -// aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator) -// } - } -// MARK: - UITableViewDataSourcePrefetching -//extension SearchResultViewController: UITableViewDataSourcePrefetching { -// func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { -// aspectTableView(tableView, cancelPrefetchingForRowsAt: indexPaths) -// } -// -// func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { -// aspectTableView(tableView, cancelPrefetchingForRowsAt: indexPaths) -// } -//} - -// MARK: - AVPlayerViewControllerDelegate -//extension SearchResultViewController: AVPlayerViewControllerDelegate { -// func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) { -// handlePlayerViewController(playerViewController, willBeginFullScreenPresentationWithAnimationCoordinator: coordinator) -// } -// -// func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) { -// handlePlayerViewController(playerViewController, willEndFullScreenPresentationWithAnimationCoordinator: coordinator) -// } -//} - // MARK: - StatusTableViewCellDelegate extension SearchResultViewController: StatusTableViewCellDelegate { } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift index c4cfa0f54..cd9e866bc 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift @@ -62,139 +62,6 @@ final class SearchResultViewModel { domain: authContext.mastodonAuthenticationBox.domain, additionalTweetPredicate: nil ) - -// Publishers.CombineLatest( -// items, -// statusFetchedResultsController.objectIDs.removeDuplicates() -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] items, statusObjectIDs in -// guard let self = self else { return } -// guard let diffableDataSource = self.diffableDataSource else { return } -// -// var snapshot = NSDiffableDataSourceSnapshot() -// snapshot.appendSections([.main]) -// -// // append account & hashtag items -// -// var items = items -// if self.searchScope == .all { -// // all search scope not paging. it's safe sort on whole dataset -// items.sort(by: { ($0.sortKey ?? "") < ($1.sortKey ?? "")}) -// } -// snapshot.appendItems(items, toSection: .main) -// -// var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:] -// let oldSnapshot = diffableDataSource.snapshot() -// for item in oldSnapshot.itemIdentifiers { -// guard case let .status(objectID, attribute) = item else { continue } -// oldSnapshotAttributeDict[objectID] = attribute -// } -// -// // append statuses -// var statusItems: [SearchResultItem] = [] -// for objectID in statusObjectIDs { -// let attribute = oldSnapshotAttributeDict[objectID] ?? Item.StatusAttribute() -// statusItems.append(.status(statusObjectID: objectID, attribute: attribute)) -// } -// snapshot.appendItems(statusItems, toSection: .main) -// -// if let currentState = self.stateMachine.currentState { -// switch currentState { -// case is State.Loading, is State.Fail, is State.Idle: -// let attribute = SearchResultItem.BottomLoaderAttribute(isEmptyResult: false) -// snapshot.appendItems([.bottomLoader(attribute: attribute)], toSection: .main) -// case is State.Fail: -// break -// case is State.NoMore: -// if snapshot.itemIdentifiers.isEmpty { -// let attribute = SearchResultItem.BottomLoaderAttribute(isEmptyResult: true) -// snapshot.appendItems([.bottomLoader(attribute: attribute)], toSection: .main) -// } -// default: -// break -// } -// } -// -// diffableDataSource.defaultRowAnimation = .fade -// diffableDataSource.apply(snapshot, animatingDifferences: true) { [weak self] in -// guard let self = self else { return } -// self.didDataSourceUpdate.send() -// } -// -// } -// .store(in: &disposeBag) } } - -extension SearchResultViewModel { - func persistSearchHistory(for item: SearchResultItem) { - fatalError() -// guard let box = context.authenticationService.activeMastodonAuthenticationBox.value else { return } -// let property = SearchHistory.Property(domain: box.domain, userID: box.userID) -// let domain = box.domain -// -// switch item { -// case .account(let entity): -// let managedObjectContext = context.backgroundManagedObjectContext -// managedObjectContext.performChanges { -// let (user, _) = APIService.CoreData.createOrMergeMastodonUser( -// into: managedObjectContext, -// for: nil, -// in: domain, -// entity: entity, -// userCache: nil, -// networkDate: Date(), -// log: OSLog.api -// ) -// if let searchHistory = user.findSearchHistory(domain: box.domain, userID: box.userID) { -// searchHistory.update(updatedAt: Date()) -// } else { -// SearchHistory.insert(into: managedObjectContext, property: property, account: user) -// } -// } -// .sink { result in -// switch result { -// case .failure(let error): -// assertionFailure(error.localizedDescription) -// case .success: -// break -// } -// } -// .store(in: &context.disposeBag) -// -// case .hashtag(let entity): -// let managedObjectContext = context.backgroundManagedObjectContext -// var tag: Tag? -// managedObjectContext.performChanges { -// let (hashtag, _) = APIService.CoreData.createOrMergeTag( -// into: managedObjectContext, -// entity: entity -// ) -// tag = hashtag -// if let searchHistory = hashtag.findSearchHistory(domain: box.domain, userID: box.userID) { -// searchHistory.update(updatedAt: Date()) -// } else { -// _ = SearchHistory.insert(into: managedObjectContext, property: property, hashtag: hashtag) -// } -// } -// .sink { result in -// switch result { -// case .failure(let error): -// assertionFailure(error.localizedDescription) -// case .success: -// print(tag?.searchHistories) -// break -// } -// } -// .store(in: &context.disposeBag) -// -// case .status: -// // FIXME: -// break -// case .bottomLoader: -// break -// } - } -} From e8509a063de6147be6fbc4fe101b35385ffe9d44 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 15 Sep 2023 11:37:33 +0200 Subject: [PATCH 03/34] Show title instead of "x" for clear-all-button (IOS-141) Also: Move stuff around. It doesn't make sense for search-diffable-stuff to live in an enitrely different folder than the rest. --- .../input/Base.lproj/app.json | 1 + Mastodon.xcodeproj/project.pbxproj | 12 +-- .../Search/Search/SearchViewController.swift | 1 - .../SearchDetailViewController.swift | 2 - ...ySectionHeaderCollectionReusableView.swift | 10 +- ...toryUserCollectionViewCell+ViewModel.swift | 2 +- .../SearchHistory}/SearchHistoryItem.swift | 0 .../SearchHistory}/SearchHistorySection.swift | 7 +- .../SearchHistoryViewController.swift | 4 + .../View/SearchHistoryTableHeaderView.swift | 100 ------------------ .../SearchResult}/SearchResultItem.swift | 0 .../SearchResult}/SearchResultSection.swift | 0 .../StatusFetchedResultsController.swift | 2 - .../Generated/Strings.swift | 2 + .../Resources/Base.lproj/Localizable.strings | 3 +- 15 files changed, 18 insertions(+), 128 deletions(-) rename Mastodon/{Diffable/Search => Scene/Search/SearchDetail/SearchHistory}/SearchHistoryItem.swift (100%) rename Mastodon/{Diffable/Search => Scene/Search/SearchDetail/SearchHistory}/SearchHistorySection.swift (92%) delete mode 100644 Mastodon/Scene/Search/SearchDetail/SearchHistory/View/SearchHistoryTableHeaderView.swift rename Mastodon/{Diffable/Search => Scene/Search/SearchDetail/SearchResult}/SearchResultItem.swift (100%) rename Mastodon/{Diffable/Search => Scene/Search/SearchDetail/SearchResult}/SearchResultSection.swift (100%) diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json index 739573267..f22ec3098 100644 --- a/Localization/StringsConvertor/input/Base.lproj/app.json +++ b/Localization/StringsConvertor/input/Base.lproj/app.json @@ -661,6 +661,7 @@ "no_results": "No results" }, "recent_search": "Recent searches", + "clear_all": "Clear all", "clear": "Clear" } }, diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index e8c01abdd..40a6befb6 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -252,7 +252,6 @@ DB4AA6B327BA34B6009EC082 /* CellFrameCacheContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4AA6B227BA34B6009EC082 /* CellFrameCacheContainer.swift */; }; DB4F0963269ED06300D62E92 /* SearchResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0962269ED06300D62E92 /* SearchResultViewController.swift */; }; DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0965269ED52200D62E92 /* SearchResultViewModel.swift */; }; - DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */; }; DB4F096A269EDAD200D62E92 /* SearchResultViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0969269EDAD200D62E92 /* SearchResultViewModel+State.swift */; }; DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */; }; DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */; }; @@ -927,7 +926,6 @@ DB4B779626CA50BA00B087B3 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = th; path = th.lproj/Intents.stringsdict; sourceTree = ""; }; DB4F0962269ED06300D62E92 /* SearchResultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultViewController.swift; sourceTree = ""; }; DB4F0965269ED52200D62E92 /* SearchResultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultViewModel.swift; sourceTree = ""; }; - DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryTableHeaderView.swift; sourceTree = ""; }; DB4F0969269EDAD200D62E92 /* SearchResultViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewModel+State.swift"; sourceTree = ""; }; DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryViewModel.swift; sourceTree = ""; }; DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySection.swift; sourceTree = ""; }; @@ -2099,6 +2097,8 @@ DB4F0965269ED52200D62E92 /* SearchResultViewModel.swift */, DB0FCB932797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift */, DB4F0969269EDAD200D62E92 /* SearchResultViewModel+State.swift */, + 2D198648261C0B8500F0B013 /* SearchResultSection.swift */, + 2D198642261BF09500F0B013 /* SearchResultItem.swift */, ); path = SearchResult; sourceTree = ""; @@ -2108,10 +2108,6 @@ children = ( DB0FCB852796BDA1006C02E2 /* SearchSection.swift */, DB0FCB872796BDA9006C02E2 /* SearchItem.swift */, - 2D198648261C0B8500F0B013 /* SearchResultSection.swift */, - 2D198642261BF09500F0B013 /* SearchResultItem.swift */, - DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */, - DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */, ); path = Search; sourceTree = ""; @@ -2137,7 +2133,6 @@ DB4F098026A0475500D62E92 /* View */ = { isa = PBXGroup; children = ( - DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */, ); path = View; sourceTree = ""; @@ -2920,6 +2915,8 @@ DB63F76127996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift */, DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */, DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */, + DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */, + DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */, ); path = SearchHistory; sourceTree = ""; @@ -3694,7 +3691,6 @@ 2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */, 6213AF5A28939C8400BCADB6 /* BookmarkViewModel.swift in Sources */, 5B24BBDB262DB14800A9381B /* ReportStatusViewModel+Diffable.swift in Sources */, - DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */, 5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */, 2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */, DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */, diff --git a/Mastodon/Scene/Search/Search/SearchViewController.swift b/Mastodon/Scene/Search/Search/SearchViewController.swift index e1505121d..41bc55ee0 100644 --- a/Mastodon/Scene/Search/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/Search/SearchViewController.swift @@ -171,7 +171,6 @@ extension SearchViewController { // MARK: - UISearchBarDelegate extension SearchViewController: UISearchBarDelegate { func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) searchBarTapPublisher.send("") return false } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift index 84a0aedb2..09889fc2d 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift @@ -315,8 +315,6 @@ extension SearchDetailViewController: UISearchBarDelegate { } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - // dismiss or pop if isModal { dismiss(animated: true, completion: nil) diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistorySectionHeaderCollectionReusableView.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistorySectionHeaderCollectionReusableView.swift index 170f59934..ecab554e1 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistorySectionHeaderCollectionReusableView.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistorySectionHeaderCollectionReusableView.swift @@ -30,8 +30,9 @@ final class SearchHistorySectionHeaderCollectionReusableView: UICollectionReusab let clearButton: UIButton = { let button = UIButton(type: .system) - button.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal) - button.tintColor = Asset.Colors.Label.secondary.color + + button.setTitle(L10n.Scene.Search.Searching.clearAll, for: .normal) + button.tintColor = Asset.Colors.Brand.blurple.color button.accessibilityLabel = L10n.Scene.Search.Searching.clear return button @@ -47,9 +48,6 @@ final class SearchHistorySectionHeaderCollectionReusableView: UICollectionReusab _init() } -} - -extension SearchHistorySectionHeaderCollectionReusableView { private func _init() { primaryLabel.translatesAutoresizingMaskIntoConstraints = false addSubview(primaryLabel) @@ -72,9 +70,7 @@ extension SearchHistorySectionHeaderCollectionReusableView { clearButton.addTarget(self, action: #selector(SearchHistorySectionHeaderCollectionReusableView.clearButtonDidPressed(_:)), for: .touchUpInside) } -} -extension SearchHistorySectionHeaderCollectionReusableView { @objc private func clearButtonDidPressed(_ sender: UIButton) { delegate?.searchHistorySectionHeaderCollectionReusableView(self, clearButtonDidPressed: sender) } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift index e31f050bd..31350811d 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift @@ -31,7 +31,7 @@ extension SearchHistoryUserCollectionViewCell { func configure( me: MastodonUser?, viewModel: ViewModel, - delegate: UserViewDelegate? + delegate: SearchHistorySectionHeaderCollectionReusableViewDelegate? ) { let user = viewModel.value diff --git a/Mastodon/Diffable/Search/SearchHistoryItem.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryItem.swift similarity index 100% rename from Mastodon/Diffable/Search/SearchHistoryItem.swift rename to Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryItem.swift diff --git a/Mastodon/Diffable/Search/SearchHistorySection.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift similarity index 92% rename from Mastodon/Diffable/Search/SearchHistorySection.swift rename to Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift index 813c5b59a..d0cbc05ee 100644 --- a/Mastodon/Diffable/Search/SearchHistorySection.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift @@ -78,13 +78,8 @@ extension SearchHistorySection { } } - let trendHeaderRegister = UICollectionView.SupplementaryRegistration(elementKind: UICollectionView.elementKindSectionHeader) { [weak dataSource] supplementaryView, elementKind, indexPath in + let trendHeaderRegister = UICollectionView.SupplementaryRegistration(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in supplementaryView.delegate = configuration.searchHistorySectionHeaderCollectionReusableViewDelegate - - guard let _ = dataSource else { return } - // let sections = dataSource.snapshot().sectionIdentifiers - // guard indexPath.section < sections.count else { return } - // let section = sections[indexPath.section] } dataSource.supplementaryViewProvider = { (collectionView: UICollectionView, elementKind: String, indexPath: IndexPath) in diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift index 2ef2c8440..d55259d03 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift @@ -118,6 +118,10 @@ extension SearchHistoryViewController: SearchHistorySectionHeaderCollectionReusa try await DataSourceFacade.responseToDeleteSearchHistory( provider: self ) + + await MainActor.run { + button.isEnabled = false + } } } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/View/SearchHistoryTableHeaderView.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/View/SearchHistoryTableHeaderView.swift deleted file mode 100644 index d827231c0..000000000 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/View/SearchHistoryTableHeaderView.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// SearchHistoryTableHeaderView.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-7-14. -// - -import os.log -import UIKit -import Combine -import MastodonAsset -import MastodonCore -import MastodonLocalization -import MastodonUI - -protocol SearchHistoryTableHeaderViewDelegate: AnyObject { - func searchHistoryTableHeaderView(_ searchHistoryTableHeaderView: SearchHistoryTableHeaderView, clearSearchHistoryButtonDidPressed button: UIButton) -} - -final class SearchHistoryTableHeaderView: UIView { - - let logger = Logger(subsystem: "SearchHistory", category: "UI") - - weak var delegate: SearchHistoryTableHeaderViewDelegate? - var disposeBag = Set() - - let recentSearchesLabel: UILabel = { - let label = UILabel() - label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) - label.textColor = Asset.Colors.Label.primary.color - label.text = L10n.Scene.Search.Searching.recentSearch - return label - }() - - let clearSearchHistoryButton: HighlightDimmableButton = { - let button = HighlightDimmableButton(type: .custom) - button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10) - button.setTitleColor(Asset.Colors.Brand.blurple.color, for: .normal) - button.setTitle(L10n.Scene.Search.Searching.clear, for: .normal) - return button - }() - - override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension SearchHistoryTableHeaderView { - private func _init() { - preservesSuperviewLayoutMargins = true - - recentSearchesLabel.translatesAutoresizingMaskIntoConstraints = false - addSubview(recentSearchesLabel) - NSLayoutConstraint.activate([ - recentSearchesLabel.topAnchor.constraint(equalTo: topAnchor, constant: 16), - recentSearchesLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), - bottomAnchor.constraint(equalTo: recentSearchesLabel.bottomAnchor, constant: 16), - ]) - - clearSearchHistoryButton.translatesAutoresizingMaskIntoConstraints = false - addSubview(clearSearchHistoryButton) - NSLayoutConstraint.activate([ - clearSearchHistoryButton.centerYAnchor.constraint(equalTo: recentSearchesLabel.centerYAnchor), - clearSearchHistoryButton.leadingAnchor.constraint(equalTo: recentSearchesLabel.trailingAnchor), - clearSearchHistoryButton.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), - ]) - clearSearchHistoryButton.setContentHuggingPriority(.defaultHigh + 10, for: .horizontal) - - clearSearchHistoryButton.addTarget(self, action: #selector(SearchHistoryTableHeaderView.clearSearchHistoryButtonDidPressed(_:)), for: .touchUpInside) - - setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) - ThemeService.shared.currentTheme - .receive(on: RunLoop.main) - .sink { [weak self] theme in - guard let self = self else { return } - self.setupBackgroundColor(theme: theme) - } - .store(in: &disposeBag) - } -} - -extension SearchHistoryTableHeaderView { - @objc private func clearSearchHistoryButtonDidPressed(_ sender: UIButton) { - logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - delegate?.searchHistoryTableHeaderView(self, clearSearchHistoryButtonDidPressed: sender) - } -} - -extension SearchHistoryTableHeaderView { - private func setupBackgroundColor(theme: Theme) { - backgroundColor = theme.systemGroupedBackgroundColor - } -} diff --git a/Mastodon/Diffable/Search/SearchResultItem.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultItem.swift similarity index 100% rename from Mastodon/Diffable/Search/SearchResultItem.swift rename to Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultItem.swift diff --git a/Mastodon/Diffable/Search/SearchResultSection.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift similarity index 100% rename from Mastodon/Diffable/Search/SearchResultSection.swift rename to Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift index c08673acb..bb4184bfc 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift @@ -90,8 +90,6 @@ extension StatusFetchedResultsController { // MARK: - NSFetchedResultsControllerDelegate extension StatusFetchedResultsController: NSFetchedResultsControllerDelegate { public func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - let indexes = statusIDs let objects = fetchedResultsController.fetchedObjects ?? [] diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 27f3c06d0..dcc6f84f1 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -1277,6 +1277,8 @@ public enum L10n { public enum Searching { /// Clear public static let clear = L10n.tr("Localizable", "Scene.Search.Searching.Clear", fallback: "Clear") + /// Clear all + public static let clearAll = L10n.tr("Localizable", "Scene.Search.Searching.ClearAll", fallback: "Clear all") /// Recent searches public static let recentSearch = L10n.tr("Localizable", "Scene.Search.Searching.RecentSearch", fallback: "Recent searches") public enum EmptyState { diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 7f00f1cc2..7269b0c4a 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -440,6 +440,7 @@ uploaded to Mastodon."; "Scene.Search.SearchBar.Cancel" = "Cancel"; "Scene.Search.SearchBar.Placeholder" = "Search hashtags and users"; "Scene.Search.Searching.Clear" = "Clear"; +"Scene.Search.Searching.ClearAll" = "Clear all"; "Scene.Search.Searching.EmptyState.NoResults" = "No results"; "Scene.Search.Searching.RecentSearch" = "Recent searches"; "Scene.Search.Searching.Segment.All" = "All"; @@ -554,4 +555,4 @@ uploaded to Mastodon."; "Widget.MultipleFollowers.ConfigurationDescription" = "Show number of followers for multiple accounts."; "Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers"; "Widget.MultipleFollowers.MockUser.AccountName" = "another@follower.social"; -"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower"; \ No newline at end of file +"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower"; From 2e384f3cb58895512d78d24d9d57fc38ff9f9e9b Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 15 Sep 2023 17:45:22 +0200 Subject: [PATCH 04/34] 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: --- Mastodon.xcodeproj/project.pbxproj | 28 +++ .../HomeTimelineViewModel+Diffable.swift | 7 +- .../Search/Search/SearchViewController.swift | 60 ------ .../Scene/Search/Search/SearchViewModel.swift | 27 --- ...rchResultDefaultSectionTableViewCell.swift | 26 +++ .../SearchResultOverviewSection.swift | 71 +++++++ ...chResultsOverviewTableViewController.swift | 166 ++++++++++++++++ .../SearchDetailViewController.swift | 188 ++++-------------- .../SearchDetail/SearchDetailViewModel.swift | 34 +--- .../Cell/HashtagTableViewCell.swift | 2 + .../SearchResultViewController.swift | 2 - .../SearchResultViewModel+State.swift | 1 - .../SearchResult/SearchResultViewModel.swift | 4 +- .../TableviewCell/StatusTableViewCell.swift | 2 + .../TableviewCell/UserTableViewCell.swift | 3 +- .../UserFetchedResultsController.swift | 1 - .../Entity/Mastodon+Entity+History.swift | 2 +- .../View/Content/StatusView+ViewModel.swift | 3 - 18 files changed, 355 insertions(+), 272 deletions(-) create mode 100644 Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultDefaultSectionTableViewCell.swift create mode 100644 Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift create mode 100644 Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 40a6befb6..0636ceb29 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -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 = ""; }; D8099079294BC9390050219F /* PrivacyTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyTableViewCell.swift; sourceTree = ""; }; D809907B294D25510050219F /* PrivacyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyViewModel.swift; sourceTree = ""; }; + D81A22742AB4643200905D71 /* SearchResultsOverviewTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsOverviewTableViewController.swift; sourceTree = ""; }; + D81A22772AB4782400905D71 /* SearchResultOverviewSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultOverviewSection.swift; sourceTree = ""; }; + D81A227A2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultDefaultSectionTableViewCell.swift; sourceTree = ""; }; D82463522A52B47B00A3DBDD /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = be; path = be.lproj/Intents.strings; sourceTree = ""; }; D82463532A52B47B00A3DBDD /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = be; path = be.lproj/WidgetExtension.strings; sourceTree = ""; }; D82463542A52B47B00A3DBDD /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = be; path = be.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -1791,6 +1797,24 @@ path = Privacy; sourceTree = ""; }; + D81A22732AB4641F00905D71 /* Search Results Overview */ = { + isa = PBXGroup; + children = ( + D81A22792AB47B8400905D71 /* Cells */, + D81A22742AB4643200905D71 /* SearchResultsOverviewTableViewController.swift */, + D81A22772AB4782400905D71 /* SearchResultOverviewSection.swift */, + ); + path = "Search Results Overview"; + sourceTree = ""; + }; + D81A22792AB47B8400905D71 /* Cells */ = { + isa = PBXGroup; + children = ( + D81A227A2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift */, + ); + path = Cells; + sourceTree = ""; + }; 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 */, diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index 24cf2258a..9feee053e 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -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 = { let newItems = records.map { record in diff --git a/Mastodon/Scene/Search/Search/SearchViewController.swift b/Mastodon/Scene/Search/Search/SearchViewController.swift index 41bc55ee0..093976e63 100644 --- a/Mastodon/Scene/Search/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/Search/SearchViewController.swift @@ -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() @@ -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) -// } -// } -//} diff --git a/Mastodon/Scene/Search/Search/SearchViewModel.swift b/Mastodon/Scene/Search/Search/SearchViewModel.swift index 51d614280..620099bfc 100644 --- a/Mastodon/Scene/Search/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/Search/SearchViewModel.swift @@ -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, Error> { response } } -// .catch { error in Just(Result, 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) } - } diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultDefaultSectionTableViewCell.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultDefaultSectionTableViewCell.swift new file mode 100644 index 000000000..49ba6d111 --- /dev/null +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultDefaultSectionTableViewCell.swift @@ -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 + } +} diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift new file mode 100644 index 000000000..895f26ff1 --- /dev/null +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift @@ -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) + + 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 + } + } + } +} diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift new file mode 100644 index 000000000..60adc503e --- /dev/null +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -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? + + 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(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() + 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) + } +} diff --git a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift index 09889fc2d..3868e02dc 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift @@ -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() var observations = Set() @@ -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) - - // bind searchText - viewModel.searchText - .assign(to: \.value, on: searchResultViewController.viewModel.searchText) - .store(in: &searchResultViewController.disposeBag) - - // bind navigationBarFrame - viewModel.navigationBarFrame - .receive(on: DispatchQueue.main) - .assign(to: \.value, on: searchResultViewController.viewModel.navigationBarFrame) - .store(in: &searchResultViewController.disposeBag) - return searchResultViewController + 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() } - // 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 + // bind search trigger + // "local" search + viewModel.searchText + .removeDuplicates() .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) - } + .sink { [weak self] searchText in + guard let self else { return } + + 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 } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewModel.swift index 425722e37..04228ba5a 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewModel.swift @@ -5,7 +5,6 @@ // Created by MainasuK Cirno on 2021-7-13. // -import os.log import Foundation import CoreGraphics import Combine @@ -15,48 +14,37 @@ import MastodonAsset import MastodonLocalization final class SearchDetailViewModel { - + // input let authContext: AuthContext var needsBecomeFirstResponder = false let viewDidAppear = PassthroughSubject() let navigationBarFrame = CurrentValueSubject(.zero) - + // output let searchScopes = SearchScope.allCases let selectedSearchScope = CurrentValueSubject(.all) let searchText: CurrentValueSubject let searchActionPublisher = PassthroughSubject() - + init(authContext: AuthContext, initialSearchText: String = "") { self.authContext = authContext self.searchText = CurrentValueSubject(initialSearchText) } } -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 { +enum SearchScope: CaseIterable { + case all + case people + case hashtags + case posts + + var searchType: Mastodon.API.V2.Search.SearchType { + switch self { case .all: return .default case .people: return .accounts case .hashtags: return .hashtags case .posts: return .statuses - } } } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/Cell/HashtagTableViewCell.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/Cell/HashtagTableViewCell.swift index c8938c549..ccb74e31e 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/Cell/HashtagTableViewCell.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/Cell/HashtagTableViewCell.swift @@ -9,6 +9,8 @@ import UIKit import MetaTextKit final class HashtagTableViewCell: UITableViewCell { + + static let reuseIdentifier = "HashtagTableViewCell" let primaryLabel = MetaLabel(style: .statusName) diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift index 093e0f971..cf7a6a0f6 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift @@ -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) } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift index 3fdf65436..47832e156 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift @@ -5,7 +5,6 @@ // Created by MainasuK Cirno on 2021-7-14. // -import os.log import Foundation import GameplayKit import MastodonSDK diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift index cd9e866bc..0c5c5868f 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift @@ -20,7 +20,7 @@ final class SearchResultViewModel { // input let context: AppContext let authContext: AuthContext - let searchScope: SearchDetailViewModel.SearchScope + let searchScope: SearchScope let searchText = CurrentValueSubject("") @Published var hashtags: [Mastodon.Entity.Tag] = [] let userFetchedResultsController: UserFetchedResultsController @@ -48,7 +48,7 @@ final class SearchResultViewModel { }() let didDataSourceUpdate = PassthroughSubject() - 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 diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index 16285ebeb..6c8c82527 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -13,6 +13,8 @@ import MastodonLocalization import MastodonUI final class StatusTableViewCell: UITableViewCell { + + static let reuseIdentifier = "StatusTableViewCell" static let marginForRegularHorizontalSizeClass: CGFloat = 64 diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell.swift index a05b80e9c..0f316bad8 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell.swift @@ -16,7 +16,8 @@ import MastodonSDK protocol UserTableViewCellDelegate: UserViewDelegate, AnyObject { } final class UserTableViewCell: UITableViewCell { - + + static let reuseIdentifier = "UserTableViewCell" weak var delegate: UserTableViewCellDelegate? let userView = UserView() diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift index d95a62bbb..cf4f1fc07 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift @@ -97,7 +97,6 @@ extension UserFetchedResultsController { // MARK: - NSFetchedResultsControllerDelegate extension UserFetchedResultsController: NSFetchedResultsControllerDelegate { public func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) let indexes = userIDs let objects = fetchedResultsController.fetchedObjects ?? [] diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift index 5f49d80f0..1acd3e84c 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift @@ -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 diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index d7d2faf48..ebae0d12e 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -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() From ed11d01267dfb59974c9c079a7b977ffbedffcce Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sat, 16 Sep 2023 17:57:29 +0200 Subject: [PATCH 05/34] Show up to three hashtags and up to three users (IOS-141) --- .../HomeTimelineViewModel+Diffable.swift | 5 -- .../SearchResultOverviewSection.swift | 33 +++++--- ...chResultsOverviewTableViewController.swift | 81 ++++++++++++++----- .../SearchDetailViewController.swift | 29 ++----- .../MastodonSDK/API/Mastodon+API.swift | 1 - 5 files changed, 87 insertions(+), 62 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index 9feee053e..6da6a60fa 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -87,11 +87,8 @@ extension HomeTimelineViewModel { let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers if !hasChanges && !self.hasPendingStatusEditReload { - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot not changes") self.didLoadLatest.send() return - } else { - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot has changes") } guard let difference = self.calculateReloadSnapshotDifference( @@ -101,7 +98,6 @@ extension HomeTimelineViewModel { ) else { self.updateSnapshotUsingReloadData(snapshot: newSnapshot) self.didLoadLatest.send() - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): applied new snapshot") return } @@ -111,7 +107,6 @@ extension HomeTimelineViewModel { contentOffset.y = tableView.contentOffset.y - difference.sourceDistanceToTableViewTopEdge tableView.setContentOffset(contentOffset, animated: false) self.didLoadLatest.send() - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): applied new snapshot") self.hasPendingStatusEditReload = false } // end Task } diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift index 895f26ff1..37d352b07 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift @@ -48,24 +48,37 @@ enum SearchResultOverviewItem: Hashable { } enum SuggestionSectionEntry: Hashable { - //TODO: Use User instead case hashtag(tag: Mastodon.Entity.Tag) - case profile(ManagedObjectRecord) + case profile(user: Mastodon.Entity.Account) var title: String? { - if case let .hashtag(tag) = self { - return tag.name - } else { - return nil + switch self { + + case .hashtag(tag: let tag): + return tag.name + case .profile(user: let user): + return "\(user.displayName) — \(user.acct)" } +// 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 + switch self { + + case .hashtag(tag: _): + return UIImage(systemName: "number") + + case .profile(user: _): + return UIImage(systemName: "person.circle") + } +// if case let .hashtag(tag) = self { +// } else { +// } } } } diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index 60adc503e..32078309b 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -23,6 +23,7 @@ class SearchResultsOverviewTableViewController: UIViewController { var dataSource: UITableViewDiffableDataSource? weak var delegate: SearchResultsOverviewTableViewControllerDeleagte? + var activeTask: Task? init(appContext: AppContext, authContext: AuthContext) { @@ -48,22 +49,29 @@ class SearchResultsOverviewTableViewController: UIViewController { 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 + guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultDefaultSectionTableViewCell.reuseIdentifier, for: indexPath) as? SearchResultDefaultSectionTableViewCell else { fatalError() } - 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 - } + cell.configure(item: suggestion) + return cell +// 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): +// //TODO: Use `UserFetchedResultsController` or `Persistence.MastodonUser.fetch` ??? +// 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 +// } } } @@ -79,9 +87,20 @@ class SearchResultsOverviewTableViewController: UIViewController { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func showStandardSearch(for searchText: String) { + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.default, .suggestions]) + dataSource?.apply(snapshot, animatingDifferences: false) + } + + func showStandardSearch(for searchText: String) { + + guard let dataSource else { return } + + var snapshot = dataSource.snapshot() + snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .default)) snapshot.appendItems([.default(.posts(searchText)), .default(.people(searchText)), .default(.profile(searchText, authContext.mastodonAuthenticationBox.domain))], toSection: .default) @@ -90,18 +109,29 @@ class SearchResultsOverviewTableViewController: UIViewController { //TODO: Check if Mastodon-URL snapshot.appendItems([.default(.openLink(searchText))], toSection: .default) } - dataSource?.apply(snapshot, animatingDifferences: false) + + dataSource.apply(snapshot, animatingDifferences: false) } func searchForSuggestions(for searchText: String) { + activeTask?.cancel() + + guard let dataSource else { return } + + var snapshot = dataSource.snapshot() + snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .suggestions)) + dataSource.apply(snapshot, animatingDifferences: false) + + guard searchText.isNotEmpty else { return } + let query = Mastodon.API.V2.Search.Query( q: searchText, type: .default, resolve: true ) - Task { + let searchTask = Task { do { let searchResult = try await appContext.apiService.search( query: query, @@ -111,22 +141,29 @@ class SearchResultsOverviewTableViewController: UIViewController { let firstThreeHashtags = searchResult.hashtags.prefix(3) let firstThreeUsers = searchResult.accounts.prefix(3) - guard var snapshot = dataSource?.snapshot() else { return } + var snapshot = dataSource.snapshot() - 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) + if firstThreeHashtags.isNotEmpty { + snapshot.appendItems(firstThreeHashtags.map { .suggestion(.hashtag(tag: $0)) }, toSection: .suggestions ) } + if firstThreeUsers.isNotEmpty { + snapshot.appendItems(firstThreeUsers.map { .suggestion(.profile(user: $0)) }, toSection: .suggestions ) + } + guard Task.isCancelled == false else { return } + + await MainActor.run { + dataSource.apply(snapshot, animatingDifferences: false) + } } catch { // do nothing + print(error.localizedDescription) } } + + activeTask = searchTask } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift index 3868e02dc..4e1f7696e 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift @@ -131,29 +131,6 @@ final class SearchDetailViewController: UIViewController, NeedsDependency { searchResultsOverviewViewController.view.pinToParent() } - // bind search trigger - // "local" search - viewModel.searchText - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak self] searchText in - guard let self else { return } - - self.searchResultsOverviewViewController.showStandardSearch(for: searchText) - } - .store(in: &disposeBag) - - // delayed search on server - viewModel.searchText - .removeDuplicates() - .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true) - .sink { [weak self] searchText in - guard let self else { return } - - self.searchResultsOverviewViewController.searchForSuggestions(for: searchText) - } - .store(in: &disposeBag) - // bind search history display viewModel.searchText .removeDuplicates() @@ -263,7 +240,11 @@ extension SearchDetailViewController { extension SearchDetailViewController: UISearchBarDelegate { func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { - viewModel.searchText.value = searchText.trimmingCharacters(in: .whitespacesAndNewlines) + let trimmedSearchText = searchText.trimmingCharacters(in: .whitespacesAndNewlines) + viewModel.searchText.value = trimmedSearchText + + searchResultsOverviewViewController.showStandardSearch(for: trimmedSearchText) + searchResultsOverviewViewController.searchForSuggestions(for: trimmedSearchText) } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index 76bebe5a0..57d99dc41 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -208,7 +208,6 @@ extension Mastodon.API { return try Mastodon.API.decoder.decode(type, from: data) } catch let decodeError { #if DEBUG - os_log(.info, "%{public}s[%{public}ld], %{public}s: decode fail. content %s", ((#file as NSString).lastPathComponent), #line, #function, String(data: data, encoding: .utf8) ?? "") debugPrint(decodeError) #endif From 1afecc85eae8d8d6f9a67856520bb29ec382fdce Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sat, 16 Sep 2023 18:39:33 +0200 Subject: [PATCH 06/34] Show hashtag-page (IOS-141) --- Mastodon/Coordinator/NeedsDependency.swift | 1 + .../Provider/DataSourceFacade+Hashtag.swift | 2 +- ...chResultsOverviewTableViewController.swift | 56 ++++++++++++------- .../SearchDetailViewController.swift | 9 +-- .../Service/API/APIService+Account.swift | 12 ++-- .../Entity/Mastodon+Entity+Tag.swift | 7 +++ 6 files changed, 55 insertions(+), 32 deletions(-) diff --git a/Mastodon/Coordinator/NeedsDependency.swift b/Mastodon/Coordinator/NeedsDependency.swift index c035437ac..46fbcd1ff 100644 --- a/Mastodon/Coordinator/NeedsDependency.swift +++ b/Mastodon/Coordinator/NeedsDependency.swift @@ -9,6 +9,7 @@ import UIKit import MastodonCore protocol NeedsDependency: AnyObject { + //FIXME: Get rid of ! ~@zeitschlag var context: AppContext! { get set } var coordinator: SceneCoordinator! { get set } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift index 6135c904a..9b0001351 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift @@ -26,7 +26,7 @@ extension DataSourceFacade { @MainActor static func coordinateToHashtagScene( - provider: DataSourceProvider & AuthContextProvider, + provider: NeedsDependency & UIViewController & AuthContextProvider, tag: Mastodon.Entity.Tag ) async { let hashtagTimelineViewModel = HashtagTimelineViewModel( diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index 32078309b..a4b8b123b 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -5,19 +5,19 @@ 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 { +class SearchResultsOverviewTableViewController: UIViewController, NeedsDependency, AuthContextProvider { // similar to the other search results view controller but without the whole statemachine bullshit // with scope all - let appContext: AppContext + var context: AppContext! let authContext: AuthContext + var coordinator: SceneCoordinator! private let tableView: UITableView var dataSource: UITableViewDiffableDataSource? @@ -25,10 +25,11 @@ class SearchResultsOverviewTableViewController: UIViewController { weak var delegate: SearchResultsOverviewTableViewControllerDeleagte? var activeTask: Task? - init(appContext: AppContext, authContext: AuthContext) { + init(appContext: AppContext, authContext: AuthContext, coordinator: SceneCoordinator) { - self.appContext = appContext + self.context = appContext self.authContext = authContext + self.coordinator = coordinator tableView = UITableView(frame: .zero, style: .insetGrouped) tableView.translatesAutoresizingMaskIntoConstraints = false @@ -50,7 +51,6 @@ class SearchResultsOverviewTableViewController: UIViewController { case .suggestion(let suggestion): - guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultDefaultSectionTableViewCell.reuseIdentifier, for: indexPath) as? SearchResultDefaultSectionTableViewCell else { fatalError() } cell.configure(item: suggestion) @@ -59,17 +59,26 @@ class SearchResultsOverviewTableViewController: UIViewController { // 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): // //TODO: Use `UserFetchedResultsController` or `Persistence.MastodonUser.fetch` ??? +// // guard let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as? UserTableViewCell else { fatalError() } + + // how the fuck do I get a MastodonUser??? +// try await managedObjectContext.perform { +// Persistence.MastodonUser.fetch(in: managedObjectContext, +// context: Persistence.MastodonUser.PersistContext( +// domain: domain, +// entity: profile.value, +// cache: nil, +// networkDate: profile.netwo +// )) +// } // -//// cell.configure(me: <#T##MastodonUser?#>, tableView: <#T##UITableView#>, viewModel: <#T##UserTableViewCell.ViewModel#>, delegate: <#T##UserTableViewCellDelegate?#>) -// + + // cell.configure(me: <#T##MastodonUser?#>, tableView: <#T##UITableView#>, viewModel: <#T##UserTableViewCell.ViewModel#>, delegate: <#T##UserTableViewCellDelegate?#>) + // return cell // } } @@ -87,8 +96,8 @@ class SearchResultsOverviewTableViewController: UIViewController { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) + override func viewDidLoad() { + super.viewDidLoad() var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.default, .suggestions]) @@ -133,7 +142,7 @@ class SearchResultsOverviewTableViewController: UIViewController { let searchTask = Task { do { - let searchResult = try await appContext.apiService.search( + let searchResult = try await context.apiService.search( query: query, authenticationBox: authContext.mastodonAuthenticationBox ).value @@ -165,6 +174,15 @@ class SearchResultsOverviewTableViewController: UIViewController { activeTask = searchTask } + + func showPosts(tag: Mastodon.Entity.Tag) { + Task { + await DataSourceFacade.coordinateToHashtagScene( + provider: self, + tag: tag + ) + } + } } //MARK: UITableViewDelegate @@ -179,8 +197,8 @@ extension SearchResultsOverviewTableViewController: UITableViewDelegate { switch item { case .default(let defaultSectionEntry): switch defaultSectionEntry { - case .posts(let string): - delegate?.showPosts(self) + case .posts(let hashtag): + showPosts(tag: Mastodon.Entity.Tag(name: hashtag, url: authContext.mastodonAuthenticationBox.domain)) case .people(let string): delegate?.showPeople(self) case .profile(let profile, let instanceName): @@ -191,8 +209,8 @@ extension SearchResultsOverviewTableViewController: UITableViewDelegate { case .suggestion(let suggestionSectionEntry): switch suggestionSectionEntry { - case .hashtag(_): - delegate?.showPosts(self) + case .hashtag(let tag): + showPosts(tag: tag) case .profile(_): delegate?.showProfile(self) } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift index 4e1f7696e..25dc48e45 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift @@ -23,12 +23,13 @@ final class CustomSearchController: UISearchController { // Fake search bar not works on iPad with UISplitViewController // check device and fallback to standard UISearchController final class SearchDetailViewController: UIViewController, NeedsDependency { + var disposeBag = Set() var observations = Set() weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - + let isPhoneDevice: Bool = { return UIDevice.current.userInterfaceIdiom == .phone }() @@ -81,7 +82,7 @@ final class SearchDetailViewController: UIViewController, NeedsDependency { }() private(set) lazy var searchResultsOverviewViewController: SearchResultsOverviewTableViewController = { - let searchResultsOverviewViewController = SearchResultsOverviewTableViewController(appContext: context, authContext: viewModel.authContext) + let searchResultsOverviewViewController = SearchResultsOverviewTableViewController(appContext: context, authContext: viewModel.authContext, coordinator: coordinator) return searchResultsOverviewViewController }() @@ -259,10 +260,6 @@ extension SearchDetailViewController: UISearchBarDelegate { //MARK: SearchResultsOverviewViewControllerDelegate extension SearchDetailViewController: SearchResultsOverviewTableViewControllerDeleagte { - func showPosts(_ viewController: UIViewController) { - //TODO: Implement - } - func showPeople(_ viewController: UIViewController) { //TODO: Implement } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift index d68984587..812c558d8 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift @@ -230,12 +230,12 @@ extension APIService { var result: MastodonUser? try await managedObjectContext.perform { result = Persistence.MastodonUser.fetch(in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: domain, - entity: response.value, - cache: nil, - networkDate: response.networkDate - )) + context: Persistence.MastodonUser.PersistContext( + domain: domain, + entity: response.value, + cache: nil, + networkDate: response.networkDate + )) } return result } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift index 8d7baa6a6..6a3516904 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift @@ -25,6 +25,13 @@ extension Mastodon.Entity { public let history: [History]? public let following: Bool? + public init(name: String, url: String, history: [History]? = nil, following: Bool? = nil) { + self.name = name + self.url = url + self.history = history + self.following = following + } + enum CodingKeys: String, CodingKey { case name case url From a304fb21086eb8eb3ab4f2bfbb2ef5554c54108e Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 17 Sep 2023 12:48:58 +0200 Subject: [PATCH 07/34] Show profile-page for suggested accounts in search (IOS-141) --- Mastodon/Diffable/User/UserSection.swift | 49 +++++----- .../Provider/DataSourceFacade+Profile.swift | 2 +- ...chResultsOverviewTableViewController.swift | 92 +++++++++++++------ .../SearchResult/SearchResultSection.swift | 2 +- .../UserTableViewCell+ViewModel.swift | 83 ++++++++--------- 5 files changed, 130 insertions(+), 98 deletions(-) diff --git a/Mastodon/Diffable/User/UserSection.swift b/Mastodon/Diffable/User/UserSection.swift index cbad1ff72..24f13ddc6 100644 --- a/Mastodon/Diffable/User/UserSection.swift +++ b/Mastodon/Diffable/User/UserSection.swift @@ -39,30 +39,31 @@ extension UserSection { return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in switch item { - case .user(let record): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell - context.managedObjectContext.performAndWait { - guard let user = record.object(in: context.managedObjectContext) else { return } - configure( - context: context, - authContext: authContext, - tableView: tableView, - cell: cell, - viewModel: UserTableViewCell.ViewModel(value: .user(user), - followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), - blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(), - followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher() - ), - configuration: configuration - ) - } - - return cell - case .bottomLoader: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell - cell.startAnimating() - return cell - case .bottomHeader(let text): + case .user(let record): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell + context.managedObjectContext.performAndWait { + guard let user = record.object(in: context.managedObjectContext) else { return } + configure( + context: context, + authContext: authContext, + tableView: tableView, + cell: cell, + viewModel: UserTableViewCell.ViewModel( + user: user, + followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), + blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(), + followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher() + ), + configuration: configuration + ) + } + + return cell + case .bottomLoader: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell + cell.startAnimating() + return cell + case .bottomHeader(let text): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineFooterTableViewCell.self), for: indexPath) as! TimelineFooterTableViewCell cell.messageLabel.text = text return cell diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index f434bc6ac..161e52e87 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -33,7 +33,7 @@ extension DataSourceFacade { @MainActor static func coordinateToProfileScene( - provider: DataSourceProvider & AuthContextProvider, + provider: NeedsDependency & UIViewController & AuthContextProvider, user: ManagedObjectRecord ) async { guard let user = user.object(in: provider.context.managedObjectContext) else { diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index a4b8b123b..b3026e279 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -51,36 +51,52 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc case .suggestion(let suggestion): - guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultDefaultSectionTableViewCell.reuseIdentifier, for: indexPath) as? SearchResultDefaultSectionTableViewCell else { fatalError() } + switch suggestion { - cell.configure(item: suggestion) - return cell + case .hashtag(let hashtag): -// switch suggestion { -// -// case .hashtag(let hashtag): -// -// case .profile(let profile): -// //TODO: Use `UserFetchedResultsController` or `Persistence.MastodonUser.fetch` ??? -// -// guard let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as? UserTableViewCell else { fatalError() } + + 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() } // how the fuck do I get a MastodonUser??? -// try await managedObjectContext.perform { -// Persistence.MastodonUser.fetch(in: managedObjectContext, -// context: Persistence.MastodonUser.PersistContext( -// domain: domain, -// entity: profile.value, -// cache: nil, -// networkDate: profile.netwo -// )) -// } -// + let managedObjectContext = appContext.managedObjectContext + Task { + do { - // cell.configure(me: <#T##MastodonUser?#>, tableView: <#T##UITableView#>, viewModel: <#T##UserTableViewCell.ViewModel#>, delegate: <#T##UserTableViewCellDelegate?#>) + try await managedObjectContext.perform { + guard let user = Persistence.MastodonUser.fetch(in: managedObjectContext, + context: Persistence.MastodonUser.PersistContext( + domain: authContext.mastodonAuthenticationBox.domain, + entity: profile, + cache: nil, + networkDate: Date() + )) else { return } -// return cell -// } + cell.configure( + me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user, + tableView: tableView, + viewModel: UserTableViewCell.ViewModel( + user: user, + followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), + blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(), + followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()), + delegate: nil) + } + } + catch { + // do nothing + } + } + + return cell + } } } @@ -183,6 +199,28 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc ) } } + + func showProfile(for account: Mastodon.Entity.Account) { + let managedObjectContext = context.managedObjectContext + let domain = authContext.mastodonAuthenticationBox.domain + + Task { + let user = try await managedObjectContext.perform { + return Persistence.MastodonUser.fetch(in: managedObjectContext, + context: Persistence.MastodonUser.PersistContext( + domain: domain, + entity: account, + cache: nil, + networkDate: Date() + )) + } + + if let user { + await DataSourceFacade.coordinateToProfileScene(provider:self, + user: user.asRecord) + } + } + } } //MARK: UITableViewDelegate @@ -198,8 +236,10 @@ extension SearchResultsOverviewTableViewController: UITableViewDelegate { case .default(let defaultSectionEntry): switch defaultSectionEntry { case .posts(let hashtag): + //FIXME: Show statuses instead of tag-content. Reuse SearchResultsViewController with statuses here? showPosts(tag: Mastodon.Entity.Tag(name: hashtag, url: authContext.mastodonAuthenticationBox.domain)) case .people(let string): + //FIXME: Invoke SearchResultsViewController with people-scope here delegate?.showPeople(self) case .profile(let profile, let instanceName): delegate?.showProfile(self) @@ -211,8 +251,8 @@ extension SearchResultsOverviewTableViewController: UITableViewDelegate { case .hashtag(let tag): showPosts(tag: tag) - case .profile(_): - delegate?.showProfile(self) + case .profile(let account): + showProfile(for: account) } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift index 90560150e..49eeddb32 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift @@ -53,7 +53,7 @@ extension SearchResultSection { authContext: authContext, tableView: tableView, cell: cell, - viewModel: UserTableViewCell.ViewModel(value: .user(user), + viewModel: UserTableViewCell.ViewModel(user: user, followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(), followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()), diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift index b83b8b47d..7f2727a91 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift @@ -12,72 +12,63 @@ import Combine extension UserTableViewCell { final class ViewModel { - let value: Value + let user: MastodonUser let followedUsers: AnyPublisher<[String], Never> let blockedUsers: AnyPublisher<[String], Never> let followRequestedUsers: AnyPublisher<[String], Never> - init(value: Value, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>, followRequestedUsers: AnyPublisher<[String], Never>) { - self.value = value + init(user: MastodonUser, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>, followRequestedUsers: AnyPublisher<[String], Never>) { + self.user = user self.followedUsers = followedUsers self.followRequestedUsers = followRequestedUsers self.blockedUsers = blockedUsers } - - enum Value { - case user(MastodonUser) - // case status(Status) - } } } extension UserTableViewCell { func configure( - me: MastodonUser?, + me: MastodonUser? = nil, tableView: UITableView, viewModel: ViewModel, delegate: UserTableViewCellDelegate? ) { - switch viewModel.value { - case .user(let user): - userView.configure(user: user, delegate: delegate) - - guard let me = me else { - return userView.setButtonState(.none) - } - - if user == me { - userView.setButtonState(.none) - } else { - userView.setButtonState(.loading) - } - - Publishers.CombineLatest3( - viewModel.followedUsers, - viewModel.followRequestedUsers, - viewModel.blockedUsers - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] followed, requested, blocked in - if blocked.contains(user.id) { - self?.userView.setButtonState(.blocked) - } else if followed.contains(user.id) { - self?.userView.setButtonState(.unfollow) - } else if requested.contains(user.id) { - self?.userView.setButtonState(.pending) - } else if user.locked { - self?.userView.setButtonState(.request) - } else if user != me { - self?.userView.setButtonState(.follow) - } - } - .store(in: &disposeBag) - + userView.configure(user: viewModel.user, delegate: delegate) + + guard let me = me else { + return userView.setButtonState(.none) } - - self.delegate = delegate + + if viewModel.user == me { + userView.setButtonState(.none) + } else { + userView.setButtonState(.loading) + } + + Publishers.CombineLatest3( + viewModel.followedUsers, + viewModel.followRequestedUsers, + viewModel.blockedUsers + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] followed, requested, blocked in + if blocked.contains(viewModel.user.id) { + self?.userView.setButtonState(.blocked) + } else if followed.contains(viewModel.user.id) { + self?.userView.setButtonState(.unfollow) + } else if requested.contains(viewModel.user.id) { + self?.userView.setButtonState(.pending) + } else if viewModel.user.locked { + self?.userView.setButtonState(.request) + } else if viewModel.user != me { + self?.userView.setButtonState(.follow) + } + } + .store(in: &disposeBag) + + self.delegate = delegate } } From 1ad591fd8246000f8d6d6c12e6ea82e099b6241f Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 17 Sep 2023 13:01:46 +0200 Subject: [PATCH 08/34] Search for people (IOS-141) --- Mastodon/Coordinator/SceneCoordinator.swift | 313 +++++++++--------- .../SearchResultOverviewSection.swift | 36 +- ...chResultsOverviewTableViewController.swift | 30 +- 3 files changed, 189 insertions(+), 190 deletions(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 4908a533a..73eda67eb 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -153,6 +153,7 @@ extension SceneCoordinator { // search case searchDetail(viewModel: SearchDetailViewModel) + case searchResult(viewModel: SearchResultViewModel) // compose case compose(viewModel: ComposeViewModel) @@ -376,159 +377,169 @@ private extension SceneCoordinator { let viewController: UIViewController? switch scene { - case .welcome: - let _viewController = WelcomeViewController() - viewController = _viewController - case .mastodonPickServer(let viewModel): - let _viewController = MastodonPickServerViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .mastodonRegister(let viewModel): - let _viewController = MastodonRegisterViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .mastodonServerRules(let viewModel): - let _viewController = MastodonServerRulesViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .mastodonConfirmEmail(let viewModel): - let _viewController = MastodonConfirmEmailViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .mastodonLogin: - let loginViewController = MastodonLoginViewController(appContext: appContext, - authenticationViewModel: AuthenticationViewModel(context: appContext, coordinator: self, isAuthenticationExist: false), - sceneCoordinator: self) - loginViewController.delegate = self + case .welcome: + let _viewController = WelcomeViewController() + viewController = _viewController + case .mastodonPickServer(let viewModel): + let _viewController = MastodonPickServerViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .mastodonRegister(let viewModel): + let _viewController = MastodonRegisterViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .mastodonServerRules(let viewModel): + let _viewController = MastodonServerRulesViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .mastodonConfirmEmail(let viewModel): + let _viewController = MastodonConfirmEmailViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .mastodonLogin: + let loginViewController = MastodonLoginViewController(appContext: appContext, + authenticationViewModel: AuthenticationViewModel(context: appContext, coordinator: self, isAuthenticationExist: false), + sceneCoordinator: self) + loginViewController.delegate = self - viewController = loginViewController - case .mastodonPrivacyPolicies(let viewModel): - let privacyViewController = PrivacyTableViewController(context: appContext, coordinator: self, viewModel: viewModel) - viewController = privacyViewController - case .mastodonResendEmail(let viewModel): - let _viewController = MastodonResendEmailViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .mastodonWebView(let viewModel): - let _viewController = WebViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .searchDetail(let viewModel): - let _viewController = SearchDetailViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .compose(let viewModel): - let _viewController = ComposeViewController(viewModel: viewModel) - viewController = _viewController - case .thread(let viewModel): - let _viewController = ThreadViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .editHistory(let viewModel): - let editHistoryViewController = StatusEditHistoryViewController(viewModel: viewModel) - viewController = editHistoryViewController - case .hashtagTimeline(let viewModel): - let _viewController = HashtagTimelineViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .accountList(let viewModel): - let _viewController = AccountListViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .profile(let viewModel): - let _viewController = ProfileViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .bookmark(let viewModel): - let _viewController = BookmarkViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .followedTags(let viewModel): - let _viewController = FollowedTagsViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .favorite(let viewModel): - let _viewController = FavoriteViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .follower(let viewModel): - let _viewController = FollowerListViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .following(let viewModel): - let _viewController = FollowingListViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .familiarFollowers(let viewModel): - let _viewController = FamiliarFollowersViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .rebloggedBy(let viewModel): - let _viewController = RebloggedByViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .favoritedBy(let viewModel): - let _viewController = FavoritedByViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .report(let viewModel): - let _viewController = ReportViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .reportServerRules(let viewModel): - let _viewController = ReportServerRulesViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .reportStatus(let viewModel): - let _viewController = ReportStatusViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .reportSupplementary(let viewModel): - let _viewController = ReportSupplementaryViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .reportResult(let viewModel): - let _viewController = ReportResultViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .suggestionAccount(let viewModel): - let _viewController = SuggestionAccountViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .mediaPreview(let viewModel): - let _viewController = MediaPreviewViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .safari(let url): - guard let scheme = url.scheme?.lowercased(), - scheme == "http" || scheme == "https" else { - return nil - } - let _viewController = SFSafariViewController(url: url) - _viewController.preferredBarTintColor = ThemeService.shared.currentTheme.value.navigationBarBackgroundColor - _viewController.preferredControlTintColor = Asset.Colors.Brand.blurple.color - viewController = _viewController + viewController = loginViewController + case .mastodonPrivacyPolicies(let viewModel): + let privacyViewController = PrivacyTableViewController(context: appContext, coordinator: self, viewModel: viewModel) + viewController = privacyViewController + case .mastodonResendEmail(let viewModel): + let _viewController = MastodonResendEmailViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .mastodonWebView(let viewModel): + let _viewController = WebViewController() + _viewController.viewModel = viewModel + viewController = _viewController - case .alertController(let alertController): - if let popoverPresentationController = alertController.popoverPresentationController { - assert( - popoverPresentationController.sourceView != nil || - popoverPresentationController.sourceRect != .zero || - popoverPresentationController.barButtonItem != nil - ) - } - viewController = alertController - case .activityViewController(let activityViewController, let sourceView, let barButtonItem): - activityViewController.popoverPresentationController?.sourceView = sourceView - activityViewController.popoverPresentationController?.barButtonItem = barButtonItem - viewController = activityViewController - case .settings(let viewModel): - let _viewController = SettingsViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .editStatus(let viewModel): - let composeViewController = ComposeViewController(viewModel: viewModel) - viewController = composeViewController + + case .searchDetail(let viewModel): + let _viewController = SearchDetailViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .searchResult(let viewModel): + let searchResultViewController = SearchResultViewController() + searchResultViewController.context = appContext + searchResultViewController.coordinator = self + searchResultViewController.viewModel = viewModel + viewController = searchResultViewController + + + case .compose(let viewModel): + let _viewController = ComposeViewController(viewModel: viewModel) + viewController = _viewController + case .thread(let viewModel): + let _viewController = ThreadViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .editHistory(let viewModel): + let editHistoryViewController = StatusEditHistoryViewController(viewModel: viewModel) + viewController = editHistoryViewController + case .hashtagTimeline(let viewModel): + let _viewController = HashtagTimelineViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .accountList(let viewModel): + let _viewController = AccountListViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .profile(let viewModel): + let _viewController = ProfileViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .bookmark(let viewModel): + let _viewController = BookmarkViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .followedTags(let viewModel): + let _viewController = FollowedTagsViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .favorite(let viewModel): + let _viewController = FavoriteViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .follower(let viewModel): + let _viewController = FollowerListViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .following(let viewModel): + let _viewController = FollowingListViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .familiarFollowers(let viewModel): + let _viewController = FamiliarFollowersViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .rebloggedBy(let viewModel): + let _viewController = RebloggedByViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .favoritedBy(let viewModel): + let _viewController = FavoritedByViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .report(let viewModel): + let _viewController = ReportViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .reportServerRules(let viewModel): + let _viewController = ReportServerRulesViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .reportStatus(let viewModel): + let _viewController = ReportStatusViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .reportSupplementary(let viewModel): + let _viewController = ReportSupplementaryViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .reportResult(let viewModel): + let _viewController = ReportResultViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .suggestionAccount(let viewModel): + let _viewController = SuggestionAccountViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .mediaPreview(let viewModel): + let _viewController = MediaPreviewViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .safari(let url): + guard let scheme = url.scheme?.lowercased(), + scheme == "http" || scheme == "https" else { + return nil + } + let _viewController = SFSafariViewController(url: url) + _viewController.preferredBarTintColor = ThemeService.shared.currentTheme.value.navigationBarBackgroundColor + _viewController.preferredControlTintColor = Asset.Colors.Brand.blurple.color + viewController = _viewController + + case .alertController(let alertController): + if let popoverPresentationController = alertController.popoverPresentationController { + assert( + popoverPresentationController.sourceView != nil || + popoverPresentationController.sourceRect != .zero || + popoverPresentationController.barButtonItem != nil + ) + } + viewController = alertController + case .activityViewController(let activityViewController, let sourceView, let barButtonItem): + activityViewController.popoverPresentationController?.sourceView = sourceView + activityViewController.popoverPresentationController?.barButtonItem = barButtonItem + viewController = activityViewController + case .settings(let viewModel): + let _viewController = SettingsViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .editStatus(let viewModel): + let composeViewController = ComposeViewController(viewModel: viewModel) + viewController = composeViewController } setupDependency(for: viewController as? NeedsDependency) diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift index 37d352b07..0909f5eab 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift @@ -46,39 +46,25 @@ enum SearchResultOverviewItem: Hashable { } } } - + enum SuggestionSectionEntry: Hashable { case hashtag(tag: Mastodon.Entity.Tag) case profile(user: Mastodon.Entity.Account) - + var title: String? { - switch self { - - case .hashtag(tag: let tag): - return tag.name - case .profile(user: let user): - return "\(user.displayName) — \(user.acct)" + if case let .hashtag(tag) = self { + return tag.name + } else { + return nil } -// if case let .hashtag(tag) = self { -// return tag.name -// } else { -// return nil -// } } - + var icon: UIImage? { - switch self { - - case .hashtag(tag: _): - return UIImage(systemName: "number") - - case .profile(user: _): - return UIImage(systemName: "person.circle") - + if case .hashtag(_) = self { + return UIImage(systemName: "number") + } else { + return nil } -// if case let .hashtag(tag) = self { -// } else { -// } } } } diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index b3026e279..5097dd605 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -61,23 +61,21 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc 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() } - // how the fuck do I get a MastodonUser??? let managedObjectContext = appContext.managedObjectContext Task { do { try await managedObjectContext.perform { guard let user = Persistence.MastodonUser.fetch(in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: authContext.mastodonAuthenticationBox.domain, - entity: profile, - cache: nil, - networkDate: Date() - )) else { return } + context: Persistence.MastodonUser.PersistContext( + domain: authContext.mastodonAuthenticationBox.domain, + entity: profile, + cache: nil, + networkDate: Date() + )) else { return } cell.configure( me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user, @@ -89,8 +87,7 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()), delegate: nil) } - } - catch { + } catch { // do nothing } } @@ -105,7 +102,6 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc tableView.delegate = self self.dataSource = dataSource - view.addSubview(tableView) tableView.pinToParent() } @@ -221,6 +217,13 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc } } } + + func searchForPeople(withName searchText: String) { + let searchResultViewModel = SearchResultViewModel(context: context, authContext: authContext, searchScope: .people) + searchResultViewModel.searchText.value = searchText + + coordinator.present(scene: .searchResult(viewModel: searchResultViewModel), transition: .show) + } } //MARK: UITableViewDelegate @@ -238,9 +241,8 @@ extension SearchResultsOverviewTableViewController: UITableViewDelegate { case .posts(let hashtag): //FIXME: Show statuses instead of tag-content. Reuse SearchResultsViewController with statuses here? showPosts(tag: Mastodon.Entity.Tag(name: hashtag, url: authContext.mastodonAuthenticationBox.domain)) - case .people(let string): - //FIXME: Invoke SearchResultsViewController with people-scope here - delegate?.showPeople(self) + case .people(let searchText): + searchForPeople(withName: searchText) case .profile(let profile, let instanceName): delegate?.showProfile(self) case .openLink(let string): From ed56e7aebe1d0d71da7c1222743a30fa2b6ffc1b Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 17 Sep 2023 13:18:29 +0200 Subject: [PATCH 09/34] Show posts with a certain keyword instead of Hashtag (IOS-141) --- .../SearchResultsOverviewTableViewController.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index 5097dd605..e818f9418 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -224,6 +224,13 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc coordinator.present(scene: .searchResult(viewModel: searchResultViewModel), transition: .show) } + + func searchForPosts(withSearchText searchText: String) { + let searchResultViewModel = SearchResultViewModel(context: context, authContext: authContext, searchScope: .posts) + searchResultViewModel.searchText.value = searchText + + coordinator.present(scene: .searchResult(viewModel: searchResultViewModel), transition: .show) + } } //MARK: UITableViewDelegate @@ -238,9 +245,8 @@ extension SearchResultsOverviewTableViewController: UITableViewDelegate { switch item { case .default(let defaultSectionEntry): switch defaultSectionEntry { - case .posts(let hashtag): - //FIXME: Show statuses instead of tag-content. Reuse SearchResultsViewController with statuses here? - showPosts(tag: Mastodon.Entity.Tag(name: hashtag, url: authContext.mastodonAuthenticationBox.domain)) + case .posts(let searchText): + searchForPosts(withSearchText: searchText) case .people(let searchText): searchForPeople(withName: searchText) case .profile(let profile, let instanceName): From 558f88e77a39d9460106fc067d1ea8f63ba8bf77 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 17 Sep 2023 13:34:08 +0200 Subject: [PATCH 10/34] Sprinkle in some localization (IOS-141) --- .../input/Base.lproj/app.json | 10 ++++---- .../SearchResultOverviewSection.swift | 16 ++++++------- .../SearchResultViewController.swift | 2 ++ .../Generated/Strings.swift | 24 +++++++++++-------- .../Resources/Base.lproj/Localizable.strings | 8 +++---- 5 files changed, 32 insertions(+), 28 deletions(-) diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json index f22ec3098..139f20347 100644 --- a/Localization/StringsConvertor/input/Base.lproj/app.json +++ b/Localization/StringsConvertor/input/Base.lproj/app.json @@ -651,12 +651,10 @@ } }, "searching": { - "segment": { - "all": "All", - "people": "People", - "hashtags": "Hashtags", - "posts": "Posts" - }, + "posts": "Posts with \"%@\"", + "people": "People with \"%@\"", + "profile": "Go to @%@@%@", + "url": "Open Link", "empty_state": { "no_results": "No results" }, diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift index 0909f5eab..8fcc93734 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift @@ -2,6 +2,7 @@ import UIKit import MastodonSDK +import MastodonLocalization import CoreDataStack enum SearchResultOverviewSection: Hashable { @@ -21,15 +22,14 @@ enum SearchResultOverviewItem: Hashable { var title: String { switch self { - //TODO: Add localization case .posts(let text): - return "Posts with \(text)" + return L10n.Scene.Search.Searching.posts(text) case .people(let username): - return "People with \(username)" + return L10n.Scene.Search.Searching.people(username) case .profile(let username, let instanceName): - return "Go to @\(username)@\(instanceName)" + return L10n.Scene.Search.Searching.profile(username, instanceName) case .openLink(_): - return "Open Link" + return L10n.Scene.Search.Searching.url } } @@ -46,11 +46,11 @@ enum SearchResultOverviewItem: Hashable { } } } - + enum SuggestionSectionEntry: Hashable { case hashtag(tag: Mastodon.Entity.Tag) case profile(user: Mastodon.Entity.Account) - + var title: String? { if case let .hashtag(tag) = self { return tag.name @@ -58,7 +58,7 @@ enum SearchResultOverviewItem: Hashable { return nil } } - + var icon: UIImage? { if case .hashtag(_) = self { return UIImage(systemName: "number") diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift index cf7a6a0f6..731117ec6 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift @@ -116,6 +116,8 @@ extension SearchResultViewController { self.tableView.verticalScrollIndicatorInsets.top = frame.height } .store(in: &disposeBag) + + title = viewModel.searchText.value } override func viewWillAppear(_ animated: Bool) { diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index dcc6f84f1..9ff595057 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -1279,22 +1279,26 @@ public enum L10n { public static let clear = L10n.tr("Localizable", "Scene.Search.Searching.Clear", fallback: "Clear") /// Clear all public static let clearAll = L10n.tr("Localizable", "Scene.Search.Searching.ClearAll", fallback: "Clear all") + /// People with "%@" + public static func people(_ p1: Any) -> String { + return L10n.tr("Localizable", "Scene.Search.Searching.People", String(describing: p1), fallback: "People with \"%@\"") + } + /// Posts with "%@" + public static func posts(_ p1: Any) -> String { + return L10n.tr("Localizable", "Scene.Search.Searching.Posts", String(describing: p1), fallback: "Posts with \"%@\"") + } + /// Go to @%@@%@ + public static func profile(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "Scene.Search.Searching.Profile", String(describing: p1), String(describing: p2), fallback: "Go to @%@@%@") + } /// Recent searches public static let recentSearch = L10n.tr("Localizable", "Scene.Search.Searching.RecentSearch", fallback: "Recent searches") + /// Open Link + public static let url = L10n.tr("Localizable", "Scene.Search.Searching.Url", fallback: "Open Link") public enum EmptyState { /// No results public static let noResults = L10n.tr("Localizable", "Scene.Search.Searching.EmptyState.NoResults", fallback: "No results") } - public enum Segment { - /// All - public static let all = L10n.tr("Localizable", "Scene.Search.Searching.Segment.All", fallback: "All") - /// Hashtags - public static let hashtags = L10n.tr("Localizable", "Scene.Search.Searching.Segment.Hashtags", fallback: "Hashtags") - /// People - public static let people = L10n.tr("Localizable", "Scene.Search.Searching.Segment.People", fallback: "People") - /// Posts - public static let posts = L10n.tr("Localizable", "Scene.Search.Searching.Segment.Posts", fallback: "Posts") - } } } public enum ServerPicker { diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 7269b0c4a..964d7ba42 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -443,10 +443,10 @@ uploaded to Mastodon."; "Scene.Search.Searching.ClearAll" = "Clear all"; "Scene.Search.Searching.EmptyState.NoResults" = "No results"; "Scene.Search.Searching.RecentSearch" = "Recent searches"; -"Scene.Search.Searching.Segment.All" = "All"; -"Scene.Search.Searching.Segment.Hashtags" = "Hashtags"; -"Scene.Search.Searching.Segment.People" = "People"; -"Scene.Search.Searching.Segment.Posts" = "Posts"; +"Scene.Search.Searching.Posts" = "Posts with \"%@\""; +"Scene.Search.Searching.People" = "People with \"%@\""; +"Scene.Search.Searching.Profile" = "Go to @%@@%@"; +"Scene.Search.Searching.Url" = "Open Link"; "Scene.Search.Title" = "Search"; "Scene.ServerPicker.Button.Category.Academia" = "academia"; "Scene.ServerPicker.Button.Category.Activism" = "activism"; From c0f2dc5649d5592a2e8ac6afd5b2c49025346ac7 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 17 Sep 2023 13:52:35 +0200 Subject: [PATCH 11/34] Add recent searches to recent searches (IOS-141) --- .../Protocol/Provider/DataSourceFacade+SearchHistory.swift | 3 ++- .../SearchResultsOverviewTableViewController.swift | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift index 18d238c02..1193bb676 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift @@ -8,11 +8,12 @@ import Foundation import CoreDataStack import MastodonCore +import UIKit extension DataSourceFacade { static func responseToCreateSearchHistory( - provider: DataSourceProvider & AuthContextProvider, + provider: NeedsDependency & UIViewController & AuthContextProvider, item: DataSourceItem ) async { switch item { diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index e818f9418..229a5296f 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -193,6 +193,9 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc provider: self, tag: tag ) + + await DataSourceFacade.responseToCreateSearchHistory(provider: self, + item: .hashtag(tag: .entity(tag))) } } @@ -214,6 +217,9 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc if let user { await DataSourceFacade.coordinateToProfileScene(provider:self, user: user.asRecord) + + await DataSourceFacade.responseToCreateSearchHistory(provider: self, + item: .user(record: user.asRecord)) } } } From 15436738d54fcfecfaa379c11ef2089b3746945c Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 17 Sep 2023 14:34:10 +0200 Subject: [PATCH 12/34] Go to user (IOS-141) --- .../Provider/DataSourceFacade+Profile.swift | 221 ------------------ ...chResultsOverviewTableViewController.swift | 35 ++- 2 files changed, 31 insertions(+), 225 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index 161e52e87..219e7ddef 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -127,227 +127,6 @@ extension DataSourceFacade { let barButtonItem: UIBarButtonItem? } -// @MainActor -// static func createProfileActionMenu( -// dependency: NeedsDependency, -// user: ManagedObjectRecord -// ) -> UIMenu { -// var children: [UIMenuElement] = [] -// let name = mastodonUser.displayNameWithFallback -// -// if let shareUser = shareUser { -// let shareAction = UIAction( -// title: L10n.Common.Controls.Actions.shareUser(name), -// image: UIImage(systemName: "square.and.arrow.up"), -// identifier: nil, -// discoverabilityTitle: nil, -// attributes: [], -// state: .off -// ) { [weak provider, weak sourceView, weak barButtonItem] _ in -// guard let provider = provider else { return } -// let activityViewController = createActivityViewControllerForMastodonUser(mastodonUser: shareUser, dependency: provider) -// provider.coordinator.present( -// scene: .activityViewController( -// activityViewController: activityViewController, -// sourceView: sourceView, -// barButtonItem: barButtonItem -// ), -// from: provider, -// transition: .activityViewControllerPresent(animated: true, completion: nil) -// ) -// } -// children.append(shareAction) -// } -// -// if let shareStatus = shareStatus { -// let shareAction = UIAction( -// title: L10n.Common.Controls.Actions.sharePost, -// image: UIImage(systemName: "square.and.arrow.up"), -// identifier: nil, -// discoverabilityTitle: nil, -// attributes: [], -// state: .off -// ) { [weak provider, weak sourceView, weak barButtonItem] _ in -// guard let provider = provider else { return } -// let activityViewController = createActivityViewControllerForMastodonUser(status: shareStatus, dependency: provider) -// provider.coordinator.present( -// scene: .activityViewController( -// activityViewController: activityViewController, -// sourceView: sourceView, -// barButtonItem: barButtonItem -// ), -// from: provider, -// transition: .activityViewControllerPresent(animated: true, completion: nil) -// ) -// } -// children.append(shareAction) -// } -// -// if !isMyself { -// // mute -// let muteAction = UIAction( -// title: isMuting ? L10n.Common.Controls.Friendship.unmuteUser(name) : L10n.Common.Controls.Friendship.mute, -// image: isMuting ? UIImage(systemName: "speaker") : UIImage(systemName: "speaker.slash"), -// discoverabilityTitle: isMuting ? nil : L10n.Common.Controls.Friendship.muteUser(name), -// attributes: isMuting ? [] : .destructive, -// state: .off -// ) { [weak provider, weak cell] _ in -// guard let provider = provider else { return } -// -// UserProviderFacade.toggleUserMuteRelationship( -// provider: provider, -// cell: cell -// ) -// .sink { _ in -// // do nothing -// } receiveValue: { _ in -// // do nothing -// } -// .store(in: &provider.context.disposeBag) -// } -// if isMuting { -// children.append(muteAction) -// } else { -// let muteMenu = UIMenu(title: L10n.Common.Controls.Friendship.muteUser(name), image: UIImage(systemName: "speaker.slash"), options: [], children: [muteAction]) -// children.append(muteMenu) -// } -// } -// -// if !isMyself { -// // block -// let blockAction = UIAction( -// title: isBlocking ? L10n.Common.Controls.Friendship.unblockUser(name) : L10n.Common.Controls.Friendship.block, -// image: isBlocking ? UIImage(systemName: "hand.raised.slash") : UIImage(systemName: "hand.raised"), -// discoverabilityTitle: isBlocking ? nil : L10n.Common.Controls.Friendship.blockUser(name), -// attributes: isBlocking ? [] : .destructive, -// state: .off -// ) { [weak provider, weak cell] _ in -// guard let provider = provider else { return } -// -// UserProviderFacade.toggleUserBlockRelationship( -// provider: provider, -// cell: cell -// ) -// .sink { _ in -// // do nothing -// } receiveValue: { _ in -// // do nothing -// } -// .store(in: &provider.context.disposeBag) -// } -// if isBlocking { -// children.append(blockAction) -// } else { -// let blockMenu = UIMenu(title: L10n.Common.Controls.Friendship.blockUser(name), image: UIImage(systemName: "hand.raised"), options: [], children: [blockAction]) -// children.append(blockMenu) -// } -// } -// -// if !isMyself { -// let reportAction = UIAction( -// title: L10n.Common.Controls.Actions.reportUser(name), -// image: UIImage(systemName: "flag"), -// identifier: nil, -// discoverabilityTitle: nil, -// attributes: [], -// state: .off -// ) { [weak provider] _ in -// guard let provider = provider else { return } -// guard let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { -// return -// } -// let viewModel = ReportViewModel( -// context: provider.context, -// domain: authenticationBox.domain, -// user: mastodonUser, -// status: nil -// ) -// provider.coordinator.present( -// scene: .report(viewModel: viewModel), -// from: provider, -// transition: .modal(animated: true, completion: nil) -// ) -// } -// children.append(reportAction) -// } -// -// if !isInSameDomain { -// if isDomainBlocking { -// let unblockDomainAction = UIAction( -// title: L10n.Common.Controls.Actions.unblockDomain(mastodonUser.domainFromAcct), -// image: UIImage(systemName: "nosign"), -// identifier: nil, -// discoverabilityTitle: nil, -// attributes: [], -// state: .off -// ) { [weak provider, weak cell] _ in -// guard let provider = provider else { return } -// provider.context.blockDomainService.unblockDomain(userProvider: provider, cell: cell) -// } -// children.append(unblockDomainAction) -// } else { -// let blockDomainAction = UIAction( -// title: L10n.Common.Controls.Actions.blockDomain(mastodonUser.domainFromAcct), -// image: UIImage(systemName: "nosign"), -// identifier: nil, -// discoverabilityTitle: nil, -// attributes: [], -// state: .off -// ) { [weak provider, weak cell] _ in -// guard let provider = provider else { return } -// -// let alertController = UIAlertController(title: L10n.Common.Alerts.BlockDomain.title(mastodonUser.domainFromAcct), message: nil, preferredStyle: .alert) -// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .default) { _ in } -// alertController.addAction(cancelAction) -// let blockDomainAction = UIAlertAction(title: L10n.Common.Alerts.BlockDomain.blockEntireDomain, style: .destructive) { [weak provider, weak cell] _ in -// guard let provider = provider else { return } -// provider.context.blockDomainService.blockDomain(userProvider: provider, cell: cell) -// } -// alertController.addAction(blockDomainAction) -// provider.present(alertController, animated: true, completion: nil) -// } -// children.append(blockDomainAction) -// } -// } -// -// if let status = shareStatus, isMyself { -// let deleteAction = UIAction( -// title: L10n.Common.Controls.Actions.delete, -// image: UIImage(systemName: "delete.left"), -// identifier: nil, -// discoverabilityTitle: nil, -// attributes: [.destructive], -// state: .off -// ) { [weak provider] _ in -// guard let provider = provider else { return } -// -// let alertController = UIAlertController(title: L10n.Common.Alerts.DeletePost.title, message: nil, preferredStyle: .alert) -// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .default) { _ in } -// alertController.addAction(cancelAction) -// let deleteAction = UIAlertAction(title: L10n.Common.Alerts.DeletePost.delete, style: .destructive) { [weak provider] _ in -// guard let provider = provider else { return } -// guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return } -// provider.context.apiService.deleteStatus( -// domain: activeMastodonAuthenticationBox.domain, -// statusID: status.id, -// authorizationBox: activeMastodonAuthenticationBox -// ) -// .sink { _ in -// // do nothing -// } receiveValue: { _ in -// // do nothing -// } -// .store(in: &provider.context.disposeBag) -// } -// alertController.addAction(deleteAction) -// provider.present(alertController, animated: true, completion: nil) -// } -// children.append(deleteAction) -// } -// -// return UIMenu(title: "", options: [], children: children) -// } - static func createActivityViewController( dependency: NeedsDependency, user: ManagedObjectRecord diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index 229a5296f..ccfd4fc73 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -195,7 +195,7 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc ) await DataSourceFacade.responseToCreateSearchHistory(provider: self, - item: .hashtag(tag: .entity(tag))) + item: .hashtag(tag: .entity(tag))) } } @@ -219,7 +219,7 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc user: user.asRecord) await DataSourceFacade.responseToCreateSearchHistory(provider: self, - item: .user(record: user.asRecord)) + item: .user(record: user.asRecord)) } } } @@ -237,6 +237,33 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc coordinator.present(scene: .searchResult(viewModel: searchResultViewModel), transition: .show) } + + func searchForPerson(username: String, domain: String) { + let acct = "\(username)@\(domain)" + let query = Mastodon.API.V2.Search.Query( + q: acct, + type: .default, + resolve: true + ) + + Task { + let searchResult = try await context.apiService.search( + query: query, + authenticationBox: authContext.mastodonAuthenticationBox + ).value + + if let account = searchResult.accounts.first(where: { $0.acctWithDomainIfMissing(domain).lowercased() == acct.lowercased() }) { + showProfile(for: account) + } else { + await MainActor.run { + let alertController = UIAlertController(title: "No User Account", message: "There's no Useraccount \"\(username)\" on \(domain)", preferredStyle: .alert) + let okAction = UIAlertAction(title: "OK", style: .default) + alertController.addAction(okAction) + coordinator.present(scene: .alertController(alertController: alertController), transition: .alertController(animated: true)) + } + } + } + } } //MARK: UITableViewDelegate @@ -255,8 +282,8 @@ extension SearchResultsOverviewTableViewController: UITableViewDelegate { searchForPosts(withSearchText: searchText) case .people(let searchText): searchForPeople(withName: searchText) - case .profile(let profile, let instanceName): - delegate?.showProfile(self) + case .profile(let username, let domain): + searchForPerson(username: username, domain: domain) case .openLink(let string): delegate?.openLink(self) } From 7abc536d01ab30e3a3b69e6a6fabef32f3535ecf Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 17 Sep 2023 14:40:23 +0200 Subject: [PATCH 13/34] Add localization to "No such user"-alert (IOS-141) --- Localization/app.json | 14 ++++++++------ .../SearchResultsOverviewTableViewController.swift | 8 ++++++-- .../MastodonLocalization/Generated/Strings.swift | 8 ++++++++ .../Resources/Base.lproj/Localizable.strings | 4 ++++ 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 739573267..8a7817d40 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -651,15 +651,17 @@ } }, "searching": { - "segment": { - "all": "All", - "people": "People", - "hashtags": "Hashtags", - "posts": "Posts" - }, + "posts": "Posts with \"%@\"", + "people": "People with \"%@\"", + "profile": "Go to @%@@%@", + "url": "Open Link", "empty_state": { "no_results": "No results" }, + "no_user": { + "title": "No User Account Found", + "message": "There's no Useraccount \"%@\" on %@" + } "recent_search": "Recent searches", "clear": "Clear" } diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index ccfd4fc73..a90fb747a 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -3,6 +3,7 @@ import UIKit import MastodonCore import MastodonSDK +import MastodonLocalization protocol SearchResultsOverviewTableViewControllerDeleagte: AnyObject { func showPeople(_ viewController: UIViewController) @@ -256,8 +257,11 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc showProfile(for: account) } else { await MainActor.run { - let alertController = UIAlertController(title: "No User Account", message: "There's no Useraccount \"\(username)\" on \(domain)", preferredStyle: .alert) - let okAction = UIAlertAction(title: "OK", style: .default) + let alertTitle = L10n.Scene.Search.Searching.NoUser.title + let alertMessage = L10n.Scene.Search.Searching.NoUser.message(username, domain) + + let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) alertController.addAction(okAction) coordinator.present(scene: .alertController(alertController: alertController), transition: .alertController(animated: true)) } diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 9ff595057..4484b6e96 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -1299,6 +1299,14 @@ public enum L10n { /// No results public static let noResults = L10n.tr("Localizable", "Scene.Search.Searching.EmptyState.NoResults", fallback: "No results") } + public enum NoUser { + /// There's no Useraccount "%@" on %@ + public static func message(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "Scene.Search.Searching.NoUser.Message", String(describing: p1), String(describing: p2), fallback: "There's no Useraccount \"%@\" on %@") + } + /// No User Account Found + public static let title = L10n.tr("Localizable", "Scene.Search.Searching.NoUser.Title", fallback: "No User Account Found") + } } } public enum ServerPicker { diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 964d7ba42..0b5102ec0 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -447,6 +447,10 @@ uploaded to Mastodon."; "Scene.Search.Searching.People" = "People with \"%@\""; "Scene.Search.Searching.Profile" = "Go to @%@@%@"; "Scene.Search.Searching.Url" = "Open Link"; + +"Scene.Search.Searching.NoUser.Title" = "No User Account Found"; +"Scene.Search.Searching.NoUser.Message" = "There's no Useraccount \"%@\" on %@"; + "Scene.Search.Title" = "Search"; "Scene.ServerPicker.Button.Category.Academia" = "academia"; "Scene.ServerPicker.Button.Category.Activism" = "activism"; From ed5a0f3abcb747d5d7b32d0349fad036eaf2289e Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 17 Sep 2023 15:16:25 +0200 Subject: [PATCH 14/34] Show max. 10 items in history (IOS-141) --- .../SearchHistory/SearchHistoryViewModel+Diffable.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel+Diffable.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel+Diffable.swift index 92b70bf99..26f8f572f 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel+Diffable.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel+Diffable.swift @@ -53,10 +53,12 @@ extension SearchHistoryViewModel { return users + hashtags } + + let mostRecentItems = Array(items.prefix(10)) var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) - snapshot.appendItems(items, toSection: .main) - await diffableDataSource.apply(snapshot, animatingDifferences: false) + snapshot.appendItems(mostRecentItems, toSection: .main) + await diffableDataSource.apply(snapshot, animatingDifferences: true) } catch { // do nothing } From cde28f576c6d8a030200faf5a5ff53e7f3a671f8 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 17 Sep 2023 15:16:47 +0200 Subject: [PATCH 15/34] Seach-key dismisses keyboard (IOS-141) --- .../Search/SearchDetail/SearchDetailViewController.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift index 25dc48e45..e0f099bbe 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift @@ -248,6 +248,10 @@ extension SearchDetailViewController: UISearchBarDelegate { searchResultsOverviewViewController.searchForSuggestions(for: trimmedSearchText) } + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + searchBar.resignFirstResponder() + } + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { // dismiss or pop if isModal { From 4378c1e97109d1fd2f1021affb06db35bc4549e6 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 17 Sep 2023 16:57:15 +0200 Subject: [PATCH 16/34] Go to link (IOS-141) It's either a profile (then show the profile) or a status (than show the status) or a link (then open the link in safari) --- ...chResultsOverviewTableViewController.swift | 54 +++++++++++++++++-- .../Sources/MastodonSDK/Extension/URL.swift | 11 ++++ 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index a90fb747a..173be218e 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -118,7 +118,6 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc } func showStandardSearch(for searchText: String) { - guard let dataSource else { return } var snapshot = dataSource.snapshot() @@ -127,8 +126,7 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc .default(.people(searchText)), .default(.profile(searchText, authContext.mastodonAuthenticationBox.domain))], toSection: .default) - if URL(string: searchText) != nil { - //TODO: Check if Mastodon-URL + if URL(string: searchText)?.isValidURL() ?? false { snapshot.appendItems([.default(.openLink(searchText))], toSection: .default) } @@ -188,6 +186,8 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc activeTask = searchTask } + //MARK: - Actions + func showPosts(tag: Mastodon.Entity.Tag) { Task { await DataSourceFacade.coordinateToHashtagScene( @@ -268,6 +268,50 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc } } } + + func goTo(link: String) { + + let query = Mastodon.API.V2.Search.Query( + q: link, + type: .default, + resolve: true + ) + + let authContext = self.authContext + let managedObjectContext = context.managedObjectContext + + Task { + let searchResult = try await context.apiService.search( + query: query, + authenticationBox: authContext.mastodonAuthenticationBox + ).value + + if let account = searchResult.accounts.first { + showProfile(for: account) + } else if let status = searchResult.statuses.first { + + let status = try await managedObjectContext.perform { + return Persistence.Status.fetch(in: managedObjectContext, context: Persistence.Status.PersistContext( + domain: authContext.mastodonAuthenticationBox.domain, + entity: status, + me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user, + statusCache: nil, + userCache: nil, + networkDate: Date())) + } + + guard let status else { return } + + await DataSourceFacade.coordinateToStatusThreadScene( + provider: self, + target: .status, // remove reblog wrapper + status: status.asRecord + ) + } else if let url = URL(string: link) { + coordinator.present(scene: .safari(url: url), transition: .safariPresent(animated: true)) + } + } + } } //MARK: UITableViewDelegate @@ -288,8 +332,8 @@ extension SearchResultsOverviewTableViewController: UITableViewDelegate { searchForPeople(withName: searchText) case .profile(let username, let domain): searchForPerson(username: username, domain: domain) - case .openLink(let string): - delegate?.openLink(self) + case .openLink(let urlString): + goTo(link: urlString) } case .suggestion(let suggestionSectionEntry): switch suggestionSectionEntry { diff --git a/MastodonSDK/Sources/MastodonSDK/Extension/URL.swift b/MastodonSDK/Sources/MastodonSDK/Extension/URL.swift index a9345c7b3..5675fc8c5 100644 --- a/MastodonSDK/Sources/MastodonSDK/Extension/URL.swift +++ b/MastodonSDK/Sources/MastodonSDK/Extension/URL.swift @@ -11,4 +11,15 @@ extension URL { public static func httpScheme(domain: String) -> String { return domain.hasSuffix(".onion") ? "http" : "https" } + + // inspired by https://stackoverflow.com/a/49072718 + public func isValidURL() -> Bool { + if let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue), + let match = detector.firstMatch(in: absoluteString, options: [], range: NSRange(location: 0, length: absoluteString.utf16.count)) { + // it is a link, if the match covers the whole string + return match.range.length == absoluteString.utf16.count + } else { + return false + } + } } From cb4c5a938d8d95b72737ff0d8de80cfd76b0576c Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 17 Sep 2023 17:21:53 +0200 Subject: [PATCH 17/34] Consider mastodon-domains for other instances and profiles (IOS-141) --- .../SearchResultOverviewSection.swift | 2 +- ...earchResultsOverviewTableViewController.swift | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift index 8fcc93734..6cc4b8942 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift @@ -17,7 +17,7 @@ enum SearchResultOverviewItem: Hashable { enum DefaultSectionEntry: Hashable { case posts(String) case people(String) - case profile(String, String) + case profile(username: String, domain: String) case openLink(String) var title: String { diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index 173be218e..797bd65e6 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -123,8 +123,20 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc var snapshot = dataSource.snapshot() snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .default)) snapshot.appendItems([.default(.posts(searchText)), - .default(.people(searchText)), - .default(.profile(searchText, authContext.mastodonAuthenticationBox.domain))], toSection: .default) + .default(.people(searchText))], toSection: .default) + let components = searchText.split(separator: "@") + if components.count == 2 { + let username = String(components[0]) + + let domain = String(components[1]) + if domain.split(separator: ".").count >= 2 { + snapshot.appendItems([.default(.profile(username: username, domain: domain))], toSection: .default) + } else { + snapshot.appendItems([.default(.profile(username: username, domain: authContext.mastodonAuthenticationBox.domain))], toSection: .default) + } + } else { + snapshot.appendItems([.default(.profile(username: searchText, domain: authContext.mastodonAuthenticationBox.domain))], toSection: .default) + } if URL(string: searchText)?.isValidURL() ?? false { snapshot.appendItems([.default(.openLink(searchText))], toSection: .default) From c4b31112a47e9d546dfd9c825b506818a6fb04ee Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 17 Sep 2023 17:27:42 +0200 Subject: [PATCH 18/34] Fix build (IOS-141) --- Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift index 41f5d58de..e2431fc28 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift @@ -5,14 +5,14 @@ // Created by MainasuK on 2022-1-17. // -import Foundation +import UIKit import CoreData import CoreDataStack import MastodonCore extension DataSourceFacade { static func coordinateToStatusThreadScene( - provider: DataSourceProvider & AuthContextProvider, + provider: NeedsDependency & UIViewController & AuthContextProvider, target: StatusTarget, status: ManagedObjectRecord ) async { @@ -40,7 +40,7 @@ extension DataSourceFacade { @MainActor static func coordinateToStatusThreadScene( - provider: DataSourceProvider & AuthContextProvider, + provider: NeedsDependency & UIViewController & AuthContextProvider, root: StatusItem.Thread ) async { let threadViewModel = ThreadViewModel( From 11863b94580c9497e4eb08cd93722f3bea75362c Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 17 Sep 2023 17:44:38 +0200 Subject: [PATCH 19/34] Show magnifying glass next to hashtags (IOS-141) --- Mastodon.xcodeproj/project.pbxproj | 8 -------- .../Cells/SearchResultDefaultSectionTableViewCell.swift | 4 ++-- .../SearchDetail/SearchHistory/SearchHistorySection.swift | 3 +++ 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 0636ceb29..dc441b9e8 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -2154,13 +2154,6 @@ path = Status; sourceTree = ""; }; - DB4F098026A0475500D62E92 /* View */ = { - isa = PBXGroup; - children = ( - ); - path = View; - sourceTree = ""; - }; DB4FFC2D269EC39C00D62E92 /* Search */ = { isa = PBXGroup; children = ( @@ -2935,7 +2928,6 @@ isa = PBXGroup; children = ( DB63F7502799449300455B82 /* Cell */, - DB4F098026A0475500D62E92 /* View */, DBF1D250269DB01200C1C08A /* SearchHistoryViewController.swift */, DB63F76127996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift */, DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */, diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultDefaultSectionTableViewCell.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultDefaultSectionTableViewCell.swift index 49ba6d111..94bc48dcc 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultDefaultSectionTableViewCell.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultDefaultSectionTableViewCell.swift @@ -12,7 +12,7 @@ class SearchResultDefaultSectionTableViewCell: UITableViewCell { content.text = item.title content.imageProperties.tintColor = Asset.Colors.Brand.blurple.color - self.contentConfiguration = content + contentConfiguration = content } func configure(item: SearchResultOverviewItem.SuggestionSectionEntry) { @@ -21,6 +21,6 @@ class SearchResultDefaultSectionTableViewCell: UITableViewCell { content.text = item.title content.imageProperties.tintColor = Asset.Colors.Brand.blurple.color - self.contentConfiguration = content + contentConfiguration = content } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift index d0cbc05ee..2c0e19451 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift @@ -8,6 +8,7 @@ import UIKit import CoreDataStack import MastodonCore +import MastodonAsset enum SearchHistorySection: Hashable { case main @@ -47,6 +48,8 @@ extension SearchHistorySection { context.managedObjectContext.performAndWait { guard let hashtag = item.object(in: context.managedObjectContext) else { return } var contentConfiguration = cell.defaultContentConfiguration() + contentConfiguration.image = UIImage(systemName: "magnifyingglass") + contentConfiguration.imageProperties.tintColor = Asset.Colors.Brand.blurple.color contentConfiguration.text = "#" + hashtag.name cell.contentConfiguration = contentConfiguration } From b74f17c6b6ce14955c2d258e90def8eec5e51fbd Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 17 Sep 2023 18:14:42 +0200 Subject: [PATCH 20/34] Don't show follow-button for me (IOS-141) --- ...archResultsOverviewTableViewController.swift | 11 ----------- .../SearchDetailViewController.swift | 17 ----------------- ...istoryUserCollectionViewCell+ViewModel.swift | 5 +++-- .../UserTableViewCell+ViewModel.swift | 4 +++- .../View/Content/UserView+ViewModel.swift | 5 +---- .../MastodonUI/View/Content/UserView.swift | 7 +++++++ 6 files changed, 14 insertions(+), 35 deletions(-) diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index 797bd65e6..c6d6175c7 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -5,17 +5,8 @@ import MastodonCore import MastodonSDK import MastodonLocalization -protocol SearchResultsOverviewTableViewControllerDeleagte: AnyObject { - 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, NeedsDependency, AuthContextProvider { - // similar to the other search results view controller but without the whole statemachine bullshit - // with scope all - var context: AppContext! let authContext: AuthContext var coordinator: SceneCoordinator! @@ -23,7 +14,6 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc private let tableView: UITableView var dataSource: UITableViewDiffableDataSource? - weak var delegate: SearchResultsOverviewTableViewControllerDeleagte? var activeTask: Task? init(appContext: AppContext, authContext: AuthContext, coordinator: SceneCoordinator) { @@ -330,7 +320,6 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc 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] diff --git a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift index e0f099bbe..990dab66e 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift @@ -115,8 +115,6 @@ final class SearchDetailViewController: UIViewController, NeedsDependency { searchHistoryViewController.view.pinToParent() } - searchResultsOverviewViewController.delegate = self - addChild(searchResultsOverviewViewController) searchResultsOverviewViewController.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(searchResultsOverviewViewController.view) @@ -261,18 +259,3 @@ extension SearchDetailViewController: UISearchBarDelegate { } } } - -//MARK: SearchResultsOverviewViewControllerDelegate -extension SearchDetailViewController: SearchResultsOverviewTableViewControllerDeleagte { - func showPeople(_ viewController: UIViewController) { - //TODO: Implement - } - - func showProfile(_ viewController: UIViewController) { - //TODO: Implement - } - - func openLink(_ viewController: UIViewController) { - //TODO: Implement - } -} diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift index 31350811d..8e9710e9a 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift @@ -54,7 +54,9 @@ extension SearchHistoryUserCollectionViewCell { ) .receive(on: DispatchQueue.main) .sink { [weak self] followed, requested, blocked in - if blocked.contains(user.id) { + if user == me { + self?.userView.setButtonState(.none) + } else if blocked.contains(user.id) { self?.userView.setButtonState(.blocked) } else if followed.contains(user.id) { self?.userView.setButtonState(.unfollow) @@ -67,6 +69,5 @@ extension SearchHistoryUserCollectionViewCell { } } .store(in: &_disposeBag) - } } diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift index 7f2727a91..526e74ab3 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift @@ -54,7 +54,9 @@ extension UserTableViewCell { ) .receive(on: DispatchQueue.main) .sink { [weak self] followed, requested, blocked in - if blocked.contains(viewModel.user.id) { + if viewModel.user == me { + self?.userView.setButtonState(.none) + } else if blocked.contains(viewModel.user.id) { self?.userView.setButtonState(.blocked) } else if followed.contains(viewModel.user.id) { self?.userView.setButtonState(.unfollow) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift index 1b9a88772..4127ed4d4 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift @@ -6,7 +6,6 @@ // import CoreDataStack -import os.log import UIKit import Combine import MetaTextKit @@ -19,9 +18,7 @@ extension UserView { public final class ViewModel: ObservableObject { public var disposeBag = Set() public var observations = Set() - - let logger = Logger(subsystem: "StatusView", category: "ViewModel") - + @Published public var authorAvatarImage: UIImage? @Published public var authorAvatarImageURL: URL? @Published public var authorName: MetaContent? diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift index 2ea1f1a1f..9221c7f90 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift @@ -261,41 +261,48 @@ public extension UserView { switch state { case .loading: + followButtonWrapper.isHidden = false followButton.isHidden = false followButton.setTitle(nil, for: .normal) followButton.setBackgroundColor(Asset.Colors.Button.disabled.color, for: .normal) case .follow: + followButtonWrapper.isHidden = false followButton.isHidden = false followButton.setTitle(L10n.Common.Controls.Friendship.follow, for: .normal) followButton.setBackgroundColor(Asset.Colors.Button.userFollow.color, for: .normal) followButton.setTitleColor(.white, for: .normal) case .request: + followButtonWrapper.isHidden = false followButton.isHidden = false followButton.setTitle(L10n.Common.Controls.Friendship.request, for: .normal) followButton.setBackgroundColor(Asset.Colors.Button.userFollow.color, for: .normal) followButton.setTitleColor(.white, for: .normal) case .pending: + followButtonWrapper.isHidden = false followButton.isHidden = false followButton.setTitle(L10n.Common.Controls.Friendship.pending, for: .normal) followButton.setTitleColor(Asset.Colors.Button.userFollowingTitle.color, for: .normal) followButton.setBackgroundColor(Asset.Colors.Button.userFollowing.color, for: .normal) case .unfollow: + followButtonWrapper.isHidden = false followButton.isHidden = false followButton.setTitle(L10n.Common.Controls.Friendship.following, for: .normal) followButton.setBackgroundColor(Asset.Colors.Button.userFollowing.color, for: .normal) followButton.setTitleColor(Asset.Colors.Button.userFollowingTitle.color, for: .normal) case .blocked: + followButtonWrapper.isHidden = false followButton.isHidden = false followButton.setTitle(L10n.Common.Controls.Friendship.blocked, for: .normal) followButton.setBackgroundColor(Asset.Colors.Button.userBlocked.color, for: .normal) followButton.setTitleColor(.systemRed, for: .normal) case .none: + followButtonWrapper.isHidden = true followButton.isHidden = true followButton.setTitle(nil, for: .normal) followButton.setBackgroundColor(.clear, for: .normal) From a7bab76f9670a2d64cff553ebbbeafce74429453 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 18 Sep 2023 16:12:42 +0200 Subject: [PATCH 21/34] Add a cell for profiles in search results (IOS-141) --- Mastodon.xcodeproj/project.pbxproj | 4 + .../SearchResultsProfileTableViewCell.swift | 170 ++++++++++++++++++ ...chResultsOverviewTableViewController.swift | 47 ++--- .../Entity/Mastodon+Entity+Account.swift | 7 + .../Extension/FLAnimatedImageView.swift | 4 +- 5 files changed, 194 insertions(+), 38 deletions(-) create mode 100644 Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultsProfileTableViewCell.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index dc441b9e8..f4936e22f 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -151,6 +151,7 @@ D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */; }; D8BE30B32A179E26006B8270 /* SuggestionAccountTableViewFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */; }; D8BEBCB62A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */; }; + D8D688F62AB869CB000F651A /* SearchResultsProfileTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */; }; D8E5C346296DAB84007E76A7 /* DataSourceFacade+Status+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */; }; D8E5C349296DB8A3007E76A7 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */; }; D8F0372C29D232730027DE2E /* HashtagIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */; }; @@ -803,6 +804,7 @@ D8A6FE6629325F5900666A47 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = ""; }; D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountTableViewFooter.swift; sourceTree = ""; }; D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountTableViewCell+ViewModel.swift"; sourceTree = ""; }; + D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsProfileTableViewCell.swift; sourceTree = ""; }; D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status+History.swift"; sourceTree = ""; }; D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = ""; }; D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagIntentHandler.swift; sourceTree = ""; }; @@ -1811,6 +1813,7 @@ isa = PBXGroup; children = ( D81A227A2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift */, + D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */, ); path = Cells; sourceTree = ""; @@ -3890,6 +3893,7 @@ DBFEEC99279BDCDE004F81DD /* ProfileAboutViewModel.swift in Sources */, 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */, DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */, + D8D688F62AB869CB000F651A /* SearchResultsProfileTableViewCell.swift in Sources */, 2A82294F29262EE000D2A1F7 /* AppContext+NextAccount.swift in Sources */, DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */, DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */, diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultsProfileTableViewCell.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultsProfileTableViewCell.swift new file mode 100644 index 000000000..25af454b8 --- /dev/null +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultsProfileTableViewCell.swift @@ -0,0 +1,170 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import UIKit +import MastodonSDK +import MastodonUI +import MetaTextKit +import MastodonLocalization +import MastodonMeta +import MastodonCore +import MastodonAsset + +class SearchResultsProfileTableViewCell: UITableViewCell { + static let reuseIdentifier = "SearchResultsProfileTableViewCell" + + private static var metricFormatter = MastodonMetricFormatter() + + private let avatarImageWrapperView: UIView + let avatarImageView: AvatarImageView + + private let metaInformationStackView: UIStackView + + private let upperLineStackView: UIStackView + let displayNameLabel: MetaLabel + let acctLabel: UILabel + + private let lowerLineStackView: UIStackView + let followersLabel: UILabel + let verifiedLinkImageView: UIImageView + let verifiedLinkLabel: MetaLabel + + private let contentStackView: UIStackView + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + + avatarImageView = AvatarImageView() + avatarImageView.cornerConfiguration = AvatarImageView.CornerConfiguration(corner: .fixed(radius: 8)) + avatarImageView.translatesAutoresizingMaskIntoConstraints = false + + avatarImageWrapperView = UIView() + avatarImageWrapperView.translatesAutoresizingMaskIntoConstraints = false + avatarImageWrapperView.addSubview(avatarImageView) + + displayNameLabel = MetaLabel(style: .statusName) + displayNameLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + displayNameLabel.setContentHuggingPriority(.required, for: .horizontal) + + acctLabel = UILabel() + acctLabel.textColor = .secondaryLabel + acctLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) + acctLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + + upperLineStackView = UIStackView(arrangedSubviews: [displayNameLabel, acctLabel]) + upperLineStackView.distribution = .fill + upperLineStackView.alignment = .center + + followersLabel = UILabel() + followersLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + followersLabel.textColor = .secondaryLabel + followersLabel.setContentHuggingPriority(.required, for: .horizontal) + + verifiedLinkImageView = UIImageView() + verifiedLinkImageView.setContentCompressionResistancePriority(.defaultHigh - 1, for: .vertical) + verifiedLinkImageView.setContentHuggingPriority(.required, for: .horizontal) + verifiedLinkImageView.contentMode = .scaleAspectFit + + verifiedLinkLabel = MetaLabel(style: .profileFieldValue) + verifiedLinkLabel.setContentCompressionResistancePriority(.defaultHigh - 2, for: .horizontal) + verifiedLinkLabel.translatesAutoresizingMaskIntoConstraints = false + verifiedLinkLabel.textAttributes = [ + .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)), + .foregroundColor: UIColor.secondaryLabel + ] + verifiedLinkLabel.linkAttributes = [ + .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)), + .foregroundColor: Asset.Colors.Brand.blurple.color + ] + verifiedLinkLabel.isUserInteractionEnabled = false + + lowerLineStackView = UIStackView(arrangedSubviews: [followersLabel, verifiedLinkImageView, verifiedLinkLabel]) + lowerLineStackView.distribution = .fill + lowerLineStackView.alignment = .center + lowerLineStackView.spacing = 4 + lowerLineStackView.setCustomSpacing(2, after: verifiedLinkImageView) + + metaInformationStackView = UIStackView(arrangedSubviews: [upperLineStackView, lowerLineStackView]) + metaInformationStackView.axis = .vertical + metaInformationStackView.alignment = .leading + + contentStackView = UIStackView(arrangedSubviews: [avatarImageWrapperView, metaInformationStackView]) + contentStackView.translatesAutoresizingMaskIntoConstraints = false + contentStackView.axis = .horizontal + contentStackView.alignment = .center + contentStackView.spacing = 16 + + super.init(style: style, reuseIdentifier: reuseIdentifier) + + contentView.addSubview(contentStackView) + setupConstraints() + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + private func setupConstraints() { + let constraints = [ + contentStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + contentStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + contentView.trailingAnchor.constraint(greaterThanOrEqualTo: contentStackView.trailingAnchor, constant: 16), + contentView.bottomAnchor.constraint(equalTo: contentStackView.bottomAnchor, constant: 8), + + upperLineStackView.trailingAnchor.constraint(greaterThanOrEqualTo: metaInformationStackView.trailingAnchor), + lowerLineStackView.trailingAnchor.constraint(greaterThanOrEqualTo: metaInformationStackView.trailingAnchor), + metaInformationStackView.trailingAnchor.constraint(greaterThanOrEqualTo: contentStackView.trailingAnchor), + + avatarImageView.widthAnchor.constraint(equalToConstant: 30), + avatarImageView.heightAnchor.constraint(equalTo: avatarImageView.widthAnchor), + avatarImageView.topAnchor.constraint(greaterThanOrEqualTo: avatarImageWrapperView.topAnchor), + avatarImageView.leadingAnchor.constraint(equalTo: avatarImageWrapperView.leadingAnchor), + avatarImageWrapperView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor), + avatarImageWrapperView.bottomAnchor.constraint(greaterThanOrEqualTo: avatarImageView.bottomAnchor), + avatarImageView.centerYAnchor.constraint(equalTo: avatarImageWrapperView.centerYAnchor), + ] + + NSLayoutConstraint.activate(constraints) + } + + override func prepareForReuse() { + super.prepareForReuse() + + avatarImageView.prepareForReuse() + } + + func configure(with account: Mastodon.Entity.Account) { + let displayNameMetaContent: MetaContent + do { + let content = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojis?.asDictionary ?? [:]) + displayNameMetaContent = try MastodonMetaContent.convert(document: content) + } catch { + displayNameMetaContent = PlaintextMetaContent(string: account.displayNameWithFallback) + } + + displayNameLabel.configure(content: displayNameMetaContent) + acctLabel.text = account.acct + followersLabel.attributedText = NSAttributedString( + format: NSAttributedString(string: L10n.Common.UserList.followersCount("%@"), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))]), + args: NSAttributedString(string: Self.metricFormatter.string(from: account.followersCount) ?? account.followersCount.formatted(), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))]) + ) + + avatarImageView.setImage(url: account.avatarImageURL()) + + if let verifiedLink = account.verifiedLink?.value { + verifiedLinkImageView.image = UIImage(systemName: "checkmark") + verifiedLinkImageView.tintColor = Asset.Colors.Brand.blurple.color + + let verifiedLinkMetaContent: MetaContent + do { + let mastodonContent = MastodonContent(content: verifiedLink, emojis: [:]) + verifiedLinkMetaContent = try MastodonMetaContent.convert(document: mastodonContent) + } catch { + verifiedLinkMetaContent = PlaintextMetaContent(string: verifiedLink) + } + + verifiedLinkLabel.configure(content: verifiedLinkMetaContent) + } else { + verifiedLinkImageView.image = UIImage(systemName: "questionmark.circle") + verifiedLinkImageView.tintColor = .secondaryLabel + + verifiedLinkLabel.configure(content: PlaintextMetaContent(string: L10n.Common.UserList.noVerifiedLink)) + } + } +} diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index c6d6175c7..c25bea693 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -28,11 +28,15 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc 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) + tableView.register(SearchResultsProfileTableViewCell.self, forCellReuseIdentifier: SearchResultsProfileTableViewCell.reuseIdentifier) + + super.init(nibName: nil, bundle: nil) + + let dataSource = UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, itemIdentifier in + + guard let self else { fatalError("Ooops, no self!?") } - let dataSource = UITableViewDiffableDataSource(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() } @@ -41,54 +45,23 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc 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() } + guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultsProfileTableViewCell.reuseIdentifier, for: indexPath) as? SearchResultsProfileTableViewCell else { fatalError() } - let managedObjectContext = appContext.managedObjectContext - Task { - do { - - try await managedObjectContext.perform { - guard let user = Persistence.MastodonUser.fetch(in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: authContext.mastodonAuthenticationBox.domain, - entity: profile, - cache: nil, - networkDate: Date() - )) else { return } - - cell.configure( - me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user, - tableView: tableView, - viewModel: UserTableViewCell.ViewModel( - user: user, - followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), - blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(), - followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()), - delegate: nil) - } - } catch { - // do nothing - } - } + cell.configure(with: profile) return cell } } } - super.init(nibName: nil, bundle: nil) tableView.dataSource = dataSource tableView.delegate = self self.dataSource = dataSource @@ -349,3 +322,5 @@ extension SearchResultsOverviewTableViewController: UITableViewDelegate { tableView.deselectRow(at: indexPath, animated: true) } } + +extension SearchResultsOverviewTableViewController: UserTableViewCellDelegate {} diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift index 38bcf14fc..34fdad5ba 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift @@ -92,3 +92,10 @@ extension Mastodon.Entity.Account { return acct } } + +extension Mastodon.Entity.Account { + public var verifiedLink: Mastodon.Entity.Field? { + let firstVerified = fields?.first(where: { $0.verifiedAt != nil }) + return firstVerified + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Extension/FLAnimatedImageView.swift b/MastodonSDK/Sources/MastodonUI/Extension/FLAnimatedImageView.swift index 3fc92a4b5..68c7fbc95 100644 --- a/MastodonSDK/Sources/MastodonUI/Extension/FLAnimatedImageView.swift +++ b/MastodonSDK/Sources/MastodonUI/Extension/FLAnimatedImageView.swift @@ -39,8 +39,8 @@ extension FLAnimatedImageView { public func setImage( url: URL?, - placeholder: UIImage?, - scaleToSize: CGSize? + placeholder: UIImage? = nil, + scaleToSize: CGSize? = nil ) { // cancel task cancelTask() From ce37a8eb47c42cc36b2df9544cade88bf45c01f5 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 18 Sep 2023 17:05:29 +0200 Subject: [PATCH 22/34] Copy condensed version of user-view to collection-view in search-history (IOS-141) --- Mastodon.xcodeproj/project.pbxproj | 4 - ...toryUserCollectionViewCell+ViewModel.swift | 73 ------ .../SearchHistoryUserCollectionViewCell.swift | 219 +++++++++++++----- .../SearchHistory/SearchHistorySection.swift | 11 +- .../SearchHistoryViewController.swift | 2 - .../SearchResult/SearchResultSection.swift | 2 +- .../Entity/Mastodon/MastodonUser.swift | 7 + .../MastodonUI/View/Button/AvatarButton.swift | 24 -- 8 files changed, 175 insertions(+), 167 deletions(-) delete mode 100644 Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index f4936e22f..a61ad3f16 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -300,7 +300,6 @@ DB63F74F2799405600455B82 /* SearchHistoryViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */; }; DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */; }; DB63F7542799491600455B82 /* DataSourceFacade+SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F7532799491600455B82 /* DataSourceFacade+SearchHistory.swift */; }; - DB63F75A279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F759279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift */; }; DB63F76227996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F76127996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift */; }; DB63F764279A5E3C00455B82 /* NotificationTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F763279A5E3C00455B82 /* NotificationTimelineViewController.swift */; }; DB63F767279A5EB300455B82 /* NotificationTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F766279A5EB300455B82 /* NotificationTimelineViewModel.swift */; }; @@ -993,7 +992,6 @@ DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryViewModel+Diffable.swift"; sourceTree = ""; }; DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySectionHeaderCollectionReusableView.swift; sourceTree = ""; }; DB63F7532799491600455B82 /* DataSourceFacade+SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+SearchHistory.swift"; sourceTree = ""; }; - DB63F759279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryUserCollectionViewCell+ViewModel.swift"; sourceTree = ""; }; DB63F76127996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryViewController+DataSourceProvider.swift"; sourceTree = ""; }; DB63F763279A5E3C00455B82 /* NotificationTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTimelineViewController.swift; sourceTree = ""; }; DB63F766279A5EB300455B82 /* NotificationTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTimelineViewModel.swift; sourceTree = ""; }; @@ -2295,7 +2293,6 @@ isa = PBXGroup; children = ( DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */, - DB63F759279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift */, DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */, ); path = Cell; @@ -3560,7 +3557,6 @@ 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 */, 5D0393962612D266007FE196 /* WebViewModel.swift in Sources */, 5B24BBDA262DB14800A9381B /* ReportViewModel.swift in Sources */, diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift deleted file mode 100644 index 8e9710e9a..000000000 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// SearchHistoryUserCollectionViewCell+ViewModel.swift -// Mastodon -// -// Created by MainasuK on 2022-1-20. -// - -import Foundation -import CoreDataStack -import MastodonUI -import Combine - -extension SearchHistoryUserCollectionViewCell { - final class ViewModel { - let value: MastodonUser - - let followedUsers: AnyPublisher<[String], Never> - let blockedUsers: AnyPublisher<[String], Never> - let followRequestedUsers: AnyPublisher<[String], Never> - - init(value: MastodonUser, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>, followRequestedUsers: AnyPublisher<[String], Never>) { - self.value = value - self.followedUsers = followedUsers - self.followRequestedUsers = followRequestedUsers - self.blockedUsers = blockedUsers - } - } -} - -extension SearchHistoryUserCollectionViewCell { - func configure( - me: MastodonUser?, - viewModel: ViewModel, - delegate: SearchHistorySectionHeaderCollectionReusableViewDelegate? - ) { - let user = viewModel.value - - userView.configure(user: user, delegate: delegate) - - guard let me = me else { - return userView.setButtonState(.none) - } - - if user == me { - userView.setButtonState(.none) - } else { - userView.setButtonState(.loading) - } - - Publishers.CombineLatest3( - viewModel.followedUsers, - viewModel.followRequestedUsers, - viewModel.blockedUsers - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] followed, requested, blocked in - if user == me { - self?.userView.setButtonState(.none) - } else if blocked.contains(user.id) { - self?.userView.setButtonState(.blocked) - } else if followed.contains(user.id) { - self?.userView.setButtonState(.unfollow) - } else if requested.contains(user.id) { - self?.userView.setButtonState(.pending) - } else if user.locked { - self?.userView.setButtonState(.request) - } else if user != me { - self?.userView.setButtonState(.follow) - } - } - .store(in: &_disposeBag) - } -} diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift index 1da7075f2..3e655417c 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift @@ -1,74 +1,187 @@ -// -// SearchHistoryUserCollectionViewCell.swift -// Mastodon -// -// Created by MainasuK on 2022-1-20. -// +// Copyright © 2023 Mastodon gGmbH. All rights reserved. import UIKit -import Combine -import MastodonCore +import MastodonSDK import MastodonUI +import MetaTextKit +import MastodonLocalization +import MastodonMeta +import MastodonCore +import MastodonAsset +import CoreDataStack + +class SearchHistoryUserCollectionViewCell: UICollectionViewCell { + static let reuseIdentifier = "SearchHistoryUserCollectionViewCell" + + private static var metricFormatter = MastodonMetricFormatter() + + private let avatarImageWrapperView: UIView + let avatarImageView: AvatarImageView + + private let metaInformationStackView: UIStackView + + private let upperLineStackView: UIStackView + let displayNameLabel: MetaLabel + let acctLabel: UILabel + + private let lowerLineStackView: UIStackView + let followersLabel: UILabel + let verifiedLinkImageView: UIImageView + let verifiedLinkLabel: MetaLabel + + private let contentStackView: UIStackView + + override init(frame: CGRect) { + avatarImageView = AvatarImageView() + avatarImageView.cornerConfiguration = AvatarImageView.CornerConfiguration(corner: .fixed(radius: 8)) + avatarImageView.translatesAutoresizingMaskIntoConstraints = false + + avatarImageWrapperView = UIView() + avatarImageWrapperView.translatesAutoresizingMaskIntoConstraints = false + avatarImageWrapperView.addSubview(avatarImageView) + + displayNameLabel = MetaLabel(style: .statusName) + displayNameLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + displayNameLabel.setContentHuggingPriority(.required, for: .horizontal) + + acctLabel = UILabel() + acctLabel.textColor = .secondaryLabel + acctLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) + acctLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + + upperLineStackView = UIStackView(arrangedSubviews: [displayNameLabel, acctLabel]) + upperLineStackView.distribution = .fill + upperLineStackView.alignment = .center + + followersLabel = UILabel() + followersLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + followersLabel.textColor = .secondaryLabel + followersLabel.setContentHuggingPriority(.required, for: .horizontal) + + verifiedLinkImageView = UIImageView() + verifiedLinkImageView.setContentCompressionResistancePriority(.defaultHigh - 1, for: .vertical) + verifiedLinkImageView.setContentHuggingPriority(.required, for: .horizontal) + verifiedLinkImageView.contentMode = .scaleAspectFit + + verifiedLinkLabel = MetaLabel(style: .profileFieldValue) + verifiedLinkLabel.setContentCompressionResistancePriority(.defaultHigh - 2, for: .horizontal) + verifiedLinkLabel.translatesAutoresizingMaskIntoConstraints = false + verifiedLinkLabel.textAttributes = [ + .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)), + .foregroundColor: UIColor.secondaryLabel + ] + verifiedLinkLabel.linkAttributes = [ + .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)), + .foregroundColor: Asset.Colors.Brand.blurple.color + ] + verifiedLinkLabel.isUserInteractionEnabled = false + + lowerLineStackView = UIStackView(arrangedSubviews: [followersLabel, verifiedLinkImageView, verifiedLinkLabel]) + lowerLineStackView.distribution = .fill + lowerLineStackView.alignment = .center + lowerLineStackView.spacing = 4 + lowerLineStackView.setCustomSpacing(2, after: verifiedLinkImageView) + + metaInformationStackView = UIStackView(arrangedSubviews: [upperLineStackView, lowerLineStackView]) + metaInformationStackView.axis = .vertical + metaInformationStackView.alignment = .leading + + contentStackView = UIStackView(arrangedSubviews: [avatarImageWrapperView, metaInformationStackView]) + contentStackView.translatesAutoresizingMaskIntoConstraints = false + contentStackView.axis = .horizontal + contentStackView.alignment = .center + contentStackView.spacing = 16 + + super.init(frame: .zero) + + contentView.addSubview(contentStackView) + setupConstraints() + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + private func setupConstraints() { + let constraints = [ + contentStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + contentStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + contentView.trailingAnchor.constraint(greaterThanOrEqualTo: contentStackView.trailingAnchor, constant: 16), + contentView.bottomAnchor.constraint(equalTo: contentStackView.bottomAnchor, constant: 8), + + upperLineStackView.trailingAnchor.constraint(greaterThanOrEqualTo: metaInformationStackView.trailingAnchor), + lowerLineStackView.trailingAnchor.constraint(greaterThanOrEqualTo: metaInformationStackView.trailingAnchor), + metaInformationStackView.trailingAnchor.constraint(greaterThanOrEqualTo: contentStackView.trailingAnchor), + + avatarImageView.widthAnchor.constraint(equalToConstant: 30), + avatarImageView.heightAnchor.constraint(equalTo: avatarImageView.widthAnchor), + avatarImageView.topAnchor.constraint(greaterThanOrEqualTo: avatarImageWrapperView.topAnchor), + avatarImageView.leadingAnchor.constraint(equalTo: avatarImageWrapperView.leadingAnchor), + avatarImageWrapperView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor), + avatarImageWrapperView.bottomAnchor.constraint(greaterThanOrEqualTo: avatarImageView.bottomAnchor), + avatarImageView.centerYAnchor.constraint(equalTo: avatarImageWrapperView.centerYAnchor), + ] + + NSLayoutConstraint.activate(constraints) + } -final class SearchHistoryUserCollectionViewCell: UICollectionViewCell { - - var _disposeBag = Set() - - let userView = UserView() - override func prepareForReuse() { super.prepareForReuse() - - userView.prepareForReuse() - } - - override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} -extension SearchHistoryUserCollectionViewCell { - - private func _init() { - ThemeService.shared.currentTheme - .map { $0.secondarySystemGroupedBackgroundColor } - .sink { [weak self] backgroundColor in - guard let self = self else { return } - self.backgroundColor = backgroundColor - self.setNeedsUpdateConfiguration() + avatarImageView.prepareForReuse() + } + + func configure(with user: MastodonUser) { + let displayNameMetaContent: MetaContent + do { + let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojis.asDictionary) + displayNameMetaContent = try MastodonMetaContent.convert(document: content) + } catch { + displayNameMetaContent = PlaintextMetaContent(string: user.displayNameWithFallback) + } + + displayNameLabel.configure(content: displayNameMetaContent) + acctLabel.text = user.acct + followersLabel.attributedText = NSAttributedString( + format: NSAttributedString(string: L10n.Common.UserList.followersCount("%@"), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))]), + args: NSAttributedString(string: Self.metricFormatter.string(from: Int(user.followersCount)) ?? user.followersCount.formatted(), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))]) + ) + + avatarImageView.setImage(url: user.avatarImageURL()) + + if let verifiedLink = user.verifiedLink?.value { + verifiedLinkImageView.image = UIImage(systemName: "checkmark") + verifiedLinkImageView.tintColor = Asset.Colors.Brand.blurple.color + + let verifiedLinkMetaContent: MetaContent + do { + let mastodonContent = MastodonContent(content: verifiedLink, emojis: [:]) + verifiedLinkMetaContent = try MastodonMetaContent.convert(document: mastodonContent) + } catch { + verifiedLinkMetaContent = PlaintextMetaContent(string: verifiedLink) } - .store(in: &_disposeBag) - - userView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(userView) - NSLayoutConstraint.activate([ - userView.topAnchor.constraint(equalTo: contentView.topAnchor), - userView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), - contentView.trailingAnchor.constraint(equalTo: userView.trailingAnchor, constant: 16), - userView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - ]) - userView.accessibilityTraits.insert(.button) + verifiedLinkLabel.configure(content: verifiedLinkMetaContent) + } else { + verifiedLinkImageView.image = UIImage(systemName: "questionmark.circle") + verifiedLinkImageView.tintColor = .secondaryLabel + + verifiedLinkLabel.configure(content: PlaintextMetaContent(string: L10n.Common.UserList.noVerifiedLink)) + } } - + override func updateConfiguration(using state: UICellConfigurationState) { super.updateConfiguration(using: state) - + var backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell() backgroundConfiguration.backgroundColorTransformer = .init { _ in if state.isHighlighted || state.isSelected { return ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor + } else { + return ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor } - return ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor } + self.backgroundConfiguration = backgroundConfiguration + } - } + diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift index 2c0e19451..aa43f2467 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift @@ -31,16 +31,7 @@ extension SearchHistorySection { let userCellRegister = UICollectionView.CellRegistration> { cell, indexPath, item in context.managedObjectContext.performAndWait { guard let user = item.object(in: context.managedObjectContext) else { return } - cell.configure( - me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user, - viewModel: SearchHistoryUserCollectionViewCell.ViewModel( - value: user, - followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), - blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(), - followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher() - ), - delegate: configuration.searchHistorySectionHeaderCollectionReusableViewDelegate - ) + cell.configure(with: user) } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift index d55259d03..86340feed 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift @@ -14,8 +14,6 @@ import MastodonUI final class SearchHistoryViewController: UIViewController, NeedsDependency { - let logger = Logger(subsystem: "SearchHistoryViewController", category: "ViewController") - weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift index 49eeddb32..a7b10ae1f 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift @@ -45,7 +45,7 @@ extension SearchResultSection { return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in switch item { case .user(let record): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell + let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as! UserTableViewCell context.managedObjectContext.performAndWait { guard let user = record.object(in: context.managedObjectContext) else { return } configure( diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift index fe668ccb9..6f3c5f8eb 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift @@ -548,3 +548,10 @@ extension MastodonUser: AutoUpdatableObject { } } } + +extension MastodonUser { + public var verifiedLink: MastodonField? { + let firstVerified = fields.first(where: { $0.verifiedAt != nil }) + return firstVerified + } +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Button/AvatarButton.swift b/MastodonSDK/Sources/MastodonUI/View/Button/AvatarButton.swift index ab5c8a70e..6d399c1b2 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Button/AvatarButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Button/AvatarButton.swift @@ -5,7 +5,6 @@ // Created by MainasuK Cirno on 2021-7-21. // -import os.log import UIKit import MastodonLocalization @@ -117,26 +116,3 @@ extension AvatarButton { } } - -#if canImport(SwiftUI) && DEBUG -import SwiftUI - -struct AvatarButton_Previews: PreviewProvider { - - static var previews: some View { - UIViewPreview(width: 42) { - let avatarButton = AvatarButton() - avatarButton.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - avatarButton.widthAnchor.constraint(equalToConstant: 42), - avatarButton.heightAnchor.constraint(equalToConstant: 42), - ]) - return avatarButton - } - .previewLayout(.fixed(width: 42, height: 42)) - } - -} - -#endif - From dd569fe0acb29001c262e4936bd29bda3cf8dcf0 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 18 Sep 2023 17:21:36 +0200 Subject: [PATCH 23/34] Put CondensedUserView into their own class (IOS-141) --- .../SearchResultsProfileTableViewCell.swift | 153 +------------ ...chResultsOverviewTableViewController.swift | 2 +- .../SearchHistoryUserCollectionViewCell.swift | 156 +------------ .../SearchHistory/SearchHistorySection.swift | 2 +- .../View/Content/CondensedUserView.swift | 205 ++++++++++++++++++ 5 files changed, 220 insertions(+), 298 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonUI/View/Content/CondensedUserView.swift diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultsProfileTableViewCell.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultsProfileTableViewCell.swift index 25af454b8..505e1fa80 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultsProfileTableViewCell.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultsProfileTableViewCell.swift @@ -1,170 +1,29 @@ // Copyright © 2023 Mastodon gGmbH. All rights reserved. import UIKit -import MastodonSDK import MastodonUI -import MetaTextKit -import MastodonLocalization -import MastodonMeta -import MastodonCore -import MastodonAsset class SearchResultsProfileTableViewCell: UITableViewCell { static let reuseIdentifier = "SearchResultsProfileTableViewCell" - private static var metricFormatter = MastodonMetricFormatter() - - private let avatarImageWrapperView: UIView - let avatarImageView: AvatarImageView - - private let metaInformationStackView: UIStackView - - private let upperLineStackView: UIStackView - let displayNameLabel: MetaLabel - let acctLabel: UILabel - - private let lowerLineStackView: UIStackView - let followersLabel: UILabel - let verifiedLinkImageView: UIImageView - let verifiedLinkLabel: MetaLabel - - private let contentStackView: UIStackView + let condensedUserView: CondensedUserView override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - avatarImageView = AvatarImageView() - avatarImageView.cornerConfiguration = AvatarImageView.CornerConfiguration(corner: .fixed(radius: 8)) - avatarImageView.translatesAutoresizingMaskIntoConstraints = false - - avatarImageWrapperView = UIView() - avatarImageWrapperView.translatesAutoresizingMaskIntoConstraints = false - avatarImageWrapperView.addSubview(avatarImageView) - - displayNameLabel = MetaLabel(style: .statusName) - displayNameLabel.setContentCompressionResistancePriority(.required, for: .horizontal) - displayNameLabel.setContentHuggingPriority(.required, for: .horizontal) - - acctLabel = UILabel() - acctLabel.textColor = .secondaryLabel - acctLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) - acctLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - - upperLineStackView = UIStackView(arrangedSubviews: [displayNameLabel, acctLabel]) - upperLineStackView.distribution = .fill - upperLineStackView.alignment = .center - - followersLabel = UILabel() - followersLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - followersLabel.textColor = .secondaryLabel - followersLabel.setContentHuggingPriority(.required, for: .horizontal) - - verifiedLinkImageView = UIImageView() - verifiedLinkImageView.setContentCompressionResistancePriority(.defaultHigh - 1, for: .vertical) - verifiedLinkImageView.setContentHuggingPriority(.required, for: .horizontal) - verifiedLinkImageView.contentMode = .scaleAspectFit - - verifiedLinkLabel = MetaLabel(style: .profileFieldValue) - verifiedLinkLabel.setContentCompressionResistancePriority(.defaultHigh - 2, for: .horizontal) - verifiedLinkLabel.translatesAutoresizingMaskIntoConstraints = false - verifiedLinkLabel.textAttributes = [ - .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)), - .foregroundColor: UIColor.secondaryLabel - ] - verifiedLinkLabel.linkAttributes = [ - .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)), - .foregroundColor: Asset.Colors.Brand.blurple.color - ] - verifiedLinkLabel.isUserInteractionEnabled = false - - lowerLineStackView = UIStackView(arrangedSubviews: [followersLabel, verifiedLinkImageView, verifiedLinkLabel]) - lowerLineStackView.distribution = .fill - lowerLineStackView.alignment = .center - lowerLineStackView.spacing = 4 - lowerLineStackView.setCustomSpacing(2, after: verifiedLinkImageView) - - metaInformationStackView = UIStackView(arrangedSubviews: [upperLineStackView, lowerLineStackView]) - metaInformationStackView.axis = .vertical - metaInformationStackView.alignment = .leading - - contentStackView = UIStackView(arrangedSubviews: [avatarImageWrapperView, metaInformationStackView]) - contentStackView.translatesAutoresizingMaskIntoConstraints = false - contentStackView.axis = .horizontal - contentStackView.alignment = .center - contentStackView.spacing = 16 + condensedUserView = CondensedUserView(frame: .zero) + condensedUserView.translatesAutoresizingMaskIntoConstraints = false super.init(style: style, reuseIdentifier: reuseIdentifier) - contentView.addSubview(contentStackView) - setupConstraints() + contentView.addSubview(condensedUserView) + condensedUserView.pinToParent() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - private func setupConstraints() { - let constraints = [ - contentStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), - contentStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), - contentView.trailingAnchor.constraint(greaterThanOrEqualTo: contentStackView.trailingAnchor, constant: 16), - contentView.bottomAnchor.constraint(equalTo: contentStackView.bottomAnchor, constant: 8), - - upperLineStackView.trailingAnchor.constraint(greaterThanOrEqualTo: metaInformationStackView.trailingAnchor), - lowerLineStackView.trailingAnchor.constraint(greaterThanOrEqualTo: metaInformationStackView.trailingAnchor), - metaInformationStackView.trailingAnchor.constraint(greaterThanOrEqualTo: contentStackView.trailingAnchor), - - avatarImageView.widthAnchor.constraint(equalToConstant: 30), - avatarImageView.heightAnchor.constraint(equalTo: avatarImageView.widthAnchor), - avatarImageView.topAnchor.constraint(greaterThanOrEqualTo: avatarImageWrapperView.topAnchor), - avatarImageView.leadingAnchor.constraint(equalTo: avatarImageWrapperView.leadingAnchor), - avatarImageWrapperView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor), - avatarImageWrapperView.bottomAnchor.constraint(greaterThanOrEqualTo: avatarImageView.bottomAnchor), - avatarImageView.centerYAnchor.constraint(equalTo: avatarImageWrapperView.centerYAnchor), - ] - - NSLayoutConstraint.activate(constraints) - } - override func prepareForReuse() { super.prepareForReuse() - avatarImageView.prepareForReuse() - } - - func configure(with account: Mastodon.Entity.Account) { - let displayNameMetaContent: MetaContent - do { - let content = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojis?.asDictionary ?? [:]) - displayNameMetaContent = try MastodonMetaContent.convert(document: content) - } catch { - displayNameMetaContent = PlaintextMetaContent(string: account.displayNameWithFallback) - } - - displayNameLabel.configure(content: displayNameMetaContent) - acctLabel.text = account.acct - followersLabel.attributedText = NSAttributedString( - format: NSAttributedString(string: L10n.Common.UserList.followersCount("%@"), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))]), - args: NSAttributedString(string: Self.metricFormatter.string(from: account.followersCount) ?? account.followersCount.formatted(), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))]) - ) - - avatarImageView.setImage(url: account.avatarImageURL()) - - if let verifiedLink = account.verifiedLink?.value { - verifiedLinkImageView.image = UIImage(systemName: "checkmark") - verifiedLinkImageView.tintColor = Asset.Colors.Brand.blurple.color - - let verifiedLinkMetaContent: MetaContent - do { - let mastodonContent = MastodonContent(content: verifiedLink, emojis: [:]) - verifiedLinkMetaContent = try MastodonMetaContent.convert(document: mastodonContent) - } catch { - verifiedLinkMetaContent = PlaintextMetaContent(string: verifiedLink) - } - - verifiedLinkLabel.configure(content: verifiedLinkMetaContent) - } else { - verifiedLinkImageView.image = UIImage(systemName: "questionmark.circle") - verifiedLinkImageView.tintColor = .secondaryLabel - - verifiedLinkLabel.configure(content: PlaintextMetaContent(string: L10n.Common.UserList.noVerifiedLink)) - } + condensedUserView.prepareForReuse() } } diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index c25bea693..5687562bb 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -55,7 +55,7 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc case .profile(let profile): guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultsProfileTableViewCell.reuseIdentifier, for: indexPath) as? SearchResultsProfileTableViewCell else { fatalError() } - cell.configure(with: profile) + cell.condensedUserView.configure(with: profile) return cell } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift index 3e655417c..c17492b2e 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift @@ -1,171 +1,29 @@ // Copyright © 2023 Mastodon gGmbH. All rights reserved. import UIKit -import MastodonSDK import MastodonUI -import MetaTextKit -import MastodonLocalization -import MastodonMeta import MastodonCore -import MastodonAsset -import CoreDataStack class SearchHistoryUserCollectionViewCell: UICollectionViewCell { static let reuseIdentifier = "SearchHistoryUserCollectionViewCell" - private static var metricFormatter = MastodonMetricFormatter() - - private let avatarImageWrapperView: UIView - let avatarImageView: AvatarImageView - - private let metaInformationStackView: UIStackView - - private let upperLineStackView: UIStackView - let displayNameLabel: MetaLabel - let acctLabel: UILabel - - private let lowerLineStackView: UIStackView - let followersLabel: UILabel - let verifiedLinkImageView: UIImageView - let verifiedLinkLabel: MetaLabel - - private let contentStackView: UIStackView + let condensedUserView: CondensedUserView override init(frame: CGRect) { - avatarImageView = AvatarImageView() - avatarImageView.cornerConfiguration = AvatarImageView.CornerConfiguration(corner: .fixed(radius: 8)) - avatarImageView.translatesAutoresizingMaskIntoConstraints = false + condensedUserView = CondensedUserView(frame: .zero) + condensedUserView.translatesAutoresizingMaskIntoConstraints = false + super.init(frame: frame) - avatarImageWrapperView = UIView() - avatarImageWrapperView.translatesAutoresizingMaskIntoConstraints = false - avatarImageWrapperView.addSubview(avatarImageView) - - displayNameLabel = MetaLabel(style: .statusName) - displayNameLabel.setContentCompressionResistancePriority(.required, for: .horizontal) - displayNameLabel.setContentHuggingPriority(.required, for: .horizontal) - - acctLabel = UILabel() - acctLabel.textColor = .secondaryLabel - acctLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) - acctLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - - upperLineStackView = UIStackView(arrangedSubviews: [displayNameLabel, acctLabel]) - upperLineStackView.distribution = .fill - upperLineStackView.alignment = .center - - followersLabel = UILabel() - followersLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - followersLabel.textColor = .secondaryLabel - followersLabel.setContentHuggingPriority(.required, for: .horizontal) - - verifiedLinkImageView = UIImageView() - verifiedLinkImageView.setContentCompressionResistancePriority(.defaultHigh - 1, for: .vertical) - verifiedLinkImageView.setContentHuggingPriority(.required, for: .horizontal) - verifiedLinkImageView.contentMode = .scaleAspectFit - - verifiedLinkLabel = MetaLabel(style: .profileFieldValue) - verifiedLinkLabel.setContentCompressionResistancePriority(.defaultHigh - 2, for: .horizontal) - verifiedLinkLabel.translatesAutoresizingMaskIntoConstraints = false - verifiedLinkLabel.textAttributes = [ - .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)), - .foregroundColor: UIColor.secondaryLabel - ] - verifiedLinkLabel.linkAttributes = [ - .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)), - .foregroundColor: Asset.Colors.Brand.blurple.color - ] - verifiedLinkLabel.isUserInteractionEnabled = false - - lowerLineStackView = UIStackView(arrangedSubviews: [followersLabel, verifiedLinkImageView, verifiedLinkLabel]) - lowerLineStackView.distribution = .fill - lowerLineStackView.alignment = .center - lowerLineStackView.spacing = 4 - lowerLineStackView.setCustomSpacing(2, after: verifiedLinkImageView) - - metaInformationStackView = UIStackView(arrangedSubviews: [upperLineStackView, lowerLineStackView]) - metaInformationStackView.axis = .vertical - metaInformationStackView.alignment = .leading - - contentStackView = UIStackView(arrangedSubviews: [avatarImageWrapperView, metaInformationStackView]) - contentStackView.translatesAutoresizingMaskIntoConstraints = false - contentStackView.axis = .horizontal - contentStackView.alignment = .center - contentStackView.spacing = 16 - - super.init(frame: .zero) - - contentView.addSubview(contentStackView) - setupConstraints() + contentView.addSubview(condensedUserView) + condensedUserView.pinToParent() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - private func setupConstraints() { - let constraints = [ - contentStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), - contentStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), - contentView.trailingAnchor.constraint(greaterThanOrEqualTo: contentStackView.trailingAnchor, constant: 16), - contentView.bottomAnchor.constraint(equalTo: contentStackView.bottomAnchor, constant: 8), - - upperLineStackView.trailingAnchor.constraint(greaterThanOrEqualTo: metaInformationStackView.trailingAnchor), - lowerLineStackView.trailingAnchor.constraint(greaterThanOrEqualTo: metaInformationStackView.trailingAnchor), - metaInformationStackView.trailingAnchor.constraint(greaterThanOrEqualTo: contentStackView.trailingAnchor), - - avatarImageView.widthAnchor.constraint(equalToConstant: 30), - avatarImageView.heightAnchor.constraint(equalTo: avatarImageView.widthAnchor), - avatarImageView.topAnchor.constraint(greaterThanOrEqualTo: avatarImageWrapperView.topAnchor), - avatarImageView.leadingAnchor.constraint(equalTo: avatarImageWrapperView.leadingAnchor), - avatarImageWrapperView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor), - avatarImageWrapperView.bottomAnchor.constraint(greaterThanOrEqualTo: avatarImageView.bottomAnchor), - avatarImageView.centerYAnchor.constraint(equalTo: avatarImageWrapperView.centerYAnchor), - ] - - NSLayoutConstraint.activate(constraints) - } - override func prepareForReuse() { super.prepareForReuse() - avatarImageView.prepareForReuse() - } - - func configure(with user: MastodonUser) { - let displayNameMetaContent: MetaContent - do { - let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojis.asDictionary) - displayNameMetaContent = try MastodonMetaContent.convert(document: content) - } catch { - displayNameMetaContent = PlaintextMetaContent(string: user.displayNameWithFallback) - } - - displayNameLabel.configure(content: displayNameMetaContent) - acctLabel.text = user.acct - followersLabel.attributedText = NSAttributedString( - format: NSAttributedString(string: L10n.Common.UserList.followersCount("%@"), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))]), - args: NSAttributedString(string: Self.metricFormatter.string(from: Int(user.followersCount)) ?? user.followersCount.formatted(), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))]) - ) - - avatarImageView.setImage(url: user.avatarImageURL()) - - if let verifiedLink = user.verifiedLink?.value { - verifiedLinkImageView.image = UIImage(systemName: "checkmark") - verifiedLinkImageView.tintColor = Asset.Colors.Brand.blurple.color - - let verifiedLinkMetaContent: MetaContent - do { - let mastodonContent = MastodonContent(content: verifiedLink, emojis: [:]) - verifiedLinkMetaContent = try MastodonMetaContent.convert(document: mastodonContent) - } catch { - verifiedLinkMetaContent = PlaintextMetaContent(string: verifiedLink) - } - - verifiedLinkLabel.configure(content: verifiedLinkMetaContent) - } else { - verifiedLinkImageView.image = UIImage(systemName: "questionmark.circle") - verifiedLinkImageView.tintColor = .secondaryLabel - - verifiedLinkLabel.configure(content: PlaintextMetaContent(string: L10n.Common.UserList.noVerifiedLink)) - } + condensedUserView.prepareForReuse() } override func updateConfiguration(using state: UICellConfigurationState) { diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift index aa43f2467..082e1224f 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift @@ -31,7 +31,7 @@ extension SearchHistorySection { let userCellRegister = UICollectionView.CellRegistration> { cell, indexPath, item in context.managedObjectContext.performAndWait { guard let user = item.object(in: context.managedObjectContext) else { return } - cell.configure(with: user) + cell.condensedUserView.configure(with: user) } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/CondensedUserView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/CondensedUserView.swift new file mode 100644 index 000000000..be766f5e6 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/View/Content/CondensedUserView.swift @@ -0,0 +1,205 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import UIKit +import CoreDataStack +import MetaTextKit +import MastodonAsset +import MastodonLocalization +import MastodonMeta +import MastodonCore +import MastodonSDK + +public class CondensedUserView: UIView { + private static var metricFormatter = MastodonMetricFormatter() + + private let avatarImageWrapperView: UIView + let avatarImageView: AvatarImageView + + private let metaInformationStackView: UIStackView + + private let upperLineStackView: UIStackView + let displayNameLabel: MetaLabel + let acctLabel: UILabel + + private let lowerLineStackView: UIStackView + let followersLabel: UILabel + let verifiedLinkImageView: UIImageView + let verifiedLinkLabel: MetaLabel + + private let contentStackView: UIStackView + + public override init(frame: CGRect) { + avatarImageView = AvatarImageView() + avatarImageView.cornerConfiguration = AvatarImageView.CornerConfiguration(corner: .fixed(radius: 8)) + avatarImageView.translatesAutoresizingMaskIntoConstraints = false + + avatarImageWrapperView = UIView() + avatarImageWrapperView.translatesAutoresizingMaskIntoConstraints = false + avatarImageWrapperView.addSubview(avatarImageView) + + displayNameLabel = MetaLabel(style: .statusName) + displayNameLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + displayNameLabel.setContentHuggingPriority(.required, for: .horizontal) + + acctLabel = UILabel() + acctLabel.textColor = .secondaryLabel + acctLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) + acctLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + + upperLineStackView = UIStackView(arrangedSubviews: [displayNameLabel, acctLabel]) + upperLineStackView.distribution = .fill + upperLineStackView.alignment = .center + + followersLabel = UILabel() + followersLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + followersLabel.textColor = .secondaryLabel + followersLabel.setContentHuggingPriority(.required, for: .horizontal) + + verifiedLinkImageView = UIImageView() + verifiedLinkImageView.setContentCompressionResistancePriority(.defaultHigh - 1, for: .vertical) + verifiedLinkImageView.setContentHuggingPriority(.required, for: .horizontal) + verifiedLinkImageView.contentMode = .scaleAspectFit + + verifiedLinkLabel = MetaLabel(style: .profileFieldValue) + verifiedLinkLabel.setContentCompressionResistancePriority(.defaultHigh - 2, for: .horizontal) + verifiedLinkLabel.translatesAutoresizingMaskIntoConstraints = false + verifiedLinkLabel.textAttributes = [ + .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)), + .foregroundColor: UIColor.secondaryLabel + ] + verifiedLinkLabel.linkAttributes = [ + .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)), + .foregroundColor: Asset.Colors.Brand.blurple.color + ] + verifiedLinkLabel.isUserInteractionEnabled = false + + lowerLineStackView = UIStackView(arrangedSubviews: [followersLabel, verifiedLinkImageView, verifiedLinkLabel]) + lowerLineStackView.distribution = .fill + lowerLineStackView.alignment = .center + lowerLineStackView.spacing = 4 + lowerLineStackView.setCustomSpacing(2, after: verifiedLinkImageView) + + metaInformationStackView = UIStackView(arrangedSubviews: [upperLineStackView, lowerLineStackView]) + metaInformationStackView.axis = .vertical + metaInformationStackView.alignment = .leading + + contentStackView = UIStackView(arrangedSubviews: [avatarImageWrapperView, metaInformationStackView]) + contentStackView.translatesAutoresizingMaskIntoConstraints = false + contentStackView.axis = .horizontal + contentStackView.alignment = .center + contentStackView.spacing = 16 + + super.init(frame: .zero) + + addSubview(contentStackView) + setupConstraints() + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + private func setupConstraints() { + let constraints = [ + contentStackView.topAnchor.constraint(equalTo: topAnchor, constant: 8), + contentStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + trailingAnchor.constraint(greaterThanOrEqualTo: contentStackView.trailingAnchor, constant: 16), + bottomAnchor.constraint(equalTo: contentStackView.bottomAnchor, constant: 8), + + upperLineStackView.trailingAnchor.constraint(greaterThanOrEqualTo: metaInformationStackView.trailingAnchor), + lowerLineStackView.trailingAnchor.constraint(greaterThanOrEqualTo: metaInformationStackView.trailingAnchor), + metaInformationStackView.trailingAnchor.constraint(greaterThanOrEqualTo: contentStackView.trailingAnchor), + + avatarImageView.widthAnchor.constraint(equalToConstant: 30), + avatarImageView.heightAnchor.constraint(equalTo: avatarImageView.widthAnchor), + avatarImageView.topAnchor.constraint(greaterThanOrEqualTo: avatarImageWrapperView.topAnchor), + avatarImageView.leadingAnchor.constraint(equalTo: avatarImageWrapperView.leadingAnchor), + avatarImageWrapperView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor), + avatarImageWrapperView.bottomAnchor.constraint(greaterThanOrEqualTo: avatarImageView.bottomAnchor), + avatarImageView.centerYAnchor.constraint(equalTo: avatarImageWrapperView.centerYAnchor), + ] + + NSLayoutConstraint.activate(constraints) + } + + public func prepareForReuse() { + avatarImageView.prepareForReuse() + } + + public func configure(with user: MastodonUser) { + let displayNameMetaContent: MetaContent + do { + let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojis.asDictionary) + displayNameMetaContent = try MastodonMetaContent.convert(document: content) + } catch { + displayNameMetaContent = PlaintextMetaContent(string: user.displayNameWithFallback) + } + + displayNameLabel.configure(content: displayNameMetaContent) + acctLabel.text = user.acct + followersLabel.attributedText = NSAttributedString( + format: NSAttributedString(string: L10n.Common.UserList.followersCount("%@"), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))]), + args: NSAttributedString(string: Self.metricFormatter.string(from: Int(user.followersCount)) ?? user.followersCount.formatted(), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))]) + ) + + avatarImageView.setImage(url: user.avatarImageURL()) + + if let verifiedLink = user.verifiedLink?.value { + verifiedLinkImageView.image = UIImage(systemName: "checkmark") + verifiedLinkImageView.tintColor = Asset.Colors.Brand.blurple.color + + let verifiedLinkMetaContent: MetaContent + do { + let mastodonContent = MastodonContent(content: verifiedLink, emojis: [:]) + verifiedLinkMetaContent = try MastodonMetaContent.convert(document: mastodonContent) + } catch { + verifiedLinkMetaContent = PlaintextMetaContent(string: verifiedLink) + } + + verifiedLinkLabel.configure(content: verifiedLinkMetaContent) + } else { + verifiedLinkImageView.image = UIImage(systemName: "questionmark.circle") + verifiedLinkImageView.tintColor = .secondaryLabel + + verifiedLinkLabel.configure(content: PlaintextMetaContent(string: L10n.Common.UserList.noVerifiedLink)) + } + } + + public func configure(with account: Mastodon.Entity.Account) { + let displayNameMetaContent: MetaContent + do { + let content = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojis?.asDictionary ?? [:]) + displayNameMetaContent = try MastodonMetaContent.convert(document: content) + } catch { + displayNameMetaContent = PlaintextMetaContent(string: account.displayNameWithFallback) + } + + displayNameLabel.configure(content: displayNameMetaContent) + acctLabel.text = account.acct + followersLabel.attributedText = NSAttributedString( + format: NSAttributedString(string: L10n.Common.UserList.followersCount("%@"), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))]), + args: NSAttributedString(string: Self.metricFormatter.string(from: account.followersCount) ?? account.followersCount.formatted(), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))]) + ) + + avatarImageView.setImage(url: account.avatarImageURL()) + + if let verifiedLink = account.verifiedLink?.value { + verifiedLinkImageView.image = UIImage(systemName: "checkmark") + verifiedLinkImageView.tintColor = Asset.Colors.Brand.blurple.color + + let verifiedLinkMetaContent: MetaContent + do { + let mastodonContent = MastodonContent(content: verifiedLink, emojis: [:]) + verifiedLinkMetaContent = try MastodonMetaContent.convert(document: mastodonContent) + } catch { + verifiedLinkMetaContent = PlaintextMetaContent(string: verifiedLink) + } + + verifiedLinkLabel.configure(content: verifiedLinkMetaContent) + } else { + verifiedLinkImageView.image = UIImage(systemName: "questionmark.circle") + verifiedLinkImageView.tintColor = .secondaryLabel + + verifiedLinkLabel.configure(content: PlaintextMetaContent(string: L10n.Common.UserList.noVerifiedLink)) + } + } + +} From 3ce8e29244681116e5ebc7a132f04c4ce73b99b9 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 18 Sep 2023 17:32:01 +0200 Subject: [PATCH 24/34] Don't prioritize hashtags or users (IOS-141) --- .../SearchHistoryViewModel+Diffable.swift | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel+Diffable.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel+Diffable.swift index 26f8f572f..a8af8e845 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel+Diffable.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel+Diffable.swift @@ -37,21 +37,18 @@ extension SearchHistoryViewModel { do { let managedObjectContext = self.context.managedObjectContext let items: [SearchHistoryItem] = try await managedObjectContext.perform { - var users: [SearchHistoryItem] = [] - var hashtags: [SearchHistoryItem] = [] - + var items: [SearchHistoryItem] = [] + for record in records { guard let searchHistory = record.object(in: managedObjectContext) else { continue } if let user = searchHistory.account { - users.append(.user(.init(objectID: user.objectID))) + items.append(.user(.init(objectID: user.objectID))) } else if let hashtag = searchHistory.hashtag { - hashtags.append(.hashtag(.init(objectID: hashtag.objectID))) - } else { - continue + items.append(.hashtag(.init(objectID: hashtag.objectID))) } } - return users + hashtags + return items } let mostRecentItems = Array(items.prefix(10)) From ee472c5920feea0ec902b780a2889da0991f624f Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 18 Sep 2023 17:45:02 +0200 Subject: [PATCH 25/34] Fix separator-inset (IOS-141) --- .../SearchResultsOverviewTableViewController.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index 5687562bb..70ae8899b 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -25,16 +25,16 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc tableView = UITableView(frame: .zero, style: .insetGrouped) tableView.translatesAutoresizingMaskIntoConstraints = false tableView.backgroundColor = .systemGroupedBackground + tableView.separatorInset.left = 62 tableView.register(SearchResultDefaultSectionTableViewCell.self, forCellReuseIdentifier: SearchResultDefaultSectionTableViewCell.reuseIdentifier) tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: StatusTableViewCell.reuseIdentifier) tableView.register(HashtagTableViewCell.self, forCellReuseIdentifier: HashtagTableViewCell.reuseIdentifier) tableView.register(SearchResultsProfileTableViewCell.self, forCellReuseIdentifier: SearchResultsProfileTableViewCell.reuseIdentifier) + super.init(nibName: nil, bundle: nil) - let dataSource = UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, itemIdentifier in - - guard let self else { fatalError("Ooops, no self!?") } + let dataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, itemIdentifier in switch itemIdentifier { case .default(let item): From 5cb1280088d4c1a1ffae5aa0ae8a90cf33c38857 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 18 Sep 2023 17:48:26 +0200 Subject: [PATCH 26/34] Fix separator-inset on search-history (IOS-141) --- .../SearchHistory/SearchHistoryViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift index 86340feed..91d51d1c7 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift @@ -22,6 +22,8 @@ final class SearchHistoryViewController: UIViewController, NeedsDependency { let collectionView: UICollectionView = { var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) + configuration.separatorConfiguration.bottomSeparatorInsets.leading = 62 + configuration.separatorConfiguration.topSeparatorInsets.leading = 62 configuration.backgroundColor = .clear configuration.headerMode = .supplementary let layout = UICollectionViewCompositionalLayout.list(using: configuration) From fa6b3fed246995786c8d06d0b3509fb122a9097d Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 18 Sep 2023 17:58:17 +0200 Subject: [PATCH 27/34] Add https if there's no prefix (IOS-141) --- ...earchResultsOverviewTableViewController.swift | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index 70ae8899b..89b8e2e7e 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -282,8 +282,20 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc target: .status, // remove reblog wrapper status: status.asRecord ) - } else if let url = URL(string: link) { - coordinator.present(scene: .safari(url: url), transition: .safariPresent(animated: true)) + } else if var url = URL(string: link) { + let prefixedURL: URL? + if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) { + if components.scheme == nil { + components.scheme = "https" + } + prefixedURL = components.url + } else { + prefixedURL = url + } + + guard let prefixedURL else { return } + + coordinator.present(scene: .safari(url: prefixedURL), transition: .safariPresent(animated: true)) } } } From c1b80a73c248310e30d69bf6ed1b3baeb628f56c Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 18 Sep 2023 21:17:39 +0200 Subject: [PATCH 28/34] Refactor navigation-logic into a coordinator (IOS-141) --- Mastodon.xcodeproj/project.pbxproj | 4 + Mastodon/Coordinator/SceneCoordinator.swift | 2 +- .../SearchResultOverviewCoordinator.swift | 173 ++++++++++++++++++ ...chResultsOverviewTableViewController.swift | 171 +++-------------- .../SearchDetailViewController.swift | 22 ++- ...ySectionHeaderCollectionReusableView.swift | 2 +- .../SearchHistoryViewController.swift | 2 - 7 files changed, 220 insertions(+), 156 deletions(-) create mode 100644 Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index a61ad3f16..20af38cce 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -152,6 +152,7 @@ D8BE30B32A179E26006B8270 /* SuggestionAccountTableViewFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */; }; D8BEBCB62A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */; }; D8D688F62AB869CB000F651A /* SearchResultsProfileTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */; }; + D8D688F92AB8B970000F651A /* SearchResultOverviewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */; }; D8E5C346296DAB84007E76A7 /* DataSourceFacade+Status+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */; }; D8E5C349296DB8A3007E76A7 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */; }; D8F0372C29D232730027DE2E /* HashtagIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */; }; @@ -804,6 +805,7 @@ D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountTableViewFooter.swift; sourceTree = ""; }; D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountTableViewCell+ViewModel.swift"; sourceTree = ""; }; D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsProfileTableViewCell.swift; sourceTree = ""; }; + D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultOverviewCoordinator.swift; sourceTree = ""; }; D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status+History.swift"; sourceTree = ""; }; D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = ""; }; D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagIntentHandler.swift; sourceTree = ""; }; @@ -1803,6 +1805,7 @@ D81A22792AB47B8400905D71 /* Cells */, D81A22742AB4643200905D71 /* SearchResultsOverviewTableViewController.swift */, D81A22772AB4782400905D71 /* SearchResultOverviewSection.swift */, + D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */, ); path = "Search Results Overview"; sourceTree = ""; @@ -3806,6 +3809,7 @@ DB63F77B279ACAE500455B82 /* DataSourceFacade+Favorite.swift in Sources */, DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */, 2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */, + D8D688F92AB8B970000F651A /* SearchResultOverviewCoordinator.swift in Sources */, DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */, 2A3D9B7E29A8F33A00F30313 /* StatusHistoryView.swift in Sources */, D81A227B2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift in Sources */, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 73eda67eb..4455e46d2 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -417,7 +417,7 @@ private extension SceneCoordinator { case .searchDetail(let viewModel): - let _viewController = SearchDetailViewController() + let _viewController = SearchDetailViewController(appContext: appContext, sceneCoordinator: self, authContext: viewModel.authContext) _viewController.viewModel = viewModel viewController = _viewController case .searchResult(let viewModel): diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift new file mode 100644 index 000000000..758b01684 --- /dev/null +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift @@ -0,0 +1,173 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import UIKit +import MastodonCore +import MastodonSDK +import MastodonLocalization + +protocol Coordinator { + func start() +} + +class SearchResultOverviewCoordinator: Coordinator { + + let overviewViewController: SearchResultsOverviewTableViewController + let sceneCoordinator: SceneCoordinator + let context: AppContext + let authContext: AuthContext + + var activeTask: Task? + + init(appContext: AppContext, authContext: AuthContext, sceneCoordinator: SceneCoordinator) { + self.sceneCoordinator = sceneCoordinator + self.context = appContext + self.authContext = authContext + + overviewViewController = SearchResultsOverviewTableViewController(appContext: appContext, authContext: authContext, sceneCoordinator: sceneCoordinator) + } + + func start() { + overviewViewController.delegate = self + } +} + +extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControllerDelegate { + @MainActor + func searchForPosts(_ viewController: SearchResultsOverviewTableViewController, withSearchText searchText: String) { + let searchResultViewModel = SearchResultViewModel(context: context, authContext: authContext, searchScope: .posts) + searchResultViewModel.searchText.value = searchText + + sceneCoordinator.present(scene: .searchResult(viewModel: searchResultViewModel), transition: .show) + } + + func showPosts(_ viewController: SearchResultsOverviewTableViewController, tag: Mastodon.Entity.Tag) { + Task { + await DataSourceFacade.coordinateToHashtagScene( + provider: viewController, + tag: tag + ) + + await DataSourceFacade.responseToCreateSearchHistory(provider: viewController, + item: .hashtag(tag: .entity(tag))) + } + } + + @MainActor + func searchForPeople(_ viewController: SearchResultsOverviewTableViewController, withName searchText: String) { + let searchResultViewModel = SearchResultViewModel(context: context, authContext: authContext, searchScope: .people) + searchResultViewModel.searchText.value = searchText + + sceneCoordinator.present(scene: .searchResult(viewModel: searchResultViewModel), transition: .show) + } + + func goTo(_ viewController: SearchResultsOverviewTableViewController, urlString: String) { + + let query = Mastodon.API.V2.Search.Query( + q: urlString, + type: .default, + resolve: true + ) + + let authContext = self.authContext + let managedObjectContext = context.managedObjectContext + + Task { + let searchResult = try await context.apiService.search( + query: query, + authenticationBox: authContext.mastodonAuthenticationBox + ).value + + if let account = searchResult.accounts.first { + showProfile(viewController, for: account) + } else if let status = searchResult.statuses.first { + + let status = try await managedObjectContext.perform { + return Persistence.Status.fetch(in: managedObjectContext, context: Persistence.Status.PersistContext( + domain: authContext.mastodonAuthenticationBox.domain, + entity: status, + me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user, + statusCache: nil, + userCache: nil, + networkDate: Date())) + } + + guard let status else { return } + + await DataSourceFacade.coordinateToStatusThreadScene( + provider: viewController, + target: .status, // remove reblog wrapper + status: status.asRecord + ) + } else if let url = URL(string: urlString) { + let prefixedURL: URL? + if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) { + if components.scheme == nil { + components.scheme = "https" + } + prefixedURL = components.url + } else { + prefixedURL = url + } + + guard let prefixedURL else { return } + + await sceneCoordinator.present(scene: .safari(url: prefixedURL), transition: .safariPresent(animated: true)) + } + } + } + + func showProfile(_ viewController: SearchResultsOverviewTableViewController, for account: Mastodon.Entity.Account) { + let managedObjectContext = context.managedObjectContext + let domain = authContext.mastodonAuthenticationBox.domain + + Task { + let user = try await managedObjectContext.perform { + return Persistence.MastodonUser.fetch(in: managedObjectContext, + context: Persistence.MastodonUser.PersistContext( + domain: domain, + entity: account, + cache: nil, + networkDate: Date() + )) + } + + if let user { + await DataSourceFacade.coordinateToProfileScene(provider: viewController, + user: user.asRecord) + + await DataSourceFacade.responseToCreateSearchHistory(provider: viewController, + item: .user(record: user.asRecord)) + } + } + } + + func searchForPerson(_ viewController: SearchResultsOverviewTableViewController, username: String, domain: String) { + let acct = "\(username)@\(domain)" + let query = Mastodon.API.V2.Search.Query( + q: acct, + type: .default, + resolve: true + ) + + Task { + let searchResult = try await context.apiService.search( + query: query, + authenticationBox: authContext.mastodonAuthenticationBox + ).value + + if let account = searchResult.accounts.first(where: { $0.acctWithDomainIfMissing(domain).lowercased() == acct.lowercased() }) { + showProfile(viewController, for: account) + } else { + await MainActor.run { + let alertTitle = L10n.Scene.Search.Searching.NoUser.title + let alertMessage = L10n.Scene.Search.Searching.NoUser.message(username, domain) + + let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) + alertController.addAction(okAction) + sceneCoordinator.present(scene: .alertController(alertController: alertController), transition: .alertController(animated: true)) + } + } + } + } +} diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index 89b8e2e7e..726a19b2b 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -5,22 +5,32 @@ import MastodonCore import MastodonSDK import MastodonLocalization -// we could move lots of this stuff to a coordinator, it's too much for work a viewcontroller +protocol SearchResultsOverviewTableViewControllerDelegate: AnyObject { + func goTo(_ viewController: SearchResultsOverviewTableViewController, urlString: String) + func showPosts(_ viewController: SearchResultsOverviewTableViewController, tag: Mastodon.Entity.Tag) + func searchForPosts(_ viewController: SearchResultsOverviewTableViewController, withSearchText searchText: String) + func searchForPeople(_ viewController: SearchResultsOverviewTableViewController, withName searchText: String) + func showProfile(_ viewController: SearchResultsOverviewTableViewController, for account: Mastodon.Entity.Account) + func searchForPerson(_ viewController: SearchResultsOverviewTableViewController, username: String, domain: String) +} + class SearchResultsOverviewTableViewController: UIViewController, NeedsDependency, AuthContextProvider { - var context: AppContext! let authContext: AuthContext + var context: AppContext! var coordinator: SceneCoordinator! private let tableView: UITableView var dataSource: UITableViewDiffableDataSource? + weak var delegate: SearchResultsOverviewTableViewControllerDelegate? + var activeTask: Task? - init(appContext: AppContext, authContext: AuthContext, coordinator: SceneCoordinator) { + init(appContext: AppContext, authContext: AuthContext, sceneCoordinator: SceneCoordinator) { - self.context = appContext self.authContext = authContext - self.coordinator = coordinator + self.context = appContext + self.coordinator = sceneCoordinator tableView = UITableView(frame: .zero, style: .insetGrouped) tableView.translatesAutoresizingMaskIntoConstraints = false @@ -160,145 +170,6 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc activeTask = searchTask } - - //MARK: - Actions - - func showPosts(tag: Mastodon.Entity.Tag) { - Task { - await DataSourceFacade.coordinateToHashtagScene( - provider: self, - tag: tag - ) - - await DataSourceFacade.responseToCreateSearchHistory(provider: self, - item: .hashtag(tag: .entity(tag))) - } - } - - func showProfile(for account: Mastodon.Entity.Account) { - let managedObjectContext = context.managedObjectContext - let domain = authContext.mastodonAuthenticationBox.domain - - Task { - let user = try await managedObjectContext.perform { - return Persistence.MastodonUser.fetch(in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: domain, - entity: account, - cache: nil, - networkDate: Date() - )) - } - - if let user { - await DataSourceFacade.coordinateToProfileScene(provider:self, - user: user.asRecord) - - await DataSourceFacade.responseToCreateSearchHistory(provider: self, - item: .user(record: user.asRecord)) - } - } - } - - func searchForPeople(withName searchText: String) { - let searchResultViewModel = SearchResultViewModel(context: context, authContext: authContext, searchScope: .people) - searchResultViewModel.searchText.value = searchText - - coordinator.present(scene: .searchResult(viewModel: searchResultViewModel), transition: .show) - } - - func searchForPosts(withSearchText searchText: String) { - let searchResultViewModel = SearchResultViewModel(context: context, authContext: authContext, searchScope: .posts) - searchResultViewModel.searchText.value = searchText - - coordinator.present(scene: .searchResult(viewModel: searchResultViewModel), transition: .show) - } - - func searchForPerson(username: String, domain: String) { - let acct = "\(username)@\(domain)" - let query = Mastodon.API.V2.Search.Query( - q: acct, - type: .default, - resolve: true - ) - - Task { - let searchResult = try await context.apiService.search( - query: query, - authenticationBox: authContext.mastodonAuthenticationBox - ).value - - if let account = searchResult.accounts.first(where: { $0.acctWithDomainIfMissing(domain).lowercased() == acct.lowercased() }) { - showProfile(for: account) - } else { - await MainActor.run { - let alertTitle = L10n.Scene.Search.Searching.NoUser.title - let alertMessage = L10n.Scene.Search.Searching.NoUser.message(username, domain) - - let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert) - let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) - alertController.addAction(okAction) - coordinator.present(scene: .alertController(alertController: alertController), transition: .alertController(animated: true)) - } - } - } - } - - func goTo(link: String) { - - let query = Mastodon.API.V2.Search.Query( - q: link, - type: .default, - resolve: true - ) - - let authContext = self.authContext - let managedObjectContext = context.managedObjectContext - - Task { - let searchResult = try await context.apiService.search( - query: query, - authenticationBox: authContext.mastodonAuthenticationBox - ).value - - if let account = searchResult.accounts.first { - showProfile(for: account) - } else if let status = searchResult.statuses.first { - - let status = try await managedObjectContext.perform { - return Persistence.Status.fetch(in: managedObjectContext, context: Persistence.Status.PersistContext( - domain: authContext.mastodonAuthenticationBox.domain, - entity: status, - me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user, - statusCache: nil, - userCache: nil, - networkDate: Date())) - } - - guard let status else { return } - - await DataSourceFacade.coordinateToStatusThreadScene( - provider: self, - target: .status, // remove reblog wrapper - status: status.asRecord - ) - } else if var url = URL(string: link) { - let prefixedURL: URL? - if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) { - if components.scheme == nil { - components.scheme = "https" - } - prefixedURL = components.url - } else { - prefixedURL = url - } - - guard let prefixedURL else { return } - - coordinator.present(scene: .safari(url: prefixedURL), transition: .safariPresent(animated: true)) - } - } - } } //MARK: UITableViewDelegate @@ -313,21 +184,21 @@ extension SearchResultsOverviewTableViewController: UITableViewDelegate { case .default(let defaultSectionEntry): switch defaultSectionEntry { case .posts(let searchText): - searchForPosts(withSearchText: searchText) + delegate?.searchForPosts(self, withSearchText: searchText) case .people(let searchText): - searchForPeople(withName: searchText) + delegate?.searchForPeople(self, withName: searchText) case .profile(let username, let domain): - searchForPerson(username: username, domain: domain) + delegate?.searchForPerson(self, username: username, domain: domain) case .openLink(let urlString): - goTo(link: urlString) + delegate?.goTo(self, urlString: urlString) } case .suggestion(let suggestionSectionEntry): switch suggestionSectionEntry { case .hashtag(let tag): - showPosts(tag: tag) + delegate?.showPosts(self, tag: tag) case .profile(let account): - showProfile(for: account) + delegate?.showProfile(self, for: account) } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift index 990dab66e..82cb9795f 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift @@ -26,6 +26,7 @@ final class SearchDetailViewController: UIViewController, NeedsDependency { var disposeBag = Set() var observations = Set() + let searchResultOverviewCoordinator: SearchResultOverviewCoordinator weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } @@ -82,11 +83,28 @@ final class SearchDetailViewController: UIViewController, NeedsDependency { }() private(set) lazy var searchResultsOverviewViewController: SearchResultsOverviewTableViewController = { - let searchResultsOverviewViewController = SearchResultsOverviewTableViewController(appContext: context, authContext: viewModel.authContext, coordinator: coordinator) - return searchResultsOverviewViewController + return searchResultOverviewCoordinator.overviewViewController }() + //MARK: - init + + init(appContext: AppContext, sceneCoordinator: SceneCoordinator, authContext: AuthContext) { + self.context = appContext + self.coordinator = sceneCoordinator + + self.searchResultOverviewCoordinator = SearchResultOverviewCoordinator(appContext: appContext, authContext: authContext, sceneCoordinator: sceneCoordinator) + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + //MARK: - UIViewController + override func viewDidLoad() { + + searchResultOverviewCoordinator.start() + super.viewDidLoad() setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistorySectionHeaderCollectionReusableView.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistorySectionHeaderCollectionReusableView.swift index ecab554e1..6375f5d29 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistorySectionHeaderCollectionReusableView.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistorySectionHeaderCollectionReusableView.swift @@ -11,7 +11,7 @@ import MastodonAsset import MastodonLocalization import MastodonUI -protocol SearchHistorySectionHeaderCollectionReusableViewDelegate: AnyObject, UserViewDelegate { +protocol SearchHistorySectionHeaderCollectionReusableViewDelegate: AnyObject { func searchHistorySectionHeaderCollectionReusableView(_ searchHistorySectionHeaderCollectionReusableView: SearchHistorySectionHeaderCollectionReusableView, clearButtonDidPressed button: UIButton) } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift index 91d51d1c7..8645da5ac 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift @@ -125,5 +125,3 @@ extension SearchHistoryViewController: SearchHistorySectionHeaderCollectionReusa } } } - -extension SearchHistoryViewController: UserTableViewCellDelegate {} From 68dc99004c1cc47fa5d7047741683ef968cf76da Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 18 Sep 2023 23:10:59 +0200 Subject: [PATCH 29/34] Fix background-color (IOS-141) --- .../Cells/SearchResultDefaultSectionTableViewCell.swift | 8 ++++++++ .../Cells/SearchResultsProfileTableViewCell.swift | 1 + .../Cell/SearchHistoryUserCollectionViewCell.swift | 2 +- .../SearchDetail/SearchHistory/SearchHistorySection.swift | 4 ++-- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultDefaultSectionTableViewCell.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultDefaultSectionTableViewCell.swift index 94bc48dcc..e6cd2b902 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultDefaultSectionTableViewCell.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultDefaultSectionTableViewCell.swift @@ -6,6 +6,14 @@ import MastodonAsset class SearchResultDefaultSectionTableViewCell: UITableViewCell { static let reuseIdentifier = "SearchResultDefaultSectionTableViewCell" + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + backgroundColor = .secondarySystemGroupedBackground + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + func configure(item: SearchResultOverviewItem.DefaultSectionEntry) { var content = UIListContentConfiguration.cell() content.image = item.icon diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultsProfileTableViewCell.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultsProfileTableViewCell.swift index 505e1fa80..303c20aa8 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultsProfileTableViewCell.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/Cells/SearchResultsProfileTableViewCell.swift @@ -17,6 +17,7 @@ class SearchResultsProfileTableViewCell: UITableViewCell { contentView.addSubview(condensedUserView) condensedUserView.pinToParent() + backgroundColor = .secondarySystemGroupedBackground } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift index c17492b2e..db315b1d1 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift @@ -34,7 +34,7 @@ class SearchHistoryUserCollectionViewCell: UICollectionViewCell { if state.isHighlighted || state.isSelected { return ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor } else { - return ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor + return .secondarySystemGroupedBackground } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift index 082e1224f..4269cdda7 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift @@ -48,13 +48,13 @@ extension SearchHistorySection { var backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell() backgroundConfiguration.backgroundColorTransformer = .init { [weak cell] _ in guard let state = cell?.configurationState else { - return ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor + return .secondarySystemGroupedBackground } if state.isHighlighted || state.isSelected { return ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor } - return ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor + return .secondarySystemGroupedBackground } cell.backgroundConfiguration = backgroundConfiguration } From 0c5b3a64cf3c034a75b39bed16e9be3842e7307a Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 20 Sep 2023 16:03:46 +0200 Subject: [PATCH 30/34] Adopt search to follow mastodon-logic (IOS-141) as defined here: https://github.com/mastodon/mastodon/blob/main/app/javascript/mastodon/features/compose/components/search.jsx#L280-L310 --- Localization/app.json | 5 +- .../SearchResultOverviewSection.swift | 17 ++++--- ...chResultsOverviewTableViewController.swift | 48 +++++++++++++------ .../Generated/Strings.swift | 18 ++++--- .../Resources/Base.lproj/Localizable.strings | 7 +-- .../ComposeContent/Helper/MastodonRegex.swift | 5 ++ 6 files changed, 67 insertions(+), 33 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 8a7817d40..b82bcb151 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -651,8 +651,9 @@ } }, "searching": { - "posts": "Posts with \"%@\"", - "people": "People with \"%@\"", + "posts": "Posts matching \"%@\"", + "people": "People matching \"%@\"", + "hashtag": "Go to #%@", "profile": "Go to @%@@%@", "url": "Open Link", "empty_state": { diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift index 6cc4b8942..a9a1438c9 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewSection.swift @@ -15,9 +15,10 @@ enum SearchResultOverviewItem: Hashable { case suggestion(SuggestionSectionEntry) enum DefaultSectionEntry: Hashable { - case posts(String) - case people(String) - case profile(username: String, domain: String) + case showHashtag(hashtag: String) + case posts(matching: String) + case people(matching: String) + case showProfile(username: String, domain: String) case openLink(String) var title: String { @@ -26,23 +27,27 @@ enum SearchResultOverviewItem: Hashable { return L10n.Scene.Search.Searching.posts(text) case .people(let username): return L10n.Scene.Search.Searching.people(username) - case .profile(let username, let instanceName): + case .showProfile(let username, let instanceName): return L10n.Scene.Search.Searching.profile(username, instanceName) case .openLink(_): return L10n.Scene.Search.Searching.url + case .showHashtag(let hashtag): + return L10n.Scene.Search.Searching.hashtag(hashtag) } } var icon: UIImage? { switch self { case .posts(_): - return UIImage(systemName: "number") + return UIImage(systemName: "magnifyingglass") case .people(_): return UIImage(systemName: "person.2") - case .profile(_, _): + case .showProfile(_, _): return UIImage(systemName: "person.crop.circle") case .openLink(_): return UIImage(systemName: "link") + case .showHashtag(_): + return UIImage(systemName: "number") } } } diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index 726a19b2b..2d4d61cef 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -4,6 +4,7 @@ import UIKit import MastodonCore import MastodonSDK import MastodonLocalization +import MastodonUI protocol SearchResultsOverviewTableViewControllerDelegate: AnyObject { func goTo(_ viewController: SearchResultsOverviewTableViewController, urlString: String) @@ -95,26 +96,40 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc var snapshot = dataSource.snapshot() snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .default)) - snapshot.appendItems([.default(.posts(searchText)), - .default(.people(searchText))], toSection: .default) - let components = searchText.split(separator: "@") - if components.count == 2 { - let username = String(components[0]) - let domain = String(components[1]) - if domain.split(separator: ".").count >= 2 { - snapshot.appendItems([.default(.profile(username: username, domain: domain))], toSection: .default) - } else { - snapshot.appendItems([.default(.profile(username: username, domain: authContext.mastodonAuthenticationBox.domain))], toSection: .default) + if searchText.lowercased().starts(with: "https://") && (searchText.contains(" ") == false) { + if URL(string: searchText)?.isValidURL() ?? false { + snapshot.appendItems([.default(.openLink(searchText))], toSection: .default) } - } else { - snapshot.appendItems([.default(.profile(username: searchText, domain: authContext.mastodonAuthenticationBox.domain))], toSection: .default) } - if URL(string: searchText)?.isValidURL() ?? false { - snapshot.appendItems([.default(.openLink(searchText))], toSection: .default) + //TODO: Check for Hashtag-Regex! + if searchText.starts(with: "#") && searchText.length > 1 { + snapshot.appendItems([.default(.showHashtag(hashtag: searchText.replacingOccurrences(of: "#", with: "")))], + toSection: .default) } + if searchText.length > 1, + let usernameRegex = try? NSRegularExpression(pattern: MastodonRegex.Search.username, options: .caseInsensitive), + usernameRegex.firstMatch(in: searchText, range: NSRange(location: 0, length: searchText.length-1)) != nil { + let components = searchText.split(separator: "@") + if components.count == 2 { + let username = String(components[0]).replacingOccurrences(of: "@", with: "") + + let domain = String(components[1]) + if domain.split(separator: ".").count >= 2 { + snapshot.appendItems([.default(.showProfile(username: username, domain: domain))], toSection: .default) + } else { + snapshot.appendItems([.default(.showProfile(username: username, domain: authContext.mastodonAuthenticationBox.domain))], toSection: .default) + } + } else { + snapshot.appendItems([.default(.showProfile(username: searchText, domain: authContext.mastodonAuthenticationBox.domain))], toSection: .default) + } + } + + snapshot.appendItems([.default(.posts(matching: searchText)), + .default(.people(matching: searchText))], toSection: .default) + dataSource.apply(snapshot, animatingDifferences: false) } @@ -187,10 +202,13 @@ extension SearchResultsOverviewTableViewController: UITableViewDelegate { delegate?.searchForPosts(self, withSearchText: searchText) case .people(let searchText): delegate?.searchForPeople(self, withName: searchText) - case .profile(let username, let domain): + case .showProfile(let username, let domain): delegate?.searchForPerson(self, username: username, domain: domain) case .openLink(let urlString): delegate?.goTo(self, urlString: urlString) + case .showHashtag(let hashtagText): + let tag = Mastodon.Entity.Tag(name: hashtagText, url: "") + delegate?.showPosts(self, tag: tag) } case .suggestion(let suggestionSectionEntry): switch suggestionSectionEntry { diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 4484b6e96..3367b4805 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -1279,13 +1279,17 @@ public enum L10n { public static let clear = L10n.tr("Localizable", "Scene.Search.Searching.Clear", fallback: "Clear") /// Clear all public static let clearAll = L10n.tr("Localizable", "Scene.Search.Searching.ClearAll", fallback: "Clear all") - /// People with "%@" - public static func people(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Search.Searching.People", String(describing: p1), fallback: "People with \"%@\"") + /// Go to #%@ + public static func hashtag(_ p1: Any) -> String { + return L10n.tr("Localizable", "Scene.Search.Searching.Hashtag", String(describing: p1), fallback: "Go to #%@") } - /// Posts with "%@" + /// People matching "%@" + public static func people(_ p1: Any) -> String { + return L10n.tr("Localizable", "Scene.Search.Searching.People", String(describing: p1), fallback: "People matching \"%@\"") + } + /// Posts matching "%@" public static func posts(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Search.Searching.Posts", String(describing: p1), fallback: "Posts with \"%@\"") + return L10n.tr("Localizable", "Scene.Search.Searching.Posts", String(describing: p1), fallback: "Posts matching \"%@\"") } /// Go to @%@@%@ public static func profile(_ p1: Any, _ p2: Any) -> String { @@ -1293,8 +1297,8 @@ public enum L10n { } /// Recent searches public static let recentSearch = L10n.tr("Localizable", "Scene.Search.Searching.RecentSearch", fallback: "Recent searches") - /// Open Link - public static let url = L10n.tr("Localizable", "Scene.Search.Searching.Url", fallback: "Open Link") + /// Open URL in Mastodon + public static let url = L10n.tr("Localizable", "Scene.Search.Searching.Url", fallback: "Open URL in Mastodon") public enum EmptyState { /// No results public static let noResults = L10n.tr("Localizable", "Scene.Search.Searching.EmptyState.NoResults", fallback: "No results") diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 0b5102ec0..9ef23c951 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -443,10 +443,11 @@ uploaded to Mastodon."; "Scene.Search.Searching.ClearAll" = "Clear all"; "Scene.Search.Searching.EmptyState.NoResults" = "No results"; "Scene.Search.Searching.RecentSearch" = "Recent searches"; -"Scene.Search.Searching.Posts" = "Posts with \"%@\""; -"Scene.Search.Searching.People" = "People with \"%@\""; +"Scene.Search.Searching.Posts" = "Posts matching \"%@\""; +"Scene.Search.Searching.People" = "People matching \"%@\""; "Scene.Search.Searching.Profile" = "Go to @%@@%@"; -"Scene.Search.Searching.Url" = "Open Link"; +"Scene.Search.Searching.Hashtag" = "Go to #%@"; +"Scene.Search.Searching.Url" = "Open URL in Mastodon"; "Scene.Search.Searching.NoUser.Title" = "No User Account Found"; "Scene.Search.Searching.NoUser.Message" = "There's no Useraccount \"%@\" on %@"; diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Helper/MastodonRegex.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Helper/MastodonRegex.swift index c8c3f498b..0cc925f6d 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Helper/MastodonRegex.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Helper/MastodonRegex.swift @@ -22,4 +22,9 @@ public enum MastodonRegex { /// #… /// :… public static let autoCompletePattern = "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))|(^\\B:|\\s:)([a-zA-Z0-9_]+)" + + public enum Search { + public static let username = "^@?[a-z0-9_-]+(@[\\S]+)?$" + } } + From d9243c25ce90f92ddb6fdd1aabcb373966782615 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 20 Sep 2023 16:04:23 +0200 Subject: [PATCH 31/34] Only search for accounts when searching for a person (IOS-141) --- .../SearchResultOverviewCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift index 758b01684..4dc337b9a 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift @@ -145,7 +145,7 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl let acct = "\(username)@\(domain)" let query = Mastodon.API.V2.Search.Query( q: acct, - type: .default, + type: .accounts, resolve: true ) From e041a7e086324e1e6dd965ddeea7a5e91896d63e Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 20 Sep 2023 16:29:30 +0200 Subject: [PATCH 32/34] Add Hashtag-Regex (IOS-141) --- .../SearchResultsOverviewTableViewController.swift | 4 +++- .../Scene/ComposeContent/Helper/MastodonRegex.swift | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift index 2d4d61cef..db38027bc 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultsOverviewTableViewController.swift @@ -103,10 +103,12 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc } } - //TODO: Check for Hashtag-Regex! if searchText.starts(with: "#") && searchText.length > 1 { snapshot.appendItems([.default(.showHashtag(hashtag: searchText.replacingOccurrences(of: "#", with: "")))], toSection: .default) + } else if searchText.length > 1, let hashtagRegex = try? NSRegularExpression(pattern: MastodonRegex.Search.hashtag, options: .caseInsensitive), hashtagRegex.firstMatch(in: searchText, range: NSRange(location: 0, length: searchText.length-1)) != nil { + snapshot.appendItems([.default(.showHashtag(hashtag: searchText.replacingOccurrences(of: "#", with: "")))], + toSection: .default) } if searchText.length > 1, diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Helper/MastodonRegex.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Helper/MastodonRegex.swift index 0cc925f6d..76df54124 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Helper/MastodonRegex.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Helper/MastodonRegex.swift @@ -25,6 +25,15 @@ public enum MastodonRegex { public enum Search { public static let username = "^@?[a-z0-9_-]+(@[\\S]+)?$" + + /// See: https://github.com/mastodon/mastodon/blob/main/app/javascript/mastodon/utils/hashtags.ts + public static var hashtag: String { + let word = "\\p{L}\\p{M}\\p{N}\\p{Pc}" + let alpha = "\\p{L}\\p{M}" + let hashtag_separators = "_\\u00b7\\u200c" + + return "^(([\(word)_][\(word)\(hashtag_separators)]*[\(alpha)\(hashtag_separators)][\(word)\(hashtag_separators)]*[\(word)_])|([\(word)_]*[\(alpha)][\(word)_]*))$" + } } } From a0f7454a3d4ccde89d10cbbe27eeed9e0a1a1eea Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 20 Sep 2023 18:47:35 +0200 Subject: [PATCH 33/34] Show loading-indicator (IOS-141) aaaaand simplify things as we don't need a super-dynamic search-result-screen anymore. --- .../SearchResultOverviewCoordinator.swift | 6 +- .../SearchResult/SearchResultSection.swift | 2 - ...ultViewController+DataSourceProvider.swift | 2 + .../SearchResultViewController.swift | 85 +------------------ .../SearchResultViewModel+Diffable.swift | 6 +- .../SearchResultViewModel+State.swift | 35 ++++---- .../SearchResult/SearchResultViewModel.swift | 9 +- 7 files changed, 31 insertions(+), 114 deletions(-) diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift index 4dc337b9a..a2d6c8e24 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift @@ -34,8 +34,7 @@ class SearchResultOverviewCoordinator: Coordinator { extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControllerDelegate { @MainActor func searchForPosts(_ viewController: SearchResultsOverviewTableViewController, withSearchText searchText: String) { - let searchResultViewModel = SearchResultViewModel(context: context, authContext: authContext, searchScope: .posts) - searchResultViewModel.searchText.value = searchText + let searchResultViewModel = SearchResultViewModel(context: context, authContext: authContext, searchScope: .posts, searchText: searchText) sceneCoordinator.present(scene: .searchResult(viewModel: searchResultViewModel), transition: .show) } @@ -54,8 +53,7 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl @MainActor func searchForPeople(_ viewController: SearchResultsOverviewTableViewController, withName searchText: String) { - let searchResultViewModel = SearchResultViewModel(context: context, authContext: authContext, searchScope: .people) - searchResultViewModel.searchText.value = searchText + let searchResultViewModel = SearchResultViewModel(context: context, authContext: authContext, searchScope: .people, searchText: searchText) sceneCoordinator.present(scene: .searchResult(viewModel: searchResultViewModel), transition: .show) } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift index a7b10ae1f..dbede1795 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift @@ -23,8 +23,6 @@ enum SearchResultSection: Hashable { extension SearchResultSection { - static let logger = Logger(subsystem: "SearchResultSection", category: "logic") - struct Configuration { let authContext: AuthContext weak var statusViewTableViewCellDelegate: StatusTableViewCellDelegate? diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift index a2f41aefb..f2e8c7c6e 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift @@ -71,6 +71,8 @@ extension SearchResultViewController { case .notification: assertionFailure() } // end switch + + tableView.deselectRow(at: indexPath, animated: true) } // end Task } // end func } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift index 731117ec6..fa8b844a6 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift @@ -11,6 +11,7 @@ import Combine import CoreDataStack import MastodonCore import MastodonUI +import MastodonAsset final class SearchResultViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { @@ -37,14 +38,7 @@ extension SearchResultViewController { override func viewDidLoad() { super.viewDidLoad() - setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) - ThemeService.shared.currentTheme - .receive(on: DispatchQueue.main) - .sink { [weak self] theme in - guard let self = self else { return } - self.setupBackgroundColor(theme: theme) - } - .store(in: &disposeBag) + view.backgroundColor = Asset.Theme.System.systemGroupedBackground.color tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) @@ -68,85 +62,14 @@ extension SearchResultViewController { } .store(in: &disposeBag) - // listen keyboard events and set content inset - let keyboardEventPublishers = Publishers.CombineLatest3( - KeyboardResponderService.shared.isShow, - KeyboardResponderService.shared.state, - KeyboardResponderService.shared.endFrame - ) - Publishers.CombineLatest3( - keyboardEventPublishers, - viewModel.viewDidAppear, - viewModel.didDataSourceUpdate - ) - .sink(receiveValue: { [weak self] keyboardEvents, _, _ in - guard let self = self else { return } - let (isShow, state, endFrame) = keyboardEvents - - // update keyboard background color - guard isShow, state == .dock else { - self.tableView.contentInset.bottom = 0 - self.tableView.verticalScrollIndicatorInsets.bottom = 0 - return - } - // isShow AND dock state - - // adjust inset for tableView - let contentFrame = self.view.convert(self.tableView.frame, to: nil) - let padding = contentFrame.maxY - endFrame.minY - guard padding > 0 else { - self.tableView.contentInset.bottom = self.view.safeAreaInsets.bottom - self.tableView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom - return - } - - self.tableView.contentInset.bottom = padding - self.view.safeAreaInsets.bottom - self.tableView.verticalScrollIndicatorInsets.bottom = padding - self.view.safeAreaInsets.bottom - }) - .store(in: &disposeBag) -// - // works for already onscreen page - viewModel.navigationBarFrame - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak self] frame in - guard let self = self else { return } - guard self.viewModel.viewDidAppear.value else { return } - self.tableView.contentInset.top = frame.height - self.tableView.verticalScrollIndicatorInsets.top = frame.height - } - .store(in: &disposeBag) - - title = viewModel.searchText.value - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - // works for appearing page - if !viewModel.viewDidAppear.value { - tableView.contentInset.top = viewModel.navigationBarFrame.value.height - tableView.contentOffset.y = -viewModel.navigationBarFrame.value.height - } - - tableView.deselectRow(with: transitionCoordinator, animated: animated) + title = viewModel.searchText } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - viewModel.viewDidAppear.value = true + viewModel.stateMachine.enter(SearchResultViewModel.State.Initial.self) } - -} - -extension SearchResultViewController { - private func setupBackgroundColor(theme: Theme) { - view.backgroundColor = theme.systemGroupedBackgroundColor -// tableView.backgroundColor = theme.systemBackgroundColor -// searchHeader.backgroundColor = theme.systemGroupedBackgroundColor - } - } // MARK: - StatusTableViewCellDelegate diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+Diffable.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+Diffable.swift index 225f79b02..5b74ba8aa 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+Diffable.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+Diffable.swift @@ -65,8 +65,7 @@ extension SearchResultViewModel { if let currentState = self.stateMachine.currentState { switch currentState { case is State.Loading, - is State.Fail, - is State.Idle: + is State.Fail: let attribute = SearchResultItem.BottomLoaderAttribute(isEmptyResult: false) snapshot.appendItems([.bottomLoader(attribute: attribute)], toSection: .main) case is State.NoMore: @@ -74,6 +73,9 @@ extension SearchResultViewModel { let attribute = SearchResultItem.BottomLoaderAttribute(isEmptyResult: true) snapshot.appendItems([.bottomLoader(attribute: attribute)], toSection: .main) } + case is State.Idle: + // do nothing + break default: break } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift index 47832e156..4858f0d2d 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift @@ -22,7 +22,7 @@ extension SearchResultViewModel { } @MainActor - func enter(state: State.Type) { + public func enter(state: State.Type) { stateMachine?.enter(state) } } @@ -31,24 +31,27 @@ extension SearchResultViewModel { extension SearchResultViewModel.State { class Initial: SearchResultViewModel.State { override func isValidNextState(_ stateClass: AnyClass) -> Bool { - guard let viewModel = viewModel else { return false } - return stateClass == Loading.self && !viewModel.searchText.value.isEmpty + return stateClass == Loading.self + } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + + guard let viewModel else { return } + + viewModel.items = [.bottomLoader(attribute: .init(isEmptyResult: false))] } } class Loading: SearchResultViewModel.State { - var previousSearchText = "" var offset: Int? = nil var latestLoadingToken = UUID() override func isValidNextState(_ stateClass: AnyClass) -> Bool { - guard let viewModel = self.viewModel else { return false } switch stateClass { case is Fail.Type, is Idle.Type, is NoMore.Type: return true - case is Loading.Type: - return viewModel.searchText.value != previousSearchText default: return false } @@ -56,12 +59,11 @@ extension SearchResultViewModel.State { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + guard let viewModel, let stateMachine = stateMachine else { return } - let searchText = viewModel.searchText.value let searchType = viewModel.searchScope.searchType - if previousState is NoMore && previousSearchText == searchText { + if previousState is NoMore { // same searchText from NoMore // break the loading and resume NoMore state stateMachine.enter(NoMore.self) @@ -71,17 +73,12 @@ extension SearchResultViewModel.State { // viewModel.items.value = viewModel.items.value } - guard !searchText.isEmpty else { + guard viewModel.searchText.isEmpty == false else { stateMachine.enter(Fail.self) return } - if searchText != previousSearchText { - previousSearchText = searchText - offset = nil - } else { - offset = viewModel.items.count - } + offset = viewModel.items.count // not set offset for all case // and assert other cases the items are all the same type elements @@ -93,7 +90,7 @@ extension SearchResultViewModel.State { }() let query = Mastodon.API.V2.Search.Query( - q: searchText, + q: viewModel.searchText, type: searchType, accountID: nil, maxID: nil, @@ -115,8 +112,6 @@ extension SearchResultViewModel.State { authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) - // discard result when search text is outdated - guard searchText == self.previousSearchText else { return } // discard result when request not the latest one guard id == self.latestLoadingToken else { return } // discard result when state is not Loading diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift index 0c5c5868f..f66921535 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift @@ -21,13 +21,12 @@ final class SearchResultViewModel { let context: AppContext let authContext: AuthContext let searchScope: SearchScope - let searchText = CurrentValueSubject("") + let searchText: String @Published var hashtags: [Mastodon.Entity.Tag] = [] let userFetchedResultsController: UserFetchedResultsController let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() - let viewDidAppear = CurrentValueSubject(false) var cellFrameCache = NSCache() var navigationBarFrame = CurrentValueSubject(.zero) @@ -43,15 +42,16 @@ final class SearchResultViewModel { State.Idle(viewModel: self), State.NoMore(viewModel: self), ]) - stateMachine.enter(State.Initial.self) return stateMachine }() let didDataSourceUpdate = PassthroughSubject() - init(context: AppContext, authContext: AuthContext, searchScope: SearchScope = .all) { + init(context: AppContext, authContext: AuthContext, searchScope: SearchScope = .all, searchText: String) { self.context = context self.authContext = authContext self.searchScope = searchScope + self.searchText = searchText + self.userFetchedResultsController = UserFetchedResultsController( managedObjectContext: context.managedObjectContext, domain: authContext.mastodonAuthenticationBox.domain, @@ -63,5 +63,4 @@ final class SearchResultViewModel { additionalTweetPredicate: nil ) } - } From a7a387dad5cf4c939990fafacd5d1e626dd1cabe Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 20 Sep 2023 18:56:34 +0200 Subject: [PATCH 34/34] Add a typealias for `NeedsDependency & UIViewController` (IOS-141) Well, okay, `ViewControllerWithDependencies` not super creative. --- Mastodon/Coordinator/NeedsDependency.swift | 2 ++ Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift | 2 +- Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift | 2 +- .../Protocol/Provider/DataSourceFacade+SearchHistory.swift | 2 +- Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift | 4 ++-- Mastodon/Protocol/Provider/DataSourceProvider.swift | 2 +- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Mastodon/Coordinator/NeedsDependency.swift b/Mastodon/Coordinator/NeedsDependency.swift index 46fbcd1ff..be22fc8cd 100644 --- a/Mastodon/Coordinator/NeedsDependency.swift +++ b/Mastodon/Coordinator/NeedsDependency.swift @@ -14,6 +14,8 @@ protocol NeedsDependency: AnyObject { var coordinator: SceneCoordinator! { get set } } +typealias ViewControllerWithDependencies = NeedsDependency & UIViewController + extension UISceneSession { private struct AssociatedKeys { static var sceneCoordinator = "SceneCoordinator" diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift index 9b0001351..2422adf6c 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift @@ -26,7 +26,7 @@ extension DataSourceFacade { @MainActor static func coordinateToHashtagScene( - provider: NeedsDependency & UIViewController & AuthContextProvider, + provider: ViewControllerWithDependencies & AuthContextProvider, tag: Mastodon.Entity.Tag ) async { let hashtagTimelineViewModel = HashtagTimelineViewModel( diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index 219e7ddef..8f77a1888 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -33,7 +33,7 @@ extension DataSourceFacade { @MainActor static func coordinateToProfileScene( - provider: NeedsDependency & UIViewController & AuthContextProvider, + provider: ViewControllerWithDependencies & AuthContextProvider, user: ManagedObjectRecord ) async { guard let user = user.object(in: provider.context.managedObjectContext) else { diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift index 1193bb676..48d6b7f03 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift @@ -13,7 +13,7 @@ import UIKit extension DataSourceFacade { static func responseToCreateSearchHistory( - provider: NeedsDependency & UIViewController & AuthContextProvider, + provider: ViewControllerWithDependencies & AuthContextProvider, item: DataSourceItem ) async { switch item { diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift index e2431fc28..ad8d0e671 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift @@ -12,7 +12,7 @@ import MastodonCore extension DataSourceFacade { static func coordinateToStatusThreadScene( - provider: NeedsDependency & UIViewController & AuthContextProvider, + provider: ViewControllerWithDependencies & AuthContextProvider, target: StatusTarget, status: ManagedObjectRecord ) async { @@ -40,7 +40,7 @@ extension DataSourceFacade { @MainActor static func coordinateToStatusThreadScene( - provider: NeedsDependency & UIViewController & AuthContextProvider, + provider: ViewControllerWithDependencies & AuthContextProvider, root: StatusItem.Thread ) async { let threadViewModel = ThreadViewModel( diff --git a/Mastodon/Protocol/Provider/DataSourceProvider.swift b/Mastodon/Protocol/Provider/DataSourceProvider.swift index d7c86f56d..e4287b6b9 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider.swift @@ -44,6 +44,6 @@ extension DataSourceItem { } } -protocol DataSourceProvider: NeedsDependency & UIViewController { +protocol DataSourceProvider: ViewControllerWithDependencies { func item(from source: DataSourceItem.Source) async -> DataSourceItem? }