// // AccountViewController.swift // Mastodon // // Created by Cirno MainasuK on 2021-9-13. // import UIKit import Combine import CoreDataStack import MastodonAsset import MastodonLocalization import MastodonCore final class AccountListViewController: UIViewController, NeedsDependency { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() var viewModel: AccountListViewModel! private(set) lazy var addBarButtonItem: UIBarButtonItem = { let barButtonItem = UIBarButtonItem( image: UIImage(systemName: "plus"), style: .plain, target: self, action: #selector(AccountListViewController.addBarButtonItem(_:)) ) return barButtonItem }() lazy var dragIndicatorView = DragIndicatorView { [weak self] in self?.dismiss(animated: true, completion: nil) } private(set) lazy var tableView: UITableView = { let tableView = UITableView() tableView.register(AccountListTableViewCell.self, forCellReuseIdentifier: String(describing: AccountListTableViewCell.self)) tableView.register(AddAccountTableViewCell.self, forCellReuseIdentifier: String(describing: AddAccountTableViewCell.self)) tableView.register(LogoutOfAllAccountsCell.self, forCellReuseIdentifier: LogoutOfAllAccountsCell.reuseIdentifier) tableView.backgroundColor = .clear tableView.tableFooterView = UIView() tableView.separatorInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 0) return tableView }() override var supportedInterfaceOrientations: UIInterfaceOrientationMask { .portraitOnPhone } } extension AccountListViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .secondarySystemGroupedBackground navigationItem.rightBarButtonItem = addBarButtonItem dragIndicatorView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(dragIndicatorView) NSLayoutConstraint.activate([ dragIndicatorView.topAnchor.constraint(equalTo: view.topAnchor), dragIndicatorView.leadingAnchor.constraint(equalTo: view.leadingAnchor), dragIndicatorView.trailingAnchor.constraint(equalTo: view.trailingAnchor), dragIndicatorView.heightAnchor.constraint(equalToConstant: DragIndicatorView.height).priority(.required - 1), ]) tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) NSLayoutConstraint.activate([ tableView.topAnchor.constraint(equalTo: dragIndicatorView.bottomAnchor), tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) tableView.delegate = self viewModel.setupDiffableDataSource(tableView: tableView) } } extension AccountListViewController { @objc private func addBarButtonItem(_ sender: UIBarButtonItem) { _ = coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil)) } override func accessibilityPerformEscape() -> Bool { dismiss(animated: true, completion: nil) return true } } // MARK: - UITableViewDelegate extension AccountListViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { guard let diffableDataSource = viewModel.diffableDataSource, let item = diffableDataSource.itemIdentifier(for: indexPath) else { return nil } switch item { case .authentication(let record): let logoutAction = UIContextualAction(style: .destructive, title: L10n.Scene.AccountList.logout, handler: { [weak self] action, view, completion in guard let self else { return } UserDefaults.shared.setNotificationCountWithAccessToken(accessToken: record.userAccessToken, value: 0) Task { @MainActor in do { try await self.viewModel.context.authenticationService.signOutMastodonUser(authentication: record) let userIdentifier = record FileManager.default.invalidateHomeTimelineCache(for: userIdentifier) FileManager.default.invalidateNotificationsAll(for: userIdentifier) FileManager.default.invalidateNotificationsMentions(for: userIdentifier) self.coordinator.setup() } catch { assertionFailure("Failed to delete Authentication: \(error)") } } }) logoutAction.image = UIImage(systemName: "rectangle.portrait.and.arrow.forward") let swipeConfiguration = UISwipeActionsConfiguration(actions: [logoutAction]) swipeConfiguration.performsFirstActionWithFullSwipe = false return swipeConfiguration case .addAccount, .logoutOfAllAccounts: return nil } } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) guard let diffableDataSource = viewModel.diffableDataSource, let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } switch item { case .authentication(let record): assert(Thread.isMainThread) Task { @MainActor in let isActive = try await context.authenticationService.activeMastodonUser(domain: record.domain, userID: record.userID) guard isActive else { return } self.coordinator.setup() } // end Task case .addAccount: // TODO: add dismiss entry for welcome scene _ = coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil)) case .logoutOfAllAccounts: let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) let logoutAction = UIAlertAction(title: L10n.Scene.AccountList.logoutAllAccounts, style: .destructive) { _ in Task { @MainActor in self.coordinator.showLoading() for authenticationBox in self.context.authenticationService.mastodonAuthenticationBoxes { try? await self.context.authenticationService.signOutMastodonUser(authenticationBox: authenticationBox) } self.coordinator.hideLoading() self.coordinator.setup() } } alert.addAction(logoutAction) alert.popoverPresentationController?.sourceView = tableView.cellForRow(at: indexPath) let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .default) alert.addAction(cancelAction) present(alert, animated: true) } } }