diff --git a/CHANGELOG.md b/CHANGELOG.md index 67fe84009..b5f3cc134 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Release Notes +- Added support for user setting and displaying pronouns. +- Added display of website urls for user profiles. + ## [1.0.2] - 2024-11-26Z ### Release Notes diff --git a/Nos.xcodeproj/project.pbxproj b/Nos.xcodeproj/project.pbxproj index 3a9aae362..680d715e8 100644 --- a/Nos.xcodeproj/project.pbxproj +++ b/Nos.xcodeproj/project.pbxproj @@ -712,6 +712,7 @@ 04C9D7982CC29EDD00EAAD4D /* FeaturedAuthor+Cohort5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeaturedAuthor+Cohort5.swift"; sourceTree = ""; }; 04F16AA62CBDBD91003AD693 /* DeleteConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteConfirmationView.swift; sourceTree = ""; }; 2D06BB9C2AE249D70085F509 /* ThreadRootView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadRootView.swift; sourceTree = ""; }; + 2D3C71A52CEE6F7100625BCB /* Nos 20.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Nos 20.xcdatamodel"; sourceTree = ""; }; 2D4010A12AD87DF300F93AD4 /* KnownFollowersView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KnownFollowersView.swift; sourceTree = ""; }; 3A1C296E2B2A537C0020B753 /* Moderation.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Moderation.xcstrings; sourceTree = ""; }; 3A67449B2B294712002B8DE0 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; @@ -2164,7 +2165,7 @@ C9B737702AB24D5F00398BE7 /* XCRemoteSwiftPackageReference "SwiftGenPlugin" */, C91565BF2B2368FA0068EECA /* XCRemoteSwiftPackageReference "ViewInspector" */, 3AD3185B2B294E6200026B07 /* XCRemoteSwiftPackageReference "xcstrings-tool-plugin" */, - C9FD34F42BCEC89C008F8D95 /* XCRemoteSwiftPackageReference "secp256k1.swift" */, + C9FD34F42BCEC89C008F8D95 /* XCRemoteSwiftPackageReference "secp256k1" */, C9FD35112BCED5A6008F8D95 /* XCRemoteSwiftPackageReference "nostr-sdk-ios" */, 03C49ABE2C938A9C00502321 /* XCRemoteSwiftPackageReference "SwiftSoup" */, 039389212CA4985C00698978 /* XCRemoteSwiftPackageReference "SDWebImageWebPCoder" */, @@ -2799,11 +2800,11 @@ /* Begin PBXTargetDependency section */ 3AD3185D2B294E9000026B07 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - productRef = 3AD3185C2B294E9000026B07 /* plugin:XCStringsToolPlugin */; + productRef = 3AD3185C2B294E9000026B07 /* XCStringsToolPlugin */; }; 3AEABEF32B2BF806001BC933 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - productRef = 3AEABEF22B2BF806001BC933 /* plugin:XCStringsToolPlugin */; + productRef = 3AEABEF22B2BF806001BC933 /* XCStringsToolPlugin */; }; C90862C229E9804B00C35A71 /* PBXTargetDependency */ = { isa = PBXTargetDependency; @@ -2812,11 +2813,11 @@ }; C9A6C7442AD83F7A001F9500 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - productRef = C9A6C7432AD83F7A001F9500 /* plugin:SwiftGenPlugin */; + productRef = C9A6C7432AD83F7A001F9500 /* SwiftGenPlugin */; }; C9D573402AB24A3700E06BB4 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - productRef = C9D5733F2AB24A3700E06BB4 /* plugin:SwiftGenPlugin */; + productRef = C9D5733F2AB24A3700E06BB4 /* SwiftGenPlugin */; }; C9DEBFE6298941020078B43A /* PBXTargetDependency */ = { isa = PBXTargetDependency; @@ -3673,7 +3674,7 @@ minimumVersion = 4.0.0; }; }; - C9FD34F42BCEC89C008F8D95 /* XCRemoteSwiftPackageReference "secp256k1.swift" */ = { + C9FD34F42BCEC89C008F8D95 /* XCRemoteSwiftPackageReference "secp256k1" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/GigaBitcoin/secp256k1.swift"; requirement = { @@ -3712,12 +3713,12 @@ package = 03C49ABE2C938A9C00502321 /* XCRemoteSwiftPackageReference "SwiftSoup" */; productName = SwiftSoup; }; - 3AD3185C2B294E9000026B07 /* plugin:XCStringsToolPlugin */ = { + 3AD3185C2B294E9000026B07 /* XCStringsToolPlugin */ = { isa = XCSwiftPackageProductDependency; package = 3AD3185B2B294E6200026B07 /* XCRemoteSwiftPackageReference "xcstrings-tool-plugin" */; productName = "plugin:XCStringsToolPlugin"; }; - 3AEABEF22B2BF806001BC933 /* plugin:XCStringsToolPlugin */ = { + 3AEABEF22B2BF806001BC933 /* XCStringsToolPlugin */ = { isa = XCSwiftPackageProductDependency; package = 3AD3185B2B294E6200026B07 /* XCRemoteSwiftPackageReference "xcstrings-tool-plugin" */; productName = "plugin:XCStringsToolPlugin"; @@ -3791,7 +3792,7 @@ package = C99DBF7C2A9E81CF00F7068F /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */; productName = SDWebImageSwiftUI; }; - C9A6C7432AD83F7A001F9500 /* plugin:SwiftGenPlugin */ = { + C9A6C7432AD83F7A001F9500 /* SwiftGenPlugin */ = { isa = XCSwiftPackageProductDependency; package = C9B737702AB24D5F00398BE7 /* XCRemoteSwiftPackageReference "SwiftGenPlugin" */; productName = "plugin:SwiftGenPlugin"; @@ -3811,7 +3812,7 @@ package = C9B71DBC2A8E9BAD0031ED9F /* XCRemoteSwiftPackageReference "sentry-cocoa" */; productName = Sentry; }; - C9D5733F2AB24A3700E06BB4 /* plugin:SwiftGenPlugin */ = { + C9D5733F2AB24A3700E06BB4 /* SwiftGenPlugin */ = { isa = XCSwiftPackageProductDependency; package = C9C8450C2AB249DB00654BC1 /* XCRemoteSwiftPackageReference "SwiftGenPlugin" */; productName = "plugin:SwiftGenPlugin"; @@ -3823,12 +3824,12 @@ }; C9FD34F52BCEC89C008F8D95 /* secp256k1 */ = { isa = XCSwiftPackageProductDependency; - package = C9FD34F42BCEC89C008F8D95 /* XCRemoteSwiftPackageReference "secp256k1.swift" */; + package = C9FD34F42BCEC89C008F8D95 /* XCRemoteSwiftPackageReference "secp256k1" */; productName = secp256k1; }; C9FD34F72BCEC8B5008F8D95 /* secp256k1 */ = { isa = XCSwiftPackageProductDependency; - package = C9FD34F42BCEC89C008F8D95 /* XCRemoteSwiftPackageReference "secp256k1.swift" */; + package = C9FD34F42BCEC89C008F8D95 /* XCRemoteSwiftPackageReference "secp256k1" */; productName = secp256k1; }; C9FD35122BCED5A6008F8D95 /* NostrSDK */ = { @@ -3852,6 +3853,7 @@ C936B4572A4C7B7C00DF1EB9 /* Nos.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 2D3C71A52CEE6F7100625BCB /* Nos 20.xcdatamodel */, C95057C62CC69FD70024EC9C /* Nos 19.xcdatamodel */, C9BB9FE32CBEFF560045DC5A /* Nos 18.xcdatamodel */, C9D2839E2CB9B177007ADCB9 /* Nos 17.xcdatamodel */, @@ -3864,7 +3866,7 @@ C9C547562A4F1D1A006B0741 /* Nos 9.xcdatamodel */, 5BFF66AF2A4B55FC00AA79DD /* Nos 10.xcdatamodel */, ); - currentVersion = C95057C62CC69FD70024EC9C /* Nos 19.xcdatamodel */; + currentVersion = 2D3C71A52CEE6F7100625BCB /* Nos 20.xcdatamodel */; path = Nos.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Nos/Assets/Localization/Localizable.xcstrings b/Nos/Assets/Localization/Localizable.xcstrings index 468fc13f1..edd3694c3 100644 --- a/Nos/Assets/Localization/Localizable.xcstrings +++ b/Nos/Assets/Localization/Localizable.xcstrings @@ -14352,6 +14352,17 @@ } } }, + "pronouns" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pronouns" + } + } + } + }, "pubkey" : { "extractionState" : "manual", "localizations" : { diff --git a/Nos/Models/CoreData/Event+Hydration.swift b/Nos/Models/CoreData/Event+Hydration.swift index a585e2b43..860c2e0a0 100644 --- a/Nos/Models/CoreData/Event+Hydration.swift +++ b/Nos/Models/CoreData/Event+Hydration.swift @@ -173,6 +173,7 @@ extension Event { newAuthor.profilePhotoURL = metadata.profilePhotoURL newAuthor.website = metadata.website newAuthor.nip05 = metadata.nip05 + newAuthor.pronouns = metadata.pronouns } catch { print("Failed to decode metaData event with ID \(String(describing: identifier))") } diff --git a/Nos/Models/CoreData/Generated/Author+CoreDataProperties.swift b/Nos/Models/CoreData/Generated/Author+CoreDataProperties.swift index 27d1338fc..174b7b7d0 100644 --- a/Nos/Models/CoreData/Generated/Author+CoreDataProperties.swift +++ b/Nos/Models/CoreData/Generated/Author+CoreDataProperties.swift @@ -17,6 +17,7 @@ extension Author { @NSManaged public var name: String? @NSManaged public var website: String? @NSManaged public var nip05: String? + @NSManaged public var pronouns: String? @NSManaged public var profilePhotoURL: URL? @NSManaged public var rawMetadata: Data? @NSManaged public var events: Set diff --git a/Nos/Models/CoreData/Nos.xcdatamodeld/.xccurrentversion b/Nos/Models/CoreData/Nos.xcdatamodeld/.xccurrentversion index 9cb70141b..d7c309e78 100644 --- a/Nos/Models/CoreData/Nos.xcdatamodeld/.xccurrentversion +++ b/Nos/Models/CoreData/Nos.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Nos 19.xcdatamodel + Nos 20.xcdatamodel diff --git a/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 20.xcdatamodel/.xccurrentversion b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 20.xcdatamodel/.xccurrentversion new file mode 100644 index 000000000..6c8a1eef9 --- /dev/null +++ b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 20.xcdatamodel/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + Nos.xcdatamodel + + diff --git a/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 20.xcdatamodel/Nos.xcdatamodel/contents b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 20.xcdatamodel/Nos.xcdatamodel/contents new file mode 100644 index 000000000..1a418ef2c --- /dev/null +++ b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 20.xcdatamodel/Nos.xcdatamodel/contents @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 20.xcdatamodel/contents b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 20.xcdatamodel/contents new file mode 100644 index 000000000..ea7c202e8 --- /dev/null +++ b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 20.xcdatamodel/contents @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Nos/Models/JSONEvent.swift b/Nos/Models/JSONEvent.swift index c824e5b8c..4e32031c9 100644 --- a/Nos/Models/JSONEvent.swift +++ b/Nos/Models/JSONEvent.swift @@ -175,13 +175,14 @@ struct MetadataEventJSON: Codable { var about: String? var website: String? var picture: String? + var pronouns: String? var profilePhotoURL: URL? { URL(string: picture ?? "") } private enum CodingKeys: String, CodingKey { - case displayName = "display_name", name, nip05, about, website, picture + case displayName = "display_name", name, nip05, about, website, picture, pronouns } var dictionary: [String: String] { @@ -192,6 +193,7 @@ struct MetadataEventJSON: Codable { "about": about ?? "", "website": website ?? "", "picture": picture ?? "", + "pronouns": pronouns ?? "", ] } } diff --git a/Nos/Service/CurrentUser+PublishEvents.swift b/Nos/Service/CurrentUser+PublishEvents.swift index c12bf26b3..e24268695 100644 --- a/Nos/Service/CurrentUser+PublishEvents.swift +++ b/Nos/Service/CurrentUser+PublishEvents.swift @@ -12,7 +12,8 @@ extension CurrentUser { nip05: author.nip05, about: author.about, website: author.website, - picture: author.profilePhotoURL?.absoluteString + picture: author.profilePhotoURL?.absoluteString, + pronouns: author.pronouns ).dictionary if let rawData = author.rawMetadata { // Tack on any unsupported fields back onto the dictionary before @@ -211,6 +212,7 @@ extension CurrentUser { author.nip05 = nil author.profilePhotoURL = nil author.rawMetadata = nil + author.pronouns = nil try viewContext.save() try await publishMetadata() diff --git a/Nos/Views/Components/BioView.swift b/Nos/Views/Components/BioView.swift index 1caff8689..e555b66b0 100644 --- a/Nos/Views/Components/BioView.swift +++ b/Nos/Views/Components/BioView.swift @@ -113,7 +113,9 @@ struct BioView: View { } private func updateShouldShowReadMore() { - shouldShowReadMore = intrinsicSize.height != truncatedSize.height + shouldShowReadMore = (author.pronouns?.isEmpty == false) || + (author.website?.isEmpty == false) || + (intrinsicSize.height > truncatedSize.height) } fileprivate struct IntrinsicSizePreferenceKey: PreferenceKey { diff --git a/Nos/Views/Components/Form/NosFormField.swift b/Nos/Views/Components/Form/NosFormField.swift index 89f39dc3d..8fe11fbde 100644 --- a/Nos/Views/Components/Form/NosFormField.swift +++ b/Nos/Views/Components/Form/NosFormField.swift @@ -43,7 +43,7 @@ struct NosFormField_Previews: PreviewProvider { WithState(initialValue: "") { text in NosFormField("about") { TextField("", text: text) - .textInputAutocapitalization(.none) + .textInputAutocapitalization(.never) .foregroundColor(.primaryTxt) .autocorrectionDisabled() } diff --git a/Nos/Views/Components/Form/NosTextEditor.swift b/Nos/Views/Components/Form/NosTextEditor.swift index 5098c6a46..6b351305b 100644 --- a/Nos/Views/Components/Form/NosTextEditor.swift +++ b/Nos/Views/Components/Form/NosTextEditor.swift @@ -14,7 +14,7 @@ struct NosTextEditor: View { var body: some View { NosFormField(label) { TextEditor(text: $text) - .textInputAutocapitalization(.none) + .textInputAutocapitalization(.never) .foregroundColor(.primaryTxt) .scrollContentBackground(.hidden) .autocorrectionDisabled() diff --git a/Nos/Views/Components/Form/NosTextField.swift b/Nos/Views/Components/Form/NosTextField.swift index 25988ac63..f4fc2b86f 100644 --- a/Nos/Views/Components/Form/NosTextField.swift +++ b/Nos/Views/Components/Form/NosTextField.swift @@ -14,7 +14,7 @@ struct NosTextField: View { var body: some View { NosFormField(label) { TextField("", text: $text) - .textInputAutocapitalization(.none) + .textInputAutocapitalization(.never) .foregroundColor(.primaryTxt) .autocorrectionDisabled() } diff --git a/Nos/Views/Onboarding/AccountSuccessView.swift b/Nos/Views/Onboarding/AccountSuccessView.swift index c8ce69a74..a8a7666a8 100644 --- a/Nos/Views/Onboarding/AccountSuccessView.swift +++ b/Nos/Views/Onboarding/AccountSuccessView.swift @@ -134,7 +134,7 @@ fileprivate struct ConnectingLine: Shape { } #Preview("All steps completed") { - var state = OnboardingState() + let state = OnboardingState() state.displayNameSucceeded = true state.usernameSucceeded = true diff --git a/Nos/Views/Profile/BioSheet.swift b/Nos/Views/Profile/BioSheet.swift index 46be5d1f1..9bc0c31aa 100644 --- a/Nos/Views/Profile/BioSheet.swift +++ b/Nos/Views/Profile/BioSheet.swift @@ -19,7 +19,33 @@ struct BioSheet: View { ) return bio } + + private var pronouns: String? { + guard let pronouns = author.pronouns, !pronouns.isEmpty else { + return nil + } + return pronouns + } + + private var website: String? { + guard let website = author.website, !website.isEmpty else { + return nil + } + return website + } + private var websiteURL: URL? { + guard let website = author.website, !website.isEmpty, let url = URL(string: website) else { + return nil + } + + guard let scheme = url.scheme, !scheme.isEmpty else { + return URL(string: "https://\(website)") + } + + return url + } + var body: some View { ScrollView(.vertical) { VStack(alignment: .leading, spacing: 13) { @@ -43,7 +69,57 @@ struct BioSheet: View { if author.hasMostrNIP05 { ActivityPubBadgeView(author: author) } + + if let website { + Text("website") + .font(.subheadline.weight(.semibold)) + .foregroundStyle(Color.secondaryTxt) + .lineSpacing(10) + .shadow( + color: Color.bioSheetShadow, + radius: 4, + x: 0, + y: 4 + ) + .padding(.top, 34) + if let websiteURL { + Link(destination: websiteURL) { + Text(website) + .textSelection(.enabled) + .font(.body) + .foregroundStyle(Color.primaryTxt) + .tint(.accent) + } + .underline() + } else { + Text(website) + .textSelection(.enabled) + .font(.body) + .foregroundStyle(Color.primaryTxt) + .tint(.accent) + } + } + + if let pronouns { + Text("pronouns") + .font(.subheadline.weight(.semibold)) + .foregroundStyle(Color.secondaryTxt) + .lineSpacing(10) + .shadow( + color: Color.bioSheetShadow, + radius: 4, + x: 0, + y: 4 + ) + .padding(.top, 34) + Text(pronouns) + .textSelection(.enabled) + .font(.body) + .foregroundStyle(Color.primaryTxt) + .tint(.accent) + } + if let bio { Text("bio") .font(.subheadline.weight(.semibold)) diff --git a/Nos/Views/Profile/Edit/ProfileEditView.swift b/Nos/Views/Profile/Edit/ProfileEditView.swift index 3b44d9068..8aa214096 100644 --- a/Nos/Views/Profile/Edit/ProfileEditView.swift +++ b/Nos/Views/Profile/Edit/ProfileEditView.swift @@ -22,6 +22,7 @@ struct ProfileEditView: View { @State private var bioText: String = "" @State private var avatarText: String = "" @State private var website: String = "" + @State private var pronouns: String = "" @State private var showNIP05Wizard = false @State private var showConfirmationDialog = false @State private var saveError: SaveProfileError? @@ -77,6 +78,8 @@ struct ProfileEditView: View { .frame(maxHeight: 200) FormSeparator() NosTextField("website", text: $website) + FormSeparator() + NosTextField("pronouns", text: $pronouns) } } .sheet(isPresented: $showNIP05Wizard) { @@ -130,6 +133,7 @@ struct ProfileEditView: View { bioText = author.about ?? "" avatarText = author.profilePhotoURL?.absoluteString ?? "" website = author.website ?? "" + pronouns = author.pronouns ?? "" } private func save() async { @@ -137,6 +141,7 @@ struct ProfileEditView: View { author.about = bioText author.profilePhotoURL = URL(string: avatarText) author.website = website + author.pronouns = pronouns do { try viewContext.save() try await currentUser.publishMetadata() diff --git a/Nos/Views/Profile/ProfileHeader.swift b/Nos/Views/Profile/ProfileHeader.swift index c68b304d8..e916c9f10 100644 --- a/Nos/Views/Profile/ProfileHeader.swift +++ b/Nos/Views/Profile/ProfileHeader.swift @@ -34,6 +34,20 @@ struct ProfileHeader: View { } return false } + + private var shouldShowWebsite: Bool { + if let website = author.website { + return website.isEmpty == false + } + return false + } + + private var shouldShowPronouns: Bool { + if let pronouns = author.pronouns { + return pronouns.isEmpty == false + } + return false + } private var knownFollowers: [Follow] { author.followers.filter { diff --git a/NosTests/Models/CoreData/AuthorTests.swift b/NosTests/Models/CoreData/AuthorTests.swift index 55c33aaec..92a604eb8 100644 --- a/NosTests/Models/CoreData/AuthorTests.swift +++ b/NosTests/Models/CoreData/AuthorTests.swift @@ -268,4 +268,42 @@ final class AuthorTests: CoreDataTestCase { // Assert XCTAssertEqual(authors, [eve, carl, bob]) } + + /// Test that the `pronouns` field can be set and saved correctly in Core Data. + func testSetPronouns() throws { + let context = persistenceController.viewContext + let author = try Author.findOrCreate(by: "testAuthor", context: context) + + // Set the pronouns + let pronouns = "they/them" + author.pronouns = pronouns + + // Save the context + try context.save() + + // Fetch the saved author to verify + let fetchedAuthor = try Author.find(by: "testAuthor", context: context) + + XCTAssertNotNil(fetchedAuthor) + XCTAssertEqual(fetchedAuthor?.pronouns, pronouns, "The pronouns should match the saved value.") + } + + /// Test that the `pronouns` field can be retrieved correctly from Core Data. + func testGetPronouns() throws { + let context = persistenceController.viewContext + + // Create and set up an author with pronouns + let pronouns = "she/her" + let author = try Author.findOrCreate(by: "testAuthor2", context: context) + author.pronouns = pronouns + + // Save the context + try context.save() + + // Fetch the author again and verify pronouns + let fetchedAuthor = try Author.find(by: "testAuthor2", context: context) + + XCTAssertNotNil(fetchedAuthor, "The author should have been fetched successfully.") + XCTAssertEqual(fetchedAuthor?.pronouns, pronouns, "The fetched pronouns should match the saved value.") + } }