diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index d8f183a..0eaf43c 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ D0030982250C6C8500EACB32 /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0030981250C6C8500EACB32 /* URL+Extensions.swift */; }; + D00CB2ED2533ACC00080096B /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00CB2EC2533ACC00080096B /* StatusView.swift */; }; D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C6FAB252024BD003D0300 /* Array+Extensions.swift */; }; D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01EF22325182B1F00650C6B /* AccountHeaderView.swift */; }; D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */; }; @@ -15,13 +16,11 @@ D02E1F95250B13210071AD56 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E1F94250B13210071AD56 /* SafariView.swift */; }; D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; }; D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; }; - D0625E5F250F0CFF00502611 /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5E250F0CFF00502611 /* StatusView.swift */; }; D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; }; D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; }; D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; }; D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; }; D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */; }; - D0B7434925100DBB00C13DB6 /* StatusView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D0B7434825100DBB00C13DB6 /* StatusView.xib */; }; D0B8510C25259E56004E0744 /* LoadMoreCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B8510B25259E56004E0744 /* LoadMoreCell.swift */; }; D0BEB1F324F8EE8C001B0F04 /* StatusAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F224F8EE8C001B0F04 /* StatusAttachmentView.swift */; }; D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */; }; @@ -99,6 +98,7 @@ /* Begin PBXFileReference section */ D0030981250C6C8500EACB32 /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = ""; }; + D00CB2EC2533ACC00080096B /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = ""; }; D01C6FAB252024BD003D0300 /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = ""; }; D01EF22325182B1F00650C6B /* AccountHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountHeaderView.swift; sourceTree = ""; }; D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchFallthroughTextView.swift; sourceTree = ""; }; @@ -107,7 +107,6 @@ D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; }; D0625E58250F092900502611 /* StatusListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusListCell.swift; sourceTree = ""; }; D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentConfiguration.swift; sourceTree = ""; }; - D0625E5E250F0CFF00502611 /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = ""; }; D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D0666A2524C677B400F3F04B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D06BC5E525202AD90079541D /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; @@ -116,7 +115,6 @@ D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = ""; }; D0B32F4F250B373600311912 /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = ""; }; D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileCollection+Extensions.swift"; sourceTree = ""; }; - D0B7434825100DBB00C13DB6 /* StatusView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = StatusView.xib; sourceTree = ""; }; D0B8510B25259E56004E0744 /* LoadMoreCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreCell.swift; sourceTree = ""; }; D0BDF66524FD7A6400C7FA1C /* ServiceLayer */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ServiceLayer; sourceTree = ""; }; D0BEB1F224F8EE8C001B0F04 /* StatusAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentView.swift; sourceTree = ""; }; @@ -245,8 +243,7 @@ D0BEB1F224F8EE8C001B0F04 /* StatusAttachmentView.swift */, D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */, D0625E58250F092900502611 /* StatusListCell.swift */, - D0625E5E250F0CFF00502611 /* StatusView.swift */, - D0B7434825100DBB00C13DB6 /* StatusView.xib */, + D00CB2EC2533ACC00080096B /* StatusView.swift */, ); path = Status; sourceTree = ""; @@ -491,7 +488,6 @@ buildActionMask = 2147483647; files = ( D0C7D4C524F7616A001EBDBB /* Localizable.strings in Resources */, - D0B7434925100DBB00C13DB6 /* StatusView.xib in Resources */, D0C7D4C224F7616A001EBDBB /* Assets.xcassets in Resources */, D0C7D4C624F7616A001EBDBB /* Localizable.stringsdict in Resources */, ); @@ -552,7 +548,6 @@ D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */, D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */, D0C7D4D624F7616A001EBDBB /* NSMutableAttributedString+Extensions.swift in Sources */, - D0625E5F250F0CFF00502611 /* StatusView.swift in Sources */, D0625E59250F092900502611 /* StatusListCell.swift in Sources */, D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */, D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */, @@ -576,6 +571,7 @@ D01F41E424F8889700D55A2D /* StatusAttachmentsView.swift in Sources */, D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */, D0030982250C6C8500EACB32 /* URL+Extensions.swift in Sources */, + D00CB2ED2533ACC00080096B /* StatusView.swift in Sources */, D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */, D0C7D4C424F7616A001EBDBB /* AppDelegate.swift in Sources */, D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */, diff --git a/Views/AccountHeaderView.swift b/Views/AccountHeaderView.swift index cd57bae..72a6364 100644 --- a/Views/AccountHeaderView.swift +++ b/Views/AccountHeaderView.swift @@ -95,7 +95,7 @@ private extension AccountHeaderView { equalTo: headerImageView.widthAnchor, multiplier: 1 / 3) - headerImageAspectRatioConstraint.priority = .init(999) + headerImageAspectRatioConstraint.priority = .justBelowMax NSLayoutConstraint.activate([ headerImageAspectRatioConstraint, diff --git a/Views/AccountView.swift b/Views/AccountView.swift index ea04655..f158acb 100644 --- a/Views/AccountView.swift +++ b/Views/AccountView.swift @@ -57,15 +57,13 @@ extension AccountView: UITextViewDelegate { } private extension AccountView { - static let avatarDimension: CGFloat = 50 - func initialSetup() { let stackView = UIStackView() addSubview(avatarImageView) addSubview(stackView) avatarImageView.translatesAutoresizingMaskIntoConstraints = false - avatarImageView.layer.cornerRadius = Self.avatarDimension / 2 + avatarImageView.layer.cornerRadius = .avatarDimension / 2 avatarImageView.clipsToBounds = true stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical @@ -85,8 +83,8 @@ private extension AccountView { noteTextView.delegate = self NSLayoutConstraint.activate([ - avatarImageView.widthAnchor.constraint(equalToConstant: Self.avatarDimension), - avatarImageView.heightAnchor.constraint(equalToConstant: Self.avatarDimension), + avatarImageView.widthAnchor.constraint(equalToConstant: .avatarDimension), + avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension), avatarImageView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor), avatarImageView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), avatarImageView.bottomAnchor.constraint(lessThanOrEqualTo: readableContentGuide.bottomAnchor), diff --git a/Views/SecondaryNavigationView.swift b/Views/SecondaryNavigationView.swift index 8ace9c8..75289a6 100644 --- a/Views/SecondaryNavigationView.swift +++ b/Views/SecondaryNavigationView.swift @@ -18,7 +18,7 @@ struct SecondaryNavigationView: View { label: { HStack { KFImage(viewModel.identification.identity.image, - options: .downsampled(dimension: 50, scaleFactor: displayScale)) + options: .downsampled(dimension: .avatarDimension, scaleFactor: displayScale)) VStack(alignment: .leading) { if viewModel.identification.identity.authenticated { if let account = viewModel.identification.identity.account { diff --git a/Views/Status/StatusAttachmentsView.swift b/Views/Status/StatusAttachmentsView.swift index 1d3a110..878c4e4 100644 --- a/Views/Status/StatusAttachmentsView.swift +++ b/Views/Status/StatusAttachmentsView.swift @@ -7,6 +7,7 @@ final class StatusAttachmentsView: UIView { private let containerStackView = UIStackView() private let leftStackView = UIStackView() private let rightStackView = UIStackView() + private var aspectRatioConstraint: NSLayoutConstraint? var attachmentViewModels = [AttachmentViewModel]() { didSet { @@ -32,6 +33,19 @@ final class StatusAttachmentsView: UIView { leftStackView.addArrangedSubview(attachmentView) } } + + let newAspectRatio: CGFloat + + if attachmentViewModels.count == 1, let aspectRatio = attachmentViewModels.first?.aspectRatio { + newAspectRatio = max(CGFloat(aspectRatio), 16 / 9) + } else { + newAspectRatio = 16 / 9 + } + + aspectRatioConstraint?.isActive = false + aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: newAspectRatio) + aspectRatioConstraint?.priority = .justBelowMax + aspectRatioConstraint?.isActive = true } } diff --git a/Views/Status/StatusView.swift b/Views/Status/StatusView.swift index 26e8539..5068fb6 100644 --- a/Views/Status/StatusView.swift +++ b/Views/Status/StatusView.swift @@ -1,53 +1,48 @@ // Copyright © 2020 Metabolist. All rights reserved. +// swiftlint:disable file_length import Kingfisher import UIKit -class StatusView: UIView { - @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 toggleShowMoreButton: 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: StatusAttachmentsView! - @IBOutlet weak var cardView: CardView! - @IBOutlet weak var showMoreView: 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! +final class StatusView: UIView { + let avatarImageView = AnimatedImageView() + let avatarButton = UIButton() + let infoIcon = UIImageView() + let infoLabel = UILabel() + let displayNameLabel = UILabel() + let accountLabel = UILabel() + let timeLabel = UILabel() + let spoilerTextLabel = UILabel() + let toggleShowMoreButton = UIButton(type: .system) + let contentTextView = TouchFallthroughTextView() + let attachmentsView = StatusAttachmentsView() + let cardView = CardView() + let contextParentTimeLabel = UILabel() + let timeApplicationDividerLabel = UILabel() + let applicationButton = UIButton(type: .system) + let rebloggedByButton = UIButton() + let favoritedByButton = UIButton() + let replyButton = UIButton() + let reblogButton = UIButton() + let favoriteButton = UIButton() + let shareButton = UIButton() + let menuButton = UIButton() + private let containerStackView = UIStackView() + private let sideStackView = UIStackView() + private let mainStackView = UIStackView() + private let nameAccountContainerStackView = UIStackView() + private let nameAccountTimeStackView = UIStackView() + private let contextParentTimeApplicationStackView = UIStackView() + private let contextParentTopNameAccountSpacingView = UIView() + private let contextParentBottomNameAccountSpacingView = UIView() + private let interactionsDividerView = UIView() + private let interactionsStackView = UIStackView() + private let buttonsDividerView = UIView() + private let buttonsStackView = UIStackView() + private let inReplyToView = UIView() + private let hasReplyFollowingView = UIView() private var statusConfiguration: StatusContentConfiguration - @IBOutlet private var separatorConstraints: [NSLayoutConstraint]! init(configuration: StatusContentConfiguration) { self.statusConfiguration = configuration @@ -61,14 +56,6 @@ class StatusView: UIView { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - override func layoutSubviews() { - super.layoutSubviews() - - for button: UIButton in [toggleShowMoreButton] where button.frame.height != 0 { - button.layer.cornerRadius = button.frame.height / 2 - } - } } extension StatusView: UIContentView { @@ -79,8 +66,6 @@ extension StatusView: UIContentView { self.statusConfiguration = statusConfiguration - avatarImageView.kf.cancelDownloadTask() - contextParentAvatarImageView.kf.cancelDownloadTask() applyStatusConfiguration() } } @@ -104,46 +89,78 @@ extension StatusView: UITextViewDelegate { } private extension StatusView { + static let actionButtonTitleEdgeInsets = UIEdgeInsets(top: 0, left: 2, bottom: 0, right: 0) + + var actionButtons: [UIButton] { + [replyButton, reblogButton, favoriteButton, shareButton, menuButton] + } + // swiftlint:disable function_body_length func initialSetup() { - Bundle.main.loadNibNamed(String(describing: type(of: self)), owner: self, options: nil) + addSubview(containerStackView) + containerStackView.translatesAutoresizingMaskIntoConstraints = false + containerStackView.spacing = .defaultSpacing - addSubview(baseView) + infoIcon.tintColor = .secondaryLabel + infoIcon.setContentCompressionResistancePriority(.required, for: .vertical) - baseView.translatesAutoresizingMaskIntoConstraints = false - baseView.backgroundColor = .clear + sideStackView.axis = .vertical + sideStackView.alignment = .trailing + sideStackView.spacing = .compactSpacing + sideStackView.addArrangedSubview(infoIcon) + sideStackView.addArrangedSubview(UIView()) + containerStackView.addArrangedSubview(sideStackView) - NSLayoutConstraint.activate([ - 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) - ]) + mainStackView.axis = .vertical + mainStackView.spacing = .compactSpacing + containerStackView.addArrangedSubview(mainStackView) - for constraint in separatorConstraints { - constraint.constant = .hairline - } + infoLabel.font = .preferredFont(forTextStyle: .caption1) + infoLabel.textColor = .secondaryLabel + infoLabel.adjustsFontForContentSizeCategory = true + infoLabel.setContentHuggingPriority(.required, for: .vertical) + mainStackView.addArrangedSubview(infoLabel) - avatarImageView.kf.indicatorType = .activity - contextParentAvatarImageView.kf.indicatorType = .activity + displayNameLabel.font = .preferredFont(forTextStyle: .headline) + displayNameLabel.adjustsFontForContentSizeCategory = true + displayNameLabel.setContentHuggingPriority(.required, for: .horizontal) + displayNameLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + nameAccountTimeStackView.addArrangedSubview(displayNameLabel) - contentTextView.delegate = self + accountLabel.font = .preferredFont(forTextStyle: .subheadline) + accountLabel.adjustsFontForContentSizeCategory = true + accountLabel.textColor = .secondaryLabel + nameAccountTimeStackView.addArrangedSubview(accountLabel) - avatarButton.setBackgroundImage(.highlightedButtonBackground, for: .highlighted) - contextParentAvatarButton.setBackgroundImage(.highlightedButtonBackground, for: .highlighted) + timeLabel.font = .preferredFont(forTextStyle: .subheadline) + timeLabel.adjustsFontForContentSizeCategory = true + timeLabel.textColor = .secondaryLabel + timeLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + timeLabel.setContentHuggingPriority(.required, for: .horizontal) + nameAccountTimeStackView.addArrangedSubview(timeLabel) - let accountAction = UIAction { [weak self] _ in self?.statusConfiguration.viewModel.accountSelected() } + nameAccountContainerStackView.spacing = .defaultSpacing + nameAccountContainerStackView.addArrangedSubview(nameAccountTimeStackView) + mainStackView.addArrangedSubview(nameAccountContainerStackView) - avatarButton.addAction(accountAction, for: .touchUpInside) - contextParentAvatarButton.addAction(accountAction, for: .touchUpInside) + spoilerTextLabel.numberOfLines = 0 + spoilerTextLabel.adjustsFontForContentSizeCategory = true + mainStackView.addArrangedSubview(spoilerTextLabel) + toggleShowMoreButton.titleLabel?.font = .preferredFont(forTextStyle: .headline) + toggleShowMoreButton.titleLabel?.adjustsFontForContentSizeCategory = true toggleShowMoreButton.addAction( UIAction { [weak self] _ in self?.statusConfiguration.viewModel.toggleShowMore() }, for: .touchUpInside) + mainStackView.addArrangedSubview(toggleShowMoreButton) + + contentTextView.adjustsFontForContentSizeCategory = true + contentTextView.isScrollEnabled = false + contentTextView.backgroundColor = .clear + contentTextView.delegate = self + mainStackView.addArrangedSubview(contentTextView) + + mainStackView.addArrangedSubview(attachmentsView) cardView.button.addAction( UIAction { [weak self] _ in @@ -155,24 +172,25 @@ private extension StatusView { viewModel.urlSelected(url) }, for: .touchUpInside) + mainStackView.addArrangedSubview(cardView) - let favoriteAction = UIAction { [weak self] _ in self?.statusConfiguration.viewModel.toggleFavorited() } + contextParentTimeLabel.font = .preferredFont(forTextStyle: .footnote) + contextParentTimeLabel.adjustsFontForContentSizeCategory = true + contextParentTimeLabel.textColor = .secondaryLabel + contextParentTimeLabel.setContentHuggingPriority(.required, for: .horizontal) + contextParentTimeApplicationStackView.addArrangedSubview(contextParentTimeLabel) - favoriteButton.addAction(favoriteAction, for: .touchUpInside) - contextParentFavoriteButton.addAction(favoriteAction, for: .touchUpInside) - - shareButton.addAction( - UIAction { [weak self] _ in self?.statusConfiguration.viewModel.shareStatus() }, - for: .touchUpInside) - - contextParentRebloggedByButton.addAction( - UIAction { [weak self] _ in self?.statusConfiguration.viewModel.rebloggedBySelected() }, - for: .touchUpInside) - - contextParentFavoritedByButton.addAction( - UIAction { [weak self] _ in self?.statusConfiguration.viewModel.favoritedBySelected() }, - for: .touchUpInside) + timeApplicationDividerLabel.font = .preferredFont(forTextStyle: .footnote) + timeApplicationDividerLabel.adjustsFontForContentSizeCategory = true + timeApplicationDividerLabel.textColor = .secondaryLabel + timeApplicationDividerLabel.text = "•" + timeApplicationDividerLabel.setContentHuggingPriority(.required, for: .horizontal) + contextParentTimeApplicationStackView.addArrangedSubview(timeApplicationDividerLabel) + applicationButton.titleLabel?.font = .preferredFont(forTextStyle: .footnote) + applicationButton.titleLabel?.adjustsFontForContentSizeCategory = true + applicationButton.setTitleColor(.secondaryLabel, for: .disabled) + applicationButton.setContentHuggingPriority(.required, for: .horizontal) applicationButton.addAction( UIAction { [weak self] _ in guard @@ -183,96 +201,126 @@ private extension StatusView { viewModel.urlSelected(url) }, for: .touchUpInside) + contextParentTimeApplicationStackView.addArrangedSubview(applicationButton) + contextParentTimeApplicationStackView.addArrangedSubview(UIView()) + + contextParentTimeApplicationStackView.spacing = .compactSpacing + mainStackView.addArrangedSubview(contextParentTimeApplicationStackView) + + for view in [interactionsDividerView, buttonsDividerView] { + view.backgroundColor = .opaqueSeparator + view.heightAnchor.constraint(equalToConstant: .hairline).isActive = true + } + + mainStackView.addArrangedSubview(interactionsDividerView) + mainStackView.addArrangedSubview(interactionsStackView) + mainStackView.addArrangedSubview(buttonsDividerView) + + rebloggedByButton.contentHorizontalAlignment = .leading + rebloggedByButton.addAction( + UIAction { [weak self] _ in self?.statusConfiguration.viewModel.rebloggedBySelected() }, + for: .touchUpInside) + interactionsStackView.addArrangedSubview(rebloggedByButton) + + favoritedByButton.contentHorizontalAlignment = .leading + favoritedByButton.addAction( + UIAction { [weak self] _ in self?.statusConfiguration.viewModel.favoritedBySelected() }, + for: .touchUpInside) + interactionsStackView.addArrangedSubview(favoritedByButton) + interactionsStackView.distribution = .fillEqually + + favoriteButton.addAction( + UIAction { [weak self] _ in self?.statusConfiguration.viewModel.toggleFavorited() }, + for: .touchUpInside) + + shareButton.addAction( + UIAction { [weak self] _ in self?.statusConfiguration.viewModel.shareStatus() }, + for: .touchUpInside) + + for button in actionButtons { + button.titleLabel?.font = .preferredFont(forTextStyle: .footnote) + button.titleLabel?.adjustsFontSizeToFitWidth = true + button.tintColor = .secondaryLabel + button.setTitleColor(.secondaryLabel, for: .normal) + button.titleEdgeInsets = Self.actionButtonTitleEdgeInsets + buttonsStackView.addArrangedSubview(button) + button.widthAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension).isActive = true + } + + buttonsStackView.distribution = .equalSpacing + mainStackView.addArrangedSubview(buttonsStackView) + + avatarImageView.layer.cornerRadius = .avatarDimension / 2 + avatarImageView.clipsToBounds = true + + let avatarHeightConstraint = avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension) + + avatarHeightConstraint.priority = .justBelowMax + + avatarButton.translatesAutoresizingMaskIntoConstraints = false + avatarImageView.addSubview(avatarButton) + avatarImageView.isUserInteractionEnabled = true + avatarButton.setBackgroundImage(.highlightedButtonBackground, for: .highlighted) + + avatarButton.addAction( + UIAction { [weak self] _ in self?.statusConfiguration.viewModel.accountSelected() }, + for: .touchUpInside) + + for view in [inReplyToView, hasReplyFollowingView] { + addSubview(view) + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .opaqueSeparator + view.widthAnchor.constraint(equalToConstant: .hairline).isActive = true + } + + NSLayoutConstraint.activate([ + containerStackView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor), + containerStackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), + containerStackView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor), + avatarImageView.widthAnchor.constraint(equalToConstant: .avatarDimension), + avatarHeightConstraint, + sideStackView.widthAnchor.constraint(equalToConstant: .avatarDimension), + infoIcon.centerYAnchor.constraint(equalTo: infoLabel.centerYAnchor), + avatarButton.leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor), + avatarButton.topAnchor.constraint(equalTo: avatarImageView.topAnchor), + avatarButton.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), + avatarButton.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor) + ]) applyStatusConfiguration() } func applyStatusConfiguration() { let viewModel = statusConfiguration.viewModel + let isContextParent = viewModel.configuration.isContextParent let mutableContent = NSMutableAttributedString(attributedString: viewModel.content) let mutableDisplayName = NSMutableAttributedString(string: viewModel.displayName) let mutableSpoilerText = NSMutableAttributedString(string: viewModel.spoilerText) - let contentTextStyle: UIFont.TextStyle = viewModel.configuration.isContextParent ? .title3 : .callout - let contentFont = UIFont.preferredFont(forTextStyle: contentTextStyle) + let contentFont = UIFont.preferredFont(forTextStyle: isContextParent ? .title3 : .callout) + let contentRange = NSRange(location: 0, length: mutableContent.length) - contentTextView.shouldFallthrough = !viewModel.configuration.isContextParent - avatarReplyContextView.isHidden = viewModel.configuration.isContextParent - nameDateView.isHidden = viewModel.configuration.isContextParent - contextParentAvatarNameView.isHidden = !viewModel.configuration.isContextParent - actionButtonsView.isHidden = viewModel.configuration.isContextParent - contextParentItems.isHidden = !viewModel.configuration.isContextParent + contentTextView.shouldFallthrough = !isContextParent + sideStackView.isHidden = isContextParent + avatarImageView.removeFromSuperview() - let avatarImageView: UIImageView - let displayNameLabel: UILabel - let accountLabel: UILabel - - if viewModel.configuration.isContextParent { - avatarImageView = contextParentAvatarImageView - displayNameLabel = contextParentDisplayNameLabel - accountLabel = contextParentAccountLabel + if isContextParent { + nameAccountContainerStackView.insertArrangedSubview(avatarImageView, at: 0) } else { - avatarImageView = self.avatarImageView - displayNameLabel = self.displayNameLabel - accountLabel = self.accountLabel + sideStackView.insertArrangedSubview(avatarImageView, at: 1) } - 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 == "" - toggleShowMoreButton.setTitle( - viewModel.shouldShowMore - ? 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) - toggleShowMoreButton.isHidden = viewModel.spoilerText == "" - 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) + NSLayoutConstraint.activate([ + inReplyToView.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor), + inReplyToView.topAnchor.constraint(equalTo: topAnchor), + inReplyToView.bottomAnchor.constraint(equalTo: avatarImageView.topAnchor), + hasReplyFollowingView.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor), + hasReplyFollowingView.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor), + hasReplyFollowingView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) - 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 + inReplyToView.isHidden = !viewModel.configuration.isReplyInContext + hasReplyFollowingView.isHidden = !viewModel.configuration.hasReplyFollowing if viewModel.isReblog { @@ -280,77 +328,148 @@ private extension StatusView { 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( + mutableMetaText.insert(emoji: viewModel.rebloggedByDisplayNameEmoji, view: infoLabel) + mutableMetaText.resizeAttachments(toLineHeight: infoLabel.font.lineHeight) + infoLabel.attributedText = mutableMetaText + infoIcon.image = UIImage( systemName: "arrow.2.squarepath", withConfiguration: UIImage.SymbolConfiguration(scale: .small)) - metaLabel.isHidden = false - metaIcon.isHidden = false + infoLabel.isHidden = false + infoIcon.isHidden = false } else if viewModel.configuration.isPinned { - metaLabel.text = NSLocalizedString("status.pinned-post", comment: "") - metaIcon.image = UIImage( + infoLabel.text = NSLocalizedString("status.pinned-post", comment: "") + infoIcon.image = UIImage( systemName: "pin", withConfiguration: UIImage.SymbolConfiguration(scale: .small)) - metaLabel.isHidden = false - metaIcon.isHidden = false + infoLabel.isHidden = false + infoIcon.isHidden = false } else { - metaLabel.isHidden = true - metaIcon.isHidden = true + infoLabel.isHidden = true + infoIcon.isHidden = true } + mutableContent.removeAttribute(.font, range: contentRange) + mutableContent.addAttributes( + [.font: contentFont, .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.font = contentFont + spoilerTextLabel.attributedText = mutableSpoilerText + spoilerTextLabel.isHidden = spoilerTextLabel.text == "" + toggleShowMoreButton.setTitle( + viewModel.shouldShowMore + ? NSLocalizedString("status.show-less", comment: "") + : NSLocalizedString("status.show-more", comment: ""), + for: .normal) + toggleShowMoreButton.isHidden = viewModel.spoilerText == "" + + contentTextView.isHidden = !viewModel.shouldShowMore + + nameAccountTimeStackView.axis = isContextParent ? .vertical : .horizontal + nameAccountTimeStackView.alignment = isContextParent ? .leading : .fill + nameAccountTimeStackView.spacing = isContextParent ? 0 : .compactSpacing + + contextParentTopNameAccountSpacingView.removeFromSuperview() + contextParentBottomNameAccountSpacingView.removeFromSuperview() + + if isContextParent { + nameAccountTimeStackView.insertArrangedSubview(contextParentTopNameAccountSpacingView, at: 0) + nameAccountTimeStackView.addArrangedSubview(contextParentBottomNameAccountSpacingView) + contextParentTopNameAccountSpacingView.heightAnchor + .constraint(equalTo: contextParentBottomNameAccountSpacingView.heightAnchor).isActive = true + } + + accountLabel.text = viewModel.accountName + timeLabel.text = viewModel.time + timeLabel.isHidden = isContextParent + attachmentsView.isHidden = viewModel.attachmentViewModels.count == 0 attachmentsView.attachmentViewModels = viewModel.attachmentViewModels - setNeedsLayout() cardView.viewModel = viewModel.cardViewModel cardView.isHidden = viewModel.cardViewModel == nil - showMoreView.isHidden = !viewModel.shouldShowMore + contextParentTimeLabel.text = viewModel.contextParentTime + timeApplicationDividerLabel.isHidden = viewModel.applicationName == nil + applicationButton.isHidden = viewModel.applicationName == nil + applicationButton.setTitle(viewModel.applicationName, for: .normal) + applicationButton.isEnabled = viewModel.applicationURL != nil + contextParentTimeApplicationStackView.isHidden = !isContextParent - inReplyToView.isHidden = !viewModel.configuration.isReplyInContext + let noReblogs = viewModel.reblogsCount == 0 + let noFavorites = viewModel.favoritesCount == 0 + let noInteractions = !isContextParent || (noReblogs && noFavorites) - hasReplyFollowingView.isHidden = !viewModel.configuration.hasReplyFollowing - } - // swiftlint:enable function_body_length + setAttributedLocalizedTitle( + button: rebloggedByButton, + localizationKey: "status.reblogs-count", + count: viewModel.reblogsCount) + rebloggedByButton.isHidden = noReblogs + setAttributedLocalizedTitle( + button: favoritedByButton, + localizationKey: "status.favorites-count", + count: viewModel.favoritesCount) + favoritedByButton.isHidden = noFavorites - func setReblogButtonColor(reblogged: Bool) { - let reblogColor: UIColor = reblogged ? .systemGreen : .secondaryLabel - let reblogButton: UIButton + interactionsDividerView.isHidden = noInteractions + interactionsStackView.isHidden = noInteractions + buttonsDividerView.isHidden = !isContextParent - if statusConfiguration.viewModel.configuration.isContextParent { - reblogButton = contextParentReblogButton - } else { - reblogButton = self.reblogButton + for button in actionButtons { + button.contentHorizontalAlignment = isContextParent ? .center : .leading + + if isContextParent { + button.heightAnchor.constraint(equalToConstant: .minimumButtonDimension).isActive = true + } else { + button.heightAnchor.constraint(greaterThanOrEqualToConstant: 0).isActive = true + } } + setButtonImages(scale: isContextParent ? .medium : .small) + + replyButton.setCountTitle(count: viewModel.repliesCount, isContextParent: isContextParent) + reblogButton.setCountTitle(count: viewModel.reblogsCount, isContextParent: isContextParent) + favoriteButton.setCountTitle(count: viewModel.favoritesCount, isContextParent: isContextParent) + + let reblogColor: UIColor = viewModel.reblogged ? .systemGreen : .secondaryLabel + reblogButton.tintColor = reblogColor reblogButton.setTitleColor(reblogColor, for: .normal) - } + reblogButton.isEnabled = viewModel.canBeReblogged - func setFavoriteButtonColor(favorited: Bool) { - let favoriteColor: UIColor = favorited ? .systemYellow : .secondaryLabel - let favoriteButton: UIButton - let scale: UIImage.SymbolScale - - if statusConfiguration.viewModel.configuration.isContextParent { - favoriteButton = contextParentFavoriteButton - scale = .medium - } else { - favoriteButton = self.favoriteButton - scale = .small - } + let favoriteColor: UIColor = viewModel.favorited ? .systemYellow : .secondaryLabel favoriteButton.tintColor = favoriteColor favoriteButton.setTitleColor(favoriteColor, for: .normal) - favoriteButton.setImage(UIImage( - systemName: favorited ? "star.fill" : "star", - withConfiguration: UIImage.SymbolConfiguration(scale: scale)), - for: .normal) + + avatarImageView.kf.setImage(with: viewModel.avatarURL) + } + // swiftlint:enable function_body_length + + func setButtonImages(scale: UIImage.SymbolScale) { + replyButton.setImage(UIImage(systemName: "bubble.right", + withConfiguration: UIImage.SymbolConfiguration(scale: scale)), for: .normal) + reblogButton.setImage(UIImage(systemName: "arrow.2.squarepath", + withConfiguration: UIImage.SymbolConfiguration(scale: scale)), for: .normal) + favoriteButton.setImage(UIImage(systemName: statusConfiguration.viewModel.favorited ? "star.fill" : "star", + withConfiguration: UIImage.SymbolConfiguration(scale: scale)), for: .normal) + shareButton.setImage(UIImage(systemName: "square.and.arrow.up", + withConfiguration: UIImage.SymbolConfiguration(scale: scale)), for: .normal) + menuButton.setImage(UIImage(systemName: "ellipsis", + withConfiguration: UIImage.SymbolConfiguration(scale: scale)), for: .normal) } - private func setAttributedLocalizedTitle(button: UIButton, localizationKey: String, count: Int) { + func setAttributedLocalizedTitle(button: UIButton, localizationKey: String, count: Int) { let localizedTitle = String.localizedStringWithFormat(NSLocalizedString(localizationKey, comment: ""), count) button.setAttributedTitle(localizedTitle.countEmphasizedAttributedString(count: count), for: .normal) @@ -359,3 +478,10 @@ private extension StatusView { for: .highlighted) } } + +private extension UIButton { + func setCountTitle(count: Int, isContextParent: Bool) { + setTitle((isContextParent || count == 0) ? "" : String(count), for: .normal) + } +} +// swiftlint:enable file_length diff --git a/Views/Status/StatusView.xib b/Views/Status/StatusView.xib deleted file mode 100644 index 84a2bab..0000000 --- a/Views/Status/StatusView.xib +++ /dev/null @@ -1,454 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Views/ViewConstants.swift b/Views/ViewConstants.swift index aa76712..24bfce8 100644 --- a/Views/ViewConstants.swift +++ b/Views/ViewConstants.swift @@ -6,7 +6,9 @@ extension CGFloat { static let defaultSpacing: Self = 8 static let compactSpacing: Self = 4 static let defaultCornerRadius: Self = 8 + static let avatarDimension: Self = 50 static let hairline = 1 / UIScreen.main.scale + static let minimumButtonDimension: Self = 44 } extension TimeInterval { @@ -16,3 +18,7 @@ extension TimeInterval { extension UIImage { static let highlightedButtonBackground = UIColor(white: 0, alpha: 0.5).image() } + +extension UILayoutPriority { + static let justBelowMax: Self = .init(999) +}