// // ProfileTableViewController.swift // Tusker // // Created by Shadowfacts on 8/27/18. // Copyright © 2018 Shadowfacts. All rights reserved. // import UIKit import Pachyderm import SafariServices class ProfileTableViewController: EnhancedTableViewController { var accountID: String! { didSet { if shouldLoadOnAccountIDSet { DispatchQueue.main.async { self.updateAccountUI() } } } } var timelineSegments: [TimelineSegment] = [] { didSet { DispatchQueue.main.async { self.tableView.reloadData() } } } var older: RequestRange? var newer: RequestRange? var shouldLoadOnAccountIDSet = false var loadingVC: LoadingViewController? = nil init(accountID: String?) { self.accountID = accountID super.init(style: .plain) self.refreshControl = UIRefreshControl() refreshControl!.addTarget(self, action: #selector(refreshStatuses(_:)), for: .valueChanged) navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composePressed(_:))) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemeneted") } override func viewDidLoad() { super.viewDidLoad() tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 140 tableView.register(UINib(nibName: "StatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell") tableView.register(UINib(nibName: "ProfileHeaderTableViewCell", bundle: nil), forCellReuseIdentifier: "headerCell") tableView.prefetchDataSource = self if let accountID = accountID { if MastodonCache.account(for: accountID) != nil { updateAccountUI() } else { loadingVC = LoadingViewController() embedChild(loadingVC!) MastodonCache.account(for: accountID) { (account) in guard account != nil else { let alert = UIAlertController(title: "Something Went Wrong", message: "Couldn't load the selected account", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (_) in self.navigationController!.popViewController(animated: true) })) DispatchQueue.main.async { self.present(alert, animated: true) } return } DispatchQueue.main.async { self.updateAccountUI() self.tableView.reloadData() } } } } else { loadingVC = LoadingViewController() embedChild(loadingVC!) shouldLoadOnAccountIDSet = true } NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) } func updateAccountUI() { loadingVC?.removeViewAndController() updateUIForPreferences() getStatuses() { response in guard case let .success(statuses, pagination) = response else { fatalError() } MastodonCache.addAll(statuses: statuses) self.timelineSegments.append(TimelineSegment(objects: statuses)) self.older = pagination?.older self.newer = pagination?.newer } } @objc func updateUIForPreferences() { guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } navigationItem.title = account.realDisplayName } func getStatuses(for range: RequestRange = .default, completion: @escaping Client.Callback<[Status]>) { let request = Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: false, excludeReplies: !Preferences.shared.showRepliesInProfiles) MastodonController.client.run(request, completion: completion) } func sendMessageMentioning() { guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } let vc = UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct)) present(vc, animated: true) } // MARK: - Table view data source override func numberOfSections(in tableView: UITableView) -> Int { return 1 + timelineSegments.count } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if section == 0 { return accountID == nil || MastodonCache.account(for: accountID) == nil ? 0 : 1 } else { return timelineSegments[section - 1].count } } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { if indexPath.section == 0 { guard let cell = tableView.dequeueReusableCell(withIdentifier: "headerCell", for: indexPath) as? ProfileHeaderTableViewCell else { fatalError() } cell.selectionStyle = .none cell.delegate = self cell.updateUI(for: accountID) return cell } else { guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else { fatalError() } let statusID = timelineSegments[indexPath.section - 1][indexPath.row] cell.updateUI(statusID: statusID) cell.delegate = self return cell } } override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { if timelineSegments.count > 0 && indexPath.section == timelineSegments.count && indexPath.row == timelineSegments[indexPath.section - 1].count - 1 { let older = self.older ?? RequestRange.after(id: timelineSegments.last!.last!, count: nil) getStatuses(for: older) { response in guard case let .success(newStatuses, pagination) = response else { fatalError() } MastodonCache.addAll(statuses: newStatuses) self.timelineSegments[indexPath.section - 1].append(objects: newStatuses) self.older = pagination?.older } } } override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return true } override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration() } override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration() } @objc func refreshStatuses(_ sender: Any) { let newer = self.newer ?? RequestRange.after(id: timelineSegments.first!.first!, count: nil) getStatuses(for: newer) { response in guard case let .success(newStatuses, pagination) = response else { fatalError() } MastodonCache.addAll(statuses: newStatuses) self.timelineSegments[0].insertAtBeginning(objects: newStatuses) self.newer = pagination?.newer DispatchQueue.main.async { self.refreshControl?.endRefreshing() } } } @objc func composePressed(_ sender: Any) { sendMessageMentioning() } } extension ProfileTableViewController: StatusTableViewCellDelegate { func statusCollapsedStateChanged() { // causes the table view to recalculate the cell heights tableView.beginUpdates() tableView.endUpdates() } } extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate { func showMoreOptions() { let account = MastodonCache.account(for: accountID)! MastodonCache.relationship(for: account.id) { [weak self] (relationship) in guard let self = self else { return } let customActivities: [UIActivity] if let relationship = relationship { let toggleFollowActivity = relationship.following ? UnfollowAccountActivity() : FollowAccountActivity() customActivities = [ SendMessageActivity(), toggleFollowActivity, OpenInSafariActivity() ] } else { customActivities = [ SendMessageActivity(), OpenInSafariActivity() ] } DispatchQueue.main.async { let activityController = UIActivityViewController(activityItems: [account.url, account], applicationActivities: customActivities) activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: account.url) self.present(activityController, animated: true) } } } } extension ProfileTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths where indexPath.section > 0 { let statusID = timelineSegments[indexPath.section - 1][indexPath.row] guard let status = MastodonCache.status(for: statusID) else { continue } ImageCache.avatars.get(status.account.avatar, completion: nil) for attachment in status.attachments { ImageCache.attachments.get(attachment.url, completion: nil) } } } func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths where indexPath.section > 0 { let statusID = timelineSegments[indexPath.section - 1][indexPath.row] guard let status = MastodonCache.status(for: statusID) else { continue } ImageCache.avatars.cancel(status.account.avatar) for attachment in status.attachments { ImageCache.attachments.cancel(attachment.url) } } } }