mirror of
https://github.com/metabolist/metatext.git
synced 2024-12-22 05:26:30 +00:00
More results footers
This commit is contained in:
parent
cb6032bf4f
commit
93e4fa7496
19 changed files with 154 additions and 28 deletions
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
7
DB/Sources/DB/Entities/MoreResults.swift
Normal file
7
DB/Sources/DB/Entities/MoreResults.swift
Normal file
|
@ -0,0 +1,7 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct MoreResults: Hashable {
|
||||
public let scope: SearchScope
|
||||
}
|
10
DB/Sources/DB/Entities/SearchScope.swift
Normal file
10
DB/Sources/DB/Entities/SearchScope.swift
Normal file
|
@ -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
|
||||
}
|
|
@ -30,6 +30,13 @@ final class TableViewDataSource: UITableViewDiffableDataSource<CollectionSection
|
|||
conversationListCell.viewModel = conversationViewModel
|
||||
case let (tagTableViewCell as TagTableViewCell, tagViewModel as TagViewModel):
|
||||
tagTableViewCell.viewModel = tagViewModel
|
||||
case let (_, moreResultsViewModel as MoreResultsViewModel):
|
||||
var configuration = cell.defaultContentConfiguration()
|
||||
|
||||
configuration.text = moreResultsViewModel.scope.moreDescription
|
||||
|
||||
cell.contentConfiguration = configuration
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@ extension CollectionItem {
|
|||
LoadMoreCell.self,
|
||||
NotificationListCell.self,
|
||||
ConversationListCell.self,
|
||||
TagTableViewCell.self]
|
||||
TagTableViewCell.self,
|
||||
UITableViewCell.self]
|
||||
|
||||
var cellClass: AnyClass {
|
||||
switch self {
|
||||
|
@ -26,6 +27,8 @@ extension CollectionItem {
|
|||
return ConversationListCell.self
|
||||
case .tag:
|
||||
return TagTableViewCell.self
|
||||
case .moreResults:
|
||||
return UITableViewCell.self
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,6 +57,8 @@ extension CollectionItem {
|
|||
conversation: conversation)
|
||||
case let .tag(tag):
|
||||
return TagView.estimatedHeight(width: width, tag: tag)
|
||||
case .moreResults:
|
||||
return UITableView.automaticDimension
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import Foundation
|
||||
import ViewModels
|
||||
|
||||
extension SearchViewModel.Scope {
|
||||
extension SearchScope {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .all:
|
||||
|
@ -16,4 +16,17 @@ extension SearchViewModel.Scope {
|
|||
return NSLocalizedString("search.scope.tags", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
var moreDescription: String? {
|
||||
switch self {
|
||||
case .all:
|
||||
return nil
|
||||
case .accounts:
|
||||
return NSLocalizedString("more-results.accounts", comment: "")
|
||||
case .statuses:
|
||||
return NSLocalizedString("more-results.statuses", comment: "")
|
||||
case .tags:
|
||||
return NSLocalizedString("more-results.tags", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -160,6 +160,9 @@
|
|||
"filter.context.thread" = "Conversations";
|
||||
"filter.context.account" = "Profiles";
|
||||
"filter.context.unknown" = "Unknown context";
|
||||
"more-results.accounts" = "More people";
|
||||
"more-results.statuses" = "More posts";
|
||||
"more-results.tags" = "More hashtags";
|
||||
"notifications" = "Notifications";
|
||||
"notifications.reblogged-your-status" = "%@ boosted your status";
|
||||
"notifications.favourited-your-status" = "%@ favorited your status";
|
||||
|
|
|
@ -95,7 +95,7 @@
|
|||
D08E52EE257D757100FA2C5F /* CompositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52ED257D757100FA2C5F /* CompositionView.swift */; };
|
||||
D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52ED257D757100FA2C5F /* CompositionView.swift */; };
|
||||
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EA59472522B8B600804347 /* ViewConstants.swift */; };
|
||||
D097F41B25BE3E1A00859F2C /* SearchViewModel+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D097F41A25BE3E1A00859F2C /* SearchViewModel+Extensions.swift */; };
|
||||
D097F41B25BE3E1A00859F2C /* SearchScope+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D097F41A25BE3E1A00859F2C /* SearchScope+Extensions.swift */; };
|
||||
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; };
|
||||
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */; };
|
||||
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; };
|
||||
|
@ -269,7 +269,7 @@
|
|||
D08E52C6257C7AEE00FA2C5F /* ShareErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareErrorViewController.swift; sourceTree = "<group>"; };
|
||||
D08E52D1257C811200FA2C5F /* ShareExtensionError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShareExtensionError+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D08E52ED257D757100FA2C5F /* CompositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionView.swift; sourceTree = "<group>"; };
|
||||
D097F41A25BE3E1A00859F2C /* SearchViewModel+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewModel+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D097F41A25BE3E1A00859F2C /* SearchScope+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchScope+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; };
|
||||
D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusView.swift; sourceTree = "<group>"; };
|
||||
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; };
|
||||
|
@ -606,7 +606,7 @@
|
|||
D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */,
|
||||
D07EC7CE25B13921006DF726 /* PickerEmoji+Extensions.swift */,
|
||||
D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */,
|
||||
D097F41A25BE3E1A00859F2C /* SearchViewModel+Extensions.swift */,
|
||||
D097F41A25BE3E1A00859F2C /* SearchScope+Extensions.swift */,
|
||||
D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */,
|
||||
D0C7D46A24F76169001EBDBB /* String+Extensions.swift */,
|
||||
D07EC81025B232C2006DF726 /* SystemEmoji+Extensions.swift */,
|
||||
|
@ -864,7 +864,7 @@
|
|||
D08B8D622540DE3B00B1EBEF /* ZoomTransitionController.swift in Sources */,
|
||||
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */,
|
||||
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */,
|
||||
D097F41B25BE3E1A00859F2C /* SearchViewModel+Extensions.swift in Sources */,
|
||||
D097F41B25BE3E1A00859F2C /* SearchScope+Extensions.swift in Sources */,
|
||||
D035F8B325B9616000DC75ED /* Timeline+Extensions.swift in Sources */,
|
||||
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */,
|
||||
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import DB
|
||||
|
||||
public typealias MoreResults = DB.MoreResults
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import DB
|
||||
|
||||
public typealias SearchScope = DB.SearchScope
|
|
@ -10,6 +10,7 @@ public enum Navigation {
|
|||
case url(URL)
|
||||
case collection(CollectionService)
|
||||
case profile(ProfileService)
|
||||
case searchScope(SearchScope)
|
||||
case webfingerStart
|
||||
case webfingerEnd
|
||||
}
|
||||
|
|
|
@ -21,12 +21,12 @@ public struct SearchService {
|
|||
self.contentDatabase = contentDatabase
|
||||
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
|
||||
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||
sections = resultsSubject.scan(.empty) {
|
||||
sections = resultsSubject.scan((.empty, nil)) {
|
||||
let (results, search) = $1
|
||||
|
||||
return search.offset == nil ? results : $0.appending(results)
|
||||
return (search.offset == nil ? results : $0.0.appending(results), search.limit)
|
||||
}
|
||||
.flatMap(contentDatabase.publisher(results:)).eraseToAnyPublisher()
|
||||
.flatMap(contentDatabase.publisher(results:limit:)).eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import UIKit
|
||||
import ViewModels
|
||||
|
||||
|
@ -7,6 +8,7 @@ final class ExploreViewController: UICollectionViewController {
|
|||
private let viewModel: ExploreViewModel
|
||||
private let rootViewModel: RootViewModel
|
||||
private let identification: Identification
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
5
ViewModels/Sources/ViewModels/Entities/SearchScope.swift
Normal file
5
ViewModels/Sources/ViewModels/Entities/SearchScope.swift
Normal file
|
@ -0,0 +1,5 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import ServiceLayer
|
||||
|
||||
public typealias SearchScope = ServiceLayer.SearchScope
|
20
ViewModels/Sources/ViewModels/MoreResultsViewModel.swift
Normal file
20
ViewModels/Sources/ViewModels/MoreResultsViewModel.swift
Normal file
|
@ -0,0 +1,20 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import ServiceLayer
|
||||
|
||||
public final class MoreResultsViewModel: ObservableObject, CollectionItemViewModel {
|
||||
public var events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||
|
||||
private let moreResults: MoreResults
|
||||
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
|
||||
|
||||
init(moreResults: MoreResults) {
|
||||
self.moreResults = moreResults
|
||||
events = eventsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
public extension MoreResultsViewModel {
|
||||
var scope: SearchScope { moreResults.scope }
|
||||
}
|
|
@ -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<AnyCancellable>()
|
||||
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue