diff --git a/WordPress/Classes/Utility/WPContentSearchHelper.swift b/WordPress/Classes/Utility/WPContentSearchHelper.swift deleted file mode 100644 index fb5f80dfeec2..000000000000 --- a/WordPress/Classes/Utility/WPContentSearchHelper.swift +++ /dev/null @@ -1,82 +0,0 @@ -import UIKit - -/// WPContentSearchHelper is a helper class to encapsulate searches over a time interval. -/// -/// The helper is configured with an interval and callback to call once the time elapses. -/// The helper automatically cancels pending callbacks when a new text update is incurred. -class WPContentSearchHelper: NSObject { - - /// The current searchText set on the helper. - @objc var searchText: String? = nil - - // MARK: - Methods for configuring the timing of search callbacks. - - fileprivate var observers = [WPContentSearchObserver]() - fileprivate let defaultDeferredSearchObservationInterval = TimeInterval(0.30) - - @objc func configureImmediateSearch(_ handler: @escaping ()->Void) { - let observer = WPContentSearchObserver() - observer.interval = 0.0 - observer.completion = handler - observers.append(observer) - } - - /// Add a search callback configured as a common deferred search. - @objc func configureDeferredSearch(_ handler: @escaping ()->Void) { - let observer = WPContentSearchObserver() - observer.interval = defaultDeferredSearchObservationInterval - observer.completion = handler - observers.append(observer) - } - - /// Remove any current configuration, such as local and remote search callbacks. - @objc func resetConfiguration() { - stopAllObservers() - observers.removeAll() - } - - // MARK: - Methods for updating the search. - - /// Update the current search text, ideally in real-time along with user input. - @objc func searchUpdated(_ text: String?) { - stopAllObservers() - searchText = text - for observer in observers { - let timer = Timer.init(timeInterval: observer.interval, - target: observer, - selector: #selector(WPContentSearchObserver.timerFired), - userInfo: nil, - repeats: false) - RunLoop.main.add(timer, forMode: RunLoop.Mode.common) - } - } - - /// Cancel the current search and any pending callbacks. - @objc func searchCanceled() { - stopAllObservers() - } - - // MARK: - Private Methods - - /// Stop the observers from firing. - fileprivate func stopAllObservers() { - for observer in observers { - observer.timer?.invalidate() - observer.timer = nil - } - } -} - -// MARK: - Private Classes - -/// Object encapsulating the callback and timing information. -private class WPContentSearchObserver: NSObject { - - @objc var interval = TimeInterval(0.0) - @objc var timer: Timer? - @objc var completion = {} - - @objc func timerFired() { - completion() - } -} diff --git a/WordPress/Classes/ViewRelated/Pages/PageListViewController.swift b/WordPress/Classes/ViewRelated/Pages/PageListViewController.swift index 9640cfcee357..034f40622fcf 100644 --- a/WordPress/Classes/ViewRelated/Pages/PageListViewController.swift +++ b/WordPress/Classes/ViewRelated/Pages/PageListViewController.swift @@ -276,74 +276,6 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe _tableViewHandler.refreshTableView() } - override func syncPostsMatchingSearchText() { - guard let searchText = searchController.searchBar.text, !searchText.isEmpty() else { - return - } - - postsSyncWithSearchDidBegin() - - let author = filterSettings.shouldShowOnlyMyPosts() ? blogUserID() : nil - let postService = PostService(managedObjectContext: managedObjectContext()) - let options = PostServiceSyncOptions() - options.statuses = filterSettings.availablePostListFilters().flatMap { $0.statuses.strings } - options.authorID = author - options.number = 20 - options.purgesLocalSync = false - options.search = searchText - - postService.syncPosts( - ofType: postTypeToSync(), - with: options, - for: blog, - success: { [weak self] posts in - self?.postsSyncWithSearchEnded() - }, failure: { [weak self] (error) in - self?.postsSyncWithSearchEnded() - } - ) - } - - override func sortDescriptorsForFetchRequest() -> [NSSortDescriptor] { - if !searchController.isActive { - return super.sortDescriptorsForFetchRequest() - } - - let descriptor = NSSortDescriptor(key: BasePost.statusKeyPath, ascending: true) - return [descriptor] - } - - override func updateForLocalPostsMatchingSearchText() { - guard searchController.isActive else { - hideNoResultsView() - return - } - - _tableViewHandler.isSearching = true - updateAndPerformFetchRequest() - tableView.reloadData() - - hideNoResultsView() - - if let text = searchController.searchBar.text, - text.isEmpty || - tableViewHandler.resultsController?.fetchedObjects?.count == 0 { - showNoResultsView() - } - } - - override func showNoResultsView() { - super.showNoResultsView() - - if searchController.isActive { - noResultsViewController.view.frame = CGRect(x: 0.0, - y: searchController.searchBar.bounds.height, - width: tableView.frame.width, - height: max(tableView.frame.height, tableView.contentSize.height)) - tableView.bringSubviewToFront(noResultsViewController.view) - } - } - override func syncContentEnded(_ syncHelper: WPContentSyncHelper) { guard syncHelper.hasMoreContent else { super.syncContentEnded(syncHelper) @@ -383,12 +315,11 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe predicates.append(basePredicate) } - let searchText = currentSearchTerm() ?? "" - let filterPredicate = searchController.isActive ? NSPredicate(format: "postTitle CONTAINS[cd] %@", searchText) : filterSettings.currentPostListFilter().predicateForFetchRequest + let filterPredicate = filterSettings.currentPostListFilter().predicateForFetchRequest // If we have recently trashed posts, create an OR predicate to find posts matching the filter, // or posts that were recently deleted. - if searchText.count == 0 && recentlyTrashedPostObjectIDs.count > 0 { + if recentlyTrashedPostObjectIDs.count > 0 { let trashedPredicate = NSPredicate(format: "SELF IN %@", recentlyTrashedPostObjectIDs) @@ -397,18 +328,8 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe predicates.append(filterPredicate) } - if searchText.count > 0 { - let searchPredicate = NSPredicate(format: "postTitle CONTAINS[cd] %@", searchText) - predicates.append(searchPredicate) - } - - if RemoteFeatureFlag.siteEditorMVP.enabled(), - blog.blockEditorSettings?.isFSETheme ?? false, - let homepageID = blog.homepagePageID, - let homepageType = blog.homepageType, - homepageType == .page { - let homepagePredicate = NSPredicate(format: "postID != %i", homepageID) - predicates.append(homepagePredicate) + if let predicate = PostSearchViewModel.makeHomepagePredicate(for: blog) { + predicates.append(predicate) } let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) @@ -423,29 +344,6 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe return Page.sectionIdentifier(dateKeyPath: sortField.keyPath) } - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - guard _tableViewHandler.groupResults else { - return 0.0 - } - return Constant.Size.pageSectionHeaderHeight - } - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - guard _tableViewHandler.groupResults else { - return UIView(frame: .zero) - } - - let sectionInfo = _tableViewHandler.resultsController.sections?[section] - let nibName = String(describing: PageListSectionHeaderView.self) - let headerView = Bundle.main.loadNibNamed(nibName, owner: nil, options: nil)?.first as? PageListSectionHeaderView - - if let sectionInfo = sectionInfo, let headerView = headerView { - headerView.setTitle(PostSearchHeader.title(forStatus: sectionInfo.name)) - } - - return headerView - } - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) @@ -497,7 +395,7 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe let filterType = filterSettings.currentPostListFilter().filterType if cell.reuseIdentifier == Constant.Identifiers.pageCellIdentifier { - cell.indentationWidth = _tableViewHandler.isSearching ? 0.0 : Constant.Size.pageListTableViewCellLeading + cell.indentationWidth = Constant.Size.pageListTableViewCellLeading cell.indentationLevel = filterType != .published ? 0 : page.hierarchyIndex cell.onAction = { [weak self] cell, button, page in self?.handleMenuAction(fromCell: cell, fromButton: button, forPage: page) @@ -855,7 +753,6 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe let newIndex = _tableViewHandler.index(for: selectedPage) let pages = _tableViewHandler.removePage(from: newIndex) let parentPageNavigationController = ParentPageSettingsViewController.navigationController(with: pages, selectedPage: selectedPage, onClose: { [weak self] in - self?._tableViewHandler.isSearching = false self?._tableViewHandler.refreshTableView(at: index) }, onSuccess: { [weak self] in self?.handleSetParentSuccess() @@ -1006,30 +903,6 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe alertController.presentFromRootViewController() } - // MARK: - UISearchControllerDelegate - - override func updateSearchResults(for searchController: UISearchController) { - super.updateSearchResults(for: searchController) - } - - override func willDismissSearchController(_ searchController: UISearchController) { - _tableViewHandler.isSearching = false - _tableViewHandler.refreshTableView() - super.willDismissSearchController(searchController) - } - - func didPresentSearchController(_ searchController: UISearchController) { - tableView.verticalScrollIndicatorInsets.top = searchController.searchBar.bounds.height + searchController.searchBar.frame.origin.y - view.safeAreaInsets.top - } - - func didDismissSearchController(_ searchController: UISearchController) { - self.hideNoResultsView() - } - - enum Animations { - static let searchDismissDuration: TimeInterval = 0.3 - } - // MARK: - NetworkAwareUI override func noConnectionMessage() -> String { @@ -1056,20 +929,12 @@ private extension PageListViewController { return } - if searchController.isActive { - if currentSearchTerm()?.count == 0 { - noResultsViewController.configureForNoSearchResults(title: NoResultsText.searchPages) - } else { - noResultsViewController.configureForNoSearchResults(title: noResultsTitle()) - } - } else { - let accessoryView = syncHelper.isSyncing ? NoResultsViewController.loadingAccessoryView() : nil + let accessoryView = syncHelper.isSyncing ? NoResultsViewController.loadingAccessoryView() : nil - noResultsViewController.configure(title: noResultsTitle(), - buttonTitle: noResultsButtonTitle(), - image: noResultsImageName, - accessoryView: accessoryView) - } + noResultsViewController.configure(title: noResultsTitle(), + buttonTitle: noResultsButtonTitle(), + image: noResultsImageName, + accessoryView: accessoryView) } var noResultsImageName: String { @@ -1077,7 +942,7 @@ private extension PageListViewController { } func noResultsButtonTitle() -> String? { - if syncHelper.isSyncing == true || isSearching() { + if syncHelper.isSyncing == true { return nil } @@ -1089,11 +954,6 @@ private extension PageListViewController { if syncHelper.isSyncing == true { return NoResultsText.fetchingTitle } - - if isSearching() { - return NoResultsText.noMatchesTitle - } - return noResultsFilteredTitle() } @@ -1116,12 +976,10 @@ private extension PageListViewController { struct NoResultsText { static let buttonTitle = NSLocalizedString("Create Page", comment: "Button title, encourages users to create their first page on their blog.") static let fetchingTitle = NSLocalizedString("Fetching pages...", comment: "A brief prompt shown when the reader is empty, letting the user know the app is currently fetching new pages.") - static let noMatchesTitle = NSLocalizedString("No pages matching your search", comment: "Displayed when the user is searching the pages list and there are no matching pages") static let noDraftsTitle = NSLocalizedString("You don't have any draft pages", comment: "Displayed when the user views drafts in the pages list and there are no pages") static let noScheduledTitle = NSLocalizedString("You don't have any scheduled pages", comment: "Displayed when the user views scheduled pages in the pages list and there are no pages") static let noTrashedTitle = NSLocalizedString("You don't have any trashed pages", comment: "Displayed when the user views trashed in the pages list and there are no pages") static let noPublishedTitle = NSLocalizedString("You haven't published any pages yet", comment: "Displayed when the user views published pages in the pages list and there are no pages") - static let searchPages = NSLocalizedString("Search pages", comment: "Text displayed when the search controller will be presented") static let noConnectionTitle: String = NSLocalizedString("Unable to load pages right now.", comment: "Title for No results full page screen displayedfrom pages list when there is no connection") static let noConnectionSubtitle: String = NSLocalizedString("Check your network connection and try again. Or draft a page.", comment: "Subtitle for No results full page screen displayed from pages list when there is no connection") } diff --git a/WordPress/Classes/ViewRelated/Post/AbstractPostListViewController.swift b/WordPress/Classes/ViewRelated/Post/AbstractPostListViewController.swift index 6f4693e4525a..4af4d5a54208 100644 --- a/WordPress/Classes/ViewRelated/Post/AbstractPostListViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/AbstractPostListViewController.swift @@ -8,7 +8,6 @@ import WordPressFlux class AbstractPostListViewController: UIViewController, WPContentSyncHelperDelegate, UISearchControllerDelegate, - UISearchResultsUpdating, WPTableViewHandlerDelegate, NetworkAwareUI // This protocol is not in an extension so that subclasses can override noConnectionMessage() { @@ -78,11 +77,6 @@ class AbstractPostListViewController: UIViewController, return syncHelper }() - @objc lazy var searchHelper: WPContentSearchHelper = { - let searchHelper = WPContentSearchHelper() - return searchHelper - }() - @objc lazy var noResultsViewController: NoResultsViewController = { let noResultsViewController = NoResultsViewController.controller() noResultsViewController.delegate = self @@ -99,12 +93,11 @@ class AbstractPostListViewController: UIViewController, let filterTabBar = FilterTabBar() - private let searchResultsViewController = PostSearchViewController() + private lazy var searchResultsViewController = PostSearchViewController(viewModel: PostSearchViewModel(blog: blog, postType: postTypeToSync())) - @objc var searchController: UISearchController! - @objc var recentlyTrashedPostObjectIDs = [NSManagedObjectID]() // IDs of trashed posts. Cleared on refresh or when filter changes. + private lazy var searchController = UISearchController(searchResultsController: searchResultsViewController) - fileprivate var searchesSyncing = 0 + @objc var recentlyTrashedPostObjectIDs = [NSManagedObjectID]() // IDs of trashed posts. Cleared on refresh or when filter changes. private var emptyResults: Bool { return tableViewHandler.resultsController?.fetchedObjects?.count == 0 @@ -135,7 +128,6 @@ class AbstractPostListViewController: UIViewController, configureWindowlessCell() configureNavbar() configureSearchController() - configureSearchHelper() configureAuthorFilter() configureGhostableTableView() configureNavigationBarAppearance() @@ -174,10 +166,6 @@ class AbstractPostListViewController: UIViewController, override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - if searchController?.isActive == true { - searchController?.isActive = false - } - dismissAllNetworkErrorNotices() NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) @@ -197,6 +185,7 @@ class AbstractPostListViewController: UIViewController, tableViewController.didMove(toParent: self) tableView.backgroundColor = .white + tableView.sectionHeaderTopPadding = 0 tableView.refreshControl = refreshControl refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged) } @@ -268,9 +257,8 @@ class AbstractPostListViewController: UIViewController, } private func configureSearchController() { - searchController = UISearchController(searchResultsController: searchResultsViewController) searchController.delegate = self - searchController.searchResultsUpdater = self + searchController.searchResultsUpdater = searchResultsViewController searchController.showsSearchResultsController = true definesPresentationContext = true @@ -307,16 +295,6 @@ class AbstractPostListViewController: UIViewController, ghostableTableView.isScrollEnabled = false } - @objc func configureSearchHelper() { - searchHelper.resetConfiguration() - searchHelper.configureImmediateSearch({ [weak self] in - self?.updateForLocalPostsMatchingSearchText() - }) - searchHelper.configureDeferredSearch({ [weak self] in - self?.syncPostsMatchingSearchText() - }) - } - @objc func propertiesForAnalytics() -> [String: AnyObject] { var properties = [String: AnyObject]() @@ -412,10 +390,9 @@ class AbstractPostListViewController: UIViewController, return } - // Set the predicate based on filtering by the oldestPostDate and not searching. let filter = filterSettings.currentPostListFilter() - if let oldestPostDate = filter.oldestPostDate, !isSearching() { + if let oldestPostDate = filter.oldestPostDate { // Filter posts by any posts newer than the filter's oldestPostDate. // Also include any posts that don't have a date set, such as local posts created without a connection. @@ -424,12 +401,12 @@ class AbstractPostListViewController: UIViewController, predicate = NSCompoundPredicate.init(andPredicateWithSubpredicates: [predicate, datePredicate]) } - // Set up the fetchLimit based on filtering or searching - if filter.oldestPostDate != nil || isSearching() == true { - // If filtering by the oldestPostDate or searching, the fetchLimit should be disabled. + // Set up the fetchLimit based on filtering + if filter.oldestPostDate != nil { + // If filtering by the oldestPostDate, the fetchLimit should be disabled. fetchRequest.fetchLimit = 0 } else { - // If not filtering by the oldestPostDate or searching, set the fetchLimit to the default number of posts. + // If not filtering by the oldestPostDate, set the fetchLimit to the default number of posts. fetchRequest.fetchLimit = fetchLimit } @@ -489,7 +466,7 @@ class AbstractPostListViewController: UIViewController, // See estimatedHeightForRowAtIndexPath estimatedHeightsCache.setObject(cell.frame.height as AnyObject, forKey: indexPath as AnyObject) - guard isViewOnScreen() && !isSearching() else { + guard isViewOnScreen() else { return } @@ -612,12 +589,6 @@ class AbstractPostListViewController: UIViewController, } success?(filter.hasMore) - - if strongSelf.isSearching() { - // If we're currently searching, go ahead and request a sync with the searchText since - // an action was triggered to syncContent. - strongSelf.syncPostsMatchingSearchText() - } }, failure: {[weak self] (error: Error?) -> () in guard let strongSelf = self, @@ -771,82 +742,6 @@ class AbstractPostListViewController: UIViewController, stopGhost() } - // MARK: - Searching - - @objc func isSearching() -> Bool { - return searchController.isActive && !(currentSearchTerm() ?? "").isEmpty - } - - @objc func currentSearchTerm() -> String? { - return searchController.searchBar.text - } - - @objc func updateForLocalPostsMatchingSearchText() { - updateAndPerformFetchRequest() - tableView.reloadData() - - let filter = filterSettings.currentPostListFilter() - if filter.hasMore && emptyResults { - // If the filter detects there are more posts, but there are none that match the current search - // hide the no results view while the upcoming syncPostsMatchingSearchText() may in fact load results. - hideNoResultsView() - postListFooterView.isHidden = true - } else { - refreshResults() - } - } - - @objc func isSyncingPostsWithSearch() -> Bool { - return searchesSyncing > 0 - } - - @objc func postsSyncWithSearchDidBegin() { - searchesSyncing += 1 - postListFooterView.showSpinner(true) - postListFooterView.isHidden = false - } - - @objc func postsSyncWithSearchEnded() { - searchesSyncing -= 1 - assert(searchesSyncing >= 0, "Expected Int searchesSyncing to be 0 or greater while searching.") - if !isSyncingPostsWithSearch() { - postListFooterView.showSpinner(false) - refreshResults() - } - } - - @objc func syncPostsMatchingSearchText() { - guard let searchText = searchController.searchBar.text, !searchText.isEmpty() else { - return - } - let filter = filterSettings.currentPostListFilter() - guard filter.hasMore else { - return - } - - postsSyncWithSearchDidBegin() - - let author = filterSettings.shouldShowOnlyMyPosts() ? blogUserID() : nil - let postService = PostService(managedObjectContext: managedObjectContext()) - let options = PostServiceSyncOptions() - options.statuses = filter.statuses.strings - options.authorID = author - options.number = 20 - options.purgesLocalSync = false - options.search = searchText - - postService.syncPosts( - ofType: postTypeToSync(), - with: options, - for: blog, - success: { [weak self] posts in - self?.postsSyncWithSearchEnded() - }, failure: { [weak self] (error) in - self?.postsSyncWithSearchEnded() - } - ) - } - // MARK: - Actions @objc func publishPost(_ apost: AbstractPost, completion: (() -> Void)? = nil) { @@ -1086,21 +981,12 @@ class AbstractPostListViewController: UIViewController, WPAnalytics.track(.postListStatusFilterChanged, withProperties: propertiesForAnalytics()) } - // MARK: - Search Controller Delegate Methods + // MARK: - UISearchControllerDelegate func willPresentSearchController(_ searchController: UISearchController) { WPAnalytics.track(.postListSearchOpened, withProperties: propertiesForAnalytics()) } - func willDismissSearchController(_ searchController: UISearchController) { - searchController.searchBar.text = nil - searchHelper.searchCanceled() - } - - func updateSearchResults(for searchController: UISearchController) { - searchHelper.searchUpdated(searchController.searchBar.text) - } - // MARK: - NetworkAwareUI func contentIsEmpty() -> Bool { diff --git a/WordPress/Classes/ViewRelated/Post/PostListViewController.swift b/WordPress/Classes/ViewRelated/Post/PostListViewController.swift index 17fe3c1a6796..574cf77dcfb3 100644 --- a/WordPress/Classes/ViewRelated/Post/PostListViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/PostListViewController.swift @@ -15,7 +15,7 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe private let statsStoryboardName = "SiteStats" private let currentPostListStatusFilterKey = "CurrentPostListStatusFilterKey" private var postCellIdentifier: String { - return isCompact || isSearching() ? postCompactCellIdentifier : postCardTextCellIdentifier + return isCompact ? postCompactCellIdentifier : postCardTextCellIdentifier } static private let postsViewControllerRestorationKey = "PostsViewControllerRestorationKey" @@ -201,28 +201,6 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe return postListHeightForFooterView } - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - guard _tableViewHandler.isSearching else { - return 0.0 - } - return Constants.searchHeaderHeight - } - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView! { - guard _tableViewHandler.isSearching, - let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: ActivityListSectionHeaderView.identifier) as? ActivityListSectionHeaderView else { - return UIView(frame: .zero) - } - - let sectionInfo = _tableViewHandler.resultsController?.sections?[section] - - if let sectionInfo = sectionInfo { - headerView.titleLabel.text = PostSearchHeader.title(forStatus: sectionInfo.name) - } - - return headerView - } - override func selectedFilterDidChange(_ filterBar: FilterTabBar) { updateGhostableTableViewOptions() super.selectedFilterDidChange(filterBar) @@ -419,12 +397,11 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe predicates.append(basePredicate) } - let searchText = currentSearchTerm() ?? "" - let filterPredicate = searchController.isActive ? NSPredicate(format: "postTitle CONTAINS[cd] %@", searchText) : filterSettings.currentPostListFilter().predicateForFetchRequest + let filterPredicate = filterSettings.currentPostListFilter().predicateForFetchRequest // If we have recently trashed posts, create an OR predicate to find posts matching the filter, // or posts that were recently deleted. - if searchText.count == 0 && recentlyTrashedPostObjectIDs.count > 0 { + if recentlyTrashedPostObjectIDs.count > 0 { let trashedPredicate = NSPredicate(format: "SELF IN %@", recentlyTrashedPostObjectIDs) predicates.append(NSCompoundPredicate(orPredicateWithSubpredicates: [filterPredicate, trashedPredicate])) @@ -440,11 +417,6 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe predicates.append(authorPredicate) } - if searchText.count > 0 { - let searchPredicate = NSPredicate(format: "postTitle CONTAINS[cd] %@", searchText) - predicates.append(searchPredicate) - } - let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) return predicate } @@ -703,40 +675,6 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe BlazeFlowCoordinator.presentBlaze(in: self, source: .postsList, blog: blog, post: post) } - // MARK: - Searching - - override func updateForLocalPostsMatchingSearchText() { - // If the user taps and starts to type right away, avoid doing the search - // while the tableViewHandler is not ready yet - if !_tableViewHandler.isSearching, let search = currentSearchTerm(), !search.isEmpty { - return - } - - super.updateForLocalPostsMatchingSearchText() - } - - func didPresentSearchController(_ searchController: UISearchController) { - _tableViewHandler.isSearching = true - } - - override func sortDescriptorsForFetchRequest() -> [NSSortDescriptor] { - if !isSearching() { - return super.sortDescriptorsForFetchRequest() - } - - let descriptor = NSSortDescriptor(key: BasePost.statusKeyPath, ascending: true) - return [descriptor] - } - - override func willDismissSearchController(_ searchController: UISearchController) { - _tableViewHandler.isSearching = false - super.willDismissSearchController(searchController) - } - - enum Animations { - static let searchDismissDuration: TimeInterval = 0.3 - } - // MARK: - NetworkAwareUI override func noConnectionMessage() -> String { @@ -746,7 +684,6 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe private enum Constants { static let exhibitionModeKey = "showCompactPosts" - static let searchHeaderHeight: CGFloat = 40 static let card = "card" static let compact = "compact" static let source = "post_list" @@ -764,20 +701,12 @@ private extension PostListViewController { return } - if searchController.isActive { - if currentSearchTerm()?.count == 0 { - noResultsViewController.configureForNoSearchResults(title: NoResultsText.searchPosts) - } else { - noResultsViewController.configureForNoSearchResults(title: noResultsTitle()) - } - } else { - let accessoryView = syncHelper.isSyncing ? NoResultsViewController.loadingAccessoryView() : nil + let accessoryView = syncHelper.isSyncing ? NoResultsViewController.loadingAccessoryView() : nil - noResultsViewController.configure(title: noResultsTitle(), - buttonTitle: noResultsButtonTitle(), - image: noResultsImageName, - accessoryView: accessoryView) - } + noResultsViewController.configure(title: noResultsTitle(), + buttonTitle: noResultsButtonTitle(), + image: noResultsImageName, + accessoryView: accessoryView) } var noResultsImageName: String { @@ -785,7 +714,7 @@ private extension PostListViewController { } func noResultsButtonTitle() -> String? { - if syncHelper.isSyncing == true || isSearching() { + if syncHelper.isSyncing == true { return nil } @@ -797,11 +726,6 @@ private extension PostListViewController { if syncHelper.isSyncing == true { return NoResultsText.fetchingTitle } - - if isSearching() { - return NoResultsText.noMatchesTitle - } - return noResultsFilteredTitle() } @@ -824,20 +748,17 @@ private extension PostListViewController { struct NoResultsText { static let buttonTitle = NSLocalizedString("Create Post", comment: "Button title, encourages users to create post on their blog.") static let fetchingTitle = NSLocalizedString("Fetching posts...", comment: "A brief prompt shown when the reader is empty, letting the user know the app is currently fetching new posts.") - static let noMatchesTitle = NSLocalizedString("No posts matching your search", comment: "Displayed when the user is searching the posts list and there are no matching posts") static let noDraftsTitle = NSLocalizedString("You don't have any draft posts", comment: "Displayed when the user views drafts in the posts list and there are no posts") static let noScheduledTitle = NSLocalizedString("You don't have any scheduled posts", comment: "Displayed when the user views scheduled posts in the posts list and there are no posts") static let noTrashedTitle = NSLocalizedString("You don't have any trashed posts", comment: "Displayed when the user views trashed in the posts list and there are no posts") static let noPublishedTitle = NSLocalizedString("You haven't published any posts yet", comment: "Displayed when the user views published posts in the posts list and there are no posts") static let noConnectionTitle: String = NSLocalizedString("Unable to load posts right now.", comment: "Title for No results full page screen displayedfrom post list when there is no connection") static let noConnectionSubtitle: String = NSLocalizedString("Check your network connection and try again. Or draft a post.", comment: "Subtitle for No results full page screen displayed from post list when there is no connection") - static let searchPosts = NSLocalizedString("Search posts", comment: "Text displayed when the search controller will be presented") } } extension PostListViewController: PostActionSheetDelegate { func showActionSheet(_ postCardStatusViewModel: PostCardStatusViewModel, from view: UIView) { - let isCompactOrSearching = isCompact || searchController.isActive - postActionSheet.show(for: postCardStatusViewModel, from: view, isCompactOrSearching: isCompactOrSearching) + postActionSheet.show(for: postCardStatusViewModel, from: view, isCompactOrSearching: isCompact) } } diff --git a/WordPress/Classes/ViewRelated/Post/PostSearchViewController.swift b/WordPress/Classes/ViewRelated/Post/PostSearchViewController.swift deleted file mode 100644 index 2f527ca347c0..000000000000 --- a/WordPress/Classes/ViewRelated/Post/PostSearchViewController.swift +++ /dev/null @@ -1,20 +0,0 @@ -import UIKit - -final class PostSearchViewController: UITableViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellID") - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - 10 - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "cellID", for: indexPath) - cell.textLabel?.text = indexPath.row.description - return cell - } -} diff --git a/WordPress/Classes/ViewRelated/Post/Search/PostSearchViewController.swift b/WordPress/Classes/ViewRelated/Post/Search/PostSearchViewController.swift new file mode 100644 index 000000000000..71ba026b65c4 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/Search/PostSearchViewController.swift @@ -0,0 +1,53 @@ +import UIKit +import Combine + +// TODO: Add loading and empty states +final class PostSearchViewController: UITableViewController, UISearchResultsUpdating, NSFetchedResultsControllerDelegate { + private let viewModel: PostSearchViewModel + private var fetchResultsViewController: NSFetchedResultsController { + viewModel.fetchResultsController + } + + init(viewModel: PostSearchViewModel) { + self.viewModel = viewModel + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellID") + + fetchResultsViewController.delegate = self + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + fetchResultsViewController.fetchedObjects?.count ?? 0 + } + + // TODO: Update new cells and display in sections + // TODO: Add context menus and navigation (reuse with the plain list) + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "cellID", for: indexPath) + let post = fetchResultsViewController.object(at: indexPath) + cell.textLabel?.text = post.titleForDisplay() + return cell + } + + // MARK: - UISearchResultsUpdating + + func updateSearchResults(for searchController: UISearchController) { + viewModel.searchText = searchController.searchBar.text ?? "" + } + + // MARK: - NSFetchedResultsControllerDelegate + + func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + tableView.reloadData() + } +} diff --git a/WordPress/Classes/ViewRelated/Post/Search/PostSearchViewModel.swift b/WordPress/Classes/ViewRelated/Post/Search/PostSearchViewModel.swift new file mode 100644 index 000000000000..8d68b43cb56b --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/Search/PostSearchViewModel.swift @@ -0,0 +1,123 @@ +import Foundation +import Combine + +final class PostSearchViewModel { + @Published var searchText = "" + + private let blog: Blog + private let postType: PostServiceType + private let coreDataStack: CoreDataStack + private var cancellables: [AnyCancellable] = [] + + private(set) var fetchResultsController: NSFetchedResultsController! + + init(blog: Blog, + postType: PostServiceType, + coreDataStack: CoreDataStack = ContextManager.shared + ) { + self.blog = blog + self.postType = postType + self.coreDataStack = coreDataStack + + fetchResultsController = NSFetchedResultsController( + fetchRequest: makeFetchRequest(searchTerm: ""), + managedObjectContext: coreDataStack.mainContext, + sectionNameKeyPath: nil, + cacheName: nil + ) + + $searchText.dropFirst().sink { [weak self] in + self?.performLocalSearch(with: $0) + }.store(in: &cancellables) + + $searchText + .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true) + .removeDuplicates() + .filter { $0.count > 1 } + .sink { [weak self] in + self?.syncPostsMatchingSearchTerm($0) + }.store(in: &cancellables) + } + + // MARK: - Local Search + + private func performLocalSearch(with searchTerm: String) { + fetchResultsController.fetchRequest.predicate = makePredicate(searchTerm: searchTerm) + do { + try fetchResultsController.performFetch() + } catch { + assertionFailure("Failed to perform search: \(error)") + } + } + + // TODO: Move search to the background + private func makeFetchRequest(searchTerm: String) -> NSFetchRequest { + let request = NSFetchRequest(entityName: makeEntityName()) + request.predicate = makePredicate(searchTerm: searchTerm) + // TODO: Update sort descriptors + request.sortDescriptors = [NSSortDescriptor(key: "date_created_gmt", ascending: true)] + request.fetchBatchSize = 40 + return request + } + + private func makeEntityName() -> String { + switch postType { + case .post: return String(describing: Post.self) + case .page: return String(describing: Page.self) + default: fatalError("Unsupported post type: \(postType)") + } + } + + private func makePredicate(searchTerm: String) -> NSPredicate { + var predicates = [NSPredicate]() + + // Show all original posts without a revision & revision posts. + predicates.append(NSPredicate(format: "blog = %@ && revision = nil", blog)) + predicates.append(NSPredicate(format: "postTitle CONTAINS[cd] %@", searchText)) + + if postType == .page, let predicate = PostSearchViewModel.makeHomepagePredicate(for: blog) { + predicates.append(predicate) + } + + return NSCompoundPredicate(andPredicateWithSubpredicates: predicates) + } + + static func makeHomepagePredicate(for blog: Blog) -> NSPredicate? { + guard RemoteFeatureFlag.siteEditorMVP.enabled(), + blog.blockEditorSettings?.isFSETheme ?? false, + let homepageID = blog.homepagePageID, + let homepageType = blog.homepageType, + homepageType == .page else { + return nil + } + return NSPredicate(format: "postID != %i", homepageID) + } + + // MARK: - Remote Search (Sync) + + // TODO: Implement remove search + private func syncPostsMatchingSearchTerm(_ searchTerm: String) { +// let filter = filterSettings.currentPostListFilter() +// guard filter.hasMore else { +// return +// } + + let postService = PostService(managedObjectContext: coreDataStack.mainContext) + + let options = PostServiceSyncOptions() + options.number = 20 + options.purgesLocalSync = false + options.search = searchTerm + + postService.syncPosts( + ofType: postType, + with: options, + for: blog, + success: { _ in + // TODO: + }, failure: { _ in + // TODO: + } + ) + } +} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 8048ab69f8b6..4e9891d5931d 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -226,7 +226,6 @@ 03216ECD27995F3500D444CA /* SchedulingViewControllerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03216ECB27995F3500D444CA /* SchedulingViewControllerPresenter.swift */; }; 069A4AA62664448F00413FA9 /* GutenbergFeaturedImageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 069A4AA52664448F00413FA9 /* GutenbergFeaturedImageHelper.swift */; }; 069A4AA72664448F00413FA9 /* GutenbergFeaturedImageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 069A4AA52664448F00413FA9 /* GutenbergFeaturedImageHelper.swift */; }; - 0807CB721CE670A800CDBDAC /* WPContentSearchHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0807CB711CE670A800CDBDAC /* WPContentSearchHelper.swift */; }; 080C44A91CE14A9F00B3A02F /* MenuDetailsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 080C449E1CE14A9F00B3A02F /* MenuDetailsViewController.m */; }; 0815CF461E96F22600069916 /* MediaImportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0815CF451E96F22600069916 /* MediaImportService.swift */; }; 081E4B4C281C019A0085E89C /* TooltipAnchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081E4B4B281C019A0085E89C /* TooltipAnchor.swift */; }; @@ -469,6 +468,8 @@ 0CD382862A4B6FCF00612173 /* DashboardBlazeCardCellViewModelTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD382852A4B6FCE00612173 /* DashboardBlazeCardCellViewModelTest.swift */; }; 0CD9CC9F2AD73A560044A33C /* PostSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD9CC9E2AD73A560044A33C /* PostSearchViewController.swift */; }; 0CD9CCA02AD73A560044A33C /* PostSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD9CC9E2AD73A560044A33C /* PostSearchViewController.swift */; }; + 0CD9CCA32AD831590044A33C /* PostSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD9CCA22AD831590044A33C /* PostSearchViewModel.swift */; }; + 0CD9CCA42AD831590044A33C /* PostSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD9CCA22AD831590044A33C /* PostSearchViewModel.swift */; }; 0CDEC40C2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDEC40B2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift */; }; 0CDEC40D2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDEC40B2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift */; }; 0CED95602A460F4B0020F420 /* DebugFeatureFlagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CED955F2A460F4B0020F420 /* DebugFeatureFlagsView.swift */; }; @@ -4107,7 +4108,6 @@ FABB20C82602FC2C00C8785C /* ActivityPluginRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E4A773320F800ED001C706D /* ActivityPluginRange.swift */; }; FABB20C92602FC2C00C8785C /* ShareNoticeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7462BFCF2028C49800B552D8 /* ShareNoticeViewModel.swift */; }; FABB20CA2602FC2C00C8785C /* Routes+Reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A4A36820EE51870071C2CA /* Routes+Reader.swift */; }; - FABB20CB2602FC2C00C8785C /* WPContentSearchHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0807CB711CE670A800CDBDAC /* WPContentSearchHelper.swift */; }; FABB20CC2602FC2C00C8785C /* SiteStatsViewModel+AsyncBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A73B7142362FBAE004624A8 /* SiteStatsViewModel+AsyncBlock.swift */; }; FABB20CD2602FC2C00C8785C /* PostUploadOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74AF4D711FE417D200E3EBFE /* PostUploadOperation.swift */; }; FABB20CE2602FC2C00C8785C /* VerticallyStackedButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 37022D901981BF9200F322B7 /* VerticallyStackedButton.m */; }; @@ -5871,7 +5871,6 @@ 03216EC5279946CA00D444CA /* SchedulingDatePickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchedulingDatePickerViewController.swift; sourceTree = ""; }; 03216ECB27995F3500D444CA /* SchedulingViewControllerPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchedulingViewControllerPresenter.swift; sourceTree = ""; }; 069A4AA52664448F00413FA9 /* GutenbergFeaturedImageHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergFeaturedImageHelper.swift; sourceTree = ""; }; - 0807CB711CE670A800CDBDAC /* WPContentSearchHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WPContentSearchHelper.swift; sourceTree = ""; }; 080C449D1CE14A9F00B3A02F /* MenuDetailsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuDetailsViewController.h; sourceTree = ""; }; 080C449E1CE14A9F00B3A02F /* MenuDetailsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuDetailsViewController.m; sourceTree = ""; }; 0815CF451E96F22600069916 /* MediaImportService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaImportService.swift; sourceTree = ""; }; @@ -6126,6 +6125,7 @@ 0CD382822A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCardCellViewModel.swift; sourceTree = ""; }; 0CD382852A4B6FCE00612173 /* DashboardBlazeCardCellViewModelTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCardCellViewModelTest.swift; sourceTree = ""; }; 0CD9CC9E2AD73A560044A33C /* PostSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSearchViewController.swift; sourceTree = ""; }; + 0CD9CCA22AD831590044A33C /* PostSearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSearchViewModel.swift; sourceTree = ""; }; 0CDEC40B2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCampaignsCardView.swift; sourceTree = ""; }; 0CED955F2A460F4B0020F420 /* DebugFeatureFlagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugFeatureFlagsView.swift; sourceTree = ""; }; 0CF7D6C22ABB753A006D1E89 /* MediaImageServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaImageServiceTests.swift; sourceTree = ""; }; @@ -10201,6 +10201,15 @@ path = BlogPersonalization; sourceTree = ""; }; + 0CD9CCA12AD8313E0044A33C /* Search */ = { + isa = PBXGroup; + children = ( + 0CD9CC9E2AD73A560044A33C /* PostSearchViewController.swift */, + 0CD9CCA22AD831590044A33C /* PostSearchViewModel.swift */, + ); + path = Search; + sourceTree = ""; + }; 1705CEE6260A57F900F23763 /* ContentViews */ = { isa = PBXGroup; children = ( @@ -12451,7 +12460,6 @@ isa = PBXGroup; children = ( 591232681CCEAA5100B86207 /* AbstractPostListViewController.swift */, - 0CD9CC9E2AD73A560044A33C /* PostSearchViewController.swift */, 590E873A1CB8205700D1B734 /* PostListViewController.swift */, E684383D221F535900752258 /* LoadMoreCounter.swift */, ); @@ -13599,7 +13607,6 @@ 292CECFF1027259000BD407D /* SFHFKeychainUtils.m */, 594399911B45091000539E21 /* WPAuthTokenIssueSolver.h */, 594399921B45091000539E21 /* WPAuthTokenIssueSolver.m */, - 0807CB711CE670A800CDBDAC /* WPContentSearchHelper.swift */, 5D6C4B051B603E03005E3C43 /* WPContentSyncHelper.swift */, E114D798153D85A800984182 /* WPError.h */, E114D799153D85A800984182 /* WPError.m */, @@ -14763,6 +14770,7 @@ F57402A5235FF71F00374346 /* Scheduling */, 4349B0A6218A2E810034118A /* Revisions */, 5D1EBF56187C9B95003393F8 /* Categories */, + 0CD9CCA12AD8313E0044A33C /* Search */, 5DF3DD691A9377220051A229 /* Controllers */, 5DF3DD6B1A93773B0051A229 /* Style */, 5D09CBA61ACDE532007A23BD /* Utils */, @@ -20872,7 +20880,6 @@ FA98A2502833F1DC003B9233 /* QuickStartChecklistConfigurable.swift in Sources */, 7462BFD02028C49800B552D8 /* ShareNoticeViewModel.swift in Sources */, 17A4A36920EE51870071C2CA /* Routes+Reader.swift in Sources */, - 0807CB721CE670A800CDBDAC /* WPContentSearchHelper.swift in Sources */, C395FB262821FE7B00AE7C11 /* RemoteSiteDesign+Thumbnail.swift in Sources */, 9A73B7152362FBAE004624A8 /* SiteStatsViewModel+AsyncBlock.swift in Sources */, 74AF4D7C1FE417D200E3EBFE /* PostUploadOperation.swift in Sources */, @@ -21939,6 +21946,7 @@ 17A4A36C20EE55320071C2CA /* ReaderCoordinator.swift in Sources */, FAB800B225AEE3C600D5D54A /* RestoreCompleteView.swift in Sources */, B532D4EE199D4418006E4DF6 /* NoteBlockImageTableViewCell.swift in Sources */, + 0CD9CCA32AD831590044A33C /* PostSearchViewModel.swift in Sources */, 93FA59DD18D88C1C001446BC /* PostCategoryService.m in Sources */, 436D564F211E122D00CEAA33 /* RegisterDomainDetailsServiceProxy.swift in Sources */, F5B8A60F23CE56A1001B7359 /* PreviewDeviceSelectionViewController.swift in Sources */, @@ -23648,7 +23656,6 @@ FABB20C82602FC2C00C8785C /* ActivityPluginRange.swift in Sources */, FABB20C92602FC2C00C8785C /* ShareNoticeViewModel.swift in Sources */, FABB20CA2602FC2C00C8785C /* Routes+Reader.swift in Sources */, - FABB20CB2602FC2C00C8785C /* WPContentSearchHelper.swift in Sources */, FABB20CC2602FC2C00C8785C /* SiteStatsViewModel+AsyncBlock.swift in Sources */, FABB20CD2602FC2C00C8785C /* PostUploadOperation.swift in Sources */, FABB20CE2602FC2C00C8785C /* VerticallyStackedButton.m in Sources */, @@ -23850,6 +23857,7 @@ FABB215F2602FC2C00C8785C /* ContextManager+ErrorHandling.swift in Sources */, 0C7762242AAFD39700E07A88 /* SiteMediaAddMediaMenuController.swift in Sources */, C383555A288B02B00062E402 /* JetpackBannerWrapperViewController.swift in Sources */, + 0CD9CCA42AD831590044A33C /* PostSearchViewModel.swift in Sources */, FABB21602602FC2C00C8785C /* EmptyActionView.swift in Sources */, FABB21612602FC2C00C8785C /* ReaderShowAttributionAction.swift in Sources */, FABB21622602FC2C00C8785C /* LinkSettingsViewController.swift in Sources */,