diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index 5454b56..f9d0a6f 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -8,8 +8,6 @@ /* Begin PBXBuildFile section */ D0030982250C6C8500EACB32 /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0030981250C6C8500EACB32 /* URL+Extensions.swift */; }; - D01F41D724F880C400D55A2D /* StatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D01F41D424F880C400D55A2D /* StatusTableViewCell.xib */; }; - D01F41D824F880C400D55A2D /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D524F880C400D55A2D /* StatusTableViewCell.swift */; }; D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */; }; D01F41E424F8889700D55A2D /* AttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* AttachmentsView.swift */; }; D02E1F95250B13210071AD56 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E1F94250B13210071AD56 /* SafariView.swift */; }; @@ -18,6 +16,7 @@ D0625E5F250F0CFF00502611 /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5E250F0CFF00502611 /* StatusView.swift */; }; D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; }; D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; }; + D0B7434925100DBB00C13DB6 /* StatusView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D0B7434825100DBB00C13DB6 /* StatusView.xib */; }; D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */; }; D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */; }; D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */; }; @@ -85,8 +84,6 @@ /* Begin PBXFileReference section */ D0030981250C6C8500EACB32 /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = ""; }; - D01F41D424F880C400D55A2D /* StatusTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = StatusTableViewCell.xib; sourceTree = ""; }; - D01F41D524F880C400D55A2D /* StatusTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = ""; }; D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchFallthroughTextView.swift; sourceTree = ""; }; D01F41E224F8889700D55A2D /* AttachmentsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentsView.swift; sourceTree = ""; }; D02E1F94250B13210071AD56 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = ""; }; @@ -99,6 +96,7 @@ D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = ""; }; D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = ""; }; D0B32F4F250B373600311912 /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = ""; }; + D0B7434825100DBB00C13DB6 /* StatusView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = StatusView.xib; sourceTree = ""; }; D0BDF66524FD7A6400C7FA1C /* ServiceLayer */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ServiceLayer; sourceTree = ""; }; D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = ""; }; D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableFooterView.swift; sourceTree = ""; }; @@ -171,16 +169,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - D01F41D324F8807E00D55A2D /* Status Cell */ = { - isa = PBXGroup; - children = ( - D01F41D524F880C400D55A2D /* StatusTableViewCell.swift */, - D01F41D424F880C400D55A2D /* StatusTableViewCell.xib */, - D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */, - ); - path = "Status Cell"; - sourceTree = ""; - }; D01F41E024F8885900D55A2D /* Attachments */ = { isa = PBXGroup; children = ( @@ -230,9 +218,11 @@ D0625E55250F086B00502611 /* Status */ = { isa = PBXGroup; children = ( - D0625E58250F092900502611 /* StatusListCell.swift */, D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */, + D0625E58250F092900502611 /* StatusListCell.swift */, D0625E5E250F0CFF00502611 /* StatusView.swift */, + D0B7434825100DBB00C13DB6 /* StatusView.xib */, + D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */, ); path = Status; sourceTree = ""; @@ -280,7 +270,6 @@ D0C7D42724F76169001EBDBB /* RootView.swift */, D02E1F94250B13210071AD56 /* SafariView.swift */, D0C7D42924F76169001EBDBB /* SecondaryNavigationView.swift */, - D01F41D324F8807E00D55A2D /* Status Cell */, D0C7D42524F76169001EBDBB /* StatusListView.swift */, D0C7D42E24F76169001EBDBB /* TabNavigationView.swift */, ); @@ -457,7 +446,7 @@ buildActionMask = 2147483647; files = ( D0C7D4C524F7616A001EBDBB /* Localizable.strings in Resources */, - D01F41D724F880C400D55A2D /* StatusTableViewCell.xib in Resources */, + D0B7434925100DBB00C13DB6 /* StatusView.xib in Resources */, D0C7D4C224F7616A001EBDBB /* Assets.xcassets in Resources */, D0C7D4C624F7616A001EBDBB /* Localizable.stringsdict in Resources */, ); @@ -533,7 +522,6 @@ D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */, D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */, D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */, - D01F41D824F880C400D55A2D /* StatusTableViewCell.swift in Sources */, D0C7D49B24F7616A001EBDBB /* PreferencesView.swift in Sources */, D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */, ); diff --git a/View Controllers/StatusListViewController.swift b/View Controllers/StatusListViewController.swift index 1c0f934..33178ae 100644 --- a/View Controllers/StatusListViewController.swift +++ b/View Controllers/StatusListViewController.swift @@ -22,7 +22,6 @@ final class StatusListViewController: UITableViewController { else { return nil } cell.viewModel = self.viewModel.statusViewModel(id: statusID) -// cell.delegate = self return cell } @@ -146,21 +145,7 @@ extension StatusListViewController: UITableViewDataSourcePrefetching { } } -extension StatusListViewController: StatusTableViewCellDelegate { - func statusTableViewCellDidHaveShareButtonTapped(_ cell: StatusTableViewCell) { - guard let url = cell.viewModel?.sharingURL else { return } - - share(url: url) - } -} - private extension StatusListViewController { - func share(url: URL) { - let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: nil) - - present(activityViewController, animated: true, completion: nil) - } - func sizeTableHeaderFooterViews() { // https://useyourloaf.com/blog/variable-height-table-view-header/ if let headerView = tableView.tableHeaderView { diff --git a/Views/Status Cell/StatusTableViewCell.swift b/Views/Status Cell/StatusTableViewCell.swift deleted file mode 100644 index d4d6286..0000000 --- a/Views/Status Cell/StatusTableViewCell.swift +++ /dev/null @@ -1,369 +0,0 @@ -// Copyright © 2020 Metabolist. All rights reserved. - -import AVKit -import Kingfisher -import UIKit -import ViewModels - -protocol StatusTableViewCellDelegate: class { - func statusTableViewCellDidHaveShareButtonTapped(_ cell: StatusTableViewCell) -} - -final class StatusTableViewCell: UITableViewCell { - @IBOutlet weak var metaIcon: UIImageView! - @IBOutlet weak var metaLabel: UILabel! - @IBOutlet weak var contentTextView: TouchFallthroughTextView! - @IBOutlet weak var avatarButton: UIButton! - @IBOutlet weak var avatarImageView: AnimatedImageView! - @IBOutlet weak var displayNameLabel: UILabel! - @IBOutlet weak var accountLabel: UILabel! - @IBOutlet weak var timeLabel: UILabel! - @IBOutlet weak var spoilerTextLabel: UILabel! - @IBOutlet weak var toggleSensitiveContentButton: UIButton! - @IBOutlet weak var replyButton: UIButton! - @IBOutlet weak var reblogButton: UIButton! - @IBOutlet weak var favoriteButton: UIButton! - @IBOutlet weak var shareButton: UIButton! - @IBOutlet weak var attachmentsView: AttachmentsView! - @IBOutlet weak var cardView: UIView! - @IBOutlet weak var cardImageView: UIImageView! - @IBOutlet weak var cardTitleLabel: UILabel! - @IBOutlet weak var cardDescriptionLabel: UILabel! - @IBOutlet weak var cardURLLabel: UILabel! - @IBOutlet weak var cardButton: UIButton! - @IBOutlet weak var sensitiveContentView: UIStackView! - @IBOutlet weak var hasReplyFollowingView: UIView! - @IBOutlet weak var inReplyToView: UIView! - @IBOutlet weak var avatarReplyContextView: UIView! - @IBOutlet weak var nameDateView: UIStackView! - @IBOutlet weak var contextParentAvatarNameView: UIStackView! - @IBOutlet weak var contextParentAvatarImageView: AnimatedImageView! - @IBOutlet weak var contextParentAvatarButton: UIButton! - @IBOutlet weak var contextParentDisplayNameLabel: UILabel! - @IBOutlet weak var contextParentAccountLabel: UILabel! - @IBOutlet weak var actionButtonsView: UIStackView! - @IBOutlet weak var contextParentReplyButton: UIButton! - @IBOutlet weak var contextParentReblogButton: UIButton! - @IBOutlet weak var contextParentFavoriteButton: UIButton! - @IBOutlet weak var contextParentShareButton: UIButton! - @IBOutlet weak var contextParentActionsButton: UIButton! - @IBOutlet weak var contextParentTimeLabel: UILabel! - @IBOutlet weak var timeApplicationDividerView: UILabel! - @IBOutlet weak var applicationButton: UIButton! - @IBOutlet weak var contextParentRebloggedByButton: UIButton! - @IBOutlet weak var contextParentFavoritedByButton: UIButton! - @IBOutlet weak var contextParentItems: UIStackView! - @IBOutlet weak var contextParentRebloggedByFavoritedByView: UIStackView! - @IBOutlet weak var contextParentRebloggedByFavoritedBySeparator: UIView! - - weak var delegate: StatusTableViewCellDelegate? - - @IBOutlet private var separatorConstraints: [NSLayoutConstraint]! - - var viewModel: StatusViewModel? { - didSet { - guard let viewModel = viewModel else { return } - - let mutableContent = NSMutableAttributedString(attributedString: viewModel.content) - let mutableDisplayName = NSMutableAttributedString(string: viewModel.displayName) - let mutableSpoilerText = NSMutableAttributedString(string: viewModel.spoilerText) - let contentFont = UIFont.preferredFont(forTextStyle: viewModel.isContextParent ? .title3 : .callout) - - contentTextView.shouldFallthrough = !viewModel.isContextParent - avatarReplyContextView.isHidden = viewModel.isContextParent - nameDateView.isHidden = viewModel.isContextParent - contextParentAvatarNameView.isHidden = !viewModel.isContextParent - actionButtonsView.isHidden = viewModel.isContextParent - contextParentItems.isHidden = !viewModel.isContextParent - - let avatarImageView: UIImageView - let displayNameLabel: UILabel - let accountLabel: UILabel - - if viewModel.isContextParent { - avatarImageView = contextParentAvatarImageView - displayNameLabel = contextParentDisplayNameLabel - accountLabel = contextParentAccountLabel - } else { - avatarImageView = self.avatarImageView - displayNameLabel = self.displayNameLabel - accountLabel = self.accountLabel - } - - let contentRange = NSRange(location: 0, length: mutableContent.length) - mutableContent.removeAttribute(.font, range: contentRange) - mutableContent.addAttributes( - [.font: contentFont as Any, - .foregroundColor: UIColor.label], - range: contentRange) - mutableContent.insert(emoji: viewModel.contentEmoji, view: contentTextView) - mutableContent.resizeAttachments(toLineHeight: contentFont.lineHeight) - contentTextView.attributedText = mutableContent - contentTextView.isHidden = contentTextView.text == "" - mutableDisplayName.insert(emoji: viewModel.displayNameEmoji, view: displayNameLabel) - mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight) - displayNameLabel.attributedText = mutableDisplayName - mutableSpoilerText.insert(emoji: viewModel.contentEmoji, view: spoilerTextLabel) - mutableSpoilerText.resizeAttachments(toLineHeight: spoilerTextLabel.font.lineHeight) - spoilerTextLabel.attributedText = mutableSpoilerText - spoilerTextLabel.isHidden = !viewModel.sensitive || spoilerTextLabel.text == "" - toggleSensitiveContentButton.setTitle( - viewModel.shouldDisplaySensitiveContent - ? NSLocalizedString("status.show-less", comment: "") - : NSLocalizedString("status.show-more", comment: ""), - for: .normal) - accountLabel.text = viewModel.accountName - timeLabel.text = viewModel.time - contextParentTimeLabel.text = viewModel.contextParentTime - timeApplicationDividerView.isHidden = viewModel.applicationName == nil - applicationButton.isHidden = viewModel.applicationName == nil - applicationButton.setTitle(viewModel.applicationName, for: .normal) - applicationButton.isEnabled = viewModel.applicationURL != nil - avatarImageView.kf.setImage(with: viewModel.avatarURL) - toggleSensitiveContentButton.isHidden = !viewModel.sensitive - replyButton.setTitle(viewModel.repliesCount == 0 ? "" : String(viewModel.repliesCount), for: .normal) - reblogButton.setTitle(viewModel.reblogsCount == 0 ? "" : String(viewModel.reblogsCount), for: .normal) - setReblogButtonColor(reblogged: viewModel.reblogged) - favoriteButton.setTitle(viewModel.favoritesCount == 0 ? "" : String(viewModel.favoritesCount), for: .normal) - setFavoriteButtonColor(favorited: viewModel.favorited) - - reblogButton.isEnabled = viewModel.canBeReblogged - contextParentReblogButton.isEnabled = viewModel.canBeReblogged - - let noReblogs = viewModel.reblogsCount == 0 - let noFavorites = viewModel.favoritesCount == 0 - let noInteractions = noReblogs && noFavorites - - setAttributedLocalizedTitle( - button: contextParentRebloggedByButton, - localizationKey: "status.reblogs-count", - count: viewModel.reblogsCount) - contextParentRebloggedByButton.isHidden = noReblogs - setAttributedLocalizedTitle( - button: contextParentFavoritedByButton, - localizationKey: "status.favorites-count", - count: viewModel.favoritesCount) - contextParentFavoritedByButton.isHidden = noFavorites - - contextParentRebloggedByFavoritedByView.isHidden = noInteractions - contextParentRebloggedByFavoritedBySeparator.isHidden = noInteractions - - if - viewModel.isReblog { - let metaText = String.localizedStringWithFormat( - NSLocalizedString("status.reblogged-by", comment: ""), - viewModel.rebloggedByDisplayName) - let mutableMetaText = NSMutableAttributedString(string: metaText) - mutableMetaText.insert(emoji: viewModel.rebloggedByDisplayNameEmoji, view: metaLabel) - mutableMetaText.resizeAttachments(toLineHeight: metaLabel.font.lineHeight) - metaLabel.attributedText = mutableMetaText - metaIcon.image = UIImage( - systemName: "arrow.2.squarepath", - withConfiguration: UIImage.SymbolConfiguration(scale: .small)) - metaLabel.isHidden = false - metaIcon.isHidden = false - } else if viewModel.isPinned { - metaLabel.text = NSLocalizedString("status.pinned-post", comment: "") - metaIcon.image = UIImage( - systemName: "pin", - withConfiguration: UIImage.SymbolConfiguration(scale: .small)) - metaLabel.isHidden = false - metaIcon.isHidden = false - } else { - metaLabel.isHidden = true - metaIcon.isHidden = true - } - - attachmentsView.isHidden = viewModel.attachmentViewModels.count == 0 - attachmentsView.attachmentViewModels = viewModel.attachmentViewModels - setNeedsLayout() - - if let cardURL = viewModel.cardURL { - cardTitleLabel.text = viewModel.cardTitle - cardDescriptionLabel.text = viewModel.cardDescription - cardDescriptionLabel.isHidden = cardDescriptionLabel.text == "" - || cardDescriptionLabel.text == cardTitleLabel.text - if - let host = cardURL.host, host.hasPrefix("www."), - let withoutWww = cardURL.host?.components(separatedBy: "www.").last { - cardURLLabel.text = withoutWww - } else { - cardURLLabel.text = cardURL.host - } - - if let cardImageURL = viewModel.cardImageURL { - cardImageView.isHidden = false - cardImageView.kf.setImage(with: cardImageURL) - } else { - cardImageView.isHidden = true - } - cardView.isHidden = false - } else { - cardView.isHidden = true - } - - sensitiveContentView.isHidden = !viewModel.shouldDisplaySensitiveContent - - inReplyToView.isHidden = !viewModel.isReplyInContext - - hasReplyFollowingView.isHidden = !viewModel.hasReplyFollowing - } - } - - override func awakeFromNib() { - super.awakeFromNib() - - for constraint in separatorConstraints { - constraint.constant = 1 / UIScreen.main.scale - } - - avatarImageView.kf.indicatorType = .activity - contextParentAvatarImageView.kf.indicatorType = .activity - cardImageView.kf.indicatorType = .activity - - contentTextView.delegate = self - - let highlightedButtonBackgroundImage = UIColor(white: 0, alpha: 0.5).image() - - cardButton.setBackgroundImage(highlightedButtonBackgroundImage, for: .highlighted) - avatarButton.setBackgroundImage(highlightedButtonBackgroundImage, for: .highlighted) - contextParentAvatarButton.setBackgroundImage(highlightedButtonBackgroundImage, for: .highlighted) - } - - override func prepareForReuse() { - super.prepareForReuse() - - avatarImageView.kf.cancelDownloadTask() - cardImageView.kf.cancelDownloadTask() - } - - override func layoutSubviews() { - super.layoutSubviews() - - for button: UIButton in [toggleSensitiveContentButton] where button.frame.height != 0 { - button.layer.cornerRadius = button.frame.height / 2 - } - - if hasReplyFollowingView.isHidden { - separatorInset.right = UIDevice.current.userInterfaceIdiom == .phone ? 0 : layoutMargins.right - } else { - separatorInset.right = .greatestFiniteMagnitude - } - - separatorInset.left = UIDevice.current.userInterfaceIdiom == .phone ? 0 : layoutMargins.left - } -} - -extension StatusTableViewCell { - @IBAction func avatarButtonTapped(_ sender: Any) { - - } - - @IBAction func toggleSensitiveContentButtonTapped(_ sender: Any) { - - } - - @IBAction func cardButtonTapped(_ sender: UIButton) { - - } - - @IBAction func replyButtonTapped(_ sender: UIButton) { - - } - - @IBAction func reblogButtonTapped(_ sender: UIButton) { - - } - - @IBAction func favoriteButtonTapped(_ sender: UIButton) { - viewModel?.toggleFavorited() - } - - @IBAction func actionsButtonTapped(_ sender: Any) { - - } - - @IBAction func shareButtonTapped(_ sender: Any) { - delegate?.statusTableViewCellDidHaveShareButtonTapped(self) - } - - @IBAction func applicationButtonTapped(_ sender: Any) { - - } - - @IBAction func contextParentRebloggedByButtonTapped(_ sender: Any) { - - } - - @IBAction func contextParentFavoritedByButtonTapped(_ sender: Any) { - - } -} - -extension StatusTableViewCell: UITextViewDelegate { - func textView( - _ textView: UITextView, - shouldInteractWith URL: URL, - in characterRange: NSRange, - interaction: UITextItemInteraction) -> Bool { - switch interaction { - case .invokeDefaultAction: print(URL); return false - case .preview: return false - case .presentActions: return false - @unknown default: return false - } - } -} - -private extension StatusTableViewCell { - private static let defaultAspectRatioConstraintMultiplier: CGFloat = 4.0 / 3.0 - private static let hasReplyFollowingSeparatorInsets = UIEdgeInsets( - top: 0, - left: 0, - bottom: 0, - right: .greatestFiniteMagnitude) - - private func setReblogButtonColor(reblogged: Bool) { - let reblogColor: UIColor = reblogged ? .systemGreen : .secondaryLabel - let reblogButton: UIButton - - if viewModel?.isContextParent ?? false { - reblogButton = contextParentReblogButton - } else { - reblogButton = self.reblogButton - } - - reblogButton.tintColor = reblogColor - reblogButton.setTitleColor(reblogColor, for: .normal) - } - - private func setFavoriteButtonColor(favorited: Bool) { - let favoriteColor: UIColor = favorited ? .systemYellow : .secondaryLabel - let favoriteButton: UIButton - let scale: UIImage.SymbolScale - - if viewModel?.isContextParent ?? false { - favoriteButton = contextParentFavoriteButton - scale = .medium - } else { - favoriteButton = self.favoriteButton - scale = .small - } - - favoriteButton.tintColor = favoriteColor - favoriteButton.setTitleColor(favoriteColor, for: .normal) - favoriteButton.setImage(UIImage( - systemName: favorited ? "star.fill" : "star", - withConfiguration: UIImage.SymbolConfiguration(scale: scale)), - for: .normal) - } - - private func setAttributedLocalizedTitle(button: UIButton, localizationKey: String, count: Int) { - let localizedTitle = String.localizedStringWithFormat(NSLocalizedString(localizationKey, comment: ""), count) - - button.setAttributedTitle(localizedTitle.countEmphasizedAttributedString(count: count), for: .normal) - button.setAttributedTitle( - localizedTitle.countEmphasizedAttributedString(count: count, highlighted: true), - for: .highlighted) - } -} diff --git a/Views/Status Cell/StatusTableViewCell.xib b/Views/Status Cell/StatusTableViewCell.xib deleted file mode 100644 index 7867c9b..0000000 --- a/Views/Status Cell/StatusTableViewCell.xib +++ /dev/null @@ -1,563 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Views/Status/StatusContentConfiguration.swift b/Views/Status/StatusContentConfiguration.swift index 51cc70a..af1ed77 100644 --- a/Views/Status/StatusContentConfiguration.swift +++ b/Views/Status/StatusContentConfiguration.swift @@ -13,7 +13,6 @@ extension StatusContentConfiguration: UIContentConfiguration { } func updated(for state: UIConfigurationState) -> StatusContentConfiguration { - // TODO: Update stuff? self } } diff --git a/Views/Status/StatusListCell.swift b/Views/Status/StatusListCell.swift index 70c478a..a180bb4 100644 --- a/Views/Status/StatusListCell.swift +++ b/Views/Status/StatusListCell.swift @@ -15,6 +15,12 @@ class StatusListCell: UITableViewCell { override func layoutSubviews() { super.layoutSubviews() + if viewModel?.hasReplyFollowing ?? false { + separatorInset.right = .greatestFiniteMagnitude + } else { + separatorInset.right = UIDevice.current.userInterfaceIdiom == .phone ? 0 : layoutMargins.right + } + separatorInset.left = UIDevice.current.userInterfaceIdiom == .phone ? 0 : layoutMargins.left } } diff --git a/Views/Status/StatusView.swift b/Views/Status/StatusView.swift index b0458fd..aeba6a3 100644 --- a/Views/Status/StatusView.swift +++ b/Views/Status/StatusView.swift @@ -1,25 +1,79 @@ // Copyright © 2020 Metabolist. All rights reserved. +import Kingfisher import UIKit class StatusView: UIView { - private var statusConfiguration: StatusContentConfiguration + @IBOutlet var baseView: UIView! + @IBOutlet weak var metaIcon: UIImageView! + @IBOutlet weak var metaLabel: UILabel! + @IBOutlet weak var contentTextView: TouchFallthroughTextView! + @IBOutlet weak var avatarButton: UIButton! + @IBOutlet weak var avatarImageView: AnimatedImageView! + @IBOutlet weak var displayNameLabel: UILabel! + @IBOutlet weak var accountLabel: UILabel! + @IBOutlet weak var timeLabel: UILabel! + @IBOutlet weak var spoilerTextLabel: UILabel! + @IBOutlet weak var toggleSensitiveContentButton: UIButton! + @IBOutlet weak var replyButton: UIButton! + @IBOutlet weak var reblogButton: UIButton! + @IBOutlet weak var favoriteButton: UIButton! + @IBOutlet weak var shareButton: UIButton! + @IBOutlet weak var attachmentsView: AttachmentsView! + @IBOutlet weak var cardView: UIView! + @IBOutlet weak var cardImageView: UIImageView! + @IBOutlet weak var cardTitleLabel: UILabel! + @IBOutlet weak var cardDescriptionLabel: UILabel! + @IBOutlet weak var cardURLLabel: UILabel! + @IBOutlet weak var cardButton: UIButton! + @IBOutlet weak var sensitiveContentView: UIStackView! + @IBOutlet weak var hasReplyFollowingView: UIView! + @IBOutlet weak var inReplyToView: UIView! + @IBOutlet weak var avatarReplyContextView: UIView! + @IBOutlet weak var nameDateView: UIStackView! + @IBOutlet weak var contextParentAvatarNameView: UIStackView! + @IBOutlet weak var contextParentAvatarImageView: AnimatedImageView! + @IBOutlet weak var contextParentAvatarButton: UIButton! + @IBOutlet weak var contextParentDisplayNameLabel: UILabel! + @IBOutlet weak var contextParentAccountLabel: UILabel! + @IBOutlet weak var actionButtonsView: UIStackView! + @IBOutlet weak var contextParentReplyButton: UIButton! + @IBOutlet weak var contextParentReblogButton: UIButton! + @IBOutlet weak var contextParentFavoriteButton: UIButton! + @IBOutlet weak var contextParentShareButton: UIButton! + @IBOutlet weak var contextParentActionsButton: UIButton! + @IBOutlet weak var contextParentTimeLabel: UILabel! + @IBOutlet weak var timeApplicationDividerView: UILabel! + @IBOutlet weak var applicationButton: UIButton! + @IBOutlet weak var contextParentRebloggedByButton: UIButton! + @IBOutlet weak var contextParentFavoritedByButton: UIButton! + @IBOutlet weak var contextParentItems: UIStackView! + @IBOutlet weak var contextParentRebloggedByFavoritedByView: UIStackView! + @IBOutlet weak var contextParentRebloggedByFavoritedBySeparator: UIView! - private let content = TouchFallthroughTextView() + private var statusConfiguration: StatusContentConfiguration + @IBOutlet private var separatorConstraints: [NSLayoutConstraint]! init(configuration: StatusContentConfiguration) { self.statusConfiguration = configuration super.init(frame: .zero) - setup() - applyStatusConfiguration() + initialSetup() } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func layoutSubviews() { + super.layoutSubviews() + + for button: UIButton in [toggleSensitiveContentButton] where button.frame.height != 0 { + button.layer.cornerRadius = button.frame.height / 2 + } + } } extension StatusView: UIContentView { @@ -30,40 +84,263 @@ extension StatusView: UIContentView { self.statusConfiguration = statusConfiguration + avatarImageView.kf.cancelDownloadTask() + contextParentAvatarImageView.kf.cancelDownloadTask() + cardImageView.kf.cancelDownloadTask() applyStatusConfiguration() } } } +extension StatusView: UITextViewDelegate { + func textView( + _ textView: UITextView, + shouldInteractWith URL: URL, + in characterRange: NSRange, + interaction: UITextItemInteraction) -> Bool { + switch interaction { + case .invokeDefaultAction: print(URL); return false + case .preview: return false + case .presentActions: return false + @unknown default: return false + } + } +} + private extension StatusView { - func setup() { - addSubview(content) - content.translatesAutoresizingMaskIntoConstraints = false - content.isScrollEnabled = false - content.isEditable = false - content.backgroundColor = .clear + func initialSetup() { + Bundle.main.loadNibNamed(String(describing: type(of: self)), owner: self, options: nil) + + addSubview(baseView) + + baseView.translatesAutoresizingMaskIntoConstraints = false + baseView.backgroundColor = .clear + NSLayoutConstraint.activate([ - content.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), - content.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), - content.topAnchor.constraint(equalTo: readableContentGuide.topAnchor), - content.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor) + baseView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor), + baseView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), + baseView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), + baseView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor), + // These have "Placeholder" checked in the xib file so they can + // be set to go beyond the readable content guide + inReplyToView.topAnchor.constraint(equalTo: topAnchor), + hasReplyFollowingView.bottomAnchor.constraint(equalTo: bottomAnchor) ]) + + for constraint in separatorConstraints { + constraint.constant = 1 / UIScreen.main.scale + } + + avatarImageView.kf.indicatorType = .activity + contextParentAvatarImageView.kf.indicatorType = .activity + cardImageView.kf.indicatorType = .activity + + contentTextView.delegate = self + + let highlightedButtonBackgroundImage = UIColor(white: 0, alpha: 0.5).image() + + cardButton.setBackgroundImage(highlightedButtonBackgroundImage, for: .highlighted) + avatarButton.setBackgroundImage(highlightedButtonBackgroundImage, for: .highlighted) + contextParentAvatarButton.setBackgroundImage(highlightedButtonBackgroundImage, for: .highlighted) + + let favoriteAction = UIAction { [weak self] _ in self?.statusConfiguration.viewModel.toggleFavorited() } + + favoriteButton.addAction(favoriteAction, for: .touchUpInside) + contextParentFavoriteButton.addAction(favoriteAction, for: .touchUpInside) + + applyStatusConfiguration() } + // swiftlint:disable function_body_length func applyStatusConfiguration() { let viewModel = statusConfiguration.viewModel let mutableContent = NSMutableAttributedString(attributedString: viewModel.content) + let mutableDisplayName = NSMutableAttributedString(string: viewModel.displayName) + let mutableSpoilerText = NSMutableAttributedString(string: viewModel.spoilerText) let contentFont = UIFont.preferredFont(forTextStyle: viewModel.isContextParent ? .title3 : .callout) + contentTextView.shouldFallthrough = !viewModel.isContextParent + avatarReplyContextView.isHidden = viewModel.isContextParent + nameDateView.isHidden = viewModel.isContextParent + contextParentAvatarNameView.isHidden = !viewModel.isContextParent + actionButtonsView.isHidden = viewModel.isContextParent + contextParentItems.isHidden = !viewModel.isContextParent + + let avatarImageView: UIImageView + let displayNameLabel: UILabel + let accountLabel: UILabel + + if viewModel.isContextParent { + avatarImageView = contextParentAvatarImageView + displayNameLabel = contextParentDisplayNameLabel + accountLabel = contextParentAccountLabel + } else { + avatarImageView = self.avatarImageView + displayNameLabel = self.displayNameLabel + accountLabel = self.accountLabel + } + let contentRange = NSRange(location: 0, length: mutableContent.length) mutableContent.removeAttribute(.font, range: contentRange) mutableContent.addAttributes( [.font: contentFont as Any, .foregroundColor: UIColor.label], range: contentRange) - mutableContent.insert(emoji: viewModel.contentEmoji, view: content) + mutableContent.insert(emoji: viewModel.contentEmoji, view: contentTextView) mutableContent.resizeAttachments(toLineHeight: contentFont.lineHeight) - content.attributedText = mutableContent -// content.isHidden = contentTextView.text == "" + contentTextView.attributedText = mutableContent + contentTextView.isHidden = contentTextView.text == "" + mutableDisplayName.insert(emoji: viewModel.displayNameEmoji, view: displayNameLabel) + mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight) + displayNameLabel.attributedText = mutableDisplayName + mutableSpoilerText.insert(emoji: viewModel.contentEmoji, view: spoilerTextLabel) + mutableSpoilerText.resizeAttachments(toLineHeight: spoilerTextLabel.font.lineHeight) + spoilerTextLabel.attributedText = mutableSpoilerText + spoilerTextLabel.isHidden = !viewModel.sensitive || spoilerTextLabel.text == "" + toggleSensitiveContentButton.setTitle( + viewModel.shouldDisplaySensitiveContent + ? NSLocalizedString("status.show-less", comment: "") + : NSLocalizedString("status.show-more", comment: ""), + for: .normal) + accountLabel.text = viewModel.accountName + timeLabel.text = viewModel.time + contextParentTimeLabel.text = viewModel.contextParentTime + timeApplicationDividerView.isHidden = viewModel.applicationName == nil + applicationButton.isHidden = viewModel.applicationName == nil + applicationButton.setTitle(viewModel.applicationName, for: .normal) + applicationButton.isEnabled = viewModel.applicationURL != nil + avatarImageView.kf.setImage(with: viewModel.avatarURL) + toggleSensitiveContentButton.isHidden = !viewModel.sensitive + replyButton.setTitle(viewModel.repliesCount == 0 ? "" : String(viewModel.repliesCount), for: .normal) + reblogButton.setTitle(viewModel.reblogsCount == 0 ? "" : String(viewModel.reblogsCount), for: .normal) + setReblogButtonColor(reblogged: viewModel.reblogged) + favoriteButton.setTitle(viewModel.favoritesCount == 0 ? "" : String(viewModel.favoritesCount), for: .normal) + setFavoriteButtonColor(favorited: viewModel.favorited) + + reblogButton.isEnabled = viewModel.canBeReblogged + contextParentReblogButton.isEnabled = viewModel.canBeReblogged + + let noReblogs = viewModel.reblogsCount == 0 + let noFavorites = viewModel.favoritesCount == 0 + let noInteractions = noReblogs && noFavorites + + setAttributedLocalizedTitle( + button: contextParentRebloggedByButton, + localizationKey: "status.reblogs-count", + count: viewModel.reblogsCount) + contextParentRebloggedByButton.isHidden = noReblogs + setAttributedLocalizedTitle( + button: contextParentFavoritedByButton, + localizationKey: "status.favorites-count", + count: viewModel.favoritesCount) + contextParentFavoritedByButton.isHidden = noFavorites + + contextParentRebloggedByFavoritedByView.isHidden = noInteractions + contextParentRebloggedByFavoritedBySeparator.isHidden = noInteractions + + if + viewModel.isReblog { + let metaText = String.localizedStringWithFormat( + NSLocalizedString("status.reblogged-by", comment: ""), + viewModel.rebloggedByDisplayName) + let mutableMetaText = NSMutableAttributedString(string: metaText) + mutableMetaText.insert(emoji: viewModel.rebloggedByDisplayNameEmoji, view: metaLabel) + mutableMetaText.resizeAttachments(toLineHeight: metaLabel.font.lineHeight) + metaLabel.attributedText = mutableMetaText + metaIcon.image = UIImage( + systemName: "arrow.2.squarepath", + withConfiguration: UIImage.SymbolConfiguration(scale: .small)) + metaLabel.isHidden = false + metaIcon.isHidden = false + } else if viewModel.isPinned { + metaLabel.text = NSLocalizedString("status.pinned-post", comment: "") + metaIcon.image = UIImage( + systemName: "pin", + withConfiguration: UIImage.SymbolConfiguration(scale: .small)) + metaLabel.isHidden = false + metaIcon.isHidden = false + } else { + metaLabel.isHidden = true + metaIcon.isHidden = true + } + + attachmentsView.isHidden = viewModel.attachmentViewModels.count == 0 + attachmentsView.attachmentViewModels = viewModel.attachmentViewModels + setNeedsLayout() + + if let cardURL = viewModel.cardURL { + cardTitleLabel.text = viewModel.cardTitle + cardDescriptionLabel.text = viewModel.cardDescription + cardDescriptionLabel.isHidden = cardDescriptionLabel.text == "" + || cardDescriptionLabel.text == cardTitleLabel.text + if + let host = cardURL.host, host.hasPrefix("www."), + let withoutWww = cardURL.host?.components(separatedBy: "www.").last { + cardURLLabel.text = withoutWww + } else { + cardURLLabel.text = cardURL.host + } + + if let cardImageURL = viewModel.cardImageURL { + cardImageView.isHidden = false + cardImageView.kf.setImage(with: cardImageURL) + } else { + cardImageView.isHidden = true + } + cardView.isHidden = false + } else { + cardView.isHidden = true + } + + sensitiveContentView.isHidden = !viewModel.shouldDisplaySensitiveContent + + inReplyToView.isHidden = !viewModel.isReplyInContext + + hasReplyFollowingView.isHidden = !viewModel.hasReplyFollowing + } + // swiftlint:enable function_body_length + + func setReblogButtonColor(reblogged: Bool) { + let reblogColor: UIColor = reblogged ? .systemGreen : .secondaryLabel + let reblogButton: UIButton + + if statusConfiguration.viewModel.isContextParent { + reblogButton = contextParentReblogButton + } else { + reblogButton = self.reblogButton + } + + reblogButton.tintColor = reblogColor + reblogButton.setTitleColor(reblogColor, for: .normal) + } + + func setFavoriteButtonColor(favorited: Bool) { + let favoriteColor: UIColor = favorited ? .systemYellow : .secondaryLabel + let favoriteButton: UIButton + let scale: UIImage.SymbolScale + + if statusConfiguration.viewModel.isContextParent { + favoriteButton = contextParentFavoriteButton + scale = .medium + } else { + favoriteButton = self.favoriteButton + scale = .small + } + + favoriteButton.tintColor = favoriteColor + favoriteButton.setTitleColor(favoriteColor, for: .normal) + favoriteButton.setImage(UIImage( + systemName: favorited ? "star.fill" : "star", + withConfiguration: UIImage.SymbolConfiguration(scale: scale)), + for: .normal) + } + + private func setAttributedLocalizedTitle(button: UIButton, localizationKey: String, count: Int) { + let localizedTitle = String.localizedStringWithFormat(NSLocalizedString(localizationKey, comment: ""), count) + + button.setAttributedTitle(localizedTitle.countEmphasizedAttributedString(count: count), for: .normal) + button.setAttributedTitle( + localizedTitle.countEmphasizedAttributedString(count: count, highlighted: true), + for: .highlighted) } } diff --git a/Views/Status/StatusView.xib b/Views/Status/StatusView.xib new file mode 100644 index 0000000..19379fd --- /dev/null +++ b/Views/Status/StatusView.xib @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Views/Status Cell/TouchFallthroughTextView.swift b/Views/Status/TouchFallthroughTextView.swift similarity index 100% rename from Views/Status Cell/TouchFallthroughTextView.swift rename to Views/Status/TouchFallthroughTextView.swift