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.save" = "Save";
"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.loadingAvatarFailed" = "Loading avatar failed.";

View File

@ -266,5 +266,6 @@
"editProfile.title.website" = "Strona";
"editProfile.title.save" = "Zapisz";
"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.loadingAvatarFailed" = "Błąd podczas wczytywania zdjęcia.";

View File

@ -170,4 +170,13 @@ public extension PixelfedClientAuthenticated {
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 search(SearchQuery, Int)
case updateCredentials(String, String, String, Data?)
case updateAvatar(Data?)
}
}
@ -59,6 +60,8 @@ extension Pixelfed.Account: TargetType {
return "\(apiPath)/search"
case .updateCredentials(_, _, _, _):
return "\(apiPath)/update_credentials"
case .updateAvatar(_):
return "\(apiPath)/update_credentials"
}
}
@ -66,8 +69,10 @@ extension Pixelfed.Account: TargetType {
switch self {
case .follow(_), .unfollow(_), .block(_), .unblock(_), .mute(_), .unmute(_):
return .post
case .updateCredentials(_, _, _, _):
return .patch
case .updateCredentials(_, _, _, _), .updateAvatar(_):
// 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:
return .get
}
@ -113,19 +118,10 @@ extension Pixelfed.Account: TargetType {
minId = _minId
limit = _limit
page = _page
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 .updateCredentials(_, _, _, _), .updateAvatar(_):
return [
("_pe", "1")
]
case .account(_), .verifyCredentials:
return [
("_pe", "1")
@ -155,12 +151,8 @@ extension Pixelfed.Account: TargetType {
public var headers: [String: String]? {
switch self {
case .updateCredentials(_, _, _, let image):
if image != nil {
return ["content-type": "multipart/form-data; boundary=\(multipartBoundary)"]
} else {
return ["content-type": "application/x-www-form-urlencoded"]
}
case .updateCredentials(_, _, _, _), .updateAvatar(_):
return ["content-type": "multipart/form-data; boundary=\(multipartBoundary)"]
default:
return [:].contentTypeApplicationJson
}
@ -169,18 +161,25 @@ extension Pixelfed.Account: TargetType {
public var httpBody: Data? {
switch self {
case .updateCredentials(let displayName, let bio, let website, let image):
if let image {
let formDataBuilder = MultipartFormData(boundary: multipartBoundary)
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")
let formDataBuilder = MultipartFormData(boundary: multipartBoundary)
formDataBuilder.addTextField(named: "display_name", value: displayName)
formDataBuilder.addTextField(named: "note", value: bio)
formDataBuilder.addTextField(named: "website", value: website)
return formDataBuilder.build()
} else {
return nil
if let image {
formDataBuilder.addDataField(named: "avatar", fileName: "avatar.jpg", data: image, mimeType: "image/jpeg")
}
return formDataBuilder.build()
case .updateAvatar(let image):
let formDataBuilder = MultipartFormData(boundary: multipartBoundary)
if let image {
formDataBuilder.addDataField(named: "avatar", fileName: "avatar.jpg", data: image, mimeType: "image/jpeg")
}
return formDataBuilder.build()
default:
return nil
}

View File

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

View File

@ -86,5 +86,9 @@ extension Client {
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)
}
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 acct: String
public let avatar: URL?
public let avatarData: Data?
public let clientId: String
public let clientSecret: String
public let clientVapidKey: String
@ -29,6 +28,8 @@ public class AccountModel: ObservableObject, Identifiable {
public let username: String
public let lastSeenStatusId: String?
@Published public var avatarData: Data?
init(accountData: AccountData) {
self.accessToken = accountData.accessToken
self.refreshToken = accountData.refreshToken

View File

@ -22,6 +22,9 @@ struct EditProfileView: View {
@State private var avatarData: Data?
private let account: Account
private let bioMaxLength = 200
private let displayNameMaxLength = 30
private let websiteMaxLength = 120
init(account: Account) {
self.account = account
@ -64,8 +67,20 @@ struct EditProfileView: View {
}
Text("@\(self.account.acct)")
.font(.subheadline)
.font(.headline)
.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()
@ -74,21 +89,50 @@ struct EditProfileView: View {
.listRowBackground(Color(UIColor.systemGroupedBackground))
.listRowSeparator(Visibility.hidden)
Section("editProfile.title.displayName") {
Section {
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)
.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)
.autocapitalization(.none)
.keyboardType(.URL)
.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 {
@ -123,12 +167,26 @@ struct EditProfileView: View {
matching: .images)
}
@MainActor
private func saveProfile() async {
do {
let savedAccount = try await self.client.accounts?.update(displayName: self.displayName,
bio: self.bio,
website: self.website,
image: self.avatarData)
_ = try await self.client.accounts?.update(displayName: self.displayName,
bio: self.bio,
website: self.website,
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
ToastrService.shared.showSuccess("editProfile.title.accountSaved", imageSystemName: "person.crop.circle")
@ -162,7 +220,9 @@ struct EditProfileView: View {
return
}
self.avatarData = data
withAnimation(.linear) {
self.avatarData = data
}
self.saveDisabled = false
} catch {