IceCubesApp/Packages/Account/Sources/Account/AccountDetailHeaderView.swift

305 lines
9.2 KiB
Swift
Raw Normal View History

2022-12-18 19:30:19 +00:00
import DesignSystem
2023-01-17 10:36:01 +00:00
import EmojiText
2022-12-22 09:53:36 +00:00
import Env
2023-01-17 10:36:01 +00:00
import Models
2022-12-25 06:43:02 +00:00
import NukeUI
2023-01-17 10:36:01 +00:00
import Shimmer
import SwiftUI
2022-12-17 12:37:46 +00:00
struct AccountDetailHeaderView: View {
2023-02-12 15:13:57 +00:00
enum Constants {
static let headerHeight: CGFloat = 200
}
2023-02-12 15:29:41 +00:00
2022-12-24 14:09:17 +00:00
@EnvironmentObject private var theme: Theme
2022-12-22 09:56:24 +00:00
@EnvironmentObject private var quickLook: QuickLook
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var currentAccount: CurrentAccount
2022-12-17 12:37:46 +00:00
@Environment(\.redactionReasons) private var reasons
@Environment(\.isSupporter) private var isSupporter: Bool
2023-01-17 10:36:01 +00:00
@ObservedObject var viewModel: AccountDetailViewModel
2022-12-20 08:37:07 +00:00
let account: Account
2022-12-27 08:11:12 +00:00
let scrollViewProxy: ScrollViewProxy?
2023-01-17 10:36:01 +00:00
2022-12-17 12:37:46 +00:00
var body: some View {
VStack(alignment: .leading) {
ZStack(alignment: .bottomTrailing) {
Rectangle()
.frame(height: Constants.headerHeight)
.overlay {
headerImageView
}
if viewModel.relationship?.followedBy == true {
Text("account.relation.follows-you")
.font(.scaledFootnote)
.fontWeight(.semibold)
.padding(4)
.background(.ultraThinMaterial)
.cornerRadius(4)
.padding(8)
}
}
2022-12-17 12:37:46 +00:00
accountInfoView
}
}
2023-01-17 10:36:01 +00:00
2022-12-17 12:37:46 +00:00
private var headerImageView: some View {
2023-01-19 07:41:45 +00:00
ZStack(alignment: .bottomTrailing) {
if reasons.contains(.placeholder) {
Rectangle()
.foregroundColor(theme.secondaryBackgroundColor)
2023-02-12 15:13:57 +00:00
.frame(height: Constants.headerHeight)
2023-01-19 07:41:45 +00:00
} else {
LazyImage(url: account.header) { state in
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.overlay(account.haveHeader ? .black.opacity(0.50) : .clear)
.frame(height: Constants.headerHeight)
.clipped()
2023-01-19 07:41:45 +00:00
} else if state.isLoading {
theme.secondaryBackgroundColor
2023-02-12 15:13:57 +00:00
.frame(height: Constants.headerHeight)
2023-01-19 07:41:45 +00:00
.shimmering()
} else {
theme.secondaryBackgroundColor
2023-02-12 15:13:57 +00:00
.frame(height: Constants.headerHeight)
2023-01-19 07:41:45 +00:00
}
2022-12-25 06:43:02 +00:00
}
2023-02-12 15:13:57 +00:00
.frame(height: Constants.headerHeight)
2023-01-19 07:41:45 +00:00
}
2022-12-20 08:37:07 +00:00
}
.background(theme.secondaryBackgroundColor)
2023-02-12 15:13:57 +00:00
.frame(height: Constants.headerHeight)
2022-12-20 08:37:07 +00:00
.contentShape(Rectangle())
.onTapGesture {
guard account.haveHeader else {
return
}
2022-12-22 09:56:24 +00:00
Task {
await quickLook.prepareFor(urls: [account.header], selectedURL: account.header)
}
2022-12-20 08:37:07 +00:00
}
2022-12-17 12:37:46 +00:00
}
2023-01-17 10:36:01 +00:00
2022-12-17 12:37:46 +00:00
private var accountAvatarView: some View {
HStack {
ZStack(alignment: .topTrailing) {
AvatarView(url: account.avatar, size: .account)
.onTapGesture {
guard account.haveAvatar else {
return
}
Task {
await quickLook.prepareFor(urls: [account.avatar], selectedURL: account.avatar)
}
2023-01-17 10:36:01 +00:00
}
if viewModel.isCurrentUser, isSupporter {
Image(systemName: "checkmark.seal.fill")
.resizable()
.frame(width: 25, height: 25)
.foregroundColor(theme.tintColor)
.offset(x: 10, y: -10)
2022-12-22 09:56:24 +00:00
}
}
2022-12-17 12:37:46 +00:00
Spacer()
Group {
2022-12-27 08:11:12 +00:00
Button {
withAnimation {
scrollViewProxy?.scrollTo("status", anchor: .top)
}
} label: {
makeCustomInfoLabel(title: "account.posts", count: account.statusesCount)
2022-12-27 08:11:12 +00:00
}
2023-02-12 15:13:57 +00:00
.buttonStyle(.borderless)
2023-02-12 15:29:41 +00:00
2023-02-12 15:13:57 +00:00
Button {
routerPath.navigate(to: .following(id: account.id))
} label: {
makeCustomInfoLabel(title: "account.following", count: account.followingCount)
2022-12-23 17:47:19 +00:00
}
2023-02-12 15:13:57 +00:00
.buttonStyle(.borderless)
Button {
routerPath.navigate(to: .followers(id: account.id))
} label: {
makeCustomInfoLabel(
title: "account.followers",
count: account.followersCount,
needsBadge: currentAccount.account?.id == account.id && !currentAccount.followRequests.isEmpty
)
2022-12-23 17:47:19 +00:00
}
2023-02-12 15:13:57 +00:00
.buttonStyle(.borderless)
2023-02-12 15:29:41 +00:00
2022-12-17 12:37:46 +00:00
}.offset(y: 20)
}
}
2023-01-17 10:36:01 +00:00
2022-12-17 12:37:46 +00:00
private var accountInfoView: some View {
Group {
accountAvatarView
HStack(alignment: .firstTextBaseline) {
2022-12-20 16:11:12 +00:00
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .center, spacing: 2) {
EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis)
.font(.scaledHeadline)
.emojiSize(Font.scaledHeadlinePointSize)
if account.bot {
2023-02-26 16:33:16 +00:00
Text(Image(systemName: "poweroutlet.type.b.fill"))
.font(.footnote)
}
if account.locked {
Text(Image(systemName: "lock.fill"))
.font(.footnote)
}
if viewModel.relationship?.blocking == true {
Text(Image(systemName: "person.crop.circle.badge.xmark.fill"))
.font(.footnote)
}
if viewModel.relationship?.muting == true {
Text(Image(systemName: "speaker.slash.fill"))
.font(.footnote)
}
}
2022-12-24 12:41:25 +00:00
Text("@\(account.acct)")
.font(.scaledCallout)
2023-01-17 10:36:01 +00:00
.foregroundColor(.gray)
.textSelection(.enabled)
joinedAtView
2022-12-20 16:11:12 +00:00
}
Spacer()
if let relationship = viewModel.relationship, !viewModel.isCurrentUser {
HStack {
FollowButton(viewModel: .init(accountId: account.id,
relationship: relationship,
shouldDisplayNotify: true,
relationshipUpdated: { relationship in
2023-01-22 05:38:30 +00:00
viewModel.relationship = relationship
}))
}
2022-12-20 16:11:12 +00:00
}
}
2023-02-21 06:23:42 +00:00
if let note = viewModel.relationship?.note, !note.isEmpty,
2023-02-21 06:23:42 +00:00
!viewModel.isCurrentUser
{
makeNoteView(note)
}
EmojiTextApp(account.note, emojis: account.emojis)
.font(.scaledBody)
2023-02-22 21:13:46 +00:00
.emojiSize(Font.scaledBodyPointSize)
2022-12-17 12:37:46 +00:00
.padding(.top, 8)
.textSelection(.enabled)
.environment(\.openURL, OpenURLAction { url in
routerPath.handle(url: url)
})
2023-02-21 06:23:42 +00:00
2023-02-19 10:35:46 +00:00
fieldsView
2022-12-17 12:37:46 +00:00
}
.padding(.horizontal, .layoutPadding)
2022-12-17 12:37:46 +00:00
.offset(y: -40)
}
2023-01-17 10:36:01 +00:00
private func makeCustomInfoLabel(title: LocalizedStringKey, count: Int, needsBadge: Bool = false) -> some View {
2022-12-17 12:37:46 +00:00
VStack {
Text(count, format: .number.notation(.compactName))
.font(.scaledHeadline)
2022-12-24 14:09:17 +00:00
.foregroundColor(theme.tintColor)
.overlay(alignment: .trailing) {
if needsBadge {
Circle()
.fill(Color.red)
.frame(width: 9, height: 9)
.offset(x: 12)
}
}
2022-12-17 12:37:46 +00:00
Text(title)
.font(.scaledFootnote)
2022-12-17 12:37:46 +00:00
.foregroundColor(.gray)
}
}
@ViewBuilder
private var joinedAtView: some View {
if let joinedAt = viewModel.account?.createdAt.asDate {
HStack(spacing: 4) {
Image(systemName: "calendar")
Text("account.joined")
Text(joinedAt, style: .date)
}
.foregroundColor(.gray)
.font(.footnote)
.padding(.top, 6)
}
}
2023-02-21 06:23:42 +00:00
@ViewBuilder
private func makeNoteView(_ note: String) -> some View {
VStack(alignment: .leading, spacing: 4) {
Text("account.relation.note.label")
.foregroundColor(.gray)
Text(note)
2023-02-18 20:25:45 +00:00
.frame(maxWidth: .infinity, alignment: .leading)
.padding(8)
.background(theme.secondaryBackgroundColor)
.cornerRadius(4)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(.gray.opacity(0.35), lineWidth: 1)
)
}
}
2023-02-21 06:23:42 +00:00
2023-02-19 10:35:46 +00:00
@ViewBuilder
private var fieldsView: some View {
if !viewModel.fields.isEmpty {
VStack(alignment: .leading) {
ForEach(viewModel.fields) { field in
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(field.name)
.font(.scaledHeadline)
HStack {
if field.verifiedAt != nil {
Image(systemName: "checkmark.seal")
.foregroundColor(Color.green.opacity(0.80))
}
EmojiTextApp(field.value, emojis: viewModel.account?.emojis ?? [])
2023-02-22 21:13:46 +00:00
.emojiSize(Font.scaledBodyPointSize)
2023-02-19 10:35:46 +00:00
.foregroundColor(theme.tintColor)
.environment(\.openURL, OpenURLAction { url in
routerPath.handle(url: url)
2023-02-19 10:35:46 +00:00
})
}
.font(.scaledBody)
if viewModel.fields.last != field {
Divider()
.padding(.vertical, 4)
}
}
Spacer()
}
}
}
.padding(8)
.background(theme.secondaryBackgroundColor)
.cornerRadius(4)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(.gray.opacity(0.35), lineWidth: 1)
)
}
}
2022-12-17 12:37:46 +00:00
}
struct AccountDetailHeaderView_Previews: PreviewProvider {
static var previews: some View {
AccountDetailHeaderView(viewModel: .init(account: .placeholder()),
2022-12-20 16:11:12 +00:00
account: .placeholder(),
2023-02-12 15:13:57 +00:00
scrollViewProxy: nil)
2022-12-17 12:37:46 +00:00
}
}