diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt index a97d59138..c3c35aa7a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt @@ -196,8 +196,8 @@ fun ChatInfoLayout( } ContactPreferencesButton(openPreferences) } - SectionDividerSpaced() + if (contact.contactLink != null) { val context = LocalContext.current SectionView(stringResource(R.string.address_section_title).uppercase()) { @@ -208,6 +208,17 @@ fun ChatInfoLayout( SectionDividerSpaced() } + if (developerTools) { + SectionView(title = stringResource(R.string.contact_info_section_title_contact)) { + val connLevel = contact.activeConn.connLevel + val connLevelDesc = + if (connLevel == 0) stringResource(R.string.conn_level_desc_direct) + else String.format(generalGetString(R.string.conn_level_desc_indirect), connLevel) + InfoRow(stringResource(R.string.info_row_connection), connLevelDesc) + } + SectionDividerSpaced() + } + SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) { SwitchAddressButton(switchContactAddress) if (connStats != null) { diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 2017bae84..5f6094f3a 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -1140,6 +1140,7 @@ Member will be removed from group - this cannot be undone! Remove MEMBER + CONTACT Role Change role Change diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index b72006b3f..2e083d86e 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -131,6 +131,14 @@ struct ChatInfoView: View { } } + if developerTools { + Section("Contact") { + let connLevel = contact.activeConn.connLevel + let connLevelDesc = connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), connLevel) + infoRow("Connection", connLevelDesc) + } + } + Section("Servers") { networkStatusRow() .onTapGesture { diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 4b6b5d259..f344d24cb 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -2656,7 +2656,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do _ -> Nothing processDirectMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> Maybe Contact -> m () - processDirectMessage agentMsg connEntity conn@Connection {connId, viaUserContactLink, groupLinkId, customUserProfileId} = \case + processDirectMessage agentMsg connEntity conn@Connection {connId, viaUserContactLink, customUserProfileId} = \case Nothing -> case agentMsg of CONF confId _ connInfo -> do -- [incognito] send saved profile @@ -2758,7 +2758,6 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do whenUserNtfs user $ do setActive $ ActiveC c showToast (c <> "> ") "connected" - forM_ groupLinkId $ \_ -> probeMatchingContacts ct $ contactConnIncognito ct forM_ viaUserContactLink $ \userContactLinkId -> withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case Just (UserContactLink {autoAccept = Just AutoAccept {autoReply = mc_}}, groupId_, gLinkMemRole) -> do @@ -2903,6 +2902,11 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do whenUserNtfs user $ do setActive $ ActiveG gName showToast ("#" <> gName) "you are connected to group" + withStore' (\db -> getContactViaMember db user m) >>= \case + Nothing -> messageWarning "connected host does not have contact" + Just ct@Contact {activeConn = Connection {groupLinkId}} -> do + let connectedIncognito = contactConnIncognito ct || memberIncognito membership + forM_ groupLinkId $ \_ -> probeMatchingContacts ct connectedIncognito GCInviteeMember -> do memberConnectedChatItem gInfo m toView $ CRJoinedGroupMember user gInfo m {memberStatus = GSMemConnected} @@ -3825,8 +3829,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do probeMatch c1@Contact {contactId = cId1, profile = p1} c2@Contact {contactId = cId2, profile = p2} probe = if profilesMatch (fromLocalProfile p1) (fromLocalProfile p2) && cId1 /= cId2 then do - void . sendDirectContactMessage c1 $ XInfoProbeOk probe - mergeContacts c1 c2 + let (toCt, fromCt) = mergeToFromContacts c1 c2 + void . sendDirectContactMessage toCt $ XInfoProbeOk probe + mergeContacts toCt fromCt else messageWarning "probeMatch ignored: profiles don't match or same contact id" xInfoProbeOk :: Contact -> Probe -> m () @@ -3834,9 +3839,22 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do r <- withStore' $ \db -> matchSentProbe db user c1 probe forM_ r $ \c2@Contact {contactId = cId2} -> if cId1 /= cId2 - then mergeContacts c1 c2 + then do + let (toCt, fromCt) = mergeToFromContacts c1 c2 + mergeContacts toCt fromCt else messageWarning "xInfoProbeOk ignored: same contact id" + mergeToFromContacts :: Contact -> Contact -> (Contact, Contact) + mergeToFromContacts c1 c2 + | d1 && not d2 = (c1, c2) + | d2 && not d1 = (c2, c1) + | ctCreatedAt c1 <= ctCreatedAt c2 = (c1, c2) + | otherwise = (c2, c1) + where + d1 = directOrUsed c1 + d2 = directOrUsed c2 + ctCreatedAt Contact {createdAt} = createdAt + -- to party accepting call xCallInv :: Contact -> CallId -> CallInvitation -> RcvMessage -> MsgMeta -> m () xCallInv ct@Contact {contactId} callId CallInvitation {callType, callDhPubKey} msg@RcvMessage {sharedMsgId_} msgMeta = do @@ -3943,9 +3961,10 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do messageError $ eventName <> ": wrong call state " <> T.pack (show $ callStateTag callState) mergeContacts :: Contact -> Contact -> m () - mergeContacts c1 c2 = do - withStore' $ \db -> mergeContactRecords db userId c1 c2 - toView $ CRContactsMerged user c1 c2 + mergeContacts toCt fromCt = do + deleteAgentConnectionAsync user $ aConnId $ contactConn fromCt + withStore' $ \db -> mergeContactRecords db userId toCt fromCt + toView $ CRContactsMerged user toCt fromCt saveConnInfo :: Connection -> ConnInfo -> m () saveConnInfo activeConn connInfo = do diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 12ac37032..e6b7a3805 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -1708,9 +1708,8 @@ matchSentProbe db user@User {userId} _from@Contact {contactId} (Probe probe) = d cId : _ -> eitherToMaybe <$> runExceptT (getContact db user cId) mergeContactRecords :: DB.Connection -> UserId -> Contact -> Contact -> IO () -mergeContactRecords db userId ct1 ct2 = do - let (toCt, fromCt) = toFromContacts ct1 ct2 - Contact {contactId = toContactId} = toCt +mergeContactRecords db userId toCt fromCt = do + let Contact {contactId = toContactId} = toCt Contact {contactId = fromContactId, localDisplayName} = fromCt currentTs <- getCurrentTime -- TODO next query fixes incorrect unused contacts deletion; consider more thorough fix @@ -1719,10 +1718,6 @@ mergeContactRecords db userId ct1 ct2 = do db "UPDATE contacts SET contact_used = 1, updated_at = ? WHERE user_id = ? AND contact_id = ?" (currentTs, userId, toContactId) - DB.execute - db - "UPDATE connections SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?" - (toContactId, currentTs, fromContactId, userId) DB.execute db "UPDATE connections SET via_contact = ?, updated_at = ? WHERE via_contact = ? AND user_id = ?" @@ -1735,6 +1730,10 @@ mergeContactRecords db userId ct1 ct2 = do db "UPDATE chat_items SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?" (toContactId, currentTs, fromContactId, userId) + DB.execute + db + "UPDATE chat_item_reactions SET contact_id = ?, updated_at = ? WHERE contact_id = ?" + (toContactId, currentTs, fromContactId) DB.executeNamed db [sql| @@ -1754,17 +1753,6 @@ mergeContactRecords db userId ct1 ct2 = do deleteContactProfile_ db userId fromContactId DB.execute db "DELETE FROM contacts WHERE contact_id = ? AND user_id = ?" (fromContactId, userId) DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (localDisplayName, userId) - where - toFromContacts :: Contact -> Contact -> (Contact, Contact) - toFromContacts c1 c2 - | d1 && not d2 = (c1, c2) - | d2 && not d1 = (c2, c1) - | ctCreatedAt c1 <= ctCreatedAt c2 = (c1, c2) - | otherwise = (c2, c1) - where - d1 = directOrUsed c1 - d2 = directOrUsed c2 - ctCreatedAt Contact {createdAt} = createdAt getConnectionEntity :: DB.Connection -> User -> AgentConnId -> ExceptT StoreError IO ConnectionEntity getConnectionEntity db user@User {userId, userContactId} agentConnId = do @@ -2364,6 +2352,7 @@ createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Co :. (userId, localDisplayName, contactId, localProfileId profile, createdAt, createdAt) ) +-- this method differs from getViaGroupContact in that it does not join with groups on contacts.via_group getContactViaMember :: DB.Connection -> User -> GroupMember -> IO (Maybe Contact) getContactViaMember db user@User {userId} GroupMember {groupMemberId} = maybeFirstRow (toContact user) $ diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index c13318250..70fb01a13 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -116,11 +116,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, testView} liveItems ts CRGroupCreated u g -> ttyUser u $ viewGroupCreated g CRGroupMembers u g -> ttyUser u $ viewGroupMembers g CRGroupsList u gs -> ttyUser u $ viewGroupsList gs - CRSentGroupInvitation u g c _ -> - ttyUser u $ - if viaGroupLink . contactConn $ c - then [ttyContact' c <> " invited to group " <> ttyGroup' g <> " via your group link"] - else ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c] + CRSentGroupInvitation u g c _ -> ttyUser u ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c] CRFileTransferStatus u ftStatus -> ttyUser u $ viewFileTransferStatus ftStatus CRFileTransferStatusXFTP u ci -> ttyUser u $ viewFileTransferStatusXFTP ci CRUserProfile u p -> ttyUser u $ viewUserProfile p diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 305f1620c..860826e8c 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -1544,7 +1544,7 @@ testGroupLink = concurrentlyN_ [ do alice <## "bob (Bob): contact is connected" - alice <## "bob invited to group #team via your group link" + alice <## "invitation to join the group #team sent to bob" alice <## "#team: bob joined the group", do bob <## "alice (Alice): contact is connected" @@ -1580,7 +1580,7 @@ testGroupLink = <### [ "cath_1 (Catherine): contact is connected", "contact cath_1 is merged into cath", "use @cath to send messages", - EndsWith "invited to group #team via your group link", + StartsWith "invitation to join the group #team sent to cath", EndsWith "joined the group" ], cath @@ -1634,7 +1634,7 @@ testGroupLinkDeleteGroupRejoin = concurrentlyN_ [ do alice <## "bob (Bob): contact is connected" - alice <## "bob invited to group #team via your group link" + alice <## "invitation to join the group #team sent to bob" alice <## "#team: bob joined the group", do bob <## "alice (Alice): contact is connected" @@ -1660,7 +1660,7 @@ testGroupLinkDeleteGroupRejoin = <### [ "bob_1 (Bob): contact is connected", "contact bob_1 is merged into bob", "use @bob to send messages", - EndsWith "invited to group #team via your group link", + StartsWith "invitation to join the group #team sent to bob", EndsWith "joined the group" ], bob @@ -1690,7 +1690,7 @@ testGroupLinkContactUsed = concurrentlyN_ [ do alice <## "bob (Bob): contact is connected" - alice <## "bob invited to group #team via your group link" + alice <## "invitation to join the group #team sent to bob" alice <## "#team: bob joined the group", do bob <## "alice (Alice): contact is connected" @@ -1752,7 +1752,7 @@ testGroupLinkIncognitoMembership = [ do bob <## ("cath (Catherine): contact is connected, your incognito profile for this contact is " <> bobIncognito) bob <## "use /i cath to print out this incognito profile again" - bob <## "cath invited to group #team via your group link" + bob <## "invitation to join the group #team sent to cath" bob <## "#team: cath joined the group", do cath <## (bobIncognito <> ": contact is connected") @@ -1778,7 +1778,7 @@ testGroupLinkIncognitoMembership = [ do bob <## (danIncognito <> ": contact is connected, your incognito profile for this contact is " <> bobIncognito) bob <## ("use /i " <> danIncognito <> " to print out this incognito profile again") - bob <## (danIncognito <> " invited to group #team via your group link") + bob <## ("invitation to join the group #team sent to " <> danIncognito) bob <## ("#team: " <> danIncognito <> " joined the group"), do dan <## (bobIncognito <> ": contact is connected, your incognito profile for this contact is " <> danIncognito) @@ -1841,7 +1841,7 @@ testGroupLinkUnusedHostContactDeleted = concurrentlyN_ [ do alice <## "bob (Bob): contact is connected" - alice <## "bob invited to group #team via your group link" + alice <## "invitation to join the group #team sent to bob" alice <## "#team: bob joined the group", do bob <## "alice (Alice): contact is connected" @@ -1861,7 +1861,7 @@ testGroupLinkUnusedHostContactDeleted = <### [ "bob_1 (Bob): contact is connected", "contact bob_1 is merged into bob", "use @bob to send messages", - EndsWith "invited to group #club via your group link", + StartsWith "invitation to join the group #club sent to bob", EndsWith "joined the group" ], bob @@ -1936,7 +1936,7 @@ testGroupLinkIncognitoUnusedHostContactsDeleted = concurrentlyN_ [ do alice <## (bobIncognito <> ": contact is connected") - alice <## (bobIncognito <> " invited to group #" <> group <> " via your group link") + alice <## ("invitation to join the group #" <> group <> " sent to " <> bobIncognito) alice <## ("#" <> group <> ": " <> bobIncognito <> " joined the group"), do bob <## (bobsAliceContact <> " (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito) @@ -1973,7 +1973,7 @@ testGroupLinkMemberRole = concurrentlyN_ [ do alice <## "bob (Bob): contact is connected" - alice <## "bob invited to group #team via your group link" + alice <## "invitation to join the group #team sent to bob" alice <## "#team: bob joined the group", do bob <## "alice (Alice): contact is connected" @@ -1986,12 +1986,11 @@ testGroupLinkMemberRole = cath ##> ("/c " <> gLink) cath <## "connection request sent!" alice <## "cath (Catherine): accepting request to join group #team..." - -- if contact existed it is merged concurrentlyN_ [ alice <### [ "cath (Catherine): contact is connected", - EndsWith "invited to group #team via your group link", - EndsWith "joined the group" + "invitation to join the group #team sent to cath", + "#team: cath joined the group" ], cath <### [ "alice (Alice): contact is connected", @@ -2041,7 +2040,7 @@ testGroupLinkLeaveDelete = <### [ "bob_1 (Bob): contact is connected", "contact bob_1 is merged into bob", "use @bob to send messages", - EndsWith "invited to group #team via your group link", + StartsWith "invitation to join the group #team sent to bob", EndsWith "joined the group" ], bob @@ -2057,7 +2056,7 @@ testGroupLinkLeaveDelete = concurrentlyN_ [ alice <### [ "cath (Catherine): contact is connected", - "cath invited to group #team via your group link", + "invitation to join the group #team sent to cath", "#team: cath joined the group" ], cath diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index db8d63457..58b80fa1e 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -267,7 +267,11 @@ cc <##.. ls = do unless prefix $ print ("expected to start from one of: " <> show ls, ", got: " <> l) prefix `shouldBe` True -data ConsoleResponse = ConsoleString String | WithTime String | EndsWith String +data ConsoleResponse + = ConsoleString String + | WithTime String + | EndsWith String + | StartsWith String deriving (Show) instance IsString ConsoleResponse where fromString = ConsoleString @@ -287,6 +291,7 @@ getInAnyOrder f cc ls = do ConsoleString s -> l == s WithTime s -> dropTime_ l == Just s EndsWith s -> s `isSuffixOf` l + StartsWith s -> s `isPrefixOf` l (<###) :: HasCallStack => TestCC -> [ConsoleResponse] -> Expectation (<###) = getInAnyOrder id