Account list wip
This commit is contained in:
parent
3328306c44
commit
a2f84197ef
|
@ -0,0 +1,34 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import HTTP
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
public enum AccountsEndpoint {
|
||||||
|
case statusFavouritedBy(id: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AccountsEndpoint: Endpoint {
|
||||||
|
public typealias ResultType = [Account]
|
||||||
|
|
||||||
|
public var context: [String] {
|
||||||
|
switch self {
|
||||||
|
case .statusFavouritedBy:
|
||||||
|
return defaultContext + ["statuses"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var pathComponentsInContext: [String] {
|
||||||
|
switch self {
|
||||||
|
case let .statusFavouritedBy(id):
|
||||||
|
return [id, "favourited_by"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var method: HTTPMethod {
|
||||||
|
switch self {
|
||||||
|
case .statusFavouritedBy:
|
||||||
|
return .get
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,8 @@ import MastodonAPI
|
||||||
|
|
||||||
public struct AccountListService {
|
public struct AccountListService {
|
||||||
public let accountSections: AnyPublisher<[[Account]], Error>
|
public let accountSections: AnyPublisher<[[Account]], Error>
|
||||||
public let paginates: Bool
|
public let nextPageMaxIDs: AnyPublisher<String?, Never>
|
||||||
|
public let navigationService: NavigationService
|
||||||
|
|
||||||
private let mastodonAPIClient: MastodonAPIClient
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
private let contentDatabase: ContentDatabase
|
private let contentDatabase: ContentDatabase
|
||||||
|
@ -16,7 +17,31 @@ public struct AccountListService {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AccountListService {
|
extension AccountListService {
|
||||||
|
init(favoritedByStatusID statusID: String, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||||
|
let accountSectionsSubject = PassthroughSubject<[[Account]], Error>()
|
||||||
|
let nextPageMaxIDsSubject = PassthroughSubject<String?, Never>()
|
||||||
|
|
||||||
|
self.init(
|
||||||
|
accountSections: accountSectionsSubject.eraseToAnyPublisher(),
|
||||||
|
nextPageMaxIDs: nextPageMaxIDsSubject.eraseToAnyPublisher(),
|
||||||
|
navigationService: NavigationService(
|
||||||
|
status: nil,
|
||||||
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
contentDatabase: contentDatabase),
|
||||||
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
contentDatabase: contentDatabase) { maxID, minID -> AnyPublisher<Never, Error> in
|
||||||
|
mastodonAPIClient.pagedRequest(
|
||||||
|
AccountsEndpoint.statusFavouritedBy(id: statusID), maxID: maxID, minID: minID)
|
||||||
|
.handleEvents(
|
||||||
|
receiveOutput: {
|
||||||
|
nextPageMaxIDsSubject.send($0.info.maxID)
|
||||||
|
accountSectionsSubject.send([$0.result])
|
||||||
|
},
|
||||||
|
receiveCompletion: accountSectionsSubject.send)
|
||||||
|
.flatMap { contentDatabase.insert(accounts: $0.result) }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension AccountListService {
|
public extension AccountListService {
|
||||||
|
|
|
@ -8,13 +8,13 @@ import MastodonAPI
|
||||||
|
|
||||||
public struct AccountService {
|
public struct AccountService {
|
||||||
public let account: Account
|
public let account: Account
|
||||||
public let urlService: URLService
|
public let navigationService: NavigationService
|
||||||
private let mastodonAPIClient: MastodonAPIClient
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
private let contentDatabase: ContentDatabase
|
private let contentDatabase: ContentDatabase
|
||||||
|
|
||||||
init(account: Account, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
init(account: Account, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||||
self.account = account
|
self.account = account
|
||||||
self.urlService = URLService(
|
self.navigationService = NavigationService(
|
||||||
status: nil,
|
status: nil,
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
contentDatabase: contentDatabase)
|
contentDatabase: contentDatabase)
|
||||||
|
|
|
@ -6,14 +6,13 @@ import Foundation
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import MastodonAPI
|
import MastodonAPI
|
||||||
|
|
||||||
public enum URLItem {
|
public enum Navigation {
|
||||||
case url(URL)
|
case url(URL)
|
||||||
case statusID(String)
|
case statusList(StatusListService)
|
||||||
case accountID(String)
|
case accountStatuses(AccountStatusesService)
|
||||||
case tag(String)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct URLService {
|
public struct NavigationService {
|
||||||
private let status: Status?
|
private let status: Status?
|
||||||
private let mastodonAPIClient: MastodonAPIClient
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
private let contentDatabase: ContentDatabase
|
private let contentDatabase: ContentDatabase
|
||||||
|
@ -25,21 +24,49 @@ public struct URLService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension URLService {
|
public extension NavigationService {
|
||||||
func item(url: URL) -> AnyPublisher<URLItem, Never> {
|
func item(url: URL) -> AnyPublisher<Navigation, Never> {
|
||||||
if let tag = tag(url: url) {
|
if let tag = tag(url: url) {
|
||||||
return Just(.tag(tag)).eraseToAnyPublisher()
|
return Just(
|
||||||
|
.statusList(
|
||||||
|
StatusListService(
|
||||||
|
timeline: .tag(tag),
|
||||||
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
contentDatabase: contentDatabase)))
|
||||||
|
.eraseToAnyPublisher()
|
||||||
} else if let accountID = accountID(url: url) {
|
} else if let accountID = accountID(url: url) {
|
||||||
return Just(.accountID(accountID)).eraseToAnyPublisher()
|
return Just(.accountStatuses(accountStatusesService(id: accountID))).eraseToAnyPublisher()
|
||||||
} else if mastodonAPIClient.instanceURL.host == url.host, let statusID = url.statusID {
|
} else if mastodonAPIClient.instanceURL.host == url.host, let statusID = url.statusID {
|
||||||
return Just(.statusID(statusID)).eraseToAnyPublisher()
|
return Just(
|
||||||
|
.statusList(
|
||||||
|
StatusListService(
|
||||||
|
statusID: statusID,
|
||||||
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
contentDatabase: contentDatabase)))
|
||||||
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
return Just(.url(url)).eraseToAnyPublisher()
|
return Just(.url(url)).eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func contextStatusListService(id: String) -> StatusListService {
|
||||||
|
StatusListService(statusID: id, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension URLService {
|
func accountStatusesService(id: String) -> AccountStatusesService {
|
||||||
|
AccountStatusesService(id: id, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusService(status: Status) -> StatusService {
|
||||||
|
StatusService(status: status, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
|
}
|
||||||
|
|
||||||
|
func accountService(account: Account) -> AccountService {
|
||||||
|
AccountService(account: account, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension NavigationService {
|
||||||
func tag(url: URL) -> String? {
|
func tag(url: URL) -> String? {
|
||||||
if status?.tags.first(where: { $0.url.path.lowercased() == url.path.lowercased() }) != nil {
|
if status?.tags.first(where: { $0.url.path.lowercased() == url.path.lowercased() }) != nil {
|
||||||
return url.lastPathComponent
|
return url.lastPathComponent
|
|
@ -11,6 +11,7 @@ public struct StatusListService {
|
||||||
public let nextPageMaxIDs: AnyPublisher<String?, Never>
|
public let nextPageMaxIDs: AnyPublisher<String?, Never>
|
||||||
public let contextParentID: String?
|
public let contextParentID: String?
|
||||||
public let title: String?
|
public let title: String?
|
||||||
|
public let navigationService: NavigationService
|
||||||
|
|
||||||
private let filterContext: Filter.Context
|
private let filterContext: Filter.Context
|
||||||
private let mastodonAPIClient: MastodonAPIClient
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
|
@ -41,6 +42,10 @@ extension StatusListService {
|
||||||
nextPageMaxIDs: nextPageMaxIDsSubject.eraseToAnyPublisher(),
|
nextPageMaxIDs: nextPageMaxIDsSubject.eraseToAnyPublisher(),
|
||||||
contextParentID: nil,
|
contextParentID: nil,
|
||||||
title: title,
|
title: title,
|
||||||
|
navigationService: NavigationService(
|
||||||
|
status: nil,
|
||||||
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
contentDatabase: contentDatabase),
|
||||||
filterContext: filterContext,
|
filterContext: filterContext,
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
contentDatabase: contentDatabase) { maxID, minID in
|
contentDatabase: contentDatabase) { maxID, minID in
|
||||||
|
@ -51,6 +56,29 @@ extension StatusListService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(statusID: String, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||||
|
self.init(statusSections: contentDatabase.contextObservation(parentID: statusID),
|
||||||
|
nextPageMaxIDs: Empty().eraseToAnyPublisher(),
|
||||||
|
contextParentID: statusID,
|
||||||
|
title: nil,
|
||||||
|
navigationService: NavigationService(
|
||||||
|
status: nil,
|
||||||
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
contentDatabase: contentDatabase),
|
||||||
|
filterContext: .thread,
|
||||||
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
contentDatabase: contentDatabase) { _, _ in
|
||||||
|
Publishers.Merge(
|
||||||
|
mastodonAPIClient.request(StatusEndpoint.status(id: statusID))
|
||||||
|
.flatMap(contentDatabase.insert(status:))
|
||||||
|
.eraseToAnyPublisher(),
|
||||||
|
mastodonAPIClient.request(ContextEndpoint.context(id: statusID))
|
||||||
|
.flatMap { contentDatabase.insert(context: $0, parentID: statusID) }
|
||||||
|
.eraseToAnyPublisher())
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init(
|
init(
|
||||||
accountID: String,
|
accountID: String,
|
||||||
collection: CurrentValueSubject<AccountStatusCollection, Never>,
|
collection: CurrentValueSubject<AccountStatusCollection, Never>,
|
||||||
|
@ -65,6 +93,10 @@ extension StatusListService {
|
||||||
nextPageMaxIDs: nextPageMaxIDsSubject.eraseToAnyPublisher(),
|
nextPageMaxIDs: nextPageMaxIDsSubject.eraseToAnyPublisher(),
|
||||||
contextParentID: nil,
|
contextParentID: nil,
|
||||||
title: nil,
|
title: nil,
|
||||||
|
navigationService: NavigationService(
|
||||||
|
status: nil,
|
||||||
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
contentDatabase: contentDatabase),
|
||||||
filterContext: .account,
|
filterContext: .account,
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
contentDatabase: contentDatabase) { maxID, minID in
|
contentDatabase: contentDatabase) { maxID, minID in
|
||||||
|
@ -90,7 +122,12 @@ extension StatusListService {
|
||||||
pinned: false)
|
pinned: false)
|
||||||
return mastodonAPIClient.pagedRequest(endpoint, maxID: maxID, minID: minID)
|
return mastodonAPIClient.pagedRequest(endpoint, maxID: maxID, minID: minID)
|
||||||
.handleEvents(receiveOutput: { nextPageMaxIDsSubject.send($0.info.maxID) })
|
.handleEvents(receiveOutput: { nextPageMaxIDsSubject.send($0.info.maxID) })
|
||||||
.flatMap { contentDatabase.insert(statuses: $0.result, accountID: accountID, collection: collection.value) }
|
.flatMap {
|
||||||
|
contentDatabase.insert(
|
||||||
|
statuses: $0.result,
|
||||||
|
accountID: accountID,
|
||||||
|
collection: collection.value)
|
||||||
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,35 +141,4 @@ public extension StatusListService {
|
||||||
var filters: AnyPublisher<[Filter], Error> {
|
var filters: AnyPublisher<[Filter], Error> {
|
||||||
contentDatabase.activeFiltersObservation(date: Date(), context: filterContext)
|
contentDatabase.activeFiltersObservation(date: Date(), context: filterContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusService(status: Status) -> StatusService {
|
|
||||||
StatusService(status: status, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
|
||||||
}
|
|
||||||
|
|
||||||
func service(timeline: Timeline) -> Self {
|
|
||||||
Self(timeline: timeline, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
|
||||||
}
|
|
||||||
|
|
||||||
func service(accountID: String) -> AccountStatusesService {
|
|
||||||
AccountStatusesService(id: accountID, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
|
||||||
}
|
|
||||||
|
|
||||||
func contextService(statusID: String) -> Self {
|
|
||||||
Self(statusSections: contentDatabase.contextObservation(parentID: statusID),
|
|
||||||
nextPageMaxIDs: Empty().eraseToAnyPublisher(),
|
|
||||||
contextParentID: statusID,
|
|
||||||
title: nil,
|
|
||||||
filterContext: .thread,
|
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
|
||||||
contentDatabase: contentDatabase) { _, _ in
|
|
||||||
Publishers.Merge(
|
|
||||||
mastodonAPIClient.request(StatusEndpoint.status(id: statusID))
|
|
||||||
.flatMap(contentDatabase.insert(status:))
|
|
||||||
.eraseToAnyPublisher(),
|
|
||||||
mastodonAPIClient.request(ContextEndpoint.context(id: statusID))
|
|
||||||
.flatMap { contentDatabase.insert(context: $0, parentID: statusID) }
|
|
||||||
.eraseToAnyPublisher())
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,13 @@ import MastodonAPI
|
||||||
|
|
||||||
public struct StatusService {
|
public struct StatusService {
|
||||||
public let status: Status
|
public let status: Status
|
||||||
public let urlService: URLService
|
public let navigationService: NavigationService
|
||||||
private let mastodonAPIClient: MastodonAPIClient
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
private let contentDatabase: ContentDatabase
|
private let contentDatabase: ContentDatabase
|
||||||
|
|
||||||
init(status: Status, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
init(status: Status, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||||
self.status = status
|
self.status = status
|
||||||
self.urlService = URLService(
|
self.navigationService = NavigationService(
|
||||||
status: status.displayStatus,
|
status: status.displayStatus,
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
contentDatabase: contentDatabase)
|
contentDatabase: contentDatabase)
|
||||||
|
@ -31,4 +31,11 @@ public extension StatusService {
|
||||||
.flatMap(contentDatabase.insert(status:))
|
.flatMap(contentDatabase.insert(status:))
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func favoritedByService() -> AccountListService {
|
||||||
|
AccountListService(
|
||||||
|
favoritedByStatusID: status.id,
|
||||||
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
contentDatabase: contentDatabase)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,126 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
public class AccountListViewModel: ObservableObject {
|
public final class AccountListViewModel: ObservableObject {
|
||||||
|
@Published public private(set) var items = [[CollectionItem]]()
|
||||||
|
@Published public var alertItem: AlertItem?
|
||||||
|
public let navigationEvents: AnyPublisher<NavigationEvent, Never>
|
||||||
|
public private(set) var nextPageMaxID: String?
|
||||||
|
|
||||||
|
private let accountListService: AccountListService
|
||||||
|
private var accounts = [String: Account]()
|
||||||
|
private var accountViewModelCache = [Account: (AccountViewModel, AnyCancellable)]()
|
||||||
|
private let navigationEventsSubject = PassthroughSubject<NavigationEvent, Never>()
|
||||||
|
private let loadingSubject = PassthroughSubject<Bool, Never>()
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init(accountListService: AccountListService) {
|
||||||
|
self.accountListService = accountListService
|
||||||
|
navigationEvents = navigationEventsSubject.eraseToAnyPublisher()
|
||||||
|
|
||||||
|
accountListService.accountSections
|
||||||
|
.handleEvents(receiveOutput: { [weak self] in
|
||||||
|
self?.cleanViewModelCache(newAccountSections: $0)
|
||||||
|
self?.accounts = Dictionary(uniqueKeysWithValues: Set($0.reduce([], +)).map { ($0.id, $0) })
|
||||||
|
})
|
||||||
|
.map { $0.map { $0.map { CollectionItem(id: $0.id, kind: .account) } } }
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.assign(to: &$items)
|
||||||
|
|
||||||
|
accountListService.nextPageMaxIDs
|
||||||
|
.sink { [weak self] in self?.nextPageMaxID = $0 }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AccountListViewModel: CollectionViewModel {
|
||||||
|
public var collectionItems: AnyPublisher<[[CollectionItem]], Never> { $items.eraseToAnyPublisher() }
|
||||||
|
|
||||||
|
public var title: AnyPublisher<String?, Never> { Just(nil).eraseToAnyPublisher() }
|
||||||
|
|
||||||
|
public var alertItems: AnyPublisher<AlertItem, Never> { $alertItem.compactMap { $0 }.eraseToAnyPublisher() }
|
||||||
|
|
||||||
|
public var loading: AnyPublisher<Bool, Never> { loadingSubject.eraseToAnyPublisher() }
|
||||||
|
|
||||||
|
public var maintainScrollPositionOfItem: CollectionItem? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public func request(maxID: String?, minID: String?) {
|
||||||
|
accountListService.request(maxID: maxID, minID: minID)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.handleEvents(
|
||||||
|
receiveSubscription: { [weak self] _ in self?.loadingSubject.send(true) },
|
||||||
|
receiveCompletion: { [weak self] _ in self?.loadingSubject.send(false) })
|
||||||
|
.sink { _ in }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func itemSelected(_ item: CollectionItem) {
|
||||||
|
switch item.kind {
|
||||||
|
case .account:
|
||||||
|
navigationEventsSubject.send(
|
||||||
|
.collectionNavigation(
|
||||||
|
AccountStatusesViewModel(
|
||||||
|
accountStatusesService: accountListService
|
||||||
|
.navigationService
|
||||||
|
.accountStatusesService(id: item.id))))
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func canSelect(item: CollectionItem) -> Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
public func viewModel(item: CollectionItem) -> Any? {
|
||||||
|
switch item.kind {
|
||||||
|
case .account:
|
||||||
|
return accountViewModel(id: item.id)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AccountListViewModel {
|
||||||
|
func accountViewModel(id: String) -> AccountViewModel? {
|
||||||
|
guard let account = accounts[id] else { return nil }
|
||||||
|
|
||||||
|
var accountViewModel: AccountViewModel
|
||||||
|
|
||||||
|
if let cachedViewModel = accountViewModelCache[account]?.0 {
|
||||||
|
accountViewModel = cachedViewModel
|
||||||
|
} else {
|
||||||
|
accountViewModel = AccountViewModel(
|
||||||
|
accountService: accountListService.navigationService.accountService(account: account))
|
||||||
|
accountViewModelCache[account] = (accountViewModel,
|
||||||
|
accountViewModel.events
|
||||||
|
.flatMap { $0 }
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.sink { [weak self] in
|
||||||
|
guard
|
||||||
|
let self = self,
|
||||||
|
let event = NavigationEvent($0)
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
self.navigationEventsSubject.send(event)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return accountViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanViewModelCache(newAccountSections: [[Account]]) {
|
||||||
|
let newAccounts = Set(newAccountSections.reduce([], +))
|
||||||
|
|
||||||
|
accountViewModelCache = accountViewModelCache.filter { newAccounts.contains($0.key) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,14 @@ import Mastodon
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
|
|
||||||
public class AccountViewModel: ObservableObject {
|
public class AccountViewModel: ObservableObject {
|
||||||
|
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||||
|
|
||||||
private let accountService: AccountService
|
private let accountService: AccountService
|
||||||
|
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
|
||||||
|
|
||||||
init(accountService: AccountService) {
|
init(accountService: AccountService) {
|
||||||
self.accountService = accountService
|
self.accountService = accountService
|
||||||
|
events = eventsSubject.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
|
public enum CollectionItemEvent {
|
||||||
|
case ignorableOutput
|
||||||
|
case navigation(Navigation)
|
||||||
|
case accountListNavigation(AccountListViewModel)
|
||||||
|
case share(URL)
|
||||||
|
}
|
|
@ -7,3 +7,25 @@ public enum NavigationEvent {
|
||||||
case urlNavigation(URL)
|
case urlNavigation(URL)
|
||||||
case share(URL)
|
case share(URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NavigationEvent {
|
||||||
|
init?(_ event: CollectionItemEvent) {
|
||||||
|
switch event {
|
||||||
|
case .ignorableOutput:
|
||||||
|
return nil
|
||||||
|
case let .navigation(item):
|
||||||
|
switch item {
|
||||||
|
case let .url(url):
|
||||||
|
self = .urlNavigation(url)
|
||||||
|
case let .statusList(statusListService):
|
||||||
|
self = .collectionNavigation(StatusListViewModel(statusListService: statusListService))
|
||||||
|
case let .accountStatuses(accountStatusesService):
|
||||||
|
self = .collectionNavigation(AccountStatusesViewModel(accountStatusesService: accountStatusesService))
|
||||||
|
}
|
||||||
|
case let .accountListNavigation(accountListViewModel):
|
||||||
|
self = .collectionNavigation(accountListViewModel)
|
||||||
|
case let .share(url):
|
||||||
|
self = .share(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -64,9 +64,7 @@ extension StatusListViewModel: CollectionViewModel {
|
||||||
|
|
||||||
public var alertItems: AnyPublisher<AlertItem, Never> { $alertItem.compactMap { $0 }.eraseToAnyPublisher() }
|
public var alertItems: AnyPublisher<AlertItem, Never> { $alertItem.compactMap { $0 }.eraseToAnyPublisher() }
|
||||||
|
|
||||||
public var loading: AnyPublisher<Bool, Never> {
|
public var loading: AnyPublisher<Bool, Never> { loadingSubject.eraseToAnyPublisher() }
|
||||||
loadingSubject.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func itemSelected(_ item: CollectionItem) {
|
public func itemSelected(_ item: CollectionItem) {
|
||||||
switch item.kind {
|
switch item.kind {
|
||||||
|
@ -76,7 +74,9 @@ extension StatusListViewModel: CollectionViewModel {
|
||||||
navigationEventsSubject.send(
|
navigationEventsSubject.send(
|
||||||
.collectionNavigation(
|
.collectionNavigation(
|
||||||
StatusListViewModel(
|
StatusListViewModel(
|
||||||
statusListService: statusListService.contextService(statusID: displayStatusID))))
|
statusListService: statusListService
|
||||||
|
.navigationService
|
||||||
|
.contextStatusListService(id: displayStatusID))))
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,15 @@ extension StatusListViewModel: CollectionViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension StatusListViewModel {
|
private extension StatusListViewModel {
|
||||||
|
static func filter(statusSections: [[Status]], regularExpression: String?) -> [[Status]] {
|
||||||
|
guard let regEx = regularExpression else { return statusSections }
|
||||||
|
|
||||||
|
return statusSections.map {
|
||||||
|
$0.filter { $0.filterableContent.range(of: regEx, options: [.regularExpression, .caseInsensitive]) == nil }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var contextParentID: String? { statusListService.contextParentID }
|
var contextParentID: String? { statusListService.contextParentID }
|
||||||
|
|
||||||
func statusViewModel(id: String) -> StatusViewModel? {
|
func statusViewModel(id: String) -> StatusViewModel? {
|
||||||
|
@ -111,15 +119,18 @@ public extension StatusListViewModel {
|
||||||
if let cachedViewModel = statusViewModelCache[status]?.0 {
|
if let cachedViewModel = statusViewModelCache[status]?.0 {
|
||||||
statusViewModel = cachedViewModel
|
statusViewModel = cachedViewModel
|
||||||
} else {
|
} else {
|
||||||
statusViewModel = StatusViewModel(statusService: statusListService.statusService(status: status))
|
statusViewModel = StatusViewModel(
|
||||||
|
statusService: statusListService.navigationService.statusService(status: status))
|
||||||
statusViewModelCache[status] = (statusViewModel,
|
statusViewModelCache[status] = (statusViewModel,
|
||||||
statusViewModel.events
|
statusViewModel.events
|
||||||
.flatMap { $0 }
|
.flatMap { $0 }
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.sink { [weak self] in
|
.sink { [weak self] in
|
||||||
guard let self = self,
|
guard
|
||||||
let event = self.navigationEvent(statusEvent: $0)
|
let self = self,
|
||||||
|
let event = NavigationEvent($0)
|
||||||
else { return }
|
else { return }
|
||||||
|
|
||||||
self.navigationEventsSubject.send(event)
|
self.navigationEventsSubject.send(event)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -131,44 +142,6 @@ public extension StatusListViewModel {
|
||||||
|
|
||||||
return statusViewModel
|
return statusViewModel
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private extension StatusListViewModel {
|
|
||||||
static func filter(statusSections: [[Status]], regularExpression: String?) -> [[Status]] {
|
|
||||||
guard let regEx = regularExpression else { return statusSections }
|
|
||||||
|
|
||||||
return statusSections.map {
|
|
||||||
$0.filter { $0.filterableContent.range(of: regEx, options: [.regularExpression, .caseInsensitive]) == nil }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func navigationEvent(statusEvent: StatusViewModel.Event) -> NavigationEvent? {
|
|
||||||
switch statusEvent {
|
|
||||||
case .ignorableOutput:
|
|
||||||
return nil
|
|
||||||
case let .navigation(item):
|
|
||||||
switch item {
|
|
||||||
case let .url(url):
|
|
||||||
return .urlNavigation(url)
|
|
||||||
case let .accountID(id):
|
|
||||||
return .collectionNavigation(
|
|
||||||
AccountStatusesViewModel(accountStatusesService: statusListService.service(accountID: id)))
|
|
||||||
case let .statusID(id):
|
|
||||||
return .collectionNavigation(
|
|
||||||
StatusListViewModel(
|
|
||||||
statusListService: statusListService.contextService(statusID: id)))
|
|
||||||
case let .tag(tag):
|
|
||||||
return .collectionNavigation(
|
|
||||||
StatusListViewModel(
|
|
||||||
statusListService: statusListService.service(timeline: Timeline.tag(tag))))
|
|
||||||
}
|
|
||||||
case let .accountListNavigation(accountListViewModel):
|
|
||||||
// return .collectionNavigation(accountListViewModel)
|
|
||||||
return nil
|
|
||||||
case let .share(url):
|
|
||||||
return .share(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func determineIfScrollPositionShouldBeMaintained(newStatusSections: [[Status]]) {
|
func determineIfScrollPositionShouldBeMaintained(newStatusSections: [[Status]]) {
|
||||||
maintainScrollPositionOfItem = nil // clear old value
|
maintainScrollPositionOfItem = nil // clear old value
|
||||||
|
|
|
@ -22,10 +22,10 @@ public struct StatusViewModel {
|
||||||
public var isReplyInContext = false
|
public var isReplyInContext = false
|
||||||
public var hasReplyFollowing = false
|
public var hasReplyFollowing = false
|
||||||
public var sensitiveContentToggled = false
|
public var sensitiveContentToggled = false
|
||||||
public let events: AnyPublisher<AnyPublisher<Event, Error>, Never>
|
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||||
|
|
||||||
private let statusService: StatusService
|
private let statusService: StatusService
|
||||||
private let eventsSubject = PassthroughSubject<AnyPublisher<Event, Error>, Never>()
|
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
|
||||||
|
|
||||||
init(statusService: StatusService) {
|
init(statusService: StatusService) {
|
||||||
self.statusService = statusService
|
self.statusService = statusService
|
||||||
|
@ -49,15 +49,6 @@ public struct StatusViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension StatusViewModel {
|
|
||||||
enum Event {
|
|
||||||
case ignorableOutput
|
|
||||||
case navigation(URLItem)
|
|
||||||
case accountListNavigation(AccountListViewModel)
|
|
||||||
case share(URL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension StatusViewModel {
|
public extension StatusViewModel {
|
||||||
var shouldDisplaySensitiveContent: Bool {
|
var shouldDisplaySensitiveContent: Bool {
|
||||||
if statusService.status.displayStatus.sensitive {
|
if statusService.status.displayStatus.sensitive {
|
||||||
|
@ -118,31 +109,42 @@ public extension StatusViewModel {
|
||||||
|
|
||||||
func urlSelected(_ url: URL) {
|
func urlSelected(_ url: URL) {
|
||||||
eventsSubject.send(
|
eventsSubject.send(
|
||||||
statusService.urlService.item(url: url)
|
statusService.navigationService.item(url: url)
|
||||||
.map { Event.navigation($0) }
|
.map { CollectionItemEvent.navigation($0) }
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
.eraseToAnyPublisher())
|
.eraseToAnyPublisher())
|
||||||
}
|
}
|
||||||
|
|
||||||
func accountSelected() {
|
func accountSelected() {
|
||||||
eventsSubject.send(
|
eventsSubject.send(
|
||||||
Just(Event.navigation(.accountID(statusService.status.displayStatus.account.id)))
|
Just(CollectionItemEvent.navigation(
|
||||||
|
.accountStatuses(
|
||||||
|
statusService.navigationService.accountStatusesService(
|
||||||
|
id: statusService.status.displayStatus.account.id))))
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
.eraseToAnyPublisher())
|
.eraseToAnyPublisher())
|
||||||
}
|
}
|
||||||
|
|
||||||
func favoritedBySelected() {
|
func favoritedBySelected() {
|
||||||
|
eventsSubject.send(
|
||||||
|
Just(CollectionItemEvent.accountListNavigation(
|
||||||
|
AccountListViewModel(
|
||||||
|
accountListService: statusService.favoritedByService())))
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.eraseToAnyPublisher())
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleFavorited() {
|
func toggleFavorited() {
|
||||||
eventsSubject.send(statusService.toggleFavorited().map { _ in Event.ignorableOutput }.eraseToAnyPublisher())
|
eventsSubject.send(
|
||||||
|
statusService.toggleFavorited()
|
||||||
|
.map { _ in CollectionItemEvent.ignorableOutput }
|
||||||
|
.eraseToAnyPublisher())
|
||||||
}
|
}
|
||||||
|
|
||||||
func shareStatus() {
|
func shareStatus() {
|
||||||
guard let url = statusService.status.displayStatus.url else { return }
|
guard let url = statusService.status.displayStatus.url else { return }
|
||||||
|
|
||||||
eventsSubject.send(Just(Event.share(url)).setFailureType(to: Error.self).eraseToAnyPublisher())
|
eventsSubject.send(Just(CollectionItemEvent.share(url)).setFailureType(to: Error.self).eraseToAnyPublisher())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue