Improve edit user profile.
This commit is contained in:
parent
537f344cf2
commit
d437478797
|
@ -263,6 +263,7 @@
|
|||
"editProfile.navigationBar.title" = "Edit profile";
|
||||
"editProfile.title.displayName" = "Display name";
|
||||
"editProfile.title.bio" = "Bio";
|
||||
"editProfile.title.website" = "Website";
|
||||
"editProfile.title.save" = "Save";
|
||||
"editProfile.title.accountSaved" = "Profile has been updated.";
|
||||
"editProfile.error.saveAccountFailed" = "Saving profile failed.";
|
||||
|
|
|
@ -263,6 +263,7 @@
|
|||
"editProfile.navigationBar.title" = "Edutuj profil";
|
||||
"editProfile.title.displayName" = "Wyświetlana nazwa";
|
||||
"editProfile.title.bio" = "Bio";
|
||||
"editProfile.title.website" = "Strona";
|
||||
"editProfile.title.save" = "Zapisz";
|
||||
"editProfile.title.accountSaved" = "Profil zaktualizowano.";
|
||||
"editProfile.error.saveAccountFailed" = "Błąd podczas aktualizacji profilu.";
|
||||
|
|
|
@ -81,6 +81,9 @@ public struct Account: Codable {
|
|||
/// An extra attribute returned only when an account is silenced. If true, indicates that the account should be hidden behind a warning screen.
|
||||
public let limited: Bool?
|
||||
|
||||
/// User website.
|
||||
public let website: String?
|
||||
|
||||
/// When the most recent status was posted.
|
||||
/// NULLABLE String (ISO 8601 Date), or null if no statuses
|
||||
public let lastStatusAt: String?
|
||||
|
@ -115,6 +118,7 @@ public struct Account: Codable {
|
|||
case limited
|
||||
case lastStatusAt = "last_status_at"
|
||||
case recentPosts = "recent_posts"
|
||||
case website
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -162,10 +162,10 @@ public extension PixelfedClientAuthenticated {
|
|||
return try await downloadJson([Status].self, request: request)
|
||||
}
|
||||
|
||||
func update(displayName: String, bio: String, image: Data?) async throws -> Account {
|
||||
func update(displayName: String, bio: String, website: String, image: Data?) async throws -> Account {
|
||||
let request = try Self.request(
|
||||
for: baseURL,
|
||||
target: Pixelfed.Account.updateCredentials(displayName, bio, image),
|
||||
target: Pixelfed.Account.updateCredentials(displayName, bio, website, image),
|
||||
withBearerToken: token)
|
||||
|
||||
return try await downloadJson(Account.self, request: request)
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
fileprivate let multipartBoundary = UUID().uuidString
|
||||
|
||||
extension Pixelfed {
|
||||
public enum Account {
|
||||
case account(EntityId)
|
||||
|
@ -23,12 +21,13 @@ extension Pixelfed {
|
|||
case unmute(EntityId)
|
||||
case relationships([EntityId])
|
||||
case search(SearchQuery, Int)
|
||||
case updateCredentials(String, String, Data?)
|
||||
case updateCredentials(String, String, String, Data?)
|
||||
}
|
||||
}
|
||||
|
||||
extension Pixelfed.Account: TargetType {
|
||||
fileprivate var apiPath: String { return "/api/v1/accounts" }
|
||||
private var apiPath: String { return "/api/v1/accounts" }
|
||||
private var multipartBoundary: String { "d76a15ab-d0d4-499a-a3c6-62a86d0d2a74" }
|
||||
|
||||
public var path: String {
|
||||
switch self {
|
||||
|
@ -58,7 +57,7 @@ extension Pixelfed.Account: TargetType {
|
|||
return "\(apiPath)/relationships"
|
||||
case .search(_, _):
|
||||
return "\(apiPath)/search"
|
||||
case .updateCredentials(_, _, _):
|
||||
case .updateCredentials(_, _, _, _):
|
||||
return "\(apiPath)/update_credentials"
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +66,7 @@ extension Pixelfed.Account: TargetType {
|
|||
switch self {
|
||||
case .follow(_), .unfollow(_), .block(_), .unblock(_), .mute(_), .unmute(_):
|
||||
return .post
|
||||
case .updateCredentials(_, _, _):
|
||||
case .updateCredentials(_, _, _, _):
|
||||
return .patch
|
||||
default:
|
||||
return .get
|
||||
|
@ -114,10 +113,22 @@ extension Pixelfed.Account: TargetType {
|
|||
minId = _minId
|
||||
limit = _limit
|
||||
page = _page
|
||||
case .updateCredentials(let displayName, let bio, _):
|
||||
case .updateCredentials(let displayName, let bio, let website, let image):
|
||||
if image == nil {
|
||||
return [
|
||||
("display_name", displayName),
|
||||
("note", bio),
|
||||
("website", website),
|
||||
("_pe", "1")
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
("_pe", "1")
|
||||
]
|
||||
}
|
||||
case .account(_), .verifyCredentials:
|
||||
return [
|
||||
("display_name", displayName),
|
||||
("note", bio)
|
||||
("_pe", "1")
|
||||
]
|
||||
default:
|
||||
return nil
|
||||
|
@ -144,7 +155,7 @@ extension Pixelfed.Account: TargetType {
|
|||
|
||||
public var headers: [String: String]? {
|
||||
switch self {
|
||||
case .updateCredentials(_, _, let image):
|
||||
case .updateCredentials(_, _, _, let image):
|
||||
if image != nil {
|
||||
return ["content-type": "multipart/form-data; boundary=\(multipartBoundary)"]
|
||||
} else {
|
||||
|
@ -157,10 +168,15 @@ extension Pixelfed.Account: TargetType {
|
|||
|
||||
public var httpBody: Data? {
|
||||
switch self {
|
||||
case .updateCredentials(_, _, let image):
|
||||
case .updateCredentials(let displayName, let bio, let website, let image):
|
||||
if let image {
|
||||
let formDataBuilder = MultipartFormData(boundary: multipartBoundary)
|
||||
formDataBuilder.addDataField(named: "file", fileName: "avatar.jpg", data: image, mimeType: "image/jpeg")
|
||||
|
||||
formDataBuilder.addTextField(named: "display_name", value: displayName)
|
||||
formDataBuilder.addTextField(named: "note", value: bio)
|
||||
formDataBuilder.addTextField(named: "website", value: website)
|
||||
formDataBuilder.addDataField(named: "avatar", fileName: "avatar.jpg", data: image, mimeType: "image/jpeg")
|
||||
|
||||
return formDataBuilder.build()
|
||||
} else {
|
||||
return nil
|
||||
|
|
|
@ -1278,7 +1278,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 81;
|
||||
CURRENT_PROJECT_VERSION = 82;
|
||||
DEVELOPMENT_TEAM = B2U9FEKYP8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = VernissageWidget/Info.plist;
|
||||
|
@ -1306,7 +1306,7 @@
|
|||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 81;
|
||||
CURRENT_PROJECT_VERSION = 82;
|
||||
DEVELOPMENT_TEAM = B2U9FEKYP8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = VernissageWidget/Info.plist;
|
||||
|
|
|
@ -82,6 +82,9 @@ public class ApplicationState: ObservableObject {
|
|||
/// Status which should be shown from URL.
|
||||
@Published var showStatusId: String? = nil
|
||||
|
||||
/// Updated user profile.
|
||||
@Published var updatedProfile: Account?
|
||||
|
||||
public func changeApplicationState(accountModel: AccountModel, instance: Instance?, lastSeenStatusId: String?) {
|
||||
self.account = accountModel
|
||||
self.lastSeenStatusId = lastSeenStatusId
|
||||
|
|
|
@ -83,8 +83,8 @@ extension Client {
|
|||
return try await pixelfedClient.bookmarks(limit: limit, page: page)
|
||||
}
|
||||
|
||||
func update(displayName: String, bio: String, image: Data?) async throws -> Account {
|
||||
return try await pixelfedClient.update(displayName: displayName, bio: bio, image: image)
|
||||
func update(displayName: String, bio: String, website: String, image: Data?) async throws -> Account {
|
||||
return try await pixelfedClient.update(displayName: displayName, bio: bio, website: website, image: image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ struct EditProfileView: View {
|
|||
@State private var saveDisabled = false
|
||||
@State private var displayName: String = ""
|
||||
@State private var bio: String = ""
|
||||
@State private var website: String = ""
|
||||
@State private var avatarData: Data?
|
||||
|
||||
private let account: Account
|
||||
|
@ -28,45 +29,51 @@ struct EditProfileView: View {
|
|||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
ZStack {
|
||||
if let avatarData, let uiAvatar = UIImage(data: avatarData) {
|
||||
Image(uiImage: uiAvatar)
|
||||
.resizable()
|
||||
.clipShape(applicationState.avatarShape.shape())
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 96, height: 96)
|
||||
} else {
|
||||
UserAvatar(accountAvatar: account.avatar, size: .profile)
|
||||
}
|
||||
|
||||
BottomRight {
|
||||
Button {
|
||||
self.photosPickerVisible = true
|
||||
} label: {
|
||||
Image(systemName: "person.crop.circle.badge.plus")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
.frame(width: 96, height: 96)
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
ZStack {
|
||||
if let avatarData, let uiAvatar = UIImage(data: avatarData) {
|
||||
Image(uiImage: uiAvatar)
|
||||
.resizable()
|
||||
.clipShape(applicationState.avatarShape.shape())
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 120, height: 120)
|
||||
} else {
|
||||
UserAvatar(accountAvatar: account.avatar, size: .large)
|
||||
}
|
||||
|
||||
Text("@\(self.account.acct)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.lightGrayColor)
|
||||
LoadingIndicator(isVisible: $saveDisabled)
|
||||
|
||||
BottomRight {
|
||||
Button {
|
||||
self.photosPickerVisible = true
|
||||
} label: {
|
||||
ZStack {
|
||||
Circle()
|
||||
.foregroundColor(.accentColor.opacity(0.8))
|
||||
.frame(width: 40, height: 40)
|
||||
Image(systemName: "person.crop.circle.badge.plus")
|
||||
.font(.title2)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
.frame(width: 130, height: 130)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
Text("@\(self.account.acct)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.lightGrayColor)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(0)
|
||||
.padding(-10)
|
||||
.listRowBackground(Color(UIColor.systemGroupedBackground))
|
||||
.listRowSeparator(Visibility.hidden)
|
||||
|
||||
|
||||
Section("editProfile.title.displayName") {
|
||||
TextField("", text: $displayName)
|
||||
|
@ -76,6 +83,13 @@ struct EditProfileView: View {
|
|||
TextField("", text: $bio, axis: .vertical)
|
||||
.lineLimit(5, reservesSpace: true)
|
||||
}
|
||||
|
||||
Section("editProfile.title.website") {
|
||||
TextField("", text: $website)
|
||||
.autocapitalization(.none)
|
||||
.keyboardType(.URL)
|
||||
.autocorrectionDisabled()
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
|
@ -91,6 +105,7 @@ struct EditProfileView: View {
|
|||
.navigationTitle("editProfile.navigationBar.title")
|
||||
.onAppear {
|
||||
self.displayName = self.account.displayName ?? String.empty()
|
||||
self.website = self.account.website ?? String.empty()
|
||||
|
||||
let markdownBio = self.account.note?.asMarkdown ?? String.empty()
|
||||
if let attributedString = try? AttributedString(markdown: markdownBio) {
|
||||
|
@ -110,7 +125,12 @@ struct EditProfileView: View {
|
|||
|
||||
private func saveProfile() async {
|
||||
do {
|
||||
_ = try await self.client.accounts?.update(displayName: self.displayName, bio: self.bio, image: self.avatarData)
|
||||
let savedAccount = try await self.client.accounts?.update(displayName: self.displayName,
|
||||
bio: self.bio,
|
||||
website: self.website,
|
||||
image: self.avatarData)
|
||||
self.applicationState.updatedProfile = savedAccount
|
||||
|
||||
ToastrService.shared.showSuccess("editProfile.title.accountSaved", imageSystemName: "person.crop.circle")
|
||||
dismiss()
|
||||
} catch {
|
||||
|
@ -137,7 +157,7 @@ struct EditProfileView: View {
|
|||
}
|
||||
|
||||
guard let data = image
|
||||
.resized(to: .init(width: 400, height: 400))
|
||||
.resized(to: .init(width: 800, height: 800))
|
||||
.getJpegData() else {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ private struct OffsetPreferenceKey: PreferenceKey {
|
|||
|
||||
struct HomeFeedView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@Environment(\.refresh) private var refresh
|
||||
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
@EnvironmentObject var routerPath: RouterPath
|
||||
|
@ -89,9 +90,7 @@ struct HomeFeedView: View {
|
|||
.opacity(self.opacity)
|
||||
.onTapGesture {
|
||||
Task {
|
||||
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3))
|
||||
await self.refreshData()
|
||||
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7))
|
||||
await self.refresh?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,11 +59,11 @@ struct UserProfileHeaderView: View {
|
|||
VStack(alignment: .leading) {
|
||||
Text(account.displayNameWithoutEmojis)
|
||||
.foregroundColor(.mainTextColor)
|
||||
.font(.footnote)
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
Text("@\(account.acct)")
|
||||
.foregroundColor(.lightGrayColor)
|
||||
.font(.footnote)
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
@ -78,12 +78,22 @@ struct UserProfileHeaderView: View {
|
|||
.environment(\.openURL, OpenURLAction { url in
|
||||
routerPath.handle(url: url)
|
||||
})
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
if let website = account.website, let url = URL(string: website) {
|
||||
HStack {
|
||||
Image(systemName: "link")
|
||||
Link(website, destination: url)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.bottom, 2)
|
||||
.font(.footnote)
|
||||
}
|
||||
|
||||
Text(String(format: NSLocalizedString("userProfile.title.joined", comment: "Joined"), account.createdAt.toRelative(.isoDateTimeMilliSec)))
|
||||
.foregroundColor(.lightGrayColor.opacity(0.5))
|
||||
.font(.footnote)
|
||||
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ struct UserProfileView: View {
|
|||
@State private var account: Account? = nil
|
||||
@State private var relationship: Relationship? = nil
|
||||
@State private var state: ViewState = .loading
|
||||
@State private var viewId = UUID().uuidString
|
||||
|
||||
var body: some View {
|
||||
self.mainBody()
|
||||
|
@ -47,8 +48,17 @@ struct UserProfileView: View {
|
|||
if let account = self.account {
|
||||
ScrollView {
|
||||
UserProfileHeaderView(account: account, relationship: relationship)
|
||||
.id(self.viewId)
|
||||
UserProfileStatusesView(accountId: account.id)
|
||||
}
|
||||
.onAppear {
|
||||
if let updatedProfile = self.applicationState.updatedProfile {
|
||||
self.account = nil
|
||||
self.account = updatedProfile
|
||||
self.applicationState.updatedProfile = nil
|
||||
self.viewId = UUID().uuidString
|
||||
}
|
||||
}
|
||||
}
|
||||
case .error(let error):
|
||||
ErrorView(error: error) {
|
||||
|
|
|
@ -11,7 +11,7 @@ struct UserAvatar: View {
|
|||
@EnvironmentObject var applicationState: ApplicationState
|
||||
|
||||
public enum Size {
|
||||
case mini, list, comment, profile
|
||||
case mini, list, comment, profile, large
|
||||
|
||||
public var size: CGSize {
|
||||
switch self {
|
||||
|
@ -23,6 +23,8 @@ struct UserAvatar: View {
|
|||
return .init(width: 48, height: 48)
|
||||
case .profile:
|
||||
return .init(width: 96, height: 96)
|
||||
case .large:
|
||||
return .init(width: 120, height: 120)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue