mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-12-22 23:26:37 +00:00
Support edit profile
This commit is contained in:
parent
be4b61ed30
commit
71ec57f915
8 changed files with 270 additions and 3 deletions
|
@ -2,6 +2,7 @@ import SwiftUI
|
|||
import Env
|
||||
import DesignSystem
|
||||
import RevenueCat
|
||||
import Shimmer
|
||||
|
||||
struct SupportAppView: View {
|
||||
enum Tips: String, CaseIterable {
|
||||
|
@ -67,7 +68,18 @@ struct SupportAppView: View {
|
|||
|
||||
Section {
|
||||
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 {
|
||||
ForEach(products, id: \.productIdentifier) { product in
|
||||
let tip = Tips(productId: product.productIdentifier)
|
||||
|
@ -123,7 +135,9 @@ struct SupportAppView: View {
|
|||
loadingProducts = true
|
||||
Purchases.shared.getProducts(Tips.allCases.map{ $0.productId }) { products in
|
||||
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 isCreateListAlertPresented: Bool = false
|
||||
@State private var createListTitle: String = ""
|
||||
@State private var isEditingAccount: Bool = false
|
||||
|
||||
/// When coming from a URL like a mention tap in a status.
|
||||
public init(accountId: String) {
|
||||
|
@ -98,6 +99,17 @@ public struct AccountDetailView: View {
|
|||
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)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
|
@ -356,9 +368,20 @@ public struct AccountDetailView: View {
|
|||
Label("Add/Remove from lists", systemImage: "list.bullet")
|
||||
}
|
||||
}
|
||||
|
||||
if let url = account.url {
|
||||
ShareLink(item: url)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
if isCurrentUser {
|
||||
Button {
|
||||
isEditingAccount = true
|
||||
} label: {
|
||||
Label("Edit Info", systemImage: "pencil")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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 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 username: String
|
||||
public let displayName: String
|
||||
|
@ -31,6 +40,9 @@ public struct Account: Codable, Identifiable, Equatable, Hashable {
|
|||
public let locked: Bool
|
||||
public let emojis: [Emoji]
|
||||
public let url: URL?
|
||||
public let source: Source?
|
||||
public let bot: Bool
|
||||
public let discoverable: Bool?
|
||||
|
||||
public static func placeholder() -> Account {
|
||||
.init(id: UUID().uuidString,
|
||||
|
@ -48,7 +60,10 @@ public struct Account: Codable, Identifiable, Equatable, Hashable {
|
|||
fields: [],
|
||||
locked: false,
|
||||
emojis: [],
|
||||
url: nil)
|
||||
url: nil,
|
||||
source: nil,
|
||||
bot: false,
|
||||
discoverable: true)
|
||||
}
|
||||
|
||||
public static func placeholders() -> [Account] {
|
||||
|
|
|
@ -93,6 +93,13 @@ public class Client: ObservableObject, Equatable {
|
|||
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 {
|
||||
try await makeEntityRequest(endpoint: endpoint, method: "PUT")
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Foundation
|
||||
import Models
|
||||
|
||||
public enum Accounts: Endpoint {
|
||||
case accounts(id: String)
|
||||
|
@ -7,6 +8,13 @@ public enum Accounts: Endpoint {
|
|||
case followedTags
|
||||
case featuredTags(id: String)
|
||||
case verifyCredentials
|
||||
case updateCredentials(displayName: String,
|
||||
note: String,
|
||||
privacy: Visibility,
|
||||
isSensitive: Bool,
|
||||
isBot: Bool,
|
||||
isLocked: Bool,
|
||||
isDiscoverable: Bool)
|
||||
case statuses(id: String,
|
||||
sinceId: String?,
|
||||
tag: String?,
|
||||
|
@ -37,6 +45,8 @@ public enum Accounts: Endpoint {
|
|||
return "accounts/\(id)/featured_tags"
|
||||
case .verifyCredentials:
|
||||
return "accounts/verify_credentials"
|
||||
case .updateCredentials:
|
||||
return "accounts/update_credentials"
|
||||
case .statuses(let id, _, _, _, _, _):
|
||||
return "accounts/\(id)/statuses"
|
||||
case .relationships:
|
||||
|
@ -96,6 +106,17 @@ public enum Accounts: Endpoint {
|
|||
case let .bookmarks(sinceId):
|
||||
guard let sinceId else { return nil }
|
||||
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:
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import Models
|
||||
|
||||
extension Visibility {
|
||||
public static var supportDefault: [Visibility] {
|
||||
[.pub, .priv, .unlisted]
|
||||
}
|
||||
|
||||
public var iconName: String {
|
||||
switch self {
|
||||
case .pub:
|
||||
|
|
Loading…
Reference in a new issue