311 lines
13 KiB
Swift
311 lines
13 KiB
Swift
//
|
|
// TimelineSection.swift
|
|
// Mastodon
|
|
//
|
|
// Created by sxiaojian on 2021/1/27.
|
|
//
|
|
|
|
import Combine
|
|
import CoreData
|
|
import CoreDataStack
|
|
import os.log
|
|
import UIKit
|
|
import AVKit
|
|
import AlamofireImage
|
|
import MastodonMeta
|
|
import MastodonSDK
|
|
import NaturalLanguage
|
|
import MastodonUI
|
|
|
|
enum StatusSection: Equatable, Hashable {
|
|
case main
|
|
}
|
|
|
|
extension StatusSection {
|
|
|
|
static let logger = Logger(subsystem: "StatusSection", category: "logic")
|
|
|
|
struct Configuration {
|
|
weak var statusTableViewCellDelegate: StatusTableViewCellDelegate?
|
|
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
|
|
let filterContext: Mastodon.Entity.Filter.Context?
|
|
let activeFilters: Published<[Mastodon.Entity.Filter]>.Publisher?
|
|
}
|
|
|
|
static func diffableDataSource(
|
|
tableView: UITableView,
|
|
context: AppContext,
|
|
configuration: Configuration
|
|
) -> UITableViewDiffableDataSource<StatusSection, StatusItem> {
|
|
tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self))
|
|
tableView.register(TimelineMiddleLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self))
|
|
tableView.register(StatusThreadRootTableViewCell.self, forCellReuseIdentifier: String(describing: StatusThreadRootTableViewCell.self))
|
|
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
|
|
|
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
|
switch item {
|
|
case .feed(let record):
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
|
|
context.managedObjectContext.performAndWait {
|
|
guard let feed = record.object(in: context.managedObjectContext) else { return }
|
|
configure(
|
|
context: context,
|
|
tableView: tableView,
|
|
cell: cell,
|
|
viewModel: StatusTableViewCell.ViewModel(value: .feed(feed)),
|
|
configuration: configuration
|
|
)
|
|
}
|
|
return cell
|
|
case .feedLoader(let record):
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
|
|
context.managedObjectContext.performAndWait {
|
|
guard let feed = record.object(in: context.managedObjectContext) else { return }
|
|
configure(
|
|
cell: cell,
|
|
feed: feed,
|
|
configuration: configuration
|
|
)
|
|
}
|
|
return cell
|
|
case .status(let record):
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
|
|
context.managedObjectContext.performAndWait {
|
|
guard let status = record.object(in: context.managedObjectContext) else { return }
|
|
configure(
|
|
context: context,
|
|
tableView: tableView,
|
|
cell: cell,
|
|
viewModel: StatusTableViewCell.ViewModel(value: .status(status)),
|
|
configuration: configuration
|
|
)
|
|
}
|
|
return cell
|
|
case .thread(let thread):
|
|
let cell = dequeueConfiguredReusableCell(
|
|
context: context,
|
|
tableView: tableView,
|
|
indexPath: indexPath,
|
|
configuration: ThreadCellRegistrationConfiguration(
|
|
thread: thread,
|
|
configuration: configuration
|
|
)
|
|
)
|
|
return cell
|
|
case .topLoader:
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
|
cell.activityIndicatorView.startAnimating()
|
|
return cell
|
|
case .bottomLoader:
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
|
cell.activityIndicatorView.startAnimating()
|
|
return cell
|
|
}
|
|
}
|
|
} // end func
|
|
|
|
}
|
|
|
|
extension StatusSection {
|
|
|
|
struct ThreadCellRegistrationConfiguration {
|
|
let thread: StatusItem.Thread
|
|
let configuration: Configuration
|
|
}
|
|
|
|
static func dequeueConfiguredReusableCell(
|
|
context: AppContext,
|
|
tableView: UITableView,
|
|
indexPath: IndexPath,
|
|
configuration: ThreadCellRegistrationConfiguration
|
|
) -> UITableViewCell {
|
|
let managedObjectContext = context.managedObjectContext
|
|
|
|
switch configuration.thread {
|
|
case .root(let threadContext):
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusThreadRootTableViewCell.self), for: indexPath) as! StatusThreadRootTableViewCell
|
|
managedObjectContext.performAndWait {
|
|
guard let status = threadContext.status.object(in: managedObjectContext) else { return }
|
|
StatusSection.configure(
|
|
context: context,
|
|
tableView: tableView,
|
|
cell: cell,
|
|
viewModel: StatusThreadRootTableViewCell.ViewModel(value: .status(status)),
|
|
configuration: configuration.configuration
|
|
)
|
|
}
|
|
return cell
|
|
case .reply(let threadContext),
|
|
.leaf(let threadContext):
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
|
|
managedObjectContext.performAndWait {
|
|
guard let status = threadContext.status.object(in: managedObjectContext) else { return }
|
|
StatusSection.configure(
|
|
context: context,
|
|
tableView: tableView,
|
|
cell: cell,
|
|
viewModel: StatusTableViewCell.ViewModel(value: .status(status)),
|
|
configuration: configuration.configuration
|
|
)
|
|
}
|
|
return cell
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension StatusSection {
|
|
|
|
public static func setupStatusPollDataSource(
|
|
context: AppContext,
|
|
statusView: StatusView
|
|
) {
|
|
let managedObjectContext = context.managedObjectContext
|
|
statusView.pollTableViewDiffableDataSource = UITableViewDiffableDataSource<PollSection, PollItem>(tableView: statusView.pollTableView) { tableView, indexPath, item in
|
|
switch item {
|
|
case .option(let record):
|
|
// Fix cell reuse animation issue
|
|
let cell: PollOptionTableViewCell = {
|
|
let _cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PollOptionTableViewCell.self) + "@\(indexPath.row)#\(indexPath.section)") as? PollOptionTableViewCell
|
|
_cell?.prepareForReuse()
|
|
return _cell ?? PollOptionTableViewCell()
|
|
}()
|
|
|
|
context.authenticationService.activeMastodonAuthenticationBox
|
|
.map { $0 as UserIdentifier? }
|
|
.assign(to: \.userIdentifier, on: cell.pollOptionView.viewModel)
|
|
.store(in: &cell.disposeBag)
|
|
|
|
managedObjectContext.performAndWait {
|
|
guard let option = record.object(in: managedObjectContext) else {
|
|
assertionFailure()
|
|
return
|
|
}
|
|
|
|
cell.pollOptionView.configure(pollOption: option)
|
|
|
|
// trigger update if needs
|
|
let needsUpdatePoll: Bool = {
|
|
// check first option in poll to trigger update poll only once
|
|
guard option.index == 0 else { return false }
|
|
|
|
let poll = option.poll
|
|
guard !poll.expired else {
|
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): poll expired. Skip update poll \(poll.id)")
|
|
return false
|
|
}
|
|
|
|
let now = Date()
|
|
let timeIntervalSinceUpdate = now.timeIntervalSince(poll.updatedAt)
|
|
#if DEBUG
|
|
let autoRefreshTimeInterval: TimeInterval = 3 // speedup testing
|
|
#else
|
|
let autoRefreshTimeInterval: TimeInterval = 30
|
|
#endif
|
|
|
|
guard timeIntervalSinceUpdate > autoRefreshTimeInterval else {
|
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): skip update poll \(poll.id) due to recent updated")
|
|
return false
|
|
}
|
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update poll \(poll.id)…")
|
|
return true
|
|
}()
|
|
|
|
if needsUpdatePoll, let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value
|
|
{
|
|
let pollRecord: ManagedObjectRecord<Poll> = .init(objectID: option.poll.objectID)
|
|
Task { [weak context] in
|
|
guard let context = context else { return }
|
|
_ = try await context.apiService.poll(
|
|
poll: pollRecord,
|
|
authenticationBox: authenticationBox
|
|
)
|
|
}
|
|
}
|
|
} // end managedObjectContext.performAndWait
|
|
return cell
|
|
}
|
|
}
|
|
var _snapshot = NSDiffableDataSourceSnapshot<PollSection, PollItem>()
|
|
_snapshot.appendSections([.main])
|
|
if #available(iOS 15.0, *) {
|
|
statusView.pollTableViewDiffableDataSource?.applySnapshotUsingReloadData(_snapshot)
|
|
} else {
|
|
statusView.pollTableViewDiffableDataSource?.apply(_snapshot, animatingDifferences: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension StatusSection {
|
|
|
|
static func configure(
|
|
context: AppContext,
|
|
tableView: UITableView,
|
|
cell: StatusTableViewCell,
|
|
viewModel: StatusTableViewCell.ViewModel,
|
|
configuration: Configuration
|
|
) {
|
|
setupStatusPollDataSource(
|
|
context: context,
|
|
statusView: cell.statusView
|
|
)
|
|
|
|
context.authenticationService.activeMastodonAuthenticationBox
|
|
.map { $0 as UserIdentifier? }
|
|
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
|
|
.store(in: &cell.disposeBag)
|
|
|
|
cell.configure(
|
|
tableView: tableView,
|
|
viewModel: viewModel,
|
|
delegate: configuration.statusTableViewCellDelegate
|
|
)
|
|
|
|
cell.statusView.viewModel.filterContext = configuration.filterContext
|
|
configuration.activeFilters?
|
|
.assign(to: \.activeFilters, on: cell.statusView.viewModel)
|
|
.store(in: &cell.disposeBag)
|
|
}
|
|
|
|
static func configure(
|
|
context: AppContext,
|
|
tableView: UITableView,
|
|
cell: StatusThreadRootTableViewCell,
|
|
viewModel: StatusThreadRootTableViewCell.ViewModel,
|
|
configuration: Configuration
|
|
) {
|
|
setupStatusPollDataSource(
|
|
context: context,
|
|
statusView: cell.statusView
|
|
)
|
|
|
|
context.authenticationService.activeMastodonAuthenticationBox
|
|
.map { $0 as UserIdentifier? }
|
|
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
|
|
.store(in: &cell.disposeBag)
|
|
|
|
cell.configure(
|
|
tableView: tableView,
|
|
viewModel: viewModel,
|
|
delegate: configuration.statusTableViewCellDelegate
|
|
)
|
|
|
|
cell.statusView.viewModel.filterContext = configuration.filterContext
|
|
configuration.activeFilters?
|
|
.assign(to: \.activeFilters, on: cell.statusView.viewModel)
|
|
.store(in: &cell.disposeBag)
|
|
}
|
|
|
|
static func configure(
|
|
cell: TimelineMiddleLoaderTableViewCell,
|
|
feed: Feed,
|
|
configuration: Configuration
|
|
) {
|
|
cell.configure(
|
|
feed: feed,
|
|
delegate: configuration.timelineMiddleLoaderTableViewCellDelegate
|
|
)
|
|
}
|
|
|
|
}
|