mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 17:50:59 +00:00
Collections refactor WIP
This commit is contained in:
parent
90d750464b
commit
f3e1baecaa
23 changed files with 313 additions and 275 deletions
|
@ -215,7 +215,7 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func observation(timeline: Timeline) -> AnyPublisher<[[Timeline.Item]], Error> {
|
func observation(timeline: Timeline) -> AnyPublisher<[[CollectionItem]], Error> {
|
||||||
ValueObservation.tracking(
|
ValueObservation.tracking(
|
||||||
TimelineItemsInfo.request(TimelineRecord.filter(TimelineRecord.Columns.id == timeline.id)).fetchOne)
|
TimelineItemsInfo.request(TimelineRecord.filter(TimelineRecord.Columns.id == timeline.id)).fetchOne)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
|
@ -225,7 +225,7 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func contextObservation(parentID: String) -> AnyPublisher<[[Timeline.Item]], Error> {
|
func contextObservation(parentID: String) -> AnyPublisher<[[CollectionItem]], Error> {
|
||||||
ValueObservation.tracking(
|
ValueObservation.tracking(
|
||||||
ContextItemsInfo.request(StatusRecord.filter(StatusRecord.Columns.id == parentID)).fetchOne)
|
ContextItemsInfo.request(StatusRecord.filter(StatusRecord.Columns.id == parentID)).fetchOne)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
|
|
|
@ -21,7 +21,7 @@ extension ContextItemsInfo {
|
||||||
addingIncludes(request).asRequest(of: self)
|
addingIncludes(request).asRequest(of: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func items(filters: [Filter]) -> [[Timeline.Item]] {
|
func items(filters: [Filter]) -> [[CollectionItem]] {
|
||||||
let regularExpression = filters.regularExpression(context: .thread)
|
let regularExpression = filters.regularExpression(context: .thread)
|
||||||
|
|
||||||
return [ancestors, [parent], descendants].map { section in
|
return [ancestors, [parent], descendants].map { section in
|
||||||
|
|
|
@ -28,11 +28,11 @@ extension TimelineItemsInfo {
|
||||||
addingIncludes(request).asRequest(of: self)
|
addingIncludes(request).asRequest(of: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func items(filters: [Filter]) -> [[Timeline.Item]] {
|
func items(filters: [Filter]) -> [[CollectionItem]] {
|
||||||
let timeline = Timeline(record: timelineRecord)!
|
let timeline = Timeline(record: timelineRecord)!
|
||||||
let filterRegularExpression = filters.regularExpression(context: timeline.filterContext)
|
let filterRegularExpression = filters.regularExpression(context: timeline.filterContext)
|
||||||
var timelineItems = statusInfos.filtered(regularExpression: filterRegularExpression)
|
var timelineItems = statusInfos.filtered(regularExpression: filterRegularExpression)
|
||||||
.map { Timeline.Item.status(.init(status: .init(info: $0))) }
|
.map { CollectionItem.status(.init(status: .init(info: $0))) }
|
||||||
|
|
||||||
for loadMoreRecord in loadMoreRecords {
|
for loadMoreRecord in loadMoreRecords {
|
||||||
guard let index = timelineItems.firstIndex(where: {
|
guard let index = timelineItems.firstIndex(where: {
|
||||||
|
@ -51,7 +51,7 @@ extension TimelineItemsInfo {
|
||||||
|
|
||||||
if let pinnedStatusInfos = pinnedStatusesInfo?.pinnedStatusInfos {
|
if let pinnedStatusInfos = pinnedStatusesInfo?.pinnedStatusInfos {
|
||||||
return [pinnedStatusInfos.filtered(regularExpression: filterRegularExpression)
|
return [pinnedStatusInfos.filtered(regularExpression: filterRegularExpression)
|
||||||
.map { Timeline.Item.status(.init(status: .init(info: $0), pinned: true)) },
|
.map { CollectionItem.status(.init(status: .init(info: $0), pinned: true)) },
|
||||||
timelineItems]
|
timelineItems]
|
||||||
} else {
|
} else {
|
||||||
return [timelineItems]
|
return [timelineItems]
|
||||||
|
|
25
DB/Sources/DB/Entities/CollectionItem.swift
Normal file
25
DB/Sources/DB/Entities/CollectionItem.swift
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
public enum CollectionItem: Hashable {
|
||||||
|
case status(StatusConfiguration)
|
||||||
|
case loadMore(LoadMore)
|
||||||
|
case account(Account)
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension CollectionItem {
|
||||||
|
struct StatusConfiguration: Hashable {
|
||||||
|
public let status: Status
|
||||||
|
public let pinned: Bool
|
||||||
|
public let isReplyInContext: Bool
|
||||||
|
public let hasReplyFollowing: Bool
|
||||||
|
|
||||||
|
init(status: Status, pinned: Bool = false, isReplyInContext: Bool = false, hasReplyFollowing: Bool = false) {
|
||||||
|
self.status = status
|
||||||
|
self.pinned = pinned
|
||||||
|
self.isReplyInContext = isReplyInContext
|
||||||
|
self.hasReplyFollowing = hasReplyFollowing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,11 +16,6 @@ public extension Timeline {
|
||||||
static let unauthenticatedDefaults: [Timeline] = [.local, .federated]
|
static let unauthenticatedDefaults: [Timeline] = [.local, .federated]
|
||||||
static let authenticatedDefaults: [Timeline] = [.home, .local, .federated]
|
static let authenticatedDefaults: [Timeline] = [.home, .local, .federated]
|
||||||
|
|
||||||
enum Item: Hashable {
|
|
||||||
case status(StatusConfiguration)
|
|
||||||
case loadMore(LoadMore)
|
|
||||||
}
|
|
||||||
|
|
||||||
var filterContext: Filter.Context {
|
var filterContext: Filter.Context {
|
||||||
switch self {
|
switch self {
|
||||||
case .home, .list:
|
case .home, .list:
|
||||||
|
@ -33,22 +28,6 @@ public extension Timeline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Timeline.Item {
|
|
||||||
struct StatusConfiguration: Hashable {
|
|
||||||
public let status: Status
|
|
||||||
public let pinned: Bool
|
|
||||||
public let isReplyInContext: Bool
|
|
||||||
public let hasReplyFollowing: Bool
|
|
||||||
|
|
||||||
init(status: Status, pinned: Bool = false, isReplyInContext: Bool = false, hasReplyFollowing: Bool = false) {
|
|
||||||
self.status = status
|
|
||||||
self.pinned = pinned
|
|
||||||
self.isReplyInContext = isReplyInContext
|
|
||||||
self.hasReplyFollowing = hasReplyFollowing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Timeline: Identifiable {
|
extension Timeline: Identifiable {
|
||||||
public var id: String {
|
public var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import DB
|
||||||
|
|
||||||
|
public typealias CollectionItem = DB.CollectionItem
|
|
@ -6,8 +6,8 @@ import Foundation
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import MastodonAPI
|
import MastodonAPI
|
||||||
|
|
||||||
public struct AccountListService {
|
public struct AccountListService: CollectionService {
|
||||||
public let accountSections: AnyPublisher<[[Account]], Error>
|
public let sections: AnyPublisher<[[CollectionItem]], Error>
|
||||||
public let nextPageMaxIDs: AnyPublisher<String?, Never>
|
public let nextPageMaxIDs: AnyPublisher<String?, Never>
|
||||||
public let navigationService: NavigationService
|
public let navigationService: NavigationService
|
||||||
|
|
||||||
|
@ -29,7 +29,9 @@ extension AccountListService {
|
||||||
let nextPageMaxIDsSubject = PassthroughSubject<String?, Never>()
|
let nextPageMaxIDsSubject = PassthroughSubject<String?, Never>()
|
||||||
|
|
||||||
self.init(
|
self.init(
|
||||||
accountSections: contentDatabase.accountListObservation(list).map { [$0] }.eraseToAnyPublisher(),
|
sections: contentDatabase.accountListObservation(list)
|
||||||
|
.map { [$0.map { CollectionItem.account($0) }] }
|
||||||
|
.eraseToAnyPublisher(),
|
||||||
nextPageMaxIDs: nextPageMaxIDsSubject.eraseToAnyPublisher(),
|
nextPageMaxIDs: nextPageMaxIDsSubject.eraseToAnyPublisher(),
|
||||||
navigationService: NavigationService(
|
navigationService: NavigationService(
|
||||||
status: nil,
|
status: nil,
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
public protocol CollectionService {
|
||||||
|
var sections: AnyPublisher<[[CollectionItem]], Error> { get }
|
||||||
|
var nextPageMaxIDs: AnyPublisher<String?, Never> { get }
|
||||||
|
var navigationService: NavigationService { get }
|
||||||
|
var title: String? { get }
|
||||||
|
var contextParentID: String? { get }
|
||||||
|
func request(maxID: String?, minID: String?) -> AnyPublisher<Never, Error>
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CollectionService {
|
||||||
|
public var title: String? { nil }
|
||||||
|
public var contextParentID: String? { nil }
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ import MastodonAPI
|
||||||
|
|
||||||
public enum Navigation {
|
public enum Navigation {
|
||||||
case url(URL)
|
case url(URL)
|
||||||
case statusList(StatusListService)
|
case collection(CollectionService)
|
||||||
case profile(ProfileService)
|
case profile(ProfileService)
|
||||||
case webfingerStart
|
case webfingerStart
|
||||||
case webfingerEnd
|
case webfingerEnd
|
||||||
|
@ -30,7 +30,7 @@ public extension NavigationService {
|
||||||
func item(url: URL) -> AnyPublisher<Navigation, Never> {
|
func item(url: URL) -> AnyPublisher<Navigation, Never> {
|
||||||
if let tag = tag(url: url) {
|
if let tag = tag(url: url) {
|
||||||
return Just(
|
return Just(
|
||||||
.statusList(
|
.collection(
|
||||||
StatusListService(
|
StatusListService(
|
||||||
timeline: .tag(tag),
|
timeline: .tag(tag),
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
@ -40,7 +40,7 @@ public extension NavigationService {
|
||||||
return Just(.profile(profileService(id: accountID))).eraseToAnyPublisher()
|
return Just(.profile(profileService(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(
|
return Just(
|
||||||
.statusList(
|
.collection(
|
||||||
StatusListService(
|
StatusListService(
|
||||||
statusID: statusID,
|
statusID: statusID,
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
@ -112,7 +112,7 @@ private extension NavigationService {
|
||||||
receiveCompletion: { _ in navigationSubject.send(.webfingerEnd) })
|
receiveCompletion: { _ in navigationSubject.send(.webfingerEnd) })
|
||||||
.map { results -> Navigation in
|
.map { results -> Navigation in
|
||||||
if let tag = results.hashtags.first {
|
if let tag = results.hashtags.first {
|
||||||
return .statusList(
|
return .collection(
|
||||||
StatusListService(
|
StatusListService(
|
||||||
timeline: .tag(tag.name),
|
timeline: .tag(tag.name),
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
@ -120,7 +120,7 @@ private extension NavigationService {
|
||||||
} else if let account = results.accounts.first {
|
} else if let account = results.accounts.first {
|
||||||
return .profile(profileService(account: account))
|
return .profile(profileService(account: account))
|
||||||
} else if let status = results.statuses.first {
|
} else if let status = results.statuses.first {
|
||||||
return .statusList(
|
return .collection(
|
||||||
StatusListService(
|
StatusListService(
|
||||||
statusID: status.id,
|
statusID: status.id,
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
|
|
@ -6,8 +6,8 @@ import Foundation
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import MastodonAPI
|
import MastodonAPI
|
||||||
|
|
||||||
public struct StatusListService {
|
public struct StatusListService: CollectionService {
|
||||||
public let sections: AnyPublisher<[[Timeline.Item]], Error>
|
public let sections: AnyPublisher<[[CollectionItem]], Error>
|
||||||
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?
|
||||||
|
|
|
@ -15,11 +15,13 @@ class TableViewController: UITableViewController {
|
||||||
DispatchQueue(label: "com.metabolist.metatext.collection.data-source-queue")
|
DispatchQueue(label: "com.metabolist.metatext.collection.data-source-queue")
|
||||||
|
|
||||||
private lazy var dataSource: UITableViewDiffableDataSource<Int, CollectionItemIdentifier> = {
|
private lazy var dataSource: UITableViewDiffableDataSource<Int, CollectionItemIdentifier> = {
|
||||||
UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, item in
|
UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, identifier in
|
||||||
guard let self = self, let cellViewModel = self.viewModel.viewModel(item: item) else { return nil }
|
guard let self = self,
|
||||||
|
let cellViewModel = self.viewModel.viewModel(identifier: identifier)
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
let cell = tableView.dequeueReusableCell(
|
let cell = tableView.dequeueReusableCell(
|
||||||
withIdentifier: String(describing: item.kind.cellClass),
|
withIdentifier: String(describing: identifier.kind.cellClass),
|
||||||
for: indexPath)
|
for: indexPath)
|
||||||
|
|
||||||
switch (cell, cellViewModel) {
|
switch (cell, cellViewModel) {
|
||||||
|
@ -111,17 +113,17 @@ class TableViewController: UITableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
|
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return true }
|
guard let identifier = dataSource.itemIdentifier(for: indexPath) else { return true }
|
||||||
|
|
||||||
return viewModel.canSelect(item: item)
|
return viewModel.canSelect(identifier: identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
tableView.deselectRow(at: indexPath, animated: true)
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
guard let identifier = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
|
||||||
viewModel.itemSelected(item)
|
viewModel.select(identifier: identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLayoutSubviews() {
|
override func viewDidLayoutSubviews() {
|
||||||
|
@ -185,7 +187,7 @@ private extension TableViewController {
|
||||||
func setupViewModelBindings() {
|
func setupViewModelBindings() {
|
||||||
viewModel.title.sink { [weak self] in self?.navigationItem.title = $0 }.store(in: &cancellables)
|
viewModel.title.sink { [weak self] in self?.navigationItem.title = $0 }.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.collectionItems.sink { [weak self] in self?.update(items: $0) }.store(in: &cancellables)
|
viewModel.sections.sink { [weak self] in self?.update(items: $0) }.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.navigationEvents.receive(on: DispatchQueue.main).sink { [weak self] in
|
viewModel.navigationEvents.receive(on: DispatchQueue.main).sink { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
|
@ -1,130 +1,130 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Combine
|
//import Combine
|
||||||
import Foundation
|
//import Foundation
|
||||||
import Mastodon
|
//import Mastodon
|
||||||
import ServiceLayer
|
//import ServiceLayer
|
||||||
|
//
|
||||||
public final class AccountListViewModel: ObservableObject {
|
//public final class AccountListViewModel: ObservableObject {
|
||||||
@Published public private(set) var items = [[CollectionItemIdentifier]]()
|
// @Published public private(set) var items = [[CollectionItemIdentifier]]()
|
||||||
@Published public var alertItem: AlertItem?
|
// @Published public var alertItem: AlertItem?
|
||||||
public let navigationEvents: AnyPublisher<NavigationEvent, Never>
|
// public let navigationEvents: AnyPublisher<NavigationEvent, Never>
|
||||||
public private(set) var nextPageMaxID: String?
|
// public private(set) var nextPageMaxID: String?
|
||||||
|
//
|
||||||
private let accountListService: AccountListService
|
// private let accountListService: AccountListService
|
||||||
private var accounts = [String: Account]()
|
// private var accounts = [String: Account]()
|
||||||
private var accountViewModelCache = [Account: (AccountViewModel, AnyCancellable)]()
|
// private var accountViewModelCache = [Account: (AccountViewModel, AnyCancellable)]()
|
||||||
private let navigationEventsSubject = PassthroughSubject<NavigationEvent, Never>()
|
// private let navigationEventsSubject = PassthroughSubject<NavigationEvent, Never>()
|
||||||
private let loadingSubject = PassthroughSubject<Bool, Never>()
|
// private let loadingSubject = PassthroughSubject<Bool, Never>()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
// private var cancellables = Set<AnyCancellable>()
|
||||||
|
//
|
||||||
init(accountListService: AccountListService) {
|
// init(accountListService: AccountListService) {
|
||||||
self.accountListService = accountListService
|
// self.accountListService = accountListService
|
||||||
navigationEvents = navigationEventsSubject.eraseToAnyPublisher()
|
// navigationEvents = navigationEventsSubject.eraseToAnyPublisher()
|
||||||
|
//
|
||||||
accountListService.accountSections
|
// accountListService.accountSections
|
||||||
.handleEvents(receiveOutput: { [weak self] in
|
// .handleEvents(receiveOutput: { [weak self] in
|
||||||
self?.cleanViewModelCache(newAccountSections: $0)
|
// self?.cleanViewModelCache(newAccountSections: $0)
|
||||||
self?.accounts = Dictionary(uniqueKeysWithValues: Set($0.reduce([], +)).map { ($0.id, $0) })
|
// self?.accounts = Dictionary(uniqueKeysWithValues: Set($0.reduce([], +)).map { ($0.id, $0) })
|
||||||
})
|
// })
|
||||||
.map { $0.map { $0.map(CollectionItemIdentifier.init(account:)) } }
|
// .map { $0.map { $0.map(CollectionItemIdentifier.init(account:)) } }
|
||||||
.receive(on: DispatchQueue.main)
|
// .receive(on: DispatchQueue.main)
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
// .assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.assign(to: &$items)
|
// .assign(to: &$items)
|
||||||
|
//
|
||||||
accountListService.nextPageMaxIDs
|
// accountListService.nextPageMaxIDs
|
||||||
.sink { [weak self] in self?.nextPageMaxID = $0 }
|
// .sink { [weak self] in self?.nextPageMaxID = $0 }
|
||||||
.store(in: &cancellables)
|
// .store(in: &cancellables)
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
extension AccountListViewModel: CollectionViewModel {
|
//extension AccountListViewModel: CollectionViewModel {
|
||||||
public var collectionItems: AnyPublisher<[[CollectionItemIdentifier]], Never> { $items.eraseToAnyPublisher() }
|
// public var collectionItems: AnyPublisher<[[CollectionItemIdentifier]], Never> { $items.eraseToAnyPublisher() }
|
||||||
|
//
|
||||||
public var title: AnyPublisher<String?, Never> { Just(nil).eraseToAnyPublisher() }
|
// public var title: AnyPublisher<String?, Never> { Just(nil).eraseToAnyPublisher() }
|
||||||
|
//
|
||||||
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> { loadingSubject.eraseToAnyPublisher() }
|
// public var loading: AnyPublisher<Bool, Never> { loadingSubject.eraseToAnyPublisher() }
|
||||||
|
//
|
||||||
public var maintainScrollPositionOfItem: CollectionItemIdentifier? {
|
// public var maintainScrollPositionOfItem: CollectionItemIdentifier? {
|
||||||
nil
|
// nil
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
public func request(maxID: String?, minID: String?) {
|
// public func request(maxID: String?, minID: String?) {
|
||||||
accountListService.request(maxID: maxID, minID: minID)
|
// accountListService.request(maxID: maxID, minID: minID)
|
||||||
.receive(on: DispatchQueue.main)
|
// .receive(on: DispatchQueue.main)
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
// .assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.handleEvents(
|
// .handleEvents(
|
||||||
receiveSubscription: { [weak self] _ in self?.loadingSubject.send(true) },
|
// receiveSubscription: { [weak self] _ in self?.loadingSubject.send(true) },
|
||||||
receiveCompletion: { [weak self] _ in self?.loadingSubject.send(false) })
|
// receiveCompletion: { [weak self] _ in self?.loadingSubject.send(false) })
|
||||||
.sink { _ in }
|
// .sink { _ in }
|
||||||
.store(in: &cancellables)
|
// .store(in: &cancellables)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
public func itemSelected(_ item: CollectionItemIdentifier) {
|
// public func itemSelected(_ item: CollectionItemIdentifier) {
|
||||||
switch item.kind {
|
// switch item.kind {
|
||||||
case .account:
|
// case .account:
|
||||||
let navigationService = accountListService.navigationService
|
// let navigationService = accountListService.navigationService
|
||||||
let profileService: ProfileService
|
// let profileService: ProfileService
|
||||||
|
//
|
||||||
if let account = accounts[item.id] {
|
// if let account = accounts[item.id] {
|
||||||
profileService = navigationService.profileService(account: account)
|
// profileService = navigationService.profileService(account: account)
|
||||||
} else {
|
// } else {
|
||||||
profileService = navigationService.profileService(id: item.id)
|
// profileService = navigationService.profileService(id: item.id)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
navigationEventsSubject.send(.profileNavigation(ProfileViewModel(profileService: profileService)))
|
// navigationEventsSubject.send(.profileNavigation(ProfileViewModel(profileService: profileService)))
|
||||||
default:
|
// default:
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
public func canSelect(item: CollectionItemIdentifier) -> Bool {
|
// public func canSelect(item: CollectionItemIdentifier) -> Bool {
|
||||||
true
|
// true
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
public func viewModel(item: CollectionItemIdentifier) -> Any? {
|
// public func viewModel(item: CollectionItemIdentifier) -> CollectionItemViewModel? {
|
||||||
switch item.kind {
|
// switch item.kind {
|
||||||
case .account:
|
// case .account:
|
||||||
return accountViewModel(id: item.id)
|
// return accountViewModel(id: item.id)
|
||||||
default:
|
// default:
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
private extension AccountListViewModel {
|
//private extension AccountListViewModel {
|
||||||
func accountViewModel(id: String) -> AccountViewModel? {
|
// func accountViewModel(id: String) -> AccountViewModel? {
|
||||||
guard let account = accounts[id] else { return nil }
|
// guard let account = accounts[id] else { return nil }
|
||||||
|
//
|
||||||
var accountViewModel: AccountViewModel
|
// var accountViewModel: AccountViewModel
|
||||||
|
//
|
||||||
if let cachedViewModel = accountViewModelCache[account]?.0 {
|
// if let cachedViewModel = accountViewModelCache[account]?.0 {
|
||||||
accountViewModel = cachedViewModel
|
// accountViewModel = cachedViewModel
|
||||||
} else {
|
// } else {
|
||||||
accountViewModel = AccountViewModel(
|
// accountViewModel = AccountViewModel(
|
||||||
accountService: accountListService.navigationService.accountService(account: account))
|
// accountService: accountListService.navigationService.accountService(account: account))
|
||||||
accountViewModelCache[account] = (accountViewModel,
|
// accountViewModelCache[account] = (accountViewModel,
|
||||||
accountViewModel.events
|
// accountViewModel.events
|
||||||
.flatMap { $0 }
|
// .flatMap { $0 }
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
// .assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.sink { [weak self] in
|
// .sink { [weak self] in
|
||||||
guard
|
// guard
|
||||||
let self = self,
|
// let self = self,
|
||||||
let event = NavigationEvent($0)
|
// let event = NavigationEvent($0)
|
||||||
else { return }
|
// else { return }
|
||||||
|
//
|
||||||
self.navigationEventsSubject.send(event)
|
// self.navigationEventsSubject.send(event)
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
return accountViewModel
|
// return accountViewModel
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
func cleanViewModelCache(newAccountSections: [[Account]]) {
|
// func cleanViewModelCache(newAccountSections: [[Account]]) {
|
||||||
let newAccounts = Set(newAccountSections.reduce([], +))
|
// let newAccounts = Set(newAccountSections.reduce([], +))
|
||||||
|
//
|
||||||
accountViewModelCache = accountViewModelCache.filter { newAccounts.contains($0.key) }
|
// accountViewModelCache = accountViewModelCache.filter { newAccounts.contains($0.key) }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import Foundation
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
|
|
||||||
public class AccountViewModel: ObservableObject {
|
public struct AccountViewModel: CollectionItemViewModel {
|
||||||
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||||
|
|
||||||
private let accountService: AccountService
|
private let accountService: AccountService
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public protocol CollectionItemViewModel {
|
||||||
|
var events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never> { get }
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public protocol CollectionViewModel {
|
public protocol CollectionViewModel {
|
||||||
var collectionItems: AnyPublisher<[[CollectionItemIdentifier]], Never> { get }
|
var sections: AnyPublisher<[[CollectionItemIdentifier]], Never> { get }
|
||||||
var title: AnyPublisher<String?, Never> { get }
|
var title: AnyPublisher<String?, Never> { get }
|
||||||
var alertItems: AnyPublisher<AlertItem, Never> { get }
|
var alertItems: AnyPublisher<AlertItem, Never> { get }
|
||||||
var loading: AnyPublisher<Bool, Never> { get }
|
var loading: AnyPublisher<Bool, Never> { get }
|
||||||
|
@ -12,7 +12,7 @@ public protocol CollectionViewModel {
|
||||||
var nextPageMaxID: String? { get }
|
var nextPageMaxID: String? { get }
|
||||||
var maintainScrollPositionOfItem: CollectionItemIdentifier? { get }
|
var maintainScrollPositionOfItem: CollectionItemIdentifier? { get }
|
||||||
func request(maxID: String?, minID: String?)
|
func request(maxID: String?, minID: String?)
|
||||||
func itemSelected(_ item: CollectionItemIdentifier)
|
func select(identifier: CollectionItemIdentifier)
|
||||||
func canSelect(item: CollectionItemIdentifier) -> Bool
|
func canSelect(identifier: CollectionItemIdentifier) -> Bool
|
||||||
func viewModel(item: CollectionItemIdentifier) -> Any?
|
func viewModel(identifier: CollectionItemIdentifier) -> CollectionItemViewModel?
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,5 @@ import ServiceLayer
|
||||||
public enum CollectionItemEvent {
|
public enum CollectionItemEvent {
|
||||||
case ignorableOutput
|
case ignorableOutput
|
||||||
case navigation(Navigation)
|
case navigation(Navigation)
|
||||||
case accountListNavigation(AccountListViewModel)
|
|
||||||
case share(URL)
|
case share(URL)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
public struct CollectionItemIdentifier: Hashable {
|
public struct CollectionItemIdentifier: Hashable {
|
||||||
public let id: String
|
public let id: String
|
||||||
|
@ -21,8 +22,8 @@ public extension CollectionItemIdentifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CollectionItemIdentifier {
|
extension CollectionItemIdentifier {
|
||||||
init(timelineItem: Timeline.Item) {
|
init(item: CollectionItem) {
|
||||||
switch timelineItem {
|
switch item {
|
||||||
case let .status(configuration):
|
case let .status(configuration):
|
||||||
id = configuration.status.id
|
id = configuration.status.id
|
||||||
kind = .status
|
kind = .status
|
||||||
|
@ -31,12 +32,10 @@ extension CollectionItemIdentifier {
|
||||||
id = loadMore.afterStatusId
|
id = loadMore.afterStatusId
|
||||||
kind = .loadMore
|
kind = .loadMore
|
||||||
info = [:]
|
info = [:]
|
||||||
}
|
case let .account(account):
|
||||||
}
|
|
||||||
|
|
||||||
init(account: Account) {
|
|
||||||
id = account.id
|
id = account.id
|
||||||
kind = .account
|
kind = .account
|
||||||
info = [:]
|
info = [:]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,8 @@ extension NavigationEvent {
|
||||||
switch item {
|
switch item {
|
||||||
case let .url(url):
|
case let .url(url):
|
||||||
self = .urlNavigation(url)
|
self = .urlNavigation(url)
|
||||||
case let .statusList(statusListService):
|
case let .collection(statusListService):
|
||||||
self = .collectionNavigation(StatusListViewModel(statusListService: statusListService))
|
self = .collectionNavigation(ListViewModel(collectionService: statusListService))
|
||||||
case let .profile(profileService):
|
case let .profile(profileService):
|
||||||
self = .profileNavigation(ProfileViewModel(profileService: profileService))
|
self = .profileNavigation(ProfileViewModel(profileService: profileService))
|
||||||
case .webfingerStart:
|
case .webfingerStart:
|
||||||
|
@ -29,8 +29,6 @@ extension NavigationEvent {
|
||||||
case .webfingerEnd:
|
case .webfingerEnd:
|
||||||
self = .webfingerEnd
|
self = .webfingerEnd
|
||||||
}
|
}
|
||||||
case let .accountListNavigation(accountListViewModel):
|
|
||||||
self = .collectionNavigation(accountListViewModel)
|
|
||||||
case let .share(url):
|
case let .share(url):
|
||||||
self = .share(url)
|
self = .share(url)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,39 +5,39 @@ import Foundation
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
|
|
||||||
final public class StatusListViewModel: ObservableObject {
|
final public class ListViewModel: ObservableObject {
|
||||||
@Published public private(set) var items = [[CollectionItemIdentifier]]()
|
@Published public private(set) var identifiers = [[CollectionItemIdentifier]]()
|
||||||
@Published public var alertItem: AlertItem?
|
@Published public var alertItem: AlertItem?
|
||||||
public private(set) var nextPageMaxID: String?
|
public private(set) var nextPageMaxID: String?
|
||||||
public private(set) var maintainScrollPositionOfItem: CollectionItemIdentifier?
|
public private(set) var maintainScrollPositionOfItem: CollectionItemIdentifier?
|
||||||
|
|
||||||
private var timelineItems = [CollectionItemIdentifier: Timeline.Item]()
|
private var items = [CollectionItemIdentifier: CollectionItem]()
|
||||||
private let statusListService: StatusListService
|
private let collectionService: CollectionService
|
||||||
private var viewModelCache = [Timeline.Item: (Any, AnyCancellable)]()
|
private var viewModelCache = [CollectionItem: (CollectionItemViewModel, AnyCancellable)]()
|
||||||
private let navigationEventsSubject = PassthroughSubject<NavigationEvent, Never>()
|
private let navigationEventsSubject = PassthroughSubject<NavigationEvent, Never>()
|
||||||
private let loadingSubject = PassthroughSubject<Bool, Never>()
|
private let loadingSubject = PassthroughSubject<Bool, Never>()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(statusListService: StatusListService) {
|
init(collectionService: CollectionService) {
|
||||||
self.statusListService = statusListService
|
self.collectionService = collectionService
|
||||||
|
|
||||||
statusListService.sections
|
collectionService.sections
|
||||||
.handleEvents(receiveOutput: { [weak self] in self?.process(sections: $0) })
|
.handleEvents(receiveOutput: { [weak self] in self?.process(sections: $0) })
|
||||||
.map { $0.map { $0.map(CollectionItemIdentifier.init(timelineItem:)) } }
|
.map { $0.map { $0.map(CollectionItemIdentifier.init(item:)) } }
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.assign(to: &$items)
|
.assign(to: &$identifiers)
|
||||||
|
|
||||||
statusListService.nextPageMaxIDs
|
collectionService.nextPageMaxIDs
|
||||||
.sink { [weak self] in self?.nextPageMaxID = $0 }
|
.sink { [weak self] in self?.nextPageMaxID = $0 }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusListViewModel: CollectionViewModel {
|
extension ListViewModel: CollectionViewModel {
|
||||||
public var collectionItems: AnyPublisher<[[CollectionItemIdentifier]], Never> { $items.eraseToAnyPublisher() }
|
public var sections: AnyPublisher<[[CollectionItemIdentifier]], Never> { $identifiers.eraseToAnyPublisher() }
|
||||||
|
|
||||||
public var title: AnyPublisher<String?, Never> { Just(statusListService.title).eraseToAnyPublisher() }
|
public var title: AnyPublisher<String?, Never> { Just(collectionService.title).eraseToAnyPublisher() }
|
||||||
|
|
||||||
public var alertItems: AnyPublisher<AlertItem, Never> { $alertItem.compactMap { $0 }.eraseToAnyPublisher() }
|
public var alertItems: AnyPublisher<AlertItem, Never> { $alertItem.compactMap { $0 }.eraseToAnyPublisher() }
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ extension StatusListViewModel: CollectionViewModel {
|
||||||
public var navigationEvents: AnyPublisher<NavigationEvent, Never> { navigationEventsSubject.eraseToAnyPublisher() }
|
public var navigationEvents: AnyPublisher<NavigationEvent, Never> { navigationEventsSubject.eraseToAnyPublisher() }
|
||||||
|
|
||||||
public func request(maxID: String? = nil, minID: String? = nil) {
|
public func request(maxID: String? = nil, minID: String? = nil) {
|
||||||
statusListService.request(maxID: maxID, minID: minID)
|
collectionService.request(maxID: maxID, minID: minID)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.handleEvents(
|
.handleEvents(
|
||||||
|
@ -56,45 +56,50 @@ extension StatusListViewModel: CollectionViewModel {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func itemSelected(_ item: CollectionItemIdentifier) {
|
public func select(identifier: CollectionItemIdentifier) {
|
||||||
guard let timelineItem = timelineItems[item] else { return }
|
guard let item = items[identifier] else { return }
|
||||||
|
|
||||||
switch timelineItem {
|
switch item {
|
||||||
case let .status(configuration):
|
case let .status(configuration):
|
||||||
navigationEventsSubject.send(
|
navigationEventsSubject.send(
|
||||||
.collectionNavigation(
|
.collectionNavigation(
|
||||||
StatusListViewModel(
|
ListViewModel(
|
||||||
statusListService: statusListService
|
collectionService: collectionService
|
||||||
.navigationService
|
.navigationService
|
||||||
.contextStatusListService(id: configuration.status.displayStatus.id))))
|
.contextStatusListService(id: configuration.status.displayStatus.id))))
|
||||||
case .loadMore:
|
case .loadMore:
|
||||||
loadMoreViewModel(item: item)?.loadMore()
|
loadMoreViewModel(item: identifier)?.loadMore()
|
||||||
|
case let .account(account):
|
||||||
|
navigationEventsSubject.send(
|
||||||
|
.profileNavigation(
|
||||||
|
ProfileViewModel(
|
||||||
|
profileService: collectionService.navigationService.profileService(account: account))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func canSelect(item: CollectionItemIdentifier) -> Bool {
|
public func canSelect(identifier: CollectionItemIdentifier) -> Bool {
|
||||||
if case .status = item.kind, item.id == statusListService.contextParentID {
|
if case .status = identifier.kind, identifier.id == collectionService.contextParentID {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
public func viewModel(item: CollectionItemIdentifier) -> Any? {
|
public func viewModel(identifier: CollectionItemIdentifier) -> CollectionItemViewModel? {
|
||||||
switch item.kind {
|
switch identifier.kind {
|
||||||
case .status:
|
case .status:
|
||||||
return statusViewModel(item: item)
|
return statusViewModel(item: identifier)
|
||||||
case .loadMore:
|
case .loadMore:
|
||||||
return loadMoreViewModel(item: item)
|
return loadMoreViewModel(item: identifier)
|
||||||
default:
|
case .account:
|
||||||
return nil
|
return accountViewModel(item: identifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension StatusListViewModel {
|
private extension ListViewModel {
|
||||||
func statusViewModel(item: CollectionItemIdentifier) -> StatusViewModel? {
|
func statusViewModel(item: CollectionItemIdentifier) -> StatusViewModel? {
|
||||||
guard let timelineItem = timelineItems[item],
|
guard let timelineItem = items[item],
|
||||||
case let .status(configuration) = timelineItem
|
case let .status(configuration) = timelineItem
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
|
@ -104,22 +109,11 @@ private extension StatusListViewModel {
|
||||||
statusViewModel = cachedViewModel
|
statusViewModel = cachedViewModel
|
||||||
} else {
|
} else {
|
||||||
statusViewModel = StatusViewModel(
|
statusViewModel = StatusViewModel(
|
||||||
statusService: statusListService.navigationService.statusService(status: configuration.status))
|
statusService: collectionService.navigationService.statusService(status: configuration.status))
|
||||||
viewModelCache[timelineItem] = (statusViewModel,
|
cache(viewModel: statusViewModel, forItem: timelineItem)
|
||||||
statusViewModel.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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
statusViewModel.isContextParent = configuration.status.id == statusListService.contextParentID
|
statusViewModel.isContextParent = configuration.status.id == collectionService.contextParentID
|
||||||
statusViewModel.isPinned = configuration.pinned
|
statusViewModel.isPinned = configuration.pinned
|
||||||
statusViewModel.isReplyInContext = configuration.isReplyInContext
|
statusViewModel.isReplyInContext = configuration.isReplyInContext
|
||||||
statusViewModel.hasReplyFollowing = configuration.hasReplyFollowing
|
statusViewModel.hasReplyFollowing = configuration.hasReplyFollowing
|
||||||
|
@ -128,7 +122,7 @@ private extension StatusListViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadMoreViewModel(item: CollectionItemIdentifier) -> LoadMoreViewModel? {
|
func loadMoreViewModel(item: CollectionItemIdentifier) -> LoadMoreViewModel? {
|
||||||
guard let timelineItem = timelineItems[item],
|
guard let timelineItem = items[item],
|
||||||
case let .loadMore(loadMore) = timelineItem
|
case let .loadMore(loadMore) = timelineItem
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
|
@ -137,40 +131,54 @@ private extension StatusListViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
let loadMoreViewModel = LoadMoreViewModel(
|
let loadMoreViewModel = LoadMoreViewModel(
|
||||||
loadMoreService: statusListService.navigationService.loadMoreService(loadMore: loadMore))
|
loadMoreService: collectionService.navigationService.loadMoreService(loadMore: loadMore))
|
||||||
|
|
||||||
viewModelCache[timelineItem] = (loadMoreViewModel, loadMoreViewModel.events
|
cache(viewModel: loadMoreViewModel, forItem: timelineItem)
|
||||||
.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 loadMoreViewModel
|
return loadMoreViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
func process(sections: [[Timeline.Item]]) {
|
func accountViewModel(item: CollectionItemIdentifier) -> AccountViewModel? {
|
||||||
|
guard let timelineItem = items[item],
|
||||||
|
case let .account(account) = timelineItem
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
|
var accountViewModel: AccountViewModel
|
||||||
|
|
||||||
|
if let cachedViewModel = viewModelCache[timelineItem]?.0 as? AccountViewModel {
|
||||||
|
accountViewModel = cachedViewModel
|
||||||
|
} else {
|
||||||
|
accountViewModel = AccountViewModel(
|
||||||
|
accountService: collectionService.navigationService.accountService(account: account))
|
||||||
|
cache(viewModel: accountViewModel, forItem: timelineItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return accountViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
func cache(viewModel: CollectionItemViewModel, forItem item: CollectionItem) {
|
||||||
|
viewModelCache[item] = (viewModel, viewModel.events.flatMap { $0.compactMap(NavigationEvent.init) }
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.sink { [weak self] in self?.navigationEventsSubject.send($0) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func process(sections: [[CollectionItem]]) {
|
||||||
determineIfScrollPositionShouldBeMaintained(newSections: sections)
|
determineIfScrollPositionShouldBeMaintained(newSections: sections)
|
||||||
|
|
||||||
let timelineItemKeys = Set(sections.reduce([], +))
|
let timelineItemKeys = Set(sections.reduce([], +))
|
||||||
|
|
||||||
timelineItems = Dictionary(uniqueKeysWithValues: timelineItemKeys.map { (.init(timelineItem: $0), $0) })
|
items = Dictionary(uniqueKeysWithValues: timelineItemKeys.map { (.init(item: $0), $0) })
|
||||||
viewModelCache = viewModelCache.filter { timelineItemKeys.contains($0.key) }
|
viewModelCache = viewModelCache.filter { timelineItemKeys.contains($0.key) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func determineIfScrollPositionShouldBeMaintained(newSections: [[Timeline.Item]]) {
|
func determineIfScrollPositionShouldBeMaintained(newSections: [[CollectionItem]]) {
|
||||||
maintainScrollPositionOfItem = nil // clear old value
|
maintainScrollPositionOfItem = nil // clear old value
|
||||||
|
|
||||||
// Maintain scroll position of parent after initial load of context
|
// Maintain scroll position of parent after initial load of context
|
||||||
if let contextParentID = statusListService.contextParentID {
|
if let contextParentID = collectionService.contextParentID {
|
||||||
let contextParentIdentifier = CollectionItemIdentifier(id: contextParentID, kind: .status, info: [:])
|
let contextParentIdentifier = CollectionItemIdentifier(id: contextParentID, kind: .status, info: [:])
|
||||||
|
|
||||||
if items == [[], [contextParentIdentifier], []] || items.isEmpty {
|
if identifiers == [[], [contextParentIdentifier], []] || identifiers.isEmpty {
|
||||||
maintainScrollPositionOfItem = contextParentIdentifier
|
maintainScrollPositionOfItem = contextParentIdentifier
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
import Combine
|
import Combine
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
|
|
||||||
final public class LoadMoreViewModel: ObservableObject {
|
final public class LoadMoreViewModel: ObservableObject, CollectionItemViewModel {
|
||||||
public var direction = LoadMore.Direction.up
|
public var direction = LoadMore.Direction.up
|
||||||
@Published public private(set) var loading = false
|
@Published public private(set) var loading = false
|
||||||
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||||
|
|
|
@ -90,8 +90,8 @@ public extension NavigationViewModel {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func viewModel(timeline: Timeline) -> StatusListViewModel {
|
func viewModel(timeline: Timeline) -> ListViewModel {
|
||||||
StatusListViewModel(statusListService: identification.service.service(timeline: timeline))
|
ListViewModel(collectionService: identification.service.service(timeline: timeline))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,14 +11,14 @@ final public class ProfileViewModel {
|
||||||
@Published public var alertItem: AlertItem?
|
@Published public var alertItem: AlertItem?
|
||||||
|
|
||||||
private let profileService: ProfileService
|
private let profileService: ProfileService
|
||||||
private let collectionViewModel: CurrentValueSubject<StatusListViewModel, Never>
|
private let collectionViewModel: CurrentValueSubject<ListViewModel, Never>
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(profileService: ProfileService) {
|
init(profileService: ProfileService) {
|
||||||
self.profileService = profileService
|
self.profileService = profileService
|
||||||
|
|
||||||
collectionViewModel = CurrentValueSubject(
|
collectionViewModel = CurrentValueSubject(
|
||||||
StatusListViewModel(statusListService: profileService.statusListService(profileCollection: .statuses)))
|
ListViewModel(collectionService: profileService.statusListService(profileCollection: .statuses)))
|
||||||
|
|
||||||
profileService.accountServicePublisher
|
profileService.accountServicePublisher
|
||||||
.map(AccountViewModel.init(accountService:))
|
.map(AccountViewModel.init(accountService:))
|
||||||
|
@ -27,7 +27,7 @@ final public class ProfileViewModel {
|
||||||
|
|
||||||
$collection.dropFirst()
|
$collection.dropFirst()
|
||||||
.map(profileService.statusListService(profileCollection:))
|
.map(profileService.statusListService(profileCollection:))
|
||||||
.map(StatusListViewModel.init(statusListService:))
|
.map(ListViewModel.init(collectionService:))
|
||||||
.sink { [weak self] in
|
.sink { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
@ -39,8 +39,8 @@ final public class ProfileViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileViewModel: CollectionViewModel {
|
extension ProfileViewModel: CollectionViewModel {
|
||||||
public var collectionItems: AnyPublisher<[[CollectionItemIdentifier]], Never> {
|
public var sections: AnyPublisher<[[CollectionItemIdentifier]], Never> {
|
||||||
collectionViewModel.flatMap(\.collectionItems).eraseToAnyPublisher()
|
collectionViewModel.flatMap(\.sections).eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
public var title: AnyPublisher<String?, Never> {
|
public var title: AnyPublisher<String?, Never> {
|
||||||
|
@ -85,15 +85,15 @@ extension ProfileViewModel: CollectionViewModel {
|
||||||
collectionViewModel.value.request(maxID: maxID, minID: minID)
|
collectionViewModel.value.request(maxID: maxID, minID: minID)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func itemSelected(_ item: CollectionItemIdentifier) {
|
public func select(identifier: CollectionItemIdentifier) {
|
||||||
collectionViewModel.value.itemSelected(item)
|
collectionViewModel.value.select(identifier: identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func canSelect(item: CollectionItemIdentifier) -> Bool {
|
public func canSelect(identifier: CollectionItemIdentifier) -> Bool {
|
||||||
collectionViewModel.value.canSelect(item: item)
|
collectionViewModel.value.canSelect(identifier: identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func viewModel(item: CollectionItemIdentifier) -> Any? {
|
public func viewModel(identifier: CollectionItemIdentifier) -> CollectionItemViewModel? {
|
||||||
collectionViewModel.value.viewModel(item: item)
|
collectionViewModel.value.viewModel(identifier: identifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import Foundation
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
|
|
||||||
public struct StatusViewModel {
|
public struct StatusViewModel: CollectionItemViewModel {
|
||||||
public let content: NSAttributedString
|
public let content: NSAttributedString
|
||||||
public let contentEmoji: [Emoji]
|
public let contentEmoji: [Emoji]
|
||||||
public let displayName: String
|
public let displayName: String
|
||||||
|
@ -127,18 +127,14 @@ public extension StatusViewModel {
|
||||||
|
|
||||||
func rebloggedBySelected() {
|
func rebloggedBySelected() {
|
||||||
eventsSubject.send(
|
eventsSubject.send(
|
||||||
Just(CollectionItemEvent.accountListNavigation(
|
Just(CollectionItemEvent.navigation(.collection(statusService.rebloggedByService())))
|
||||||
AccountListViewModel(
|
|
||||||
accountListService: statusService.rebloggedByService())))
|
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
.eraseToAnyPublisher())
|
.eraseToAnyPublisher())
|
||||||
}
|
}
|
||||||
|
|
||||||
func favoritedBySelected() {
|
func favoritedBySelected() {
|
||||||
eventsSubject.send(
|
eventsSubject.send(
|
||||||
Just(CollectionItemEvent.accountListNavigation(
|
Just(CollectionItemEvent.navigation(.collection(statusService.favoritedByService())))
|
||||||
AccountListViewModel(
|
|
||||||
accountListService: statusService.favoritedByService())))
|
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
.eraseToAnyPublisher())
|
.eraseToAnyPublisher())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue