From 2edb65d302afab48ecaa82fd0b691831ca06a0e9 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 7 Sep 2019 17:10:58 -0400 Subject: [PATCH] Show favorite, reblog, and full timestamp in conversation main status --- ...ActionAccountListTableViewController.swift | 108 +++++++++------- Tusker/TuskerNavigationDelegate.swift | 21 ++-- ...ActionNotificationGroupTableViewCell.swift | 30 ++++- .../ConversationMainStatusTableViewCell.swift | 67 +++++----- .../ConversationMainStatusTableViewCell.xib | 117 +++++++++++++----- 5 files changed, 221 insertions(+), 122 deletions(-) diff --git a/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift b/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift index c916a33198..1a38cb7a4c 100644 --- a/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift +++ b/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift @@ -7,22 +7,46 @@ // import UIKit +import Pachyderm class StatusActionAccountListTableViewController: EnhancedTableViewController { private let statusCell = "statusCell" private let accountCell = "accountCell" + let actionType: ActionType let statusID: String - let accountIDs: [String] + var accountIDs: [String]? { + didSet { + tableView.reloadData() + } + } - init(statusID: String, accountIDs: [String]) { + /// If `true`, a warning will be shown below the account list describing that the total favs/reblogs may be innacurate. + var showInacurateCountWarning = false + + /** + 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. + */ + init(actionType: ActionType, statusID: String, accountIDs: [String]?) { + self.actionType = actionType self.statusID = statusID self.accountIDs = accountIDs super.init(style: .grouped) + + 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") + } } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -39,6 +63,25 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { tableView.alwaysBounceVertical = true tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: CGFloat.leastNormalMagnitude)) + + if accountIDs == nil { + // account IDs haven't been set, so perform a request to load them + guard let status = MastodonCache.status(for: statusID) else { + fatalError("Missing cached status \(statusID)") + } + + tableView.tableFooterView = UIActivityIndicatorView(style: .large) + + let request = actionType == .favorite ? Status.getFavourites(status) : Status.getReblogs(status) + MastodonController.client.run(request) { (response) in + guard case let .success(accounts, _) = response else { fatalError() } + MastodonCache.addAll(accounts: accounts) + DispatchQueue.main.async { + self.accountIDs = accounts.map { $0.id } + self.tableView.tableFooterView = nil + } + } + } } // MARK: - Table view data source @@ -52,7 +95,11 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { case 0: // status return 1 case 1: // accounts - return accountIDs.count + if let accountIDs = accountIDs { + return accountIDs.count + } else { + return 0 + } default: fatalError("Invalid section \(section)") } @@ -66,7 +113,8 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { cell.delegate = self return cell case 1: - guard let cell = tableView.dequeueReusableCell(withIdentifier: accountCell, for: indexPath) as? AccountTableViewCell else { fatalError() } + guard let accountIDs = accountIDs, + let cell = tableView.dequeueReusableCell(withIdentifier: accountCell, for: indexPath) as? AccountTableViewCell else { fatalError() } cell.updateUI(accountID: accountIDs[indexPath.row]) cell.delegate = self return cell @@ -74,51 +122,15 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { fatalError("Invalid section \(indexPath.section)") } } - - /* - // Override to support conditional editing of the table view. - override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - // Return false if you do not want the specified item to be editable. - return true + + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + guard section == 1, showInacurateCountWarning else { return nil } + return NSLocalizedString("Favorite and reblog counts for posts originating from instances other than your own may not be accurate.", comment: "shown on lists of status total actions") } - */ - - /* - // Override to support editing the table view. - override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { - if editingStyle == .delete { - // Delete the row from the data source - tableView.deleteRows(at: [indexPath], with: .fade) - } else if editingStyle == .insert { - // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view - } + + enum ActionType { + case favorite, reblog } - */ - - /* - // Override to support rearranging the table view. - override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) { - - } - */ - - /* - // Override to support conditional rearranging of the table view. - override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { - // Return false if you do not want the item to be re-orderable. - return true - } - */ - - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - } - */ } diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index 5915ba576b..ad1c2dd43e 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -12,6 +12,8 @@ import Pachyderm protocol TuskerNavigationDelegate { + func show(_ vc: UIViewController) + func selected(account accountID: String) func selected(mention: Mention) @@ -44,11 +46,15 @@ protocol TuskerNavigationDelegate { func showFollowedByList(accountIDs: [String]) - func showStatusActionAccountList(statusID: String, accountIDs: [String], action: Pachyderm.Notification.Kind) + func statusActionAccountList(action: StatusActionAccountListTableViewController.ActionType, statusID: String, accountIDs: [String]?) -> StatusActionAccountListTableViewController } extension TuskerNavigationDelegate where Self: UIViewController { + func show(_ vc: UIViewController) { + show(vc, sender: self) + } + func selected(account accountID: String) { // don't open if the account is the same as the current one if let profileController = self as? ProfileTableViewController, @@ -179,17 +185,8 @@ extension TuskerNavigationDelegate where Self: UIViewController { show(vc, sender: self) } - func showStatusActionAccountList(statusID: String, accountIDs: [String], action: Pachyderm.Notification.Kind) { - let vc = StatusActionAccountListTableViewController(statusID: statusID, accountIDs: accountIDs) - switch action { - case .favourite: - vc.title = NSLocalizedString("Favorited By", comment: "status favorited by accounts list title") - case .reblog: - vc.title = NSLocalizedString("Reblogged By", comment: "status reblogged by accounts list title") - default: - fatalError("Invalid notification type for action account list, only favourite and relog are allowed") - } - show(vc, sender: self) + func statusActionAccountList(action: StatusActionAccountListTableViewController.ActionType, statusID: String, accountIDs: [String]?) -> StatusActionAccountListTableViewController { + return StatusActionAccountListTableViewController(actionType: action, statusID: statusID, accountIDs: accountIDs) } } diff --git a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift index 4f98d00581..5bcd94f93c 100644 --- a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift +++ b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift @@ -153,21 +153,41 @@ class ActionNotificationGroupTableViewCell: UITableViewCell { override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) - if selected { + if selected, let delegate = delegate { let notifications = group.notificationIDs.compactMap(MastodonCache.notification(for:)) let accountIDs = notifications.map { $0.account.id } - delegate?.showStatusActionAccountList(statusID: statusID, accountIDs: accountIDs, action: notifications.first!.kind) + let action: StatusActionAccountListTableViewController.ActionType + switch notifications.first!.kind { + case .favourite: + action = .favorite + case .reblog: + action = .reblog + default: + fatalError() + } + let vc = delegate.statusActionAccountList(action: action, statusID: statusID, accountIDs: accountIDs) + delegate.show(vc) } } - + } extension ActionNotificationGroupTableViewCell: MenuPreviewProvider { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { return (content: { - let accountIDs = self.group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account.id } - return StatusActionAccountListTableViewController(statusID: self.statusID, accountIDs: accountIDs) + let notifications = self.group.notificationIDs.compactMap(MastodonCache.notification(for:)) + let accountIDs = notifications.map { $0.account.id } + let action: StatusActionAccountListTableViewController.ActionType + switch notifications.first!.kind { + case .favourite: + action = .favorite + case .reblog: + action = .reblog + default: + fatalError() + } + return self.delegate?.statusActionAccountList(action: action, statusID: self.statusID, accountIDs: accountIDs) }, actions: { return self.actionsForNotificationGroup(self.group) }) diff --git a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift index ec230480f8..32abc28614 100644 --- a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift +++ b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift @@ -12,6 +12,13 @@ import Pachyderm class ConversationMainStatusTableViewCell: UITableViewCell { + static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .medium + return formatter + }() + var delegate: StatusTableViewCellDelegate? { didSet { contentLabel.navigationDelegate = delegate @@ -22,7 +29,9 @@ class ConversationMainStatusTableViewCell: UITableViewCell { @IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var contentLabel: StatusContentLabel! @IBOutlet weak var avatarImageView: UIImageView! - @IBOutlet weak var timestampLabel: UILabel! + @IBOutlet weak var totalFavoritesButton: UIButton! + @IBOutlet weak var totalReblogsButton: UIButton! + @IBOutlet weak var timestampAndClientLabel: UILabel! @IBOutlet weak var attachmentsView: AttachmentsContainerView! @IBOutlet weak var favoriteButton: UIButton! @IBOutlet weak var reblogButton: UIButton! @@ -46,8 +55,7 @@ class ConversationMainStatusTableViewCell: UITableViewCell { } var avatarURL: URL? - var updateTimestampWorkItem: DispatchWorkItem? - + var statusUpdater: Cancellable? var accountUpdater: Cancellable? @@ -93,8 +101,13 @@ class ConversationMainStatusTableViewCell: UITableViewCell { updateUI(account: account) updateUIForPreferences() - updateTimestamp() + var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt) + if let application = status.application { + timestampAndClientText += " • \(application.name)" + } + timestampAndClientLabel.text = timestampAndClientText + attachmentsView.updateUI(status: status) let realStatus = status.reblog ?? status @@ -106,6 +119,10 @@ class ConversationMainStatusTableViewCell: UITableViewCell { private func updateStatusState(status: Status) { favorited = status.favourited ?? false reblogged = status.reblogged ?? false + + // todo: localize me + totalFavoritesButton.setTitle("\(status.favouritesCount) Favorite\(status.favouritesCount == 1 ? "" : "s")", for: .normal) + totalReblogsButton.setTitle("\(status.reblogsCount) Reblog\(status.reblogsCount == 1 ? "" : "s")", for: .normal) } private func updateUI(account: Account) { @@ -128,35 +145,10 @@ class ConversationMainStatusTableViewCell: UITableViewCell { displayNameLabel.text = account.realDisplayName } - func updateTimestamp() { - guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } - - timestampLabel.text = status.createdAt.timeAgoString() - let delay: DispatchTimeInterval? - switch status.createdAt.timeAgo().1 { - case .second: - delay = .seconds(10) - case .minute: - delay = .seconds(60) - default: - delay = nil - } - if let delay = delay { - updateTimestampWorkItem = DispatchWorkItem { - self.updateTimestamp() - } - DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) - } else { - updateTimestampWorkItem = nil - } - } - override func prepareForReuse() { if let url = avatarURL { ImageCache.avatars.cancel(url) } - updateTimestampWorkItem?.cancel() - updateTimestampWorkItem = nil attachmentsView.subviews.forEach { $0.removeFromSuperview() } } @@ -219,6 +211,23 @@ class ConversationMainStatusTableViewCell: UITableViewCell { delegate?.showMoreOptions(forStatus: statusID) } + @IBAction func totalFavoritesPressed(_ sender: Any) { + if let delegate = delegate { + // accounts aren't known, pass nil so the VC will load them + let vc = delegate.statusActionAccountList(action: .favorite, statusID: statusID, accountIDs: nil) + vc.showInacurateCountWarning = true + delegate.show(vc) + } + } + + @IBAction func totalReblogsPressed(_ sender: Any) { + if let delegate = delegate { + // accounts aren't known, pass nil so the VC will load them + let vc = delegate.statusActionAccountList(action: .reblog, statusID: statusID, accountIDs: nil) + vc.showInacurateCountWarning = true + delegate.show(vc) + } + } } extension ConversationMainStatusTableViewCell: AttachmentViewDelegate { diff --git a/Tusker/Views/Status/ConversationMainStatusTableViewCell.xib b/Tusker/Views/Status/ConversationMainStatusTableViewCell.xib index 0adbb42321..466d65fa82 100644 --- a/Tusker/Views/Status/ConversationMainStatusTableViewCell.xib +++ b/Tusker/Views/Status/ConversationMainStatusTableViewCell.xib @@ -1,24 +1,23 @@ - + - + - - + - - + + - + @@ -28,7 +27,7 @@ @@ -58,28 +51,91 @@ + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - +