2022-11-29 11:18:06 +00:00
|
|
|
import SwiftUI
|
|
|
|
import Models
|
|
|
|
import Network
|
2022-12-18 19:30:19 +00:00
|
|
|
import Status
|
|
|
|
import Shimmer
|
|
|
|
import DesignSystem
|
2022-12-22 09:53:36 +00:00
|
|
|
import Env
|
2022-11-29 11:18:06 +00:00
|
|
|
|
2022-12-21 07:35:26 +00:00
|
|
|
public struct AccountDetailView: View {
|
2022-12-20 19:33:45 +00:00
|
|
|
@Environment(\.redactionReasons) private var reasons
|
2022-12-26 07:47:41 +00:00
|
|
|
@EnvironmentObject private var watcher: StreamWatcher
|
|
|
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
2022-12-24 14:09:17 +00:00
|
|
|
@EnvironmentObject private var theme: Theme
|
2022-11-29 11:18:06 +00:00
|
|
|
@EnvironmentObject private var client: Client
|
2022-12-21 08:42:05 +00:00
|
|
|
@EnvironmentObject private var routeurPath: RouterPath
|
|
|
|
|
2022-11-29 11:18:06 +00:00
|
|
|
@StateObject private var viewModel: AccountDetailViewModel
|
2022-12-20 08:37:07 +00:00
|
|
|
@State private var scrollOffset: CGFloat = 0
|
2022-12-21 19:53:23 +00:00
|
|
|
@State private var isFieldsSheetDisplayed: Bool = false
|
2022-12-27 12:49:54 +00:00
|
|
|
@State private var isCurrentUser: Bool = false
|
2022-12-20 15:08:09 +00:00
|
|
|
|
2022-12-21 08:42:05 +00:00
|
|
|
/// When coming from a URL like a mention tap in a status.
|
2022-11-29 11:18:06 +00:00
|
|
|
public init(accountId: String) {
|
|
|
|
_viewModel = StateObject(wrappedValue: .init(accountId: accountId))
|
|
|
|
}
|
|
|
|
|
2022-12-21 08:42:05 +00:00
|
|
|
/// When the account is already fetched by the parent caller.
|
2022-12-27 12:49:54 +00:00
|
|
|
public init(account: Account) {
|
|
|
|
_viewModel = StateObject(wrappedValue: .init(account: account))
|
2022-12-17 12:37:46 +00:00
|
|
|
}
|
|
|
|
|
2022-11-29 11:18:06 +00:00
|
|
|
public var body: some View {
|
2022-12-27 08:11:12 +00:00
|
|
|
ScrollViewReader { proxy in
|
|
|
|
ScrollViewOffsetReader { offset in
|
|
|
|
self.scrollOffset = offset
|
|
|
|
} content: {
|
|
|
|
LazyVStack(alignment: .leading) {
|
|
|
|
makeHeaderView(proxy: proxy)
|
|
|
|
familliarFollowers
|
|
|
|
.offset(y: -36)
|
|
|
|
featuredTagsView
|
|
|
|
.offset(y: -36)
|
|
|
|
Group {
|
|
|
|
if isCurrentUser {
|
|
|
|
Picker("", selection: $viewModel.selectedTab) {
|
|
|
|
ForEach(AccountDetailViewModel.Tab.allCases, id: \.self) { tab in
|
|
|
|
Text(tab.title).tag(tab)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.pickerStyle(.segmented)
|
|
|
|
.padding(.horizontal, DS.Constants.layoutPadding)
|
|
|
|
.offset(y: -20)
|
|
|
|
} else {
|
|
|
|
Divider()
|
|
|
|
.offset(y: -20)
|
2022-12-21 07:35:26 +00:00
|
|
|
}
|
|
|
|
}
|
2022-12-27 08:11:12 +00:00
|
|
|
.id("status")
|
|
|
|
|
|
|
|
switch viewModel.tabState {
|
|
|
|
case .statuses:
|
|
|
|
StatusesListView(fetcher: viewModel)
|
|
|
|
case let .followedTags(tags):
|
|
|
|
makeTagsListView(tags: tags)
|
|
|
|
}
|
2022-12-21 08:42:05 +00:00
|
|
|
}
|
2022-11-29 11:18:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
.task {
|
2022-12-20 19:33:45 +00:00
|
|
|
guard reasons != .placeholder else { return }
|
2022-12-27 12:49:54 +00:00
|
|
|
isCurrentUser = currentAccount.account?.id == viewModel.accountId
|
|
|
|
viewModel.isCurrentUser = isCurrentUser
|
2022-11-29 11:18:06 +00:00
|
|
|
viewModel.client = client
|
|
|
|
await viewModel.fetchAccount()
|
2022-12-20 15:08:09 +00:00
|
|
|
if viewModel.statuses.isEmpty {
|
|
|
|
await viewModel.fetchStatuses()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.refreshable {
|
|
|
|
Task {
|
|
|
|
await viewModel.fetchAccount()
|
|
|
|
await viewModel.fetchStatuses()
|
|
|
|
}
|
2022-12-18 19:30:19 +00:00
|
|
|
}
|
2022-12-26 07:47:41 +00:00
|
|
|
.onChange(of: watcher.latestEvent?.id) { id in
|
|
|
|
if let latestEvent = watcher.latestEvent,
|
|
|
|
viewModel.accountId == currentAccount.account?.id {
|
|
|
|
viewModel.handleEvent(event: latestEvent, currentAccount: currentAccount)
|
|
|
|
}
|
|
|
|
}
|
2022-12-20 08:37:07 +00:00
|
|
|
.edgesIgnoringSafeArea(.top)
|
2022-12-23 09:41:55 +00:00
|
|
|
.navigationTitle(Text(scrollOffset < -200 ? viewModel.title : ""))
|
2022-12-18 19:30:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@ViewBuilder
|
2022-12-27 08:11:12 +00:00
|
|
|
private func makeHeaderView(proxy: ScrollViewProxy?) -> some View {
|
2022-12-21 08:42:05 +00:00
|
|
|
switch viewModel.accountState {
|
2022-12-18 19:30:19 +00:00
|
|
|
case .loading:
|
2022-12-20 16:11:12 +00:00
|
|
|
AccountDetailHeaderView(isCurrentUser: isCurrentUser,
|
|
|
|
account: .placeholder(),
|
2022-12-23 15:21:31 +00:00
|
|
|
relationship: .placeholder(),
|
2022-12-27 08:11:12 +00:00
|
|
|
scrollViewProxy: proxy,
|
2022-12-21 11:47:07 +00:00
|
|
|
scrollOffset: $scrollOffset)
|
2022-12-18 19:30:19 +00:00
|
|
|
.redacted(reason: .placeholder)
|
|
|
|
case let .data(account):
|
2022-12-20 16:11:12 +00:00
|
|
|
AccountDetailHeaderView(isCurrentUser: isCurrentUser,
|
|
|
|
account: account,
|
2022-12-23 15:21:31 +00:00
|
|
|
relationship: viewModel.relationship,
|
2022-12-27 08:11:12 +00:00
|
|
|
scrollViewProxy: proxy,
|
2022-12-21 11:47:07 +00:00
|
|
|
scrollOffset: $scrollOffset)
|
2022-12-18 19:30:19 +00:00
|
|
|
case let .error(error):
|
|
|
|
Text("Error: \(error.localizedDescription)")
|
|
|
|
}
|
|
|
|
}
|
2022-12-21 19:53:23 +00:00
|
|
|
|
2022-12-21 19:26:38 +00:00
|
|
|
@ViewBuilder
|
|
|
|
private var featuredTagsView: some View {
|
2022-12-21 19:53:23 +00:00
|
|
|
if !viewModel.featuredTags.isEmpty || !viewModel.fields.isEmpty {
|
2022-12-21 19:26:38 +00:00
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
|
|
HStack(spacing: 4) {
|
2022-12-21 19:53:23 +00:00
|
|
|
if !viewModel.fields.isEmpty {
|
2022-12-21 19:26:38 +00:00
|
|
|
Button {
|
2022-12-21 19:53:23 +00:00
|
|
|
isFieldsSheetDisplayed.toggle()
|
2022-12-21 19:26:38 +00:00
|
|
|
} label: {
|
|
|
|
VStack(alignment: .leading, spacing: 0) {
|
2022-12-21 19:53:23 +00:00
|
|
|
Text("About")
|
2022-12-21 19:26:38 +00:00
|
|
|
.font(.callout)
|
2022-12-21 19:53:23 +00:00
|
|
|
Text("\(viewModel.fields.count) fields")
|
2022-12-21 19:26:38 +00:00
|
|
|
.font(.caption2)
|
|
|
|
}
|
2022-12-21 19:53:23 +00:00
|
|
|
}
|
|
|
|
.buttonStyle(.bordered)
|
|
|
|
.sheet(isPresented: $isFieldsSheetDisplayed) {
|
|
|
|
fieldSheetView
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !viewModel.featuredTags.isEmpty {
|
|
|
|
ForEach(viewModel.featuredTags) { tag in
|
|
|
|
Button {
|
|
|
|
routeurPath.navigate(to: .hashTag(tag: tag.name, account: viewModel.accountId))
|
|
|
|
} label: {
|
|
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
|
|
Text("#\(tag.name)")
|
|
|
|
.font(.callout)
|
|
|
|
Text("\(tag.statusesCount) posts")
|
|
|
|
.font(.caption2)
|
|
|
|
}
|
|
|
|
}.buttonStyle(.bordered)
|
|
|
|
}
|
2022-12-21 19:26:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
.padding(.leading, DS.Constants.layoutPadding)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-22 11:26:11 +00:00
|
|
|
@ViewBuilder
|
|
|
|
private var familliarFollowers: some View {
|
|
|
|
if !viewModel.familliarFollowers.isEmpty {
|
2022-12-23 15:21:31 +00:00
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
2022-12-22 11:26:11 +00:00
|
|
|
Text("Also followed by")
|
|
|
|
.font(.headline)
|
|
|
|
.padding(.leading, DS.Constants.layoutPadding)
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
|
|
LazyHStack(spacing: 0) {
|
|
|
|
ForEach(viewModel.familliarFollowers) { account in
|
|
|
|
AvatarView(url: account.avatar, size: .badge)
|
|
|
|
.onTapGesture {
|
|
|
|
routeurPath.navigate(to: .accountDetailWithAccount(account: account))
|
|
|
|
}
|
|
|
|
.padding(.leading, -4)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.padding(.leading, DS.Constants.layoutPadding + 4)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.padding(.top, 2)
|
|
|
|
.padding(.bottom, 12)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-21 19:53:23 +00:00
|
|
|
private var fieldSheetView: some View {
|
|
|
|
NavigationStack {
|
|
|
|
List {
|
|
|
|
ForEach(viewModel.fields) { field in
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
|
|
Text(field.name)
|
|
|
|
.font(.headline)
|
|
|
|
HStack {
|
|
|
|
if field.verifiedAt != nil {
|
|
|
|
Image(systemName: "checkmark.seal")
|
|
|
|
.foregroundColor(Color.green.opacity(0.80))
|
|
|
|
}
|
|
|
|
Text(field.value.asSafeAttributedString)
|
2022-12-24 14:09:17 +00:00
|
|
|
.foregroundColor(theme.tintColor)
|
2022-12-21 19:53:23 +00:00
|
|
|
}
|
|
|
|
.font(.body)
|
|
|
|
}
|
|
|
|
.listRowBackground(field.verifiedAt != nil ? Color.green.opacity(0.15) : nil)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.navigationTitle("About")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-21 08:42:05 +00:00
|
|
|
private func makeTagsListView(tags: [Tag]) -> some View {
|
|
|
|
Group {
|
|
|
|
ForEach(tags) { tag in
|
2022-12-23 09:41:55 +00:00
|
|
|
TagRowView(tag: tag)
|
|
|
|
.padding(.horizontal, DS.Constants.layoutPadding)
|
|
|
|
.padding(.vertical, 8)
|
2022-12-21 08:42:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-12-17 12:37:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
struct AccountDetailView_Previews: PreviewProvider {
|
|
|
|
static var previews: some View {
|
|
|
|
AccountDetailView(account: .placeholder())
|
2022-11-29 11:18:06 +00:00
|
|
|
}
|
|
|
|
}
|
2022-12-17 12:37:46 +00:00
|
|
|
|