Improve edit profile screen.

This commit is contained in:
Marcin Czachursk 2023-03-25 17:01:28 +01:00
parent d437478797
commit 88ca1a3f5c
8 changed files with 121 additions and 46 deletions

View File

@ -266,5 +266,6 @@
"editProfile.title.website" = "Website"; "editProfile.title.website" = "Website";
"editProfile.title.save" = "Save"; "editProfile.title.save" = "Save";
"editProfile.title.accountSaved" = "Profile has been updated."; "editProfile.title.accountSaved" = "Profile has been updated.";
"editProfile.title.photoInfo" = "The changed photo will be visible in the app and on the website with a small delay.";
"editProfile.error.saveAccountFailed" = "Saving profile failed."; "editProfile.error.saveAccountFailed" = "Saving profile failed.";
"editProfile.error.loadingAvatarFailed" = "Loading avatar failed."; "editProfile.error.loadingAvatarFailed" = "Loading avatar failed.";

View File

@ -266,5 +266,6 @@
"editProfile.title.website" = "Strona"; "editProfile.title.website" = "Strona";
"editProfile.title.save" = "Zapisz"; "editProfile.title.save" = "Zapisz";
"editProfile.title.accountSaved" = "Profil zaktualizowano."; "editProfile.title.accountSaved" = "Profil zaktualizowano.";
"editProfile.title.photoInfo" = "Zmienione zdjęcie będzie widoczne w aplikacji oraz na stronie z małym opóźnieniem.";
"editProfile.error.saveAccountFailed" = "Błąd podczas aktualizacji profilu."; "editProfile.error.saveAccountFailed" = "Błąd podczas aktualizacji profilu.";
"editProfile.error.loadingAvatarFailed" = "Błąd podczas wczytywania zdjęcia."; "editProfile.error.loadingAvatarFailed" = "Błąd podczas wczytywania zdjęcia.";

View File

@ -170,4 +170,13 @@ public extension PixelfedClientAuthenticated {
return try await downloadJson(Account.self, request: request) return try await downloadJson(Account.self, request: request)
} }
func avatar(image: Data?) async throws -> Account {
let request = try Self.request(
for: baseURL,
target: Pixelfed.Account.updateAvatar(image),
withBearerToken: token)
return try await downloadJson(Account.self, request: request)
}
} }

View File

