2022-01-27 14:23:39 +01:00
|
|
|
//
|
|
|
|
// UserView.swift
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// Created by MainasuK on 2022-1-19.
|
|
|
|
//
|
|
|
|
|
|
|
|
import UIKit
|
|
|
|
import Combine
|
|
|
|
import MetaTextKit
|
2023-04-20 16:28:30 +02:00
|
|
|
import MastodonAsset
|
2023-04-25 12:48:53 +02:00
|
|
|
import MastodonLocalization
|
2023-04-20 16:28:30 +02:00
|
|
|
import os
|
2023-04-25 12:48:53 +02:00
|
|
|
import CoreDataStack
|
|
|
|
|
|
|
|
public protocol UserViewDelegate: AnyObject {
|
|
|
|
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser)
|
|
|
|
}
|
2022-01-27 14:23:39 +01:00
|
|
|
|
|
|
|
public final class UserView: UIView {
|
|
|
|
|
2023-04-25 10:54:10 +02:00
|
|
|
public enum ButtonState {
|
|
|
|
case none, follow, unfollow, blocked
|
|
|
|
}
|
|
|
|
|
2023-04-25 12:48:53 +02:00
|
|
|
private var currentButtonState: ButtonState = .none
|
|
|
|
|
|
|
|
public weak var delegate: UserViewDelegate?
|
|
|
|
|
2022-01-27 14:23:39 +01:00
|
|
|
public var disposeBag = Set<AnyCancellable>()
|
|
|
|
|
|
|
|
public private(set) lazy var viewModel: ViewModel = {
|
|
|
|
let viewModel = ViewModel()
|
|
|
|
viewModel.bind(userView: self)
|
|
|
|
return viewModel
|
|
|
|
}()
|
|
|
|
|
|
|
|
public let containerStackView: UIStackView = {
|
|
|
|
let stackView = UIStackView()
|
|
|
|
stackView.axis = .horizontal
|
|
|
|
stackView.alignment = .center
|
|
|
|
stackView.spacing = 12
|
|
|
|
stackView.layoutMargins = UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0)
|
|
|
|
stackView.isLayoutMarginsRelativeArrangement = true
|
|
|
|
return stackView
|
|
|
|
}()
|
|
|
|
|
|
|
|
// avatar
|
|
|
|
public let avatarButton = AvatarButton()
|
|
|
|
|
|
|
|
// author name
|
|
|
|
public let authorNameLabel = MetaLabel(style: .statusName)
|
|
|
|
|
|
|
|
// author username
|
|
|
|
public let authorUsernameLabel = MetaLabel(style: .statusUsername)
|
|
|
|
|
2023-04-20 16:28:30 +02:00
|
|
|
public let authorFollowersLabel: UILabel = {
|
|
|
|
let label = UILabel()
|
|
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
label.textColor = .secondaryLabel
|
|
|
|
return label
|
|
|
|
}()
|
|
|
|
|
|
|
|
public let authorVerifiedLabel: MetaLabel = {
|
|
|
|
let label = MetaLabel(style: .profileFieldValue)
|
|
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
label.textAttributes = [
|
|
|
|
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)),
|
|
|
|
.foregroundColor: UIColor.secondaryLabel
|
|
|
|
]
|
|
|
|
label.linkAttributes = [
|
|
|
|
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold)),
|
|
|
|
.foregroundColor: Asset.Colors.brand.color
|
|
|
|
]
|
|
|
|
label.isUserInteractionEnabled = false
|
|
|
|
return label
|
|
|
|
}()
|
|
|
|
|
|
|
|
public let authorVerifiedImageView: UIImageView = {
|
|
|
|
let imageView = UIImageView()
|
|
|
|
imageView.setContentHuggingPriority(.required, for: .horizontal)
|
|
|
|
imageView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
|
|
|
|
imageView.setContentHuggingPriority(.required, for: .vertical)
|
|
|
|
imageView.contentMode = .scaleAspectFit
|
|
|
|
return imageView
|
|
|
|
}()
|
|
|
|
|
2023-04-21 15:03:52 +02:00
|
|
|
private let verifiedStackView: UIStackView = {
|
2023-04-20 16:28:30 +02:00
|
|
|
let stackView = UIStackView()
|
|
|
|
stackView.axis = .horizontal
|
|
|
|
stackView.alignment = .center
|
|
|
|
return stackView
|
|
|
|
}()
|
2023-04-21 15:03:52 +02:00
|
|
|
|
|
|
|
private let verifiedStackCenterSpacerView: UILabel = {
|
|
|
|
let label = UILabel()
|
|
|
|
label.text = " · "
|
|
|
|
label.textColor = .secondaryLabel
|
|
|
|
return label
|
|
|
|
}()
|
|
|
|
|
2023-05-05 14:26:36 +02:00
|
|
|
private let followButton: FollowButton = {
|
2023-04-25 10:54:10 +02:00
|
|
|
let button = FollowButton()
|
|
|
|
button.cornerRadius = 10
|
|
|
|
button.isHidden = true
|
|
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
button.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
|
button.setContentHuggingPriority(.required, for: .horizontal)
|
|
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
button.widthAnchor.constraint(equalToConstant: 96),
|
|
|
|
button.heightAnchor.constraint(equalToConstant: 36)
|
|
|
|
])
|
|
|
|
|
|
|
|
return button
|
|
|
|
}()
|
2023-04-25 12:48:53 +02:00
|
|
|
|
2022-01-27 14:23:39 +01:00
|
|
|
public func prepareForReuse() {
|
|
|
|
disposeBag.removeAll()
|
|
|
|
|
|
|
|
// viewModel.objects.removeAll()
|
|
|
|
viewModel.authorAvatarImageURL = nil
|
|
|
|
|
|
|
|
avatarButton.avatarImageView.cancelTask()
|
2023-04-25 12:48:53 +02:00
|
|
|
setButtonState(.none)
|
2022-01-27 14:23:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public override init(frame: CGRect) {
|
|
|
|
super.init(frame: frame)
|
|
|
|
_init()
|
|
|
|
}
|
|
|
|
|
|
|
|
public required init?(coder: NSCoder) {
|
|
|
|
super.init(coder: coder)
|
|
|
|
_init()
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
extension UserView {
|
|
|
|
|
|
|
|
private func _init() {
|
|
|
|
// container
|
|
|
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
addSubview(containerStackView)
|
2022-11-17 17:45:27 +01:00
|
|
|
containerStackView.pinToParent()
|
2022-01-27 14:23:39 +01:00
|
|
|
|
|
|
|
avatarButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
containerStackView.addArrangedSubview(avatarButton)
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
avatarButton.widthAnchor.constraint(equalToConstant: 28).priority(.required - 1),
|
|
|
|
avatarButton.heightAnchor.constraint(equalToConstant: 28).priority(.required - 1),
|
|
|
|
])
|
|
|
|
avatarButton.setContentHuggingPriority(.defaultLow, for: .vertical)
|
|
|
|
avatarButton.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
|
|
|
|
|
// label container
|
|
|
|
let labelStackView = UIStackView()
|
|
|
|
labelStackView.axis = .vertical
|
|
|
|
containerStackView.addArrangedSubview(labelStackView)
|
|
|
|
|
2023-04-25 10:54:10 +02:00
|
|
|
// follow button
|
|
|
|
containerStackView.addArrangedSubview(followButton)
|
|
|
|
|
2023-04-20 16:28:30 +02:00
|
|
|
let nameStackView = UIStackView()
|
|
|
|
nameStackView.axis = .horizontal
|
|
|
|
|
|
|
|
let nameSpacer = UIView()
|
|
|
|
nameSpacer.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
|
|
|
|
|
nameStackView.addArrangedSubview(authorNameLabel)
|
|
|
|
nameStackView.addArrangedSubview(authorUsernameLabel)
|
|
|
|
nameStackView.addArrangedSubview(nameSpacer)
|
|
|
|
|
2022-01-27 14:23:39 +01:00
|
|
|
authorNameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
2023-04-20 16:28:30 +02:00
|
|
|
authorNameLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
|
|
|
|
2022-01-27 14:23:39 +01:00
|
|
|
authorUsernameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
2023-04-25 10:53:41 +02:00
|
|
|
authorUsernameLabel.setContentCompressionResistancePriority(.defaultHigh - 1, for: .horizontal)
|
2022-01-27 14:23:39 +01:00
|
|
|
|
2023-04-20 16:28:30 +02:00
|
|
|
labelStackView.addArrangedSubview(nameStackView)
|
|
|
|
|
|
|
|
let verifiedSpacerView = UIView()
|
2023-04-21 15:03:52 +02:00
|
|
|
let verifiedStackTrailingSpacerView = UIView()
|
|
|
|
|
|
|
|
verifiedStackTrailingSpacerView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
|
|
|
|
|
|
|
let verifiedContainerStack = UIStackView()
|
|
|
|
verifiedContainerStack.axis = .horizontal
|
|
|
|
verifiedContainerStack.alignment = .center
|
2023-04-20 16:28:30 +02:00
|
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
authorVerifiedImageView.widthAnchor.constraint(equalToConstant: 15),
|
|
|
|
verifiedSpacerView.widthAnchor.constraint(equalToConstant: 2)
|
|
|
|
])
|
|
|
|
|
2023-04-21 15:03:52 +02:00
|
|
|
verifiedContainerStack.addArrangedSubview(authorVerifiedImageView)
|
|
|
|
verifiedContainerStack.addArrangedSubview(verifiedSpacerView)
|
|
|
|
verifiedContainerStack.addArrangedSubview(authorVerifiedLabel)
|
|
|
|
|
|
|
|
verifiedStackView.addArrangedSubview(authorFollowersLabel)
|
|
|
|
verifiedStackView.addArrangedSubview(verifiedStackCenterSpacerView)
|
|
|
|
verifiedStackView.addArrangedSubview(verifiedContainerStack)
|
|
|
|
verifiedStackView.addArrangedSubview(verifiedStackTrailingSpacerView)
|
2023-04-20 16:28:30 +02:00
|
|
|
|
|
|
|
labelStackView.addArrangedSubview(verifiedStackView)
|
|
|
|
|
2022-01-27 14:23:39 +01:00
|
|
|
avatarButton.isUserInteractionEnabled = false
|
|
|
|
authorNameLabel.isUserInteractionEnabled = false
|
|
|
|
authorUsernameLabel.isUserInteractionEnabled = false
|
2023-02-07 04:22:22 +01:00
|
|
|
|
|
|
|
isAccessibilityElement = true
|
2022-01-27 14:23:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2023-04-25 10:54:10 +02:00
|
|
|
|
|
|
|
private final class FollowButton: RoundedEdgesButton {
|
|
|
|
|
|
|
|
init() {
|
|
|
|
super.init(frame: .zero)
|
|
|
|
configureAppearance()
|
|
|
|
}
|
|
|
|
|
|
|
|
required init?(coder: NSCoder) {
|
|
|
|
fatalError("init(coder:) has not been implemented")
|
|
|
|
}
|
|
|
|
|
|
|
|
private func configureAppearance() {
|
|
|
|
setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal)
|
|
|
|
setTitleColor(Asset.Colors.Label.primaryReverse.color.withAlphaComponent(0.5), for: .highlighted)
|
|
|
|
switch traitCollection.userInterfaceStyle {
|
|
|
|
case .dark:
|
|
|
|
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundDark.color), for: .normal)
|
|
|
|
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedDark.color), for: .highlighted)
|
|
|
|
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedDark.color), for: .disabled)
|
|
|
|
default:
|
|
|
|
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundLight.color), for: .normal)
|
|
|
|
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .highlighted)
|
|
|
|
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .disabled)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-04-25 12:48:53 +02:00
|
|
|
|
|
|
|
public extension UserView {
|
|
|
|
private func prepareButtonStateLayout(for state: ButtonState) {
|
|
|
|
switch state {
|
|
|
|
case .none:
|
|
|
|
verifiedStackView.axis = .horizontal
|
|
|
|
verifiedStackView.alignment = .leading
|
|
|
|
verifiedStackCenterSpacerView.isHidden = false
|
|
|
|
followButton.isHidden = true
|
|
|
|
default:
|
|
|
|
verifiedStackView.axis = .vertical
|
|
|
|
verifiedStackView.alignment = .leading
|
|
|
|
verifiedStackCenterSpacerView.isHidden = true
|
|
|
|
followButton.isHidden = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc private func didTapButton() {
|
|
|
|
guard let user = viewModel.user else { return }
|
|
|
|
delegate?.userView(self, didTapButtonWith: currentButtonState, for: user)
|
|
|
|
}
|
|
|
|
|
|
|
|
func setButtonState(_ state: ButtonState) {
|
|
|
|
currentButtonState = state
|
|
|
|
prepareButtonStateLayout(for: state)
|
|
|
|
|
|
|
|
switch state {
|
|
|
|
case .follow:
|
|
|
|
followButton.setTitle(L10n.Common.Controls.Friendship.follow, for: .normal)
|
|
|
|
followButton.setBackgroundColor(Asset.Colors.Button.userFollow.color, for: .normal)
|
|
|
|
followButton.setTitleColor(.white, for: .normal)
|
|
|
|
case .unfollow:
|
|
|
|
followButton.setTitle(L10n.Common.Controls.Friendship.following, for: .normal)
|
|
|
|
followButton.setBackgroundColor(Asset.Colors.Button.userFollowing.color, for: .normal)
|
2023-05-04 11:56:34 +02:00
|
|
|
followButton.setTitleColor(Asset.Colors.Button.userFollowingTitle.color, for: .normal)
|
2023-04-25 12:48:53 +02:00
|
|
|
case .blocked:
|
|
|
|
followButton.setTitle(L10n.Common.Controls.Friendship.blocked, for: .normal)
|
|
|
|
followButton.setBackgroundColor(Asset.Colors.Button.userBlocked.color, for: .normal)
|
|
|
|
followButton.setTitleColor(.systemRed, for: .normal)
|
|
|
|
|
|
|
|
case .none:
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
followButton.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
|
|
|
|
followButton.titleLabel?.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .boldSystemFont(ofSize: 15))
|
|
|
|
}
|
|
|
|
}
|