Show favorite, reblog, and full timestamp in conversation main status

This commit is contained in:
Shadowfacts 2019-09-07 17:10:58 -04:00
parent 818c0afec6
commit 2edb65d302
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
5 changed files with 221 additions and 122 deletions

View File

@ -7,22 +7,46 @@
// //
import UIKit import UIKit
import Pachyderm
class StatusActionAccountListTableViewController: EnhancedTableViewController { class StatusActionAccountListTableViewController: EnhancedTableViewController {
private let statusCell = "statusCell" private let statusCell = "statusCell"
private let accountCell = "accountCell" private let accountCell = "accountCell"
let actionType: ActionType
let statusID: String 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.statusID = statusID
self.accountIDs = accountIDs self.accountIDs = accountIDs
super.init(style: .grouped) 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) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
@ -39,6 +63,25 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
tableView.alwaysBounceVertical = true tableView.alwaysBounceVertical = true
tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: CGFloat.leastNormalMagnitude)) 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 // MARK: - Table view data source
@ -52,7 +95,11 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
case 0: // status case 0: // status
return 1 return 1
case 1: // accounts case 1: // accounts
return accountIDs.count if let accountIDs = accountIDs {
return accountIDs.count
} else {
return 0
}
default: default:
fatalError("Invalid section \(section)") fatalError("Invalid section \(section)")
} }
@ -66,7 +113,8 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
cell.delegate = self cell.delegate = self
return cell return cell
case 1: 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.updateUI(accountID: accountIDs[indexPath.row])
cell.delegate = self cell.delegate = self
return cell return cell
@ -74,51 +122,15 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
fatalError("Invalid section \(indexPath.section)") fatalError("Invalid section \(indexPath.section)")
} }
} }
/* override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
// Override to support conditional editing of the table view. guard section == 1, showInacurateCountWarning else { return nil }
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 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")
// Return false if you do not want the specified item to be editable.
return true
} }
*/
enum ActionType {
/* case favorite, reblog
// 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
}
} }
*/
/*
// 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.
}
*/
} }

View File

