mastodon-ios/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift

174 lines
5.9 KiB
Swift
Raw Normal View History

2021-02-07 07:42:50 +01:00
//
// HomeTimelineViewModel.swift
// Mastodon
//
// Created by sxiaojian on 2021/2/5.
//
import func AVFoundation.AVMakeRect
import UIKit
import AVKit
import Combine
import CoreData
import CoreDataStack
import GameplayKit
import AlamofireImage
2022-10-08 07:43:06 +02:00
import MastodonCore
import MastodonUI
import MastodonSDK
2021-02-07 07:42:50 +01:00
final class HomeTimelineViewModel: NSObject {
var disposeBag = Set<AnyCancellable>()
var observations = Set<NSKeyValueObservation>()
// input
let context: AppContext
let authContext: AuthContext
2024-01-08 11:17:40 +01:00
let dataController: FeedDataController
let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel
let listBatchFetchViewModel = ListBatchFetchViewModel()
var presentedSuggestions = false
@Published var lastAutomaticFetchTimestamp: Date? = nil
@Published var scrollPositionRecord: ScrollPositionRecord? = nil
@Published var displaySettingBarButtonItem = true
@Published var hasPendingStatusEditReload = false
2021-03-15 13:03:40 +01:00
2021-02-07 07:42:50 +01:00
weak var tableView: UITableView?
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
let timelineIsEmpty = CurrentValueSubject<Bool, Never>(false)
2021-04-22 04:11:19 +02:00
let homeTimelineNeedRefresh = PassthroughSubject<Void, Never>()
2021-02-07 07:42:50 +01:00
// output
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?
let didLoadLatest = PassthroughSubject<Void, Never>()
2021-02-07 07:42:50 +01:00
// top loader
private(set) lazy var loadLatestStateMachine: GKStateMachine = {
// exclude timeline middle fetcher state
let stateMachine = GKStateMachine(states: [
LoadLatestState.Initial(viewModel: self),
LoadLatestState.Loading(viewModel: self),
LoadLatestState.LoadingManually(viewModel: self),
2021-02-07 07:42:50 +01:00
LoadLatestState.Fail(viewModel: self),
LoadLatestState.Idle(viewModel: self),
])
stateMachine.enter(LoadLatestState.Initial.self)
return stateMachine
}()
lazy var loadLatestStateMachinePublisher = CurrentValueSubject<LoadLatestState?, Never>(nil)
2021-02-07 07:42:50 +01:00
// bottom loader
2021-07-23 13:10:27 +02:00
private(set) lazy var loadOldestStateMachine: GKStateMachine = {
2021-02-07 07:42:50 +01:00
// exclude timeline middle fetcher state
let stateMachine = GKStateMachine(states: [
LoadOldestState.Initial(viewModel: self),
LoadOldestState.Loading(viewModel: self),
LoadOldestState.Fail(viewModel: self),
LoadOldestState.Idle(viewModel: self),
LoadOldestState.NoMore(viewModel: self),
])
stateMachine.enter(LoadOldestState.Initial.self)
return stateMachine
}()
lazy var loadOldestStateMachinePublisher = CurrentValueSubject<LoadOldestState?, Never>(nil)
2021-02-07 07:42:50 +01:00
var cellFrameCache = NSCache<NSNumber, NSValue>()
init(context: AppContext, authContext: AuthContext) {
2021-02-07 07:42:50 +01:00
self.context = context
self.authContext = authContext
2024-01-08 11:17:40 +01:00
self.dataController = FeedDataController(context: context, authContext: authContext)
self.homeTimelineNavigationBarTitleViewModel = HomeTimelineNavigationBarTitleViewModel(context: context)
2021-02-07 07:42:50 +01:00
super.init()
2024-01-08 11:17:40 +01:00
self.dataController.records = (try? FileManager.default.cachedHomeTimeline(for: authContext.mastodonAuthenticationBox).map {
MastodonFeed.fromStatus($0, kind: .home)
}) ?? []
2021-04-22 04:11:19 +02:00
homeTimelineNeedRefresh
.sink { [weak self] _ in
self?.loadLatestStateMachine.enter(LoadLatestState.Loading.self)
}
.store(in: &disposeBag)
// refresh after publish post
homeTimelineNavigationBarTitleViewModel.isPublished
.delay(for: 2, scheduler: DispatchQueue.main)
.sink { [weak self] isPublished in
guard let self = self else { return }
self.homeTimelineNeedRefresh.send()
}
.store(in: &disposeBag)
2023-11-22 13:18:41 +01:00
2024-01-08 11:17:40 +01:00
self.dataController.$records
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink(receiveValue: { feeds in
let items: [MastodonStatus] = feeds.compactMap { feed -> MastodonStatus? in
guard let status = feed.status else { return nil }
return status
}
FileManager.default.cacheHomeTimeline(items: items, for: authContext.mastodonAuthenticationBox)
})
.store(in: &disposeBag)
2024-01-08 11:17:40 +01:00
self.dataController.loadInitial(kind: .home)
2021-02-07 07:42:50 +01:00
}
}
extension HomeTimelineViewModel {
struct ScrollPositionRecord {
let item: StatusItem
let offset: CGFloat
let timestamp: Date
}
}
extension HomeTimelineViewModel {
func timelineDidReachEnd() {
2024-01-08 11:17:40 +01:00
dataController.loadNext(kind: .home)
}
}
extension HomeTimelineViewModel {
// load timeline gap
func loadMore(item: StatusItem) async {
guard case let .feedLoader(record) = item else { return }
guard let diffableDataSource = diffableDataSource else { return }
var snapshot = diffableDataSource.snapshot()
2023-11-22 12:32:04 +01:00
guard let status = record.status else { return }
record.isLoadingMore = true
2023-11-22 13:18:41 +01:00
// reconfigure item
snapshot.reconfigureItems([item])
await updateSnapshotUsingReloadData(snapshot: snapshot)
await AuthenticationServiceProvider.shared.fetchAccounts(apiService: context.apiService)
// fetch data
2023-11-22 12:32:04 +01:00
let maxID = status.id
_ = try? await context.apiService.homeTimeline(
maxID: maxID,
authenticationBox: authContext.mastodonAuthenticationBox
)
record.isLoadingMore = false
// reconfigure item again
snapshot.reconfigureItems([item])
await updateSnapshotUsingReloadData(snapshot: snapshot)
}
}
// MARK: - SuggestionAccountViewModelDelegate
extension HomeTimelineViewModel: SuggestionAccountViewModelDelegate {
}