From e9a3eb8f100c8c6512f3e66d1a781996778a80fb Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:31:05 +0000 Subject: [PATCH] core: fix retrying connection via contact card (preset contacts) (#6104) * core: fix retrying connection via contact card (preset contacts) * ios * kotlin * banner --- .../Views/Contacts/ContactListNavLink.swift | 10 ++- apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 ++--- apps/ios/SimpleXChat/ChatTypes.swift | 2 +- .../chat/simplex/common/model/ChatModel.kt | 2 +- .../views/chatlist/ChatListNavLinkView.kt | 62 ++++++++++++------- .../views/contacts/ContactPreviewView.kt | 3 + .../commonMain/resources/MR/base/strings.xml | 1 + src/Simplex/Chat/Library/Commands.hs | 49 ++++++++++----- src/Simplex/Chat/Library/Subscriber.hs | 1 + 9 files changed, 95 insertions(+), 51 deletions(-) diff --git a/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift index d231f0d8aa..253dca67c5 100644 --- a/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift +++ b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift @@ -208,8 +208,14 @@ struct ContactListNavLink: View { .tint(.red) } .confirmationDialog("Connect with \(contact.chatViewName)", isPresented: $showConnectContactViaAddressDialog, titleVisibility: .visible) { - Button("Use current profile") { connectContactViaAddress_(contact, false) } - Button("Use new incognito profile") { connectContactViaAddress_(contact, true) } + if !contact.profileChangeProhibited { + Button("Use current profile") { connectContactViaAddress_(contact, false) } + Button("Use new incognito profile") { connectContactViaAddress_(contact, true) } + } else if !contact.contactConnIncognito { + Button("Use current profile") { connectContactViaAddress_(contact, false) } + } else { + Button("Use incognito profile") { connectContactViaAddress_(contact, true) } + } } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 086eba9413..dbd86c4406 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -178,8 +178,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.7-HtMnGcLT3joL6B8buibOdF-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.7-HtMnGcLT3joL6B8buibOdF-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.7-HtMnGcLT3joL6B8buibOdF.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.7-HtMnGcLT3joL6B8buibOdF.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.0-CLkstteZ5zsL7AgM2nJh38-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.0-CLkstteZ5zsL7AgM2nJh38-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.0-CLkstteZ5zsL7AgM2nJh38.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.0-CLkstteZ5zsL7AgM2nJh38.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -543,8 +543,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.7-HtMnGcLT3joL6B8buibOdF-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.7-HtMnGcLT3joL6B8buibOdF-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.7-HtMnGcLT3joL6B8buibOdF.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.7-HtMnGcLT3joL6B8buibOdF.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.0-CLkstteZ5zsL7AgM2nJh38-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.1.0-CLkstteZ5zsL7AgM2nJh38-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.0-CLkstteZ5zsL7AgM2nJh38.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.1.0-CLkstteZ5zsL7AgM2nJh38.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -704,8 +704,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.7-HtMnGcLT3joL6B8buibOdF-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.7-HtMnGcLT3joL6B8buibOdF.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.0-CLkstteZ5zsL7AgM2nJh38-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.0-CLkstteZ5zsL7AgM2nJh38.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -790,8 +790,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.7-HtMnGcLT3joL6B8buibOdF-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.7-HtMnGcLT3joL6B8buibOdF.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.0-CLkstteZ5zsL7AgM2nJh38-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.0-CLkstteZ5zsL7AgM2nJh38.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 4e73795546..9b7f4ac5ee 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1793,7 +1793,7 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { } public var isContactCard: Bool { - activeConn == nil && profile.contactLink != nil && active && preparedContact == nil && contactRequestId == nil + (activeConn == nil || activeConn?.connStatus == .prepared) && profile.contactLink != nil && active && preparedContact == nil && contactRequestId == nil } public var contactConnIncognito: Bool { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 779e6ec88b..a7ede11160 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1778,7 +1778,7 @@ data class Contact( } val isContactCard: Boolean = - activeConn == null && profile.contactLink != null && active && preparedContact == null && contactRequestId == null + (activeConn == null || activeConn.connStatus == ConnStatus.Prepared) && profile.contactLink != null && active && preparedContact == null && contactRequestId == null val contactConnIncognito = activeConn?.customUserProfileId != null diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 4f06277e6f..aa74596361 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -794,33 +794,49 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( close: (() -> Unit)?, openChat: Boolean ) { + @Composable + fun UseCurrentProfileButton() { + SectionItemView({ + AlertManager.privacySensitive.hideAlert() + withBGApi { + val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = false) + if (ok && openChat) { + close?.invoke() + openDirectChat(rhId, contact.contactId) + } + } + }) { + Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + + @Composable + fun UseIncognitoProfileButton(text: String) { + SectionItemView({ + AlertManager.privacySensitive.hideAlert() + withBGApi { + val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = true) + if (ok && openChat) { + close?.invoke() + openDirectChat(rhId, contact.contactId) + } + } + }) { + Text(text, Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + AlertManager.privacySensitive.showAlertDialogButtonsColumn( title = String.format(generalGetString(MR.strings.connect_with_contact_name_question), contact.chatViewName), buttons = { Column { - SectionItemView({ - AlertManager.privacySensitive.hideAlert() - withBGApi { - close?.invoke() - val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = false) - if (ok && openChat) { - openDirectChat(rhId, contact.contactId) - } - } - }) { - Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - SectionItemView({ - AlertManager.privacySensitive.hideAlert() - withBGApi { - close?.invoke() - val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = true) - if (ok && openChat) { - openDirectChat(rhId, contact.contactId) - } - } - }) { - Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + if (!contact.profileChangeProhibited) { + UseCurrentProfileButton() + UseIncognitoProfileButton(generalGetString(MR.strings.connect_use_new_incognito_profile)) + } else if (!contact.contactConnIncognito) { + UseCurrentProfileButton() + } else { + UseIncognitoProfileButton(generalGetString(MR.strings.connect_use_incognito_profile)) } SectionItemView({ AlertManager.privacySensitive.hideAlert() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt index 90e23e1cb5..b86f6d7a3e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt @@ -104,6 +104,9 @@ fun ContactPreviewView( modifier = Modifier .size(21.dp) ) + if (chat.chatInfo.incognito) { + Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) + } } if (showDeletedChatIcon && chat.chatInfo.chatDeleted) { diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index af51bd5ba6..98c2b7235e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -9,6 +9,7 @@ Join group? Use current profile Use new incognito profile + Use incognito profile Your profile will be sent to the contact that you received this link from. You will connect to all group members. Connect diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 0066bc1ee9..170684959f 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1909,14 +1909,18 @@ processChatCommand vr nm = \case Connect _ Nothing -> throwChatError CEInvalidConnReq APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do ct@Contact {activeConn, profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db vr user contactId - when (isJust activeConn) $ throwCmdError "contact already has connection" ccLink <- case contactLink of Just (CLFull cReq) -> pure $ CCLink cReq Nothing Just (CLShort sLnk) -> do (cReq, _cData) <- getShortLinkConnReq user sLnk pure $ CCLink cReq $ Just sLnk Nothing -> throwCmdError "no address in contact profile" - connectContactViaAddress user incognito ct ccLink + connectContactViaAddress user incognito ct ccLink `catchChatError` \e -> do + -- get updated contact, in case connection was started - in UI it would lock ability to change incognito choice + -- on next connection attempt, in case server received request while client got network error + ct' <- withFastStore $ \db -> getContact db vr user contactId + toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct') + throwError e ConnectSimplex incognito -> withUser $ \user -> do plan <- contactRequestPlan user adminContactReq Nothing `catchChatError` const (pure $ CPContactAddress (CAPOk Nothing)) connectWithPlan user incognito (ACCL SCMContact (CCLink adminContactReq Nothing)) plan @@ -3021,7 +3025,6 @@ processChatCommand vr nm = \case let incognitoProfile = fromLocalProfile <$> localIncognitoProfile conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ inGroup PQSupportOn pure $ CVRSentInvitation conn' incognitoProfile - mkXContactId = maybe (XContactId <$> drgRandomBytes 16) pure connect' groupLinkId cReqHash xContactId_ = do let inGroup = isJust groupLinkId pqSup = if inGroup then PQSupportOff else PQSupportOn @@ -3035,19 +3038,31 @@ processChatCommand vr nm = \case conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ inGroup pqSup pure $ CVRSentInvitation conn' incognitoProfile connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> CreatedLinkContact -> CM ChatResponse - connectContactViaAddress user@User {userId} incognito ct@Contact {contactId} (CCLink cReq shortLink) = - withInvitationLock "connectContactViaAddress" (strEncode cReq) $ do - newXContactId <- XContactId <$> drgRandomBytes 16 - let pqSup = PQSupportOn - (connId, chatV) <- prepareContact user cReq pqSup - let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq - -- [incognito] generate profile to send - incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - subMode <- chatReadVar subscriptionMode - conn <- withFastStore' $ \db -> createConnReqConnection db userId connId (Just $ PCEContact ct) cReqHash shortLink newXContactId incognitoProfile Nothing subMode chatV pqSup - void $ joinContact user conn cReq incognitoProfile newXContactId Nothing Nothing False pqSup - ct' <- withStore $ \db -> getContact db vr user contactId - pure $ CRSentInvitationToContact user ct' incognitoProfile + connectContactViaAddress user@User {userId} incognito ct@Contact {contactId, activeConn} (CCLink cReq shortLink) = + withInvitationLock "connectContactViaAddress" (strEncode cReq) $ + case activeConn of + Nothing -> do + let pqSup = PQSupportOn + (connId, chatV) <- prepareContact user cReq pqSup + newXContactId <- XContactId <$> drgRandomBytes 16 + -- [incognito] generate profile to send + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + subMode <- chatReadVar subscriptionMode + let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq + conn <- withFastStore' $ \db -> createConnReqConnection db userId connId (Just $ PCEContact ct) cReqHash shortLink newXContactId incognitoProfile Nothing subMode chatV pqSup + void $ joinContact user conn cReq incognitoProfile newXContactId Nothing Nothing False pqSup + ct' <- withStore $ \db -> getContact db vr user contactId + pure $ CRSentInvitationToContact user ct' incognitoProfile + Just conn@Connection {connStatus, xContactId = xContactId_, customUserProfileId} -> case connStatus of + ConnPrepared -> do + when (incognito /= isJust customUserProfileId) $ throwCmdError "incognito mode is different from prepared connection" + xContactId <- mkXContactId xContactId_ + localIncognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId + let incognitoProfile = fromLocalProfile <$> localIncognitoProfile + void $ joinContact user conn cReq incognitoProfile xContactId Nothing Nothing False PQSupportOn + ct' <- withStore $ \db -> getContact db vr user contactId + pure $ CRSentInvitationToContact user ct' incognitoProfile + _ -> throwCmdError "contact already has connection" prepareContact :: User -> ConnReqContact -> PQSupport -> CM (ConnId, VersionChat) prepareContact user cReq pqSup = do -- 0) toggle disabled - PQSupportOff @@ -3059,6 +3074,8 @@ processChatCommand vr nm = \case let chatV = agentToChatVersion agentV connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup pure (connId, chatV) + mkXContactId :: Maybe XContactId -> CM XContactId + mkXContactId = maybe (XContactId <$> drgRandomBytes 16) pure joinContact :: User -> Connection -> ConnReqContact -> Maybe Profile -> XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Bool -> PQSupport -> CM Connection joinContact user conn@Connection {connChatVersion = chatV} cReq incognitoProfile xContactId welcomeSharedMsgId msg_ inGroup pqSup = do let profileToSend = diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 5c90554e53..5166ce7ecb 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -546,6 +546,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (gInfo, host) <- withStore $ \db -> do liftIO $ deleteContactCardKeepConn db connId ct createGroupInvitedViaLink db vr user conn'' glInv + void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart) -- [incognito] send saved profile incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId) let profileToSend = userProfileInGroup user (fromLocalProfile <$> incognitoProfile)