@ -22,6 +22,7 @@ extension Pixelfed {
case relationships([EntityId]) case relationships([EntityId])
case search(SearchQuery, Int) case search(SearchQuery, Int)
case updateCredentials(String, String, String, Data?) case updateCredentials(String, String, String, Data?)
case updateAvatar(Data?)
} }
} }
@ -59,6 +60,8 @@ extension Pixelfed.Account: TargetType {
return "\(apiPath)/search" return "\(apiPath)/search"
case .updateCredentials(_, _, _, _): case .updateCredentials(_, _, _, _):
return "\(apiPath)/update_credentials" return "\(apiPath)/update_credentials"
case .updateAvatar(_):
return "\(apiPath)/update_credentials"
} }
} }
@ -66,8 +69,10 @@ extension Pixelfed.Account: TargetType {
switch self { switch self {
case .follow(_), .unfollow(_), .block(_), .unblock(_), .mute(_), .unmute(_): case .follow(_), .unfollow(_), .block(_), .unblock(_), .mute(_), .unmute(_):
return .post return .post
case .updateCredentials(_, _, _, _): case .updateCredentials(_, _, _, _), .updateAvatar(_):
return .patch // Mastodon API uses PATCH, however in Pixelfed we have to use POST: https://github.com/pixelfed/pixelfed/issues/4250
// Also it seems that we have to speparatelly save text fields and avatar(?).
return .post
default: default:
return .get return .get
} }
@ -113,19 +118,10 @@ extension Pixelfed.Account: TargetType {
minId = _minId minId = _minId
limit = _limit limit = _limit
page = _page page = _page
case .updateCredentials(let displayName, let bio, let website, let image): case .updateCredentials(_, _, _, _), .updateAvatar(_):
if image == nil {
return [
("display_name", displayName),
("note", bio),
("website", website),
("_pe", "1")
]
} else {
return [ return [
("_pe", "1") ("_pe", "1")
] ]
}
case .account(_), .verifyCredentials: case .account(_), .verifyCredentials:
return [ return [
("_pe", "1") ("_pe", "1")
@ -155,12 +151,8 @@ extension Pixelfed.Account: TargetType {
public var headers: [String: String]? { public var headers: [String: String]? {
switch self { switch self {
case .updateCredentials(_, _, _, let image): case .updateCredentials(_, _, _, _), .updateAvatar(_):
if image != nil {
return ["content-type": "multipart/form-data; boundary=\(multipartBoundary)"] return ["content-type": "multipart/form-data; boundary=\(multipartBoundary)"]
} else {
return ["content-type": "application/x-www-form-urlencoded"]
}
default: default:
return [:].contentTypeApplicationJson return [:].contentTypeApplicationJson
} }
@ -169,18 +161,25 @@ extension Pixelfed.Account: TargetType {
public var httpBody: Data? { public var httpBody: Data? {
switch self { switch self {
case .updateCredentials(let displayName, let bio, let website, let image): case .updateCredentials(let displayName, let bio, let website, let image):
if let image {
let formDataBuilder = MultipartFormData(boundary: multipartBoundary) let formDataBuilder = MultipartFormData(boundary: multipartBoundary)
formDataBuilder.addTextField(named: "display_name", value: displayName) formDataBuilder.addTextField(named: "display_name", value: displayName)
formDataBuilder.addTextField(named: "note", value: bio) formDataBuilder.addTextField(named: "note", value: bio)
formDataBuilder.addTextField(named: "website", value: website) formDataBuilder.addTextField(named: "website", value: website)
if let image {
formDataBuilder.addDataField(named: "avatar", fileName: "avatar.jpg", data: image, mimeType: "image/jpeg") formDataBuilder.addDataField(named: "avatar", fileName: "avatar.jpg", data: image, mimeType: "image/jpeg")
}
return formDataBuilder.build() return formDataBuilder.build()
} else { case .updateAvatar(let image):
return nil let formDataBuilder = MultipartFormData(boundary: multipartBoundary)
if let image {
formDataBuilder.addDataField(named: "avatar", fileName: "avatar.jpg", data: image, mimeType: "image/jpeg")
} }
return formDataBuilder.build()
default: default:
return nil return nil
} }

View File

@ -1278,7 +1278,7 @@
CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements; CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 82; CURRENT_PROJECT_VERSION = 83;
DEVELOPMENT_TEAM = B2U9FEKYP8; DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageWidget/Info.plist; INFOPLIST_FILE = VernissageWidget/Info.plist;
@ -1306,7 +1306,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements; CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 82; CURRENT_PROJECT_VERSION = 83;
DEVELOPMENT_TEAM = B2U9FEKYP8; DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageWidget/Info.plist; INFOPLIST_FILE = VernissageWidget/Info.plist;
@ -1454,7 +1454,7 @@
CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements; CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 81; CURRENT_PROJECT_VERSION = 83;
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
DEVELOPMENT_TEAM = B2U9FEKYP8; DEVELOPMENT_TEAM = B2U9FEKYP8;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@ -1494,7 +1494,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements; CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 81; CURRENT_PROJECT_VERSION = 83;
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
DEVELOPMENT_TEAM = B2U9FEKYP8; DEVELOPMENT_TEAM = B2U9FEKYP8;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;

View File

@ -86,5 +86,9 @@ extension Client {
func update(displayName: String, bio: String, website: String, image: Data?) async throws -> Account { 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) return try await pixelfedClient.update(displayName: displayName, bio: bio, website: website, image: image)
} }
func avatar(image: Data?) async throws -> Account {
return try await pixelfedClient.avatar(image: image)
}
} }
} }

View File

@ -12,7 +12,6 @@ public class AccountModel: ObservableObject, Identifiable {
public let refreshToken: String? public let refreshToken: String?
public let acct: String public let acct: String
public let avatar: URL? public let avatar: URL?
public let avatarData: Data?
public let clientId: String public let clientId: String
public let clientSecret: String public let clientSecret: String
public let clientVapidKey: String public let clientVapidKey: String
@ -29,6 +28,8 @@ public class AccountModel: ObservableObject, Identifiable {
public let username: String public let username: String
public let lastSeenStatusId: String? public let lastSeenStatusId: String?
@Published public var avatarData: Data?
init(accountData: AccountData) { init(accountData: AccountData) {
self.accessToken = accountData.accessToken self.accessToken = accountData.accessToken
self.refreshToken = accountData.refreshToken self.refreshToken = accountData.refreshToken

View File

@ -22,6 +22,9 @@ struct EditProfileView: View {
@State private var avatarData: Data? @State private var avatarData: Data?
private let account: Account private let account: Account
private let bioMaxLength = 200
private let displayNameMaxLength = 30
private let websiteMaxLength = 120
init(account: Account) { init(account: Account) {
self.account = account self.account = account
@ -64,8 +67,20 @@ struct EditProfileView: View {
} }
Text("@\(self.account.acct)") Text("@\(self.account.acct)")
.font(.subheadline) .font(.headline)
.foregroundColor(.lightGrayColor) .foregroundColor(.lightGrayColor)
if self.avatarData != nil {
HStack {
Image(systemName: "info.circle")
.font(.body)
.foregroundColor(.accentColor)
Text("editProfile.title.photoInfo")
.font(.footnote)
.foregroundColor(.lightGrayColor)
}
.padding(.top, 4)
}
} }
Spacer() Spacer()
@ -74,21 +89,50 @@ struct EditProfileView: View {
.listRowBackground(Color(UIColor.systemGroupedBackground)) .listRowBackground(Color(UIColor.systemGroupedBackground))
.listRowSeparator(Visibility.hidden) .listRowSeparator(Visibility.hidden)
Section {
Section("editProfile.title.displayName") {
TextField("", text: $displayName) TextField("", text: $displayName)
.onChange(of: self.displayName, perform: { newValue in
self.displayName = String(self.displayName.prefix(self.displayNameMaxLength))
})
} header: {
Text("editProfile.title.displayName", comment: "Display name")
} footer: {
HStack {
Spacer()
Text("\(self.displayName.count)/\(self.displayNameMaxLength)")
}
} }
Section("editProfile.title.bio") { Section {
TextField("", text: $bio, axis: .vertical) TextField("", text: $bio, axis: .vertical)
.lineLimit(5, reservesSpace: true) .lineLimit(5, reservesSpace: true)
.onChange(of: self.bio, perform: { newValue in
self.bio = String(self.bio.prefix(self.bioMaxLength))
})
} header: {
Text("editProfile.title.bio", comment: "Bio")
} footer: {
HStack {
Spacer()
Text("\(self.bio.count)/\(self.bioMaxLength)")
}
} }
Section("editProfile.title.website") { Section {
TextField("", text: $website) TextField("", text: $website)
.autocapitalization(.none) .autocapitalization(.none)
.keyboardType(.URL) .keyboardType(.URL)
.autocorrectionDisabled() .autocorrectionDisabled()
.onChange(of: self.website, perform: { newValue in
self.website = String(self.website.prefix(self.websiteMaxLength))
})
} header: {
Text("editProfile.title.website", comment: "Website")
} footer: {
HStack {
Spacer()
Text("\(self.website.count)/\(self.websiteMaxLength)")
}
} }
} }
.toolbar { .toolbar {
@ -123,12 +167,26 @@ struct EditProfileView: View {
matching: .images) matching: .images)
} }
@MainActor
private func saveProfile() async { private func saveProfile() async {
do { do {
let savedAccount = try await self.client.accounts?.update(displayName: self.displayName, _ = try await self.client.accounts?.update(displayName: self.displayName,
bio: self.bio, bio: self.bio,
website: self.website, website: self.website,
image: self.avatarData) image: nil)
if let avatarData = self.avatarData {
_ = try await self.client.accounts?.avatar(image: avatarData)
if let accountData = AccountDataHandler.shared.getAccountData(accountId: self.account.id) {
accountData.avatarData = avatarData
self.applicationState.account?.avatarData = avatarData
CoreDataHandler.shared.save()
}
}
let savedAccount = try await self.client.accounts?.account(withId: self.account.id)
// self.applicationState.account?.avatar,
self.applicationState.updatedProfile = savedAccount self.applicationState.updatedProfile = savedAccount
ToastrService.shared.showSuccess("editProfile.title.accountSaved", imageSystemName: "person.crop.circle") ToastrService.shared.showSuccess("editProfile.title.accountSaved", imageSystemName: "person.crop.circle")
@ -162,7 +220,9 @@ struct EditProfileView: View {
return return
} }
withAnimation(.linear) {
self.avatarData = data self.avatarData = data
}
self.saveDisabled = false self.saveDisabled = false
} catch { } catch {