Tusker/Tusker/Screens/Conversation/ConversationViewController....

263 lines
9.1 KiB
Swift

//
// ConversationViewController.swift
// Tusker
//
// Created by Shadowfacts on 1/17/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class ConversationViewController: UIViewController {
weak var mastodonController: MastodonController!
let mainStatusID: String
let mainStatusState: CollapseState
var statusIDToScrollToOnLoad: String {
didSet {
if case .displaying(let vc) = state {
vc.statusIDToScrollToOnLoad = statusIDToScrollToOnLoad
}
}
}
var showStatusesAutomatically = false {
didSet {
if case .displaying(let vc) = state {
vc.showStatusesAutomatically = showStatusesAutomatically
}
}
}
private var collapseBarButtonItem: UIBarButtonItem!
private var state: State = .unloaded {
didSet {
switch oldValue {
case .loading(let indicator):
indicator.removeFromSuperview()
case .displaying(let vc):
vc.removeViewAndController()
default:
break
}
switch state {
case .unloaded:
break
case .loading(let indicator):
indicator.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(indicator)
NSLayoutConstraint.activate([
indicator.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
indicator.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
])
case .displaying(let vc):
embedChild(vc)
case .notFound:
showMainStatusNotFound()
}
updateVisibilityBarButtonItem()
}
}
init(for mainStatusID: String, state mainStatusState: CollapseState, mastodonController: MastodonController) {
self.mainStatusID = mainStatusID
self.mainStatusState = mainStatusState
self.statusIDToScrollToOnLoad = mainStatusID
self.mastodonController = mastodonController
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = NSLocalizedString("Conversation", comment: "conversation screen title")
view.backgroundColor = .secondarySystemBackground
collapseBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "eye.fill")!, style: .plain, target: self, action: #selector(toggleCollapseButtonPressed))
updateVisibilityBarButtonItem()
navigationItem.rightBarButtonItem = collapseBarButtonItem
// disable transparent background when scroll to top because it looks weird when items earlier in the thread load in
// (it remains transparent slightly too long, resulting in a flash of the content under the transparent bar)
let appearance = UINavigationBarAppearance()
appearance.configureWithDefaultBackground()
navigationItem.scrollEdgeAppearance = appearance
}
private func updateVisibilityBarButtonItem() {
switch state {
case .loading(_), .displaying(_):
collapseBarButtonItem.isEnabled = true
default:
collapseBarButtonItem.isEnabled = false
}
collapseBarButtonItem.isSelected = showStatusesAutomatically
collapseBarButtonItem.accessibilityLabel = showStatusesAutomatically ? "Collapse All" : "Expand All"
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
Task {
await loadMainStatus()
}
}
// MARK: Loading
private func loadMainStatus() async {
@MainActor
func doLoadMainStatus() async -> StatusMO? {
switch await FetchStatusService(statusID: mainStatusID, mastodonController: mastodonController).run() {
case .loaded(let status):
return mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
case .notFound:
state = .notFound
showMainStatusNotFound()
return nil
case .error(let error):
self.showMainStatusError(error)
return nil
}
}
if let cached = mastodonController.persistentContainer.status(for: mainStatusID) {
// if we have a cached copy, display it immediately but still try to refresh it
Task {
await doLoadMainStatus()
}
await mainStatusLoaded(cached)
} else {
// otherwise, show a loading indicator while loading the main status
let indicator = UIActivityIndicatorView(style: .medium)
indicator.startAnimating()
state = .loading(indicator)
if let status = await doLoadMainStatus() {
await mainStatusLoaded(status)
}
}
}
@MainActor
private func mainStatusLoaded(_ mainStatus: StatusMO) async {
let vc = ConversationTableViewController(for: mainStatusID, state: mainStatusState, mastodonController: mastodonController)
vc.statusIDToScrollToOnLoad = statusIDToScrollToOnLoad
vc.showStatusesAutomatically = showStatusesAutomatically
vc.addMainStatus(mainStatus)
state = .displaying(vc)
await loadContext(for: mainStatus)
}
@MainActor
private func loadContext(for mainStatus: StatusMO) async {
guard case .displaying(_) = state else {
return
}
let request = Status.getContext(mainStatus.id)
do {
let (context, _) = try await mastodonController.run(request)
guard case .displaying(let vc) = state else {
return
}
await vc.addContext(context, for: mainStatus)
} catch {
guard case .displaying(_) = state else {
return
}
let error = error as! Client.Error
let config = ToastConfiguration(from: error, with: "Error Loading Context", in: self) { [weak self] toast in
toast.dismissToast(animated: true)
await self?.loadContext(for: mainStatus)
}
self.showToast(configuration: config, animated: true)
}
}
private func showMainStatusNotFound() {
let emoji = UILabel()
emoji.font = .systemFont(ofSize: 64)
emoji.text = "🤷"
let title = UILabel()
title.textColor = .secondaryLabel
title.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
title.adjustsFontForContentSizeCategory = true
title.text = "Not Found"
let subtitle = UILabel()
subtitle.textColor = .secondaryLabel
subtitle.font = .preferredFont(forTextStyle: .body)
subtitle.adjustsFontForContentSizeCategory = true
subtitle.text = "The post you are looking for may have been deleted, or may not be visible to you."
subtitle.numberOfLines = 0
subtitle.textAlignment = .center
let stack = UIStackView(arrangedSubviews: [
emoji,
title,
subtitle,
])
stack.axis = .vertical
stack.alignment = .center
stack.spacing = 8
stack.isAccessibilityElement = true
stack.accessibilityLabel = "\(title.text!). \(subtitle.text!)"
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalToSystemSpacingAfter: view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1),
view.safeAreaLayoutGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: stack.trailingAnchor, multiplier: 1),
stack.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
])
}
private func showMainStatusError(_ error: Client.Error) {
let config = ToastConfiguration(from: error, with: "Error Loading Status", in: self) { [weak self] toast in
toast.dismissToast(animated: true)
await self?.loadMainStatus()
}
self.showToast(configuration: config, animated: true)
}
// MARK: Interaction
@objc func toggleCollapseButtonPressed() {
guard case .displaying(let vc) = state else {
return
}
showStatusesAutomatically = !showStatusesAutomatically
vc.updateVisibleCellCollapseState()
updateVisibilityBarButtonItem()
}
}
extension ConversationViewController {
enum State {
case unloaded
case loading(UIActivityIndicatorView)
case displaying(ConversationTableViewController)
case notFound
}
}
extension ConversationViewController: ToastableViewController {
}