Support edit profile
This commit is contained in:
parent
be4b61ed30
commit
71ec57f915
@ -2,6 +2,7 @@ import SwiftUI
|
|||||||
import Env
|
import Env
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import RevenueCat
|
import RevenueCat
|
||||||
|
import Shimmer
|
||||||
|
|
||||||
struct SupportAppView: View {
|
struct SupportAppView: View {
|
||||||
enum Tips: String, CaseIterable {
|
enum Tips: String, CaseIterable {
|
||||||
@ -67,7 +68,18 @@ struct SupportAppView: View {
|
|||||||
|
|
||||||
Section {
|
Section {
|
||||||
if loadingProducts {
|
if loadingProducts {
|
||||||
ProgressView()
|
HStack {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Loading ...")
|
||||||
|
.font(.subheadline)
|
||||||
|
Text("Loading subtitle...")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
.redacted(reason: .placeholder)
|
||||||
|
.shimmering()
|
||||||
} else {
|
} else {
|
||||||
ForEach(products, id: \.productIdentifier) { product in
|
ForEach(products, id: \.productIdentifier) { product in
|
||||||
let tip = Tips(productId: product.productIdentifier)
|
let tip = Tips(productId: product.productIdentifier)
|
||||||
@ -123,7 +135,9 @@ struct SupportAppView: View {
|
|||||||
loadingProducts = true
|
loadingProducts = true
|
||||||
Purchases.shared.getProducts(Tips.allCases.map{ $0.productId }) { products in
|
Purchases.shared.getProducts(Tips.allCases.map{ $0.productId }) { products in
|
||||||
self.products = products.sorted(by: { $0.price < $1.price })
|
self.products = products.sorted(by: { $0.price < $1.price })
|
||||||
loadingProducts = false
|
withAnimation {
|
||||||
|
loadingProducts = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ public struct AccountDetailView: View {
|
|||||||
@State private var isCurrentUser: Bool = false
|
@State private var isCurrentUser: Bool = false
|
||||||
@State private var isCreateListAlertPresented: Bool = false
|
@State private var isCreateListAlertPresented: Bool = false
|
||||||
@State private var createListTitle: String = ""
|
@State private var createListTitle: String = ""
|
||||||
|
@State private var isEditingAccount: Bool = false
|
||||||
|
|
||||||
/// When coming from a URL like a mention tap in a status.
|
/// When coming from a URL like a mention tap in a status.
|
||||||
public init(accountId: String) {
|
public init(accountId: String) {
|
||||||
@ -98,6 +99,17 @@ public struct AccountDetailView: View {
|
|||||||
viewModel.handleEvent(event: latestEvent, currentAccount: currentAccount)
|
viewModel.handleEvent(event: latestEvent, currentAccount: currentAccount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: isEditingAccount, perform: { isEditing in
|
||||||
|
if !isEditing {
|
||||||
|
Task {
|
||||||
|
await viewModel.fetchAccount()
|
||||||
|
await preferences.refreshServerPreferences()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sheet(isPresented: $isEditingAccount, content: {
|
||||||
|
EditAccountView()
|
||||||
|
})
|
||||||
.edgesIgnoringSafeArea(.top)
|
.edgesIgnoringSafeArea(.top)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
@ -356,9 +368,20 @@ public struct AccountDetailView: View {
|
|||||||
Label("Add/Remove from lists", systemImage: "list.bullet")
|
Label("Add/Remove from lists", systemImage: "list.bullet")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let url = account.url {
|
if let url = account.url {
|
||||||
ShareLink(item: url)
|
ShareLink(item: url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
if isCurrentUser {
|
||||||
|
Button {
|
||||||
|
isEditingAccount = true
|
||||||
|
} label: {
|
||||||
|
Label("Edit Info", systemImage: "pencil")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
|
122
Packages/Account/Sources/Account/Edit/EditAccountView.swift
Normal file
122
Packages/Account/Sources/Account/Edit/EditAccountView.swift
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
import DesignSystem
|
||||||
|
|
||||||
|
struct EditAccountView: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@EnvironmentObject private var client: Client
|
||||||
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
|
@StateObject private var viewModel = EditAccountViewModel()
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
if viewModel.isLoading {
|
||||||
|
loadingSection
|
||||||
|
} else {
|
||||||
|
aboutSections
|
||||||
|
postSettingsSection
|
||||||
|
accountSection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
.navigationTitle("Edit Profile")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
toolbarContent
|
||||||
|
}
|
||||||
|
.alert("Error while saving your profile",
|
||||||
|
isPresented: $viewModel.saveError,
|
||||||
|
actions: {
|
||||||
|
Button("Ok", action: { })
|
||||||
|
}, message: { Text("Error while saving your profile, please try again.") })
|
||||||
|
.task {
|
||||||
|
viewModel.client = client
|
||||||
|
await viewModel.fetchAccount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var loadingSection: some View {
|
||||||
|
Section {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
ProgressView()
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var aboutSections: some View {
|
||||||
|
Section("Display Name") {
|
||||||
|
TextField("Display Name", text: $viewModel.displayName)
|
||||||
|
}
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
Section("About") {
|
||||||
|
TextField("About", text: $viewModel.note, axis: .vertical)
|
||||||
|
.frame(maxHeight: 150)
|
||||||
|
}
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var postSettingsSection: some View {
|
||||||
|
Section("Post settings") {
|
||||||
|
Picker(selection: $viewModel.postPrivacy) {
|
||||||
|
ForEach(Models.Visibility.supportDefault, id: \.rawValue) { privacy in
|
||||||
|
Text(privacy.title).tag(privacy)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Default privacy", systemImage: "lock")
|
||||||
|
}
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
Toggle(isOn: $viewModel.isSensitive) {
|
||||||
|
Label("Sensitive content", systemImage: "eye")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var accountSection: some View {
|
||||||
|
Section("Account settings") {
|
||||||
|
Toggle(isOn: $viewModel.isLocked) {
|
||||||
|
Label("Private", systemImage: "lock")
|
||||||
|
}
|
||||||
|
Toggle(isOn: $viewModel.isBot) {
|
||||||
|
Label("Bot account", systemImage: "laptopcomputer.trianglebadge.exclamationmark")
|
||||||
|
}
|
||||||
|
Toggle(isOn: $viewModel.isDiscoverable) {
|
||||||
|
Label("Discoverable", systemImage: "magnifyingglass")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ToolbarContentBuilder
|
||||||
|
private var toolbarContent: some ToolbarContent {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button("Cancel") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
await viewModel.save()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
if viewModel.isSaving {
|
||||||
|
ProgressView()
|
||||||
|
} else {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class EditAccountViewModel: ObservableObject {
|
||||||
|
public var client: Client?
|
||||||
|
|
||||||
|
@Published var displayName: String = ""
|
||||||
|
@Published var note: String = ""
|
||||||
|
@Published var postPrivacy = Models.Visibility.pub
|
||||||
|
@Published var isSensitive: Bool = false
|
||||||
|
@Published var isBot: Bool = false
|
||||||
|
@Published var isLocked: Bool = false
|
||||||
|
@Published var isDiscoverable: Bool = false
|
||||||
|
|
||||||
|
@Published var isLoading: Bool = true
|
||||||
|
@Published var isSaving: Bool = false
|
||||||
|
@Published var saveError: Bool = false
|
||||||
|
|
||||||
|
init() { }
|
||||||
|
|
||||||
|
func fetchAccount() async {
|
||||||
|
guard let client else { return }
|
||||||
|
do {
|
||||||
|
let account: Account = try await client.get(endpoint: Accounts.verifyCredentials)
|
||||||
|
displayName = account.displayName
|
||||||
|
note = account.source?.note ?? ""
|
||||||
|
postPrivacy = account.source?.privacy ?? .pub
|
||||||
|
isSensitive = account.source?.sensitive ?? false
|
||||||
|
isBot = account.bot
|
||||||
|
isLocked = account.locked
|
||||||
|
isDiscoverable = account.discoverable ?? false
|
||||||
|
withAnimation {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
func save() async {
|
||||||
|
isSaving = true
|
||||||
|
do {
|
||||||
|
let response =
|
||||||
|
try await client?.patch(endpoint: Accounts.updateCredentials(displayName: displayName,
|
||||||
|
note: note,
|
||||||
|
privacy: postPrivacy,
|
||||||
|
isSensitive: isSensitive,
|
||||||
|
isBot: isBot,
|
||||||
|
isLocked: isLocked,
|
||||||
|
isDiscoverable: isDiscoverable))
|
||||||
|
if response?.statusCode != 200 {
|
||||||
|
saveError = true
|
||||||
|
}
|
||||||
|
isSaving = false
|
||||||
|
} catch {
|
||||||
|
isSaving = false
|
||||||
|
saveError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,6 +15,15 @@ public struct Account: Codable, Identifiable, Equatable, Hashable {
|
|||||||
public let value: HTMLString
|
public let value: HTMLString
|
||||||
public let verifiedAt: String?
|
public let verifiedAt: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct Source: Codable, Equatable {
|
||||||
|
public let privacy: Visibility
|
||||||
|
public let sensitive: Bool
|
||||||
|
public let language: String?
|
||||||
|
public let note: String
|
||||||
|
public let fields: [Field]
|
||||||
|
}
|
||||||
|
|
||||||
public let id: String
|
public let id: String
|
||||||
public let username: String
|
public let username: String
|
||||||
public let displayName: String
|
public let displayName: String
|
||||||
@ -31,6 +40,9 @@ public struct Account: Codable, Identifiable, Equatable, Hashable {
|
|||||||
public let locked: Bool
|
public let locked: Bool
|
||||||
public let emojis: [Emoji]
|
public let emojis: [Emoji]
|
||||||
public let url: URL?
|
public let url: URL?
|
||||||
|
public let source: Source?
|
||||||
|
public let bot: Bool
|
||||||
|
public let discoverable: Bool?
|
||||||
|
|
||||||
public static func placeholder() -> Account {
|
public static func placeholder() -> Account {
|
||||||
.init(id: UUID().uuidString,
|
.init(id: UUID().uuidString,
|
||||||
@ -48,7 +60,10 @@ public struct Account: Codable, Identifiable, Equatable, Hashable {
|
|||||||
fields: [],
|
fields: [],
|
||||||
locked: false,
|
locked: false,
|
||||||
emojis: [],
|
emojis: [],
|
||||||
url: nil)
|
url: nil,
|
||||||
|
source: nil,
|
||||||
|
bot: false,
|
||||||
|
discoverable: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func placeholders() -> [Account] {
|
public static func placeholders() -> [Account] {
|
||||||
|
@ -93,6 +93,13 @@ public class Client: ObservableObject, Equatable {
|
|||||||
return httpResponse as? HTTPURLResponse
|
return httpResponse as? HTTPURLResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func patch(endpoint: Endpoint) async throws -> HTTPURLResponse? {
|
||||||
|
let url = makeURL(endpoint: endpoint)
|
||||||
|
let request = makeURLRequest(url: url, httpMethod: "PATCH")
|
||||||
|
let (_, httpResponse) = try await urlSession.data(for: request)
|
||||||
|
return httpResponse as? HTTPURLResponse
|
||||||
|
}
|
||||||
|
|
||||||
public func put<Entity: Decodable>(endpoint: Endpoint) async throws -> Entity {
|
public func put<Entity: Decodable>(endpoint: Endpoint) async throws -> Entity {
|
||||||
try await makeEntityRequest(endpoint: endpoint, method: "PUT")
|
try await makeEntityRequest(endpoint: endpoint, method: "PUT")
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import Models
|
||||||
|
|
||||||
public enum Accounts: Endpoint {
|
public enum Accounts: Endpoint {
|
||||||
case accounts(id: String)
|
case accounts(id: String)
|
||||||
@ -7,6 +8,13 @@ public enum Accounts: Endpoint {
|
|||||||
case followedTags
|
case followedTags
|
||||||
case featuredTags(id: String)
|
case featuredTags(id: String)
|
||||||
case verifyCredentials
|
case verifyCredentials
|
||||||
|
case updateCredentials(displayName: String,
|
||||||
|
note: String,
|
||||||
|
privacy: Visibility,
|
||||||
|
isSensitive: Bool,
|
||||||
|
isBot: Bool,
|
||||||
|
isLocked: Bool,
|
||||||
|
isDiscoverable: Bool)
|
||||||
case statuses(id: String,
|
case statuses(id: String,
|
||||||
sinceId: String?,
|
sinceId: String?,
|
||||||
tag: String?,
|
tag: String?,
|
||||||
@ -37,6 +45,8 @@ public enum Accounts: Endpoint {
|
|||||||
return "accounts/\(id)/featured_tags"
|
return "accounts/\(id)/featured_tags"
|
||||||
case .verifyCredentials:
|
case .verifyCredentials:
|
||||||
return "accounts/verify_credentials"
|
return "accounts/verify_credentials"
|
||||||
|
case .updateCredentials:
|
||||||
|
return "accounts/update_credentials"
|
||||||
case .statuses(let id, _, _, _, _, _):
|
case .statuses(let id, _, _, _, _, _):
|
||||||
return "accounts/\(id)/statuses"
|
return "accounts/\(id)/statuses"
|
||||||
case .relationships:
|
case .relationships:
|
||||||
@ -96,6 +106,17 @@ public enum Accounts: Endpoint {
|
|||||||
case let .bookmarks(sinceId):
|
case let .bookmarks(sinceId):
|
||||||
guard let sinceId else { return nil }
|
guard let sinceId else { return nil }
|
||||||
return [.init(name: "max_id", value: sinceId)]
|
return [.init(name: "max_id", value: sinceId)]
|
||||||
|
case let .updateCredentials(displayName, note, privacy,
|
||||||
|
isSensitive, isBot, isLocked, isDiscoverable):
|
||||||
|
var params: [URLQueryItem] = []
|
||||||
|
params.append(.init(name: "display_name", value: displayName))
|
||||||
|
params.append(.init(name: "note", value: note))
|
||||||
|
params.append(.init(name: "source[privacy]", value: privacy.rawValue))
|
||||||
|
params.append(.init(name: "source[sensitive]", value: isSensitive ? "true" : "false"))
|
||||||
|
params.append(.init(name: "bot", value: isBot ? "true" : "false"))
|
||||||
|
params.append(.init(name: "locked", value: isLocked ? "true" : "false"))
|
||||||
|
params.append(.init(name: "discoverable", value: isDiscoverable ? "true" : "false"))
|
||||||
|
return params
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import Models
|
import Models
|
||||||
|
|
||||||
extension Visibility {
|
extension Visibility {
|
||||||
|
public static var supportDefault: [Visibility] {
|
||||||
|
[.pub, .priv, .unlisted]
|
||||||
|
}
|
||||||
|
|
||||||
public var iconName: String {
|
public var iconName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .pub:
|
case .pub:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user