// // StatusActionAccountListViewController.swift // Tusker // // Created by Shadowfacts on 1/17/23. // Copyright © 2023 Shadowfacts. All rights reserved. // import UIKit import Pachyderm class StatusActionAccountListViewController: UIViewController { private let mastodonController: MastodonController private let actionType: StatusActionAccountListViewController.ActionType private let statusID: String private let statusState: CollapseState private var accountIDs: [String]? /// If `true`, a warning will be shown below the account list describing that the total favs/reblogs may be innacurate. var showInaccurateCountWarning = false 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): vc.view.translatesAutoresizingMaskIntoConstraints = false embedChild(vc) case .notFound: showStatusNotFound() } } } /** Creates a new view controller showing the accounts that performed the given action on the given status. - Parameter actionType The action that this VC is for. - Parameter statusID The ID of the status to show. - Parameter accountIDs The accounts that will be shown. If `nil` is passed, a request will be performed to load the accounts. - Parameter mastodonController The `MastodonController` instance this view controller uses. */ init(actionType: StatusActionAccountListViewController.ActionType, statusID: String, statusState: CollapseState, accountIDs: [String]?, mastodonController: MastodonController) { self.mastodonController = mastodonController self.actionType = actionType self.statusID = statusID self.statusState = statusState self.accountIDs = accountIDs super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() switch actionType { case .favorite: title = NSLocalizedString("Favorited By", comment: "status favorited by accounts list title") case .reblog: title = NSLocalizedString("Reblogged By", comment: "status reblogged by accounts list title") } view.backgroundColor = .appBackground NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if case .unloaded = state { Task { await loadStatus() } } } @objc private func handleStatusDeleted(_ notification: Foundation.Notification) { guard let userInfo = notification.userInfo, let accountID = mastodonController.accountInfo?.id, userInfo["accountID"] as? String == accountID, let statusIDs = userInfo["statusIDs"] as? [String] else { return } if statusIDs.contains(statusID) { state = .notFound } } // MARK: Loading private func loadStatus() async { @MainActor func doLoadStatus() async -> StatusMO? { switch await FetchStatusService(statusID: statusID, mastodonController: mastodonController).run() { case .loaded(let status): return mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status) case .notFound: state = .notFound return nil case .error(let error): self.showStatusError(error) return nil } } if let cached = mastodonController.persistentContainer.status(for: statusID) { await statusLoaded(cached) } else { let indicator = UIActivityIndicatorView(style: .medium) indicator.startAnimating() state = .loading(indicator) if let status = await doLoadStatus() { await statusLoaded(status) } } } private func statusLoaded(_ status: StatusMO) async { let vc = StatusActionAccountListCollectionViewController(statusID: statusID, actionType: actionType, mastodonController: mastodonController) vc.addStatus(status, state: statusState, showInaccurateCountWarning: showInaccurateCountWarning) if let accountIDs { vc.setAccounts(accountIDs, animated: false) } state = .displaying(vc) } private func showStatusNotFound() { let notFoundView = StatusNotFoundView(frame: .zero) notFoundView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(notFoundView) NSLayoutConstraint.activate([ notFoundView.leadingAnchor.constraint(equalToSystemSpacingAfter: view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1), view.safeAreaLayoutGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: notFoundView.trailingAnchor, multiplier: 1), notFoundView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor), ]) } private func showStatusError(_ error: Client.Error) { let config = ToastConfiguration(from: error, with: "Error Loading Status", in: self) { [weak self] toast in toast.dismissToast(animated: true) await self?.loadStatus() } self.showToast(configuration: config, animated: true) } } extension StatusActionAccountListViewController { enum State { case unloaded case loading(UIActivityIndicatorView) case displaying(StatusActionAccountListCollectionViewController) case notFound } } extension StatusActionAccountListViewController { enum ActionType { case favorite, reblog } } extension StatusActionAccountListViewController: ToastableViewController { var toastScrollView: UIScrollView? { if case .displaying(let vc) = state { return vc.toastScrollView } else { return nil } } }