diff --git a/Localization/en.lproj/Localizable.strings b/Localization/en.lproj/Localizable.strings index aa94c20..eb38252 100644 --- a/Localization/en.lproj/Localizable.strings +++ b/Localization/en.lproj/Localizable.strings @@ -268,6 +268,8 @@ "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.title.privateAccount" = "Private account"; -"editProfile.title.privateAccountInfo" = "When your account is private, only people you approve can see your photos and videos on pixelfed. Your existing followers won't be affected."; +"editProfile.title.privateAccountInfo" = "When your account is private, only people you approve can see your photos and videos on Pixelfed. Your existing followers won't be affected."; "editProfile.error.saveAccountFailed" = "Saving profile failed."; "editProfile.error.loadingAvatarFailed" = "Loading avatar failed."; +"editProfile.error.noProfileData" = "Profile data cannot be displayed."; +"editProfile.error.loadingAccountFailed" = "Error during download account from server."; diff --git a/Localization/pl.lproj/Localizable.strings b/Localization/pl.lproj/Localizable.strings index 68f8591..73230c4 100644 --- a/Localization/pl.lproj/Localizable.strings +++ b/Localization/pl.lproj/Localizable.strings @@ -271,3 +271,5 @@ "editProfile.title.privateAccountInfo" = "Kiedy Twoje konto jest prywatne, tylko osoby, które zaakceptujesz mogą oglądać Twoje zdjęcia i filmy na Pixelfed. Nie wpłynie to na Twoich obecnych obserwujących."; "editProfile.error.saveAccountFailed" = "Błąd podczas aktualizacji profilu."; "editProfile.error.loadingAvatarFailed" = "Błąd podczas wczytywania zdjęcia."; +"editProfile.error.noProfileData" = "Dane profilu nie mogą zostać wyświetlone."; +"editProfile.error.loadingAccountFailed" = "Błąd podczas pobierania profilu użytkownika."; diff --git a/PixelfedKit/Sources/PixelfedKit/Entities/Account.swift b/PixelfedKit/Sources/PixelfedKit/Entities/Account.swift index fd78afa..c43d85c 100644 --- a/PixelfedKit/Sources/PixelfedKit/Entities/Account.swift +++ b/PixelfedKit/Sources/PixelfedKit/Entities/Account.swift @@ -84,6 +84,9 @@ public struct Account: Codable { /// User website. public let website: String? + /// An extra attribute that contains source values to be used with API methods that verify credentials and update credentials. + public let source: Source? + /// When the most recent status was posted. /// NULLABLE String (ISO 8601 Date), or null if no statuses public let lastStatusAt: String? @@ -119,6 +122,7 @@ public struct Account: Codable { case lastStatusAt = "last_status_at" case recentPosts = "recent_posts" case website + case source } } diff --git a/PixelfedKit/Sources/PixelfedKit/Entities/Source.swift b/PixelfedKit/Sources/PixelfedKit/Entities/Source.swift new file mode 100644 index 0000000..70d7313 --- /dev/null +++ b/PixelfedKit/Sources/PixelfedKit/Entities/Source.swift @@ -0,0 +1,18 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import Foundation + +/// An extra attribute that contains source values to be used with API methods that verify credentials and update credentials. +public struct Source: Codable { + + /// Profile bio, in plain-text instead of in HTML. + public let note: String + + private enum CodingKeys: String, CodingKey { + case note + } +} diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index 5362024..0d654af 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -162,6 +162,7 @@ F89AC00529A1F9B500F4159F /* AppMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89AC00429A1F9B500F4159F /* AppMetadata.swift */; }; F89AC00729A208CC00F4159F /* PlaceSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89AC00629A208CC00F4159F /* PlaceSelectorView.swift */; }; F89AC00929A20C5C00F4159F /* Client+Places.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89AC00829A20C5C00F4159F /* Client+Places.swift */; }; + F89B5CC029D019B600549F2F /* HTMLString in Frameworks */ = {isa = PBXBuildFile; productRef = F89B5CBF29D019B600549F2F /* HTMLString */; }; F89CEB802984198600A1376F /* AttachmentData+HighestImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89CEB7F2984198600A1376F /* AttachmentData+HighestImage.swift */; }; F89D6C3F29716E41001DA3D4 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C3E29716E41001DA3D4 /* Theme.swift */; }; F89D6C4229717FDC001DA3D4 /* AccountsSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C4129717FDC001DA3D4 /* AccountsSectionView.swift */; }; @@ -452,6 +453,7 @@ F83E00ED29A2237C005D25A3 /* PixelfedKit in Frameworks */, F8210DD92966BB7E001D9973 /* NukeUI in Frameworks */, F85E132529741F05006A051D /* ActivityIndicatorView in Frameworks */, + F89B5CC029D019B600549F2F /* HTMLString in Frameworks */, F8B1E64F2973F61400EE0D10 /* Drops in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -974,6 +976,7 @@ F85E132429741F05006A051D /* ActivityIndicatorView */, F88E4D4C297EA4290057491A /* EmojiText */, F83E00EC29A2237C005D25A3 /* PixelfedKit */, + F89B5CBF29D019B600549F2F /* HTMLString */, ); productName = Vernissage; productReference = F88C2468295C37B80006098B /* Vernissage.app */; @@ -1012,6 +1015,7 @@ F8B1E64D2973F61400EE0D10 /* XCRemoteSwiftPackageReference "Drops" */, F85E132329741F05006A051D /* XCRemoteSwiftPackageReference "ActivityIndicatorView" */, F88E4D4B297EA4290057491A /* XCRemoteSwiftPackageReference "EmojiText" */, + F89B5CBE29D019B600549F2F /* XCRemoteSwiftPackageReference "HTMLString" */, ); productRefGroup = F88C2469295C37B80006098B /* Products */; projectDirPath = ""; @@ -1580,6 +1584,14 @@ minimumVersion = 2.6.0; }; }; + F89B5CBE29D019B600549F2F /* XCRemoteSwiftPackageReference "HTMLString" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/alexaubry/HTMLString"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.0.0; + }; + }; F8B1E64D2973F61400EE0D10 /* XCRemoteSwiftPackageReference "Drops" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/omaralbeik/Drops"; @@ -1624,6 +1636,11 @@ package = F88E4D4B297EA4290057491A /* XCRemoteSwiftPackageReference "EmojiText" */; productName = EmojiText; }; + F89B5CBF29D019B600549F2F /* HTMLString */ = { + isa = XCSwiftPackageProductDependency; + package = F89B5CBE29D019B600549F2F /* XCRemoteSwiftPackageReference "HTMLString" */; + productName = HTMLString; + }; F8B1E64E2973F61400EE0D10 /* Drops */ = { isa = XCSwiftPackageProductDependency; package = F8B1E64D2973F61400EE0D10 /* XCRemoteSwiftPackageReference "Drops" */; diff --git a/Vernissage/AppRouteur.swift b/Vernissage/AppRouteur.swift index 2ecad00..6633508 100644 --- a/Vernissage/AppRouteur.swift +++ b/Vernissage/AppRouteur.swift @@ -44,8 +44,8 @@ extension View { AccountsPhotoView(listType: listType) case .search: SearchView() - case .editProfile(let account): - EditProfileView(account: account) + case .editProfile: + EditProfileView() } } } diff --git a/Vernissage/Services/RouterPath.swift b/Vernissage/Services/RouterPath.swift index 1803869..660cf2c 100644 --- a/Vernissage/Services/RouterPath.swift +++ b/Vernissage/Services/RouterPath.swift @@ -22,7 +22,7 @@ enum RouteurDestinations: Hashable { case hashtags(listType: HashtagsView.ListType) case accountsPhoto(listType: AccountsPhotoView.ListType) case search - case editProfile(account: Account) + case editProfile } enum SheetDestinations: Identifiable { diff --git a/Vernissage/Views/EditProfileView.swift b/Vernissage/Views/EditProfileView.swift index 3b475d6..b8e1fc0 100644 --- a/Vernissage/Views/EditProfileView.swift +++ b/Vernissage/Views/EditProfileView.swift @@ -7,12 +7,14 @@ import PhotosUI import SwiftUI import PixelfedKit +import HTMLString struct EditProfileView: View { @EnvironmentObject private var applicationState: ApplicationState @EnvironmentObject private var client: Client @Environment(\.dismiss) private var dismiss + @State private var account: Account? @State private var photosPickerVisible = false @State private var selectedItems: [PhotosPickerItem] = [] @State private var saveDisabled = false @@ -21,17 +23,36 @@ struct EditProfileView: View { @State private var website: String = "" @State private var isPrivate = false @State private var avatarData: Data? + @State private var state: ViewState = .loading - private let account: Account private let bioMaxLength = 200 private let displayNameMaxLength = 30 private let websiteMaxLength = 120 - - init(account: Account) { - self.account = account - } var body: some View { + switch state { + case .loading: + LoadingIndicator() + .task { + await self.loadData() + } + case .loaded: + if let account = self.account { + self.editForm(account: account) + } else { + NoDataView(imageSystemName: "person.crop.circle", text: "editProfile.error.noProfileData") + } + case .error(let error): + ErrorView(error: error) { + self.state = .loading + await self.loadData() + } + .padding() + } + } + + @ViewBuilder + private func editForm(account: Account) -> some View { Form { HStack { Spacer() @@ -67,7 +88,7 @@ struct EditProfileView: View { .frame(width: 130, height: 130) } - Text("@\(self.account.acct)") + Text("@\(account.acct)") .font(.headline) .foregroundColor(.lightGrayColor) @@ -146,7 +167,7 @@ struct EditProfileView: View { .toolbar { ToolbarItem(placement: .primaryAction) { ActionButton(showLoader: true) { - await self.saveProfile() + await self.saveProfile(account: account) } label: { Text("editProfile.title.save", comment: "Save") } @@ -156,13 +177,18 @@ struct EditProfileView: View { } .navigationTitle("editProfile.navigationBar.title") .onAppear { - self.displayName = self.account.displayName ?? String.empty() - self.website = self.account.website ?? String.empty() - self.isPrivate = self.account.locked + self.displayName = account.displayName ?? String.empty() + self.website = account.website ?? String.empty() + self.isPrivate = account.locked - let markdownBio = self.account.note?.asMarkdown ?? String.empty() - if let attributedString = try? AttributedString(markdown: markdownBio) { - self.bio = String(attributedString.characters) + // Bio should be set from source property (which is plain text). + if let note = account.source?.note { + self.bio = note.removingHTMLEntities() + } else { + let markdownBio = account.note?.asMarkdown ?? String.empty() + if let attributedString = try? AttributedString(markdown: markdownBio) { + self.bio = String(attributedString.characters) + } } } .onChange(of: self.selectedItems) { selectedItem in @@ -176,8 +202,22 @@ struct EditProfileView: View { matching: .images) } + private func loadData() async { + do { + self.account = try await self.client.accounts?.pixelfedClient.verifyCredentials() + self.state = .loaded + } catch { + if !Task.isCancelled { + ErrorService.shared.handle(error, message: "editProfile.error.loadingAccountFailed", showToastr: true) + self.state = .error(error) + } else { + ErrorService.shared.handle(error, message: "editProfile.error.loadingAccountFailed", showToastr: false) + } + } + } + @MainActor - private func saveProfile() async { + private func saveProfile(account: Account) async { do { _ = try await self.client.accounts?.update(displayName: self.displayName, bio: self.bio, @@ -188,15 +228,14 @@ struct EditProfileView: View { if let avatarData = self.avatarData { _ = try await self.client.accounts?.avatar(image: avatarData) - if let accountData = AccountDataHandler.shared.getAccountData(accountId: self.account.id) { + if let accountData = AccountDataHandler.shared.getAccountData(accountId: 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, + let savedAccount = try await self.client.accounts?.account(withId: account.id) self.applicationState.updatedProfile = savedAccount ToastrService.shared.showSuccess("editProfile.title.accountSaved", imageSystemName: "person.crop.circle") diff --git a/Vernissage/Views/HomeFeedView.swift b/Vernissage/Views/HomeFeedView.swift index 5297c96..69a6862 100644 --- a/Vernissage/Views/HomeFeedView.swift +++ b/Vernissage/Views/HomeFeedView.swift @@ -13,7 +13,6 @@ 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 @@ -88,11 +87,6 @@ struct HomeFeedView: View { self.newPhotosView() .offset(y: self.offset) .opacity(self.opacity) - .onTapGesture { - Task { - await self.refresh?() - } - } } .refreshable { HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3)) diff --git a/Vernissage/Views/ThirdPartyView.swift b/Vernissage/Views/ThirdPartyView.swift index 9cbe98f..12f787c 100644 --- a/Vernissage/Views/ThirdPartyView.swift +++ b/Vernissage/Views/ThirdPartyView.swift @@ -41,8 +41,8 @@ struct ThirdPartyView: View { Section("OAuth authrisation") { VStack(alignment: .leading) { - Link("https://github.com/OAuthSwift/OAuthSwift.git", - destination: URL(string: "https://github.com/OAuthSwift/OAuthSwift.git")!) + Link("https://github.com/OAuthSwift/OAuthSwift", + destination: URL(string: "https://github.com/OAuthSwift/OAuthSwift")!) .padding(.bottom, 4) Text("Swift based OAuth library for iOS and macOS.") } @@ -61,13 +61,23 @@ struct ThirdPartyView: View { Section("Loaders") { VStack(alignment: .leading) { - Link("https://github.com/exyte/ActivityIndicatorView.git", - destination: URL(string: "https://github.com/exyte/ActivityIndicatorView.git")!) + Link("https://github.com/exyte/ActivityIndicatorView", + destination: URL(string: "https://github.com/exyte/ActivityIndicatorView")!) .padding(.bottom, 4) Text("A number of preset loading indicators created with SwiftUI.") } .font(.footnote) } + + Section("HTML String") { + VStack(alignment: .leading) { + Link("https://github.com/alexisakers/HTMLString", + destination: URL(string: "https://github.com/alexisakers/HTMLString")!) + .padding(.bottom, 4) + Text("HTMLString is a library written in Swift that allows your program to add and remove HTML entities in Strings.") + } + .font(.footnote) + } } .navigationTitle("thirdParty.navigationBar.title") .navigationBarTitleDisplayMode(.inline) diff --git a/Vernissage/Views/UserProfileView/UserProfileView.swift b/Vernissage/Views/UserProfileView/UserProfileView.swift index cce861e..e91c181 100644 --- a/Vernissage/Views/UserProfileView/UserProfileView.swift +++ b/Vernissage/Views/UserProfileView/UserProfileView.swift @@ -169,14 +169,11 @@ struct UserProfileView: View { Label(NSLocalizedString("userProfile.title.bookmarks", comment: "Bookmarks"), systemImage: "bookmark") } - if let account = self.account { - Divider() - - NavigationLink(value: RouteurDestinations.editProfile(account: account)) { - Label(NSLocalizedString("userProfile.title.edit", comment: "Edit profile"), systemImage: "pencil") - } - } + Divider() + NavigationLink(value: RouteurDestinations.editProfile) { + Label(NSLocalizedString("userProfile.title.edit", comment: "Edit profile"), systemImage: "pencil") + } }, label: { Image(systemName: "gear") .tint(.mainTextColor)