android, desktop, ios: connect via SimpleX name (#7068)

* android, desktop, ios: connect via SimpleX name

* android, desktop, ios: open known contact on name lookup; surface prepared contact

Name search opens the contact (not list-filter); resolved/prepared contacts and groups are added to the chat list so they're visible and openable. Kotlin compile-verified; iOS edits pattern-matched, pending Xcode build.

* feat(names): UI names role + agent NAME error

Parity with the core names rework (#7045):

- Add `names` to ServerRoles (Android + iOS) and a per-operator
  "To resolve names" toggle under the SMP section (xftp has no names
  role; the shared ServerRoles field stays false there).
- Mirror the new agent error: NameErrorType + a NAME case on both
  AgentErrorType and ProtocolErrorType (the SMP ErrorType mirror), so
  the new SMP/agent NAME errors decode instead of crashing the decoder.
- Remove ChatErrorType.SimplexNameResolverUnavailable (deleted in core)
  and repoint its "name resolution unavailable" alert to the agent
  NAME NO_SERVERS error, reusing the existing strings.

Android (multiplatform) compiles clean; iOS mirrors the same changes
(builds in Xcode).

* feat(names): UI warning when no server resolves names

Mirror core USWNoNamesServers: add the NoNamesServers variant to
UserServersWarning (Kotlin sealed class + Swift enum) and its
globalWarning / globalServersWarning branch, rendered by the existing
ServersWarningFooter / ServersWarningView. Matches the noChatRelays
warning exactly.

* fix(servers): show all validation errors and warnings, not just the first

globalServersError/Warning returned only the first entry, so a second
warning (e.g. no names servers behind no chat relays) or a second error
(e.g. no XFTP servers behind no SMP servers) was never displayed. Make
them return all entries (globalServersErrors/Warnings) and render one
footer row each, across the three combined-footer views. Per-protocol
SMP/XFTP footers are unchanged.

* docs(names): add SimpleX name UI plan

* feat(names): add name model fields + SimplexName helpers

* feat(names): verify + set-name API & responses

* docs(names): bump core sync to 5008b4e62

* feat(names): show name + verification on chat info

* feat(names): add Verify SimpleX names privacy toggle

* feat(names): add set-name screens (user + channel)

* update ui

* fix kotlin

* fix codable

* fix ios

* fix errors

* api in UI

* send name as string in protocol

* update simplexmq, capitalize

* verify that name is in profile for own and known contacts and channels as condition of name resolution

* update simplexmq

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
sh
2026-06-30 11:26:04 +01:00
committed by GitHub
co-authored by Evgeny Poberezkin Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
parent ec8fe669c7
commit 9bd38c4aec
46 changed files with 1371 additions and 207 deletions
+1 -1
View File
@@ -1405,7 +1405,7 @@ data ChatError
| ChatErrorRemoteHost {rhKey :: RHKey, remoteHostError :: RemoteHostError}
deriving (Show, Exception)
-- why a resolved simplex name could not be used (the name itself resolved; an unregistered name is the agent's NAME NOT_FOUND)
-- why a resolved SimpleX name could not be used (the name itself resolved; an unregistered name is the agent's NAME NOT_FOUND)
data SimplexNameError
= SNENoValidLink -- the name's record has no usable contact/channel link
| SNEUnknownName -- the resolved link's profile has no name, or a different name
+32 -14
View File
@@ -1507,7 +1507,7 @@ processChatCommand cxt nm = \case
unless (nameResolvesTo sl nrSimplexContact) $ throwCmdError "name does not point to your address"
pure $ Just (CLShort sl)
_ -> throwCmdError "create the address short link and add it to name"
let p' = (fromLocalProfile p :: Profile) {simplexName = (`SimplexNameClaim` Nothing) <$> name_, contactLink = cl'}
let p' = (fromLocalProfile p :: Profile) {simplexName = mkSimplexNameClaim name_ Nothing, contactLink = cl'}
updateProfile_ user p' True $ withFastStore $ \db -> do
user' <- updateUserProfile db user p'
liftIO $ setUserSimplexName db user' name_
@@ -4207,17 +4207,30 @@ processChatCommand cxt nm = \case
Nothing -> do
l' <- resolveSLink
(FixedLinkData {linkConnReq = cReq, rootKey}, cData) <- getShortLinkConnReq nm user l'
contactSLinkData_ <- mapM linkDataBadge =<< liftIO (decodeLinkUserData cData)
let linkProfile_ = (\ContactShortLinkData {profile} -> profile) <$> contactSLinkData_
linkName_ = linkProfile_ >>= \Profile {simplexName} -> claimName <$> simplexName
verifiedName_ = case nl of CTName ni -> Just ni; _ -> Nothing
refreshContact ct' = case (verifiedName_, linkProfile_) of
(Just _, Just p) -> updateContactFromLinkData user ct' p
_ -> pure ct'
forM_ verifiedName_ $ \ni -> verifyNameClaim ni linkName_
withFastStore' (\db -> getContactWithoutConnViaShortAddress db cxt user l') >>= \case
Just ct' | not (contactDeleted ct') -> pure (con l' cReq, CPContactAddress (CAPContactViaAddress ct'))
Just ct' | not (contactDeleted ct') -> do
ct'' <- refreshContact ct'
pure (con l' cReq, CPContactAddress (CAPContactViaAddress ct''))
_ -> do
contactSLinkData_ <- mapM linkDataBadge =<< liftIO (decodeLinkUserData cData)
let ContactLinkData _ UserContactData {owners} = cData
ov = verifyLinkOwner rootKey owners l' sig_
plan <- contactRequestPlan user cReq contactSLinkData_ ov
case (nl, plan) of
(CTName ni, CPContactAddress cap@(CAPOk (Just ContactShortLinkData {profile = Profile {simplexName}}) _ _)) -> do
_ <- verifyNameClaim ni (claimName <$> simplexName)
pure (con l' cReq, CPContactAddress cap {verifiedName = Just ni})
case plan of
CPContactAddress cap@(CAPOk {}) -> pure (con l' cReq, CPContactAddress cap {verifiedName = verifiedName_})
CPContactAddress (CAPKnown ct') -> do
ct'' <- refreshContact ct'
pure (con l' cReq, CPContactAddress (CAPKnown ct''))
CPContactAddress (CAPContactViaAddress ct') -> do
ct'' <- refreshContact ct'
pure (con l' cReq, CPContactAddress (CAPContactViaAddress ct''))
_ -> pure (con l' cReq, plan)
where
knownLinkPlans = withFastStore $ \db ->
@@ -4265,14 +4278,19 @@ processChatCommand cxt nm = \case
(Nothing, Nothing) -> pure ()
_ -> throwChatError CEInvalidConnReq
let ov = verifyLinkOwner rootKey owners l' sig_
verifiedName_ = case nl of CTName ni -> Just ni; _ -> Nothing
claimedName GroupProfile {publicGroup} = claimName <$> (publicGroup >>= publicGroupAccess >>= publicGroupClaim)
plan <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ ov
case (nl, plan) of
(CTName ni, CPGroupLink glp@(GLPOk (Just _) (Just gld) _ _)) -> do
let GroupShortLinkData {groupProfile = GroupProfile {publicGroup = pg}} = gld
gName = claimName <$> (pg >>= publicGroupAccess >>= publicGroupClaim)
_ <- verifyNameClaim ni gName
pure (con l' cReq, CPGroupLink glp {verifiedName = Just ni})
_ -> pure (con l' cReq, plan)
forM_ verifiedName_ $ \ni ->
verifyNameClaim ni $ case plan of
CPGroupLink (GLPOk _ (Just GroupShortLinkData {groupProfile = gp}) _ _) -> claimedName gp
CPGroupLink (GLPKnown GroupInfo {groupProfile = gp} _ _ _) -> claimedName gp
CPGroupLink (GLPOwnLink GroupInfo {groupProfile = gp}) -> claimedName gp
CPGroupLink (GLPConnectingProhibit (Just GroupInfo {groupProfile = gp})) -> claimedName gp
_ -> maybe Nothing (\GroupShortLinkData {groupProfile = gp} -> claimedName gp) groupSLinkData_
pure $ case plan of
CPGroupLink glp@(GLPOk {}) -> (con l' cReq, CPGroupLink glp {verifiedName = verifiedName_})
_ -> (con l' cReq, plan)
where
unsupportedGroupType = \case
Just GroupShortLinkData {groupProfile = GroupProfile {publicGroup = Just PublicGroupProfile {groupType}}} -> groupType /= GTChannel
+13
View File
@@ -1467,6 +1467,19 @@ updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupSummary = G
Just PublicGroupData {publicMemberCount} -> Just publicMemberCount /= localCount
_ -> False
updateContactFromLinkData :: User -> Contact -> Profile -> CM Contact
updateContactFromLinkData user ct@Contact {contactId, profile = profile@LocalProfile {simplexName = prevClaim, contactDomainVerification}} linkProfile@Profile {simplexName = newClaim}
| profileChanged || verifyChanged = do
cxt <- chatStoreCxt
when profileChanged $ void $ withStore $ \db -> updateContactProfile db cxt user ct linkProfile
when verifyChanged $ withStore' $ \db -> setContactDomainVerified db user contactId True
withStore $ \db -> getContact db cxt user contactId
| otherwise = pure ct
where
profileChanged = fromLocalProfile profile /= linkProfile
claimChanged = (claimName <$> prevClaim) /= (claimName <$> newClaim)
verifyChanged = contactDomainVerification /= Just True || claimChanged
-- TODO [relays] owner: set owners on updating link data (multi-owner)
groupLinkData :: GroupInfo -> GroupLink -> [GroupRelay] -> (UserConnLinkData 'CMContact, CRClientData)
groupLinkData gInfo@GroupInfo {groupProfile, groupSummary = GroupSummary {publicMemberCount}, membership = GroupMember {memberId}, groupKeys} GroupLink {groupLinkId} groupRelays =
+3 -3
View File
@@ -74,16 +74,16 @@ instance ToField NameClaimProof where toField = toField . encodeJSON
instance FromField NameClaimProof where fromField = fromTextField_ decodeJSON
data SimplexNameClaim = SimplexNameClaim
{ name :: SimplexNameInfo,
{ name :: StrJSON "SimplexNameInfo" SimplexNameInfo,
proof :: Maybe NameClaimProof
}
deriving (Eq, Show)
mkSimplexNameClaim :: Maybe SimplexNameInfo -> Maybe NameClaimProof -> Maybe SimplexNameClaim
mkSimplexNameClaim name_ proof_ = (`SimplexNameClaim` proof_) <$> name_
mkSimplexNameClaim name_ proof_ = (\n -> SimplexNameClaim (StrJSON n) proof_) <$> name_
claimName :: SimplexNameClaim -> SimplexNameInfo
claimName (SimplexNameClaim n _) = n
claimName (SimplexNameClaim n _) = unStrJSON n
claimProof :: SimplexNameClaim -> Maybe NameClaimProof
claimProof (SimplexNameClaim _ p) = p
+1 -1
View File
@@ -393,7 +393,7 @@ setUserSimplexName db user@User {userId, profile = p@LocalProfile {profileId}} n
db
"UPDATE contact_profiles SET simplex_name = ?, updated_at = ? WHERE user_id = ? AND contact_profile_id = ?"
(name_, ts, userId, profileId)
pure (user :: User) {profile = p {simplexName = (`SimplexNameClaim` Nothing) <$> name_}}
pure (user :: User) {profile = p {simplexName = mkSimplexNameClaim name_ Nothing}}
setUserProfileContactLink :: DB.Connection -> User -> Maybe UserContactLink -> IO User
setUserProfileContactLink db user@User {userId, profile = p@LocalProfile {profileId}} ucl_ = do
+12 -13
View File
@@ -1128,17 +1128,13 @@ simplexChatContact' = \case
CLFull (CRContactUri crData) -> CLFull $ CRContactUri crData {crScheme = simplexChat}
l@(CLShort _) -> l
shareLinkStr :: Maybe SimplexNameInfo -> B.ByteString -> B.ByteString
shareLinkStr (Just ni) _ = strEncode ni
shareLinkStr Nothing fallback = fallback
groupDomainName :: GroupInfo -> Maybe SimplexNameInfo
groupDomainName GroupInfo {groupProfile = GroupProfile {publicGroup}} =
claimName <$> (publicGroup >>= publicGroupAccess >>= publicGroupClaim)
viewNameVerified :: Maybe SimplexNameInfo -> Maybe Text -> [StyledString]
viewNameVerified name_ result =
let nameStr = maybe "name" (\ni -> "simplex name " <> shortNameInfoStr ni) name_
let nameStr = maybe "name" (\ni -> "SimpleX name " <> shortNameInfoStr ni) name_
in case result of
Nothing -> [plain nameStr <> " verified"]
Just reason -> [plain nameStr <> " not verified: " <> plain reason]
@@ -1155,7 +1151,7 @@ simplexNameStatus (Just ni) status hasProof = case status of
| hasProof -> [line "unverified"]
| otherwise -> []
where
line s = "simplex name: " <> plain (shortNameInfoStr ni) <> " (" <> s <> ")"
line s = "SimpleX name: " <> plain (shortNameInfoStr ni) <> " (" <> s <> ")"
-- TODO [short links] show all settings
viewAddressSettings :: AddressSettings -> [StyledString]
@@ -1174,12 +1170,14 @@ groupLink_ :: StyledString -> GroupInfo -> GroupLink -> [StyledString]
groupLink_ intro g GroupLink {connLinkContact = CCLink cReq shortLink, acceptMemberRole} =
[ intro,
"",
plain $ shareLinkStr (groupDomainName g) $ maybe cReqStr strEncode shortLink,
"",
"Anybody can connect to you and join group as " <> showRole acceptMemberRole <> " with: " <> highlight' "/c <group_link_above>",
"to show it again: " <> highlight ("/show link #" <> viewGroupName g),
"to delete it: " <> highlight ("/delete link #" <> viewGroupName g) <> " (joined members will remain connected to you)"
plain $ maybe cReqStr strEncode shortLink
]
<> ["SimpleX name: " <> plain (shortNameInfoStr ni) | Just ni <- [groupDomainName g]]
<> [ "",
"Anybody can connect to you and join group as " <> showRole acceptMemberRole <> " with: " <> highlight' "/c <group_link_above>",
"to show it again: " <> highlight ("/show link #" <> viewGroupName g),
"to delete it: " <> highlight ("/delete link #" <> viewGroupName g) <> " (joined members will remain connected to you)"
]
<> ["The group link for old clients: " <> plain cReqStr | isJust shortLink]
where
cReqStr = strEncode $ simplexChatContact cReq
@@ -1253,8 +1251,9 @@ viewGroupLinkRelaysUpdated g groupLink relays =
<> map showRelay relays
<>
[ "group link:",
plain $ shareLinkStr (groupDomainName g) $ maybe cReqStr strEncode shortLink
plain $ maybe cReqStr strEncode shortLink
]
<> ["SimpleX name: " <> plain (shortNameInfoStr ni) | Just ni <- [groupDomainName g]]
where
GroupLink {connLinkContact = CCLink cReq shortLink} = groupLink
cReqStr = strEncode $ simplexChatContact cReq
@@ -2686,7 +2685,7 @@ viewChatError isCmd logLevel testView = \case
let reason = case nameErr of
SNENoValidLink -> "has no usable connection link"
SNEUnknownName -> "is not included in the connection link's profile"
in ["simplex name " <> plain (shortNameInfoStr ni) <> " " <> reason]
in ["SimpleX name " <> plain (shortNameInfoStr ni) <> " " <> reason]
CEUnsupportedConnReq -> [ "", "Connection link is not supported by the your app version, please ugrade it.", plain updateStr]
CEInvalidChatMessage Connection {connId} msgMeta_ msg e ->
[ plain $