Support edit profile

This commit is contained in:
Thomas Ricouard 2023-01-10 08:24:05 +01:00
parent be4b61ed30
commit 71ec57f915
8 changed files with 270 additions and 3 deletions

View file

@ -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
}
}
}
}

View file

@ -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: {

View 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")
}
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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] {

View file

@ -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")
}

View file

@ -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
}

View file

@ -1,6 +1,10 @@
import Models
extension Visibility {
public static var supportDefault: [Visibility] {
[.pub, .priv, .unlisted]
}
public var iconName: String {
switch self {
case .pub: