diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 77c024b18..a03d3a79f 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -279,7 +279,7 @@ processChatCommand = \case APIGetChatItems _pagination -> pure $ chatCmdError "not implemented" APISendMessage (ChatRef cType chatId) (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user@User {userId} -> withChatLock "sendMessage" $ case cType of CTDirect -> do - ct@Contact {localDisplayName = c, contactUsed} <- withStore $ \db -> getContact db userId chatId + ct@Contact {localDisplayName = c, contactUsed} <- withStore $ \db -> getContact db user chatId unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct (fileInvitation_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer ct (msgContainer, quotedItem_) <- prepareMsg fileInvitation_ @@ -396,7 +396,7 @@ processChatCommand = \case unzipMaybe3 _ = (Nothing, Nothing, Nothing) APIUpdateChatItem (ChatRef cType chatId) itemId mc -> withUser $ \user@User {userId} -> withChatLock "updateChatItem" $ case cType of CTDirect -> do - (ct@Contact {contactId, localDisplayName = c}, ci) <- withStore $ \db -> (,) <$> getContact db userId chatId <*> getDirectChatItem db userId chatId itemId + (ct@Contact {contactId, localDisplayName = c}, ci) <- withStore $ \db -> (,) <$> getContact db user chatId <*> getDirectChatItem db userId chatId itemId case ci of CChatItem SMDSnd ChatItem {meta = CIMeta {itemSharedMsgId}, content = ciContent} -> do case (ciContent, itemSharedMsgId) of @@ -425,7 +425,7 @@ processChatCommand = \case CTContactConnection -> pure $ chatCmdError "not supported" APIDeleteChatItem (ChatRef cType chatId) itemId mode -> withUser $ \user@User {userId} -> withChatLock "deleteChatItem" $ case cType of CTDirect -> do - (ct@Contact {localDisplayName = c}, CChatItem msgDir deletedItem@ChatItem {meta = CIMeta {itemSharedMsgId}, file}) <- withStore $ \db -> (,) <$> getContact db userId chatId <*> getDirectChatItem db userId chatId itemId + (ct@Contact {localDisplayName = c}, CChatItem msgDir deletedItem@ChatItem {meta = CIMeta {itemSharedMsgId}, file}) <- withStore $ \db -> (,) <$> getContact db user chatId <*> getDirectChatItem db userId chatId itemId case (mode, msgDir, itemSharedMsgId) of (CIDMInternal, _, _) -> do deleteCIFile user file @@ -468,10 +468,10 @@ processChatCommand = \case CTGroup -> withStore' (\db -> updateGroupChatItemsRead db chatId fromToIds) $> CRCmdOk CTContactRequest -> pure $ chatCmdError "not supported" CTContactConnection -> pure $ chatCmdError "not supported" - APIChatUnread (ChatRef cType chatId) unreadChat -> withUser $ \user@User {userId} -> case cType of + APIChatUnread (ChatRef cType chatId) unreadChat -> withUser $ \user -> case cType of CTDirect -> do withStore $ \db -> do - ct <- getContact db userId chatId + ct <- getContact db user chatId liftIO $ updateContactUnreadChat db user ct unreadChat pure CRCmdOk CTGroup -> do @@ -482,7 +482,7 @@ processChatCommand = \case _ -> pure $ chatCmdError "not supported" APIDeleteChat (ChatRef cType chatId) -> withUser $ \user@User {userId} -> case cType of CTDirect -> do - ct@Contact {localDisplayName} <- withStore $ \db -> getContact db userId chatId + ct@Contact {localDisplayName} <- withStore $ \db -> getContact db user chatId filesInfo <- withStore' $ \db -> getContactFileInfo db user ct conns <- withStore $ \db -> getContactConnections db userId ct withChatLock "deleteChat direct" . procCmd $ do @@ -516,9 +516,9 @@ processChatCommand = \case withStore' $ \db -> deleteGroup db user gInfo pure $ CRGroupDeletedUser gInfo CTContactRequest -> pure $ chatCmdError "not supported" - APIClearChat (ChatRef cType chatId) -> withUser $ \user@User {userId} -> case cType of + APIClearChat (ChatRef cType chatId) -> withUser $ \user -> case cType of CTDirect -> do - ct <- withStore $ \db -> getContact db userId chatId + ct <- withStore $ \db -> getContact db user chatId filesInfo <- withStore' $ \db -> getContactFileInfo db user ct maxItemTs_ <- withStore' $ \db -> getContactMaxItemTs db user ct forM_ filesInfo $ \fileInfo -> deleteFile user fileInfo @@ -561,7 +561,7 @@ processChatCommand = \case pure $ CRContactRequestRejected cReq APISendCallInvitation contactId callType -> withUser $ \user@User {userId} -> do -- party initiating call - ct <- withStore $ \db -> getContact db userId contactId + ct <- withStore $ \db -> getContact db user contactId calls <- asks currentCalls withChatLock "sendCallInvitation" $ do callId <- CallId <$> (asks idsDrg >>= liftIO . (`randomBytes` 16)) @@ -629,27 +629,27 @@ processChatCommand = \case (SndMessage {msgId}, _) <- sendDirectContactMessage ct (XCallEnd callId) updateCallItemStatus userId ct call WCSDisconnected $ Just msgId pure Nothing - APIGetCallInvitations -> withUser $ \User {userId} -> do + APIGetCallInvitations -> withUser $ \user -> do calls <- asks currentCalls >>= readTVarIO let invs = mapMaybe callInvitation $ M.elems calls - CRCallInvitations <$> mapM (rcvCallInvitation userId) invs + CRCallInvitations <$> mapM (rcvCallInvitation user) invs where callInvitation Call {contactId, callState, callTs} = case callState of CallInvitationReceived {peerCallType, sharedKey} -> Just (contactId, callTs, peerCallType, sharedKey) _ -> Nothing - rcvCallInvitation userId (contactId, callTs, peerCallType, sharedKey) = do - contact <- withStore (\db -> getContact db userId contactId) + rcvCallInvitation user (contactId, callTs, peerCallType, sharedKey) = do + contact <- withStore (\db -> getContact db user contactId) pure RcvCallInvitation {contact, callType = peerCallType, sharedKey, callTs} APICallStatus contactId receivedStatus -> withCurrentCall contactId $ \userId ct call -> updateCallItemStatus userId ct call receivedStatus Nothing $> Just call APIUpdateProfile profile -> withUser (`updateProfile` profile) - APISetContactPrefs contactId prefs' -> withUser $ \user@User {userId} -> do - ct <- withStore $ \db -> getContact db userId contactId + APISetContactPrefs contactId prefs' -> withUser $ \user -> do + ct <- withStore $ \db -> getContact db user contactId updateContactPrefs user ct prefs' - APISetContactAlias contactId localAlias -> withUser $ \User {userId} -> do + APISetContactAlias contactId localAlias -> withUser $ \user@User {userId} -> do ct' <- withStore $ \db -> do - ct <- getContact db userId contactId + ct <- getContact db user contactId liftIO $ updateContactAlias db userId ct localAlias pure $ CRContactAliasUpdated ct' APISetConnectionAlias connId localAlias -> withUser $ \User {userId} -> do @@ -692,10 +692,10 @@ processChatCommand = \case APIGetChatItemTTL -> CRChatItemTTL <$> withUser (\user -> withStore' (`getChatItemTTL` user)) APISetNetworkConfig cfg -> withUser' $ \_ -> withAgent (`setNetworkConfig` cfg) $> CRCmdOk APIGetNetworkConfig -> CRNetworkConfig <$> withUser' (\_ -> withAgent getNetworkConfig) - APISetChatSettings (ChatRef cType chatId) chatSettings -> withUser $ \user@User {userId} -> case cType of + APISetChatSettings (ChatRef cType chatId) chatSettings -> withUser $ \user -> case cType of CTDirect -> do ct <- withStore $ \db -> do - ct <- getContact db userId chatId + ct <- getContact db user chatId liftIO $ updateContactSettings db user chatId chatSettings pure ct withAgent $ \a -> toggleConnectionNtfs a (contactConnId ct) (enableNtfs chatSettings) @@ -709,9 +709,9 @@ processChatCommand = \case withAgent (\a -> toggleConnectionNtfs a connId $ enableNtfs chatSettings) `catchError` (toView . CRChatError) pure CRCmdOk _ -> pure $ chatCmdError "not supported" - APIContactInfo contactId -> withUser $ \User {userId} -> do + 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 userId contactId + ct@Contact {activeConn = Connection {customUserProfileId}} <- withStore $ \db -> getContact db user contactId incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) connectionStats <- withAgent (`getConnectionServers` contactConnId ct) pure $ CRContactInfo ct connectionStats (fmap fromLocalProfile incognitoProfile) @@ -719,8 +719,8 @@ processChatCommand = \case (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m) pure $ CRGroupMemberInfo g m connectionStats - APISwitchContact contactId -> withUser $ \User {userId} -> do - ct <- withStore $ \db -> getContact db userId contactId + APISwitchContact contactId -> withUser $ \user -> do + ct <- withStore $ \db -> getContact db user contactId withAgent $ \a -> switchConnectionAsync a "" $ contactConnId ct pure CRCmdOk APISwitchGroupMember gId gMemberId -> withUser $ \user -> do @@ -837,9 +837,9 @@ processChatCommand = \case gVar <- asks idsDrg groupInfo <- withStore (\db -> createNewGroup db gVar user gProfile) pure $ CRGroupCreated groupInfo - APIAddMember groupId contactId memRole -> withUser $ \user@User {userId} -> withChatLock "addMember" $ do + APIAddMember groupId contactId memRole -> withUser $ \user -> withChatLock "addMember" $ do -- TODO for large groups: no need to load all members to determine if contact is a member - (group, contact) <- withStore $ \db -> (,) <$> getGroup db user groupId <*> getContact db userId contactId + (group, contact) <- withStore $ \db -> (,) <$> getGroup db user groupId <*> getContact db user contactId let Group gInfo@GroupInfo {membership} members = group GroupMember {memberRole = userRole} = membership Contact {localDisplayName = cName} = contact @@ -891,7 +891,7 @@ processChatCommand = \case Just m -> changeMemberRole user gInfo members m $ SGEMemberRole memberId (fromLocalProfile $ memberProfile m) memRole _ -> throwChatError CEGroupMemberNotFound where - changeMemberRole user@User {userId} gInfo@GroupInfo {membership} members m gEvent = do + changeMemberRole user gInfo@GroupInfo {membership} members m gEvent = do let GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberContactId, localDisplayName = cName} = m GroupMember {memberRole = userRole} = membership canChangeRole = userRole >= GRAdmin && userRole >= mRole && userRole >= memRole && memberCurrent membership @@ -901,7 +901,7 @@ processChatCommand = \case withStore' $ \db -> updateGroupMemberRole db user m memRole case mStatus of GSMemInvited -> do - withStore (\db -> (,) <$> mapM (getContact db userId) memberContactId <*> liftIO (getMemberInvitation db user $ groupMemberId' m)) >>= \case + withStore (\db -> (,) <$> mapM (getContact db user) memberContactId <*> liftIO (getMemberInvitation db user $ groupMemberId' m)) >>= \case (Just ct, Just cReq) -> sendGrpInvitation user ct gInfo (m :: GroupMember) {memberRole = memRole} cReq _ -> throwChatError $ CEGroupCantResendInvitation gInfo cName _ -> do @@ -1051,7 +1051,7 @@ processChatCommand = \case sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId withStore (\db -> getChatRefByFileId db user fileId) >>= \case ChatRef CTDirect contactId -> do - contact <- withStore $ \db -> getContact db userId contactId + contact <- withStore $ \db -> getContact db user contactId void . sendDirectContactMessage contact $ XFileCancel sharedMsgId ChatRef CTGroup groupId -> do Group gInfo ms <- withStore $ \db -> getGroup db user groupId @@ -1114,7 +1114,7 @@ processChatCommand = \case connectViaContact :: User -> ConnectionRequestUri 'CMContact -> m ChatResponse connectViaContact user@User {userId} cReq@(CRContactUri ConnReqUriData {crClientData}) = withChatLock "connectViaContact" $ do let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq - withStore' (\db -> getConnReqContactXContactId db userId cReqHash) >>= \case + withStore' (\db -> getConnReqContactXContactId db user cReqHash) >>= \case (Just contact, _) -> pure $ CRContactAlreadyExists contact (_, xContactId_) -> procCmd $ do let randomXContactId = XContactId <$> (asks idsDrg >>= liftIO . (`randomBytes` 16)) @@ -1165,25 +1165,22 @@ processChatCommand = \case void (sendDirectContactMessage ct $ XInfo mergedProfile) `catchError` (toView . CRChatError) pure $ CRUserProfileUpdated (fromLocalProfile p) p' updateContactPrefs :: User -> Contact -> Preferences -> m ChatResponse - updateContactPrefs user@User {userId} ct@Contact {contactId, activeConn = Connection {customUserProfileId}, userPreferences = contactUserPrefs} contactUserPrefs' - | contactUserPrefs == contactUserPrefs' = pure $ CRContactPrefsUpdated ct ct $ contactUserPreferences user ct -- nothing changed actually + updateContactPrefs user@User {userId} ct@Contact {activeConn = Connection {customUserProfileId}, userPreferences = contactUserPrefs} contactUserPrefs' + | contactUserPrefs == contactUserPrefs' = pure $ CRContactPrefsUpdated ct ct | otherwise = do - withStore' $ \db -> updateContactUserPreferences db userId contactId contactUserPrefs' - -- [incognito] filter out contacts with whom user has incognito connections - let ct' = (ct :: Contact) {userPreferences = contactUserPrefs'} + ct' <- withStore' $ \db -> updateContactUserPreferences db user ct contactUserPrefs' incognitoProfile <- forM customUserProfileId $ \profileId -> withStore $ \db -> getProfileById db userId profileId let p' = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct') withChatLock "updateProfile" . procCmd $ do void (sendDirectContactMessage ct' $ XInfo p') `catchError` (toView . CRChatError) - pure $ CRContactPrefsUpdated ct ct' $ contactUserPreferences user ct' - + pure $ CRContactPrefsUpdated ct ct' isReady :: Contact -> Bool isReady ct = let s = connStatus $ activeConn (ct :: Contact) in s == ConnReady || s == ConnSndReady withCurrentCall :: ContactId -> (UserId -> Contact -> Call -> m (Maybe Call)) -> m ChatResponse withCurrentCall ctId action = withUser $ \user@User {userId} -> do - ct <- withStore $ \db -> getContact db userId ctId + ct <- withStore $ \db -> getContact db user ctId calls <- asks currentCalls withChatLock "currentCall" $ atomically (TM.lookup ctId calls) >>= \case @@ -1313,7 +1310,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = F chatRef <- withStore $ \db -> getChatRefByFileId db user fileId case (chatRef, grpMemberId) of (ChatRef CTDirect contactId, Nothing) -> do - ct <- withStore $ \db -> getContact db userId contactId + ct <- withStore $ \db -> getContact db user contactId (msg, ci) <- acceptFile void $ sendDirectContactMessage ct msg pure ci @@ -2057,7 +2054,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = where profileContactRequest :: InvitationId -> Profile -> Maybe XContactId -> m () profileContactRequest invId p xContactId_ = do - withStore (\db -> createOrUpdateContactRequest db userId userContactLinkId invId p xContactId_) >>= \case + withStore (\db -> createOrUpdateContactRequest db user userContactLinkId invId p xContactId_) >>= \case CORContact contact -> toView $ CRContactRequestAlreadyAccepted contact CORRequest cReq@UserContactRequest {localDisplayName} -> do withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case @@ -2151,7 +2148,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = if connectedIncognito then withStore' $ \db -> deleteSentProbe db userId probeId else do - cs <- withStore' $ \db -> getMatchingContacts db userId ct + cs <- withStore' $ \db -> getMatchingContacts db user ct let probeHash = ProbeHash $ C.sha256Hash (unProbe probe) forM_ cs $ \c -> sendProbeHash c probeHash probeId `catchError` const (pure ()) where @@ -2466,21 +2463,21 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = xInfo :: Contact -> Profile -> m () xInfo c@Contact {profile = p} p' = unless (fromLocalProfile p == p') $ do - c' <- withStore $ \db -> updateContactProfile db userId c p' - toView $ CRContactUpdated c c' $ contactUserPreferences user c' + c' <- withStore $ \db -> updateContactProfile db user c p' + toView $ CRContactUpdated c c' xInfoProbe :: Contact -> Probe -> m () xInfoProbe c2 probe = -- [incognito] unless connected incognito unless (contactConnIncognito c2) $ do - r <- withStore' $ \db -> matchReceivedProbe db userId c2 probe + r <- withStore' $ \db -> matchReceivedProbe db user c2 probe forM_ r $ \c1 -> probeMatch c1 c2 probe xInfoProbeCheck :: Contact -> ProbeHash -> m () xInfoProbeCheck c1 probeHash = -- [incognito] unless connected incognito unless (contactConnIncognito c1) $ do - r <- withStore' $ \db -> matchReceivedProbeHash db userId c1 probeHash + r <- withStore' $ \db -> matchReceivedProbeHash db user c1 probeHash forM_ r . uncurry $ probeMatch c1 probeMatch :: Contact -> Contact -> Probe -> m () @@ -2493,7 +2490,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = xInfoProbeOk :: Contact -> Probe -> m () xInfoProbeOk c1@Contact {contactId = cId1} probe = do - r <- withStore' $ \db -> matchSentProbe db userId c1 probe + r <- withStore' $ \db -> matchSentProbe db user c1 probe forM_ r $ \c2@Contact {contactId = cId2} -> if cId1 /= cId2 then mergeContacts c1 c2 @@ -2608,7 +2605,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = ChatMessage {chatMsgEvent} <- parseChatMessage connInfo case chatMsgEvent of XInfo p -> do - ct <- withStore $ \db -> createDirectContact db userId activeConn p + ct <- withStore $ \db -> createDirectContact db user activeConn p toView $ CRContactConnecting ct -- TODO show/log error, other events in SMP confirmation _ -> pure () diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index df267f98b..c5d3a090d 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -296,7 +296,7 @@ data ChatResponse | CRInvitation {connReqInvitation :: ConnReqInvitation} | CRSentConfirmation | CRSentInvitation {customUserProfile :: Maybe Profile} - | CRContactUpdated {fromContact :: Contact, toContact :: Contact, preferences :: ContactUserPreferences} + | CRContactUpdated {fromContact :: Contact, toContact :: Contact} | CRContactsMerged {intoContact :: Contact, mergedContact :: Contact} | CRContactDeleted {contact :: Contact} | CRChatCleared {chatInfo :: AChatInfo} @@ -322,7 +322,7 @@ data ChatResponse | CRUserProfileUpdated {fromProfile :: Profile, toProfile :: Profile} | CRContactAliasUpdated {toContact :: Contact} | CRConnectionAliasUpdated {toConnection :: PendingContactConnection} - | CRContactPrefsUpdated {fromContact :: Contact, toContact :: Contact, preferences :: ContactUserPreferences} + | CRContactPrefsUpdated {fromContact :: Contact, toContact :: Contact} | CRContactConnecting {contact :: Contact} | CRContactConnected {contact :: Contact, userCustomProfile :: Maybe Profile} | CRContactAnotherClient {contact :: Contact} diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index c69ae463f..da49845e2 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -414,7 +414,7 @@ getUsers db = toUser :: (UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData, Maybe Preferences) -> User toUser (userId, userContactId, profileId, activeUser, displayName, fullName, image, userPreferences) = let profile = LocalProfile {profileId, displayName, fullName, image, preferences = userPreferences, localAlias = ""} - in User {userId, userContactId, localDisplayName = displayName, profile, activeUser} + in User {userId, userContactId, localDisplayName = displayName, profile, activeUser, fullPreferences = mergePreferences Nothing userPreferences} setActiveUser :: DB.Connection -> UserId -> IO () setActiveUser db userId = do @@ -438,15 +438,15 @@ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile grou pccConnId <- insertedRowId db pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connReqInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt} -getConnReqContactXContactId :: DB.Connection -> UserId -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId) -getConnReqContactXContactId db userId cReqHash = do +getConnReqContactXContactId :: DB.Connection -> User -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId) +getConnReqContactXContactId db user@User {userId} cReqHash = do getContact' >>= \case c@(Just _) -> pure (c, Nothing) Nothing -> (Nothing,) <$> getXContactId where getContact' :: IO (Maybe Contact) getContact' = - maybeFirstRow toContact $ + maybeFirstRow (toContact user) $ DB.query db [sql| @@ -534,11 +534,14 @@ createConnection_ db userId connType entityId acId viaContact viaUserContactLink where ent ct = if connType == ct then entityId else Nothing -createDirectContact :: DB.Connection -> UserId -> Connection -> Profile -> ExceptT StoreError IO Contact -createDirectContact db userId activeConn@Connection {connId, localAlias} profile = do +createDirectContact :: DB.Connection -> User -> Connection -> Profile -> ExceptT StoreError IO Contact +createDirectContact db user@User {userId} activeConn@Connection {connId, localAlias} p@Profile {preferences} = do createdAt <- liftIO getCurrentTime - (localDisplayName, contactId, profileId) <- createContact_ db userId connId profile localAlias Nothing createdAt - pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile localAlias, activeConn, viaGroup = Nothing, contactUsed = False, chatSettings = defaultChatSettings, userPreferences = emptyChatPrefs, createdAt, updatedAt = createdAt} + (localDisplayName, contactId, profileId) <- createContact_ db userId connId p localAlias Nothing createdAt + let profile = toLocalProfile profileId p localAlias + userPreferences = emptyChatPrefs + mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn + pure $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup = Nothing, contactUsed = False, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt, updatedAt = createdAt} createContact_ :: DB.Connection -> UserId -> Int64 -> Profile -> LocalAlias -> Maybe Int64 -> UTCTime -> ExceptT StoreError IO (Text, ContactId, ProfileId) createContact_ db userId connId Profile {displayName, fullName, image, preferences} localAlias viaGroup currentTs = @@ -632,24 +635,32 @@ updateUserProfile db User {userId, userContactId, localDisplayName, profile = Lo updateContactProfile_' db userId profileId p' currentTs updateContact_ db userId userContactId localDisplayName newName currentTs -updateContactProfile :: DB.Connection -> UserId -> Contact -> Profile -> ExceptT StoreError IO Contact -updateContactProfile db userId c@Contact {contactId, localDisplayName, profile = LocalProfile {profileId, displayName, localAlias}} p'@Profile {displayName = newName} - | displayName == newName = - liftIO $ updateContactProfile_ db userId profileId p' $> (c :: Contact) {profile = toLocalProfile profileId p' localAlias} +updateContactProfile :: DB.Connection -> User -> Contact -> Profile -> ExceptT StoreError IO Contact +updateContactProfile db user@User {userId} c p' + | displayName == newName = do + liftIO $ updateContactProfile_ db userId profileId p' + pure $ c {profile, mergedPreferences} | otherwise = ExceptT . withLocalDisplayName db userId newName $ \ldn -> do currentTs <- getCurrentTime updateContactProfile_' db userId profileId p' currentTs updateContact_ db userId contactId localDisplayName ldn currentTs - pure . Right $ (c :: Contact) {localDisplayName = ldn, profile = toLocalProfile profileId p' localAlias} + pure . Right $ c {localDisplayName = ldn, profile, mergedPreferences} + where + Contact {contactId, localDisplayName, profile = LocalProfile {profileId, displayName, localAlias}, activeConn, userPreferences} = c + Profile {displayName = newName, preferences} = p' + profile = toLocalProfile profileId p' localAlias + mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn -updateContactUserPreferences :: DB.Connection -> UserId -> Int64 -> Preferences -> IO () -updateContactUserPreferences db userId contactId userPreferences = do +updateContactUserPreferences :: DB.Connection -> User -> Contact -> Preferences -> IO Contact +updateContactUserPreferences db user@User {userId} c@Contact {contactId, activeConn} userPreferences = do updatedAt <- getCurrentTime DB.execute db "UPDATE contacts SET user_preferences = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (userPreferences, updatedAt, userId, contactId) + let mergedPreferences = contactUserPreferences user userPreferences (preferences' c) $ connIncognito activeConn + pure $ c {mergedPreferences, userPreferences} updateContactAlias :: DB.Connection -> UserId -> Contact -> LocalAlias -> IO Contact updateContactAlias db userId c@Contact {profile = lp@LocalProfile {profileId}} localAlias = do @@ -722,33 +733,35 @@ updateContact_ db userId contactId displayName newName updatedAt = do type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, LocalAlias, Bool, Maybe Bool) :. (Maybe Preferences, Preferences, UTCTime, UTCTime) -toContact :: ContactRow :. ConnectionRow -> Contact -toContact (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, localAlias, contactUsed, enableNtfs_) :. (preferences, userPreferences, createdAt, updatedAt)) :. connRow) = +toContact :: User -> ContactRow :. ConnectionRow -> Contact +toContact user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, localAlias, contactUsed, enableNtfs_) :. (preferences, userPreferences, createdAt, updatedAt)) :. connRow) = let profile = LocalProfile {profileId, displayName, fullName, image, preferences, localAlias} activeConn = toConnection connRow chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} - in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, createdAt, updatedAt} + mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt} -toContactOrError :: ContactRow :. MaybeConnectionRow -> Either StoreError Contact -toContactOrError (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, localAlias, contactUsed, enableNtfs_) :. (preferences, userPreferences, createdAt, updatedAt)) :. connRow) = +toContactOrError :: User -> ContactRow :. MaybeConnectionRow -> Either StoreError Contact +toContactOrError user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, localAlias, contactUsed, enableNtfs_) :. (preferences, userPreferences, createdAt, updatedAt)) :. connRow) = let profile = LocalProfile {profileId, displayName, fullName, image, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} in case toMaybeConnection connRow of Just activeConn -> - Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, createdAt, updatedAt} + let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn + in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt} _ -> Left $ SEContactNotReady localDisplayName -- TODO return the last connection that is ready, not any last connection -- requires updating connection status getContactByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Contact -getContactByName db user@User {userId} localDisplayName = do +getContactByName db user localDisplayName = do cId <- getContactIdByName db user localDisplayName - getContact db userId cId + getContact db user cId getUserContacts :: DB.Connection -> User -> IO [Contact] -getUserContacts db User {userId} = do +getUserContacts db user@User {userId} = do contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ?" (Only userId) - rights <$> mapM (runExceptT . getContact db userId) contactIds + rights <$> mapM (runExceptT . getContact db user) contactIds createUserContactLink :: DB.Connection -> UserId -> ConnId -> ConnReqContact -> ExceptT StoreError IO () createUserContactLink db userId agentConnId cReq = @@ -977,8 +990,8 @@ getGroupLinkId db User {userId} GroupInfo {groupId} = fmap join . maybeFirstRow fromOnly $ DB.query db "SELECT group_link_id FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId) -createOrUpdateContactRequest :: DB.Connection -> UserId -> Int64 -> InvitationId -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest -createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayName, fullName, image, preferences} xContactId_ = +createOrUpdateContactRequest :: DB.Connection -> User -> Int64 -> InvitationId -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest +createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profile {displayName, fullName, image, preferences} xContactId_ = liftIO (maybeM getContact' xContactId_) >>= \case Just contact -> pure $ CORContact contact Nothing -> CORRequest <$> createOrUpdate_ @@ -1014,7 +1027,7 @@ createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayN insertedRowId db getContact' :: XContactId -> IO (Maybe Contact) getContact' xContactId = - maybeFirstRow toContact $ + maybeFirstRow (toContact user) $ DB.query db [sql| @@ -1133,20 +1146,21 @@ deleteContactRequest db userId contactRequestId = do DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId) createAcceptedContact :: DB.Connection -> User -> ConnId -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> IO Contact -createAcceptedContact db User {userId, profile = LocalProfile {preferences}} agentConnId localDisplayName profileId profile userContactLinkId xContactId incognitoProfile = do +createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId localDisplayName profileId profile userContactLinkId xContactId incognitoProfile = do DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) createdAt <- getCurrentTime customUserProfileId <- forM incognitoProfile $ \case NewIncognito p -> createIncognitoProfile_ db userId createdAt p ExistingIncognito LocalProfile {profileId = pId} -> pure pId - let contactUserPrefs = fromMaybe emptyChatPrefs $ incognitoProfile >> preferences + let userPreferences = fromMaybe emptyChatPrefs $ incognitoProfile >> preferences DB.execute db "INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, xcontact_id) VALUES (?,?,?,?,?,?,?,?)" - (userId, localDisplayName, profileId, True, contactUserPrefs, createdAt, createdAt, xContactId) + (userId, localDisplayName, profileId, True, userPreferences, createdAt, createdAt, xContactId) contactId <- insertedRowId db activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId Nothing (Just userContactLinkId) customUserProfileId 0 createdAt - pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn, viaGroup = Nothing, contactUsed = False, chatSettings = defaultChatSettings, userPreferences = contactUserPrefs, createdAt = createdAt, updatedAt = createdAt} + let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn + pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn, viaGroup = Nothing, contactUsed = False, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt} getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer] getLiveSndFileTransfers db User {userId} = do @@ -1249,8 +1263,8 @@ toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, v Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only createdAt) toMaybeConnection _ = Nothing -getMatchingContacts :: DB.Connection -> UserId -> Contact -> IO [Contact] -getMatchingContacts db userId Contact {contactId, profile = LocalProfile {displayName, fullName, image}} = do +getMatchingContacts :: DB.Connection -> User -> Contact -> IO [Contact] +getMatchingContacts db user@User {userId} Contact {contactId, profile = LocalProfile {displayName, fullName, image}} = do contactIds <- map fromOnly <$> DB.query @@ -1264,7 +1278,7 @@ getMatchingContacts db userId Contact {contactId, profile = LocalProfile {displa AND ((p.image IS NULL AND ? IS NULL) OR p.image = ?) |] (userId, contactId, displayName, fullName, image, image) - rights <$> mapM (runExceptT . getContact db userId) contactIds + rights <$> mapM (runExceptT . getContact db user) contactIds createSentProbe :: DB.Connection -> TVar ChaChaDRG -> UserId -> Contact -> ExceptT StoreError IO (Probe, Int64) createSentProbe db gVar userId _to@Contact {contactId} = @@ -1291,8 +1305,8 @@ deleteSentProbe db userId probeId = "DELETE FROM sent_probes WHERE user_id = ? AND sent_probe_id = ?" (userId, probeId) -matchReceivedProbe :: DB.Connection -> UserId -> Contact -> Probe -> IO (Maybe Contact) -matchReceivedProbe db userId _from@Contact {contactId} (Probe probe) = do +matchReceivedProbe :: DB.Connection -> User -> Contact -> Probe -> IO (Maybe Contact) +matchReceivedProbe db user@User {userId} _from@Contact {contactId} (Probe probe) = do let probeHash = C.sha256Hash probe contactIds <- map fromOnly @@ -1312,10 +1326,10 @@ matchReceivedProbe db userId _from@Contact {contactId} (Probe probe) = do (contactId, probe, probeHash, userId, currentTs, currentTs) case contactIds of [] -> pure Nothing - cId : _ -> eitherToMaybe <$> runExceptT (getContact db userId cId) + cId : _ -> eitherToMaybe <$> runExceptT (getContact db user cId) -matchReceivedProbeHash :: DB.Connection -> UserId -> Contact -> ProbeHash -> IO (Maybe (Contact, Probe)) -matchReceivedProbeHash db userId _from@Contact {contactId} (ProbeHash probeHash) = do +matchReceivedProbeHash :: DB.Connection -> User -> Contact -> ProbeHash -> IO (Maybe (Contact, Probe)) +matchReceivedProbeHash db user@User {userId} _from@Contact {contactId} (ProbeHash probeHash) = do namesAndProbes <- DB.query db @@ -1335,10 +1349,10 @@ matchReceivedProbeHash db userId _from@Contact {contactId} (ProbeHash probeHash) [] -> pure Nothing (cId, probe) : _ -> either (const Nothing) (Just . (,Probe probe)) - <$> runExceptT (getContact db userId cId) + <$> runExceptT (getContact db user cId) -matchSentProbe :: DB.Connection -> UserId -> Contact -> Probe -> IO (Maybe Contact) -matchSentProbe db userId _from@Contact {contactId} (Probe probe) = do +matchSentProbe :: DB.Connection -> User -> Contact -> Probe -> IO (Maybe Contact) +matchSentProbe db user@User {userId} _from@Contact {contactId} (Probe probe) = do contactIds <- map fromOnly <$> DB.query @@ -1353,7 +1367,7 @@ matchSentProbe db userId _from@Contact {contactId} (Probe probe) = do (userId, probe, contactId) case contactIds of [] -> pure Nothing - cId : _ -> eitherToMaybe <$> runExceptT (getContact db userId cId) + cId : _ -> eitherToMaybe <$> runExceptT (getContact db user cId) mergeContactRecords :: DB.Connection -> UserId -> Contact -> Contact -> IO () mergeContactRecords db userId Contact {contactId = toContactId} Contact {contactId = fromContactId, localDisplayName} = do @@ -1438,7 +1452,8 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, localAlias, viaGroup, contactUsed, enableNtfs_) :. (preferences, userPreferences, createdAt, updatedAt)] = let profile = LocalProfile {profileId, displayName, fullName, image, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} - in Right $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, createdAt, updatedAt} + mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn + in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt} toContact' _ _ _ = Left $ SEInternalError "referenced contact not found" getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember) getGroupAndMember_ groupMemberId c = ExceptT $ do @@ -1590,6 +1605,7 @@ updateConnectionStatus db Connection {connId} connStatus = do createNewGroup :: DB.Connection -> TVar ChaChaDRG -> User -> GroupProfile -> ExceptT StoreError IO GroupInfo createNewGroup db gVar user@User {userId} groupProfile = ExceptT $ do let GroupProfile {displayName, fullName, image, groupPreferences} = groupProfile + fullGroupPreferences = mergeGroupPreferences groupPreferences currentTs <- getCurrentTime withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do groupId <- liftIO $ do @@ -1606,7 +1622,7 @@ createNewGroup db gVar user@User {userId} groupProfile = ExceptT $ do 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} - pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, membership, hostConnCustomUserProfileId = Nothing, chatSettings, createdAt = currentTs, updatedAt = currentTs} + pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId = Nothing, chatSettings, createdAt = currentTs, updatedAt = currentTs} -- | creates a new group record for the group the current user was invited to, or returns an existing one createGroupInvitation :: DB.Connection -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO (GroupInfo, GroupMemberId) @@ -1633,6 +1649,7 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId, activeCo createGroupInvitation_ :: ExceptT StoreError IO (GroupInfo, GroupMemberId) createGroupInvitation_ = do let GroupProfile {displayName, fullName, image, groupPreferences} = groupProfile + fullGroupPreferences = mergeGroupPreferences groupPreferences ExceptT $ withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do currentTs <- liftIO getCurrentTime @@ -1650,7 +1667,7 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId, activeCo 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} - pure (GroupInfo {groupId, localDisplayName, groupProfile, membership, hostConnCustomUserProfileId = customUserProfileId, chatSettings, createdAt = currentTs, updatedAt = currentTs}, groupMemberId) + pure (GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId = customUserProfileId, chatSettings, createdAt = currentTs, updatedAt = currentTs}, groupMemberId) getHostMemberId_ :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO GroupMemberId getHostMemberId_ db User {userId} groupId = @@ -1805,7 +1822,8 @@ toGroupInfo :: Int64 -> GroupInfoRow -> GroupInfo toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, image, hostConnCustomUserProfileId, enableNtfs_, groupPreferences, createdAt, updatedAt) :. userMemberRow) = let membership = toGroupMember userContactId userMemberRow chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} - in GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, fullName, image, groupPreferences}, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt} + fullGroupPreferences = mergeGroupPreferences groupPreferences + in GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, fullName, image, groupPreferences}, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt} getGroupMember :: DB.Connection -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember getGroupMember db user@User {userId} groupId groupMemberId = @@ -1976,8 +1994,8 @@ createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Co ) getContactViaMember :: DB.Connection -> User -> GroupMember -> IO (Maybe Contact) -getContactViaMember db User {userId} GroupMember {groupMemberId} = - maybeFirstRow toContact $ +getContactViaMember db user@User {userId} GroupMember {groupMemberId} = + maybeFirstRow (toContact user) $ DB.query db [sql| @@ -2102,7 +2120,7 @@ cleanupMemberContactAndProfile_ :: DB.Connection -> User -> GroupMember -> IO () cleanupMemberContactAndProfile_ db user@User {userId} m@GroupMember {groupMemberId, localDisplayName, memberContactId, memberContactProfileId, memberProfile = LocalProfile {profileId}} = case memberContactId of Just contactId -> - runExceptT (getContact db userId contactId) >>= \case + runExceptT (getContact db user contactId) >>= \case Right ct@Contact {activeConn = Connection {connLevel, viaGroupLink}, contactUsed} -> unless ((connLevel == 0 && not viaGroupLink) || contactUsed) $ deleteContact db user ct _ -> pure () @@ -2320,7 +2338,7 @@ 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 {userId} GroupMember {groupMemberId} = +getViaGroupContact db user@User {userId} GroupMember {groupMemberId} = maybeFirstRow toContact' $ DB.query db @@ -2347,7 +2365,8 @@ getViaGroupContact db User {userId} GroupMember {groupMemberId} = let profile = LocalProfile {profileId, displayName, fullName, image, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} activeConn = toConnection connRow - in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, createdAt, updatedAt} + mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt} createSndDirectFileTransfer :: DB.Connection -> UserId -> Contact -> FilePath -> FileInvitation -> Maybe ConnId -> Integer -> IO FileTransferMeta createSndDirectFileTransfer db userId Contact {contactId} filePath FileInvitation {fileName, fileSize, fileInline} acId_ chunkSize = do @@ -2659,7 +2678,7 @@ getRcvFileTransfer db user@User {userId} fileId = do rfi_ = \case (Just filePath, Just connId, Just agentConnId, _, _, _, _) -> pure $ Just RcvFileInfo {filePath, connId, agentConnId} (Just filePath, Nothing, Nothing, Just contactId, _, _, True) -> do - Contact {activeConn = Connection {connId, agentConnId}} <- getContact db userId contactId + Contact {activeConn = Connection {connId, agentConnId}} <- getContact db user contactId pure $ Just RcvFileInfo {filePath, connId, agentConnId} (Just filePath, Nothing, Nothing, _, Just groupId, Just groupMemberId, True) -> do getGroupMember db user groupId groupMemberId >>= \case @@ -3194,7 +3213,7 @@ getChatPreviews db user withPCC = do ts (AChat _ Chat {chatInfo}) = chatInfoUpdatedAt chatInfo getDirectChatPreviews_ :: DB.Connection -> User -> IO [AChat] -getDirectChatPreviews_ db User {userId} = do +getDirectChatPreviews_ db user@User {userId} = do tz <- getCurrentTimeZone currentTs <- getCurrentTime map (toDirectChatPreview tz currentTs) @@ -3253,7 +3272,7 @@ getDirectChatPreviews_ db User {userId} = do where toDirectChatPreview :: TimeZone -> UTCTime -> ContactRow :. ConnectionRow :. ChatStatsRow :. MaybeChatItemRow :. QuoteRow -> AChat toDirectChatPreview tz currentTs (contactRow :. connRow :. statsRow :. ciRow_) = - let contact = toContact $ contactRow :. connRow + let contact = toContact user $ contactRow :. connRow ci_ = toDirectChatItemList tz currentTs ciRow_ stats = toChatStats statsRow in AChat SCTDirect $ Chat (DirectChat contact) ci_ stats @@ -3420,8 +3439,8 @@ getDirectChat db user contactId pagination search_ = do CPBefore beforeId count -> getDirectChatBefore_ db user contactId beforeId count search getDirectChatLast_ :: DB.Connection -> User -> Int64 -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect) -getDirectChatLast_ db User {userId} contactId count search = do - contact <- getContact db userId contactId +getDirectChatLast_ db user@User {userId} contactId count search = do + contact <- getContact db user contactId let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} chatItems <- ExceptT getDirectChatItemsLast_ pure $ Chat (DirectChat contact) (reverse chatItems) stats @@ -3451,8 +3470,8 @@ getDirectChatLast_ db User {userId} contactId count search = do (userId, contactId, search, count) getDirectChatAfter_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect) -getDirectChatAfter_ db User {userId} contactId afterChatItemId count search = do - contact <- getContact db userId contactId +getDirectChatAfter_ db user@User {userId} contactId afterChatItemId count search = do + contact <- getContact db user contactId let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} chatItems <- ExceptT getDirectChatItemsAfter_ pure $ Chat (DirectChat contact) chatItems stats @@ -3483,8 +3502,8 @@ getDirectChatAfter_ db User {userId} contactId afterChatItemId count search = do (userId, contactId, search, afterChatItemId, count) getDirectChatBefore_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect) -getDirectChatBefore_ db User {userId} contactId beforeChatItemId count search = do - contact <- getContact db userId contactId +getDirectChatBefore_ db user@User {userId} contactId beforeChatItemId count search = do + contact <- getContact db user contactId let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} chatItems <- ExceptT getDirectChatItemsBefore_ pure $ Chat (DirectChat contact) (reverse chatItems) stats @@ -3519,9 +3538,9 @@ getContactIdByName db User {userId} cName = ExceptT . firstRow fromOnly (SEContactNotFoundByName cName) $ DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND local_display_name = ?" (userId, cName) -getContact :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO Contact -getContact db userId contactId = - ExceptT . fmap join . firstRow toContactOrError (SEContactNotFound contactId) $ +getContact :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO Contact +getContact db user@User {userId} contactId = + ExceptT . fmap join . firstRow (toContactOrError user) (SEContactNotFound contactId) $ DB.query db [sql| @@ -4076,7 +4095,7 @@ getChatItemByGroupId db user@User {userId} groupId = do getAChatItem_ :: DB.Connection -> User -> ChatItemId -> ChatRef -> ExceptT StoreError IO AChatItem getAChatItem_ db user@User {userId} itemId = \case ChatRef CTDirect contactId -> do - ct <- getContact db userId contactId + ct <- getContact db user contactId (CChatItem msgDir ci) <- getDirectChatItem db userId contactId itemId pure $ AChatItem SCTDirect msgDir (DirectChat ct) ci ChatRef CTGroup groupId -> do diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 4997a3b92..e2c00835c 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -66,6 +66,7 @@ data User = User userContactId :: ContactId, localDisplayName :: ContactName, profile :: LocalProfile, + fullPreferences :: FullPreferences, activeUser :: Bool } deriving (Show, Generic, FromJSON) @@ -87,6 +88,7 @@ data Contact = Contact contactUsed :: Bool, chatSettings :: ChatSettings, userPreferences :: Preferences, + mergedPreferences :: ContactUserPreferences, createdAt :: UTCTime, updatedAt :: UTCTime } @@ -100,13 +102,10 @@ contactConn :: Contact -> Connection contactConn = activeConn contactConnId :: Contact -> ConnId -contactConnId Contact {activeConn} = aConnId activeConn +contactConnId = aConnId . contactConn contactConnIncognito :: Contact -> Bool -contactConnIncognito = isJust . customUserProfileId' - -customUserProfileId' :: Contact -> Maybe Int64 -customUserProfileId' Contact {activeConn} = customUserProfileId (activeConn :: Connection) +contactConnIncognito = connIncognito . contactConn data ContactRef = ContactRef { contactId :: ContactId, @@ -207,6 +206,7 @@ data GroupInfo = GroupInfo { groupId :: GroupId, localDisplayName :: GroupName, groupProfile :: GroupProfile, + fullGroupPreferences :: FullGroupPreferences, membership :: GroupMember, hostConnCustomUserProfileId :: Maybe ProfileId, chatSettings :: ChatSettings, @@ -293,6 +293,39 @@ data Preferences = Preferences } deriving (Eq, Show, Generic, FromJSON) +instance ToJSON Preferences where + toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} + +instance ToField Preferences where + toField = toField . encodeJSON + +instance FromField Preferences where + fromField = fromTextField_ decodeJSON + +groupPrefSel :: ChatFeature -> GroupPreferences -> Maybe GroupPreference +groupPrefSel = \case + CFFullDelete -> fullDelete + -- CFReceipts -> receipts + CFVoice -> voice + +class GroupPreferenceI p where + getGroupPreference :: ChatFeature -> p -> GroupPreference + +instance GroupPreferenceI GroupPreferences where + getGroupPreference pt prefs = fromMaybe (getGroupPreference pt defaultGroupPrefs) (groupPrefSel pt prefs) + +instance GroupPreferenceI (Maybe GroupPreferences) where + getGroupPreference pt prefs = fromMaybe (getGroupPreference pt defaultGroupPrefs) (groupPrefSel pt =<< prefs) + +instance GroupPreferenceI FullGroupPreferences where + getGroupPreference = \case + CFFullDelete -> fullDelete + -- CFReceipts -> receipts + CFVoice -> voice + {-# INLINE getGroupPreference #-} + +-- collection of optional group preferences data GroupPreferences = GroupPreferences { fullDelete :: Maybe GroupPreference, -- receipts :: Maybe GroupPreference, @@ -317,7 +350,20 @@ data FullPreferences = FullPreferences -- receipts :: Preference, voice :: Preference } - deriving (Eq) + deriving (Eq, Show, Generic, FromJSON) + +instance ToJSON FullPreferences where toEncoding = J.genericToEncoding J.defaultOptions + +-- full collection of group preferences defined in the app - it is used to ensure we include all preferences and to simplify processing +-- if some of the preferences are not defined in GroupPreferences, defaults from defaultGroupPrefs are used here. +data FullGroupPreferences = FullGroupPreferences + { fullDelete :: GroupPreference, + -- receipts :: GroupPreference, + voice :: GroupPreference + } + deriving (Eq, Show, Generic, FromJSON) + +instance ToJSON FullGroupPreferences where toEncoding = J.genericToEncoding J.defaultOptions -- merged preferences of user for a given contact - they differentiate between specific preferences for the contact and global user preferences data ContactUserPreferences = ContactUserPreferences @@ -325,17 +371,17 @@ data ContactUserPreferences = ContactUserPreferences -- receipts :: ContactUserPreference, voice :: ContactUserPreference } - deriving (Show, Generic) + deriving (Eq, Show, Generic) data ContactUserPreference = ContactUserPreference { enabled :: PrefEnabled, userPreference :: ContactUserPref, contactPreference :: Preference } - deriving (Show, Generic) + deriving (Eq, Show, Generic) data ContactUserPref = CUPContact {preference :: Preference} | CUPUser {preference :: Preference} - deriving (Show, Generic) + deriving (Eq, Show, Generic) instance ToJSON ContactUserPreferences where toEncoding = J.genericToEncoding J.defaultOptions @@ -364,26 +410,24 @@ defaultChatPrefs = emptyChatPrefs :: Preferences emptyChatPrefs = Preferences Nothing Nothing -instance ToJSON Preferences where - toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} - toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} - -instance ToField Preferences where - toField = toField . encodeJSON - -instance FromField Preferences where - fromField = fromTextField_ decodeJSON +defaultGroupPrefs :: FullGroupPreferences +defaultGroupPrefs = + FullGroupPreferences + { fullDelete = GroupPreference {enable = FEOff}, + -- receipts = GroupPreference {enable = FEOff}, + voice = GroupPreference {enable = FEOn} + } data Preference = Preference {allow :: FeatureAllowed} deriving (Eq, Show, Generic, FromJSON) +instance ToJSON Preference where toEncoding = J.genericToEncoding J.defaultOptions + data GroupPreference = GroupPreference {enable :: GroupFeatureEnabled} deriving (Eq, Show, Generic, FromJSON) -instance ToJSON Preference where toEncoding = J.genericToEncoding J.defaultOptions - instance ToJSON GroupPreference where toEncoding = J.genericToEncoding J.defaultOptions data FeatureAllowed @@ -392,9 +436,6 @@ data FeatureAllowed | FANo -- do not allow deriving (Eq, Show, Generic) -data GroupFeatureEnabled = FEOn | FEOff - deriving (Eq, Show, Generic) - instance FromField FeatureAllowed where fromField = fromBlobField_ strDecode instance ToField FeatureAllowed where toField = toField . strEncode @@ -418,6 +459,9 @@ instance ToJSON FeatureAllowed where toJSON = strToJSON toEncoding = strToJEncoding +data GroupFeatureEnabled = FEOn | FEOff + deriving (Eq, Show, Generic) + instance FromField GroupFeatureEnabled where fromField = fromBlobField_ strDecode instance ToField GroupFeatureEnabled where toField = toField . strEncode @@ -452,12 +496,25 @@ mergePreferences contactPrefs userPreferences = in fromMaybe (getPreference pt defaultChatPrefs) $ (contactPrefs >>= sel) <|> (userPreferences >>= sel) mergeUserChatPrefs :: User -> Contact -> FullPreferences -mergeUserChatPrefs user ct = - let userPrefs = if contactConnIncognito ct then Nothing else preferences' user - in mergePreferences (Just $ userPreferences ct) userPrefs +mergeUserChatPrefs user ct = mergeUserChatPrefs' user (contactConnIncognito ct) (userPreferences ct) + +mergeUserChatPrefs' :: User -> Bool -> Preferences -> FullPreferences +mergeUserChatPrefs' user connectedIncognito userPreferences = + let userPrefs = if connectedIncognito then Nothing else preferences' user + in mergePreferences (Just userPreferences) userPrefs + +mergeGroupPreferences :: Maybe GroupPreferences -> FullGroupPreferences +mergeGroupPreferences groupPreferences = + FullGroupPreferences + { fullDelete = pref CFFullDelete, + -- receipts = pref CFReceipts, + voice = pref CFVoice + } + where + pref pt = fromMaybe (getGroupPreference pt defaultGroupPrefs) (groupPreferences >>= groupPrefSel pt) data PrefEnabled = PrefEnabled {forUser :: Bool, forContact :: Bool} - deriving (Show, Generic) + deriving (Eq, Show, Generic) instance ToJSON PrefEnabled where toJSON = J.genericToJSON J.defaultOptions @@ -471,8 +528,8 @@ prefEnabled Preference {allow = user} Preference {allow = contact} = case (user, (FANo, _) -> PrefEnabled False False _ -> PrefEnabled True True -contactUserPreferences :: User -> Contact -> ContactUserPreferences -contactUserPreferences user ct = +contactUserPreferences :: User -> Preferences -> Maybe Preferences -> Bool -> ContactUserPreferences +contactUserPreferences user userPreferences contactPreferences connectedIncognito = ContactUserPreferences { fullDelete = pref CFFullDelete, -- receipts = pref CFReceipts, @@ -483,19 +540,19 @@ contactUserPreferences user ct = ContactUserPreference { enabled = prefEnabled userPref ctPref, -- incognito contact cannot have default user preference used - userPreference = if contactConnIncognito ct then CUPContact ctUserPref else maybe (CUPUser userPref) CUPContact ctUserPref_, + userPreference = if connectedIncognito then CUPContact ctUserPref else maybe (CUPUser userPref) CUPContact ctUserPref_, contactPreference = ctPref } where - ctUserPref = getPreference pt $ userPreferences ct - ctUserPref_ = chatPrefSel pt $ userPreferences ct + ctUserPref = getPreference pt userPreferences + ctUserPref_ = chatPrefSel pt userPreferences userPref = getPreference pt ctUserPrefs ctPref = getPreference pt ctPrefs - ctUserPrefs = mergeUserChatPrefs user ct - ctPrefs = mergePreferences (preferences' ct) Nothing + ctUserPrefs = mergeUserChatPrefs' user connectedIncognito userPreferences + ctPrefs = mergePreferences contactPreferences Nothing -getContactUserPrefefence :: ChatFeature -> ContactUserPreferences -> ContactUserPreference -getContactUserPrefefence = \case +getContactUserPreference :: ChatFeature -> ContactUserPreferences -> ContactUserPreference +getContactUserPreference = \case CFFullDelete -> fullDelete -- CFReceipts -> receipts CFVoice -> voice @@ -1144,6 +1201,9 @@ data Connection = Connection aConnId :: Connection -> ConnId aConnId Connection {agentConnId = AgentConnId cId} = cId +connIncognito :: Connection -> Bool +connIncognito Connection {customUserProfileId} = isJust customUserProfileId + instance ToJSON Connection where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 3a795fb2a..8791a75af 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -125,13 +125,13 @@ responseToView user_ testView ts = \case CRSndGroupFileCancelled _ ftm fts -> viewSndGroupFileCancelled ftm fts CRRcvFileCancelled ft -> receivingFile_ "cancelled" ft CRUserProfileUpdated p p' -> viewUserProfileUpdated p p' - CRContactPrefsUpdated {fromContact, toContact, preferences} -> case user_ of - Just user -> viewUserContactPrefsUpdated user fromContact toContact preferences + CRContactPrefsUpdated {fromContact, toContact} -> case user_ of + Just user -> viewUserContactPrefsUpdated user fromContact toContact _ -> ["unexpected chat event CRContactPrefsUpdated without current user"] CRContactAliasUpdated c -> viewContactAliasUpdated c CRConnectionAliasUpdated c -> viewConnectionAliasUpdated c - CRContactUpdated {fromContact = c, toContact = c', preferences} -> case user_ of - Just user -> viewContactUpdated c c' <> viewContactPrefsUpdated user c c' preferences + CRContactUpdated {fromContact = c, toContact = c'} -> case user_ of + Just user -> viewContactUpdated c c' <> viewContactPrefsUpdated user c c' _ -> ["unexpected chat event CRContactUpdated without current user"] CRContactsMerged intoCt mergedCt -> viewContactsMerged intoCt mergedCt CRReceivedContactRequest UserContactRequest {localDisplayName = c, profile} -> viewReceivedContactRequest c profile @@ -710,15 +710,15 @@ viewUserProfileUpdated Profile {displayName = n, fullName, image, preferences} P | otherwise = ["user profile is changed to " <> ttyFullName n' fullName' <> notified] notified = " (your contacts are notified)" -viewUserContactPrefsUpdated :: User -> Contact -> Contact -> ContactUserPreferences -> [StyledString] -viewUserContactPrefsUpdated user ct ct' cups +viewUserContactPrefsUpdated :: User -> Contact -> Contact -> [StyledString] +viewUserContactPrefsUpdated user ct ct'@Contact {mergedPreferences = cups} | null prefs = ["your preferences for " <> ttyContact' ct' <> " did not change"] | otherwise = ("you updated preferences for " <> ttyContact' ct' <> ":") : prefs where prefs = viewContactPreferences user ct ct' cups -viewContactPrefsUpdated :: User -> Contact -> Contact -> ContactUserPreferences -> [StyledString] -viewContactPrefsUpdated user ct ct' cups +viewContactPrefsUpdated :: User -> Contact -> Contact -> [StyledString] +viewContactPrefsUpdated user ct ct'@Contact {mergedPreferences = cups} | null prefs = [] | otherwise = (ttyContact' ct' <> " updated preferences for you:") : prefs where @@ -736,7 +736,7 @@ viewContactPref userPrefs userPrefs' ctPrefs cups pt userPref = getPreference pt userPrefs userPref' = getPreference pt userPrefs' ctPref = getPreference pt ctPrefs - ContactUserPreference {enabled, userPreference, contactPreference} = getContactUserPrefefence pt cups + ContactUserPreference {enabled, userPreference, contactPreference} = getContactUserPreference pt cups viewPrefsUpdated :: Maybe Preferences -> Maybe Preferences -> [StyledString] viewPrefsUpdated ps ps' diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index 6a37b024d..7d0d67a2a 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -32,9 +32,9 @@ activeUserExists = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\" activeUser :: String #if defined(darwin_HOST_OS) && defined(swiftJSON) -activeUser = "{\"resp\":{\"activeUser\":{\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"activeUser\":true}}}}" +activeUser = "{\"resp\":{\"activeUser\":{\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"fullDelete\":{\"allow\":\"no\"},\"voice\":{\"allow\":\"yes\"}},\"activeUser\":true}}}" #else -activeUser = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"activeUser\":true}}}" +activeUser = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"fullDelete\":{\"allow\":\"no\"},\"voice\":{\"allow\":\"yes\"}},\"activeUser\":true}}}" #endif chatStarted :: String