Account header progress
This commit is contained in:
parent
a4b94bf33c
commit
67e1a59ffd
|
@ -1,5 +1,6 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
"account.field.verified" = "Verified %@";
|
||||
"account.statuses" = "Posts";
|
||||
"account.statuses-and-replies" = "Posts & Replies";
|
||||
"account.media" = "Media";
|
||||
|
@ -32,6 +33,7 @@
|
|||
"lists.new-list-title" = "New List Title";
|
||||
"load-more" = "Load More";
|
||||
"messages" = "Messages";
|
||||
"ok" = "OK";
|
||||
"pending.pending-confirmation" = "Your account is pending confirmation";
|
||||
"preferences" = "Preferences";
|
||||
"preferences.app" = "App Preferences";
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
D00702312555F4AE00F38136 /* ConversationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00702302555F4AE00F38136 /* ConversationView.swift */; };
|
||||
D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */; };
|
||||
D007023E25562A2800F38136 /* ConversationAvatarsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D007023D25562A2800F38136 /* ConversationAvatarsView.swift */; };
|
||||
D0070252255921B100F38136 /* AccountFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0070251255921B100F38136 /* AccountFieldView.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 */; };
|
||||
|
@ -124,6 +125,7 @@
|
|||
D00702302555F4AE00F38136 /* ConversationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationView.swift; sourceTree = "<group>"; };
|
||||
D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationContentConfiguration.swift; sourceTree = "<group>"; };
|
||||
D007023D25562A2800F38136 /* ConversationAvatarsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationAvatarsView.swift; sourceTree = "<group>"; };
|
||||
D0070251255921B100F38136 /* AccountFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFieldView.swift; sourceTree = "<group>"; };
|
||||
D00CB2EC2533ACC00080096B /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
|
||||
D01C6FAB252024BD003D0300 /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D01EF22325182B1F00650C6B /* AccountHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountHeaderView.swift; sourceTree = "<group>"; };
|
||||
|
@ -342,6 +344,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D0F0B112251A86A000942152 /* AccountContentConfiguration.swift */,
|
||||
D0070251255921B100F38136 /* AccountFieldView.swift */,
|
||||
D01EF22325182B1F00650C6B /* AccountHeaderView.swift */,
|
||||
D0F0B125251A90F400942152 /* AccountListCell.swift */,
|
||||
D0F0B10D251A868200942152 /* AccountView.swift */,
|
||||
|
@ -674,6 +677,7 @@
|
|||
D00702312555F4AE00F38136 /* ConversationView.swift in Sources */,
|
||||
D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */,
|
||||
D08B8D4A253FC36500B1EBEF /* ImageNavigationController.swift in Sources */,
|
||||
D0070252255921B100F38136 /* AccountFieldView.swift in Sources */,
|
||||
D0030982250C6C8500EACB32 /* URL+Extensions.swift in Sources */,
|
||||
D00CB2ED2533ACC00080096B /* StatusView.swift in Sources */,
|
||||
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */,
|
||||
|
|
|
@ -34,6 +34,8 @@ public extension AccountViewModel {
|
|||
|
||||
var accountName: String { "@".appending(accountService.account.acct) }
|
||||
|
||||
var fields: [Account.Field] { accountService.account.fields }
|
||||
|
||||
var note: NSAttributedString { accountService.account.note.attributed }
|
||||
|
||||
var emoji: [Emoji] { accountService.account.emojis }
|
||||
|
|
|
@ -49,6 +49,12 @@ public extension ProfileViewModel {
|
|||
|
||||
imagePresentationsSubject.send(accountViewModel.headerURL)
|
||||
}
|
||||
|
||||
func presentAvatar() {
|
||||
guard let accountViewModel = accountViewModel else { return }
|
||||
|
||||
imagePresentationsSubject.send(accountViewModel.avatarURL(profile: true))
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileViewModel: CollectionViewModel {
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Mastodon
|
||||
import UIKit
|
||||
|
||||
final class AccountFieldView: UIView {
|
||||
let nameLabel = UILabel()
|
||||
let valueTextView = TouchFallthroughTextView()
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
init(field: Account.Field, emoji: [Emoji]) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
let verified = field.verifiedAt != nil
|
||||
|
||||
backgroundColor = .systemBackground
|
||||
|
||||
let nameBackgroundView = UIView()
|
||||
|
||||
addSubview(nameBackgroundView)
|
||||
nameBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
nameBackgroundView.backgroundColor = .secondarySystemBackground
|
||||
|
||||
let valueBackgroundView = UIView()
|
||||
|
||||
addSubview(valueBackgroundView)
|
||||
valueBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
valueBackgroundView.backgroundColor = verified
|
||||
? UIColor.systemGreen.withAlphaComponent(0.25)
|
||||
: .systemBackground
|
||||
|
||||
addSubview(nameLabel)
|
||||
nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
nameLabel.numberOfLines = 0
|
||||
nameLabel.font = .preferredFont(forTextStyle: .headline)
|
||||
nameLabel.textAlignment = .center
|
||||
nameLabel.textColor = .secondaryLabel
|
||||
|
||||
let mutableName = NSMutableAttributedString(string: field.name)
|
||||
|
||||
mutableName.insert(emoji: emoji, view: nameLabel)
|
||||
mutableName.resizeAttachments(toLineHeight: nameLabel.font.lineHeight)
|
||||
nameLabel.attributedText = mutableName
|
||||
|
||||
let dividerView = UIView()
|
||||
|
||||
addSubview(dividerView)
|
||||
dividerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
dividerView.backgroundColor = .separator
|
||||
|
||||
addSubview(valueTextView)
|
||||
valueTextView.translatesAutoresizingMaskIntoConstraints = false
|
||||
valueTextView.isScrollEnabled = false
|
||||
valueTextView.backgroundColor = .clear
|
||||
|
||||
if verified {
|
||||
valueTextView.linkTextAttributes = [
|
||||
.foregroundColor: UIColor.systemGreen as Any,
|
||||
.underlineColor: UIColor.clear]
|
||||
}
|
||||
|
||||
let valueFont = UIFont.preferredFont(forTextStyle: verified ? .headline : .body)
|
||||
let mutableValue = NSMutableAttributedString(attributedString: field.value.attributed)
|
||||
let valueRange = NSRange(location: 0, length: mutableValue.length)
|
||||
|
||||
mutableValue.removeAttribute(.font, range: valueRange)
|
||||
mutableValue.addAttributes(
|
||||
[.font: valueFont as Any,
|
||||
.foregroundColor: UIColor.label],
|
||||
range: valueRange)
|
||||
mutableValue.insert(emoji: emoji, view: valueTextView)
|
||||
mutableValue.resizeAttachments(toLineHeight: valueFont.lineHeight)
|
||||
|
||||
valueTextView.attributedText = mutableValue
|
||||
valueTextView.textAlignment = .center
|
||||
|
||||
let checkButton = UIButton()
|
||||
|
||||
checkButton.setImage(
|
||||
UIImage(
|
||||
systemName: "checkmark",
|
||||
withConfiguration: UIImage.SymbolConfiguration(scale: .small)),
|
||||
for: .normal)
|
||||
|
||||
addSubview(checkButton)
|
||||
checkButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
checkButton.tintColor = .systemGreen
|
||||
checkButton.isHidden = !verified
|
||||
checkButton.showsMenuAsPrimaryAction = true
|
||||
|
||||
if let verifiedAt = field.verifiedAt {
|
||||
checkButton.menu = UIMenu(
|
||||
title: String.localizedStringWithFormat(
|
||||
NSLocalizedString("account.field.verified", comment: ""),
|
||||
Self.dateFormatter.string(from: verifiedAt)),
|
||||
options: .displayInline,
|
||||
children: [UIAction(title: NSLocalizedString("ok", comment: "")) { _ in }])
|
||||
}
|
||||
|
||||
let nameLabelBottomConstraint = nameLabel.bottomAnchor.constraint(
|
||||
equalTo: bottomAnchor,
|
||||
constant: -.defaultSpacing)
|
||||
let valueTextViewBottomConstraint = valueTextView.bottomAnchor.constraint(
|
||||
lessThanOrEqualTo: bottomAnchor,
|
||||
constant: -.defaultSpacing)
|
||||
|
||||
for constraint in [nameLabelBottomConstraint, valueTextViewBottomConstraint] {
|
||||
constraint.priority = .justBelowMax
|
||||
}
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
nameLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: .defaultSpacing),
|
||||
nameLabel.topAnchor.constraint(equalTo: topAnchor, constant: .defaultSpacing),
|
||||
nameLabelBottomConstraint,
|
||||
dividerView.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: .defaultSpacing),
|
||||
dividerView.topAnchor.constraint(equalTo: topAnchor),
|
||||
dividerView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
dividerView.widthAnchor.constraint(equalToConstant: .hairline),
|
||||
checkButton.leadingAnchor.constraint(equalTo: dividerView.trailingAnchor, constant: .defaultSpacing),
|
||||
valueTextView.leadingAnchor.constraint(
|
||||
equalTo: verified ? checkButton.trailingAnchor : dividerView.trailingAnchor,
|
||||
constant: .defaultSpacing),
|
||||
valueTextView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: .defaultSpacing),
|
||||
valueTextView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -.defaultSpacing),
|
||||
valueTextViewBottomConstraint,
|
||||
nameLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1 / 3),
|
||||
checkButton.centerYAnchor.constraint(equalTo: valueTextView.centerYAnchor),
|
||||
valueTextView.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor),
|
||||
nameBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
nameBackgroundView.topAnchor.constraint(equalTo: topAnchor),
|
||||
nameBackgroundView.trailingAnchor.constraint(equalTo: dividerView.leadingAnchor),
|
||||
nameBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
valueBackgroundView.leadingAnchor.constraint(equalTo: dividerView.trailingAnchor),
|
||||
valueBackgroundView.topAnchor.constraint(equalTo: topAnchor),
|
||||
valueBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
valueBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
private extension AccountFieldView {
|
||||
static let dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
|
||||
formatter.dateStyle = .full
|
||||
|
||||
return formatter
|
||||
}()
|
||||
}
|
|
@ -7,6 +7,11 @@ import ViewModels
|
|||
final class AccountHeaderView: UIView {
|
||||
let headerImageView = AnimatedImageView()
|
||||
let headerButton = UIButton()
|
||||
let avatarImageView = UIImageView()
|
||||
let avatarButton = UIButton()
|
||||
let displayNameLabel = UILabel()
|
||||
let accountLabel = UILabel()
|
||||
let fieldsStackView = UIStackView()
|
||||
let noteTextView = TouchFallthroughTextView()
|
||||
let segmentedControl = UISegmentedControl()
|
||||
|
||||
|
@ -15,6 +20,35 @@ final class AccountHeaderView: UIView {
|
|||
if let accountViewModel = viewModel?.accountViewModel {
|
||||
headerImageView.kf.setImage(with: accountViewModel.headerURL)
|
||||
headerImageView.tag = accountViewModel.headerURL.hashValue
|
||||
avatarImageView.kf.setImage(with: accountViewModel.avatarURL(profile: true))
|
||||
avatarImageView.tag = accountViewModel.avatarURL(profile: true).hashValue
|
||||
|
||||
if accountViewModel.displayName.isEmpty {
|
||||
displayNameLabel.isHidden = true
|
||||
} else {
|
||||
let mutableDisplayName = NSMutableAttributedString(string: accountViewModel.displayName)
|
||||
|
||||
mutableDisplayName.insert(emoji: accountViewModel.emoji, view: displayNameLabel)
|
||||
mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight)
|
||||
displayNameLabel.attributedText = mutableDisplayName
|
||||
}
|
||||
|
||||
accountLabel.text = accountViewModel.accountName
|
||||
|
||||
for view in fieldsStackView.arrangedSubviews {
|
||||
fieldsStackView.removeArrangedSubview(view)
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
|
||||
for field in accountViewModel.fields {
|
||||
let fieldView = AccountFieldView(field: field, emoji: accountViewModel.emoji)
|
||||
|
||||
fieldView.valueTextView.delegate = self
|
||||
|
||||
fieldsStackView.addArrangedSubview(fieldView)
|
||||
}
|
||||
|
||||
fieldsStackView.isHidden = accountViewModel.fields.isEmpty
|
||||
|
||||
let noteFont = UIFont.preferredFont(forTextStyle: .callout)
|
||||
let mutableNote = NSMutableAttributedString(attributedString: accountViewModel.note)
|
||||
|
@ -64,6 +98,8 @@ extension AccountHeaderView: UITextViewDelegate {
|
|||
}
|
||||
|
||||
private extension AccountHeaderView {
|
||||
static let avatarDimension = CGFloat.avatarDimension * 2
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
func initialSetup() {
|
||||
let baseStackView = UIStackView()
|
||||
|
@ -78,17 +114,51 @@ private extension AccountHeaderView {
|
|||
headerButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
headerButton.setBackgroundImage(.highlightedButtonBackground, for: .highlighted)
|
||||
|
||||
headerButton.addAction(
|
||||
UIAction { [weak self] _ in self?.viewModel?.presentHeader() },
|
||||
for: .touchUpInside)
|
||||
headerButton.addAction(UIAction { [weak self] _ in self?.viewModel?.presentHeader() }, for: .touchUpInside)
|
||||
|
||||
addSubview(avatarImageView)
|
||||
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarImageView.contentMode = .scaleAspectFill
|
||||
avatarImageView.clipsToBounds = true
|
||||
avatarImageView.isUserInteractionEnabled = true
|
||||
avatarImageView.layer.cornerRadius = Self.avatarDimension / 2
|
||||
avatarImageView.layer.borderWidth = .compactSpacing
|
||||
avatarImageView.layer.borderColor = UIColor.systemBackground.cgColor
|
||||
|
||||
avatarImageView.addSubview(avatarButton)
|
||||
avatarButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarButton.setBackgroundImage(.highlightedButtonBackground, for: .highlighted)
|
||||
|
||||
avatarButton.addAction(UIAction { [weak self] _ in self?.viewModel?.presentAvatar() }, for: .touchUpInside)
|
||||
|
||||
addSubview(baseStackView)
|
||||
baseStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
baseStackView.axis = .vertical
|
||||
baseStackView.spacing = .defaultSpacing
|
||||
|
||||
baseStackView.addArrangedSubview(displayNameLabel)
|
||||
displayNameLabel.numberOfLines = 0
|
||||
displayNameLabel.font = .preferredFont(forTextStyle: .headline)
|
||||
displayNameLabel.adjustsFontForContentSizeCategory = true
|
||||
|
||||
baseStackView.addArrangedSubview(accountLabel)
|
||||
accountLabel.numberOfLines = 0
|
||||
accountLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||
accountLabel.adjustsFontForContentSizeCategory = true
|
||||
accountLabel.textColor = .secondaryLabel
|
||||
|
||||
baseStackView.addArrangedSubview(fieldsStackView)
|
||||
fieldsStackView.axis = .vertical
|
||||
fieldsStackView.spacing = .hairline
|
||||
fieldsStackView.backgroundColor = .separator
|
||||
fieldsStackView.clipsToBounds = true
|
||||
fieldsStackView.layer.borderColor = UIColor.separator.cgColor
|
||||
fieldsStackView.layer.borderWidth = .hairline
|
||||
fieldsStackView.layer.cornerRadius = .defaultCornerRadius
|
||||
|
||||
baseStackView.addArrangedSubview(noteTextView)
|
||||
noteTextView.isScrollEnabled = false
|
||||
noteTextView.delegate = self
|
||||
baseStackView.addArrangedSubview(noteTextView)
|
||||
|
||||
for (index, collection) in ProfileCollection.allCases.enumerated() {
|
||||
segmentedControl.insertSegment(
|
||||
|
@ -119,10 +189,18 @@ private extension AccountHeaderView {
|
|||
headerButton.topAnchor.constraint(equalTo: headerImageView.topAnchor),
|
||||
headerButton.bottomAnchor.constraint(equalTo: headerImageView.bottomAnchor),
|
||||
headerButton.trailingAnchor.constraint(equalTo: headerImageView.trailingAnchor),
|
||||
baseStackView.topAnchor.constraint(equalTo: headerImageView.bottomAnchor),
|
||||
avatarImageView.heightAnchor.constraint(equalToConstant: Self.avatarDimension),
|
||||
avatarImageView.widthAnchor.constraint(equalToConstant: Self.avatarDimension),
|
||||
avatarImageView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||
avatarImageView.centerYAnchor.constraint(equalTo: headerImageView.bottomAnchor),
|
||||
avatarButton.leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor),
|
||||
avatarButton.topAnchor.constraint(equalTo: avatarImageView.topAnchor),
|
||||
avatarButton.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor),
|
||||
avatarButton.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor),
|
||||
baseStackView.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: .defaultSpacing),
|
||||
baseStackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||
baseStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||
baseStackView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
baseStackView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue