metatext/View Controllers/TableViewController.swift

894 lines
36 KiB
Swift
Raw Normal View History

2020-08-21 02:29:01 +00:00
// Copyright © 2020 Metabolist. All rights reserved.
2020-10-20 06:41:10 +00:00
import AVKit
2020-08-21 02:29:01 +00:00
import Combine
2021-01-11 22:45:30 +00:00
import Mastodon
2021-02-22 23:59:33 +00:00
import SDWebImage
2020-09-05 02:31:43 +00:00
import SwiftUI
2020-09-01 07:33:49 +00:00
import ViewModels
2020-08-21 02:29:01 +00:00
2021-01-10 05:56:15 +00:00
// swiftlint:disable file_length
2020-09-27 01:44:33 +00:00
class TableViewController: UITableViewController {
2020-10-22 22:16:06 +00:00
var transitionViewTag = -1
2020-09-23 01:00:56 +00:00
private let viewModel: CollectionViewModel
2021-02-15 08:47:30 +00:00
private let rootViewModel: RootViewModel?
2020-08-28 22:39:17 +00:00
private let loadingTableFooterView = LoadingTableFooterView()
2020-09-26 06:37:30 +00:00
private let webfingerIndicatorView = WebfingerIndicatorView()
2021-02-21 23:00:56 +00:00
private let newItemsView = NewItemsView()
2021-01-16 19:41:01 +00:00
@Published private var loading = false
2021-01-19 21:09:12 +00:00
private var visibleLoadMoreViews = Set<LoadMoreView>()
2020-08-21 02:29:01 +00:00
private var cancellables = Set<AnyCancellable>()
2020-10-15 07:44:01 +00:00
private var cellHeightCaches = [CGFloat: [CollectionItem: CGFloat]]()
2021-01-08 06:11:33 +00:00
private var shouldKeepPlayingVideoAfterDismissal = false
2021-02-21 23:00:56 +00:00
private var newItemsViewHiddenConstraint: NSLayoutConstraint?
private var newItemsViewVisibleConstraint: NSLayoutConstraint?
2021-01-25 02:10:41 +00:00
private let insetBottom: Bool
2021-01-23 03:48:33 +00:00
private weak var parentNavigationController: UINavigationController?
2020-08-21 02:29:01 +00:00
2020-10-07 21:06:26 +00:00
private lazy var dataSource: TableViewDataSource = {
2021-01-31 01:43:48 +00:00
.init(tableView: tableView, viewModel: viewModel)
2020-08-21 02:29:01 +00:00
}()
2021-01-23 03:48:33 +00:00
init(viewModel: CollectionViewModel,
2021-02-15 08:47:30 +00:00
rootViewModel: RootViewModel? = nil,
2021-01-25 02:10:41 +00:00
insetBottom: Bool = true,
2021-01-23 03:48:33 +00:00
parentNavigationController: UINavigationController? = nil) {
2020-08-21 02:29:01 +00:00
self.viewModel = viewModel
2021-01-10 05:56:15 +00:00
self.rootViewModel = rootViewModel
2021-01-25 02:10:41 +00:00
self.insetBottom = insetBottom
2021-01-23 03:48:33 +00:00
self.parentNavigationController = parentNavigationController
2020-08-21 02:29:01 +00:00
super.init(style: .plain)
}
2020-08-28 22:39:17 +00:00
@available(*, unavailable)
2020-08-21 02:29:01 +00:00
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = dataSource
2021-01-31 07:43:48 +00:00
tableView.prefetchDataSource = self
2020-08-21 02:29:01 +00:00
tableView.cellLayoutMarginsFollowReadableWidth = true
2020-08-28 22:39:17 +00:00
tableView.tableFooterView = UIView()
2021-01-25 02:10:41 +00:00
tableView.contentInset.bottom = bottomInset
2021-02-02 04:44:56 +00:00
tableView.isAccessibilityElement = false
tableView.shouldGroupAccessibilityChildren = true
2020-08-21 02:29:01 +00:00
2021-01-16 20:06:35 +00:00
if viewModel.canRefresh {
refreshControl = UIRefreshControl()
refreshControl?.addAction(
UIAction { [weak self] _ in
2021-01-29 02:41:41 +00:00
self?.refreshIfAble() },
2021-01-16 20:06:35 +00:00
for: .valueChanged)
}
2020-09-26 06:37:30 +00:00
view.addSubview(webfingerIndicatorView)
webfingerIndicatorView.translatesAutoresizingMaskIntoConstraints = false
2020-09-14 23:32:34 +00:00
2021-02-21 23:00:56 +00:00
view.addSubview(newItemsView)
newItemsView.translatesAutoresizingMaskIntoConstraints = false
newItemsView.alpha = 0
newItemsViewHiddenConstraint = newItemsView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
newItemsViewHiddenConstraint?.isActive = true
newItemsViewVisibleConstraint = newItemsView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,
constant: .defaultSpacing)
2020-09-26 06:37:30 +00:00
NSLayoutConstraint.activate([
webfingerIndicatorView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
2021-02-21 23:00:56 +00:00
webfingerIndicatorView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
newItemsView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor)
2020-09-26 06:37:30 +00:00
])
2020-08-28 22:39:17 +00:00
2021-02-21 23:00:56 +00:00
newItemsView.button.addAction(UIAction { [weak self] _ in
self?.newItemsTapped()
self?.hideNewItemsView()
},
for: .touchUpInside)
2021-02-25 06:33:27 +00:00
newItemsView.button.accessibilityCustomActions = [
UIAccessibilityCustomAction(name: NSLocalizedString("dismiss", comment: "")) { [weak self] _ in
self?.hideNewItemsView()
return true
}]
2021-02-21 23:00:56 +00:00
2020-09-26 06:37:30 +00:00
setupViewModelBindings()
2021-01-29 02:41:41 +00:00
viewModel.request(maxId: nil, minId: nil, search: nil)
2020-08-21 02:29:01 +00:00
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
2021-01-29 02:41:41 +00:00
refreshIfAble()
2020-08-21 02:29:01 +00:00
}
2020-10-05 01:25:02 +00:00
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.isDragging else { return }
let up = scrollView.panGestureRecognizer.translation(in: scrollView.superview).y > 0
for loadMoreView in visibleLoadMoreViews {
loadMoreView.directionChanged(up: up)
}
}
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
for loadMoreView in visibleLoadMoreViews {
loadMoreView.finalizeDirectionChange()
}
}
2020-08-21 02:29:01 +00:00
override func tableView(_ tableView: UITableView,
willDisplay cell: UITableViewCell,
forRowAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
2020-10-15 07:44:01 +00:00
var heightCache = cellHeightCaches[tableView.frame.width] ?? [CollectionItem: CGFloat]()
2020-08-21 02:29:01 +00:00
heightCache[item] = cell.frame.height
cellHeightCaches[tableView.frame.width] = heightCache
2021-01-16 05:54:38 +00:00
2021-01-16 23:39:42 +00:00
if !loading,
indexPath.section == dataSource.numberOfSections(in: tableView) - 1,
2021-01-25 02:10:41 +00:00
indexPath.row == dataSource.tableView(tableView, numberOfRowsInSection: indexPath.section) - 1 {
viewModel.requestNextPage(fromIndexPath: indexPath)
2021-01-16 23:39:42 +00:00
}
2021-01-19 21:09:12 +00:00
if let loadMoreView = cell.contentView as? LoadMoreView {
visibleLoadMoreViews.insert(loadMoreView)
}
2021-05-10 04:39:36 +00:00
(cell.contentView as? AnnouncementView)?.dismissIfUnread()
2021-01-19 21:09:12 +00:00
}
override func tableView(_ tableView: UITableView,
didEndDisplaying cell: UITableViewCell,
forRowAt indexPath: IndexPath) {
if let loadMoreView = cell.contentView as? LoadMoreView {
visibleLoadMoreViews.remove(loadMoreView)
}
2020-08-21 02:29:01 +00:00
}
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return UITableView.automaticDimension }
2021-01-19 00:46:38 +00:00
return cellHeightCaches[tableView.frame.width]?[item]
?? item.estimatedHeight(width: tableView.readableContentGuide.layoutFrame.width,
identityContext: viewModel.identityContext)
2020-08-21 02:29:01 +00:00
}
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
2021-02-02 21:32:39 +00:00
if case .loadMore = dataSource.itemIdentifier(for: indexPath), UIAccessibility.isVoiceOverRunning {
return false
}
return viewModel.canSelect(indexPath: indexPath)
2020-08-21 02:29:01 +00:00
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
2020-10-05 01:25:02 +00:00
tableView.deselectRow(at: indexPath, animated: true)
2020-10-05 07:50:59 +00:00
viewModel.select(indexPath: indexPath)
2020-08-21 02:29:01 +00:00
}
2020-08-28 22:39:17 +00:00
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
sizeTableHeaderFooterViews()
}
2021-03-03 05:02:07 +00:00
func configureRightBarButtonItem(expandAllState: ExpandAllState) {
switch expandAllState {
case .hidden:
navigationItem.rightBarButtonItem = nil
case .expand:
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: NSLocalizedString("status.show-more-all-button.accessibilty-label", comment: ""),
image: UIImage(systemName: "eye"),
primaryAction: UIAction { [weak self] _ in self?.viewModel.toggleExpandAll() })
case .collapse:
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: NSLocalizedString("status.show-less-all-button.accessibilty-label", comment: ""),
image: UIImage(systemName: "eye.slash"),
primaryAction: UIAction { [weak self] _ in self?.viewModel.toggleExpandAll() })
}
}
2020-08-28 22:39:17 +00:00
}
2020-09-27 05:54:06 +00:00
extension TableViewController {
2021-02-10 07:46:00 +00:00
func confirm(message: String, style: UIAlertAction.Style = .default, action: @escaping () -> Void) {
let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
let cancelAction = UIAlertAction(title: NSLocalizedString("cancel", comment: ""), style: .cancel, handler: nil)
let okAction = UIAlertAction(title: NSLocalizedString("ok", comment: ""), style: style) { _ in
action()
}
alertController.addAction(cancelAction)
alertController.addAction(okAction)
present(alertController, animated: true)
}
2021-01-10 05:56:15 +00:00
func report(reportViewModel: ReportViewModel) {
let reportViewController = ReportViewController(viewModel: reportViewModel)
let navigationController = UINavigationController(rootViewController: reportViewController)
present(navigationController, animated: true)
}
2021-03-03 00:50:22 +00:00
func addRemoveFromLists(accountViewModel: AccountViewModel) {
let addRemoveFromListsView = AddRemoveFromListsView(viewModel: .init(accountViewModel: accountViewModel))
let addRemoveFromListsController = UIHostingController(rootView: addRemoveFromListsView)
show(addRemoveFromListsController, sender: self)
}
2020-09-27 05:54:06 +00:00
func sizeTableHeaderFooterViews() {
// https://useyourloaf.com/blog/variable-height-table-view-header/
if let headerView = tableView.tableHeaderView {
let size = headerView.systemLayoutSizeFitting(
CGSize(width: tableView.frame.width, height: .greatestFiniteMagnitude),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel)
if headerView.frame.size.height != size.height {
headerView.frame.size.height = size.height
tableView.tableHeaderView = headerView
tableView.layoutIfNeeded()
}
view.insertSubview(webfingerIndicatorView, aboveSubview: headerView)
}
if let footerView = tableView.tableFooterView {
let size = footerView.systemLayoutSizeFitting(
CGSize(width: tableView.frame.width, height: .greatestFiniteMagnitude),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel)
if footerView.frame.size.height != size.height {
footerView.frame.size.height = size.height
tableView.tableFooterView = footerView
tableView.layoutIfNeeded()
}
}
}
2021-03-03 06:55:35 +00:00
}
2021-02-05 02:56:14 +00:00
2021-03-03 06:55:35 +00:00
extension TableViewController: NavigationHandling {
2021-02-05 02:56:14 +00:00
func handle(navigation: Navigation) {
switch navigation {
case let .collection(collectionService):
let vc = TableViewController(
viewModel: CollectionItemsViewModel(
collectionService: collectionService,
identityContext: viewModel.identityContext),
rootViewModel: rootViewModel,
parentNavigationController: parentNavigationController)
if let parentNavigationController = parentNavigationController {
parentNavigationController.pushViewController(vc, animated: true)
} else {
show(vc, sender: self)
}
2021-03-03 06:55:35 +00:00
webfingerIndicatorView.stopAnimating()
2021-02-05 02:56:14 +00:00
case let .profile(profileService):
let vc = ProfileViewController(
viewModel: ProfileViewModel(
profileService: profileService,
identityContext: viewModel.identityContext),
rootViewModel: rootViewModel,
identityContext: viewModel.identityContext,
parentNavigationController: parentNavigationController)
if let parentNavigationController = parentNavigationController {
parentNavigationController.pushViewController(vc, animated: true)
} else {
show(vc, sender: self)
}
2021-03-03 06:55:35 +00:00
webfingerIndicatorView.stopAnimating()
2021-02-05 02:56:14 +00:00
case let .notification(notificationService):
navigate(toNotification: notificationService.notification)
case let .url(url):
2021-03-03 06:55:35 +00:00
open(url: url, identityContext: viewModel.identityContext)
webfingerIndicatorView.stopAnimating()
2021-02-05 02:56:14 +00:00
case .searchScope:
break
case .webfingerStart:
webfingerIndicatorView.startAnimating()
case .webfingerEnd:
webfingerIndicatorView.stopAnimating()
}
}
2020-09-27 05:54:06 +00:00
}
2021-01-31 07:43:48 +00:00
extension TableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
let urls = indexPaths.compactMap(dataSource.itemIdentifier(for:))
.reduce(Set<URL>()) { $0.union($1.mediaPrefetchURLs(identityContext: viewModel.identityContext)) }
2021-02-22 23:59:33 +00:00
SDWebImagePrefetcher.shared.prefetchURLs(Array(urls))
2021-01-31 07:43:48 +00:00
}
}
2020-10-20 06:41:10 +00:00
extension TableViewController: AVPlayerViewControllerDelegate {
func playerViewController(
_ playerViewController: AVPlayerViewController,
willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
playerViewController.player?.isMuted = true
AVAudioSession.decrementPresentedPlayerViewControllerCount()
2021-01-08 06:11:33 +00:00
coordinator.animate(alongsideTransition: nil) { _ in
if self.shouldKeepPlayingVideoAfterDismissal {
playerViewController.player?.play()
}
}
2020-10-20 06:41:10 +00:00
}
}
2021-04-25 19:38:36 +00:00
extension TableViewController: UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController,
traitCollection: UITraitCollection) -> UIModalPresentationStyle {
.none
}
}
2020-10-22 05:05:50 +00:00
extension TableViewController: ZoomAnimatorDelegate {
func transitionWillStartWith(zoomAnimator: ZoomAnimator) {
view.layoutIfNeeded()
guard let imageViewController = (presentedViewController as? ImageNavigationController)?.currentViewController
else { return }
if imageViewController.playerView.tag != 0 {
transitionViewTag = imageViewController.playerView.tag
} else if imageViewController.imageView.tag != 0 {
transitionViewTag = imageViewController.imageView.tag
}
}
func transitionDidEndWith(zoomAnimator: ZoomAnimator) {
}
2020-10-15 07:44:01 +00:00
2020-10-22 05:05:50 +00:00
func referenceView(for zoomAnimator: ZoomAnimator) -> UIView? {
2020-10-22 22:16:06 +00:00
view.viewWithTag(transitionViewTag)
2020-10-22 05:05:50 +00:00
}
func referenceViewFrameInTransitioningView(for zoomAnimator: ZoomAnimator) -> CGRect? {
guard let referenceView = referenceView(for: zoomAnimator) else { return nil }
2020-12-03 20:41:28 +00:00
return parent?.view.convert(referenceView.frame, from: referenceView.superview)
2020-10-22 05:05:50 +00:00
}
}
2021-02-06 01:16:59 +00:00
extension TableViewController: ScrollableToTop {
func scrollToTop(animated: Bool) {
2021-02-21 23:00:56 +00:00
guard !dataSource.snapshot().itemIdentifiers.isEmpty else { return }
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
2021-02-06 01:16:59 +00:00
}
}
2020-10-22 05:05:50 +00:00
private extension TableViewController {
2020-12-11 02:51:08 +00:00
static let bottomInset: CGFloat = .newStatusButtonDimension + .defaultSpacing * 4
static let loadingFooterDebounceInterval: TimeInterval = 0.75
2020-12-11 02:51:08 +00:00
2021-01-25 02:10:41 +00:00
var bottomInset: CGFloat { insetBottom ? Self.bottomInset : 0 }
2021-02-06 04:27:08 +00:00
// swiftlint:disable:next function_body_length
2020-09-26 06:37:30 +00:00
func setupViewModelBindings() {
viewModel.title.sink { [weak self] in self?.navigationItem.title = $0 }.store(in: &cancellables)
viewModel.titleLocalizationComponents.receive(on: DispatchQueue.main).sink { [weak self] in
2020-12-03 01:41:22 +00:00
guard let key = $0.first else { return }
self?.navigationItem.title = String(
format: NSLocalizedString(key, comment: ""),
arguments: Array($0.suffix(from: 1)))
}
.store(in: &cancellables)
2021-02-06 04:23:05 +00:00
viewModel.updates.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.update($0) }
.store(in: &cancellables)
2020-09-26 06:37:30 +00:00
2020-10-07 00:31:29 +00:00
viewModel.events.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.handle(event: $0) }
.store(in: &cancellables)
2020-09-26 06:37:30 +00:00
2020-10-14 00:03:01 +00:00
viewModel.expandAll.receive(on: DispatchQueue.main)
2021-03-03 05:02:07 +00:00
.sink { [weak self] in self?.configureRightBarButtonItem(expandAllState: $0) }
2020-10-07 21:06:26 +00:00
.store(in: &cancellables)
2021-01-16 19:41:01 +00:00
viewModel.loading.receive(on: DispatchQueue.main).assign(to: &$loading)
$loading.combineLatest(
2021-01-16 20:06:35 +00:00
$loading.debounce(
for: .seconds(Self.loadingFooterDebounceInterval),
scheduler: DispatchQueue.main))
.sink { [weak self] loading, debouncedLoading in
2021-01-16 20:06:35 +00:00
guard let self = self else { return }
2020-09-26 06:37:30 +00:00
2021-01-19 03:24:43 +00:00
let refreshControlVisibile = self.refreshControl?.isRefreshing ?? false
if !loading, refreshControlVisibile {
2021-01-16 20:06:35 +00:00
self.refreshControl?.endRefreshing()
}
self.tableView.tableFooterView =
loading && debouncedLoading && !refreshControlVisibile ? self.loadingTableFooterView : UIView()
2021-01-16 20:06:35 +00:00
self.sizeTableHeaderFooterViews()
}
.store(in: &cancellables)
2020-10-06 23:12:11 +00:00
2021-01-04 01:19:33 +00:00
viewModel.alertItems
.compactMap { $0 }
2021-03-06 05:48:11 +00:00
.sink { [weak self] in
2021-03-09 04:40:22 +00:00
guard let self = self, self.isVisible, self.presentedViewController == nil else { return }
2021-03-06 05:48:11 +00:00
self.present(alertItem: $0)
}
2021-01-04 01:19:33 +00:00
.store(in: &cancellables)
2020-10-06 23:12:11 +00:00
tableView.publisher(for: \.contentOffset)
2021-02-27 19:40:06 +00:00
.removeDuplicates()
.handleEvents(receiveOutput: { [weak self] _ in
guard let self = self else { return }
if (self.newItemsView.layer.animationKeys() ?? []).isEmpty, self.newItemsView.alpha > 0 {
self.hideNewItemsView()
}
})
2020-10-06 23:12:11 +00:00
.compactMap { [weak self] _ in self?.tableView.indexPathsForVisibleRows?.first }
2021-02-27 19:40:06 +00:00
.sink { [weak self] in
guard let self = self else { return }
self.viewModel.viewedAtTop(indexPath: $0)
}
2020-10-06 23:12:11 +00:00
.store(in: &cancellables)
2021-01-27 22:27:54 +00:00
NotificationCenter.default.publisher(for: UIScene.willEnterForegroundNotification)
2021-01-29 02:41:41 +00:00
.merge(with: NotificationCenter.default.publisher(for: NewStatusViewController.newStatusPostedNotification))
2021-03-06 05:48:11 +00:00
.sink { [weak self] _ in
guard let self = self, self.isVisible else { return }
self.refreshIfAble()
}
2021-01-27 22:27:54 +00:00
.store(in: &cancellables)
NotificationCenter.default.publisher(for: LoadMoreView.accessibilityCustomAction)
.sink { [weak self] notification in
guard let self = self,
let loadMoreView = notification.object as? LoadMoreView,
let cell = self.tableView.visibleCells.first(where: { $0.contentView === loadMoreView }),
let indexPath = self.tableView.indexPath(for: cell)
else { return }
self.tableView(self.tableView, didSelectRowAt: indexPath)
}
.store(in: &cancellables)
2020-09-26 06:37:30 +00:00
}
2020-10-07 21:06:26 +00:00
func update(_ update: CollectionUpdate) {
2021-01-17 02:47:43 +00:00
let positionMaintenanceOffset: CGFloat
2021-02-06 04:23:05 +00:00
let preUpdateContentOffsetY = tableView.contentOffset.y
var setPreviousOffset = false
2021-02-21 23:00:56 +00:00
let firstItemId = dataSource.snapshot().itemIdentifiers.first?.itemId
2021-01-17 02:47:43 +00:00
if let itemId = update.maintainScrollPositionItemId,
let indexPath = dataSource.indexPath(itemId: itemId) {
positionMaintenanceOffset = tableView.rectForRow(at: indexPath).origin.y
2021-02-06 04:23:05 +00:00
- tableView.safeAreaInsets.top - preUpdateContentOffsetY
2021-01-17 02:47:43 +00:00
} else {
positionMaintenanceOffset = 0
2020-09-15 05:41:09 +00:00
}
2021-02-06 04:23:05 +00:00
if let headerView = tableView.tableHeaderView,
let headerViewWindowFrame = view.window?.convert(headerView.frame, from: headerView),
headerViewWindowFrame.maxY > 0 {
setPreviousOffset = true
}
self.dataSource.applySnapshotUsingReloadData(update.sections.snapshot()) { [weak self] in
2020-09-15 05:41:09 +00:00
guard let self = self else { return }
2021-01-17 02:47:43 +00:00
if let itemId = update.maintainScrollPositionItemId,
2021-02-19 06:24:00 +00:00
let indexPath = self.dataSource.indexPath(itemId: itemId) {
2021-01-17 02:16:43 +00:00
if update.shouldAdjustContentInset {
2020-10-27 03:01:12 +00:00
self.tableView.contentInset.bottom = max(
2021-01-17 02:16:43 +00:00
self.tableView.safeAreaLayoutGuide.layoutFrame.height
- self.tableView.rectForRow(at: indexPath).height,
2021-01-25 02:10:41 +00:00
self.bottomInset)
2020-10-27 03:01:12 +00:00
}
2020-10-07 21:06:26 +00:00
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
2021-01-17 02:47:43 +00:00
self.tableView.contentOffset.y -= positionMaintenanceOffset
2021-02-21 23:00:56 +00:00
if self.viewModel.announcesNewItems,
let firstItemId = firstItemId,
let newFirstItem = self.dataSource.snapshot().itemIdentifiers.first,
let newFirstItemId = newFirstItem.itemId,
newFirstItemId > firstItemId {
DispatchQueue.main.async {
self.announceNewItems(newestItem: newFirstItem)
}
}
2021-02-06 04:23:05 +00:00
} else if setPreviousOffset {
self.tableView.contentOffset.y = preUpdateContentOffsetY
2020-09-15 05:41:09 +00:00
}
2021-02-27 19:40:06 +00:00
self.tableView.layoutIfNeeded()
2020-09-15 05:41:09 +00:00
}
}
// swiftlint:disable:next cyclomatic_complexity function_body_length
2020-10-07 00:31:29 +00:00
func handle(event: CollectionItemEvent) {
switch event {
case .ignorableOutput:
break
case .contextParentDeleted:
navigationController?.popViewController(animated: true)
2021-02-03 21:51:45 +00:00
case .refresh:
refreshIfAble()
2020-10-07 00:31:29 +00:00
case let .share(url):
share(url: url)
2020-10-20 06:41:10 +00:00
case let .navigation(navigation):
2021-01-10 05:56:15 +00:00
handle(navigation: navigation)
2021-04-25 19:38:36 +00:00
case let .reload(collectionItem):
reload(collectionItem: collectionItem)
case let .presentEmojiPicker(sourceViewTag, selectionAction):
presentEmojiPicker(sourceViewTag: sourceViewTag, selectionAction: selectionAction)
2020-10-20 06:41:10 +00:00
case let .attachment(attachmentViewModel, statusViewModel):
present(attachmentViewModel: attachmentViewModel, statusViewModel: statusViewModel)
case let .compose(identity, inReplyToViewModel, redraft, redraftWasContextParent, directMessageTo):
2021-03-21 23:23:41 +00:00
compose(identity: identity,
inReplyToViewModel: inReplyToViewModel,
redraft: redraft,
redraftWasContextParent: redraftWasContextParent,
2021-03-21 23:23:41 +00:00
directMessageTo: directMessageTo)
2021-01-11 23:40:46 +00:00
case let .confirmDelete(statusViewModel, redraft):
confirmDelete(statusViewModel: statusViewModel, redraft: redraft)
2021-02-11 03:52:21 +00:00
case let .confirmUnfollow(accountViewModel):
confirmUnfollow(accountViewModel: accountViewModel)
case let .confirmHideReblogs(accountViewModel):
confirmHideReblogs(accountViewModel: accountViewModel)
case let .confirmShowReblogs(accountViewModel):
confirmShowReblogs(accountViewModel: accountViewModel)
2021-02-10 23:41:41 +00:00
case let .confirmMute(accountViewModel):
confirmMute(muteViewModel: accountViewModel.muteViewModel())
case let .confirmUnmute(accountViewModel):
confirmUnmute(accountViewModel: accountViewModel)
2021-02-10 07:46:00 +00:00
case let .confirmBlock(accountViewModel):
confirmBlock(accountViewModel: accountViewModel)
case let .confirmUnblock(accountViewModel):
confirmUnblock(accountViewModel: accountViewModel)
case let .confirmDomainBlock(accountViewModel):
confirmDomainBlock(accountViewModel: accountViewModel)
case let .confirmDomainUnblock(accountViewModel):
confirmDomainUnblock(accountViewModel: accountViewModel)
2020-11-30 02:54:11 +00:00
case let .report(reportViewModel):
2021-01-10 05:56:15 +00:00
report(reportViewModel: reportViewModel)
2021-01-27 00:15:52 +00:00
case let .accountListEdit(accountViewModel, edit):
accountListEdit(accountViewModel: accountViewModel, edit: edit)
2021-01-10 05:56:15 +00:00
}
}
2021-02-05 02:56:14 +00:00
func navigate(toNotification: MastodonNotification) {
guard let item = dataSource.snapshot().itemIdentifiers.first(where: {
guard case let .notification(notification, _) = $0 else { return false }
2021-01-23 03:48:33 +00:00
2021-02-05 02:56:14 +00:00
return notification.id == toNotification.id
}),
let indexPath = dataSource.indexPath(for: item)
else { return }
2021-01-23 03:48:33 +00:00
2021-02-05 02:56:14 +00:00
tableView.scrollToRow(at: indexPath, at: .none, animated: !UIAccessibility.isReduceMotionEnabled)
viewModel.select(indexPath: indexPath)
2020-10-20 06:41:10 +00:00
}
2021-04-25 19:38:36 +00:00
func reload(collectionItem: CollectionItem) {
var snapshot = dataSource.snapshot()
snapshot.reloadItems([collectionItem])
dataSource.apply(snapshot, animatingDifferences: false)
}
func presentEmojiPicker(sourceViewTag: Int, selectionAction: @escaping (String) -> Void) {
guard let fromView = view.viewWithTag(sourceViewTag) else { return }
let emojiPickerViewModel = EmojiPickerViewModel(identityContext: viewModel.identityContext)
let emojiPickerController = EmojiPickerViewController(
viewModel: emojiPickerViewModel,
selectionAction: { [weak self] in
selectionAction($1.name)
self?.dismiss(animated: true)
},
deletionAction: nil,
searchPresentationAction: nil)
let navigationController = UINavigationController(rootViewController: emojiPickerController)
navigationController.preferredContentSize = .init(
width: view.readableContentGuide.layoutFrame.width,
height: view.frame.height / 2)
navigationController.modalPresentationStyle = .popover
navigationController.popoverPresentationController?.delegate = self
navigationController.popoverPresentationController?.sourceView = fromView
navigationController.popoverPresentationController?.backgroundColor = .clear
present(navigationController, animated: true)
}
2020-10-20 06:41:10 +00:00
func present(attachmentViewModel: AttachmentViewModel, statusViewModel: StatusViewModel) {
switch attachmentViewModel.attachment.type {
case .audio, .video:
let playerViewController = AVPlayerViewController()
let player: AVPlayer
if attachmentViewModel.attachment.type == .video {
2021-03-29 06:04:14 +00:00
player = PlayerCache.shared.player(url: attachmentViewModel.attachment.url.url)
2020-10-20 06:41:10 +00:00
} else {
2021-03-29 06:04:14 +00:00
player = AVPlayer(url: attachmentViewModel.attachment.url.url)
2020-10-20 06:41:10 +00:00
}
playerViewController.delegate = self
playerViewController.player = player
2021-01-08 06:11:33 +00:00
shouldKeepPlayingVideoAfterDismissal = attachmentViewModel.shouldAutoplay
2020-10-20 06:41:10 +00:00
present(playerViewController, animated: true) {
AVAudioSession.incrementPresentedPlayerViewControllerCount()
2020-10-20 06:41:10 +00:00
player.isMuted = false
player.play()
}
case .image, .gifv:
2020-10-21 08:07:13 +00:00
let imagePageViewController = ImagePageViewController(
initiallyVisible: attachmentViewModel,
statusViewModel: statusViewModel)
let imageNavigationController = ImageNavigationController(imagePageViewController: imagePageViewController)
2020-10-22 05:05:50 +00:00
imageNavigationController.transitionController.fromDelegate = self
transitionViewTag = attachmentViewModel.tag
2020-10-21 08:07:13 +00:00
present(imageNavigationController, animated: true)
2020-10-20 06:41:10 +00:00
case .unknown:
break
2020-10-07 00:31:29 +00:00
}
}
2021-03-21 23:23:41 +00:00
func compose(identity: Identity?,
inReplyToViewModel: StatusViewModel?,
redraft: Status?,
redraftWasContextParent: Bool,
2021-03-21 23:23:41 +00:00
directMessageTo: AccountViewModel?) {
if redraftWasContextParent {
navigationController?.popViewController(animated: true)
}
2021-02-15 08:47:30 +00:00
rootViewModel?.navigationViewModel?.presentedNewStatusViewModel = rootViewModel?.newStatusViewModel(
identityContext: viewModel.identityContext,
2021-03-21 23:23:41 +00:00
identity: identity,
2021-01-11 22:45:30 +00:00
inReplyTo: inReplyToViewModel,
2021-03-02 00:53:36 +00:00
redraft: redraft,
directMessageTo: directMessageTo)
2021-01-10 05:56:15 +00:00
}
2021-01-11 23:40:46 +00:00
func confirmDelete(statusViewModel: StatusViewModel, redraft: Bool) {
2021-01-31 01:43:48 +00:00
let deleteAndRedraftConfirmMessage: String
let deleteConfirmMessage: String
switch viewModel.identityContext.appPreferences.statusWord {
case .toot:
deleteAndRedraftConfirmMessage = NSLocalizedString("status.delete-and-redraft.confirm.toot", comment: "")
deleteConfirmMessage = NSLocalizedString("status.delete.confirm.toot", comment: "")
case .post:
deleteAndRedraftConfirmMessage = NSLocalizedString("status.delete-and-redraft.confirm.post", comment: "")
deleteConfirmMessage = NSLocalizedString("status.delete.confirm.post", comment: "")
}
2021-01-11 23:40:46 +00:00
let alertController = UIAlertController(
title: nil,
message: redraft
2021-01-31 01:43:48 +00:00
? deleteAndRedraftConfirmMessage
: deleteConfirmMessage,
2021-01-11 23:40:46 +00:00
preferredStyle: .alert)
let deleteAction = UIAlertAction(
title: redraft
? NSLocalizedString("status.delete-and-redraft", comment: "")
: NSLocalizedString("status.delete", comment: ""),
style: .destructive) { _ in
redraft ? statusViewModel.deleteAndRedraft() : statusViewModel.delete()
}
let cancelAction = UIAlertAction(title: NSLocalizedString("cancel", comment: ""), style: .cancel) { _ in }
alertController.addAction(deleteAction)
alertController.addAction(cancelAction)
present(alertController, animated: true)
}
2021-02-11 03:52:21 +00:00
func confirmUnfollow(accountViewModel: AccountViewModel) {
confirm(message: String.localizedStringWithFormat(
NSLocalizedString("account.unfollow.confirm-%@", comment: ""),
accountViewModel.accountName)) {
accountViewModel.unfollow()
}
}
func confirmHideReblogs(accountViewModel: AccountViewModel) {
confirm(message: String.localizedStringWithFormat(
NSLocalizedString("account.hide-reblogs.confirm-%@", comment: ""),
accountViewModel.accountName)) {
accountViewModel.hideReblogs()
}
}
func confirmShowReblogs(accountViewModel: AccountViewModel) {
confirm(message: String.localizedStringWithFormat(
NSLocalizedString("account.show-reblogs.confirm-%@", comment: ""),
accountViewModel.accountName)) {
accountViewModel.showReblogs()
}
}
2021-02-10 23:41:41 +00:00
func confirmMute(muteViewModel: MuteViewModel) {
let muteViewController = MuteViewController(viewModel: muteViewModel)
let navigationController = UINavigationController(rootViewController: muteViewController)
present(navigationController, animated: true)
}
func confirmUnmute(accountViewModel: AccountViewModel) {
confirm(message: String.localizedStringWithFormat(
NSLocalizedString("account.unmute.confirm-%@", comment: ""),
accountViewModel.accountName)) {
accountViewModel.unmute()
}
}
2021-02-10 07:46:00 +00:00
func confirmBlock(accountViewModel: AccountViewModel) {
let alertController = UIAlertController(
title: nil,
message: String.localizedStringWithFormat(
NSLocalizedString("account.block.confirm-%@", comment: ""),
accountViewModel.accountName), preferredStyle: .alert)
let blockAction = UIAlertAction(title: NSLocalizedString("account.block", comment: ""),
style: .destructive) { _ in
accountViewModel.block()
}
let blockAndReportAction = UIAlertAction(title: NSLocalizedString("account.block-and-report", comment: ""),
style: .destructive) { [weak self] _ in
accountViewModel.block()
self?.report(reportViewModel: accountViewModel.reportViewModel())
}
let cancelAction = UIAlertAction(title: NSLocalizedString("cancel", comment: ""), style: .cancel) { _ in }
alertController.addAction(blockAction)
alertController.addAction(blockAndReportAction)
alertController.addAction(cancelAction)
present(alertController, animated: true)
}
func confirmUnblock(accountViewModel: AccountViewModel) {
confirm(message: String.localizedStringWithFormat(
NSLocalizedString("account.unblock.confirm-%@", comment: ""),
accountViewModel.accountName)) {
accountViewModel.unblock()
}
}
func confirmDomainBlock(accountViewModel: AccountViewModel) {
guard let domain = accountViewModel.domain else { return }
confirm(message: String.localizedStringWithFormat(
NSLocalizedString("account.domain-block.confirm-%@", comment: ""),
domain),
style: .destructive) {
accountViewModel.domainBlock()
}
}
func confirmDomainUnblock(accountViewModel: AccountViewModel) {
guard let domain = accountViewModel.domain else { return }
confirm(message: String.localizedStringWithFormat(
NSLocalizedString("account.domain-unblock.confirm-%@", comment: ""),
domain)) {
accountViewModel.domainUnblock()
}
}
2021-01-27 00:15:52 +00:00
func accountListEdit(accountViewModel: AccountViewModel, edit: CollectionItemEvent.AccountListEdit) {
viewModel.applyAccountListEdit(viewModel: accountViewModel, edit: edit)
}
2020-09-14 23:32:34 +00:00
func share(url: URL) {
2021-01-31 13:57:30 +00:00
let activityViewController = UIActivityViewController(
activityItems: [url],
2021-02-06 08:07:23 +00:00
applicationActivities: [OpenInDefaultBrowserActivity()])
2020-09-14 23:32:34 +00:00
2021-01-19 19:00:26 +00:00
if UIDevice.current.userInterfaceIdiom == .pad {
guard let sourceView = tableView.viewWithTag(url.hashValue) else { return }
activityViewController.popoverPresentationController?.sourceView = sourceView
}
2020-09-14 23:32:34 +00:00
present(activityViewController, animated: true, completion: nil)
}
2021-01-29 02:41:41 +00:00
func refreshIfAble() {
if viewModel.canRefresh {
viewModel.request(maxId: nil, minId: nil, search: nil)
}
}
2021-02-21 23:00:56 +00:00
func newItemsTapped() {
scrollToTop(animated: true)
}
func announceNewItems(newestItem: CollectionItem) {
switch newestItem {
case .status:
switch viewModel.identityContext.appPreferences.statusWord {
case .toot:
newItemsView.title = NSLocalizedString("status.new-items.toot", comment: "")
case .post:
newItemsView.title = NSLocalizedString("status.new-items.post", comment: "")
}
case .notification:
newItemsView.title = NSLocalizedString("notification.new-items", comment: "")
default:
return
}
newItemsView.layoutIfNeeded()
UIView.animate(withDuration: .zeroIfReduceMotion(.defaultAnimationDuration),
delay: 0,
usingSpringWithDamping: 0.5,
initialSpringVelocity: 5,
options: .curveEaseInOut) {
self.newItemsView.alpha = 1
self.newItemsViewHiddenConstraint?.isActive = false
self.newItemsViewVisibleConstraint?.isActive = true
self.view.layoutIfNeeded()
} completion: { _ in
2021-03-12 19:09:40 +00:00
self.reloadVisibleItems()
2021-02-21 23:00:56 +00:00
}
}
func hideNewItemsView() {
UIView.animate(withDuration: .zeroIfReduceMotion(.defaultAnimationDuration)) {
self.newItemsView.alpha = 0
self.newItemsViewHiddenConstraint?.isActive = true
self.newItemsViewVisibleConstraint?.isActive = false
self.view.layoutIfNeeded()
2021-02-27 19:40:06 +00:00
} completion: { _ in
2021-03-12 19:09:40 +00:00
self.reloadVisibleItems()
2021-02-21 23:00:56 +00:00
}
}
2021-03-12 19:09:40 +00:00
func reloadVisibleItems() {
guard let visibleItems = tableView.indexPathsForVisibleRows?.compactMap(dataSource.itemIdentifier(for:))
else { return }
var snapshot = dataSource.snapshot()
snapshot.reloadItems(visibleItems)
dataSource.apply(snapshot, animatingDifferences: false)
}
2020-08-21 02:29:01 +00:00
}
2021-01-10 05:56:15 +00:00
// swiftlint:enable file_length