Account header progress

This commit is contained in:
Justin Mazzocchi 2020-11-09 22:27:08 -08:00
parent a4b94bf33c
commit 67e1a59ffd
No known key found for this signature in database
GPG Key ID: E223E6937AAFB01C
6 changed files with 252 additions and 6 deletions

View File

@ -1,5 +1,6 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
"account.field.verified" = "Verified %@";
"account.statuses" = "Posts"; "account.statuses" = "Posts";
"account.statuses-and-replies" = "Posts & Replies"; "account.statuses-and-replies" = "Posts & Replies";
"account.media" = "Media"; "account.media" = "Media";
@ -32,6 +33,7 @@
"lists.new-list-title" = "New List Title"; "lists.new-list-title" = "New List Title";
"load-more" = "Load More"; "load-more" = "Load More";
"messages" = "Messages"; "messages" = "Messages";
"ok" = "OK";
"pending.pending-confirmation" = "Your account is pending confirmation"; "pending.pending-confirmation" = "Your account is pending confirmation";
"preferences" = "Preferences"; "preferences" = "Preferences";
"preferences.app" = "App Preferences"; "preferences.app" = "App Preferences";

View File

@ -12,6 +12,7 @@
D00702312555F4AE00F38136 /* ConversationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00702302555F4AE00F38136 /* ConversationView.swift */; }; D00702312555F4AE00F38136 /* ConversationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00702302555F4AE00F38136 /* ConversationView.swift */; };
D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */; }; D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */; };
D007023E25562A2800F38136 /* ConversationAvatarsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D007023D25562A2800F38136 /* ConversationAvatarsView.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 */; }; D00CB2ED2533ACC00080096B /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00CB2EC2533ACC00080096B /* StatusView.swift */; };
D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C6FAB252024BD003D0300 /* Array+Extensions.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 */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; D01EF22325182B1F00650C6B /* AccountHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountHeaderView.swift; sourceTree = "<group>"; };
@ -342,6 +344,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D0F0B112251A86A000942152 /* AccountContentConfiguration.swift */, D0F0B112251A86A000942152 /* AccountContentConfiguration.swift */,
D0070251255921B100F38136 /* AccountFieldView.swift */,
D01EF22325182B1F00650C6B /* AccountHeaderView.swift */, D01EF22325182B1F00650C6B /* AccountHeaderView.swift */,
D0F0B125251A90F400942152 /* AccountListCell.swift */, D0F0B125251A90F400942152 /* AccountListCell.swift */,
D0F0B10D251A868200942152 /* AccountView.swift */, D0F0B10D251A868200942152 /* AccountView.swift */,
@ -674,6 +677,7 @@
D00702312555F4AE00F38136 /* ConversationView.swift in Sources */, D00702312555F4AE00F38136 /* ConversationView.swift in Sources */,
D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */, D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */,
D08B8D4A253FC36500B1EBEF /* ImageNavigationController.swift in Sources */, D08B8D4A253FC36500B1EBEF /* ImageNavigationController.swift in Sources */,
D0070252255921B100F38136 /* AccountFieldView.swift in Sources */,
D0030982250C6C8500EACB32 /* URL+Extensions.swift in Sources */, D0030982250C6C8500EACB32 /* URL+Extensions.swift in Sources */,
D00CB2ED2533ACC00080096B /* StatusView.swift in Sources */, D00CB2ED2533ACC00080096B /* StatusView.swift in Sources */,
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */, D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */,

View File

