diff --git a/DB/Sources/DB/Content/ContentDatabase.swift b/DB/Sources/DB/Content/ContentDatabase.swift index fc4f42a..4e37e78 100644 --- a/DB/Sources/DB/Content/ContentDatabase.swift +++ b/DB/Sources/DB/Content/ContentDatabase.swift @@ -508,7 +508,8 @@ public extension ContentDatabase { .eraseToAnyPublisher() } - func publisher(results: Results) -> AnyPublisher<[CollectionSection], Error> { + // swiftlint:disable:next function_body_length + func publisher(results: Results, limit: Int?) -> AnyPublisher<[CollectionSection], Error> { let accountIds = results.accounts.map(\.id) let statusIds = results.statuses.map(\.id) @@ -518,12 +519,18 @@ public extension ContentDatabase { .fetchAll) .removeDuplicates() .publisher(in: databaseWriter) - .map { - $0.sorted { + .map { infos -> [CollectionItem] in + var accounts = infos.sorted { accountIds.firstIndex(of: $0.record.id) ?? 0 < accountIds.firstIndex(of: $1.record.id) ?? 0 } .map { CollectionItem.account(.init(info: $0)) } + + if let limit = limit, accounts.count >= limit { + accounts.append(.moreResults(.init(scope: .accounts))) + } + + return accounts } let statusesPublisher = ValueObservation.tracking( @@ -532,8 +539,8 @@ public extension ContentDatabase { .fetchAll) .removeDuplicates() .publisher(in: databaseWriter) - .map { - $0.sorted { + .map { infos -> [CollectionItem] in + var statuses = infos.sorted { statusIds.firstIndex(of: $0.record.id) ?? 0 < statusIds.firstIndex(of: $1.record.id) ?? 0 } @@ -543,13 +550,25 @@ public extension ContentDatabase { .init(showContentToggled: $0.showContentToggled, showAttachmentsToggled: $0.showAttachmentsToggled)) } + + if let limit = limit, statuses.count >= limit { + statuses.append(.moreResults(.init(scope: .statuses))) + } + + return statuses } + var hashtags = results.hashtags.map(CollectionItem.tag) + + if let limit = limit, hashtags.count >= limit { + hashtags.append(.moreResults(.init(scope: .tags))) + } + return accountsPublisher.combineLatest(statusesPublisher) .map { accounts, statuses in [.init(items: accounts, titleLocalizedStringKey: "search.scope.accounts"), .init(items: statuses, titleLocalizedStringKey: "search.scope.statuses"), - .init(items: results.hashtags.map(CollectionItem.tag), titleLocalizedStringKey: "search.scope.tags")] + .init(items: hashtags, titleLocalizedStringKey: "search.scope.tags")] .filter { !$0.items.isEmpty } } .removeDuplicates() diff --git a/DB/Sources/DB/Entities/CollectionItem.swift b/DB/Sources/DB/Entities/CollectionItem.swift index 35a4e81..9f01b9c 100644 --- a/DB/Sources/DB/Entities/CollectionItem.swift +++ b/DB/Sources/DB/Entities/CollectionItem.swift @@ -9,6 +9,7 @@ public enum CollectionItem: Hashable { case notification(MastodonNotification, StatusConfiguration?) case conversation(Conversation) case tag(Tag) + case moreResults(MoreResults) } public extension CollectionItem { @@ -51,6 +52,8 @@ public extension CollectionItem { return conversation.id case let .tag(tag): return tag.name + case .moreResults: + return nil } } } diff --git a/DB/Sources/DB/Entities/MoreResults.swift b/DB/Sources/DB/Entities/MoreResults.swift new file mode 100644 index 0000000..a29d2ec --- /dev/null +++ b/DB/Sources/DB/Entities/MoreResults.swift @@ -0,0 +1,7 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Foundation + +public struct MoreResults: Hashable { + public let scope: SearchScope +} diff --git a/DB/Sources/DB/Entities/SearchScope.swift b/DB/Sources/DB/Entities/SearchScope.swift new file mode 100644 index 0000000..0cf4095 --- /dev/null +++ b/DB/Sources/DB/Entities/SearchScope.swift @@ -0,0 +1,10 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Foundation + +public enum SearchScope: Int, CaseIterable { + case all + case accounts + case statuses + case tags +} diff --git a/Data Sources/TableViewDataSource.swift b/Data Sources/TableViewDataSource.swift index 1494c49..848dd65 100644 --- a/Data Sources/TableViewDataSource.swift +++ b/Data Sources/TableViewDataSource.swift @@ -30,6 +30,13 @@ final class TableViewDataSource: UITableViewDiffableDataSource() init(viewModel: ExploreViewModel, rootViewModel: RootViewModel, identification: Identification) { self.viewModel = viewModel @@ -40,15 +42,29 @@ final class ExploreViewController: UICollectionViewController { let searchController = UISearchController(searchResultsController: searchResultsController) - searchController.searchBar.scopeButtonTitles = SearchViewModel.Scope.allCases.map(\.title) + searchController.searchBar.scopeButtonTitles = SearchScope.allCases.map(\.title) searchController.searchResultsUpdater = self navigationItem.searchController = searchController + + viewModel.searchViewModel.events.sink { [weak self] in + if case let .navigation(navigation) = $0, + case let .searchScope(scope) = navigation { + searchController.searchBar.selectedScopeButtonIndex = scope.rawValue + self?.updateSearchResults(for: searchController) + } + } + .store(in: &cancellables) } } extension ExploreViewController: UISearchResultsUpdating { func updateSearchResults(for searchController: UISearchController) { - if let scope = SearchViewModel.Scope(rawValue: searchController.searchBar.selectedScopeButtonIndex) { + if let scope = SearchScope(rawValue: searchController.searchBar.selectedScopeButtonIndex) { + if scope != viewModel.searchViewModel.scope, + let scrollView = searchController.searchResultsController?.view as? UIScrollView { + scrollView.setContentOffset(.init(x: 0, y: -scrollView.safeAreaInsets.top), animated: false) + } + viewModel.searchViewModel.scope = scope } diff --git a/View Controllers/TableViewController.swift b/View Controllers/TableViewController.swift index 529a22c..055391a 100644 --- a/View Controllers/TableViewController.swift +++ b/View Controllers/TableViewController.swift @@ -372,6 +372,8 @@ private extension TableViewController { } case let .url(url): present(SFSafariViewController(url: url), animated: true) + case .searchScope: + break case .webfingerStart: webfingerIndicatorView.startAnimating() case .webfingerEnd: diff --git a/ViewModels/Sources/ViewModels/CollectionItemsViewModel.swift b/ViewModels/Sources/ViewModels/CollectionItemsViewModel.swift index f1fbf46..d60a25a 100644 --- a/ViewModels/Sources/ViewModels/CollectionItemsViewModel.swift +++ b/ViewModels/Sources/ViewModels/CollectionItemsViewModel.swift @@ -5,6 +5,7 @@ import Foundation import Mastodon import ServiceLayer +// swiftlint:disable file_length public class CollectionItemsViewModel: ObservableObject { @Published public var alertItem: AlertItem? public private(set) var nextPageMaxId: String? @@ -166,6 +167,8 @@ extension CollectionItemsViewModel: CollectionViewModel { .navigation(.collection(collectionService .navigationService .timelineService(timeline: .tag(tag.name))))) + case let .moreResults(moreResults): + eventsSubject.send(.navigation(.searchScope(moreResults.scope))) } } @@ -277,6 +280,16 @@ extension CollectionItemsViewModel: CollectionViewModel { cache(viewModel: viewModel, forItem: item) + return viewModel + case let .moreResults(moreResults): + if let cachedViewModel = cachedViewModel { + return cachedViewModel + } + + let viewModel = MoreResultsViewModel(moreResults: moreResults) + + cache(viewModel: viewModel, forItem: item) + return viewModel } } @@ -405,3 +418,4 @@ private extension CollectionItemsViewModel { return nil } } +// swiftlint:enable file_length diff --git a/ViewModels/Sources/ViewModels/Entities/SearchScope.swift b/ViewModels/Sources/ViewModels/Entities/SearchScope.swift new file mode 100644 index 0000000..fb28c30 --- /dev/null +++ b/ViewModels/Sources/ViewModels/Entities/SearchScope.swift @@ -0,0 +1,5 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import ServiceLayer + +public typealias SearchScope = ServiceLayer.SearchScope diff --git a/ViewModels/Sources/ViewModels/MoreResultsViewModel.swift b/ViewModels/Sources/ViewModels/MoreResultsViewModel.swift new file mode 100644 index 0000000..dfe56c3 --- /dev/null +++ b/ViewModels/Sources/ViewModels/MoreResultsViewModel.swift @@ -0,0 +1,20 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Combine +import ServiceLayer + +public final class MoreResultsViewModel: ObservableObject, CollectionItemViewModel { + public var events: AnyPublisher, Never> + + private let moreResults: MoreResults + private let eventsSubject = PassthroughSubject, Never>() + + init(moreResults: MoreResults) { + self.moreResults = moreResults + events = eventsSubject.eraseToAnyPublisher() + } +} + +public extension MoreResultsViewModel { + var scope: SearchScope { moreResults.scope } +} diff --git a/ViewModels/Sources/ViewModels/SearchViewModel.swift b/ViewModels/Sources/ViewModels/SearchViewModel.swift index 84385d4..781dc7e 100644 --- a/ViewModels/Sources/ViewModels/SearchViewModel.swift +++ b/ViewModels/Sources/ViewModels/SearchViewModel.swift @@ -6,7 +6,7 @@ import ServiceLayer public final class SearchViewModel: CollectionItemsViewModel { @Published public var query = "" - @Published public var scope = Scope.all + @Published public var scope = SearchScope.all private let searchService: SearchService private var cancellables = Set() @@ -45,20 +45,11 @@ public final class SearchViewModel: CollectionItemsViewModel { } } -public extension SearchViewModel { - enum Scope: Int, CaseIterable { - case all - case accounts - case statuses - case tags - } -} - private extension SearchViewModel { static let throttleInterval: TimeInterval = 0.5 } -private extension SearchViewModel.Scope { +private extension SearchScope { var type: Search.SearchType? { switch self { case .all: