From 71ec57f91523e0ac868b587720802f9332ab3b00 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Tue, 10 Jan 2023 08:24:05 +0100 Subject: [PATCH] Support edit profile --- .../App/Tabs/Settings/SupportAppView.swift | 18 ++- .../Sources/Account/AccountDetailView.swift | 23 ++++ .../Account/Edit/EditAccountView.swift | 122 ++++++++++++++++++ .../Account/Edit/EditAccountViewModel.swift | 61 +++++++++ Packages/Models/Sources/Models/Account.swift | 17 ++- Packages/Network/Sources/Network/Client.swift | 7 + .../Sources/Network/Endpoint/Accounts.swift | 21 +++ .../Sources/Status/Ext/Visibility.swift | 4 + 8 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 Packages/Account/Sources/Account/Edit/EditAccountView.swift create mode 100644 Packages/Account/Sources/Account/Edit/EditAccountViewModel.swift diff --git a/IceCubesApp/App/Tabs/Settings/SupportAppView.swift b/IceCubesApp/App/Tabs/Settings/SupportAppView.swift index 3c4b16d3..378b8fce 100644 --- a/IceCubesApp/App/Tabs/Settings/SupportAppView.swift +++ b/IceCubesApp/App/Tabs/Settings/SupportAppView.swift @@ -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 + } } } } diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index da1a1994..7023899d 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -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: { diff --git a/Packages/Account/Sources/Account/Edit/EditAccountView.swift b/Packages/Account/Sources/Account/Edit/EditAccountView.swift new file mode 100644 index 00000000..9362597e --- /dev/null +++ b/Packages/Account/Sources/Account/Edit/EditAccountView.swift @@ -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") + } + } + } + } +} diff --git a/Packages/Account/Sources/Account/Edit/EditAccountViewModel.swift b/Packages/Account/Sources/Account/Edit/EditAccountViewModel.swift new file mode 100644 index 00000000..972e719b --- /dev/null +++ b/Packages/Account/Sources/Account/Edit/EditAccountViewModel.swift @@ -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 + } + } + +} diff --git a/Packages/Models/Sources/Models/Account.swift b/Packages/Models/Sources/Models/Account.swift index dacece6b..861495ea 100644 --- a/Packages/Models/Sources/Models/Account.swift +++ b/Packages/Models/Sources/Models/Account.swift @@ -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] { diff --git a/Packages/Network/Sources/Network/Client.swift b/Packages/Network/Sources/Network/Client.swift index 91af8752..3e9b982c 100644 --- a/Packages/Network/Sources/Network/Client.swift +++ b/Packages/Network/Sources/Network/Client.swift @@ -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(endpoint: Endpoint) async throws -> Entity { try await makeEntityRequest(endpoint: endpoint, method: "PUT") } diff --git a/Packages/Network/Sources/Network/Endpoint/Accounts.swift b/Packages/Network/Sources/Network/Endpoint/Accounts.swift index d22aee6a..ae32039f 100644 --- a/Packages/Network/Sources/Network/Endpoint/Accounts.swift +++ b/Packages/Network/Sources/Network/Endpoint/Accounts.swift @@ -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 } diff --git a/Packages/Status/Sources/Status/Ext/Visibility.swift b/Packages/Status/Sources/Status/Ext/Visibility.swift index 73553094..2a37505b 100644 --- a/Packages/Status/Sources/Status/Ext/Visibility.swift +++ b/Packages/Status/Sources/Status/Ext/Visibility.swift @@ -1,6 +1,10 @@ import Models extension Visibility { + public static var supportDefault: [Visibility] { + [.pub, .priv, .unlisted] + } + public var iconName: String { switch self { case .pub: