mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 00:01:00 +00:00
Search wip
This commit is contained in:
parent
7c17618065
commit
c4da421846
27 changed files with 357 additions and 49 deletions
|
@ -425,6 +425,38 @@ public extension ContentDatabase {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func process(results: Results) -> AnyPublisher<[[CollectionItem]], Error> {
|
||||
databaseWriter.writePublisher { db -> ([StatusInfo], [Status.Id]) in
|
||||
for account in results.accounts {
|
||||
try account.save(db)
|
||||
}
|
||||
|
||||
for status in results.statuses {
|
||||
try status.save(db)
|
||||
}
|
||||
|
||||
let ids = results.statuses.map(\.id)
|
||||
let statusInfos = try StatusInfo.request(
|
||||
StatusRecord.filter(ids.contains(StatusRecord.Columns.id)))
|
||||
.fetchAll(db)
|
||||
|
||||
return (statusInfos, ids)
|
||||
}
|
||||
.map { statusInfos, ids -> [[CollectionItem]] in
|
||||
[
|
||||
results.accounts.map(CollectionItem.account),
|
||||
statusInfos
|
||||
.sorted { ids.firstIndex(of: $0.record.id) ?? 0 < ids.firstIndex(of: $1.record.id) ?? 0 }
|
||||
.map {
|
||||
.status(.init(info: $0),
|
||||
.init(showContentToggled: $0.showContentToggled,
|
||||
showAttachmentsToggled: $0.showAttachmentsToggled))
|
||||
}
|
||||
]
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func timelinePublisher(_ timeline: Timeline) -> AnyPublisher<[[CollectionItem]], Error> {
|
||||
ValueObservation.tracking(
|
||||
TimelineItemsInfo.request(TimelineRecord.filter(TimelineRecord.Columns.id == timeline.id)).fetchOne)
|
||||
|
|
|
@ -5,7 +5,43 @@ import HTTP
|
|||
import Mastodon
|
||||
|
||||
public enum ResultsEndpoint {
|
||||
case search(query: String, resolve: Bool)
|
||||
case search(Search)
|
||||
}
|
||||
|
||||
public extension ResultsEndpoint {
|
||||
struct Search {
|
||||
public let query: String
|
||||
public let type: SearchType?
|
||||
public let excludeUnreviewed: Bool
|
||||
public let resolve: Bool
|
||||
public let limit: Int?
|
||||
public let offset: Int?
|
||||
public let following: Bool
|
||||
|
||||
public init(query: String,
|
||||
type: SearchType? = nil,
|
||||
excludeUnreviewed: Bool = false,
|
||||
resolve: Bool = false,
|
||||
limit: Int? = nil,
|
||||
offset: Int? = nil,
|
||||
following: Bool = false) {
|
||||
self.query = query
|
||||
self.type = type
|
||||
self.excludeUnreviewed = excludeUnreviewed
|
||||
self.resolve = resolve
|
||||
self.limit = limit
|
||||
self.offset = offset
|
||||
self.following = following
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension ResultsEndpoint.Search {
|
||||
enum SearchType: String {
|
||||
case accounts
|
||||
case hashtags
|
||||
case statuses
|
||||
}
|
||||
}
|
||||
|
||||
extension ResultsEndpoint: Endpoint {
|
||||
|
@ -34,13 +70,33 @@ extension ResultsEndpoint: Endpoint {
|
|||
|
||||
public var queryParameters: [URLQueryItem] {
|
||||
switch self {
|
||||
case let .search(query, resolve):
|
||||
var params = [URLQueryItem(name: "q", value: query)]
|
||||
case let .search(search):
|
||||
var params = [URLQueryItem(name: "q", value: search.query)]
|
||||
|
||||
if resolve {
|
||||
if let type = search.type {
|
||||
params.append(.init(name: "type", value: type.rawValue))
|
||||
}
|
||||
|
||||
if search.excludeUnreviewed {
|
||||
params.append(.init(name: "exclude_unreviewed", value: "true"))
|
||||
}
|
||||
|
||||
if search.resolve {
|
||||
params.append(.init(name: "resolve", value: "true"))
|
||||
}
|
||||
|
||||
if let limit = search.limit {
|
||||
params.append(.init(name: "limit", value: String(limit)))
|
||||
}
|
||||
|
||||
if let offset = search.offset {
|
||||
params.append(.init(name: "offset", value: String(offset)))
|
||||
}
|
||||
|
||||
if search.following {
|
||||
params.append(.init(name: "following", value: "true"))
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,6 +74,7 @@
|
|||
D07EC81125B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC81025B232C2006DF726 /* SystemEmoji+Extensions.swift */; };
|
||||
D07EC81225B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC81025B232C2006DF726 /* SystemEmoji+Extensions.swift */; };
|
||||
D0849C7F25903C4900A5EBCC /* Status+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */; };
|
||||
D087671625BAA8C0001FDD43 /* ExploreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087671525BAA8C0001FDD43 /* ExploreViewController.swift */; };
|
||||
D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */; };
|
||||
D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */; };
|
||||
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */; };
|
||||
|
@ -244,6 +245,7 @@
|
|||
D07EC81025B232C2006DF726 /* SystemEmoji+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SystemEmoji+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Status+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = "<group>"; };
|
||||
D087671525BAA8C0001FDD43 /* ExploreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreViewController.swift; sourceTree = "<group>"; };
|
||||
D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerViewController.swift; sourceTree = "<group>"; };
|
||||
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = "<group>"; };
|
||||
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePageViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -549,6 +551,7 @@
|
|||
children = (
|
||||
D05936CE25A8D79800754FDF /* EditAttachmentViewController.swift */,
|
||||
D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */,
|
||||
D087671525BAA8C0001FDD43 /* ExploreViewController.swift */,
|
||||
D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */,
|
||||
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */,
|
||||
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */,
|
||||
|
@ -916,6 +919,7 @@
|
|||
D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */,
|
||||
D0FCC105259C4E61000B67DF /* NewStatusViewController.swift in Sources */,
|
||||
D0F2D54B2581CF7D00986197 /* VisualEffectBlur.swift in Sources */,
|
||||
D087671625BAA8C0001FDD43 /* ExploreViewController.swift in Sources */,
|
||||
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */,
|
||||
D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */,
|
||||
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */,
|
||||
|
|
5
ServiceLayer/Sources/ServiceLayer/Entities/Search.swift
Normal file
5
ServiceLayer/Sources/ServiceLayer/Entities/Search.swift
Normal file
|
@ -0,0 +1,5 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import MastodonAPI
|
||||
|
||||
public typealias Search = ResultsEndpoint.Search
|
|
@ -34,7 +34,7 @@ public struct AccountListService {
|
|||
}
|
||||
|
||||
extension AccountListService: CollectionService {
|
||||
public func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error> {
|
||||
public func request(maxId: String?, minId: String?, search: Search?) -> AnyPublisher<Never, Error> {
|
||||
mastodonAPIClient.pagedRequest(endpoint, maxId: maxId, minId: minId)
|
||||
.handleEvents(receiveOutput: {
|
||||
guard let maxId = $0.info.maxId else { return }
|
||||
|
|
|
@ -12,7 +12,7 @@ public protocol CollectionService {
|
|||
var titleLocalizationComponents: AnyPublisher<[String], Never> { get }
|
||||
var navigationService: NavigationService { get }
|
||||
var markerTimeline: Marker.Timeline? { get }
|
||||
func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error>
|
||||
func request(maxId: String?, minId: String?, search: Search?) -> AnyPublisher<Never, Error>
|
||||
}
|
||||
|
||||
extension CollectionService {
|
||||
|
|
|
@ -24,7 +24,7 @@ public struct ContextService {
|
|||
}
|
||||
|
||||
extension ContextService: CollectionService {
|
||||
public func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error> {
|
||||
public func request(maxId: String?, minId: String?, search: Search?) -> AnyPublisher<Never, Error> {
|
||||
mastodonAPIClient.request(StatusEndpoint.status(id: id))
|
||||
.flatMap(contentDatabase.insert(status:))
|
||||
.merge(with: mastodonAPIClient.request(ContextEndpoint.context(id: id))
|
||||
|
|
|
@ -27,7 +27,7 @@ public struct ConversationsService {
|
|||
}
|
||||
|
||||
extension ConversationsService: CollectionService {
|
||||
public func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error> {
|
||||
public func request(maxId: String?, minId: String?, search: Search?) -> AnyPublisher<Never, Error> {
|
||||
mastodonAPIClient.pagedRequest(ConversationsEndpoint.conversations, maxId: maxId, minId: minId)
|
||||
.handleEvents(receiveOutput: {
|
||||
guard let maxId = $0.info.maxId else { return }
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import DB
|
||||
import Foundation
|
||||
import Mastodon
|
||||
import MastodonAPI
|
||||
|
||||
public struct ExploreService {
|
||||
private let mastodonAPIClient: MastodonAPIClient
|
||||
private let contentDatabase: ContentDatabase
|
||||
|
||||
init(mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||
self.mastodonAPIClient = mastodonAPIClient
|
||||
self.contentDatabase = contentDatabase
|
||||
}
|
||||
}
|
||||
|
||||
public extension ExploreService {
|
||||
func searchService() -> SearchService {
|
||||
SearchService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||
}
|
||||
}
|
|
@ -249,6 +249,10 @@ public extension IdentityService {
|
|||
contentDatabase: contentDatabase)
|
||||
}
|
||||
|
||||
func exploreService() -> ExploreService {
|
||||
ExploreService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||
}
|
||||
|
||||
func notificationsService() -> NotificationsService {
|
||||
NotificationsService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ private extension NavigationService {
|
|||
func webfinger(url: URL) -> AnyPublisher<Navigation, Never> {
|
||||
let navigationSubject = PassthroughSubject<Navigation, Never>()
|
||||
|
||||
let request = mastodonAPIClient.request(ResultsEndpoint.search(query: url.absoluteString, resolve: true))
|
||||
let request = mastodonAPIClient.request(ResultsEndpoint.search(.init(query: url.absoluteString, resolve: true)))
|
||||
.handleEvents(
|
||||
receiveSubscription: { _ in navigationSubject.send(.webfingerStart) },
|
||||
receiveCompletion: { _ in navigationSubject.send(.webfingerEnd) })
|
||||
|
|
|
@ -39,7 +39,7 @@ public struct NotificationsService {
|
|||
extension NotificationsService: CollectionService {
|
||||
public var markerTimeline: Marker.Timeline? { .notifications }
|
||||
|
||||
public func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error> {
|
||||
public func request(maxId: String?, minId: String?, search: Search?) -> AnyPublisher<Never, Error> {
|
||||
mastodonAPIClient.pagedRequest(NotificationsEndpoint.notifications, maxId: maxId, minId: minId)
|
||||
.handleEvents(receiveOutput: {
|
||||
guard let maxId = $0.info.maxId, maxId < nextPageMaxIdSubject.value else { return }
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import DB
|
||||
import Foundation
|
||||
import Mastodon
|
||||
import MastodonAPI
|
||||
|
||||
public struct SearchService {
|
||||
public let sections: AnyPublisher<[[CollectionItem]], Error>
|
||||
public let navigationService: NavigationService
|
||||
public let nextPageMaxId: AnyPublisher<String, Never>
|
||||
|
||||
private let mastodonAPIClient: MastodonAPIClient
|
||||
private let contentDatabase: ContentDatabase
|
||||
private let nextPageMaxIdSubject = PassthroughSubject<String, Never>()
|
||||
private let sectionsSubject = PassthroughSubject<[[CollectionItem]], Error>()
|
||||
|
||||
init(mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||
self.mastodonAPIClient = mastodonAPIClient
|
||||
self.contentDatabase = contentDatabase
|
||||
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
|
||||
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||
sections = sectionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchService: CollectionService {
|
||||
public func request(maxId: String?, minId: String?, search: Search?) -> AnyPublisher<Never, Error> {
|
||||
guard let search = search else { return Empty().eraseToAnyPublisher() }
|
||||
|
||||
return mastodonAPIClient.request(ResultsEndpoint.search(search))
|
||||
.flatMap(contentDatabase.process(results:))
|
||||
.handleEvents(receiveOutput: sectionsSubject.send)
|
||||
.ignoreOutput()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
|
@ -44,7 +44,7 @@ extension TimelineService: CollectionService {
|
|||
}
|
||||
}
|
||||
|
||||
public func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error> {
|
||||
public func request(maxId: String?, minId: String?, search: Search?) -> AnyPublisher<Never, Error> {
|
||||
mastodonAPIClient.pagedRequest(timeline.endpoint, maxId: maxId, minId: minId)
|
||||
.handleEvents(receiveOutput: {
|
||||
if let maxId = $0.info.maxId {
|
||||
|
|
50
View Controllers/ExploreViewController.swift
Normal file
50
View Controllers/ExploreViewController.swift
Normal file
|
@ -0,0 +1,50 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import ViewModels
|
||||
|
||||
final class ExploreViewController: UICollectionViewController {
|
||||
private let viewModel: ExploreViewModel
|
||||
private let rootViewModel: RootViewModel
|
||||
private let identification: Identification
|
||||
|
||||
init(viewModel: ExploreViewModel, rootViewModel: RootViewModel, identification: Identification) {
|
||||
self.viewModel = viewModel
|
||||
self.rootViewModel = rootViewModel
|
||||
self.identification = identification
|
||||
|
||||
super.init(collectionViewLayout: UICollectionViewFlowLayout())
|
||||
|
||||
tabBarItem = UITabBarItem(
|
||||
title: NSLocalizedString("main-navigation.explore", comment: ""),
|
||||
image: UIImage(systemName: "magnifyingglass"),
|
||||
selectedImage: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
navigationItem.title = NSLocalizedString("main-navigation.explore", comment: "")
|
||||
|
||||
let searchController = UISearchController(
|
||||
searchResultsController: TableViewController(
|
||||
viewModel: viewModel.searchViewModel,
|
||||
rootViewModel: rootViewModel,
|
||||
identification: identification,
|
||||
parentNavigationController: navigationController))
|
||||
|
||||
searchController.searchResultsUpdater = self
|
||||
navigationItem.searchController = searchController
|
||||
}
|
||||
}
|
||||
|
||||
extension ExploreViewController: UISearchResultsUpdating {
|
||||
func updateSearchResults(for searchController: UISearchController) {
|
||||
viewModel.searchViewModel.query = searchController.searchBar.text ?? ""
|
||||
}
|
||||
}
|
|
@ -54,9 +54,15 @@ final class MainNavigationViewController: UITabBarController {
|
|||
|
||||
private extension MainNavigationViewController {
|
||||
func setupViewControllers() {
|
||||
var controllers: [UIViewController] = [TimelinesViewController(
|
||||
viewModel: viewModel,
|
||||
rootViewModel: rootViewModel)]
|
||||
var controllers: [UIViewController] = [
|
||||
TimelinesViewController(
|
||||
viewModel: viewModel,
|
||||
rootViewModel: rootViewModel),
|
||||
ExploreViewController(
|
||||
viewModel: viewModel.exploreViewModel,
|
||||
rootViewModel: rootViewModel,
|
||||
identification: viewModel.identification)
|
||||
]
|
||||
|
||||
if let notificationsViewModel = viewModel.notificationsViewModel {
|
||||
let notificationsViewController = TableViewController(
|
||||
|
|
|
@ -9,10 +9,18 @@ final class ProfileViewController: TableViewController {
|
|||
private let viewModel: ProfileViewModel
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
required init(viewModel: ProfileViewModel, rootViewModel: RootViewModel, identification: Identification) {
|
||||
required init(
|
||||
viewModel: ProfileViewModel,
|
||||
rootViewModel: RootViewModel,
|
||||
identification: Identification,
|
||||
parentNavigationController: UINavigationController?) {
|
||||
self.viewModel = viewModel
|
||||
|
||||
super.init(viewModel: viewModel, rootViewModel: rootViewModel, identification: identification)
|
||||
super.init(
|
||||
viewModel: viewModel,
|
||||
rootViewModel: rootViewModel,
|
||||
identification: identification,
|
||||
parentNavigationController: parentNavigationController)
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
|
|
@ -21,15 +21,20 @@ class TableViewController: UITableViewController {
|
|||
private var cancellables = Set<AnyCancellable>()
|
||||
private var cellHeightCaches = [CGFloat: [CollectionItem: CGFloat]]()
|
||||
private var shouldKeepPlayingVideoAfterDismissal = false
|
||||
private weak var parentNavigationController: UINavigationController?
|
||||
|
||||
private lazy var dataSource: TableViewDataSource = {
|
||||
.init(tableView: tableView, viewModelProvider: viewModel.viewModel(indexPath:))
|
||||
}()
|
||||
|
||||
init(viewModel: CollectionViewModel, rootViewModel: RootViewModel, identification: Identification) {
|
||||
init(viewModel: CollectionViewModel,
|
||||
rootViewModel: RootViewModel,
|
||||
identification: Identification,
|
||||
parentNavigationController: UINavigationController? = nil) {
|
||||
self.viewModel = viewModel
|
||||
self.rootViewModel = rootViewModel
|
||||
self.identification = identification
|
||||
self.parentNavigationController = parentNavigationController
|
||||
|
||||
super.init(style: .plain)
|
||||
}
|
||||
|
@ -51,7 +56,7 @@ class TableViewController: UITableViewController {
|
|||
refreshControl = UIRefreshControl()
|
||||
refreshControl?.addAction(
|
||||
UIAction { [weak self] _ in
|
||||
self?.viewModel.request(maxId: nil, minId: nil) },
|
||||
self?.viewModel.request(maxId: nil, minId: nil, search: nil) },
|
||||
for: .valueChanged)
|
||||
}
|
||||
|
||||
|
@ -69,7 +74,7 @@ class TableViewController: UITableViewController {
|
|||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
viewModel.request(maxId: nil, minId: nil)
|
||||
viewModel.request(maxId: nil, minId: nil, search: nil)
|
||||
}
|
||||
|
||||
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
|
@ -104,7 +109,8 @@ class TableViewController: UITableViewController {
|
|||
let maxId = viewModel.preferLastPresentIdOverNextPageMaxId
|
||||
? dataSource.itemIdentifier(for: indexPath)?.itemId
|
||||
: viewModel.nextPageMaxId {
|
||||
viewModel.request(maxId: maxId, minId: nil)
|
||||
// TODO: search offset
|
||||
viewModel.request(maxId: maxId, minId: nil, search: nil)
|
||||
}
|
||||
|
||||
if let loadMoreView = cell.contentView as? LoadMoreView {
|
||||
|
@ -336,21 +342,33 @@ private extension TableViewController {
|
|||
func handle(navigation: Navigation) {
|
||||
switch navigation {
|
||||
case let .collection(collectionService):
|
||||
show(TableViewController(
|
||||
viewModel: CollectionItemsViewModel(
|
||||
collectionService: collectionService,
|
||||
identification: identification),
|
||||
rootViewModel: rootViewModel,
|
||||
let vc = TableViewController(
|
||||
viewModel: CollectionItemsViewModel(
|
||||
collectionService: collectionService,
|
||||
identification: identification),
|
||||
sender: self)
|
||||
rootViewModel: rootViewModel,
|
||||
identification: identification,
|
||||
parentNavigationController: parentNavigationController)
|
||||
|
||||
if let parentNavigationController = parentNavigationController {
|
||||
parentNavigationController.pushViewController(vc, animated: true)
|
||||
} else {
|
||||
show(vc, sender: self)
|
||||
}
|
||||
case let .profile(profileService):
|
||||
show(ProfileViewController(
|
||||
viewModel: ProfileViewModel(
|
||||
profileService: profileService,
|
||||
identification: identification),
|
||||
rootViewModel: rootViewModel,
|
||||
let vc = ProfileViewController(
|
||||
viewModel: ProfileViewModel(
|
||||
profileService: profileService,
|
||||
identification: identification),
|
||||
sender: self)
|
||||
rootViewModel: rootViewModel,
|
||||
identification: identification,
|
||||
parentNavigationController: parentNavigationController)
|
||||
|
||||
if let parentNavigationController = parentNavigationController {
|
||||
parentNavigationController.pushViewController(vc, animated: true)
|
||||
} else {
|
||||
show(vc, sender: self)
|
||||
}
|
||||
case let .url(url):
|
||||
present(SFSafariViewController(url: url), animated: true)
|
||||
case .webfingerStart:
|
||||
|
|
|
@ -35,6 +35,11 @@ final class TimelinesViewController: UIPageViewController {
|
|||
if let firstViewController = timelineViewControllers.first {
|
||||
setViewControllers([firstViewController], direction: .forward, animated: false)
|
||||
}
|
||||
|
||||
tabBarItem = UITabBarItem(
|
||||
title: NSLocalizedString("main-navigation.timelines", comment: ""),
|
||||
image: UIImage(systemName: "newspaper"),
|
||||
selectedImage: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
|
@ -48,11 +53,6 @@ final class TimelinesViewController: UIPageViewController {
|
|||
dataSource = self
|
||||
delegate = self
|
||||
|
||||
tabBarItem = UITabBarItem(
|
||||
title: NSLocalizedString("main-navigation.timelines", comment: ""),
|
||||
image: UIImage(systemName: "newspaper"),
|
||||
selectedImage: nil)
|
||||
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "megaphone"), primaryAction: nil)
|
||||
navigationItem.titleView = segmentedControl
|
||||
segmentedControl.selectedSegmentIndex = 0
|
||||
|
|
|
@ -5,7 +5,7 @@ import Foundation
|
|||
import Mastodon
|
||||
import ServiceLayer
|
||||
|
||||
public final class CollectionItemsViewModel: ObservableObject {
|
||||
public class CollectionItemsViewModel: ObservableObject {
|
||||
@Published public var alertItem: AlertItem?
|
||||
public private(set) var nextPageMaxId: String?
|
||||
|
||||
|
@ -82,7 +82,7 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
|||
|
||||
public var canRefresh: Bool { collectionService.canRefresh }
|
||||
|
||||
public func request(maxId: String? = nil, minId: String? = nil) {
|
||||
public func request(maxId: String? = nil, minId: String? = nil, search: Search?) {
|
||||
let publisher: AnyPublisher<Never, Error>
|
||||
|
||||
if let markerTimeline = collectionService.markerTimeline,
|
||||
|
@ -90,19 +90,22 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
|||
!hasRequestedUsingMarker {
|
||||
publisher = identification.service.getMarker(markerTimeline)
|
||||
.flatMap { [weak self] in
|
||||
self?.collectionService.request(maxId: $0.lastReadId, minId: nil) ?? Empty().eraseToAnyPublisher()
|
||||
self?.collectionService.request(maxId: $0.lastReadId, minId: nil, search: nil)
|
||||
?? Empty().eraseToAnyPublisher()
|
||||
}
|
||||
.catch { [weak self] _ in
|
||||
self?.collectionService.request(maxId: nil, minId: nil) ?? Empty().eraseToAnyPublisher()
|
||||
self?.collectionService.request(maxId: nil, minId: nil, search: nil)
|
||||
?? Empty().eraseToAnyPublisher()
|
||||
}
|
||||
.collect()
|
||||
.flatMap { [weak self] _ in
|
||||
self?.collectionService.request(maxId: nil, minId: nil) ?? Empty().eraseToAnyPublisher()
|
||||
self?.collectionService.request(maxId: nil, minId: nil, search: nil)
|
||||
?? Empty().eraseToAnyPublisher()
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
self.hasRequestedUsingMarker = true
|
||||
} else {
|
||||
publisher = collectionService.request(maxId: realMaxId(maxId: maxId), minId: minId)
|
||||
publisher = collectionService.request(maxId: realMaxId(maxId: maxId), minId: minId, search: search)
|
||||
}
|
||||
|
||||
publisher
|
||||
|
|
|
@ -14,7 +14,7 @@ public protocol CollectionViewModel {
|
|||
var nextPageMaxId: String? { get }
|
||||
var preferLastPresentIdOverNextPageMaxId: Bool { get }
|
||||
var canRefresh: Bool { get }
|
||||
func request(maxId: String?, minId: String?)
|
||||
func request(maxId: String?, minId: String?, search: Search?)
|
||||
func viewedAtTop(indexPath: IndexPath)
|
||||
func select(indexPath: IndexPath)
|
||||
func canSelect(indexPath: IndexPath) -> Bool
|
||||
|
|
5
ViewModels/Sources/ViewModels/Entities/Search.swift
Normal file
5
ViewModels/Sources/ViewModels/Entities/Search.swift
Normal file
|
@ -0,0 +1,5 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import ServiceLayer
|
||||
|
||||
public typealias Search = ServiceLayer.Search
|
19
ViewModels/Sources/ViewModels/ExploreViewModel.swift
Normal file
19
ViewModels/Sources/ViewModels/ExploreViewModel.swift
Normal file
|
@ -0,0 +1,19 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import ServiceLayer
|
||||
|
||||
public final class ExploreViewModel: ObservableObject {
|
||||
public let searchViewModel: SearchViewModel
|
||||
|
||||
private let exploreService: ExploreService
|
||||
private let identification: Identification
|
||||
|
||||
init(service: ExploreService, identification: Identification) {
|
||||
exploreService = service
|
||||
self.identification = identification
|
||||
searchViewModel = SearchViewModel(
|
||||
searchService: exploreService.searchService(),
|
||||
identification: identification)
|
||||
}
|
||||
}
|
|
@ -13,13 +13,23 @@ public final class NavigationViewModel: ObservableObject {
|
|||
@Published public var presentingSecondaryNavigation = false
|
||||
@Published public var alertItem: AlertItem?
|
||||
|
||||
public lazy var exploreViewModel: ExploreViewModel = {
|
||||
let exploreViewModel = ExploreViewModel(
|
||||
service: identification.service.exploreService(),
|
||||
identification: identification)
|
||||
|
||||
// TODO: initial request
|
||||
|
||||
return exploreViewModel
|
||||
}()
|
||||
|
||||
public lazy var notificationsViewModel: CollectionViewModel? = {
|
||||
if identification.identity.authenticated {
|
||||
let notificationsViewModel = CollectionItemsViewModel(
|
||||
collectionService: identification.service.notificationsService(),
|
||||
identification: identification)
|
||||
|
||||
notificationsViewModel.request(maxId: nil, minId: nil)
|
||||
notificationsViewModel.request(maxId: nil, minId: nil, search: nil)
|
||||
|
||||
return notificationsViewModel
|
||||
} else {
|
||||
|
@ -33,7 +43,7 @@ public final class NavigationViewModel: ObservableObject {
|
|||
collectionService: identification.service.conversationsService(),
|
||||
identification: identification)
|
||||
|
||||
conversationsViewModel.request(maxId: nil, minId: nil)
|
||||
conversationsViewModel.request(maxId: nil, minId: nil, search: nil)
|
||||
|
||||
return conversationsViewModel
|
||||
} else {
|
||||
|
|
|
@ -105,7 +105,7 @@ extension ProfileViewModel: CollectionViewModel {
|
|||
|
||||
public var canRefresh: Bool { collectionViewModel.value.canRefresh }
|
||||
|
||||
public func request(maxId: String?, minId: String?) {
|
||||
public func request(maxId: String?, minId: String?, search: Search?) {
|
||||
if case .statuses = collection, maxId == nil {
|
||||
profileService.fetchPinnedStatuses()
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
|
@ -113,7 +113,7 @@ extension ProfileViewModel: CollectionViewModel {
|
|||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
collectionViewModel.value.request(maxId: maxId, minId: minId)
|
||||
collectionViewModel.value.request(maxId: maxId, minId: minId, search: nil)
|
||||
}
|
||||
|
||||
public func viewedAtTop(indexPath: IndexPath) {
|
||||
|
|
27
ViewModels/Sources/ViewModels/SearchViewModel.swift
Normal file
27
ViewModels/Sources/ViewModels/SearchViewModel.swift
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import ServiceLayer
|
||||
|
||||
public final class SearchViewModel: CollectionItemsViewModel {
|
||||
@Published public var query = ""
|
||||
|
||||
private let searchService: SearchService
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
public init(searchService: SearchService, identification: Identification) {
|
||||
self.searchService = searchService
|
||||
|
||||
super.init(collectionService: searchService, identification: identification)
|
||||
|
||||
$query.throttle(for: .seconds(Self.queryThrottleInterval), scheduler: DispatchQueue.global(), latest: true)
|
||||
.sink { [weak self] in self?.request(maxId: nil, minId: nil, search: .init(query: $0, limit: Self.limit)) }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
private extension SearchViewModel {
|
||||
static let queryThrottleInterval: TimeInterval = 0.5
|
||||
static let limit = 5
|
||||
}
|
|
@ -310,7 +310,7 @@ private extension AccountHeaderView {
|
|||
segmentedControl.insertSegment(
|
||||
action: UIAction(title: collection.title) { [weak self] _ in
|
||||
self?.viewModel?.collection = collection
|
||||
self?.viewModel?.request(maxId: nil, minId: nil)
|
||||
self?.viewModel?.request(maxId: nil, minId: nil, search: nil)
|
||||
},
|
||||
at: index,
|
||||
animated: false)
|
||||
|
|
Loading…
Reference in a new issue