@ -34,6 +34,8 @@ public extension AccountViewModel {
var accountName: String { "@".appending(accountService.account.acct) } var accountName: String { "@".appending(accountService.account.acct) }
var fields: [Account.Field] { accountService.account.fields }
var note: NSAttributedString { accountService.account.note.attributed } var note: NSAttributedString { accountService.account.note.attributed }
var emoji: [Emoji] { accountService.account.emojis } var emoji: [Emoji] { accountService.account.emojis }

View File

@ -49,6 +49,12 @@ public extension ProfileViewModel {
imagePresentationsSubject.send(accountViewModel.headerURL) imagePresentationsSubject.send(accountViewModel.headerURL)
} }
func presentAvatar() {
guard let accountViewModel = accountViewModel else { return }
imagePresentationsSubject.send(accountViewModel.avatarURL(profile: true))
}
} }
extension ProfileViewModel: CollectionViewModel { extension ProfileViewModel: CollectionViewModel {

View File

@ -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
}()
}

View File

@ -7,6 +7,11 @@ import ViewModels
final class AccountHeaderView: UIView { final class AccountHeaderView: UIView {
let headerImageView = AnimatedImageView() let headerImageView = AnimatedImageView()
let headerButton = UIButton() let headerButton = UIButton()
let avatarImageView = UIImageView()
let avatarButton = UIButton()
let displayNameLabel = UILabel()
let accountLabel = UILabel()
let fieldsStackView = UIStackView()
let noteTextView = TouchFallthroughTextView() let noteTextView = TouchFallthroughTextView()
let segmentedControl = UISegmentedControl() let segmentedControl = UISegmentedControl()
@ -15,6 +20,35 @@ final class AccountHeaderView: UIView {
if let accountViewModel = viewModel?.accountViewModel { if let accountViewModel = viewModel?.accountViewModel {
headerImageView.kf.setImage(with: accountViewModel.headerURL) headerImageView.kf.setImage(with: accountViewModel.headerURL)
headerImageView.tag = accountViewModel.headerURL.hashValue 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 noteFont = UIFont.preferredFont(forTextStyle: .callout)
let mutableNote = NSMutableAttributedString(attributedString: accountViewModel.note) let mutableNote = NSMutableAttributedString(attributedString: accountViewModel.note)
@ -64,6 +98,8 @@ extension AccountHeaderView: UITextViewDelegate {
} }
private extension AccountHeaderView { private extension AccountHeaderView {
static let avatarDimension = CGFloat.avatarDimension * 2
// swiftlint:disable:next function_body_length // swiftlint:disable:next function_body_length
func initialSetup() { func initialSetup() {
let baseStackView = UIStackView() let baseStackView = UIStackView()
@ -78,17 +114,51 @@ private extension AccountHeaderView {
headerButton.translatesAutoresizingMaskIntoConstraints = false headerButton.translatesAutoresizingMaskIntoConstraints = false
headerButton.setBackgroundImage(.highlightedButtonBackground, for: .highlighted) headerButton.setBackgroundImage(.highlightedButtonBackground, for: .highlighted)
headerButton.addAction( headerButton.addAction(UIAction { [weak self] _ in self?.viewModel?.presentHeader() }, for: .touchUpInside)
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) addSubview(baseStackView)
baseStackView.translatesAutoresizingMaskIntoConstraints = false baseStackView.translatesAutoresizingMaskIntoConstraints = false
baseStackView.axis = .vertical 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.isScrollEnabled = false
noteTextView.delegate = self noteTextView.delegate = self
baseStackView.addArrangedSubview(noteTextView)
for (index, collection) in ProfileCollection.allCases.enumerated() { for (index, collection) in ProfileCollection.allCases.enumerated() {
segmentedControl.insertSegment( segmentedControl.insertSegment(
@ -119,10 +189,18 @@ private extension AccountHeaderView {
headerButton.topAnchor.constraint(equalTo: headerImageView.topAnchor), headerButton.topAnchor.constraint(equalTo: headerImageView.topAnchor),
headerButton.bottomAnchor.constraint(equalTo: headerImageView.bottomAnchor), headerButton.bottomAnchor.constraint(equalTo: headerImageView.bottomAnchor),
headerButton.trailingAnchor.constraint(equalTo: headerImageView.trailingAnchor), 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.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
baseStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), baseStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
baseStackView.bottomAnchor.constraint(equalTo: bottomAnchor) baseStackView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor)
]) ])
} }
} }