@ -12,6 +12,8 @@ import Pachyderm
protocol TuskerNavigationDelegate { protocol TuskerNavigationDelegate {
func show(_ vc: UIViewController)
func selected(account accountID: String) func selected(account accountID: String)
func selected(mention: Mention) func selected(mention: Mention)
@ -44,11 +46,15 @@ protocol TuskerNavigationDelegate {
func showFollowedByList(accountIDs: [String]) 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 { extension TuskerNavigationDelegate where Self: UIViewController {
func show(_ vc: UIViewController) {
show(vc, sender: self)
}
func selected(account accountID: String) { func selected(account accountID: String) {
// don't open if the account is the same as the current one // don't open if the account is the same as the current one
if let profileController = self as? ProfileTableViewController, if let profileController = self as? ProfileTableViewController,
@ -179,17 +185,8 @@ extension TuskerNavigationDelegate where Self: UIViewController {
show(vc, sender: self) show(vc, sender: self)
} }
func showStatusActionAccountList(statusID: String, accountIDs: [String], action: Pachyderm.Notification.Kind) { func statusActionAccountList(action: StatusActionAccountListTableViewController.ActionType, statusID: String, accountIDs: [String]?) -> StatusActionAccountListTableViewController {
let vc = StatusActionAccountListTableViewController(statusID: statusID, accountIDs: accountIDs) return StatusActionAccountListTableViewController(actionType: action, 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)
} }
} }

View File

@ -153,21 +153,41 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
override func setSelected(_ selected: Bool, animated: Bool) { override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated) super.setSelected(selected, animated: animated)
if selected { if selected, let delegate = delegate {
let notifications = group.notificationIDs.compactMap(MastodonCache.notification(for:)) let notifications = group.notificationIDs.compactMap(MastodonCache.notification(for:))
let accountIDs = notifications.map { $0.account.id } 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 { extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
return (content: { return (content: {
let accountIDs = self.group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account.id } let notifications = self.group.notificationIDs.compactMap(MastodonCache.notification(for:))
return StatusActionAccountListTableViewController(statusID: self.statusID, accountIDs: accountIDs) 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: { }, actions: {
return self.actionsForNotificationGroup(self.group) return self.actionsForNotificationGroup(self.group)
}) })

View File

@ -12,6 +12,13 @@ import Pachyderm
class ConversationMainStatusTableViewCell: UITableViewCell { class ConversationMainStatusTableViewCell: UITableViewCell {
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .medium
return formatter
}()
var delegate: StatusTableViewCellDelegate? { var delegate: StatusTableViewCellDelegate? {
didSet { didSet {
contentLabel.navigationDelegate = delegate contentLabel.navigationDelegate = delegate
@ -22,7 +29,9 @@ class ConversationMainStatusTableViewCell: UITableViewCell {
@IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var usernameLabel: UILabel!
@IBOutlet weak var contentLabel: StatusContentLabel! @IBOutlet weak var contentLabel: StatusContentLabel!
@IBOutlet weak var avatarImageView: UIImageView! @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 attachmentsView: AttachmentsContainerView!
@IBOutlet weak var favoriteButton: UIButton! @IBOutlet weak var favoriteButton: UIButton!
@IBOutlet weak var reblogButton: UIButton! @IBOutlet weak var reblogButton: UIButton!
@ -46,8 +55,7 @@ class ConversationMainStatusTableViewCell: UITableViewCell {
} }
var avatarURL: URL? var avatarURL: URL?
var updateTimestampWorkItem: DispatchWorkItem?
var statusUpdater: Cancellable? var statusUpdater: Cancellable?
var accountUpdater: Cancellable? var accountUpdater: Cancellable?
@ -93,8 +101,13 @@ class ConversationMainStatusTableViewCell: UITableViewCell {
updateUI(account: account) updateUI(account: account)
updateUIForPreferences() 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) attachmentsView.updateUI(status: status)
let realStatus = status.reblog ?? status let realStatus = status.reblog ?? status
@ -106,6 +119,10 @@ class ConversationMainStatusTableViewCell: UITableViewCell {
private func updateStatusState(status: Status) { private func updateStatusState(status: Status) {
favorited = status.favourited ?? false favorited = status.favourited ?? false
reblogged = status.reblogged ?? 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) { private func updateUI(account: Account) {
@ -128,35 +145,10 @@ class ConversationMainStatusTableViewCell: UITableViewCell {
displayNameLabel.text = account.realDisplayName 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() { override func prepareForReuse() {
if let url = avatarURL { if let url = avatarURL {
ImageCache.avatars.cancel(url) ImageCache.avatars.cancel(url)
} }
updateTimestampWorkItem?.cancel()
updateTimestampWorkItem = nil
attachmentsView.subviews.forEach { $0.removeFromSuperview() } attachmentsView.subviews.forEach { $0.removeFromSuperview() }
} }
@ -219,6 +211,23 @@ class ConversationMainStatusTableViewCell: UITableViewCell {
delegate?.showMoreOptions(forStatus: statusID) 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 { extension ConversationMainStatusTableViewCell: AttachmentViewDelegate {

View File

@ -1,24 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14810.11" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14865.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14766.13"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14819.2"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
<capability name="iOS 13.0 system colors" minToolsVersion="11.0"/>
</dependencies> </dependencies>
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="ConversationMainStatusTableViewCell" customModule="Tusker" customModuleProvider="target"> <view contentMode="scaleToFill" id="iN0-l3-epB" customClass="ConversationMainStatusTableViewCell" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="200"/> <rect key="frame" x="0.0" y="0.0" width="375" height="238"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="GuG-Qd-B8I"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="GuG-Qd-B8I">
<rect key="frame" x="16" y="8" width="343" height="184"/> <rect key="frame" x="16" y="8" width="343" height="222"/>
<subviews> <subviews>
<view contentMode="scaleToFill" verticalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="Cnd-Fj-B7l"> <view contentMode="scaleToFill" verticalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="Cnd-Fj-B7l">
<rect key="frame" x="0.0" y="0.0" width="343" height="158"/> <rect key="frame" x="0.0" y="0.0" width="343" height="118.5"/>
<subviews> <subviews>
<imageView contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="mB9-HO-1vf"> <imageView contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="mB9-HO-1vf">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/> <rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
@ -28,7 +27,7 @@
</constraints> </constraints>
</imageView> </imageView>
<label opaque="NO" contentMode="left" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lZY-2e-17d"> <label opaque="NO" contentMode="left" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lZY-2e-17d">
<rect key="frame" x="58" y="0.0" width="252.5" height="29"/> <rect key="frame" x="58" y="0.0" width="277" height="29"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="24"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="24"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@ -36,21 +35,15 @@
<label opaque="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SWg-Ka-QyP"> <label opaque="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SWg-Ka-QyP">
<rect key="frame" x="58" y="29" width="285" height="20.5"/> <rect key="frame" x="58" y="29" width="285" height="20.5"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="17"/> <fontDescription key="fontDescription" type="system" weight="light" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="secondaryLabelColor"/> <color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="249" text="Content" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TgY-hs-Klo" customClass="StatusContentLabel" customModule="Tusker" customModuleProvider="target"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="249" text="Content" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TgY-hs-Klo" customClass="StatusContentLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="54" width="343" height="104"/> <rect key="frame" x="0.0" y="54" width="343" height="64.5"/>
<fontDescription key="fontDescription" type="system" pointSize="20"/> <fontDescription key="fontDescription" type="system" pointSize="20"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2m" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="R3N-Pg-4hn">
<rect key="frame" x="318.5" y="0.0" width="24.5" height="29"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
</subviews> </subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints> <constraints>
@ -58,28 +51,91 @@
<constraint firstAttribute="trailing" secondItem="SWg-Ka-QyP" secondAttribute="trailing" id="4g6-BT-eW4"/> <constraint firstAttribute="trailing" secondItem="SWg-Ka-QyP" secondAttribute="trailing" id="4g6-BT-eW4"/>
<constraint firstAttribute="bottom" secondItem="TgY-hs-Klo" secondAttribute="bottom" id="5Og-Pd-Vck"/> <constraint firstAttribute="bottom" secondItem="TgY-hs-Klo" secondAttribute="bottom" id="5Og-Pd-Vck"/>
<constraint firstItem="lZY-2e-17d" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="8fU-y9-K5Z"/> <constraint firstItem="lZY-2e-17d" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="8fU-y9-K5Z"/>
<constraint firstAttribute="trailingMargin" secondItem="lZY-2e-17d" secondAttribute="trailing" id="AAJ-pd-omx"/>
<constraint firstItem="lZY-2e-17d" firstAttribute="leading" secondItem="mB9-HO-1vf" secondAttribute="trailing" constant="8" id="Aqj-co-Szp"/> <constraint firstItem="lZY-2e-17d" firstAttribute="leading" secondItem="mB9-HO-1vf" secondAttribute="trailing" constant="8" id="Aqj-co-Szp"/>
<constraint firstItem="R3N-Pg-4hn" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="Gl6-Ss-hKQ"/>
<constraint firstAttribute="trailing" secondItem="R3N-Pg-4hn" secondAttribute="trailing" id="Oiy-II-fvT"/>
<constraint firstItem="mB9-HO-1vf" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="R7P-rD-Gbm"/> <constraint firstItem="mB9-HO-1vf" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="R7P-rD-Gbm"/>
<constraint firstAttribute="trailing" secondItem="TgY-hs-Klo" secondAttribute="trailing" id="SOE-Q5-IWd"/> <constraint firstAttribute="trailing" secondItem="TgY-hs-Klo" secondAttribute="trailing" id="SOE-Q5-IWd"/>
<constraint firstItem="mB9-HO-1vf" firstAttribute="leading" secondItem="Cnd-Fj-B7l" secondAttribute="leading" id="bxq-Fs-1aH"/> <constraint firstItem="mB9-HO-1vf" firstAttribute="leading" secondItem="Cnd-Fj-B7l" secondAttribute="leading" id="bxq-Fs-1aH"/>
<constraint firstItem="R3N-Pg-4hn" firstAttribute="leading" secondItem="lZY-2e-17d" secondAttribute="trailing" constant="8" id="dVP-Er-Z2l"/>
<constraint firstItem="SWg-Ka-QyP" firstAttribute="leading" secondItem="mB9-HO-1vf" secondAttribute="trailing" constant="8" id="e45-gE-myI"/> <constraint firstItem="SWg-Ka-QyP" firstAttribute="leading" secondItem="mB9-HO-1vf" secondAttribute="trailing" constant="8" id="e45-gE-myI"/>
<constraint firstItem="R3N-Pg-4hn" firstAttribute="height" secondItem="lZY-2e-17d" secondAttribute="height" id="fBi-wD-elQ"/>
<constraint firstItem="TgY-hs-Klo" firstAttribute="top" secondItem="mB9-HO-1vf" secondAttribute="bottom" constant="4" id="l6y-Rr-Nmc"/> <constraint firstItem="TgY-hs-Klo" firstAttribute="top" secondItem="mB9-HO-1vf" secondAttribute="bottom" constant="4" id="l6y-Rr-Nmc"/>
<constraint firstItem="SWg-Ka-QyP" firstAttribute="top" secondItem="lZY-2e-17d" secondAttribute="bottom" id="lvX-1b-8cN"/> <constraint firstItem="SWg-Ka-QyP" firstAttribute="top" secondItem="lZY-2e-17d" secondAttribute="bottom" id="lvX-1b-8cN"/>
</constraints> </constraints>
</view> </view>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IF9-9U-Gk0" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target"> <view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IF9-9U-Gk0" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="162" width="343" height="0.0"/> <rect key="frame" x="0.0" y="122.5" width="343" height="0.0"/>
<color key="backgroundColor" cocoaTouchSystemColor="secondarySystemBackgroundColor"/> <color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints> <constraints>
<constraint firstAttribute="height" priority="999" constant="200" id="UMv-Bk-ZyY"/> <constraint firstAttribute="height" priority="999" constant="200" id="UMv-Bk-ZyY"/>
</constraints> </constraints>
</view> </view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ejU-sO-Og5">
<rect key="frame" x="0.0" y="126.5" width="343" height="0.5"/>
<color key="backgroundColor" systemColor="opaqueSeparatorColor" red="0.77647058820000003" green="0.77647058820000003" blue="0.7843137255" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="0.5" id="DRI-lB-TyG"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="HZv-qj-gi6">
<rect key="frame" x="0.0" y="135" width="343" height="18"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="252" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="yyj-Bs-Vjq">
<rect key="frame" x="0.0" y="0.0" width="163.5" height="18"/>
<constraints>
<constraint firstAttribute="height" constant="18" id="F9W-LW-swd"/>
</constraints>
<state key="normal" title="2 Favorites">
<color key="titleColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="totalFavoritesPressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="cj1-BB-TuR"/>
</connections>
</button>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dYU-Hk-ee3">
<rect key="frame" x="171.5" y="0.0" width="0.5" height="18"/>
<color key="backgroundColor" systemColor="opaqueSeparatorColor" red="0.77647058820000003" green="0.77647058820000003" blue="0.7843137255" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="width" constant="0.5" id="kcB-YZ-hDW"/>
</constraints>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dem-vG-cPB">
<rect key="frame" x="180" y="0.0" width="163" height="18"/>
<constraints>
<constraint firstAttribute="height" constant="18" id="k0P-W7-wMF"/>
</constraints>
<state key="normal" title="1 Reblog">
<color key="titleColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="totalReblogsPressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="duG-bV-hcI"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="dem-vG-cPB" firstAttribute="width" secondItem="yyj-Bs-Vjq" secondAttribute="width" id="gcs-mS-vcQ"/>
</constraints>
</stackView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="pcy-jH-lL9">
<rect key="frame" x="0.0" y="161" width="343" height="0.5"/>
<color key="backgroundColor" systemColor="opaqueSeparatorColor" red="0.77647058820000003" green="0.77647058820000003" blue="0.7843137255" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="0.5" id="0Ga-Fr-g0g"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sep 7, 2019 12:12:53 PM • Web" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YHN-wG-YWi">
<rect key="frame" x="0.0" y="169.5" width="343" height="18"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="3Fp-Nj-sVj">
<rect key="frame" x="0.0" y="195.5" width="343" height="0.5"/>
<color key="backgroundColor" systemColor="opaqueSeparatorColor" red="0.77647058820000003" green="0.77647058820000003" blue="0.7843137255" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="0.5" id="akf-Kl-8mK"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="3Bg-XP-d13"> <stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="3Bg-XP-d13">
<rect key="frame" x="0.0" y="166" width="343" height="18"/> <rect key="frame" x="0.0" y="204" width="343" height="18"/>
<subviews> <subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2cc-lE-AdG"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2cc-lE-AdG">
<rect key="frame" x="0.0" y="0.0" width="21.5" height="18"/> <rect key="frame" x="0.0" y="0.0" width="21.5" height="18"/>
@ -96,14 +152,14 @@
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="GUG-f7-Hdy"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="GUG-f7-Hdy">
<rect key="frame" x="215.5" y="0.0" width="23" height="18"/> <rect key="frame" x="215.5" y="0.0" width="22.5" height="18"/>
<state key="normal" image="repeat" catalog="system"/> <state key="normal" image="repeat" catalog="system"/>
<connections> <connections>
<action selector="reblogPressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="SAf-RN-q8N"/> <action selector="reblogPressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="SAf-RN-q8N"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ujo-Ap-dmK"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ujo-Ap-dmK">
<rect key="frame" x="324.5" y="0.0" width="18.5" height="18"/> <rect key="frame" x="324" y="0.0" width="19" height="18"/>
<state key="normal" image="ellipsis" catalog="system"/> <state key="normal" image="ellipsis" catalog="system"/>
<connections> <connections>
<action selector="morePressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="dWb-67-CoL"/> <action selector="morePressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="dWb-67-CoL"/>
@ -118,11 +174,14 @@
<constraints> <constraints>
<constraint firstItem="Cnd-Fj-B7l" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="2hS-RG-81T"/> <constraint firstItem="Cnd-Fj-B7l" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="2hS-RG-81T"/>
<constraint firstItem="IF9-9U-Gk0" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="8A8-wi-7sg"/> <constraint firstItem="IF9-9U-Gk0" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="8A8-wi-7sg"/>
<constraint firstItem="pcy-jH-lL9" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="bPY-g6-swp"/>
<constraint firstItem="ejU-sO-Og5" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="biK-oQ-SLy"/>
<constraint firstItem="3Bg-XP-d13" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="iIq-gh-90O"/> <constraint firstItem="3Bg-XP-d13" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="iIq-gh-90O"/>
<constraint firstItem="3Fp-Nj-sVj" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="kfI-WN-ouW"/>
</constraints> </constraints>
</stackView> </stackView>
</subviews> </subviews>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints> <constraints>
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="GuG-Qd-B8I" secondAttribute="bottom" constant="8" id="2F3-0f-0tC"/> <constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="GuG-Qd-B8I" secondAttribute="bottom" constant="8" id="2F3-0f-0tC"/>
<constraint firstItem="GuG-Qd-B8I" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="PFH-TI-ZQ9"/> <constraint firstItem="GuG-Qd-B8I" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="PFH-TI-ZQ9"/>
@ -138,10 +197,12 @@
<outlet property="displayNameLabel" destination="lZY-2e-17d" id="7og-23-eHy"/> <outlet property="displayNameLabel" destination="lZY-2e-17d" id="7og-23-eHy"/>
<outlet property="favoriteButton" destination="DhN-rJ-jdA" id="b2Q-ch-kSP"/> <outlet property="favoriteButton" destination="DhN-rJ-jdA" id="b2Q-ch-kSP"/>
<outlet property="reblogButton" destination="GUG-f7-Hdy" id="WtT-Ph-DQm"/> <outlet property="reblogButton" destination="GUG-f7-Hdy" id="WtT-Ph-DQm"/>
<outlet property="timestampLabel" destination="R3N-Pg-4hn" id="z5j-Tw-Lk5"/> <outlet property="timestampAndClientLabel" destination="YHN-wG-YWi" id="Onb-fe-qwG"/>
<outlet property="totalFavoritesButton" destination="yyj-Bs-Vjq" id="4pV-Qi-Z2X"/>
<outlet property="totalReblogsButton" destination="dem-vG-cPB" id="i9E-Qn-d76"/>
<outlet property="usernameLabel" destination="SWg-Ka-QyP" id="h2I-g4-AD9"/> <outlet property="usernameLabel" destination="SWg-Ka-QyP" id="h2I-g4-AD9"/>
</connections> </connections>
<point key="canvasLocation" x="40.799999999999997" y="-163.71814092953525"/> <point key="canvasLocation" x="40.799999999999997" y="-146.62668665667167"/>
</view> </view>
</objects> </objects>
<resources> <resources>