diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 240f1e2de6..357d3a28c5 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -1496,7 +1496,7 @@ enum NameRegistration: Hashable { } } -// stock derivation cannot read this: the core tags it flat as {"type": ...}, not in swift's nested shape +// stock derivation cannot decode this: the core encodes it with a flat "type" tag, not swift's nested shape extension NameRegistration: Decodable { private enum CodingKeys: String, CodingKey { case type, expires, graceUntil, reservedReason_, pricing, reservedReason diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 99217ccec6..76b6ffb7da 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -1344,7 +1344,7 @@ func recordSimplexNameResolved(_ domain: SimplexDomain, _ reg: NameRegistration? saveNamesResolvedAt(m) } -func forgetSimplexNameResolved(_ fullDomainName: String) { +func clearSimplexNameResolved(_ fullDomainName: String) { var m = simplexNamesResolvedAtDefault.get() if m.removeValue(forKey: fullDomainName) != nil { saveNamesResolvedAt(m) } } @@ -1358,13 +1358,6 @@ private func nameCentsPerYear(_ pricing: NamePricing, _ domain: SimplexDomain) - return "$\(cents / 100)" + (cents % 100 == 0 ? "" : String(format: ".%02lld", cents % 100)) } -private func showNameNotRegisteredAlert() { - showAlert( - NSLocalizedString("Name not registered", comment: "alert title"), - message: NSLocalizedString("This SimpleX name is not registered. Please check the name.", comment: "") - ) -} - private func showNameRegistrationAlert( domain: SimplexDomain, reg: NameRegistration, @@ -1520,6 +1513,7 @@ func planAndConnect( case .contactAddress(.ownLink, _), .groupLink(.ownLink, _): true default: false } + let notConnectable = if case .nameNotConnectable = connectionPlan { true } else { false } let handled = await MainActor.run { var openExisting: (() -> Void)? = nil if let contact = knownContact { @@ -1545,7 +1539,7 @@ func planAndConnect( domain: nameDomain, reg: nameReg, isOwn: isOwnName, - notConnectable: { if case .nameNotConnectable = connectionPlan { true } else { false } }(), + notConnectable: notConnectable, hasLocalChat: knownContact != nil || knownGroup != nil, openExistingChat: openExisting, cleanup: cleanup @@ -1554,19 +1548,12 @@ func planAndConnect( if handled { return } } guard let connectionLink = result.connLink else { - await MainActor.run { - if case .nameNotConnectable = connectionPlan { showNameNotRegisteredAlert() } - cleanup?() - } + await MainActor.run { cleanup?() } return } switch connectionPlan { case .nameNotConnectable: - logger.debug("planAndConnect, .nameNotConnectable") - await MainActor.run { - showNameNotRegisteredAlert() - cleanup?() - } + break case let .invitationLink(ilp): switch ilp { case let .ok(contactSLinkData_, ownerVerification): diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 6ad2667fcd..97d3c715f3 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -205,7 +205,7 @@ struct UserAddressView: View { save: { simplexDomain in do { let u = try await apiSetUserDomain(simplexDomain) - if let d = chatModel.currentUser?.profile.contactDomain?.domain { forgetSimplexNameResolved(d) } + if let d = chatModel.currentUser?.profile.contactDomain?.domain { clearSimplexNameResolved(d) } await MainActor.run { chatModel.updateUser(u) } return true } catch { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index 11d84d727d..db334450ae 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -108,7 +108,7 @@ fun recordSimplexNameResolved(domain: SimplexDomain, reg: NameRegistration?) { saveNamesResolvedAt(m) } -fun forgetSimplexNameResolved(fullDomainName: String) { +fun clearSimplexNameResolved(fullDomainName: String) { val m = loadNamesResolvedAt() if (m.remove(fullDomainName) != null) saveNamesResolvedAt(m) } @@ -254,12 +254,7 @@ private suspend fun planAndConnectTask( val label = if (it.nameType == SimplexNameType.publicGroup) MR.strings.connect_plan_join_name else MR.strings.connect_plan_connect_to_name generalGetString(label).format(it.shortStr) } - val nameReg = when (connectionPlan) { - is ConnectionPlan.NameNotConnectable -> connectionPlan.nameRegistration - is ConnectionPlan.ContactAddress -> connectionPlan.nameRegistration_ - is ConnectionPlan.GroupLink -> connectionPlan.nameRegistration_ - else -> null - } + val nameReg = connectionPlan.nameRegistration() val nameDomain = when (connectionPlan) { is ConnectionPlan.NameNotConnectable -> connectionPlan.simplexDomain else -> planSimplexName?.nameDomain @@ -290,24 +285,11 @@ private suspend fun planAndConnectTask( if (handled) return completable } val connectionLink = connectionLink_ ?: run { - when (connectionPlan) { - is ConnectionPlan.NameNotConnectable -> AlertManager.privacySensitive.showAlertMsg( - generalGetString(MR.strings.simplex_name_not_registered), - generalGetString(MR.strings.simplex_name_not_found_desc) - ) - else -> {} - } cleanup() return completable } when (connectionPlan) { - is ConnectionPlan.NameNotConnectable -> { - AlertManager.privacySensitive.showAlertMsg( - generalGetString(MR.strings.simplex_name_not_registered), - generalGetString(MR.strings.simplex_name_not_found_desc) - ) - cleanup() - } + is ConnectionPlan.NameNotConnectable -> {} is ConnectionPlan.InvitationLink -> when (connectionPlan.invitationLinkPlan) { is InvitationLinkPlan.Ok -> if (connectionPlan.invitationLinkPlan.contactSLinkData_ != null) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index a18bc4faee..401d241220 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -388,7 +388,7 @@ private fun UserAddressLayout( save = { simplexDomain -> try { val u = chatModel.controller.apiSetUserDomain(user?.remoteHostId, simplexDomain) - domain?.let { forgetSimplexNameResolved(it) } + domain?.let { clearSimplexNameResolved(it) } withContext(Dispatchers.Main) { chatModel.updateUser(u) } true } catch (e: Exception) { diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index f4e95ff885..2d366a20f0 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -1150,9 +1150,9 @@ data ChatDeleteMode data ConnectionPlan = CPInvitationLink {invitationLinkPlan :: InvitationLinkPlan} - | CPContactAddress {contactAddressPlan :: ContactAddressPlan, nameRegistration_ :: Maybe NameRegistration} -- nameRegistration_ is set when the target was a name + | CPContactAddress {contactAddressPlan :: ContactAddressPlan, nameRegistration_ :: Maybe NameRegistration} | CPGroupLink {groupLinkPlan :: GroupLinkPlan, nameRegistration_ :: Maybe NameRegistration} - | CPNameNotConnectable {simplexDomain :: SimplexDomain, nameRegistration :: NameRegistration} -- the name is not registered, expired or has no usable link, and no local chat has it + | CPNameNotConnectable {simplexDomain :: SimplexDomain, nameRegistration :: NameRegistration} | CPError {chatError :: ChatError} deriving (Show) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 123eb573d9..e75bd1e3d1 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -4419,14 +4419,13 @@ processChatCommand cxt nm = \case tryAllErrors (resolveNameRegistration user nm d) >>= \case Right reg -> do expired <- nameExpired reg - case reg of - NRRegistered {nameRecord = nr} - | not expired && isJust (firstNameLink CCTChannel (nrSimplexChannel nr)) -> - (setPlanRegistration reg <$> connectPlanName NTPublicGroup (Right reg)) `catchAllErrors` \e -> - (setPlanRegistration reg <$> connectPlanName NTContact (Right reg) `catchAllErrors` \_ -> throwError e) - | not expired && isJust (firstNameLink CCTContact (nrSimplexContact nr)) -> - setPlanRegistration reg <$> connectPlanName NTContact (Right reg) - _ -> connectPlanLocal reg + let connectable nameType = not expired && nameHasLink nameType reg + if + | connectable NTPublicGroup -> + (setPlanRegistration reg <$> connectPlanName NTPublicGroup (Right reg)) `catchAllErrors` \e -> + (setPlanRegistration reg <$> connectPlanName NTContact (Right reg) `catchAllErrors` \_ -> throwError e) + | connectable NTContact -> setPlanRegistration reg <$> connectPlanName NTContact (Right reg) + | otherwise -> connectPlanLocal reg Left e -> connectPlanNoName e where connectPlanLocal reg = @@ -4438,10 +4437,9 @@ processChatCommand cxt nm = \case connectPlanName nameType nr_ = setOtherName <$> connectPlan user (nameTarget nameType) resolveMode sig_ (Just nr_) where setOtherName (l, planName, _, p) = (l, planName, otherName, p) - otherName = case nr_ of - Right NRRegistered {nameRecord = NameRecord {nrSimplexContact, nrSimplexChannel}} -> case nameType of - NTContact -> SimplexNameInfo NTPublicGroup d <$ firstNameLink CCTChannel nrSimplexChannel - NTPublicGroup -> SimplexNameInfo NTContact d <$ firstNameLink CCTContact nrSimplexContact + otherName = case (nameType, nr_) of + (NTContact, Right reg) | nameHasLink NTPublicGroup reg -> Just $ SimplexNameInfo NTPublicGroup d + (NTPublicGroup, Right reg) | nameHasLink NTContact reg -> Just $ SimplexNameInfo NTContact d _ -> Nothing nameTarget nameType = ACTarget SCMContact $ CTShortContact $ CTName $ SimplexNameInfo nameType d connectPlanNoName e = @@ -4454,10 +4452,10 @@ processChatCommand cxt nm = \case | CTName ni <- nl, isNothing nameRec, resolveMode /= PRMNever -> do reg <- resolveNameRegistration user nm (nameDomain ni) expired <- nameExpired reg - if nameHasLink ni reg && not expired - then setPlanRegistration reg <$> connectPlan user (ACTarget SCMContact (CTShortContact nl)) resolveMode sig_ (Just (Right reg)) + if nameHasLink (nameType ni) reg && not expired + then setPlanRegistration reg <$> connectPlan user (ACTarget SCMContact ct) resolveMode sig_ (Just (Right reg)) else - (setPlanRegistration reg <$> connectPlan user (ACTarget SCMContact (CTShortContact nl)) PRMNever sig_ (Just (Right reg))) + (setPlanRegistration reg <$> connectPlan user (ACTarget SCMContact ct) PRMNever sig_ (Just (Right reg))) `catchAllErrors` \_ -> pure (Nothing, Nothing, Nothing, CPNameNotConnectable (nameDomain ni) reg) CTShortContact nl -> (\(l, p) -> (Just l, simplexName_, Nothing, p)) <$> case ctType of @@ -5124,8 +5122,8 @@ nameExpired = \case NRRegistered {expires = Just expires} -> (expires <) <$> liftIO getSystemSeconds _ -> pure False -nameHasLink :: SimplexNameInfo -> NameRegistration -> Bool -nameHasLink SimplexNameInfo {nameType} = \case +nameHasLink :: SimplexNameType -> NameRegistration -> Bool +nameHasLink nameType = \case NRRegistered {nameRecord = NameRecord {nrSimplexContact, nrSimplexChannel}} -> case nameType of NTContact -> isJust (firstNameLink CCTContact nrSimplexContact) NTPublicGroup -> isJust (firstNameLink CCTChannel nrSimplexChannel) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index ad14a6a5f4..0438f3359a 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -2250,11 +2250,10 @@ viewNameRegistration = \case ["registered" <> expiryNote expires graceUntil <> maybe "" ((", reserved: " <>) . plain . textEncode) reservedReason_] Just NRAvailable {pricing = NamePricing {basePrice = USDCents c, minLabelLength}} -> ["available: " <> plain (show c) <> " cents/year, min length " <> plain (show minLabelLength)] - Just NRReserved {reservedReason} -> ["reserved: " <> plain (textEncode reservedReason :: Text)] + Just NRReserved {reservedReason} -> ["reserved: " <> plain (textEncode reservedReason)] Nothing -> [] - expiryNote expires graceUntil = case expires of - Nothing -> "" - Just e -> ", expires " <> plain (show (roundedSeconds e)) <> maybe "" (\g -> ", grace until " <> plain (show (roundedSeconds g))) graceUntil + expiryNote expires graceUntil = maybe "" (\e -> ", expires " <> showTime e <> maybe "" ((", grace until " <>) . showTime) graceUntil) expires + showTime = plain . show . roundedSeconds viewConnectionPlan :: ChatConfig -> Maybe ACreatedConnLink -> ConnectionPlan -> [StyledString] viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case @@ -2277,10 +2276,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case | business -> ("business address: " <>) _ -> ("invitation link: " <>) CPContactAddress cap _ -> case cap of - CAPOk contactSLinkData ov addressChanged -> - [addrOrBiz contactSLinkData (if addressChanged then "ok to connect, address changed" else "ok to connect")] - <> viewSigVerification ov - <> [viewJSON contactSLinkData | testView] + CAPOk contactSLinkData ov addressChanged -> [addrOrBiz contactSLinkData ("ok to connect" <> (if addressChanged then ", address changed" else ""))] <> viewSigVerification ov <> [viewJSON contactSLinkData | testView] CAPOwnLink -> [ctAddr "own address"] CAPConnectingConfirmReconnect -> [ctAddr "connecting, allowed to reconnect"] CAPConnectingProhibit ct -> [ctAddr ("connecting to contact " <> ttyContact' ct)] diff --git a/tests/ChatTests/Names.hs b/tests/ChatTests/Names.hs index ef8a35b6a2..4269477eb0 100644 --- a/tests/ChatTests/Names.hs +++ b/tests/ChatTests/Names.hs @@ -48,7 +48,7 @@ testConnectByName :: HasCallStack => TestParams -> IO () testConnectByName ps = withSmpServerAndNames $ \reg -> testChat2 aliceProfile bobProfile (test reg) ps where - aliceName = aliceSimplexName + aliceName = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "alice" []) test reg alice bob = do mapM_ enableNamesRole [alice, bob] alice ##> "/ad" @@ -393,7 +393,7 @@ testPlanNameReservedOther = withAliceName $ \reg _l _alice bob -> do testPlanNameNoValidLink :: HasCallStack => TestParams -> IO () testPlanNameNoValidLink = withAliceName $ \reg _l _alice bob -> do - registerName reg boogaloo (emptyNameRecord "boogaloo.simplex") + registerName reg boogaloo (emptyRecord "boogaloo.simplex") bob ##> "/_connect plan 1 @boogaloo.simplex" bob <## "SimpleX name boogaloo.simplex: nothing to connect to" bob <## "registered" diff --git a/tests/NameResolver.hs b/tests/NameResolver.hs index 9cfdfd96ae..9b13e2f918 100644 --- a/tests/NameResolver.hs +++ b/tests/NameResolver.hs @@ -15,7 +15,7 @@ module NameResolver registerAvailableName, unregisterName, failNameResolution, - emptyNameRecord, + emptyRecord, contactNameRecord, channelNameRecord, contactAndChannelNameRecord, @@ -61,16 +61,14 @@ withNameResolver action = do Just (AnswerRegistration registration) -> (ok200, J.encode NameResponse {lastBlockTs = Nothing, registration}) Nothing -> (ok200, J.encode NameResponse {lastBlockTs = Nothing, registration = NRAvailable {pricing = testPricing 1}}) --- | Register a name's domain to resolve to the given record, as a live registration. +-- | Register a name's domain to resolve to the given record. registerName :: NameRegistry -> SimplexNameInfo -> NameRecord -> IO () registerName reg ni nameRecord = registerRegistration reg ni NRRegistered {expires = Nothing, graceUntil = Nothing, reservedReason_ = Nothing, nameRecord} --- | Register any registration the registry could answer with. registerRegistration :: NameRegistry -> SimplexNameInfo -> NameRegistration -> IO () registerRegistration reg ni r = atomically $ modifyTVar' reg $ M.insert (registryKey ni) (AnswerRegistration r) --- | A name that expired a day ago, renewable by its owner for another 30 days. registerExpiredName :: NameRegistry -> SimplexNameInfo -> NameRecord -> IO () registerExpiredName reg ni nameRecord = do RoundedSystemTime now <- getSystemSeconds @@ -78,19 +76,15 @@ registerExpiredName reg ni nameRecord = do graceUntil = Just $ RoundedSystemTime (now + 30 * 86400) registerRegistration reg ni NRRegistered {expires, graceUntil, reservedReason_ = Nothing, nameRecord} --- | A name the registry holds back. registerReservedName :: NameRegistry -> SimplexNameInfo -> NameReservedReason -> IO () registerReservedName reg ni reservedReason = registerRegistration reg ni NRReserved {reservedReason} --- | A name that is free, priced with the given minimum label length. registerAvailableName :: NameRegistry -> SimplexNameInfo -> Int -> IO () registerAvailableName reg ni minLen = registerRegistration reg ni NRAvailable {pricing = testPricing minLen} --- | Make the resolver fail for this name, as a registry that is down or erroring would. failNameResolution :: NameRegistry -> SimplexNameInfo -> IO () failNameResolution reg ni = atomically $ modifyTVar' reg $ M.insert (registryKey ni) AnswerFails --- | Drop a name, so it resolves as available again. unregisterName :: NameRegistry -> SimplexNameInfo -> IO () unregisterName reg ni = atomically $ modifyTVar' reg $ M.delete (registryKey ni) @@ -112,10 +106,6 @@ contactAndChannelNameRecord :: Text -> Text -> Text -> NameRecord contactAndChannelNameRecord name contactLink channelLink = (emptyRecord name) {nrSimplexContact = [contactLink], nrSimplexChannel = [channelLink]} --- | A registered name whose record holds no usable link. -emptyNameRecord :: Text -> NameRecord -emptyNameRecord = emptyRecord - emptyRecord :: Text -> NameRecord emptyRecord name = NameRecord