Improve edit user profile.

This commit is contained in:
Marcin Czachursk 2023-03-25 11:37:02 +01:00
parent 537f344cf2
commit d437478797
13 changed files with 124 additions and 58 deletions

View File

@ -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.";

View File

@ -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.";

View File

@ -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
}
}

View File

@ -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)

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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?()
}
}
}

View File

@ -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()
}

View File

@ -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) {

View File

@ -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)
}
}
}