From 303d0eedf5346ccfabfb4137c1c981d184f511f9 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Wed, 4 Oct 2023 20:58:22 +0400
Subject: [PATCH 01/80] core: merge new contacts with existing contacts and
group members (#3173)
---
docs/rfcs/2023-09-29-merge-scenarios.md | 99 +++++
simplex-chat.cabal | 1 +
src/Simplex/Chat.hs | 272 ++++++++++----
src/Simplex/Chat/Controller.hs | 8 +-
.../Migrations/M20231002_conn_initiated.hs | 28 ++
src/Simplex/Chat/Migrations/chat_schema.sql | 4 +
src/Simplex/Chat/Store/Connections.hs | 2 +-
src/Simplex/Chat/Store/Direct.hs | 41 ++-
src/Simplex/Chat/Store/Groups.hs | 290 +++++++++------
src/Simplex/Chat/Store/Messages.hs | 2 +-
src/Simplex/Chat/Store/Migrations.hs | 4 +-
src/Simplex/Chat/Store/Profiles.hs | 4 +-
src/Simplex/Chat/Store/Shared.hs | 14 +-
src/Simplex/Chat/Types.hs | 19 +-
src/Simplex/Chat/View.hs | 16 +-
tests/ChatTests/Direct.hs | 132 +++++--
tests/ChatTests/Groups.hs | 347 +++++++++++++++++-
tests/ChatTests/Profiles.hs | 34 ++
tests/ChatTests/Utils.hs | 35 +-
19 files changed, 1087 insertions(+), 265 deletions(-)
create mode 100644 docs/rfcs/2023-09-29-merge-scenarios.md
create mode 100644 src/Simplex/Chat/Migrations/M20231002_conn_initiated.hs
diff --git a/docs/rfcs/2023-09-29-merge-scenarios.md b/docs/rfcs/2023-09-29-merge-scenarios.md
new file mode 100644
index 0000000000..42a7425f05
--- /dev/null
+++ b/docs/rfcs/2023-09-29-merge-scenarios.md
@@ -0,0 +1,99 @@
+# Merge scenarios
+
+## Problem
+
+Chat client allows multiple contact and group members records referring to the same "identity" be disassociated from one another. In some cases initially there is knowledge about the fact, and in some other cases it could be established via probe mechanism.
+
+There are cases already addressing this problem:
+- Contact merge or contact and group member merge (depending on creation of direct connection between members) when new member joins group, via probe mechanism.
+- Contact merge when connecting via group link.
+- Contact and group member association when sending direct message to a new contact.
+- Existing / deleted contact being preserved when receiving invitation direct message (XGrpDirectInv) from a group member.
+- Repeat contact requests to the already connected contact being prohibited; repeat contact requests being squashed on the receiving side (XContactId mechanism).
+
+Cases ignoring this problem:
+- Duplicate contacts on repeat connections via invitation links.
+- Duplicate contacts on repeat connection via contact request, when the existing contact was not created via a contact request to the same address (it could be created via any other means - via invitation link, via request to an old address, via group member).
+- Contact and group member records (possibly for many groups) not being merged when contact connects and group member records already exist.
+- Group member records in different groups not being merged if contact doesn't exist.
+- Duplicate contacts, or contact and group member records not being merged when connecting via contact address present in contact/group member profile.
+
+This problem is a direct consequence of lack of user identity in the platform, and in some cases we even consider it a feature. For example, duplicate contacts via repeat connections can be used for having conversation scopes. Though in general it seems to bring more confusion. It also limits some interactions, such as sending direct message to group members, or viewing a list of groups contact is member of (not implemented).
+
+On the other hand, solving all these cases reduces the privacy of the main profile, since the client cooperates with other probing clients blindly. To keep this property, we can add an opt-out "Merge contacts" client setting which would affect existing and new probe mechanisms. (It would act as an Incognito mode currently does - launch probes w/t launching probe hashes, and never confirm received probes) We can also ignore it, since this can be worked around via Incognito profiles or multiple user profiles.
+
+There is one more problem that could be addressed in the same scope - repeat group join via group link fails if the contact with host wasn't deleted. This happens due to group links re-using contact address machinery together with prohibiting repeat connections. This could be treated in a similar way to how it is treated for contact addresses: instead of simply prohibiting repeat connection, if group exists, it would be opened; if group doesn't exist, client would send host a request to re-invite them.
+
+## Solution
+
+### Duplicate contacts on repeat connections via invitation links
+
+Can be solved by probing contacts.
+
+Check connection with oneself and ask for confirmation.
+
+### Duplicate contacts on repeat connection via contact request
+
+Can be solved by probing contacts.
+
+Special case - records can be directly associated w/t probing when address is known in a contact / group member profile, see below.
+
+### Contact and group member records not merged when contact connects and group member records already exist
+
+Can be solved by probing group members.
+
+Send probe hashes to all viable group members? Or should group member records already have been merged between each other? (see below)
+
+In all cases above - who should initiate probing? It doesn't matter much, but possibly the contact that started connection.
+
+### Group member records in different groups not being merged if contact doesn't exist
+
+Currently multiple group members share the same "identity" by being associated to the same contact. However, it is allowed to have a group member record not associated contact. It can happen if group member's associated contact is deleted, or, with the latest changes that enable skipping creation of direct connections between group members, it could never exist in the first place.
+
+Can be solved by probing group members, merge could be done in one of the following ways:
+- Have surrogate contact records always associated to group member records, not available for use as regular contacts and using group member connections for probing.
+- Merge on the level of contact profiles.
+
+The latter seems more straightforward.
+
+Some more factors to consider:
+- Group member record may have both associated contact to probe via, and matching group member records in other groups.
+- Matching group member records in other groups may have associated contact records, which in turn may have associated contact records different from contact record associated to group member in question.
+- Client to which probes are sent may have contact record deleted, but have group member connection - in this case merge would be possible only if probe is sent to group member.
+- Member connections shouldn't be merged, because generally they're established via hosts, and hosts of different groups may have had different level of trust.
+
+Probably the solution is to:
+- send probe to associated contact if it exists (implemented currently)
+- if associated contact doesn't exist send probe to group member (not implemented?)
+- send probe hashes to all matching contacts (implemented)
+- send probe hashes to all matching group members in other groups that don't have associated contact records (not implemented)
+- merge all contacts that confirmed (currently only the first confirming contact is merged)
+- merge all group members that confirmed (not implemented, merge profiles?)
+
+Check: if both group member and associated confirm probe, will they be properly merged?
+
+### Connecting via profile address
+
+Having contact address in profile is not proof that it belongs that user (malicious client can put arbitrary address in profile), so it shouldn't be used to directly associate contact or group member records.
+
+When connecting via contact address, having it associated with a contact record can be used as a sufficient condition to send probe hash, even if profile doesn't match. (index on contact_profiles.contact_link, lookup contact by contact_profile_id)
+
+To consider:
+
+Currently group member address is shown even if direct messages are prohibited in group. There're two reasons this preference is usually enabled in a group:
+- to prevent abuse (in public groups),
+- to prevent members from direct communication.
+
+Member address being shown regardless of this preference undermines the second use case.
+
+### Repeat group join via group link
+
+**If group still exists:**
+
+Open group.
+
+How to check for group existence? Currently we save group_link_id on host contact's connection. It may have been deleted by the time of repeated connection via group link. Probably we should also save group_link_id or even the full contact address on the group record itself.
+
+**If group doesn't exist:**
+
+For group links allow repeat contact request even if host contact exists (unlike for regular contact requests).
diff --git a/simplex-chat.cabal b/simplex-chat.cabal
index 0b613310f7..30c4c62dc1 100644
--- a/simplex-chat.cabal
+++ b/simplex-chat.cabal
@@ -114,6 +114,7 @@ library
Simplex.Chat.Migrations.M20230913_member_contacts
Simplex.Chat.Migrations.M20230914_member_probes
Simplex.Chat.Migrations.M20230926_contact_status
+ Simplex.Chat.Migrations.M20231002_conn_initiated
Simplex.Chat.Mobile
Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 5d370ee651..32d59c9cc5 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -39,6 +39,7 @@ import Data.Fixed (div')
import Data.Functor (($>))
import Data.Int (Int64)
import Data.List (find, foldl', isSuffixOf, partition, sortOn)
+import Data.List (intercalate)
import Data.List.NonEmpty (NonEmpty, nonEmpty)
import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
@@ -212,7 +213,8 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
showLiveItems <- newTVarIO False
userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg
tempDirectory <- newTVarIO tempDir
- pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, subscriptionMode, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, filesFolder, expireCIThreads, expireCIFlags, cleanupManagerAsync, timedItemThreads, showLiveItems, userXFTPFileConfig, tempDirectory, logFilePath = logFile}
+ contactMergeEnabled <- newTVarIO True
+ pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, subscriptionMode, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, filesFolder, expireCIThreads, expireCIFlags, cleanupManagerAsync, timedItemThreads, showLiveItems, userXFTPFileConfig, tempDirectory, logFilePath = logFile, contactMergeEnabled}
where
configServers :: DefaultAgentServers
configServers =
@@ -489,6 +491,9 @@ processChatCommand = \case
APISetXFTPConfig cfg -> do
asks userXFTPFileConfig >>= atomically . (`writeTVar` cfg)
ok_
+ SetContactMergeEnabled onOff -> do
+ asks contactMergeEnabled >>= atomically . (`writeTVar` onOff)
+ ok_
APIExportArchive cfg -> checkChatStopped $ exportArchive cfg >> ok_
ExportArchive -> do
ts <- liftIO getCurrentTime
@@ -2667,6 +2672,7 @@ cleanupManager = do
forM_ us $ cleanupUser interval stepDelay
forM_ us' $ cleanupUser interval stepDelay
cleanupMessages `catchChatError` (toView . CRChatError Nothing)
+ cleanupProbes `catchChatError` (toView . CRChatError Nothing)
liftIO $ threadDelay' $ diffToMicroseconds interval
where
runWithoutInitialDelay cleanupInterval = flip catchChatError (toView . CRChatError Nothing) $ do
@@ -2694,6 +2700,10 @@ cleanupManager = do
ts <- liftIO getCurrentTime
let cutoffTs = addUTCTime (- (30 * nominalDay)) ts
withStoreCtx' (Just "cleanupManager, deleteOldMessages") (`deleteOldMessages` cutoffTs)
+ cleanupProbes = do
+ ts <- liftIO getCurrentTime
+ let cutoffTs = addUTCTime (- (14 * nominalDay)) ts
+ withStore' (`deleteOldProbes` cutoffTs)
startProximateTimedItemThread :: ChatMonad m => User -> (ChatRef, ChatItemId) -> UTCTime -> m ()
startProximateTimedItemThread user itemRef deleteAt = do
@@ -2976,7 +2986,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, peerChatVRange, viaUserContactLink, groupLinkId, customUserProfileId, connectionCode} = \case
+ processDirectMessage agentMsg connEntity conn@Connection {connId, peerChatVRange, viaUserContactLink, customUserProfileId, connectionCode} = \case
Nothing -> case agentMsg of
CONF confId _ connInfo -> do
-- [incognito] send saved profile
@@ -3040,9 +3050,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
XInfo p -> xInfo ct' p
XDirectDel -> xDirectDel ct' msg msgMeta
XGrpInv gInv -> processGroupInvitation ct' gInv msg msgMeta
- XInfoProbe probe -> xInfoProbe (CGMContact ct') probe
- XInfoProbeCheck probeHash -> xInfoProbeCheck ct' probeHash
- XInfoProbeOk probe -> xInfoProbeOk ct' probe
+ XInfoProbe probe -> xInfoProbe (COMContact ct') probe
+ XInfoProbeCheck probeHash -> xInfoProbeCheck (COMContact ct') probeHash
+ XInfoProbeOk probe -> xInfoProbeOk (COMContact ct') probe
XCallInv callId invitation -> xCallInv ct' callId invitation msg msgMeta
XCallOffer callId offer -> xCallOffer ct' callId offer msg msgMeta
XCallAnswer callId answer -> xCallAnswer ct' callId answer msg msgMeta
@@ -3095,7 +3105,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
whenUserNtfs user $ do
setActive $ ActiveC c
showToast (c <> "> ") "connected"
- forM_ groupLinkId $ \_ -> probeMatchingContacts ct $ contactConnIncognito ct
+ when (contactConnInitiated conn) $ do
+ probeMatchingContactsAndMembers ct (contactConnIncognito ct)
+ withStore' $ \db -> resetContactConnInitiated db user conn
forM_ viaUserContactLink $ \userContactLinkId ->
withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case
Just (UserContactLink {autoAccept = Just AutoAccept {autoReply = mc_}}, groupId_, gLinkMemRole) -> do
@@ -3113,7 +3125,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
when (maybe False ((== ConnReady) . connStatus) activeConn) $ do
notifyMemberConnected gInfo m $ Just ct
let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo
- when (memberCategory m == GCPreMember) $ probeMatchingContacts ct connectedIncognito
+ when (memberCategory m == GCPreMember) $ probeMatchingContactsAndMembers ct connectedIncognito
SENT msgId -> do
sentMsgDeliveryEvent conn msgId
checkSndInlineFTComplete conn msgId
@@ -3281,12 +3293,12 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
Nothing -> do
notifyMemberConnected gInfo m Nothing
let connectedIncognito = memberIncognito membership
- when (memberCategory m == GCPreMember) $ probeMatchingMemberContact gInfo m connectedIncognito
+ when (memberCategory m == GCPreMember) $ probeMatchingMemberContact m connectedIncognito
Just ct@Contact {activeConn = Connection {connStatus}} ->
when (connStatus == ConnReady) $ do
notifyMemberConnected gInfo m $ Just ct
let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo
- when (memberCategory m == GCPreMember) $ probeMatchingContacts ct connectedIncognito
+ when (memberCategory m == GCPreMember) $ probeMatchingContactsAndMembers ct connectedIncognito
MSG msgMeta _msgFlags msgBody -> do
cmdId <- createAckCmd conn
withAckMessage agentConnId cmdId msgMeta $ do
@@ -3314,9 +3326,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
XGrpDel -> xGrpDel gInfo m' msg msgMeta
XGrpInfo p' -> xGrpInfo gInfo m' p' msg msgMeta
XGrpDirectInv connReq mContent_ -> canSend m' $ xGrpDirectInv gInfo m' conn' connReq mContent_ msg msgMeta
- XInfoProbe probe -> xInfoProbe (CGMGroupMember gInfo m') probe
- -- XInfoProbeCheck -- TODO merge members?
- -- XInfoProbeOk -- TODO merge members?
+ XInfoProbe probe -> xInfoProbe (COMGroupMember m') probe
+ XInfoProbeCheck probeHash -> xInfoProbeCheck (COMGroupMember m') probeHash
+ XInfoProbeOk probe -> xInfoProbeOk (COMGroupMember m') probe
BFileChunk sharedMsgId chunk -> bFileChunkGroup gInfo sharedMsgId chunk msgMeta
_ -> messageError $ "unsupported message: " <> T.pack (show event)
currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo
@@ -3679,45 +3691,56 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
setActive $ ActiveG g
showToast ("#" <> g) $ "member " <> c <> " is connected"
- probeMatchingContacts :: Contact -> IncognitoEnabled -> m ()
- probeMatchingContacts ct connectedIncognito = do
+ probeMatchingContactsAndMembers :: Contact -> IncognitoEnabled -> m ()
+ probeMatchingContactsAndMembers ct connectedIncognito = do
gVar <- asks idsDrg
- if connectedIncognito
- then sendProbe . Probe =<< liftIO (encodedRandomBytes gVar 32)
- else do
- (probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId (CGMContact ct)
+ contactMerge <- readTVarIO =<< asks contactMergeEnabled
+ if contactMerge && not connectedIncognito
+ then do
+ (probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId (COMContact ct)
+ -- ! when making changes to probe-and-merge mechanism,
+ -- ! test scenario in which recipient receives probe after probe hashes (not covered in tests):
+ -- sendProbe -> sendProbeHashes (currently)
+ -- sendProbeHashes -> sendProbe (reversed - change order in code, may add delay)
sendProbe probe
- cs <- withStore' $ \db -> getMatchingContacts db user ct
- sendProbeHashes cs probe probeId
+ cs <- map COMContact <$> withStore' (\db -> getMatchingContacts db user ct)
+ ms <- map COMGroupMember <$> withStore' (\db -> getMatchingMembers db user ct)
+ sendProbeHashes (cs <> ms) probe probeId
+ else sendProbe . Probe =<< liftIO (encodedRandomBytes gVar 32)
where
sendProbe :: Probe -> m ()
sendProbe probe = void . sendDirectContactMessage ct $ XInfoProbe probe
- probeMatchingMemberContact :: GroupInfo -> GroupMember -> IncognitoEnabled -> m ()
- probeMatchingMemberContact _ GroupMember {activeConn = Nothing} _ = pure ()
- probeMatchingMemberContact g m@GroupMember {groupId, activeConn = Just conn} connectedIncognito = do
+ probeMatchingMemberContact :: GroupMember -> IncognitoEnabled -> m ()
+ probeMatchingMemberContact GroupMember {activeConn = Nothing} _ = pure ()
+ probeMatchingMemberContact m@GroupMember {groupId, activeConn = Just conn} connectedIncognito = do
gVar <- asks idsDrg
- if connectedIncognito
- then sendProbe . Probe =<< liftIO (encodedRandomBytes gVar 32)
- else do
- (probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId $ CGMGroupMember g m
+ contactMerge <- readTVarIO =<< asks contactMergeEnabled
+ if contactMerge && not connectedIncognito
+ then do
+ (probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId $ COMGroupMember m
sendProbe probe
- cs <- withStore' $ \db -> getMatchingMemberContacts db user m
+ cs <- map COMContact <$> withStore' (\db -> getMatchingMemberContacts db user m)
sendProbeHashes cs probe probeId
+ else sendProbe . Probe =<< liftIO (encodedRandomBytes gVar 32)
where
sendProbe :: Probe -> m ()
sendProbe probe = void $ sendDirectMessage conn (XInfoProbe probe) (GroupId groupId)
- -- TODO currently we only send probe hashes to contacts
- sendProbeHashes :: [Contact] -> Probe -> Int64 -> m ()
- sendProbeHashes cs probe probeId =
- forM_ cs $ \c -> sendProbeHash c `catchChatError` \_ -> pure ()
+ sendProbeHashes :: [ContactOrMember] -> Probe -> Int64 -> m ()
+ sendProbeHashes cgms probe probeId =
+ forM_ cgms $ \cgm -> sendProbeHash cgm `catchChatError` \_ -> pure ()
where
probeHash = ProbeHash $ C.sha256Hash (unProbe probe)
- sendProbeHash :: Contact -> m ()
- sendProbeHash c = do
+ sendProbeHash :: ContactOrMember -> m ()
+ sendProbeHash cgm@(COMContact c) = do
void . sendDirectContactMessage c $ XInfoProbeCheck probeHash
- withStore' $ \db -> createSentProbeHash db userId probeId $ CGMContact c
+ withStore' $ \db -> createSentProbeHash db userId probeId cgm
+ sendProbeHash (COMGroupMember GroupMember {activeConn = Nothing}) = pure ()
+ sendProbeHash cgm@(COMGroupMember m@GroupMember {groupId, activeConn = Just conn}) =
+ when (memberCurrent m) $ do
+ void $ sendDirectMessage conn (XInfoProbeCheck probeHash) (GroupId groupId)
+ withStore' $ \db -> createSentProbeHash db userId probeId cgm
messageWarning :: Text -> m ()
messageWarning = toView . CRMessageError user "warning"
@@ -4303,48 +4326,100 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
(_, param) = groupFeatureState p
createInternalChatItem user (CDGroupRcv g m) (CIRcvGroupFeature (toGroupFeature f) (toGroupPreference p) param) Nothing
- xInfoProbe :: ContactOrGroupMember -> Probe -> m ()
- xInfoProbe cgm2 probe =
+ xInfoProbe :: ContactOrMember -> Probe -> m ()
+ xInfoProbe cgm2 probe = do
+ contactMerge <- readTVarIO =<< asks contactMergeEnabled
-- [incognito] unless connected incognito
- unless (contactOrGroupMemberIncognito cgm2) $ do
- r <- withStore' $ \db -> matchReceivedProbe db user cgm2 probe
- forM_ r $ \case
- CGMContact c1 -> probeMatch c1 cgm2 probe
- CGMGroupMember _ _ -> messageWarning "xInfoProbe ignored: matched member (no probe hashes sent to members)"
+ when (contactMerge && not (contactOrMemberIncognito cgm2)) $ do
+ cgm1s <- withStore' $ \db -> matchReceivedProbe db user cgm2 probe
+ let cgm1s' = filter (not . contactOrMemberIncognito) cgm1s
+ probeMatches cgm1s' cgm2
+ where
+ probeMatches :: [ContactOrMember] -> ContactOrMember -> m ()
+ probeMatches [] _ = pure ()
+ probeMatches (cgm1' : cgm1s') cgm2' = do
+ cgm2''_ <- probeMatch cgm1' cgm2' probe `catchChatError` \_ -> pure (Just cgm2')
+ let cgm2'' = fromMaybe cgm2' cgm2''_
+ probeMatches cgm1s' cgm2''
- -- TODO currently we send probe hashes only to contacts
- xInfoProbeCheck :: Contact -> ProbeHash -> m ()
- xInfoProbeCheck c1 probeHash =
+ xInfoProbeCheck :: ContactOrMember -> ProbeHash -> m ()
+ xInfoProbeCheck cgm1 probeHash = do
+ contactMerge <- readTVarIO =<< asks contactMergeEnabled
-- [incognito] unless connected incognito
- unless (contactConnIncognito c1) $ do
- r <- withStore' $ \db -> matchReceivedProbeHash db user (CGMContact c1) probeHash
- forM_ r . uncurry $ probeMatch c1
+ when (contactMerge && not (contactOrMemberIncognito cgm1)) $ do
+ cgm2Probe_ <- withStore' $ \db -> matchReceivedProbeHash db user cgm1 probeHash
+ forM_ cgm2Probe_ $ \(cgm2, probe) ->
+ unless (contactOrMemberIncognito cgm2) $
+ void $ probeMatch cgm1 cgm2 probe
- probeMatch :: Contact -> ContactOrGroupMember -> Probe -> m ()
- probeMatch c1@Contact {contactId = cId1, profile = p1} cgm2 probe =
- case cgm2 of
- CGMContact c2@Contact {contactId = cId2, profile = p2}
- | cId1 /= cId2 && profilesMatch p1 p2 -> do
- void . sendDirectContactMessage c1 $ XInfoProbeOk probe
- mergeContacts c1 c2
- | otherwise -> messageWarning "probeMatch ignored: profiles don't match or same contact id"
- CGMGroupMember g m2@GroupMember {memberProfile = p2, memberContactId}
- | isNothing memberContactId && profilesMatch p1 p2 -> do
- void . sendDirectContactMessage c1 $ XInfoProbeOk probe
- connectContactToMember c1 g m2
- | otherwise -> messageWarning "probeMatch ignored: profiles don't match or member already has contact"
+ probeMatch :: ContactOrMember -> ContactOrMember -> Probe -> m (Maybe ContactOrMember)
+ probeMatch cgm1 cgm2 probe =
+ case cgm1 of
+ COMContact c1@Contact {contactId = cId1, profile = p1} ->
+ case cgm2 of
+ COMContact c2@Contact {contactId = cId2, profile = p2}
+ | cId1 /= cId2 && profilesMatch p1 p2 -> do
+ void . sendDirectContactMessage c1 $ XInfoProbeOk probe
+ COMContact <$$> mergeContacts c1 c2
+ | otherwise -> messageWarning "probeMatch ignored: profiles don't match or same contact id" >> pure Nothing
+ COMGroupMember m2@GroupMember {memberProfile = p2, memberContactId}
+ | profilesMatch p1 p2 -> case memberContactId of
+ Nothing -> do
+ void . sendDirectContactMessage c1 $ XInfoProbeOk probe
+ COMContact <$$> associateMemberAndContact c1 m2
+ Just mCtId
+ | mCtId /= cId1 -> do
+ void . sendDirectContactMessage c1 $ XInfoProbeOk probe
+ mCt <- withStore $ \db -> getContact db user mCtId
+ COMContact <$$> mergeContacts c1 mCt
+ | otherwise -> messageWarning "probeMatch ignored: same contact id" >> pure Nothing
+ | otherwise -> messageWarning "probeMatch ignored: profiles don't match" >> pure Nothing
+ COMGroupMember GroupMember {activeConn = Nothing} -> pure Nothing
+ COMGroupMember m1@GroupMember {groupId, memberProfile = p1, memberContactId, activeConn = Just conn} ->
+ case cgm2 of
+ COMContact c2@Contact {contactId = cId2, profile = p2}
+ | memberCurrent m1 && profilesMatch p1 p2 -> case memberContactId of
+ Nothing -> do
+ void $ sendDirectMessage conn (XInfoProbeOk probe) (GroupId groupId)
+ COMContact <$$> associateMemberAndContact c2 m1
+ Just mCtId
+ | mCtId /= cId2 -> do
+ void $ sendDirectMessage conn (XInfoProbeOk probe) (GroupId groupId)
+ mCt <- withStore $ \db -> getContact db user mCtId
+ COMContact <$$> mergeContacts c2 mCt
+ | otherwise -> messageWarning "probeMatch ignored: same contact id" >> pure Nothing
+ | otherwise -> messageWarning "probeMatch ignored: profiles don't match or member not current" >> pure Nothing
+ COMGroupMember _ -> messageWarning "probeMatch ignored: members are not matched with members" >> pure Nothing
- -- TODO currently we send probe hashes only to contacts
- xInfoProbeOk :: Contact -> Probe -> m ()
- xInfoProbeOk c1@Contact {contactId = cId1} probe =
- withStore' (\db -> matchSentProbe db user (CGMContact c1) probe) >>= \case
- Just (CGMContact c2@Contact {contactId = cId2})
- | cId1 /= cId2 -> mergeContacts c1 c2
- | otherwise -> messageWarning "xInfoProbeOk ignored: same contact id"
- Just (CGMGroupMember g m2@GroupMember {memberContactId})
- | isNothing memberContactId -> connectContactToMember c1 g m2
- | otherwise -> messageWarning "xInfoProbeOk ignored: member already has contact"
- _ -> pure ()
+ xInfoProbeOk :: ContactOrMember -> Probe -> m ()
+ xInfoProbeOk cgm1 probe = do
+ cgm2 <- withStore' $ \db -> matchSentProbe db user cgm1 probe
+ case cgm1 of
+ COMContact c1@Contact {contactId = cId1} ->
+ case cgm2 of
+ Just (COMContact c2@Contact {contactId = cId2})
+ | cId1 /= cId2 -> void $ mergeContacts c1 c2
+ | otherwise -> messageWarning "xInfoProbeOk ignored: same contact id"
+ Just (COMGroupMember m2@GroupMember {memberContactId}) ->
+ case memberContactId of
+ Nothing -> void $ associateMemberAndContact c1 m2
+ Just mCtId
+ | mCtId /= cId1 -> do
+ mCt <- withStore $ \db -> getContact db user mCtId
+ void $ mergeContacts c1 mCt
+ | otherwise -> messageWarning "xInfoProbeOk ignored: same contact id"
+ _ -> pure ()
+ COMGroupMember m1@GroupMember {memberContactId} ->
+ case cgm2 of
+ Just (COMContact c2@Contact {contactId = cId2}) -> case memberContactId of
+ Nothing -> void $ associateMemberAndContact c2 m1
+ Just mCtId
+ | mCtId /= cId2 -> do
+ mCt <- withStore $ \db -> getContact db user mCtId
+ void $ mergeContacts c2 mCt
+ | otherwise -> messageWarning "xInfoProbeOk ignored: same contact id"
+ Just (COMGroupMember _) -> messageWarning "xInfoProbeOk ignored: members are not matched with members"
+ _ -> pure ()
-- to party accepting call
xCallInv :: Contact -> CallId -> CallInvitation -> RcvMessage -> MsgMeta -> m ()
@@ -4451,15 +4526,53 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
msgCallStateError eventName Call {callState} =
messageError $ eventName <> ": wrong call state " <> T.pack (show $ callStateTag callState)
- mergeContacts :: Contact -> Contact -> m ()
+ mergeContacts :: Contact -> Contact -> m (Maybe Contact)
mergeContacts c1 c2 = do
- withStore' $ \db -> mergeContactRecords db userId c1 c2
- toView $ CRContactsMerged user c1 c2
+ let Contact {localDisplayName = cLDN1, profile = LocalProfile {displayName}} = c1
+ Contact {localDisplayName = cLDN2} = c2
+ case (suffixOrd displayName cLDN1, suffixOrd displayName cLDN2) of
+ (Just cOrd1, Just cOrd2)
+ | cOrd1 < cOrd2 -> merge c1 c2
+ | cOrd2 < cOrd1 -> merge c2 c1
+ | otherwise -> pure Nothing
+ _ -> pure Nothing
+ where
+ merge c1' c2' = do
+ c2'' <- withStore $ \db -> mergeContactRecords db user c1' c2'
+ toView $ CRContactsMerged user c1' c2' c2''
+ pure $ Just c2''
- connectContactToMember :: Contact -> GroupInfo -> GroupMember -> m ()
- connectContactToMember c1 g m2 = do
- withStore' $ \db -> updateMemberContact db user c1 m2
- toView $ CRMemberContactConnected user c1 g m2
+ associateMemberAndContact :: Contact -> GroupMember -> m (Maybe Contact)
+ associateMemberAndContact c m = do
+ let Contact {localDisplayName = cLDN, profile = LocalProfile {displayName}} = c
+ GroupMember {localDisplayName = mLDN} = m
+ case (suffixOrd displayName cLDN, suffixOrd displayName mLDN) of
+ (Just cOrd, Just mOrd)
+ | cOrd < mOrd -> Just <$> associateMemberWithContact c m
+ | mOrd < cOrd -> Just <$> associateContactWithMember m c
+ | otherwise -> pure Nothing
+ _ -> pure Nothing
+
+ suffixOrd :: ContactName -> ContactName -> Maybe Int
+ suffixOrd displayName localDisplayName
+ | localDisplayName == displayName = Just 0
+ | otherwise = case T.stripPrefix (displayName <> "_") localDisplayName of
+ Just suffix -> readMaybe $ T.unpack suffix
+ Nothing -> Nothing
+
+ associateMemberWithContact :: Contact -> GroupMember -> m Contact
+ associateMemberWithContact c1 m2@GroupMember {groupId} = do
+ withStore' $ \db -> associateMemberWithContactRecord db user c1 m2
+ g <- withStore $ \db -> getGroupInfo db user groupId
+ toView $ CRContactAndMemberAssociated user c1 g m2 c1
+ pure c1
+
+ associateContactWithMember :: GroupMember -> Contact -> m Contact
+ associateContactWithMember m1@GroupMember {groupId} c2 = do
+ c2' <- withStore $ \db -> associateContactWithMemberRecord db user m1 c2
+ g <- withStore $ \db -> getGroupInfo db user groupId
+ toView $ CRContactAndMemberAssociated user c2 g m1 c2'
+ pure c2'
saveConnInfo :: Connection -> ConnInfo -> m Connection
saveConnInfo activeConn connInfo = do
@@ -5388,6 +5501,7 @@ chatCommandP =
("/_files_folder " <|> "/files_folder ") *> (SetFilesFolder <$> filePath),
"/_xftp " *> (APISetXFTPConfig <$> ("on " *> (Just <$> jsonP) <|> ("off" $> Nothing))),
"/xftp " *> (APISetXFTPConfig <$> ("on" *> (Just <$> xftpCfgP) <|> ("off" $> Nothing))),
+ "/contact_merge " *> (SetContactMergeEnabled <$> onOffP),
"/_db export " *> (APIExportArchive <$> jsonP),
"/db export" $> ExportArchive,
"/_db import " *> (APIImportArchive <$> jsonP),
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index 61840b8e84..d859231faa 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -191,7 +191,8 @@ data ChatController = ChatController
showLiveItems :: TVar Bool,
userXFTPFileConfig :: TVar (Maybe XFTPFileConfig),
tempDirectory :: TVar (Maybe FilePath),
- logFilePath :: Maybe FilePath
+ logFilePath :: Maybe FilePath,
+ contactMergeEnabled :: TVar Bool
}
data HelpSection = HSMain | HSFiles | HSGroups | HSContacts | HSMyAddress | HSIncognito | HSMarkdown | HSMessages | HSSettings | HSDatabase
@@ -230,6 +231,7 @@ data ChatCommand
| SetTempFolder FilePath
| SetFilesFolder FilePath
| APISetXFTPConfig (Maybe XFTPFileConfig)
+ | SetContactMergeEnabled Bool
| APIExportArchive ArchiveConfig
| ExportArchive
| APIImportArchive ArchiveConfig
@@ -490,7 +492,7 @@ data ChatResponse
| CRSentConfirmation {user :: User}
| CRSentInvitation {user :: User, customUserProfile :: Maybe Profile}
| CRContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact}
- | CRContactsMerged {user :: User, intoContact :: Contact, mergedContact :: Contact}
+ | CRContactsMerged {user :: User, intoContact :: Contact, mergedContact :: Contact, updatedContact :: Contact}
| CRContactDeleted {user :: User, contact :: Contact}
| CRContactDeletedByContact {user :: User, contact :: Contact}
| CRChatCleared {user :: User, chatInfo :: AChatInfo}
@@ -562,7 +564,7 @@ data ChatResponse
| CRNewMemberContact {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember}
| CRNewMemberContactSentInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember}
| CRNewMemberContactReceivedInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember}
- | CRMemberContactConnected {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember}
+ | CRContactAndMemberAssociated {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember, updatedContact :: Contact}
| CRMemberSubError {user :: User, groupInfo :: GroupInfo, member :: GroupMember, chatError :: ChatError}
| CRMemberSubSummary {user :: User, memberSubscriptions :: [MemberSubStatus]}
| CRGroupSubscribed {user :: User, groupInfo :: GroupInfo}
diff --git a/src/Simplex/Chat/Migrations/M20231002_conn_initiated.hs b/src/Simplex/Chat/Migrations/M20231002_conn_initiated.hs
new file mode 100644
index 0000000000..a0f6009af2
--- /dev/null
+++ b/src/Simplex/Chat/Migrations/M20231002_conn_initiated.hs
@@ -0,0 +1,28 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+module Simplex.Chat.Migrations.M20231002_conn_initiated where
+
+import Database.SQLite.Simple (Query)
+import Database.SQLite.Simple.QQ (sql)
+
+m20231002_conn_initiated :: Query
+m20231002_conn_initiated =
+ [sql|
+ALTER TABLE connections ADD COLUMN contact_conn_initiated INTEGER NOT NULL DEFAULT 0;
+
+UPDATE connections SET conn_req_inv = NULL WHERE conn_status IN ('ready', 'deleted');
+
+CREATE INDEX idx_sent_probes_created_at ON sent_probes(created_at);
+CREATE INDEX idx_sent_probe_hashes_created_at ON sent_probe_hashes(created_at);
+CREATE INDEX idx_received_probes_created_at ON received_probes(created_at);
+|]
+
+down_m20231002_conn_initiated :: Query
+down_m20231002_conn_initiated =
+ [sql|
+DROP INDEX idx_sent_probes_created_at;
+DROP INDEX idx_sent_probe_hashes_created_at;
+DROP INDEX idx_received_probes_created_at;
+
+ALTER TABLE connections DROP COLUMN contact_conn_initiated;
+|]
diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql
index 65ceb7d19b..e88d83e42d 100644
--- a/src/Simplex/Chat/Migrations/chat_schema.sql
+++ b/src/Simplex/Chat/Migrations/chat_schema.sql
@@ -264,6 +264,7 @@ CREATE TABLE connections(
peer_chat_min_version INTEGER NOT NULL DEFAULT 1,
peer_chat_max_version INTEGER NOT NULL DEFAULT 1,
to_subscribe INTEGER DEFAULT 0 NOT NULL,
+ contact_conn_initiated INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(snd_file_id, connection_id)
REFERENCES snd_files(file_id, connection_id)
ON DELETE CASCADE
@@ -732,3 +733,6 @@ CREATE INDEX idx_received_probes_user_id ON received_probes(user_id);
CREATE INDEX idx_received_probes_contact_id ON received_probes(contact_id);
CREATE INDEX idx_received_probes_probe ON received_probes(probe);
CREATE INDEX idx_received_probes_probe_hash ON received_probes(probe_hash);
+CREATE INDEX idx_sent_probes_created_at ON sent_probes(created_at);
+CREATE INDEX idx_sent_probe_hashes_created_at ON sent_probe_hashes(created_at);
+CREATE INDEX idx_received_probes_created_at ON received_probes(created_at);
diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs
index 93f3349ca2..383db3c59c 100644
--- a/src/Simplex/Chat/Store/Connections.hs
+++ b/src/Simplex/Chat/Store/Connections.hs
@@ -58,7 +58,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
db
[sql|
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
- conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter,
+ conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter,
peer_chat_min_version, peer_chat_max_version
FROM connections
WHERE user_id = ? AND agent_conn_id = ?
diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs
index 886c735053..20b81def8f 100644
--- a/src/Simplex/Chat/Store/Direct.hs
+++ b/src/Simplex/Chat/Store/Direct.hs
@@ -62,6 +62,7 @@ module Simplex.Chat.Store.Direct
updateConnectionStatus,
updateContactSettings,
setConnConnReqInv,
+ resetContactConnInitiated,
)
where
@@ -126,11 +127,11 @@ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile grou
db
[sql|
INSERT INTO connections (
- user_id, agent_conn_id, conn_status, conn_type,
+ user_id, agent_conn_id, conn_status, conn_type, contact_conn_initiated,
via_contact_uri_hash, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, created_at, updated_at, to_subscribe
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
- ((userId, acId, pccConnStatus, ConnContact, cReqHash, xContactId) :. (customUserProfileId, isJust groupLinkId, groupLinkId, createdAt, createdAt, subMode == SMOnlyCreate))
+ ((userId, acId, pccConnStatus, ConnContact, True, cReqHash, xContactId) :. (customUserProfileId, isJust groupLinkId, groupLinkId, createdAt, createdAt, subMode == SMOnlyCreate))
pccConnId <- insertedRowId db
pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connReqInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt}
@@ -151,7 +152,7 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
-- Connection
- c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
+ c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct
@@ -174,13 +175,14 @@ createDirectConnection :: DB.Connection -> User -> ConnId -> ConnReqInvitation -
createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile subMode = do
createdAt <- getCurrentTime
customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile
+ let contactConnInitiated = pccConnStatus == ConnNew
DB.execute
db
[sql|
INSERT INTO connections
- (user_id, agent_conn_id, conn_req_inv, conn_status, conn_type, custom_user_profile_id, created_at, updated_at, to_subscribe) VALUES (?,?,?,?,?,?,?,?,?)
+ (user_id, agent_conn_id, conn_req_inv, conn_status, conn_type, contact_conn_initiated, custom_user_profile_id, created_at, updated_at, to_subscribe) VALUES (?,?,?,?,?,?,?,?,?,?)
|]
- (userId, acId, cReq, pccConnStatus, ConnContact, customUserProfileId, createdAt, createdAt, subMode == SMOnlyCreate)
+ (userId, acId, cReq, pccConnStatus, ConnContact, contactConnInitiated, customUserProfileId, createdAt, createdAt, subMode == SMOnlyCreate)
pccConnId <- insertedRowId db
pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, groupLinkId = Nothing, customUserProfileId, connReqInv = Just cReq, localAlias = "", createdAt, updatedAt = createdAt}
@@ -508,7 +510,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
-- Connection
- c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
+ c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct
@@ -672,7 +674,7 @@ getContact_ db user@User {userId} contactId deleted =
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
-- Connection
- c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
+ c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct
@@ -722,7 +724,7 @@ getContactConnections db userId Contact {contactId} =
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
- c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
+ c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
FROM connections c
JOIN contacts ct ON ct.contact_id = c.contact_id
@@ -739,7 +741,7 @@ getConnectionById db User {userId} connId = ExceptT $ do
db
[sql|
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
- conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter,
+ conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter,
peer_chat_min_version, peer_chat_max_version
FROM connections
WHERE user_id = ? AND connection_id = ?
@@ -773,7 +775,11 @@ getConnectionsContacts db agentConnIds = do
updateConnectionStatus :: DB.Connection -> Connection -> ConnStatus -> IO ()
updateConnectionStatus db Connection {connId} connStatus = do
currentTs <- getCurrentTime
- DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ? WHERE connection_id = ?" (connStatus, currentTs, connId)
+ if connStatus == ConnReady
+ then
+ DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ?, conn_req_inv = NULL WHERE connection_id = ?" (connStatus, currentTs, connId)
+ else
+ DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ? WHERE connection_id = ?" (connStatus, currentTs, connId)
updateContactSettings :: DB.Connection -> User -> Int64 -> ChatSettings -> IO ()
updateContactSettings db User {userId} contactId ChatSettings {enableNtfs, sendRcpts, favorite} =
@@ -790,3 +796,16 @@ setConnConnReqInv db User {userId} connId connReq = do
WHERE user_id = ? AND connection_id = ?
|]
(connReq, updatedAt, userId, connId)
+
+resetContactConnInitiated :: DB.Connection -> User -> Connection -> IO ()
+resetContactConnInitiated db User {userId} Connection {connId} = do
+ updatedAt <- getCurrentTime
+ DB.execute
+ db
+ [sql|
+ UPDATE connections
+ SET contact_conn_initiated = 0, updated_at = ?
+ WHERE user_id = ? AND connection_id = ?
+ |]
+ (updatedAt, userId, connId)
+
diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs
index e72ca8e8ca..f6a4233c1f 100644
--- a/src/Simplex/Chat/Store/Groups.hs
+++ b/src/Simplex/Chat/Store/Groups.hs
@@ -77,6 +77,7 @@ module Simplex.Chat.Store.Groups
getViaGroupMember,
getViaGroupContact,
getMatchingContacts,
+ getMatchingMembers,
getMatchingMemberContacts,
createSentProbe,
createSentProbeHash,
@@ -84,7 +85,9 @@ module Simplex.Chat.Store.Groups
matchReceivedProbeHash,
matchSentProbe,
mergeContactRecords,
- updateMemberContact,
+ associateMemberWithContactRecord,
+ associateContactWithMemberRecord,
+ deleteOldProbes,
updateGroupSettings,
getXGrpMemIntroContDirect,
getXGrpMemIntroContGroup,
@@ -105,7 +108,7 @@ import Crypto.Random (ChaChaDRG)
import Data.Either (rights)
import Data.Int (Int64)
import Data.List (sortOn)
-import Data.Maybe (fromMaybe, isNothing)
+import Data.Maybe (fromMaybe, isNothing, catMaybes, isJust)
import Data.Ord (Down (..))
import Data.Text (Text)
import Data.Time.Clock (UTCTime (..), getCurrentTime)
@@ -169,7 +172,7 @@ getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} =
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
- c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
+ c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
@@ -251,7 +254,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId =
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
- c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
+ c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
@@ -544,7 +547,7 @@ groupMemberQuery =
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
- c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
+ c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
@@ -703,7 +706,7 @@ getContactViaMember db user@User {userId} GroupMember {groupMemberId} =
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
-- Connection
- c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
+ c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct
@@ -1013,7 +1016,7 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} =
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
- c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
+ c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
FROM group_members m
JOIN contacts ct ON ct.contact_id = m.contact_id
@@ -1047,7 +1050,7 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} =
ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, ct.via_group, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
p.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
- c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
+ c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
@@ -1176,13 +1179,32 @@ getMatchingContacts db user@User {userId} Contact {contactId, profile = LocalPro
AND p.display_name = ? AND p.full_name = ?
|]
+getMatchingMembers :: DB.Connection -> User -> Contact -> IO [GroupMember]
+getMatchingMembers db user@User {userId} Contact {profile = LocalProfile {displayName, fullName, image}} = do
+ memberIds <-
+ map fromOnly <$> case image of
+ Just img -> DB.query db (q <> " AND p.image = ?") (userId, GCUserMember, displayName, fullName, img)
+ Nothing -> DB.query db (q <> " AND p.image is NULL") (userId, GCUserMember, displayName, fullName)
+ filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db user) memberIds
+ where
+ -- only match with members without associated contact
+ q =
+ [sql|
+ SELECT m.group_member_id
+ FROM group_members m
+ JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
+ WHERE m.user_id = ? AND m.contact_id IS NULL
+ AND m.member_category != ?
+ AND p.display_name = ? AND p.full_name = ?
+ |]
+
getMatchingMemberContacts :: DB.Connection -> User -> GroupMember -> IO [Contact]
getMatchingMemberContacts _ _ GroupMember {memberContactId = Just _} = pure []
getMatchingMemberContacts db user@User {userId} GroupMember {memberProfile = LocalProfile {displayName, fullName, image}} = do
contactIds <-
map fromOnly <$> case image of
- Just img -> DB.query db (q <> " AND p.image = ?") (userId, displayName, fullName, img)
- Nothing -> DB.query db (q <> " AND p.image is NULL") (userId, displayName, fullName)
+ Just img -> DB.query db (q <> " AND p.image = ?") (userId, CSActive, displayName, fullName, img)
+ Nothing -> DB.query db (q <> " AND p.image is NULL") (userId, CSActive, displayName, fullName)
rights <$> mapM (runExceptT . getContact db user) contactIds
where
q =
@@ -1191,55 +1213,54 @@ getMatchingMemberContacts db user@User {userId} GroupMember {memberProfile = Loc
FROM contacts ct
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
WHERE ct.user_id = ?
- AND ct.deleted = 0
+ AND ct.contact_status = ? AND ct.deleted = 0
AND p.display_name = ? AND p.full_name = ?
|]
-createSentProbe :: DB.Connection -> TVar ChaChaDRG -> UserId -> ContactOrGroupMember -> ExceptT StoreError IO (Probe, Int64)
+createSentProbe :: DB.Connection -> TVar ChaChaDRG -> UserId -> ContactOrMember -> ExceptT StoreError IO (Probe, Int64)
createSentProbe db gVar userId to =
createWithRandomBytes 32 gVar $ \probe -> do
currentTs <- getCurrentTime
- let (ctId, gmId) = contactOrGroupMemberIds to
+ let (ctId, gmId) = contactOrMemberIds to
DB.execute
db
"INSERT INTO sent_probes (contact_id, group_member_id, probe, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)"
(ctId, gmId, probe, userId, currentTs, currentTs)
- (Probe probe,) <$> insertedRowId db
+ (Probe probe,) <$> insertedRowId db
-createSentProbeHash :: DB.Connection -> UserId -> Int64 -> ContactOrGroupMember -> IO ()
+createSentProbeHash :: DB.Connection -> UserId -> Int64 -> ContactOrMember -> IO ()
createSentProbeHash db userId probeId to = do
currentTs <- getCurrentTime
- let (ctId, gmId) = contactOrGroupMemberIds to
+ let (ctId, gmId) = contactOrMemberIds to
DB.execute
db
"INSERT INTO sent_probe_hashes (sent_probe_id, contact_id, group_member_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)"
(probeId, ctId, gmId, userId, currentTs, currentTs)
-matchReceivedProbe :: DB.Connection -> User -> ContactOrGroupMember -> Probe -> IO (Maybe ContactOrGroupMember)
+matchReceivedProbe :: DB.Connection -> User -> ContactOrMember -> Probe -> IO [ContactOrMember]
matchReceivedProbe db user@User {userId} from (Probe probe) = do
let probeHash = C.sha256Hash probe
cgmIds <-
- maybeFirstRow id $
- DB.query
- db
- [sql|
- SELECT r.contact_id, g.group_id, r.group_member_id
- FROM received_probes r
- LEFT JOIN contacts c ON r.contact_id = c.contact_id AND c.deleted = 0
- LEFT JOIN group_members m ON r.group_member_id = m.group_member_id
- LEFT JOIN groups g ON g.group_id = m.group_id
- WHERE r.user_id = ? AND r.probe_hash = ? AND r.probe IS NULL
- |]
- (userId, probeHash)
+ DB.query
+ db
+ [sql|
+ SELECT r.contact_id, g.group_id, r.group_member_id
+ FROM received_probes r
+ LEFT JOIN contacts c ON r.contact_id = c.contact_id AND c.deleted = 0
+ LEFT JOIN group_members m ON r.group_member_id = m.group_member_id
+ LEFT JOIN groups g ON g.group_id = m.group_id
+ WHERE r.user_id = ? AND r.probe_hash = ? AND r.probe IS NULL
+ |]
+ (userId, probeHash)
currentTs <- getCurrentTime
- let (ctId, gmId) = contactOrGroupMemberIds from
+ let (ctId, gmId) = contactOrMemberIds from
DB.execute
db
"INSERT INTO received_probes (contact_id, group_member_id, probe, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
(ctId, gmId, probe, probeHash, userId, currentTs, currentTs)
- pure cgmIds $>>= getContactOrGroupMember_ db user
+ catMaybes <$> mapM (getContactOrMember_ db user) cgmIds
-matchReceivedProbeHash :: DB.Connection -> User -> ContactOrGroupMember -> ProbeHash -> IO (Maybe (ContactOrGroupMember, Probe))
+matchReceivedProbeHash :: DB.Connection -> User -> ContactOrMember -> ProbeHash -> IO (Maybe (ContactOrMember, Probe))
matchReceivedProbeHash db user@User {userId} from (ProbeHash probeHash) = do
probeIds <-
maybeFirstRow id $
@@ -1255,18 +1276,18 @@ matchReceivedProbeHash db user@User {userId} from (ProbeHash probeHash) = do
|]
(userId, probeHash)
currentTs <- getCurrentTime
- let (ctId, gmId) = contactOrGroupMemberIds from
+ let (ctId, gmId) = contactOrMemberIds from
DB.execute
db
"INSERT INTO received_probes (contact_id, group_member_id, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)"
(ctId, gmId, probeHash, userId, currentTs, currentTs)
- pure probeIds $>>= \(Only probe :. cgmIds) -> (,Probe probe) <$$> getContactOrGroupMember_ db user cgmIds
+ pure probeIds $>>= \(Only probe :. cgmIds) -> (,Probe probe) <$$> getContactOrMember_ db user cgmIds
-matchSentProbe :: DB.Connection -> User -> ContactOrGroupMember -> Probe -> IO (Maybe ContactOrGroupMember)
-matchSentProbe db user@User {userId} _from (Probe probe) =
- cgmIds $>>= getContactOrGroupMember_ db user
+matchSentProbe :: DB.Connection -> User -> ContactOrMember -> Probe -> IO (Maybe ContactOrMember)
+matchSentProbe db user@User {userId} _from (Probe probe) =
+ cgmIds $>>= getContactOrMember_ db user
where
- (ctId, gmId) = contactOrGroupMemberIds _from
+ (ctId, gmId) = contactOrMemberIds _from
cgmIds =
maybeFirstRow id $
DB.query
@@ -1283,80 +1304,103 @@ matchSentProbe db user@User {userId} _from (Probe probe) =
|]
(userId, probe, ctId, gmId)
-getContactOrGroupMember_ :: DB.Connection -> User -> (Maybe ContactId, Maybe GroupId, Maybe GroupMemberId) -> IO (Maybe ContactOrGroupMember)
-getContactOrGroupMember_ db user ids =
+getContactOrMember_ :: DB.Connection -> User -> (Maybe ContactId, Maybe GroupId, Maybe GroupMemberId) -> IO (Maybe ContactOrMember)
+getContactOrMember_ db user ids =
fmap eitherToMaybe . runExceptT $ case ids of
- (Just ctId, _, _) -> CGMContact <$> getContact db user ctId
- (_, Just gId, Just gmId) -> CGMGroupMember <$> getGroupInfo db user gId <*> getGroupMember db user gId gmId
+ (Just ctId, _, _) -> COMContact <$> getContact db user ctId
+ (_, Just gId, Just gmId) -> COMGroupMember <$> getGroupMember db user gId gmId
_ -> throwError $ SEInternalError ""
-mergeContactRecords :: DB.Connection -> UserId -> Contact -> Contact -> IO ()
-mergeContactRecords db userId ct1 ct2 = do
- let (toCt, fromCt) = toFromContacts ct1 ct2
- Contact {contactId = toContactId} = toCt
- Contact {contactId = fromContactId, localDisplayName} = fromCt
- currentTs <- getCurrentTime
- -- TODO next query fixes incorrect unused contacts deletion; consider more thorough fix
- when (contactDirect toCt && not (contactUsed toCt)) $
+-- connection being verified and connection level 0 have priority over requested merge direction;
+-- if requested merge direction is overruled, keepLDN is kept
+mergeContactRecords :: DB.Connection -> User -> Contact -> Contact -> ExceptT StoreError IO Contact
+mergeContactRecords db user@User {userId} to@Contact {localDisplayName = keepLDN} from = do
+ let (toCt, fromCt) = checkToFromContacts
+ Contact {contactId = toContactId, localDisplayName = toLDN} = toCt
+ Contact {contactId = fromContactId, localDisplayName = fromLDN} = fromCt
+ liftIO $ do
+ currentTs <- getCurrentTime
+ -- next query fixes incorrect unused contacts deletion
+ when (contactDirect toCt && not (contactUsed toCt)) $
+ DB.execute
+ db
+ "UPDATE contacts SET contact_used = 1, updated_at = ? WHERE user_id = ? AND contact_id = ?"
+ (currentTs, userId, toContactId)
DB.execute
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 = ?"
- (toContactId, currentTs, fromContactId, userId)
- DB.execute
- db
- "UPDATE group_members SET invited_by = ?, updated_at = ? WHERE invited_by = ? AND user_id = ?"
- (toContactId, currentTs, fromContactId, userId)
- DB.execute
- db
- "UPDATE chat_items SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?"
- (toContactId, currentTs, fromContactId, userId)
- DB.executeNamed
- db
- [sql|
- UPDATE group_members
- SET contact_id = :to_contact_id,
- local_display_name = (SELECT local_display_name FROM contacts WHERE contact_id = :to_contact_id),
- contact_profile_id = (SELECT contact_profile_id FROM contacts WHERE contact_id = :to_contact_id),
- updated_at = :updated_at
- WHERE contact_id = :from_contact_id
- AND user_id = :user_id
- |]
- [ ":to_contact_id" := toContactId,
- ":from_contact_id" := fromContactId,
- ":user_id" := userId,
- ":updated_at" := currentTs
- ]
- deleteContactProfile_ db userId fromContactId
- DB.execute db "DELETE FROM contacts WHERE contact_id = ? AND user_id = ?" (fromContactId, userId)
- deleteUnusedDisplayName_ db userId localDisplayName
+ "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 = ?"
+ (toContactId, currentTs, fromContactId, userId)
+ DB.execute
+ db
+ "UPDATE group_members SET invited_by = ?, updated_at = ? WHERE invited_by = ? AND user_id = ?"
+ (toContactId, currentTs, fromContactId, userId)
+ DB.execute
+ db
+ "UPDATE chat_items SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?"
+ (toContactId, currentTs, fromContactId, userId)
+ DB.execute
+ db
+ "UPDATE sent_probes SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?"
+ (toContactId, currentTs, fromContactId, userId)
+ DB.execute
+ db
+ "UPDATE sent_probe_hashes SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?"
+ (toContactId, currentTs, fromContactId, userId)
+ DB.execute
+ db
+ "UPDATE received_probes SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?"
+ (toContactId, currentTs, fromContactId, userId)
+ DB.executeNamed
+ db
+ [sql|
+ UPDATE group_members
+ SET contact_id = :to_contact_id,
+ local_display_name = (SELECT local_display_name FROM contacts WHERE contact_id = :to_contact_id),
+ contact_profile_id = (SELECT contact_profile_id FROM contacts WHERE contact_id = :to_contact_id),
+ updated_at = :updated_at
+ WHERE contact_id = :from_contact_id
+ AND user_id = :user_id
+ |]
+ [ ":to_contact_id" := toContactId,
+ ":from_contact_id" := fromContactId,
+ ":user_id" := userId,
+ ":updated_at" := currentTs
+ ]
+ deleteContactProfile_ db userId fromContactId
+ DB.execute db "DELETE FROM contacts WHERE contact_id = ? AND user_id = ?" (fromContactId, userId)
+ deleteUnusedDisplayName_ db userId fromLDN
+ when (keepLDN /= toLDN && keepLDN == fromLDN) $
+ DB.execute
+ db
+ [sql|
+ UPDATE display_names
+ SET local_display_name = ?, updated_at = ?
+ WHERE user_id = ? AND local_display_name = ?
+ |]
+ (keepLDN, currentTs, userId, toLDN)
+ getContact db user toContactId
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)
+ checkToFromContacts :: (Contact, Contact)
+ checkToFromContacts
+ | vrfFrom && not vrfTo = (from, to)
+ | dirFrom && not vrfTo && not dirTo = (from, to)
+ | otherwise = (to, from)
where
- d1 = directOrUsed c1
- d2 = directOrUsed c2
- ctCreatedAt Contact {createdAt} = createdAt
+ vrfTo = isJust $ contactSecurityCode to
+ vrfFrom = isJust $ contactSecurityCode from
+ dirTo = let Contact {activeConn = Connection {connLevel = clTo}} = to in clTo == 0
+ dirFrom = let Contact {activeConn = Connection {connLevel = clFrom}} = from in clFrom == 0
-updateMemberContact :: DB.Connection -> User -> Contact -> GroupMember -> IO ()
-updateMemberContact
+associateMemberWithContactRecord :: DB.Connection -> User -> Contact -> GroupMember -> IO ()
+associateMemberWithContactRecord
db
User {userId}
Contact {contactId, localDisplayName, profile = LocalProfile {profileId}}
GroupMember {groupId, groupMemberId, localDisplayName = memLDN, memberProfile = LocalProfile {profileId = memProfileId}} = do
- -- TODO possibly, we should update profiles and local_display_names of all members linked to the same remote user,
- -- once we decide on how we identify it, either based on shared contact_profile_id or on local_display_name
currentTs <- getCurrentTime
DB.execute
db
@@ -1369,6 +1413,34 @@ updateMemberContact
when (memProfileId /= profileId) $ deleteUnusedProfile_ db userId memProfileId
when (memLDN /= localDisplayName) $ deleteUnusedDisplayName_ db userId memLDN
+associateContactWithMemberRecord :: DB.Connection -> User -> GroupMember -> Contact -> ExceptT StoreError IO Contact
+associateContactWithMemberRecord
+ db
+ user@User {userId}
+ GroupMember {groupId, groupMemberId, localDisplayName = memLDN, memberProfile = LocalProfile {profileId = memProfileId}}
+ Contact {contactId, localDisplayName, profile = LocalProfile {profileId}} = do
+ liftIO $ do
+ currentTs <- getCurrentTime
+ DB.execute
+ db
+ [sql|
+ UPDATE group_members
+ SET contact_id = ?, updated_at = ?
+ WHERE user_id = ? AND group_id = ? AND group_member_id = ?
+ |]
+ (contactId, currentTs, userId, groupId, groupMemberId)
+ DB.execute
+ db
+ [sql|
+ UPDATE contacts
+ SET local_display_name = ?, contact_profile_id = ?, updated_at = ?
+ WHERE user_id = ? AND contact_id = ?
+ |]
+ (memLDN, memProfileId, currentTs, userId, contactId)
+ when (profileId /= memProfileId) $ deleteUnusedProfile_ db userId profileId
+ when (localDisplayName /= memLDN) $ deleteUnusedDisplayName_ db userId localDisplayName
+ getContact db user contactId
+
deleteUnusedDisplayName_ :: DB.Connection -> UserId -> ContactName -> IO ()
deleteUnusedDisplayName_ db userId localDisplayName =
DB.executeNamed
@@ -1407,6 +1479,12 @@ deleteUnusedDisplayName_ db userId localDisplayName =
|]
[":user_id" := userId, ":local_display_name" := localDisplayName]
+deleteOldProbes :: DB.Connection -> UTCTime -> IO ()
+deleteOldProbes db createdAtCutoff = do
+ DB.execute db "DELETE FROM sent_probes WHERE created_at <= ?" (Only createdAtCutoff)
+ DB.execute db "DELETE FROM sent_probe_hashes WHERE created_at <= ?" (Only createdAtCutoff)
+ DB.execute db "DELETE FROM received_probes WHERE created_at <= ?" (Only createdAtCutoff)
+
updateGroupSettings :: DB.Connection -> User -> Int64 -> ChatSettings -> IO ()
updateGroupSettings db User {userId} groupId ChatSettings {enableNtfs, sendRcpts, favorite} =
DB.execute db "UPDATE groups SET enable_ntfs = ?, send_rcpts = ?, favorite = ? WHERE user_id = ? AND group_id = ?" (enableNtfs, sendRcpts, favorite, userId, groupId)
@@ -1511,15 +1589,15 @@ createMemberContact
db
[sql|
INSERT INTO connections (
- user_id, agent_conn_id, conn_req_inv, conn_level, conn_status, conn_type, contact_id, custom_user_profile_id,
+ user_id, agent_conn_id, conn_req_inv, conn_level, conn_status, conn_type, contact_conn_initiated, contact_id, custom_user_profile_id,
peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
- ( (userId, acId, cReq, connLevel, ConnNew, ConnContact, contactId, customUserProfileId)
+ ( (userId, acId, cReq, connLevel, ConnNew, ConnContact, True, contactId, customUserProfileId)
:. (minV, maxV, currentTs, currentTs, subMode == SMOnlyCreate)
)
connId <- insertedRowId db
- let ctConn = Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connType = ConnContact, entityId = Just contactId, viaContact = Nothing, viaUserContactLink = Nothing, viaGroupLink = False, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
+ let ctConn = Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connType = ConnContact, contactConnInitiated = True, entityId = Just contactId, viaContact = Nothing, viaUserContactLink = Nothing, viaGroupLink = False, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False}
@@ -1622,9 +1700,9 @@ createMemberContactConn_
peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|]
- ( (userId, acId, connLevel, ConnNew, ConnContact, contactId, customUserProfileId)
+ ( (userId, acId, connLevel, ConnJoined, ConnContact, contactId, customUserProfileId)
:. (minV, maxV, currentTs, currentTs, subMode == SMOnlyCreate)
)
connId <- insertedRowId db
setCommandConnId db user cmdId connId
- pure Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connType = ConnContact, entityId = Just contactId, viaContact = Nothing, viaUserContactLink = Nothing, viaGroupLink = False, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
+ pure Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connType = ConnContact, contactConnInitiated = False, entityId = Just contactId, viaContact = Nothing, viaUserContactLink = Nothing, viaGroupLink = False, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnJoined, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs
index 458944b6e5..b996f7626f 100644
--- a/src/Simplex/Chat/Store/Messages.hs
+++ b/src/Simplex/Chat/Store/Messages.hs
@@ -481,7 +481,7 @@ getDirectChatPreviews_ db user@User {userId} = do
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
-- Connection
- c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
+ c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version,
-- ChatStats
diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs
index 2f5bfec9ea..3ef68874b0 100644
--- a/src/Simplex/Chat/Store/Migrations.hs
+++ b/src/Simplex/Chat/Store/Migrations.hs
@@ -82,6 +82,7 @@ import Simplex.Chat.Migrations.M20230903_connections_to_subscribe
import Simplex.Chat.Migrations.M20230913_member_contacts
import Simplex.Chat.Migrations.M20230914_member_probes
import Simplex.Chat.Migrations.M20230926_contact_status
+import Simplex.Chat.Migrations.M20231002_conn_initiated
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -163,7 +164,8 @@ schemaMigrations =
("20230903_connections_to_subscribe", m20230903_connections_to_subscribe, Just down_m20230903_connections_to_subscribe),
("20230913_member_contacts", m20230913_member_contacts, Just down_m20230913_member_contacts),
("20230914_member_probes", m20230914_member_probes, Just down_m20230914_member_probes),
- ("20230926_contact_status", m20230926_contact_status, Just down_m20230926_contact_status)
+ ("20230926_contact_status", m20230926_contact_status, Just down_m20230926_contact_status),
+ ("20231002_conn_initiated", m20231002_conn_initiated, Just down_m20231002_conn_initiated)
]
-- | The list of migrations in ascending order by date
diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs
index e521cb43cf..a577796810 100644
--- a/src/Simplex/Chat/Store/Profiles.hs
+++ b/src/Simplex/Chat/Store/Profiles.hs
@@ -320,7 +320,7 @@ getUserAddressConnections db User {userId} = do
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
- c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
+ c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
@@ -335,7 +335,7 @@ getUserContactLinks db User {userId} =
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
- c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
+ c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version,
uc.user_contact_link_id, uc.conn_req_contact, uc.group_id
FROM connections c
diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs
index 4dc4f6e82d..3ff765b75d 100644
--- a/src/Simplex/Chat/Store/Shared.hs
+++ b/src/Simplex/Chat/Store/Shared.hs
@@ -138,16 +138,16 @@ toFileInfo (fileId, fileStatus, filePath) = CIFileInfo {fileId, fileStatus, file
type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64)
-type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, Int, Version, Version)
+type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, Bool, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, Int, Version, Version)
-type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe Int, Maybe Version, Maybe Version)
+type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Bool, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe Int, Maybe Version, Maybe Version)
toConnection :: ConnectionRow -> Connection
-toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter, minVer, maxVer)) =
+toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter, minVer, maxVer)) =
let entityId = entityId_ connType
connectionCode = SecurityCode <$> code_ <*> verifiedAt_
peerChatVRange = JVersionRange $ fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
- in Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, connectionCode, authErrCounter, createdAt}
+ in Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias, entityId, connectionCode, authErrCounter, createdAt}
where
entityId_ :: ConnType -> Maybe Int64
entityId_ ConnContact = contactId
@@ -157,8 +157,8 @@ toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroup
entityId_ ConnUserContact = userContactLinkId
toMaybeConnection :: MaybeConnectionRow -> Maybe Connection
-toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just authErrCounter, Just minVer, Just maxVer)) =
- Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter, minVer, maxVer))
+toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just authErrCounter, Just minVer, Just maxVer)) =
+ Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter, minVer, maxVer))
toMaybeConnection _ = Nothing
createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> VersionRange -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> SubscriptionMode -> IO Connection
@@ -180,7 +180,7 @@ createConnection_ db userId connType entityId acId peerChatVRange@(VersionRange
:. (minV, maxV, subMode == SMOnlyCreate)
)
connId <- insertedRowId db
- pure Connection {connId, agentConnId = AgentConnId acId, peerChatVRange = JVersionRange peerChatVRange, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
+ pure Connection {connId, agentConnId = AgentConnId acId, peerChatVRange = JVersionRange peerChatVRange, connType, contactConnInitiated = False, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
where
ent ct = if connType == ct then entityId else Nothing
diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs
index 43265671b6..529d2bf019 100644
--- a/src/Simplex/Chat/Types.hs
+++ b/src/Simplex/Chat/Types.hs
@@ -244,18 +244,18 @@ data ContactRef = ContactRef
instance ToJSON ContactRef where toEncoding = J.genericToEncoding J.defaultOptions
-data ContactOrGroupMember = CGMContact Contact | CGMGroupMember GroupInfo GroupMember
+data ContactOrMember = COMContact Contact | COMGroupMember GroupMember
deriving (Show)
-contactOrGroupMemberIds :: ContactOrGroupMember -> (Maybe ContactId, Maybe GroupMemberId)
-contactOrGroupMemberIds = \case
- CGMContact Contact {contactId} -> (Just contactId, Nothing)
- CGMGroupMember _ GroupMember {groupMemberId} -> (Nothing, Just groupMemberId)
+contactOrMemberIds :: ContactOrMember -> (Maybe ContactId, Maybe GroupMemberId)
+contactOrMemberIds = \case
+ COMContact Contact {contactId} -> (Just contactId, Nothing)
+ COMGroupMember GroupMember {groupMemberId} -> (Nothing, Just groupMemberId)
-contactOrGroupMemberIncognito :: ContactOrGroupMember -> IncognitoEnabled
-contactOrGroupMemberIncognito = \case
- CGMContact ct -> contactConnIncognito ct
- CGMGroupMember _ m -> memberIncognito m
+contactOrMemberIncognito :: ContactOrMember -> IncognitoEnabled
+contactOrMemberIncognito = \case
+ COMContact ct -> contactConnIncognito ct
+ COMGroupMember m -> memberIncognito m
data UserContact = UserContact
{ userContactLinkId :: Int64,
@@ -1235,6 +1235,7 @@ data Connection = Connection
customUserProfileId :: Maybe Int64,
connType :: ConnType,
connStatus :: ConnStatus,
+ contactConnInitiated :: Bool,
localAlias :: Text,
entityId :: Maybe Int64, -- contact, group member, file ID or user contact ID
connectionCode :: Maybe SecurityCode,
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index 5eb9df3ad2..b981929efc 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -175,7 +175,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
CRContactAliasUpdated u c -> ttyUser u $ viewContactAliasUpdated c
CRConnectionAliasUpdated u c -> ttyUser u $ viewConnectionAliasUpdated c
CRContactUpdated {user = u, fromContact = c, toContact = c'} -> ttyUser u $ viewContactUpdated c c' <> viewContactPrefsUpdated u c c'
- CRContactsMerged u intoCt mergedCt -> ttyUser u $ viewContactsMerged intoCt mergedCt
+ CRContactsMerged u intoCt mergedCt ct' -> ttyUser u $ viewContactsMerged intoCt mergedCt ct'
CRReceivedContactRequest u UserContactRequest {localDisplayName = c, profile} -> ttyUser u $ viewReceivedContactRequest c profile
CRRcvFileStart u ci -> ttyUser u $ receivingFile_' "started" ci
CRRcvFileComplete u ci -> ttyUser u $ receivingFile_' "completed" ci
@@ -236,7 +236,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
CRNewMemberContact u _ g m -> ttyUser u ["contact for member " <> ttyGroup' g <> " " <> ttyMember m <> " is created"]
CRNewMemberContactSentInv u _ct g m -> ttyUser u ["sent invitation to connect directly to member " <> ttyGroup' g <> " " <> ttyMember m]
CRNewMemberContactReceivedInv u ct g m -> ttyUser u [ttyGroup' g <> " " <> ttyMember m <> " is creating direct contact " <> ttyContact' ct <> " with you"]
- CRMemberContactConnected u ct g m -> ttyUser u ["member " <> ttyGroup' g <> " " <> ttyMember m <> " is merged into " <> ttyContact' ct]
+ CRContactAndMemberAssociated u ct g m ct' -> ttyUser u $ viewContactAndMemberAssociated ct g m ct'
CRMemberSubError u g m e -> ttyUser u [ttyGroup' g <> " member " <> ttyMember m <> " error: " <> sShow e]
CRMemberSubSummary u summary -> ttyUser u $ viewErrorsSummary (filter (isJust . memberError) summary) " group member errors"
CRGroupSubscribed u g -> ttyUser u $ viewGroupSubscribed g
@@ -891,12 +891,18 @@ groupInvitation' g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfil
Just mp -> " to join as " <> incognitoProfile' (fromLocalProfile mp) <> ", "
Nothing -> " to join, "
-viewContactsMerged :: Contact -> Contact -> [StyledString]
-viewContactsMerged c1 c2 =
+viewContactsMerged :: Contact -> Contact -> Contact -> [StyledString]
+viewContactsMerged c1 c2 ct' =
[ "contact " <> ttyContact' c2 <> " is merged into " <> ttyContact' c1,
- "use " <> ttyToContact' c1 <> highlight' "" <> " to send messages"
+ "use " <> ttyToContact' ct' <> highlight' "" <> " to send messages"
]
+viewContactAndMemberAssociated :: Contact -> GroupInfo -> GroupMember -> Contact -> [StyledString]
+viewContactAndMemberAssociated ct g m ct' =
+ [ "contact and member are merged: " <> ttyContact' ct <> ", " <> ttyGroup' g <> " " <> ttyMember m,
+ "use " <> ttyToContact' ct' <> highlight' "" <> " to send messages"
+ ]
+
viewUserProfile :: Profile -> [StyledString]
viewUserProfile Profile {displayName, fullName} =
[ "user profile: " <> ttyFullName displayName fullName,
diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs
index 5c4d96cc94..a8f9fbf9c6 100644
--- a/tests/ChatTests/Direct.hs
+++ b/tests/ChatTests/Direct.hs
@@ -29,7 +29,8 @@ import Test.Hspec
chatDirectTests :: SpecWith FilePath
chatDirectTests = do
describe "direct messages" $ do
- describe "add contact and send/receive message" testAddContact
+ describe "add contact and send/receive messages" testAddContact
+ it "clear chat with contact" testContactClear
it "deleting contact deletes profile" testDeleteContactDeletesProfile
it "unused contact is deleted silently" testDeleteUnusedContactSilent
it "direct message quoted replies" testDirectMessageQuotedReply
@@ -40,6 +41,9 @@ chatDirectTests = do
it "direct timed message" testDirectTimedMessage
it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact
it "should send multiline message" testMultilineMessage
+ describe "contact merge" $ do
+ it "merge duplicate contacts" testContactMerge
+ it "new contact should merge with multiple existing contacts" testMergeContactMultipleContacts
describe "SMP servers" $ do
it "get and set SMP servers" testGetSetSMPServers
it "test SMP server connection" testTestSMPServerConnection
@@ -140,35 +144,6 @@ testAddContact = versionTestMatrix2 runTestAddContact
bob #> "@alice how are you?"
alice <# "bob> how are you?"
chatsManyMessages alice bob
- -- test adding the same contact one more time - local name will be different
- alice ##> "/c"
- inv' <- getInvitation alice
- bob ##> ("/c " <> inv')
- bob <## "confirmation sent!"
- concurrently_
- (bob <## "alice_1 (Alice): contact is connected")
- (alice <## "bob_1 (Bob): contact is connected")
- alice #> "@bob_1 hello"
- bob <# "alice_1> hello"
- bob #> "@alice_1 hi"
- alice <# "bob_1> hi"
- alice @@@ [("@bob_1", "hi"), ("@bob", "how are you?")]
- bob @@@ [("@alice_1", "hi"), ("@alice", "how are you?")]
- -- test deleting contact
- alice ##> "/d bob_1"
- alice <## "bob_1: contact is deleted"
- bob <## "alice_1 (Alice) deleted contact with you"
- alice ##> "@bob_1 hey"
- alice <## "no contact bob_1"
- alice @@@ [("@bob", "how are you?")]
- alice `hasContactProfiles` ["alice", "bob"]
- bob @@@ [("@alice_1", "contact deleted"), ("@alice", "how are you?")]
- bob `hasContactProfiles` ["alice", "alice", "bob"]
- -- test clearing chat
- alice #$> ("/clear bob", id, "bob: all messages are removed locally ONLY")
- alice #$> ("/_get chat @2 count=100", chat, [])
- bob #$> ("/clear alice", id, "alice: all messages are removed locally ONLY")
- bob #$> ("/_get chat @2 count=100", chat, [])
chatsEmpty alice bob = do
alice @@@ [("@bob", lastChatFeature)]
alice #$> ("/_get chat @2 count=100", chat, chatFeatures)
@@ -195,6 +170,101 @@ testAddContact = versionTestMatrix2 runTestAddContact
alice #$> ("/_read chat @2", id, "ok")
bob #$> ("/_read chat @2", id, "ok")
+testContactMerge :: HasCallStack => FilePath -> IO ()
+testContactMerge =
+ testChat2 aliceProfile bobProfile $
+ \alice bob -> do
+ connectUsers alice bob
+ alice <##> bob
+
+ alice ##> "/c"
+ inv' <- getInvitation alice
+ bob ##> ("/c " <> inv')
+ bob <## "confirmation sent!"
+ concurrentlyN_
+ [ alice
+ <### [ "bob_1 (Bob): contact is connected",
+ "contact bob_1 is merged into bob",
+ "use @bob to send messages"
+ ],
+ bob
+ <### [ "alice_1 (Alice): contact is connected",
+ "contact alice_1 is merged into alice",
+ "use @alice to send messages"
+ ]
+ ]
+ alice <##> bob
+ alice @@@ [("@bob", "hey")]
+ alice `hasContactProfiles` ["alice", "bob"]
+ bob @@@ [("@alice", "hey")]
+ bob `hasContactProfiles` ["bob", "alice"]
+
+testMergeContactMultipleContacts :: HasCallStack => FilePath -> IO ()
+testMergeContactMultipleContacts =
+ testChat2 aliceProfile bobProfile $
+ \alice bob -> do
+ bob ##> "/contact_merge off"
+ bob <## "ok"
+
+ connectUsers alice bob
+
+ alice ##> "/c"
+ inv' <- getInvitation alice
+ bob ##> ("/c " <> inv')
+ bob <## "confirmation sent!"
+ concurrently_
+ (alice <## "bob_1 (Bob): contact is connected")
+ (bob <## "alice_1 (Alice): contact is connected")
+
+ alice `hasContactProfiles` ["alice", "bob", "bob"]
+ bob `hasContactProfiles` ["bob", "alice", "alice"]
+
+ threadDelay 500000
+
+ bob ##> "/contact_merge on"
+ bob <## "ok"
+
+ alice ##> "/c"
+ inv'' <- getInvitation alice
+ bob ##> ("/c " <> inv'')
+ bob <## "confirmation sent!"
+ concurrentlyN_
+ [ alice
+ <### [ "bob_2 (Bob): contact is connected",
+ StartsWith "contact bob_2 is merged into bob",
+ StartsWith "use @bob",
+ StartsWith "contact bob_1 is merged into bob",
+ StartsWith "use @bob"
+ ],
+ bob
+ <### [ "alice_2 (Alice): contact is connected",
+ StartsWith "contact alice_2 is merged into alice",
+ StartsWith "use @alice",
+ StartsWith "contact alice_1 is merged into alice",
+ StartsWith "use @alice"
+ ]
+ ]
+ alice <##> bob
+
+ alice ##> "/contacts"
+ alice <## "bob (Bob)"
+ bob ##> "/contacts"
+ bob <## "alice (Alice)"
+ alice `hasContactProfiles` ["alice", "bob"]
+ bob `hasContactProfiles` ["bob", "alice"]
+
+testContactClear :: HasCallStack => FilePath -> IO ()
+testContactClear =
+ testChat2 aliceProfile bobProfile $
+ \alice bob -> do
+ connectUsers alice bob
+ alice <##> bob
+ threadDelay 500000
+ alice #$> ("/clear bob", id, "bob: all messages are removed locally ONLY")
+ alice #$> ("/_get chat @2 count=100", chat, [])
+ bob #$> ("/clear alice", id, "alice: all messages are removed locally ONLY")
+ bob #$> ("/_get chat @2 count=100", chat, [])
+
testDeleteContactDeletesProfile :: HasCallStack => FilePath -> IO ()
testDeleteContactDeletesProfile =
testChat2 aliceProfile bobProfile $
@@ -2104,7 +2174,7 @@ testMsgDecryptError tmp =
withTestChat tmp "bob" $ \bob -> do
bob <## "1 contacts connected (use /cs for the list)"
alice #> "@bob hello again"
- bob <# "alice> skipped message ID 9..11"
+ bob <# "alice> skipped message ID 10..12"
bob <# "alice> hello again"
bob #> "@alice received!"
alice <# "bob> received!"
diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs
index 4280810ca6..083d2f85be 100644
--- a/tests/ChatTests/Groups.hs
+++ b/tests/ChatTests/Groups.hs
@@ -73,7 +73,14 @@ chatGroupTests = do
testNoDirect _1 _0 False
testNoDirect _1 _1 False
it "members have different local display names in different groups" testNoDirectDifferentLDNs
- it "member should connect to contact when profile match" testConnectMemberToContact
+ describe "merge members and contacts" $ do
+ it "new member should merge with existing contact" testMergeMemberExistingContact
+ it "new member should merge with multiple existing contacts" testMergeMemberMultipleContacts
+ it "new contact should merge with existing member" testMergeContactExistingMember
+ it "new contact should merge with existing member with associated contact" testMergeContactExistingMemberWithContact
+ it "new contact should merge with multiple existing members" testMergeContactMultipleMembers
+ it "new contact should merge with both existing members and contacts" testMergeContactExistingMembersAndContacts
+ it "new member contact is merged with existing contact" testMergeMemberContact
describe "create member contact" $ do
it "create contact with group member with invitation message" testMemberContactMessage
it "create contact with group member without invitation message" testMemberContactNoMessage
@@ -2734,8 +2741,8 @@ testNoDirectDifferentLDNs =
bob <# ("#" <> gName <> " " <> cathLDN <> "> hey")
]
-testConnectMemberToContact :: HasCallStack => FilePath -> IO ()
-testConnectMemberToContact =
+testMergeMemberExistingContact :: HasCallStack => FilePath -> IO ()
+testMergeMemberExistingContact =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
connectUsers alice bob
@@ -2750,13 +2757,15 @@ testConnectMemberToContact =
[ do
alice <## "#team: you joined the group"
alice <## "#team: member cath_1 (Catherine) is connected"
- alice <## "member #team cath_1 is merged into cath",
+ alice <## "contact and member are merged: cath, #team cath_1"
+ alice <## "use @cath to send messages",
do
bob <## "#team: alice joined the group",
do
cath <## "#team: bob added alice_1 (Alice) to the group (connecting...)"
cath <## "#team: new member alice_1 is connected"
- cath <## "member #team alice_1 is merged into alice"
+ cath <## "contact and member are merged: alice, #team alice_1"
+ cath <## "use @alice to send messages"
]
alice <##> cath
alice #> "#team hello"
@@ -2779,6 +2788,334 @@ testConnectMemberToContact =
alice `hasContactProfiles` ["alice", "bob", "cath"]
cath `hasContactProfiles` ["cath", "alice", "bob"]
+testMergeMemberMultipleContacts :: HasCallStack => FilePath -> IO ()
+testMergeMemberMultipleContacts =
+ testChat3 aliceProfile bobProfile cathProfile $
+ \alice bob cath -> do
+ alice ##> "/contact_merge off"
+ alice <## "ok"
+
+ connectUsers alice bob
+ connectUsers alice cath
+
+ alice ##> "/c"
+ inv' <- getInvitation alice
+ cath ##> ("/c " <> inv')
+ cath <## "confirmation sent!"
+ concurrently_
+ (alice <## "cath_1 (Catherine): contact is connected")
+ (cath <## "alice_1 (Alice): contact is connected")
+
+ alice `hasContactProfiles` ["alice", "bob", "cath", "cath"]
+ cath `hasContactProfiles` ["cath", "alice", "alice"]
+
+ alice ##> "/contact_merge on"
+ alice <## "ok"
+
+ createGroup2 "team" bob cath
+ bob ##> "/a #team alice"
+ bob <## "invitation to join the group #team sent to alice"
+ alice <## "#team: bob invites you to join the group as member"
+ alice <## "use /j team to accept"
+ alice ##> "/j team"
+ concurrentlyN_
+ [ alice
+ <### [ "#team: you joined the group",
+ "#team: member cath_2 (Catherine) is connected",
+ StartsWith "contact and member are merged: cath",
+ StartsWith "use @cath",
+ StartsWith "contact cath_",
+ StartsWith "use @cath"
+ ],
+ bob <## "#team: alice joined the group",
+ cath
+ <### [ "#team: bob added alice_2 (Alice) to the group (connecting...)",
+ "#team: new member alice_2 is connected",
+ StartsWith "contact and member are merged: alice",
+ StartsWith "use @alice",
+ StartsWith "contact alice_",
+ StartsWith "use @alice"
+ ]
+ ]
+ alice <##> cath
+ alice #> "#team hello"
+ bob <# "#team alice> hello"
+ cath <# "#team alice> hello"
+ cath #> "#team hello too"
+ bob <# "#team cath> hello too"
+ alice <# "#team cath> hello too"
+
+ alice ##> "/contacts"
+ alice
+ <### [ "bob (Bob)",
+ "cath (Catherine)"
+ ]
+ cath ##> "/contacts"
+ cath
+ <### [ "alice (Alice)",
+ "bob (Bob)"
+ ]
+ alice `hasContactProfiles` ["alice", "bob", "cath"]
+ cath `hasContactProfiles` ["cath", "alice", "bob"]
+
+testMergeContactExistingMember :: HasCallStack => FilePath -> IO ()
+testMergeContactExistingMember =
+ testChat3 aliceProfile bobProfile cathProfile $
+ \alice bob cath -> do
+ createGroup3 "team" alice bob cath
+
+ bob ##> "/c"
+ inv' <- getInvitation bob
+ cath ##> ("/c " <> inv')
+ cath <## "confirmation sent!"
+ concurrentlyN_
+ [ bob
+ <### [ "cath_1 (Catherine): contact is connected",
+ "contact and member are merged: cath_1, #team cath",
+ "use @cath to send messages"
+ ],
+ cath
+ <### [ "bob_1 (Bob): contact is connected",
+ "contact and member are merged: bob_1, #team bob",
+ "use @bob to send messages"
+ ]
+ ]
+ bob <##> cath
+
+ bob ##> "/contacts"
+ bob <### ["alice (Alice)", "cath (Catherine)"]
+ cath ##> "/contacts"
+ cath <### ["alice (Alice)", "bob (Bob)"]
+ bob `hasContactProfiles` ["alice", "bob", "cath"]
+ cath `hasContactProfiles` ["cath", "alice", "bob"]
+
+testMergeContactExistingMemberWithContact :: HasCallStack => FilePath -> IO ()
+testMergeContactExistingMemberWithContact =
+ testChat3 aliceProfile bobProfile cathProfile $
+ \alice bob cath -> do
+ createGroup3 "team" alice bob cath
+
+ -- create contact, delete only for bob so he would send probe hash to member
+ bob ##> "/_create member contact #1 3"
+ bob <## "contact for member #team cath is created"
+
+ bob ##> "/_invite member contact @3 text hi"
+ bob
+ <### [ "sent invitation to connect directly to member #team cath",
+ WithTime "@cath hi"
+ ]
+ cath
+ <### [ "#team bob is creating direct contact bob with you",
+ WithTime "bob> hi"
+ ]
+ concurrently_
+ (bob <## "cath (Catherine): contact is connected")
+ (cath <## "bob (Bob): contact is connected")
+ bob <##> cath
+
+ bob ##> "/_delete @3 notify=off"
+ bob <## "cath: contact is deleted"
+
+ bob ##> "/contacts"
+ bob <### ["alice (Alice)"]
+ cath ##> "/contacts"
+ cath <### ["alice (Alice)", "bob (Bob)"]
+ bob `hasContactProfiles` ["alice", "bob", "cath"]
+ cath `hasContactProfiles` ["cath", "alice", "bob"]
+
+ -- contact connects, member is merged
+ bob ##> "/c"
+ inv' <- getInvitation bob
+ cath ##> ("/c " <> inv')
+ cath <## "confirmation sent!"
+ concurrentlyN_
+ [ bob
+ <### [ "cath_1 (Catherine): contact is connected",
+ "contact and member are merged: cath_1, #team cath",
+ "use @cath to send messages"
+ ],
+ cath
+ <### [ "bob_1 (Bob): contact is connected",
+ "contact bob_1 is merged into bob",
+ "use @bob to send messages"
+ ]
+ ]
+ bob <##> cath
+
+ bob ##> "/contacts"
+ bob <### ["alice (Alice)", "cath (Catherine)"]
+ cath ##> "/contacts"
+ cath <### ["alice (Alice)", "bob (Bob)"]
+ bob `hasContactProfiles` ["alice", "bob", "cath"]
+ cath `hasContactProfiles` ["cath", "alice", "bob"]
+
+testMergeContactMultipleMembers :: HasCallStack => FilePath -> IO ()
+testMergeContactMultipleMembers =
+ testChat3 aliceProfile bobProfile cathProfile $
+ \alice bob cath -> do
+ create2Groups3 "team" "club" alice bob cath
+
+ bob `hasContactProfiles` ["alice", "bob", "cath", "cath"]
+ cath `hasContactProfiles` ["cath", "alice", "bob", "bob"]
+
+ bob ##> "/c"
+ inv' <- getInvitation bob
+ cath ##> ("/c " <> inv')
+ cath <## "confirmation sent!"
+ concurrentlyN_
+ [ bob
+ <### [ "cath_2 (Catherine): contact is connected",
+ StartsWith "contact and member are merged: cath",
+ StartsWith "use @cath",
+ StartsWith "contact and member are merged: cath",
+ StartsWith "use @cath"
+ ],
+ cath
+ <### [ "bob_2 (Bob): contact is connected",
+ StartsWith "contact and member are merged: bob",
+ StartsWith "use @bob",
+ StartsWith "contact and member are merged: bob",
+ StartsWith "use @bob"
+ ]
+ ]
+ bob <##> cath
+
+ bob ##> "/contacts"
+ bob <### ["alice (Alice)", "cath (Catherine)"]
+ cath ##> "/contacts"
+ cath <### ["alice (Alice)", "bob (Bob)"]
+ bob `hasContactProfiles` ["alice", "bob", "cath"]
+ cath `hasContactProfiles` ["cath", "alice", "bob"]
+
+testMergeContactExistingMembersAndContacts :: HasCallStack => FilePath -> IO ()
+testMergeContactExistingMembersAndContacts =
+ testChat3 aliceProfile bobProfile cathProfile $
+ \alice bob cath -> do
+ bob ##> "/contact_merge off"
+ bob <## "ok"
+
+ create2Groups3 "team" "club" alice bob cath
+
+ bob ##> "/c"
+ inv' <- getInvitation bob
+ cath ##> ("/c " <> inv')
+ cath <## "confirmation sent!"
+ concurrently_
+ (bob <## "cath_2 (Catherine): contact is connected")
+ (cath <## "bob_2 (Bob): contact is connected")
+
+ bob `hasContactProfiles` ["alice", "bob", "cath", "cath", "cath"]
+ cath `hasContactProfiles` ["cath", "alice", "bob", "bob", "bob"]
+
+ bob ##> "/contact_merge on"
+ bob <## "ok"
+
+ bob ##> "/c"
+ inv'' <- getInvitation bob
+ cath ##> ("/c " <> inv'')
+ cath <## "confirmation sent!"
+ concurrentlyN_
+ [ bob
+ <### [ "cath_3 (Catherine): contact is connected",
+ StartsWith "contact and member are merged: cath",
+ StartsWith "use @cath",
+ StartsWith "contact and member are merged: cath",
+ StartsWith "use @cath",
+ StartsWith "contact cath_3 is merged into cath",
+ StartsWith "use @cath"
+ ],
+ cath
+ <### [ "bob_3 (Bob): contact is connected",
+ StartsWith "contact and member are merged: bob",
+ StartsWith "use @bob",
+ StartsWith "contact and member are merged: bob",
+ StartsWith "use @bob",
+ StartsWith "contact bob_3 is merged into bob",
+ StartsWith "use @bob"
+ ]
+ ]
+ bob <##> cath
+
+ bob ##> "/contacts"
+ bob <### ["alice (Alice)", "cath (Catherine)"]
+ cath ##> "/contacts"
+ cath <### ["alice (Alice)", "bob (Bob)"]
+ bob `hasContactProfiles` ["alice", "bob", "cath"]
+ cath `hasContactProfiles` ["cath", "alice", "bob"]
+
+testMergeMemberContact :: HasCallStack => FilePath -> IO ()
+testMergeMemberContact =
+ testChat3 aliceProfile bobProfile cathProfile $
+ \alice bob cath -> do
+ bob ##> "/contact_merge off"
+ bob <## "ok"
+
+ createGroup3 "team" alice bob cath
+
+ bob ##> "/c"
+ inv' <- getInvitation bob
+ cath ##> ("/c " <> inv')
+ cath <## "confirmation sent!"
+ concurrently_
+ (bob <## "cath_1 (Catherine): contact is connected")
+ (cath <## "bob_1 (Bob): contact is connected")
+
+ bob `hasContactProfiles` ["alice", "bob", "cath", "cath"]
+ cath `hasContactProfiles` ["cath", "alice", "bob", "bob"]
+
+ bob ##> "/contact_merge on"
+ bob <## "ok"
+
+ -- bob and cath connect
+ bob ##> "/_create member contact #1 3"
+ bob <## "contact for member #team cath is created"
+
+ bob ##> "/_invite member contact @4 text hi"
+ bob
+ <### [ "sent invitation to connect directly to member #team cath",
+ WithTime "@cath hi"
+ ]
+ cath
+ <### [ "#team bob is creating direct contact bob with you",
+ WithTime "bob> hi"
+ ]
+ concurrentlyN_
+ [ bob
+ <### [ "cath (Catherine): contact is connected",
+ "contact cath_1 is merged into cath",
+ -- StartsWith "use @cath"
+ "use @cath to send messages"
+ ],
+ cath
+ <### [ "bob (Bob): contact is connected",
+ "contact bob_1 is merged into bob",
+ -- StartsWith "use @bob"
+ "use @bob to send messages"
+ ]
+ ]
+ bob <##> cath
+
+ bob ##> "/contacts"
+ bob <### ["alice (Alice)", "cath (Catherine)"]
+ cath ##> "/contacts"
+ cath <### ["alice (Alice)", "bob (Bob)"]
+ bob `hasContactProfiles` ["alice", "bob", "cath"]
+ cath `hasContactProfiles` ["cath", "alice", "bob"]
+
+ -- group messages work
+ alice #> "#team hello"
+ concurrently_
+ (bob <# "#team alice> hello")
+ (cath <# "#team alice> hello")
+ bob #> "#team hi there"
+ concurrently_
+ (alice <# "#team bob> hi there")
+ (cath <# "#team bob> hi there")
+ cath #> "#team hey team"
+ concurrently_
+ (alice <# "#team cath> hey team")
+ (bob <# "#team cath> hey team")
+
testMemberContactMessage :: HasCallStack => FilePath -> IO ()
testMemberContactMessage =
testChat3 aliceProfile bobProfile cathProfile $
diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs
index da6cbd156f..d7cf682560 100644
--- a/tests/ChatTests/Profiles.hs
+++ b/tests/ChatTests/Profiles.hs
@@ -20,6 +20,7 @@ chatProfileTests = do
it "use multiword profile names" testMultiWordProfileNames
describe "user contact link" $ do
it "create and connect via contact link" testUserContactLink
+ it "merge existing contact when connecting via contact link" testUserContactLinkMerge
it "add contact link to profile" testProfileLink
it "auto accept contact requests" testUserContactLinkAutoAccept
it "deduplicate contact requests" testDeduplicateContactRequests
@@ -218,6 +219,39 @@ testUserContactLink =
alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")]
alice <##> cath
+testUserContactLinkMerge :: HasCallStack => FilePath -> IO ()
+testUserContactLinkMerge =
+ testChat2 aliceProfile bobProfile $
+ \alice bob -> do
+ connectUsers alice bob
+ alice <##> bob
+
+ alice ##> "/ad"
+ cLink <- getContactLink alice True
+ bob ##> ("/c " <> cLink)
+ bob <## "connection request sent!"
+ alice <## "bob_1 (Bob) wants to connect to you!"
+ alice <## "to accept: /ac bob_1"
+ alice <## "to reject: /rc bob_1 (the sender will NOT be notified)"
+ alice @@@ [("@bob", "hey"), ("<@bob_1", "")]
+ alice ##> "/ac bob_1"
+ alice <## "bob_1 (Bob): accepting contact request..."
+ concurrentlyN_
+ [ alice
+ <### [ "bob_1 (Bob): contact is connected",
+ "contact bob_1 is merged into bob",
+ "use @bob to send messages"
+ ],
+ bob
+ <### [ "alice_1 (Alice): contact is connected",
+ "contact alice_1 is merged into alice",
+ "use @alice to send messages"
+ ]
+ ]
+ threadDelay 100000
+ alice @@@ [("@bob", lastChatFeature)]
+ alice <##> bob
+
testProfileLink :: HasCallStack => FilePath -> IO ()
testProfileLink =
testChat3 aliceProfile bobProfile cathProfile $
diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs
index 6831cf3190..d98431e815 100644
--- a/tests/ChatTests/Utils.hs
+++ b/tests/ChatTests/Utils.hs
@@ -277,7 +277,7 @@ 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,7 +287,7 @@ getInAnyOrder :: HasCallStack => (String -> String) -> TestCC -> [ConsoleRespons
getInAnyOrder _ _ [] = pure ()
getInAnyOrder f cc ls = do
line <- f <$> getTermLine cc
- let rest = filter (not . expected line) ls
+ let rest = filterFirst (expected line) ls
if length rest < length ls
then getInAnyOrder f cc rest
else error $ "unexpected output: " <> line
@@ -297,6 +297,12 @@ 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
+ filterFirst :: (a -> Bool) -> [a] -> [a]
+ filterFirst _ [] = []
+ filterFirst p (x:xs)
+ | p x = xs
+ | otherwise = x : filterFirst p xs
(<###) :: HasCallStack => TestCC -> [ConsoleResponse] -> Expectation
(<###) = getInAnyOrder id
@@ -456,8 +462,11 @@ showName (TestCC ChatController {currentUser} _ _ _ _ _) = do
pure . T.unpack $ localDisplayName <> optionalFullName localDisplayName fullName
createGroup2 :: HasCallStack => String -> TestCC -> TestCC -> IO ()
-createGroup2 gName cc1 cc2 = do
- connectUsers cc1 cc2
+createGroup2 gName cc1 cc2 = createGroup2' gName cc1 cc2 True
+
+createGroup2' :: HasCallStack => String -> TestCC -> TestCC -> Bool -> IO ()
+createGroup2' gName cc1 cc2 doConnectUsers = do
+ when doConnectUsers $ connectUsers cc1 cc2
name2 <- userName cc2
cc1 ##> ("/g " <> gName)
cc1 <## ("group #" <> gName <> " is created")
@@ -488,6 +497,24 @@ createGroup3 gName cc1 cc2 cc3 = do
cc2 <## ("#" <> gName <> ": new member " <> name3 <> " is connected")
]
+create2Groups3 :: HasCallStack => String -> String -> TestCC -> TestCC -> TestCC -> IO ()
+create2Groups3 gName1 gName2 cc1 cc2 cc3 = do
+ createGroup3 gName1 cc1 cc2 cc3
+ createGroup2' gName2 cc1 cc2 False
+ name1 <- userName cc1
+ name3 <- userName cc3
+ addMember gName2 cc1 cc3 GRAdmin
+ cc3 ##> ("/j " <> gName2)
+ concurrentlyN_
+ [ cc1 <## ("#" <> gName2 <> ": " <> name3 <> " joined the group"),
+ do
+ cc3 <## ("#" <> gName2 <> ": you joined the group")
+ cc3 <##. ("#" <> gName2 <> ": member "), -- "#gName2: member sName2 is connected"
+ do
+ cc2 <##. ("#" <> gName2 <> ": " <> name1 <> " added ") -- "#gName2: name1 added sName3 to the group (connecting...)"
+ cc2 <##. ("#" <> gName2 <> ": new member ") -- "#gName2: new member name3 is connected"
+ ]
+
addMember :: HasCallStack => String -> TestCC -> TestCC -> GroupMemberRole -> IO ()
addMember gName = fullAddMember gName ""
From 1be70169bab98d135e9618cac4771663ef20b53a Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Fri, 6 Oct 2023 14:27:13 +0400
Subject: [PATCH 02/80] docs: contact merge issues rfc (#3179)
---
.../2023-10-05-contact-merge-improvement.md | 92 +++++++++++++++++++
1 file changed, 92 insertions(+)
create mode 100644 docs/rfcs/2023-10-05-contact-merge-improvement.md
diff --git a/docs/rfcs/2023-10-05-contact-merge-improvement.md b/docs/rfcs/2023-10-05-contact-merge-improvement.md
new file mode 100644
index 0000000000..e0588998ec
--- /dev/null
+++ b/docs/rfcs/2023-10-05-contact-merge-improvement.md
@@ -0,0 +1,92 @@
+# Contact merge issues, eradication, improvement
+
+## Problem
+
+Contact merge (see mergeContactRecords) keeps connections of both contacts by assigning both connections to the merged contact.
+
+When contact is read, last ready connection is selected as contact's active connection (see, for example, getContact - connections are ordered by connection_id in descending order). This may lead to contacts reading different connections, and as a result message delivery failing. Consider following scenarios:
+- connection IDs may be in different order for each side, if one of sides inserted connection in gap of IDs after an older (different) connection was deleted. If contacts consider different connections as active for each other, they would subscribe to and send to different connections, completely destabilizing delivery.
+- XInfoProbeOk may never be delivered due to server error or processed due to client error. In this case probe sender has 2 separate contacts and user may still use old contact for sending; other side now has a single merged contact record with the latest connection as active - if they restart or resubscribe they would stop receiving messages from the old connection. Perhaps it's less of an issue because that second side would at least send to a new connection which first side (probe sender) considers active for a separate contact, and would receive messages to that new contact. Also if they're establishing a second contact-pair, they may be more likely to use this new contact-pair anyway.
+
+## Solution ideas
+
+### Don't merge contacts
+
+Still merge members and contacts.
+
+Pros:
+- no risk of destabilizing existing conversation.
+- eventually (paired with a migration for legacy merged contacts) contact reading queries may be simplified to read a single connection, as well several chat logic flows accounting for multiple contact connections.
+- direct connection not being replaced with a new connection established via group (in case of "send direct message" feature, or pre 5.3 in case of direct connections established in group).
+- connection verification status not being reset (same + duplicate direct connections).
+- feature of having separate parallel conversations with same contact is kept.
+- easy to implement:
+ - in probeMatchingContactsAndMembers only match members (getMatchingMembers), don't match contacts (remove getMatchingContacts).
+ - in probeMatch prohibit merging contacts (cgm1 is COMContact, cgm2 is COMContact case; also member with associated contact case).
+ - in xInfoProbeOk - same.
+
+Cons:
+- "split identity" of contacts - though in many cases it's status quo without latest change (https://github.com/simplex-chat/simplex-chat/pull/3173). It may be not a big enough issue to justify risking connection stability.
+- members may be matched to arbitrary contacts - need to consider consequences more thoroughly. Though at least they should match to same contact-pair on both sides. "Send direct message" member contacts will not be matched to existing contacts, though going forward with contact-member merge working it shouldn't be an issue until contact is deleted on one of sides.
+- group link host not being merged - member knowing host as a contact would have him as a separate contact now. Perhaps we could rework group link protocol to avoid contact creation - new connection would already be created for host-invitee group connection, with some new connection entity "pending group member" created on both sides (starting with next protocol version support on both sides). This member would then be merged to the existing contact. This would be the most expensive part of this change, if we choose not to ignore it.
+
+### Improve contact merge protocol
+
+- instead of relying on ordering when selecting active contact connection, add flag active_contact_conn, set it on mergeContactRecords:
+
+ ~~verified connection >~~ direct connection > newer connection
+
+ we can't set to verified connection because it's not necessarily symmetric
+
+ ~~direct connection >~~ newer connection
+
+ for backwards compatibility we can't set to direct - other side may still set to newer. Unless we bump protocol version and choose based on that.
+
+ choose "newer" connection based on created_at instead of connection_id - it should be more likely to have the same order.
+
+- initial probe sender to send new message XInfoProbeComplete after processing XInfoProbeOk to signal that old connection is good to delete.
+
+- probe receiver to keep subscribing to both connections until they receive XInfoProbeComplete.
+
+- negotiate connection to use - shared connection id?
+
+```
+ Alice Bob
+ 2 contacts | | 2 contacts
+ 2 connections | | 2 connections
+ | XInfoProbe |
+ |------------------------>|
+ | XInfoProbeCheck |
+ |------------------------>|
+ | | match probe and hash;
+ | | merge contacts
+ | | 1 contact
+ | | 2 connections
+ | | (1 active, subscribe to both)
+ | XInfoProbeOk |
+ |<------------------------|
+ merge contacts | |
+ 1 contact | |
+ 1 connection | |
+ (delete non active) | |
+ | XInfoProbeComplete |
+ |------------------------>|
+ | | 1 contact
+ | | 1 connection
+ | | (delete non active)
+ * *
+```
+
+Pros:
+- contact merge is already implemented.
+- maintains contact "identity".
+- members are matched to a single contact.
+- no need to rework group links.
+
+Cons:
+- more complex logic - more prone to error.
+- likely still risks destabilizing connection.
+- continue to maintain and add new chat logic flows accounting for multiple connections (for instance, subscription).
+- newer connection replace verified and/or direct connections. These new connections are still possible to establish via indirect and unverified group connection ("send direct message").
+- removes "parallel conversations" feature. It's not a hard loss though.
+- existing contacts with aliases still not to be merged - to be implemented. Not a con, but a special case for contact merge.
From 76fb5b6dca2d3d6e920a5647f25a2f0b6c31e192 Mon Sep 17 00:00:00 2001
From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
Date: Sun, 8 Oct 2023 02:00:40 +0800
Subject: [PATCH 03/80] android: fix lock not showing (#3181)
* android: fix lock not showing
* better fix
---
.../android/src/main/java/chat/simplex/app/MainActivity.kt | 2 +-
.../kotlin/chat/simplex/common/platform/UI.android.kt | 2 ++
.../common/src/commonMain/kotlin/chat/simplex/common/App.kt | 2 +-
.../common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt | 1 -
.../src/commonMain/kotlin/chat/simplex/common/platform/UI.kt | 2 ++
.../kotlin/chat/simplex/common/platform/UI.desktop.kt | 2 ++
6 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt
index 512e9efc10..2dff3d604b 100644
--- a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt
+++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt
@@ -91,11 +91,11 @@ class MainActivity: FragmentActivity() {
// When pressed Back and there is no one wants to process the back event, clear auth state to force re-auth on launch
AppLock.clearAuthState()
AppLock.laFailed.value = true
- AppLock.destroyedAfterBackPress.value = true
}
if (!onBackPressedDispatcher.hasEnabledCallbacks()) {
// Drop shared content
SimplexApp.context.chatModel.sharedContent.value = null
+ finish()
}
}
}
diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt
index c458561f96..ca497cbc5b 100644
--- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt
+++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt
@@ -69,3 +69,5 @@ actual fun hideKeyboard(view: Any?) {
(androidAppContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(view.windowToken, 0)
}
}
+
+actual fun androidIsFinishingMainActivity(): Boolean = (mainActivity.get()?.isFinishing == true)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt
index c08ad5f914..98b7b279a8 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt
@@ -149,7 +149,7 @@ fun MainScreen() {
LaunchedEffect(Unit) {
// With these constrains when user presses back button while on ChatList, activity destroys and shows auth request
// while the screen moves to a launcher. Detect it and prevent showing the auth
- if (!(AppLock.destroyedAfterBackPress.value && chatModel.controller.appPrefs.laMode.get() == LAMode.SYSTEM)) {
+ if (!(androidIsFinishingMainActivity() && chatModel.controller.appPrefs.laMode.get() == LAMode.SYSTEM)) {
AppLock.runAuthenticate()
}
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt
index 7228f4ebf4..a1d4c0c62a 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt
@@ -24,7 +24,6 @@ object AppLock {
// Remember result and show it after orientation change
val laFailed = mutableStateOf(false)
- val destroyedAfterBackPress = mutableStateOf(false)
fun clearAuthState() {
userAuthorized.value = null
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt
index 38629f038b..c61d8564c4 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt
@@ -14,3 +14,5 @@ expect fun LocalMultiplatformView(): Any?
@Composable
expect fun getKeyboardState(): State
expect fun hideKeyboard(view: Any?)
+
+expect fun androidIsFinishingMainActivity(): Boolean
diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt
index 1ea5018fcc..94d42ba792 100644
--- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt
+++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt
@@ -17,3 +17,5 @@ actual fun LocalMultiplatformView(): Any? = null
@Composable
actual fun getKeyboardState(): State = remember { mutableStateOf(KeyboardState.Opened) }
actual fun hideKeyboard(view: Any?) {}
+
+actual fun androidIsFinishingMainActivity(): Boolean = false
From d50c7ad7f63322f7651848b0a2d49b0c1f42ac35 Mon Sep 17 00:00:00 2001
From: Moritz Angermann
Date: Sun, 8 Oct 2023 15:37:28 +0800
Subject: [PATCH 04/80] bump hackage (#3185)
---
flake.lock | 6 +++---
flake.nix | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/flake.lock b/flake.lock
index 2c133a9b63..81b3f6f6b8 100644
--- a/flake.lock
+++ b/flake.lock
@@ -288,11 +288,11 @@
"hackage": {
"flake": false,
"locked": {
- "lastModified": 1676679913,
- "narHash": "sha256-nW7ApRgiA9uChV/UrW89HK75rIToLt7XtSrkodO0Nbc=",
+ "lastModified": 1696724662,
+ "narHash": "sha256-jV2ugSjZE0FjMYR2YIx0p2cDBqd+xxhZrRxp5BmieYk=",
"owner": "input-output-hk",
"repo": "hackage.nix",
- "rev": "c37cffd51315d8e27dd8d3faf75abf897e39c8c8",
+ "rev": "df603bff8606d8653a0876ae0c3fd1f9014882f2",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index f7e86fbb6f..f6a68246e2 100644
--- a/flake.nix
+++ b/flake.nix
@@ -31,7 +31,7 @@
let pkgs = haskellNix.legacyPackages.${system}.appendOverlays [android26]; in
let drv' = { extra-modules, pkgs', ... }: pkgs'.haskell-nix.project {
compiler-nix-name = "ghc8107";
- index-state = "2022-06-20T00:00:00Z";
+ index-state = "2023-10-06T00:00:00Z";
# We need this, to specify we want the cabal project.
# If the stack.yaml was dropped, this would not be necessary.
projectFileName = "cabal.project";
From 985b9837c38b465b21e580b5c91e4f109e7de962 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Sun, 8 Oct 2023 16:22:53 +0100
Subject: [PATCH 05/80] core: api to close database connection (#3186)
---
src/Simplex/Chat/Mobile.hs | 19 +++++++++++++++++--
1 file changed, 17 insertions(+), 2 deletions(-)
diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs
index 700548bb12..0db9d7265e 100644
--- a/src/Simplex/Chat/Mobile.hs
+++ b/src/Simplex/Chat/Mobile.hs
@@ -3,11 +3,12 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeApplications #-}
module Simplex.Chat.Mobile where
import Control.Concurrent.STM
-import Control.Exception (catch)
+import Control.Exception (catch, SomeException)
import Control.Monad.Except
import Control.Monad.Reader
import Data.Aeson (ToJSON (..))
@@ -40,8 +41,9 @@ import Simplex.Chat.Options
import Simplex.Chat.Store
import Simplex.Chat.Store.Profiles
import Simplex.Chat.Types
+import Simplex.Messaging.Agent.Client (agentClientStore)
import Simplex.Messaging.Agent.Env.SQLite (createAgentStore)
-import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError)
+import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, closeSQLiteStore)
import Simplex.Messaging.Client (defaultNetworkConfig)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
@@ -53,6 +55,8 @@ import System.Timeout (timeout)
foreign export ccall "chat_migrate_init" cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString
+foreign export ccall "chat_close_store" cChatCloseStore :: StablePtr ChatController -> IO CString
+
foreign export ccall "chat_send_cmd" cChatSendCmd :: StablePtr ChatController -> CString -> IO CJSONString
foreign export ccall "chat_recv_msg" cChatRecvMsg :: StablePtr ChatController -> IO CJSONString
@@ -95,6 +99,9 @@ cChatMigrateInit fp key conf ctrl = do
Left e -> pure e
newCStringFromLazyBS $ J.encode r
+cChatCloseStore :: StablePtr ChatController -> IO CString
+cChatCloseStore cPtr = deRefStablePtr cPtr >>= chatCloseStore >>= newCAString
+
-- | send command to chat (same syntax as in terminal for now)
cChatSendCmd :: StablePtr ChatController -> CString -> IO CJSONString
cChatSendCmd cPtr cCmd = do
@@ -195,6 +202,14 @@ chatMigrateInit dbFilePrefix dbKey confirm = runExceptT $ do
_ -> dbError e
dbError e = Left . DBMErrorSQL dbFile $ show e
+chatCloseStore :: ChatController -> IO String
+chatCloseStore ChatController {chatStore, smpAgent} = handleErr $ do
+ closeSQLiteStore chatStore
+ closeSQLiteStore $ agentClientStore smpAgent
+
+handleErr :: IO () -> IO String
+handleErr a = (a $> "") `catch` (pure . show @SomeException)
+
chatSendCmd :: ChatController -> ByteString -> IO JSONByteString
chatSendCmd cc s = J.encode . APIResponse Nothing <$> runReaderT (execChatCommand s) cc
From ab46cbc5dd5c77838002a7ebdb2263e39e43658f Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Mon, 9 Oct 2023 09:46:58 +0400
Subject: [PATCH 06/80] core: relax contact probing: don't send probe hashes to
new contacts except group link hosts; still send probe hashes to group
members (#3180)
---
src/Simplex/Chat.hs | 95 +++++-----
src/Simplex/Chat/Store/Direct.hs | 9 +-
src/Simplex/Chat/Store/Groups.hs | 136 ++++++---------
src/Simplex/Chat/Store/Messages.hs | 3 +-
tests/ChatTests/Direct.hs | 101 +++++------
tests/ChatTests/Groups.hs | 271 +++--------------------------
tests/ChatTests/Profiles.hs | 34 ----
7 files changed, 176 insertions(+), 473 deletions(-)
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 32d59c9cc5..296abf0e2b 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -39,7 +39,6 @@ import Data.Fixed (div')
import Data.Functor (($>))
import Data.Int (Int64)
import Data.List (find, foldl', isSuffixOf, partition, sortOn)
-import Data.List (intercalate)
import Data.List.NonEmpty (NonEmpty, nonEmpty)
import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
@@ -3106,7 +3105,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
setActive $ ActiveC c
showToast (c <> "> ") "connected"
when (contactConnInitiated conn) $ do
- probeMatchingContactsAndMembers ct (contactConnIncognito ct)
+ let Connection {groupLinkId} = conn
+ doProbeContacts = isJust groupLinkId
+ probeMatchingContactsAndMembers ct (contactConnIncognito ct) doProbeContacts
withStore' $ \db -> resetContactConnInitiated db user conn
forM_ viaUserContactLink $ \userContactLinkId ->
withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case
@@ -3125,7 +3126,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
when (maybe False ((== ConnReady) . connStatus) activeConn) $ do
notifyMemberConnected gInfo m $ Just ct
let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo
- when (memberCategory m == GCPreMember) $ probeMatchingContactsAndMembers ct connectedIncognito
+ when (memberCategory m == GCPreMember) $ probeMatchingContactsAndMembers ct connectedIncognito True
SENT msgId -> do
sentMsgDeliveryEvent conn msgId
checkSndInlineFTComplete conn msgId
@@ -3143,8 +3144,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
withStore' $ \db -> setConnectionVerified db user connId Nothing
let ct' = ct {activeConn = conn {connectionCode = Nothing}} :: Contact
ratchetSyncEventItem ct'
- toView $ CRContactVerificationReset user ct'
- createInternalChatItem user (CDDirectRcv ct') (CIRcvConnEvent RCEVerificationCodeReset) Nothing
+ securityCodeChanged ct'
_ -> ratchetSyncEventItem ct
where
processErr cryptoErr = do
@@ -3298,7 +3298,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
when (connStatus == ConnReady) $ do
notifyMemberConnected gInfo m $ Just ct
let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo
- when (memberCategory m == GCPreMember) $ probeMatchingContactsAndMembers ct connectedIncognito
+ when (memberCategory m == GCPreMember) $ probeMatchingContactsAndMembers ct connectedIncognito True
MSG msgMeta _msgFlags msgBody -> do
cmdId <- createAckCmd conn
withAckMessage agentConnId cmdId msgMeta $ do
@@ -3691,8 +3691,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
setActive $ ActiveG g
showToast ("#" <> g) $ "member " <> c <> " is connected"
- probeMatchingContactsAndMembers :: Contact -> IncognitoEnabled -> m ()
- probeMatchingContactsAndMembers ct connectedIncognito = do
+ probeMatchingContactsAndMembers :: Contact -> IncognitoEnabled -> Bool -> m ()
+ probeMatchingContactsAndMembers ct connectedIncognito doProbeContacts = do
gVar <- asks idsDrg
contactMerge <- readTVarIO =<< asks contactMergeEnabled
if contactMerge && not connectedIncognito
@@ -3703,7 +3703,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
-- sendProbe -> sendProbeHashes (currently)
-- sendProbeHashes -> sendProbe (reversed - change order in code, may add delay)
sendProbe probe
- cs <- map COMContact <$> withStore' (\db -> getMatchingContacts db user ct)
+ cs <- if doProbeContacts
+ then map COMContact <$> withStore' (\db -> getMatchingContacts db user ct)
+ else pure []
ms <- map COMGroupMember <$> withStore' (\db -> getMatchingMembers db user ct)
sendProbeHashes (cs <> ms) probe probeId
else sendProbe . Probe =<< liftIO (encodedRandomBytes gVar 32)
@@ -4363,32 +4365,18 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
COMContact <$$> mergeContacts c1 c2
| otherwise -> messageWarning "probeMatch ignored: profiles don't match or same contact id" >> pure Nothing
COMGroupMember m2@GroupMember {memberProfile = p2, memberContactId}
- | profilesMatch p1 p2 -> case memberContactId of
- Nothing -> do
- void . sendDirectContactMessage c1 $ XInfoProbeOk probe
- COMContact <$$> associateMemberAndContact c1 m2
- Just mCtId
- | mCtId /= cId1 -> do
- void . sendDirectContactMessage c1 $ XInfoProbeOk probe
- mCt <- withStore $ \db -> getContact db user mCtId
- COMContact <$$> mergeContacts c1 mCt
- | otherwise -> messageWarning "probeMatch ignored: same contact id" >> pure Nothing
- | otherwise -> messageWarning "probeMatch ignored: profiles don't match" >> pure Nothing
+ | isNothing memberContactId && profilesMatch p1 p2 -> do
+ void . sendDirectContactMessage c1 $ XInfoProbeOk probe
+ COMContact <$$> associateMemberAndContact c1 m2
+ | otherwise -> messageWarning "probeMatch ignored: profiles don't match or member already has contact" >> pure Nothing
COMGroupMember GroupMember {activeConn = Nothing} -> pure Nothing
COMGroupMember m1@GroupMember {groupId, memberProfile = p1, memberContactId, activeConn = Just conn} ->
case cgm2 of
- COMContact c2@Contact {contactId = cId2, profile = p2}
- | memberCurrent m1 && profilesMatch p1 p2 -> case memberContactId of
- Nothing -> do
- void $ sendDirectMessage conn (XInfoProbeOk probe) (GroupId groupId)
- COMContact <$$> associateMemberAndContact c2 m1
- Just mCtId
- | mCtId /= cId2 -> do
- void $ sendDirectMessage conn (XInfoProbeOk probe) (GroupId groupId)
- mCt <- withStore $ \db -> getContact db user mCtId
- COMContact <$$> mergeContacts c2 mCt
- | otherwise -> messageWarning "probeMatch ignored: same contact id" >> pure Nothing
- | otherwise -> messageWarning "probeMatch ignored: profiles don't match or member not current" >> pure Nothing
+ COMContact c2@Contact {profile = p2}
+ | memberCurrent m1 && isNothing memberContactId && profilesMatch p1 p2 -> do
+ void $ sendDirectMessage conn (XInfoProbeOk probe) (GroupId groupId)
+ COMContact <$$> associateMemberAndContact c2 m1
+ | otherwise -> messageWarning "probeMatch ignored: profiles don't match or member already has contact or member not current" >> pure Nothing
COMGroupMember _ -> messageWarning "probeMatch ignored: members are not matched with members" >> pure Nothing
xInfoProbeOk :: ContactOrMember -> Probe -> m ()
@@ -4400,24 +4388,15 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
Just (COMContact c2@Contact {contactId = cId2})
| cId1 /= cId2 -> void $ mergeContacts c1 c2
| otherwise -> messageWarning "xInfoProbeOk ignored: same contact id"
- Just (COMGroupMember m2@GroupMember {memberContactId}) ->
- case memberContactId of
- Nothing -> void $ associateMemberAndContact c1 m2
- Just mCtId
- | mCtId /= cId1 -> do
- mCt <- withStore $ \db -> getContact db user mCtId
- void $ mergeContacts c1 mCt
- | otherwise -> messageWarning "xInfoProbeOk ignored: same contact id"
+ Just (COMGroupMember m2@GroupMember {memberContactId})
+ | isNothing memberContactId -> void $ associateMemberAndContact c1 m2
+ | otherwise -> messageWarning "xInfoProbeOk ignored: member already has contact"
_ -> pure ()
COMGroupMember m1@GroupMember {memberContactId} ->
case cgm2 of
- Just (COMContact c2@Contact {contactId = cId2}) -> case memberContactId of
- Nothing -> void $ associateMemberAndContact c2 m1
- Just mCtId
- | mCtId /= cId2 -> do
- mCt <- withStore $ \db -> getContact db user mCtId
- void $ mergeContacts c2 mCt
- | otherwise -> messageWarning "xInfoProbeOk ignored: same contact id"
+ Just (COMContact c2)
+ | isNothing memberContactId -> void $ associateMemberAndContact c2 m1
+ | otherwise -> messageWarning "xInfoProbeOk ignored: member already has contact"
Just (COMGroupMember _) -> messageWarning "xInfoProbeOk ignored: members are not matched with members"
_ -> pure ()
@@ -4540,7 +4519,21 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
merge c1' c2' = do
c2'' <- withStore $ \db -> mergeContactRecords db user c1' c2'
toView $ CRContactsMerged user c1' c2' c2''
+ when (directOrUsed c2'') $ showSecurityCodeChanged c2''
pure $ Just c2''
+ where
+ showSecurityCodeChanged mergedCt = do
+ let sc1_ = contactSecurityCode c1'
+ sc2_ = contactSecurityCode c2'
+ scMerged_ = contactSecurityCode mergedCt
+ case (sc1_, sc2_) of
+ (Just sc1, Nothing)
+ | scMerged_ /= Just sc1 -> securityCodeChanged mergedCt
+ | otherwise -> pure ()
+ (Nothing, Just sc2)
+ | scMerged_ /= Just sc2 -> securityCodeChanged mergedCt
+ | otherwise -> pure ()
+ _ -> pure ()
associateMemberAndContact :: Contact -> GroupMember -> m (Maybe Contact)
associateMemberAndContact c m = do
@@ -4796,9 +4789,11 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
forM_ mContent_ $ \mc -> do
ci <- saveRcvChatItem user (CDDirectRcv mCt') msg msgMeta (CIRcvMsgContent mc)
toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat mCt') ci)
- securityCodeChanged ct = do
- toView $ CRContactVerificationReset user ct
- createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent RCEVerificationCodeReset) Nothing
+
+ securityCodeChanged :: Contact -> m ()
+ securityCodeChanged ct = do
+ toView $ CRContactVerificationReset user ct
+ createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent RCEVerificationCodeReset) Nothing
directMsgReceived :: Contact -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> m ()
directMsgReceived ct conn@Connection {connId} msgMeta msgRcpts = do
diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs
index 20b81def8f..91243e2319 100644
--- a/src/Simplex/Chat/Store/Direct.hs
+++ b/src/Simplex/Chat/Store/Direct.hs
@@ -159,7 +159,7 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
JOIN connections c ON c.contact_id = ct.contact_id
WHERE ct.user_id = ? AND c.via_contact_uri_hash = ? AND ct.deleted = 0
- ORDER BY c.connection_id DESC
+ ORDER BY c.created_at DESC
LIMIT 1
|]
(userId, cReqHash)
@@ -517,7 +517,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0
- ORDER BY c.connection_id DESC
+ ORDER BY c.created_at DESC
LIMIT 1
|]
(userId, xContactId)
@@ -667,7 +667,7 @@ getContact_ :: DB.Connection -> User -> Int64 -> Bool -> ExceptT StoreError IO C
getContact_ db user@User {userId} contactId deleted =
ExceptT . fmap join . firstRow (toContactOrError user) (SEContactNotFound contactId) $
DB.query
- db
+ db
[sql|
SELECT
-- Contact
@@ -686,10 +686,11 @@ getContact_ db user@User {userId} contactId deleted =
SELECT cc_connection_id FROM (
SELECT
cc.connection_id AS cc_connection_id,
+ cc.created_at AS cc_created_at,
(CASE WHEN cc.conn_status = ? OR cc.conn_status = ? THEN 1 ELSE 0 END) AS cc_conn_status_ord
FROM connections cc
WHERE cc.user_id = ct.user_id AND cc.contact_id = ct.contact_id
- ORDER BY cc_conn_status_ord DESC, cc_connection_id DESC
+ ORDER BY cc_conn_status_ord DESC, cc_created_at DESC
LIMIT 1
)
)
diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs
index f6a4233c1f..236031da94 100644
--- a/src/Simplex/Chat/Store/Groups.hs
+++ b/src/Simplex/Chat/Store/Groups.hs
@@ -107,7 +107,7 @@ import Control.Monad.IO.Class
import Crypto.Random (ChaChaDRG)
import Data.Either (rights)
import Data.Int (Int64)
-import Data.List (sortOn)
+import Data.List (partition, sortOn)
import Data.Maybe (fromMaybe, isNothing, catMaybes, isJust)
import Data.Ord (Down (..))
import Data.Text (Text)
@@ -695,31 +695,21 @@ createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Co
)
getContactViaMember :: DB.Connection -> User -> GroupMember -> ExceptT StoreError IO Contact
-getContactViaMember db user@User {userId} GroupMember {groupMemberId} =
- ExceptT $
- firstRow (toContact user) (SEContactNotFoundByMemberId groupMemberId) $
- DB.query
- db
- [sql|
- SELECT
- -- Contact
- ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
- cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
- -- Connection
- c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
- c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
- c.peer_chat_min_version, c.peer_chat_max_version
- FROM contacts ct
- JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
- JOIN connections c ON c.connection_id = (
- SELECT max(cc.connection_id)
- FROM connections cc
- where cc.contact_id = ct.contact_id
- )
- JOIN group_members m ON m.contact_id = ct.contact_id
- WHERE ct.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0
- |]
- (userId, groupMemberId)
+getContactViaMember db user@User {userId} GroupMember {groupMemberId} = do
+ contactId <-
+ ExceptT $
+ firstRow fromOnly (SEContactNotFoundByMemberId groupMemberId) $
+ DB.query
+ db
+ [sql|
+ SELECT ct.contact_id
+ FROM group_members m
+ JOIN contacts ct ON ct.contact_id = m.contact_id
+ WHERE m.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0
+ LIMIT 1
+ |]
+ (userId, groupMemberId)
+ getContact db user contactId
setNewContactMemberConnRequest :: DB.Connection -> User -> GroupMember -> ConnReqInvitation -> IO ()
setNewContactMemberConnRequest db User {userId} GroupMember {groupMemberId} connRequest = do
@@ -1041,37 +1031,21 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} =
in (groupInfo, (member :: GroupMember) {activeConn = toMaybeConnection connRow})
getViaGroupContact :: DB.Connection -> User -> GroupMember -> IO (Maybe Contact)
-getViaGroupContact db user@User {userId} GroupMember {groupMemberId} =
- maybeFirstRow toContact' $
- DB.query
- db
- [sql|
- SELECT
- ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, ct.via_group, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
- p.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
- c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
- c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
- c.peer_chat_min_version, c.peer_chat_max_version
- FROM contacts ct
- JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
- JOIN connections c ON c.connection_id = (
- SELECT max(cc.connection_id)
- FROM connections cc
- where cc.contact_id = ct.contact_id
- )
- JOIN groups g ON g.group_id = ct.via_group
- JOIN group_members m ON m.group_id = g.group_id AND m.contact_id = ct.contact_id
- WHERE ct.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0
- |]
- (userId, groupMemberId)
- where
- toContact' :: ((ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool, ContactStatus) :. (Maybe Bool, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)) :. ConnectionRow -> Contact
- toContact' (((contactId, profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)) :. connRow) =
- let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
- chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_, sendRcpts, favorite}
- activeConn = toConnection connRow
- mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
- in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent}
+getViaGroupContact db user@User {userId} GroupMember {groupMemberId} = do
+ contactId_ <-
+ maybeFirstRow fromOnly $
+ DB.query
+ db
+ [sql|
+ SELECT ct.contact_id
+ FROM group_members m
+ JOIN groups g ON g.group_id = m.group_id
+ JOIN contacts ct ON ct.contact_id = m.contact_id AND ct.via_group = g.group_id
+ WHERE m.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0
+ LIMIT 1
+ |]
+ (userId, groupMemberId)
+ maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db user) contactId_
updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo
updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, description, image, groupPreferences}
@@ -1258,7 +1232,16 @@ matchReceivedProbe db user@User {userId} from (Probe probe) = do
db
"INSERT INTO received_probes (contact_id, group_member_id, probe, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
(ctId, gmId, probe, probeHash, userId, currentTs, currentTs)
- catMaybes <$> mapM (getContactOrMember_ db user) cgmIds
+ let cgmIds' = filterFirstContactId cgmIds
+ catMaybes <$> mapM (getContactOrMember_ db user) cgmIds'
+ where
+ filterFirstContactId :: [(Maybe ContactId, Maybe GroupId, Maybe GroupMemberId)] -> [(Maybe ContactId, Maybe GroupId, Maybe GroupMemberId)]
+ filterFirstContactId cgmIds = do
+ let (ctIds, memIds) = partition (\(ctId, _, _) -> isJust ctId) cgmIds
+ ctIds' = case ctIds of
+ [] -> []
+ (x : _) -> [x]
+ ctIds' <> memIds
matchReceivedProbeHash :: DB.Connection -> User -> ContactOrMember -> ProbeHash -> IO (Maybe (ContactOrMember, Probe))
matchReceivedProbeHash db user@User {userId} from (ProbeHash probeHash) = do
@@ -1284,7 +1267,7 @@ matchReceivedProbeHash db user@User {userId} from (ProbeHash probeHash) = do
pure probeIds $>>= \(Only probe :. cgmIds) -> (,Probe probe) <$$> getContactOrMember_ db user cgmIds
matchSentProbe :: DB.Connection -> User -> ContactOrMember -> Probe -> IO (Maybe ContactOrMember)
-matchSentProbe db user@User {userId} _from (Probe probe) =
+matchSentProbe db user@User {userId} _from (Probe probe) = do
cgmIds $>>= getContactOrMember_ db user
where
(ctId, gmId) = contactOrMemberIds _from
@@ -1311,11 +1294,10 @@ getContactOrMember_ db user ids =
(_, Just gId, Just gmId) -> COMGroupMember <$> getGroupMember db user gId gmId
_ -> throwError $ SEInternalError ""
--- connection being verified and connection level 0 have priority over requested merge direction;
--- if requested merge direction is overruled, keepLDN is kept
+-- if requested merge direction is overruled (toFromContacts), keepLDN is kept
mergeContactRecords :: DB.Connection -> User -> Contact -> Contact -> ExceptT StoreError IO Contact
mergeContactRecords db user@User {userId} to@Contact {localDisplayName = keepLDN} from = do
- let (toCt, fromCt) = checkToFromContacts
+ let (toCt, fromCt) = toFromContacts to from
Contact {contactId = toContactId, localDisplayName = toLDN} = toCt
Contact {contactId = fromContactId, localDisplayName = fromLDN} = fromCt
liftIO $ do
@@ -1342,18 +1324,6 @@ mergeContactRecords db user@User {userId} to@Contact {localDisplayName = keepLDN
db
"UPDATE chat_items SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?"
(toContactId, currentTs, fromContactId, userId)
- DB.execute
- db
- "UPDATE sent_probes SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?"
- (toContactId, currentTs, fromContactId, userId)
- DB.execute
- db
- "UPDATE sent_probe_hashes SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?"
- (toContactId, currentTs, fromContactId, userId)
- DB.execute
- db
- "UPDATE received_probes SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?"
- (toContactId, currentTs, fromContactId, userId)
DB.executeNamed
db
[sql|
@@ -1384,16 +1354,16 @@ mergeContactRecords db user@User {userId} to@Contact {localDisplayName = keepLDN
(keepLDN, currentTs, userId, toLDN)
getContact db user toContactId
where
- checkToFromContacts :: (Contact, Contact)
- checkToFromContacts
- | vrfFrom && not vrfTo = (from, to)
- | dirFrom && not vrfTo && not dirTo = (from, to)
- | otherwise = (to, from)
+ 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
- vrfTo = isJust $ contactSecurityCode to
- vrfFrom = isJust $ contactSecurityCode from
- dirTo = let Contact {activeConn = Connection {connLevel = clTo}} = to in clTo == 0
- dirFrom = let Contact {activeConn = Connection {connLevel = clFrom}} = from in clFrom == 0
+ d1 = directOrUsed c1
+ d2 = directOrUsed c2
+ ctCreatedAt Contact {createdAt} = createdAt
associateMemberWithContactRecord :: DB.Connection -> User -> Contact -> GroupMember -> IO ()
associateMemberWithContactRecord
diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs
index b996f7626f..9ad0e8edc8 100644
--- a/src/Simplex/Chat/Store/Messages.hs
+++ b/src/Simplex/Chat/Store/Messages.hs
@@ -517,10 +517,11 @@ getDirectChatPreviews_ db user@User {userId} = do
SELECT cc_connection_id FROM (
SELECT
cc.connection_id AS cc_connection_id,
+ cc.created_at AS cc_created_at,
(CASE WHEN cc.conn_status = ? OR cc.conn_status = ? THEN 1 ELSE 0 END) AS cc_conn_status_ord
FROM connections cc
WHERE cc.user_id = ct.user_id AND cc.contact_id = ct.contact_id
- ORDER BY cc_conn_status_ord DESC, cc_connection_id DESC
+ ORDER BY cc_conn_status_ord DESC, cc_created_at DESC
LIMIT 1
)
)
diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs
index a8f9fbf9c6..a5fc7455c8 100644
--- a/tests/ChatTests/Direct.hs
+++ b/tests/ChatTests/Direct.hs
@@ -41,9 +41,9 @@ chatDirectTests = do
it "direct timed message" testDirectTimedMessage
it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact
it "should send multiline message" testMultilineMessage
- describe "contact merge" $ do
- it "merge duplicate contacts" testContactMerge
- it "new contact should merge with multiple existing contacts" testMergeContactMultipleContacts
+ describe "duplicate contacts" $ do
+ it "duplicate contacts are separate (contacts don't merge)" testDuplicateContactsSeparate
+ it "new contact is separate with multiple duplicate contacts (contacts don't merge)" testDuplicateContactsMultipleSeparate
describe "SMP servers" $ do
it "get and set SMP servers" testGetSetSMPServers
it "test SMP server connection" testTestSMPServerConnection
@@ -170,44 +170,13 @@ testAddContact = versionTestMatrix2 runTestAddContact
alice #$> ("/_read chat @2", id, "ok")
bob #$> ("/_read chat @2", id, "ok")
-testContactMerge :: HasCallStack => FilePath -> IO ()
-testContactMerge =
+testDuplicateContactsSeparate :: HasCallStack => FilePath -> IO ()
+testDuplicateContactsSeparate =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
alice <##> bob
- alice ##> "/c"
- inv' <- getInvitation alice
- bob ##> ("/c " <> inv')
- bob <## "confirmation sent!"
- concurrentlyN_
- [ alice
- <### [ "bob_1 (Bob): contact is connected",
- "contact bob_1 is merged into bob",
- "use @bob to send messages"
- ],
- bob
- <### [ "alice_1 (Alice): contact is connected",
- "contact alice_1 is merged into alice",
- "use @alice to send messages"
- ]
- ]
- alice <##> bob
- alice @@@ [("@bob", "hey")]
- alice `hasContactProfiles` ["alice", "bob"]
- bob @@@ [("@alice", "hey")]
- bob `hasContactProfiles` ["bob", "alice"]
-
-testMergeContactMultipleContacts :: HasCallStack => FilePath -> IO ()
-testMergeContactMultipleContacts =
- testChat2 aliceProfile bobProfile $
- \alice bob -> do
- bob ##> "/contact_merge off"
- bob <## "ok"
-
- connectUsers alice bob
-
alice ##> "/c"
inv' <- getInvitation alice
bob ##> ("/c " <> inv')
@@ -216,42 +185,56 @@ testMergeContactMultipleContacts =
(alice <## "bob_1 (Bob): contact is connected")
(bob <## "alice_1 (Alice): contact is connected")
+ alice <##> bob
+ alice #> "@bob_1 1"
+ bob <# "alice_1> 1"
+ bob #> "@alice_1 2"
+ alice <# "bob_1> 2"
+
+ alice @@@ [("@bob", "hey"), ("@bob_1", "2")]
alice `hasContactProfiles` ["alice", "bob", "bob"]
+ bob @@@ [("@alice", "hey"), ("@alice_1", "2")]
bob `hasContactProfiles` ["bob", "alice", "alice"]
- threadDelay 500000
+testDuplicateContactsMultipleSeparate :: HasCallStack => FilePath -> IO ()
+testDuplicateContactsMultipleSeparate =
+ testChat2 aliceProfile bobProfile $
+ \alice bob -> do
+ connectUsers alice bob
+ alice <##> bob
- bob ##> "/contact_merge on"
- bob <## "ok"
+ alice ##> "/c"
+ inv' <- getInvitation alice
+ bob ##> ("/c " <> inv')
+ bob <## "confirmation sent!"
+ concurrently_
+ (alice <## "bob_1 (Bob): contact is connected")
+ (bob <## "alice_1 (Alice): contact is connected")
alice ##> "/c"
inv'' <- getInvitation alice
bob ##> ("/c " <> inv'')
bob <## "confirmation sent!"
- concurrentlyN_
- [ alice
- <### [ "bob_2 (Bob): contact is connected",
- StartsWith "contact bob_2 is merged into bob",
- StartsWith "use @bob",
- StartsWith "contact bob_1 is merged into bob",
- StartsWith "use @bob"
- ],
- bob
- <### [ "alice_2 (Alice): contact is connected",
- StartsWith "contact alice_2 is merged into alice",
- StartsWith "use @alice",
- StartsWith "contact alice_1 is merged into alice",
- StartsWith "use @alice"
- ]
- ]
+ concurrently_
+ (alice <## "bob_2 (Bob): contact is connected")
+ (bob <## "alice_2 (Alice): contact is connected")
+
alice <##> bob
+ alice #> "@bob_1 1"
+ bob <# "alice_1> 1"
+ bob #> "@alice_1 2"
+ alice <# "bob_1> 2"
+ alice #> "@bob_2 3"
+ bob <# "alice_2> 3"
+ bob #> "@alice_2 4"
+ alice <# "bob_2> 4"
alice ##> "/contacts"
- alice <## "bob (Bob)"
+ alice <### ["bob (Bob)", "bob_1 (Bob)", "bob_2 (Bob)"]
bob ##> "/contacts"
- bob <## "alice (Alice)"
- alice `hasContactProfiles` ["alice", "bob"]
- bob `hasContactProfiles` ["bob", "alice"]
+ bob <### ["alice (Alice)", "alice_1 (Alice)", "alice_2 (Alice)"]
+ alice `hasContactProfiles` ["alice", "bob", "bob", "bob"]
+ bob `hasContactProfiles` ["bob", "alice", "alice", "alice"]
testContactClear :: HasCallStack => FilePath -> IO ()
testContactClear =
diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs
index 083d2f85be..9fb6ac7f9b 100644
--- a/tests/ChatTests/Groups.hs
+++ b/tests/ChatTests/Groups.hs
@@ -75,12 +75,9 @@ chatGroupTests = do
it "members have different local display names in different groups" testNoDirectDifferentLDNs
describe "merge members and contacts" $ do
it "new member should merge with existing contact" testMergeMemberExistingContact
- it "new member should merge with multiple existing contacts" testMergeMemberMultipleContacts
it "new contact should merge with existing member" testMergeContactExistingMember
- it "new contact should merge with existing member with associated contact" testMergeContactExistingMemberWithContact
it "new contact should merge with multiple existing members" testMergeContactMultipleMembers
- it "new contact should merge with both existing members and contacts" testMergeContactExistingMembersAndContacts
- it "new member contact is merged with existing contact" testMergeMemberContact
+ it "new group link host contact should merge with single existing contact out of multiple" testMergeGroupLinkHostMultipleContacts
describe "create member contact" $ do
it "create contact with group member with invitation message" testMemberContactMessage
it "create contact with group member without invitation message" testMemberContactNoMessage
@@ -2788,76 +2785,6 @@ testMergeMemberExistingContact =
alice `hasContactProfiles` ["alice", "bob", "cath"]
cath `hasContactProfiles` ["cath", "alice", "bob"]
-testMergeMemberMultipleContacts :: HasCallStack => FilePath -> IO ()
-testMergeMemberMultipleContacts =
- testChat3 aliceProfile bobProfile cathProfile $
- \alice bob cath -> do
- alice ##> "/contact_merge off"
- alice <## "ok"
-
- connectUsers alice bob
- connectUsers alice cath
-
- alice ##> "/c"
- inv' <- getInvitation alice
- cath ##> ("/c " <> inv')
- cath <## "confirmation sent!"
- concurrently_
- (alice <## "cath_1 (Catherine): contact is connected")
- (cath <## "alice_1 (Alice): contact is connected")
-
- alice `hasContactProfiles` ["alice", "bob", "cath", "cath"]
- cath `hasContactProfiles` ["cath", "alice", "alice"]
-
- alice ##> "/contact_merge on"
- alice <## "ok"
-
- createGroup2 "team" bob cath
- bob ##> "/a #team alice"
- bob <## "invitation to join the group #team sent to alice"
- alice <## "#team: bob invites you to join the group as member"
- alice <## "use /j team to accept"
- alice ##> "/j team"
- concurrentlyN_
- [ alice
- <### [ "#team: you joined the group",
- "#team: member cath_2 (Catherine) is connected",
- StartsWith "contact and member are merged: cath",
- StartsWith "use @cath",
- StartsWith "contact cath_",
- StartsWith "use @cath"
- ],
- bob <## "#team: alice joined the group",
- cath
- <### [ "#team: bob added alice_2 (Alice) to the group (connecting...)",
- "#team: new member alice_2 is connected",
- StartsWith "contact and member are merged: alice",
- StartsWith "use @alice",
- StartsWith "contact alice_",
- StartsWith "use @alice"
- ]
- ]
- alice <##> cath
- alice #> "#team hello"
- bob <# "#team alice> hello"
- cath <# "#team alice> hello"
- cath #> "#team hello too"
- bob <# "#team cath> hello too"
- alice <# "#team cath> hello too"
-
- alice ##> "/contacts"
- alice
- <### [ "bob (Bob)",
- "cath (Catherine)"
- ]
- cath ##> "/contacts"
- cath
- <### [ "alice (Alice)",
- "bob (Bob)"
- ]
- alice `hasContactProfiles` ["alice", "bob", "cath"]
- cath `hasContactProfiles` ["cath", "alice", "bob"]
-
testMergeContactExistingMember :: HasCallStack => FilePath -> IO ()
testMergeContactExistingMember =
testChat3 aliceProfile bobProfile cathProfile $
@@ -2889,66 +2816,6 @@ testMergeContactExistingMember =
bob `hasContactProfiles` ["alice", "bob", "cath"]
cath `hasContactProfiles` ["cath", "alice", "bob"]
-testMergeContactExistingMemberWithContact :: HasCallStack => FilePath -> IO ()
-testMergeContactExistingMemberWithContact =
- testChat3 aliceProfile bobProfile cathProfile $
- \alice bob cath -> do
- createGroup3 "team" alice bob cath
-
- -- create contact, delete only for bob so he would send probe hash to member
- bob ##> "/_create member contact #1 3"
- bob <## "contact for member #team cath is created"
-
- bob ##> "/_invite member contact @3 text hi"
- bob
- <### [ "sent invitation to connect directly to member #team cath",
- WithTime "@cath hi"
- ]
- cath
- <### [ "#team bob is creating direct contact bob with you",
- WithTime "bob> hi"
- ]
- concurrently_
- (bob <## "cath (Catherine): contact is connected")
- (cath <## "bob (Bob): contact is connected")
- bob <##> cath
-
- bob ##> "/_delete @3 notify=off"
- bob <## "cath: contact is deleted"
-
- bob ##> "/contacts"
- bob <### ["alice (Alice)"]
- cath ##> "/contacts"
- cath <### ["alice (Alice)", "bob (Bob)"]
- bob `hasContactProfiles` ["alice", "bob", "cath"]
- cath `hasContactProfiles` ["cath", "alice", "bob"]
-
- -- contact connects, member is merged
- bob ##> "/c"
- inv' <- getInvitation bob
- cath ##> ("/c " <> inv')
- cath <## "confirmation sent!"
- concurrentlyN_
- [ bob
- <### [ "cath_1 (Catherine): contact is connected",
- "contact and member are merged: cath_1, #team cath",
- "use @cath to send messages"
- ],
- cath
- <### [ "bob_1 (Bob): contact is connected",
- "contact bob_1 is merged into bob",
- "use @bob to send messages"
- ]
- ]
- bob <##> cath
-
- bob ##> "/contacts"
- bob <### ["alice (Alice)", "cath (Catherine)"]
- cath ##> "/contacts"
- cath <### ["alice (Alice)", "bob (Bob)"]
- bob `hasContactProfiles` ["alice", "bob", "cath"]
- cath `hasContactProfiles` ["cath", "alice", "bob"]
-
testMergeContactMultipleMembers :: HasCallStack => FilePath -> IO ()
testMergeContactMultipleMembers =
testChat3 aliceProfile bobProfile cathProfile $
@@ -2987,70 +2854,11 @@ testMergeContactMultipleMembers =
bob `hasContactProfiles` ["alice", "bob", "cath"]
cath `hasContactProfiles` ["cath", "alice", "bob"]
-testMergeContactExistingMembersAndContacts :: HasCallStack => FilePath -> IO ()
-testMergeContactExistingMembersAndContacts =
- testChat3 aliceProfile bobProfile cathProfile $
- \alice bob cath -> do
- bob ##> "/contact_merge off"
- bob <## "ok"
-
- create2Groups3 "team" "club" alice bob cath
-
- bob ##> "/c"
- inv' <- getInvitation bob
- cath ##> ("/c " <> inv')
- cath <## "confirmation sent!"
- concurrently_
- (bob <## "cath_2 (Catherine): contact is connected")
- (cath <## "bob_2 (Bob): contact is connected")
-
- bob `hasContactProfiles` ["alice", "bob", "cath", "cath", "cath"]
- cath `hasContactProfiles` ["cath", "alice", "bob", "bob", "bob"]
-
- bob ##> "/contact_merge on"
- bob <## "ok"
-
- bob ##> "/c"
- inv'' <- getInvitation bob
- cath ##> ("/c " <> inv'')
- cath <## "confirmation sent!"
- concurrentlyN_
- [ bob
- <### [ "cath_3 (Catherine): contact is connected",
- StartsWith "contact and member are merged: cath",
- StartsWith "use @cath",
- StartsWith "contact and member are merged: cath",
- StartsWith "use @cath",
- StartsWith "contact cath_3 is merged into cath",
- StartsWith "use @cath"
- ],
- cath
- <### [ "bob_3 (Bob): contact is connected",
- StartsWith "contact and member are merged: bob",
- StartsWith "use @bob",
- StartsWith "contact and member are merged: bob",
- StartsWith "use @bob",
- StartsWith "contact bob_3 is merged into bob",
- StartsWith "use @bob"
- ]
- ]
- bob <##> cath
-
- bob ##> "/contacts"
- bob <### ["alice (Alice)", "cath (Catherine)"]
- cath ##> "/contacts"
- cath <### ["alice (Alice)", "bob (Bob)"]
- bob `hasContactProfiles` ["alice", "bob", "cath"]
- cath `hasContactProfiles` ["cath", "alice", "bob"]
-
-testMergeMemberContact :: HasCallStack => FilePath -> IO ()
-testMergeMemberContact =
- testChat3 aliceProfile bobProfile cathProfile $
- \alice bob cath -> do
- bob ##> "/contact_merge off"
- bob <## "ok"
-
- createGroup3 "team" alice bob cath
+testMergeGroupLinkHostMultipleContacts :: HasCallStack => FilePath -> IO ()
+testMergeGroupLinkHostMultipleContacts =
+ testChat2 bobProfile cathProfile $
+ \bob cath -> do
+ connectUsers bob cath
bob ##> "/c"
inv' <- getInvitation bob
@@ -3060,61 +2868,40 @@ testMergeMemberContact =
(bob <## "cath_1 (Catherine): contact is connected")
(cath <## "bob_1 (Bob): contact is connected")
- bob `hasContactProfiles` ["alice", "bob", "cath", "cath"]
- cath `hasContactProfiles` ["cath", "alice", "bob", "bob"]
+ bob `hasContactProfiles` ["bob", "cath", "cath"]
+ cath `hasContactProfiles` ["cath", "bob", "bob"]
- bob ##> "/contact_merge on"
- bob <## "ok"
-
- -- bob and cath connect
- bob ##> "/_create member contact #1 3"
- bob <## "contact for member #team cath is created"
-
- bob ##> "/_invite member contact @4 text hi"
- bob
- <### [ "sent invitation to connect directly to member #team cath",
- WithTime "@cath hi"
- ]
- cath
- <### [ "#team bob is creating direct contact bob with you",
- WithTime "bob> hi"
- ]
+ bob ##> "/g party"
+ bob <## "group #party is created"
+ bob <## "to add members use /a party or /create link #party"
+ bob ##> "/create link #party"
+ gLink <- getGroupLink bob "party" GRMember True
+ cath ##> ("/c " <> gLink)
+ cath <## "connection request sent!"
+ bob <## "cath_2 (Catherine): accepting request to join group #party..."
concurrentlyN_
[ bob
- <### [ "cath (Catherine): contact is connected",
- "contact cath_1 is merged into cath",
- -- StartsWith "use @cath"
- "use @cath to send messages"
+ <### [ "cath_2 (Catherine): contact is connected",
+ EndsWith "invited to group #party via your group link",
+ EndsWith "joined the group",
+ StartsWith "contact cath_2 is merged into cath",
+ StartsWith "use @cath"
],
cath
- <### [ "bob (Bob): contact is connected",
- "contact bob_1 is merged into bob",
- -- StartsWith "use @bob"
- "use @bob to send messages"
+ <### [ "bob_2 (Bob): contact is connected",
+ "#party: you joined the group",
+ StartsWith "contact bob_2 is merged into bob",
+ StartsWith "use @bob"
]
]
bob <##> cath
bob ##> "/contacts"
- bob <### ["alice (Alice)", "cath (Catherine)"]
+ bob <### ["cath (Catherine)", "cath_1 (Catherine)"]
cath ##> "/contacts"
- cath <### ["alice (Alice)", "bob (Bob)"]
- bob `hasContactProfiles` ["alice", "bob", "cath"]
- cath `hasContactProfiles` ["cath", "alice", "bob"]
-
- -- group messages work
- alice #> "#team hello"
- concurrently_
- (bob <# "#team alice> hello")
- (cath <# "#team alice> hello")
- bob #> "#team hi there"
- concurrently_
- (alice <# "#team bob> hi there")
- (cath <# "#team bob> hi there")
- cath #> "#team hey team"
- concurrently_
- (alice <# "#team cath> hey team")
- (bob <# "#team cath> hey team")
+ cath <### ["bob (Bob)", "bob_1 (Bob)"]
+ bob `hasContactProfiles` ["bob", "cath", "cath"]
+ cath `hasContactProfiles` ["cath", "bob", "bob"]
testMemberContactMessage :: HasCallStack => FilePath -> IO ()
testMemberContactMessage =
diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs
index d7cf682560..da6cbd156f 100644
--- a/tests/ChatTests/Profiles.hs
+++ b/tests/ChatTests/Profiles.hs
@@ -20,7 +20,6 @@ chatProfileTests = do
it "use multiword profile names" testMultiWordProfileNames
describe "user contact link" $ do
it "create and connect via contact link" testUserContactLink
- it "merge existing contact when connecting via contact link" testUserContactLinkMerge
it "add contact link to profile" testProfileLink
it "auto accept contact requests" testUserContactLinkAutoAccept
it "deduplicate contact requests" testDeduplicateContactRequests
@@ -219,39 +218,6 @@ testUserContactLink =
alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")]
alice <##> cath
-testUserContactLinkMerge :: HasCallStack => FilePath -> IO ()
-testUserContactLinkMerge =
- testChat2 aliceProfile bobProfile $
- \alice bob -> do
- connectUsers alice bob
- alice <##> bob
-
- alice ##> "/ad"
- cLink <- getContactLink alice True
- bob ##> ("/c " <> cLink)
- bob <## "connection request sent!"
- alice <## "bob_1 (Bob) wants to connect to you!"
- alice <## "to accept: /ac bob_1"
- alice <## "to reject: /rc bob_1 (the sender will NOT be notified)"
- alice @@@ [("@bob", "hey"), ("<@bob_1", "")]
- alice ##> "/ac bob_1"
- alice <## "bob_1 (Bob): accepting contact request..."
- concurrentlyN_
- [ alice
- <### [ "bob_1 (Bob): contact is connected",
- "contact bob_1 is merged into bob",
- "use @bob to send messages"
- ],
- bob
- <### [ "alice_1 (Alice): contact is connected",
- "contact alice_1 is merged into alice",
- "use @alice to send messages"
- ]
- ]
- threadDelay 100000
- alice @@@ [("@bob", lastChatFeature)]
- alice <##> bob
-
testProfileLink :: HasCallStack => FilePath -> IO ()
testProfileLink =
testChat3 aliceProfile bobProfile cathProfile $
From bc26c23d589d8129bad31fbbe5b19d1ce3c8b2f8 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Mon, 9 Oct 2023 10:35:13 +0100
Subject: [PATCH 07/80] fix MobileTests (add single field JSON tag)
---
tests/MobileTests.hs | 99 ++++++++++++++++++++++++++++++++++++--------
1 file changed, 81 insertions(+), 18 deletions(-)
diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs
index 69c2207ff6..692dd5884e 100644
--- a/tests/MobileTests.hs
+++ b/tests/MobileTests.hs
@@ -65,71 +65,134 @@ mobileTests = do
it "should convert invalid name to a valid name" testValidNameCApi
noActiveUser :: LB.ByteString
+noActiveUser =
#if defined(darwin_HOST_OS) && defined(swiftJSON)
-noActiveUser = "{\"resp\":{\"chatCmdError\":{\"chatError\":{\"error\":{\"errorType\":{\"noActiveUser\":{}}}}}}}"
+ noActiveUserSwift
#else
-noActiveUser = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"noActiveUser\"}}}}"
+ noActiveUserTagged
#endif
+noActiveUserSwift :: LB.ByteString
+noActiveUserSwift = "{\"resp\":{\"_owsf\":true,\"chatCmdError\":{\"chatError\":{\"_owsf\":true,\"error\":{\"errorType\":{\"_owsf\":true,\"noActiveUser\":{}}}}}}}"
+
+noActiveUserTagged :: LB.ByteString
+noActiveUserTagged = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"noActiveUser\"}}}}"
+
activeUserExists :: LB.ByteString
+activeUserExists =
#if defined(darwin_HOST_OS) && defined(swiftJSON)
-activeUserExists = "{\"resp\":{\"chatCmdError\":{\"user_\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true},\"chatError\":{\"error\":{\"errorType\":{\"userExists\":{\"contactName\":\"alice\"}}}}}}}"
+ activeUserExistsSwift
#else
-activeUserExists = "{\"resp\":{\"type\":\"chatCmdError\",\"user_\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true},\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"userExists\",\"contactName\":\"alice\"}}}}"
+ activeUserExistsTagged
#endif
+activeUserExistsSwift :: LB.ByteString
+activeUserExistsSwift = "{\"resp\":{\"_owsf\":true,\"chatCmdError\":{\"user_\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true},\"chatError\":{\"_owsf\":true,\"error\":{\"errorType\":{\"_owsf\":true,\"userExists\":{\"contactName\":\"alice\"}}}}}}}"
+
+activeUserExistsTagged :: LB.ByteString
+activeUserExistsTagged = "{\"resp\":{\"type\":\"chatCmdError\",\"user_\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true},\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"userExists\",\"contactName\":\"alice\"}}}}"
+
activeUser :: LB.ByteString
+activeUser =
#if defined(darwin_HOST_OS) && defined(swiftJSON)
-activeUser = "{\"resp\":{\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}}"
+ activeUserSwift
#else
-activeUser = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}"
+ activeUserTagged
#endif
+activeUserSwift :: LB.ByteString
+activeUserSwift = "{\"resp\":{\"_owsf\":true,\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}}"
+
+activeUserTagged :: LB.ByteString
+activeUserTagged = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}"
+
chatStarted :: LB.ByteString
+chatStarted =
#if defined(darwin_HOST_OS) && defined(swiftJSON)
-chatStarted = "{\"resp\":{\"chatStarted\":{}}}"
+ chatStartedSwift
#else
-chatStarted = "{\"resp\":{\"type\":\"chatStarted\"}}"
+ chatStartedTagged
#endif
+chatStartedSwift :: LB.ByteString
+chatStartedSwift = "{\"resp\":{\"_owsf\":true,\"chatStarted\":{}}}"
+
+chatStartedTagged :: LB.ByteString
+chatStartedTagged = "{\"resp\":{\"type\":\"chatStarted\"}}"
+
contactSubSummary :: LB.ByteString
+contactSubSummary =
#if defined(darwin_HOST_OS) && defined(swiftJSON)
-contactSubSummary = "{\"resp\":{\"contactSubSummary\":{" <> userJSON <> ",\"contactSubscriptions\":[]}}}"
+ contactSubSummarySwift
#else
-contactSubSummary = "{\"resp\":{\"type\":\"contactSubSummary\"," <> userJSON <> ",\"contactSubscriptions\":[]}}"
+ contactSubSummaryTagged
#endif
+contactSubSummarySwift :: LB.ByteString
+contactSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"contactSubSummary\":{" <> userJSON <> ",\"contactSubscriptions\":[]}}}"
+
+contactSubSummaryTagged :: LB.ByteString
+contactSubSummaryTagged = "{\"resp\":{\"type\":\"contactSubSummary\"," <> userJSON <> ",\"contactSubscriptions\":[]}}"
+
memberSubSummary :: LB.ByteString
+memberSubSummary =
#if defined(darwin_HOST_OS) && defined(swiftJSON)
-memberSubSummary = "{\"resp\":{\"memberSubSummary\":{" <> userJSON <> ",\"memberSubscriptions\":[]}}}"
+ memberSubSummarySwift
#else
-memberSubSummary = "{\"resp\":{\"type\":\"memberSubSummary\"," <> userJSON <> ",\"memberSubscriptions\":[]}}"
+ memberSubSummaryTagged
#endif
+memberSubSummarySwift :: LB.ByteString
+memberSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"memberSubSummary\":{" <> userJSON <> ",\"memberSubscriptions\":[]}}}"
+
+memberSubSummaryTagged :: LB.ByteString
+memberSubSummaryTagged = "{\"resp\":{\"type\":\"memberSubSummary\"," <> userJSON <> ",\"memberSubscriptions\":[]}}"
+
userContactSubSummary :: LB.ByteString
+userContactSubSummary =
#if defined(darwin_HOST_OS) && defined(swiftJSON)
-userContactSubSummary = "{\"resp\":{\"userContactSubSummary\":{" <> userJSON <> ",\"userContactSubscriptions\":[]}}}"
+ userContactSubSummarySwift
#else
-userContactSubSummary = "{\"resp\":{\"type\":\"userContactSubSummary\"," <> userJSON <> ",\"userContactSubscriptions\":[]}}"
+ userContactSubSummaryTagged
#endif
+userContactSubSummarySwift :: LB.ByteString
+userContactSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"userContactSubSummary\":{" <> userJSON <> ",\"userContactSubscriptions\":[]}}}"
+
+userContactSubSummaryTagged :: LB.ByteString
+userContactSubSummaryTagged = "{\"resp\":{\"type\":\"userContactSubSummary\"," <> userJSON <> ",\"userContactSubscriptions\":[]}}"
+
pendingSubSummary :: LB.ByteString
+pendingSubSummary =
#if defined(darwin_HOST_OS) && defined(swiftJSON)
-pendingSubSummary = "{\"resp\":{\"pendingSubSummary\":{" <> userJSON <> ",\"pendingSubscriptions\":[]}}}"
+ pendingSubSummarySwift
#else
-pendingSubSummary = "{\"resp\":{\"type\":\"pendingSubSummary\"," <> userJSON <> ",\"pendingSubscriptions\":[]}}"
+ pendingSubSummaryTagged
#endif
+pendingSubSummarySwift :: LB.ByteString
+pendingSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"pendingSubSummary\":{" <> userJSON <> ",\"pendingSubscriptions\":[]}}}"
+
+pendingSubSummaryTagged :: LB.ByteString
+pendingSubSummaryTagged = "{\"resp\":{\"type\":\"pendingSubSummary\"," <> userJSON <> ",\"pendingSubscriptions\":[]}}"
+
userJSON :: LB.ByteString
userJSON = "\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}"
parsedMarkdown :: LB.ByteString
+parsedMarkdown =
#if defined(darwin_HOST_OS) && defined(swiftJSON)
-parsedMarkdown = "{\"formattedText\":[{\"format\":{\"bold\":{}},\"text\":\"hello\"}]}"
+ parsedMarkdownSwift
#else
-parsedMarkdown = "{\"formattedText\":[{\"format\":{\"type\":\"bold\"},\"text\":\"hello\"}]}"
+ parsedMarkdownTagged
#endif
+parsedMarkdownSwift :: LB.ByteString
+parsedMarkdownSwift = "{\"formattedText\":[{\"format\":{\"_owsf\":true,\"bold\":{}},\"text\":\"hello\"}]}"
+
+parsedMarkdownTagged :: LB.ByteString
+parsedMarkdownTagged = "{\"formattedText\":[{\"format\":{\"type\":\"bold\"},\"text\":\"hello\"}]}"
+
testChatApiNoUser :: FilePath -> IO ()
testChatApiNoUser tmp = do
let dbPrefix = tmp > "1"
From 73b3ea36481c11e28332c734f43c2ddeacc9c231 Mon Sep 17 00:00:00 2001
From: Moritz Angermann
Date: Mon, 9 Oct 2023 21:04:29 +0800
Subject: [PATCH 08/80] Drop entropy patch (#3191)
* Drop entropy patch
We don't need the patch anymore. We can set -fDoNotGetEntropy these days to achieve the same.
* remove entropy.patch
---------
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
---
flake.nix | 8 ++++----
scripts/nix/entropy.patch | 30 ------------------------------
2 files changed, 4 insertions(+), 34 deletions(-)
delete mode 100644 scripts/nix/entropy.patch
diff --git a/flake.nix b/flake.nix
index f6a68246e2..44a2e287ea 100644
--- a/flake.nix
+++ b/flake.nix
@@ -287,7 +287,7 @@
extra-modules = [{
packages.simplexmq.flags.swift = true;
packages.direct-sqlcipher.flags.commoncrypto = true;
- packages.entropy.patches = [ ./scripts/nix/entropy.patch ];
+ packages.entropy.flags.DoNotGetEntropy = true;
}];
}).simplex-chat.components.library.override (
iosOverrides "pkg-ios-aarch64-swift-json"
@@ -297,7 +297,7 @@
pkgs' = pkgs;
extra-modules = [{
packages.direct-sqlcipher.flags.commoncrypto = true;
- packages.entropy.patches = [ ./scripts/nix/entropy.patch ];
+ packages.entropy.flags.DoNotGetEntropy = true;
}];
}).simplex-chat.components.library.override (
iosOverrides "pkg-ios-aarch64-tagged-json"
@@ -310,7 +310,7 @@
extra-modules = [{
packages.simplexmq.flags.swift = true;
packages.direct-sqlcipher.flags.commoncrypto = true;
- packages.entropy.patches = [ ./scripts/nix/entropy.patch ];
+ packages.entropy.flags.DoNotGetEntropy = true;
}];
}).simplex-chat.components.library.override (
iosOverrides "pkg-ios-x86_64-swift-json"
@@ -320,7 +320,7 @@
pkgs' = pkgs;
extra-modules = [{
packages.direct-sqlcipher.flags.commoncrypto = true;
- packages.entropy.patches = [ ./scripts/nix/entropy.patch ];
+ packages.entropy.flags.DoNotGetEntropy = true;
}];
}).simplex-chat.components.library.override (
iosOverrides "pkg-ios-x86_64-tagged-json"
diff --git a/scripts/nix/entropy.patch b/scripts/nix/entropy.patch
deleted file mode 100644
index 2add42acb3..0000000000
--- a/scripts/nix/entropy.patch
+++ /dev/null
@@ -1,30 +0,0 @@
-diff --git a/cbits/random_initialized.c b/cbits/random_initialized.c
-index 36ac968..ab708b0 100644
---- a/cbits/random_initialized.c
-+++ b/cbits/random_initialized.c
-@@ -5,14 +5,6 @@
- #include
- #include
-
--#ifdef HAVE_GETENTROPY
--static int ensure_pool_initialized_getentropy()
--{
-- char tmp;
-- return getentropy(&tmp, sizeof(tmp));
--}
--#endif
--
- // Poll /dev/random to wait for randomness. This is a proxy for the /dev/urandom
- // pool being initialized.
- static int ensure_pool_initialized_poll()
-@@ -45,10 +37,5 @@ static int ensure_pool_initialized_poll()
- // Returns 0 on success, non-zero on failure.
- int ensure_pool_initialized()
- {
--#ifdef HAVE_GETENTROPY
-- if (ensure_pool_initialized_getentropy() == 0)
-- return 0;
--#endif
--
- return ensure_pool_initialized_poll();
- }
From 20995c691214404316d8279590af8e0dd3f1f998 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Mon, 9 Oct 2023 14:35:44 +0100
Subject: [PATCH 09/80] core: 5.3.2.0
---
package.yaml | 2 +-
simplex-chat.cabal | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.yaml b/package.yaml
index 406f9aabaa..6ebfb03c3a 100644
--- a/package.yaml
+++ b/package.yaml
@@ -1,5 +1,5 @@
name: simplex-chat
-version: 5.3.1.0
+version: 5.3.2.0
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme
diff --git a/simplex-chat.cabal b/simplex-chat.cabal
index 338346b655..4890d0e5c6 100644
--- a/simplex-chat.cabal
+++ b/simplex-chat.cabal
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
-version: 5.3.1.0
+version: 5.3.2.0
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat
From 09e5798d591cfddd90f9c98e70ccf0bd7a77633b Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Mon, 9 Oct 2023 16:56:42 +0100
Subject: [PATCH 10/80] ios: correctly parse json responses (#3193)
---
apps/ios/SimpleX.xcodeproj/project.pbxproj | 40 +++++++++++-----------
apps/ios/SimpleXChat/API.swift | 5 ++-
2 files changed, 24 insertions(+), 21 deletions(-)
diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj
index 8b45e45f95..18021e3079 100644
--- a/apps/ios/SimpleX.xcodeproj/project.pbxproj
+++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj
@@ -114,11 +114,11 @@
5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */; };
5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */; };
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
- 5CC7398D2AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC739882AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp-ghc8.10.7.a */; };
- 5CC7398E2AC9D168009470A9 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC739892AC9D168009470A9 /* libgmp.a */; };
- 5CC7398F2AC9D168009470A9 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC7398A2AC9D168009470A9 /* libffi.a */; };
- 5CC739902AC9D168009470A9 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC7398B2AC9D168009470A9 /* libgmpxx.a */; };
- 5CC739912AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC7398C2AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp.a */; };
+ 5CC739972AD44E2E009470A9 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC739922AD44E2E009470A9 /* libgmp.a */; };
+ 5CC739982AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC739932AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F-ghc8.10.7.a */; };
+ 5CC739992AD44E2E009470A9 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC739942AD44E2E009470A9 /* libffi.a */; };
+ 5CC7399A2AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC739952AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F.a */; };
+ 5CC7399B2AD44E2E009470A9 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC739962AD44E2E009470A9 /* libgmpxx.a */; };
5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; };
5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; };
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; };
@@ -395,11 +395,11 @@
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = ""; };
5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; };
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; };
- 5CC739882AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp-ghc8.10.7.a"; sourceTree = ""; };
- 5CC739892AC9D168009470A9 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; };
- 5CC7398A2AC9D168009470A9 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; };
- 5CC7398B2AC9D168009470A9 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; };
- 5CC7398C2AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp.a"; sourceTree = ""; };
+ 5CC739922AD44E2E009470A9 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; };
+ 5CC739932AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F-ghc8.10.7.a"; sourceTree = ""; };
+ 5CC739942AD44E2E009470A9 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; };
+ 5CC739952AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F.a"; sourceTree = ""; };
+ 5CC739962AD44E2E009470A9 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; };
5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = ""; };
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = ""; };
5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = ""; };
@@ -507,13 +507,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 5CC739902AC9D168009470A9 /* libgmpxx.a in Frameworks */,
+ 5CC739982AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F-ghc8.10.7.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
- 5CC7398D2AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp-ghc8.10.7.a in Frameworks */,
- 5CC7398E2AC9D168009470A9 /* libgmp.a in Frameworks */,
- 5CC739912AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp.a in Frameworks */,
- 5CC7398F2AC9D168009470A9 /* libffi.a in Frameworks */,
+ 5CC739972AD44E2E009470A9 /* libgmp.a in Frameworks */,
+ 5CC7399A2AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F.a in Frameworks */,
+ 5CC739992AD44E2E009470A9 /* libffi.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
+ 5CC7399B2AD44E2E009470A9 /* libgmpxx.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -574,11 +574,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
- 5CC7398A2AC9D168009470A9 /* libffi.a */,
- 5CC739892AC9D168009470A9 /* libgmp.a */,
- 5CC7398B2AC9D168009470A9 /* libgmpxx.a */,
- 5CC739882AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp-ghc8.10.7.a */,
- 5CC7398C2AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp.a */,
+ 5CC739942AD44E2E009470A9 /* libffi.a */,
+ 5CC739922AD44E2E009470A9 /* libgmp.a */,
+ 5CC739962AD44E2E009470A9 /* libgmpxx.a */,
+ 5CC739932AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F-ghc8.10.7.a */,
+ 5CC739952AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F.a */,
);
path = Libraries;
sourceTree = "";
diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift
index e3d202c124..0d59fe55c7 100644
--- a/apps/ios/SimpleXChat/API.swift
+++ b/apps/ios/SimpleXChat/API.swift
@@ -132,8 +132,11 @@ public func chatResponse(_ s: String) -> ChatResponse {
var type: String?
var json: String?
if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary {
- if let jResp = j["resp"] as? NSDictionary, jResp.count == 1 {
+ if let jResp = j["resp"] as? NSDictionary, jResp.count == 1 || jResp.count == 2 {
type = jResp.allKeys[0] as? String
+ if jResp.count == 2 && type == "_owsf" {
+ type = jResp.allKeys[1] as? String
+ }
if type == "apiChats" {
if let jApiChats = jResp["apiChats"] as? NSDictionary,
let user: UserRef = try? decodeObject(jApiChats["user"] as Any),
From d764b3485ab7efaf24e7a0bc4a0481d1c33bc9d4 Mon Sep 17 00:00:00 2001
From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
Date: Tue, 10 Oct 2023 00:10:47 +0800
Subject: [PATCH 11/80] desktop (windows): Github action for packaging (#3167)
* desktop (windows): Github action for packaging
* env
* path changes
---
.github/workflows/build.yml | 33 +++++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0c7a55b148..8785360693 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -293,4 +293,37 @@ jobs:
body: |
${{ steps.windows_build.outputs.bin_hash }}
+ - name: Windows build desktop
+ id: windows_desktop_build
+ if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
+ env:
+ SIMPLEX_CI_REPO_URL: ${{ secrets.SIMPLEX_CI_REPO_URL }}
+ shell: bash
+ run: |
+ scripts/desktop/build-lib-windows.sh
+ cd apps/multiplatform
+ ./gradlew packageMsi
+ path=$(echo $PWD/release/main/msi/*imple*.msi | sed 's#/\([a-z]\)#\1:#' | sed 's#/#\\#g')
+ echo "package_path=$path" >> $GITHUB_OUTPUT
+ echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
+
+ - name: Windows upload desktop package to release
+ if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
+ uses: svenstaro/upload-release-action@v2
+ with:
+ repo_token: ${{ secrets.GITHUB_TOKEN }}
+ file: ${{ steps.windows_desktop_build.outputs.package_path }}
+ asset_name: ${{ matrix.desktop_asset_name }}
+ tag: ${{ github.ref }}
+
+ - name: Windows update desktop package hash
+ if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
+ uses: softprops/action-gh-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ append_body: true
+ body: |
+ ${{ steps.windows_desktop_build.outputs.package_hash }}
+
# Windows /
From c0e22d74c4df1e2c968f43c63f8d5efd7aa530bd Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Mon, 9 Oct 2023 17:30:48 +0100
Subject: [PATCH 12/80] core: 5.4.0.1
---
package.yaml | 2 +-
simplex-chat.cabal | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.yaml b/package.yaml
index 907a2a0686..6fed41b2ae 100644
--- a/package.yaml
+++ b/package.yaml
@@ -1,5 +1,5 @@
name: simplex-chat
-version: 5.4.0.0
+version: 5.4.0.1
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme
diff --git a/simplex-chat.cabal b/simplex-chat.cabal
index 30c4c62dc1..4148f0ba83 100644
--- a/simplex-chat.cabal
+++ b/simplex-chat.cabal
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
-version: 5.4.0.0
+version: 5.4.0.1
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat
From 3ddf7b26808dc5f5825e228f24fe819c29424383 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Mon, 9 Oct 2023 18:03:03 +0100
Subject: [PATCH 13/80] ios: close database connections when app is terminating
(#3188)
* ios: close database connections when app is terminating
* update
* remove ()
* close when suspended too
* additional check
* fix
* refactore
* reset "terminating" flag
---
apps/ios/Shared/Model/SuspendChat.swift | 28 ++++++++++++++++++++-----
apps/ios/SimpleXChat/API.swift | 7 +++++++
apps/ios/SimpleXChat/AppGroup.swift | 8 +++++++
apps/ios/SimpleXChat/SimpleX.h | 1 +
4 files changed, 39 insertions(+), 5 deletions(-)
diff --git a/apps/ios/Shared/Model/SuspendChat.swift b/apps/ios/Shared/Model/SuspendChat.swift
index 58ed46a05a..1c8c32f8b9 100644
--- a/apps/ios/Shared/Model/SuspendChat.swift
+++ b/apps/ios/Shared/Model/SuspendChat.swift
@@ -19,7 +19,11 @@ let bgSuspendTimeout: Int = 5 // seconds
let terminationTimeout: Int = 3 // seconds
private func _suspendChat(timeout: Int) {
- if ChatModel.ok {
+ // this is a redundant check to prevent logical errors, like the one fixed in this PR
+ let state = appStateGroupDefault.get()
+ if !state.canSuspend {
+ logger.error("_suspendChat called, current state: \(state.rawValue, privacy: .public)")
+ } else if ChatModel.ok {
appStateGroupDefault.set(.suspending)
apiSuspendChat(timeoutMicroseconds: timeout * 1000000)
let endTask = beginBGTask(chatSuspended)
@@ -31,9 +35,7 @@ private func _suspendChat(timeout: Int) {
func suspendChat() {
suspendLockQueue.sync {
- if appStateGroupDefault.get() != .stopped {
- _suspendChat(timeout: appSuspendTimeout)
- }
+ _suspendChat(timeout: appSuspendTimeout)
}
}
@@ -45,15 +47,25 @@ func suspendBgRefresh() {
}
}
+private var terminating = false
+
func terminateChat() {
+ logger.debug("terminateChat")
suspendLockQueue.sync {
switch appStateGroupDefault.get() {
case .suspending:
// suspend instantly if already suspending
_chatSuspended()
+ // when apiSuspendChat is called with timeout 0, it won't send any events on suspension
if ChatModel.ok { apiSuspendChat(timeoutMicroseconds: 0) }
- case .stopped: ()
+ chatCloseStore()
+ case .suspended:
+ chatCloseStore()
+ case .stopped:
+ chatCloseStore()
default:
+ terminating = true
+ // the store will be closed in _chatSuspended when event is received
_suspendChat(timeout: terminationTimeout)
}
}
@@ -73,10 +85,14 @@ private func _chatSuspended() {
if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.stop()
}
+ if terminating {
+ chatCloseStore()
+ }
}
func activateChat(appState: AppState = .active) {
logger.debug("DEBUGGING: activateChat")
+ terminating = false
suspendLockQueue.sync {
appStateGroupDefault.set(appState)
if ChatModel.ok { apiActivateChat() }
@@ -85,6 +101,7 @@ func activateChat(appState: AppState = .active) {
}
func initChatAndMigrate(refreshInvitations: Bool = true) {
+ terminating = false
let m = ChatModel.shared
if (!m.chatInitialized) {
do {
@@ -97,6 +114,7 @@ func initChatAndMigrate(refreshInvitations: Bool = true) {
}
func startChatAndActivate() {
+ terminating = false
logger.debug("DEBUGGING: startChatAndActivate")
if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.start()
diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift
index e3d202c124..fbf9d3b2ca 100644
--- a/apps/ios/SimpleXChat/API.swift
+++ b/apps/ios/SimpleXChat/API.swift
@@ -50,6 +50,13 @@ public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: Migratio
return result
}
+public func chatCloseStore() {
+ let err = fromCString(chat_close_store(getChatCtrl()))
+ if err != "" {
+ logger.error("chatCloseStore error: \(err)")
+ }
+}
+
public func resetChatCtrl() {
chatController = nil
migrationResult = nil
diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift
index e09b957171..4943dbd4ea 100644
--- a/apps/ios/SimpleXChat/AppGroup.swift
+++ b/apps/ios/SimpleXChat/AppGroup.swift
@@ -80,6 +80,14 @@ public enum AppState: String {
default: return false
}
}
+
+ public var canSuspend: Bool {
+ switch self {
+ case .active: true
+ case .bgRefresh: true
+ default: false
+ }
+ }
}
public enum DBContainer: String {
diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h
index 67c2fa728c..644569c1ba 100644
--- a/apps/ios/SimpleXChat/SimpleX.h
+++ b/apps/ios/SimpleXChat/SimpleX.h
@@ -17,6 +17,7 @@ typedef void* chat_ctrl;
// the last parameter is used to return the pointer to chat controller
extern char *chat_migrate_init(char *path, char *key, char *confirm, chat_ctrl *ctrl);
+extern char *chat_close_store(chat_ctrl ctl);
extern char *chat_send_cmd(chat_ctrl ctl, char *cmd);
extern char *chat_recv_msg(chat_ctrl ctl);
extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait);
From 86c2f29920146f60ebf3d833f9ed175e5369cf60 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Mon, 9 Oct 2023 18:30:59 +0100
Subject: [PATCH 14/80] 5.3.2: ios 178, android 157, desktop 14
---
apps/ios/SimpleX.xcodeproj/project.pbxproj | 64 +++++++++++-----------
apps/multiplatform/gradle.properties | 8 +--
2 files changed, 36 insertions(+), 36 deletions(-)
diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj
index 20ba15e925..1cbe61dea0 100644
--- a/apps/ios/SimpleX.xcodeproj/project.pbxproj
+++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj
@@ -48,11 +48,6 @@
5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; };
5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A922283CEDE600C4E99E /* SoundPlayer.swift */; };
5C55A92E283D0FDE00C4E99E /* sounds in Resources */ = {isa = PBXBuildFile; fileRef = 5C55A92D283D0FDE00C4E99E /* sounds */; };
- 5C56251A2AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5625152AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a */; };
- 5C56251B2AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5625162AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a */; };
- 5C56251C2AC1DE5900A21210 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5625172AC1DE5900A21210 /* libgmpxx.a */; };
- 5C56251D2AC1DE5900A21210 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5625182AC1DE5900A21210 /* libgmp.a */; };
- 5C56251E2AC1DE5900A21210 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5625192AC1DE5900A21210 /* libffi.a */; };
5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */; };
5C58BCD6292BEBE600AF9E4F /* CIChatFeatureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C58BCD5292BEBE600AF9E4F /* CIChatFeatureView.swift */; };
5C5DB70E289ABDD200730FFF /* AppearanceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */; };
@@ -119,6 +114,11 @@
5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */; };
5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */; };
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
+ 5CC739A12AD468E4009470A9 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC7399C2AD468E4009470A9 /* libgmpxx.a */; };
+ 5CC739A22AD468E4009470A9 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC7399D2AD468E4009470A9 /* libffi.a */; };
+ 5CC739A32AD468E4009470A9 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC7399E2AD468E4009470A9 /* libgmp.a */; };
+ 5CC739A42AD468E4009470A9 /* libHSsimplex-chat-5.3.2.0-CqvLUli0CbhHnscdGdNqYI-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC7399F2AD468E4009470A9 /* libHSsimplex-chat-5.3.2.0-CqvLUli0CbhHnscdGdNqYI-ghc8.10.7.a */; };
+ 5CC739A52AD468E4009470A9 /* libHSsimplex-chat-5.3.2.0-CqvLUli0CbhHnscdGdNqYI.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC739A02AD468E4009470A9 /* libHSsimplex-chat-5.3.2.0-CqvLUli0CbhHnscdGdNqYI.a */; };
5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; };
5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; };
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; };
@@ -293,11 +293,6 @@
5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = ""; };
5C55A922283CEDE600C4E99E /* SoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundPlayer.swift; sourceTree = ""; };
5C55A92D283D0FDE00C4E99E /* sounds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = sounds; sourceTree = ""; };
- 5C5625152AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a"; sourceTree = ""; };
- 5C5625162AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a"; sourceTree = ""; };
- 5C5625172AC1DE5900A21210 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; };
- 5C5625182AC1DE5900A21210 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; };
- 5C5625192AC1DE5900A21210 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; };
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownHelp.swift; sourceTree = ""; };
5C58BCD5292BEBE600AF9E4F /* CIChatFeatureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIChatFeatureView.swift; sourceTree = ""; };
5C5B67912ABAF4B500DA9412 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; };
@@ -400,6 +395,11 @@
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = ""; };
5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; };
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; };
+ 5CC7399C2AD468E4009470A9 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; };
+ 5CC7399D2AD468E4009470A9 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; };
+ 5CC7399E2AD468E4009470A9 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; };
+ 5CC7399F2AD468E4009470A9 /* libHSsimplex-chat-5.3.2.0-CqvLUli0CbhHnscdGdNqYI-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.2.0-CqvLUli0CbhHnscdGdNqYI-ghc8.10.7.a"; sourceTree = ""; };
+ 5CC739A02AD468E4009470A9 /* libHSsimplex-chat-5.3.2.0-CqvLUli0CbhHnscdGdNqYI.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.2.0-CqvLUli0CbhHnscdGdNqYI.a"; sourceTree = ""; };
5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = ""; };
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = ""; };
5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = ""; };
@@ -507,13 +507,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 5CC739A12AD468E4009470A9 /* libgmpxx.a in Frameworks */,
+ 5CC739A32AD468E4009470A9 /* libgmp.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
- 5C56251C2AC1DE5900A21210 /* libgmpxx.a in Frameworks */,
- 5C56251B2AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a in Frameworks */,
- 5C56251A2AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a in Frameworks */,
- 5C56251E2AC1DE5900A21210 /* libffi.a in Frameworks */,
- 5C56251D2AC1DE5900A21210 /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
+ 5CC739A42AD468E4009470A9 /* libHSsimplex-chat-5.3.2.0-CqvLUli0CbhHnscdGdNqYI-ghc8.10.7.a in Frameworks */,
+ 5CC739A22AD468E4009470A9 /* libffi.a in Frameworks */,
+ 5CC739A52AD468E4009470A9 /* libHSsimplex-chat-5.3.2.0-CqvLUli0CbhHnscdGdNqYI.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -574,11 +574,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
- 5C5625192AC1DE5900A21210 /* libffi.a */,
- 5C5625182AC1DE5900A21210 /* libgmp.a */,
- 5C5625172AC1DE5900A21210 /* libgmpxx.a */,
- 5C5625152AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a */,
- 5C5625162AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a */,
+ 5CC7399D2AD468E4009470A9 /* libffi.a */,
+ 5CC7399E2AD468E4009470A9 /* libgmp.a */,
+ 5CC7399C2AD468E4009470A9 /* libgmpxx.a */,
+ 5CC7399F2AD468E4009470A9 /* libHSsimplex-chat-5.3.2.0-CqvLUli0CbhHnscdGdNqYI-ghc8.10.7.a */,
+ 5CC739A02AD468E4009470A9 /* libHSsimplex-chat-5.3.2.0-CqvLUli0CbhHnscdGdNqYI.a */,
);
path = Libraries;
sourceTree = "";
@@ -1486,7 +1486,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 174;
+ CURRENT_PROJECT_VERSION = 178;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1507,7 +1507,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 5.3.1;
+ MARKETING_VERSION = 5.3.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1528,7 +1528,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 174;
+ CURRENT_PROJECT_VERSION = 178;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1549,7 +1549,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 5.3.1;
+ MARKETING_VERSION = 5.3.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1608,7 +1608,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 174;
+ CURRENT_PROJECT_VERSION = 178;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1621,7 +1621,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 5.3.1;
+ MARKETING_VERSION = 5.3.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1640,7 +1640,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 174;
+ CURRENT_PROJECT_VERSION = 178;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1653,7 +1653,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 5.3.1;
+ MARKETING_VERSION = 5.3.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1672,7 +1672,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 174;
+ CURRENT_PROJECT_VERSION = 178;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1696,7 +1696,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Libraries/sim",
);
- MARKETING_VERSION = 5.3.1;
+ MARKETING_VERSION = 5.3.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -1718,7 +1718,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 174;
+ CURRENT_PROJECT_VERSION = 178;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1742,7 +1742,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Libraries/sim",
);
- MARKETING_VERSION = 5.3.1;
+ MARKETING_VERSION = 5.3.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties
index 0d047a7917..cd5a098f73 100644
--- a/apps/multiplatform/gradle.properties
+++ b/apps/multiplatform/gradle.properties
@@ -25,11 +25,11 @@ android.nonTransitiveRClass=true
android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2
-android.version_name=5.3.1
-android.version_code=154
+android.version_name=5.3.2
+android.version_code=157
-desktop.version_name=5.3.1
-desktop.version_code=11
+desktop.version_name=5.3.2
+desktop.version_code=14
kotlin.version=1.8.20
gradle.plugin.version=7.4.2
From a67b79952b467bb543ace2122feb46230ccbedcd Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Tue, 10 Oct 2023 21:19:04 +0400
Subject: [PATCH 15/80] core: connection plan api; check connection plan before
connecting in terminal api (#3176)
---
simplex-chat.cabal | 1 +
src/Simplex/Chat.hs | 112 +++++++--
src/Simplex/Chat/Controller.hs | 61 +++++
.../M20231009_via_group_link_uri_hash.hs | 24 ++
src/Simplex/Chat/Migrations/chat_schema.sql | 7 +-
src/Simplex/Chat/Store/Connections.hs | 9 +-
src/Simplex/Chat/Store/Direct.hs | 48 ++--
src/Simplex/Chat/Store/Groups.hs | 43 ++++
src/Simplex/Chat/Store/Migrations.hs | 4 +-
src/Simplex/Chat/Store/Profiles.hs | 15 +-
src/Simplex/Chat/Types.hs | 6 +
src/Simplex/Chat/View.hs | 37 +++
tests/ChatTests/Direct.hs | 67 +++++
tests/ChatTests/Groups.hs | 237 ++++++++++++++++++
tests/ChatTests/Profiles.hs | 165 +++++++++++-
15 files changed, 784 insertions(+), 52 deletions(-)
create mode 100644 src/Simplex/Chat/Migrations/M20231009_via_group_link_uri_hash.hs
diff --git a/simplex-chat.cabal b/simplex-chat.cabal
index 4148f0ba83..5a84a1cde8 100644
--- a/simplex-chat.cabal
+++ b/simplex-chat.cabal
@@ -115,6 +115,7 @@ library
Simplex.Chat.Migrations.M20230914_member_probes
Simplex.Chat.Migrations.M20230926_contact_status
Simplex.Chat.Migrations.M20231002_conn_initiated
+ Simplex.Chat.Migrations.M20231009_via_group_link_uri_hash
Simplex.Chat.Mobile
Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 296abf0e2b..6f43f5c0f8 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -902,7 +902,7 @@ processChatCommand = \case
filesInfo <- withStore' $ \db -> getContactFileInfo db user ct
withChatLock "deleteChat direct" . procCmd $ do
deleteFilesAndConns user filesInfo
- when (isReady ct && contactActive ct && notify) $
+ when (contactReady ct && contactActive ct && notify) $
void (sendDirectContactMessage ct XDirectDel) `catchChatError` const (pure ())
contactConnIds <- map aConnId <$> withStore (\db -> getContactConnections db userId ct)
deleteAgentConnectionsAsync user contactConnIds
@@ -1311,6 +1311,8 @@ processChatCommand = \case
case conn'_ of
Just conn' -> pure $ CRConnectionIncognitoUpdated user conn'
Nothing -> throwChatError CEConnectionIncognitoChangeProhibited
+ APIConnectPlan userId cReqUri -> withUserId userId $ \user -> withChatLock "connectPlan" . procCmd $
+ CRConnectionPlan user <$> connectPlan user cReqUri
APIConnect userId incognito (Just (ACR SCMInvitation cReq)) -> withUserId userId $ \user -> withChatLock "connect" . procCmd $ do
subMode <- chatReadVar subscriptionMode
-- [incognito] generate profile to send
@@ -1323,11 +1325,16 @@ processChatCommand = \case
pure $ CRSentConfirmation user
APIConnect userId incognito (Just (ACR SCMContact cReq)) -> withUserId userId $ \user -> connectViaContact user incognito cReq
APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq
- Connect incognito cReqUri -> withUser $ \User {userId} ->
- processChatCommand $ APIConnect userId incognito cReqUri
- ConnectSimplex incognito -> withUser $ \user ->
- -- [incognito] generate profile to send
- connectViaContact user incognito adminContactReq
+ Connect incognito aCReqUri@(Just cReqUri) -> withUser $ \user@User {userId} -> do
+ plan <- connectPlan user cReqUri `catchChatError` const (pure $ CPInvitationLink ILPOk)
+ unless (connectionPlanOk plan) $ throwChatError (CEConnectionPlan plan)
+ processChatCommand $ APIConnect userId incognito aCReqUri
+ Connect _ Nothing -> throwChatError CEInvalidConnReq
+ ConnectSimplex incognito -> withUser $ \user@User {userId} -> do
+ let cReqUri = ACR SCMContact adminContactReq
+ plan <- connectPlan user cReqUri `catchChatError` const (pure $ CPInvitationLink ILPOk)
+ unless (connectionPlanOk plan) $ throwChatError (CEConnectionPlan plan)
+ processChatCommand $ APIConnect userId incognito (Just cReqUri)
DeleteContact cName -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId) True
ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect
APIListContacts userId -> withUserId userId $ \user ->
@@ -1423,7 +1430,7 @@ processChatCommand = \case
processChatCommand . APISendMessage chatRef True Nothing $ ComposedMessage Nothing Nothing mc
SendMessageBroadcast msg -> withUser $ \user -> do
contacts <- withStore' (`getUserContacts` user)
- let cts = filter (\ct -> isReady ct && contactActive ct && directOrUsed ct) contacts
+ let cts = filter (\ct -> contactReady ct && contactActive ct && directOrUsed ct) contacts
ChatConfig {logLevel} <- asks config
withChatLock "sendMessageBroadcast" . procCmd $ do
(successes, failures) <- foldM (sendAndCount user logLevel) (0, 0) cts
@@ -1924,19 +1931,36 @@ processChatCommand = \case
_ -> throwChatError $ CECommandError "not supported"
connectViaContact :: User -> IncognitoEnabled -> ConnectionRequestUri 'CMContact -> m ChatResponse
connectViaContact user@User {userId} incognito cReq@(CRContactUri ConnReqUriData {crClientData}) = withChatLock "connectViaContact" $ do
- let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
- withStore' (\db -> getConnReqContactXContactId db user cReqHash) >>= \case
- (Just contact, _) -> pure $ CRContactAlreadyExists user contact
- (_, xContactId_) -> procCmd $ do
- let randomXContactId = XContactId <$> drgRandomBytes 16
- xContactId <- maybe randomXContactId pure xContactId_
- subMode <- chatReadVar subscriptionMode
+ let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
+ cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
+ case groupLinkId of
+ -- contact address
+ Nothing ->
+ withStore' (\db -> getConnReqContactXContactId db user cReqHash) >>= \case
+ (Just contact, _) -> pure $ CRContactAlreadyExists user contact
+ (_, xContactId_) -> procCmd $ do
+ let randomXContactId = XContactId <$> drgRandomBytes 16
+ xContactId <- maybe randomXContactId pure xContactId_
+ connect' Nothing cReqHash xContactId
+ -- group link
+ Just gLinkId ->
+ withStore' (\db -> getConnReqContactXContactId db user cReqHash) >>= \case
+ (Just _contact, _) -> procCmd $ do
+ -- allow repeat contact request
+ newXContactId <- XContactId <$> drgRandomBytes 16
+ connect' (Just gLinkId) cReqHash newXContactId
+ (_, xContactId_) -> procCmd $ do
+ let randomXContactId = XContactId <$> drgRandomBytes 16
+ xContactId <- maybe randomXContactId pure xContactId_
+ connect' (Just gLinkId) cReqHash xContactId
+ where
+ connect' groupLinkId cReqHash xContactId = do
-- [incognito] generate profile to send
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
let profileToSend = userProfileToSend user incognitoProfile Nothing
dm <- directMessage (XContact profileToSend $ Just xContactId)
+ subMode <- chatReadVar subscriptionMode
connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm subMode
- let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode
toView $ CRNewContactConnection user conn
pure $ CRSentInvitation user incognitoProfile
@@ -1975,7 +1999,7 @@ processChatCommand = \case
-- read contacts before user update to correctly merge preferences
-- [incognito] filter out contacts with whom user has incognito connections
contacts <-
- filter (\ct -> isReady ct && contactActive ct && not (contactConnIncognito ct))
+ filter (\ct -> contactReady ct && contactActive ct && not (contactConnIncognito ct))
<$> withStore' (`getUserContacts` user)
user' <- updateUser
asks currentUser >>= atomically . (`writeTVar` Just user')
@@ -2046,10 +2070,6 @@ processChatCommand = \case
g@(Group GroupInfo {groupProfile = p} _) <- withStore $ \db ->
getGroupIdByName db user gName >>= getGroup db user
runUpdateGroupProfile user g $ update p
- isReady :: Contact -> Bool
- isReady ct =
- let s = connStatus $ ct.activeConn
- in s == ConnReady || s == ConnSndReady
withCurrentCall :: ContactId -> (User -> Contact -> Call -> m (Maybe Call)) -> m ChatResponse
withCurrentCall ctId action = do
(user, ct) <- withStore $ \db -> do
@@ -2168,6 +2188,54 @@ processChatCommand = \case
pure (gId, chatSettings)
_ -> throwChatError $ CECommandError "not supported"
processChatCommand $ APISetChatSettings (ChatRef cType chatId) $ updateSettings chatSettings
+ connectPlan :: User -> AConnectionRequestUri -> m ConnectionPlan
+ connectPlan user (ACR SCMInvitation cReq) = do
+ withStore' (\db -> getConnectionEntityByConnReq db user cReq) >>= \case
+ Nothing -> pure $ CPInvitationLink ILPOk
+ Just (RcvDirectMsgConnection conn ct_) -> do
+ let Connection {connStatus, contactConnInitiated} = conn
+ if
+ | connStatus == ConnNew && contactConnInitiated ->
+ pure $ CPInvitationLink ILPOwnLink
+ | not (connReady conn) ->
+ pure $ CPInvitationLink (ILPConnecting ct_)
+ | otherwise -> case ct_ of
+ Just ct -> pure $ CPInvitationLink (ILPKnown ct)
+ Nothing -> throwChatError $ CEInternalError "ready RcvDirectMsgConnection connection should have associated contact"
+ Just _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection"
+ connectPlan user (ACR SCMContact cReq) = do
+ let CRContactUri ConnReqUriData {crClientData} = cReq
+ groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
+ case groupLinkId of
+ -- contact address
+ Nothing ->
+ withStore' (`getUserContactLinkByConnReq` cReq) >>= \case
+ Just _ -> pure $ CPContactAddress CAPOwnLink
+ Nothing -> do
+ let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
+ withStore' (\db -> getContactByConnReqHash db user cReqHash) >>= \case
+ Nothing -> pure $ CPContactAddress CAPOk
+ Just ct
+ | not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnecting ct)
+ | otherwise -> pure $ CPContactAddress (CAPKnown ct)
+ -- group link
+ Just _ ->
+ withStore' (\db -> getGroupInfoByUserContactLinkConnReq db user cReq) >>= \case
+ Just g -> pure $ CPGroupLink (GLPOwnLink g)
+ Nothing -> do
+ let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
+ ct_ <- withStore' $ \db -> getContactByConnReqHash db user cReqHash
+ gInfo_ <- withStore' $ \db -> getGroupInfoByGroupLinkHash db user cReqHash
+ case (gInfo_, ct_) of
+ (Nothing, Nothing) -> pure $ CPGroupLink GLPOk
+ (Nothing, Just ct)
+ | not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnecting gInfo_)
+ | otherwise -> pure $ CPGroupLink GLPOk
+ (Just gInfo@GroupInfo {membership}, _)
+ | not (memberActive membership) && not (memberRemoved membership) ->
+ pure $ CPGroupLink (GLPConnecting gInfo_)
+ | memberActive membership -> pure $ CPGroupLink (GLPKnown gInfo)
+ | otherwise -> pure $ CPGroupLink GLPOk
assertDirectAllowed :: ChatMonad m => User -> MsgDirection -> Contact -> CMEventTag e -> m ()
assertDirectAllowed user dir ct event =
@@ -4230,7 +4298,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> m ()
processGroupInvitation ct inv msg msgMeta = do
- let Contact {localDisplayName = c, activeConn = Connection {peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'}} = ct
+ let Contact {localDisplayName = c, activeConn = Connection {connId, peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'}} = ct
GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} = inv
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c)
@@ -4243,6 +4311,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
dm <- directMessage $ XGrpAcpt memberId
connIds <- joinAgentConnectionAsync user True connRequest dm subMode
withStore' $ \db -> do
+ setViaGroupLinkHash db groupId connId
createMemberConnectionAsync db user hostId connIds (fromJVersionRange peerChatVRange) subMode
updateGroupMemberStatusById db userId hostId GSMemAccepted
updateGroupMemberStatus db userId membership GSMemAccepted
@@ -5642,6 +5711,7 @@ chatCommandP =
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <* char_ '@' <*> (Just <$> displayName) <* A.space <*> quotedMsg <*> msgTextP),
"/_contacts " *> (APIListContacts <$> A.decimal),
"/contacts" $> ListContacts,
+ "/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> strP),
"/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> ((Just <$> strP) <|> A.takeByteString $> Nothing)),
"/_connect " *> (APIAddContact <$> A.decimal <*> incognitoOnOffP),
"/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP),
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index d859231faa..3466371da8 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -338,6 +338,7 @@ data ChatCommand
| APIAddContact UserId IncognitoEnabled
| AddContact IncognitoEnabled
| APISetConnectionIncognito Int64 IncognitoEnabled
+ | APIConnectPlan UserId AConnectionRequestUri
| APIConnect UserId IncognitoEnabled (Maybe AConnectionRequestUri)
| Connect IncognitoEnabled (Maybe AConnectionRequestUri)
| ConnectSimplex IncognitoEnabled -- UserId (not used in UI)
@@ -489,6 +490,7 @@ data ChatResponse
| CRVersionInfo {versionInfo :: CoreVersionInfo, chatMigrations :: [UpMigration], agentMigrations :: [UpMigration]}
| CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation, connection :: PendingContactConnection}
| CRConnectionIncognitoUpdated {user :: User, toConnection :: PendingContactConnection}
+ | CRConnectionPlan {user :: User, connectionPlan :: ConnectionPlan}
| CRSentConfirmation {user :: User}
| CRSentInvitation {user :: User, customUserProfile :: Maybe Profile}
| CRContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact}
@@ -624,6 +626,64 @@ instance ToJSON ChatResponse where
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CR"
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CR"
+data ConnectionPlan
+ = CPInvitationLink {invitationLinkPlan :: InvitationLinkPlan}
+ | CPContactAddress {contactAddressPlan :: ContactAddressPlan}
+ | CPGroupLink {groupLinkPlan :: GroupLinkPlan}
+ deriving (Show, Generic)
+
+instance ToJSON ConnectionPlan where
+ toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CP"
+ toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CP"
+
+data InvitationLinkPlan
+ = ILPOk
+ | ILPOwnLink
+ | ILPConnecting {contact_ :: Maybe Contact}
+ | ILPKnown {contact :: Contact}
+ deriving (Show, Generic)
+
+instance ToJSON InvitationLinkPlan where
+ toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "ILP"
+ toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "ILP"
+
+data ContactAddressPlan
+ = CAPOk
+ | CAPOwnLink
+ | CAPConnecting {contact :: Contact}
+ | CAPKnown {contact :: Contact}
+ deriving (Show, Generic)
+
+instance ToJSON ContactAddressPlan where
+ toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CAP"
+ toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CAP"
+
+data GroupLinkPlan
+ = GLPOk
+ | GLPOwnLink {groupInfo :: GroupInfo}
+ | GLPConnecting {groupInfo_ :: Maybe GroupInfo}
+ | GLPKnown {groupInfo :: GroupInfo}
+ deriving (Show, Generic)
+
+instance ToJSON GroupLinkPlan where
+ toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "GLP"
+ toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "GLP"
+
+connectionPlanOk :: ConnectionPlan -> Bool
+connectionPlanOk = \case
+ CPInvitationLink ilp -> case ilp of
+ ILPOk -> True
+ ILPOwnLink -> True
+ _ -> False
+ CPContactAddress cap -> case cap of
+ CAPOk -> True
+ CAPOwnLink -> True
+ _ -> False
+ CPGroupLink glp -> case glp of
+ GLPOk -> True
+ GLPOwnLink _ -> True
+ _ -> False
+
newtype UserPwd = UserPwd {unUserPwd :: Text}
deriving (Eq, Show)
@@ -888,6 +948,7 @@ data ChatErrorType
| CEChatNotStarted
| CEChatNotStopped
| CEChatStoreChanged
+ | CEConnectionPlan {connectionPlan :: ConnectionPlan}
| CEInvalidConnReq
| CEInvalidChatMessage {connection :: Connection, msgMeta :: Maybe MsgMetaJSON, messageData :: Text, message :: String}
| CEContactNotFound {contactName :: ContactName, suspectedMember :: Maybe (GroupInfo, GroupMember)}
diff --git a/src/Simplex/Chat/Migrations/M20231009_via_group_link_uri_hash.hs b/src/Simplex/Chat/Migrations/M20231009_via_group_link_uri_hash.hs
new file mode 100644
index 0000000000..41c9887a04
--- /dev/null
+++ b/src/Simplex/Chat/Migrations/M20231009_via_group_link_uri_hash.hs
@@ -0,0 +1,24 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+module Simplex.Chat.Migrations.M20231009_via_group_link_uri_hash where
+
+import Database.SQLite.Simple (Query)
+import Database.SQLite.Simple.QQ (sql)
+
+m20231009_via_group_link_uri_hash :: Query
+m20231009_via_group_link_uri_hash =
+ [sql|
+CREATE INDEX idx_connections_conn_req_inv ON connections(conn_req_inv);
+
+ALTER TABLE groups ADD COLUMN via_group_link_uri_hash BLOB;
+CREATE INDEX idx_groups_via_group_link_uri_hash ON groups(via_group_link_uri_hash);
+|]
+
+down_m20231009_via_group_link_uri_hash :: Query
+down_m20231009_via_group_link_uri_hash =
+ [sql|
+DROP INDEX idx_groups_via_group_link_uri_hash;
+ALTER TABLE groups DROP COLUMN via_group_link_uri_hash;
+
+DROP INDEX idx_connections_conn_req_inv;
+|]
diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql
index e88d83e42d..542acbbebd 100644
--- a/src/Simplex/Chat/Migrations/chat_schema.sql
+++ b/src/Simplex/Chat/Migrations/chat_schema.sql
@@ -117,7 +117,8 @@ CREATE TABLE groups(
unread_chat INTEGER DEFAULT 0 CHECK(unread_chat NOT NULL),
chat_ts TEXT,
favorite INTEGER NOT NULL DEFAULT 0,
- send_rcpts INTEGER, -- received
+ send_rcpts INTEGER,
+ via_group_link_uri_hash BLOB, -- received
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
@@ -736,3 +737,7 @@ CREATE INDEX idx_received_probes_probe_hash ON received_probes(probe_hash);
CREATE INDEX idx_sent_probes_created_at ON sent_probes(created_at);
CREATE INDEX idx_sent_probe_hashes_created_at ON sent_probe_hashes(created_at);
CREATE INDEX idx_received_probes_created_at ON received_probes(created_at);
+CREATE INDEX idx_connections_conn_req_inv ON connections(conn_req_inv);
+CREATE INDEX idx_groups_via_group_link_uri_hash ON groups(
+ via_group_link_uri_hash
+);
diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs
index 383db3c59c..c9e846a81f 100644
--- a/src/Simplex/Chat/Store/Connections.hs
+++ b/src/Simplex/Chat/Store/Connections.hs
@@ -9,6 +9,7 @@
module Simplex.Chat.Store.Connections
( getConnectionEntity,
+ getConnectionEntityByConnReq,
getConnectionsToSubscribe,
unsetConnectionToSubscribe,
)
@@ -31,7 +32,7 @@ import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Messaging.Agent.Protocol (ConnId)
-import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow')
+import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import Simplex.Messaging.Util (eitherToMaybe)
@@ -152,6 +153,12 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
userContact_ [(cReq, groupId)] = Right UserContact {userContactLinkId, connReqContact = cReq, groupId}
userContact_ _ = Left SEUserContactLinkNotFound
+getConnectionEntityByConnReq :: DB.Connection -> User -> ConnReqInvitation -> IO (Maybe ConnectionEntity)
+getConnectionEntityByConnReq db user cReq = do
+ connId_ <- maybeFirstRow fromOnly $
+ DB.query db "SELECT agent_conn_id FROM connections WHERE conn_req_inv = ? LIMIT 1" (Only cReq)
+ maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db user) connId_
+
getConnectionsToSubscribe :: DB.Connection -> IO ([ConnId], [ConnectionEntity])
getConnectionsToSubscribe db = do
aConnIds <- map fromOnly <$> DB.query_ db "SELECT agent_conn_id FROM connections where to_subscribe = 1"
diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs
index 91243e2319..7227797193 100644
--- a/src/Simplex/Chat/Store/Direct.hs
+++ b/src/Simplex/Chat/Store/Direct.hs
@@ -25,6 +25,7 @@ module Simplex.Chat.Store.Direct
createConnReqConnection,
getProfileById,
getConnReqContactXContactId,
+ getContactByConnReqHash,
createDirectContact,
deleteContactConnectionsAndFiles,
deleteContact,
@@ -137,32 +138,10 @@ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile grou
getConnReqContactXContactId :: DB.Connection -> User -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId)
getConnReqContactXContactId db user@User {userId} cReqHash = do
- getContact' >>= \case
+ getContactByConnReqHash db user cReqHash >>= \case
c@(Just _) -> pure (c, Nothing)
Nothing -> (Nothing,) <$> getXContactId
where
- getContact' :: IO (Maybe Contact)
- getContact' =
- maybeFirstRow (toContact user) $
- DB.query
- db
- [sql|
- SELECT
- -- Contact
- ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
- cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
- -- Connection
- c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
- c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
- c.peer_chat_min_version, c.peer_chat_max_version
- FROM contacts ct
- JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
- JOIN connections c ON c.contact_id = ct.contact_id
- WHERE ct.user_id = ? AND c.via_contact_uri_hash = ? AND ct.deleted = 0
- ORDER BY c.created_at DESC
- LIMIT 1
- |]
- (userId, cReqHash)
getXContactId :: IO (Maybe XContactId)
getXContactId =
maybeFirstRow fromOnly $
@@ -171,6 +150,29 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do
"SELECT xcontact_id FROM connections WHERE user_id = ? AND via_contact_uri_hash = ? LIMIT 1"
(userId, cReqHash)
+getContactByConnReqHash :: DB.Connection -> User -> ConnReqUriHash -> IO (Maybe Contact)
+getContactByConnReqHash db user@User {userId} cReqHash =
+ maybeFirstRow (toContact user) $
+ DB.query
+ db
+ [sql|
+ SELECT
+ -- Contact
+ ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
+ cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
+ -- Connection
+ c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
+ c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
+ c.peer_chat_min_version, c.peer_chat_max_version
+ FROM contacts ct
+ JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
+ JOIN connections c ON c.contact_id = ct.contact_id
+ WHERE ct.user_id = ? AND c.via_contact_uri_hash = ? AND ct.contact_status = ? AND ct.deleted = 0
+ ORDER BY c.created_at DESC
+ LIMIT 1
+ |]
+ (userId, cReqHash, CSActive)
+
createDirectConnection :: DB.Connection -> User -> ConnId -> ConnReqInvitation -> ConnStatus -> Maybe Profile -> SubscriptionMode -> IO PendingContactConnection
createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile subMode = do
createdAt <- getCurrentTime
diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs
index 236031da94..20fb8c7217 100644
--- a/src/Simplex/Chat/Store/Groups.hs
+++ b/src/Simplex/Chat/Store/Groups.hs
@@ -31,9 +31,12 @@ module Simplex.Chat.Store.Groups
getGroupAndMember,
createNewGroup,
createGroupInvitation,
+ setViaGroupLinkHash,
setGroupInvitationChatItemId,
getGroup,
getGroupInfo,
+ getGroupInfoByUserContactLinkConnReq,
+ getGroupInfoByGroupLinkHash,
updateGroupProfile,
getGroupIdByName,
getGroupMemberIdByName,
@@ -405,6 +408,17 @@ createContactMemberInv_ db User {userId, userContactId} groupId userOrContact Me
)
pure $ Right incognitoLdn
+setViaGroupLinkHash :: DB.Connection -> GroupId -> Int64 -> IO ()
+setViaGroupLinkHash db groupId connId =
+ DB.execute
+ db
+ [sql|
+ UPDATE groups
+ SET via_group_link_uri_hash = (SELECT via_contact_uri_hash FROM connections WHERE connection_id = ?)
+ WHERE group_id = ?
+ |]
+ (connId, groupId)
+
setGroupInvitationChatItemId :: DB.Connection -> User -> GroupId -> ChatItemId -> IO ()
setGroupInvitationChatItemId db User {userId} groupId chatItemId = do
currentTs <- getCurrentTime
@@ -1102,6 +1116,35 @@ getGroupInfo db User {userId, userContactId} groupId =
|]
(groupId, userId, userContactId)
+getGroupInfoByUserContactLinkConnReq :: DB.Connection -> User -> ConnReqContact -> IO (Maybe GroupInfo)
+getGroupInfoByUserContactLinkConnReq db user cReq = do
+ groupId_ <- maybeFirstRow fromOnly $
+ DB.query
+ db
+ [sql|
+ SELECT group_id
+ FROM user_contact_links
+ WHERE conn_req_contact = ?
+ |]
+ (Only cReq)
+ maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db user) groupId_
+
+getGroupInfoByGroupLinkHash :: DB.Connection -> User -> ConnReqUriHash -> IO (Maybe GroupInfo)
+getGroupInfoByGroupLinkHash db user@User {userId, userContactId} groupLinkHash = do
+ groupId_ <- maybeFirstRow fromOnly $
+ DB.query
+ db
+ [sql|
+ SELECT g.group_id
+ FROM groups g
+ JOIN group_members mu ON mu.group_id = g.group_id
+ WHERE g.user_id = ? AND g.via_group_link_uri_hash = ?
+ AND mu.contact_id = ? AND mu.member_status NOT IN (?,?,?)
+ LIMIT 1
+ |]
+ (userId, groupLinkHash, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted)
+ maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db user) groupId_
+
getGroupIdByName :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO GroupId
getGroupIdByName db User {userId} gName =
ExceptT . firstRow fromOnly (SEGroupNotFoundByName gName) $
diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs
index 3ef68874b0..5c44b8cded 100644
--- a/src/Simplex/Chat/Store/Migrations.hs
+++ b/src/Simplex/Chat/Store/Migrations.hs
@@ -83,6 +83,7 @@ import Simplex.Chat.Migrations.M20230913_member_contacts
import Simplex.Chat.Migrations.M20230914_member_probes
import Simplex.Chat.Migrations.M20230926_contact_status
import Simplex.Chat.Migrations.M20231002_conn_initiated
+import Simplex.Chat.Migrations.M20231009_via_group_link_uri_hash
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -165,7 +166,8 @@ schemaMigrations =
("20230913_member_contacts", m20230913_member_contacts, Just down_m20230913_member_contacts),
("20230914_member_probes", m20230914_member_probes, Just down_m20230914_member_probes),
("20230926_contact_status", m20230926_contact_status, Just down_m20230926_contact_status),
- ("20231002_conn_initiated", m20231002_conn_initiated, Just down_m20231002_conn_initiated)
+ ("20231002_conn_initiated", m20231002_conn_initiated, Just down_m20231002_conn_initiated),
+ ("20231009_via_group_link_uri_hash", m20231009_via_group_link_uri_hash, Just down_m20231009_via_group_link_uri_hash)
]
-- | The list of migrations in ascending order by date
diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs
index a577796810..5b5a6eb671 100644
--- a/src/Simplex/Chat/Store/Profiles.hs
+++ b/src/Simplex/Chat/Store/Profiles.hs
@@ -42,6 +42,7 @@ module Simplex.Chat.Store.Profiles
deleteUserAddress,
getUserAddress,
getUserContactLinkById,
+ getUserContactLinkByConnReq,
updateUserAddressAutoAccept,
getProtocolServers,
overwriteProtocolServers,
@@ -86,7 +87,7 @@ import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SubscriptionMode)
import Simplex.Messaging.Transport.Client (TransportHost)
-import Simplex.Messaging.Util (safeDecodeUtf8)
+import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8)
createUserRecord :: DB.Connection -> AgentUserId -> Profile -> Bool -> ExceptT StoreError IO User
createUserRecord db auId p activeUser = createUserRecordAt db auId p activeUser =<< liftIO getCurrentTime
@@ -440,6 +441,18 @@ getUserContactLinkById db userId userContactLinkId =
|]
(userId, userContactLinkId)
+getUserContactLinkByConnReq :: DB.Connection -> ConnReqContact -> IO (Maybe UserContactLink)
+getUserContactLinkByConnReq db cReq =
+ maybeFirstRow toUserContactLink $
+ DB.query
+ db
+ [sql|
+ SELECT conn_req_contact, auto_accept, auto_accept_incognito, auto_reply_msg_content
+ FROM user_contact_links
+ WHERE conn_req_contact = ?
+ |]
+ (Only cReq)
+
updateUserAddressAutoAccept :: DB.Connection -> User -> Maybe AutoAccept -> ExceptT StoreError IO UserContactLink
updateUserAddressAutoAccept db user@User {userId} autoAccept = do
link <- getUserAddress db user
diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs
index 529d2bf019..864ebd7227 100644
--- a/src/Simplex/Chat/Types.hs
+++ b/src/Simplex/Chat/Types.hs
@@ -206,6 +206,9 @@ directOrUsed ct@Contact {contactUsed} =
anyDirectOrUsed :: Contact -> Bool
anyDirectOrUsed Contact {contactUsed, activeConn = Connection {connLevel}} = connLevel == 0 || contactUsed
+contactReady :: Contact -> Bool
+contactReady Contact {activeConn} = connReady activeConn
+
contactActive :: Contact -> Bool
contactActive Contact {contactStatus} = contactStatus == CSActive
@@ -1244,6 +1247,9 @@ data Connection = Connection
}
deriving (Eq, Show, Generic)
+connReady :: Connection -> Bool
+connReady Connection {connStatus} = connStatus == ConnReady || connStatus == ConnSndReady
+
authErrDisableCount :: Int
authErrDisableCount = 10
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index b981929efc..bb5e854cd2 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -148,6 +148,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
CRVersionInfo info _ _ -> viewVersionInfo logLevel info
CRInvitation u cReq _ -> ttyUser u $ viewConnReqInvitation cReq
CRConnectionIncognitoUpdated u c -> ttyUser u $ viewConnectionIncognitoUpdated c
+ CRConnectionPlan u connectionPlan -> ttyUser u $ viewConnectionPlan connectionPlan
CRSentConfirmation u -> ttyUser u ["confirmation sent!"]
CRSentInvitation u customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView
CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"]
@@ -1223,6 +1224,41 @@ viewConnectionIncognitoUpdated PendingContactConnection {pccConnId, customUserPr
| isJust customUserProfileId = ["connection " <> sShow pccConnId <> " changed to incognito"]
| otherwise = ["connection " <> sShow pccConnId <> " changed to non incognito"]
+viewConnectionPlan :: ConnectionPlan -> [StyledString]
+viewConnectionPlan = \case
+ CPInvitationLink ilp -> case ilp of
+ ILPOk -> [invLink "ok to connect"]
+ ILPOwnLink -> [invLink "own link"]
+ ILPConnecting Nothing -> [invLink "connecting"]
+ ILPConnecting (Just ct) -> [invLink ("connecting to contact " <> ttyContact' ct)]
+ ILPKnown ct ->
+ [ invLink ("known contact " <> ttyContact' ct),
+ "use " <> ttyToContact' ct <> highlight' "" <> " to send messages"
+ ]
+ where
+ invLink = ("invitation link: " <>)
+ CPContactAddress cap -> case cap of
+ CAPOk -> [ctAddr "ok to connect"]
+ CAPOwnLink -> [ctAddr "own address"]
+ CAPConnecting ct -> [ctAddr ("connecting to contact " <> ttyContact' ct)]
+ CAPKnown ct ->
+ [ ctAddr ("known contact " <> ttyContact' ct),
+ "use " <> ttyToContact' ct <> highlight' "" <> " to send messages"
+ ]
+ where
+ ctAddr = ("contact address: " <>)
+ CPGroupLink glp -> case glp of
+ GLPOk -> [grpLink "ok to connect"]
+ GLPOwnLink g -> [grpLink "own link for group " <> ttyGroup' g]
+ GLPConnecting Nothing -> [grpLink "connecting"]
+ GLPConnecting (Just g) -> [grpLink ("connecting to group " <> ttyGroup' g)]
+ GLPKnown g ->
+ [ grpLink ("known group " <> ttyGroup' g),
+ "use " <> ttyToGroup g <> highlight' "" <> " to send messages"
+ ]
+ where
+ grpLink = ("group link: " <>)
+
viewContactUpdated :: Contact -> Contact -> [StyledString]
viewContactUpdated
Contact {localDisplayName = n, profile = LocalProfile {fullName, contactLink}}
@@ -1565,6 +1601,7 @@ viewChatError logLevel = \case
CEChatNotStarted -> ["error: chat not started"]
CEChatNotStopped -> ["error: chat not stopped"]
CEChatStoreChanged -> ["error: chat store changed, please restart chat"]
+ CEConnectionPlan connectionPlan -> viewConnectionPlan connectionPlan
CEInvalidConnReq -> viewInvalidConnReq
CEInvalidChatMessage Connection {connId} msgMeta_ msg e ->
[ plain $
diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs
index a5fc7455c8..47333906bb 100644
--- a/tests/ChatTests/Direct.hs
+++ b/tests/ChatTests/Direct.hs
@@ -44,6 +44,10 @@ chatDirectTests = do
describe "duplicate contacts" $ do
it "duplicate contacts are separate (contacts don't merge)" testDuplicateContactsSeparate
it "new contact is separate with multiple duplicate contacts (contacts don't merge)" testDuplicateContactsMultipleSeparate
+ describe "invitation link connection plan" $ do
+ it "invitation link ok to connect" testPlanInvitationLinkOk
+ it "own invitation link" testPlanInvitationLinkOwn
+ it "connecting via invitation link" testPlanInvitationLinkConnecting
describe "SMP servers" $ do
it "get and set SMP servers" testGetSetSMPServers
it "test SMP server connection" testTestSMPServerConnection
@@ -236,6 +240,69 @@ testDuplicateContactsMultipleSeparate =
alice `hasContactProfiles` ["alice", "bob", "bob", "bob"]
bob `hasContactProfiles` ["bob", "alice", "alice", "alice"]
+testPlanInvitationLinkOk :: HasCallStack => FilePath -> IO ()
+testPlanInvitationLinkOk =
+ testChat2 aliceProfile bobProfile $
+ \alice bob -> do
+ alice ##> "/c"
+ inv <- getInvitation alice
+ bob ##> ("/_connect plan 1 " <> inv)
+ bob <## "invitation link: ok to connect"
+
+ bob ##> ("/c " <> inv)
+ bob <## "confirmation sent!"
+ concurrently_
+ (alice <## "bob (Bob): contact is connected")
+ (bob <## "alice (Alice): contact is connected")
+
+ bob ##> ("/_connect plan 1 " <> inv)
+ bob <## "invitation link: ok to connect" -- conn_req_inv is forgotten after connection
+
+ alice <##> bob
+
+testPlanInvitationLinkOwn :: HasCallStack => FilePath -> IO ()
+testPlanInvitationLinkOwn tmp =
+ withNewTestChat tmp "alice" aliceProfile $ \alice -> do
+ alice ##> "/c"
+ inv <- getInvitation alice
+ alice ##> ("/_connect plan 1 " <> inv)
+ alice <## "invitation link: own link"
+
+ alice ##> ("/c " <> inv)
+ alice <## "confirmation sent!"
+ alice
+ <### [ "alice_1 (Alice): contact is connected",
+ "alice_2 (Alice): contact is connected"
+ ]
+
+ alice ##> ("/_connect plan 1 " <> inv)
+ alice <## "invitation link: ok to connect" -- conn_req_inv is forgotten after connection
+
+ alice @@@ [("@alice_1", lastChatFeature), ("@alice_2", lastChatFeature)]
+ alice `send` "@alice_2 hi"
+ alice
+ <### [ WithTime "@alice_2 hi",
+ WithTime "alice_1> hi"
+ ]
+ alice `send` "@alice_1 hey"
+ alice
+ <### [ WithTime "@alice_1 hey",
+ WithTime "alice_2> hey"
+ ]
+ alice @@@ [("@alice_1", "hey"), ("@alice_2", "hey")]
+
+testPlanInvitationLinkConnecting :: HasCallStack => FilePath -> IO ()
+testPlanInvitationLinkConnecting tmp = do
+ inv <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do
+ alice ##> "/c"
+ getInvitation alice
+ withNewTestChat tmp "bob" bobProfile $ \bob -> do
+ bob ##> ("/c " <> inv)
+ bob <## "confirmation sent!"
+
+ bob ##> ("/_connect plan 1 " <> inv)
+ bob <## "invitation link: connecting"
+
testContactClear :: HasCallStack => FilePath -> IO ()
testContactClear =
testChat2 aliceProfile bobProfile $
diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs
index 9fb6ac7f9b..997beec4ea 100644
--- a/tests/ChatTests/Groups.hs
+++ b/tests/ChatTests/Groups.hs
@@ -57,6 +57,12 @@ chatGroupTests = do
it "leaving groups with unused host contacts deletes incognito profiles" testGroupLinkIncognitoUnusedHostContactsDeleted
it "group link member role" testGroupLinkMemberRole
it "leaving and deleting the group joined via link should NOT delete previously existing direct contacts" testGroupLinkLeaveDelete
+ describe "group link connection plan" $ do
+ it "group link ok to connect; known group" testPlanGroupLinkOkKnown
+ it "group is known if host contact was deleted" testPlanHostContactDeletedGroupLinkKnown
+ it "own group link" testPlanGroupLinkOwn
+ it "connecting via group link" testPlanGroupLinkConnecting
+ it "re-join existing group after leaving" testPlanGroupLinkLeaveRejoin
describe "group message errors" $ do
it "show message decryption error" testGroupMsgDecryptError
it "should report ratchet de-synchronization, synchronize ratchets" testGroupSyncRatchet
@@ -2251,6 +2257,237 @@ testGroupLinkLeaveDelete =
bob <## "alice (Alice)"
bob <## "cath (Catherine)"
+testPlanGroupLinkOkKnown :: HasCallStack => FilePath -> IO ()
+testPlanGroupLinkOkKnown =
+ testChat2 aliceProfile bobProfile $
+ \alice bob -> do
+ alice ##> "/g team"
+ alice <## "group #team is created"
+ alice <## "to add members use /a team or /create link #team"
+ alice ##> "/create link #team"
+ gLink <- getGroupLink alice "team" GRMember True
+
+ bob ##> ("/_connect plan 1 " <> gLink)
+ bob <## "group link: ok to connect"
+
+ bob ##> ("/c " <> gLink)
+ bob <## "connection request sent!"
+ alice <## "bob (Bob): accepting request to join group #team..."
+ concurrentlyN_
+ [ do
+ alice <## "bob (Bob): contact is connected"
+ alice <## "bob invited to group #team via your group link"
+ alice <## "#team: bob joined the group",
+ do
+ bob <## "alice (Alice): contact is connected"
+ bob <## "#team: you joined the group"
+ ]
+ alice #> "#team hi"
+ bob <# "#team alice> hi"
+ bob #> "#team hey"
+ alice <# "#team bob> hey"
+
+ bob ##> ("/_connect plan 1 " <> gLink)
+ bob <## "group link: known group #team"
+ bob <## "use #team to send messages"
+
+ bob ##> ("/c " <> gLink)
+ bob <## "group link: known group #team"
+ bob <## "use #team to send messages"
+
+testPlanHostContactDeletedGroupLinkKnown :: HasCallStack => FilePath -> IO ()
+testPlanHostContactDeletedGroupLinkKnown =
+ testChat2 aliceProfile bobProfile $
+ \alice bob -> do
+ alice ##> "/g team"
+ alice <## "group #team is created"
+ alice <## "to add members use /a team or /create link #team"
+ alice ##> "/create link #team"
+ gLink <- getGroupLink alice "team" GRMember True
+
+ bob ##> ("/c " <> gLink)
+ bob <## "connection request sent!"
+ alice <## "bob (Bob): accepting request to join group #team..."
+ concurrentlyN_
+ [ do
+ alice <## "bob (Bob): contact is connected"
+ alice <## "bob invited to group #team via your group link"
+ alice <## "#team: bob joined the group",
+ do
+ bob <## "alice (Alice): contact is connected"
+ bob <## "#team: you joined the group"
+ ]
+ alice #> "#team hi"
+ bob <# "#team alice> hi"
+ bob #> "#team hey"
+ alice <# "#team bob> hey"
+
+ alice <##> bob
+ threadDelay 500000
+ bob ##> "/d alice"
+ bob <## "alice: contact is deleted"
+ alice <## "bob (Bob) deleted contact with you"
+
+ bob ##> ("/_connect plan 1 " <> gLink)
+ bob <## "group link: known group #team"
+ bob <## "use #team to send messages"
+
+ bob ##> ("/c " <> gLink)
+ bob <## "group link: known group #team"
+ bob <## "use #team to send messages"
+
+testPlanGroupLinkOwn :: HasCallStack => FilePath -> IO ()
+testPlanGroupLinkOwn tmp =
+ withNewTestChat tmp "alice" aliceProfile $ \alice -> do
+ alice ##> "/g team"
+ alice <## "group #team is created"
+ alice <## "to add members use /a team or /create link #team"
+ alice ##> "/create link #team"
+ gLink <- getGroupLink alice "team" GRMember True
+
+ alice ##> ("/_connect plan 1 " <> gLink)
+ alice <## "group link: own link for group #team"
+
+ alice ##> ("/c " <> gLink)
+ alice <## "connection request sent!"
+ alice <## "alice_1 (Alice): accepting request to join group #team..."
+ alice
+ <### [ "alice_1 (Alice): contact is connected",
+ "alice_1 invited to group #team via your group link",
+ "#team: alice_1 joined the group",
+ "alice_2 (Alice): contact is connected",
+ "#team_1: you joined the group",
+ "contact alice_2 is merged into alice_1",
+ "use @alice_1 to send messages"
+ ]
+ alice `send` "#team 1"
+ alice
+ <### [ WithTime "#team 1",
+ WithTime "#team_1 alice_1> 1"
+ ]
+ alice `send` "#team_1 2"
+ alice
+ <### [ WithTime "#team_1 2",
+ WithTime "#team alice_1> 2"
+ ]
+
+ alice ##> ("/_connect plan 1 " <> gLink)
+ alice <## "group link: own link for group #team"
+
+ -- group works if merged contact is deleted
+ alice ##> "/d alice_1"
+ alice <## "alice_1: contact is deleted"
+
+ alice `send` "#team 3"
+ alice
+ <### [ WithTime "#team 3",
+ WithTime "#team_1 alice_1> 3"
+ ]
+ alice `send` "#team_1 4"
+ alice
+ <### [ WithTime "#team_1 4",
+ WithTime "#team alice_1> 4"
+ ]
+
+testPlanGroupLinkConnecting :: HasCallStack => FilePath -> IO ()
+testPlanGroupLinkConnecting tmp = do
+ gLink <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do
+ alice ##> "/g team"
+ alice <## "group #team is created"
+ alice <## "to add members use /a team or /create link #team"
+ alice ##> "/create link #team"
+ getGroupLink alice "team" GRMember True
+ withNewTestChat tmp "bob" bobProfile $ \bob -> do
+ bob ##> ("/c " <> gLink)
+ bob <## "connection request sent!"
+ withTestChat tmp "alice" $ \alice -> do
+ alice
+ <### [ "1 group links active",
+ "#team: group is empty",
+ "bob (Bob): accepting request to join group #team..."
+ ]
+ withTestChat tmp "bob" $ \bob -> do
+ threadDelay 500000
+ bob ##> ("/_connect plan 1 " <> gLink)
+ bob <## "group link: connecting"
+
+ bob ##> ("/c " <> gLink)
+ bob <## "group link: connecting"
+
+testPlanGroupLinkLeaveRejoin :: HasCallStack => FilePath -> IO ()
+testPlanGroupLinkLeaveRejoin =
+ testChat2 aliceProfile bobProfile $
+ \alice bob -> do
+ alice ##> "/g team"
+ alice <## "group #team is created"
+ alice <## "to add members use /a team or /create link #team"
+ alice ##> "/create link #team"
+ gLink <- getGroupLink alice "team" GRMember True
+
+ bob ##> ("/c " <> gLink)
+ bob <## "connection request sent!"
+ alice <## "bob (Bob): accepting request to join group #team..."
+ concurrentlyN_
+ [ do
+ alice <## "bob (Bob): contact is connected"
+ alice <## "bob invited to group #team via your group link"
+ alice <## "#team: bob joined the group",
+ do
+ bob <## "alice (Alice): contact is connected"
+ bob <## "#team: you joined the group"
+ ]
+
+ bob ##> ("/_connect plan 1 " <> gLink)
+ bob <## "group link: known group #team"
+ bob <## "use #team to send messages"
+
+ bob ##> ("/c " <> gLink)
+ bob <## "group link: known group #team"
+ bob <## "use #team to send messages"
+
+ bob ##> "/leave #team"
+ concurrentlyN_
+ [ do
+ bob <## "#team: you left the group"
+ bob <## "use /d #team to delete the group",
+ alice <## "#team: bob left the group"
+ ]
+
+ bob ##> ("/_connect plan 1 " <> gLink)
+ bob <## "group link: ok to connect"
+
+ bob ##> ("/c " <> gLink)
+ bob <## "connection request sent!"
+ alice <## "bob_1 (Bob): accepting request to join group #team..."
+ concurrentlyN_
+ [ alice
+ <### [ "bob_1 (Bob): contact is connected",
+ "bob_1 invited to group #team via your group link",
+ EndsWith "joined the group",
+ "contact bob_1 is merged into bob",
+ "use @bob to send messages"
+ ],
+ bob
+ <### [ "alice_1 (Alice): contact is connected",
+ "#team_1: you joined the group",
+ "contact alice_1 is merged into alice",
+ "use @alice to send messages"
+ ]
+ ]
+
+ alice #> "#team hi"
+ bob <# "#team_1 alice> hi"
+ bob #> "#team_1 hey"
+ alice <# "#team bob> hey"
+
+ bob ##> ("/_connect plan 1 " <> gLink)
+ bob <## "group link: known group #team_1"
+ bob <## "use #team_1 to send messages"
+
+ bob ##> ("/c " <> gLink)
+ bob <## "group link: known group #team_1"
+ bob <## "use #team_1 to send messages"
+
testGroupMsgDecryptError :: HasCallStack => FilePath -> IO ()
testGroupMsgDecryptError tmp =
withNewTestChat tmp "alice" aliceProfile $ \alice -> do
diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs
index da6cbd156f..0d7683c4d7 100644
--- a/tests/ChatTests/Profiles.hs
+++ b/tests/ChatTests/Profiles.hs
@@ -28,6 +28,11 @@ chatProfileTests = do
it "delete connection requests when contact link deleted" testDeleteConnectionRequests
it "auto-reply message" testAutoReplyMessage
it "auto-reply message in incognito" testAutoReplyMessageInIncognito
+ describe "contact address connection plan" $ do
+ it "contact address ok to connect; known contact" testPlanAddressOkKnown
+ it "own contact address" testPlanAddressOwn
+ it "connecting via contact address" testPlanAddressConnecting
+ it "re-connect with deleted contact" testPlanAddressContactDeletedReconnected
describe "incognito" $ do
it "connect incognito via invitation link" testConnectIncognitoInvitationLink
it "connect incognito via contact address" testConnectIncognitoContactAddress
@@ -369,7 +374,8 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $
(alice <## "bob (Bob): contact is connected")
bob ##> ("/c " <> cLink)
- bob <## "alice (Alice): contact already exists"
+ bob <## "contact address: known contact alice"
+ bob <## "use @alice to send messages"
alice @@@ [("@bob", lastChatFeature)]
bob @@@ [("@alice", lastChatFeature), (":2", ""), (":1", "")]
bob ##> "/_delete :1"
@@ -382,7 +388,8 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $
bob @@@ [("@alice", "hey")]
bob ##> ("/c " <> cLink)
- bob <## "alice (Alice): contact already exists"
+ bob <## "contact address: known contact alice"
+ bob <## "use @alice to send messages"
alice <##> bob
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi"), (0, "hey"), (1, "hi"), (0, "hey")])
@@ -440,7 +447,8 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile
(alice <## "robert (Robert): contact is connected")
bob ##> ("/c " <> cLink)
- bob <## "alice (Alice): contact already exists"
+ bob <## "contact address: known contact alice"
+ bob <## "use @alice to send messages"
alice @@@ [("@robert", lastChatFeature)]
bob @@@ [("@alice", lastChatFeature), (":3", ""), (":2", ""), (":1", "")]
bob ##> "/_delete :1"
@@ -455,7 +463,8 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile
bob @@@ [("@alice", "hey")]
bob ##> ("/c " <> cLink)
- bob <## "alice (Alice): contact already exists"
+ bob <## "contact address: known contact alice"
+ bob <## "use @alice to send messages"
alice <##> bob
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi"), (0, "hey"), (1, "hi"), (0, "hey")])
@@ -566,6 +575,154 @@ testAutoReplyMessageInIncognito = testChat2 aliceProfile bobProfile $
]
]
+testPlanAddressOkKnown :: HasCallStack => FilePath -> IO ()
+testPlanAddressOkKnown =
+ testChat2 aliceProfile bobProfile $
+ \alice bob -> do
+ alice ##> "/ad"
+ cLink <- getContactLink alice True
+
+ bob ##> ("/_connect plan 1 " <> cLink)
+ bob <## "contact address: ok to connect"
+
+ bob ##> ("/c " <> cLink)
+ alice <#? bob
+ alice @@@ [("<@bob", "")]
+ alice ##> "/ac bob"
+ alice <## "bob (Bob): accepting contact request..."
+ concurrently_
+ (bob <## "alice (Alice): contact is connected")
+ (alice <## "bob (Bob): contact is connected")
+ alice <##> bob
+
+ bob ##> ("/_connect plan 1 " <> cLink)
+ bob <## "contact address: known contact alice"
+ bob <## "use @alice to send messages"
+
+ bob ##> ("/c " <> cLink)
+ bob <## "contact address: known contact alice"
+ bob <## "use @alice to send messages"
+
+testPlanAddressOwn :: HasCallStack => FilePath -> IO ()
+testPlanAddressOwn tmp =
+ withNewTestChat tmp "alice" aliceProfile $ \alice -> do
+ alice ##> "/ad"
+ cLink <- getContactLink alice True
+
+ alice ##> ("/_connect plan 1 " <> cLink)
+ alice <## "contact address: own address"
+
+ alice ##> ("/c " <> cLink)
+ alice <## "connection request sent!"
+ alice <## "alice_1 (Alice) wants to connect to you!"
+ alice <## "to accept: /ac alice_1"
+ alice <## ("to reject: /rc alice_1 (the sender will NOT be notified)")
+ alice @@@ [("<@alice_1", ""), (":2","")]
+ alice ##> "/ac alice_1"
+ alice <## "alice_1 (Alice): accepting contact request..."
+ alice
+ <### [ "alice_1 (Alice): contact is connected",
+ "alice_2 (Alice): contact is connected"
+ ]
+
+ alice @@@ [("@alice_1", lastChatFeature), ("@alice_2", lastChatFeature)]
+ alice `send` "@alice_2 hi"
+ alice
+ <### [ WithTime "@alice_2 hi",
+ WithTime "alice_1> hi"
+ ]
+ alice `send` "@alice_1 hey"
+ alice
+ <### [ WithTime "@alice_1 hey",
+ WithTime "alice_2> hey"
+ ]
+ alice @@@ [("@alice_1", "hey"), ("@alice_2", "hey")]
+
+ alice ##> ("/_connect plan 1 " <> cLink)
+ alice <## "contact address: own address"
+
+ alice ##> ("/c " <> cLink)
+ alice <## "alice_2 (Alice): contact already exists"
+
+testPlanAddressConnecting :: HasCallStack => FilePath -> IO ()
+testPlanAddressConnecting tmp = do
+ cLink <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do
+ alice ##> "/ad"
+ getContactLink alice True
+ withNewTestChat tmp "bob" bobProfile $ \bob -> do
+ bob ##> ("/c " <> cLink)
+ bob <## "connection request sent!"
+ withTestChat tmp "alice" $ \alice -> do
+ alice <## "Your address is active! To show: /sa"
+ alice <## "bob (Bob) wants to connect to you!"
+ alice <## "to accept: /ac bob"
+ alice <## "to reject: /rc bob (the sender will NOT be notified)"
+ alice ##> "/ac bob"
+ alice <## "bob (Bob): accepting contact request..."
+ withTestChat tmp "bob" $ \bob -> do
+ threadDelay 500000
+ bob @@@ [("@alice", "")]
+ bob ##> ("/_connect plan 1 " <> cLink)
+ bob <## "contact address: connecting to contact alice"
+
+ bob ##> ("/c " <> cLink)
+ bob <## "contact address: connecting to contact alice"
+
+testPlanAddressContactDeletedReconnected :: HasCallStack => FilePath -> IO ()
+testPlanAddressContactDeletedReconnected =
+ testChat2 aliceProfile bobProfile $
+ \alice bob -> do
+ alice ##> "/ad"
+ cLink <- getContactLink alice True
+
+ bob ##> ("/c " <> cLink)
+ alice <#? bob
+ alice ##> "/ac bob"
+ alice <## "bob (Bob): accepting contact request..."
+ concurrently_
+ (bob <## "alice (Alice): contact is connected")
+ (alice <## "bob (Bob): contact is connected")
+ alice <##> bob
+
+ bob ##> ("/_connect plan 1 " <> cLink)
+ bob <## "contact address: known contact alice"
+ bob <## "use @alice to send messages"
+
+ bob ##> ("/c " <> cLink)
+ bob <## "contact address: known contact alice"
+ bob <## "use @alice to send messages"
+
+ alice ##> "/d bob"
+ alice <## "bob: contact is deleted"
+ bob <## "alice (Alice) deleted contact with you"
+
+ bob ##> ("/_connect plan 1 " <> cLink)
+ bob <## "contact address: ok to connect"
+
+ bob ##> ("/c " <> cLink)
+ bob <## "connection request sent!"
+ alice <## "bob (Bob) wants to connect to you!"
+ alice <## "to accept: /ac bob"
+ alice <## "to reject: /rc bob (the sender will NOT be notified)"
+ alice ##> "/ac bob"
+ alice <## "bob (Bob): accepting contact request..."
+ concurrently_
+ (bob <## "alice_1 (Alice): contact is connected")
+ (alice <## "bob (Bob): contact is connected")
+
+ alice #> "@bob hi"
+ bob <# "alice_1> hi"
+ bob #> "@alice_1 hey"
+ alice <# "bob> hey"
+
+ bob ##> ("/_connect plan 1 " <> cLink)
+ bob <## "contact address: known contact alice_1"
+ bob <## "use @alice_1 to send messages"
+
+ bob ##> ("/c " <> cLink)
+ bob <## "contact address: known contact alice_1"
+ bob <## "use @alice_1 to send messages"
+
testConnectIncognitoInvitationLink :: HasCallStack => FilePath -> IO ()
testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
From 4ecf94dfad04d93a4515a6e0b4c7338a0b8487fc Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Wed, 11 Oct 2023 09:50:11 +0100
Subject: [PATCH 16/80] core: move CLI notifications and active chat to view
layer (for remote CLI) (#3196)
* core: move CLI notifications to view layer (to allow notifications in remote CLI)
* remove unused
* refactor activeTo
* move activeTo to ChatTerminal
* refactor
* move back
* remove extension
---
apps/simplex-bot-advanced/Main.hs | 2 +-
apps/simplex-bot/Main.hs | 2 +-
apps/simplex-broadcast-bot/Main.hs | 2 +-
apps/simplex-chat/Main.hs | 2 +-
apps/simplex-chat/Server.hs | 2 +-
apps/simplex-directory-service/Main.hs | 2 +-
src/Simplex/Chat.hs | 183 +++++++++-------------
src/Simplex/Chat/Controller.hs | 25 +--
src/Simplex/Chat/Core.hs | 6 +-
src/Simplex/Chat/Messages.hs | 6 +-
src/Simplex/Chat/Mobile.hs | 2 +-
src/Simplex/Chat/Store/Profiles.hs | 2 +-
src/Simplex/Chat/Terminal.hs | 8 +-
src/Simplex/Chat/Terminal/Input.hs | 38 +++--
src/Simplex/Chat/Terminal/Notification.hs | 3 +-
src/Simplex/Chat/Terminal/Output.hs | 133 +++++++++++++++-
src/Simplex/Chat/Types.hs | 2 -
src/Simplex/Chat/View.hs | 2 +-
tests/Bots/BroadcastTests.hs | 2 +-
tests/Bots/DirectoryTests.hs | 2 +-
tests/ChatClient.hs | 4 +-
21 files changed, 249 insertions(+), 181 deletions(-)
diff --git a/apps/simplex-bot-advanced/Main.hs b/apps/simplex-bot-advanced/Main.hs
index 04d8e4ffa1..2af76d9961 100644
--- a/apps/simplex-bot-advanced/Main.hs
+++ b/apps/simplex-bot-advanced/Main.hs
@@ -24,7 +24,7 @@ import Text.Read
main :: IO ()
main = do
opts <- welcomeGetOpts
- simplexChatCore terminalChatConfig opts Nothing mySquaringBot
+ simplexChatCore terminalChatConfig opts mySquaringBot
welcomeGetOpts :: IO ChatOpts
welcomeGetOpts = do
diff --git a/apps/simplex-bot/Main.hs b/apps/simplex-bot/Main.hs
index d4ad9f9079..c24f9c251f 100644
--- a/apps/simplex-bot/Main.hs
+++ b/apps/simplex-bot/Main.hs
@@ -13,7 +13,7 @@ import Text.Read
main :: IO ()
main = do
opts <- welcomeGetOpts
- simplexChatCore terminalChatConfig opts Nothing $
+ simplexChatCore terminalChatConfig opts $
chatBotRepl welcomeMessage $ \_contact msg ->
pure $ case readMaybe msg :: Maybe Integer of
Just n -> msg <> " * " <> msg <> " = " <> show (n * n)
diff --git a/apps/simplex-broadcast-bot/Main.hs b/apps/simplex-broadcast-bot/Main.hs
index 15bb743b56..3130437e0f 100644
--- a/apps/simplex-broadcast-bot/Main.hs
+++ b/apps/simplex-broadcast-bot/Main.hs
@@ -8,4 +8,4 @@ import Simplex.Chat.Terminal (terminalChatConfig)
main :: IO ()
main = do
opts <- welcomeGetOpts
- simplexChatCore terminalChatConfig (mkChatOpts opts) Nothing $ broadcastBot opts
+ simplexChatCore terminalChatConfig (mkChatOpts opts) $ broadcastBot opts
diff --git a/apps/simplex-chat/Main.hs b/apps/simplex-chat/Main.hs
index 8dd02623e2..f5d95e57f0 100644
--- a/apps/simplex-chat/Main.hs
+++ b/apps/simplex-chat/Main.hs
@@ -27,7 +27,7 @@ main = do
welcome opts
t <- withTerminal pure
simplexChatTerminal terminalChatConfig opts t
- else simplexChatCore terminalChatConfig opts Nothing $ \user cc -> do
+ else simplexChatCore terminalChatConfig opts $ \user cc -> do
r <- sendChatCmdStr cc chatCmd
ts <- getCurrentTime
tz <- getCurrentTimeZone
diff --git a/apps/simplex-chat/Server.hs b/apps/simplex-chat/Server.hs
index 6f198340f8..d96350cd29 100644
--- a/apps/simplex-chat/Server.hs
+++ b/apps/simplex-chat/Server.hs
@@ -30,7 +30,7 @@ import UnliftIO.STM
simplexChatServer :: ChatServerConfig -> ChatConfig -> ChatOpts -> IO ()
simplexChatServer srvCfg cfg opts =
- simplexChatCore cfg opts Nothing . const $ runChatServer srvCfg
+ simplexChatCore cfg opts . const $ runChatServer srvCfg
data ChatServerConfig = ChatServerConfig
{ chatPort :: ServiceName,
diff --git a/apps/simplex-directory-service/Main.hs b/apps/simplex-directory-service/Main.hs
index 434e42d851..af9c9dd252 100644
--- a/apps/simplex-directory-service/Main.hs
+++ b/apps/simplex-directory-service/Main.hs
@@ -12,4 +12,4 @@ main :: IO ()
main = do
opts@DirectoryOpts {directoryLog} <- welcomeGetOpts
st <- restoreDirectoryStore directoryLog
- simplexChatCore terminalChatConfig (mkChatOpts opts) Nothing $ directoryService st opts
+ simplexChatCore terminalChatConfig (mkChatOpts opts) $ directoryService st opts
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 6f43f5c0f8..5e2a231c97 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -183,13 +183,11 @@ createChatDatabase filePrefix key confirmMigrations = runExceptT $ do
agentStore <- ExceptT $ createAgentStore (agentStoreFile filePrefix) key confirmMigrations
pure ChatDatabase {chatStore, agentStore}
-newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Maybe (Notification -> IO ()) -> IO ChatController
-newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, tempDir} ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize}, optFilesFolder, showReactions, allowInstantFiles, autoAcceptFileSize} sendToast = do
+newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> IO ChatController
+newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, tempDir} ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize}, optFilesFolder, showReactions, allowInstantFiles, autoAcceptFileSize} = do
let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize}
- sendNotification = fromMaybe (const $ pure ()) sendToast
firstTime = dbNew chatStore
- activeTo <- newTVarIO ActiveNone
currentUser <- newTVarIO user
servers <- agentServers config
smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore
@@ -197,7 +195,6 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
idsDrg <- newTVarIO =<< liftIO drgNew
inputQ <- newTBQueueIO tbqSize
outputQ <- newTBQueueIO tbqSize
- notifyQ <- newTBQueueIO tbqSize
subscriptionMode <- newTVarIO SMSubscribe
chatLock <- newEmptyTMVarIO
sndFiles <- newTVarIO M.empty
@@ -213,7 +210,34 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg
tempDirectory <- newTVarIO tempDir
contactMergeEnabled <- newTVarIO True
- pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, subscriptionMode, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, filesFolder, expireCIThreads, expireCIFlags, cleanupManagerAsync, timedItemThreads, showLiveItems, userXFTPFileConfig, tempDirectory, logFilePath = logFile, contactMergeEnabled}
+ pure
+ ChatController
+ { firstTime,
+ currentUser,
+ smpAgent,
+ agentAsync,
+ chatStore,
+ chatStoreChanged,
+ idsDrg,
+ inputQ,
+ outputQ,
+ subscriptionMode,
+ chatLock,
+ sndFiles,
+ rcvFiles,
+ currentCalls,
+ config,
+ filesFolder,
+ expireCIThreads,
+ expireCIFlags,
+ cleanupManagerAsync,
+ timedItemThreads,
+ showLiveItems,
+ userXFTPFileConfig,
+ tempDirectory,
+ logFilePath = logFile,
+ contactMergeEnabled
+ }
where
configServers :: DefaultAgentServers
configServers =
@@ -260,7 +284,7 @@ startChatController subConns enableExpireCIs startXFTPWorkers = do
readTVarIO s >>= maybe (start s users) (pure . fst)
where
start s users = do
- a1 <- async $ race_ notificationSubscriber agentSubscriber
+ a1 <- async agentSubscriber
a2 <-
if subConns
then Just <$> async (subscribeUsers False users)
@@ -376,7 +400,6 @@ processChatCommand = \case
user <- withStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts
storeServers user smpServers
storeServers user xftpServers
- setActive ActiveNone
atomically . writeTVar u $ Just user
pure $ CRActiveUser user
where
@@ -402,7 +425,6 @@ processChatCommand = \case
user' <- privateGetUser userId'
validateUserPassword user user' viewPwd_
withStoreCtx' (Just "APISetActiveUser, setActiveUser") $ \db -> setActiveUser db userId'
- setActive ActiveNone
let user'' = user' {activeUser = True}
asks currentUser >>= atomically . (`writeTVar` Just user'')
pure $ CRActiveUser user''
@@ -532,7 +554,7 @@ processChatCommand = \case
CTContactConnection -> pure $ chatCmdError (Just user) "not supported"
APIGetChatItems pagination search -> withUser $ \user -> do
chatItems <- withStore $ \db -> getAllChatItems db user pagination search
- pure $ CRChatItems user chatItems
+ pure $ CRChatItems user Nothing chatItems
APIGetChatItemInfo chatRef itemId -> withUser $ \user -> do
(aci@(AChatItem cType dir _ ci), versions) <- withStore $ \db ->
(,) <$> getAChatItem db user chatRef itemId <*> liftIO (getChatItemVersions db itemId)
@@ -546,7 +568,7 @@ processChatCommand = \case
pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses}
APISendMessage (ChatRef cType chatId) live itemTTL (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user@User {userId} -> withChatLock "sendMessage" $ case cType of
CTDirect -> do
- ct@Contact {contactId, localDisplayName = c, contactUsed} <- withStore $ \db -> getContact db user chatId
+ ct@Contact {contactId, contactUsed} <- withStore $ \db -> getContact db user chatId
assertDirectAllowed user MDSnd ct XMsgNew_
unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct
if isVoice mc && not (featureAllowed SCFVoice forUser ct)
@@ -563,7 +585,6 @@ processChatCommand = \case
ci <- saveSndChatItem' user (CDDirectSnd ct) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live
forM_ (timed_ >>= timedDeleteAt') $
startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci)
- setActive $ ActiveC c
pure $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci)
where
setupSndFileTransfer :: Contact -> m (Maybe (FileInvitation, CIFile 'MDSnd, FileTransferMeta))
@@ -614,7 +635,7 @@ processChatCommand = \case
assertUserGroupRole gInfo GRAuthor
send g
where
- send g@(Group gInfo@GroupInfo {groupId, membership, localDisplayName = gName} ms)
+ send g@(Group gInfo@GroupInfo {groupId, membership} ms)
| isVoice mc && not (groupFeatureAllowed SGFVoice gInfo) = notAllowedError GFVoice
| not (isVoice mc) && isJust file_ && not (groupFeatureAllowed SGFFiles gInfo) = notAllowedError GFFiles
| otherwise = do
@@ -629,7 +650,6 @@ processChatCommand = \case
createGroupSndStatus db (chatItemId' ci) groupMemberId CISSndNew
forM_ (timed_ >>= timedDeleteAt') $
startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci)
- setActive $ ActiveG gName
pure $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci)
notAllowedError f = pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText f))
setupSndFileTransfer :: Group -> Int -> m (Maybe (FileInvitation, CIFile 'MDSnd, FileTransferMeta))
@@ -734,7 +754,7 @@ processChatCommand = \case
unzipMaybe3 _ = (Nothing, Nothing, Nothing)
APIUpdateChatItem (ChatRef cType chatId) itemId live mc -> withUser $ \user -> withChatLock "updateChatItem" $ case cType of
CTDirect -> do
- (ct@Contact {contactId, localDisplayName = c}, cci) <- withStore $ \db -> (,) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId
+ (ct@Contact {contactId}, cci) <- withStore $ \db -> (,) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId
assertDirectAllowed user MDSnd ct XMsgUpdate_
case cci of
CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do
@@ -750,13 +770,12 @@ processChatCommand = \case
addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc)
updateDirectChatItem' db user contactId ci (CISndMsgContent mc) live $ Just msgId
startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci'
- setActive $ ActiveC c
pure $ CRChatItemUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci')
else pure $ CRChatItemNotChanged user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci)
_ -> throwChatError CEInvalidChatItemUpdate
CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate
CTGroup -> do
- Group gInfo@GroupInfo {groupId, localDisplayName = gName} ms <- withStore $ \db -> getGroup db user chatId
+ Group gInfo@GroupInfo {groupId} ms <- withStore $ \db -> getGroup db user chatId
assertUserGroupRole gInfo GRAuthor
cci <- withStore $ \db -> getGroupChatItem db user chatId itemId
case cci of
@@ -773,7 +792,6 @@ processChatCommand = \case
addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc)
updateGroupChatItem db user groupId ci (CISndMsgContent mc) live $ Just msgId
startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci'
- setActive $ ActiveG gName
pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci')
else pure $ CRChatItemNotChanged user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci)
_ -> throwChatError CEInvalidChatItemUpdate
@@ -782,13 +800,12 @@ processChatCommand = \case
CTContactConnection -> pure $ chatCmdError (Just user) "not supported"
APIDeleteChatItem (ChatRef cType chatId) itemId mode -> withUser $ \user -> withChatLock "deleteChatItem" $ case cType of
CTDirect -> do
- (ct@Contact {localDisplayName = c}, ci@(CChatItem msgDir ChatItem {meta = CIMeta {itemSharedMsgId, editable}})) <- withStore $ \db -> (,) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId
+ (ct, ci@(CChatItem msgDir ChatItem {meta = CIMeta {itemSharedMsgId, editable}})) <- withStore $ \db -> (,) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId
case (mode, msgDir, itemSharedMsgId, editable) of
(CIDMInternal, _, _, _) -> deleteDirectCI user ct ci True False
(CIDMBroadcast, SMDSnd, Just itemSharedMId, True) -> do
assertDirectAllowed user MDSnd ct XMsgDel_
(SndMessage {msgId}, _) <- sendDirectContactMessage ct (XMsgDel itemSharedMId Nothing)
- setActive $ ActiveC c
if featureAllowed SCFFullDelete forUser ct
then deleteDirectCI user ct ci True False
else markDirectCIDeleted user ct ci msgId True =<< liftIO getCurrentTime
@@ -898,7 +915,7 @@ processChatCommand = \case
_ -> pure $ chatCmdError (Just user) "not supported"
APIDeleteChat (ChatRef cType chatId) notify -> withUser $ \user@User {userId} -> case cType of
CTDirect -> do
- ct@Contact {localDisplayName} <- withStore $ \db -> getContact db user chatId
+ ct <- withStore $ \db -> getContact db user chatId
filesInfo <- withStore' $ \db -> getContactFileInfo db user ct
withChatLock "deleteChat direct" . procCmd $ do
deleteFilesAndConns user filesInfo
@@ -910,7 +927,6 @@ processChatCommand = \case
-- (possibly, race condition on integrity check?)
withStore' $ \db -> deleteContactConnectionsAndFiles db userId ct
withStore' $ \db -> deleteContact db user ct
- unsetActive $ ActiveC localDisplayName
pure $ CRContactDeleted user ct
CTContactConnection -> withChatLock "deleteChat contactConnection" . procCmd $ do
conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withStore $ \db -> getPendingContactConnection db userId chatId
@@ -1698,11 +1714,10 @@ processChatCommand = \case
LastMessages (Just chatName) count search -> withUser $ \user -> do
chatRef <- getChatRef user chatName
chatResp <- processChatCommand $ APIGetChat chatRef (CPLast count) search
- setActive $ chatActiveTo chatName
- pure $ CRChatItems user (aChatItems . chat $ chatResp)
+ pure $ CRChatItems user (Just chatName) (aChatItems . chat $ chatResp)
LastMessages Nothing count search -> withUser $ \user -> do
chatItems <- withStore $ \db -> getAllChatItems db user (CPLast count) search
- pure $ CRChatItems user chatItems
+ pure $ CRChatItems user Nothing chatItems
LastChatItemId (Just chatName) index -> withUser $ \user -> do
chatRef <- getChatRef user chatName
chatResp <- processChatCommand (APIGetChat chatRef (CPLast $ index + 1) Nothing)
@@ -1714,10 +1729,10 @@ processChatCommand = \case
chatItem <- withStore $ \db -> do
chatRef <- getChatRefViaItemId db user itemId
getAChatItem db user chatRef itemId
- pure $ CRChatItems user ((: []) chatItem)
+ pure $ CRChatItems user Nothing ((: []) chatItem)
ShowChatItem Nothing -> withUser $ \user -> do
chatItems <- withStore $ \db -> getAllChatItems db user (CPLast 1) Nothing
- pure $ CRChatItems user chatItems
+ pure $ CRChatItems user Nothing chatItems
ShowChatItemInfo chatName msg -> withUser $ \user -> do
chatRef <- getChatRef user chatName
itemId <- getChatItemIdByText user chatRef msg
@@ -2059,8 +2074,7 @@ processChatCommand = \case
when (memberRemoved membership) $ throwChatError CEGroupMemberUserRemoved
unless (memberActive membership) $ throwChatError CEGroupMemberNotActive
delGroupChatItem :: User -> GroupInfo -> CChatItem 'CTGroup -> MessageId -> Maybe GroupMember -> m ChatResponse
- delGroupChatItem user gInfo@GroupInfo {localDisplayName = gName} ci msgId byGroupMember = do
- setActive $ ActiveG gName
+ delGroupChatItem user gInfo ci msgId byGroupMember = do
deletedTs <- liftIO getCurrentTime
if groupFeatureAllowed SGFFullDelete gInfo
then deleteGroupCI user gInfo ci True False byGroupMember deletedTs
@@ -2117,7 +2131,6 @@ processChatCommand = \case
let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole
ci <- saveSndChatItem user (CDDirectSnd ct) msg content
toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci)
- setActive $ ActiveG localDisplayName
sndContactCITimed :: Bool -> Contact -> Maybe Int -> m (Maybe CITimed)
sndContactCITimed live = sndCITimed_ live . contactTimedTTL
sndGroupCITimed :: Bool -> GroupInfo -> Maybe Int -> m (Maybe CITimed)
@@ -2167,7 +2180,6 @@ processChatCommand = \case
users <- withStore' getUsers
unless (length users > 1 && (isJust (viewPwdHash user) || length (filter (isNothing . viewPwdHash) users) > 1)) $
throwChatError (CECantDeleteLastUser userId)
- setActive ActiveNone
deleteChatUser :: User -> Bool -> m ChatResponse
deleteChatUser user delSMPQueues = do
filesInfo <- withStore' (`getUserFileInfo` user)
@@ -2867,17 +2879,16 @@ processAgentMessageNoConn :: forall m. ChatMonad m => ACommand 'Agent 'AENone ->
processAgentMessageNoConn = \case
CONNECT p h -> hostEvent $ CRHostConnected p h
DISCONNECT p h -> hostEvent $ CRHostDisconnected p h
- DOWN srv conns -> serverEvent srv conns CRContactsDisconnected "disconnected"
- UP srv conns -> serverEvent srv conns CRContactsSubscribed "connected"
+ DOWN srv conns -> serverEvent srv conns CRContactsDisconnected
+ UP srv conns -> serverEvent srv conns CRContactsSubscribed
SUSPENDED -> toView CRChatSuspended
DEL_USER agentUserId -> toView $ CRAgentUserDeleted agentUserId
where
hostEvent :: ChatResponse -> m ()
hostEvent = whenM (asks $ hostEvents . config) . toView
- serverEvent srv@(SMPServer host _ _) conns event str = do
- cs <- withStore' $ \db -> getConnectionsContacts db conns
+ serverEvent srv conns event = do
+ cs <- withStore' (`getConnectionsContacts` conns)
toView $ event srv cs
- showToast ("server " <> str) (safeDecodeUtf8 $ strEncode host)
processAgentMsgSndFile :: forall m. ChatMonad m => ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> m ()
processAgentMsgSndFile _corrId aFileId msg =
@@ -3014,10 +3025,7 @@ processAgentMsgRcvFile _corrId aFileId msg =
processAgentMessageConn :: forall m. ChatMonad m => User -> ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> m ()
processAgentMessageConn user _ agentConnId END =
withStore (\db -> getConnectionEntity db user $ AgentConnId agentConnId) >>= \case
- RcvDirectMsgConnection _ (Just ct@Contact {localDisplayName = c}) -> do
- toView $ CRContactAnotherClient user ct
- whenUserNtfs user $ showToast (c <> "> ") "connected to another client"
- unsetActive $ ActiveC c
+ RcvDirectMsgConnection _ (Just ct) -> toView $ CRContactAnotherClient user ct
entity -> toView $ CRSubscriptionEnd user entity
processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
entity <- withStore (\db -> getConnectionEntity db user $ AgentConnId agentConnId) >>= updateConnStatus
@@ -3084,7 +3092,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
-- TODO add debugging output
_ -> pure ()
- Just ct@Contact {localDisplayName = c, contactId} -> case agentMsg of
+ Just ct@Contact {contactId} -> case agentMsg of
INV (ACR _ cReq) ->
-- [async agent commands] XGrpMemIntro continuation on receiving INV
withCompletedCommand conn agentMsg $ \_ ->
@@ -3169,9 +3177,6 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
toView $ CRContactConnected user ct (fmap fromLocalProfile incognitoProfile)
when (directOrUsed ct) $ createFeatureEnabledItems ct
- whenUserNtfs user $ do
- setActive $ ActiveC c
- showToast (c <> "> ") "connected"
when (contactConnInitiated conn) $ do
let Connection {groupLinkId} = conn
doProbeContacts = isJust groupLinkId
@@ -3248,7 +3253,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
_ -> pure ()
processGroupMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> m ()
- processGroupMessage agentMsg connEntity conn@Connection {connId, connectionCode} gInfo@GroupInfo {groupId, localDisplayName = gName, groupProfile, membership, chatSettings} m = case agentMsg of
+ processGroupMessage agentMsg connEntity conn@Connection {connId, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of
INV (ACR _ cReq) ->
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} ->
case cReq of
@@ -3338,15 +3343,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
let GroupInfo {groupProfile = GroupProfile {description}} = gInfo
memberConnectedChatItem gInfo m
forM_ description $ groupDescriptionChatItem gInfo m
- whenUserNtfs user $ do
- setActive $ ActiveG gName
- showToast ("#" <> gName) "you are connected to group"
GCInviteeMember -> do
memberConnectedChatItem gInfo m
toView $ CRJoinedGroupMember user gInfo m {memberStatus = GSMemConnected}
- whenGroupNtfs user gInfo $ do
- setActive $ ActiveG gName
- showToast ("#" <> gName) $ "member " <> m.localDisplayName <> " is connected"
intros <- withStore' $ \db -> createIntroductions db members m
void . sendGroupMessage user gInfo members . XGrpMemNew $ memberInfo m
forM_ intros $ \intro ->
@@ -3642,7 +3641,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
profileContactRequest invId chatVRange p xContactId_ = do
withStore (\db -> createOrUpdateContactRequest db user userContactLinkId invId chatVRange p xContactId_) >>= \case
CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact
- CORRequest cReq@UserContactRequest {localDisplayName} -> do
+ CORRequest cReq -> do
withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case
Just (UserContactLink {autoAccept}, groupId_, _) ->
case autoAccept of
@@ -3657,10 +3656,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo
ct <- acceptContactRequestAsync user cReq profileMode
toView $ CRAcceptingGroupJoinRequest user gInfo ct
- _ -> do
- toView $ CRReceivedContactRequest user cReq
- whenUserNtfs user $
- showToast (localDisplayName <> "> ") "wants to connect to you"
+ _ -> toView $ CRReceivedContactRequest user cReq
_ -> pure ()
incAuthErrCounter :: ConnectionEntity -> Connection -> AgentErrorType -> m ()
@@ -3751,13 +3747,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvMsgContent $ MCText descr) Nothing
notifyMemberConnected :: GroupInfo -> GroupMember -> Maybe Contact -> m ()
- notifyMemberConnected gInfo m@GroupMember {localDisplayName = c} ct_ = do
+ notifyMemberConnected gInfo m ct_ = do
memberConnectedChatItem gInfo m
toView $ CRConnectedToGroupMember user gInfo m ct_
- let g = groupName' gInfo
- whenGroupNtfs user gInfo $ do
- setActive $ ActiveG g
- showToast ("#" <> g) $ "member " <> c <> " is connected"
probeMatchingContactsAndMembers :: Contact -> IncognitoEnabled -> Bool -> m ()
probeMatchingContactsAndMembers ct connectedIncognito doProbeContacts = do
@@ -3819,7 +3811,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
messageError = toView . CRMessageError user "error"
newContentMessage :: Contact -> MsgContainer -> RcvMessage -> MsgMeta -> m ()
- newContentMessage ct@Contact {localDisplayName = c, contactUsed} mc msg@RcvMessage {sharedMsgId_} msgMeta = do
+ newContentMessage ct@Contact {contactUsed} mc msg@RcvMessage {sharedMsgId_} msgMeta = do
unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
let ExtMsgContent content fInv_ _ _ = mcExtMsgContent mc
@@ -3832,23 +3824,18 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
if isVoice content && not (featureAllowed SCFVoice forContact ct)
then do
void $ newChatItem (CIRcvChatFeatureRejected CFVoice) Nothing Nothing False
- setActive $ ActiveC c
else do
let ExtMsgContent _ _ itemTTL live_ = mcExtMsgContent mc
timed_ = rcvContactCITimed ct itemTTL
live = fromMaybe False live_
file_ <- processFileInvitation fInv_ content $ \db -> createRcvFileTransfer db userId ct
- ChatItem {formattedText} <- newChatItem (CIRcvMsgContent content) (snd <$> file_) timed_ live
+ newChatItem (CIRcvMsgContent content) (snd <$> file_) timed_ live
autoAcceptFile file_
- whenContactNtfs user ct $ do
- showMsgToast (c <> "> ") content formattedText
- setActive $ ActiveC c
where
newChatItem ciContent ciFile_ timed_ live = do
ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ msgMeta ciContent ciFile_ timed_ live
reactions <- maybe (pure []) (\sharedMsgId -> withStore' $ \db -> getDirectCIReactions db ct sharedMsgId) sharedMsgId_
toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci {reactions})
- pure ci
autoAcceptFile :: Maybe (RcvFileTransfer, CIFile 'MDRcv) -> m ()
autoAcceptFile = mapM_ $ \(ft, CIFile {fileSize}) -> do
@@ -3907,7 +3894,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
pure (ft, CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol})
messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m ()
- messageUpdate ct@Contact {contactId, localDisplayName = c} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do
+ messageUpdate ct@Contact {contactId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
updateRcvChatItem `catchCINotFound` \_ -> do
-- This patches initial sharedMsgId into chat item when locally deleted chat item
@@ -3919,7 +3906,6 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
createChatItemVersion db (chatItemId' ci) brokerTs mc
updateDirectChatItem' db user contactId ci content live Nothing
toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci')
- setActive $ ActiveC c
where
MsgMeta {broker = (_, brokerTs)} = msgMeta
content = CIRcvMsgContent mc
@@ -4006,7 +3992,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
e -> throwError e
newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> MsgMeta -> m ()
- newGroupContentMessage gInfo m@GroupMember {localDisplayName = c, memberId, memberRole} mc msg@RcvMessage {sharedMsgId_} msgMeta
+ newGroupContentMessage gInfo m@GroupMember {memberId, memberRole} mc msg@RcvMessage {sharedMsgId_} msgMeta
| isVoice content && not (groupFeatureAllowed SGFVoice gInfo) = rejected GFVoice
| not (isVoice content) && isJust fInv_ && not (groupFeatureAllowed SGFFiles gInfo) = rejected GFFiles
| otherwise = do
@@ -4036,20 +4022,15 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
toView cr
createItem timed_ live = do
file_ <- processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId m
- ChatItem {formattedText} <- newChatItem (CIRcvMsgContent content) (snd <$> file_) timed_ live
+ newChatItem (CIRcvMsgContent content) (snd <$> file_) timed_ live
autoAcceptFile file_
- let g = groupName' gInfo
- whenGroupNtfs user gInfo $ do
- showMsgToast ("#" <> g <> " " <> c <> "> ") content formattedText
- setActive $ ActiveG g
newChatItem ciContent ciFile_ timed_ live = do
ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ msgMeta ciContent ciFile_ timed_ live
reactions <- maybe (pure []) (\sharedMsgId -> withStore' $ \db -> getGroupCIReactions db gInfo memberId sharedMsgId) sharedMsgId_
groupMsgToView gInfo m ci {reactions} msgMeta
- pure ci
groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m ()
- groupMessageUpdate gInfo@GroupInfo {groupId, localDisplayName = g} m@GroupMember {groupMemberId, memberId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl_ live_ =
+ groupMessageUpdate gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId, memberId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl_ live_ =
updateRcvChatItem `catchCINotFound` \_ -> do
-- This patches initial sharedMsgId into chat item when locally deleted chat item
-- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete).
@@ -4060,7 +4041,6 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
createChatItemVersion db (chatItemId' ci) brokerTs mc
updateGroupChatItem db user groupId ci content live Nothing
toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci')
- setActive $ ActiveG g
where
MsgMeta {broker = (_, brokerTs)} = msgMeta
content = CIRcvMsgContent mc
@@ -4079,7 +4059,6 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc)
updateGroupChatItem db user groupId ci content live $ Just msgId
toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci')
- setActive $ ActiveG g
startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci'
else toView $ CRChatItemNotChanged user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci)
else messageError "x.msg.update: group member attempted to update a message of another member"
@@ -4115,7 +4094,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
-- TODO remove once XFile is discontinued
processFileInvitation' :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> m ()
- processFileInvitation' ct@Contact {localDisplayName = c} fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} msgMeta = do
+ processFileInvitation' ct fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} msgMeta = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
ChatConfig {fileChunkSize} <- asks config
inline <- receiveInlineMode fInv Nothing fileChunkSize
@@ -4124,13 +4103,10 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol}
ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ msgMeta (CIRcvMsgContent $ MCFile "") ciFile Nothing False
toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci)
- whenContactNtfs user ct $ do
- showToast (c <> "> ") "wants to send a file"
- setActive $ ActiveC c
-- TODO remove once XFile is discontinued
processGroupFileInvitation' :: GroupInfo -> GroupMember -> FileInvitation -> RcvMessage -> MsgMeta -> m ()
- processGroupFileInvitation' gInfo m@GroupMember {localDisplayName = c} fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} msgMeta = do
+ processGroupFileInvitation' gInfo m fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} msgMeta = do
ChatConfig {fileChunkSize} <- asks config
inline <- receiveInlineMode fInv Nothing fileChunkSize
RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvGroupFileTransfer db userId m fInv inline fileChunkSize
@@ -4138,10 +4114,6 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol}
ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ msgMeta (CIRcvMsgContent $ MCFile "") ciFile Nothing False
groupMsgToView gInfo m ci msgMeta
- let g = groupName' gInfo
- whenGroupNtfs user gInfo $ do
- showToast ("#" <> g <> " " <> c <> "> ") "wants to send a file"
- setActive $ ActiveG g
receiveInlineMode :: FileInvitation -> Maybe MsgContent -> Integer -> m (Maybe InlineFileMode)
receiveInlineMode FileInvitation {fileSize, fileInline, fileDescr} mc_ chSize = case (fileInline, fileDescr) of
@@ -4322,8 +4294,6 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci)
toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci)
toView $ CRReceivedGroupInvitation {user, groupInfo = gInfo, contact = ct, fromMemberRole = fromRole, memberRole = memRole}
- whenContactNtfs user ct $
- showToast ("#" <> localDisplayName <> " " <> c <> "> ") "invited you to join the group"
where
sameGroupLinkId :: Maybe GroupLinkId -> Maybe GroupLinkId -> Bool
sameGroupLinkId (Just gli) (Just gli') = gli == gli'
@@ -5470,29 +5440,20 @@ getCreateActiveUser st testView = do
getWithPrompt :: String -> IO String
getWithPrompt s = putStr (s <> ": ") >> hFlush stdout >> getLine
-whenUserNtfs :: ChatMonad' m => User -> m () -> m ()
-whenUserNtfs User {showNtfs, activeUser} = when $ showNtfs || activeUser
+userNtf :: User -> Bool
+userNtf User {showNtfs, activeUser} = showNtfs || activeUser
-whenContactNtfs :: ChatMonad' m => User -> Contact -> m () -> m ()
-whenContactNtfs user Contact {chatSettings} = whenUserNtfs user . when (enableNtfs chatSettings)
+chatNtf :: User -> ChatInfo c -> Bool
+chatNtf user = \case
+ DirectChat ct -> contactNtf user ct
+ GroupChat g -> groupNtf user g
+ _ -> False
-whenGroupNtfs :: ChatMonad' m => User -> GroupInfo -> m () -> m ()
-whenGroupNtfs user GroupInfo {chatSettings} = whenUserNtfs user . when (enableNtfs chatSettings)
+contactNtf :: User -> Contact -> Bool
+contactNtf user Contact {chatSettings} = userNtf user && enableNtfs chatSettings
-showMsgToast :: ChatMonad' m => Text -> MsgContent -> Maybe MarkdownList -> m ()
-showMsgToast from mc md_ = showToast from $ maybe (msgContentText mc) (mconcat . map hideSecret) md_
- where
- hideSecret :: FormattedText -> Text
- hideSecret FormattedText {format = Just Secret} = "..."
- hideSecret FormattedText {text} = text
-
-showToast :: ChatMonad' m => Text -> Text -> m ()
-showToast title text = atomically . (`writeTBQueue` Notification {title, text}) =<< asks notifyQ
-
-notificationSubscriber :: ChatMonad' m => m ()
-notificationSubscriber = do
- ChatController {notifyQ, sendNotification} <- ask
- forever $ atomically (readTBQueue notifyQ) >>= liftIO . sendNotification
+groupNtf :: User -> GroupInfo -> Bool
+groupNtf user GroupInfo {chatSettings} = userNtf user && enableNtfs chatSettings
withUser' :: ChatMonad m => (User -> m ChatResponse) -> m ChatResponse
withUser' action =
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index 3466371da8..d97d59c5c5 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -34,8 +34,7 @@ import Data.List.NonEmpty (NonEmpty)
import Data.Map.Strict (Map)
import Data.String
import Data.Text (Text)
-import Data.Time (NominalDiffTime)
-import Data.Time.Clock (UTCTime)
+import Data.Time (NominalDiffTime, UTCTime)
import Data.Version (showVersion)
import GHC.Generics (Generic)
import Language.Haskell.TH (Exp, Q, runIO)
@@ -153,20 +152,10 @@ defaultInlineFilesConfig =
receiveInstant = True -- allow receiving instant files, within receiveChunks limit
}
-data ActiveTo = ActiveNone | ActiveC ContactName | ActiveG GroupName
- deriving (Eq)
-
-chatActiveTo :: ChatName -> ActiveTo
-chatActiveTo (ChatName cType name) = case cType of
- CTDirect -> ActiveC name
- CTGroup -> ActiveG name
- _ -> ActiveNone
-
data ChatDatabase = ChatDatabase {chatStore :: SQLiteStore, agentStore :: SQLiteStore}
data ChatController = ChatController
{ currentUser :: TVar (Maybe User),
- activeTo :: TVar ActiveTo,
firstTime :: Bool,
smpAgent :: AgentClient,
agentAsync :: TVar (Maybe (Async (), Maybe (Async ()))),
@@ -175,8 +164,6 @@ data ChatController = ChatController
idsDrg :: TVar ChaChaDRG,
inputQ :: TBQueue String,
outputQ :: TBQueue (Maybe CorrId, ChatResponse),
- notifyQ :: TBQueue Notification,
- sendNotification :: Notification -> IO (),
subscriptionMode :: TVar SubscriptionMode,
chatLock :: Lock,
sndFiles :: TVar (Map Int64 Handle),
@@ -433,7 +420,7 @@ data ChatResponse
| CRApiChats {user :: User, chats :: [AChat]}
| CRChats {chats :: [AChat]}
| CRApiChat {user :: User, chat :: AChat}
- | CRChatItems {user :: User, chatItems :: [AChatItem]}
+ | CRChatItems {user :: User, chatName_ :: Maybe ChatName, chatItems :: [AChatItem]}
| CRChatItemInfo {user :: User, chatItem :: AChatItem, chatItemInfo :: ChatItemInfo}
| CRChatItemId User (Maybe ChatItemId)
| CRApiParsedMarkdown {formattedText :: Maybe MarkdownList}
@@ -1074,14 +1061,6 @@ mkChatError = ChatError . CEException . show
chatCmdError :: Maybe User -> String -> ChatResponse
chatCmdError user = CRChatCmdError user . ChatError . CECommandError
-setActive :: (MonadUnliftIO m, MonadReader ChatController m) => ActiveTo -> m ()
-setActive to = asks activeTo >>= atomically . (`writeTVar` to)
-
-unsetActive :: (MonadUnliftIO m, MonadReader ChatController m) => ActiveTo -> m ()
-unsetActive a = asks activeTo >>= atomically . (`modifyTVar` unset)
- where
- unset a' = if a == a' then ActiveNone else a'
-
toView :: ChatMonad' m => ChatResponse -> m ()
toView event = do
q <- asks outputQ
diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs
index 4af161ab41..870779cfda 100644
--- a/src/Simplex/Chat/Core.hs
+++ b/src/Simplex/Chat/Core.hs
@@ -14,8 +14,8 @@ import Simplex.Chat.Types
import System.Exit (exitFailure)
import UnliftIO.Async
-simplexChatCore :: ChatConfig -> ChatOpts -> Maybe (Notification -> IO ()) -> (User -> ChatController -> IO ()) -> IO ()
-simplexChatCore cfg@ChatConfig {confirmMigrations, testView} opts@ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, dbKey, logAgent}} sendToast chat =
+simplexChatCore :: ChatConfig -> ChatOpts -> (User -> ChatController -> IO ()) -> IO ()
+simplexChatCore cfg@ChatConfig {confirmMigrations, testView} opts@ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, dbKey, logAgent}} chat =
case logAgent of
Just level -> do
setLogLevel level
@@ -28,7 +28,7 @@ simplexChatCore cfg@ChatConfig {confirmMigrations, testView} opts@ChatOpts {core
exitFailure
run db@ChatDatabase {chatStore} = do
u <- getCreateActiveUser chatStore testView
- cc <- newChatController db (Just u) cfg opts sendToast
+ cc <- newChatController db (Just u) cfg opts
runSimplexChat opts u cc chat
runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController -> IO ()) -> IO ()
diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs
index 79463d2107..3831fad03e 100644
--- a/src/Simplex/Chat/Messages.hs
+++ b/src/Simplex/Chat/Messages.hs
@@ -50,8 +50,10 @@ import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
data ChatType = CTDirect | CTGroup | CTContactRequest | CTContactConnection
deriving (Eq, Show, Ord, Generic)
-data ChatName = ChatName ChatType Text
- deriving (Show)
+data ChatName = ChatName {chatType :: ChatType, chatName :: Text}
+ deriving (Show, Generic)
+
+instance ToJSON ChatName where toEncoding = J.genericToEncoding J.defaultOptions
chatTypeStr :: ChatType -> String
chatTypeStr = \case
diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs
index 3d841f18c4..0a970d2c8e 100644
--- a/src/Simplex/Chat/Mobile.hs
+++ b/src/Simplex/Chat/Mobile.hs
@@ -196,7 +196,7 @@ chatMigrateInit dbFilePrefix dbKey confirm = runExceptT $ do
where
initialize st db = do
user_ <- getActiveUser_ st
- newChatController db user_ defaultMobileConfig (mobileChatOpts dbFilePrefix dbKey) Nothing
+ newChatController db user_ defaultMobileConfig (mobileChatOpts dbFilePrefix dbKey)
migrate createStore dbFile confirmMigrations =
ExceptT $
(first (DBMErrorMigration dbFile) <$> createStore dbFile dbKey confirmMigrations)
diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs
index 5b5a6eb671..fa573e4e60 100644
--- a/src/Simplex/Chat/Store/Profiles.hs
+++ b/src/Simplex/Chat/Store/Profiles.hs
@@ -87,7 +87,7 @@ import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SubscriptionMode)
import Simplex.Messaging.Transport.Client (TransportHost)
-import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8)
+import Simplex.Messaging.Util (safeDecodeUtf8)
createUserRecord :: DB.Connection -> AgentUserId -> Profile -> Bool -> ExceptT StoreError IO User
createUserRecord db auId p activeUser = createUserRecordAt db auId p activeUser =<< liftIO getCurrentTime
diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs
index 0ef3d3bace..68aaa51318 100644
--- a/src/Simplex/Chat/Terminal.hs
+++ b/src/Simplex/Chat/Terminal.hs
@@ -15,7 +15,6 @@ import Simplex.Chat.Core
import Simplex.Chat.Help (chatWelcome)
import Simplex.Chat.Options
import Simplex.Chat.Terminal.Input
-import Simplex.Chat.Terminal.Notification
import Simplex.Chat.Terminal.Output
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
import Simplex.Messaging.Client (defaultNetworkConfig)
@@ -40,10 +39,9 @@ terminalChatConfig =
}
simplexChatTerminal :: WithTerminal t => ChatConfig -> ChatOpts -> t -> IO ()
-simplexChatTerminal cfg opts t = do
- sendToast <- if muteNotifications opts then pure Nothing else Just <$> initializeNotifications
- handle checkDBKeyError . simplexChatCore cfg opts sendToast $ \u cc -> do
- ct <- newChatTerminal t
+simplexChatTerminal cfg opts t =
+ handle checkDBKeyError . simplexChatCore cfg opts $ \u cc -> do
+ ct <- newChatTerminal t opts
when (firstTime cc) . printToTerminal ct $ chatWelcome u
runChatTerminal ct cc
diff --git a/src/Simplex/Chat/Terminal/Input.hs b/src/Simplex/Chat/Terminal/Input.hs
index 8841f15ffd..0fd95cf680 100644
--- a/src/Simplex/Chat/Terminal/Input.hs
+++ b/src/Simplex/Chat/Terminal/Input.hs
@@ -57,14 +57,26 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
cmd = parseChatCommand bs
unless (isMessage cmd) $ echo s
r <- runReaderT (execChatCommand bs) cc
- case r of
- CRChatCmdError _ _ -> when (isMessage cmd) $ echo s
- CRChatError _ _ -> when (isMessage cmd) $ echo s
- _ -> pure ()
+ processResp s cmd r
printRespToTerminal ct cc False r
startLiveMessage cmd r
where
echo s = printToTerminal ct [plain s]
+ processResp s cmd = \case
+ CRActiveUser _ -> setActive ct ""
+ CRChatItems u chatName_ _ -> whenCurrUser cc u $ mapM_ (setActive ct . chatActiveTo) chatName_
+ CRNewChatItem u (AChatItem _ SMDSnd cInfo _) -> whenCurrUser cc u $ setActiveChat ct cInfo
+ CRChatItemUpdated u (AChatItem _ SMDSnd cInfo _) -> whenCurrUser cc u $ setActiveChat ct cInfo
+ CRChatItemDeleted u (AChatItem _ _ cInfo _) _ _ _ -> whenCurrUser cc u $ setActiveChat ct cInfo
+ CRContactDeleted u c -> whenCurrUser cc u $ unsetActiveContact ct c
+ CRGroupDeletedUser u g -> whenCurrUser cc u $ unsetActiveGroup ct g
+ CRSentGroupInvitation u g _ _ -> whenCurrUser cc u $ setActiveGroup ct g
+ CRChatCmdError _ _ -> when (isMessage cmd) $ echo s
+ CRChatError _ _ -> when (isMessage cmd) $ echo s
+ CRCmdOk _ -> case cmd of
+ Right APIDeleteUser {} -> setActive ct ""
+ _ -> pure ()
+ _ -> pure ()
isMessage = \case
Right SendMessage {} -> True
Right SendLiveMessage {} -> True
@@ -134,7 +146,7 @@ runTerminalInput ct cc = withChatTerm ct $ do
receiveFromTTY cc ct
receiveFromTTY :: forall m. MonadTerminal m => ChatController -> ChatTerminal -> m ()
-receiveFromTTY cc@ChatController {inputQ, activeTo, currentUser, chatStore} ct@ChatTerminal {termSize, termState, liveMessageState} =
+receiveFromTTY cc@ChatController {inputQ, currentUser, chatStore} ct@ChatTerminal {termSize, termState, liveMessageState, activeTo} =
forever $ getKey >>= liftIO . processKey >> withTermLock ct (updateInput ct)
where
processKey :: (Key, Modifiers) -> IO ()
@@ -153,11 +165,11 @@ receiveFromTTY cc@ChatController {inputQ, activeTo, currentUser, chatStore} ct@C
when (inputString ts /= "" || isLive) $
atomically (submitInput live ts) >>= mapM_ (uncurry endLiveMessage)
update key = do
- ac <- readTVarIO activeTo
+ chatPrefix <- readTVarIO activeTo
live <- isJust <$> readTVarIO liveMessageState
ts <- readTVarIO termState
user_ <- readTVarIO currentUser
- ts' <- updateTermState user_ chatStore ac live (width termSize) key ts
+ ts' <- updateTermState user_ chatStore chatPrefix live (width termSize) key ts
atomically $ writeTVar termState $! ts'
endLiveMessage :: String -> LiveMessage -> IO ()
@@ -203,8 +215,8 @@ data AutoComplete
| ACCommand Text
| ACNone
-updateTermState :: Maybe User -> SQLiteStore -> ActiveTo -> Bool -> Int -> (Key, Modifiers) -> TerminalState -> IO TerminalState
-updateTermState user_ st ac live tw (key, ms) ts@TerminalState {inputString = s, inputPosition = p, autoComplete = acp} = case key of
+updateTermState :: Maybe User -> SQLiteStore -> String -> Bool -> Int -> (Key, Modifiers) -> TerminalState -> IO TerminalState
+updateTermState user_ st chatPrefix live tw (key, ms) ts@TerminalState {inputString = s, inputPosition = p, autoComplete = acp} = case key of
CharKey c
| ms == mempty || ms == shiftKey -> pure $ insertChars $ charsWithContact [c]
| ms == altKey && c == 'b' -> pure $ setPosition prevWordPos
@@ -326,17 +338,13 @@ updateTermState user_ st ac live tw (key, ms) ts@TerminalState {inputString = s,
charsWithContact cs
| live = cs
| null s && cs /= "@" && cs /= "#" && cs /= "/" && cs /= ">" && cs /= "\\" && cs /= "!" && cs /= "+" && cs /= "-" =
- contactPrefix <> cs
+ chatPrefix <> cs
| (s == ">" || s == "\\" || s == "!") && cs == " " =
- cs <> contactPrefix
+ cs <> chatPrefix
| otherwise = cs
insertChars = ts' . if p >= length s then append else insert
append cs = let s' = s <> cs in (s', length s')
insert cs = let (b, a) = splitAt p s in (b <> cs <> a, p + length cs)
- contactPrefix = case ac of
- ActiveNone -> ""
- ActiveC c -> "@" <> T.unpack c <> " "
- ActiveG g -> "#" <> T.unpack g <> " "
backDeleteChar
| p == 0 || null s = ts
| p >= length s = ts' (init s, length s - 1)
diff --git a/src/Simplex/Chat/Terminal/Notification.hs b/src/Simplex/Chat/Terminal/Notification.hs
index 98031fe525..87bed5be1a 100644
--- a/src/Simplex/Chat/Terminal/Notification.hs
+++ b/src/Simplex/Chat/Terminal/Notification.hs
@@ -13,13 +13,14 @@ import qualified Data.Map as M
import Data.Maybe (fromMaybe, isJust)
import Data.Text (Text)
import qualified Data.Text as T
-import Simplex.Chat.Types
import Simplex.Messaging.Util (catchAll_)
import System.Directory (createDirectoryIfMissing, doesFileExist, findExecutable, getAppUserDataDirectory)
import System.FilePath (combine)
import System.Info (os)
import System.Process (readCreateProcess, shell)
+data Notification = Notification {title :: Text, text :: Text}
+
initializeNotifications :: IO (Notification -> IO ())
initializeNotifications =
hideException <$> case os of
diff --git a/src/Simplex/Chat/Terminal/Output.hs b/src/Simplex/Chat/Terminal/Output.hs
index db6f16f3ca..556e4f792e 100644
--- a/src/Simplex/Chat/Terminal/Output.hs
+++ b/src/Simplex/Chat/Terminal/Output.hs
@@ -3,6 +3,7 @@
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
@@ -14,13 +15,24 @@ import Control.Monad.Catch (MonadMask)
import Control.Monad.Except
import Control.Monad.Reader
import Data.List (intercalate)
+import Data.Text (Text)
+import qualified Data.Text as T
import Data.Time.Clock (getCurrentTime)
import Data.Time.LocalTime (getCurrentTimeZone)
-import Simplex.Chat (processChatCommand)
+import Simplex.Chat (processChatCommand, chatNtf, contactNtf, groupNtf, userNtf)
import Simplex.Chat.Controller
-import Simplex.Chat.Messages hiding (NewChatItem (..))
+import Simplex.Chat.Markdown
+import Simplex.Chat.Messages
+import Simplex.Chat.Messages.CIContent (CIContent(..), SMsgDirection (..))
+import Simplex.Chat.Options
+import Simplex.Chat.Protocol (MsgContent (..), msgContentText)
import Simplex.Chat.Styled
+import Simplex.Chat.Terminal.Notification (Notification (..), initializeNotifications)
+import Simplex.Chat.Types (Contact, GroupInfo (..), User (..), UserContactRequest (..))
import Simplex.Chat.View
+import Simplex.Messaging.Agent.Protocol
+import Simplex.Messaging.Encoding.String
+import Simplex.Messaging.Util (safeDecodeUtf8)
import System.Console.ANSI.Types
import System.IO (IOMode (..), hPutStrLn, withFile)
import System.Mem.Weak (Weak)
@@ -34,7 +46,9 @@ data ChatTerminal = ChatTerminal
termSize :: Size,
liveMessageState :: TVar (Maybe LiveMessage),
nextMessageRow :: TVar Int,
- termLock :: TMVar ()
+ termLock :: TMVar (),
+ sendNotification :: Maybe (Notification -> IO ()),
+ activeTo :: TVar String
}
data TerminalState = TerminalState
@@ -79,16 +93,28 @@ instance WithTerminal VirtualTerminal where
withChatTerm :: (MonadIO m, MonadMask m) => ChatTerminal -> (forall t. WithTerminal t => TerminalT t m a) -> m a
withChatTerm ChatTerminal {termDevice = TerminalDevice t} action = withTerm t $ runTerminalT action
-newChatTerminal :: WithTerminal t => t -> IO ChatTerminal
-newChatTerminal t = do
+newChatTerminal :: WithTerminal t => t -> ChatOpts -> IO ChatTerminal
+newChatTerminal t opts = do
termSize <- withTerm t . runTerminalT $ getWindowSize
let lastRow = height termSize - 1
termState <- newTVarIO mkTermState
liveMessageState <- newTVarIO Nothing
termLock <- newTMVarIO ()
nextMessageRow <- newTVarIO lastRow
+ sendNotification <- if muteNotifications opts then pure Nothing else Just <$> initializeNotifications
+ activeTo <- newTVarIO ""
-- threadDelay 500000 -- this delay is the same as timeout in getTerminalSize
- return ChatTerminal {termDevice = TerminalDevice t, termState, termSize, liveMessageState, nextMessageRow, termLock}
+ pure
+ ChatTerminal
+ { termDevice = TerminalDevice t,
+ termState,
+ termSize,
+ liveMessageState,
+ nextMessageRow,
+ termLock,
+ sendNotification,
+ activeTo
+ }
mkTermState :: TerminalState
mkTermState =
@@ -122,6 +148,7 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} = d
_ -> printToTerminal ct
liveItems <- readTVarIO showLiveItems
responseString cc liveItems r >>= printResp
+ responseNotification ct cc r
where
markChatItemRead (AChatItem _ _ chat item@ChatItem {chatDir, meta = CIMeta {itemStatus}}) =
case (muted chat chatDir, itemStatus) of
@@ -132,6 +159,100 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} = d
_ -> pure ()
logResponse path s = withFile path AppendMode $ \h -> mapM_ (hPutStrLn h . unStyle) s
+responseNotification :: ChatTerminal -> ChatController -> ChatResponse -> IO ()
+responseNotification t@ChatTerminal {sendNotification} cc = \case
+ CRNewChatItem u (AChatItem _ SMDRcv cInfo ChatItem {chatDir, content = CIRcvMsgContent mc, formattedText}) ->
+ when (chatNtf u cInfo) $ do
+ whenCurrUser cc u $ setActiveChat t cInfo
+ case (cInfo, chatDir) of
+ (DirectChat ct, _) -> sendNtf (viewContactName ct <> "> ", text)
+ (GroupChat g, CIGroupRcv m) -> sendNtf (fromGroup_ g m, text)
+ _ -> pure ()
+ where
+ text = msgText mc formattedText
+ CRChatItemUpdated u (AChatItem _ SMDRcv cInfo ChatItem {content = CIRcvMsgContent _}) ->
+ whenCurrUser cc u $ when (chatNtf u cInfo) $ setActiveChat t cInfo
+ CRContactConnected u ct _ -> when (contactNtf u ct) $ do
+ whenCurrUser cc u $ setActiveContact t ct
+ sendNtf (viewContactName ct <> "> ", "connected")
+ CRContactAnotherClient u ct -> do
+ whenCurrUser cc u $ unsetActiveContact t ct
+ when (contactNtf u ct) $ sendNtf (viewContactName ct <> "> ", "connected to another client")
+ CRContactsDisconnected srv _ -> serverNtf srv "disconnected"
+ CRContactsSubscribed srv _ -> serverNtf srv "connected"
+ CRReceivedGroupInvitation u g ct _ _ ->
+ when (contactNtf u ct) $
+ sendNtf ("#" <> viewGroupName g <> " " <> viewContactName ct <> "> ", "invited you to join the group")
+ CRUserJoinedGroup u g _ -> when (groupNtf u g) $ do
+ whenCurrUser cc u $ setActiveGroup t g
+ sendNtf ("#" <> viewGroupName g, "you are connected to group")
+ CRJoinedGroupMember u g m ->
+ when (groupNtf u g) $ sendNtf ("#" <> viewGroupName g, "member " <> viewMemberName m <> " is connected")
+ CRConnectedToGroupMember u g m _ ->
+ when (groupNtf u g) $ sendNtf ("#" <> viewGroupName g, "member " <> viewMemberName m <> " is connected")
+ CRReceivedContactRequest u UserContactRequest {localDisplayName = n} ->
+ when (userNtf u) $ sendNtf (viewName n <> ">", "wants to connect to you")
+ _ -> pure ()
+ where
+ sendNtf = maybe (\_ -> pure ()) (. uncurry Notification) sendNotification
+ serverNtf (SMPServer host _ _) str = sendNtf ("server " <> str, safeDecodeUtf8 $ strEncode host)
+
+msgText :: MsgContent -> Maybe MarkdownList -> Text
+msgText (MCFile _) _ = "wants to send a file"
+msgText mc md_ = maybe (msgContentText mc) (mconcat . map hideSecret) md_
+ where
+ hideSecret :: FormattedText -> Text
+ hideSecret FormattedText {format = Just Secret} = "..."
+ hideSecret FormattedText {text} = text
+
+chatActiveTo :: ChatName -> String
+chatActiveTo (ChatName cType name) = case cType of
+ CTDirect -> T.unpack $ "@" <> viewName name <> " "
+ CTGroup -> T.unpack $ "#" <> viewName name <> " "
+ _ -> ""
+
+chatInfoActiveTo :: ChatInfo c -> String
+chatInfoActiveTo = \case
+ DirectChat c -> contactActiveTo c
+ GroupChat g -> groupActiveTo g
+ _ -> ""
+
+contactActiveTo :: Contact -> String
+contactActiveTo c = T.unpack $ "@" <> viewContactName c <> " "
+
+groupActiveTo :: GroupInfo -> String
+groupActiveTo g = T.unpack $ "#" <> viewGroupName g <> " "
+
+setActiveChat :: ChatTerminal -> ChatInfo c -> IO ()
+setActiveChat t = setActive t . chatInfoActiveTo
+
+setActiveContact :: ChatTerminal -> Contact -> IO ()
+setActiveContact t = setActive t . contactActiveTo
+
+setActiveGroup :: ChatTerminal -> GroupInfo -> IO ()
+setActiveGroup t = setActive t . groupActiveTo
+
+setActive :: ChatTerminal -> String -> IO ()
+setActive ChatTerminal {activeTo} to = atomically $ writeTVar activeTo to
+
+unsetActiveContact :: ChatTerminal -> Contact -> IO ()
+unsetActiveContact t = unsetActive t . contactActiveTo
+
+unsetActiveGroup :: ChatTerminal -> GroupInfo -> IO ()
+unsetActiveGroup t = unsetActive t . groupActiveTo
+
+unsetActive :: ChatTerminal -> String -> IO ()
+unsetActive ChatTerminal {activeTo} to' = atomically $ modifyTVar activeTo unset
+ where
+ unset to = if to == to' then "" else to
+
+whenCurrUser :: ChatController -> User -> IO () -> IO ()
+whenCurrUser cc u a = do
+ u_ <- readTVarIO $ currentUser cc
+ when (sameUser u u_) a
+ where
+ sameUser User {userId = uId} = maybe False $ \User {userId} -> userId == uId
+
printRespToTerminal :: ChatTerminal -> ChatController -> Bool -> ChatResponse -> IO ()
printRespToTerminal ct cc liveItems r = responseString cc liveItems r >>= printToTerminal ct
diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs
index 864ebd7227..de56baad92 100644
--- a/src/Simplex/Chat/Types.hs
+++ b/src/Simplex/Chat/Types.hs
@@ -1424,8 +1424,6 @@ serializeIntroStatus = \case
GMIntroToConnected -> "to-con"
GMIntroConnected -> "con"
-data Notification = Notification {title :: Text, text :: Text}
-
textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a
textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . textDecode
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index bb5e854cd2..f60b7cd82f 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -103,7 +103,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
CRContactCode u ct code -> ttyUser u $ viewContactCode ct code testView
CRGroupMemberCode u g m code -> ttyUser u $ viewGroupMemberCode g m code testView
CRNewChatItem u (AChatItem _ _ chat item) -> ttyUser u $ unmuted chat item $ viewChatItem chat item False ts tz <> viewItemReactions item
- CRChatItems u chatItems -> ttyUser u $ concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts tz <> viewItemReactions item) chatItems
+ CRChatItems u _ chatItems -> ttyUser u $ concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts tz <> viewItemReactions item) chatItems
CRChatItemInfo u ci ciInfo -> ttyUser u $ viewChatItemInfo ci ciInfo tz
CRChatItemId u itemId -> ttyUser u [plain $ maybe "no item" show itemId]
CRChatItemStatusUpdated u ci -> ttyUser u $ viewChatItemStatusUpdated ci ts tz testView showReceipts
diff --git a/tests/Bots/BroadcastTests.hs b/tests/Bots/BroadcastTests.hs
index ae2d67c7f0..ed0b9e069a 100644
--- a/tests/Bots/BroadcastTests.hs
+++ b/tests/Bots/BroadcastTests.hs
@@ -26,7 +26,7 @@ withBroadcastBot :: BroadcastBotOpts -> IO () -> IO ()
withBroadcastBot opts test =
bracket (forkIO bot) killThread (\_ -> threadDelay 500000 >> test)
where
- bot = simplexChatCore testCfg (mkChatOpts opts) Nothing $ broadcastBot opts
+ bot = simplexChatCore testCfg (mkChatOpts opts) $ broadcastBot opts
broadcastBotProfile :: Profile
broadcastBotProfile = Profile {displayName = "broadcast_bot", fullName = "Broadcast Bot", image = Nothing, contactLink = Nothing, preferences = Nothing}
diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs
index 0e315190c5..3e1c32a6f7 100644
--- a/tests/Bots/DirectoryTests.hs
+++ b/tests/Bots/DirectoryTests.hs
@@ -827,7 +827,7 @@ runDirectory cfg opts@DirectoryOpts {directoryLog} action = do
threadDelay 500000
action `finally` (mapM_ hClose (directoryLogFile st) >> killThread t)
where
- bot st = simplexChatCore cfg (mkChatOpts opts) Nothing $ directoryService st opts
+ bot st = simplexChatCore cfg (mkChatOpts opts) $ directoryService st opts
registerGroup :: TestCC -> TestCC -> String -> String -> IO ()
registerGroup su u n fn = registerGroupId su u n fn 1 1
diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs
index 7da5263253..aae3b5c4c7 100644
--- a/tests/ChatClient.hs
+++ b/tests/ChatClient.hs
@@ -161,8 +161,8 @@ startTestChat tmp cfg opts@ChatOpts {coreOptions = CoreChatOpts {dbKey}} dbPrefi
startTestChat_ :: ChatDatabase -> ChatConfig -> ChatOpts -> User -> IO TestCC
startTestChat_ db cfg opts user = do
t <- withVirtualTerminal termSettings pure
- ct <- newChatTerminal t
- cc <- newChatController db (Just user) cfg opts Nothing -- no notifications
+ ct <- newChatTerminal t opts
+ cc <- newChatController db (Just user) cfg opts
chatAsync <- async . runSimplexChat opts user cc . const $ runChatTerminal ct
atomically . unless (maintenance opts) $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry
termQ <- newTQueueIO
From b03fe183bb2dece22e9d7cbdfada70e85e9a774c Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Wed, 11 Oct 2023 15:26:44 +0400
Subject: [PATCH 17/80] tests: modify
testMemberContactInvitedConnectionReplaced to not rely on chat item order,
print output (#3198)
---
tests/ChatClient.hs | 2 ++
tests/ChatTests/Groups.hs | 10 ++++++----
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs
index aae3b5c4c7..fae460e908 100644
--- a/tests/ChatClient.hs
+++ b/tests/ChatClient.hs
@@ -210,6 +210,8 @@ withTestChatOpts tmp = withTestChatCfgOpts tmp testCfg
withTestChatCfgOpts :: HasCallStack => FilePath -> ChatConfig -> ChatOpts -> String -> (HasCallStack => TestCC -> IO a) -> IO a
withTestChatCfgOpts tmp cfg opts dbPrefix = bracket (startTestChat tmp cfg opts dbPrefix) (\cc -> cc / 100000 >> stopTestChat cc)
+-- enable output for specific chat controller, use like this:
+-- withNewTestChat tmp "alice" aliceProfile $ \a -> withTestOutput a $ \alice -> do ...
withTestOutput :: HasCallStack => TestCC -> (HasCallStack => TestCC -> IO a) -> IO a
withTestOutput cc runTest = runTest cc {printOutput = True}
diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs
index 997beec4ea..6578df84c0 100644
--- a/tests/ChatTests/Groups.hs
+++ b/tests/ChatTests/Groups.hs
@@ -3252,9 +3252,9 @@ testMemberContactProhibitedRepeatInv =
testMemberContactInvitedConnectionReplaced :: HasCallStack => FilePath -> IO ()
testMemberContactInvitedConnectionReplaced tmp = do
- withNewTestChat tmp "alice" aliceProfile $ \alice -> do
- withNewTestChat tmp "bob" bobProfile $ \bob -> do
- withNewTestChat tmp "cath" cathProfile $ \cath -> do
+ withNewTestChat tmp "alice" aliceProfile $ \a -> withTestOutput a $ \alice -> do
+ withNewTestChat tmp "bob" bobProfile $ \b -> withTestOutput b $ \bob -> do
+ withNewTestChat tmp "cath" cathProfile $ \c -> withTestOutput c $ \cath -> do
createGroup3 "team" alice bob cath
alice ##> "/d bob"
@@ -3277,7 +3277,9 @@ testMemberContactInvitedConnectionReplaced tmp = do
(alice <## "bob (Bob): contact is connected")
(bob <## "alice (Alice): contact is connected")
- bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "received invitation to join group team as admin"), (0, "contact deleted"), (0, "hi"), (0, "security code changed")] <> chatFeatures)
+ bob ##> "/_get chat @2 count=100"
+ items <- chat <$> getTermLine bob
+ items `shouldContain` [(0, "received invitation to join group team as admin"), (0, "contact deleted"), (0, "hi"), (0, "security code changed")]
withTestChat tmp "bob" $ \bob -> do
subscriptions bob 1
From bca9473d7704b3476452144b58a96d7ffb9e8728 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Wed, 11 Oct 2023 19:10:38 +0100
Subject: [PATCH 18/80] core: settings to hide member messages, to show only
reply (and mention) notifications (#3190)
* core: settings to hide member messages, to show only reply (and mention) notifications
* change type for showMessages
* commands for member settings
* member and notification settings
* test
* take member settings into account when showing messages and notifications
* fix to show sent messages
* store blocked items
* types
* rename to MFMentions
---
simplex-chat.cabal | 1 +
src/Simplex/Chat.hs | 132 ++++++++++--------
src/Simplex/Chat/Controller.hs | 4 +-
src/Simplex/Chat/Messages.hs | 37 +++--
src/Simplex/Chat/Messages/CIContent.hs | 14 +-
.../Migrations/M20231010_member_settings.hs | 18 +++
src/Simplex/Chat/Migrations/chat_schema.sql | 1 +
src/Simplex/Chat/Protocol.hs | 5 +
src/Simplex/Chat/Store/Connections.hs | 8 +-
src/Simplex/Chat/Store/Groups.hs | 51 ++++---
src/Simplex/Chat/Store/Messages.hs | 96 ++++++++-----
src/Simplex/Chat/Store/Migrations.hs | 4 +-
src/Simplex/Chat/Store/Shared.hs | 6 +-
src/Simplex/Chat/Terminal/Output.hs | 36 ++---
src/Simplex/Chat/Types.hs | 62 +++++++-
src/Simplex/Chat/View.hs | 121 ++++++++++------
tests/ChatTests/Direct.hs | 71 +++++++++-
tests/ChatTests/Groups.hs | 1 -
18 files changed, 441 insertions(+), 227 deletions(-)
create mode 100644 src/Simplex/Chat/Migrations/M20231010_member_settings.hs
diff --git a/simplex-chat.cabal b/simplex-chat.cabal
index 5a84a1cde8..b431b0ddf3 100644
--- a/simplex-chat.cabal
+++ b/simplex-chat.cabal
@@ -116,6 +116,7 @@ library
Simplex.Chat.Migrations.M20230926_contact_status
Simplex.Chat.Migrations.M20231002_conn_initiated
Simplex.Chat.Migrations.M20231009_via_group_link_uri_hash
+ Simplex.Chat.Migrations.M20231010_member_settings
Simplex.Chat.Mobile
Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 5e2a231c97..b7421d9366 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -800,7 +800,7 @@ processChatCommand = \case
CTContactConnection -> pure $ chatCmdError (Just user) "not supported"
APIDeleteChatItem (ChatRef cType chatId) itemId mode -> withUser $ \user -> withChatLock "deleteChatItem" $ case cType of
CTDirect -> do
- (ct, ci@(CChatItem msgDir ChatItem {meta = CIMeta {itemSharedMsgId, editable}})) <- withStore $ \db -> (,) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId
+ (ct, CChatItem msgDir ci@ChatItem {meta = CIMeta {itemSharedMsgId, editable}}) <- withStore $ \db -> (,) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId
case (mode, msgDir, itemSharedMsgId, editable) of
(CIDMInternal, _, _, _) -> deleteDirectCI user ct ci True False
(CIDMBroadcast, SMDSnd, Just itemSharedMId, True) -> do
@@ -812,7 +812,7 @@ processChatCommand = \case
(CIDMBroadcast, _, _, _) -> throwChatError CEInvalidChatItemDelete
CTGroup -> do
Group gInfo ms <- withStore $ \db -> getGroup db user chatId
- ci@(CChatItem msgDir ChatItem {meta = CIMeta {itemSharedMsgId, editable}}) <- withStore $ \db -> getGroupChatItem db user chatId itemId
+ CChatItem msgDir ci@ChatItem {meta = CIMeta {itemSharedMsgId, editable}} <- withStore $ \db -> getGroupChatItem db user chatId itemId
case (mode, msgDir, itemSharedMsgId, editable) of
(CIDMInternal, _, _, _) -> deleteGroupCI user gInfo ci True False Nothing =<< liftIO getCurrentTime
(CIDMBroadcast, SMDSnd, Just itemSharedMId, True) -> do
@@ -824,7 +824,7 @@ processChatCommand = \case
CTContactConnection -> pure $ chatCmdError (Just user) "not supported"
APIDeleteMemberChatItem gId mId itemId -> withUser $ \user -> withChatLock "deleteChatItem" $ do
Group gInfo@GroupInfo {membership} ms <- withStore $ \db -> getGroup db user gId
- ci@(CChatItem _ ChatItem {chatDir, meta = CIMeta {itemSharedMsgId}}) <- withStore $ \db -> getGroupChatItem db user gId itemId
+ CChatItem _ ci@ChatItem {chatDir, meta = CIMeta {itemSharedMsgId}} <- withStore $ \db -> getGroupChatItem db user gId itemId
case (chatDir, itemSharedMsgId) of
(CIGroupRcv GroupMember {groupMemberId, memberRole, memberId}, Just itemSharedMId) -> do
when (groupMemberId /= mId) $ throwChatError CEInvalidChatItemDelete
@@ -1178,7 +1178,7 @@ processChatCommand = \case
ct <- getContact db user chatId
liftIO $ updateContactSettings db user chatId chatSettings
pure ct
- withAgent $ \a -> toggleConnectionNtfs a (contactConnId ct) (enableNtfs chatSettings)
+ withAgent $ \a -> toggleConnectionNtfs a (contactConnId ct) (chatHasNtfs chatSettings)
ok user
CTGroup -> do
ms <- withStore $ \db -> do
@@ -1186,9 +1186,17 @@ processChatCommand = \case
liftIO $ updateGroupSettings db user chatId chatSettings
pure ms
forM_ (filter memberActive ms) $ \m -> forM_ (memberConnId m) $ \connId ->
- withAgent (\a -> toggleConnectionNtfs a connId $ enableNtfs chatSettings) `catchChatError` (toView . CRChatError (Just user))
+ withAgent (\a -> toggleConnectionNtfs a connId $ chatHasNtfs chatSettings) `catchChatError` (toView . CRChatError (Just user))
ok user
_ -> pure $ chatCmdError (Just user) "not supported"
+ APISetMemberSettings gId gMemberId settings -> withUser $ \user -> do
+ m <- withStore $ \db -> do
+ liftIO $ updateGroupMemberSettings db user gId gMemberId settings
+ getGroupMember db user gId gMemberId
+ when (memberActive m) $ forM_ (memberConnId m) $ \connId -> do
+ let ntfOn = showMessages $ memberSettings m
+ withAgent (\a -> toggleConnectionNtfs a connId ntfOn) `catchChatError` (toView . CRChatError (Just user))
+ ok user
APIContactInfo contactId -> withUser $ \user@User {userId} -> do
-- [incognito] print user's incognito profile for this contact
ct@Contact {activeConn = Connection {customUserProfileId}} <- withStore $ \db -> getContact db user contactId
@@ -1283,6 +1291,11 @@ processChatCommand = \case
_ -> throwChatError CEGroupMemberNotActive
SetShowMessages cName ntfOn -> updateChatSettings cName (\cs -> cs {enableNtfs = ntfOn})
SetSendReceipts cName rcptsOn_ -> updateChatSettings cName (\cs -> cs {sendRcpts = rcptsOn_})
+ SetShowMemberMessages gName mName showMessages -> withUser $ \user -> do
+ (gId, mId) <- getGroupAndMemberId user gName mName
+ m <- withStore $ \db -> getGroupMember db user gId mId
+ let settings = (memberSettings m) {showMessages}
+ processChatCommand $ APISetMemberSettings gId mId settings
ContactInfo cName -> withContactName cName APIContactInfo
ShowGroupInfo gName -> withUser $ \user -> do
groupId <- withStore $ \db -> getGroupIdByName db user gName
@@ -2073,7 +2086,7 @@ processChatCommand = \case
when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined g)
when (memberRemoved membership) $ throwChatError CEGroupMemberUserRemoved
unless (memberActive membership) $ throwChatError CEGroupMemberNotActive
- delGroupChatItem :: User -> GroupInfo -> CChatItem 'CTGroup -> MessageId -> Maybe GroupMember -> m ChatResponse
+ delGroupChatItem :: MsgDirectionI d => User -> GroupInfo -> ChatItem 'CTGroup d -> MessageId -> Maybe GroupMember -> m ChatResponse
delGroupChatItem user gInfo ci msgId byGroupMember = do
deletedTs <- liftIO getCurrentTime
if groupFeatureAllowed SGFFullDelete gInfo
@@ -2813,10 +2826,10 @@ deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do
waitChatStarted
case cType of
CTDirect -> do
- (ct, ci) <- withStoreCtx (Just "deleteTimedItem, getContact ...") $ \db -> (,) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId
+ (ct, CChatItem _ ci) <- withStore $ \db -> (,) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId
deleteDirectCI user ct ci True True >>= toView
CTGroup -> do
- (gInfo, ci) <- withStoreCtx (Just "deleteTimedItem, getGroupInfo ...") $ \db -> (,) <$> getGroupInfo db user chatId <*> getGroupChatItem db user chatId itemId
+ (gInfo, CChatItem _ ci) <- withStore $ \db -> (,) <$> getGroupInfo db user chatId <*> getGroupChatItem db user chatId itemId
deletedTs <- liftIO getCurrentTime
deleteGroupCI user gInfo ci True True Nothing deletedTs >>= toView
_ -> toView . CRChatError (Just user) . ChatError $ CEInternalError "bad deleteTimedItem cType"
@@ -3335,7 +3348,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
updateGroupMemberStatus db userId membership GSMemConnected
-- possible improvement: check for each pending message, requires keeping track of connection state
unless (connDisabled conn) $ sendPendingGroupMessages user m conn
- withAgent $ \a -> toggleConnectionNtfs a (aConnId conn) $ enableNtfs chatSettings
+ withAgent $ \a -> toggleConnectionNtfs a (aConnId conn) $ chatHasNtfs chatSettings
case memberCategory m of
GCHostMember -> do
toView $ CRUserJoinedGroup user gInfo {membership = membership {memberStatus = GSMemConnected}} m {memberStatus = GSMemConnected}
@@ -3932,7 +3945,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
deleteRcvChatItem `catchCINotFound` (toView . CRChatItemDeletedNotFound user ct)
where
deleteRcvChatItem = do
- ci@(CChatItem msgDir _) <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId
+ CChatItem msgDir ci <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId
case msgDir of
SMDRcv ->
if featureAllowed SCFFullDelete forContact ct
@@ -4013,21 +4026,21 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
createItem timed_ live
| groupFeatureAllowed SGFFullDelete gInfo = do
ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ msgMeta CIRcvModerated Nothing timed_ False
- ci' <- withStore' $ \db -> updateGroupChatItemModerated db user gInfo (CChatItem SMDRcv ci) moderator moderatedAt
- toView $ CRNewChatItem user ci'
+ ci' <- withStore' $ \db -> updateGroupChatItemModerated db user gInfo ci moderator moderatedAt
+ toView $ CRNewChatItem user $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci'
| otherwise = do
file_ <- processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId m
ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ msgMeta (CIRcvMsgContent content) (snd <$> file_) timed_ False
- cr <- markGroupCIDeleted user gInfo (CChatItem SMDRcv ci) createdByMsgId False (Just moderator) moderatedAt
- toView cr
+ toView =<< markGroupCIDeleted user gInfo ci createdByMsgId False (Just moderator) moderatedAt
createItem timed_ live = do
file_ <- processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId m
newChatItem (CIRcvMsgContent content) (snd <$> file_) timed_ live
- autoAcceptFile file_
+ when (showMessages $ memberSettings m) $ autoAcceptFile file_
newChatItem ciContent ciFile_ timed_ live = do
ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ msgMeta ciContent ciFile_ timed_ live
+ ci' <- blockedMember m ci $ withStore' $ \db -> markGroupChatItemBlocked db user gInfo ci
reactions <- maybe (pure []) (\sharedMsgId -> withStore' $ \db -> getGroupCIReactions db gInfo memberId sharedMsgId) sharedMsgId_
- groupMsgToView gInfo m ci {reactions} msgMeta
+ groupMsgToView gInfo m ci' {reactions} msgMeta
groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m ()
groupMessageUpdate gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId, memberId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl_ live_ =
@@ -4039,7 +4052,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg (Just sharedMsgId) msgMeta content Nothing timed_ live
ci' <- withStore' $ \db -> do
createChatItemVersion db (chatItemId' ci) brokerTs mc
- updateGroupChatItem db user groupId ci content live Nothing
+ ci' <- updateGroupChatItem db user groupId ci content live Nothing
+ blockedMember m ci' $ markGroupChatItemBlocked db user gInfo ci'
toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci')
where
MsgMeta {broker = (_, brokerTs)} = msgMeta
@@ -4068,7 +4082,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
groupMessageDelete gInfo@GroupInfo {groupId, membership} m@GroupMember {memberId, memberRole = senderRole} sharedMsgId sndMemberId_ RcvMessage {msgId} MsgMeta {broker = (_, brokerTs)} = do
let msgMemberId = fromMaybe memberId sndMemberId_
withStore' (\db -> runExceptT $ getGroupMemberCIBySharedMsgId db user groupId msgMemberId sharedMsgId) >>= \case
- Right ci@(CChatItem _ ChatItem {chatDir}) -> case chatDir of
+ Right (CChatItem _ ci@ChatItem {chatDir}) -> case chatDir of
CIGroupRcv mem
| sameMemberId memberId mem && msgMemberId == memberId -> delete ci Nothing >>= toView
| otherwise -> deleteMsg mem ci
@@ -4078,7 +4092,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
| senderRole < GRAdmin -> messageError $ "x.msg.del: message not found, message of another member with insufficient member permissions, " <> tshow e
| otherwise -> withStore' $ \db -> createCIModeration db gInfo m msgMemberId sharedMsgId msgId brokerTs
where
- deleteMsg :: GroupMember -> CChatItem 'CTGroup -> m ()
+ deleteMsg :: MsgDirectionI d => GroupMember -> ChatItem 'CTGroup d -> m ()
deleteMsg mem ci = case sndMemberId_ of
Just sndMemberId
| sameMemberId sndMemberId mem -> checkRole mem $ delete ci (Just m) >>= toView
@@ -4088,6 +4102,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
| senderRole < GRAdmin || senderRole < memberRole =
messageError "x.msg.del: message of another member with insufficient member permissions"
| otherwise = a
+ delete :: MsgDirectionI d => ChatItem 'CTGroup d -> Maybe GroupMember -> m ChatResponse
delete ci byGroupMember
| groupFeatureAllowed SGFFullDelete gInfo = deleteGroupCI user gInfo ci False False byGroupMember brokerTs
| otherwise = markGroupCIDeleted user gInfo ci msgId False byGroupMember brokerTs
@@ -4113,7 +4128,13 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol}
ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ msgMeta (CIRcvMsgContent $ MCFile "") ciFile Nothing False
- groupMsgToView gInfo m ci msgMeta
+ ci' <- blockedMember m ci $ withStore' $ \db -> markGroupChatItemBlocked db user gInfo ci
+ groupMsgToView gInfo m ci' msgMeta
+
+ blockedMember :: Monad m' => GroupMember -> ChatItem c d -> m' (ChatItem c d) -> m' (ChatItem c d)
+ blockedMember m ci blockedCI
+ | showMessages (memberSettings m) = pure ci
+ | otherwise = blockedCI
receiveInlineMode :: FileInvitation -> Maybe MsgContent -> Integer -> m (Maybe InlineFileMode)
receiveInlineMode FileInvitation {fileSize, fileInline, fileDescr} mc_ chSize = case (fileInline, fileDescr) of
@@ -4632,7 +4653,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
toView $ CRJoinedGroupMemberConnecting user gInfo m newMember
xGrpMemIntro :: GroupInfo -> GroupMember -> MemberInfo -> m ()
- xGrpMemIntro gInfo@GroupInfo {chatSettings = ChatSettings {enableNtfs}} m@GroupMember {memberRole, localDisplayName = c} memInfo@(MemberInfo memId _ memberChatVRange _) = do
+ xGrpMemIntro gInfo@GroupInfo {chatSettings} m@GroupMember {memberRole, localDisplayName = c} memInfo@(MemberInfo memId _ memberChatVRange _) = do
case memberCategory m of
GCHostMember -> do
members <- withStore' $ \db -> getGroupMembers db user gInfo
@@ -4652,7 +4673,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
void $ withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnIds directConnIds customUserProfileId subMode
_ -> messageError "x.grp.mem.intro can be only sent by host member"
where
- createConn subMode = createAgentConnectionAsync user CFCreateConnGrpMemInv enableNtfs SCMInvitation subMode
+ createConn subMode = createAgentConnectionAsync user CFCreateConnGrpMemInv (chatHasNtfs chatSettings) SCMInvitation subMode
sendXGrpMemInv :: Int64 -> Maybe ConnReqInvitation -> XGrpMemIntroCont -> m ()
sendXGrpMemInv hostConnId directConnReq XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} = do
@@ -4675,7 +4696,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
_ -> messageError "x.grp.mem.inv can be only sent by invitee member"
xGrpMemFwd :: GroupInfo -> GroupMember -> MemberInfo -> IntroInvitation -> m ()
- xGrpMemFwd gInfo@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} m memInfo@(MemberInfo memId memRole memberChatVRange _) introInv@IntroInvitation {groupConnReq, directConnReq} = do
+ xGrpMemFwd gInfo@GroupInfo {membership, chatSettings} m memInfo@(MemberInfo memId memRole memberChatVRange _) introInv@IntroInvitation {groupConnReq, directConnReq} = do
checkHostRole m memRole
members <- withStore' $ \db -> getGroupMembers db user gInfo
toMember <- case find (sameMemberId memId) members of
@@ -4690,8 +4711,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
-- [incognito] send membership incognito profile, create direct connection as incognito
dm <- directMessage $ XGrpMemInfo membership.memberId (fromLocalProfile $ memberProfile membership)
-- [async agent commands] no continuation needed, but commands should be asynchronous for stability
- groupConnIds <- joinAgentConnectionAsync user enableNtfs groupConnReq dm subMode
- directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user enableNtfs dcr dm subMode
+ groupConnIds <- joinAgentConnectionAsync user (chatHasNtfs chatSettings) groupConnReq dm subMode
+ directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user True dcr dm subMode
let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo
mcvr = maybe chatInitialVRange fromChatVRange memberChatVRange
withStore' $ \db -> createIntroToMemberContact db user m toMember mcvr groupConnIds directConnIds customUserProfileId subMode
@@ -5215,20 +5236,22 @@ mkChatItem cd ciId content file quotedItem sharedMsgId itemTimed live itemTs cur
meta = mkCIMeta ciId content itemText itemStatus sharedMsgId Nothing False itemTimed (justTrue live) currentTs itemTs currentTs currentTs
pure ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem, reactions = [], file}
-deleteDirectCI :: ChatMonad m => User -> Contact -> CChatItem 'CTDirect -> Bool -> Bool -> m ChatResponse
-deleteDirectCI user ct ci@(CChatItem msgDir deletedItem@ChatItem {file}) byUser timed = do
+deleteDirectCI :: (ChatMonad m, MsgDirectionI d) => User -> Contact -> ChatItem 'CTDirect d -> Bool -> Bool -> m ChatResponse
+deleteDirectCI user ct ci@ChatItem {file} byUser timed = do
deleteCIFile user file
withStoreCtx' (Just "deleteDirectCI, deleteDirectChatItem") $ \db -> deleteDirectChatItem db user ct ci
- pure $ CRChatItemDeleted user (AChatItem SCTDirect msgDir (DirectChat ct) deletedItem) Nothing byUser timed
+ pure $ CRChatItemDeleted user (AChatItem SCTDirect msgDirection (DirectChat ct) ci) Nothing byUser timed
-deleteGroupCI :: ChatMonad m => User -> GroupInfo -> CChatItem 'CTGroup -> Bool -> Bool -> Maybe GroupMember -> UTCTime -> m ChatResponse
-deleteGroupCI user gInfo ci@(CChatItem msgDir deletedItem@ChatItem {file}) byUser timed byGroupMember_ deletedTs = do
+deleteGroupCI :: (ChatMonad m, MsgDirectionI d) => User -> GroupInfo -> ChatItem 'CTGroup d -> Bool -> Bool -> Maybe GroupMember -> UTCTime -> m ChatResponse
+deleteGroupCI user gInfo ci@ChatItem {file} byUser timed byGroupMember_ deletedTs = do
deleteCIFile user file
toCi <- withStoreCtx' (Just "deleteGroupCI, deleteGroupChatItem ...") $ \db ->
case byGroupMember_ of
Nothing -> deleteGroupChatItem db user gInfo ci $> Nothing
Just m -> Just <$> updateGroupChatItemModerated db user gInfo ci m deletedTs
- pure $ CRChatItemDeleted user (AChatItem SCTGroup msgDir (GroupChat gInfo) deletedItem) toCi byUser timed
+ pure $ CRChatItemDeleted user (gItem ci) (gItem <$> toCi) byUser timed
+ where
+ gItem = AChatItem SCTGroup msgDirection (GroupChat gInfo)
deleteCIFile :: (ChatMonad m, MsgDirectionI d) => User -> Maybe (CIFile d) -> m ()
deleteCIFile user file_ =
@@ -5236,25 +5259,21 @@ deleteCIFile user file_ =
fileAgentConnIds <- deleteFile' user (mkCIFileInfo file) True
deleteAgentConnectionsAsync user fileAgentConnIds
-markDirectCIDeleted :: ChatMonad m => User -> Contact -> CChatItem 'CTDirect -> MessageId -> Bool -> UTCTime -> m ChatResponse
-markDirectCIDeleted user ct@Contact {contactId} ci@(CChatItem _ ChatItem {file}) msgId byUser deletedTs = do
+markDirectCIDeleted :: (ChatMonad m, MsgDirectionI d) => User -> Contact -> ChatItem 'CTDirect d -> MessageId -> Bool -> UTCTime -> m ChatResponse
+markDirectCIDeleted user ct ci@ChatItem {file} msgId byUser deletedTs = do
cancelCIFile user file
- toCi <- withStore $ \db -> do
- liftIO $ markDirectChatItemDeleted db user ct ci msgId deletedTs
- getDirectChatItem db user contactId (cchatItemId ci)
- pure $ CRChatItemDeleted user (ctItem ci) (Just $ ctItem toCi) byUser False
+ ci' <- withStore' $ \db -> markDirectChatItemDeleted db user ct ci msgId deletedTs
+ pure $ CRChatItemDeleted user (ctItem ci) (Just $ ctItem ci') byUser False
where
- ctItem (CChatItem msgDir ci') = AChatItem SCTDirect msgDir (DirectChat ct) ci'
+ ctItem = AChatItem SCTDirect msgDirection (DirectChat ct)
-markGroupCIDeleted :: ChatMonad m => User -> GroupInfo -> CChatItem 'CTGroup -> MessageId -> Bool -> Maybe GroupMember -> UTCTime -> m ChatResponse
-markGroupCIDeleted user gInfo@GroupInfo {groupId} ci@(CChatItem _ ChatItem {file}) msgId byUser byGroupMember_ deletedTs = do
+markGroupCIDeleted :: (ChatMonad m, MsgDirectionI d) => User -> GroupInfo -> ChatItem 'CTGroup d -> MessageId -> Bool -> Maybe GroupMember -> UTCTime -> m ChatResponse
+markGroupCIDeleted user gInfo ci@ChatItem {file} msgId byUser byGroupMember_ deletedTs = do
cancelCIFile user file
- toCi <- withStore $ \db -> do
- liftIO $ markGroupChatItemDeleted db user gInfo ci msgId byGroupMember_ deletedTs
- getGroupChatItem db user groupId (cchatItemId ci)
- pure $ CRChatItemDeleted user (gItem ci) (Just $ gItem toCi) byUser False
+ ci' <- withStore' $ \db -> markGroupChatItemDeleted db user gInfo ci msgId byGroupMember_ deletedTs
+ pure $ CRChatItemDeleted user (gItem ci) (Just $ gItem ci') byUser False
where
- gItem (CChatItem msgDir ci') = AChatItem SCTGroup msgDir (GroupChat gInfo) ci'
+ gItem = AChatItem SCTGroup msgDirection (GroupChat gInfo)
cancelCIFile :: (ChatMonad m, MsgDirectionI d) => User -> Maybe (CIFile d) -> m ()
cancelCIFile user file_ =
@@ -5440,21 +5459,6 @@ getCreateActiveUser st testView = do
getWithPrompt :: String -> IO String
getWithPrompt s = putStr (s <> ": ") >> hFlush stdout >> getLine
-userNtf :: User -> Bool
-userNtf User {showNtfs, activeUser} = showNtfs || activeUser
-
-chatNtf :: User -> ChatInfo c -> Bool
-chatNtf user = \case
- DirectChat ct -> contactNtf user ct
- GroupChat g -> groupNtf user g
- _ -> False
-
-contactNtf :: User -> Contact -> Bool
-contactNtf user Contact {chatSettings} = userNtf user && enableNtfs chatSettings
-
-groupNtf :: User -> GroupInfo -> Bool
-groupNtf user GroupInfo {chatSettings} = userNtf user && enableNtfs chatSettings
-
withUser' :: ChatMonad m => (User -> m ChatResponse) -> m ChatResponse
withUser' action =
asks currentUser
@@ -5492,9 +5496,12 @@ withAgent action =
chatCommandP :: Parser ChatCommand
chatCommandP =
choice
- [ "/mute " *> ((`SetShowMessages` False) <$> chatNameP),
- "/unmute " *> ((`SetShowMessages` True) <$> chatNameP),
+ [ "/mute " *> ((`SetShowMessages` MFNone) <$> chatNameP),
+ "/unmute " *> ((`SetShowMessages` MFAll) <$> chatNameP),
+ "/unmute mentions " *> ((`SetShowMessages` MFMentions) <$> chatNameP),
"/receipts " *> (SetSendReceipts <$> chatNameP <* " " <*> ((Just <$> onOffP) <|> ("default" $> Nothing))),
+ "/block #" *> (SetShowMemberMessages <$> displayName <* A.space <*> (char_ '@' *> displayName) <*> pure False),
+ "/unblock #" *> (SetShowMemberMessages <$> displayName <* A.space <*> (char_ '@' *> displayName) <*> pure True),
"/_create user " *> (CreateActiveUser <$> jsonP),
"/create user " *> (CreateActiveUser <$> newUserP),
"/users" $> ListUsers,
@@ -5598,6 +5605,7 @@ chatCommandP =
("/network" <|> "/net") $> APIGetNetworkConfig,
"/reconnect" $> ReconnectAllServers,
"/_settings " *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP),
+ "/_member settings #" *> (APISetMemberSettings <$> A.decimal <* A.space <*> A.decimal <* A.space <*> jsonP),
"/_info #" *> (APIGroupMemberInfo <$> A.decimal <* A.space <*> A.decimal),
"/_info #" *> (APIGroupInfo <$> A.decimal),
"/_info @" *> (APIContactInfo <$> A.decimal),
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index d97d59c5c5..af9f34d2d4 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -288,6 +288,7 @@ data ChatCommand
| APIGetNetworkConfig
| ReconnectAllServers
| APISetChatSettings ChatRef ChatSettings
+ | APISetMemberSettings GroupId GroupMemberId GroupMemberSettings
| APIContactInfo ContactId
| APIGroupInfo GroupId
| APIGroupMemberInfo GroupId GroupMemberId
@@ -303,8 +304,9 @@ data ChatCommand
| APIVerifyGroupMember GroupId GroupMemberId (Maybe Text)
| APIEnableContact ContactId
| APIEnableGroupMember GroupId GroupMemberId
- | SetShowMessages ChatName Bool
+ | SetShowMessages ChatName MsgFilter
| SetSendReceipts ChatName (Maybe Bool)
+ | SetShowMemberMessages GroupName ContactName Bool
| ContactInfo ContactName
| ShowGroupInfo GroupName
| GroupMemberInfo GroupName ContactName
diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs
index 3831fad03e..22506218aa 100644
--- a/src/Simplex/Chat/Messages.hs
+++ b/src/Simplex/Chat/Messages.hs
@@ -150,6 +150,19 @@ instance MsgDirectionI d => ToJSON (ChatItem c d) where
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
+isReference :: ChatItem c d -> Bool
+isReference ChatItem {chatDir, quotedItem} = case chatDir of
+ CIDirectRcv -> userItem quotedItem
+ CIGroupRcv _ -> userItem quotedItem
+ _ -> False
+ where
+ userItem = \case
+ Nothing -> False
+ Just CIQuote {chatDir = cd} -> case cd of
+ CIQDirectSnd -> True
+ CIQGroupSnd -> True
+ _ -> False
+
data CIDirection (c :: ChatType) (d :: MsgDirection) where
CIDirectSnd :: CIDirection 'CTDirect 'MDSnd
CIDirectRcv :: CIDirection 'CTDirect 'MDRcv
@@ -220,26 +233,6 @@ ciReactionAllowed :: ChatItem c d -> Bool
ciReactionAllowed ChatItem {meta = CIMeta {itemDeleted = Just _}} = False
ciReactionAllowed ChatItem {content} = isJust $ ciMsgContent content
-data CIDeletedState = CIDeletedState
- { markedDeleted :: Bool,
- deletedByMember :: Maybe GroupMember
- }
- deriving (Show, Eq)
-
-chatItemDeletedState :: ChatItem c d -> Maybe CIDeletedState
-chatItemDeletedState ChatItem {meta = CIMeta {itemDeleted}, content} =
- ciDeletedToDeletedState <$> itemDeleted
- where
- ciDeletedToDeletedState cid =
- case content of
- CISndModerated -> CIDeletedState {markedDeleted = False, deletedByMember = byMember cid}
- CIRcvModerated -> CIDeletedState {markedDeleted = False, deletedByMember = byMember cid}
- _ -> CIDeletedState {markedDeleted = True, deletedByMember = byMember cid}
- byMember :: CIDeleted c -> Maybe GroupMember
- byMember = \case
- CIModerated _ m -> Just m
- CIDeleted _ -> Nothing
-
data ChatDirection (c :: ChatType) (d :: MsgDirection) where
CDDirectSnd :: Contact -> ChatDirection 'CTDirect 'MDSnd
CDDirectRcv :: Contact -> ChatDirection 'CTDirect 'MDRcv
@@ -929,6 +922,7 @@ checkDirection x = case testEquality (msgDirection @d) (msgDirection @d') of
data CIDeleted (c :: ChatType) where
CIDeleted :: Maybe UTCTime -> CIDeleted c
+ CIBlocked :: Maybe UTCTime -> CIDeleted c
CIModerated :: Maybe UTCTime -> GroupMember -> CIDeleted 'CTGroup
deriving instance Show (CIDeleted c)
@@ -939,6 +933,7 @@ instance ToJSON (CIDeleted d) where
data JSONCIDeleted
= JCIDDeleted {deletedTs :: Maybe UTCTime}
+ | JCIBlocked {deletedTs :: Maybe UTCTime}
| JCIDModerated {deletedTs :: Maybe UTCTime, byGroupMember :: GroupMember}
deriving (Show, Generic)
@@ -949,11 +944,13 @@ instance ToJSON JSONCIDeleted where
jsonCIDeleted :: CIDeleted d -> JSONCIDeleted
jsonCIDeleted = \case
CIDeleted ts -> JCIDDeleted ts
+ CIBlocked ts -> JCIBlocked ts
CIModerated ts m -> JCIDModerated ts m
itemDeletedTs :: CIDeleted d -> Maybe UTCTime
itemDeletedTs = \case
CIDeleted ts -> ts
+ CIBlocked ts -> ts
CIModerated ts _ -> ts
data ChatItemInfo = ChatItemInfo
diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs
index 9abc8e4644..d3cdbcf3e4 100644
--- a/src/Simplex/Chat/Messages/CIContent.hs
+++ b/src/Simplex/Chat/Messages/CIContent.hs
@@ -19,12 +19,8 @@ import Data.Int (Int64)
import Data.Text (Text)
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
import Data.Type.Equality
-import Data.Typeable (Typeable)
import Data.Word (Word32)
-import Database.SQLite.Simple (ResultError (..), SQLData (..))
-import Database.SQLite.Simple.FromField (Field, FromField (..), returnError)
-import Database.SQLite.Simple.Internal (Field (..))
-import Database.SQLite.Simple.Ok
+import Database.SQLite.Simple.FromField (FromField (..))
import Database.SQLite.Simple.ToField (ToField (..))
import GHC.Generics (Generic)
import Simplex.Chat.Protocol
@@ -50,14 +46,6 @@ instance FromField AMsgDirection where fromField = fromIntField_ $ fmap fromMsgD
instance ToField MsgDirection where toField = toField . msgDirectionInt
-fromIntField_ :: Typeable a => (Int64 -> Maybe a) -> Field -> Ok a
-fromIntField_ fromInt = \case
- f@(Field (SQLInteger i) _) ->
- case fromInt i of
- Just x -> Ok x
- _ -> returnError ConversionFailed f ("invalid integer: " <> show i)
- f -> returnError ConversionFailed f "expecting SQLInteger column type"
-
data SMsgDirection (d :: MsgDirection) where
SMDRcv :: SMsgDirection 'MDRcv
SMDSnd :: SMsgDirection 'MDSnd
diff --git a/src/Simplex/Chat/Migrations/M20231010_member_settings.hs b/src/Simplex/Chat/Migrations/M20231010_member_settings.hs
new file mode 100644
index 0000000000..e31203e572
--- /dev/null
+++ b/src/Simplex/Chat/Migrations/M20231010_member_settings.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+module Simplex.Chat.Migrations.M20231010_member_settings where
+
+import Database.SQLite.Simple (Query)
+import Database.SQLite.Simple.QQ (sql)
+
+m20231010_member_settings :: Query
+m20231010_member_settings =
+ [sql|
+ALTER TABLE group_members ADD COLUMN show_messages INTEGER NOT NULL DEFAULT 1;
+|]
+
+down_m20231010_member_settings :: Query
+down_m20231010_member_settings =
+ [sql|
+ALTER TABLE group_members DROP COLUMN show_messages;
+|]
diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql
index 542acbbebd..7308ef89ff 100644
--- a/src/Simplex/Chat/Migrations/chat_schema.sql
+++ b/src/Simplex/Chat/Migrations/chat_schema.sql
@@ -145,6 +145,7 @@ CREATE TABLE group_members(
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL),
member_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL,
+ show_messages INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs
index bbdddf8ce0..0f69efe7c0 100644
--- a/src/Simplex/Chat/Protocol.hs
+++ b/src/Simplex/Chat/Protocol.hs
@@ -378,6 +378,11 @@ mcExtMsgContent = \case
MCQuote _ c -> c
MCForward c -> c
+isQuote :: MsgContainer -> Bool
+isQuote = \case
+ MCQuote {} -> True
+ _ -> False
+
data LinkPreview = LinkPreview {uri :: Text, title :: Text, description :: Text, image :: ImageData, content :: Maybe LinkContent}
deriving (Eq, Show, Generic)
diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs
index c9e846a81f..3ef77cbb65 100644
--- a/src/Simplex/Chat/Store/Connections.hs
+++ b/src/Simplex/Chat/Store/Connections.hs
@@ -79,10 +79,10 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0
|]
(userId, contactId)
- toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool, ContactStatus) :. (Maybe Bool, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)] -> Either StoreError Contact
+ toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)] -> Either StoreError Contact
toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)] =
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
- chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_, sendRcpts, favorite}
+ chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent}
toContact' _ _ _ = Left $ SEInternalError "referenced contact not found"
@@ -97,11 +97,11 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.created_at, g.updated_at, g.chat_ts,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
- mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
+ mu.member_status, mu.show_messages, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
-- GroupInfo {membership = GroupMember {memberProfile}}
pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences,
-- from GroupMember
- m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
+ m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.show_messages,
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs
index 20fb8c7217..30e45a82dc 100644
--- a/src/Simplex/Chat/Store/Groups.hs
+++ b/src/Simplex/Chat/Store/Groups.hs
@@ -92,6 +92,7 @@ module Simplex.Chat.Store.Groups
associateContactWithMemberRecord,
deleteOldProbes,
updateGroupSettings,
+ updateGroupMemberSettings,
getXGrpMemIntroContDirect,
getXGrpMemIntroContGroup,
getHostConnId,
@@ -131,30 +132,31 @@ import Simplex.Messaging.Util (eitherToMaybe, ($>>=), (<$$>))
import Simplex.Messaging.Version
import UnliftIO.STM
-type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe Bool, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime) :. GroupMemberRow
+type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime) :. GroupMemberRow
-type GroupMemberRow = ((Int64, Int64, MemberId, GroupMemberRole, GroupMemberCategory, GroupMemberStatus) :. (Maybe Int64, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences))
+type GroupMemberRow = ((Int64, Int64, MemberId, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Bool) :. (Maybe Int64, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences))
-type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus) :. (Maybe Int64, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences))
+type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe Bool) :. (Maybe Int64, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences))
toGroupInfo :: Int64 -> GroupInfoRow -> GroupInfo
toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs) :. userMemberRow) =
let membership = toGroupMember userContactId userMemberRow
- chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_, sendRcpts, favorite}
+ chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
fullGroupPreferences = mergeGroupPreferences groupPreferences
groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences}
in GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs}
toGroupMember :: Int64 -> GroupMemberRow -> GroupMember
-toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences)) =
+toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus, showMessages) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences)) =
let memberProfile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
+ memberSettings = GroupMemberSettings {showMessages}
invitedBy = toInvitedBy userContactId invitedById
activeConn = Nothing
in GroupMember {..}
toMaybeGroupMember :: Int64 -> MaybeGroupMemberRow -> Maybe GroupMember
-toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just memberId, Just memberRole, Just memberCategory, Just memberStatus) :. (invitedById, Just localDisplayName, memberContactId, Just memberContactProfileId, Just profileId, Just displayName, Just fullName, image, contactLink, Just localAlias, contactPreferences)) =
- Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, contactPreferences))
+toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just memberId, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages) :. (invitedById, Just localDisplayName, memberContactId, Just memberContactProfileId, Just profileId, Just displayName, Just fullName, image, contactLink, Just localAlias, contactPreferences)) =
+ Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus, showMessages) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, contactPreferences))
toMaybeGroupMember _ _ = Nothing
createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> ConnReqContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO ()
@@ -250,11 +252,11 @@ getGroupAndMember db User {userId, userContactId} groupMemberId =
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.created_at, g.updated_at, g.chat_ts,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
- mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
+ mu.member_status, mu.show_messages, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
-- GroupInfo {membership = GroupMember {memberProfile}}
pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences,
-- from GroupMember
- m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
+ m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.show_messages,
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
@@ -300,7 +302,7 @@ createNewGroup db gVar user@User {userId} groupProfile = ExceptT $ do
insertedRowId db
memberId <- liftIO $ encodedRandomBytes gVar 12
membership <- createContactMemberInv_ db user groupId user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing currentTs
- let chatSettings = ChatSettings {enableNtfs = True, sendRcpts = Nothing, favorite = False}
+ let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False}
pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId = Nothing, chatSettings, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs}
-- | creates a new group record for the group the current user was invited to, or returns an existing one
@@ -345,7 +347,7 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId, activeCo
insertedRowId db
GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId contact fromMember GCHostMember GSMemInvited IBUnknown Nothing currentTs
membership <- createContactMemberInv_ db user groupId user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfileId currentTs
- let chatSettings = ChatSettings {enableNtfs = True, sendRcpts = Nothing, favorite = False}
+ let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False}
pure (GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId = customUserProfileId, chatSettings, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs}, groupMemberId)
getHostMemberId_ :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO GroupMemberId
@@ -369,6 +371,7 @@ createContactMemberInv_ db User {userId, userContactId} groupId userOrContact Me
memberRole,
memberCategory,
memberStatus,
+ memberSettings = defaultMemberSettings,
invitedBy,
localDisplayName,
memberProfile,
@@ -493,7 +496,7 @@ getUserGroupDetails db User {userId, userContactId} _contactId_ search_ =
db
[sql|
SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.created_at, g.updated_at, g.chat_ts,
- mu.group_member_id, g.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status,
+ mu.group_member_id, g.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.show_messages,
mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences
FROM groups g
JOIN group_profiles gp USING (group_profile_id)
@@ -558,7 +561,7 @@ groupMemberQuery :: Query
groupMemberQuery =
[sql|
SELECT
- m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
+ m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.show_messages,
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
@@ -665,6 +668,7 @@ createNewContactMember db gVar User {userId, userContactId} groupId Contact {con
memberRole,
memberCategory = GCInviteeMember,
memberStatus = GSMemInvited,
+ memberSettings = defaultMemberSettings,
invitedBy = IBUser,
localDisplayName,
memberProfile = profile,
@@ -815,7 +819,8 @@ createNewMember_
|]
(groupId, memberId, memberRole, memberCategory, memberStatus, invitedById, userId, localDisplayName, memberContactId, memberContactProfileId, createdAt, createdAt)
groupMemberId <- insertedRowId db
- pure GroupMember {groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus, invitedBy, localDisplayName, memberProfile = toLocalProfile memberContactProfileId memberProfile "", memberContactId, memberContactProfileId, activeConn}
+ let memberSettings = defaultMemberSettings
+ pure GroupMember {groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus, memberSettings, invitedBy, localDisplayName, memberProfile = toLocalProfile memberContactProfileId memberProfile "", memberContactId, memberContactProfileId, activeConn}
checkGroupMemberHasItems :: DB.Connection -> User -> GroupMember -> IO (Maybe ChatItemId)
checkGroupMemberHasItems db User {userId} GroupMember {groupMemberId, groupId} =
@@ -1013,11 +1018,11 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} =
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.created_at, g.updated_at, g.chat_ts,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
- mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
+ mu.member_status, mu.show_messages, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
-- GroupInfo {membership = GroupMember {memberProfile}}
pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences,
-- via GroupMember
- m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
+ m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.show_messages,
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
@@ -1106,7 +1111,7 @@ getGroupInfo db User {userId, userContactId} groupId =
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.created_at, g.updated_at, g.chat_ts,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
- mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
+ mu.member_status, mu.show_messages, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences
FROM groups g
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
@@ -1502,6 +1507,18 @@ updateGroupSettings :: DB.Connection -> User -> Int64 -> ChatSettings -> IO ()
updateGroupSettings db User {userId} groupId ChatSettings {enableNtfs, sendRcpts, favorite} =
DB.execute db "UPDATE groups SET enable_ntfs = ?, send_rcpts = ?, favorite = ? WHERE user_id = ? AND group_id = ?" (enableNtfs, sendRcpts, favorite, userId, groupId)
+updateGroupMemberSettings :: DB.Connection -> User -> GroupId -> GroupMemberId -> GroupMemberSettings -> IO ()
+updateGroupMemberSettings db User {userId} gId gMemberId GroupMemberSettings {showMessages} = do
+ currentTs <- getCurrentTime
+ DB.execute
+ db
+ [sql|
+ UPDATE group_members
+ SET show_messages = ?, updated_at = ?
+ WHERE user_id = ? AND group_id = ? AND group_member_id = ?
+ |]
+ (showMessages, currentTs, userId, gId, gMemberId)
+
getXGrpMemIntroContDirect :: DB.Connection -> User -> Contact -> IO (Maybe (Int64, XGrpMemIntroCont))
getXGrpMemIntroContDirect db User {userId} Contact {contactId} = do
fmap join . maybeFirstRow toCont $
diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs
index 9ad0e8edc8..0f9abaa465 100644
--- a/src/Simplex/Chat/Store/Messages.hs
+++ b/src/Simplex/Chat/Store/Messages.hs
@@ -4,6 +4,7 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
@@ -50,6 +51,7 @@ module Simplex.Chat.Store.Messages
deleteGroupChatItem,
updateGroupChatItemModerated,
markGroupChatItemDeleted,
+ markGroupChatItemBlocked,
updateDirectChatItemsRead,
getDirectUnreadTimedItems,
setDirectChatItemDeleteAt,
@@ -438,7 +440,7 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe
SELECT i.chat_item_id,
-- GroupMember
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category,
- m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
+ m.member_status, m.show_messages, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
@@ -548,7 +550,7 @@ getGroupChatPreviews_ db User {userId, userContactId} = do
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.created_at, g.updated_at, g.chat_ts,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
- mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
+ mu.member_status, mu.show_messages, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences,
-- ChatStats
COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), g.unread_chat,
@@ -558,17 +560,17 @@ getGroupChatPreviews_ db User {userId, userContactId} = do
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
-- Maybe GroupMember - sender
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category,
- m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
+ m.member_status, m.show_messages, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
-- quoted ChatItem
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent,
-- quoted GroupMember
rm.group_member_id, rm.group_id, rm.member_id, rm.member_role, rm.member_category,
- rm.member_status, rm.invited_by, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id,
+ rm.member_status, rm.show_messages, rm.invited_by, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id,
rp.display_name, rp.full_name, rp.image, rp.contact_link, rp.local_alias, rp.preferences,
-- deleted by GroupMember
dbm.group_member_id, dbm.group_id, dbm.member_id, dbm.member_role, dbm.member_category,
- dbm.member_status, dbm.invited_by, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id,
+ dbm.member_status, dbm.show_messages, dbm.invited_by, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id,
dbp.display_name, dbp.full_name, dbp.image, dbp.contact_link, dbp.local_alias, dbp.preferences
FROM groups g
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
@@ -962,9 +964,9 @@ type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath,
type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe Bool)
-type ChatItemRow = (Int64, ChatItemTs, AMsgDirection, Text, Text, ACIStatus, Maybe SharedMsgId) :. (Bool, Maybe UTCTime, Maybe Bool, UTCTime, UTCTime) :. ChatItemModeRow :. MaybeCIFIleRow
+type ChatItemRow = (Int64, ChatItemTs, AMsgDirection, Text, Text, ACIStatus, Maybe SharedMsgId) :. (Int, Maybe UTCTime, Maybe Bool, UTCTime, UTCTime) :. ChatItemModeRow :. MaybeCIFIleRow
-type MaybeChatItemRow = (Maybe Int64, Maybe ChatItemTs, Maybe AMsgDirection, Maybe Text, Maybe Text, Maybe ACIStatus, Maybe SharedMsgId) :. (Maybe Bool, Maybe UTCTime, Maybe Bool, Maybe UTCTime, Maybe UTCTime) :. ChatItemModeRow :. MaybeCIFIleRow
+type MaybeChatItemRow = (Maybe Int64, Maybe ChatItemTs, Maybe AMsgDirection, Maybe Text, Maybe Text, Maybe ACIStatus, Maybe SharedMsgId) :. (Maybe Int, Maybe UTCTime, Maybe Bool, Maybe UTCTime, Maybe UTCTime) :. ChatItemModeRow :. MaybeCIFIleRow
type QuoteRow = (Maybe ChatItemId, Maybe SharedMsgId, Maybe UTCTime, Maybe MsgContent, Maybe Bool)
@@ -1007,7 +1009,9 @@ toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentT
badItem = Left $ SEBadChatItem itemId
ciMeta :: CIContent d -> CIStatus d -> CIMeta 'CTDirect d
ciMeta content status =
- let itemDeleted' = if itemDeleted then Just (CIDeleted @'CTDirect deletedTs) else Nothing
+ let itemDeleted' = case itemDeleted of
+ DBCINotDeleted -> Nothing
+ _ -> Just (CIDeleted @'CTDirect deletedTs)
itemEdited' = fromMaybe False itemEdited
in mkCIMeta itemId content itemText status sharedMsgId itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs createdAt updatedAt
ciTimed :: Maybe CITimed
@@ -1063,10 +1067,10 @@ toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir,
badItem = Left $ SEBadChatItem itemId
ciMeta :: CIContent d -> CIStatus d -> CIMeta 'CTGroup d
ciMeta content status =
- let itemDeleted' =
- if itemDeleted
- then Just (maybe (CIDeleted @'CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_)
- else Nothing
+ let itemDeleted' = case itemDeleted of
+ DBCINotDeleted -> Nothing
+ DBCIBlocked -> Just (CIBlocked @'CTGroup deletedTs)
+ _ -> Just (maybe (CIDeleted @'CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_)
itemEdited' = fromMaybe False itemEdited
in mkCIMeta itemId content itemText status sharedMsgId itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs createdAt updatedAt
ciTimed :: Maybe CITimed
@@ -1225,8 +1229,8 @@ createChatItemVersion db itemId itemVersionTs msgContent =
|]
(itemId, toMCText msgContent, itemVersionTs)
-deleteDirectChatItem :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> IO ()
-deleteDirectChatItem db User {userId} Contact {contactId} (CChatItem _ ci) = do
+deleteDirectChatItem :: DB.Connection -> User -> Contact -> ChatItem 'CTDirect d -> IO ()
+deleteDirectChatItem db User {userId} Contact {contactId} ci = do
let itemId = chatItemId' ci
deleteChatItemMessages_ db itemId
deleteChatItemVersions_ db itemId
@@ -1257,8 +1261,8 @@ deleteChatItemVersions_ :: DB.Connection -> ChatItemId -> IO ()
deleteChatItemVersions_ db itemId =
DB.execute db "DELETE FROM chat_item_versions WHERE chat_item_id = ?" (Only itemId)
-markDirectChatItemDeleted :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> MessageId -> UTCTime -> IO ()
-markDirectChatItemDeleted db User {userId} Contact {contactId} (CChatItem _ ci) msgId deletedTs = do
+markDirectChatItemDeleted :: DB.Connection -> User -> Contact -> ChatItem 'CTDirect d -> MessageId -> UTCTime -> IO (ChatItem 'CTDirect d)
+markDirectChatItemDeleted db User {userId} Contact {contactId} ci@ChatItem {meta} msgId deletedTs = do
currentTs <- liftIO getCurrentTime
let itemId = chatItemId' ci
insertChatItemMessage_ db itemId msgId currentTs
@@ -1266,10 +1270,11 @@ markDirectChatItemDeleted db User {userId} Contact {contactId} (CChatItem _ ci)
db
[sql|
UPDATE chat_items
- SET item_deleted = 1, item_deleted_ts = ?, updated_at = ?
+ SET item_deleted = ?, item_deleted_ts = ?, updated_at = ?
WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?
|]
- (deletedTs, currentTs, userId, contactId, itemId)
+ (DBCIDeleted, deletedTs, currentTs, userId, contactId, itemId)
+ pure ci {meta = meta {itemDeleted = Just $ CIDeleted $ Just deletedTs}}
getDirectChatItemBySharedMsgId :: DB.Connection -> User -> ContactId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTDirect)
getDirectChatItemBySharedMsgId db user@User {userId} contactId sharedMsgId = do
@@ -1380,8 +1385,8 @@ updateGroupChatItem_ db User {userId} groupId ChatItem {content, meta} msgId_ =
((content, itemText, itemStatus, itemDeleted', itemDeletedTs', itemEdited, itemLive, updatedAt) :. ciTimedRow itemTimed :. (userId, groupId, itemId))
forM_ msgId_ $ \msgId -> insertChatItemMessage_ db itemId msgId updatedAt
-deleteGroupChatItem :: DB.Connection -> User -> GroupInfo -> CChatItem 'CTGroup -> IO ()
-deleteGroupChatItem db User {userId} g@GroupInfo {groupId} (CChatItem _ ci) = do
+deleteGroupChatItem :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> IO ()
+deleteGroupChatItem db User {userId} g@GroupInfo {groupId} ci = do
let itemId = chatItemId' ci
deleteChatItemMessages_ db itemId
deleteChatItemVersions_ db itemId
@@ -1394,10 +1399,10 @@ deleteGroupChatItem db User {userId} g@GroupInfo {groupId} (CChatItem _ ci) = do
|]
(userId, groupId, itemId)
-updateGroupChatItemModerated :: DB.Connection -> User -> GroupInfo -> CChatItem 'CTGroup -> GroupMember -> UTCTime -> IO AChatItem
-updateGroupChatItemModerated db User {userId} gInfo@GroupInfo {groupId} (CChatItem msgDir ci) m@GroupMember {groupMemberId} deletedTs = do
+updateGroupChatItemModerated :: forall d. MsgDirectionI d => DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> GroupMember -> UTCTime -> IO (ChatItem 'CTGroup d)
+updateGroupChatItemModerated db User {userId} GroupInfo {groupId} ci m@GroupMember {groupMemberId} deletedTs = do
currentTs <- getCurrentTime
- let toContent = msgDirToModeratedContent_ msgDir
+ let toContent = msgDirToModeratedContent_ $ msgDirection @d
toText = ciModeratedText
itemId = chatItemId' ci
deleteChatItemMessages_ db itemId
@@ -1411,24 +1416,47 @@ updateGroupChatItemModerated db User {userId} gInfo@GroupInfo {groupId} (CChatIt
WHERE user_id = ? AND group_id = ? AND chat_item_id = ?
|]
(deletedTs, groupMemberId, toContent, toText, currentTs, userId, groupId, itemId)
- pure $ AChatItem SCTGroup msgDir (GroupChat gInfo) (ci {content = toContent, meta = (meta ci) {itemText = toText, itemDeleted = Just (CIModerated (Just currentTs) m), editable = False}, formattedText = Nothing})
+ pure $ ci {content = toContent, meta = (meta ci) {itemText = toText, itemDeleted = Just (CIModerated (Just currentTs) m), editable = False}, formattedText = Nothing}
-markGroupChatItemDeleted :: DB.Connection -> User -> GroupInfo -> CChatItem 'CTGroup -> MessageId -> Maybe GroupMember -> UTCTime -> IO ()
-markGroupChatItemDeleted db User {userId} GroupInfo {groupId} (CChatItem _ ci) msgId byGroupMember_ deletedTs = do
+pattern DBCINotDeleted :: Int
+pattern DBCINotDeleted = 0
+
+pattern DBCIDeleted :: Int
+pattern DBCIDeleted = 1
+
+pattern DBCIBlocked :: Int
+pattern DBCIBlocked = 2
+
+markGroupChatItemDeleted :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> MessageId -> Maybe GroupMember -> UTCTime -> IO (ChatItem 'CTGroup d)
+markGroupChatItemDeleted db User {userId} GroupInfo {groupId} ci@ChatItem {meta} msgId byGroupMember_ deletedTs = do
currentTs <- liftIO getCurrentTime
let itemId = chatItemId' ci
- deletedByGroupMemberId = case byGroupMember_ of
- Just GroupMember {groupMemberId} -> Just groupMemberId
- _ -> Nothing
+ (deletedByGroupMemberId, itemDeleted) = case byGroupMember_ of
+ Just m@GroupMember {groupMemberId} -> (Just groupMemberId, Just $ CIModerated (Just deletedTs) m)
+ _ -> (Nothing, Just $ CIDeleted @'CTGroup (Just deletedTs))
insertChatItemMessage_ db itemId msgId currentTs
DB.execute
db
[sql|
UPDATE chat_items
- SET item_deleted = 1, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ?
+ SET item_deleted = ?, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ?
WHERE user_id = ? AND group_id = ? AND chat_item_id = ?
|]
- (deletedTs, deletedByGroupMemberId, currentTs, userId, groupId, itemId)
+ (DBCIDeleted, deletedTs, deletedByGroupMemberId, currentTs, userId, groupId, itemId)
+ pure ci {meta = meta {itemDeleted}}
+
+markGroupChatItemBlocked :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup 'MDRcv -> IO (ChatItem 'CTGroup 'MDRcv)
+markGroupChatItemBlocked db User {userId} GroupInfo {groupId} ci@ChatItem {meta} = do
+ deletedTs <- getCurrentTime
+ DB.execute
+ db
+ [sql|
+ UPDATE chat_items
+ SET item_deleted = ?, item_deleted_ts = ?, updated_at = ?
+ WHERE user_id = ? AND group_id = ? AND chat_item_id = ?
+ |]
+ (DBCIBlocked, deletedTs, deletedTs, userId, groupId, chatItemId' ci)
+ pure ci {meta = meta {itemDeleted = Just $ CIBlocked $ Just deletedTs}}
getGroupChatItemBySharedMsgId :: DB.Connection -> User -> GroupId -> GroupMemberId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTGroup)
getGroupChatItemBySharedMsgId db user@User {userId} groupId groupMemberId sharedMsgId = do
@@ -1486,17 +1514,17 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
-- GroupMember
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category,
- m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
+ m.member_status, m.show_messages, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
-- quoted ChatItem
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent,
-- quoted GroupMember
rm.group_member_id, rm.group_id, rm.member_id, rm.member_role, rm.member_category,
- rm.member_status, rm.invited_by, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id,
+ rm.member_status, rm.show_messages, rm.invited_by, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id,
rp.display_name, rp.full_name, rp.image, rp.contact_link, rp.local_alias, rp.preferences,
-- deleted by GroupMember
dbm.group_member_id, dbm.group_id, dbm.member_id, dbm.member_role, dbm.member_category,
- dbm.member_status, dbm.invited_by, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id,
+ dbm.member_status, dbm.show_messages, dbm.invited_by, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id,
dbp.display_name, dbp.full_name, dbp.image, dbp.contact_link, dbp.local_alias, dbp.preferences
FROM chat_items i
LEFT JOIN files f ON f.chat_item_id = i.chat_item_id
diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs
index 5c44b8cded..60783f3664 100644
--- a/src/Simplex/Chat/Store/Migrations.hs
+++ b/src/Simplex/Chat/Store/Migrations.hs
@@ -84,6 +84,7 @@ import Simplex.Chat.Migrations.M20230914_member_probes
import Simplex.Chat.Migrations.M20230926_contact_status
import Simplex.Chat.Migrations.M20231002_conn_initiated
import Simplex.Chat.Migrations.M20231009_via_group_link_uri_hash
+import Simplex.Chat.Migrations.M20231010_member_settings
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -167,7 +168,8 @@ schemaMigrations =
("20230914_member_probes", m20230914_member_probes, Just down_m20230914_member_probes),
("20230926_contact_status", m20230926_contact_status, Just down_m20230926_contact_status),
("20231002_conn_initiated", m20231002_conn_initiated, Just down_m20231002_conn_initiated),
- ("20231009_via_group_link_uri_hash", m20231009_via_group_link_uri_hash, Just down_m20231009_via_group_link_uri_hash)
+ ("20231009_via_group_link_uri_hash", m20231009_via_group_link_uri_hash, Just down_m20231009_via_group_link_uri_hash),
+ ("20231010_member_settings", m20231010_member_settings, Just down_m20231010_member_settings)
]
-- | The list of migrations in ascending order by date
diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs
index 3ff765b75d..2a90b54d7f 100644
--- a/src/Simplex/Chat/Store/Shared.hs
+++ b/src/Simplex/Chat/Store/Shared.hs
@@ -241,20 +241,20 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId =
|]
[":user_id" := userId, ":profile_id" := profileId]
-type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe Bool, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)
+type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)
toContact :: User -> ContactRow :. ConnectionRow -> Contact
toContact user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)) :. connRow) =
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
activeConn = toConnection connRow
- chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_, sendRcpts, favorite}
+ chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent}
toContactOrError :: User -> ContactRow :. MaybeConnectionRow -> Either StoreError Contact
toContactOrError user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)) :. connRow) =
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
- chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_, sendRcpts, favorite}
+ chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
in case toMaybeConnection connRow of
Just activeConn ->
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
diff --git a/src/Simplex/Chat/Terminal/Output.hs b/src/Simplex/Chat/Terminal/Output.hs
index 556e4f792e..a45390e8c3 100644
--- a/src/Simplex/Chat/Terminal/Output.hs
+++ b/src/Simplex/Chat/Terminal/Output.hs
@@ -19,7 +19,7 @@ import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Clock (getCurrentTime)
import Data.Time.LocalTime (getCurrentTimeZone)
-import Simplex.Chat (processChatCommand, chatNtf, contactNtf, groupNtf, userNtf)
+import Simplex.Chat (processChatCommand)
import Simplex.Chat.Controller
import Simplex.Chat.Markdown
import Simplex.Chat.Messages
@@ -28,7 +28,7 @@ import Simplex.Chat.Options
import Simplex.Chat.Protocol (MsgContent (..), msgContentText)
import Simplex.Chat.Styled
import Simplex.Chat.Terminal.Notification (Notification (..), initializeNotifications)
-import Simplex.Chat.Types (Contact, GroupInfo (..), User (..), UserContactRequest (..))
+import Simplex.Chat.Types
import Simplex.Chat.View
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Encoding.String
@@ -140,8 +140,8 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} = d
forever $ do
(_, r) <- atomically $ readTBQueue outputQ
case r of
- CRNewChatItem _ ci -> markChatItemRead ci
- CRChatItemUpdated _ ci -> markChatItemRead ci
+ CRNewChatItem u ci -> markChatItemRead u ci
+ CRChatItemUpdated u ci -> markChatItemRead u ci
_ -> pure ()
let printResp = case logFilePath of
Just path -> if logResponseToFile r then logResponse path else printToTerminal ct
@@ -150,10 +150,10 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} = d
responseString cc liveItems r >>= printResp
responseNotification ct cc r
where
- markChatItemRead (AChatItem _ _ chat item@ChatItem {chatDir, meta = CIMeta {itemStatus}}) =
- case (muted chat chatDir, itemStatus) of
- (False, CISRcvNew) -> do
- let itemId = chatItemId' item
+ markChatItemRead u (AChatItem _ _ chat ci@ChatItem {chatDir, meta = CIMeta {itemStatus}}) =
+ case (chatDirNtf u chat chatDir (isReference ci), itemStatus) of
+ (True, CISRcvNew) -> do
+ let itemId = chatItemId' ci
chatRef = chatInfoToRef chat
void $ runReaderT (runExceptT $ processChatCommand (APIChatRead chatRef (Just (itemId, itemId)))) cc
_ -> pure ()
@@ -161,8 +161,8 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} = d
responseNotification :: ChatTerminal -> ChatController -> ChatResponse -> IO ()
responseNotification t@ChatTerminal {sendNotification} cc = \case
- CRNewChatItem u (AChatItem _ SMDRcv cInfo ChatItem {chatDir, content = CIRcvMsgContent mc, formattedText}) ->
- when (chatNtf u cInfo) $ do
+ CRNewChatItem u (AChatItem _ SMDRcv cInfo ci@ChatItem {chatDir, content = CIRcvMsgContent mc, formattedText}) ->
+ when (chatDirNtf u cInfo chatDir $ isReference ci) $ do
whenCurrUser cc u $ setActiveChat t cInfo
case (cInfo, chatDir) of
(DirectChat ct, _) -> sendNtf (viewContactName ct <> "> ", text)
@@ -170,26 +170,26 @@ responseNotification t@ChatTerminal {sendNotification} cc = \case
_ -> pure ()
where
text = msgText mc formattedText
- CRChatItemUpdated u (AChatItem _ SMDRcv cInfo ChatItem {content = CIRcvMsgContent _}) ->
- whenCurrUser cc u $ when (chatNtf u cInfo) $ setActiveChat t cInfo
- CRContactConnected u ct _ -> when (contactNtf u ct) $ do
+ CRChatItemUpdated u (AChatItem _ SMDRcv cInfo ci@ChatItem {chatDir, content = CIRcvMsgContent _}) ->
+ whenCurrUser cc u $ when (chatDirNtf u cInfo chatDir $ isReference ci) $ setActiveChat t cInfo
+ CRContactConnected u ct _ -> when (contactNtf u ct False) $ do
whenCurrUser cc u $ setActiveContact t ct
sendNtf (viewContactName ct <> "> ", "connected")
CRContactAnotherClient u ct -> do
whenCurrUser cc u $ unsetActiveContact t ct
- when (contactNtf u ct) $ sendNtf (viewContactName ct <> "> ", "connected to another client")
+ when (contactNtf u ct False) $ sendNtf (viewContactName ct <> "> ", "connected to another client")
CRContactsDisconnected srv _ -> serverNtf srv "disconnected"
CRContactsSubscribed srv _ -> serverNtf srv "connected"
CRReceivedGroupInvitation u g ct _ _ ->
- when (contactNtf u ct) $
+ when (contactNtf u ct False) $
sendNtf ("#" <> viewGroupName g <> " " <> viewContactName ct <> "> ", "invited you to join the group")
- CRUserJoinedGroup u g _ -> when (groupNtf u g) $ do
+ CRUserJoinedGroup u g _ -> when (groupNtf u g False) $ do
whenCurrUser cc u $ setActiveGroup t g
sendNtf ("#" <> viewGroupName g, "you are connected to group")
CRJoinedGroupMember u g m ->
- when (groupNtf u g) $ sendNtf ("#" <> viewGroupName g, "member " <> viewMemberName m <> " is connected")
+ when (groupNtf u g False) $ sendNtf ("#" <> viewGroupName g, "member " <> viewMemberName m <> " is connected")
CRConnectedToGroupMember u g m _ ->
- when (groupNtf u g) $ sendNtf ("#" <> viewGroupName g, "member " <> viewMemberName m <> " is connected")
+ when (groupNtf u g False) $ sendNtf ("#" <> viewGroupName g, "member " <> viewMemberName m <> " is connected")
CRReceivedContactRequest u UserContactRequest {localDisplayName = n} ->
when (userNtf u) $ sendNtf (viewName n <> ">", "wants to connect to you")
_ -> pure ()
diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs
index de56baad92..83d0664a04 100644
--- a/src/Simplex/Chat/Types.hs
+++ b/src/Simplex/Chat/Types.hs
@@ -37,7 +37,11 @@ import Data.Maybe (isJust)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Clock (UTCTime)
-import Database.SQLite.Simple.FromField (FromField (..))
+import Data.Typeable (Typeable)
+import Database.SQLite.Simple (ResultError (..), SQLData (..))
+import Database.SQLite.Simple.FromField (returnError, FromField(..))
+import Database.SQLite.Simple.Internal (Field (..))
+import Database.SQLite.Simple.Ok
import Database.SQLite.Simple.ToField (ToField (..))
import GHC.Generics (Generic)
import Simplex.Chat.Types.Preferences
@@ -46,7 +50,7 @@ import Simplex.FileTransfer.Description (FileDigest)
import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, SAEntity (..), UserId)
import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
import Simplex.Messaging.Encoding.String
-import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, sumTypeJSON, taggedObjectJSON)
+import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, sumTypeJSON, taggedObjectJSON, enumJSON)
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI)
import Simplex.Messaging.Util ((<$?>))
import Simplex.Messaging.Version
@@ -385,7 +389,7 @@ contactAndGroupIds = \case
-- TODO when more settings are added we should create another type to allow partial setting updates (with all Maybe properties)
data ChatSettings = ChatSettings
- { enableNtfs :: Bool,
+ { enableNtfs :: MsgFilter,
sendRcpts :: Maybe Bool,
favorite :: Bool
}
@@ -396,13 +400,48 @@ instance ToJSON ChatSettings where toEncoding = J.genericToEncoding J.defaultOpt
defaultChatSettings :: ChatSettings
defaultChatSettings =
ChatSettings
- { enableNtfs = True,
+ { enableNtfs = MFAll,
sendRcpts = Nothing,
favorite = False
}
-pattern DisableNtfs :: ChatSettings
-pattern DisableNtfs <- ChatSettings {enableNtfs = False}
+chatHasNtfs :: ChatSettings -> Bool
+chatHasNtfs ChatSettings {enableNtfs} = enableNtfs /= MFNone
+
+data MsgFilter = MFNone | MFAll | MFMentions
+ deriving (Eq, Show, Generic)
+
+instance FromJSON MsgFilter where
+ parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "MF"
+
+instance ToJSON MsgFilter where
+ toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "MF"
+ toJSON = J.genericToJSON . enumJSON $ dropPrefix "MF"
+
+instance FromField MsgFilter where fromField = fromIntField_ msgFilterIntP
+
+instance ToField MsgFilter where toField = toField . msgFilterInt
+
+msgFilterInt :: MsgFilter -> Int
+msgFilterInt = \case
+ MFNone -> 0
+ MFAll -> 1
+ MFMentions -> 2
+
+msgFilterIntP :: Int64 -> Maybe MsgFilter
+msgFilterIntP = \case
+ 0 -> Just MFNone
+ 1 -> Just MFAll
+ 2 -> Just MFMentions
+ _ -> Just MFAll
+
+fromIntField_ :: Typeable a => (Int64 -> Maybe a) -> Field -> Ok a
+fromIntField_ fromInt = \case
+ f@(Field (SQLInteger i) _) ->
+ case fromInt i of
+ Just x -> Ok x
+ _ -> returnError ConversionFailed f ("invalid integer: " <> show i)
+ f -> returnError ConversionFailed f "expecting SQLInteger column type"
featureAllowed :: SChatFeature f -> (PrefEnabled -> Bool) -> Contact -> Bool
featureAllowed feature forWhom Contact {mergedPreferences} =
@@ -630,6 +669,7 @@ data GroupMember = GroupMember
memberRole :: GroupMemberRole,
memberCategory :: GroupMemberCategory,
memberStatus :: GroupMemberStatus,
+ memberSettings :: GroupMemberSettings,
invitedBy :: InvitedBy,
localDisplayName :: ContactName,
-- for membership, memberProfile can be either user's profile or incognito profile, based on memberIncognito test.
@@ -764,6 +804,16 @@ instance ToJSON GroupMemberRole where
toJSON = strToJSON
toEncoding = strToJEncoding
+data GroupMemberSettings = GroupMemberSettings
+ { showMessages :: Bool
+ }
+ deriving (Eq, Show, Generic, FromJSON)
+
+instance ToJSON GroupMemberSettings where toEncoding = J.genericToEncoding J.defaultOptions
+
+defaultMemberSettings :: GroupMemberSettings
+defaultMemberSettings = GroupMemberSettings {showMessages = True}
+
newtype Probe = Probe {unProbe :: ByteString}
deriving (Eq, Show)
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index f60b7cd82f..f465375f12 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -102,15 +102,15 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code]
CRContactCode u ct code -> ttyUser u $ viewContactCode ct code testView
CRGroupMemberCode u g m code -> ttyUser u $ viewGroupMemberCode g m code testView
- CRNewChatItem u (AChatItem _ _ chat item) -> ttyUser u $ unmuted chat item $ viewChatItem chat item False ts tz <> viewItemReactions item
+ CRNewChatItem u (AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewChatItem chat item False ts tz <> viewItemReactions item
CRChatItems u _ chatItems -> ttyUser u $ concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts tz <> viewItemReactions item) chatItems
CRChatItemInfo u ci ciInfo -> ttyUser u $ viewChatItemInfo ci ciInfo tz
CRChatItemId u itemId -> ttyUser u [plain $ maybe "no item" show itemId]
CRChatItemStatusUpdated u ci -> ttyUser u $ viewChatItemStatusUpdated ci ts tz testView showReceipts
- CRChatItemUpdated u (AChatItem _ _ chat item) -> ttyUser u $ unmuted chat item $ viewItemUpdate chat item liveItems ts tz
+ CRChatItemUpdated u (AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewItemUpdate chat item liveItems ts tz
CRChatItemNotChanged u ci -> ttyUser u $ viewItemNotChanged ci
- CRChatItemDeleted u (AChatItem _ _ chat deletedItem) toItem byUser timed -> ttyUser u $ unmuted chat deletedItem $ viewItemDelete chat deletedItem toItem byUser timed ts tz testView
- CRChatItemReaction u added (ACIReaction _ _ chat reaction) -> ttyUser u $ unmutedReaction chat reaction $ viewItemReaction showReactions chat reaction added ts tz
+ CRChatItemDeleted u (AChatItem _ _ chat deletedItem) toItem byUser timed -> ttyUser u $ unmuted u chat deletedItem $ viewItemDelete chat deletedItem toItem byUser timed ts tz testView
+ CRChatItemReaction u added (ACIReaction _ _ chat reaction) -> ttyUser u $ unmutedReaction u chat reaction $ viewItemReaction showReactions chat reaction added ts tz
CRChatItemDeletedNotFound u Contact {localDisplayName = c} _ -> ttyUser u [ttyFrom $ c <> "> [deleted - original message not found]"]
CRBroadcastSent u mc s f t -> ttyUser u $ viewSentBroadcast mc s f ts tz t
CRMsgIntegrityError u mErr -> ttyUser u $ viewMsgIntegrityError mErr
@@ -349,24 +349,56 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
viewErrorsSummary summary s = [ttyError (T.pack . show $ length summary) <> s <> " (run with -c option to show each error)" | not (null summary)]
contactList :: [ContactRef] -> String
contactList cs = T.unpack . T.intercalate ", " $ map (\ContactRef {localDisplayName = n} -> "@" <> n) cs
- unmuted :: ChatInfo c -> ChatItem c d -> [StyledString] -> [StyledString]
- unmuted chat ChatItem {chatDir} = unmuted' chat chatDir
- unmutedReaction :: ChatInfo c -> CIReaction c d -> [StyledString] -> [StyledString]
- unmutedReaction chat CIReaction {chatDir} = unmuted' chat chatDir
- unmuted' :: ChatInfo c -> CIDirection c d -> [StyledString] -> [StyledString]
- unmuted' chat chatDir s
- | muted chat chatDir = []
- | otherwise = s
+ unmuted :: User -> ChatInfo c -> ChatItem c d -> [StyledString] -> [StyledString]
+ unmuted u chat ci@ChatItem {chatDir} = unmuted' u chat chatDir $ isReference ci
+ unmutedReaction :: User -> ChatInfo c -> CIReaction c d -> [StyledString] -> [StyledString]
+ unmutedReaction u chat CIReaction {chatDir} = unmuted' u chat chatDir False
+ unmuted' :: User -> ChatInfo c -> CIDirection c d -> Bool -> [StyledString] -> [StyledString]
+ unmuted' u chat chatDir reference s
+ | chatDirNtf u chat chatDir reference = s
+ | otherwise = []
+
+userNtf :: User -> Bool
+userNtf User {showNtfs, activeUser} = showNtfs || activeUser
+
+chatNtf :: User -> ChatInfo c -> Bool -> Bool
+chatNtf user cInfo reference = case cInfo of
+ DirectChat ct -> contactNtf user ct reference
+ GroupChat g -> groupNtf user g reference
+ _ -> False
+
+chatDirNtf :: User -> ChatInfo c -> CIDirection c d -> Bool -> Bool
+chatDirNtf user cInfo chatDir reference = case (cInfo, chatDir) of
+ (DirectChat ct, CIDirectRcv) -> contactNtf user ct reference
+ (GroupChat g, CIGroupRcv m) -> groupNtf user g reference && showMessages (memberSettings m)
+ _ -> True
+
+contactNtf :: User -> Contact -> Bool -> Bool
+contactNtf user Contact {chatSettings} reference =
+ userNtf user && showMessageNtf chatSettings reference
+
+groupNtf :: User -> GroupInfo -> Bool -> Bool
+groupNtf user GroupInfo {chatSettings} reference =
+ userNtf user && showMessageNtf chatSettings reference
+
+showMessageNtf :: ChatSettings -> Bool -> Bool
+showMessageNtf ChatSettings {enableNtfs} reference =
+ enableNtfs == MFAll || (reference && enableNtfs == MFMentions)
chatItemDeletedText :: ChatItem c d -> Maybe GroupMember -> Maybe Text
-chatItemDeletedText ci membership_ = deletedStateToText <$> chatItemDeletedState ci
+chatItemDeletedText ChatItem {meta = CIMeta {itemDeleted}, content} membership_ =
+ deletedText <$> itemDeleted
where
- deletedStateToText = \CIDeletedState {markedDeleted, deletedByMember} ->
- if markedDeleted
- then "marked deleted" <> byMember deletedByMember
- else "deleted" <> byMember deletedByMember
- byMember m_ = case (m_, membership_) of
- (Just GroupMember {groupMemberId = mId, localDisplayName = n}, Just GroupMember {groupMemberId = membershipId}) ->
+ deletedText = \case
+ CIModerated _ m -> markedDeleted content <> byMember m
+ CIDeleted _ -> markedDeleted content
+ CIBlocked _ -> "blocked"
+ markedDeleted = \case
+ CISndModerated -> "deleted"
+ CIRcvModerated -> "deleted"
+ _ -> "marked deleted"
+ byMember GroupMember {groupMemberId = mId, localDisplayName = n} = case membership_ of
+ Just GroupMember {groupMemberId = membershipId} ->
" by " <> if mId == membershipId then "you" else n
_ -> ""
@@ -385,12 +417,6 @@ viewUsersList = mapMaybe userInfo . sortOn ldn
<> ["muted" | not showNtfs]
<> [plain ("unread: " <> show count) | count /= 0]
-muted :: ChatInfo c -> CIDirection c d -> Bool
-muted chat chatDir = case (chat, chatDir) of
- (DirectChat Contact {chatSettings = DisableNtfs}, CIDirectRcv) -> True
- (GroupChat GroupInfo {chatSettings = DisableNtfs}, CIGroupRcv _) -> True
- _ -> False
-
viewGroupSubscribed :: GroupInfo -> [StyledString]
viewGroupSubscribed g = [membershipIncognito g <> ttyFullGroup g <> ": connected to server(s)"]
@@ -692,7 +718,7 @@ viewContactsList =
in map (\ct -> ctIncognito ct <> ttyFullContact ct <> muted' ct <> alias ct) . sortOn ldn
where
muted' Contact {chatSettings, localDisplayName = ldn}
- | enableNtfs chatSettings = ""
+ | chatHasNtfs chatSettings = ""
| otherwise = " (muted, you can " <> highlight ("/unmute @" <> ldn) <> ")"
alias Contact {profile = LocalProfile {localAlias}}
| localAlias == "" = ""
@@ -825,22 +851,25 @@ viewGroupMembers :: Group -> [StyledString]
viewGroupMembers (Group GroupInfo {membership} members) = map groupMember . filter (not . removedOrLeft) $ membership : members
where
removedOrLeft m = let s = memberStatus m in s == GSMemRemoved || s == GSMemLeft
- groupMember m = memIncognito m <> ttyFullMember m <> ": " <> role m <> ", " <> category m <> status m
- role :: GroupMember -> StyledString
- role m = plain . strEncode $ m.memberRole
+ groupMember m = memIncognito m <> ttyFullMember m <> ": " <> plain (intercalate ", " $ [role m] <> category m <> status m <> muted m)
+ role :: GroupMember -> String
+ role m = B.unpack . strEncode $ m.memberRole
category m = case memberCategory m of
- GCUserMember -> "you, "
- GCInviteeMember -> "invited, "
- GCHostMember -> "host, "
- _ -> ""
+ GCUserMember -> ["you"]
+ GCInviteeMember -> ["invited"]
+ GCHostMember -> ["host"]
+ _ -> []
status m = case memberStatus m of
- GSMemRemoved -> "removed"
- GSMemLeft -> "left"
- GSMemInvited -> "not yet joined"
- GSMemConnected -> "connected"
- GSMemComplete -> "connected"
- GSMemCreator -> "created group"
- _ -> ""
+ GSMemRemoved -> ["removed"]
+ GSMemLeft -> ["left"]
+ GSMemInvited -> ["not yet joined"]
+ GSMemConnected -> ["connected"]
+ GSMemComplete -> ["connected"]
+ GSMemCreator -> ["created group"]
+ _ -> []
+ muted m
+ | showMessages (memberSettings m) = []
+ | otherwise = ["blocked"]
viewContactConnected :: Contact -> Maybe Profile -> Bool -> [StyledString]
viewContactConnected ct userIncognitoProfile testView =
@@ -863,7 +892,7 @@ viewGroupsList gs = map groupSS $ sortOn (ldn_ . fst) gs
where
ldn_ :: GroupInfo -> Text
ldn_ g = T.toLower g.localDisplayName
- groupSS (g@GroupInfo {membership, chatSettings}, GroupSummary {currentMembers}) =
+ groupSS (g@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}}, GroupSummary {currentMembers}) =
case memberStatus membership of
GSMemInvited -> groupInvitation' g
s -> membershipIncognito g <> ttyFullGroup g <> viewMemberStatus s
@@ -872,9 +901,13 @@ viewGroupsList gs = map groupSS $ sortOn (ldn_ . fst) gs
GSMemRemoved -> delete "you are removed"
GSMemLeft -> delete "you left"
GSMemGroupDeleted -> delete "group deleted"
- _
- | enableNtfs chatSettings -> " (" <> memberCount <> ")"
- | otherwise -> " (" <> memberCount <> ", muted, you can " <> highlight ("/unmute #" <> viewGroupName g) <> ")"
+ _ -> " (" <> memberCount <>
+ case enableNtfs of
+ MFAll -> ")"
+ MFNone -> ", muted, " <> unmute
+ MFMentions -> ", mentions only, " <> unmute
+ where
+ unmute = "you can " <> highlight ("/unmute #" <> viewGroupName g) <> ")"
delete reason = " (" <> reason <> ", delete local copy: " <> highlight ("/d #" <> viewGroupName g) <> ")"
memberCount = sShow currentMembers <> " member" <> if currentMembers == 1 then "" else "s"
diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs
index 47333906bb..b4c3c53cd9 100644
--- a/tests/ChatTests/Direct.hs
+++ b/tests/ChatTests/Direct.hs
@@ -70,7 +70,7 @@ chatDirectTests = do
it "should not subscribe in NSE and subscribe in the app" testSubscribeAppNSE
describe "mute/unmute messages" $ do
it "mute/unmute contact" testMuteContact
- it "mute/unmute group" testMuteGroup
+ it "mute/unmute group and member" testMuteGroup
describe "multiple users" $ do
it "create second user" testCreateSecondUser
it "multiple users subscribe and receive messages after restart" testUsersSubscribeAfterRestart
@@ -1196,14 +1196,79 @@ testMuteGroup =
concurrently_
(bob )
(cath <# "#team alice> hi")
+ bob #> "#team hello"
+ concurrently_
+ (alice <# "#team bob> hello")
+ (cath <# "#team bob> hello")
+ cath `send` "> #team (hello) hello too!"
+ cath <# "#team > bob hello"
+ cath <## " hello too!"
+ concurrently_
+ (bob )
+ ( do alice <# "#team cath> > bob hello"
+ alice <## " hello too!"
+ )
+ bob ##> "/unmute mentions #team"
+ bob <## "ok"
+ alice `send` "> #team @bob (hello) hey bob!"
+ alice <# "#team > bob hello"
+ alice <## " hey bob!"
+ concurrently_
+ ( do bob <# "#team alice> > bob hello"
+ bob <## " hey bob!"
+ )
+ ( do cath <# "#team alice> > bob hello"
+ cath <## " hey bob!"
+ )
+ alice `send` "> #team @cath (hello) hey cath!"
+ alice <# "#team > cath hello too!"
+ alice <## " hey cath!"
+ concurrently_
+ (bob )
+ ( do cath <# "#team alice> > cath hello too!"
+ cath <## " hey cath!"
+ )
bob ##> "/gs"
- bob <## "#team (3 members, muted, you can /unmute #team)"
+ bob <## "#team (3 members, mentions only, you can /unmute #team)"
bob ##> "/unmute #team"
bob <## "ok"
alice #> "#team hi again"
concurrently_
(bob <# "#team alice> hi again")
(cath <# "#team alice> hi again")
+ bob ##> "/block #team alice"
+ bob <## "ok"
+ bob ##> "/ms team"
+ bob <## "bob (Bob): admin, you, connected"
+ bob <## "alice (Alice): owner, host, connected, blocked"
+ bob <## "cath (Catherine): admin, connected"
+ alice #> "#team test 1"
+ concurrently_
+ (bob )
+ (cath <# "#team alice> test 1")
+ cath #> "#team test 2"
+ concurrently_
+ (bob <# "#team cath> test 2")
+ (alice <# "#team cath> test 2")
+ bob ##> "/tail #team 3"
+ bob <# "#team alice> hi again"
+ bob <# "#team alice> test 1 [blocked]"
+ bob <# "#team cath> test 2"
+ threadDelay 1000000
+ bob ##> "/unblock #team alice"
+ bob <## "ok"
+ bob ##> "/ms team"
+ bob <## "bob (Bob): admin, you, connected"
+ bob <## "alice (Alice): owner, host, connected"
+ bob <## "cath (Catherine): admin, connected"
+ alice #> "#team test 3"
+ concurrently_
+ (bob <# "#team alice> test 3")
+ (cath <# "#team alice> test 3")
+ cath #> "#team test 4"
+ concurrently_
+ (bob <# "#team cath> test 4")
+ (alice <# "#team cath> test 4")
bob ##> "/gs"
bob <## "#team (3 members)"
@@ -1937,7 +2002,7 @@ testUserPrivacy =
-- shows hidden user when active
alice ##> "/users"
alice <## "alice (Alice)"
- alice <## "alisa (active, hidden, muted)"
+ alice <## "alisa (active, hidden, muted, unread: 1)"
-- hidden message is saved
alice ##> "/tail"
alice <##? chatHistory
diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs
index 6578df84c0..55d02b9488 100644
--- a/tests/ChatTests/Groups.hs
+++ b/tests/ChatTests/Groups.hs
@@ -1538,7 +1538,6 @@ testGroupDelayedModerationFullDelete tmp = do
testGroupAsync :: HasCallStack => FilePath -> IO ()
testGroupAsync tmp = do
- print (0 :: Integer)
withNewTestChat tmp "alice" aliceProfile $ \alice -> do
withNewTestChat tmp "bob" bobProfile $ \bob -> do
connectUsers alice bob
From 8ff6b392c2444b023cb8668069cc27709702ad6d Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Wed, 11 Oct 2023 21:15:31 +0100
Subject: [PATCH 19/80] core: rename "reference" to "mention"
---
src/Simplex/Chat/Messages.hs | 4 ++--
src/Simplex/Chat/Terminal/Output.hs | 6 +++---
src/Simplex/Chat/View.hs | 30 ++++++++++++++---------------
3 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs
index 22506218aa..21da83ad05 100644
--- a/src/Simplex/Chat/Messages.hs
+++ b/src/Simplex/Chat/Messages.hs
@@ -150,8 +150,8 @@ instance MsgDirectionI d => ToJSON (ChatItem c d) where
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
-isReference :: ChatItem c d -> Bool
-isReference ChatItem {chatDir, quotedItem} = case chatDir of
+isMention :: ChatItem c d -> Bool
+isMention ChatItem {chatDir, quotedItem} = case chatDir of
CIDirectRcv -> userItem quotedItem
CIGroupRcv _ -> userItem quotedItem
_ -> False
diff --git a/src/Simplex/Chat/Terminal/Output.hs b/src/Simplex/Chat/Terminal/Output.hs
index a45390e8c3..a623536cd9 100644
--- a/src/Simplex/Chat/Terminal/Output.hs
+++ b/src/Simplex/Chat/Terminal/Output.hs
@@ -151,7 +151,7 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} = d
responseNotification ct cc r
where
markChatItemRead u (AChatItem _ _ chat ci@ChatItem {chatDir, meta = CIMeta {itemStatus}}) =
- case (chatDirNtf u chat chatDir (isReference ci), itemStatus) of
+ case (chatDirNtf u chat chatDir (isMention ci), itemStatus) of
(True, CISRcvNew) -> do
let itemId = chatItemId' ci
chatRef = chatInfoToRef chat
@@ -162,7 +162,7 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} = d
responseNotification :: ChatTerminal -> ChatController -> ChatResponse -> IO ()
responseNotification t@ChatTerminal {sendNotification} cc = \case
CRNewChatItem u (AChatItem _ SMDRcv cInfo ci@ChatItem {chatDir, content = CIRcvMsgContent mc, formattedText}) ->
- when (chatDirNtf u cInfo chatDir $ isReference ci) $ do
+ when (chatDirNtf u cInfo chatDir $ isMention ci) $ do
whenCurrUser cc u $ setActiveChat t cInfo
case (cInfo, chatDir) of
(DirectChat ct, _) -> sendNtf (viewContactName ct <> "> ", text)
@@ -171,7 +171,7 @@ responseNotification t@ChatTerminal {sendNotification} cc = \case
where
text = msgText mc formattedText
CRChatItemUpdated u (AChatItem _ SMDRcv cInfo ci@ChatItem {chatDir, content = CIRcvMsgContent _}) ->
- whenCurrUser cc u $ when (chatDirNtf u cInfo chatDir $ isReference ci) $ setActiveChat t cInfo
+ whenCurrUser cc u $ when (chatDirNtf u cInfo chatDir $ isMention ci) $ setActiveChat t cInfo
CRContactConnected u ct _ -> when (contactNtf u ct False) $ do
whenCurrUser cc u $ setActiveContact t ct
sendNtf (viewContactName ct <> "> ", "connected")
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index f465375f12..86a10988d6 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -350,40 +350,40 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
contactList :: [ContactRef] -> String
contactList cs = T.unpack . T.intercalate ", " $ map (\ContactRef {localDisplayName = n} -> "@" <> n) cs
unmuted :: User -> ChatInfo c -> ChatItem c d -> [StyledString] -> [StyledString]
- unmuted u chat ci@ChatItem {chatDir} = unmuted' u chat chatDir $ isReference ci
+ unmuted u chat ci@ChatItem {chatDir} = unmuted' u chat chatDir $ isMention ci
unmutedReaction :: User -> ChatInfo c -> CIReaction c d -> [StyledString] -> [StyledString]
unmutedReaction u chat CIReaction {chatDir} = unmuted' u chat chatDir False
unmuted' :: User -> ChatInfo c -> CIDirection c d -> Bool -> [StyledString] -> [StyledString]
- unmuted' u chat chatDir reference s
- | chatDirNtf u chat chatDir reference = s
+ unmuted' u chat chatDir mention s
+ | chatDirNtf u chat chatDir mention = s
| otherwise = []
userNtf :: User -> Bool
userNtf User {showNtfs, activeUser} = showNtfs || activeUser
chatNtf :: User -> ChatInfo c -> Bool -> Bool
-chatNtf user cInfo reference = case cInfo of
- DirectChat ct -> contactNtf user ct reference
- GroupChat g -> groupNtf user g reference
+chatNtf user cInfo mention = case cInfo of
+ DirectChat ct -> contactNtf user ct mention
+ GroupChat g -> groupNtf user g mention
_ -> False
chatDirNtf :: User -> ChatInfo c -> CIDirection c d -> Bool -> Bool
-chatDirNtf user cInfo chatDir reference = case (cInfo, chatDir) of
- (DirectChat ct, CIDirectRcv) -> contactNtf user ct reference
- (GroupChat g, CIGroupRcv m) -> groupNtf user g reference && showMessages (memberSettings m)
+chatDirNtf user cInfo chatDir mention = case (cInfo, chatDir) of
+ (DirectChat ct, CIDirectRcv) -> contactNtf user ct mention
+ (GroupChat g, CIGroupRcv m) -> groupNtf user g mention && showMessages (memberSettings m)
_ -> True
contactNtf :: User -> Contact -> Bool -> Bool
-contactNtf user Contact {chatSettings} reference =
- userNtf user && showMessageNtf chatSettings reference
+contactNtf user Contact {chatSettings} mention =
+ userNtf user && showMessageNtf chatSettings mention
groupNtf :: User -> GroupInfo -> Bool -> Bool
-groupNtf user GroupInfo {chatSettings} reference =
- userNtf user && showMessageNtf chatSettings reference
+groupNtf user GroupInfo {chatSettings} mention =
+ userNtf user && showMessageNtf chatSettings mention
showMessageNtf :: ChatSettings -> Bool -> Bool
-showMessageNtf ChatSettings {enableNtfs} reference =
- enableNtfs == MFAll || (reference && enableNtfs == MFMentions)
+showMessageNtf ChatSettings {enableNtfs} mention =
+ enableNtfs == MFAll || (mention && enableNtfs == MFMentions)
chatItemDeletedText :: ChatItem c d -> Maybe GroupMember -> Maybe Text
chatItemDeletedText ChatItem {meta = CIMeta {itemDeleted}, content} membership_ =
From 4df8ea2e78fa89deec5857d407a15b5574450803 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Wed, 11 Oct 2023 23:07:05 +0100
Subject: [PATCH 20/80] ui: update types for notification and member settings
(#3201)
---
apps/ios/Shared/Views/Chat/ChatView.swift | 2 +-
apps/ios/SimpleX.xcodeproj/project.pbxproj | 40 +++++++++----------
apps/ios/SimpleXChat/APITypes.swift | 12 ++++--
apps/ios/SimpleXChat/ChatTypes.swift | 12 ++++--
.../chat/simplex/common/model/ChatModel.kt | 17 +++++---
.../chat/simplex/common/model/SimpleXAPI.kt | 11 ++++-
.../views/chatlist/ChatListNavLinkView.kt | 6 +--
7 files changed, 62 insertions(+), 38 deletions(-)
diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift
index 389080efc5..81473709cb 100644
--- a/apps/ios/Shared/Views/Chat/ChatView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatView.swift
@@ -965,7 +965,7 @@ struct ChatView: View {
func toggleNotifications(_ chat: Chat, enableNtfs: Bool) {
var chatSettings = chat.chatInfo.chatSettings ?? ChatSettings.defaults
- chatSettings.enableNtfs = enableNtfs
+ chatSettings.enableNtfs = enableNtfs ? .all : .none
updateChatSettings(chat, chatSettings: chatSettings)
}
diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj
index 18021e3079..35d0b1de8a 100644
--- a/apps/ios/SimpleX.xcodeproj/project.pbxproj
+++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj
@@ -85,6 +85,11 @@
5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059C4279559F40002BEB4 /* ContentView.swift */; };
5CA059EF279559F40002BEB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5CA059C5279559F40002BEB4 /* Assets.xcassets */; };
5CA7DFC329302AF000F7FDDE /* AppSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA7DFC229302AF000F7FDDE /* AppSheet.swift */; };
+ 5CA8D0162AD746C8001FD661 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA8D0112AD746C8001FD661 /* libgmpxx.a */; };
+ 5CA8D0172AD746C8001FD661 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA8D0122AD746C8001FD661 /* libffi.a */; };
+ 5CA8D0182AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA8D0132AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a */; };
+ 5CA8D0192AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA8D0142AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a */; };
+ 5CA8D01A2AD746C8001FD661 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA8D0152AD746C8001FD661 /* libgmp.a */; };
5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79929211BB900072E13 /* PreferencesView.swift */; };
5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */; };
5CB0BA882826CB3A00B3292C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CB0BA862826CB3A00B3292C /* InfoPlist.strings */; };
@@ -114,11 +119,6 @@
5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */; };
5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */; };
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
- 5CC739972AD44E2E009470A9 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC739922AD44E2E009470A9 /* libgmp.a */; };
- 5CC739982AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC739932AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F-ghc8.10.7.a */; };
- 5CC739992AD44E2E009470A9 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC739942AD44E2E009470A9 /* libffi.a */; };
- 5CC7399A2AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC739952AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F.a */; };
- 5CC7399B2AD44E2E009470A9 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC739962AD44E2E009470A9 /* libgmpxx.a */; };
5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; };
5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; };
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; };
@@ -358,6 +358,11 @@
5CA85D0A297218AA0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; };
5CA85D0C297219EF0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = "it.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; };
5CA85D0D297219EF0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; };
+ 5CA8D0112AD746C8001FD661 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; };
+ 5CA8D0122AD746C8001FD661 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; };
+ 5CA8D0132AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a"; sourceTree = ""; };
+ 5CA8D0142AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a"; sourceTree = ""; };
+ 5CA8D0152AD746C8001FD661 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; };
5CAB912529E93F9400F34A95 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; };
5CAC41182A192D8400C331A2 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; };
5CAC411A2A192DE800C331A2 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = "ja.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; };
@@ -395,11 +400,6 @@
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = ""; };
5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; };
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; };
- 5CC739922AD44E2E009470A9 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; };
- 5CC739932AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F-ghc8.10.7.a"; sourceTree = ""; };
- 5CC739942AD44E2E009470A9 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; };
- 5CC739952AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F.a"; sourceTree = ""; };
- 5CC739962AD44E2E009470A9 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; };
5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = ""; };
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = ""; };
5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = ""; };
@@ -507,13 +507,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 5CC739982AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F-ghc8.10.7.a in Frameworks */,
+ 5CA8D0162AD746C8001FD661 /* libgmpxx.a in Frameworks */,
+ 5CA8D01A2AD746C8001FD661 /* libgmp.a in Frameworks */,
+ 5CA8D0182AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a in Frameworks */,
+ 5CA8D0192AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
- 5CC739972AD44E2E009470A9 /* libgmp.a in Frameworks */,
- 5CC7399A2AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F.a in Frameworks */,
- 5CC739992AD44E2E009470A9 /* libffi.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
- 5CC7399B2AD44E2E009470A9 /* libgmpxx.a in Frameworks */,
+ 5CA8D0172AD746C8001FD661 /* libffi.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -574,11 +574,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
- 5CC739942AD44E2E009470A9 /* libffi.a */,
- 5CC739922AD44E2E009470A9 /* libgmp.a */,
- 5CC739962AD44E2E009470A9 /* libgmpxx.a */,
- 5CC739932AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F-ghc8.10.7.a */,
- 5CC739952AD44E2E009470A9 /* libHSsimplex-chat-5.4.0.0-JjDpmMNHLrsHjXbdowMF4F.a */,
+ 5CA8D0122AD746C8001FD661 /* libffi.a */,
+ 5CA8D0152AD746C8001FD661 /* libgmp.a */,
+ 5CA8D0112AD746C8001FD661 /* libgmpxx.a */,
+ 5CA8D0142AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a */,
+ 5CA8D0132AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a */,
);
path = Libraries;
sourceTree = "";
diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift
index 951f726be9..d65b9b3283 100644
--- a/apps/ios/SimpleXChat/APITypes.swift
+++ b/apps/ios/SimpleXChat/APITypes.swift
@@ -1182,17 +1182,23 @@ public struct KeepAliveOpts: Codable, Equatable {
}
public struct ChatSettings: Codable {
- public var enableNtfs: Bool
+ public var enableNtfs: MsgFilter
public var sendRcpts: Bool?
public var favorite: Bool
- public init(enableNtfs: Bool, sendRcpts: Bool?, favorite: Bool) {
+ public init(enableNtfs: MsgFilter, sendRcpts: Bool?, favorite: Bool) {
self.enableNtfs = enableNtfs
self.sendRcpts = sendRcpts
self.favorite = favorite
}
- public static let defaults: ChatSettings = ChatSettings(enableNtfs: true, sendRcpts: nil, favorite: false)
+ public static let defaults: ChatSettings = ChatSettings(enableNtfs: .all, sendRcpts: nil, favorite: false)
+}
+
+public enum MsgFilter: String, Codable {
+ case none
+ case all
+ case mentions
}
public struct UserMsgReceiptSettings: Codable {
diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift
index f9996d8400..37e4f0316a 100644
--- a/apps/ios/SimpleXChat/ChatTypes.swift
+++ b/apps/ios/SimpleXChat/ChatTypes.swift
@@ -1292,7 +1292,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
}
public var ntfsEnabled: Bool {
- self.chatSettings?.enableNtfs ?? false
+ self.chatSettings?.enableNtfs == .all
}
public var chatSettings: ChatSettings? {
@@ -1758,6 +1758,7 @@ public struct GroupMember: Identifiable, Decodable {
public var memberRole: GroupMemberRole
public var memberCategory: GroupMemberCategory
public var memberStatus: GroupMemberStatus
+ public var memberSettings: GroupMemberSettings
public var invitedBy: InvitedBy
public var localDisplayName: ContactName
public var memberProfile: LocalProfile
@@ -1851,6 +1852,7 @@ public struct GroupMember: Identifiable, Decodable {
memberRole: .admin,
memberCategory: .inviteeMember,
memberStatus: .memComplete,
+ memberSettings: GroupMemberSettings(showMessages: true),
invitedBy: .user,
localDisplayName: "alice",
memberProfile: LocalProfile.sampleData,
@@ -1860,6 +1862,10 @@ public struct GroupMember: Identifiable, Decodable {
)
}
+public struct GroupMemberSettings: Decodable {
+ var showMessages: Bool
+}
+
public struct GroupMemberRef: Decodable {
var groupMemberId: Int64
var profile: Profile
@@ -1983,8 +1989,8 @@ public enum ConnectionEntity: Decodable {
public var ntfsEnabled: Bool {
switch self {
- case let .rcvDirectMsgConnection(contact): return contact?.chatSettings.enableNtfs ?? false
- case let .rcvGroupMsgConnection(groupInfo, _): return groupInfo.chatSettings.enableNtfs
+ case let .rcvDirectMsgConnection(contact): return contact?.chatSettings.enableNtfs == .all
+ case let .rcvGroupMsgConnection(groupInfo, _): return groupInfo.chatSettings.enableNtfs == .all
case .sndFileConnection: return false
case .rcvFileConnection: return false
case let .userContactConnection(userContact): return userContact.groupId == nil
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 8687ac390a..88ad78612c 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
@@ -12,7 +12,6 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.chat.ComposeState
import chat.simplex.common.views.helpers.*
-import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
@@ -726,7 +725,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val apiId get() = contactConnection.apiId
override val ready get() = contactConnection.ready
override val sendMsgEnabled get() = contactConnection.sendMsgEnabled
- override val ntfsEnabled get() = contactConnection.incognito
+ override val ntfsEnabled get() = false
override val incognito get() = contactConnection.incognito
override fun featureEnabled(feature: ChatFeature) = contactConnection.featureEnabled(feature)
override val timedMessagesTTL: Int? get() = contactConnection.timedMessagesTTL
@@ -822,7 +821,7 @@ data class Contact(
(ready && active && !(activeConn.connectionStats?.ratchetSyncSendProhibited ?: false))
|| nextSendGrpInv
val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent
- override val ntfsEnabled get() = chatSettings.enableNtfs
+ override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All
override val incognito get() = contactConnIncognito
override fun featureEnabled(feature: ChatFeature) = when (feature) {
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.enabled.forUser
@@ -869,7 +868,7 @@ data class Contact(
activeConn = Connection.sampleData,
contactUsed = true,
contactStatus = ContactStatus.Active,
- chatSettings = ChatSettings(enableNtfs = true, sendRcpts = null, favorite = false),
+ chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false),
userPreferences = ChatPreferences.sampleData,
mergedPreferences = ContactUserPreferences.sampleData,
createdAt = Clock.System.now(),
@@ -1009,7 +1008,7 @@ data class GroupInfo (
override val apiId get() = groupId
override val ready get() = membership.memberActive
override val sendMsgEnabled get() = membership.memberActive
- override val ntfsEnabled get() = chatSettings.enableNtfs
+ override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All
override val incognito get() = membership.memberIncognito
override fun featureEnabled(feature: ChatFeature) = when (feature) {
ChatFeature.TimedMessages -> fullGroupPreferences.timedMessages.on
@@ -1041,7 +1040,7 @@ data class GroupInfo (
fullGroupPreferences = FullGroupPreferences.sampleData,
membership = GroupMember.sampleData,
hostConnCustomUserProfileId = null,
- chatSettings = ChatSettings(enableNtfs = true, sendRcpts = null, favorite = false),
+ chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false),
createdAt = Clock.System.now(),
updatedAt = Clock.System.now()
)
@@ -1073,6 +1072,7 @@ data class GroupMember (
var memberRole: GroupMemberRole,
var memberCategory: GroupMemberCategory,
var memberStatus: GroupMemberStatus,
+ var memberSettings: GroupMemberSettings,
var invitedBy: InvitedBy,
val localDisplayName: String,
val memberProfile: LocalProfile,
@@ -1140,6 +1140,7 @@ data class GroupMember (
memberRole = GroupMemberRole.Member,
memberCategory = GroupMemberCategory.InviteeMember,
memberStatus = GroupMemberStatus.MemComplete,
+ memberSettings = GroupMemberSettings(showMessages = true),
invitedBy = InvitedBy.IBUser(),
localDisplayName = "alice",
memberProfile = LocalProfile.sampleData,
@@ -1150,6 +1151,9 @@ data class GroupMember (
}
}
+@Serializable
+data class GroupMemberSettings(val showMessages: Boolean) {}
+
@Serializable
class GroupMemberRef(
val groupMemberId: Long,
@@ -1844,6 +1848,7 @@ enum class SndCIStatusProgress {
@Serializable
sealed class CIDeleted {
@Serializable @SerialName("deleted") class Deleted(val deletedTs: Instant?): CIDeleted()
+ @Serializable @SerialName("blocked") class Blocked(val deletedTs: Instant?): CIDeleted()
@Serializable @SerialName("moderated") class Moderated(val deletedTs: Instant?, val byGroupMember: GroupMember): CIDeleted()
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
index 43043d65fe..3644268ba3 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
@@ -2472,15 +2472,22 @@ data class KeepAliveOpts(
@Serializable
data class ChatSettings(
- val enableNtfs: Boolean,
+ val enableNtfs: MsgFilter,
val sendRcpts: Boolean?,
val favorite: Boolean
) {
companion object {
- val defaults: ChatSettings = ChatSettings(enableNtfs = true, sendRcpts = null, favorite = false)
+ val defaults: ChatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false)
}
}
+@Serializable
+enum class MsgFilter {
+ @SerialName("all") All,
+ @SerialName("none") None,
+ @SerialName("mentions") Mentions,
+}
+
@Serializable
data class UserMsgReceiptSettings(val enable: Boolean, val clearOverrides: Boolean)
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 41c94f21d9..566c981811 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
@@ -595,8 +595,8 @@ fun groupInvitationAcceptedAlert() {
)
}
-fun toggleNotifications(chat: Chat, enableNtfs: Boolean, chatModel: ChatModel, currentState: MutableState? = null) {
- val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(enableNtfs = enableNtfs)
+fun toggleNotifications(chat: Chat, enableAllNtfs: Boolean, chatModel: ChatModel, currentState: MutableState? = null) {
+ val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(enableNtfs = if (enableAllNtfs) MsgFilter.All else MsgFilter.None)
updateChatSettings(chat, chatSettings, chatModel, currentState)
}
@@ -627,7 +627,7 @@ fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatMo
}
if (res && newChatInfo != null) {
chatModel.updateChatInfo(newChatInfo)
- if (!chatSettings.enableNtfs) {
+ if (chatSettings.enableNtfs != MsgFilter.All) {
ntfManager.cancelNotificationsForChat(chat.id)
}
val current = currentState?.value
From 7b488c7f1ba661d6198b68c88926cd586e8b8d42 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Thu, 12 Oct 2023 11:52:14 +0400
Subject: [PATCH 21/80] tests: improve tests (#3203)
---
tests/ChatTests/Groups.hs | 2 +-
tests/ChatTests/Profiles.hs | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs
index 55d02b9488..77b70c3e78 100644
--- a/tests/ChatTests/Groups.hs
+++ b/tests/ChatTests/Groups.hs
@@ -2461,7 +2461,7 @@ testPlanGroupLinkLeaveRejoin =
concurrentlyN_
[ alice
<### [ "bob_1 (Bob): contact is connected",
- "bob_1 invited to group #team via your group link",
+ EndsWith "invited to group #team via your group link",
EndsWith "joined the group",
"contact bob_1 is merged into bob",
"use @bob to send messages"
diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs
index 0d7683c4d7..80e1709222 100644
--- a/tests/ChatTests/Profiles.hs
+++ b/tests/ChatTests/Profiles.hs
@@ -650,8 +650,10 @@ testPlanAddressConnecting tmp = do
alice ##> "/ad"
getContactLink alice True
withNewTestChat tmp "bob" bobProfile $ \bob -> do
+ threadDelay 100000
bob ##> ("/c " <> cLink)
bob <## "connection request sent!"
+ threadDelay 100000
withTestChat tmp "alice" $ \alice -> do
alice <## "Your address is active! To show: /sa"
alice <## "bob (Bob) wants to connect to you!"
From 247f2c9e618eba9fa5eef8277efe8df666467e49 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Wed, 11 Oct 2023 15:26:44 +0400
Subject: [PATCH 22/80] tests: modify
testMemberContactInvitedConnectionReplaced to not rely on chat item order,
print output (#3198)
---
tests/ChatClient.hs | 2 ++
tests/ChatTests/Groups.hs | 10 ++++++----
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs
index 7da5263253..7b3fb44998 100644
--- a/tests/ChatClient.hs
+++ b/tests/ChatClient.hs
@@ -210,6 +210,8 @@ withTestChatOpts tmp = withTestChatCfgOpts tmp testCfg
withTestChatCfgOpts :: HasCallStack => FilePath -> ChatConfig -> ChatOpts -> String -> (HasCallStack => TestCC -> IO a) -> IO a
withTestChatCfgOpts tmp cfg opts dbPrefix = bracket (startTestChat tmp cfg opts dbPrefix) (\cc -> cc / 100000 >> stopTestChat cc)
+-- enable output for specific chat controller, use like this:
+-- withNewTestChat tmp "alice" aliceProfile $ \a -> withTestOutput a $ \alice -> do ...
withTestOutput :: HasCallStack => TestCC -> (HasCallStack => TestCC -> IO a) -> IO a
withTestOutput cc runTest = runTest cc {printOutput = True}
diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs
index bf740a960f..82a5c1bd67 100644
--- a/tests/ChatTests/Groups.hs
+++ b/tests/ChatTests/Groups.hs
@@ -2886,9 +2886,9 @@ testMemberContactProhibitedRepeatInv =
testMemberContactInvitedConnectionReplaced :: HasCallStack => FilePath -> IO ()
testMemberContactInvitedConnectionReplaced tmp = do
- withNewTestChat tmp "alice" aliceProfile $ \alice -> do
- withNewTestChat tmp "bob" bobProfile $ \bob -> do
- withNewTestChat tmp "cath" cathProfile $ \cath -> do
+ withNewTestChat tmp "alice" aliceProfile $ \a -> withTestOutput a $ \alice -> do
+ withNewTestChat tmp "bob" bobProfile $ \b -> withTestOutput b $ \bob -> do
+ withNewTestChat tmp "cath" cathProfile $ \c -> withTestOutput c $ \cath -> do
createGroup3 "team" alice bob cath
alice ##> "/d bob"
@@ -2910,7 +2910,9 @@ testMemberContactInvitedConnectionReplaced tmp = do
(alice <## "bob (Bob): contact is connected")
(bob <## "alice (Alice): contact is connected")
- bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "received invitation to join group team as admin"), (0, "hi"), (0, "security code changed")] <> chatFeatures)
+ bob ##> "/_get chat @2 count=100"
+ items <- chat <$> getTermLine bob
+ items `shouldContain` [(0, "received invitation to join group team as admin"), (0, "contact deleted"), (0, "hi"), (0, "security code changed")]
withTestChat tmp "bob" $ \bob -> do
subscriptions bob 1
From b956988a83a89e357b63cfb6391132b7b84b988d Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Thu, 12 Oct 2023 12:20:31 +0400
Subject: [PATCH 23/80] tests: simplify
testMemberContactInvitedConnectionReplaced (for stable)
---
tests/ChatTests/Groups.hs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs
index 77b70c3e78..4c31805265 100644
--- a/tests/ChatTests/Groups.hs
+++ b/tests/ChatTests/Groups.hs
@@ -3278,7 +3278,7 @@ testMemberContactInvitedConnectionReplaced tmp = do
bob ##> "/_get chat @2 count=100"
items <- chat <$> getTermLine bob
- items `shouldContain` [(0, "received invitation to join group team as admin"), (0, "contact deleted"), (0, "hi"), (0, "security code changed")]
+ items `shouldContain` [(0, "security code changed")]
withTestChat tmp "bob" $ \bob -> do
subscriptions bob 1
From 5d078bec536eb0149c1205cb80d6bdbb21e25497 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Thu, 12 Oct 2023 12:20:31 +0400
Subject: [PATCH 24/80] tests: simplify
testMemberContactInvitedConnectionReplaced (for stable)
---
tests/ChatTests/Groups.hs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs
index 82a5c1bd67..1d2ff7ad97 100644
--- a/tests/ChatTests/Groups.hs
+++ b/tests/ChatTests/Groups.hs
@@ -2912,7 +2912,7 @@ testMemberContactInvitedConnectionReplaced tmp = do
bob ##> "/_get chat @2 count=100"
items <- chat <$> getTermLine bob
- items `shouldContain` [(0, "received invitation to join group team as admin"), (0, "contact deleted"), (0, "hi"), (0, "security code changed")]
+ items `shouldContain` [(0, "security code changed")]
withTestChat tmp "bob" $ \bob -> do
subscriptions bob 1
From 8ffe1c23c176b7d857b3ef534be26e2b5a67b41d Mon Sep 17 00:00:00 2001
From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
Date: Thu, 12 Oct 2023 20:19:57 +0800
Subject: [PATCH 25/80] android, desktop: better handling of parallel updates
of chats (#3204)
* android, desktop: better handling of parallel updates of chats
* one more case
---
.../main/java/chat/simplex/app/SimplexApp.kt | 32 +++++++++--------
.../chat/simplex/common/model/ChatModel.kt | 34 +++++++++++--------
.../chat/simplex/common/model/SimpleXAPI.kt | 14 +++++---
.../common/views/database/DatabaseView.kt | 8 +++--
4 files changed, 53 insertions(+), 35 deletions(-)
diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt
index f70032788b..d2c4465172 100644
--- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt
+++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt
@@ -9,12 +9,14 @@ import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.common.helpers.requiresIgnoringBattery
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
+import chat.simplex.common.model.ChatModel.updatingChatsMutex
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.common.platform.*
import chat.simplex.common.views.call.RcvCallInvitation
import com.jakewharton.processphoenix.ProcessPhoenix
import kotlinx.coroutines.*
+import kotlinx.coroutines.sync.withLock
import java.io.*
import java.util.*
import java.util.concurrent.TimeUnit
@@ -52,21 +54,23 @@ class SimplexApp: Application(), LifecycleEventObserver {
Lifecycle.Event.ON_START -> {
isAppOnForeground = true
if (chatModel.chatRunning.value == true) {
- kotlin.runCatching {
- val currentUserId = chatModel.currentUser.value?.userId
- val chats = ArrayList(chatController.apiGetChats())
- /** Active user can be changed in background while [ChatController.apiGetChats] is executing */
- if (chatModel.currentUser.value?.userId == currentUserId) {
- val currentChatId = chatModel.chatId.value
- val oldStats = if (currentChatId != null) chatModel.getChat(currentChatId)?.chatStats else null
- if (oldStats != null) {
- val indexOfCurrentChat = chats.indexOfFirst { it.id == currentChatId }
- /** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */
- if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats)
+ updatingChatsMutex.withLock {
+ kotlin.runCatching {
+ val currentUserId = chatModel.currentUser.value?.userId
+ val chats = ArrayList(chatController.apiGetChats())
+ /** Active user can be changed in background while [ChatController.apiGetChats] is executing */
+ if (chatModel.currentUser.value?.userId == currentUserId) {
+ val currentChatId = chatModel.chatId.value
+ val oldStats = if (currentChatId != null) chatModel.getChat(currentChatId)?.chatStats else null
+ if (oldStats != null) {
+ val indexOfCurrentChat = chats.indexOfFirst { it.id == currentChatId }
+ /** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */
+ if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats)
+ }
+ chatModel.updateChats(chats)
}
- chatModel.updateChats(chats)
- }
- }.onFailure { Log.e(TAG, it.stackTraceToString()) }
+ }.onFailure { Log.e(TAG, it.stackTraceToString()) }
+ }
}
}
Lifecycle.Event.ON_RESUME -> {
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 88ad78612c..ac5f9fb8a5 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
@@ -6,7 +6,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.style.TextDecoration
-import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
@@ -16,6 +15,8 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.*
import kotlinx.datetime.TimeZone
import kotlinx.serialization.*
@@ -102,6 +103,8 @@ object ChatModel {
val filesToDelete = mutableSetOf()
val simplexLinkMode by lazy { mutableStateOf(ChatController.appPrefs.simplexLinkMode.get()) }
+ var updatingChatsMutex: Mutex = Mutex()
+
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
currentUser.value
} else {
@@ -198,7 +201,7 @@ object ChatModel {
}
}
- suspend fun addChatItem(cInfo: ChatInfo, cItem: ChatItem) {
+ suspend fun addChatItem(cInfo: ChatInfo, cItem: ChatItem) = updatingChatsMutex.withLock {
// update previews
val i = getChatIndex(cInfo.id)
val chat: Chat
@@ -221,10 +224,11 @@ object ChatModel {
} else {
addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem)))
}
- // add to current chat
- if (chatId.value == cInfo.id) {
- Log.d(TAG, "TODOCHAT: addChatItem: adding to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
- withContext(Dispatchers.Main) {
+ Log.d(TAG, "TODOCHAT: addChatItem: adding to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
+ withContext(Dispatchers.Main) {
+ // add to current chat
+ if (chatId.value == cInfo.id) {
+ Log.d(TAG, "TODOCHAT: addChatItem: chatIds are equal, size ${chatItems.size}")
// Prevent situation when chat item already in the list received from backend
if (chatItems.none { it.id == cItem.id }) {
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
@@ -238,7 +242,7 @@ object ChatModel {
}
}
- suspend fun upsertChatItem(cInfo: ChatInfo, cItem: ChatItem): Boolean {
+ suspend fun upsertChatItem(cInfo: ChatInfo, cItem: ChatItem): Boolean = updatingChatsMutex.withLock {
// update previews
val i = getChatIndex(cInfo.id)
val chat: Chat
@@ -258,10 +262,10 @@ object ChatModel {
addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem)))
res = true
}
- // update current chat
- return if (chatId.value == cInfo.id) {
- Log.d(TAG, "TODOCHAT: upsertChatItem: upserting to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
- withContext(Dispatchers.Main) {
+ Log.d(TAG, "TODOCHAT: upsertChatItem: upserting to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
+ return withContext(Dispatchers.Main) {
+ // update current chat
+ if (chatId.value == cInfo.id) {
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) {
chatItems[itemIndex] = cItem
@@ -272,15 +276,15 @@ object ChatModel {
Log.d(TAG, "TODOCHAT: upsertChatItem: added to chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
true
}
+ } else {
+ res
}
- } else {
- res
}
}
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem) {
- if (chatId.value == cInfo.id) {
- withContext(Dispatchers.Main) {
+ withContext(Dispatchers.Main) {
+ if (chatId.value == cInfo.id) {
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) {
chatItems[itemIndex] = cItem
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
index 3644268ba3..dc2c3c0f25 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
@@ -4,6 +4,7 @@ import chat.simplex.common.views.helpers.*
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
+import chat.simplex.common.model.ChatModel.updatingChatsMutex
import dev.icerock.moko.resources.compose.painterResource
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
@@ -16,6 +17,7 @@ import com.charleskorn.kaml.YamlConfiguration
import chat.simplex.res.MR
import com.russhwolf.settings.Settings
import kotlinx.coroutines.*
+import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.*
@@ -349,8 +351,10 @@ object ChatController {
startReceiver()
Log.d(TAG, "startChat: started")
} else {
- val chats = apiGetChats()
- chatModel.updateChats(chats)
+ updatingChatsMutex.withLock {
+ val chats = apiGetChats()
+ chatModel.updateChats(chats)
+ }
Log.d(TAG, "startChat: running")
}
} catch (e: Error) {
@@ -384,8 +388,10 @@ object ChatController {
suspend fun getUserChatData() {
chatModel.userAddress.value = apiGetUserAddress()
chatModel.chatItemTTL.value = getChatItemTTL()
- val chats = apiGetChats()
- chatModel.updateChats(chats)
+ updatingChatsMutex.withLock {
+ val chats = apiGetChats()
+ chatModel.updateChats(chats)
+ }
}
private fun startReceiver() {
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt
index fa0f8f54d1..0cca354746 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt
@@ -20,11 +20,13 @@ import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
+import chat.simplex.common.model.ChatModel.updatingChatsMutex
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.*
import chat.simplex.common.platform.*
import chat.simplex.res.MR
+import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.*
import java.io.*
import java.net.URI
@@ -620,8 +622,10 @@ private fun afterSetCiTTL(
appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath)
withApi {
try {
- val chats = m.controller.apiGetChats()
- m.updateChats(chats)
+ updatingChatsMutex.withLock {
+ val chats = m.controller.apiGetChats()
+ m.updateChats(chats)
+ }
} catch (e: Exception) {
Log.e(TAG, "apiGetChats error: ${e.message}")
}
From 675fc19745960db809e77ced0554b6947ae3187b Mon Sep 17 00:00:00 2001
From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
Date: Fri, 13 Oct 2023 07:05:06 +0800
Subject: [PATCH 26/80] rfc: desktop calls (#3208)
* rfc: desktop calls
* errors
* html
* link
* screen sharing
* additions
* addition
* correction
---------
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
---
docs/rfcs/2023-10-12-desktop-calls.md | 34 +++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
create mode 100644 docs/rfcs/2023-10-12-desktop-calls.md
diff --git a/docs/rfcs/2023-10-12-desktop-calls.md b/docs/rfcs/2023-10-12-desktop-calls.md
new file mode 100644
index 0000000000..c956ed0115
--- /dev/null
+++ b/docs/rfcs/2023-10-12-desktop-calls.md
@@ -0,0 +1,34 @@
+# Desktop calls
+
+To make audio and video calls on desktop there are some options:
+- adapt [libwebrtc](webrtc.googlesource.com/) from Google which would be the most reliable, performant and seamless solution;
+- include some kind of WebView to the app via libraries;
+- implement a signaling server in the app and let users to use HTML page with WebRTC code that already exist for Android.
+
+## WebRTC lib
+
+To adapt libwebrtc we need to make SDK that is compatible with Java (JNI layer + desktop implementation of VideoCodecs and other features). There are two SDKs exist already: for Android and for Objective-C. Making Android SDK compatible with Java-only SDK gives a lot of problems and requires to have 5+ professional C++ developers with weeks/months for developing. Which is not something good.
+
+Another considered option was adopting Jitsi Java SDK, but it's state is not very clear, and in any case it is much more effort.
+
+## WebView
+
+Including WebView is possible but requires a lot of megabytes of storage to waste on such libs. Because only Chromium-like WebViews support WebRTC features. Which means 100+ MB to the package on top of 200+ MB now.
+
+## Standalone browser + WebRTC HTML page
+
+The last solution is what can give the most useful result: the same package size as before + already existent code which can be reused with small modifications + quality of result will depend on Chromium/Firefox/Safari devs (which is good, since they are interested in making all features working for everyone).
+
+# Details of implementation
+
+Since the code for WebView has already written (https://github.com/simplex-chat/simplex-chat/tree/0e4376bada2d0c4ec2ade7f30b0048dc8b13abd8/apps/multiplatform/android/src/main/assets/www) it can be used to make calls on a desktop browser too. The only differences are these:
+- UI needs to be changed - buttons controlling calls should be added. For example, end call, disable camera/mic. This will be added to separate HTML and JS files that would communication with the existing WebRTC JS code via the existing functional API.
+- signaling websocket server should be started in order to allow Haskell backend to talk with HTML page and to exchange messages between all parties. It is bidirectional communication between server and webpage since both parties have to send messages to each other.
+
+There is no need to have TLS-secured connection between server and webpage since it's only used locally. And browser will not be happy with self-signed certificate too.
+
+After accepting the call, webpage will be opened in the default browser. URL will look like: `https://localhost:123/simplex/call/` (to reduce questions and to namespace other files - the main UI page will be index.html, that would load via folder path) After that internal machinary will connect both parties together. Same as on Android but with a different signaling channel, in this case it's websockets.
+
+Ending the call by the user will also send a new action to signaling server to allow the backend to notify other party.
+
+The solution will also allow to make screen sharing in the future that will be supported on every OS and graphics environment where the user's browser supports it.
\ No newline at end of file
From ab290fb0683047f706f49d5b35052c44873518f7 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Fri, 13 Oct 2023 11:51:01 +0100
Subject: [PATCH 27/80] core: track network statuses, use in commands/events
(#3211)
* core: track network statuses, use in commands/events
* ui types, test
* remove comment
---
apps/ios/Shared/Model/ChatModel.swift | 38 ----------
apps/ios/Shared/Model/SimpleXAPI.swift | 27 +++++---
apps/ios/SimpleXChat/APITypes.swift | 59 ++++++++++++++--
apps/ios/SimpleXChat/ChatTypes.swift | 12 +++-
.../chat/simplex/common/model/ChatModel.kt | 20 ++++--
.../chat/simplex/common/model/SimpleXAPI.kt | 39 ++++++++---
src/Simplex/Chat.hs | 69 ++++++++++++++-----
src/Simplex/Chat/Controller.hs | 15 +++-
src/Simplex/Chat/Mobile.hs | 3 +-
src/Simplex/Chat/Types.hs | 37 +++++++++-
src/Simplex/Chat/View.hs | 10 ++-
tests/ChatTests/Direct.hs | 16 +++++
tests/MobileTests.hs | 32 ++++-----
13 files changed, 275 insertions(+), 102 deletions(-)
diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift
index a1ec2f8f53..ec1a567420 100644
--- a/apps/ios/Shared/Model/ChatModel.swift
+++ b/apps/ios/Shared/Model/ChatModel.swift
@@ -688,41 +688,3 @@ final class Chat: ObservableObject, Identifiable {
public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
}
-
-enum NetworkStatus: Decodable, Equatable {
- case unknown
- case connected
- case disconnected
- case error(String)
-
- var statusString: LocalizedStringKey {
- get {
- switch self {
- case .connected: return "connected"
- case .error: return "error"
- default: return "connecting"
- }
- }
- }
-
- var statusExplanation: LocalizedStringKey {
- get {
- switch self {
- case .connected: return "You are connected to the server used to receive messages from this contact."
- case let .error(err): return "Trying to connect to the server used to receive messages from this contact (error: \(err))."
- default: return "Trying to connect to the server used to receive messages from this contact."
- }
- }
- }
-
- var imageName: String {
- get {
- switch self {
- case .unknown: return "circle.dotted"
- case .connected: return "circle.fill"
- case .disconnected: return "ellipsis.circle.fill"
- case .error: return "exclamationmark.circle.fill"
- }
- }
- }
-}
diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index 85e66e893d..e4f64ee977 100644
--- a/apps/ios/Shared/Model/SimpleXAPI.swift
+++ b/apps/ios/Shared/Model/SimpleXAPI.swift
@@ -944,6 +944,12 @@ func apiCallStatus(_ contact: Contact, _ status: String) async throws {
}
}
+func apiGetNetworkStatuses() throws -> [ConnNetworkStatus] {
+ let r = chatSendCmdSync(.apiGetNetworkStatuses)
+ if case let .networkStatuses(_, statuses) = r { return statuses }
+ throw r
+}
+
func markChatRead(_ chat: Chat, aboveItem: ChatItem? = nil) async {
do {
if chat.chatStats.unreadCount > 0 {
@@ -1348,13 +1354,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
await updateContactsStatus(contactRefs, status: .connected)
case let .contactsDisconnected(_, contactRefs):
await updateContactsStatus(contactRefs, status: .disconnected)
- case let .contactSubError(user, contact, chatError):
- await MainActor.run {
- if active(user) {
- m.updateContact(contact)
- }
- processContactSubError(contact, chatError)
- }
case let .contactSubSummary(_, contactSubscriptions):
await MainActor.run {
for sub in contactSubscriptions {
@@ -1369,6 +1368,18 @@ func processReceivedMsg(_ res: ChatResponse) async {
}
}
}
+ case let .networkStatus(status, connections):
+ await MainActor.run {
+ for cId in connections {
+ m.networkStatuses[cId] = status
+ }
+ }
+ case let .networkStatuses(statuses): ()
+ await MainActor.run {
+ for s in statuses {
+ m.networkStatuses[s.agentConnId] = s.networkStatus
+ }
+ }
case let .newChatItem(user, aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
@@ -1649,7 +1660,7 @@ func processContactSubError(_ contact: Contact, _ chatError: ChatError) {
case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted"
default: err = String(describing: chatError)
}
- m.setContactNetworkStatus(contact, .error(err))
+ m.setContactNetworkStatus(contact, .error(connectionError: err))
}
func refreshCallInvitations() throws {
diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift
index d65b9b3283..d29e2481d5 100644
--- a/apps/ios/SimpleXChat/APITypes.swift
+++ b/apps/ios/SimpleXChat/APITypes.swift
@@ -110,6 +110,7 @@ public enum ChatCommand {
case apiEndCall(contact: Contact)
case apiGetCallInvitations
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
+ case apiGetNetworkStatuses
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
case receiveFile(fileId: Int64, encrypted: Bool, inline: Bool?)
@@ -241,6 +242,7 @@ public enum ChatCommand {
case let .apiEndCall(contact): return "/_call end @\(contact.apiId)"
case .apiGetCallInvitations: return "/_call get"
case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)"
+ case .apiGetNetworkStatuses: return "/_network_statuses"
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
case let .receiveFile(fileId, encrypted, inline):
@@ -356,6 +358,7 @@ public enum ChatCommand {
case .apiEndCall: return "apiEndCall"
case .apiGetCallInvitations: return "apiGetCallInvitations"
case .apiCallStatus: return "apiCallStatus"
+ case .apiGetNetworkStatuses: return "apiGetNetworkStatuses"
case .apiChatRead: return "apiChatRead"
case .apiChatUnread: return "apiChatUnread"
case .receiveFile: return "receiveFile"
@@ -480,11 +483,14 @@ public enum ChatResponse: Decodable, Error {
case acceptingContactRequest(user: UserRef, contact: Contact)
case contactRequestRejected(user: UserRef)
case contactUpdated(user: UserRef, toContact: Contact)
+ // TODO remove events below
case contactsSubscribed(server: String, contactRefs: [ContactRef])
case contactsDisconnected(server: String, contactRefs: [ContactRef])
- case contactSubError(user: UserRef, contact: Contact, chatError: ChatError)
case contactSubSummary(user: UserRef, contactSubscriptions: [ContactSubStatus])
- case groupSubscribed(user: UserRef, groupInfo: GroupInfo)
+ // TODO remove events above
+ case networkStatus(networkStatus: NetworkStatus, connections: [String])
+ case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus])
+ case groupSubscribed(user: UserRef, groupInfo: GroupRef)
case memberSubErrors(user: UserRef, memberSubErrors: [MemberSubError])
case groupEmpty(user: UserRef, groupInfo: GroupInfo)
case userContactLinkSubscribed
@@ -620,8 +626,9 @@ public enum ChatResponse: Decodable, Error {
case .contactUpdated: return "contactUpdated"
case .contactsSubscribed: return "contactsSubscribed"
case .contactsDisconnected: return "contactsDisconnected"
- case .contactSubError: return "contactSubError"
case .contactSubSummary: return "contactSubSummary"
+ case .networkStatus: return "networkStatus"
+ case .networkStatuses: return "networkStatuses"
case .groupSubscribed: return "groupSubscribed"
case .memberSubErrors: return "memberSubErrors"
case .groupEmpty: return "groupEmpty"
@@ -757,8 +764,9 @@ public enum ChatResponse: Decodable, Error {
case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact))
case let .contactsSubscribed(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
case let .contactsDisconnected(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
- case let .contactSubError(u, contact, chatError): return withUser(u, "contact:\n\(String(describing: contact))\nerror:\n\(String(describing: chatError))")
case let .contactSubSummary(u, contactSubscriptions): return withUser(u, String(describing: contactSubscriptions))
+ case let .networkStatus(status, conns): return "networkStatus: \(String(describing: status))\nconnections: \(String(describing: conns))"
+ case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses))
case let .groupSubscribed(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .memberSubErrors(u, memberSubErrors): return withUser(u, String(describing: memberSubErrors))
case let .groupEmpty(u, groupInfo): return withUser(u, String(describing: groupInfo))
@@ -1181,6 +1189,49 @@ public struct KeepAliveOpts: Codable, Equatable {
public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4)
}
+public enum NetworkStatus: Decodable, Equatable {
+ case unknown
+ case connected
+ case disconnected
+ case error(connectionError: String)
+
+ public var statusString: LocalizedStringKey {
+ get {
+ switch self {
+ case .connected: return "connected"
+ case .error: return "error"
+ default: return "connecting"
+ }
+ }
+ }
+
+ public var statusExplanation: LocalizedStringKey {
+ get {
+ switch self {
+ case .connected: return "You are connected to the server used to receive messages from this contact."
+ case let .error(err): return "Trying to connect to the server used to receive messages from this contact (error: \(err))."
+ default: return "Trying to connect to the server used to receive messages from this contact."
+ }
+ }
+ }
+
+ public var imageName: String {
+ get {
+ switch self {
+ case .unknown: return "circle.dotted"
+ case .connected: return "circle.fill"
+ case .disconnected: return "ellipsis.circle.fill"
+ case .error: return "exclamationmark.circle.fill"
+ }
+ }
+ }
+}
+
+public struct ConnNetworkStatus: Decodable {
+ public var agentConnId: String
+ public var networkStatus: NetworkStatus
+}
+
public struct ChatSettings: Codable {
public var enableNtfs: MsgFilter
public var sendRcpts: Bool?
diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift
index 37e4f0316a..caaccb5454 100644
--- a/apps/ios/SimpleXChat/ChatTypes.swift
+++ b/apps/ios/SimpleXChat/ChatTypes.swift
@@ -1729,6 +1729,11 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat {
)
}
+public struct GroupRef: Decodable {
+ public var groupId: Int64
+ var localDisplayName: GroupName
+}
+
public struct GroupProfile: Codable, NamedChat {
public init(displayName: String, fullName: String, description: String? = nil, image: String? = nil, groupPreferences: GroupPreferences? = nil) {
self.displayName = displayName
@@ -1871,6 +1876,11 @@ public struct GroupMemberRef: Decodable {
var profile: Profile
}
+public struct GroupMemberIds: Decodable {
+ var groupMemberId: Int64
+ var groupId: Int64
+}
+
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable {
case observer = "observer"
case member = "member"
@@ -1963,7 +1973,7 @@ public enum InvitedBy: Decodable {
}
public struct MemberSubError: Decodable {
- var member: GroupMember
+ var member: GroupMemberIds
var memberError: ChatError
}
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 ac5f9fb8a5..a4b76cc79d 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
@@ -789,16 +789,19 @@ sealed class NetworkStatus {
val statusExplanation: String get() =
when (this) {
is Connected -> generalGetString(MR.strings.connected_to_server_to_receive_messages_from_contact)
- is Error -> String.format(generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages_with_error), error)
+ is Error -> String.format(generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages_with_error), connectionError)
else -> generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages)
}
@Serializable @SerialName("unknown") class Unknown: NetworkStatus()
@Serializable @SerialName("connected") class Connected: NetworkStatus()
@Serializable @SerialName("disconnected") class Disconnected: NetworkStatus()
- @Serializable @SerialName("error") class Error(val error: String): NetworkStatus()
+ @Serializable @SerialName("error") class Error(val connectionError: String): NetworkStatus()
}
+@Serializable
+data class ConnNetworkStatus(val agentConnId: String, val networkStatus: NetworkStatus)
+
@Serializable
data class Contact(
val contactId: Long,
@@ -1051,6 +1054,9 @@ data class GroupInfo (
}
}
+@Serializable
+data class GroupRef(val groupId: Long, val localDisplayName: String)
+
@Serializable
data class GroupProfile (
override val displayName: String,
@@ -1159,11 +1165,17 @@ data class GroupMember (
data class GroupMemberSettings(val showMessages: Boolean) {}
@Serializable
-class GroupMemberRef(
+data class GroupMemberRef(
val groupMemberId: Long,
val profile: Profile
)
+@Serializable
+data class GroupMemberIds(
+ val groupMemberId: Long,
+ val groupId: Long
+)
+
@Serializable
enum class GroupMemberRole(val memberRole: String) {
@SerialName("observer") Observer("observer"), // order matters in comparisons
@@ -1257,7 +1269,7 @@ class LinkPreview (
@Serializable
class MemberSubError (
- val member: GroupMember,
+ val member: GroupMemberIds,
val memberError: ChatError
)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
index dc2c3c0f25..e19520093c 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
@@ -1082,6 +1082,13 @@ object ChatController {
return r is CR.CmdOk
}
+ suspend fun apiGetNetworkStatuses(): List? {
+ val r = sendCmd(CC.ApiGetNetworkStatuses())
+ if (r is CR.NetworkStatuses) return r.networkStatuses
+ Log.e(TAG, "apiGetNetworkStatuses bad response: ${r.responseType} ${r.details}")
+ return null
+ }
+
suspend fun apiChatRead(type: ChatType, id: Long, range: CC.ItemRange): Boolean {
val r = sendCmd(CC.ApiChatRead(type, id, range))
if (r is CR.CmdOk) return true
@@ -1425,12 +1432,6 @@ object ChatController {
}
is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, NetworkStatus.Connected())
is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, NetworkStatus.Disconnected())
- is CR.ContactSubError -> {
- if (active(r.user)) {
- chatModel.updateContact(r.contact)
- }
- processContactSubError(r.contact, r.chatError)
- }
is CR.ContactSubSummary -> {
for (sub in r.contactSubscriptions) {
if (active(r.user)) {
@@ -1444,6 +1445,16 @@ object ChatController {
}
}
}
+ is CR.NetworkStatusResp -> {
+ for (cId in r.connections) {
+ chatModel.networkStatuses[cId] = r.networkStatus
+ }
+ }
+ is CR.NetworkStatuses -> {
+ for (s in r.networkStatuses) {
+ chatModel.networkStatuses[s.agentConnId] = s.networkStatus
+ }
+ }
is CR.NewChatItem -> {
val cInfo = r.chatItem.chatInfo
val cItem = r.chatItem.chatItem
@@ -1915,6 +1926,7 @@ sealed class CC {
class ApiSendCallExtraInfo(val contact: Contact, val extraInfo: WebRTCExtraInfo): CC()
class ApiEndCall(val contact: Contact): CC()
class ApiCallStatus(val contact: Contact, val callStatus: WebRTCCallStatus): CC()
+ class ApiGetNetworkStatuses(): CC()
class ApiAcceptContact(val incognito: Boolean, val contactReqId: Long): CC()
class ApiRejectContact(val contactReqId: Long): CC()
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
@@ -2024,6 +2036,7 @@ sealed class CC {
is ApiSendCallExtraInfo -> "/_call extra @${contact.apiId} ${json.encodeToString(extraInfo)}"
is ApiEndCall -> "/_call end @${contact.apiId}"
is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}"
+ is ApiGetNetworkStatuses -> "/_network_statuses"
is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}"
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
is ReceiveFile -> "/freceive $fileId encrypt=${onOff(encrypted)}" + (if (inline == null) "" else " inline=${onOff(inline)}")
@@ -2120,6 +2133,7 @@ sealed class CC {
is ApiSendCallExtraInfo -> "apiSendCallExtraInfo"
is ApiEndCall -> "apiEndCall"
is ApiCallStatus -> "apiCallStatus"
+ is ApiGetNetworkStatuses -> "apiGetNetworkStatuses"
is ApiChatRead -> "apiChatRead"
is ApiChatUnread -> "apiChatUnread"
is ReceiveFile -> "receiveFile"
@@ -3333,11 +3347,14 @@ sealed class CR {
@Serializable @SerialName("acceptingContactRequest") class AcceptingContactRequest(val user: UserRef, val contact: Contact): CR()
@Serializable @SerialName("contactRequestRejected") class ContactRequestRejected(val user: UserRef): CR()
@Serializable @SerialName("contactUpdated") class ContactUpdated(val user: UserRef, val toContact: Contact): CR()
+ // TODO remove below
@Serializable @SerialName("contactsSubscribed") class ContactsSubscribed(val server: String, val contactRefs: List): CR()
@Serializable @SerialName("contactsDisconnected") class ContactsDisconnected(val server: String, val contactRefs: List): CR()
- @Serializable @SerialName("contactSubError") class ContactSubError(val user: UserRef, val contact: Contact, val chatError: ChatError): CR()
@Serializable @SerialName("contactSubSummary") class ContactSubSummary(val user: UserRef, val contactSubscriptions: List): CR()
- @Serializable @SerialName("groupSubscribed") class GroupSubscribed(val user: UserRef, val group: GroupInfo): CR()
+ // TODO remove above
+ @Serializable @SerialName("networkStatus") class NetworkStatusResp(val networkStatus: NetworkStatus, val connections: List): CR()
+ @Serializable @SerialName("networkStatuses") class NetworkStatuses(val user_: UserRef?, val networkStatuses: List): CR()
+ @Serializable @SerialName("groupSubscribed") class GroupSubscribed(val user: UserRef, val group: GroupRef): CR()
@Serializable @SerialName("memberSubErrors") class MemberSubErrors(val user: UserRef, val memberSubErrors: List): CR()
@Serializable @SerialName("groupEmpty") class GroupEmpty(val user: UserRef, val group: GroupInfo): CR()
@Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR()
@@ -3467,8 +3484,9 @@ sealed class CR {
is ContactUpdated -> "contactUpdated"
is ContactsSubscribed -> "contactsSubscribed"
is ContactsDisconnected -> "contactsDisconnected"
- is ContactSubError -> "contactSubError"
is ContactSubSummary -> "contactSubSummary"
+ is NetworkStatusResp -> "networkStatus"
+ is NetworkStatuses -> "networkStatuses"
is GroupSubscribed -> "groupSubscribed"
is MemberSubErrors -> "memberSubErrors"
is GroupEmpty -> "groupEmpty"
@@ -3596,8 +3614,9 @@ sealed class CR {
is ContactUpdated -> withUser(user, json.encodeToString(toContact))
is ContactsSubscribed -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}"
is ContactsDisconnected -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}"
- is ContactSubError -> withUser(user, "error:\n${chatError.string}\ncontact:\n${json.encodeToString(contact)}")
is ContactSubSummary -> withUser(user, json.encodeToString(contactSubscriptions))
+ is NetworkStatusResp -> "networkStatus $networkStatus\nconnections: $connections"
+ is NetworkStatuses -> withUser(user_, json.encodeToString(networkStatuses))
is GroupSubscribed -> withUser(user, json.encodeToString(group))
is MemberSubErrors -> withUser(user, json.encodeToString(memberSubErrors))
is GroupEmpty -> withUser(user, json.encodeToString(group))
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index b7421d9366..56077c3bf6 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -143,7 +143,8 @@ defaultChatConfig =
initialCleanupManagerDelay = 30 * 1000000, -- 30 seconds
cleanupManagerInterval = 30 * 60, -- 30 minutes
cleanupManagerStepDelay = 3 * 1000000, -- 3 seconds
- ciExpirationInterval = 30 * 60 * 1000000 -- 30 minutes
+ ciExpirationInterval = 30 * 60 * 1000000, -- 30 minutes
+ coreApi = False
}
_defaultSMPServers :: NonEmpty SMPServerWithAuth
@@ -195,6 +196,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
idsDrg <- newTVarIO =<< liftIO drgNew
inputQ <- newTBQueueIO tbqSize
outputQ <- newTBQueueIO tbqSize
+ connNetworkStatuses <- atomically TM.empty
subscriptionMode <- newTVarIO SMSubscribe
chatLock <- newEmptyTMVarIO
sndFiles <- newTVarIO M.empty
@@ -221,6 +223,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
idsDrg,
inputQ,
outputQ,
+ connNetworkStatuses,
subscriptionMode,
chatLock,
sndFiles,
@@ -1086,6 +1089,8 @@ processChatCommand = \case
user <- getUserByContactId db contactId
contact <- getContact db user contactId
pure RcvCallInvitation {user, contact, callType = peerCallType, sharedKey, callTs}
+ APIGetNetworkStatuses -> withUser $ \_ ->
+ CRNetworkStatuses Nothing . map (uncurry ConnNetworkStatus) . M.toList <$> chatReadVar connNetworkStatuses
APICallStatus contactId receivedStatus ->
withCurrentCall contactId $ \user ct call ->
updateCallItemStatus user ct call receivedStatus Nothing $> Just call
@@ -1688,6 +1693,8 @@ processChatCommand = \case
(connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode
-- [incognito] reuse membership incognito profile
ct <- withStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode
+ -- TODO not sure it is correct to set connections status here?
+ setContactNetworkStatus ct NSConnected
pure $ CRNewMemberContact user ct g m
_ -> throwChatError CEGroupMemberNotActive
APISendMemberContactInvitation contactId msgContent_ -> withUser $ \user -> do
@@ -2627,6 +2634,7 @@ subscribeUserConnections onlyNeeded agentBatchSubscribe user@User {userId} = do
rs <- withAgent $ \a -> agentBatchSubscribe a conns
-- send connection events to view
contactSubsToView rs cts ce
+-- TODO possibly, we could either disable these events or replace with less noisy for API
contactLinkSubsToView rs ucs
groupSubsToView rs gs ms ce
sndFileSubsToView rs sfts
@@ -2687,12 +2695,30 @@ subscribeUserConnections onlyNeeded agentBatchSubscribe user@User {userId} = do
let connIds = map aConnId' pcs
pure (connIds, M.fromList $ zip connIds pcs)
contactSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId Contact -> Bool -> m ()
- contactSubsToView rs cts ce = do
- toView . CRContactSubSummary user $ map (uncurry ContactSubStatus) cRs
- when ce $ mapM_ (toView . uncurry (CRContactSubError user)) cErrors
+ contactSubsToView rs cts ce = ifM (asks $ coreApi . config) notifyAPI notifyCLI
where
- cRs = resultsFor rs cts
- cErrors = sortOn (\(Contact {localDisplayName = n}, _) -> n) $ filterErrors cRs
+ notifyCLI = do
+ let cRs = resultsFor rs cts
+ cErrors = sortOn (\(Contact {localDisplayName = n}, _) -> n) $ filterErrors cRs
+ toView . CRContactSubSummary user $ map (uncurry ContactSubStatus) cRs
+ when ce $ mapM_ (toView . uncurry (CRContactSubError user)) cErrors
+ notifyAPI = do
+ let statuses = M.foldrWithKey' addStatus [] cts
+ chatModifyVar connNetworkStatuses $ M.union (M.fromList statuses)
+ toView $ CRNetworkStatuses (Just user) $ map (uncurry ConnNetworkStatus) statuses
+ where
+ addStatus :: ConnId -> Contact -> [(AgentConnId, NetworkStatus)] -> [(AgentConnId, NetworkStatus)]
+ addStatus connId ct =
+ let ns = (contactAgentConnId ct, netStatus $ resultErr connId rs)
+ in (ns :)
+ netStatus :: Maybe ChatError -> NetworkStatus
+ netStatus = maybe NSConnected $ NSError . errorNetworkStatus
+ errorNetworkStatus :: ChatError -> String
+ errorNetworkStatus = \case
+ ChatErrorAgent (BROKER _ NETWORK) _ -> "network"
+ ChatErrorAgent (SMP SMP.AUTH) _ -> "contact deleted"
+ e -> show e
+-- TODO possibly below could be replaced with less noisy events for API
contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> m ()
contactLinkSubsToView rs = toView . CRUserContactSubSummary user . map (uncurry UserContactSubStatus) . resultsFor rs
groupSubsToView :: Map ConnId (Either AgentErrorType ()) -> [Group] -> Map ConnId GroupMember -> Bool -> m ()
@@ -2742,12 +2768,12 @@ subscribeUserConnections onlyNeeded agentBatchSubscribe user@User {userId} = do
resultsFor rs = M.foldrWithKey' addResult []
where
addResult :: ConnId -> a -> [(a, Maybe ChatError)] -> [(a, Maybe ChatError)]
- addResult connId = (:) . (,err)
- where
- err = case M.lookup connId rs of
- Just (Left e) -> Just $ ChatErrorAgent e Nothing
- Just _ -> Nothing
- _ -> Just . ChatError . CEAgentNoSubResult $ AgentConnId connId
+ addResult connId = (:) . (,resultErr connId rs)
+ resultErr :: ConnId -> Map ConnId (Either AgentErrorType ()) -> Maybe ChatError
+ resultErr connId rs = case M.lookup connId rs of
+ Just (Left e) -> Just $ ChatErrorAgent e Nothing
+ Just _ -> Nothing
+ _ -> Just . ChatError . CEAgentNoSubResult $ AgentConnId connId
cleanupManager :: forall m. ChatMonad m => m ()
cleanupManager = do
@@ -2892,16 +2918,22 @@ processAgentMessageNoConn :: forall m. ChatMonad m => ACommand 'Agent 'AENone ->
processAgentMessageNoConn = \case
CONNECT p h -> hostEvent $ CRHostConnected p h
DISCONNECT p h -> hostEvent $ CRHostDisconnected p h
- DOWN srv conns -> serverEvent srv conns CRContactsDisconnected
- UP srv conns -> serverEvent srv conns CRContactsSubscribed
+ DOWN srv conns -> serverEvent srv conns NSDisconnected CRContactsDisconnected
+ UP srv conns -> serverEvent srv conns NSConnected CRContactsSubscribed
SUSPENDED -> toView CRChatSuspended
DEL_USER agentUserId -> toView $ CRAgentUserDeleted agentUserId
where
hostEvent :: ChatResponse -> m ()
hostEvent = whenM (asks $ hostEvents . config) . toView
- serverEvent srv conns event = do
- cs <- withStore' (`getConnectionsContacts` conns)
- toView $ event srv cs
+ serverEvent srv conns nsStatus event = ifM (asks $ coreApi . config) notifyAPI notifyCLI
+ where
+ notifyAPI = do
+ let connIds = map AgentConnId conns
+ chatModifyVar connNetworkStatuses $ \m -> foldl' (\m' cId -> M.insert cId nsStatus m') m connIds
+ toView $ CRNetworkStatus nsStatus connIds
+ notifyCLI = do
+ cs <- withStore' (`getConnectionsContacts` conns)
+ toView $ event srv cs
processAgentMsgSndFile :: forall m. ChatMonad m => ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> m ()
processAgentMsgSndFile _corrId aFileId msg =
@@ -3188,6 +3220,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
Nothing -> do
-- [incognito] print incognito profile used for this contact
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
+ setContactNetworkStatus ct NSConnected
toView $ CRContactConnected user ct (fmap fromLocalProfile incognitoProfile)
when (directOrUsed ct) $ createFeatureEnabledItems ct
when (contactConnInitiated conn) $ do
@@ -3762,6 +3795,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
notifyMemberConnected :: GroupInfo -> GroupMember -> Maybe Contact -> m ()
notifyMemberConnected gInfo m ct_ = do
memberConnectedChatItem gInfo m
+ mapM_ (`setContactNetworkStatus` NSConnected) ct_
toView $ CRConnectedToGroupMember user gInfo m ct_
probeMatchingContactsAndMembers :: Contact -> IncognitoEnabled -> Bool -> m ()
@@ -5569,6 +5603,7 @@ chatCommandP =
"/_call end @" *> (APIEndCall <$> A.decimal),
"/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP),
"/_call get" $> APIGetCallInvitations,
+ "/_network_statuses" $> APIGetNetworkStatuses,
"/_profile " *> (APIUpdateProfile <$> A.decimal <* A.space <*> jsonP),
"/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
"/_set alias :" *> (APISetConnectionAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index af9f34d2d4..2704c2721f 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -32,6 +32,7 @@ import Data.Char (ord)
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty)
import Data.Map.Strict (Map)
+import qualified Data.Map.Strict as M
import Data.String
import Data.Text (Text)
import Data.Time (NominalDiffTime, UTCTime)
@@ -124,7 +125,8 @@ data ChatConfig = ChatConfig
initialCleanupManagerDelay :: Int64,
cleanupManagerInterval :: NominalDiffTime,
cleanupManagerStepDelay :: Int64,
- ciExpirationInterval :: Int64 -- microseconds
+ ciExpirationInterval :: Int64, -- microseconds
+ coreApi :: Bool
}
data DefaultAgentServers = DefaultAgentServers
@@ -164,6 +166,7 @@ data ChatController = ChatController
idsDrg :: TVar ChaChaDRG,
inputQ :: TBQueue String,
outputQ :: TBQueue (Maybe CorrId, ChatResponse),
+ connNetworkStatuses :: TMap AgentConnId NetworkStatus,
subscriptionMode :: TVar SubscriptionMode,
chatLock :: Lock,
sndFiles :: TVar (Map Int64 Handle),
@@ -251,6 +254,7 @@ data ChatCommand
| APIEndCall ContactId
| APIGetCallInvitations
| APICallStatus ContactId WebRTCCallStatus
+ | APIGetNetworkStatuses
| APIUpdateProfile UserId Profile
| APISetContactPrefs ContactId Preferences
| APISetContactAlias ContactId LocalAlias
@@ -528,6 +532,8 @@ data ChatResponse
| CRContactSubError {user :: User, contact :: Contact, chatError :: ChatError}
| CRContactSubSummary {user :: User, contactSubscriptions :: [ContactSubStatus]}
| CRUserContactSubSummary {user :: User, userContactSubscriptions :: [UserContactSubStatus]}
+ | CRNetworkStatus {networkStatus :: NetworkStatus, connections :: [AgentConnId]}
+ | CRNetworkStatuses {user_ :: Maybe User, networkStatuses :: [ConnNetworkStatus]}
| CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost}
| CRHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost}
| CRGroupInvitation {user :: User, groupInfo :: GroupInfo}
@@ -1044,6 +1050,13 @@ chatWriteVar :: ChatMonad' m => (ChatController -> TVar a) -> a -> m ()
chatWriteVar f value = asks f >>= atomically . (`writeTVar` value)
{-# INLINE chatWriteVar #-}
+chatModifyVar :: ChatMonad' m => (ChatController -> TVar a) -> (a -> a) -> m ()
+chatModifyVar f newValue = asks f >>= atomically . (`modifyTVar'` newValue)
+{-# INLINE chatModifyVar #-}
+
+setContactNetworkStatus :: ChatMonad' m => Contact -> NetworkStatus -> m ()
+setContactNetworkStatus ct = chatModifyVar connNetworkStatuses . M.insert (contactAgentConnId ct)
+
tryChatError :: ChatMonad m => m a -> m (Either ChatError a)
tryChatError = tryAllErrors mkChatError
{-# INLINE tryChatError #-}
diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs
index 0a970d2c8e..b444888145 100644
--- a/src/Simplex/Chat/Mobile.hs
+++ b/src/Simplex/Chat/Mobile.hs
@@ -169,7 +169,8 @@ defaultMobileConfig :: ChatConfig
defaultMobileConfig =
defaultChatConfig
{ confirmMigrations = MCYesUp,
- logLevel = CLLError
+ logLevel = CLLError,
+ coreApi = True
}
getActiveUser_ :: SQLiteStore -> IO (Maybe User)
diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs
index 83d0664a04..9992f96ab0 100644
--- a/src/Simplex/Chat/Types.hs
+++ b/src/Simplex/Chat/Types.hs
@@ -192,6 +192,9 @@ instance ToJSON Contact where
contactConn :: Contact -> Connection
contactConn Contact {activeConn} = activeConn
+contactAgentConnId :: Contact -> AgentConnId
+contactAgentConnId Contact {activeConn = Connection {agentConnId}} = agentConnId
+
contactConnId :: Contact -> ConnId
contactConnId = aConnId . contactConn
@@ -1140,13 +1143,16 @@ liveRcvFileTransferPath ft = fp <$> liveRcvFileTransferInfo ft
fp RcvFileInfo {filePath} = filePath
newtype AgentConnId = AgentConnId ConnId
- deriving (Eq, Show)
+ deriving (Eq, Ord, Show)
instance StrEncoding AgentConnId where
strEncode (AgentConnId connId) = strEncode connId
strDecode s = AgentConnId <$> strDecode s
strP = AgentConnId <$> strP
+instance FromJSON AgentConnId where
+ parseJSON = strParseJSON "AgentConnId"
+
instance ToJSON AgentConnId where
toJSON = strToJSON
toEncoding = strToJEncoding
@@ -1477,6 +1483,35 @@ serializeIntroStatus = \case
textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a
textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . textDecode
+data NetworkStatus
+ = NSUnknown
+ | NSConnected
+ | NSDisconnected
+ | NSError {connectionError :: String}
+ deriving (Eq, Ord, Show, Generic)
+
+netStatusStr :: NetworkStatus -> String
+netStatusStr = \case
+ NSUnknown -> "unknown"
+ NSConnected -> "connected"
+ NSDisconnected -> "disconnected"
+ NSError e -> "error: " <> e
+
+instance FromJSON NetworkStatus where
+ parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "NS"
+
+instance ToJSON NetworkStatus where
+ toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "NS"
+ toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "NS"
+
+data ConnNetworkStatus = ConnNetworkStatus
+ { agentConnId :: AgentConnId,
+ networkStatus :: NetworkStatus
+ }
+ deriving (Show, Generic, FromJSON)
+
+instance ToJSON ConnNetworkStatus where toEncoding = J.genericToEncoding J.defaultOptions
+
type CommandId = Int64
aCorrId :: CommandId -> ACorrId
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index 86a10988d6..bcbc45f4eb 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -19,7 +19,7 @@ import Data.Char (isSpace, toUpper)
import Data.Function (on)
import Data.Int (Int64)
import Data.List (groupBy, intercalate, intersperse, partition, sortOn)
-import Data.List.NonEmpty (NonEmpty)
+import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
@@ -210,6 +210,8 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
(addresses, groupLinks) = partition (\UserContactSubStatus {userContact} -> isNothing . userContactGroupId $ userContact) summary
addressSS UserContactSubStatus {userContactError} = maybe ("Your address is active! To show: " <> highlight' "/sa") (\e -> "User address error: " <> sShow e <> ", to delete your address: " <> highlight' "/da") userContactError
(groupLinkErrors, groupLinksSubscribed) = partition (isJust . userContactError) groupLinks
+ CRNetworkStatus status conns -> if testView then [plain $ show (length conns) <> " connections " <> netStatusStr status] else []
+ CRNetworkStatuses u statuses -> if testView then ttyUser' u $ viewNetworkStatuses statuses else []
CRGroupInvitation u g -> ttyUser u [groupInvitation' g]
CRReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r
CRUserJoinedGroup u g _ -> ttyUser u $ viewUserJoinedGroup g
@@ -800,6 +802,12 @@ viewDirectMessagesProhibited :: MsgDirection -> Contact -> [StyledString]
viewDirectMessagesProhibited MDSnd c = ["direct messages to indirect contact " <> ttyContact' c <> " are prohibited"]
viewDirectMessagesProhibited MDRcv c = ["received prohibited direct message from indirect contact " <> ttyContact' c <> " (discarded)"]
+viewNetworkStatuses :: [ConnNetworkStatus] -> [StyledString]
+viewNetworkStatuses = map viewStatuses . L.groupBy ((==) `on` netStatus) . sortOn netStatus
+ where
+ netStatus ConnNetworkStatus {networkStatus} = networkStatus
+ viewStatuses ss@(s :| _) = plain $ show (L.length ss) <> " connections " <> netStatusStr (netStatus s)
+
viewUserJoinedGroup :: GroupInfo -> [StyledString]
viewUserJoinedGroup g =
case incognitoMembershipProfile g of
diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs
index b4c3c53cd9..677bc4c09d 100644
--- a/tests/ChatTests/Direct.hs
+++ b/tests/ChatTests/Direct.hs
@@ -119,6 +119,8 @@ chatDirectTests = do
testReqVRange vr11 supportedChatVRange
testReqVRange vr11 vr11
it "update peer version range on received messages" testUpdatePeerChatVRange
+ describe "network statuses" $ do
+ it "should get network statuses" testGetNetworkStatuses
where
testInvVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnInvChatVRange vr1 vr2
testReqVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnReqChatVRange vr1 vr2
@@ -2623,6 +2625,20 @@ testUpdatePeerChatVRange tmp =
where
cfg11 = testCfg {chatVRange = vr11} :: ChatConfig
+testGetNetworkStatuses :: HasCallStack => FilePath -> IO ()
+testGetNetworkStatuses tmp = do
+ withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
+ withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do
+ connectUsers alice bob
+ alice ##> "/_network_statuses"
+ alice <## "1 connections connected"
+ withTestChatCfg tmp cfg "alice" $ \alice ->
+ withTestChatCfg tmp cfg "bob" $ \bob -> do
+ alice <## "1 connections connected"
+ bob <## "1 connections connected"
+ where
+ cfg = testCfg {coreApi = True}
+
vr11 :: VersionRange
vr11 = mkVersionRange 1 1
diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs
index 692dd5884e..aa36b397a3 100644
--- a/tests/MobileTests.hs
+++ b/tests/MobileTests.hs
@@ -120,19 +120,19 @@ chatStartedSwift = "{\"resp\":{\"_owsf\":true,\"chatStarted\":{}}}"
chatStartedTagged :: LB.ByteString
chatStartedTagged = "{\"resp\":{\"type\":\"chatStarted\"}}"
-contactSubSummary :: LB.ByteString
-contactSubSummary =
+networkStatuses :: LB.ByteString
+networkStatuses =
#if defined(darwin_HOST_OS) && defined(swiftJSON)
- contactSubSummarySwift
+ networkStatusesSwift
#else
- contactSubSummaryTagged
+ networkStatusesTagged
#endif
-contactSubSummarySwift :: LB.ByteString
-contactSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"contactSubSummary\":{" <> userJSON <> ",\"contactSubscriptions\":[]}}}"
+networkStatusesSwift :: LB.ByteString
+networkStatusesSwift = "{\"resp\":{\"_owsf\":true,\"networkStatuses\":{\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}}"
-contactSubSummaryTagged :: LB.ByteString
-contactSubSummaryTagged = "{\"resp\":{\"type\":\"contactSubSummary\"," <> userJSON <> ",\"contactSubscriptions\":[]}}"
+networkStatusesTagged :: LB.ByteString
+networkStatusesTagged = "{\"resp\":{\"type\":\"networkStatuses\",\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}"
memberSubSummary :: LB.ByteString
memberSubSummary =
@@ -143,10 +143,10 @@ memberSubSummary =
#endif
memberSubSummarySwift :: LB.ByteString
-memberSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"memberSubSummary\":{" <> userJSON <> ",\"memberSubscriptions\":[]}}}"
+memberSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"memberSubSummary\":{\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}}"
memberSubSummaryTagged :: LB.ByteString
-memberSubSummaryTagged = "{\"resp\":{\"type\":\"memberSubSummary\"," <> userJSON <> ",\"memberSubscriptions\":[]}}"
+memberSubSummaryTagged = "{\"resp\":{\"type\":\"memberSubSummary\",\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}"
userContactSubSummary :: LB.ByteString
userContactSubSummary =
@@ -157,10 +157,10 @@ userContactSubSummary =
#endif
userContactSubSummarySwift :: LB.ByteString
-userContactSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"userContactSubSummary\":{" <> userJSON <> ",\"userContactSubscriptions\":[]}}}"
+userContactSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"userContactSubSummary\":{\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}}"
userContactSubSummaryTagged :: LB.ByteString
-userContactSubSummaryTagged = "{\"resp\":{\"type\":\"userContactSubSummary\"," <> userJSON <> ",\"userContactSubscriptions\":[]}}"
+userContactSubSummaryTagged = "{\"resp\":{\"type\":\"userContactSubSummary\",\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}"
pendingSubSummary :: LB.ByteString
pendingSubSummary =
@@ -171,13 +171,13 @@ pendingSubSummary =
#endif
pendingSubSummarySwift :: LB.ByteString
-pendingSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"pendingSubSummary\":{" <> userJSON <> ",\"pendingSubscriptions\":[]}}}"
+pendingSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"pendingSubSummary\":{\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}}"
pendingSubSummaryTagged :: LB.ByteString
-pendingSubSummaryTagged = "{\"resp\":{\"type\":\"pendingSubSummary\"," <> userJSON <> ",\"pendingSubscriptions\":[]}}"
+pendingSubSummaryTagged = "{\"resp\":{\"type\":\"pendingSubSummary\",\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}"
userJSON :: LB.ByteString
-userJSON = "\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}"
+userJSON = "{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}"
parsedMarkdown :: LB.ByteString
parsedMarkdown =
@@ -215,7 +215,7 @@ testChatApi tmp = do
chatSendCmd cc "/u" `shouldReturn` activeUser
chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUserExists
chatSendCmd cc "/_start" `shouldReturn` chatStarted
- chatRecvMsg cc `shouldReturn` contactSubSummary
+ chatRecvMsg cc `shouldReturn` networkStatuses
chatRecvMsg cc `shouldReturn` userContactSubSummary
chatRecvMsg cc `shouldReturn` memberSubSummary
chatRecvMsgWait cc 10000 `shouldReturn` pendingSubSummary
From 07047a3ef365bf779bb2d74281e37e45a2cdf57d Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Fri, 13 Oct 2023 14:29:48 +0100
Subject: [PATCH 28/80] ios: fix pattern match
---
apps/ios/Shared/Model/SimpleXAPI.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index e4f64ee977..18e7f7efbb 100644
--- a/apps/ios/Shared/Model/SimpleXAPI.swift
+++ b/apps/ios/Shared/Model/SimpleXAPI.swift
@@ -1374,7 +1374,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
m.networkStatuses[cId] = status
}
}
- case let .networkStatuses(statuses): ()
+ case let .networkStatuses(_, statuses): ()
await MainActor.run {
for s in statuses {
m.networkStatuses[s.agentConnId] = s.networkStatus
From c609303348784a23a134424faaad1e2813b247ab Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Fri, 13 Oct 2023 19:19:00 +0400
Subject: [PATCH 29/80] ios: connect plan (#3205)
* ios: connect plan
* improvements
* wording
* fixes
* rework to use dismissAllSheets with callback
* rework
* update texts
* Update apps/ios/Shared/Views/NewChat/NewChatButton.swift
* Update apps/ios/Shared/Views/NewChat/NewChatButton.swift
---------
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
---
apps/ios/Shared/ContentView.swift | 37 +-
apps/ios/Shared/Model/ChatModel.swift | 10 +
apps/ios/Shared/Model/SimpleXAPI.swift | 21 +-
.../Chat/Group/GroupMemberInfoView.swift | 35 +-
.../Shared/Views/NewChat/NewChatButton.swift | 346 ++++++++++++++++--
.../Views/NewChat/PasteToConnectView.swift | 18 +-
.../Views/NewChat/ScanToConnectView.swift | 18 +-
apps/ios/SimpleX.xcodeproj/project.pbxproj | 30 +-
apps/ios/SimpleXChat/APITypes.swift | 34 ++
9 files changed, 437 insertions(+), 112 deletions(-)
diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift
index 3dbcf47004..04eadac2c1 100644
--- a/apps/ios/Shared/ContentView.swift
+++ b/apps/ios/Shared/ContentView.swift
@@ -31,11 +31,11 @@ struct ContentView: View {
@State private var chatListActionSheet: ChatListActionSheet? = nil
private enum ChatListActionSheet: Identifiable {
- case connectViaUrl(action: ConnReqType, link: String)
+ case planAndConnectSheet(sheet: PlanAndConnectActionSheet)
var id: String {
switch self {
- case let .connectViaUrl(_, link): return "connectViaUrl \(link)"
+ case let .planAndConnectSheet(sheet): return sheet.id
}
}
}
@@ -93,7 +93,7 @@ struct ContentView: View {
mainView()
.actionSheet(item: $chatListActionSheet) { sheet in
switch sheet {
- case let .connectViaUrl(action, link): return connectViaUrlSheet(action, link)
+ case let .planAndConnectSheet(sheet): return planAndConnectActionSheet(sheet, dismiss: false)
}
}
} else {
@@ -290,12 +290,19 @@ struct ContentView: View {
if let url = m.appOpenUrl {
m.appOpenUrl = nil
var path = url.path
- logger.debug("ContentView.connectViaUrl path: \(path)")
if (path == "/contact" || path == "/invitation") {
path.removeFirst()
- let action: ConnReqType = path == "contact" ? .contact : .invitation
- let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
- chatListActionSheet = .connectViaUrl(action: action, link: link)
+ // TODO normalize in backend; revert
+ // let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
+ var link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
+ link = link.starts(with: "simplex:/") ? link.replacingOccurrences(of: "simplex:/", with: "https://simplex.chat/") : link
+ planAndConnect(
+ link,
+ showAlert: showPlanAndConnectAlert,
+ showActionSheet: { chatListActionSheet = .planAndConnectSheet(sheet: $0) },
+ dismiss: false,
+ incognito: nil
+ )
} else {
AlertManager.shared.showAlert(Alert(title: Text("Error: URL is invalid")))
}
@@ -303,20 +310,8 @@ struct ContentView: View {
}
}
- private func connectViaUrlSheet(_ action: ConnReqType, _ link: String) -> ActionSheet {
- let title: LocalizedStringKey
- switch action {
- case .contact: title = "Connect via contact link"
- case .invitation: title = "Connect via one-time link"
- }
- return ActionSheet(
- title: Text(title),
- buttons: [
- .default(Text("Use current profile")) { connectViaLink(link, incognito: false) },
- .default(Text("Use new incognito profile")) { connectViaLink(link, incognito: true) },
- .cancel()
- ]
- )
+ private func showPlanAndConnectAlert(_ alert: PlanAndConnectAlert) {
+ AlertManager.shared.showAlert(planAndConnectAlert(alert, dismiss: false))
}
}
diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift
index ec1a567420..b1a83dd5e6 100644
--- a/apps/ios/Shared/Model/ChatModel.swift
+++ b/apps/ios/Shared/Model/ChatModel.swift
@@ -152,6 +152,16 @@ final class ChatModel: ObservableObject {
}
}
+ func getGroupChat(_ groupId: Int64) -> Chat? {
+ chats.first { chat in
+ if case let .group(groupInfo) = chat.chatInfo {
+ return groupInfo.groupId == groupId
+ } else {
+ return false
+ }
+ }
+ }
+
private func getChatIndex(_ id: String) -> Int? {
chats.firstIndex(where: { $0.id == id })
}
diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index 18e7f7efbb..a1c8cee774 100644
--- a/apps/ios/Shared/Model/SimpleXAPI.swift
+++ b/apps/ios/Shared/Model/SimpleXAPI.swift
@@ -586,6 +586,15 @@ func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> P
throw r
}
+func apiConnectPlan(connReq: String) async throws -> ConnectionPlan {
+ logger.error("apiConnectPlan connReq: \(connReq)")
+ let userId = try currentUserId("apiConnectPlan")
+ let r = await chatSendCmd(.apiConnectPlan(userId: userId, connReq: connReq))
+ if case let .connectionPlan(_, connectionPlan) = r { return connectionPlan }
+ logger.error("apiConnectPlan error: \(responseError(r))")
+ throw r
+}
+
func apiConnect(incognito: Bool, connReq: String) async -> ConnReqType? {
let (connReqType, alert) = await apiConnect_(incognito: incognito, connReq: connReq)
if let alert = alert {
@@ -610,10 +619,7 @@ func apiConnect_(incognito: Bool, connReq: String) async -> (ConnReqType?, Alert
if let c = m.getContactChat(contact.contactId) {
await MainActor.run { m.chatId = c.id }
}
- let alert = mkAlert(
- title: "Contact already exists",
- message: "You are already connected to \(contact.displayName)."
- )
+ let alert = contactAlreadyExistsAlert(contact)
return (nil, alert)
case .chatCmdError(_, .error(.invalidConnReq)):
let alert = mkAlert(
@@ -641,6 +647,13 @@ func apiConnect_(incognito: Bool, connReq: String) async -> (ConnReqType?, Alert
return (nil, alert)
}
+func contactAlreadyExistsAlert(_ contact: Contact) -> Alert {
+ mkAlert(
+ title: "Contact already exists",
+ message: "You are already connected to \(contact.displayName)."
+ )
+}
+
private func connectionErrorAlert(_ r: ChatResponse) -> Alert {
if let networkErrorAlert = networkErrorAlert(r) {
return networkErrorAlert
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
index 5ec14f5be1..3d10101dee 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
@@ -19,7 +19,7 @@ struct GroupMemberInfoView: View {
@State private var connectionCode: String? = nil
@State private var newRole: GroupMemberRole = .member
@State private var alert: GroupMemberInfoViewAlert?
- @State private var connectToMemberDialog: Bool = false
+ @State private var sheet: PlanAndConnectActionSheet?
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@State private var justOpened = true
@State private var progressIndicator = false
@@ -30,9 +30,8 @@ struct GroupMemberInfoView: View {
case switchAddressAlert
case abortSwitchAddressAlert
case syncConnectionForceAlert
- case connRequestSentAlert(type: ConnReqType)
+ case planAndConnectAlert(alert: PlanAndConnectAlert)
case error(title: LocalizedStringKey, error: LocalizedStringKey)
- case other(alert: Alert)
var id: String {
switch self {
@@ -41,9 +40,8 @@ struct GroupMemberInfoView: View {
case .switchAddressAlert: return "switchAddressAlert"
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
- case .connRequestSentAlert: return "connRequestSentAlert"
+ case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)"
case let .error(title, _): return "error \(title)"
- case let .other(alert): return "other \(alert)"
}
}
}
@@ -206,11 +204,11 @@ struct GroupMemberInfoView: View {
case .switchAddressAlert: return switchAddressAlert(switchMemberAddress)
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress)
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) })
- case let .connRequestSentAlert(type): return connReqSentAlert(type)
+ case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true)
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
- case let .other(alert): return alert
}
}
+ .actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) }
if progressIndicator {
ProgressView().scaleEffect(2)
@@ -220,25 +218,16 @@ struct GroupMemberInfoView: View {
func connectViaAddressButton(_ contactLink: String) -> some View {
Button {
- connectToMemberDialog = true
+ planAndConnect(
+ contactLink,
+ showAlert: { alert = .planAndConnectAlert(alert: $0) },
+ showActionSheet: { sheet = $0 },
+ dismiss: true,
+ incognito: nil
+ )
} label: {
Label("Connect", systemImage: "link")
}
- .confirmationDialog("Connect directly", isPresented: $connectToMemberDialog, titleVisibility: .visible) {
- Button("Use current profile") { connectViaAddress(incognito: false, contactLink: contactLink) }
- Button("Use new incognito profile") { connectViaAddress(incognito: true, contactLink: contactLink) }
- }
- }
-
- func connectViaAddress(incognito: Bool, contactLink: String) {
- Task {
- let (connReqType, connectAlert) = await apiConnect_(incognito: incognito, connReq: contactLink)
- if let connReqType = connReqType {
- alert = .connRequestSentAlert(type: connReqType)
- } else if let connectAlert = connectAlert {
- alert = .other(alert: connectAlert)
- }
- }
}
func knownDirectChatButton(_ chat: Chat) -> some View {
diff --git a/apps/ios/Shared/Views/NewChat/NewChatButton.swift b/apps/ios/Shared/Views/NewChat/NewChatButton.swift
index a727ad6be0..e4fbd8bd47 100644
--- a/apps/ios/Shared/Views/NewChat/NewChatButton.swift
+++ b/apps/ios/Shared/Views/NewChat/NewChatButton.swift
@@ -58,65 +58,331 @@ struct NewChatButton: View {
}
}
-enum ConnReqType: Equatable {
- case contact
- case invitation
+enum PlanAndConnectAlert: Identifiable {
+ case ownInvitationLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
+ case invitationLinkConnecting(connectionLink: String)
+ case ownContactAddressConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
+ case groupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
+ case groupLinkConnecting(connectionLink: String, groupInfo: GroupInfo?)
+
+ var id: String {
+ switch self {
+ case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink)"
+ case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink)"
+ case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink)"
+ case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink)"
+ case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink)"
+ }
+ }
}
-func connectViaLink(_ connectionLink: String, dismiss: DismissAction? = nil, incognito: Bool) {
- Task {
- if let connReqType = await apiConnect(incognito: incognito, connReq: connectionLink) {
- DispatchQueue.main.async {
- dismiss?()
- AlertManager.shared.showAlert(connReqSentAlert(connReqType))
- }
+func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool) -> Alert {
+ switch alert {
+ case let .ownInvitationLinkConfirmConnect(connectionLink, connectionPlan, incognito):
+ return Alert(
+ title: Text("Connect to yourself?"),
+ message: Text("This is your own one-time link!"),
+ primaryButton: .destructive(
+ Text(incognito ? "Connect incognito" : "Connect"),
+ action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
+ ),
+ secondaryButton: .cancel()
+ )
+ case .invitationLinkConnecting:
+ return Alert(
+ title: Text("Already connecting!"),
+ message: Text("You are already connecting via this one-time link!")
+ )
+ case let .ownContactAddressConfirmConnect(connectionLink, connectionPlan, incognito):
+ return Alert(
+ title: Text("Connect to yourself?"),
+ message: Text("This is your own SimpleX address!"),
+ primaryButton: .destructive(
+ Text(incognito ? "Connect incognito" : "Connect"),
+ action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
+ ),
+ secondaryButton: .cancel()
+ )
+ case let .groupLinkConfirmConnect(connectionLink, connectionPlan, incognito):
+ return Alert(
+ title: Text("Join group?"),
+ message: Text("You will connect to all group members."),
+ primaryButton: .default(
+ Text(incognito ? "Join incognito" : "Join"),
+ action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
+ ),
+ secondaryButton: .cancel()
+ )
+ case let .groupLinkConnecting(_, groupInfo):
+ if let groupInfo = groupInfo {
+ return Alert(
+ title: Text("Group already exists!"),
+ message: Text("You are already joining the group \(groupInfo.displayName).")
+ )
} else {
- DispatchQueue.main.async {
- dismiss?()
+ return Alert(
+ title: Text("Already joining the group!"),
+ message: Text("You are already joining the group via this link.")
+ )
+ }
+ }
+}
+
+enum PlanAndConnectActionSheet: Identifiable {
+ case askCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan?, title: LocalizedStringKey)
+ case ownLinkAskCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan, title: LocalizedStringKey)
+ case ownGroupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo)
+
+ var id: String {
+ switch self {
+ case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink)"
+ case let .ownLinkAskCurrentOrIncognitoProfile(connectionLink, _, _): return "ownLinkAskCurrentOrIncognitoProfile \(connectionLink)"
+ case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink)"
+ }
+ }
+}
+
+func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool) -> ActionSheet {
+ switch sheet {
+ case let .askCurrentOrIncognitoProfile(connectionLink, connectionPlan, title):
+ return ActionSheet(
+ title: Text(title),
+ buttons: [
+ .default(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) },
+ .default(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) },
+ .cancel()
+ ]
+ )
+ case let .ownLinkAskCurrentOrIncognitoProfile(connectionLink, connectionPlan, title):
+ return ActionSheet(
+ title: Text(title),
+ buttons: [
+ .destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) },
+ .destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) },
+ .cancel()
+ ]
+ )
+ case let .ownGroupLinkConfirmConnect(connectionLink, connectionPlan, incognito, groupInfo):
+ if let incognito = incognito {
+ return ActionSheet(
+ title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"),
+ buttons: [
+ .default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) },
+ .destructive(Text(incognito ? "Join incognito" : "Join with current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) },
+ .cancel()
+ ]
+ )
+ } else {
+ return ActionSheet(
+ title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"),
+ buttons: [
+ .default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) },
+ .destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) },
+ .destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) },
+ .cancel()
+ ]
+ )
+ }
+ }
+}
+
+func planAndConnect(
+ _ connectionLink: String,
+ showAlert: @escaping (PlanAndConnectAlert) -> Void,
+ showActionSheet: @escaping (PlanAndConnectActionSheet) -> Void,
+ dismiss: Bool,
+ incognito: Bool?
+) {
+ Task {
+ do {
+ let connectionPlan = try await apiConnectPlan(connReq: connectionLink)
+ switch connectionPlan {
+ case let .invitationLink(ilp):
+ switch ilp {
+ case .ok:
+ logger.debug("planAndConnect, .invitationLink, .ok, incognito=\(incognito?.description ?? "nil")")
+ if let incognito = incognito {
+ connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito)
+ } else {
+ showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link"))
+ }
+ case .ownLink:
+ logger.debug("planAndConnect, .invitationLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
+ if let incognito = incognito {
+ showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
+ } else {
+ showActionSheet(.ownLinkAskCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!"))
+ }
+ case let .connecting(contact_):
+ logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")")
+ if let contact = contact_ {
+ openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
+ } else {
+ showAlert(.invitationLinkConnecting(connectionLink: connectionLink))
+ }
+ case let .known(contact):
+ logger.debug("planAndConnect, .invitationLink, .known, incognito=\(incognito?.description ?? "nil")")
+ openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
+ }
+ case let .contactAddress(cap):
+ switch cap {
+ case .ok:
+ logger.debug("planAndConnect, .contactAddress, .ok, incognito=\(incognito?.description ?? "nil")")
+ if let incognito = incognito {
+ connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito)
+ } else {
+ showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address"))
+ }
+ case .ownLink:
+ logger.debug("planAndConnect, .contactAddress, .ownLink, incognito=\(incognito?.description ?? "nil")")
+ if let incognito = incognito {
+ showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
+ } else {
+ showActionSheet(.ownLinkAskCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!"))
+ }
+ case let .connecting(contact):
+ logger.debug("planAndConnect, .contactAddress, .connecting, incognito=\(incognito?.description ?? "nil")")
+ openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
+ case let .known(contact):
+ logger.debug("planAndConnect, .contactAddress, .known, incognito=\(incognito?.description ?? "nil")")
+ openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
+ }
+ case let .groupLink(glp):
+ switch glp {
+ case .ok:
+ if let incognito = incognito {
+ showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
+ } else {
+ showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group"))
+ }
+ case let .ownLink(groupInfo):
+ logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
+ showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo))
+ case let .connecting(groupInfo_):
+ logger.debug("planAndConnect, .groupLink, .connecting, incognito=\(incognito?.description ?? "nil")")
+ showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_))
+ case let .known(groupInfo):
+ logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")")
+ openKnownGroup(groupInfo, dismiss: dismiss) { AlertManager.shared.showAlert(groupAlreadyExistsAlert(groupInfo)) }
+ }
+ }
+ } catch {
+ logger.debug("planAndConnect, plan error")
+ if let incognito = incognito {
+ connectViaLink(connectionLink, connectionPlan: nil, dismiss: dismiss, incognito: incognito)
+ } else {
+ showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: nil, title: "Connect via link"))
}
}
}
}
-struct CReqClientData: Decodable {
- var type: String
- var groupLinkId: String?
-}
-
-func parseLinkQueryData(_ connectionLink: String) -> CReqClientData? {
- if let hashIndex = connectionLink.firstIndex(of: "#"),
- let urlQuery = URL(string: String(connectionLink[connectionLink.index(after: hashIndex)...])),
- let components = URLComponents(url: urlQuery, resolvingAgainstBaseURL: false),
- let data = components.queryItems?.first(where: { $0.name == "data" })?.value,
- let d = data.data(using: .utf8),
- let crData = try? getJSONDecoder().decode(CReqClientData.self, from: d) {
- return crData
- } else {
- return nil
+private func connectViaLink(_ connectionLink: String, connectionPlan: ConnectionPlan?, dismiss: Bool, incognito: Bool) {
+ Task {
+ if let connReqType = await apiConnect(incognito: incognito, connReq: connectionLink) {
+ let crt: ConnReqType
+ if let plan = connectionPlan {
+ crt = planToConnReqType(plan)
+ } else {
+ crt = connReqType
+ }
+ DispatchQueue.main.async {
+ if dismiss {
+ dismissAllSheets(animated: true) {
+ AlertManager.shared.showAlert(connReqSentAlert(crt))
+ }
+ } else {
+ AlertManager.shared.showAlert(connReqSentAlert(crt))
+ }
+ }
+ } else {
+ if dismiss {
+ DispatchQueue.main.async {
+ dismissAllSheets(animated: true)
+ }
+ }
+ }
}
}
-func checkCRDataGroup(_ crData: CReqClientData) -> Bool {
- return crData.type == "group" && crData.groupLinkId != nil
+func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
+ Task {
+ let m = ChatModel.shared
+ if let c = m.getContactChat(contact.contactId) {
+ DispatchQueue.main.async {
+ if dismiss {
+ dismissAllSheets(animated: true) {
+ m.chatId = c.id
+ showAlreadyExistsAlert?()
+ }
+ } else {
+ m.chatId = c.id
+ showAlreadyExistsAlert?()
+ }
+ }
+ }
+ }
}
-func groupLinkAlert(_ connectionLink: String, incognito: Bool) -> Alert {
- return Alert(
- title: Text("Connect via group link?"),
- message: Text("You will join a group this link refers to and connect to its group members."),
- primaryButton: .default(Text(incognito ? "Connect incognito" : "Connect")) {
- connectViaLink(connectionLink, incognito: incognito)
- },
- secondaryButton: .cancel()
+func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
+ Task {
+ let m = ChatModel.shared
+ if let g = m.getGroupChat(groupInfo.groupId) {
+ DispatchQueue.main.async {
+ if dismiss {
+ dismissAllSheets(animated: true) {
+ m.chatId = g.id
+ showAlreadyExistsAlert?()
+ }
+ } else {
+ m.chatId = g.id
+ showAlreadyExistsAlert?()
+ }
+ }
+ }
+ }
+}
+
+func contactAlreadyConnectingAlert(_ contact: Contact) -> Alert {
+ mkAlert(
+ title: "Contact already exists",
+ message: "You are already connecting to \(contact.displayName)."
)
}
+func groupAlreadyExistsAlert(_ groupInfo: GroupInfo) -> Alert {
+ mkAlert(
+ title: "Group already exists",
+ message: "You are already in group \(groupInfo.displayName)."
+ )
+}
+
+enum ConnReqType: Equatable {
+ case invitation
+ case contact
+ case groupLink
+
+ var connReqSentText: LocalizedStringKey {
+ switch self {
+ case .invitation: return "You will be connected when your contact's device is online, please wait or check later!"
+ case .contact: return "You will be connected when your connection request is accepted, please wait or check later!"
+ case .groupLink: return "You will be connected when group link host's device is online, please wait or check later!"
+ }
+ }
+}
+
+private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType {
+ switch connectionPlan {
+ case .invitationLink: return .invitation
+ case .contactAddress: return .contact
+ case .groupLink: return .groupLink
+ }
+}
+
func connReqSentAlert(_ type: ConnReqType) -> Alert {
return mkAlert(
title: "Connection request sent!",
- message: type == .contact
- ? "You will be connected when your connection request is accepted, please wait or check later!"
- : "You will be connected when your contact's device is online, please wait or check later!"
+ message: type.connReqSentText
)
}
diff --git a/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift b/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift
index ec199327ce..af84f19fec 100644
--- a/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift
+++ b/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift
@@ -14,6 +14,8 @@ struct PasteToConnectView: View {
@State private var connectionLink: String = ""
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
@FocusState private var linkEditorFocused: Bool
+ @State private var alert: PlanAndConnectAlert?
+ @State private var sheet: PlanAndConnectActionSheet?
var body: some View {
List {
@@ -57,6 +59,8 @@ struct PasteToConnectView: View {
+ Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.")
}
}
+ .alert(item: $alert) { a in planAndConnectAlert(a, dismiss: true) }
+ .actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) }
}
private func linkEditor() -> some View {
@@ -83,13 +87,13 @@ struct PasteToConnectView: View {
private func connect() {
let link = connectionLink.trimmingCharacters(in: .whitespaces)
- if let crData = parseLinkQueryData(link),
- checkCRDataGroup(crData) {
- dismiss()
- AlertManager.shared.showAlert(groupLinkAlert(link, incognito: incognitoDefault))
- } else {
- connectViaLink(link, dismiss: dismiss, incognito: incognitoDefault)
- }
+ planAndConnect(
+ link,
+ showAlert: { alert = $0 },
+ showActionSheet: { sheet = $0 },
+ dismiss: true,
+ incognito: incognitoDefault
+ )
}
}
diff --git a/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift b/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift
index 01c8a4659e..c55ba1502e 100644
--- a/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift
+++ b/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift
@@ -13,6 +13,8 @@ import CodeScanner
struct ScanToConnectView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
+ @State private var alert: PlanAndConnectAlert?
+ @State private var sheet: PlanAndConnectActionSheet?
var body: some View {
ScrollView {
@@ -49,18 +51,20 @@ struct ScanToConnectView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
.background(Color(.systemGroupedBackground))
+ .alert(item: $alert) { a in planAndConnectAlert(a, dismiss: true) }
+ .actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) }
}
func processQRCode(_ resp: Result) {
switch resp {
case let .success(r):
- if let crData = parseLinkQueryData(r.string),
- checkCRDataGroup(crData) {
- dismiss()
- AlertManager.shared.showAlert(groupLinkAlert(r.string, incognito: incognitoDefault))
- } else {
- Task { connectViaLink(r.string, dismiss: dismiss, incognito: incognitoDefault) }
- }
+ planAndConnect(
+ r.string,
+ showAlert: { alert = $0 },
+ showActionSheet: { sheet = $0 },
+ dismiss: true,
+ incognito: incognitoDefault
+ )
case let .failure(e):
logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)")
dismiss()
diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj
index 35d0b1de8a..2a90e75d85 100644
--- a/apps/ios/SimpleX.xcodeproj/project.pbxproj
+++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj
@@ -176,6 +176,11 @@
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
+ 64AB9C832AD6B6B900B21C4C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64AB9C7E2AD6B6B900B21C4C /* libgmp.a */; };
+ 64AB9C842AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64AB9C7F2AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a */; };
+ 64AB9C852AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64AB9C802AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a */; };
+ 64AB9C862AD6B6B900B21C4C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64AB9C812AD6B6B900B21C4C /* libffi.a */; };
+ 64AB9C872AD6B6B900B21C4C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64AB9C822AD6B6B900B21C4C /* libgmpxx.a */; };
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
@@ -458,6 +463,11 @@
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = ""; };
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = ""; };
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = ""; };
+ 64AB9C7E2AD6B6B900B21C4C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; };
+ 64AB9C7F2AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a"; sourceTree = ""; };
+ 64AB9C802AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a"; sourceTree = ""; };
+ 64AB9C812AD6B6B900B21C4C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; };
+ 64AB9C822AD6B6B900B21C4C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; };
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = ""; };
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; };
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; };
@@ -507,13 +517,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 5CA8D0162AD746C8001FD661 /* libgmpxx.a in Frameworks */,
- 5CA8D01A2AD746C8001FD661 /* libgmp.a in Frameworks */,
- 5CA8D0182AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a in Frameworks */,
- 5CA8D0192AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a in Frameworks */,
+ 64AB9C842AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a in Frameworks */,
+ 64AB9C862AD6B6B900B21C4C /* libffi.a in Frameworks */,
+ 64AB9C852AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
+ 64AB9C832AD6B6B900B21C4C /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
- 5CA8D0172AD746C8001FD661 /* libffi.a in Frameworks */,
+ 64AB9C872AD6B6B900B21C4C /* libgmpxx.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -574,11 +584,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
- 5CA8D0122AD746C8001FD661 /* libffi.a */,
- 5CA8D0152AD746C8001FD661 /* libgmp.a */,
- 5CA8D0112AD746C8001FD661 /* libgmpxx.a */,
- 5CA8D0142AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a */,
- 5CA8D0132AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a */,
+ 64AB9C812AD6B6B900B21C4C /* libffi.a */,
+ 64AB9C7E2AD6B6B900B21C4C /* libgmp.a */,
+ 64AB9C822AD6B6B900B21C4C /* libgmpxx.a */,
+ 64AB9C7F2AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a */,
+ 64AB9C802AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a */,
);
path = Libraries;
sourceTree = "";
diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift
index d29e2481d5..9eb9b9084c 100644
--- a/apps/ios/SimpleXChat/APITypes.swift
+++ b/apps/ios/SimpleXChat/APITypes.swift
@@ -86,6 +86,7 @@ public enum ChatCommand {
case apiVerifyGroupMember(groupId: Int64, groupMemberId: Int64, connectionCode: String?)
case apiAddContact(userId: Int64, incognito: Bool)
case apiSetConnectionIncognito(connId: Int64, incognito: Bool)
+ case apiConnectPlan(userId: Int64, connReq: String)
case apiConnect(userId: Int64, incognito: Bool, connReq: String)
case apiDeleteChat(type: ChatType, id: Int64)
case apiClearChat(type: ChatType, id: Int64)
@@ -219,6 +220,7 @@ public enum ChatCommand {
case let .apiVerifyGroupMember(groupId, groupMemberId, .none): return "/_verify code #\(groupId) \(groupMemberId)"
case let .apiAddContact(userId, incognito): return "/_connect \(userId) incognito=\(onOff(incognito))"
case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))"
+ case let .apiConnectPlan(userId, connReq): return "/_connect plan \(userId) \(connReq)"
case let .apiConnect(userId, incognito, connReq): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connReq)"
case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
@@ -335,6 +337,7 @@ public enum ChatCommand {
case .apiVerifyGroupMember: return "apiVerifyGroupMember"
case .apiAddContact: return "apiAddContact"
case .apiSetConnectionIncognito: return "apiSetConnectionIncognito"
+ case .apiConnectPlan: return "apiConnectPlan"
case .apiConnect: return "apiConnect"
case .apiDeleteChat: return "apiDeleteChat"
case .apiClearChat: return "apiClearChat"
@@ -460,6 +463,7 @@ public enum ChatResponse: Decodable, Error {
case connectionVerified(user: UserRef, verified: Bool, expectedCode: String)
case invitation(user: UserRef, connReqInvitation: String, connection: PendingContactConnection)
case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection)
+ case connectionPlan(user: UserRef, connectionPlan: ConnectionPlan)
case sentConfirmation(user: UserRef)
case sentInvitation(user: UserRef)
case contactAlreadyExists(user: UserRef, contact: Contact)
@@ -601,6 +605,7 @@ public enum ChatResponse: Decodable, Error {
case .connectionVerified: return "connectionVerified"
case .invitation: return "invitation"
case .connectionIncognitoUpdated: return "connectionIncognitoUpdated"
+ case .connectionPlan: return "connectionPlan"
case .sentConfirmation: return "sentConfirmation"
case .sentInvitation: return "sentInvitation"
case .contactAlreadyExists: return "contactAlreadyExists"
@@ -739,6 +744,7 @@ public enum ChatResponse: Decodable, Error {
case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)")
case let .invitation(u, connReqInvitation, _): return withUser(u, connReqInvitation)
case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection))
+ case let .connectionPlan(u, connectionPlan): return withUser(u, String(describing: connectionPlan))
case .sentConfirmation: return noDetails
case .sentInvitation: return noDetails
case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact))
@@ -859,6 +865,33 @@ public func chatError(_ chatResponse: ChatResponse) -> ChatErrorType? {
}
}
+public enum ConnectionPlan: Decodable {
+ case invitationLink(invitationLinkPlan: InvitationLinkPlan)
+ case contactAddress(contactAddressPlan: ContactAddressPlan)
+ case groupLink(groupLinkPlan: GroupLinkPlan)
+}
+
+public enum InvitationLinkPlan: Decodable {
+ case ok
+ case ownLink
+ case connecting(contact_: Contact?)
+ case known(contact: Contact)
+}
+
+public enum ContactAddressPlan: Decodable {
+ case ok
+ case ownLink
+ case connecting(contact: Contact)
+ case known(contact: Contact)
+}
+
+public enum GroupLinkPlan: Decodable {
+ case ok
+ case ownLink(groupInfo: GroupInfo)
+ case connecting(groupInfo_: GroupInfo?)
+ case known(groupInfo: GroupInfo)
+}
+
struct NewUser: Encodable {
var profile: Profile?
var sameServers: Bool
@@ -1477,6 +1510,7 @@ public enum ChatErrorType: Decodable {
case chatNotStarted
case chatNotStopped
case chatStoreChanged
+ case connectionPlan(connectionPlan: ConnectionPlan)
case invalidConnReq
case invalidChatMessage(connection: Connection, message: String)
case contactNotReady(contact: Contact)
From a35dc263b78fa6a30dfe2b2702f82ff03469e9d2 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Fri, 13 Oct 2023 22:28:55 +0100
Subject: [PATCH 30/80] website: remove address from connect page
---
website/src/_includes/contact_page.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/website/src/_includes/contact_page.html b/website/src/_includes/contact_page.html
index a03a3aea4f..6beb148f8d 100644
--- a/website/src/_includes/contact_page.html
+++ b/website/src/_includes/contact_page.html
@@ -152,7 +152,7 @@
v1.0.0+, {{ "copy-the-command-below-text" | i18n({}, lang ) | safe }}
- /c https://simplex.chat/contact#/?v=1&smp=smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im/KBCmxJ3-lEjpWLPPkI6OWPk-YJneU5uY%23MCowBQYDK2VuAyEAtixHJWDXvYWcoe-77vIfjvI6XWEuzUsapMS9nVHP_Go=
+
From 838751fe782fdd72fe4d88267b6b75b903aff27a Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Sun, 15 Oct 2023 18:12:13 +0100
Subject: [PATCH 31/80] ios: fix Protect screen not hiding message previews
(#3226)
---
apps/ios/Shared/Views/ChatList/ChatPreviewView.swift | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift
index 3ac8fada74..1e153baf44 100644
--- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift
@@ -105,14 +105,17 @@ struct ChatPreviewView: View {
private func chatPreviewLayout(_ text: Text, draft: Bool = false) -> some View {
ZStack(alignment: .topTrailing) {
- text
+ let t = text
.lineLimit(2)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .topLeading)
.padding(.leading, 8)
.padding(.trailing, 36)
- .privacySensitive(!showChatPreviews && !draft)
- .redacted(reason: .privacy)
+ if !showChatPreviews && !draft {
+ t.privacySensitive(true).redacted(reason: .privacy)
+ } else {
+ t
+ }
let s = chat.chatStats
if s.unreadCount > 0 || s.unreadChat {
unreadCountText(s.unreadCount)
From c2a320640b224dad03e05f72a1ecb836cb6fefbc Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Sun, 15 Oct 2023 18:16:12 +0100
Subject: [PATCH 32/80] core: local encryption for auto-received inline files
(e.g. small voice messages) (#3224)
* core: local encryption for auto-received inline files
* update view, test
---
apps/ios/Shared/Model/SimpleXAPI.swift | 7 +++
.../ios/SimpleX NSE/NotificationService.swift | 7 +++
apps/ios/SimpleXChat/APITypes.swift | 23 +++++----
.../chat/simplex/common/model/SimpleXAPI.kt | 20 +++++++-
src/Simplex/Chat.hs | 44 ++++++++++-------
src/Simplex/Chat/Controller.hs | 6 ++-
src/Simplex/Chat/View.hs | 49 ++++++++++---------
tests/ChatTests/Files.hs | 33 ++++++++++++-
8 files changed, 133 insertions(+), 56 deletions(-)
diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index a1c8cee774..91c5b1b36d 100644
--- a/apps/ios/Shared/Model/SimpleXAPI.swift
+++ b/apps/ios/Shared/Model/SimpleXAPI.swift
@@ -257,6 +257,12 @@ func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
throw r
}
+func apiSetEncryptLocalFiles(_ enable: Boolean) throws {
+ let r = chatSendCmdSync(.apiSetEncryptLocalFiles(enable: enable))
+ if case .cmdOk = r { return }
+ throw r
+}
+
func apiExportArchive(config: ArchiveConfig) async throws {
try await sendCommandOkResp(.apiExportArchive(config: config))
}
@@ -1152,6 +1158,7 @@ func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
try setXFTPConfig(getXFTPCfg())
+ // try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
m.chatInitialized = true
m.currentUser = try apiGetActiveUser()
if m.currentUser == nil {
diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift
index 645fdb5952..2fb069a86c 100644
--- a/apps/ios/SimpleX NSE/NotificationService.swift
+++ b/apps/ios/SimpleX NSE/NotificationService.swift
@@ -216,6 +216,7 @@ func startChat() -> DBMigrationResult? {
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
try setXFTPConfig(xftpConfig)
+ // try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
let justStarted = try apiStartChat()
chatStarted = true
if justStarted {
@@ -351,6 +352,12 @@ func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
throw r
}
+func apiSetEncryptLocalFiles(_ enable: Boolean) throws {
+ let r = chatSendCmdSync(.apiSetEncryptLocalFiles(enable: enable))
+ if case .cmdOk = r { return }
+ throw r
+}
+
func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
guard apiGetActiveUser() != nil else {
logger.debug("no active user")
diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift
index 9eb9b9084c..ddab4c73bd 100644
--- a/apps/ios/SimpleXChat/APITypes.swift
+++ b/apps/ios/SimpleXChat/APITypes.swift
@@ -32,6 +32,7 @@ public enum ChatCommand {
case setTempFolder(tempFolder: String)
case setFilesFolder(filesFolder: String)
case apiSetXFTPConfig(config: XFTPFileConfig?)
+ case apiSetEncryptLocalFiles(enable: Bool)
case apiExportArchive(config: ArchiveConfig)
case apiImportArchive(config: ArchiveConfig)
case apiDeleteStorage
@@ -114,8 +115,8 @@ public enum ChatCommand {
case apiGetNetworkStatuses
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
- case receiveFile(fileId: Int64, encrypted: Bool, inline: Bool?)
- case setFileToReceive(fileId: Int64, encrypted: Bool)
+ case receiveFile(fileId: Int64, encrypted: Bool?, inline: Bool?)
+ case setFileToReceive(fileId: Int64, encrypted: Bool?)
case cancelFile(fileId: Int64)
case showVersion
case string(String)
@@ -152,6 +153,7 @@ public enum ChatCommand {
} else {
return "/_xftp off"
}
+ case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))"
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
case .apiDeleteStorage: return "/_db delete"
@@ -247,13 +249,8 @@ public enum ChatCommand {
case .apiGetNetworkStatuses: return "/_network_statuses"
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
- case let .receiveFile(fileId, encrypted, inline):
- let s = "/freceive \(fileId) encrypt=\(onOff(encrypted))"
- if let inline = inline {
- return s + " inline=\(onOff(inline))"
- }
- return s
- case let .setFileToReceive(fileId, encrypted): return "/_set_file_to_receive \(fileId) encrypt=\(onOff(encrypted))"
+ case let .receiveFile(fileId, encrypt, inline): return "/freceive \(fileId)\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))"
+ case let .setFileToReceive(fileId, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("encrypt", encrypt))"
case let .cancelFile(fileId): return "/fcancel \(fileId)"
case .showVersion: return "/version"
case let .string(str): return str
@@ -283,6 +280,7 @@ public enum ChatCommand {
case .setTempFolder: return "setTempFolder"
case .setFilesFolder: return "setFilesFolder"
case .apiSetXFTPConfig: return "apiSetXFTPConfig"
+ case .apiSetEncryptLocalFiles: return "apiSetEncryptLocalFiles"
case .apiExportArchive: return "apiExportArchive"
case .apiImportArchive: return "apiImportArchive"
case .apiDeleteStorage: return "apiDeleteStorage"
@@ -420,6 +418,13 @@ public enum ChatCommand {
b ? "on" : "off"
}
+ private func onOffParam(_ param: String, _ b: Bool?) -> String {
+ if let b = b {
+ return " \(param)=\(onOff(b))"
+ }
+ return ""
+ }
+
private func maybePwd(_ pwd: String?) -> String {
pwd == "" || pwd == nil ? "" : " " + encodeJSON(pwd)
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
index e19520093c..f49984b942 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
@@ -338,6 +338,7 @@ object ChatController {
apiSetTempFolder(coreTmpDir.absolutePath)
apiSetFilesFolder(appFilesDir.absolutePath)
apiSetXFTPConfig(getXFTPCfg())
+// apiSetEncryptLocalFiles(appPrefs.privacyEncryptLocalFiles.get())
val justStarted = apiStartChat()
val users = listUsers()
chatModel.users.clear()
@@ -559,6 +560,8 @@ object ChatController {
throw Error("apiSetXFTPConfig bad response: ${r.responseType} ${r.details}")
}
+ suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(CC.ApiSetEncryptLocalFiles(enable))
+
suspend fun apiExportArchive(config: ArchiveConfig) {
val r = sendCmd(CC.ApiExportArchive(config))
if (r is CR.CmdOk) return
@@ -1327,6 +1330,13 @@ object ChatController {
}
}
+ private suspend fun sendCommandOkResp(cmd: CC): Boolean {
+ val r = sendCmd(cmd)
+ val ok = r is CR.CmdOk
+ if (!ok) apiErrorAlert(cmd.cmdType, generalGetString(MR.strings.error), r)
+ return ok
+ }
+
suspend fun apiGetVersion(): CoreVersionInfo? {
val r = sendCmd(CC.ShowVersion())
return if (r is CR.VersionInfo) {
@@ -1858,6 +1868,7 @@ sealed class CC {
class SetTempFolder(val tempFolder: String): CC()
class SetFilesFolder(val filesFolder: String): CC()
class ApiSetXFTPConfig(val config: XFTPFileConfig?): CC()
+ class ApiSetEncryptLocalFiles(val enable: Boolean): CC()
class ApiExportArchive(val config: ArchiveConfig): CC()
class ApiImportArchive(val config: ArchiveConfig): CC()
class ApiDeleteStorage: CC()
@@ -1931,7 +1942,7 @@ sealed class CC {
class ApiRejectContact(val contactReqId: Long): CC()
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
- class ReceiveFile(val fileId: Long, val encrypted: Boolean, val inline: Boolean?): CC()
+ class ReceiveFile(val fileId: Long, val encrypt: Boolean?, val inline: Boolean?): CC()
class CancelFile(val fileId: Long): CC()
class ShowVersion(): CC()
@@ -1963,6 +1974,7 @@ sealed class CC {
is SetTempFolder -> "/_temp_folder $tempFolder"
is SetFilesFolder -> "/_files_folder $filesFolder"
is ApiSetXFTPConfig -> if (config != null) "/_xftp on ${json.encodeToString(config)}" else "/_xftp off"
+ is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}"
is ApiExportArchive -> "/_db export ${json.encodeToString(config)}"
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
is ApiDeleteStorage -> "/_db delete"
@@ -2039,7 +2051,10 @@ sealed class CC {
is ApiGetNetworkStatuses -> "/_network_statuses"
is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}"
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
- is ReceiveFile -> "/freceive $fileId encrypt=${onOff(encrypted)}" + (if (inline == null) "" else " inline=${onOff(inline)}")
+ is ReceiveFile ->
+ "/freceive $fileId" +
+ (if (encrypt == null) "" else " encrypt=${onOff(encrypt)}") +
+ (if (inline == null) "" else " inline=${onOff(inline)}")
is CancelFile -> "/fcancel $fileId"
is ShowVersion -> "/version"
}
@@ -2063,6 +2078,7 @@ sealed class CC {
is SetTempFolder -> "setTempFolder"
is SetFilesFolder -> "setFilesFolder"
is ApiSetXFTPConfig -> "apiSetXFTPConfig"
+ is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles"
is ApiExportArchive -> "apiExportArchive"
is ApiImportArchive -> "apiImportArchive"
is ApiDeleteStorage -> "apiDeleteStorage"
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 56077c3bf6..90a6617059 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -209,6 +209,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
cleanupManagerAsync <- newTVarIO Nothing
timedItemThreads <- atomically TM.empty
showLiveItems <- newTVarIO False
+ encryptLocalFiles <- newTVarIO False
userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg
tempDirectory <- newTVarIO tempDir
contactMergeEnabled <- newTVarIO True
@@ -236,6 +237,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
cleanupManagerAsync,
timedItemThreads,
showLiveItems,
+ encryptLocalFiles,
userXFTPFileConfig,
tempDirectory,
logFilePath = logFile,
@@ -515,6 +517,7 @@ processChatCommand = \case
APISetXFTPConfig cfg -> do
asks userXFTPFileConfig >>= atomically . (`writeTVar` cfg)
ok_
+ APISetEncryptLocalFiles on -> chatWriteVar encryptLocalFiles on >> ok_
SetContactMergeEnabled onOff -> do
asks contactMergeEnabled >>= atomically . (`writeTVar` onOff)
ok_
@@ -1773,19 +1776,16 @@ processChatCommand = \case
ForwardFile chatName fileId -> forwardFile chatName fileId SendFile
ForwardImage chatName fileId -> forwardFile chatName fileId SendImage
SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO"
- ReceiveFile fileId encrypted rcvInline_ filePath_ -> withUser $ \_ ->
+ ReceiveFile fileId encrypted_ rcvInline_ filePath_ -> withUser $ \_ ->
withChatLock "receiveFile" . procCmd $ do
(user, ft) <- withStore (`getRcvFileTransferById` fileId)
- ft' <- if encrypted then encryptLocalFile ft else pure ft
+ encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles
+ ft' <- (if encrypt then setFileToEncrypt else pure) ft
receiveFile' user ft' rcvInline_ filePath_
- where
- encryptLocalFile ft = do
- cfArgs <- liftIO $ CF.randomArgs
- withStore' $ \db -> setFileCryptoArgs db fileId cfArgs
- pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs}
- SetFileToReceive fileId encrypted -> withUser $ \_ -> do
+ SetFileToReceive fileId encrypted_ -> withUser $ \_ -> do
withChatLock "setFileToReceive" . procCmd $ do
- cfArgs <- if encrypted then Just <$> liftIO CF.randomArgs else pure Nothing
+ encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles
+ cfArgs <- if encrypt then Just <$> liftIO CF.randomArgs else pure Nothing
withStore' $ \db -> setRcvFileToReceive db fileId cfArgs
ok_
CancelFile fileId -> withUser $ \user@User {userId} ->
@@ -2410,6 +2410,12 @@ toFSFilePath :: ChatMonad' m => FilePath -> m FilePath
toFSFilePath f =
maybe f (> f) <$> (readTVarIO =<< asks filesFolder)
+setFileToEncrypt :: ChatMonad m => RcvFileTransfer -> m RcvFileTransfer
+setFileToEncrypt ft@RcvFileTransfer {fileId} = do
+ cfArgs <- liftIO CF.randomArgs
+ withStore' $ \db -> setFileCryptoArgs db fileId cfArgs
+ pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs}
+
receiveFile' :: ChatMonad m => User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> m ChatResponse
receiveFile' user ft rcvInline_ filePath_ = do
(CRRcvFileAccepted user <$> acceptFileReceive user ft rcvInline_ filePath_) `catchChatError` processError
@@ -3931,14 +3937,17 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
inline <- receiveInlineMode fInv (Just mc) fileChunkSize
ft@RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFT db fInv inline fileChunkSize
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
- (filePath, fileStatus) <- case inline of
+ (filePath, fileStatus, ft') <- case inline of
Just IFMSent -> do
+ encrypt <- chatReadVar encryptLocalFiles
+ ft' <- (if encrypt then setFileToEncrypt else pure) ft
fPath <- getRcvFilePath fileId Nothing fileName True
- withStore' $ \db -> startRcvInlineFT db user ft fPath inline
- pure (Just fPath, CIFSRcvAccepted)
- _ -> pure (Nothing, CIFSRcvInvitation)
- let fileSource = CF.plain <$> filePath
- pure (ft, CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol})
+ withStore' $ \db -> startRcvInlineFT db user ft' fPath inline
+ pure (Just fPath, CIFSRcvAccepted, ft')
+ _ -> pure (Nothing, CIFSRcvInvitation, ft)
+ let RcvFileTransfer {cryptoArgs} = ft'
+ fileSource = (`CryptoFile` cryptoArgs) <$> filePath
+ pure (ft', CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol})
messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m ()
messageUpdate ct@Contact {contactId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do
@@ -5567,6 +5576,7 @@ chatCommandP =
("/_files_folder " <|> "/files_folder ") *> (SetFilesFolder <$> filePath),
"/_xftp " *> (APISetXFTPConfig <$> ("on " *> (Just <$> jsonP) <|> ("off" $> Nothing))),
"/xftp " *> (APISetXFTPConfig <$> ("on" *> (Just <$> xftpCfgP) <|> ("off" $> Nothing))),
+ "/_files_encrypt " *> (APISetEncryptLocalFiles <$> onOffP),
"/contact_merge " *> (SetContactMergeEnabled <$> onOffP),
"/_db export " *> (APIExportArchive <$> jsonP),
"/db export" $> ExportArchive,
@@ -5743,8 +5753,8 @@ chatCommandP =
("/fforward " <|> "/ff ") *> (ForwardFile <$> chatNameP' <* A.space <*> A.decimal),
("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal),
("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath),
- ("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> (" encrypt=" *> onOffP <|> pure False) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)),
- "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> (" encrypt=" *> onOffP <|> pure False)),
+ ("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> optional (" encrypt=" *> onOffP) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)),
+ "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> optional (" encrypt=" *> onOffP)),
("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal),
("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal),
"/simplex" *> (ConnectSimplex <$> incognitoP),
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index 2704c2721f..0a9b886940 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -179,6 +179,7 @@ data ChatController = ChatController
cleanupManagerAsync :: TVar (Maybe (Async ())),
timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))),
showLiveItems :: TVar Bool,
+ encryptLocalFiles :: TVar Bool,
userXFTPFileConfig :: TVar (Maybe XFTPFileConfig),
tempDirectory :: TVar (Maybe FilePath),
logFilePath :: Maybe FilePath,
@@ -221,6 +222,7 @@ data ChatCommand
| SetTempFolder FilePath
| SetFilesFolder FilePath
| APISetXFTPConfig (Maybe XFTPFileConfig)
+ | APISetEncryptLocalFiles Bool
| SetContactMergeEnabled Bool
| APIExportArchive ArchiveConfig
| ExportArchive
@@ -393,8 +395,8 @@ data ChatCommand
| ForwardFile ChatName FileTransferId
| ForwardImage ChatName FileTransferId
| SendFileDescription ChatName FilePath
- | ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath}
- | SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Bool}
+ | ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Maybe Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath}
+ | SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Maybe Bool}
| CancelFile FileTransferId
| FileStatus FileTransferId
| ShowProfile -- UserId (not used in UI)
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index bcbc45f4eb..08b1579bde 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -166,7 +166,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
CRRcvFileDescrReady _ _ -> []
CRRcvFileDescrNotReady _ _ -> []
CRRcvFileProgressXFTP {} -> []
- CRRcvFileAccepted u ci -> ttyUser u $ savingFile' testView ci
+ CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci
CRRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft
CRSndFileCancelled u _ ftm fts -> ttyUser u $ viewSndFileCancelled ftm fts
CRRcvFileCancelled u _ ft -> ttyUser u $ receivingFile_ "cancelled" ft
@@ -178,10 +178,10 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
CRContactUpdated {user = u, fromContact = c, toContact = c'} -> ttyUser u $ viewContactUpdated c c' <> viewContactPrefsUpdated u c c'
CRContactsMerged u intoCt mergedCt ct' -> ttyUser u $ viewContactsMerged intoCt mergedCt ct'
CRReceivedContactRequest u UserContactRequest {localDisplayName = c, profile} -> ttyUser u $ viewReceivedContactRequest c profile
- CRRcvFileStart u ci -> ttyUser u $ receivingFile_' "started" ci
- CRRcvFileComplete u ci -> ttyUser u $ receivingFile_' "completed" ci
+ CRRcvFileStart u ci -> ttyUser u $ receivingFile_' testView "started" ci
+ CRRcvFileComplete u ci -> ttyUser u $ receivingFile_' testView "completed" ci
CRRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft
- CRRcvFileError u ci e -> ttyUser u $ receivingFile_' "error" ci <> [sShow e]
+ CRRcvFileError u ci e -> ttyUser u $ receivingFile_' testView "error" ci <> [sShow e]
CRSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft
CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft
CRSndFileStartXFTP {} -> []
@@ -1449,27 +1449,28 @@ humanReadableSize size
mB = kB * 1024
gB = mB * 1024
-savingFile' :: Bool -> AChatItem -> [StyledString]
-savingFile' testView (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileSource = Just (CryptoFile filePath cfArgs_)}, chatDir}) =
- let from = case (chat, chatDir) of
- (DirectChat Contact {localDisplayName = c}, CIDirectRcv) -> " from " <> ttyContact c
- (_, CIGroupRcv GroupMember {localDisplayName = m}) -> " from " <> ttyContact m
- _ -> ""
- in ["saving file " <> sShow fileId <> from <> " to " <> plain filePath] <> cfArgsStr
- where
- cfArgsStr = case cfArgs_ of
- Just cfArgs@(CFArgs key nonce)
- | testView -> [plain $ LB.unpack $ J.encode cfArgs]
- | otherwise -> [plain $ "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce]
- _ -> []
-savingFile' _ _ = ["saving file"] -- shouldn't happen
+savingFile' :: AChatItem -> [StyledString]
+savingFile' (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileSource = Just (CryptoFile filePath _)}, chatDir}) =
+ ["saving file " <> sShow fileId <> fileFrom chat chatDir <> " to " <> plain filePath]
+savingFile' _ = ["saving file"] -- shouldn't happen
-receivingFile_' :: StyledString -> AChatItem -> [StyledString]
-receivingFile_' status (AChatItem _ _ (DirectChat c) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectRcv}) =
- [status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyContact' c]
-receivingFile_' status (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupRcv m}) =
- [status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyMember m]
-receivingFile_' status _ = [status <> " receiving file"] -- shouldn't happen
+receivingFile_' :: Bool -> String -> AChatItem -> [StyledString]
+receivingFile_' testView status (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileName, fileSource = Just (CryptoFile _ cfArgs_)}, chatDir}) =
+ [plain status <> " receiving " <> fileTransferStr fileId fileName <> fileFrom chat chatDir] <> cfArgsStr cfArgs_
+ where
+ cfArgsStr (Just cfArgs@(CFArgs key nonce)) = [plain s | status == "completed"]
+ where
+ s =
+ if testView
+ then LB.toStrict $ J.encode cfArgs
+ else "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce
+ cfArgsStr _ = []
+receivingFile_' _ status _ = [plain status <> " receiving file"] -- shouldn't happen
+
+fileFrom :: ChatInfo c -> CIDirection c d -> StyledString
+fileFrom (DirectChat ct) CIDirectRcv = " from " <> ttyContact' ct
+fileFrom _ (CIGroupRcv m) = " from " <> ttyMember m
+fileFrom _ _ = ""
receivingFile_ :: StyledString -> RcvFileTransfer -> [StyledString]
receivingFile_ status ft@RcvFileTransfer {senderDisplayName = c} =
diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs
index 50f86d8e09..5231283903 100644
--- a/tests/ChatTests/Files.hs
+++ b/tests/ChatTests/Files.hs
@@ -33,6 +33,7 @@ chatFileTests = do
describe "send and receive file" $ fileTestMatrix2 runTestFileTransfer
describe "send file, receive and locally encrypt file" $ fileTestMatrix2 runTestFileTransferEncrypted
it "send and receive file inline (without accepting)" testInlineFileTransfer
+ it "send inline file, receive (without accepting) and locally encrypt" testInlineFileTransferEncrypted
xit'' "accept inline file transfer, sender cancels during transfer" testAcceptInlineFileSndCancelDuringTransfer
it "send and receive small file inline (default config)" testSmallInlineFileTransfer
it "small file sent without acceptance is ignored in terminal by default" testSmallInlineFileIgnored
@@ -107,7 +108,6 @@ runTestFileTransferEncrypted alice bob = do
bob <## "use /fr 1 [/ | ] to receive it"
bob ##> "/fr 1 encrypt=on ./tests/tmp"
bob <## "saving file 1 from alice to ./tests/tmp/test.pdf"
- Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob
concurrently_
(bob <## "started receiving file 1 (test.pdf) from alice")
(alice <## "started sending file 1 (test.pdf) to bob")
@@ -123,6 +123,7 @@ runTestFileTransferEncrypted alice bob = do
"completed sending file 1 (test.pdf) to bob"
]
]
+ Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob
src <- B.readFile "./tests/fixtures/test.pdf"
-- dest <- B.readFile "./tests/tmp/test.pdf"
-- dest `shouldBe` src
@@ -154,6 +155,34 @@ testInlineFileTransfer =
where
cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, receiveChunks = 100}}
+testInlineFileTransferEncrypted :: HasCallStack => FilePath -> IO ()
+testInlineFileTransferEncrypted =
+ testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
+ connectUsers alice bob
+ bob ##> "/_files_folder ./tests/tmp/"
+ bob <## "ok"
+ bob ##> "/_files_encrypt on"
+ bob <## "ok"
+ alice ##> "/_send @2 json {\"msgContent\":{\"type\":\"voice\", \"duration\":10, \"text\":\"\"}, \"filePath\":\"./tests/fixtures/test.jpg\"}"
+ alice <# "@bob voice message (00:10)"
+ alice <# "/f @bob ./tests/fixtures/test.jpg"
+ -- below is not shown in "sent" mode
+ -- alice <## "use /fc 1 to cancel sending"
+ bob <# "alice> voice message (00:10)"
+ bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
+ -- below is not shown in "sent" mode
+ -- bob <## "use /fr 1 [/ | ] to receive it"
+ bob <## "started receiving file 1 (test.jpg) from alice"
+ concurrently_
+ (alice <## "completed sending file 1 (test.jpg) to bob")
+ (bob <## "completed receiving file 1 (test.jpg) from alice")
+ Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob
+ src <- B.readFile "./tests/fixtures/test.jpg"
+ Right dest <- chatReadFile "./tests/tmp/test.jpg" (strEncode key) (strEncode nonce)
+ LB.toStrict dest `shouldBe` src
+ where
+ cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, receiveChunks = 100}}
+
testAcceptInlineFileSndCancelDuringTransfer :: HasCallStack => FilePath -> IO ()
testAcceptInlineFileSndCancelDuringTransfer =
testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
@@ -1077,10 +1106,10 @@ testXFTPFileTransferEncrypted =
bob <## "use /fr 1 [/ | ] to receive it"
bob ##> "/fr 1 encrypt=on ./tests/tmp/bob/"
bob <## "saving file 1 from alice to ./tests/tmp/bob/test.pdf"
- Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob
alice <## "completed uploading file 1 (test.pdf) for bob"
bob <## "started receiving file 1 (test.pdf) from alice"
bob <## "completed receiving file 1 (test.pdf) from alice"
+ Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob
Right dest <- chatReadFile "./tests/tmp/bob/test.pdf" (strEncode key) (strEncode nonce)
LB.length dest `shouldBe` fromIntegral srcLen
LB.toStrict dest `shouldBe` src
From 43b67ba157dab55d24d200a62aba42c580499d6e Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Sun, 15 Oct 2023 20:58:39 +0100
Subject: [PATCH 33/80] ui: set local file encryption in the core (#3227)
---
apps/ios/Shared/Model/SimpleXAPI.swift | 4 ++--
.../Shared/Views/UserSettings/PrivacySettings.swift | 13 +++++++++++++
apps/ios/SimpleX NSE/NotificationService.swift | 6 +++---
.../kotlin/chat/simplex/common/model/SimpleXAPI.kt | 6 +++---
.../common/views/usersettings/PrivacySettings.kt | 4 +++-
.../src/commonMain/resources/MR/base/strings.xml | 1 +
6 files changed, 25 insertions(+), 9 deletions(-)
diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index 91c5b1b36d..2e724bcd83 100644
--- a/apps/ios/Shared/Model/SimpleXAPI.swift
+++ b/apps/ios/Shared/Model/SimpleXAPI.swift
@@ -257,7 +257,7 @@ func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
throw r
}
-func apiSetEncryptLocalFiles(_ enable: Boolean) throws {
+func apiSetEncryptLocalFiles(_ enable: Bool) throws {
let r = chatSendCmdSync(.apiSetEncryptLocalFiles(enable: enable))
if case .cmdOk = r { return }
throw r
@@ -1158,7 +1158,7 @@ func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
try setXFTPConfig(getXFTPCfg())
- // try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
+ try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
m.chatInitialized = true
m.currentUser = try apiGetActiveUser()
if m.currentUser == nil {
diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
index 34b6f147bd..16555fad3a 100644
--- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
+++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
@@ -66,6 +66,9 @@ struct PrivacySettings: View {
Section {
settingsRow("lock.doc") {
Toggle("Encrypt local files", isOn: $encryptLocalFiles)
+ .onChange(of: encryptLocalFiles) {
+ setEncryptLocalFiles($0)
+ }
}
settingsRow("photo") {
Toggle("Auto-accept images", isOn: $autoAcceptImages)
@@ -183,6 +186,16 @@ struct PrivacySettings: View {
}
}
+ private func setEncryptLocalFiles(_ enable: Bool) {
+ do {
+ try apiSetEncryptLocalFiles(enable)
+ } catch let error {
+ let err = responseError(error)
+ logger.error("apiSetEncryptLocalFiles \(err)")
+ alert = .error(title: "Error", error: "\(err)")
+ }
+ }
+
private func setOrAskSendReceiptsContacts(_ enable: Bool) {
contactReceiptsOverrides = m.chats.reduce(0) { count, chat in
let sendRcpts = chat.chatInfo.contact?.chatSettings.sendRcpts
diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift
index 2fb069a86c..ea52f4be89 100644
--- a/apps/ios/SimpleX NSE/NotificationService.swift
+++ b/apps/ios/SimpleX NSE/NotificationService.swift
@@ -216,7 +216,7 @@ func startChat() -> DBMigrationResult? {
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
try setXFTPConfig(xftpConfig)
- // try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
+ try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
let justStarted = try apiStartChat()
chatStarted = true
if justStarted {
@@ -352,8 +352,8 @@ func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
throw r
}
-func apiSetEncryptLocalFiles(_ enable: Boolean) throws {
- let r = chatSendCmdSync(.apiSetEncryptLocalFiles(enable: enable))
+func apiSetEncryptLocalFiles(_ enable: Bool) throws {
+ let r = sendSimpleXCmd(.apiSetEncryptLocalFiles(enable: enable))
if case .cmdOk = r { return }
throw r
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
index f49984b942..cc8d481b59 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
@@ -338,7 +338,7 @@ object ChatController {
apiSetTempFolder(coreTmpDir.absolutePath)
apiSetFilesFolder(appFilesDir.absolutePath)
apiSetXFTPConfig(getXFTPCfg())
-// apiSetEncryptLocalFiles(appPrefs.privacyEncryptLocalFiles.get())
+ apiSetEncryptLocalFiles(appPrefs.privacyEncryptLocalFiles.get())
val justStarted = apiStartChat()
val users = listUsers()
chatModel.users.clear()
@@ -1333,7 +1333,7 @@ object ChatController {
private suspend fun sendCommandOkResp(cmd: CC): Boolean {
val r = sendCmd(cmd)
val ok = r is CR.CmdOk
- if (!ok) apiErrorAlert(cmd.cmdType, generalGetString(MR.strings.error), r)
+ if (!ok) apiErrorAlert(cmd.cmdType, generalGetString(MR.strings.error_alert_title), r)
return ok
}
@@ -1942,7 +1942,7 @@ sealed class CC {
class ApiRejectContact(val contactReqId: Long): CC()
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
- class ReceiveFile(val fileId: Long, val encrypt: Boolean?, val inline: Boolean?): CC()
+ class ReceiveFile(val fileId: Long, val encrypt: Boolean, val inline: Boolean?): CC()
class CancelFile(val fileId: Long): CC()
class ShowVersion(): CC()
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt
index ef0940b2a0..210d3c6136 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt
@@ -64,7 +64,9 @@ fun PrivacySettingsView(
SectionDividerSpaced()
SectionView(stringResource(MR.strings.settings_section_title_chats)) {
- SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles)
+ SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable ->
+ withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) }
+ })
SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
SettingsPreferenceItem(
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 7912a5ad1b..114fe49e92 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
@@ -113,6 +113,7 @@
Server requires authorization to upload, check password
Possibly, certificate fingerprint in server address is incorrect
Error setting address
+ Error
Connect
Disconnect
Create queue
From 4b6df43e97ba7ac125529c409f254c4cddb8cdda Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Mon, 16 Oct 2023 16:10:56 +0400
Subject: [PATCH 34/80] core: confirm to reconnect via address plan (#3212)
* core: confirm to reconnect plan
* rework query to prefer connections with contacts
---
src/Simplex/Chat.hs | 25 +++++++++++++++----------
src/Simplex/Chat/Controller.hs | 12 ++++++++----
src/Simplex/Chat/Store/Connections.hs | 24 ++++++++++++++++++++++++
src/Simplex/Chat/Types.hs | 3 +++
src/Simplex/Chat/View.hs | 8 +++++---
tests/ChatTests/Groups.hs | 7 +++++++
tests/ChatTests/Profiles.hs | 5 +++++
7 files changed, 67 insertions(+), 17 deletions(-)
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 90a6617059..ef56e42056 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -1364,13 +1364,13 @@ processChatCommand = \case
APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq
Connect incognito aCReqUri@(Just cReqUri) -> withUser $ \user@User {userId} -> do
plan <- connectPlan user cReqUri `catchChatError` const (pure $ CPInvitationLink ILPOk)
- unless (connectionPlanOk plan) $ throwChatError (CEConnectionPlan plan)
+ unless (connectionPlanProceed plan) $ throwChatError (CEConnectionPlan plan)
processChatCommand $ APIConnect userId incognito aCReqUri
Connect _ Nothing -> throwChatError CEInvalidConnReq
ConnectSimplex incognito -> withUser $ \user@User {userId} -> do
let cReqUri = ACR SCMContact adminContactReq
plan <- connectPlan user cReqUri `catchChatError` const (pure $ CPInvitationLink ILPOk)
- unless (connectionPlanOk plan) $ throwChatError (CEConnectionPlan plan)
+ unless (connectionPlanProceed plan) $ throwChatError (CEConnectionPlan plan)
processChatCommand $ APIConnect userId incognito (Just cReqUri)
DeleteContact cName -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId) True
ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect
@@ -2245,27 +2245,32 @@ processChatCommand = \case
Just _ -> pure $ CPContactAddress CAPOwnLink
Nothing -> do
let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
- withStore' (\db -> getContactByConnReqHash db user cReqHash) >>= \case
+ withStore' (\db -> getContactConnEntityByConnReqHash db user cReqHash) >>= \case
Nothing -> pure $ CPContactAddress CAPOk
- Just ct
- | not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnecting ct)
+ Just (RcvDirectMsgConnection _conn Nothing) -> pure $ CPContactAddress CAPConnectingConfirmReconnect
+ Just (RcvDirectMsgConnection _ (Just ct))
+ | not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnectingProhibit ct)
+ | contactDeleted ct -> pure $ CPContactAddress CAPOk
| otherwise -> pure $ CPContactAddress (CAPKnown ct)
+ Just _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection"
-- group link
Just _ ->
withStore' (\db -> getGroupInfoByUserContactLinkConnReq db user cReq) >>= \case
Just g -> pure $ CPGroupLink (GLPOwnLink g)
Nothing -> do
let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
- ct_ <- withStore' $ \db -> getContactByConnReqHash db user cReqHash
+ connEnt_ <- withStore' $ \db -> getContactConnEntityByConnReqHash db user cReqHash
gInfo_ <- withStore' $ \db -> getGroupInfoByGroupLinkHash db user cReqHash
- case (gInfo_, ct_) of
+ case (gInfo_, connEnt_) of
(Nothing, Nothing) -> pure $ CPGroupLink GLPOk
- (Nothing, Just ct)
- | not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnecting gInfo_)
+ (Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> pure $ CPGroupLink GLPConnectingConfirmReconnect
+ (Nothing, Just (RcvDirectMsgConnection _ (Just ct)))
+ | not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnectingProhibit gInfo_)
| otherwise -> pure $ CPGroupLink GLPOk
+ (Nothing, Just _) -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection"
(Just gInfo@GroupInfo {membership}, _)
| not (memberActive membership) && not (memberRemoved membership) ->
- pure $ CPGroupLink (GLPConnecting gInfo_)
+ pure $ CPGroupLink (GLPConnectingProhibit gInfo_)
| memberActive membership -> pure $ CPGroupLink (GLPKnown gInfo)
| otherwise -> pure $ CPGroupLink GLPOk
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index 0a9b886940..d8851ad87a 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -647,7 +647,8 @@ instance ToJSON InvitationLinkPlan where
data ContactAddressPlan
= CAPOk
| CAPOwnLink
- | CAPConnecting {contact :: Contact}
+ | CAPConnectingConfirmReconnect
+ | CAPConnectingProhibit {contact :: Contact}
| CAPKnown {contact :: Contact}
deriving (Show, Generic)
@@ -658,7 +659,8 @@ instance ToJSON ContactAddressPlan where
data GroupLinkPlan
= GLPOk
| GLPOwnLink {groupInfo :: GroupInfo}
- | GLPConnecting {groupInfo_ :: Maybe GroupInfo}
+ | GLPConnectingConfirmReconnect
+ | GLPConnectingProhibit {groupInfo_ :: Maybe GroupInfo}
| GLPKnown {groupInfo :: GroupInfo}
deriving (Show, Generic)
@@ -666,8 +668,8 @@ instance ToJSON GroupLinkPlan where
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "GLP"
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "GLP"
-connectionPlanOk :: ConnectionPlan -> Bool
-connectionPlanOk = \case
+connectionPlanProceed :: ConnectionPlan -> Bool
+connectionPlanProceed = \case
CPInvitationLink ilp -> case ilp of
ILPOk -> True
ILPOwnLink -> True
@@ -675,10 +677,12 @@ connectionPlanOk = \case
CPContactAddress cap -> case cap of
CAPOk -> True
CAPOwnLink -> True
+ CAPConnectingConfirmReconnect -> True
_ -> False
CPGroupLink glp -> case glp of
GLPOk -> True
GLPOwnLink _ -> True
+ GLPConnectingConfirmReconnect -> True
_ -> False
newtype UserPwd = UserPwd {unUserPwd :: Text}
diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs
index 3ef77cbb65..d8ec38269f 100644
--- a/src/Simplex/Chat/Store/Connections.hs
+++ b/src/Simplex/Chat/Store/Connections.hs
@@ -10,6 +10,7 @@
module Simplex.Chat.Store.Connections
( getConnectionEntity,
getConnectionEntityByConnReq,
+ getContactConnEntityByConnReqHash,
getConnectionsToSubscribe,
unsetConnectionToSubscribe,
)
@@ -159,6 +160,29 @@ getConnectionEntityByConnReq db user cReq = do
DB.query db "SELECT agent_conn_id FROM connections WHERE conn_req_inv = ? LIMIT 1" (Only cReq)
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db user) connId_
+-- search connection for connection plan:
+-- multiple connections can have same via_contact_uri_hash if request was repeated;
+-- this function searches for latest connection with contact so that "known contact" plan would be chosen;
+-- deleted connections are filtered out to allow re-connecting via same contact address
+getContactConnEntityByConnReqHash :: DB.Connection -> User -> ConnReqUriHash -> IO (Maybe ConnectionEntity)
+getContactConnEntityByConnReqHash db user cReqHash = do
+ connId_ <- maybeFirstRow fromOnly $
+ DB.query
+ db
+ [sql|
+ SELECT agent_conn_id FROM (
+ SELECT
+ agent_conn_id,
+ (CASE WHEN contact_id IS NOT NULL THEN 1 ELSE 0 END) AS conn_ord
+ FROM connections
+ WHERE via_contact_uri_hash = ? AND conn_status != ?
+ ORDER BY conn_ord DESC, created_at DESC
+ LIMIT 1
+ )
+ |]
+ (cReqHash, ConnDeleted)
+ maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db user) connId_
+
getConnectionsToSubscribe :: DB.Connection -> IO ([ConnId], [ConnectionEntity])
getConnectionsToSubscribe db = do
aConnIds <- map fromOnly <$> DB.query_ db "SELECT agent_conn_id FROM connections where to_subscribe = 1"
diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs
index 9992f96ab0..10b0c91d04 100644
--- a/src/Simplex/Chat/Types.hs
+++ b/src/Simplex/Chat/Types.hs
@@ -219,6 +219,9 @@ contactReady Contact {activeConn} = connReady activeConn
contactActive :: Contact -> Bool
contactActive Contact {contactStatus} = contactStatus == CSActive
+contactDeleted :: Contact -> Bool
+contactDeleted Contact {contactStatus} = contactStatus == CSDeleted
+
contactSecurityCode :: Contact -> Maybe SecurityCode
contactSecurityCode Contact {activeConn} = connectionCode activeConn
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index 08b1579bde..fa2213d083 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -1281,7 +1281,8 @@ viewConnectionPlan = \case
CPContactAddress cap -> case cap of
CAPOk -> [ctAddr "ok to connect"]
CAPOwnLink -> [ctAddr "own address"]
- CAPConnecting ct -> [ctAddr ("connecting to contact " <> ttyContact' ct)]
+ CAPConnectingConfirmReconnect -> [ctAddr "connecting, allowed to reconnect"]
+ CAPConnectingProhibit ct -> [ctAddr ("connecting to contact " <> ttyContact' ct)]
CAPKnown ct ->
[ ctAddr ("known contact " <> ttyContact' ct),
"use " <> ttyToContact' ct <> highlight' "" <> " to send messages"
@@ -1291,8 +1292,9 @@ viewConnectionPlan = \case
CPGroupLink glp -> case glp of
GLPOk -> [grpLink "ok to connect"]
GLPOwnLink g -> [grpLink "own link for group " <> ttyGroup' g]
- GLPConnecting Nothing -> [grpLink "connecting"]
- GLPConnecting (Just g) -> [grpLink ("connecting to group " <> ttyGroup' g)]
+ GLPConnectingConfirmReconnect -> [grpLink "connecting, allowed to reconnect"]
+ GLPConnectingProhibit Nothing -> [grpLink "connecting"]
+ GLPConnectingProhibit (Just g) -> [grpLink ("connecting to group " <> ttyGroup' g)]
GLPKnown g ->
[ grpLink ("known group " <> ttyGroup' g),
"use " <> ttyToGroup g <> highlight' "" <> " to send messages"
diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs
index 4c31805265..43498711e7 100644
--- a/tests/ChatTests/Groups.hs
+++ b/tests/ChatTests/Groups.hs
@@ -2397,8 +2397,15 @@ testPlanGroupLinkConnecting tmp = do
alice ##> "/create link #team"
getGroupLink alice "team" GRMember True
withNewTestChat tmp "bob" bobProfile $ \bob -> do
+ threadDelay 100000
+
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
+
+ bob ##> ("/_connect plan 1 " <> gLink)
+ bob <## "group link: connecting, allowed to reconnect"
+
+ threadDelay 100000
withTestChat tmp "alice" $ \alice -> do
alice
<### [ "1 group links active",
diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs
index 80e1709222..5682d09ee8 100644
--- a/tests/ChatTests/Profiles.hs
+++ b/tests/ChatTests/Profiles.hs
@@ -651,8 +651,13 @@ testPlanAddressConnecting tmp = do
getContactLink alice True
withNewTestChat tmp "bob" bobProfile $ \bob -> do
threadDelay 100000
+
bob ##> ("/c " <> cLink)
bob <## "connection request sent!"
+
+ bob ##> ("/_connect plan 1 " <> cLink)
+ bob <## "contact address: connecting, allowed to reconnect"
+
threadDelay 100000
withTestChat tmp "alice" $ \alice -> do
alice <## "Your address is active! To show: /sa"
From 9ed31261e1d4e7d4e6aba449fa80fa5bf0ffbd46 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Mon, 16 Oct 2023 16:16:03 +0400
Subject: [PATCH 35/80] core: check saved links and hashes by both connection
request uri schemas for connection plan (#3233)
---
src/Simplex/Chat.hs | 31 ++++++++++++++++++-------
src/Simplex/Chat/Store/Connections.hs | 14 ++++++------
src/Simplex/Chat/Store/Groups.hs | 16 ++++++-------
src/Simplex/Chat/Store/Profiles.hs | 8 +++----
tests/ChatTests/Direct.hs | 8 +++++++
tests/ChatTests/Groups.hs | 33 +++++++++++++++++++++++++++
tests/ChatTests/Profiles.hs | 28 ++++++++++++++++++++++-
tests/ChatTests/Utils.hs | 8 +++++++
8 files changed, 118 insertions(+), 28 deletions(-)
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index ef56e42056..e224f04ebb 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -2222,7 +2222,7 @@ processChatCommand = \case
processChatCommand $ APISetChatSettings (ChatRef cType chatId) $ updateSettings chatSettings
connectPlan :: User -> AConnectionRequestUri -> m ConnectionPlan
connectPlan user (ACR SCMInvitation cReq) = do
- withStore' (\db -> getConnectionEntityByConnReq db user cReq) >>= \case
+ withStore' (\db -> getConnectionEntityByConnReq db user cReqSchemas) >>= \case
Nothing -> pure $ CPInvitationLink ILPOk
Just (RcvDirectMsgConnection conn ct_) -> do
let Connection {connStatus, contactConnInitiated} = conn
@@ -2235,17 +2235,23 @@ processChatCommand = \case
Just ct -> pure $ CPInvitationLink (ILPKnown ct)
Nothing -> throwChatError $ CEInternalError "ready RcvDirectMsgConnection connection should have associated contact"
Just _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection"
+ where
+ cReqSchemas :: (ConnReqInvitation, ConnReqInvitation)
+ cReqSchemas = case cReq of
+ (CRInvitationUri crData e2e) ->
+ ( CRInvitationUri crData {crScheme = CRSSimplex} e2e,
+ CRInvitationUri crData {crScheme = simplexChat} e2e
+ )
connectPlan user (ACR SCMContact cReq) = do
let CRContactUri ConnReqUriData {crClientData} = cReq
groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
case groupLinkId of
-- contact address
Nothing ->
- withStore' (`getUserContactLinkByConnReq` cReq) >>= \case
+ withStore' (`getUserContactLinkByConnReq` cReqSchemas) >>= \case
Just _ -> pure $ CPContactAddress CAPOwnLink
Nothing -> do
- let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
- withStore' (\db -> getContactConnEntityByConnReqHash db user cReqHash) >>= \case
+ withStore' (\db -> getContactConnEntityByConnReqHash db user cReqHashes) >>= \case
Nothing -> pure $ CPContactAddress CAPOk
Just (RcvDirectMsgConnection _conn Nothing) -> pure $ CPContactAddress CAPConnectingConfirmReconnect
Just (RcvDirectMsgConnection _ (Just ct))
@@ -2255,12 +2261,11 @@ processChatCommand = \case
Just _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection"
-- group link
Just _ ->
- withStore' (\db -> getGroupInfoByUserContactLinkConnReq db user cReq) >>= \case
+ withStore' (\db -> getGroupInfoByUserContactLinkConnReq db user cReqSchemas) >>= \case
Just g -> pure $ CPGroupLink (GLPOwnLink g)
Nothing -> do
- let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
- connEnt_ <- withStore' $ \db -> getContactConnEntityByConnReqHash db user cReqHash
- gInfo_ <- withStore' $ \db -> getGroupInfoByGroupLinkHash db user cReqHash
+ connEnt_ <- withStore' $ \db -> getContactConnEntityByConnReqHash db user cReqHashes
+ gInfo_ <- withStore' $ \db -> getGroupInfoByGroupLinkHash db user cReqHashes
case (gInfo_, connEnt_) of
(Nothing, Nothing) -> pure $ CPGroupLink GLPOk
(Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> pure $ CPGroupLink GLPConnectingConfirmReconnect
@@ -2273,6 +2278,16 @@ processChatCommand = \case
pure $ CPGroupLink (GLPConnectingProhibit gInfo_)
| memberActive membership -> pure $ CPGroupLink (GLPKnown gInfo)
| otherwise -> pure $ CPGroupLink GLPOk
+ where
+ cReqSchemas :: (ConnReqContact, ConnReqContact)
+ cReqSchemas = case cReq of
+ (CRContactUri crData) ->
+ ( CRContactUri crData {crScheme = CRSSimplex},
+ CRContactUri crData {crScheme = simplexChat}
+ )
+ cReqHashes :: (ConnReqUriHash, ConnReqUriHash)
+ cReqHashes = bimap hash hash cReqSchemas
+ hash = ConnReqUriHash . C.sha256Hash . strEncode
assertDirectAllowed :: ChatMonad m => User -> MsgDirection -> Contact -> CMEventTag e -> m ()
assertDirectAllowed user dir ct event =
diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs
index d8ec38269f..59ffb57c6e 100644
--- a/src/Simplex/Chat/Store/Connections.hs
+++ b/src/Simplex/Chat/Store/Connections.hs
@@ -154,18 +154,18 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
userContact_ [(cReq, groupId)] = Right UserContact {userContactLinkId, connReqContact = cReq, groupId}
userContact_ _ = Left SEUserContactLinkNotFound
-getConnectionEntityByConnReq :: DB.Connection -> User -> ConnReqInvitation -> IO (Maybe ConnectionEntity)
-getConnectionEntityByConnReq db user cReq = do
+getConnectionEntityByConnReq :: DB.Connection -> User -> (ConnReqInvitation, ConnReqInvitation) -> IO (Maybe ConnectionEntity)
+getConnectionEntityByConnReq db user (cReqSchema1, cReqSchema2) = do
connId_ <- maybeFirstRow fromOnly $
- DB.query db "SELECT agent_conn_id FROM connections WHERE conn_req_inv = ? LIMIT 1" (Only cReq)
+ DB.query db "SELECT agent_conn_id FROM connections WHERE conn_req_inv IN (?,?) LIMIT 1" (cReqSchema1, cReqSchema2)
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db user) connId_
-- search connection for connection plan:
-- multiple connections can have same via_contact_uri_hash if request was repeated;
-- this function searches for latest connection with contact so that "known contact" plan would be chosen;
-- deleted connections are filtered out to allow re-connecting via same contact address
-getContactConnEntityByConnReqHash :: DB.Connection -> User -> ConnReqUriHash -> IO (Maybe ConnectionEntity)
-getContactConnEntityByConnReqHash db user cReqHash = do
+getContactConnEntityByConnReqHash :: DB.Connection -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe ConnectionEntity)
+getContactConnEntityByConnReqHash db user (cReqHash1, cReqHash2) = do
connId_ <- maybeFirstRow fromOnly $
DB.query
db
@@ -175,12 +175,12 @@ getContactConnEntityByConnReqHash db user cReqHash = do
agent_conn_id,
(CASE WHEN contact_id IS NOT NULL THEN 1 ELSE 0 END) AS conn_ord
FROM connections
- WHERE via_contact_uri_hash = ? AND conn_status != ?
+ WHERE via_contact_uri_hash IN (?,?) AND conn_status != ?
ORDER BY conn_ord DESC, created_at DESC
LIMIT 1
)
|]
- (cReqHash, ConnDeleted)
+ (cReqHash1, cReqHash2, ConnDeleted)
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db user) connId_
getConnectionsToSubscribe :: DB.Connection -> IO ([ConnId], [ConnectionEntity])
diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs
index 30e45a82dc..a4a19816da 100644
--- a/src/Simplex/Chat/Store/Groups.hs
+++ b/src/Simplex/Chat/Store/Groups.hs
@@ -1121,21 +1121,21 @@ getGroupInfo db User {userId, userContactId} groupId =
|]
(groupId, userId, userContactId)
-getGroupInfoByUserContactLinkConnReq :: DB.Connection -> User -> ConnReqContact -> IO (Maybe GroupInfo)
-getGroupInfoByUserContactLinkConnReq db user cReq = do
+getGroupInfoByUserContactLinkConnReq :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo)
+getGroupInfoByUserContactLinkConnReq db user (cReqSchema1, cReqSchema2) = do
groupId_ <- maybeFirstRow fromOnly $
DB.query
db
[sql|
SELECT group_id
FROM user_contact_links
- WHERE conn_req_contact = ?
+ WHERE conn_req_contact IN (?,?)
|]
- (Only cReq)
+ (cReqSchema1, cReqSchema2)
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db user) groupId_
-getGroupInfoByGroupLinkHash :: DB.Connection -> User -> ConnReqUriHash -> IO (Maybe GroupInfo)
-getGroupInfoByGroupLinkHash db user@User {userId, userContactId} groupLinkHash = do
+getGroupInfoByGroupLinkHash :: DB.Connection -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe GroupInfo)
+getGroupInfoByGroupLinkHash db user@User {userId, userContactId} (groupLinkHash1, groupLinkHash2) = do
groupId_ <- maybeFirstRow fromOnly $
DB.query
db
@@ -1143,11 +1143,11 @@ getGroupInfoByGroupLinkHash db user@User {userId, userContactId} groupLinkHash =
SELECT g.group_id
FROM groups g
JOIN group_members mu ON mu.group_id = g.group_id
- WHERE g.user_id = ? AND g.via_group_link_uri_hash = ?
+ WHERE g.user_id = ? AND g.via_group_link_uri_hash IN (?,?)
AND mu.contact_id = ? AND mu.member_status NOT IN (?,?,?)
LIMIT 1
|]
- (userId, groupLinkHash, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted)
+ (userId, groupLinkHash1, groupLinkHash2, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted)
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db user) groupId_
getGroupIdByName :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO GroupId
diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs
index fa573e4e60..c9c4806496 100644
--- a/src/Simplex/Chat/Store/Profiles.hs
+++ b/src/Simplex/Chat/Store/Profiles.hs
@@ -441,17 +441,17 @@ getUserContactLinkById db userId userContactLinkId =
|]
(userId, userContactLinkId)
-getUserContactLinkByConnReq :: DB.Connection -> ConnReqContact -> IO (Maybe UserContactLink)
-getUserContactLinkByConnReq db cReq =
+getUserContactLinkByConnReq :: DB.Connection -> (ConnReqContact, ConnReqContact) -> IO (Maybe UserContactLink)
+getUserContactLinkByConnReq db (cReqSchema1, cReqSchema2) =
maybeFirstRow toUserContactLink $
DB.query
db
[sql|
SELECT conn_req_contact, auto_accept, auto_accept_incognito, auto_reply_msg_content
FROM user_contact_links
- WHERE conn_req_contact = ?
+ WHERE conn_req_contact IN (?,?)
|]
- (Only cReq)
+ (cReqSchema1, cReqSchema2)
updateUserAddressAutoAccept :: DB.Connection -> User -> Maybe AutoAccept -> ExceptT StoreError IO UserContactLink
updateUserAddressAutoAccept db user@User {userId} autoAccept = do
diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs
index 677bc4c09d..1a133fd8e3 100644
--- a/tests/ChatTests/Direct.hs
+++ b/tests/ChatTests/Direct.hs
@@ -270,6 +270,10 @@ testPlanInvitationLinkOwn tmp =
alice ##> ("/_connect plan 1 " <> inv)
alice <## "invitation link: own link"
+ let invSchema2 = linkAnotherSchema inv
+ alice ##> ("/_connect plan 1 " <> invSchema2)
+ alice <## "invitation link: own link"
+
alice ##> ("/c " <> inv)
alice <## "confirmation sent!"
alice
@@ -305,6 +309,10 @@ testPlanInvitationLinkConnecting tmp = do
bob ##> ("/_connect plan 1 " <> inv)
bob <## "invitation link: connecting"
+ let invSchema2 = linkAnotherSchema inv
+ bob ##> ("/_connect plan 1 " <> invSchema2)
+ bob <## "invitation link: connecting"
+
testContactClear :: HasCallStack => FilePath -> IO ()
testContactClear =
testChat2 aliceProfile bobProfile $
diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs
index 43498711e7..7a8b1368bf 100644
--- a/tests/ChatTests/Groups.hs
+++ b/tests/ChatTests/Groups.hs
@@ -2290,6 +2290,11 @@ testPlanGroupLinkOkKnown =
bob <## "group link: known group #team"
bob <## "use #team to send messages"
+ let gLinkSchema2 = linkAnotherSchema gLink
+ bob ##> ("/_connect plan 1 " <> gLinkSchema2)
+ bob <## "group link: known group #team"
+ bob <## "use #team to send messages"
+
bob ##> ("/c " <> gLink)
bob <## "group link: known group #team"
bob <## "use #team to send messages"
@@ -2331,6 +2336,11 @@ testPlanHostContactDeletedGroupLinkKnown =
bob <## "group link: known group #team"
bob <## "use #team to send messages"
+ let gLinkSchema2 = linkAnotherSchema gLink
+ bob ##> ("/_connect plan 1 " <> gLinkSchema2)
+ bob <## "group link: known group #team"
+ bob <## "use #team to send messages"
+
bob ##> ("/c " <> gLink)
bob <## "group link: known group #team"
bob <## "use #team to send messages"
@@ -2347,6 +2357,10 @@ testPlanGroupLinkOwn tmp =
alice ##> ("/_connect plan 1 " <> gLink)
alice <## "group link: own link for group #team"
+ let gLinkSchema2 = linkAnotherSchema gLink
+ alice ##> ("/_connect plan 1 " <> gLinkSchema2)
+ alice <## "group link: own link for group #team"
+
alice ##> ("/c " <> gLink)
alice <## "connection request sent!"
alice <## "alice_1 (Alice): accepting request to join group #team..."
@@ -2373,6 +2387,9 @@ testPlanGroupLinkOwn tmp =
alice ##> ("/_connect plan 1 " <> gLink)
alice <## "group link: own link for group #team"
+ alice ##> ("/_connect plan 1 " <> gLinkSchema2)
+ alice <## "group link: own link for group #team"
+
-- group works if merged contact is deleted
alice ##> "/d alice_1"
alice <## "alice_1: contact is deleted"
@@ -2405,6 +2422,10 @@ testPlanGroupLinkConnecting tmp = do
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: connecting, allowed to reconnect"
+ let gLinkSchema2 = linkAnotherSchema gLink
+ bob ##> ("/_connect plan 1 " <> gLinkSchema2)
+ bob <## "group link: connecting, allowed to reconnect"
+
threadDelay 100000
withTestChat tmp "alice" $ \alice -> do
alice
@@ -2417,6 +2438,10 @@ testPlanGroupLinkConnecting tmp = do
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: connecting"
+ let gLinkSchema2 = linkAnotherSchema gLink
+ bob ##> ("/_connect plan 1 " <> gLinkSchema2)
+ bob <## "group link: connecting"
+
bob ##> ("/c " <> gLink)
bob <## "group link: connecting"
@@ -2462,6 +2487,10 @@ testPlanGroupLinkLeaveRejoin =
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: ok to connect"
+ let gLinkSchema2 = linkAnotherSchema gLink
+ bob ##> ("/_connect plan 1 " <> gLinkSchema2)
+ bob <## "group link: ok to connect"
+
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob_1 (Bob): accepting request to join group #team..."
@@ -2490,6 +2519,10 @@ testPlanGroupLinkLeaveRejoin =
bob <## "group link: known group #team_1"
bob <## "use #team_1 to send messages"
+ bob ##> ("/_connect plan 1 " <> gLinkSchema2)
+ bob <## "group link: known group #team_1"
+ bob <## "use #team_1 to send messages"
+
bob ##> ("/c " <> gLink)
bob <## "group link: known group #team_1"
bob <## "use #team_1 to send messages"
diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs
index 5682d09ee8..bd7f4c1682 100644
--- a/tests/ChatTests/Profiles.hs
+++ b/tests/ChatTests/Profiles.hs
@@ -599,6 +599,11 @@ testPlanAddressOkKnown =
bob <## "contact address: known contact alice"
bob <## "use @alice to send messages"
+ let cLinkSchema2 = linkAnotherSchema cLink
+ bob ##> ("/_connect plan 1 " <> cLinkSchema2)
+ bob <## "contact address: known contact alice"
+ bob <## "use @alice to send messages"
+
bob ##> ("/c " <> cLink)
bob <## "contact address: known contact alice"
bob <## "use @alice to send messages"
@@ -612,11 +617,15 @@ testPlanAddressOwn tmp =
alice ##> ("/_connect plan 1 " <> cLink)
alice <## "contact address: own address"
+ let cLinkSchema2 = linkAnotherSchema cLink
+ alice ##> ("/_connect plan 1 " <> cLinkSchema2)
+ alice <## "contact address: own address"
+
alice ##> ("/c " <> cLink)
alice <## "connection request sent!"
alice <## "alice_1 (Alice) wants to connect to you!"
alice <## "to accept: /ac alice_1"
- alice <## ("to reject: /rc alice_1 (the sender will NOT be notified)")
+ alice <## "to reject: /rc alice_1 (the sender will NOT be notified)"
alice @@@ [("<@alice_1", ""), (":2","")]
alice ##> "/ac alice_1"
alice <## "alice_1 (Alice): accepting contact request..."
@@ -658,6 +667,10 @@ testPlanAddressConnecting tmp = do
bob ##> ("/_connect plan 1 " <> cLink)
bob <## "contact address: connecting, allowed to reconnect"
+ let cLinkSchema2 = linkAnotherSchema cLink
+ bob ##> ("/_connect plan 1 " <> cLinkSchema2)
+ bob <## "contact address: connecting, allowed to reconnect"
+
threadDelay 100000
withTestChat tmp "alice" $ \alice -> do
alice <## "Your address is active! To show: /sa"
@@ -672,6 +685,10 @@ testPlanAddressConnecting tmp = do
bob ##> ("/_connect plan 1 " <> cLink)
bob <## "contact address: connecting to contact alice"
+ let cLinkSchema2 = linkAnotherSchema cLink
+ bob ##> ("/_connect plan 1 " <> cLinkSchema2)
+ bob <## "contact address: connecting to contact alice"
+
bob ##> ("/c " <> cLink)
bob <## "contact address: connecting to contact alice"
@@ -706,6 +723,10 @@ testPlanAddressContactDeletedReconnected =
bob ##> ("/_connect plan 1 " <> cLink)
bob <## "contact address: ok to connect"
+ let cLinkSchema2 = linkAnotherSchema cLink
+ bob ##> ("/_connect plan 1 " <> cLinkSchema2)
+ bob <## "contact address: ok to connect"
+
bob ##> ("/c " <> cLink)
bob <## "connection request sent!"
alice <## "bob (Bob) wants to connect to you!"
@@ -726,6 +747,11 @@ testPlanAddressContactDeletedReconnected =
bob <## "contact address: known contact alice_1"
bob <## "use @alice_1 to send messages"
+ let cLinkSchema2 = linkAnotherSchema cLink
+ bob ##> ("/_connect plan 1 " <> cLinkSchema2)
+ bob <## "contact address: known contact alice_1"
+ bob <## "use @alice_1 to send messages"
+
bob ##> ("/c " <> cLink)
bob <## "contact address: known contact alice_1"
bob <## "use @alice_1 to send messages"
diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs
index d98431e815..fcb6b65e41 100644
--- a/tests/ChatTests/Utils.hs
+++ b/tests/ChatTests/Utils.hs
@@ -559,3 +559,11 @@ currentChatVRangeInfo =
vRangeStr :: VersionRange -> String
vRangeStr (VersionRange minVer maxVer) = "(" <> show minVer <> ", " <> show maxVer <> ")"
+
+linkAnotherSchema :: String -> String
+linkAnotherSchema link
+ | "https://simplex.chat/" `isPrefixOf` link =
+ T.unpack $ T.replace "https://simplex.chat/" "simplex:/" $ T.pack link
+ | "simplex:/" `isPrefixOf` link =
+ T.unpack $ T.replace "simplex:/" "https://simplex.chat/" $ T.pack link
+ | otherwise = error "link starts with neither https://simplex.chat/ nor simplex:/"
From e4c8386f3ffc868afc210d432dd5a97216f2fe7a Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Mon, 16 Oct 2023 19:23:38 +0400
Subject: [PATCH 36/80] core: replace simplex:/ with simplex.chat links in
view; remove trustedUri flag from simplex links markdown format (#3235)
---
cabal.project | 2 +-
scripts/nix/sha256map.nix | 2 +-
src/Simplex/Chat.hs | 2 +-
src/Simplex/Chat/Markdown.hs | 11 ++++-------
src/Simplex/Chat/View.hs | 14 ++++++++++----
stack.yaml | 2 +-
tests/ChatTests/Profiles.hs | 1 -
tests/MarkdownTests.hs | 14 +++++++-------
tests/ProtocolTests.hs | 22 +++++++++++-----------
9 files changed, 36 insertions(+), 34 deletions(-)
diff --git a/cabal.project b/cabal.project
index 3da442ac49..9a9a3e25da 100644
--- a/cabal.project
+++ b/cabal.project
@@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: 919550948501d315aa8845cbed1781d4298d4ced
+ tag: 1ad69cf74f18f25713ce564e1629d2538313b9e0
source-repository-package
type: git
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index 0a199779d0..17d650cb09 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,5 +1,5 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."919550948501d315aa8845cbed1781d4298d4ced" = "05d0cadhlazqi2lxcb7nvyjrf8q49c6ax7b8rahawbh1zmwg38nm";
+ "https://github.com/simplex-chat/simplexmq.git"."1ad69cf74f18f25713ce564e1629d2538313b9e0" = "1kil0962pn3ksnxh7dcwcbnkidz95yl31rm4m585ps7wnh6fp0l9";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "0kiwhvml42g9anw4d2v0zd1fpc790pj9syg5x3ik4l97fnkbbwpp";
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index e224f04ebb..89f64189af 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -5936,7 +5936,7 @@ chatCommandP =
adminContactReq :: ConnReqContact
adminContactReq =
- either error id $ strDecode "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"
+ either error id $ strDecode "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"
timeItToView :: ChatMonad' m => String -> m a -> m a
timeItToView s action = do
diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs
index d18f28db31..30990f225d 100644
--- a/src/Simplex/Chat/Markdown.hs
+++ b/src/Simplex/Chat/Markdown.hs
@@ -32,7 +32,7 @@ import Simplex.Chat.Types.Util
import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), ConnReqScheme (..), ConnReqUriData (..), ConnectionRequestUri (..), SMPQueue (..))
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fstToLower, sumTypeJSON)
-import Simplex.Messaging.Protocol (ProtocolServer (..), SrvLoc (..))
+import Simplex.Messaging.Protocol (ProtocolServer (..))
import Simplex.Messaging.Util (safeDecodeUtf8)
import System.Console.ANSI.Types
import qualified Text.Email.Validate as Email
@@ -48,7 +48,7 @@ data Format
| Secret
| Colored {color :: FormatColor}
| Uri
- | SimplexLink {linkType :: SimplexLinkType, simplexUri :: Text, trustedUri :: Bool, smpHosts :: NonEmpty Text}
+ | SimplexLink {linkType :: SimplexLinkType, simplexUri :: Text, smpHosts :: NonEmpty Text}
| Email
| Phone
deriving (Eq, Show, Generic)
@@ -229,15 +229,12 @@ markdownP = mconcat <$> A.many' fragmentP
simplexUriFormat = \case
ACR _ (CRContactUri crData) ->
let uri = safeDecodeUtf8 . strEncode $ CRContactUri crData {crScheme = CRSSimplex}
- in SimplexLink (linkType' crData) uri (trustedUri' crData) $ uriHosts crData
+ in SimplexLink (linkType' crData) uri $ uriHosts crData
ACR _ (CRInvitationUri crData e2e) ->
let uri = safeDecodeUtf8 . strEncode $ CRInvitationUri crData {crScheme = CRSSimplex} e2e
- in SimplexLink XLInvitation uri (trustedUri' crData) $ uriHosts crData
+ in SimplexLink XLInvitation uri $ uriHosts crData
where
uriHosts ConnReqUriData {crSmpQueues} = L.map (safeDecodeUtf8 . strEncode) $ sconcat $ L.map (host . qServer) crSmpQueues
- trustedUri' ConnReqUriData {crScheme} = case crScheme of
- CRSSimplex -> True
- CRSAppServer (SrvLoc host _) -> host == "simplex.chat"
linkType' ConnReqUriData {crClientData} = case crClientData >>= decodeJSON of
Just (CRDataGroup _) -> XLGroup
Nothing -> XLContact
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index fa2213d083..07cbec601f 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -693,11 +693,14 @@ viewConnReqInvitation :: ConnReqInvitation -> [StyledString]
viewConnReqInvitation cReq =
[ "pass this invitation link to your contact (via another channel): ",
"",
- (plain . strEncode) cReq,
+ (plain . strEncode) (simplexChatInvitation cReq),
"",
"and ask them to connect: " <> highlight' "/c "
]
+simplexChatInvitation :: ConnReqInvitation -> ConnReqInvitation
+simplexChatInvitation (CRInvitationUri crData e2e) = CRInvitationUri crData {crScheme = simplexChat} e2e
+
viewContactNotFound :: ContactName -> Maybe (GroupInfo, GroupMember) -> [StyledString]
viewContactNotFound cName suspectedMember =
["no contact " <> ttyContact cName <> useMessageMember]
@@ -736,7 +739,7 @@ connReqContact_ :: StyledString -> ConnReqContact -> [StyledString]
connReqContact_ intro cReq =
[ intro,
"",
- (plain . strEncode) cReq,
+ (plain . strEncode) (simplexChatContact cReq),
"",
"Anybody can send you contact requests with: " <> highlight' "/c ",
"to show it again: " <> highlight' "/sa",
@@ -744,6 +747,9 @@ connReqContact_ intro cReq =
"to delete it: " <> highlight' "/da" <> " (accepted contacts will remain connected)"
]
+simplexChatContact :: ConnReqContact -> ConnReqContact
+simplexChatContact (CRContactUri crData) = CRContactUri crData {crScheme = simplexChat}
+
autoAcceptStatus_ :: Maybe AutoAccept -> [StyledString]
autoAcceptStatus_ = \case
Just AutoAccept {acceptIncognito, autoReply} ->
@@ -755,7 +761,7 @@ groupLink_ :: StyledString -> GroupInfo -> ConnReqContact -> GroupMemberRole ->
groupLink_ intro g cReq mRole =
[ intro,
"",
- (plain . strEncode) cReq,
+ (plain . strEncode) (simplexChatContact cReq),
"",
"Anybody can connect to you and join group as " <> showRole mRole <> " with: " <> highlight' "/c ",
"to show it again: " <> highlight ("/show link #" <> viewGroupName g),
@@ -1022,7 +1028,7 @@ viewContactInfo :: Contact -> ConnectionStats -> Maybe Profile -> [StyledString]
viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn} stats incognitoProfile =
["contact ID: " <> sShow contactId]
<> viewConnectionStats stats
- <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) l]) contactLink
+ <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact l)]) contactLink
<> maybe
["you've shared main profile with this contact"]
(\p -> ["you've shared incognito profile with this contact: " <> incognitoProfile' p])
diff --git a/stack.yaml b/stack.yaml
index 5d9fc214fb..6e047f7e6c 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq
- github: simplex-chat/simplexmq
- commit: 919550948501d315aa8845cbed1781d4298d4ced
+ commit: 1ad69cf74f18f25713ce564e1629d2538313b9e0
- github: kazu-yamamoto/http2
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
# - ../direct-sqlcipher
diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs
index bd7f4c1682..68b925342e 100644
--- a/tests/ChatTests/Profiles.hs
+++ b/tests/ChatTests/Profiles.hs
@@ -747,7 +747,6 @@ testPlanAddressContactDeletedReconnected =
bob <## "contact address: known contact alice_1"
bob <## "use @alice_1 to send messages"
- let cLinkSchema2 = linkAnotherSchema cLink
bob ##> ("/_connect plan 1 " <> cLinkSchema2)
bob <## "contact address: known contact alice_1"
bob <## "use @alice_1 to send messages"
diff --git a/tests/MarkdownTests.hs b/tests/MarkdownTests.hs
index 837849d7e4..83a180c745 100644
--- a/tests/MarkdownTests.hs
+++ b/tests/MarkdownTests.hs
@@ -137,8 +137,8 @@ textColor = describe "text color (red)" do
uri :: Text -> Markdown
uri = Markdown $ Just Uri
-simplexLink :: SimplexLinkType -> Text -> Bool -> NonEmpty Text -> Text -> Markdown
-simplexLink linkType simplexUri trustedUri smpHosts = Markdown $ Just SimplexLink {linkType, simplexUri, trustedUri, smpHosts}
+simplexLink :: SimplexLinkType -> Text -> NonEmpty Text -> Text -> Markdown
+simplexLink linkType simplexUri smpHosts = Markdown $ Just SimplexLink {linkType, simplexUri, smpHosts}
textWithUri :: Spec
textWithUri = describe "text with Uri" do
@@ -152,13 +152,13 @@ textWithUri = describe "text with Uri" do
parseMarkdown "this is _https://simplex.chat" `shouldBe` "this is _https://simplex.chat"
it "SimpleX links" do
let inv = "/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
- parseMarkdown ("https://simplex.chat" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) True ["smp.simplex.im"] ("https://simplex.chat" <> inv)
- parseMarkdown ("simplex:" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) True ["smp.simplex.im"] ("simplex:" <> inv)
- parseMarkdown ("https://example.com" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) False ["smp.simplex.im"] ("https://example.com" <> inv)
+ parseMarkdown ("https://simplex.chat" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] ("https://simplex.chat" <> inv)
+ parseMarkdown ("simplex:" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] ("simplex:" <> inv)
+ parseMarkdown ("https://example.com" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] ("https://example.com" <> inv)
let ct = "/contact#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D"
- parseMarkdown ("https://simplex.chat" <> ct) `shouldBe` simplexLink XLContact ("simplex:" <> ct) True ["smp.simplex.im"] ("https://simplex.chat" <> ct)
+ parseMarkdown ("https://simplex.chat" <> ct) `shouldBe` simplexLink XLContact ("simplex:" <> ct) ["smp.simplex.im"] ("https://simplex.chat" <> ct)
let gr = "/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FWHV0YU1sYlU7NqiEHkHDB6gxO1ofTync%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAWbebOqVYuBXaiqHcXYjEHCpYi6VzDlu6CVaijDTmsQU%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22mL-7Divb94GGmGmRBef5Dg%3D%3D%22%7D"
- parseMarkdown ("https://simplex.chat" <> gr) `shouldBe` simplexLink XLGroup ("simplex:" <> gr) True ["smp4.simplex.im", "o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"] ("https://simplex.chat" <> gr)
+ parseMarkdown ("https://simplex.chat" <> gr) `shouldBe` simplexLink XLGroup ("simplex:" <> gr) ["smp4.simplex.im", "o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"] ("https://simplex.chat" <> gr)
email :: Text -> Markdown
email = Markdown $ Just Email
diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs
index d62d7a470a..0b99c5a4d8 100644
--- a/tests/ProtocolTests.hs
+++ b/tests/ProtocolTests.hs
@@ -39,7 +39,7 @@ queue =
connReqData :: ConnReqUriData
connReqData =
ConnReqUriData
- { crScheme = simplexChat,
+ { crScheme = CRSSimplex,
crAgentVRange = mkVersionRange 1 1,
crSmpQueues = [queue],
crClientData = Nothing
@@ -184,7 +184,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.msg.deleted\",\"params\":{}}"
#==# XMsgDeleted
it "x.file" $
- "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
+ "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
#==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Just testConnReq, fileInline = Nothing, fileDescr = Nothing}
it "x.file without file invitation" $
"{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
@@ -193,7 +193,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}"
#==# XFileAcpt "photo.jpg"
it "x.file.acpt.inv" $
- "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}"
+ "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}"
#==# XFileAcptInv (SharedMsgId "\1\2\3\4") (Just testConnReq) "photo.jpg"
it "x.file.acpt.inv" $
"{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\"}}"
@@ -220,10 +220,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
==# XContact testProfile Nothing
it "x.grp.inv" $
- "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}"
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Nothing}
it "x.grp.inv with group link id" $
- "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}"
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Just $ GroupLinkId "\1\2\3\4"}
it "x.grp.acpt without incognito profile" $
"{\"v\":\"1\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
@@ -241,16 +241,16 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-2\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile}
it "x.grp.mem.inv" $
- "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}"
#==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq}
it "x.grp.mem.inv w/t directConnReq" $
- "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}"
#==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing}
it "x.grp.mem.fwd" $
- "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq}
it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $
- "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-2\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-2\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing}
it "x.grp.mem.info" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
@@ -271,10 +271,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.grp.del\",\"params\":{}}"
==# XGrpDel
it "x.grp.direct.inv" $
- "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
#==# XGrpDirectInv testConnReq (Just $ MCText "hello")
it "x.grp.direct.inv without content" $
- "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}"
#==# XGrpDirectInv testConnReq Nothing
it "x.info.probe" $
"{\"v\":\"1\",\"event\":\"x.info.probe\",\"params\":{\"probe\":\"AQIDBA==\"}}"
From 99c458406f55683f408b27223e8a01263035b25e Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Tue, 17 Oct 2023 11:44:35 +0400
Subject: [PATCH 37/80] android: remove browser mode for simplex links (#3239)
---
.../kotlin/chat/simplex/common/model/ChatModel.kt | 2 +-
.../chat/simplex/common/views/chat/item/TextItemView.kt | 8 ++------
.../simplex/common/views/usersettings/PrivacySettings.kt | 7 +++----
3 files changed, 6 insertions(+), 11 deletions(-)
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 a4b76cc79d..f3655d0008 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
@@ -2416,7 +2416,7 @@ sealed class Format {
@Serializable @SerialName("secret") class Secret: Format()
@Serializable @SerialName("colored") class Colored(val color: FormatColor): Format()
@Serializable @SerialName("uri") class Uri: Format()
- @Serializable @SerialName("simplexLink") class SimplexLink(val linkType: SimplexLinkType, val simplexUri: String, val trustedUri: Boolean, val smpHosts: List): Format()
+ @Serializable @SerialName("simplexLink") class SimplexLink(val linkType: SimplexLinkType, val simplexUri: String, val smpHosts: List): Format()
@Serializable @SerialName("email") class Email: Format()
@Serializable @SerialName("phone") class Phone: Format()
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt
index eabab138ba..ff1267d0fa 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt
@@ -136,12 +136,8 @@ fun MarkdownText (
val link = ft.link(linkMode)
if (link != null) {
hasLinks = true
- val ftStyle = if (ft.format is Format.SimplexLink && !ft.format.trustedUri && linkMode == SimplexLinkMode.BROWSER) {
- SpanStyle(color = Color.Red, textDecoration = TextDecoration.Underline)
- } else {
- ft.format.style
- }
- withAnnotation(tag = if (ft.format is Format.SimplexLink && linkMode != SimplexLinkMode.BROWSER) "SIMPLEX_URL" else "URL", annotation = link) {
+ val ftStyle = ft.format.style
+ withAnnotation(tag = if (ft.format is Format.SimplexLink) "SIMPLEX_URL" else "URL", annotation = link) {
withStyle(ftStyle) { append(ft.viewText(linkMode)) }
}
} else {
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt
index 210d3c6136..211fe59671 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt
@@ -92,9 +92,6 @@ fun PrivacySettingsView(
chatModel.simplexLinkMode.value = it
})
}
- if (chatModel.simplexLinkMode.value == SimplexLinkMode.BROWSER) {
- SectionTextFooter(stringResource(MR.strings.simplex_link_mode_browser_warning))
- }
SectionDividerSpaced()
val currentUser = chatModel.currentUser.value
@@ -185,8 +182,10 @@ fun PrivacySettingsView(
@Composable
private fun SimpleXLinkOptions(simplexLinkModeState: State, onSelected: (SimplexLinkMode) -> Unit) {
+ val modeValues = listOf(SimplexLinkMode.DESCRIPTION, SimplexLinkMode.FULL)
+ val pickerValues = modeValues + if (modeValues.contains(simplexLinkModeState.value)) emptyList() else listOf(simplexLinkModeState.value)
val values = remember {
- SimplexLinkMode.values().map {
+ pickerValues.map {
when (it) {
SimplexLinkMode.DESCRIPTION -> it to generalGetString(MR.strings.simplex_link_mode_description)
SimplexLinkMode.FULL -> it to generalGetString(MR.strings.simplex_link_mode_full)
From d8d47d706d50fa3f18d1b5ac8a736deb63b05b10 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Tue, 17 Oct 2023 12:56:12 +0400
Subject: [PATCH 38/80] ios: connection plan improvements; remove browser mode
for simplex links (#3237)
---
apps/ios/Shared/ContentView.swift | 5 +-
apps/ios/Shared/Views/Chat/ChatInfoView.swift | 4 +-
.../Views/Chat/ChatItem/MsgContentView.swift | 6 +-
.../Views/Chat/Group/GroupLinkView.swift | 4 +-
.../Chat/Group/GroupMemberInfoView.swift | 4 +-
.../ChatList/ContactConnectionInfo.swift | 2 +-
.../Shared/Views/NewChat/AddContactView.swift | 4 +-
.../Shared/Views/NewChat/NewChatButton.swift | 56 ++++++++++++++++---
apps/ios/Shared/Views/NewChat/QRCode.swift | 16 ++++++
.../Onboarding/CreateSimpleXAddress.swift | 4 +-
.../Views/UserSettings/PrivacySettings.swift | 8 +--
.../Views/UserSettings/SettingsView.swift | 2 +-
.../Views/UserSettings/UserAddressView.swift | 4 +-
apps/ios/SimpleXChat/APITypes.swift | 6 +-
apps/ios/SimpleXChat/ChatTypes.swift | 2 +-
15 files changed, 88 insertions(+), 39 deletions(-)
diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift
index 04eadac2c1..b69ccbb7c7 100644
--- a/apps/ios/Shared/ContentView.swift
+++ b/apps/ios/Shared/ContentView.swift
@@ -292,10 +292,7 @@ struct ContentView: View {
var path = url.path
if (path == "/contact" || path == "/invitation") {
path.removeFirst()
- // TODO normalize in backend; revert
- // let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
- var link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
- link = link.starts(with: "simplex:/") ? link.replacingOccurrences(of: "simplex:/", with: "https://simplex.chat/") : link
+ let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
planAndConnect(
link,
showAlert: showPlanAndConnectAlert,
diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift
index 81412bf310..5438eb13bc 100644
--- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift
@@ -168,9 +168,9 @@ struct ChatInfoView: View {
if let contactLink = contact.contactLink {
Section {
- QRCode(uri: contactLink)
+ SimpleXLinkQRCode(uri: contactLink)
Button {
- showShareSheet(items: [contactLink])
+ showShareSheet(items: [simplexChatLink(contactLink)])
} label: {
Label("Share address", systemImage: "square.and.arrow.up")
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift
index 498b3cb2e0..8b757ed1a3 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift
@@ -121,13 +121,11 @@ private func formatText(_ ft: FormattedText, _ preview: Bool) -> Text {
case .secret: return Text(t).foregroundColor(.clear).underline(color: .primary)
case let .colored(color): return Text(t).foregroundColor(color.uiColor)
case .uri: return linkText(t, t, preview, prefix: "")
- case let .simplexLink(linkType, simplexUri, trustedUri, smpHosts):
+ case let .simplexLink(linkType, simplexUri, smpHosts):
switch privacySimplexLinkModeDefault.get() {
case .description: return linkText(simplexLinkText(linkType, smpHosts), simplexUri, preview, prefix: "")
case .full: return linkText(t, simplexUri, preview, prefix: "")
- case .browser: return trustedUri
- ? linkText(t, t, preview, prefix: "")
- : linkText(t, t, preview, prefix: "", color: .red, uiColor: .red)
+ case .browser: return linkText(t, simplexUri, preview, prefix: "")
}
case .email: return linkText(t, t, preview, prefix: "mailto:")
case .phone: return linkText(t, t.replacingOccurrences(of: " ", with: ""), preview, prefix: "tel:")
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift
index 3731e0c4d7..781870bf5e 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift
@@ -41,9 +41,9 @@ struct GroupLinkView: View {
}
}
.frame(height: 36)
- QRCode(uri: groupLink)
+ SimpleXLinkQRCode(uri: groupLink)
Button {
- showShareSheet(items: [groupLink])
+ showShareSheet(items: [simplexChatLink(groupLink)])
} label: {
Label("Share link", systemImage: "square.and.arrow.up")
}
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
index 3d10101dee..4b64458145 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
@@ -94,9 +94,9 @@ struct GroupMemberInfoView: View {
if let contactLink = member.contactLink {
Section {
- QRCode(uri: contactLink)
+ SimpleXLinkQRCode(uri: contactLink)
Button {
- showShareSheet(items: [contactLink])
+ showShareSheet(items: [simplexChatLink(contactLink)])
} label: {
Label("Share address", systemImage: "square.and.arrow.up")
}
diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift
index 3e42d2f207..7c973c73c6 100644
--- a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift
+++ b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift
@@ -61,7 +61,7 @@ struct ContactConnectionInfo: View {
if contactConnection.initiated,
let connReqInv = contactConnection.connReqInv {
- QRCode(uri: connReqInv)
+ SimpleXLinkQRCode(uri: simplexChatLink(connReqInv))
incognitoEnabled()
shareLinkButton(connReqInv)
oneTimeLinkLearnMoreButton()
diff --git a/apps/ios/Shared/Views/NewChat/AddContactView.swift b/apps/ios/Shared/Views/NewChat/AddContactView.swift
index 31b6b64f32..344a8d1f96 100644
--- a/apps/ios/Shared/Views/NewChat/AddContactView.swift
+++ b/apps/ios/Shared/Views/NewChat/AddContactView.swift
@@ -21,7 +21,7 @@ struct AddContactView: View {
List {
Section {
if connReqInvitation != "" {
- QRCode(uri: connReqInvitation)
+ SimpleXLinkQRCode(uri: connReqInvitation)
} else {
ProgressView()
.progressViewStyle(.circular)
@@ -99,7 +99,7 @@ func sharedProfileInfo(_ incognito: Bool) -> Text {
func shareLinkButton(_ connReqInvitation: String) -> some View {
Button {
- showShareSheet(items: [connReqInvitation])
+ showShareSheet(items: [simplexChatLink(connReqInvitation)])
} label: {
settingsRow("square.and.arrow.up") {
Text("Share 1-time link")
diff --git a/apps/ios/Shared/Views/NewChat/NewChatButton.swift b/apps/ios/Shared/Views/NewChat/NewChatButton.swift
index e4fbd8bd47..8d095e907b 100644
--- a/apps/ios/Shared/Views/NewChat/NewChatButton.swift
+++ b/apps/ios/Shared/Views/NewChat/NewChatButton.swift
@@ -62,7 +62,9 @@ enum PlanAndConnectAlert: Identifiable {
case ownInvitationLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
case invitationLinkConnecting(connectionLink: String)
case ownContactAddressConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
+ case contactAddressConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
case groupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
+ case groupLinkConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
case groupLinkConnecting(connectionLink: String, groupInfo: GroupInfo?)
var id: String {
@@ -70,7 +72,9 @@ enum PlanAndConnectAlert: Identifiable {
case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink)"
case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink)"
case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink)"
+ case let .contactAddressConnectingConfirmReconnect(connectionLink, _, _): return "contactAddressConnectingConfirmReconnect \(connectionLink)"
case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink)"
+ case let .groupLinkConnectingConfirmReconnect(connectionLink, _, _): return "groupLinkConnectingConfirmReconnect \(connectionLink)"
case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink)"
}
}
@@ -103,6 +107,16 @@ func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool) -> Alert {
),
secondaryButton: .cancel()
)
+ case let .contactAddressConnectingConfirmReconnect(connectionLink, connectionPlan, incognito):
+ return Alert(
+ title: Text("Repeat connection request?"),
+ message: Text("You have already requested connection via this address!"),
+ primaryButton: .destructive(
+ Text(incognito ? "Connect incognito" : "Connect"),
+ action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
+ ),
+ secondaryButton: .cancel()
+ )
case let .groupLinkConfirmConnect(connectionLink, connectionPlan, incognito):
return Alert(
title: Text("Join group?"),
@@ -113,6 +127,16 @@ func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool) -> Alert {
),
secondaryButton: .cancel()
)
+ case let .groupLinkConnectingConfirmReconnect(connectionLink, connectionPlan, incognito):
+ return Alert(
+ title: Text("Repeat join request?"),
+ message: Text("You are already joining the group via this link!"),
+ primaryButton: .destructive(
+ Text(incognito ? "Join incognito" : "Join"),
+ action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
+ ),
+ secondaryButton: .cancel()
+ )
case let .groupLinkConnecting(_, groupInfo):
if let groupInfo = groupInfo {
return Alert(
@@ -130,13 +154,13 @@ func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool) -> Alert {
enum PlanAndConnectActionSheet: Identifiable {
case askCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan?, title: LocalizedStringKey)
- case ownLinkAskCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan, title: LocalizedStringKey)
+ case askCurrentOrIncognitoProfileDestructive(connectionLink: String, connectionPlan: ConnectionPlan, title: LocalizedStringKey)
case ownGroupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo)
var id: String {
switch self {
case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink)"
- case let .ownLinkAskCurrentOrIncognitoProfile(connectionLink, _, _): return "ownLinkAskCurrentOrIncognitoProfile \(connectionLink)"
+ case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink)"
case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink)"
}
}
@@ -153,7 +177,7 @@ func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool
.cancel()
]
)
- case let .ownLinkAskCurrentOrIncognitoProfile(connectionLink, connectionPlan, title):
+ case let .askCurrentOrIncognitoProfileDestructive(connectionLink, connectionPlan, title):
return ActionSheet(
title: Text(title),
buttons: [
@@ -211,7 +235,7 @@ func planAndConnect(
if let incognito = incognito {
showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else {
- showActionSheet(.ownLinkAskCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!"))
+ showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!"))
}
case let .connecting(contact_):
logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")")
@@ -238,10 +262,17 @@ func planAndConnect(
if let incognito = incognito {
showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else {
- showActionSheet(.ownLinkAskCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!"))
+ showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!"))
}
- case let .connecting(contact):
- logger.debug("planAndConnect, .contactAddress, .connecting, incognito=\(incognito?.description ?? "nil")")
+ case .connectingConfirmReconnect:
+ logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")")
+ if let incognito = incognito {
+ showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
+ } else {
+ showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?"))
+ }
+ case let .connectingProhibit(contact):
+ logger.debug("planAndConnect, .contactAddress, .connectingProhibit, incognito=\(incognito?.description ?? "nil")")
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
case let .known(contact):
logger.debug("planAndConnect, .contactAddress, .known, incognito=\(incognito?.description ?? "nil")")
@@ -258,8 +289,15 @@ func planAndConnect(
case let .ownLink(groupInfo):
logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo))
- case let .connecting(groupInfo_):
- logger.debug("planAndConnect, .groupLink, .connecting, incognito=\(incognito?.description ?? "nil")")
+ case .connectingConfirmReconnect:
+ logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")")
+ if let incognito = incognito {
+ showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
+ } else {
+ showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?"))
+ }
+ case let .connectingProhibit(groupInfo_):
+ logger.debug("planAndConnect, .groupLink, .connectingProhibit, incognito=\(incognito?.description ?? "nil")")
showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_))
case let .known(groupInfo):
logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")")
diff --git a/apps/ios/Shared/Views/NewChat/QRCode.swift b/apps/ios/Shared/Views/NewChat/QRCode.swift
index 0785826fa9..4bec3f8e66 100644
--- a/apps/ios/Shared/Views/NewChat/QRCode.swift
+++ b/apps/ios/Shared/Views/NewChat/QRCode.swift
@@ -28,6 +28,22 @@ struct MutableQRCode: View {
}
}
+struct SimpleXLinkQRCode: View {
+ let uri: String
+ var withLogo: Bool = true
+ var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1)
+
+ var body: some View {
+ QRCode(uri: simplexChatLink(uri), withLogo: withLogo, tintColor: tintColor)
+ }
+}
+
+func simplexChatLink(_ uri: String) -> String {
+ uri.starts(with: "simplex:/")
+ ? uri.replacingOccurrences(of: "simplex:/", with: "https://simplex.chat/")
+ : uri
+}
+
struct QRCode: View {
let uri: String
var withLogo: Bool = true
diff --git a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift
index 049c4bee08..249180686c 100644
--- a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift
+++ b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift
@@ -31,7 +31,7 @@ struct CreateSimpleXAddress: View {
Spacer()
if let userAddress = m.userAddress {
- QRCode(uri: userAddress.connReqContact)
+ SimpleXLinkQRCode(uri: userAddress.connReqContact)
.frame(maxHeight: g.size.width)
shareQRCodeButton(userAddress)
.frame(maxWidth: .infinity)
@@ -126,7 +126,7 @@ struct CreateSimpleXAddress: View {
private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View {
Button {
- showShareSheet(items: [userAddress.connReqContact])
+ showShareSheet(items: [simplexChatLink(userAddress.connReqContact)])
} label: {
Label("Share", systemImage: "square.and.arrow.up")
}
diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
index 16555fad3a..71ff7b88bf 100644
--- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
+++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
@@ -93,7 +93,9 @@ struct PrivacySettings: View {
}
settingsRow("link") {
Picker("SimpleX links", selection: $simplexLinkMode) {
- ForEach(SimpleXLinkMode.values) { mode in
+ ForEach(
+ SimpleXLinkMode.values + (SimpleXLinkMode.values.contains(simplexLinkMode) ? [] : [simplexLinkMode])
+ ) { mode in
Text(mode.text)
}
}
@@ -104,10 +106,6 @@ struct PrivacySettings: View {
}
} header: {
Text("Chats")
- } footer: {
- if case .browser = simplexLinkMode {
- Text("Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.")
- }
}
Section {
diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
index f076a4eb7d..1cc859f49e 100644
--- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift
+++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
@@ -93,7 +93,7 @@ enum SimpleXLinkMode: String, Identifiable {
case full
case browser
- static var values: [SimpleXLinkMode] = [.description, .full, .browser]
+ static var values: [SimpleXLinkMode] = [.description, .full]
public var id: Self { self }
diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift
index 86bf0048b8..60e5cc7b04 100644
--- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift
+++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift
@@ -190,7 +190,7 @@ struct UserAddressView: View {
@ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View {
Section {
- MutableQRCode(uri: Binding.constant(userAddress.connReqContact))
+ MutableQRCode(uri: Binding.constant(simplexChatLink(userAddress.connReqContact)))
shareQRCodeButton(userAddress)
if MFMailComposeViewController.canSendMail() {
shareViaEmailButton(userAddress)
@@ -248,7 +248,7 @@ struct UserAddressView: View {
private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View {
Button {
- showShareSheet(items: [userAddress.connReqContact])
+ showShareSheet(items: [simplexChatLink(userAddress.connReqContact)])
} label: {
settingsRow("square.and.arrow.up") {
Text("Share address")
diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift
index ddab4c73bd..53da91b042 100644
--- a/apps/ios/SimpleXChat/APITypes.swift
+++ b/apps/ios/SimpleXChat/APITypes.swift
@@ -886,14 +886,16 @@ public enum InvitationLinkPlan: Decodable {
public enum ContactAddressPlan: Decodable {
case ok
case ownLink
- case connecting(contact: Contact)
+ case connectingConfirmReconnect
+ case connectingProhibit(contact: Contact)
case known(contact: Contact)
}
public enum GroupLinkPlan: Decodable {
case ok
case ownLink(groupInfo: GroupInfo)
- case connecting(groupInfo_: GroupInfo?)
+ case connectingConfirmReconnect
+ case connectingProhibit(groupInfo_: GroupInfo?)
case known(groupInfo: GroupInfo)
}
diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift
index caaccb5454..c31786c700 100644
--- a/apps/ios/SimpleXChat/ChatTypes.swift
+++ b/apps/ios/SimpleXChat/ChatTypes.swift
@@ -3068,7 +3068,7 @@ public enum Format: Decodable, Equatable {
case secret
case colored(color: FormatColor)
case uri
- case simplexLink(linkType: SimplexLinkType, simplexUri: String, trustedUri: Bool, smpHosts: [String])
+ case simplexLink(linkType: SimplexLinkType, simplexUri: String, smpHosts: [String])
case email
case phone
}
From 29c8ab7c9b964a6a227560ec4a928ed3c6e15fa4 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Tue, 17 Oct 2023 18:05:16 +0100
Subject: [PATCH 39/80] ui: file & media preference for groups (#3243)
---
.../Views/Chat/Group/GroupPreferencesView.swift | 3 +--
.../common/views/chat/group/GroupPreferences.kt | 11 +++++------
2 files changed, 6 insertions(+), 8 deletions(-)
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift
index af1a778ad1..860a6febb0 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift
@@ -27,8 +27,7 @@ struct GroupPreferencesView: View {
featureSection(.directMessages, $preferences.directMessages.enable)
featureSection(.reactions, $preferences.reactions.enable)
featureSection(.voice, $preferences.voice.enable)
-// TODO uncomment in 5.3
-// featureSection(.files, $preferences.files.enable)
+ featureSection(.files, $preferences.files.enable)
if groupInfo.canEdit {
Section {
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt
index d95d2a4cff..4571a38c13 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt
@@ -103,12 +103,11 @@ private fun GroupPreferencesLayout(
FeatureSection(GroupFeature.Voice, allowVoice, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(voice = GroupPreference(enable = it)))
}
-// TODO uncomment in 5.3
-// SectionDividerSpaced(true, maxBottomPadding = false)
-// val allowFiles = remember(preferences) { mutableStateOf(preferences.files.enable) }
-// FeatureSection(GroupFeature.Files, allowFiles, groupInfo, preferences, onTTLUpdated) {
-// applyPrefs(preferences.copy(files = GroupPreference(enable = it)))
-// }
+ SectionDividerSpaced(true, maxBottomPadding = false)
+ val allowFiles = remember(preferences) { mutableStateOf(preferences.files.enable) }
+ FeatureSection(GroupFeature.Files, allowFiles, groupInfo, preferences, onTTLUpdated) {
+ applyPrefs(preferences.copy(files = GroupPreference(enable = it)))
+ }
if (groupInfo.canEdit) {
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
ResetSaveButtons(
From a02886ca5d46a444d7edd4645a415b2c16b8dd9d Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Wed, 18 Oct 2023 10:19:24 +0100
Subject: [PATCH 40/80] core: fix editing and status changes removing reactions
from view (#3245)
* core: fix editing and status changes removing reactions from view
* refactor
* refactor 2
* case
---
src/Simplex/Chat.hs | 19 ++++---
src/Simplex/Chat/Store/Messages.hs | 80 ++++++++++++++++--------------
2 files changed, 55 insertions(+), 44 deletions(-)
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 89f64189af..82499baee6 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -760,8 +760,9 @@ processChatCommand = \case
unzipMaybe3 _ = (Nothing, Nothing, Nothing)
APIUpdateChatItem (ChatRef cType chatId) itemId live mc -> withUser $ \user -> withChatLock "updateChatItem" $ case cType of
CTDirect -> do
- (ct@Contact {contactId}, cci) <- withStore $ \db -> (,) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId
+ ct@Contact {contactId} <- withStore $ \db -> getContact db user chatId
assertDirectAllowed user MDSnd ct XMsgUpdate_
+ cci <- withStore $ \db -> getDirectCIWithReactions db user ct itemId
case cci of
CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do
case (ciContent, itemSharedMsgId, editable) of
@@ -783,7 +784,7 @@ processChatCommand = \case
CTGroup -> do
Group gInfo@GroupInfo {groupId} ms <- withStore $ \db -> getGroup db user chatId
assertUserGroupRole gInfo GRAuthor
- cci <- withStore $ \db -> getGroupChatItem db user chatId itemId
+ cci <- withStore $ \db -> getGroupCIWithReactions db user gInfo itemId
case cci of
CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do
case (ciContent, itemSharedMsgId, editable) of
@@ -2390,8 +2391,8 @@ updateCallItemStatus user ct Call {chatItemId} receivedStatus msgId_ = do
forM_ aciContent_ $ \aciContent -> updateDirectChatItemView user ct chatItemId aciContent False msgId_
updateDirectChatItemView :: ChatMonad m => User -> Contact -> ChatItemId -> ACIContent -> Bool -> Maybe MessageId -> m ()
-updateDirectChatItemView user ct@Contact {contactId} chatItemId (ACIContent msgDir ciContent) live msgId_ = do
- ci' <- withStore $ \db -> updateDirectChatItem db user contactId chatItemId ciContent live msgId_
+updateDirectChatItemView user ct chatItemId (ACIContent msgDir ciContent) live msgId_ = do
+ ci' <- withStore $ \db -> updateDirectChatItem db user ct chatItemId ciContent live msgId_
toView $ CRChatItemUpdated user (AChatItem SCTDirect msgDir (DirectChat ct) ci')
callStatusItemContent :: ChatMonad m => User -> Contact -> ChatItemId -> WebRTCCallStatus -> m (Maybe ACIContent)
@@ -3996,7 +3997,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
ci' <- withStore' $ \db -> do
when changed $
addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc)
- updateDirectChatItem' db user contactId ci content live $ Just msgId
+ reactions <- getDirectCIReactions db ct sharedMsgId
+ updateDirectChatItem' db user contactId ci {reactions} content live $ Just msgId
toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci')
startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci'
else toView $ CRChatItemNotChanged user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci)
@@ -4134,7 +4136,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
ci' <- withStore' $ \db -> do
when changed $
addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc)
- updateGroupChatItem db user groupId ci content live $ Just msgId
+ reactions <- getGroupCIReactions db gInfo memberId sharedMsgId
+ updateGroupChatItem db user groupId ci {reactions} content live $ Just msgId
toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci')
startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci'
else toView $ CRChatItemNotChanged user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci)
@@ -4939,7 +4942,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemId, itemStatus}})
| itemStatus == newStatus -> pure ()
| otherwise -> do
- chatItem <- withStore $ \db -> updateDirectChatItemStatus db user contactId itemId newStatus
+ chatItem <- withStore $ \db -> updateDirectChatItemStatus db user ct itemId newStatus
toView $ CRChatItemStatusUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem)
_ -> pure ()
@@ -4962,7 +4965,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
memStatusCounts <- withStore' (`getGroupSndStatusCounts` itemId)
let newStatus = membersGroupItemStatus memStatusCounts
when (newStatus /= itemStatus) $ do
- chatItem <- withStore $ \db -> updateGroupChatItemStatus db user groupId itemId newStatus
+ chatItem <- withStore $ \db -> updateGroupChatItemStatus db user gInfo itemId newStatus
toView $ CRChatItemStatusUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) chatItem)
_ -> pure ()
diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs
index 0f9abaa465..35a8bad698 100644
--- a/src/Simplex/Chat/Store/Messages.hs
+++ b/src/Simplex/Chat/Store/Messages.hs
@@ -15,7 +15,6 @@
module Simplex.Chat.Store.Messages
( getContactConnIds_,
- getDirectChatReactions_,
-- * Message and chat item functions
deleteContactCIs,
@@ -68,9 +67,11 @@ module Simplex.Chat.Store.Messages
setGroupReaction,
getChatItemIdByAgentMsgId,
getDirectChatItem,
+ getDirectCIWithReactions,
getDirectChatItemBySharedMsgId,
getDirectChatItemByAgentMsgId,
getGroupChatItem,
+ getGroupCIWithReactions,
getGroupChatItemBySharedMsgId,
getGroupMemberCIBySharedMsgId,
getGroupChatItemByAgentMsgId,
@@ -755,7 +756,7 @@ getGroupChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String
getGroupChat db user groupId pagination search_ = do
let search = fromMaybe "" search_
g <- getGroupInfo db user groupId
- liftIO . getGroupChatReactions_ db g =<< case pagination of
+ case pagination of
CPLast count -> getGroupChatLast_ db user g count search
CPAfter afterId count -> getGroupChatAfter_ db user g afterId count search
CPBefore beforeId count -> getGroupChatBefore_ db user g beforeId count search
@@ -764,7 +765,7 @@ getGroupChatLast_ :: DB.Connection -> User -> GroupInfo -> Int -> String -> Exce
getGroupChatLast_ db user@User {userId} g@GroupInfo {groupId} count search = do
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
chatItemIds <- liftIO getGroupChatItemIdsLast_
- chatItems <- mapM (getGroupChatItem db user groupId) chatItemIds
+ chatItems <- mapM (getGroupCIWithReactions db user g) chatItemIds
pure $ Chat (GroupChat g) (reverse chatItems) stats
where
getGroupChatItemIdsLast_ :: IO [ChatItemId]
@@ -802,7 +803,7 @@ getGroupChatAfter_ db user@User {userId} g@GroupInfo {groupId} afterChatItemId c
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
afterChatItem <- getGroupChatItem db user groupId afterChatItemId
chatItemIds <- liftIO $ getGroupChatItemIdsAfter_ (chatItemTs afterChatItem)
- chatItems <- mapM (getGroupChatItem db user groupId) chatItemIds
+ chatItems <- mapM (getGroupCIWithReactions db user g) chatItemIds
pure $ Chat (GroupChat g) chatItems stats
where
getGroupChatItemIdsAfter_ :: UTCTime -> IO [ChatItemId]
@@ -825,7 +826,7 @@ getGroupChatBefore_ db user@User {userId} g@GroupInfo {groupId} beforeChatItemId
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
beforeChatItem <- getGroupChatItem db user groupId beforeChatItemId
chatItemIds <- liftIO $ getGroupChatItemIdsBefore_ (chatItemTs beforeChatItem)
- chatItems <- mapM (getGroupChatItem db user groupId) chatItemIds
+ chatItems <- mapM (getGroupCIWithReactions db user g) chatItemIds
pure $ Chat (GroupChat g) (reverse chatItems) stats
where
getGroupChatItemIdsBefore_ :: UTCTime -> IO [ChatItemId]
@@ -1149,23 +1150,24 @@ getChatItemIdByAgentMsgId db connId msgId =
|]
(connId, msgId)
-updateDirectChatItemStatus :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItemId -> CIStatus d -> ExceptT StoreError IO (ChatItem 'CTDirect d)
-updateDirectChatItemStatus db user@User {userId} contactId itemId itemStatus = do
- ci <- liftEither . correctDir =<< getDirectChatItem db user contactId itemId
+updateDirectChatItemStatus :: forall d. MsgDirectionI d => DB.Connection -> User -> Contact -> ChatItemId -> CIStatus d -> ExceptT StoreError IO (ChatItem 'CTDirect d)
+updateDirectChatItemStatus db user@User {userId} ct@Contact {contactId} itemId itemStatus = do
+ ci <- liftEither . correctDir =<< getDirectCIWithReactions db user ct itemId
currentTs <- liftIO getCurrentTime
liftIO $ DB.execute db "UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?" (itemStatus, currentTs, userId, contactId, itemId)
pure ci {meta = (meta ci) {itemStatus}}
- where
- correctDir :: CChatItem c -> Either StoreError (ChatItem c d)
- correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci
-updateDirectChatItem :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItemId -> CIContent d -> Bool -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d)
-updateDirectChatItem db user contactId itemId newContent live msgId_ = do
- ci <- liftEither . correctDir =<< getDirectChatItem db user contactId itemId
+updateDirectChatItem :: MsgDirectionI d => DB.Connection -> User -> Contact -> ChatItemId -> CIContent d -> Bool -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d)
+updateDirectChatItem db user ct@Contact {contactId} itemId newContent live msgId_ = do
+ ci <- liftEither . correctDir =<< getDirectCIWithReactions db user ct itemId
liftIO $ updateDirectChatItem' db user contactId ci newContent live msgId_
- where
- correctDir :: CChatItem c -> Either StoreError (ChatItem c d)
- correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci
+
+getDirectCIWithReactions :: DB.Connection -> User -> Contact -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTDirect)
+getDirectCIWithReactions db user ct@Contact {contactId} itemId =
+ liftIO . directCIWithReactions db ct =<< getDirectChatItem db user contactId itemId
+
+correctDir :: MsgDirectionI d => CChatItem c -> Either StoreError (ChatItem c d)
+correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci
updateDirectChatItem' :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTDirect d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTDirect d)
updateDirectChatItem' db User {userId} contactId ci newContent live msgId_ = do
@@ -1303,7 +1305,7 @@ getDirectChatItemIdBySharedMsgId_ db userId contactId sharedMsgId =
getDirectChatItem :: DB.Connection -> User -> Int64 -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTDirect)
getDirectChatItem db User {userId} contactId itemId = ExceptT $ do
currentTs <- getCurrentTime
- join <$> firstRow (toDirectChatItem currentTs) (SEChatItemNotFound itemId) getItem
+ firstRow' (toDirectChatItem currentTs) (SEChatItemNotFound itemId) getItem
where
getItem =
DB.query
@@ -1351,17 +1353,26 @@ getDirectChatItemIdByText' db User {userId} contactId msg =
|]
(userId, contactId, msg <> "%")
-updateGroupChatItemStatus :: forall d. MsgDirectionI d => DB.Connection -> User -> GroupId -> ChatItemId -> CIStatus d -> ExceptT StoreError IO (ChatItem 'CTGroup d)
-updateGroupChatItemStatus db user@User {userId} groupId itemId itemStatus = do
- ci <- liftEither . correctDir =<< getGroupChatItem db user groupId itemId
+updateGroupChatItemStatus :: MsgDirectionI d => DB.Connection -> User -> GroupInfo -> ChatItemId -> CIStatus d -> ExceptT StoreError IO (ChatItem 'CTGroup d)
+updateGroupChatItemStatus db user@User {userId} g@GroupInfo {groupId} itemId itemStatus = do
+ ci <- liftEither . correctDir =<< getGroupCIWithReactions db user g itemId
currentTs <- liftIO getCurrentTime
liftIO $ DB.execute db "UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND group_id = ? AND chat_item_id = ?" (itemStatus, currentTs, userId, groupId, itemId)
pure ci {meta = (meta ci) {itemStatus}}
- where
- correctDir :: CChatItem c -> Either StoreError (ChatItem c d)
- correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci
-updateGroupChatItem :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTGroup d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTGroup d)
+getGroupCIWithReactions :: DB.Connection -> User -> GroupInfo -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTGroup)
+getGroupCIWithReactions db user g@GroupInfo {groupId} itemId = do
+ liftIO . groupCIWithReactions db g =<< getGroupChatItem db user groupId itemId
+
+groupCIWithReactions :: DB.Connection -> GroupInfo -> CChatItem 'CTGroup -> IO (CChatItem 'CTGroup)
+groupCIWithReactions db g cci@(CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId}}) = case itemSharedMsgId of
+ Just sharedMsgId -> do
+ let GroupMember {memberId} = chatItemMember g ci
+ reactions <- getGroupCIReactions db g memberId sharedMsgId
+ pure $ CChatItem md ci {reactions}
+ Nothing -> pure cci
+
+updateGroupChatItem :: MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTGroup d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTGroup d)
updateGroupChatItem db user groupId ci newContent live msgId_ = do
currentTs <- liftIO getCurrentTime
let ci' = updatedChatItem ci newContent live currentTs
@@ -1370,7 +1381,7 @@ updateGroupChatItem db user groupId ci newContent live msgId_ = do
-- this function assumes that the group item with correct chat direction already exists,
-- it should be checked before calling it
-updateGroupChatItem_ :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTGroup d -> Maybe MessageId -> IO ()
+updateGroupChatItem_ :: MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTGroup d -> Maybe MessageId -> IO ()
updateGroupChatItem_ db User {userId} groupId ChatItem {content, meta} msgId_ = do
let CIMeta {itemId, itemText, itemStatus, itemDeleted, itemEdited, itemTimed, itemLive, updatedAt} = meta
itemDeleted' = isJust itemDeleted
@@ -1501,7 +1512,7 @@ getGroupChatItemByAgentMsgId db user groupId connId msgId = do
getGroupChatItem :: DB.Connection -> User -> Int64 -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTGroup)
getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
currentTs <- getCurrentTime
- join <$> firstRow (toGroupChatItem currentTs userContactId) (SEChatItemNotFound itemId) getItem
+ firstRow' (toGroupChatItem currentTs userContactId) (SEChatItemNotFound itemId) getItem
where
getItem =
DB.query
@@ -1671,18 +1682,15 @@ getChatItemVersions db itemId = do
getDirectChatReactions_ :: DB.Connection -> Contact -> Chat 'CTDirect -> IO (Chat 'CTDirect)
getDirectChatReactions_ db ct c@Chat {chatItems} = do
- chatItems' <- forM chatItems $ \(CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId}}) -> do
- reactions <- maybe (pure []) (getDirectCIReactions db ct) itemSharedMsgId
- pure $ CChatItem md ci {reactions}
+ chatItems' <- mapM (directCIWithReactions db ct) chatItems
pure c {chatItems = chatItems'}
-getGroupChatReactions_ :: DB.Connection -> GroupInfo -> Chat 'CTGroup -> IO (Chat 'CTGroup)
-getGroupChatReactions_ db g c@Chat {chatItems} = do
- chatItems' <- forM chatItems $ \(CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId}}) -> do
- let GroupMember {memberId} = chatItemMember g ci
- reactions <- maybe (pure []) (getGroupCIReactions db g memberId) itemSharedMsgId
+directCIWithReactions :: DB.Connection -> Contact -> CChatItem 'CTDirect -> IO (CChatItem 'CTDirect)
+directCIWithReactions db ct cci@(CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId}}) = case itemSharedMsgId of
+ Just sharedMsgId -> do
+ reactions <- getDirectCIReactions db ct sharedMsgId
pure $ CChatItem md ci {reactions}
- pure c {chatItems = chatItems'}
+ Nothing -> pure cci
getDirectCIReactions :: DB.Connection -> Contact -> SharedMsgId -> IO [CIReactionCount]
getDirectCIReactions db Contact {contactId} itemSharedMsgId =
From 706d6bf65b8affea286d5727a34f0500647edc21 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Wed, 18 Oct 2023 11:23:35 +0100
Subject: [PATCH 41/80] core, ui: prevent old sent items re-added to chat, and
"new" status overriding "sent" (#3246)
* core, ui: prevent old sent items re-added to chat, and "new" status overriding "sent"
* clear item statuses when changing current chat
* remove iOS hack
* remote state/published from chatItemStatuses
---
apps/ios/Shared/Model/ChatModel.swift | 18 ++++++++++--------
apps/ios/Shared/Model/SimpleXAPI.swift | 8 +++-----
apps/ios/Shared/Views/Chat/ChatView.swift | 1 +
.../chat/simplex/common/model/ChatModel.kt | 14 ++++++++++++--
.../chat/simplex/common/model/SimpleXAPI.kt | 7 ++-----
.../views/chat/group/GroupMemberInfoView.kt | 1 +
.../views/chatlist/ChatListNavLinkView.kt | 1 +
.../common/views/newchat/AddGroupView.kt | 1 +
src/Simplex/Chat.hs | 8 +++-----
9 files changed, 34 insertions(+), 25 deletions(-)
diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift
index b1a83dd5e6..3cc52d502d 100644
--- a/apps/ios/Shared/Model/ChatModel.swift
+++ b/apps/ios/Shared/Model/ChatModel.swift
@@ -62,6 +62,7 @@ final class ChatModel: ObservableObject {
// current chat
@Published var chatId: String?
@Published var reversedChatItems: [ChatItem] = []
+ var chatItemStatuses: Dictionary = [:]
@Published var chatToTop: String?
@Published var groupMembers: [GroupMember] = []
// items in the terminal view
@@ -306,7 +307,11 @@ final class ChatModel: ObservableObject {
return false
} else {
withAnimation(itemAnimation()) {
- reversedChatItems.insert(cItem, at: hasLiveDummy ? 1 : 0)
+ var ci = cItem
+ if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
+ ci.meta.itemStatus = status
+ }
+ reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
}
return true
}
@@ -319,23 +324,19 @@ final class ChatModel: ObservableObject {
}
}
- func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
+ func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem, status: CIStatus? = nil) {
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
withAnimation {
_updateChatItem(at: i, with: cItem)
}
+ } else if let status = status {
+ chatItemStatuses.updateValue(status, forKey: cItem.id)
}
}
private func _updateChatItem(at i: Int, with cItem: ChatItem) {
- let ci = reversedChatItems[i]
reversedChatItems[i] = cItem
reversedChatItems[i].viewTimestamp = .now
- // on some occasions the confirmation of message being accepted by the server (tick)
- // arrives earlier than the response from API, and item remains without tick
- if case .sndNew = cItem.meta.itemStatus {
- reversedChatItems[i].meta.itemStatus = ci.meta.itemStatus
- }
}
private func getChatItemIndex(_ cItem: ChatItem) -> Int? {
@@ -474,6 +475,7 @@ final class ChatModel: ObservableObject {
}
// clear current chat
if chatId == cInfo.id {
+ chatItemStatuses = [:]
reversedChatItems = []
}
}
diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index 2e724bcd83..6fe1bef8ee 100644
--- a/apps/ios/Shared/Model/SimpleXAPI.swift
+++ b/apps/ios/Shared/Model/SimpleXAPI.swift
@@ -312,6 +312,7 @@ func loadChat(chat: Chat, search: String = "") {
do {
let cInfo = chat.chatInfo
let m = ChatModel.shared
+ m.chatItemStatuses = [:]
m.reversedChatItems = []
let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search)
m.updateChatInfo(chat.chatInfo)
@@ -1421,11 +1422,8 @@ func processReceivedMsg(_ res: ChatResponse) async {
case let .chatItemStatusUpdated(user, aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
- if !cItem.isDeletedContent {
- let added = active(user) ? await MainActor.run { m.upsertChatItem(cInfo, cItem) } : true
- if added && cItem.showNotification {
- NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
- }
+ if !cItem.isDeletedContent && active(user) {
+ await MainActor.run { m.updateChatItem(cInfo, cItem, status: cItem.meta.itemStatus) }
}
if let endTask = m.messageDelivery[cItem.id] {
switch cItem.meta.itemStatus {
diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift
index 81473709cb..21af0ebe17 100644
--- a/apps/ios/Shared/Views/Chat/ChatView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatView.swift
@@ -91,6 +91,7 @@ struct ChatView: View {
chatModel.chatId = nil
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
if chatModel.chatId == nil {
+ chatModel.chatItemStatuses = [:]
chatModel.reversedChatItems = []
}
}
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 f3655d0008..132384219e 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
@@ -53,6 +53,7 @@ object ChatModel {
// current chat
val chatId = mutableStateOf(null)
val chatItems = mutableStateListOf()
+ val chatItemStatuses = mutableMapOf()
val groupMembers = mutableStateListOf()
val terminalItems = mutableStateListOf()
@@ -272,7 +273,13 @@ object ChatModel {
Log.d(TAG, "TODOCHAT: upsertChatItem: updated in chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
false
} else {
- chatItems.add(cItem)
+ val status = chatItemStatuses.remove(cItem.id)
+ val ci = if (status != null && cItem.meta.itemStatus is CIStatus.SndNew) {
+ cItem.copy(meta = cItem.meta.copy(itemStatus = status))
+ } else {
+ cItem
+ }
+ chatItems.add(ci)
Log.d(TAG, "TODOCHAT: upsertChatItem: added to chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
true
}
@@ -282,13 +289,15 @@ object ChatModel {
}
}
- suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem) {
+ suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) {
withContext(Dispatchers.Main) {
if (chatId.value == cInfo.id) {
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) {
chatItems[itemIndex] = cItem
}
+ } else if (status != null) {
+ chatItemStatuses[cItem.id] = status
}
}
}
@@ -326,6 +335,7 @@ object ChatModel {
}
// clear current chat
if (chatId.value == cInfo.id) {
+ chatItemStatuses.clear()
chatItems.clear()
}
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
index cc8d481b59..6bd269f24d 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
@@ -1489,11 +1489,8 @@ object ChatController {
is CR.ChatItemStatusUpdated -> {
val cInfo = r.chatItem.chatInfo
val cItem = r.chatItem.chatItem
- if (!cItem.isDeletedContent) {
- val added = if (active(r.user)) chatModel.upsertChatItem(cInfo, cItem) else true
- if (added && cItem.showNotification) {
- ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
- }
+ if (!cItem.isDeletedContent && active(r.user)) {
+ chatModel.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
}
}
is CR.ChatItemUpdated ->
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt
index e14089ec52..c76fb73a6b 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt
@@ -73,6 +73,7 @@ fun GroupMemberInfoView(
chatModel.addChat(c)
}
chatModel.chatItems.clear()
+ chatModel.chatItemStatuses.clear()
chatModel.chatItems.addAll(c.chatItems)
chatModel.chatId.value = c.id
closeAll()
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 566c981811..b45543df4e 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
@@ -141,6 +141,7 @@ suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) {
suspend fun openChat(chat: Chat, chatModel: ChatModel) {
chatModel.chatItems.clear()
+ chatModel.chatItemStatuses.clear()
chatModel.chatItems.addAll(chat.chatItems)
chatModel.chatId.value = chat.chatInfo.id
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt
index 6ad919c27d..be446f6087 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt
@@ -41,6 +41,7 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
if (groupInfo != null) {
chatModel.addChat(Chat(chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf()))
chatModel.chatItems.clear()
+ chatModel.chatItemStatuses.clear()
chatModel.chatId.value = groupInfo.id
setGroupMembers(groupInfo, chatModel)
close.invoke()
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 82499baee6..7e2c2b71e7 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -584,11 +584,11 @@ processChatCommand = \case
timed_ <- sndContactCITimed live ct itemTTL
(msgContainer, quotedItem_) <- prepareMsg fInv_ timed_
(msg@SndMessage {sharedMsgId}, _) <- sendDirectContactMessage ct (XMsgNew msgContainer)
+ ci <- saveSndChatItem' user (CDDirectSnd ct) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live
case ft_ of
Just ft@FileTransferMeta {fileInline = Just IFMSent} ->
sendDirectFileInline ct ft sharedMsgId
_ -> pure ()
- ci <- saveSndChatItem' user (CDDirectSnd ct) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live
forM_ (timed_ >>= timedDeleteAt') $
startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci)
pure $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci)
@@ -649,11 +649,11 @@ processChatCommand = \case
timed_ <- sndGroupCITimed live gInfo itemTTL
(msgContainer, quotedItem_) <- prepareMsg fInv_ timed_ membership
(msg@SndMessage {sharedMsgId}, sentToMembers) <- sendGroupMessage user gInfo ms (XMsgNew msgContainer)
- mapM_ (sendGroupFileInline ms sharedMsgId) ft_
ci <- saveSndChatItem' user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live
withStore' $ \db ->
forM_ sentToMembers $ \GroupMember {groupMemberId} ->
createGroupSndStatus db (chatItemId' ci) groupMemberId CISSndNew
+ mapM_ (sendGroupFileInline ms sharedMsgId) ft_
forM_ (timed_ >>= timedDeleteAt') $
startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci)
pure $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci)
@@ -5208,9 +5208,7 @@ deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do
let msgFlags = MsgFlags {notification = hasNotification cmEventTag}
agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgFlags msgBody
let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId}
- withStoreCtx'
- (Just $ "createSndMsgDelivery, sndMsgDelivery: " <> show sndMsgDelivery <> ", msgId: " <> show msgId <> ", cmEventTag: " <> show cmEventTag <> ", msgDeliveryStatus: MDSSndAgent")
- $ \db -> createSndMsgDelivery db sndMsgDelivery msgId
+ withStore' $ \db -> createSndMsgDelivery db sndMsgDelivery msgId
sendGroupMessage :: (MsgEncodingI e, ChatMonad m) => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> m (SndMessage, [GroupMember])
sendGroupMessage user GroupInfo {groupId} members chatMsgEvent =
From 852e77b1d9ad07ea31f932d83386eb34469f7377 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Wed, 18 Oct 2023 16:58:33 +0400
Subject: [PATCH 42/80] android: connect plan (#3242)
---
apps/ios/Shared/Model/SimpleXAPI.swift | 1 -
.../Onboarding/CreateSimpleXAddress.swift | 2 +-
.../chat/simplex/common/model/ChatModel.kt | 1 +
.../chat/simplex/common/model/SimpleXAPI.kt | 49 +++
.../simplex/common/views/chat/ChatInfoView.kt | 6 +-
.../common/views/chat/group/GroupLinkView.kt | 10 +-
.../views/chat/group/GroupMemberInfoView.kt | 50 +--
.../views/chatlist/ChatListNavLinkView.kt | 7 +
.../common/views/chatlist/ChatListView.kt | 42 +-
.../common/views/newchat/AddContactView.kt | 12 +-
.../newchat/ContactConnectionInfoView.kt | 4 +-
.../common/views/newchat/PasteToConnect.kt | 23 +-
.../simplex/common/views/newchat/QRCode.kt | 23 +
.../common/views/newchat/ScanToConnectView.kt | 412 ++++++++++++++++--
.../views/onboarding/CreateSimpleXAddress.kt | 9 +-
.../views/usersettings/UserAddressView.kt | 8 +-
.../commonMain/resources/MR/base/strings.xml | 29 +-
17 files changed, 512 insertions(+), 176 deletions(-)
diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index 6fe1bef8ee..bad15ad52f 100644
--- a/apps/ios/Shared/Model/SimpleXAPI.swift
+++ b/apps/ios/Shared/Model/SimpleXAPI.swift
@@ -594,7 +594,6 @@ func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> P
}
func apiConnectPlan(connReq: String) async throws -> ConnectionPlan {
- logger.error("apiConnectPlan connReq: \(connReq)")
let userId = try currentUserId("apiConnectPlan")
let r = await chatSendCmd(.apiConnectPlan(userId: userId, connReq: connReq))
if case let .connectionPlan(_, connectionPlan) = r { return connectionPlan }
diff --git a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift
index 249180686c..935f09cc1b 100644
--- a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift
+++ b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift
@@ -194,7 +194,7 @@ struct SendAddressMailView: View {
let messageBody = String(format: NSLocalizedString("""
Hi!
Connect to me via SimpleX Chat
- """, comment: "email text"), userAddress.connReqContact)
+ """, comment: "email text"), simplexChatLink(userAddress.connReqContact))
MailView(
isShowing: self.$showMailView,
result: $mailViewResult,
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 132384219e..ad033387ab 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
@@ -136,6 +136,7 @@ object ChatModel {
fun hasChat(id: String): Boolean = chats.toList().firstOrNull { it.id == id } != null
fun getChat(id: String): Chat? = chats.toList().firstOrNull { it.id == id }
fun getContactChat(contactId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId }
+ fun getGroupChat(groupId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId }
private fun getChatIndex(id: String): Int = chats.toList().indexOfFirst { it.id == id }
fun addChat(chat: Chat) = chats.add(index = 0, chat)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
index 6bd269f24d..8397d2edb1 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
@@ -855,6 +855,14 @@ object ChatController {
return null
}
+ suspend fun apiConnectPlan(connReq: String): ConnectionPlan? {
+ val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null }
+ val r = sendCmd(CC.APIConnectPlan(userId, connReq))
+ if (r is CR.CRConnectionPlan) return r.connectionPlan
+ Log.e(TAG, "apiConnectPlan bad response: ${r.responseType} ${r.details}")
+ return null
+ }
+
suspend fun apiConnect(incognito: Boolean, connReq: String): Boolean {
val userId = chatModel.currentUser.value?.userId ?: run {
Log.e(TAG, "apiConnect: no current user")
@@ -1914,6 +1922,7 @@ sealed class CC {
class APIVerifyGroupMember(val groupId: Long, val groupMemberId: Long, val connectionCode: String?): CC()
class APIAddContact(val userId: Long, val incognito: Boolean): CC()
class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC()
+ class APIConnectPlan(val userId: Long, val connReq: String): CC()
class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC()
class ApiDeleteChat(val type: ChatType, val id: Long): CC()
class ApiClearChat(val type: ChatType, val id: Long): CC()
@@ -2023,6 +2032,7 @@ sealed class CC {
is APIVerifyGroupMember -> "/_verify code #$groupId $groupMemberId" + if (connectionCode != null) " $connectionCode" else ""
is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}"
is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}"
+ is APIConnectPlan -> "/_connect plan $userId $connReq"
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq"
is ApiDeleteChat -> "/_delete ${chatRef(type, id)}"
is ApiClearChat -> "/_clear chat ${chatRef(type, id)}"
@@ -2124,6 +2134,7 @@ sealed class CC {
is APIVerifyGroupMember -> "apiVerifyGroupMember"
is APIAddContact -> "apiAddContact"
is ApiSetConnectionIncognito -> "apiSetConnectionIncognito"
+ is APIConnectPlan -> "apiConnectPlan"
is APIConnect -> "apiConnect"
is ApiDeleteChat -> "apiDeleteChat"
is ApiClearChat -> "apiClearChat"
@@ -3337,6 +3348,7 @@ sealed class CR {
@Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: UserRef, val verified: Boolean, val expectedCode: String): CR()
@Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR()
@Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR()
+ @Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connectionPlan: ConnectionPlan): CR()
@Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef): CR()
@Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef): CR()
@Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR()
@@ -3472,6 +3484,7 @@ sealed class CR {
is ConnectionVerified -> "connectionVerified"
is Invitation -> "invitation"
is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated"
+ is CRConnectionPlan -> "connectionPlan"
is SentConfirmation -> "sentConfirmation"
is SentInvitation -> "sentInvitation"
is ContactAlreadyExists -> "contactAlreadyExists"
@@ -3602,6 +3615,7 @@ sealed class CR {
is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode")
is Invitation -> withUser(user, connReqInvitation)
is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection))
+ is CRConnectionPlan -> withUser(user, json.encodeToString(connectionPlan))
is SentConfirmation -> withUser(user, noDetails())
is SentInvitation -> withUser(user, noDetails())
is ContactAlreadyExists -> withUser(user, json.encodeToString(contact))
@@ -3715,6 +3729,39 @@ fun chatError(r: CR): ChatErrorType? {
)
}
+@Serializable
+sealed class ConnectionPlan {
+ @Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan()
+ @Serializable @SerialName("contactAddress") class ContactAddress(val contactAddressPlan: ContactAddressPlan): ConnectionPlan()
+ @Serializable @SerialName("groupLink") class GroupLink(val groupLinkPlan: GroupLinkPlan): ConnectionPlan()
+}
+
+@Serializable
+sealed class InvitationLinkPlan {
+ @Serializable @SerialName("ok") object Ok: InvitationLinkPlan()
+ @Serializable @SerialName("ownLink") object OwnLink: InvitationLinkPlan()
+ @Serializable @SerialName("connecting") class Connecting(val contact_: Contact? = null): InvitationLinkPlan()
+ @Serializable @SerialName("known") class Known(val contact: Contact): InvitationLinkPlan()
+}
+
+@Serializable
+sealed class ContactAddressPlan {
+ @Serializable @SerialName("ok") object Ok: ContactAddressPlan()
+ @Serializable @SerialName("ownLink") object OwnLink: ContactAddressPlan()
+ @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: ContactAddressPlan()
+ @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val contact: Contact): ContactAddressPlan()
+ @Serializable @SerialName("known") class Known(val contact: Contact): ContactAddressPlan()
+}
+
+@Serializable
+sealed class GroupLinkPlan {
+ @Serializable @SerialName("ok") object Ok: GroupLinkPlan()
+ @Serializable @SerialName("ownLink") class OwnLink(val groupInfo: GroupInfo): GroupLinkPlan()
+ @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: GroupLinkPlan()
+ @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val groupInfo_: GroupInfo? = null): GroupLinkPlan()
+ @Serializable @SerialName("known") class Known(val groupInfo: GroupInfo): GroupLinkPlan()
+}
+
abstract class TerminalItem {
abstract val id: Long
val date: Instant = Clock.System.now()
@@ -3874,6 +3921,7 @@ sealed class ChatErrorType {
is ChatNotStarted -> "chatNotStarted"
is ChatNotStopped -> "chatNotStopped"
is ChatStoreChanged -> "chatStoreChanged"
+ is ConnectionPlanChatError -> "connectionPlan"
is InvalidConnReq -> "invalidConnReq"
is InvalidChatMessage -> "invalidChatMessage"
is ContactNotReady -> "contactNotReady"
@@ -3950,6 +3998,7 @@ sealed class ChatErrorType {
@Serializable @SerialName("chatNotStarted") object ChatNotStarted: ChatErrorType()
@Serializable @SerialName("chatNotStopped") object ChatNotStopped: ChatErrorType()
@Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType()
+ @Serializable @SerialName("connectionPlan") class ConnectionPlanChatError(val connectionPlan: ConnectionPlan): ChatErrorType()
@Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType()
@Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType()
@Serializable @SerialName("contactNotReady") class ContactNotReady(val contact: Contact): ChatErrorType()
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt
index 1ba742b034..01173157a9 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt
@@ -31,10 +31,10 @@ import androidx.compose.ui.unit.sp
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
-import chat.simplex.common.views.newchat.QRCode
import chat.simplex.common.views.usersettings.*
import chat.simplex.common.platform.*
import chat.simplex.common.views.chatlist.updateChatSettings
+import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
@@ -309,9 +309,9 @@ fun ChatInfoLayout(
if (contact.contactLink != null) {
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
- QRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
+ SimpleXLinkQRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
val clipboard = LocalClipboardManager.current
- ShareAddressButton { clipboard.shareText(contact.contactLink) }
+ ShareAddressButton { clipboard.shareText(simplexChatLink(contact.contactLink)) }
SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(contact.displayName))
}
SectionDividerSpaced()
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt
index 7c767f9b7e..7e1c03130a 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt
@@ -19,7 +19,7 @@ import chat.simplex.common.model.*
import chat.simplex.common.platform.shareText
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
-import chat.simplex.common.views.newchat.QRCode
+import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR
@Composable
@@ -44,14 +44,12 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
createLink()
}
}
- val clipboard = LocalClipboardManager.current
GroupLinkLayout(
groupLink = groupLink,
groupInfo,
groupLinkMemberRole,
creatingLink,
createLink = ::createLink,
- share = { clipboard.shareText(groupLink ?: return@GroupLinkLayout) },
updateLink = {
val role = groupLinkMemberRole.value
if (role != null) {
@@ -95,7 +93,6 @@ fun GroupLinkLayout(
groupLinkMemberRole: MutableState,
creatingLink: Boolean,
createLink: () -> Unit,
- share: () -> Unit,
updateLink: () -> Unit,
deleteLink: () -> Unit
) {
@@ -125,16 +122,17 @@ fun GroupLinkLayout(
}
initialLaunch = false
}
- QRCode(groupLink, Modifier.aspectRatio(1f).padding(horizontal = DEFAULT_PADDING))
+ SimpleXLinkQRCode(groupLink, Modifier.aspectRatio(1f).padding(horizontal = DEFAULT_PADDING))
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = DEFAULT_PADDING, vertical = 10.dp)
) {
+ val clipboard = LocalClipboardManager.current
SimpleButton(
stringResource(MR.strings.share_link),
icon = painterResource(MR.images.ic_share),
- click = share
+ click = { clipboard.shareText(simplexChatLink(groupLink)) }
)
SimpleButton(
stringResource(MR.strings.delete_link),
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt
index c76fb73a6b..e281c57622 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt
@@ -284,9 +284,9 @@ fun GroupMemberInfoLayout(
if (member.contactLink != null) {
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
- QRCode(member.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
+ SimpleXLinkQRCode(member.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
val clipboard = LocalClipboardManager.current
- ShareAddressButton { clipboard.shareText(member.contactLink) }
+ ShareAddressButton { clipboard.shareText(simplexChatLink(member.contactLink)) }
if (contactId != null) {
if (knownDirectChat(contactId) == null && !groupInfo.fullGroupPreferences.directMessages.on) {
ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) })
@@ -473,43 +473,17 @@ private fun updateMemberRoleDialog(
}
fun connectViaMemberAddressAlert(connReqUri: String) {
- AlertManager.shared.showAlertDialogButtonsColumn(
- title = generalGetString(MR.strings.connect_via_member_address_alert_title),
- text = AnnotatedString(generalGetString(MR.strings.connect_via_member_address_alert_desc)),
- buttons = {
- Column {
- SectionItemView({
- AlertManager.shared.hideAlert()
- val uri = URI(connReqUri)
- withUriAction(uri) { linkType ->
- withApi {
- Log.d(TAG, "connectViaUri: connecting")
- connectViaUri(chatModel, linkType, uri, incognito = false)
- }
- }
- }) {
- Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
- }
- SectionItemView({
- AlertManager.shared.hideAlert()
- val uri = URI(connReqUri)
- withUriAction(uri) { linkType ->
- withApi {
- Log.d(TAG, "connectViaUri: connecting incognito")
- connectViaUri(chatModel, linkType, uri, incognito = true)
- }
- }
- }) {
- Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
- }
- SectionItemView({
- AlertManager.shared.hideAlert()
- }) {
- Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
- }
- }
+ try {
+ val uri = URI(connReqUri)
+ withApi {
+ planAndConnect(chatModel, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() })
}
- )
+ } catch (e: RuntimeException) {
+ AlertManager.shared.showAlertMsg(
+ title = generalGetString(MR.strings.invalid_connection_link),
+ text = generalGetString(MR.strings.this_string_is_not_a_connection_link)
+ )
+ }
}
@Preview
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 b45543df4e..b16bf4c9ba 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
@@ -130,6 +130,13 @@ suspend fun openDirectChat(contactId: Long, chatModel: ChatModel) {
}
}
+suspend fun openGroupChat(groupId: Long, chatModel: ChatModel) {
+ val chat = chatModel.controller.apiGetChat(ChatType.Group, groupId)
+ if (chat != null) {
+ openChat(chat, chatModel)
+ }
+}
+
suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) {
Log.d(TAG, "TODOCHAT: openChat: opening ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt
index 6d7450a213..66ef1cf9f9 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt
@@ -316,46 +316,8 @@ fun connectIfOpenedViaUri(uri: URI, chatModel: ChatModel) {
if (chatModel.currentUser.value == null) {
chatModel.appOpenUrl.value = uri
} else {
- withUriAction(uri) { linkType ->
- val title = when (linkType) {
- ConnectionLinkType.CONTACT -> generalGetString(MR.strings.connect_via_contact_link)
- ConnectionLinkType.INVITATION -> generalGetString(MR.strings.connect_via_invitation_link)
- ConnectionLinkType.GROUP -> generalGetString(MR.strings.connect_via_group_link)
- }
- AlertManager.shared.showAlertDialogButtonsColumn(
- title = title,
- text = if (linkType == ConnectionLinkType.GROUP)
- AnnotatedString(generalGetString(MR.strings.you_will_join_group))
- else
- AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)),
- buttons = {
- Column {
- SectionItemView({
- AlertManager.shared.hideAlert()
- withApi {
- Log.d(TAG, "connectIfOpenedViaUri: connecting")
- connectViaUri(chatModel, linkType, uri, incognito = false)
- }
- }) {
- Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
- }
- SectionItemView({
- AlertManager.shared.hideAlert()
- withApi {
- Log.d(TAG, "connectIfOpenedViaUri: connecting incognito")
- connectViaUri(chatModel, linkType, uri, incognito = true)
- }
- }) {
- Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
- }
- SectionItemView({
- AlertManager.shared.hideAlert()
- }) {
- Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
- }
- }
- }
- )
+ withApi {
+ planAndConnect(chatModel, uri, incognito = null, close = null)
}
}
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt
index 8542ea52a4..ef3633d1fa 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt
@@ -34,7 +34,6 @@ fun AddContactView(
incognitoPref = chatModel.controller.appPrefs.incognito,
connReq = connReqInvitation,
contactConnection = contactConnection,
- share = { clipboard.shareText(connReqInvitation) },
learnMore = {
ModalManager.center.showModal {
Column(
@@ -56,7 +55,6 @@ fun AddContactLayout(
incognitoPref: SharedPreference,
connReq: String,
contactConnection: MutableState,
- share: () -> Unit,
learnMore: () -> Unit
) {
val incognito = remember { mutableStateOf(incognitoPref.get()) }
@@ -82,7 +80,7 @@ fun AddContactLayout(
SectionView(stringResource(MR.strings.one_time_link_short).uppercase()) {
if (connReq.isNotEmpty()) {
- QRCode(
+ SimpleXLinkQRCode(
connReq, Modifier
.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)
.aspectRatio(1f)
@@ -99,7 +97,7 @@ fun AddContactLayout(
}
IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } }
- ShareLinkButton(share)
+ ShareLinkButton(connReq)
OneTimeLinkLearnMoreButton(learnMore)
}
SectionTextFooter(sharedProfileInfo(chatModel, incognito.value))
@@ -109,11 +107,12 @@ fun AddContactLayout(
}
@Composable
-fun ShareLinkButton(onClick: () -> Unit) {
+fun ShareLinkButton(connReqInvitation: String) {
+ val clipboard = LocalClipboardManager.current
SettingsActionItem(
painterResource(MR.images.ic_share),
stringResource(MR.strings.share_invitation_link),
- onClick,
+ click = { clipboard.shareText(simplexChatLink(connReqInvitation)) },
iconColor = MaterialTheme.colors.primary,
textColor = MaterialTheme.colors.primary,
)
@@ -177,7 +176,6 @@ fun PreviewAddContactView() {
incognitoPref = SharedPreference({ false }, {}),
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
contactConnection = mutableStateOf(PendingContactConnection.getSampleData()),
- share = {},
learnMore = {},
)
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt
index 934c050d8a..d04a85d905 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt
@@ -131,13 +131,13 @@ private fun ContactConnectionInfoLayout(
SectionView {
if (!connReq.isNullOrEmpty() && contactConnection.initiated) {
- QRCode(
+ SimpleXLinkQRCode(
connReq, Modifier
.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)
.aspectRatio(1f)
)
incognitoEnabled()
- ShareLinkButton(share)
+ ShareLinkButton(connReq)
OneTimeLinkLearnMoreButton(learnMore)
} else {
incognitoEnabled()
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt
index 0c13bd4f68..f7a5a1e86b 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt
@@ -53,32 +53,14 @@ fun PasteToConnectLayout(
fun connectViaLink(connReqUri: String) {
try {
val uri = URI(connReqUri)
- withUriAction(uri) { linkType ->
- val action = suspend {
- Log.d(TAG, "connectViaUri: connecting")
- if (connectViaUri(chatModel, linkType, uri, incognito = incognito.value)) {
- close()
- }
- }
- if (linkType == ConnectionLinkType.GROUP) {
- AlertManager.shared.showAlertDialog(
- title = generalGetString(MR.strings.connect_via_group_link),
- text = generalGetString(MR.strings.you_will_join_group),
- confirmText = if (incognito.value) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
- onConfirm = { withApi { action() } }
- )
- } else action()
+ withApi {
+ planAndConnect(chatModel, uri, incognito = incognito.value, close)
}
} catch (e: RuntimeException) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.invalid_connection_link),
text = generalGetString(MR.strings.this_string_is_not_a_connection_link)
)
- } catch (e: URISyntaxException) {
- AlertManager.shared.showAlertMsg(
- title = generalGetString(MR.strings.invalid_connection_link),
- text = generalGetString(MR.strings.this_string_is_not_a_connection_link)
- )
}
}
@@ -115,6 +97,7 @@ fun PasteToConnectLayout(
painterResource(MR.images.ic_link),
stringResource(MR.strings.connect_button),
click = { connectViaLink(connectionLink.value) },
+ disabled = connectionLink.value.isEmpty() || connectionLink.value.trim().contains(" ")
)
IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } }
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt
index 6632925964..763addae66 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt
@@ -19,6 +19,29 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.launch
+@Composable
+fun SimpleXLinkQRCode(
+ connReq: String,
+ modifier: Modifier = Modifier,
+ tintColor: Color = Color(0xff062d56),
+ withLogo: Boolean = true
+) {
+ QRCode(
+ simplexChatLink(connReq),
+ modifier,
+ tintColor,
+ withLogo
+ )
+}
+
+fun simplexChatLink(uri: String): String {
+ return if (uri.startsWith("simplex:/")) {
+ uri.replace("simplex:/", "https://simplex.chat/")
+ } else {
+ uri
+ }
+}
+
@Composable
fun QRCode(
connReq: String,
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt
index e3fa922755..2f52c2cacf 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt
@@ -1,6 +1,7 @@
package chat.simplex.common.views.newchat
import SectionBottomSpacer
+import SectionItemView
import SectionTextFooter
import androidx.compose.desktop.ui.tooling.preview.Preview
import chat.simplex.common.platform.Log
@@ -10,66 +11,265 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.style.TextAlign
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.platform.TAG
import chat.simplex.common.ui.theme.*
+import chat.simplex.common.views.chatlist.openDirectChat
+import chat.simplex.common.views.chatlist.openGroupChat
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
import java.net.URI
@Composable
expect fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit)
enum class ConnectionLinkType {
- CONTACT, INVITATION, GROUP
+ INVITATION, CONTACT, GROUP
}
-@Serializable
-sealed class CReqClientData {
- @Serializable @SerialName("group") data class Group(val groupLinkId: String): CReqClientData()
-}
-
-fun withUriAction(uri: URI, run: suspend (ConnectionLinkType) -> Unit) {
- val action = uri.path?.drop(1)?.replace("/", "")
- val data = URI(uri.toString().replaceFirst("#/", "/")).getQueryParameter("data")
- val type = when {
- data != null -> {
- val parsed = runCatching {
- json.decodeFromString(CReqClientData.serializer(), data)
+suspend fun planAndConnect(
+ chatModel: ChatModel,
+ uri: URI,
+ incognito: Boolean?,
+ close: (() -> Unit)?
+) {
+ val connectionPlan = chatModel.controller.apiConnectPlan(uri.toString())
+ if (connectionPlan != null) {
+ when (connectionPlan) {
+ is ConnectionPlan.InvitationLink -> when (connectionPlan.invitationLinkPlan) {
+ InvitationLinkPlan.Ok -> {
+ Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, incognito=$incognito")
+ if (incognito != null) {
+ connectViaUri(chatModel, uri, incognito, connectionPlan, close)
+ } else {
+ askCurrentOrIncognitoProfileAlert(
+ chatModel, uri, connectionPlan, close,
+ title = generalGetString(MR.strings.connect_via_invitation_link),
+ text = AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)),
+ connectDestructive = false
+ )
+ }
+ }
+ InvitationLinkPlan.OwnLink -> {
+ Log.d(TAG, "planAndConnect, .InvitationLink, .OwnLink, incognito=$incognito")
+ if (incognito != null) {
+ AlertManager.shared.showAlertDialog(
+ title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
+ text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link),
+ confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
+ onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } },
+ destructive = true,
+ )
+ } else {
+ askCurrentOrIncognitoProfileAlert(
+ chatModel, uri, connectionPlan, close,
+ title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
+ text = AnnotatedString(generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link)),
+ connectDestructive = true
+ )
+ }
+ }
+ is InvitationLinkPlan.Connecting -> {
+ Log.d(TAG, "planAndConnect, .InvitationLink, .Connecting, incognito=$incognito")
+ val contact = connectionPlan.invitationLinkPlan.contact_
+ if (contact != null) {
+ openKnownContact(chatModel, close, contact)
+ AlertManager.shared.showAlertMsg(
+ generalGetString(MR.strings.contact_already_exists),
+ String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName)
+ )
+ } else {
+ AlertManager.shared.showAlertMsg(
+ generalGetString(MR.strings.connect_plan_already_connecting),
+ generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link)
+ )
+ }
+ }
+ is InvitationLinkPlan.Known -> {
+ Log.d(TAG, "planAndConnect, .InvitationLink, .Known, incognito=$incognito")
+ val contact = connectionPlan.invitationLinkPlan.contact
+ openKnownContact(chatModel, close, contact)
+ AlertManager.shared.showAlertMsg(
+ generalGetString(MR.strings.contact_already_exists),
+ String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName)
+ )
+ }
}
- when {
- parsed.getOrNull() is CReqClientData.Group -> ConnectionLinkType.GROUP
- else -> null
+ is ConnectionPlan.ContactAddress -> when (connectionPlan.contactAddressPlan) {
+ ContactAddressPlan.Ok -> {
+ Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, incognito=$incognito")
+ if (incognito != null) {
+ connectViaUri(chatModel, uri, incognito, connectionPlan, close)
+ } else {
+ askCurrentOrIncognitoProfileAlert(
+ chatModel, uri, connectionPlan, close,
+ title = generalGetString(MR.strings.connect_via_contact_link),
+ text = AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)),
+ connectDestructive = false
+ )
+ }
+ }
+ ContactAddressPlan.OwnLink -> {
+ Log.d(TAG, "planAndConnect, .ContactAddress, .OwnLink, incognito=$incognito")
+ if (incognito != null) {
+ AlertManager.shared.showAlertDialog(
+ title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
+ text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address),
+ confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
+ onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } },
+ destructive = true,
+ )
+ } else {
+ askCurrentOrIncognitoProfileAlert(
+ chatModel, uri, connectionPlan, close,
+ title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
+ text = AnnotatedString(generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address)),
+ connectDestructive = true
+ )
+ }
+ }
+ ContactAddressPlan.ConnectingConfirmReconnect -> {
+ Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingConfirmReconnect, incognito=$incognito")
+ if (incognito != null) {
+ AlertManager.shared.showAlertDialog(
+ title = generalGetString(MR.strings.connect_plan_repeat_connection_request),
+ text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address),
+ confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
+ onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } },
+ destructive = true,
+ )
+ } else {
+ askCurrentOrIncognitoProfileAlert(
+ chatModel, uri, connectionPlan, close,
+ title = generalGetString(MR.strings.connect_plan_repeat_connection_request),
+ text = AnnotatedString(generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address)),
+ connectDestructive = true
+ )
+ }
+ }
+ is ContactAddressPlan.ConnectingProhibit -> {
+ Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingProhibit, incognito=$incognito")
+ val contact = connectionPlan.contactAddressPlan.contact
+ openKnownContact(chatModel, close, contact)
+ AlertManager.shared.showAlertMsg(
+ generalGetString(MR.strings.contact_already_exists),
+ String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName)
+ )
+ }
+ is ContactAddressPlan.Known -> {
+ Log.d(TAG, "planAndConnect, .ContactAddress, .Known, incognito=$incognito")
+ val contact = connectionPlan.contactAddressPlan.contact
+ openKnownContact(chatModel, close, contact)
+ AlertManager.shared.showAlertMsg(
+ generalGetString(MR.strings.contact_already_exists),
+ String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName)
+ )
+ }
+ }
+ is ConnectionPlan.GroupLink -> when (connectionPlan.groupLinkPlan) {
+ GroupLinkPlan.Ok -> {
+ Log.d(TAG, "planAndConnect, .GroupLink, .Ok, incognito=$incognito")
+ if (incognito != null) {
+ AlertManager.shared.showAlertDialog(
+ title = generalGetString(MR.strings.connect_via_group_link),
+ text = generalGetString(MR.strings.you_will_join_group),
+ confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
+ onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } }
+ )
+ } else {
+ askCurrentOrIncognitoProfileAlert(
+ chatModel, uri, connectionPlan, close,
+ title = generalGetString(MR.strings.connect_via_group_link),
+ text = AnnotatedString(generalGetString(MR.strings.you_will_join_group)),
+ connectDestructive = false
+ )
+ }
+ }
+ is GroupLinkPlan.OwnLink -> {
+ Log.d(TAG, "planAndConnect, .GroupLink, .OwnLink, incognito=$incognito")
+ val groupInfo = connectionPlan.groupLinkPlan.groupInfo
+ ownGroupLinkConfirmConnect(chatModel, uri, incognito, connectionPlan, groupInfo, close)
+ }
+ GroupLinkPlan.ConnectingConfirmReconnect -> {
+ Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingConfirmReconnect, incognito=$incognito")
+ if (incognito != null) {
+ AlertManager.shared.showAlertDialog(
+ title = generalGetString(MR.strings.connect_plan_repeat_join_request),
+ text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link),
+ confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
+ onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } },
+ destructive = true,
+ )
+ } else {
+ askCurrentOrIncognitoProfileAlert(
+ chatModel, uri, connectionPlan, close,
+ title = generalGetString(MR.strings.connect_plan_repeat_join_request),
+ text = AnnotatedString(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link)),
+ connectDestructive = true
+ )
+ }
+ }
+ is GroupLinkPlan.ConnectingProhibit -> {
+ Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingProhibit, incognito=$incognito")
+ val groupInfo = connectionPlan.groupLinkPlan.groupInfo_
+ if (groupInfo != null) {
+ AlertManager.shared.showAlertMsg(
+ generalGetString(MR.strings.connect_plan_group_already_exists),
+ String.format(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_vName), groupInfo.displayName)
+ )
+ } else {
+ AlertManager.shared.showAlertMsg(
+ generalGetString(MR.strings.connect_plan_already_joining_the_group),
+ generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link)
+ )
+ }
+ }
+ is GroupLinkPlan.Known -> {
+ Log.d(TAG, "planAndConnect, .GroupLink, .Known, incognito=$incognito")
+ val groupInfo = connectionPlan.groupLinkPlan.groupInfo
+ openKnownGroup(chatModel, close, groupInfo)
+ AlertManager.shared.showAlertMsg(
+ generalGetString(MR.strings.connect_plan_group_already_exists),
+ String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName)
+ )
+ }
}
}
-
- action == "contact" -> ConnectionLinkType.CONTACT
- action == "invitation" -> ConnectionLinkType.INVITATION
- else -> null
- }
- if (type != null) {
- withApi { run(type) }
} else {
- AlertManager.shared.showAlertMsg(
- title = generalGetString(MR.strings.invalid_contact_link),
- text = generalGetString(MR.strings.this_link_is_not_a_valid_connection_link)
- )
+ Log.d(TAG, "planAndConnect, plan error")
+ if (incognito != null) {
+ connectViaUri(chatModel, uri, incognito, connectionPlan = null, close)
+ } else {
+ askCurrentOrIncognitoProfileAlert(
+ chatModel, uri, connectionPlan = null, close,
+ title = generalGetString(MR.strings.connect_plan_connect_via_link),
+ connectDestructive = false
+ )
+ }
}
}
-suspend fun connectViaUri(chatModel: ChatModel, action: ConnectionLinkType, uri: URI, incognito: Boolean): Boolean {
+suspend fun connectViaUri(
+ chatModel: ChatModel,
+ uri: URI,
+ incognito: Boolean,
+ connectionPlan: ConnectionPlan?,
+ close: (() -> Unit)?
+): Boolean {
val r = chatModel.controller.apiConnect(incognito, uri.toString())
+ val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION
if (r) {
+ close?.invoke()
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.connection_request_sent),
text =
- when (action) {
+ when (connLinkType) {
ConnectionLinkType.CONTACT -> generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted)
ConnectionLinkType.INVITATION -> generalGetString(MR.strings.you_will_be_connected_when_your_contacts_device_is_online)
ConnectionLinkType.GROUP -> generalGetString(MR.strings.you_will_be_connected_when_group_host_device_is_online)
@@ -79,6 +279,139 @@ suspend fun connectViaUri(chatModel: ChatModel, action: ConnectionLinkType, uri:
return r
}
+fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType {
+ return when(connectionPlan) {
+ is ConnectionPlan.InvitationLink -> ConnectionLinkType.INVITATION
+ is ConnectionPlan.ContactAddress -> ConnectionLinkType.CONTACT
+ is ConnectionPlan.GroupLink -> ConnectionLinkType.GROUP
+ }
+}
+
+fun askCurrentOrIncognitoProfileAlert(
+ chatModel: ChatModel,
+ uri: URI,
+ connectionPlan: ConnectionPlan?,
+ close: (() -> Unit)?,
+ title: String,
+ text: AnnotatedString? = null,
+ connectDestructive: Boolean,
+) {
+ AlertManager.shared.showAlertDialogButtonsColumn(
+ title = title,
+ text = text,
+ buttons = {
+ Column {
+ val connectColor = if (connectDestructive) MaterialTheme.colors.error else MaterialTheme.colors.primary
+ SectionItemView({
+ AlertManager.shared.hideAlert()
+ withApi {
+ connectViaUri(chatModel, uri, incognito = false, connectionPlan, close)
+ }
+ }) {
+ Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor)
+ }
+ SectionItemView({
+ AlertManager.shared.hideAlert()
+ withApi {
+ connectViaUri(chatModel, uri, incognito = true, connectionPlan, close)
+ }
+ }) {
+ Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor)
+ }
+ SectionItemView({
+ AlertManager.shared.hideAlert()
+ }) {
+ Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
+ }
+ }
+ }
+ )
+}
+
+fun openKnownContact(chatModel: ChatModel, close: (() -> Unit)?, contact: Contact) {
+ withApi {
+ val c = chatModel.getContactChat(contact.contactId)
+ if (c != null) {
+ close?.invoke()
+ openDirectChat(contact.contactId, chatModel)
+ }
+ }
+}
+
+fun ownGroupLinkConfirmConnect(
+ chatModel: ChatModel,
+ uri: URI,
+ incognito: Boolean?,
+ connectionPlan: ConnectionPlan?,
+ groupInfo: GroupInfo,
+ close: (() -> Unit)?,
+) {
+ AlertManager.shared.showAlertDialogButtonsColumn(
+ title = generalGetString(MR.strings.connect_plan_join_your_group),
+ text = AnnotatedString(String.format(generalGetString(MR.strings.connect_plan_this_is_your_link_for_group_vName), groupInfo.displayName)),
+ buttons = {
+ Column {
+ // Open group
+ SectionItemView({
+ AlertManager.shared.hideAlert()
+ openKnownGroup(chatModel, close, groupInfo)
+ }) {
+ Text(generalGetString(MR.strings.connect_plan_open_group), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
+ }
+ if (incognito != null) {
+ // Join incognito / Join with current profile
+ SectionItemView({
+ AlertManager.shared.hideAlert()
+ withApi {
+ connectViaUri(chatModel, uri, incognito, connectionPlan, close)
+ }
+ }) {
+ Text(
+ if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
+ Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error
+ )
+ }
+ } else {
+ // Use current profile
+ SectionItemView({
+ AlertManager.shared.hideAlert()
+ withApi {
+ connectViaUri(chatModel, uri, incognito = false, connectionPlan, close)
+ }
+ }) {
+ Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
+ }
+ // Use new incognito profile
+ SectionItemView({
+ AlertManager.shared.hideAlert()
+ withApi {
+ connectViaUri(chatModel, uri, incognito = true, connectionPlan, close)
+ }
+ }) {
+ Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
+ }
+ }
+ // Cancel
+ SectionItemView({
+ AlertManager.shared.hideAlert()
+ }) {
+ Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
+ }
+ }
+ }
+ )
+}
+
+fun openKnownGroup(chatModel: ChatModel, close: (() -> Unit)?, groupInfo: GroupInfo) {
+ withApi {
+ val g = chatModel.getGroupChat(groupInfo.groupId)
+ if (g != null) {
+ close?.invoke()
+ openGroupChat(groupInfo.groupId, chatModel)
+ }
+ }
+}
+
@Composable
fun ConnectContactLayout(
chatModel: ChatModel,
@@ -92,21 +425,8 @@ fun ConnectContactLayout(
QRCodeScanner { connReqUri ->
try {
val uri = URI(connReqUri)
- withUriAction(uri) { linkType ->
- val action = suspend {
- Log.d(TAG, "connectViaUri: connecting")
- if (connectViaUri(ChatModel, linkType, uri, incognito = incognito.value)) {
- close()
- }
- }
- if (linkType == ConnectionLinkType.GROUP) {
- AlertManager.shared.showAlertDialog(
- title = generalGetString(MR.strings.connect_via_group_link),
- text = generalGetString(MR.strings.you_will_join_group),
- confirmText = if (incognito.value) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
- onConfirm = { withApi { action() } }
- )
- } else action()
+ withApi {
+ planAndConnect(chatModel, uri, incognito = incognito.value, close)
}
} catch (e: RuntimeException) {
AlertManager.shared.showAlertMsg(
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt
index 72cbc3a628..0132223383 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt
@@ -18,7 +18,8 @@ import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
-import chat.simplex.common.views.newchat.QRCode
+import chat.simplex.common.views.newchat.SimpleXLinkQRCode
+import chat.simplex.common.views.newchat.simplexChatLink
import chat.simplex.res.MR
@Composable
@@ -38,7 +39,7 @@ fun CreateSimpleXAddress(m: ChatModel) {
sendEmail = { address ->
uriHandler.sendEmail(
generalGetString(MR.strings.email_invite_subject),
- generalGetString(MR.strings.email_invite_body).format(address.connReqContact)
+ generalGetString(MR.strings.email_invite_body).format(simplexChatLink(address.connReqContact))
)
},
createAddress = {
@@ -91,8 +92,8 @@ private fun CreateSimpleXAddressLayout(
Spacer(Modifier.weight(1f))
if (userAddress != null) {
- QRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
- ShareAddressButton { share(userAddress.connReqContact) }
+ SimpleXLinkQRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
+ ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) }
Spacer(Modifier.weight(1f))
ShareViaEmailButton { sendEmail(userAddress) }
Spacer(Modifier.weight(1f))
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 63f06a2aec..d03b758565 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
@@ -24,10 +24,10 @@ import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.ShareAddressButton
import chat.simplex.common.views.helpers.*
-import chat.simplex.common.views.newchat.QRCode
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.MsgContent
import chat.simplex.common.platform.*
+import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR
@Composable
@@ -100,7 +100,7 @@ fun UserAddressView(
sendEmail = { userAddress ->
uriHandler.sendEmail(
generalGetString(MR.strings.email_invite_subject),
- generalGetString(MR.strings.email_invite_body).format(userAddress.connReqContact)
+ generalGetString(MR.strings.email_invite_body).format(simplexChatLink( userAddress.connReqContact))
)
},
setProfileAddress = ::setProfileAddress,
@@ -201,8 +201,8 @@ private fun UserAddressLayout(
val autoAcceptState = remember { mutableStateOf(AutoAcceptState(userAddress)) }
val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) }
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
- QRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
- ShareAddressButton { share(userAddress.connReqContact) }
+ SimpleXLinkQRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
+ ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) }
ShareViaEmailButton { sendEmail(userAddress) }
ShareWithContactsButton(shareViaProfile, setProfileAddress)
AutoAcceptToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) }
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 114fe49e92..bd59d236d5 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
@@ -4,13 +4,13 @@
k
- Connect via contact link?
- Connect via invitation link?
- Connect via group link?
+ Connect via contact address?
+ Connect via one-time link?
+ Join group?
Use current profile
Use new incognito profile
Your profile will be sent to the contact that you received this link from.
- You will join a group this link refers to and connect to its group members.
+ You will connect to all group members.
Connect
Connect incognito
@@ -1600,4 +1600,25 @@
Coming soon!
This feature is not yet supported. Try the next release.
+
+
+ Connect to yourself?
+ This is your own one-time link!
+ You are already connecting to %1$s.
+ Already connecting!
+ You are already connecting via this one-time link!
+ This is your own SimpleX address!
+ Repeat connection request?
+ You have already requested connection via this address!
+ Join your group?
+ This is your link for group %1$s!
+ Open group
+ Repeat join request?
+ You are already joining the group via this link!
+ Group already exists!
+ You are already joining the group %1$s.
+ Already joining the group!
+ You are already joining the group via this link.
+ You are already in group %1$s.
+ Connect via link?
\ No newline at end of file
From 2219cea0268ca13eff56978c9898d515cbdcaf31 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Wed, 18 Oct 2023 21:15:37 +0100
Subject: [PATCH 43/80] core: fix type for JSON
---
src/Simplex/Chat/Messages.hs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs
index 21da83ad05..2ddb1e7bcd 100644
--- a/src/Simplex/Chat/Messages.hs
+++ b/src/Simplex/Chat/Messages.hs
@@ -933,7 +933,7 @@ instance ToJSON (CIDeleted d) where
data JSONCIDeleted
= JCIDDeleted {deletedTs :: Maybe UTCTime}
- | JCIBlocked {deletedTs :: Maybe UTCTime}
+ | JCIDBlocked {deletedTs :: Maybe UTCTime}
| JCIDModerated {deletedTs :: Maybe UTCTime, byGroupMember :: GroupMember}
deriving (Show, Generic)
@@ -944,7 +944,7 @@ instance ToJSON JSONCIDeleted where
jsonCIDeleted :: CIDeleted d -> JSONCIDeleted
jsonCIDeleted = \case
CIDeleted ts -> JCIDDeleted ts
- CIBlocked ts -> JCIBlocked ts
+ CIBlocked ts -> JCIDBlocked ts
CIModerated ts m -> JCIDModerated ts m
itemDeletedTs :: CIDeleted d -> Maybe UTCTime
From c090b68bdda6073aeca7ac94dfde8b654eec5aba Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Thu, 19 Oct 2023 19:52:59 +0400
Subject: [PATCH 44/80] ios, android: ask to notify contact or not on contact
deletion (#3247)
---
apps/ios/Shared/Model/SimpleXAPI.swift | 8 +--
apps/ios/Shared/Views/Chat/ChatInfoView.swift | 65 +++++++++--------
.../Views/ChatList/ChatListNavLink.swift | 42 ++++++-----
apps/ios/SimpleXChat/APITypes.swift | 8 ++-
.../chat/simplex/common/model/SimpleXAPI.kt | 12 ++--
.../simplex/common/views/chat/ChatInfoView.kt | 69 +++++++++++++++----
.../commonMain/resources/MR/base/strings.xml | 1 +
7 files changed, 137 insertions(+), 68 deletions(-)
diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index bad15ad52f..99e8c02847 100644
--- a/apps/ios/Shared/Model/SimpleXAPI.swift
+++ b/apps/ios/Shared/Model/SimpleXAPI.swift
@@ -671,18 +671,18 @@ private func connectionErrorAlert(_ r: ChatResponse) -> Alert {
}
}
-func apiDeleteChat(type: ChatType, id: Int64) async throws {
- let r = await chatSendCmd(.apiDeleteChat(type: type, id: id), bgTask: false)
+func apiDeleteChat(type: ChatType, id: Int64, notify: Bool? = nil) async throws {
+ let r = await chatSendCmd(.apiDeleteChat(type: type, id: id, notify: notify), bgTask: false)
if case .direct = type, case .contactDeleted = r { return }
if case .contactConnection = type, case .contactConnectionDeleted = r { return }
if case .group = type, case .groupDeletedUser = r { return }
throw r
}
-func deleteChat(_ chat: Chat) async {
+func deleteChat(_ chat: Chat, notify: Bool? = nil) async {
do {
let cInfo = chat.chatInfo
- try await apiDeleteChat(type: cInfo.chatType, id: cInfo.apiId)
+ try await apiDeleteChat(type: cInfo.chatType, id: cInfo.apiId, notify: notify)
DispatchQueue.main.async { ChatModel.shared.removeChat(cInfo.id) }
} catch let error {
logger.error("deleteChat apiDeleteChat error: \(responseError(error))")
diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift
index 5438eb13bc..ec4cc0fc4c 100644
--- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift
@@ -99,12 +99,12 @@ struct ChatInfoView: View {
@Binding var connectionCode: String?
@FocusState private var aliasTextFieldFocused: Bool
@State private var alert: ChatInfoViewAlert? = nil
+ @State private var showDeleteContactActionSheet = false
@State private var sendReceipts = SendReceipts.userDefault(true)
@State private var sendReceiptsUserDefault = true
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
enum ChatInfoViewAlert: Identifiable {
- case deleteContactAlert
case clearChatAlert
case networkStatusAlert
case switchAddressAlert
@@ -114,7 +114,6 @@ struct ChatInfoView: View {
var id: String {
switch self {
- case .deleteContactAlert: return "deleteContactAlert"
case .clearChatAlert: return "clearChatAlert"
case .networkStatusAlert: return "networkStatusAlert"
case .switchAddressAlert: return "switchAddressAlert"
@@ -233,7 +232,6 @@ struct ChatInfoView: View {
}
.alert(item: $alert) { alertItem in
switch(alertItem) {
- case .deleteContactAlert: return deleteContactAlert()
case .clearChatAlert: return clearChatAlert()
case .networkStatusAlert: return networkStatusAlert()
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
@@ -242,6 +240,26 @@ struct ChatInfoView: View {
case let .error(title, error): return mkAlert(title: title, message: error)
}
}
+ .actionSheet(isPresented: $showDeleteContactActionSheet) {
+ if contact.ready && contact.active {
+ ActionSheet(
+ title: Text("Delete contact?\nThis cannot be undone!"),
+ buttons: [
+ .destructive(Text("Delete and notify contact")) { deleteContact(notify: true) },
+ .destructive(Text("Delete")) { deleteContact(notify: false) },
+ .cancel()
+ ]
+ )
+ } else {
+ ActionSheet(
+ title: Text("Delete contact?\nThis cannot be undone!"),
+ buttons: [
+ .destructive(Text("Delete")) { deleteContact() },
+ .cancel()
+ ]
+ )
+ }
+ }
}
private func contactInfoHeader() -> some View {
@@ -414,7 +432,7 @@ struct ChatInfoView: View {
private func deleteContactButton() -> some View {
Button(role: .destructive) {
- alert = .deleteContactAlert
+ showDeleteContactActionSheet = true
} label: {
Label("Delete contact", systemImage: "trash")
.foregroundColor(Color.red)
@@ -430,30 +448,23 @@ struct ChatInfoView: View {
}
}
- private func deleteContactAlert() -> Alert {
- Alert(
- title: Text("Delete contact?"),
- message: Text("Contact and all messages will be deleted - this cannot be undone!"),
- primaryButton: .destructive(Text("Delete")) {
- Task {
- do {
- try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId)
- await MainActor.run {
- dismiss()
- chatModel.chatId = nil
- chatModel.removeChat(chat.chatInfo.id)
- }
- } catch let error {
- logger.error("deleteContactAlert apiDeleteChat error: \(responseError(error))")
- let a = getErrorAlert(error, "Error deleting contact")
- await MainActor.run {
- alert = .error(title: a.title, error: a.message)
- }
- }
+ private func deleteContact(notify: Bool? = nil) {
+ Task {
+ do {
+ try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, notify: notify)
+ await MainActor.run {
+ dismiss()
+ chatModel.chatId = nil
+ chatModel.removeChat(chat.chatInfo.id)
}
- },
- secondaryButton: .cancel()
- )
+ } catch let error {
+ logger.error("deleteContactAlert apiDeleteChat error: \(responseError(error))")
+ let a = getErrorAlert(error, "Error deleting contact")
+ await MainActor.run {
+ alert = .error(title: a.title, error: a.message)
+ }
+ }
+ }
}
private func clearChatAlert() -> Alert {
diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift
index f445ae4b5d..be912d666a 100644
--- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift
@@ -32,6 +32,7 @@ struct ChatListNavLink: View {
@State private var showJoinGroupDialog = false
@State private var showContactConnectionInfo = false
@State private var showInvalidJSON = false
+ @State private var showDeleteContactActionSheet = false
var body: some View {
switch chat.chatInfo {
@@ -64,17 +65,37 @@ struct ChatListNavLink: View {
clearChatButton()
}
Button {
- AlertManager.shared.showAlert(
- contact.ready || !contact.active
- ? deleteContactAlert(chat.chatInfo)
- : deletePendingContactAlert(chat, contact)
- )
+ if contact.ready || !contact.active {
+ showDeleteContactActionSheet = true
+ } else {
+ AlertManager.shared.showAlert(deletePendingContactAlert(chat, contact))
+ }
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
}
.frame(height: rowHeights[dynamicTypeSize])
+ .actionSheet(isPresented: $showDeleteContactActionSheet) {
+ if contact.ready && contact.active {
+ ActionSheet(
+ title: Text("Delete contact?\nThis cannot be undone!"),
+ buttons: [
+ .destructive(Text("Delete and notify contact")) { Task { await deleteChat(chat, notify: true) } },
+ .destructive(Text("Delete")) { Task { await deleteChat(chat, notify: false) } },
+ .cancel()
+ ]
+ )
+ } else {
+ ActionSheet(
+ title: Text("Delete contact?\nThis cannot be undone!"),
+ buttons: [
+ .destructive(Text("Delete")) { Task { await deleteChat(chat) } },
+ .cancel()
+ ]
+ )
+ }
+ }
}
@ViewBuilder private func groupNavLink(_ groupInfo: GroupInfo) -> some View {
@@ -269,17 +290,6 @@ struct ChatListNavLink: View {
}
}
- private func deleteContactAlert(_ chatInfo: ChatInfo) -> Alert {
- Alert(
- title: Text("Delete contact?"),
- message: Text("Contact and all messages will be deleted - this cannot be undone!"),
- primaryButton: .destructive(Text("Delete")) {
- Task { await deleteChat(chat) }
- },
- secondaryButton: .cancel()
- )
- }
-
private func deleteGroupAlert(_ groupInfo: GroupInfo) -> Alert {
Alert(
title: Text("Delete group?"),
diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift
index 53da91b042..5c7220f374 100644
--- a/apps/ios/SimpleXChat/APITypes.swift
+++ b/apps/ios/SimpleXChat/APITypes.swift
@@ -89,7 +89,7 @@ public enum ChatCommand {
case apiSetConnectionIncognito(connId: Int64, incognito: Bool)
case apiConnectPlan(userId: Int64, connReq: String)
case apiConnect(userId: Int64, incognito: Bool, connReq: String)
- case apiDeleteChat(type: ChatType, id: Int64)
+ case apiDeleteChat(type: ChatType, id: Int64, notify: Bool?)
case apiClearChat(type: ChatType, id: Int64)
case apiListContacts(userId: Int64)
case apiUpdateProfile(userId: Int64, profile: Profile)
@@ -224,7 +224,11 @@ public enum ChatCommand {
case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))"
case let .apiConnectPlan(userId, connReq): return "/_connect plan \(userId) \(connReq)"
case let .apiConnect(userId, incognito, connReq): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connReq)"
- case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
+ case let .apiDeleteChat(type, id, notify): if let notify = notify {
+ return "/_delete \(ref(type, id)) notify=\(onOff(notify))"
+ } else {
+ return "/_delete \(ref(type, id))"
+ }
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
case let .apiListContacts(userId): return "/_contacts \(userId)"
case let .apiUpdateProfile(userId, profile): return "/_profile \(userId) \(encodeJSON(profile))"
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
index 8397d2edb1..da09ea132f 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
@@ -904,8 +904,8 @@ object ChatController {
}
}
- suspend fun apiDeleteChat(type: ChatType, id: Long): Boolean {
- val r = sendCmd(CC.ApiDeleteChat(type, id))
+ suspend fun apiDeleteChat(type: ChatType, id: Long, notify: Boolean? = null): Boolean {
+ val r = sendCmd(CC.ApiDeleteChat(type, id, notify))
when {
r is CR.ContactDeleted && type == ChatType.Direct -> return true
r is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> return true
@@ -1924,7 +1924,7 @@ sealed class CC {
class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC()
class APIConnectPlan(val userId: Long, val connReq: String): CC()
class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC()
- class ApiDeleteChat(val type: ChatType, val id: Long): CC()
+ class ApiDeleteChat(val type: ChatType, val id: Long, val notify: Boolean?): CC()
class ApiClearChat(val type: ChatType, val id: Long): CC()
class ApiListContacts(val userId: Long): CC()
class ApiUpdateProfile(val userId: Long, val profile: Profile): CC()
@@ -2034,7 +2034,11 @@ sealed class CC {
is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}"
is APIConnectPlan -> "/_connect plan $userId $connReq"
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq"
- is ApiDeleteChat -> "/_delete ${chatRef(type, id)}"
+ is ApiDeleteChat -> if (notify != null) {
+ "/_delete ${chatRef(type, id)} notify=${onOff(notify)}"
+ } else {
+ "/_delete ${chatRef(type, id)}"
+ }
is ApiClearChat -> "/_clear chat ${chatRef(type, id)}"
is ApiListContacts -> "/_contacts $userId"
is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}"
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt
index 01173157a9..4564a4a6e8 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt
@@ -196,28 +196,67 @@ sealed class SendReceipts {
}
fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
- AlertManager.shared.showAlertDialog(
+ AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.delete_contact_question),
- text = generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning),
- confirmText = generalGetString(MR.strings.delete_verb),
- onConfirm = {
- withApi {
- val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
- if (r) {
- chatModel.removeChat(chatInfo.id)
- if (chatModel.chatId.value == chatInfo.id) {
- chatModel.chatId.value = null
- ModalManager.end.closeModals()
+ text = AnnotatedString(generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)),
+ buttons = {
+ Column {
+ if (chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) {
+ // Delete and notify contact
+ SectionItemView({
+ AlertManager.shared.hideAlert()
+ withApi {
+ deleteContact(chatInfo, chatModel, close, notify = true)
+ }
+ }) {
+ Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
}
- ntfManager.cancelNotificationsForChat(chatInfo.id)
- close?.invoke()
+ // Delete
+ SectionItemView({
+ AlertManager.shared.hideAlert()
+ withApi {
+ deleteContact(chatInfo, chatModel, close, notify = false)
+ }
+ }) {
+ Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
+ }
+ } else {
+ // Delete
+ SectionItemView({
+ AlertManager.shared.hideAlert()
+ withApi {
+ deleteContact(chatInfo, chatModel, close)
+ }
+ }) {
+ Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
+ }
+ }
+ // Cancel
+ SectionItemView({
+ AlertManager.shared.hideAlert()
+ }) {
+ Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
- },
- destructive = true,
+ }
)
}
+fun deleteContact(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)?, notify: Boolean? = null) {
+ withApi {
+ val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId, notify)
+ if (r) {
+ chatModel.removeChat(chatInfo.id)
+ if (chatModel.chatId.value == chatInfo.id) {
+ chatModel.chatId.value = null
+ ModalManager.end.closeModals()
+ }
+ ntfManager.cancelNotificationsForChat(chatInfo.id)
+ close?.invoke()
+ }
+ }
+}
+
fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.clear_chat_question),
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 bd59d236d5..1ae85cd050 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
@@ -350,6 +350,7 @@
Delete contact?
Contact and all messages will be deleted - this cannot be undone!
+ Delete and notify contact
Delete contact
Set contact name…
Connected
From 87d84cfccce5007cf404898b20d2142d451f9b1c Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Sat, 21 Oct 2023 19:13:32 +0400
Subject: [PATCH 45/80] core: filter connection plan query results by user_id
(#3251)
---
simplex-chat.cabal | 1 +
src/Simplex/Chat.hs | 2 +-
.../Chat/Migrations/M20231019_indexes.hs | 32 +++++++++++++++++++
src/Simplex/Chat/Migrations/chat_schema.sql | 13 +++++---
src/Simplex/Chat/Store/Connections.hs | 10 +++---
src/Simplex/Chat/Store/Direct.hs | 2 +-
src/Simplex/Chat/Store/Groups.hs | 6 ++--
src/Simplex/Chat/Store/Migrations.hs | 4 ++-
src/Simplex/Chat/Store/Profiles.hs | 8 ++---
tests/SchemaDump.hs | 4 ++-
10 files changed, 62 insertions(+), 20 deletions(-)
create mode 100644 src/Simplex/Chat/Migrations/M20231019_indexes.hs
diff --git a/simplex-chat.cabal b/simplex-chat.cabal
index b431b0ddf3..824b558135 100644
--- a/simplex-chat.cabal
+++ b/simplex-chat.cabal
@@ -117,6 +117,7 @@ library
Simplex.Chat.Migrations.M20231002_conn_initiated
Simplex.Chat.Migrations.M20231009_via_group_link_uri_hash
Simplex.Chat.Migrations.M20231010_member_settings
+ Simplex.Chat.Migrations.M20231019_indexes
Simplex.Chat.Mobile
Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 7e2c2b71e7..33dd262b01 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -2249,7 +2249,7 @@ processChatCommand = \case
case groupLinkId of
-- contact address
Nothing ->
- withStore' (`getUserContactLinkByConnReq` cReqSchemas) >>= \case
+ withStore' (\db -> getUserContactLinkByConnReq db user cReqSchemas) >>= \case
Just _ -> pure $ CPContactAddress CAPOwnLink
Nothing -> do
withStore' (\db -> getContactConnEntityByConnReqHash db user cReqHashes) >>= \case
diff --git a/src/Simplex/Chat/Migrations/M20231019_indexes.hs b/src/Simplex/Chat/Migrations/M20231019_indexes.hs
new file mode 100644
index 0000000000..40412e1778
--- /dev/null
+++ b/src/Simplex/Chat/Migrations/M20231019_indexes.hs
@@ -0,0 +1,32 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+module Simplex.Chat.Migrations.M20231019_indexes where
+
+import Database.SQLite.Simple (Query)
+import Database.SQLite.Simple.QQ (sql)
+
+m20231019_indexes :: Query
+m20231019_indexes =
+ [sql|
+DROP INDEX idx_connections_conn_req_inv;
+CREATE INDEX idx_connections_conn_req_inv ON connections(user_id, conn_req_inv);
+
+DROP INDEX idx_groups_via_group_link_uri_hash;
+CREATE INDEX idx_groups_via_group_link_uri_hash ON groups(user_id, via_group_link_uri_hash);
+
+DROP INDEX idx_connections_via_contact_uri_hash;
+CREATE INDEX idx_connections_via_contact_uri_hash ON connections(user_id, via_contact_uri_hash);
+|]
+
+down_m20231019_indexes :: Query
+down_m20231019_indexes =
+ [sql|
+DROP INDEX idx_connections_conn_req_inv;
+CREATE INDEX idx_connections_conn_req_inv ON connections(conn_req_inv);
+
+DROP INDEX idx_groups_via_group_link_uri_hash;
+CREATE INDEX idx_groups_via_group_link_uri_hash ON groups(via_group_link_uri_hash);
+
+DROP INDEX idx_connections_via_contact_uri_hash;
+CREATE INDEX idx_connections_via_contact_uri_hash ON connections(via_contact_uri_hash);
+|]
diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql
index 7308ef89ff..cadb7caf42 100644
--- a/src/Simplex/Chat/Migrations/chat_schema.sql
+++ b/src/Simplex/Chat/Migrations/chat_schema.sql
@@ -524,9 +524,6 @@ CREATE INDEX contact_profiles_index ON contact_profiles(
full_name
);
CREATE INDEX idx_groups_inv_queue_info ON groups(inv_queue_info);
-CREATE INDEX idx_connections_via_contact_uri_hash ON connections(
- via_contact_uri_hash
-);
CREATE INDEX idx_contact_requests_xcontact_id ON contact_requests(xcontact_id);
CREATE INDEX idx_contacts_xcontact_id ON contacts(xcontact_id);
CREATE INDEX idx_messages_shared_msg_id ON messages(shared_msg_id);
@@ -738,7 +735,15 @@ CREATE INDEX idx_received_probes_probe_hash ON received_probes(probe_hash);
CREATE INDEX idx_sent_probes_created_at ON sent_probes(created_at);
CREATE INDEX idx_sent_probe_hashes_created_at ON sent_probe_hashes(created_at);
CREATE INDEX idx_received_probes_created_at ON received_probes(created_at);
-CREATE INDEX idx_connections_conn_req_inv ON connections(conn_req_inv);
+CREATE INDEX idx_connections_conn_req_inv ON connections(
+ user_id,
+ conn_req_inv
+);
CREATE INDEX idx_groups_via_group_link_uri_hash ON groups(
+ user_id,
via_group_link_uri_hash
);
+CREATE INDEX idx_connections_via_contact_uri_hash ON connections(
+ user_id,
+ via_contact_uri_hash
+);
diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs
index 59ffb57c6e..d73ac705d3 100644
--- a/src/Simplex/Chat/Store/Connections.hs
+++ b/src/Simplex/Chat/Store/Connections.hs
@@ -155,9 +155,9 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
userContact_ _ = Left SEUserContactLinkNotFound
getConnectionEntityByConnReq :: DB.Connection -> User -> (ConnReqInvitation, ConnReqInvitation) -> IO (Maybe ConnectionEntity)
-getConnectionEntityByConnReq db user (cReqSchema1, cReqSchema2) = do
+getConnectionEntityByConnReq db user@User {userId} (cReqSchema1, cReqSchema2) = do
connId_ <- maybeFirstRow fromOnly $
- DB.query db "SELECT agent_conn_id FROM connections WHERE conn_req_inv IN (?,?) LIMIT 1" (cReqSchema1, cReqSchema2)
+ DB.query db "SELECT agent_conn_id FROM connections WHERE user_id = ? AND conn_req_inv IN (?,?) LIMIT 1" (userId, cReqSchema1, cReqSchema2)
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db user) connId_
-- search connection for connection plan:
@@ -165,7 +165,7 @@ getConnectionEntityByConnReq db user (cReqSchema1, cReqSchema2) = do
-- this function searches for latest connection with contact so that "known contact" plan would be chosen;
-- deleted connections are filtered out to allow re-connecting via same contact address
getContactConnEntityByConnReqHash :: DB.Connection -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe ConnectionEntity)
-getContactConnEntityByConnReqHash db user (cReqHash1, cReqHash2) = do
+getContactConnEntityByConnReqHash db user@User {userId} (cReqHash1, cReqHash2) = do
connId_ <- maybeFirstRow fromOnly $
DB.query
db
@@ -175,12 +175,12 @@ getContactConnEntityByConnReqHash db user (cReqHash1, cReqHash2) = do
agent_conn_id,
(CASE WHEN contact_id IS NOT NULL THEN 1 ELSE 0 END) AS conn_ord
FROM connections
- WHERE via_contact_uri_hash IN (?,?) AND conn_status != ?
+ WHERE user_id = ? AND via_contact_uri_hash IN (?,?) AND conn_status != ?
ORDER BY conn_ord DESC, created_at DESC
LIMIT 1
)
|]
- (cReqHash1, cReqHash2, ConnDeleted)
+ (userId, cReqHash1, cReqHash2, ConnDeleted)
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db user) connId_
getConnectionsToSubscribe :: DB.Connection -> IO ([ConnId], [ConnectionEntity])
diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs
index 7227797193..563cc337e0 100644
--- a/src/Simplex/Chat/Store/Direct.hs
+++ b/src/Simplex/Chat/Store/Direct.hs
@@ -167,7 +167,7 @@ getContactByConnReqHash db user@User {userId} cReqHash =
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
JOIN connections c ON c.contact_id = ct.contact_id
- WHERE ct.user_id = ? AND c.via_contact_uri_hash = ? AND ct.contact_status = ? AND ct.deleted = 0
+ WHERE c.user_id = ? AND c.via_contact_uri_hash = ? AND ct.contact_status = ? AND ct.deleted = 0
ORDER BY c.created_at DESC
LIMIT 1
|]
diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs
index a4a19816da..76d68cc6be 100644
--- a/src/Simplex/Chat/Store/Groups.hs
+++ b/src/Simplex/Chat/Store/Groups.hs
@@ -1122,16 +1122,16 @@ getGroupInfo db User {userId, userContactId} groupId =
(groupId, userId, userContactId)
getGroupInfoByUserContactLinkConnReq :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo)
-getGroupInfoByUserContactLinkConnReq db user (cReqSchema1, cReqSchema2) = do
+getGroupInfoByUserContactLinkConnReq db user@User {userId} (cReqSchema1, cReqSchema2) = do
groupId_ <- maybeFirstRow fromOnly $
DB.query
db
[sql|
SELECT group_id
FROM user_contact_links
- WHERE conn_req_contact IN (?,?)
+ WHERE user_id = ? AND conn_req_contact IN (?,?)
|]
- (cReqSchema1, cReqSchema2)
+ (userId, cReqSchema1, cReqSchema2)
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db user) groupId_
getGroupInfoByGroupLinkHash :: DB.Connection -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe GroupInfo)
diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs
index 60783f3664..357bfd9a28 100644
--- a/src/Simplex/Chat/Store/Migrations.hs
+++ b/src/Simplex/Chat/Store/Migrations.hs
@@ -85,6 +85,7 @@ import Simplex.Chat.Migrations.M20230926_contact_status
import Simplex.Chat.Migrations.M20231002_conn_initiated
import Simplex.Chat.Migrations.M20231009_via_group_link_uri_hash
import Simplex.Chat.Migrations.M20231010_member_settings
+import Simplex.Chat.Migrations.M20231019_indexes
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -169,7 +170,8 @@ schemaMigrations =
("20230926_contact_status", m20230926_contact_status, Just down_m20230926_contact_status),
("20231002_conn_initiated", m20231002_conn_initiated, Just down_m20231002_conn_initiated),
("20231009_via_group_link_uri_hash", m20231009_via_group_link_uri_hash, Just down_m20231009_via_group_link_uri_hash),
- ("20231010_member_settings", m20231010_member_settings, Just down_m20231010_member_settings)
+ ("20231010_member_settings", m20231010_member_settings, Just down_m20231010_member_settings),
+ ("20231019_indexes", m20231019_indexes, Just down_m20231019_indexes)
]
-- | The list of migrations in ascending order by date
diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs
index c9c4806496..80499dee82 100644
--- a/src/Simplex/Chat/Store/Profiles.hs
+++ b/src/Simplex/Chat/Store/Profiles.hs
@@ -441,17 +441,17 @@ getUserContactLinkById db userId userContactLinkId =
|]
(userId, userContactLinkId)
-getUserContactLinkByConnReq :: DB.Connection -> (ConnReqContact, ConnReqContact) -> IO (Maybe UserContactLink)
-getUserContactLinkByConnReq db (cReqSchema1, cReqSchema2) =
+getUserContactLinkByConnReq :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe UserContactLink)
+getUserContactLinkByConnReq db User {userId} (cReqSchema1, cReqSchema2) =
maybeFirstRow toUserContactLink $
DB.query
db
[sql|
SELECT conn_req_contact, auto_accept, auto_accept_incognito, auto_reply_msg_content
FROM user_contact_links
- WHERE conn_req_contact IN (?,?)
+ WHERE user_id = ? AND conn_req_contact IN (?,?)
|]
- (cReqSchema1, cReqSchema2)
+ (userId, cReqSchema1, cReqSchema2)
updateUserAddressAutoAccept :: DB.Connection -> User -> Maybe AutoAccept -> ExceptT StoreError IO UserContactLink
updateUserAddressAutoAccept db user@User {userId} autoAccept = do
diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs
index f4538e4b3b..f517d13df1 100644
--- a/tests/SchemaDump.hs
+++ b/tests/SchemaDump.hs
@@ -71,7 +71,9 @@ skipComparisonForDownMigrations =
-- on down migration idx_chat_items_timed_delete_at index moves down to the end of the file
"20230529_indexes",
-- table and index definitions move down the file, so fields are re-created as not unique
- "20230914_member_probes"
+ "20230914_member_probes",
+ -- on down migration idx_connections_via_contact_uri_hash index moves down to the end of the file
+ "20231019_indexes"
]
getSchema :: FilePath -> FilePath -> IO String
From 5c57987e9f321c98a079a495f8104b0b57d4a718 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Sun, 22 Oct 2023 13:58:51 +0100
Subject: [PATCH 46/80] add smp11, 12 and 14 to preset servers
---
src/Simplex/Chat.hs | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 33dd262b01..3581b80c29 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -150,7 +150,10 @@ defaultChatConfig =
_defaultSMPServers :: NonEmpty SMPServerWithAuth
_defaultSMPServers =
L.fromList
- [ "smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion",
+ [ "smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion",
+ "smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion",
+ "smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion",
+ "smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion",
"smp://hejn2gVIqNU6xjtGM3OwQeuk8ZEbDXVJXAlnSBJBWUA=@smp16.simplex.im,p3ktngodzi6qrf7w64mmde3syuzrv57y55hxabqcq3l5p6oi7yzze6qd.onion",
"smp://ZKe4uxF4Z_aLJJOEsC-Y6hSkXgQS5-oc442JQGkyP8M=@smp17.simplex.im,ogtwfxyi3h2h5weftjjpjmxclhb5ugufa5rcyrmg7j4xlch7qsr5nuqd.onion",
"smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion",
From 8891314507a5f5fc05c540ca83b80391dc4e7a87 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Sun, 22 Oct 2023 14:19:24 +0100
Subject: [PATCH 47/80] core: update simplexmq (fixes ordering issue during
message delivery)
---
cabal.project | 4 ++--
scripts/nix/sha256map.nix | 4 ++--
stack.yaml | 4 ++--
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/cabal.project b/cabal.project
index 9a9a3e25da..8a05c0bb95 100644
--- a/cabal.project
+++ b/cabal.project
@@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: 1ad69cf74f18f25713ce564e1629d2538313b9e0
+ tag: cf8b9c12ff5cbdc77d3b8866af2c761a546ec8fc
source-repository-package
type: git
@@ -19,7 +19,7 @@ source-repository-package
source-repository-package
type: git
location: https://github.com/kazu-yamamoto/http2.git
- tag: b5a1b7200cf5bc7044af34ba325284271f6dff25
+ tag: 804fa283f067bd3fd89b8c5f8d25b3047813a517
source-repository-package
type: git
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index 17d650cb09..60b9505fea 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,7 +1,7 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."1ad69cf74f18f25713ce564e1629d2538313b9e0" = "1kil0962pn3ksnxh7dcwcbnkidz95yl31rm4m585ps7wnh6fp0l9";
+ "https://github.com/simplex-chat/simplexmq.git"."cf8b9c12ff5cbdc77d3b8866af2c761a546ec8fc" = "0xcbvxz2nszm1sdh6gvmfzjf9n2ldsarmmzbl6j6b5hg9i1mppc6";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
- "https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb";
+ "https://github.com/kazu-yamamoto/http2.git"."804fa283f067bd3fd89b8c5f8d25b3047813a517" = "1j67wp7rfybfx3ryx08z6gqmzj85j51hmzhgx47ihgmgr47sl895";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "0kiwhvml42g9anw4d2v0zd1fpc790pj9syg5x3ik4l97fnkbbwpp";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
"https://github.com/simplex-chat/aeson.git"."aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b" = "0jz7kda8gai893vyvj96fy962ncv8dcsx71fbddyy8zrvc88jfrr";
diff --git a/stack.yaml b/stack.yaml
index 6e047f7e6c..99b9d179cd 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -49,9 +49,9 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq
- github: simplex-chat/simplexmq
- commit: 1ad69cf74f18f25713ce564e1629d2538313b9e0
+ commit: cf8b9c12ff5cbdc77d3b8866af2c761a546ec8fc
- github: kazu-yamamoto/http2
- commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
+ commit: 804fa283f067bd3fd89b8c5f8d25b3047813a517
# - ../direct-sqlcipher
- github: simplex-chat/direct-sqlcipher
commit: f814ee68b16a9447fbb467ccc8f29bdd3546bfd9
From b25c2e3a091e7bf7258c9485f790787a346835a9 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Sun, 22 Oct 2023 14:56:51 +0100
Subject: [PATCH 48/80] ui: add 10 minutes SimpleX Lock delay (#3255)
---
apps/ios/Shared/Views/UserSettings/PrivacySettings.swift | 4 +++-
.../chat/simplex/common/views/usersettings/PrivacySettings.kt | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
index 71ff7b88bf..51bfb96940 100644
--- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
+++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
@@ -356,7 +356,7 @@ struct SimplexLockView: View {
var id: Self { self }
}
- let laDelays: [Int] = [10, 30, 60, 180, 0]
+ let laDelays: [Int] = [10, 30, 60, 180, 600, 0]
func laDelayText(_ t: Int) -> LocalizedStringKey {
let m = t / 60
@@ -378,6 +378,7 @@ struct SimplexLockView: View {
Text(mode.text)
}
}
+ .frame(height: 36)
if performLA {
Picker("Lock after", selection: $laLockDelay) {
let delays = laDelays.contains(laLockDelay) ? laDelays : [laLockDelay] + laDelays
@@ -385,6 +386,7 @@ struct SimplexLockView: View {
Text(laDelayText(t))
}
}
+ .frame(height: 36)
if showChangePassword && laMode == .passcode {
Button("Change passcode") {
changeLAPassword()
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt
index 211fe59671..84ab87c653 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt
@@ -317,7 +317,7 @@ private fun showUserGroupsReceiptsAlert(
)
}
-private val laDelays = listOf(10, 30, 60, 180, 0)
+private val laDelays = listOf(10, 30, 60, 180, 600, 0)
@Composable
fun SimplexLockView(
From 79275424ea4b650e408262491c67495fd7431690 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Sun, 22 Oct 2023 15:06:55 +0100
Subject: [PATCH 49/80] core: 5.4.0.2
---
package.yaml | 2 +-
simplex-chat.cabal | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.yaml b/package.yaml
index 6fed41b2ae..6f333d9bb1 100644
--- a/package.yaml
+++ b/package.yaml
@@ -1,5 +1,5 @@
name: simplex-chat
-version: 5.4.0.1
+version: 5.4.0.2
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme
diff --git a/simplex-chat.cabal b/simplex-chat.cabal
index 824b558135..cdb659a66e 100644
--- a/simplex-chat.cabal
+++ b/simplex-chat.cabal
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
-version: 5.4.0.1
+version: 5.4.0.2
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat
From 3e0b6826bf13255cab1e5b90833e08e92f884b04 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Sun, 22 Oct 2023 17:50:08 +0100
Subject: [PATCH 50/80] ios: 5.3.2 build 179
---
apps/ios/SimpleX.xcodeproj/project.pbxproj | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj
index 1cbe61dea0..c75fa63c52 100644
--- a/apps/ios/SimpleX.xcodeproj/project.pbxproj
+++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj
@@ -1486,7 +1486,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 178;
+ CURRENT_PROJECT_VERSION = 179;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1528,7 +1528,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 178;
+ CURRENT_PROJECT_VERSION = 179;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1608,7 +1608,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 178;
+ CURRENT_PROJECT_VERSION = 179;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1640,7 +1640,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 178;
+ CURRENT_PROJECT_VERSION = 179;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1672,7 +1672,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 178;
+ CURRENT_PROJECT_VERSION = 179;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1718,7 +1718,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 178;
+ CURRENT_PROJECT_VERSION = 179;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
From 1401f562882b4a3d341ac41a8330c6adb031a86a Mon Sep 17 00:00:00 2001
From: Era Dorta
Date: Sun, 22 Oct 2023 19:15:15 +0200
Subject: [PATCH 51/80] cli: update docker image to use ghc 9.6.2 (#3234)
---
Dockerfile | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index b78e2f2448..0c0788c81d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,12 +8,12 @@ RUN a=$(arch); curl https://downloads.haskell.org/~ghcup/$a-linux-ghcup -o /usr/
chmod +x /usr/bin/ghcup
# Install ghc
-RUN ghcup install ghc 8.10.7
+RUN ghcup install ghc 9.6.2
# Install cabal
-RUN ghcup install cabal
+RUN ghcup install cabal 3.10.1.0
# Set both as default
-RUN ghcup set ghc 8.10.7 && \
- ghcup set cabal
+RUN ghcup set ghc 9.6.2 && \
+ ghcup set cabal 3.10.1.0
COPY . /project
WORKDIR /project
From 530ec701711c1767e71457ce5c0de7ae35ff0f24 Mon Sep 17 00:00:00 2001
From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
Date: Mon, 23 Oct 2023 01:47:27 +0800
Subject: [PATCH 52/80] android, desktop: support calls on desktop and moved
www dir to different root (#3219)
* android, desktop: support calls on desktop and moved www dir to different root
* add page title, fix links on Android, change timeouts
* using worker in desktop Chrome and Safari
* ui changes
* end call button in app bar
* fix android
* a lot of enhancements
* fix after merge master
* layout
* sound play on call
---------
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
---
apps/multiplatform/android/build.gradle.kts | 1 +
apps/multiplatform/common/build.gradle.kts | 2 +
.../common/views/call/CallView.android.kt | 86 ++++---
.../views/chatlist/ChatListView.android.kt | 8 +
.../chat/simplex/common/model/ChatModel.kt | 2 +-
.../chat/simplex/common/model/SimpleXAPI.kt | 10 +-
.../simplex/common/views/call/CallManager.kt | 12 +-
.../chat/simplex/common/views/call/WebRTC.kt | 43 ++--
.../simplex/common/views/chat/ChatView.kt | 75 ++++--
.../views/chat/group/GroupMemberInfoView.kt | 5 +-
.../common/views/chat/item/CICallItemView.kt | 4 +-
.../views/chatlist/ChatListNavLinkView.kt | 13 +-
.../common/views/chatlist/ChatListView.kt | 8 +
.../common/views/helpers/AlertManager.kt | 3 +-
.../common/views/helpers/ChatInfoImage.kt | 29 +++
.../resources}/assets/www/README.md | 0
.../resources/assets/www/android}/call.html | 4 +-
.../resources/assets/www/android}/style.css | 0
.../resources}/assets/www/call.html | 0
.../commonMain/resources}/assets/www/call.js | 58 ++++-
.../resources/assets/www/desktop/call.html | 50 ++++
.../www/desktop/images/ic_call_end_filled.svg | 1 +
.../assets/www/desktop/images/ic_mic.svg | 1 +
.../assets/www/desktop/images/ic_mic_off.svg | 1 +
.../www/desktop/images/ic_phone_in_talk.svg | 1 +
.../www/desktop/images/ic_videocam_filled.svg | 1 +
.../www/desktop/images/ic_videocam_off.svg | 1 +
.../www/desktop/images/ic_volume_down.svg | 1 +
.../www/desktop/images/ic_volume_up.svg | 1 +
.../resources/assets/www/desktop/style.css | 127 ++++++++++
.../resources/assets/www/desktop/ui.js | 80 ++++++
.../resources}/assets/www/lz-string.min.js | 0
.../resources/assets/www}/style.css | 0
.../common/platform/RecAndPlay.desktop.kt | 32 ++-
.../common/views/call/CallView.desktop.kt | 239 +++++++++++++++++-
.../chatlist/ChatListNavLinkView.desktop.kt | 2 +-
.../views/chatlist/ChatListView.desktop.kt | 75 ++++++
.../desktopMain/resources/media/ring_once.mp3 | Bin 0 -> 72269 bytes
packages/simplex-chat-webrtc/copy | 24 +-
packages/simplex-chat-webrtc/package.json | 2 +-
.../simplex-chat-webrtc/src/android/call.html | 26 ++
.../simplex-chat-webrtc/src/android/style.css | 41 +++
packages/simplex-chat-webrtc/src/call.ts | 80 ++++--
.../simplex-chat-webrtc/src/desktop/call.html | 50 ++++
.../src/desktop/images/ic_call_end_filled.svg | 1 +
.../src/desktop/images/ic_mic.svg | 1 +
.../src/desktop/images/ic_mic_off.svg | 1 +
.../src/desktop/images/ic_phone_in_talk.svg | 1 +
.../src/desktop/images/ic_videocam_filled.svg | 1 +
.../src/desktop/images/ic_videocam_off.svg | 1 +
.../src/desktop/images/ic_volume_down.svg | 1 +
.../src/desktop/images/ic_volume_up.svg | 1 +
.../simplex-chat-webrtc/src/desktop/style.css | 127 ++++++++++
.../simplex-chat-webrtc/src/desktop/ui.ts | 87 +++++++
54 files changed, 1262 insertions(+), 159 deletions(-)
create mode 100644 apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt
rename apps/multiplatform/{android/src/main => common/src/commonMain/resources}/assets/www/README.md (100%)
rename {packages/simplex-chat-webrtc/src => apps/multiplatform/common/src/commonMain/resources/assets/www/android}/call.html (88%)
rename apps/multiplatform/{android/src/main/assets/www => common/src/commonMain/resources/assets/www/android}/style.css (100%)
rename apps/multiplatform/{android/src/main => common/src/commonMain/resources}/assets/www/call.html (100%)
rename apps/multiplatform/{android/src/main => common/src/commonMain/resources}/assets/www/call.js (92%)
create mode 100644 apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/call.html
create mode 100644 apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/images/ic_call_end_filled.svg
create mode 100644 apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/images/ic_mic.svg
create mode 100644 apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/images/ic_mic_off.svg
create mode 100644 apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/images/ic_phone_in_talk.svg
create mode 100644 apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/images/ic_videocam_filled.svg
create mode 100644 apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/images/ic_videocam_off.svg
create mode 100644 apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/images/ic_volume_down.svg
create mode 100644 apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/images/ic_volume_up.svg
create mode 100644 apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/style.css
create mode 100644 apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/ui.js
rename apps/multiplatform/{android/src/main => common/src/commonMain/resources}/assets/www/lz-string.min.js (100%)
rename {packages/simplex-chat-webrtc/src => apps/multiplatform/common/src/commonMain/resources/assets/www}/style.css (100%)
create mode 100644 apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt
create mode 100644 apps/multiplatform/common/src/desktopMain/resources/media/ring_once.mp3
create mode 100644 packages/simplex-chat-webrtc/src/android/call.html
create mode 100644 packages/simplex-chat-webrtc/src/android/style.css
create mode 100644 packages/simplex-chat-webrtc/src/desktop/call.html
create mode 100644 packages/simplex-chat-webrtc/src/desktop/images/ic_call_end_filled.svg
create mode 100644 packages/simplex-chat-webrtc/src/desktop/images/ic_mic.svg
create mode 100644 packages/simplex-chat-webrtc/src/desktop/images/ic_mic_off.svg
create mode 100644 packages/simplex-chat-webrtc/src/desktop/images/ic_phone_in_talk.svg
create mode 100644 packages/simplex-chat-webrtc/src/desktop/images/ic_videocam_filled.svg
create mode 100644 packages/simplex-chat-webrtc/src/desktop/images/ic_videocam_off.svg
create mode 100644 packages/simplex-chat-webrtc/src/desktop/images/ic_volume_down.svg
create mode 100644 packages/simplex-chat-webrtc/src/desktop/images/ic_volume_up.svg
create mode 100644 packages/simplex-chat-webrtc/src/desktop/style.css
create mode 100644 packages/simplex-chat-webrtc/src/desktop/ui.ts
diff --git a/apps/multiplatform/android/build.gradle.kts b/apps/multiplatform/android/build.gradle.kts
index 67a8fea87b..873f33b22f 100644
--- a/apps/multiplatform/android/build.gradle.kts
+++ b/apps/multiplatform/android/build.gradle.kts
@@ -77,6 +77,7 @@ android {
}
jniLibs.useLegacyPackaging = rootProject.extra["compression.level"] as Int != 0
}
+ android.sourceSets["main"].assets.setSrcDirs(listOf("../common/src/commonMain/resources/assets"))
val isRelease = gradle.startParameter.taskNames.find { it.toLowerCase().contains("release") } != null
val isBundle = gradle.startParameter.taskNames.find { it.toLowerCase().contains("bundle") } != null
// if (isRelease) {
diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts
index 9fb40c93d8..13ca2c309d 100644
--- a/apps/multiplatform/common/build.gradle.kts
+++ b/apps/multiplatform/common/build.gradle.kts
@@ -98,6 +98,8 @@ kotlin {
implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT")
implementation("org.slf4j:slf4j-simple:2.0.7")
implementation("uk.co.caprica:vlcj:4.7.3")
+ implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf85a")
+ implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf85a")
}
}
val desktopTest by getting
diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt
index 260182b5a4..790345e97e 100644
--- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt
+++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt
@@ -18,6 +18,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -43,6 +44,9 @@ import chat.simplex.res.MR
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.datetime.Clock
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
@@ -52,7 +56,7 @@ actual fun ActiveCallView() {
val chatModel = ChatModel
BackHandler(onBack = {
val call = chatModel.activeCall.value
- if (call != null) withApi { chatModel.callManager.endCall(call) }
+ if (call != null) withBGApi { chatModel.callManager.endCall(call) }
})
val audioViaBluetooth = rememberSaveable { mutableStateOf(false) }
val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE }
@@ -112,30 +116,30 @@ actual fun ActiveCallView() {
if (call != null) {
Log.d(TAG, "has active call $call")
when (val r = apiMsg.resp) {
- is WCallResponse.Capabilities -> withApi {
+ is WCallResponse.Capabilities -> withBGApi {
val callType = CallType(call.localMedia, r.capabilities)
chatModel.controller.apiSendCallInvitation(call.contact, callType)
chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities)
}
- is WCallResponse.Offer -> withApi {
+ is WCallResponse.Offer -> withBGApi {
chatModel.controller.apiSendCallOffer(call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
chatModel.activeCall.value = call.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities)
}
- is WCallResponse.Answer -> withApi {
+ is WCallResponse.Answer -> withBGApi {
chatModel.controller.apiSendCallAnswer(call.contact, r.answer, r.iceCandidates)
chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
}
- is WCallResponse.Ice -> withApi {
+ is WCallResponse.Ice -> withBGApi {
chatModel.controller.apiSendCallExtraInfo(call.contact, r.iceCandidates)
}
is WCallResponse.Connection ->
try {
val callStatus = json.decodeFromString("\"${r.state.connectionState}\"")
if (callStatus == WebRTCCallStatus.Connected) {
- chatModel.activeCall.value = call.copy(callState = CallState.Connected)
+ chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectedAt = Clock.System.now())
setCallSound(call.soundSpeaker, audioViaBluetooth)
}
- withApi { chatModel.controller.apiCallStatus(call.contact, callStatus) }
+ withBGApi { chatModel.controller.apiCallStatus(call.contact, callStatus) }
} catch (e: Error) {
Log.d(TAG,"call status ${r.state.connectionState} not used")
}
@@ -145,9 +149,12 @@ actual fun ActiveCallView() {
setCallSound(call.soundSpeaker, audioViaBluetooth)
}
}
+ is WCallResponse.End -> {
+ withBGApi { chatModel.callManager.endCall(call) }
+ }
is WCallResponse.Ended -> {
chatModel.activeCall.value = call.copy(callState = CallState.Ended)
- withApi { chatModel.callManager.endCall(call) }
+ withBGApi { chatModel.callManager.endCall(call) }
chatModel.showCallView.value = false
}
is WCallResponse.Ok -> when (val cmd = apiMsg.command) {
@@ -162,7 +169,7 @@ actual fun ActiveCallView() {
is WCallCommand.Camera -> {
chatModel.activeCall.value = call.copy(localCamera = cmd.camera)
if (!call.audioEnabled) {
- chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Audio, enable = false)
+ chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false))
}
}
is WCallCommand.End ->
@@ -187,11 +194,14 @@ actual fun ActiveCallView() {
// Lock orientation to portrait in order to have good experience with calls
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
chatModel.activeCallViewIsVisible.value = true
+ // After the first call, End command gets added to the list which prevents making another calls
+ chatModel.callCommand.removeAll { it is WCallCommand.End }
onDispose {
activity.volumeControlStream = prevVolumeControlStream
// Unlock orientation
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
chatModel.activeCallViewIsVisible.value = false
+ chatModel.callCommand.clear()
}
}
}
@@ -201,9 +211,9 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetoot
ActiveCallOverlayLayout(
call = call,
speakerCanBeEnabled = !audioViaBluetooth.value,
- dismiss = { withApi { chatModel.callManager.endCall(call) } },
- toggleAudio = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled) },
- toggleVideo = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled) },
+ dismiss = { withBGApi { chatModel.callManager.endCall(call) } },
+ toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled)) },
+ toggleVideo = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled)) },
toggleSound = {
var call = chatModel.activeCall.value
if (call != null) {
@@ -212,7 +222,7 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetoot
setCallSound(call.soundSpeaker, audioViaBluetooth)
}
},
- flipCamera = { chatModel.callCommand.value = WCallCommand.Camera(call.localCamera.flipped) }
+ flipCamera = { chatModel.callCommand.add(WCallCommand.Camera(call.localCamera.flipped)) }
)
}
@@ -439,7 +449,7 @@ private fun DisabledBackgroundCallsButton() {
//}
@Composable
-fun WebRTCView(callCommand: MutableState, onResponse: (WVAPIMessage) -> Unit) {
+fun WebRTCView(callCommand: SnapshotStateList, onResponse: (WVAPIMessage) -> Unit) {
val scope = rememberCoroutineScope()
val webView = remember { mutableStateOf(null) }
val permissionsState = rememberMultiplePermissionsState(
@@ -470,13 +480,19 @@ fun WebRTCView(callCommand: MutableState, onResponse: (WVAPIMessa
webView.value = null
}
}
- LaunchedEffect(callCommand.value, webView.value) {
- val cmd = callCommand.value
- val wv = webView.value
- if (cmd != null && wv != null) {
- Log.d(TAG, "WebRTCView LaunchedEffect executing $cmd")
- processCommand(wv, cmd)
- callCommand.value = null
+ val wv = webView.value
+ if (wv != null) {
+ LaunchedEffect(Unit) {
+ snapshotFlow { callCommand.firstOrNull() }
+ .distinctUntilChanged()
+ .filterNotNull()
+ .collect {
+ while (callCommand.isNotEmpty()) {
+ val cmd = callCommand.removeFirst()
+ Log.d(TAG, "WebRTCView LaunchedEffect executing $cmd")
+ processCommand(wv, cmd)
+ }
+ }
}
}
val assetLoader = WebViewAssetLoader.Builder()
@@ -502,7 +518,7 @@ fun WebRTCView(callCommand: MutableState, onResponse: (WVAPIMessa
}
}
}
- this.webViewClient = LocalContentWebViewClient(assetLoader)
+ this.webViewClient = LocalContentWebViewClient(webView, assetLoader)
this.clearHistory()
this.clearCache(true)
this.addJavascriptInterface(WebRTCInterface(onResponse), "WebRTCInterface")
@@ -512,19 +528,10 @@ fun WebRTCView(callCommand: MutableState, onResponse: (WVAPIMessa
webViewSettings.javaScriptEnabled = true
webViewSettings.mediaPlaybackRequiresUserGesture = false
webViewSettings.cacheMode = WebSettings.LOAD_NO_CACHE
- this.loadUrl("file:android_asset/www/call.html")
+ this.loadUrl("file:android_asset/www/android/call.html")
}
}
- ) { wv ->
- Log.d(TAG, "WebRTCView: webview ready")
- // for debugging
- // wv.evaluateJavascript("sendMessageToNative = ({resp}) => WebRTCInterface.postMessage(JSON.stringify({command: resp}))", null)
- scope.launch {
- delay(2000L)
- wv.evaluateJavascript("sendMessageToNative = (msg) => WebRTCInterface.postMessage(JSON.stringify(msg))", null)
- webView.value = wv
- }
- }
+ ) { /* WebView */ }
}
}
}
@@ -539,19 +546,28 @@ class WebRTCInterface(private val onResponse: (WVAPIMessage) -> Unit) {
// for debugging
// onResponse(message)
onResponse(json.decodeFromString(message))
- } catch (e: Error) {
+ } catch (e: Exception) {
Log.e(TAG, "failed parsing WebView message: $message")
}
}
}
-private class LocalContentWebViewClient(private val assetLoader: WebViewAssetLoader) : WebViewClientCompat() {
+private class LocalContentWebViewClient(val webView: MutableState, private val assetLoader: WebViewAssetLoader) : WebViewClientCompat() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
return assetLoader.shouldInterceptRequest(request.url)
}
+
+ override fun onPageFinished(view: WebView, url: String) {
+ super.onPageFinished(view, url)
+ view.evaluateJavascript("sendMessageToNative = (msg) => WebRTCInterface.postMessage(JSON.stringify(msg))", null)
+ webView.value = view
+ Log.d(TAG, "WebRTCView: webview ready")
+ // for debugging
+ // view.evaluateJavascript("sendMessageToNative = ({resp}) => WebRTCInterface.postMessage(JSON.stringify({command: resp}))", null)
+ }
}
@Preview
diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt
new file mode 100644
index 0000000000..cb74664a48
--- /dev/null
+++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt
@@ -0,0 +1,8 @@
+package chat.simplex.common.views.chatlist
+
+import androidx.compose.runtime.*
+import chat.simplex.common.views.helpers.*
+import kotlinx.coroutines.flow.MutableStateFlow
+
+@Composable
+actual fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow) {}
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 ad033387ab..767c678c1a 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
@@ -88,7 +88,7 @@ object ChatModel {
val activeCallInvitation = mutableStateOf(null)
val activeCall = mutableStateOf(null)
val activeCallViewIsVisible = mutableStateOf(false)
- val callCommand = mutableStateOf(null)
+ val callCommand = mutableStateListOf()
val showCallView = mutableStateOf(false)
val switchingCall = mutableStateOf(false)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
index da09ea132f..58850ce056 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
@@ -1647,25 +1647,25 @@ object ChatController {
val useRelay = appPrefs.webrtcPolicyRelay.get()
val iceServers = getIceServers()
Log.d(TAG, ".callOffer iceServers $iceServers")
- chatModel.callCommand.value = WCallCommand.Offer(
+ chatModel.callCommand.add(WCallCommand.Offer(
offer = r.offer.rtcSession,
iceCandidates = r.offer.rtcIceCandidates,
media = r.callType.media,
aesKey = r.sharedKey,
iceServers = iceServers,
relay = useRelay
- )
+ ))
}
}
is CR.CallAnswer -> {
withCall(r, r.contact) { call ->
chatModel.activeCall.value = call.copy(callState = CallState.AnswerReceived)
- chatModel.callCommand.value = WCallCommand.Answer(answer = r.answer.rtcSession, iceCandidates = r.answer.rtcIceCandidates)
+ chatModel.callCommand.add(WCallCommand.Answer(answer = r.answer.rtcSession, iceCandidates = r.answer.rtcIceCandidates))
}
}
is CR.CallExtraInfo -> {
withCall(r, r.contact) { _ ->
- chatModel.callCommand.value = WCallCommand.Ice(iceCandidates = r.extraInfo.rtcIceCandidates)
+ chatModel.callCommand.add(WCallCommand.Ice(iceCandidates = r.extraInfo.rtcIceCandidates))
}
}
is CR.CallEnded -> {
@@ -1674,7 +1674,7 @@ object ChatController {
chatModel.callManager.reportCallRemoteEnded(invitation = invitation)
}
withCall(r, r.contact) { _ ->
- chatModel.callCommand.value = WCallCommand.End
+ chatModel.callCommand.add(WCallCommand.End)
withApi {
chatModel.activeCall.value = null
chatModel.showCallView.value = false
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt
index fe4da718a8..f601776f98 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt
@@ -3,8 +3,6 @@ package chat.simplex.common.views.call
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.withApi
-import chat.simplex.common.views.helpers.withBGApi
-import chat.simplex.common.views.usersettings.showInDevelopingAlert
import kotlinx.datetime.Clock
import kotlin.time.Duration.Companion.minutes
@@ -26,10 +24,6 @@ class CallManager(val chatModel: ChatModel) {
}
fun acceptIncomingCall(invitation: RcvCallInvitation) {
- if (appPlatform.isDesktop) {
- return showInDevelopingAlert()
- }
-
val call = chatModel.activeCall.value
if (call == null) {
justAcceptIncomingCall(invitation = invitation)
@@ -58,12 +52,12 @@ class CallManager(val chatModel: ChatModel) {
val useRelay = controller.appPrefs.webrtcPolicyRelay.get()
val iceServers = getIceServers()
Log.d(TAG, "answerIncomingCall iceServers: $iceServers")
- callCommand.value = WCallCommand.Start(
+ callCommand.add(WCallCommand.Start(
media = invitation.callType.media,
aesKey = invitation.sharedKey,
iceServers = iceServers,
relay = useRelay
- )
+ ))
callInvitations.remove(invitation.contact.id)
if (invitation.contact.id == activeCallInvitation.value?.contact?.id) {
activeCallInvitation.value = null
@@ -80,7 +74,7 @@ class CallManager(val chatModel: ChatModel) {
showCallView.value = false
} else {
Log.d(TAG, "CallManager.endCall: ending call...")
- callCommand.value = WCallCommand.End
+ callCommand.add(WCallCommand.End)
showCallView.value = false
controller.apiEndCall(call.contact)
activeCall.value = null
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt
index 3e14414fc7..4be49d4c07 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt
@@ -1,7 +1,5 @@
package chat.simplex.common.views.call
-import androidx.compose.runtime.Composable
-import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.common.model.*
import chat.simplex.res.MR
@@ -23,16 +21,17 @@ data class Call(
val videoEnabled: Boolean = localMedia == CallMediaType.Video,
val soundSpeaker: Boolean = localMedia == CallMediaType.Video,
var localCamera: VideoCamera = VideoCamera.User,
- val connectionInfo: ConnectionInfo? = null
+ val connectionInfo: ConnectionInfo? = null,
+ var connectedAt: Instant? = null
) {
val encrypted: Boolean get() = localEncrypted && sharedKey != null
val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false
- val encryptionStatus: String @Composable get() = when(callState) {
+ val encryptionStatus: String get() = when(callState) {
CallState.WaitCapabilities -> ""
- CallState.InvitationSent -> stringResource(if (localEncrypted) MR.strings.status_e2e_encrypted else MR.strings.status_no_e2e_encryption)
- CallState.InvitationAccepted -> stringResource(if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_contact_has_e2e_encryption)
- else -> stringResource(if (!localEncrypted) MR.strings.status_no_e2e_encryption else if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_e2e_encrypted)
+ CallState.InvitationSent -> generalGetString(if (localEncrypted) MR.strings.status_e2e_encrypted else MR.strings.status_no_e2e_encryption)
+ CallState.InvitationAccepted -> generalGetString(if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_contact_has_e2e_encryption)
+ else -> generalGetString(if (!localEncrypted) MR.strings.status_no_e2e_encryption else if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_e2e_encrypted)
}
val hasMedia: Boolean get() = callState == CallState.OfferSent || callState == CallState.Negotiated || callState == CallState.Connected
@@ -49,16 +48,16 @@ enum class CallState {
Connected,
Ended;
- val text: String @Composable get() = when(this) {
- WaitCapabilities -> stringResource(MR.strings.callstate_starting)
- InvitationSent -> stringResource(MR.strings.callstate_waiting_for_answer)
- InvitationAccepted -> stringResource(MR.strings.callstate_starting)
- OfferSent -> stringResource(MR.strings.callstate_waiting_for_confirmation)
- OfferReceived -> stringResource(MR.strings.callstate_received_answer)
- AnswerReceived -> stringResource(MR.strings.callstate_received_confirmation)
- Negotiated -> stringResource(MR.strings.callstate_connecting)
- Connected -> stringResource(MR.strings.callstate_connected)
- Ended -> stringResource(MR.strings.callstate_ended)
+ val text: String get() = when(this) {
+ WaitCapabilities -> generalGetString(MR.strings.callstate_starting)
+ InvitationSent -> generalGetString(MR.strings.callstate_waiting_for_answer)
+ InvitationAccepted -> generalGetString(MR.strings.callstate_starting)
+ OfferSent -> generalGetString(MR.strings.callstate_waiting_for_confirmation)
+ OfferReceived -> generalGetString(MR.strings.callstate_received_answer)
+ AnswerReceived -> generalGetString(MR.strings.callstate_received_confirmation)
+ Negotiated -> generalGetString(MR.strings.callstate_connecting)
+ Connected -> generalGetString(MR.strings.callstate_connected)
+ Ended -> generalGetString(MR.strings.callstate_ended)
}
}
@@ -67,13 +66,14 @@ enum class CallState {
@Serializable
sealed class WCallCommand {
- @Serializable @SerialName("capabilities") object Capabilities: WCallCommand()
+ @Serializable @SerialName("capabilities") data class Capabilities(val media: CallMediaType): WCallCommand()
@Serializable @SerialName("start") data class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List? = null, val relay: Boolean? = null): WCallCommand()
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List? = null, val relay: Boolean? = null): WCallCommand()
@Serializable @SerialName("answer") data class Answer (val answer: String, val iceCandidates: String): WCallCommand()
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallCommand()
@Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
@Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand()
+ @Serializable @SerialName("description") data class Description(val state: String, val description: String): WCallCommand()
@Serializable @SerialName("end") object End: WCallCommand()
}
@@ -85,6 +85,7 @@ sealed class WCallResponse {
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallResponse()
@Serializable @SerialName("connection") data class Connection(val state: ConnectionState): WCallResponse()
@Serializable @SerialName("connected") data class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
+ @Serializable @SerialName("end") object End: WCallResponse()
@Serializable @SerialName("ended") object Ended: WCallResponse()
@Serializable @SerialName("ok") object Ok: WCallResponse()
@Serializable @SerialName("error") data class Error(val message: String): WCallResponse()
@@ -106,14 +107,14 @@ sealed class WCallResponse {
}
@Serializable data class CallCapabilities(val encryption: Boolean)
@Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
- val text: String @Composable get() {
+ val text: String get() {
val local = localCandidate?.candidateType
val remote = remoteCandidate?.candidateType
return when {
local == RTCIceCandidateType.Host && remote == RTCIceCandidateType.Host ->
- stringResource(MR.strings.call_connection_peer_to_peer)
+ generalGetString(MR.strings.call_connection_peer_to_peer)
local == RTCIceCandidateType.Relay && remote == RTCIceCandidateType.Relay ->
- stringResource(MR.strings.call_connection_via_relay)
+ generalGetString(MR.strings.call_connection_via_relay)
else ->
"${local?.value ?: "unknown"} / ${remote?.value ?: "unknown"}"
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt
index 4afcdacbd8..40f2b32e58 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt
@@ -33,7 +33,6 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.*
import chat.simplex.common.platform.AudioPlayer
-import chat.simplex.common.views.usersettings.showInDevelopingAlert
import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@@ -274,23 +273,24 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
withApi { chatModel.controller.apiJoinGroup(groupId) }
},
startCall = out@ { media ->
- if (appPlatform.isDesktop) {
- return@out showInDevelopingAlert()
- }
withBGApi {
val cInfo = chat.chatInfo
if (cInfo is ChatInfo.Direct) {
chatModel.activeCall.value = Call(contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media)
chatModel.showCallView.value = true
- chatModel.callCommand.value = WCallCommand.Capabilities
+ chatModel.callCommand.add(WCallCommand.Capabilities(media))
}
}
},
+ endCall = {
+ val call = chatModel.activeCall.value
+ if (call != null) withApi { chatModel.callManager.endCall(call) }
+ },
acceptCall = { contact ->
hideKeyboard(view)
val invitation = chatModel.callInvitations.remove(contact.id)
if (invitation == null) {
- AlertManager.shared.showAlertMsg("Call already ended!")
+ AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
} else {
chatModel.callManager.acceptIncomingCall(invitation = invitation)
}
@@ -433,6 +433,7 @@ fun ChatLayout(
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit,
startCall: (CallMediaType) -> Unit,
+ endCall: () -> Unit,
acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
openDirectChat: (Long) -> Unit,
@@ -491,7 +492,7 @@ fun ChatLayout(
}
Scaffold(
- topBar = { ChatInfoToolbar(chat, back, info, startCall, addMembers, changeNtfsState, onSearchValueChanged) },
+ topBar = { ChatInfoToolbar(chat, back, info, startCall, endCall, addMembers, changeNtfsState, onSearchValueChanged) },
bottomBar = composeView,
modifier = Modifier.navigationBarsWithImePadding(),
floatingActionButton = { floatingButton.value() },
@@ -520,6 +521,7 @@ fun ChatInfoToolbar(
back: () -> Unit,
info: () -> Unit,
startCall: (CallMediaType) -> Unit,
+ endCall: () -> Unit,
addMembers: (GroupInfo) -> Unit,
changeNtfsState: (Boolean, currentValue: MutableState) -> Unit,
onSearchValueChanged: (String) -> Unit,
@@ -540,6 +542,7 @@ fun ChatInfoToolbar(
}
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
val menuItems = arrayListOf<@Composable () -> Unit>()
+ val activeCall by remember { chatModel.activeCall }
menuItems.add {
ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = {
showMenu.value = false
@@ -548,20 +551,52 @@ fun ChatInfoToolbar(
}
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.allowsFeature(ChatFeature.Calls)) {
- barButtons.add {
- IconButton({
- showMenu.value = false
- startCall(CallMediaType.Audio)
- },
- enabled = chat.chatInfo.contact.ready && chat.chatInfo.contact.active) {
- Icon(
- painterResource(MR.images.ic_call_500),
- stringResource(MR.strings.icon_descr_more_button),
- tint = if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
- )
+ if (activeCall == null) {
+ barButtons.add {
+ IconButton(
+ {
+ showMenu.value = false
+ startCall(CallMediaType.Audio)
+ },
+ enabled = chat.chatInfo.contact.ready && chat.chatInfo.contact.active
+ ) {
+ Icon(
+ painterResource(MR.images.ic_call_500),
+ stringResource(MR.strings.icon_descr_more_button),
+ tint = if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
+ )
+ }
+ }
+ } else if (activeCall?.contact?.id == chat.id) {
+ barButtons.add {
+ val call = remember { chatModel.activeCall }.value
+ val connectedAt = call?.connectedAt
+ if (connectedAt != null) {
+ val time = remember { mutableStateOf(durationText(0)) }
+ LaunchedEffect(Unit) {
+ while (true) {
+ time.value = durationText((Clock.System.now() - connectedAt).inWholeSeconds.toInt())
+ delay(250)
+ }
+ }
+ val sp50 = with(LocalDensity.current) { 50.sp.toDp() }
+ Text(time.value, Modifier.widthIn(min = sp50))
+ }
+ }
+ barButtons.add {
+ IconButton({
+ showMenu.value = false
+ endCall()
+ }) {
+ Icon(
+ painterResource(MR.images.ic_call_end_filled),
+ null,
+ tint = MaterialTheme.colors.error
+ )
+ }
}
}
- if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) {
+ if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active && activeCall == null) {
menuItems.add {
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
showMenu.value = false
@@ -1290,6 +1325,7 @@ fun PreviewChatLayout() {
cancelFile = {},
joinGroup = {},
startCall = {},
+ endCall = {},
acceptCall = { _ -> },
acceptFeature = { _, _, _ -> },
openDirectChat = { _ -> },
@@ -1359,6 +1395,7 @@ fun PreviewGroupChatLayout() {
cancelFile = {},
joinGroup = {},
startCall = {},
+ endCall = {},
acceptCall = { _ -> },
acceptFeature = { _, _, _ -> },
openDirectChat = { _ -> },
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt
index e281c57622..e486aca4a0 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt
@@ -3,7 +3,6 @@ package chat.simplex.common.views.chat.group
import InfoRow
import SectionBottomSpacer
import SectionDividerSpaced
-import SectionItemView
import SectionSpacer
import SectionTextFooter
import SectionView
@@ -35,7 +34,7 @@ import chat.simplex.common.views.newchat.*
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.*
-import chat.simplex.common.views.chatlist.openChat
+import chat.simplex.common.views.chatlist.openLoadedChat
import chat.simplex.res.MR
import kotlinx.datetime.Clock
@@ -87,7 +86,7 @@ fun GroupMemberInfoView(
if (memberContact != null) {
val memberChat = Chat(ChatInfo.Direct(memberContact), chatItems = arrayListOf())
chatModel.addChat(memberChat)
- openChat(memberChat, chatModel)
+ openLoadedChat(memberChat, chatModel)
closeAll()
chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected())
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt
index 3e4dce7f4a..6f165515ec 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt
@@ -68,7 +68,7 @@ fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) {
// sharedKey: invitation.sharedKey
// )
// m.showCallView = true
-// m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey, useWorker: true)
+// m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey: true)
// } else {
// AlertManager.shared.showAlertMsg(title: "Call already ended!")
// }
@@ -141,7 +141,7 @@ fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) {
// sharedKey: invitation.sharedKey
// )
// m.showCallView = true
-// m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey, useWorker: true)
+// m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey: true)
// } else {
// AlertManager.shared.showAlertMsg(title: "Call already ended!")
// }
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 b16bf4c9ba..2ff33ead57 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
@@ -4,7 +4,6 @@ import SectionItemView
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
@@ -13,10 +12,6 @@ import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.desktop.ui.tooling.preview.Preview
-import androidx.compose.foundation.*
-import androidx.compose.foundation.interaction.InteractionSource
-import androidx.compose.ui.graphics.drawscope.ContentDrawScope
-import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -126,14 +121,14 @@ fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel) {
suspend fun openDirectChat(contactId: Long, chatModel: ChatModel) {
val chat = chatModel.controller.apiGetChat(ChatType.Direct, contactId)
if (chat != null) {
- openChat(chat, chatModel)
+ openLoadedChat(chat, chatModel)
}
}
suspend fun openGroupChat(groupId: Long, chatModel: ChatModel) {
val chat = chatModel.controller.apiGetChat(ChatType.Group, groupId)
if (chat != null) {
- openChat(chat, chatModel)
+ openLoadedChat(chat, chatModel)
}
}
@@ -141,12 +136,12 @@ suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) {
Log.d(TAG, "TODOCHAT: openChat: opening ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId)
if (chat != null) {
- openChat(chat, chatModel)
+ openLoadedChat(chat, chatModel)
Log.d(TAG, "TODOCHAT: openChat: opened ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
}
}
-suspend fun openChat(chat: Chat, chatModel: ChatModel) {
+fun openLoadedChat(chat: Chat, chatModel: ChatModel) {
chatModel.chatItems.clear()
chatModel.chatItemStatuses.clear()
chatModel.chatItems.addAll(chat.chatItems)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt
index 66ef1cf9f9..4df62e12e3 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt
@@ -12,6 +12,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
@@ -29,6 +30,9 @@ import chat.simplex.common.views.onboarding.shouldShowWhatsNew
import chat.simplex.common.views.usersettings.SettingsView
import chat.simplex.common.views.usersettings.simplexTeamUri
import chat.simplex.common.platform.*
+import chat.simplex.common.views.call.Call
+import chat.simplex.common.views.call.CallMediaType
+import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR
import kotlinx.coroutines.*
@@ -121,6 +125,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
}
}
if (searchInList.isEmpty()) {
+ DesktopActiveCallOverlayLayout(newChatSheetState)
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
}
if (appPlatform.isAndroid) {
@@ -311,6 +316,9 @@ private fun ProgressIndicator() {
)
}
+@Composable
+expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow)
+
fun connectIfOpenedViaUri(uri: URI, chatModel: ChatModel) {
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
if (chatModel.currentUser.value == null) {
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt
index d8466e9d96..35d5b8b3ef 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt
@@ -50,11 +50,12 @@ class AlertManager {
fun showAlertDialogButtonsColumn(
title: String,
text: AnnotatedString? = null,
+ onDismissRequest: (() -> Unit)? = null,
buttons: @Composable () -> Unit,
) {
showAlert {
AlertDialog(
- onDismissRequest = ::hideAlert,
+ onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = {
Text(
title,
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt
index 3a799eddf8..abc894942f 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt
@@ -81,6 +81,35 @@ fun ProfileImage(
}
}
+/** [AccountCircleFilled] has its inner padding which leads to visible border if there is background underneath.
+ * This is workaround
+ * */
+@Composable
+fun ProfileImageForActiveCall(
+ size: Dp,
+ image: String? = null,
+ color: Color = MaterialTheme.colors.secondaryVariant,
+) {
+ if (image == null) {
+ Box(Modifier.requiredSize(size).clip(CircleShape)) {
+ Icon(
+ AccountCircleFilled,
+ contentDescription = stringResource(MR.strings.icon_descr_profile_image_placeholder),
+ tint = color,
+ modifier = Modifier.requiredSize(size + 14.dp)
+ )
+ }
+ } else {
+ val imageBitmap = base64ToBitmap(image)
+ Image(
+ imageBitmap,
+ stringResource(MR.strings.image_descr_profile_image),
+ contentScale = ContentScale.Crop,
+ modifier = Modifier.size(size).clip(CircleShape)
+ )
+ }
+}
+
@Preview
@Composable
diff --git a/apps/multiplatform/android/src/main/assets/www/README.md b/apps/multiplatform/common/src/commonMain/resources/assets/www/README.md
similarity index 100%
rename from apps/multiplatform/android/src/main/assets/www/README.md
rename to apps/multiplatform/common/src/commonMain/resources/assets/www/README.md
diff --git a/packages/simplex-chat-webrtc/src/call.html b/apps/multiplatform/common/src/commonMain/resources/assets/www/android/call.html
similarity index 88%
rename from packages/simplex-chat-webrtc/src/call.html
rename to apps/multiplatform/common/src/commonMain/resources/assets/www/android/call.html
index d7b3f6cefc..46910bfaf1 100644
--- a/packages/simplex-chat-webrtc/src/call.html
+++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/android/call.html
@@ -3,7 +3,7 @@
-
+