diff --git a/CHANGELOG.md b/CHANGELOG.md index c466b741a..47742c26d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Fixed an issue where registering a NIP-05 username field could fail silently. - Fixed an issue where users named with valid urls were unable to be mentioned correctly. - Fixed an issue where pasting an npub while composing a note created an invalid mention. - Changed "Report note" button to "Flag this content" diff --git a/Nos/Assets/Localization/Localizable.xcstrings b/Nos/Assets/Localization/Localizable.xcstrings index 5b1ccf87a..3252a24ba 100644 --- a/Nos/Assets/Localization/Localizable.xcstrings +++ b/Nos/Assets/Localization/Localizable.xcstrings @@ -10072,6 +10072,17 @@ } } }, + "retry" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retry" + } + } + } + }, "returnToChooseName" : { "extractionState" : "manual", "localizations" : { diff --git a/Nos/Controller/UNSWizardController.swift b/Nos/Controller/UNSWizardController.swift index 98b886f7a..f36c27ee6 100644 --- a/Nos/Controller/UNSWizardController.swift +++ b/Nos/Controller/UNSWizardController.swift @@ -123,7 +123,7 @@ class UNSWizardController: ObservableObject { author.nip05 = nip05 } try context.save() - await currentUser.publishMetaData() + try await currentUser.publishMetadata() state = .success } } diff --git a/Nos/Service/CurrentUser.swift b/Nos/Service/CurrentUser.swift index 515e4f23a..786dd674d 100644 --- a/Nos/Service/CurrentUser.swift +++ b/Nos/Service/CurrentUser.swift @@ -5,11 +5,17 @@ import Dependencies enum CurrentUserError: Error { case authorNotFound + case encodingError + case errorWhilePublishingToRelays var description: String? { switch self { case .authorNotFound: return "Current user's author not found" + case .encodingError: + return "An encoding error happened while saving the user data" + case .errorWhilePublishingToRelays: + return "An encoding error happened while publishing to relays" } } } @@ -263,13 +269,9 @@ enum CurrentUserError: Error { return followKeys.contains(key) } - @MainActor func publishMetaData() async { - guard let pubKey = publicKeyHex, let author = try? Author.find(by: pubKey, context: viewContext) else { - Log.debug("Error: no user") - return - } - self.author = author - + /// Builds a dictionary to be used as content when publishing a kind 0 + /// event. + private func buildMetadataJSONObject(author: Author) -> [String: String] { var metaEvent = MetadataEventJSON( displayName: author.displayName, name: author.name, @@ -279,34 +281,72 @@ enum CurrentUserError: Error { website: author.website, picture: author.profilePhotoURL?.absoluteString ).dictionary - if let rawData = author.rawMetadata { - // Tack on any unsupported fields back onto the dictionary before publish - let rawJson = try? JSONSerialization.jsonObject(with: rawData) - if let rawJson, let rawDictionary = rawJson as? [String: AnyObject] { - for key in rawDictionary.keys { - if metaEvent[key] == nil, let rawValue = rawDictionary[key] as? String { - metaEvent[key] = rawValue - Log.debug("Added \(key) : \(rawValue)") + // Tack on any unsupported fields back onto the dictionary before + // publish. + do { + let rawJson = try JSONSerialization.jsonObject(with: rawData) + if let rawDictionary = rawJson as? [String: AnyObject] { + for key in rawDictionary.keys { + guard metaEvent[key] == nil else { + continue + } + if let rawValue = rawDictionary[key] as? String { + metaEvent[key] = rawValue + Log.debug("Added \(key) : \(rawValue)") + } } } + } catch { + Log.debug("Couldn't parse a JSON from the user raw metadata") + // Continue with the metaEvent object we built previously } } + return metaEvent + } - let metaData = try? JSONSerialization.data(withJSONObject: metaEvent) - guard let metaData, let metaString = String(data: metaData, encoding: .utf8) else { - Log.debug("Error: Invalid meta data") - return + @MainActor func publishMetadata() async throws { + guard let pubKey = publicKeyHex else { + Log.debug("Error: no publicKeyHex") + throw CurrentUserError.authorNotFound + } + guard let pair = keyPair else { + Log.debug("Error: no keyPair") + throw CurrentUserError.authorNotFound + } + guard let context = viewContext else { + Log.debug("Error: no context") + throw CurrentUserError.authorNotFound + } + guard let author = try Author.find(by: pubKey, context: context) else { + Log.debug("Error: no author in DB") + throw CurrentUserError.authorNotFound } - let jsonEvent = JSONEvent(pubKey: pubKey, kind: .metaData, tags: [], content: metaString) + self.author = author + + let jsonObject = buildMetadataJSONObject(author: author) + let data = try JSONSerialization.data(withJSONObject: jsonObject) + guard let content = String(data: data, encoding: .utf8) else { + throw CurrentUserError.encodingError + } - if let pair = keyPair { - do { - try await relayService.publishToAll(event: jsonEvent, signingKey: pair, context: viewContext) - } catch { - Log.debug("failed to update Follows \(error.localizedDescription)") - } + let jsonEvent = JSONEvent( + pubKey: pubKey, + kind: .metaData, + tags: [], + content: content + ) + + do { + try await relayService.publishToAll( + event: jsonEvent, + signingKey: pair, + context: viewContext + ) + } catch { + Log.error(error.localizedDescription) + throw CurrentUserError.errorWhilePublishingToRelays } } diff --git a/Nos/Views/ProfileEdit/CreateUsernameWizard/ExcellentChoiceSheet.swift b/Nos/Views/ProfileEdit/CreateUsernameWizard/ExcellentChoiceSheet.swift index 2ebbebc1b..15b5f952c 100644 --- a/Nos/Views/ProfileEdit/CreateUsernameWizard/ExcellentChoiceSheet.swift +++ b/Nos/Views/ProfileEdit/CreateUsernameWizard/ExcellentChoiceSheet.swift @@ -102,7 +102,11 @@ struct ExcellentChoiceSheet: View { claimState = .claiming + let oldNIP05 = currentUser.author?.nip05 do { + currentUser.author?.nip05 = "\(username)@nos.social" + try currentUser.viewContext.saveIfNeeded() + try await currentUser.publishMetadata() let relays = currentUser.author?.relays.compactMap { $0.addressURL } @@ -111,13 +115,16 @@ struct ExcellentChoiceSheet: View { keyPair: keyPair, relays: relays ?? [] ) - currentUser.author?.nip05 = "\(username)@nos.social" - try currentUser.viewContext.saveIfNeeded() - await currentUser.publishMetaData() claimState = .claimed analytics.registeredNIP05Username() } catch { Log.error(error.localizedDescription) + + // Do our best reverting the changes. + currentUser.author?.nip05 = oldNIP05 + try? currentUser.viewContext.saveIfNeeded() + try? await currentUser.publishMetadata() + claimState = .failed(.unableToClaim(error)) } } diff --git a/Nos/Views/ProfileEdit/CreateUsernameWizard/ExternalNIP05/NiceWorkSheet.swift b/Nos/Views/ProfileEdit/CreateUsernameWizard/ExternalNIP05/NiceWorkSheet.swift index 0cfee79b4..62ec634b6 100644 --- a/Nos/Views/ProfileEdit/CreateUsernameWizard/ExternalNIP05/NiceWorkSheet.swift +++ b/Nos/Views/ProfileEdit/CreateUsernameWizard/ExternalNIP05/NiceWorkSheet.swift @@ -91,14 +91,20 @@ struct NiceWorkSheet: View { connectState = .connecting + let oldNIP05 = currentUser.author?.nip05 do { currentUser.author?.nip05 = "\(username)" try currentUser.viewContext.saveIfNeeded() - await currentUser.publishMetaData() + try await currentUser.publishMetadata() connectState = .connected analytics.linkedNIP05Username() } catch { Log.error(error.localizedDescription) + + // Revert the changes + currentUser.author?.nip05 = oldNIP05 + try? currentUser.viewContext.saveIfNeeded() + connectState = .failed(.unableToConnect(error)) } } diff --git a/Nos/Views/ProfileEdit/DeleteUsernameWizard/ConfirmUsernameDeletionSheet.swift b/Nos/Views/ProfileEdit/DeleteUsernameWizard/ConfirmUsernameDeletionSheet.swift index cce7a5a1f..57c1b079b 100644 --- a/Nos/Views/ProfileEdit/DeleteUsernameWizard/ConfirmUsernameDeletionSheet.swift +++ b/Nos/Views/ProfileEdit/DeleteUsernameWizard/ConfirmUsernameDeletionSheet.swift @@ -1,4 +1,5 @@ import Dependencies +import Logger import SwiftUI struct ConfirmUsernameDeletionSheet: View { @@ -89,21 +90,34 @@ struct ConfirmUsernameDeletionSheet: View { deleteState = .deleting let username = author.nosNIP05Username let isNosSocialUsername = author.hasNosNIP05 - author.nip05 = "" + let oldNIP05 = author.nip05 do { + author.nip05 = "" try viewContext.save() - await currentUser.publishMetaData() + try await currentUser.publishMetadata() if isNosSocialUsername { - try? await namesAPI.delete( - username: username, - keyPair: keyPair - ) + do { + try await namesAPI.delete( + username: username, + keyPair: keyPair + ) + } catch { + Log.debug(error.localizedDescription) + // The delete API could fail if the user didn't have + // connection or the server is down. As we can catch these + // unused usernames later, let the user continue anyway. + } } analytics.deletedNIP05Username() deleteState = .deleted isPresented = false } catch { crashReporting.report(error) + + // Reverting the changes + author.nip05 = oldNIP05 + try? viewContext.save() + deleteState = .failed(.unableToDelete(error)) } } diff --git a/Nos/Views/ProfileEdit/ProfileEditView.swift b/Nos/Views/ProfileEdit/ProfileEditView.swift index d7e321a68..34e11cf82 100644 --- a/Nos/Views/ProfileEdit/ProfileEditView.swift +++ b/Nos/Views/ProfileEdit/ProfileEditView.swift @@ -1,4 +1,5 @@ import Dependencies +import Logger import SwiftUI struct ProfileEditView: View { @@ -20,11 +21,20 @@ struct ProfileEditView: View { @State private var showUniversalNameWizard = false @State private var unsController = UNSWizardController() @State private var showConfirmationDialog = false + @State private var saveError: SaveError? init(author: Author) { self.author = author } - + + private var showAlert: Binding { + Binding { + saveError != nil + } set: { _ in + saveError = nil + } + } + var body: some View { NosForm { AvatarView(imageUrl: URL(string: avatarText), size: 99) @@ -130,11 +140,24 @@ struct ProfileEditView: View { trailing: ActionButton(title: .localizable.done) { await save() - // Go back to profile page - router.pop() } .offset(y: -3) ) + .alert(isPresented: showAlert, error: saveError) { + Button { + saveError = nil + Task { + await save() + } + } label: { + Text(.localizable.retry) + } + Button { + saveError = nil + } label: { + Text(.localizable.cancel) + } + } .id(author) .task { populateTextFields() @@ -153,7 +176,7 @@ struct ProfileEditView: View { unsText = author.uns ?? "" } - func save() async { + private func save() async { author.name = nameText author.about = bioText author.profilePhotoURL = URL(string: avatarText) @@ -161,10 +184,29 @@ struct ProfileEditView: View { author.uns = unsText do { try viewContext.save() - // Post event - await currentUser.publishMetaData() + try await currentUser.publishMetadata() + + // Go back to profile page + router.pop() + } catch CurrentUserError.errorWhilePublishingToRelays { + saveError = SaveError.unableToPublishChanges } catch { crashReporting.report(error) + saveError = SaveError.unexpectedError + } + } + + enum SaveError: LocalizedError { + case unexpectedError + case unableToPublishChanges + + var errorDescription: String? { + switch self { + case .unexpectedError: + return "Something unexpected happened" + case .unableToPublishChanges: + return "We were unable to publish your changes in the network" + } } } }