Improve edit profile screen.
This commit is contained in:
parent
d437478797
commit
88ca1a3f5c
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue