Refactoring

This commit is contained in:
Justin Mazzocchi 2021-02-04 14:24:27 -08:00
parent 19176f955c
commit 980c5f0099
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
12 changed files with 115 additions and 111 deletions

View file

@ -5,18 +5,19 @@ import Foundation
import Mastodon
import ServiceLayer
public final class AccountViewModel: CollectionItemViewModel, ObservableObject {
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
public final class AccountViewModel: ObservableObject {
public let identityContext: IdentityContext
public internal(set) var configuration = CollectionItem.AccountConfiguration.withNote
private let accountService: AccountService
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
private let eventsSubject: PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>
init(accountService: AccountService, identityContext: IdentityContext) {
init(accountService: AccountService,
identityContext: IdentityContext,
eventsSubject: PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>) {
self.accountService = accountService
self.identityContext = identityContext
events = eventsSubject.eraseToAnyPublisher()
self.eventsSubject = eventsSubject
}
}

View file

@ -1,8 +0,0 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Combine
import Foundation
public protocol CollectionItemViewModel {
var events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never> { get }
}

View file

@ -13,8 +13,8 @@ public class CollectionItemsViewModel: ObservableObject {
@Published private var lastUpdate = CollectionUpdate.empty
private let collectionService: CollectionService
private var viewModelCache = [CollectionItem: (viewModel: CollectionItemViewModel, events: AnyCancellable)]()
private let eventsSubject = PassthroughSubject<CollectionItemEvent, Never>()
private var viewModelCache = [CollectionItem: Any]()
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
private let loadingSubject = PassthroughSubject<Bool, Never>()
private let expandAllSubject: CurrentValueSubject<ExpandAllState, Never>
private var topVisibleIndexPath = IndexPath(item: 0, section: 0)
@ -83,7 +83,14 @@ extension CollectionItemsViewModel: CollectionViewModel {
public var loading: AnyPublisher<Bool, Never> { loadingSubject.eraseToAnyPublisher() }
public var events: AnyPublisher<CollectionItemEvent, Never> { eventsSubject.eraseToAnyPublisher() }
public var events: AnyPublisher<CollectionItemEvent, Never> {
eventsSubject.flatMap { [weak self] eventPublisher -> AnyPublisher<CollectionItemEvent, Never> in
guard let self = self else { return Empty().eraseToAnyPublisher() }
return eventPublisher.assignErrorsToAlertItem(to: \.alertItem, on: self).eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
public var canRefresh: Bool { collectionService.canRefresh }
@ -128,44 +135,38 @@ extension CollectionItemsViewModel: CollectionViewModel {
switch item {
case let .status(status, _):
eventsSubject.send(
.navigation(.collection(collectionService
send(event: .navigation(.collection(collectionService
.navigationService
.contextService(id: status.displayStatus.id))))
case let .loadMore(loadMore):
lastSelectedLoadMore = loadMore
(viewModel(indexPath: indexPath) as? LoadMoreViewModel)?.loadMore()
case let .account(account, _):
eventsSubject.send(
.navigation(.profile(collectionService
send(event: .navigation(.profile(collectionService
.navigationService
.profileService(account: account))))
case let .notification(notification, _):
if let status = notification.status {
eventsSubject.send(
.navigation(.collection(collectionService
send(event: .navigation(.collection(collectionService
.navigationService
.contextService(id: status.displayStatus.id))))
} else {
eventsSubject.send(
.navigation(.profile(collectionService
send(event: .navigation(.profile(collectionService
.navigationService
.profileService(account: notification.account))))
}
case let .conversation(conversation):
guard let status = conversation.lastStatus else { break }
eventsSubject.send(
.navigation(.collection(collectionService
send(event: .navigation(.collection(collectionService
.navigationService
.contextService(id: status.displayStatus.id))))
case let .tag(tag):
eventsSubject.send(
.navigation(.collection(collectionService
send(event: .navigation(.collection(collectionService
.navigationService
.timelineService(timeline: .tag(tag.name)))))
case let .moreResults(moreResults):
eventsSubject.send(.navigation(.searchScope(moreResults.scope)))
send(event: .navigation(.searchScope(moreResults.scope)))
}
}
@ -191,9 +192,9 @@ extension CollectionItemsViewModel: CollectionViewModel {
}
// swiftlint:disable:next function_body_length cyclomatic_complexity
public func viewModel(indexPath: IndexPath) -> CollectionItemViewModel {
public func viewModel(indexPath: IndexPath) -> Any {
let item = lastUpdate.sections[indexPath.section].items[indexPath.item]
let cachedViewModel = viewModelCache[item]?.viewModel
let cachedViewModel = viewModelCache[item]
switch item {
case let .status(status, configuration):
@ -204,8 +205,9 @@ extension CollectionItemsViewModel: CollectionViewModel {
} else {
viewModel = .init(
statusService: collectionService.navigationService.statusService(status: status),
identityContext: identityContext)
cache(viewModel: viewModel, forItem: item)
identityContext: identityContext,
eventsSubject: eventsSubject)
viewModelCache[item] = viewModel
}
viewModel.configuration = configuration
@ -217,9 +219,10 @@ extension CollectionItemsViewModel: CollectionViewModel {
}
let viewModel = LoadMoreViewModel(
loadMoreService: collectionService.navigationService.loadMoreService(loadMore: loadMore))
loadMoreService: collectionService.navigationService.loadMoreService(loadMore: loadMore),
eventsSubject: eventsSubject)
cache(viewModel: viewModel, forItem: item)
viewModelCache[item] = viewModel
return viewModel
case let .account(account, configuration):
@ -230,31 +233,34 @@ extension CollectionItemsViewModel: CollectionViewModel {
} else {
viewModel = AccountViewModel(
accountService: collectionService.navigationService.accountService(account: account),
identityContext: identityContext)
cache(viewModel: viewModel, forItem: item)
identityContext: identityContext,
eventsSubject: eventsSubject)
viewModelCache[item] = viewModel
}
viewModel.configuration = configuration
return viewModel
case let .notification(notification, statusConfiguration):
let viewModel: CollectionItemViewModel
let viewModel: Any
if let cachedViewModel = cachedViewModel {
viewModel = cachedViewModel
} else if let status = notification.status, let statusConfiguration = statusConfiguration {
let statusViewModel = StatusViewModel(
statusService: collectionService.navigationService.statusService(status: status),
identityContext: identityContext)
identityContext: identityContext,
eventsSubject: eventsSubject)
statusViewModel.configuration = statusConfiguration
viewModel = statusViewModel
cache(viewModel: viewModel, forItem: item)
viewModelCache[item] = viewModel
} else {
viewModel = NotificationViewModel(
notificationService: collectionService.navigationService.notificationService(
notification: notification),
identityContext: identityContext)
cache(viewModel: viewModel, forItem: item)
identityContext: identityContext,
eventsSubject: eventsSubject)
viewModelCache[item] = viewModel
}
return viewModel
@ -268,7 +274,7 @@ extension CollectionItemsViewModel: CollectionViewModel {
conversation: conversation),
identityContext: identityContext)
cache(viewModel: viewModel, forItem: item)
viewModelCache[item] = viewModel
return viewModel
case let .tag(tag):
@ -278,7 +284,7 @@ extension CollectionItemsViewModel: CollectionViewModel {
let viewModel = TagViewModel(tag: tag, identityContext: identityContext)
cache(viewModel: viewModel, forItem: item)
viewModelCache[item] = viewModel
return viewModel
case let .moreResults(moreResults):
@ -288,7 +294,7 @@ extension CollectionItemsViewModel: CollectionViewModel {
let viewModel = MoreResultsViewModel(moreResults: moreResults)
cache(viewModel: viewModel, forItem: item)
viewModelCache[item] = viewModel
return viewModel
}
@ -335,21 +341,14 @@ extension CollectionItemsViewModel: CollectionViewModel {
private extension CollectionItemsViewModel {
private static let lastReadIdDebounceInterval: TimeInterval = 0.5
func send(event: CollectionItemEvent) {
eventsSubject.send(Just(event).setFailureType(to: Error.self).eraseToAnyPublisher())
}
var lastUpdateWasContextParentOnly: Bool {
collectionService is ContextService && lastUpdate.sections.map(\.items).map(\.count) == [0, 1, 0]
}
func cache(viewModel: CollectionItemViewModel, forItem item: CollectionItem) {
viewModelCache[item] = (viewModel, viewModel.events
.flatMap { [weak self] events -> AnyPublisher<CollectionItemEvent, Never> in
guard let self = self else { return Empty().eraseToAnyPublisher() }
return events.assignErrorsToAlertItem(to: \.alertItem, on: self)
.eraseToAnyPublisher()
}
.sink { [weak self] in self?.eventsSubject.send($0) })
}
func process(sections: [CollectionSection]) {
let items = sections.map(\.items).reduce([], +)
let itemsSet = Set(items)
@ -402,7 +401,7 @@ private extension CollectionItemsViewModel {
if case let .remove(_, item, _) = removal,
case let .loadMore(loadMore) = item,
loadMore == lastSelectedLoadMore,
let direction = (viewModelCache[item]?.viewModel as? LoadMoreViewModel)?.direction,
let direction = (viewModelCache[item] as? LoadMoreViewModel)?.direction,
direction == .up,
let statusAfterLoadMore = items.first(where: {
guard case let .status(status, _) = $0 else { return false }

View file

@ -19,7 +19,7 @@ public protocol CollectionViewModel {
func viewedAtTop(indexPath: IndexPath)
func select(indexPath: IndexPath)
func canSelect(indexPath: IndexPath) -> Bool
func viewModel(indexPath: IndexPath) -> CollectionItemViewModel
func viewModel(indexPath: IndexPath) -> Any
func toggleExpandAll()
func applyAccountListEdit(viewModel: AccountViewModel, edit: CollectionItemEvent.AccountListEdit)
}

View file

@ -5,32 +5,31 @@ import Foundation
import Mastodon
import ServiceLayer
public final class ConversationViewModel: CollectionItemViewModel, ObservableObject {
public final class ConversationViewModel: ObservableObject {
public let accountViewModels: [AccountViewModel]
public let statusViewModel: StatusViewModel?
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
public let identityContext: IdentityContext
private let conversationService: ConversationService
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
init(conversationService: ConversationService, identityContext: IdentityContext) {
accountViewModels = conversationService.conversation.accounts.map {
AccountViewModel(
accountService: conversationService.navigationService.accountService(account: $0),
identityContext: identityContext)
identityContext: identityContext,
eventsSubject: .init())
}
if let status = conversationService.conversation.lastStatus {
statusViewModel = StatusViewModel(
statusService: conversationService.navigationService.statusService(status: status),
identityContext: identityContext)
identityContext: identityContext,
eventsSubject: .init())
} else {
statusViewModel = nil
}
self.conversationService = conversationService
self.identityContext = identityContext
self.events = eventsSubject.eraseToAnyPublisher()
}
}

View file

@ -3,17 +3,17 @@
import Combine
import ServiceLayer
public final class LoadMoreViewModel: ObservableObject, CollectionItemViewModel {
public final class LoadMoreViewModel: ObservableObject {
public var direction = LoadMore.Direction.up
@Published public private(set) var loading = false
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
private let loadMoreService: LoadMoreService
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
private let eventsSubject: PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>
init(loadMoreService: LoadMoreService) {
init(loadMoreService: LoadMoreService,
eventsSubject: PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>) {
self.loadMoreService = loadMoreService
events = eventsSubject.eraseToAnyPublisher()
self.eventsSubject = eventsSubject
}
}

View file

@ -3,15 +3,11 @@
import Combine
import ServiceLayer
public final class MoreResultsViewModel: ObservableObject, CollectionItemViewModel {
public var events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
public final class MoreResultsViewModel: ObservableObject {
private let moreResults: MoreResults
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
init(moreResults: MoreResults) {
self.moreResults = moreResults
events = eventsSubject.eraseToAnyPublisher()
}
}

View file

@ -5,32 +5,34 @@ import Foundation
import Mastodon
import ServiceLayer
public final class NotificationViewModel: CollectionItemViewModel, ObservableObject {
public final class NotificationViewModel: ObservableObject {
public let accountViewModel: AccountViewModel
public let statusViewModel: StatusViewModel?
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
public let identityContext: IdentityContext
private let notificationService: NotificationService
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
private let eventsSubject: PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>
init(notificationService: NotificationService, identityContext: IdentityContext) {
init(notificationService: NotificationService,
identityContext: IdentityContext,
eventsSubject: PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>) {
self.notificationService = notificationService
self.identityContext = identityContext
self.eventsSubject = eventsSubject
self.accountViewModel = AccountViewModel(
accountService: notificationService.navigationService.accountService(
account: notificationService.notification.account),
identityContext: identityContext)
identityContext: identityContext,
eventsSubject: eventsSubject)
if let status = notificationService.notification.status {
statusViewModel = StatusViewModel(
statusService: notificationService.navigationService.statusService(status: status),
identityContext: identityContext)
identityContext: identityContext,
eventsSubject: eventsSubject)
} else {
statusViewModel = nil
}
self.events = eventsSubject.eraseToAnyPublisher()
}
}

View file

@ -13,6 +13,7 @@ final public class ProfileViewModel {
private let profileService: ProfileService
private let collectionViewModel: CurrentValueSubject<CollectionItemsViewModel, Never>
private let accountEventsSubject: PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>
private let imagePresentationsSubject = PassthroughSubject<URL, Never>()
private var cancellables = Set<AnyCancellable>()
@ -25,8 +26,16 @@ final public class ProfileViewModel {
collectionService: profileService.timelineService(profileCollection: .statuses),
identityContext: identityContext))
let accountEventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
self.accountEventsSubject = accountEventsSubject
profileService.accountServicePublisher
.map { AccountViewModel(accountService: $0, identityContext: identityContext) }
.map {
AccountViewModel(accountService: $0,
identityContext: identityContext,
eventsSubject: accountEventsSubject)
}
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: &$accountViewModel)
@ -102,10 +111,12 @@ extension ProfileViewModel: CollectionViewModel {
}
public var events: AnyPublisher<CollectionItemEvent, Never> {
$accountViewModel.compactMap { $0 }
.flatMap(\.events)
.flatMap { $0 }
.assignErrorsToAlertItem(to: \.alertItem, on: self)
accountEventsSubject
.flatMap { [weak self] eventPublisher -> AnyPublisher<CollectionItemEvent, Never> in
guard let self = self else { return Empty().eraseToAnyPublisher() }
return eventPublisher.assignErrorsToAlertItem(to: \.alertItem, on: self).eraseToAnyPublisher()
}
.merge(with: collectionViewModel.flatMap(\.events))
.eraseToAnyPublisher()
}
@ -143,7 +154,7 @@ extension ProfileViewModel: CollectionViewModel {
collectionViewModel.value.canSelect(indexPath: indexPath)
}
public func viewModel(indexPath: IndexPath) -> CollectionItemViewModel {
public func viewModel(indexPath: IndexPath) -> Any {
collectionViewModel.value.viewModel(indexPath: indexPath)
}

View file

@ -22,7 +22,9 @@ public final class ReportViewModel: ObservableObject {
events = eventsSubject.eraseToAnyPublisher()
if let statusService = statusService {
statusViewModel = StatusViewModel(statusService: statusService, identityContext: identityContext)
statusViewModel = StatusViewModel(statusService: statusService,
identityContext: identityContext,
eventsSubject: .init())
elements.statusIds.insert(statusService.status.displayStatus.id)
} else {
statusViewModel = nil

View file

@ -5,7 +5,7 @@ import Foundation
import Mastodon
import ServiceLayer
public final class StatusViewModel: CollectionItemViewModel, AttachmentsRenderingViewModel, ObservableObject {
public final class StatusViewModel: AttachmentsRenderingViewModel, ObservableObject {
public let content: NSAttributedString
public let contentEmojis: [Emoji]
public let displayName: String
@ -18,15 +18,17 @@ public final class StatusViewModel: CollectionItemViewModel, AttachmentsRenderin
public let pollEmojis: [Emoji]
@Published public var pollOptionSelections = Set<Int>()
public var configuration = CollectionItem.StatusConfiguration.default
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
public let identityContext: IdentityContext
private let statusService: StatusService
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
private let eventsSubject: PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>
init(statusService: StatusService, identityContext: IdentityContext) {
init(statusService: StatusService,
identityContext: IdentityContext,
eventsSubject: PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>) {
self.statusService = statusService
self.identityContext = identityContext
self.eventsSubject = eventsSubject
content = statusService.status.displayStatus.content.attributed
contentEmojis = statusService.status.displayStatus.emojis
displayName = statusService.status.displayStatus.account.displayName.isEmpty
@ -42,7 +44,6 @@ public final class StatusViewModel: CollectionItemViewModel, AttachmentsRenderin
attachmentViewModels = statusService.status.displayStatus.mediaAttachments
.map { AttachmentViewModel(attachment: $0, identityContext: identityContext, status: statusService.status) }
pollEmojis = statusService.status.displayStatus.poll?.emojis ?? []
events = eventsSubject.eraseToAnyPublisher()
}
}
@ -221,7 +222,9 @@ public extension StatusViewModel {
}
func reply() {
let replyViewModel = Self(statusService: statusService, identityContext: identityContext)
let replyViewModel = Self(statusService: statusService,
identityContext: identityContext,
eventsSubject: .init())
replyViewModel.configuration = configuration.reply()
@ -292,7 +295,8 @@ public extension StatusViewModel {
if let inReplyToStatusService = inReplyToStatusService {
inReplyToViewModel = Self(
statusService: inReplyToStatusService,
identityContext: identityContext)
identityContext: identityContext,
eventsSubject: .init())
inReplyToViewModel?.configuration = CollectionItem.StatusConfiguration.default.reply()
} else {
inReplyToViewModel = nil

View file

@ -4,16 +4,14 @@ import Combine
import Foundation
import Mastodon
public struct TagViewModel: CollectionItemViewModel {
public struct TagViewModel {
public let identityContext: IdentityContext
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
private let tag: Tag
init(tag: Tag, identityContext: IdentityContext) {
self.tag = tag
self.identityContext = identityContext
events = Empty().eraseToAnyPublisher()
}
}