Bubble/Threaded/Components/ProfileView.swift

460 lines
20 KiB
Swift

//Made by Lumaa
import SwiftUI
struct ProfileView: View {
@Environment(AccountManager.self) private var accountManager: AccountManager
@Environment(UniversalNavigator.self) private var uniNav: UniversalNavigator
@Environment(AppDelegate.self) private var appDelegate: AppDelegate
@EnvironmentObject private var navigator: Navigator
@Namespace var accountAnims
@Namespace var animPicture
@State private var biggerPicture: Bool = false
@State private var canFollow: Bool? = nil
@State private var initialFollowing: Bool = false
@State private var isFollowing: Bool = false
@State private var accountFollows: Bool = false
@State private var accountMuted: Bool = false
@State private var accountBlocked: Bool = false
@State private var instanceBlocked: Bool = false
@State private var loadingStatuses: Bool = false
@State private var statuses: [Status]? = []
@State private var statusesPinned: [Status]? = []
@State private var lastSeen: Int?
private let animPicCurve = Animation.smooth(duration: 0.25, extraBounce: 0.0)
@State public var account: Account
var isCurrent: Bool = false
var body: some View {
ZStack (alignment: .center) {
if account != Account.placeholder() {
if biggerPicture {
big
.navigationBarBackButtonHidden()
.toolbar(.hidden, for: .navigationBar)
} else {
wholeSmall
.offset(y: isCurrent ? 50 : 0)
.toolbar {
if !isCurrent {
ToolbarItem(placement: .primaryAction) {
Menu {
if accountMuted {
Button {
guard let client = accountManager.getClient() else { return }
Task {
do {
_ = try await client.post(endpoint: Accounts.unmute(id: account.id))
accountMuted = false
HapticManager.playHaptics(haptics: Haptic.success)
} catch {
print(error)
}
}
} label: {
Label("account.unmute", systemImage: "speaker.wave.2.fill")
}
} else {
Menu {
ForEach(MuteData.MuteDuration.allCases, id: \.self) { duration in
Button {
guard let client = accountManager.getClient() else { return }
Task {
do {
_ = try await client.post(endpoint: Accounts.mute(id: account.id, json: .init(duration: duration.rawValue)))
accountMuted = true
HapticManager.playHaptics(haptics: Haptic.success)
} catch {
print(error)
}
}
} label: {
Text(duration.description)
}
}
} label: {
Label("account.mute", systemImage: "speaker.slash")
}
}
Button(role: accountBlocked ? .cancel : .destructive) {
guard let client = accountManager.getClient() else { return }
Task {
do {
let endp: Endpoint = accountBlocked ? Accounts.unblock(id: account.id) : Accounts.block(id: account.id)
_ = try await client.post(endpoint: endp)
accountBlocked.toggle()
HapticManager.playHaptics(haptics: Haptic.success)
} catch {
print(error)
}
}
} label: {
Label(accountBlocked ? "account.unblock" : "account.block", systemImage: accountBlocked ? "person.fill.badge.plus" : "person.slash.fill")
}
} label: {
Image(systemName: "shield.righthalf.filled")
.font(.title2)
}
}
}
}
.overlay(alignment: .top) {
if isCurrent {
HStack {
Button {
navigator.navigate(to: .support)
} label: {
Image(systemName: "info.bubble")
.font(.title2)
}
Spacer() // middle seperation
Button {
navigator.navigate(to: .settings)
} label: {
Image(systemName: "text.alignright")
.font(.title2)
}
}
.tint(Color(uiColor: UIColor.label))
.safeAreaPadding()
.background(Color.appBackground)
}
}
}
} else {
loading
}
}
.task {
await reloadUser()
initialFollowing = isFollowing
}
.refreshable {
if isCurrent {
guard let client = accountManager.getClient() else { return }
if let acc: Account = try? await client.get(endpoint: Accounts.verifyCredentials) {
account = acc
}
}
await reloadUser()
}
.background(Color.appBackground)
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(Color.appBackground, for: .navigationBar)
.toolbarBackground(.automatic, for: .navigationBar)
}
// MARK: - Headers
var wholeSmall: some View {
ScrollView {
VStack {
VStack (alignment: .leading) {
if account.haveHeader {
AsyncImage(url: account.header, scale: 1.0) { image in
image
.resizable()
.scaledToFill()
.frame(width: appDelegate.windowWidth - 50, height: appDelegate.windowHeight / 5)
.clipped()
.clipShape(RoundedRectangle(cornerRadius: 15.0))
.padding(.bottom)
} placeholder: {
EmptyView()
}
.onTapGesture {
let attachment: MediaAttachment = .init(id: account.id, type: "image", url: account.header)
navigator.presentedCover = .media(attachments: [attachment], selected: attachment)
}
}
unbig
Text(account.note.asRawText)
.font(.callout)
.multilineTextAlignment(.leading)
.padding(.vertical, 5)
let followCount = (account.followersCount ?? 0 - (initialFollowing ? 1 : 0)) + (isFollowing ? 1 : 0)
Text("account.followers-\(followCount)")
.foregroundStyle(Color.gray)
.multilineTextAlignment(.leading)
.font(.callout)
if canFollow != nil && (canFollow ?? true) == true {
HStack (spacing: 5) {
Button {
Task {
await followAccount()
}
} label: {
HStack {
Spacer()
Text(isFollowing ? "account.unfollow" : accountFollows ? "account.follow-back" : "account.follow")
.font(.callout)
Spacer()
}
}
.buttonStyle(LargeButton(filled: true, height: 10))
Button {
if let server = account.acct.split(separator: "@").last {
uniNav.presentedSheet = .post(content: "@\(account.username)@\(server)")
} else {
let client = accountManager.getClient()
uniNav.presentedSheet = .post(content: "@\(account.username)@\(client?.server ?? "???")")
}
} label: {
HStack {
Spacer()
Text("account.mention")
.font(.callout)
Spacer()
}
}
.buttonStyle(LargeButton(filled: false, height: 10))
}
}
}
.padding(.horizontal)
VStack {
Rectangle()
.fill(Color.gray.opacity(0.2))
.frame(width: .infinity, height: 1)
.padding(.bottom, 3)
statusesList
}
}
.safeAreaPadding(.vertical)
.padding(.horizontal)
}
}
var statusesList: some View {
LazyVStack {
if loadingStatuses == false {
if !(statusesPinned?.isEmpty ?? true) {
ForEach(statusesPinned!, id: \.id) { status in
CompactPostView(status: status, pinned: true)
}
}
if !(statuses?.isEmpty ?? true) {
ForEach(statuses!, id: \.id) { status in
CompactPostView(status: status)
.onDisappear() {
lastSeen = statuses!.firstIndex(where: { $0.id == status.id })
}
}
} else {
ContentUnavailableView("account.no-statuses", systemImage: "pencil.slash")
}
} else {
ProgressView()
.progressViewStyle(.circular)
}
}
.onAppear {
if statuses == nil {
if let client = accountManager.getClient() {
Task {
loadingStatuses = true
statuses = try await client.get(endpoint: Accounts.statuses(id: account.id, sinceId: nil, tag: nil, onlyMedia: nil, excludeReplies: nil, pinned: nil))
statusesPinned = try await client.get(endpoint: Accounts.statuses(id: account.id, sinceId: nil, tag: nil, onlyMedia: nil, excludeReplies: nil, pinned: true))
loadingStatuses = false
}
}
}
}
.onChange(of: lastSeen ?? 0) { _, new in
guard statuses != nil && new >= statuses!.count - 6 && !loadingStatuses else { return }
if let client = accountManager.getClient(), let lastStatus = statuses!.last {
Task {
// loadingStatuses = true
if let newStatuses: [Status] = try await client.get(endpoint: Accounts.statuses(id: account.id, sinceId: lastStatus.id, tag: nil, onlyMedia: nil, excludeReplies: nil, pinned: nil)) {
statuses?.append(contentsOf: newStatuses)
}
// loadingStatuses = false
}
}
}
}
func followAccount() async {
if let client = accountManager.getClient() {
Task {
let endpoint: Endpoint = isFollowing ? Accounts.unfollow(id: account.id) : Accounts.follow(id: account.id, notify: false, reblogs: true)
HapticManager.playHaptics(haptics: Haptic.tap)
_ = try await client.post(endpoint: endpoint) // Notify off until APNs? | Reblogs on by default (later changeable)
isFollowing = !isFollowing
}
}
}
func reloadUser() async {
if let client = accountManager.getClient() {
let userAcc = accountManager.getAccount()
if let ref: Account = try? await client.get(endpoint: Accounts.accounts(id: isCurrent && userAcc != nil ? userAcc!.id : account.id)) {
account = ref
await updateRelationship()
loadingStatuses = true
statuses = try? await client.get(endpoint: Accounts.statuses(id: account.id, sinceId: nil, tag: nil, onlyMedia: nil, excludeReplies: nil, pinned: nil))
statusesPinned = try? await client.get(endpoint: Accounts.statuses(id: account.id, sinceId: nil, tag: nil, onlyMedia: nil, excludeReplies: nil, pinned: true))
loadingStatuses = false
}
}
}
func updateRelationship() async {
if let client = accountManager.getClient() {
if let currentAccount: Account = try? await client.get(endpoint: Accounts.verifyCredentials) {
canFollow = currentAccount.id != account.id
guard canFollow == true else { return }
if let relationship: [Relationship] = try? await client.get(endpoint: Accounts.relationships(ids: [account.id])) {
let rel: Relationship = relationship.first!
isFollowing = rel.following
accountFollows = rel.followedBy
accountMuted = rel.muting
accountBlocked = rel.blocking
}
} else {
canFollow = false
}
}
}
var loading: some View {
ScrollView {
VStack {
unbig
.redacted(reason: .placeholder)
HStack {
Text(account.note.asRawText)
.font(.body)
.multilineTextAlignment(.leading)
.redacted(reason: .placeholder)
Spacer()
}
}
.safeAreaPadding(.vertical)
.padding(.horizontal)
}
}
var unbig: some View {
HStack {
if account.displayName != nil {
VStack(alignment: .leading) {
Text(account.displayName!)
.font(.title2.bold())
.multilineTextAlignment(.leading)
.lineLimit(1)
let server = account.acct.split(separator: "@").last
let client = accountManager.getClient()
HStack(alignment: .center) {
if server != nil {
if server! != account.username {
Text("\(account.username)")
.font(.body)
.multilineTextAlignment(.leading)
Text("\(server!.description)")
.font(.caption)
.foregroundStyle(Color.gray)
.multilineTextAlignment(.leading)
.pill()
} else {
Text("\(account.username)")
.font(.body)
.multilineTextAlignment(.leading)
Text("\(client?.server ?? "???")")
.font(.caption)
.foregroundStyle(Color.gray)
.multilineTextAlignment(.leading)
.pill()
}
} else {
Text("\(account.username)")
.font(.body)
.multilineTextAlignment(.leading)
Text("\(client?.server ?? "???")")
.font(.caption)
.foregroundStyle(Color.gray)
.multilineTextAlignment(.leading)
.pill()
}
}
}
} else {
Text(account.acct)
.font(.headline)
}
Spacer()
profilePicture
.frame(width: 75, height: 75)
}
}
var big: some View {
ZStack (alignment: .center) {
Rectangle()
.fill(Material.ultraThin)
.ignoresSafeArea()
.onTapGesture {
withAnimation(animPicCurve) {
biggerPicture.toggle()
}
}
profilePicture
.frame(width: 300, height: 300)
}
.zIndex(20)
}
var profilePicture: some View {
ProfilePicture(url: account.avatar, size: biggerPicture ? 300 : 75)
.matchedGeometryEffect(id: animPicture, in: accountAnims)
.onTapGesture {
withAnimation(animPicCurve) {
biggerPicture.toggle()
}
}
}
}
private extension View {
func pill() -> some View {
self
.padding([.horizontal], 10)
.padding([.vertical], 5)
.background(Color(uiColor: UIColor.label).opacity(0.1))
.clipShape(.capsule)
}
}