Merge branch 'master' into ios-notifications

This commit is contained in:
Evgeny Poberezkin 2023-12-11 14:11:00 +00:00
commit f9a125bc32
18 changed files with 805 additions and 302 deletions

View File

@ -125,6 +125,7 @@ library
Simplex.Chat.Migrations.M20231113_group_forward Simplex.Chat.Migrations.M20231113_group_forward
Simplex.Chat.Migrations.M20231114_remote_control Simplex.Chat.Migrations.M20231114_remote_control
Simplex.Chat.Migrations.M20231126_remote_ctrl_address Simplex.Chat.Migrations.M20231126_remote_ctrl_address
Simplex.Chat.Migrations.M20231207_chat_list_pagination
Simplex.Chat.Mobile Simplex.Chat.Mobile
Simplex.Chat.Mobile.File Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared Simplex.Chat.Mobile.Shared
@ -532,6 +533,7 @@ test-suite simplex-chat-test
Bots.DirectoryTests Bots.DirectoryTests
ChatClient ChatClient
ChatTests ChatTests
ChatTests.ChatList
ChatTests.Direct ChatTests.Direct
ChatTests.Files ChatTests.Files
ChatTests.Groups ChatTests.Groups

View File

@ -35,7 +35,7 @@ import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Char import Data.Char
import Data.Constraint (Dict (..)) import Data.Constraint (Dict (..))
import Data.Either (fromRight, rights) import Data.Either (fromRight, partitionEithers, rights)
import Data.Fixed (div') import Data.Fixed (div')
import Data.Functor (($>)) import Data.Functor (($>))
import Data.Int (Int64) import Data.Int (Int64)
@ -599,8 +599,10 @@ processChatCommand = \case
. sortOn (timeAvg . snd) . sortOn (timeAvg . snd)
. M.assocs . M.assocs
<$> withConnection st (readTVarIO . DB.slow) <$> withConnection st (readTVarIO . DB.slow)
APIGetChats userId withPCC -> withUserId' userId $ \user -> APIGetChats {userId, pendingConnections, pagination, query} -> withUserId' userId $ \user -> do
CRApiChats user <$> withStoreCtx' (Just "APIGetChats, getChatPreviews") (\db -> getChatPreviews db user withPCC) (errs, previews) <- partitionEithers <$> withStore' (\db -> getChatPreviews db user pendingConnections pagination query)
toView $ CRChatErrors (Just user) (map ChatErrorStore errs)
pure $ CRApiChats user previews
APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of
-- TODO optimize queries calculating ChatStats, currently they're disabled -- TODO optimize queries calculating ChatStats, currently they're disabled
CTDirect -> do CTDirect -> do
@ -1051,10 +1053,12 @@ processChatCommand = \case
CTContactConnection -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported"
CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> pure $ chatCmdError (Just user) "not supported"
APIAcceptContact incognito connReqId -> withUser $ \_ -> withChatLock "acceptContact" $ do APIAcceptContact incognito connReqId -> withUser $ \_ -> withChatLock "acceptContact" $ do
(user, cReq) <- withStore $ \db -> getContactRequest' db connReqId (user@User {userId}, cReq@UserContactRequest {userContactLinkId}) <- withStore $ \db -> getContactRequest' db connReqId
ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId
let contactUsed = (\(_, groupId_, _) -> isNothing groupId_) ucl
-- [incognito] generate profile to send, create connection with incognito profile -- [incognito] generate profile to send, create connection with incognito profile
incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing
ct <- acceptContactRequest user cReq incognitoProfile ct <- acceptContactRequest user cReq incognitoProfile contactUsed
pure $ CRAcceptingContactRequest user ct pure $ CRAcceptingContactRequest user ct
APIRejectContact connReqId -> withUser $ \user -> withChatLock "rejectContact" $ do APIRejectContact connReqId -> withUser $ \user -> withChatLock "rejectContact" $ do
cReq@UserContactRequest {agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId} <- cReq@UserContactRequest {agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId} <-
@ -1823,8 +1827,10 @@ processChatCommand = \case
let mc = MCText msg let mc = MCText msg
processChatCommand . APISendMessage (ChatRef CTGroup groupId) False Nothing $ ComposedMessage Nothing (Just quotedItemId) mc processChatCommand . APISendMessage (ChatRef CTGroup groupId) False Nothing $ ComposedMessage Nothing (Just quotedItemId) mc
LastChats count_ -> withUser' $ \user -> do LastChats count_ -> withUser' $ \user -> do
chats <- withStore' $ \db -> getChatPreviews db user False let count = fromMaybe 5000 count_
pure $ CRChats $ maybe id take count_ chats (errs, previews) <- partitionEithers <$> withStore' (\db -> getChatPreviews db user False (PTLast count) clqNoFilters)
toView $ CRChatErrors (Just user) (map ChatErrorStore errs)
pure $ CRChats previews
LastMessages (Just chatName) count search -> withUser $ \user -> do LastMessages (Just chatName) count search -> withUser $ \user -> do
chatRef <- getChatRef user chatName chatRef <- getChatRef user chatName
chatResp <- processChatCommand $ APIGetChat chatRef (CPLast count) search chatResp <- processChatCommand $ APIGetChat chatRef (CPLast count) search
@ -2690,21 +2696,21 @@ getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of
getTmpHandle :: FilePath -> m Handle getTmpHandle :: FilePath -> m Handle
getTmpHandle fPath = openFile fPath AppendMode `catchThrow` (ChatError . CEFileInternal . show) getTmpHandle fPath = openFile fPath AppendMode `catchThrow` (ChatError . CEFileInternal . show)
acceptContactRequest :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> m Contact acceptContactRequest :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> Bool -> m Contact
acceptContactRequest user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = cp, userContactLinkId, xContactId} incognitoProfile = do acceptContactRequest user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = cp, userContactLinkId, xContactId} incognitoProfile contactUsed = do
subMode <- chatReadVar subscriptionMode subMode <- chatReadVar subscriptionMode
let profileToSend = profileToSendOnAccept user incognitoProfile let profileToSend = profileToSendOnAccept user incognitoProfile
dm <- directMessage $ XInfo profileToSend dm <- directMessage $ XInfo profileToSend
acId <- withAgent $ \a -> acceptContact a True invId dm subMode acId <- withAgent $ \a -> acceptContact a True invId dm subMode
withStore' $ \db -> createAcceptedContact db user acId (fromJVersionRange cReqChatVRange) cName profileId cp userContactLinkId xContactId incognitoProfile subMode withStore' $ \db -> createAcceptedContact db user acId (fromJVersionRange cReqChatVRange) cName profileId cp userContactLinkId xContactId incognitoProfile subMode contactUsed
acceptContactRequestAsync :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> m Contact acceptContactRequestAsync :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> Bool -> m Contact
acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile = do acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile contactUsed = do
subMode <- chatReadVar subscriptionMode subMode <- chatReadVar subscriptionMode
let profileToSend = profileToSendOnAccept user incognitoProfile let profileToSend = profileToSendOnAccept user incognitoProfile
(cmdId, acId) <- agentAcceptContactAsync user True invId (XInfo profileToSend) subMode (cmdId, acId) <- agentAcceptContactAsync user True invId (XInfo profileToSend) subMode
withStore' $ \db -> do withStore' $ \db -> do
ct@Contact {activeConn} <- createAcceptedContact db user acId (fromJVersionRange cReqChatVRange) cName profileId p userContactLinkId xContactId incognitoProfile subMode ct@Contact {activeConn} <- createAcceptedContact db user acId (fromJVersionRange cReqChatVRange) cName profileId p userContactLinkId xContactId incognitoProfile subMode contactUsed
forM_ activeConn $ \Connection {connId} -> setCommandConnId db user cmdId connId forM_ activeConn $ \Connection {connId} -> setCommandConnId db user cmdId connId
pure ct pure ct
@ -3384,20 +3390,20 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
doProbeContacts = isJust groupLinkId doProbeContacts = isJust groupLinkId
probeMatchingContactsAndMembers ct (contactConnIncognito ct) doProbeContacts probeMatchingContactsAndMembers ct (contactConnIncognito ct) doProbeContacts
withStore' $ \db -> resetContactConnInitiated db user conn withStore' $ \db -> resetContactConnInitiated db user conn
forM_ viaUserContactLink $ \userContactLinkId -> forM_ viaUserContactLink $ \userContactLinkId -> do
withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId
Just (UserContactLink {autoAccept = Just AutoAccept {autoReply = mc_}}, groupId_, gLinkMemRole) -> do let (UserContactLink {autoAccept}, groupId_, gLinkMemRole) = ucl
forM_ mc_ $ \mc -> do forM_ autoAccept $ \(AutoAccept {autoReply = mc_}) ->
(msg, _) <- sendDirectContactMessage ct (XMsgNew $ MCSimple (extMsgContent mc Nothing)) forM_ mc_ $ \mc -> do
ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) (msg, _) <- sendDirectContactMessage ct (XMsgNew $ MCSimple (extMsgContent mc Nothing))
toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc)
forM_ groupId_ $ \groupId -> do toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci)
groupInfo <- withStore $ \db -> getGroupInfo db user groupId forM_ groupId_ $ \groupId -> do
subMode <- chatReadVar subscriptionMode groupInfo <- withStore $ \db -> getGroupInfo db user groupId
groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation subMode subMode <- chatReadVar subscriptionMode
gVar <- asks idsDrg groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation subMode
withStore $ \db -> createNewContactMemberAsync db gVar user groupInfo ct gLinkMemRole groupConnIds (fromJVersionRange peerChatVRange) subMode gVar <- asks idsDrg
_ -> pure () withStore $ \db -> createNewContactMemberAsync db gVar user groupInfo ct gLinkMemRole groupConnIds (fromJVersionRange peerChatVRange) subMode
Just (gInfo, m@GroupMember {activeConn}) -> Just (gInfo, m@GroupMember {activeConn}) ->
when (maybe False ((== ConnReady) . connStatus) activeConn) $ do when (maybe False ((== ConnReady) . connStatus) activeConn) $ do
notifyMemberConnected gInfo m $ Just ct notifyMemberConnected gInfo m $ Just ct
@ -3915,28 +3921,27 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
withStore (\db -> createOrUpdateContactRequest db user userContactLinkId invId chatVRange p xContactId_) >>= \case withStore (\db -> createOrUpdateContactRequest db user userContactLinkId invId chatVRange p xContactId_) >>= \case
CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact
CORRequest cReq -> do CORRequest cReq -> do
withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId
Just (UserContactLink {autoAccept}, groupId_, gLinkMemRole) -> let (UserContactLink {autoAccept}, groupId_, gLinkMemRole) = ucl
case autoAccept of case autoAccept of
Just AutoAccept {acceptIncognito} -> case groupId_ of Just AutoAccept {acceptIncognito} -> case groupId_ of
Nothing -> do Nothing -> do
-- [incognito] generate profile to send, create connection with incognito profile -- [incognito] generate profile to send, create connection with incognito profile
incognitoProfile <- if acceptIncognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing incognitoProfile <- if acceptIncognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing
ct <- acceptContactRequestAsync user cReq incognitoProfile ct <- acceptContactRequestAsync user cReq incognitoProfile True
toView $ CRAcceptingContactRequest user ct toView $ CRAcceptingContactRequest user ct
Just groupId -> do Just groupId -> do
gInfo <- withStore $ \db -> getGroupInfo db user groupId gInfo <- withStore $ \db -> getGroupInfo db user groupId
let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo
if isCompatibleRange chatVRange groupLinkNoContactVRange if isCompatibleRange chatVRange groupLinkNoContactVRange
then do then do
mem <- acceptGroupJoinRequestAsync user gInfo cReq gLinkMemRole profileMode mem <- acceptGroupJoinRequestAsync user gInfo cReq gLinkMemRole profileMode
createInternalChatItem user (CDGroupRcv gInfo mem) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing createInternalChatItem user (CDGroupRcv gInfo mem) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing
toView $ CRAcceptingGroupJoinRequestMember user gInfo mem toView $ CRAcceptingGroupJoinRequestMember user gInfo mem
else do else do
ct <- acceptContactRequestAsync user cReq profileMode ct <- acceptContactRequestAsync user cReq profileMode False
toView $ CRAcceptingGroupJoinRequest user gInfo ct toView $ CRAcceptingGroupJoinRequest user gInfo ct
_ -> toView $ CRReceivedContactRequest user cReq _ -> toView $ CRReceivedContactRequest user cReq
_ -> pure ()
memberCanSend :: GroupMember -> m () -> m () memberCanSend :: GroupMember -> m () -> m ()
memberCanSend mem a memberCanSend mem a
@ -4932,7 +4937,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
conn' <- updatePeerChatVRange activeConn chatVRange conn' <- updatePeerChatVRange activeConn chatVRange
case chatMsgEvent of case chatMsgEvent of
XInfo p -> do XInfo p -> do
ct <- withStore $ \db -> createDirectContact db user conn' p let contactUsed = connDirect activeConn
ct <- withStore $ \db -> createDirectContact db user conn' p contactUsed
toView $ CRContactConnecting user ct toView $ CRContactConnecting user ct
pure conn' pure conn'
XGrpLinkInv glInv -> do XGrpLinkInv glInv -> do
@ -5988,7 +5994,13 @@ chatCommandP =
"/sql chat " *> (ExecChatStoreSQL <$> textP), "/sql chat " *> (ExecChatStoreSQL <$> textP),
"/sql agent " *> (ExecAgentStoreSQL <$> textP), "/sql agent " *> (ExecAgentStoreSQL <$> textP),
"/sql slow" $> SlowSQLQueries, "/sql slow" $> SlowSQLQueries,
"/_get chats " *> (APIGetChats <$> A.decimal <*> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False)), "/_get chats "
*> ( APIGetChats
<$> A.decimal
<*> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False)
<*> (A.space *> paginationByTimeP <|> pure (PTLast 5000))
<*> (A.space *> jsonP <|> pure clqNoFilters)
),
"/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP <*> optional (" search=" *> stringP)), "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP <*> optional (" search=" *> stringP)),
"/_get items " *> (APIGetChatItems <$> chatPaginationP <*> optional (" search=" *> stringP)), "/_get items " *> (APIGetChatItems <$> chatPaginationP <*> optional (" search=" *> stringP)),
"/_get item info " *> (APIGetChatItemInfo <$> chatRefP <* A.space <*> A.decimal), "/_get item info " *> (APIGetChatItemInfo <$> chatRefP <* A.space <*> A.decimal),
@ -6228,6 +6240,10 @@ chatCommandP =
(CPLast <$ "count=" <*> A.decimal) (CPLast <$ "count=" <*> A.decimal)
<|> (CPAfter <$ "after=" <*> A.decimal <* A.space <* "count=" <*> A.decimal) <|> (CPAfter <$ "after=" <*> A.decimal <* A.space <* "count=" <*> A.decimal)
<|> (CPBefore <$ "before=" <*> A.decimal <* A.space <* "count=" <*> A.decimal) <|> (CPBefore <$ "before=" <*> A.decimal <* A.space <* "count=" <*> A.decimal)
paginationByTimeP =
(PTLast <$ "count=" <*> A.decimal)
<|> (PTAfter <$ "after=" <*> strP <* A.space <* "count=" <*> A.decimal)
<|> (PTBefore <$ "before=" <*> strP <* A.space <* "count=" <*> A.decimal)
mcTextP = MCText . safeDecodeUtf8 <$> A.takeByteString mcTextP = MCText . safeDecodeUtf8 <$> A.takeByteString
msgContentP = "text " *> mcTextP <|> "json " *> jsonP msgContentP = "text " *> mcTextP <|> "json " *> jsonP
ciDeleteMode = "broadcast" $> CIDMBroadcast <|> "internal" $> CIDMInternal ciDeleteMode = "broadcast" $> CIDMBroadcast <|> "internal" $> CIDMInternal

View File

@ -251,7 +251,7 @@ data ChatCommand
| ExecChatStoreSQL Text | ExecChatStoreSQL Text
| ExecAgentStoreSQL Text | ExecAgentStoreSQL Text
| SlowSQLQueries | SlowSQLQueries
| APIGetChats {userId :: UserId, pendingConnections :: Bool} | APIGetChats {userId :: UserId, pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery}
| APIGetChat ChatRef ChatPagination (Maybe String) | APIGetChat ChatRef ChatPagination (Maybe String)
| APIGetChatItems ChatPagination (Maybe String) | APIGetChatItems ChatPagination (Maybe String)
| APIGetChatItemInfo ChatRef ChatItemId | APIGetChatItemInfo ChatRef ChatItemId
@ -690,6 +690,7 @@ data ChatResponse
| CRMessageError {user :: User, severity :: Text, errorMessage :: Text} | CRMessageError {user :: User, severity :: Text, errorMessage :: Text}
| CRChatCmdError {user_ :: Maybe User, chatError :: ChatError} | CRChatCmdError {user_ :: Maybe User, chatError :: ChatError}
| CRChatError {user_ :: Maybe User, chatError :: ChatError} | CRChatError {user_ :: Maybe User, chatError :: ChatError}
| CRChatErrors {user_ :: Maybe User, chatErrors :: [ChatError]}
| CRArchiveImported {archiveErrors :: [ArchiveError]} | CRArchiveImported {archiveErrors :: [ArchiveError]}
| CRTimedAction {action :: String, durationMilliseconds :: Int64} | CRTimedAction {action :: String, durationMilliseconds :: Int64}
deriving (Show) deriving (Show)
@ -738,6 +739,26 @@ logResponseToFile = \case
CRMessageError {} -> True CRMessageError {} -> True
_ -> False _ -> False
data ChatPagination
= CPLast Int
| CPAfter ChatItemId Int
| CPBefore ChatItemId Int
deriving (Show)
data PaginationByTime
= PTLast Int
| PTAfter UTCTime Int
| PTBefore UTCTime Int
deriving (Show)
data ChatListQuery
= CLQFilters {favorite :: Bool, unread :: Bool}
| CLQSearch {search :: String}
deriving (Show)
clqNoFilters :: ChatListQuery
clqNoFilters = CLQFilters {favorite = False, unread = False}
data ConnectionPlan data ConnectionPlan
= CPInvitationLink {invitationLinkPlan :: InvitationLinkPlan} = CPInvitationLink {invitationLinkPlan :: InvitationLinkPlan}
| CPContactAddress {contactAddressPlan :: ContactAddressPlan} | CPContactAddress {contactAddressPlan :: ContactAddressPlan}
@ -1274,6 +1295,8 @@ withAgent action =
$(JQ.deriveJSON (enumJSON $ dropPrefix "HS") ''HelpSection) $(JQ.deriveJSON (enumJSON $ dropPrefix "HS") ''HelpSection)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CLQ") ''ChatListQuery)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "ILP") ''InvitationLinkPlan) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "ILP") ''InvitationLinkPlan)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CAP") ''ContactAddressPlan) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CAP") ''ContactAddressPlan)

View File

@ -713,12 +713,6 @@ type ChatItemId = Int64
type ChatItemTs = UTCTime type ChatItemTs = UTCTime
data ChatPagination
= CPLast Int
| CPAfter ChatItemId Int
| CPBefore ChatItemId Int
deriving (Show)
data SChatType (c :: ChatType) where data SChatType (c :: ChatType) where
SCTDirect :: SChatType 'CTDirect SCTDirect :: SChatType 'CTDirect
SCTGroup :: SChatType 'CTGroup SCTGroup :: SChatType 'CTGroup

View File

@ -8,7 +8,7 @@ import Database.SQLite.Simple.QQ (sql)
m20221222_chat_ts :: Query m20221222_chat_ts :: Query
m20221222_chat_ts = m20221222_chat_ts =
[sql| [sql|
ALTER TABLE contacts ADD COLUMN chat_ts TEXT; ALTER TABLE contacts ADD COLUMN chat_ts TEXT; -- must be not NULL
ALTER TABLE groups ADD COLUMN chat_ts TEXT; ALTER TABLE groups ADD COLUMN chat_ts TEXT; -- must be not NULL
|] |]

View File

@ -0,0 +1,44 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20231207_chat_list_pagination where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20231207_chat_list_pagination :: Query
m20231207_chat_list_pagination =
[sql|
UPDATE contacts SET contact_used = 1
WHERE contact_id = (
SELECT contact_id FROM connections
WHERE conn_level = 0 AND via_group_link = 0
);
UPDATE contacts
SET chat_ts = updated_at
WHERE chat_ts IS NULL;
UPDATE groups
SET chat_ts = updated_at
WHERE chat_ts IS NULL;
CREATE INDEX idx_contacts_chat_ts ON contacts(user_id, chat_ts);
CREATE INDEX idx_groups_chat_ts ON groups(user_id, chat_ts);
CREATE INDEX idx_contact_requests_updated_at ON contact_requests(user_id, updated_at);
CREATE INDEX idx_connections_updated_at ON connections(user_id, updated_at);
CREATE INDEX idx_chat_items_contact_id_item_status ON chat_items(contact_id, item_status);
CREATE INDEX idx_chat_items_group_id_item_status ON chat_items(group_id, item_status);
|]
down_m20231207_chat_list_pagination :: Query
down_m20231207_chat_list_pagination =
[sql|
DROP INDEX idx_contacts_chat_ts;
DROP INDEX idx_groups_chat_ts;
DROP INDEX idx_contact_requests_updated_at;
DROP INDEX idx_connections_updated_at;
DROP INDEX idx_chat_items_contact_id_item_status;
DROP INDEX idx_chat_items_group_id_item_status;
|]

View File

@ -810,3 +810,18 @@ CREATE UNIQUE INDEX idx_remote_hosts_host_fingerprint ON remote_hosts(
CREATE UNIQUE INDEX idx_remote_controllers_ctrl_fingerprint ON remote_controllers( CREATE UNIQUE INDEX idx_remote_controllers_ctrl_fingerprint ON remote_controllers(
ctrl_fingerprint ctrl_fingerprint
); );
CREATE INDEX idx_contacts_chat_ts ON contacts(user_id, chat_ts);
CREATE INDEX idx_groups_chat_ts ON groups(user_id, chat_ts);
CREATE INDEX idx_contact_requests_updated_at ON contact_requests(
user_id,
updated_at
);
CREATE INDEX idx_connections_updated_at ON connections(user_id, updated_at);
CREATE INDEX idx_chat_items_contact_id_item_status ON chat_items(
contact_id,
item_status
);
CREATE INDEX idx_chat_items_group_id_item_status ON chat_items(
group_id,
item_status
);

View File

@ -201,15 +201,15 @@ createIncognitoProfile db User {userId} p = do
createdAt <- getCurrentTime createdAt <- getCurrentTime
createIncognitoProfile_ db userId createdAt p createIncognitoProfile_ db userId createdAt p
createDirectContact :: DB.Connection -> User -> Connection -> Profile -> ExceptT StoreError IO Contact createDirectContact :: DB.Connection -> User -> Connection -> Profile -> Bool -> ExceptT StoreError IO Contact
createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p@Profile {preferences} = do createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p@Profile {preferences} contactUsed = do
currentTs <- liftIO getCurrentTime currentTs <- liftIO getCurrentTime
(localDisplayName, contactId, profileId) <- createContact_ db userId p localAlias Nothing currentTs (Just currentTs) (localDisplayName, contactId, profileId) <- createContact_ db userId p localAlias Nothing currentTs contactUsed
liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, connId) liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, connId)
let profile = toLocalProfile profileId p localAlias let profile = toLocalProfile profileId p localAlias
userPreferences = emptyChatPrefs userPreferences = emptyChatPrefs
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
pure $ Contact {contactId, localDisplayName, profile, activeConn = Just conn, viaGroup = Nothing, contactUsed = False, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False} pure $ Contact {contactId, localDisplayName, profile, activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False}
deleteContactConnectionsAndFiles :: DB.Connection -> UserId -> Contact -> IO () deleteContactConnectionsAndFiles :: DB.Connection -> UserId -> Contact -> IO ()
deleteContactConnectionsAndFiles db userId Contact {contactId} = do deleteContactConnectionsAndFiles db userId Contact {contactId} = do
@ -650,8 +650,8 @@ deleteContactRequest db User {userId} contactRequestId = do
(userId, userId, contactRequestId) (userId, userId, contactRequestId)
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId) DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId)
createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionRange -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> IO Contact createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionRange -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> Bool -> IO Contact
createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode = do createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode contactUsed = do
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
createdAt <- getCurrentTime createdAt <- getCurrentTime
customUserProfileId <- forM incognitoProfile $ \case customUserProfileId <- forM incognitoProfile $ \case
@ -660,12 +660,12 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}
let userPreferences = fromMaybe emptyChatPrefs $ incognitoProfile >> preferences let userPreferences = fromMaybe emptyChatPrefs $ incognitoProfile >> preferences
DB.execute DB.execute
db db
"INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, chat_ts, xcontact_id) VALUES (?,?,?,?,?,?,?,?,?)" "INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, chat_ts, xcontact_id, contact_used) VALUES (?,?,?,?,?,?,?,?,?,?)"
(userId, localDisplayName, profileId, True, userPreferences, createdAt, createdAt, createdAt, xContactId) (userId, localDisplayName, profileId, True, userPreferences, createdAt, createdAt, createdAt, xContactId, contactUsed)
contactId <- insertedRowId db contactId <- insertedRowId db
conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn = Just conn, viaGroup = Nothing, contactUsed = False, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False} pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False}
getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64 getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64
getContactIdByName db User {userId} cName = getContactIdByName db User {userId} cName =

View File

@ -1149,7 +1149,7 @@ createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupM
Just (directCmdId, directAgentConnId) -> do Just (directCmdId, directAgentConnId) -> do
Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId mcvr memberContactId Nothing customUserProfileId cLevel currentTs subMode Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId mcvr memberContactId Nothing customUserProfileId cLevel currentTs subMode
liftIO $ setCommandConnId db user directCmdId directConnId liftIO $ setCommandConnId db user directCmdId directConnId
(localDisplayName, contactId, memProfileId) <- createContact_ db userId memberProfile "" (Just groupId) currentTs Nothing (localDisplayName, contactId, memProfileId) <- createContact_ db userId memberProfile "" (Just groupId) currentTs False
liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, directConnId) liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, directConnId)
pure $ NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memInvitedBy = IBUnknown, memInvitedByGroupMemberId = Nothing, localDisplayName, memContactId = Just contactId, memProfileId} pure $ NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memInvitedBy = IBUnknown, memInvitedByGroupMemberId = Nothing, localDisplayName, memContactId = Just contactId, memProfileId}
Nothing -> do Nothing -> do
@ -1178,12 +1178,12 @@ createIntroToMemberContact db user@User {userId} GroupMember {memberContactId =
DB.execute DB.execute
db db
[sql| [sql|
INSERT INTO contacts (contact_profile_id, via_group, local_display_name, user_id, created_at, updated_at) INSERT INTO contacts (contact_profile_id, via_group, local_display_name, user_id, created_at, updated_at, chat_ts)
SELECT contact_profile_id, group_id, ?, ?, ?, ? SELECT contact_profile_id, group_id, ?, ?, ?, ?, ?
FROM group_members FROM group_members
WHERE group_member_id = ? WHERE group_member_id = ?
|] |]
(localDisplayName, userId, ts, ts, groupMemberId) (localDisplayName, userId, ts, ts, ts, groupMemberId)
contactId <- insertedRowId db contactId <- insertedRowId db
DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, ts, connId) DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, ts, connId)
pure contactId pure contactId

View File

@ -1,6 +1,7 @@
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-} {-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
@ -109,14 +110,15 @@ import Data.Bifunctor (first)
import Data.ByteString.Char8 (ByteString) import Data.ByteString.Char8 (ByteString)
import Data.Either (fromRight, rights) import Data.Either (fromRight, rights)
import Data.Int (Int64) import Data.Int (Int64)
import Data.List (sortOn) import Data.List (sortBy)
import Data.Maybe (fromMaybe, isJust, mapMaybe) import Data.Maybe (fromMaybe, isJust, mapMaybe)
import Data.Ord (Down (..)) import Data.Ord (Down (..), comparing)
import Data.Text (Text) import Data.Text (Text)
import Data.Time (addUTCTime) import Data.Time (addUTCTime)
import Data.Time.Clock (UTCTime (..), getCurrentTime) import Data.Time.Clock (UTCTime (..), getCurrentTime)
import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..)) import Database.SQLite.Simple (NamedParam (..), Only (..), Query, (:.) (..))
import Database.SQLite.Simple.QQ (sql) import Database.SQLite.Simple.QQ (sql)
import Simplex.Chat.Controller (ChatListQuery (..), ChatPagination (..), PaginationByTime (..))
import Simplex.Chat.Markdown import Simplex.Chat.Markdown
import Simplex.Chat.Messages import Simplex.Chat.Messages
import Simplex.Chat.Messages.CIContent import Simplex.Chat.Messages.CIContent
@ -467,7 +469,7 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe
<$> DB.queryNamed <$> DB.queryNamed
db db
[sql| [sql|
SELECT i.chat_item_id, SELECT i.chat_item_id,
-- GroupMember -- GroupMember
m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category,
m.member_status, m.show_messages, m.invited_by, m.invited_by_group_member_id, 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.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
@ -486,209 +488,402 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe
ciQuoteGroup [] = ciQuote Nothing $ CIQGroupRcv Nothing ciQuoteGroup [] = ciQuote Nothing $ CIQGroupRcv Nothing
ciQuoteGroup ((Only itemId :. memberRow) : _) = ciQuote itemId . CIQGroupRcv . Just $ toGroupMember userContactId memberRow ciQuoteGroup ((Only itemId :. memberRow) : _) = ciQuote itemId . CIQGroupRcv . Just $ toGroupMember userContactId memberRow
getChatPreviews :: DB.Connection -> User -> Bool -> IO [AChat] getChatPreviews :: DB.Connection -> User -> Bool -> PaginationByTime -> ChatListQuery -> IO [Either StoreError AChat]
getChatPreviews db user withPCC = do getChatPreviews db user withPCC pagination query = do
directChats <- getDirectChatPreviews_ db user directChats <- findDirectChatPreviews_ db user pagination query
groupChats <- getGroupChatPreviews_ db user groupChats <- findGroupChatPreviews_ db user pagination query
cReqChats <- getContactRequestChatPreviews_ db user cReqChats <- getContactRequestChatPreviews_ db user pagination query
connChats <- getContactConnectionChatPreviews_ db user withPCC connChats <- if withPCC then getContactConnectionChatPreviews_ db user pagination query else pure []
pure $ sortOn (Down . ts) (directChats <> groupChats <> cReqChats <> connChats) let refs = sortTake $ concat [directChats, groupChats, cReqChats, connChats]
mapM (runExceptT <$> getChatPreview) refs
where where
ts :: AChat -> UTCTime ts :: AChatPreviewData -> UTCTime
ts (AChat _ Chat {chatInfo, chatItems}) = case chatInfoChatTs chatInfo of ts (ACPD _ cpd) = case cpd of
Just chatTs -> chatTs (DirectChatPD t _ _) -> t
Nothing -> case chatItems of (GroupChatPD t _ _) -> t
ci : _ -> max (chatItemTs ci) (chatInfoUpdatedAt chatInfo) (ContactRequestPD t _) -> t
_ -> chatInfoUpdatedAt chatInfo (ContactConnectionPD t _) -> t
sortTake = case pagination of
PTLast count -> take count . sortBy (comparing $ Down . ts)
PTAfter _ count -> reverse . take count . sortBy (comparing ts)
PTBefore _ count -> take count . sortBy (comparing $ Down . ts)
getChatPreview :: AChatPreviewData -> ExceptT StoreError IO AChat
getChatPreview (ACPD cType cpd) = case cType of
SCTDirect -> getDirectChatPreview_ db user cpd
SCTGroup -> getGroupChatPreview_ db user cpd
SCTContactRequest -> let (ContactRequestPD _ chat) = cpd in pure chat
SCTContactConnection -> let (ContactConnectionPD _ chat) = cpd in pure chat
getDirectChatPreviews_ :: DB.Connection -> User -> IO [AChat] data ChatPreviewData (c :: ChatType) where
getDirectChatPreviews_ db user@User {userId} = do DirectChatPD :: UTCTime -> ContactId -> Maybe ChatStats -> ChatPreviewData 'CTDirect
currentTs <- getCurrentTime GroupChatPD :: UTCTime -> GroupId -> Maybe ChatStats -> ChatPreviewData 'CTGroup
map (toDirectChatPreview currentTs) ContactRequestPD :: UTCTime -> AChat -> ChatPreviewData 'CTContactRequest
<$> DB.query ContactConnectionPD :: UTCTime -> AChat -> ChatPreviewData 'CTContactConnection
db
[sql| data AChatPreviewData = forall c. ChatTypeI c => ACPD (SChatType c) (ChatPreviewData c)
SELECT
-- Contact paginationByTimeFilter :: PaginationByTime -> (Query, [NamedParam])
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, paginationByTimeFilter = \case
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, PTLast count -> ("\nORDER BY ts DESC LIMIT :count", [":count" := count])
-- Connection PTAfter ts count -> ("\nAND ts > :ts ORDER BY ts ASC LIMIT :count", [":ts" := ts, ":count" := count])
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, PTBefore ts count -> ("\nAND ts < :ts ORDER BY ts DESC LIMIT :count", [":ts" := ts, ":count" := count])
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, type MaybeChatStatsRow = (Maybe Int, Maybe ChatItemId, Maybe Bool)
-- ChatStats
COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat, toMaybeChatStats :: MaybeChatStatsRow -> Maybe ChatStats
-- ChatItem toMaybeChatStats (Just unreadCount, Just minUnreadItemId, Just unreadChat) = Just ChatStats {unreadCount, minUnreadItemId, unreadChat}
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, toMaybeChatStats _ = Nothing
-- CIFile
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, findDirectChatPreviews_ :: DB.Connection -> User -> PaginationByTime -> ChatListQuery -> IO [AChatPreviewData]
-- DirectQuote findDirectChatPreviews_ db User {userId} pagination clq =
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent map toPreview <$> getPreviews
FROM contacts ct where
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id toPreview :: (ContactId, UTCTime) :. MaybeChatStatsRow -> AChatPreviewData
LEFT JOIN connections c ON c.contact_id = ct.contact_id toPreview ((contactId, ts) :. statsRow_) =
LEFT JOIN ( ACPD SCTDirect $ DirectChatPD ts contactId (toMaybeChatStats statsRow_)
SELECT contact_id, chat_item_id, MAX(created_at) (pagQuery, pagParams) = paginationByTimeFilter pagination
FROM chat_items getPreviews = case clq of
GROUP BY contact_id CLQFilters {favorite = False, unread = False} ->
) LastItems ON LastItems.contact_id = ct.contact_id DB.queryNamed
LEFT JOIN chat_items i ON i.contact_id = LastItems.contact_id db
AND i.chat_item_id = LastItems.chat_item_id ( [sql|
LEFT JOIN files f ON f.chat_item_id = i.chat_item_id SELECT ct.contact_id, ct.chat_ts as ts, NULL, NULL, NULL
LEFT JOIN ( FROM contacts ct
SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used
FROM chat_items |]
WHERE item_status = ? <> pagQuery
GROUP BY contact_id
) ChatStats ON ChatStats.contact_id = ct.contact_id
LEFT JOIN chat_items ri ON ri.user_id = i.user_id AND ri.contact_id = i.contact_id AND ri.shared_msg_id = i.quoted_shared_msg_id
WHERE ct.user_id = ?
AND ct.is_user = 0
AND ct.deleted = 0
AND (
(
((c.conn_level = 0 AND c.via_group_link = 0) OR ct.contact_used = 1)
AND c.connection_id = (
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_created_at DESC
LIMIT 1
)
)
)
OR c.connection_id IS NULL
) )
ORDER BY i.item_ts DESC ([":user_id" := userId] <> pagParams)
|] CLQFilters {favorite = True, unread = False} ->
(CISRcvNew, userId, ConnReady, ConnSndReady) DB.queryNamed
where db
toDirectChatPreview :: UTCTime -> ContactRow :. MaybeConnectionRow :. ChatStatsRow :. MaybeChatItemRow :. QuoteRow -> AChat ( [sql|
toDirectChatPreview currentTs (contactRow :. connRow :. statsRow :. ciRow_) = SELECT ct.contact_id, ct.chat_ts as ts, NULL, NULL, NULL
let contact = toContact user $ contactRow :. connRow FROM contacts ct
ci_ = toDirectChatItemList currentTs ciRow_ WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used
stats = toChatStats statsRow AND ct.favorite = 1
in AChat SCTDirect $ Chat (DirectChat contact) ci_ stats |]
<> pagQuery
)
([":user_id" := userId] <> pagParams)
CLQFilters {favorite = False, unread = True} ->
DB.queryNamed
db
( [sql|
SELECT ct.contact_id, ct.chat_ts as ts, COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat
FROM contacts ct
LEFT JOIN (
SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread
FROM chat_items
WHERE item_status = :rcv_new
GROUP BY contact_id
) ChatStats ON ChatStats.contact_id = ct.contact_id
WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used
AND (ct.unread_chat = 1 OR ChatStats.UnreadCount > 0)
|]
<> pagQuery
)
([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams)
CLQFilters {favorite = True, unread = True} ->
DB.queryNamed
db
( [sql|
SELECT ct.contact_id, ct.chat_ts as ts, COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat
FROM contacts ct
LEFT JOIN (
SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread
FROM chat_items
WHERE item_status = :rcv_new
GROUP BY contact_id
) ChatStats ON ChatStats.contact_id = ct.contact_id
WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used
AND (ct.favorite = 1
OR ct.unread_chat = 1 OR ChatStats.UnreadCount > 0)
|]
<> pagQuery
)
([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams)
CLQSearch {search} ->
DB.queryNamed
db
( [sql|
SELECT ct.contact_id, ct.chat_ts as ts, NULL, NULL, NULL
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used
AND (
ct.local_display_name LIKE '%' || :search || '%'
OR cp.display_name LIKE '%' || :search || '%'
OR cp.full_name LIKE '%' || :search || '%'
OR cp.local_alias LIKE '%' || :search || '%'
)
|]
<> pagQuery
)
([":user_id" := userId, ":search" := search] <> pagParams)
getGroupChatPreviews_ :: DB.Connection -> User -> IO [AChat] getDirectChatPreview_ :: DB.Connection -> User -> ChatPreviewData 'CTDirect -> ExceptT StoreError IO AChat
getGroupChatPreviews_ db User {userId, userContactId} = do getDirectChatPreview_ db user (DirectChatPD _ contactId stats_) = do
currentTs <- getCurrentTime contact <- getContact db user contactId
map (toGroupChatPreview currentTs) lastItem <- getLastItem
<$> DB.query stats <- maybe getChatStats pure stats_
db pure $ AChat SCTDirect (Chat (DirectChat contact) lastItem stats)
[sql|
SELECT
-- GroupInfo
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.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.invited_by, mu.invited_by_group_member_id, 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,
-- ChatItem
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live,
-- CIFile
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,
-- CIMeta forwardedByMember
i.forwarded_by_group_member_id,
-- Maybe GroupMember - sender
m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category,
m.member_status, m.show_messages, m.invited_by, m.invited_by_group_member_id, 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.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category,
rm.member_status, rm.show_messages, rm.invited_by, rm.invited_by_group_member_id, 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.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category,
dbm.member_status, dbm.show_messages, dbm.invited_by, dbm.invited_by_group_member_id, 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
JOIN group_members mu ON mu.group_id = g.group_id
JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id)
LEFT JOIN (
SELECT group_id, chat_item_id, MAX(item_ts)
FROM chat_items
GROUP BY group_id
) LastItems ON LastItems.group_id = g.group_id
LEFT JOIN chat_items i ON i.group_id = LastItems.group_id
AND i.chat_item_id = LastItems.chat_item_id
LEFT JOIN files f ON f.chat_item_id = i.chat_item_id
LEFT JOIN (
SELECT group_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread
FROM chat_items
WHERE item_status = ?
GROUP BY group_id
) ChatStats ON ChatStats.group_id = g.group_id
LEFT JOIN group_members m ON m.group_member_id = i.group_member_id
LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
LEFT JOIN chat_items ri ON ri.shared_msg_id = i.quoted_shared_msg_id AND ri.group_id = i.group_id
LEFT JOIN group_members rm ON rm.group_member_id = ri.group_member_id
LEFT JOIN contact_profiles rp ON rp.contact_profile_id = COALESCE(rm.member_profile_id, rm.contact_profile_id)
LEFT JOIN group_members dbm ON dbm.group_member_id = i.item_deleted_by_group_member_id
LEFT JOIN contact_profiles dbp ON dbp.contact_profile_id = COALESCE(dbm.member_profile_id, dbm.contact_profile_id)
WHERE g.user_id = ? AND mu.contact_id = ?
ORDER BY i.item_ts DESC
|]
(CISRcvNew, userId, userContactId)
where where
toGroupChatPreview :: UTCTime -> GroupInfoRow :. ChatStatsRow :. MaybeGroupChatItemRow -> AChat getLastItem :: ExceptT StoreError IO [CChatItem 'CTDirect]
toGroupChatPreview currentTs (groupInfoRow :. statsRow :. ciRow_) = getLastItem =
let groupInfo = toGroupInfo userContactId groupInfoRow liftIO getLastItemId >>= \case
ci_ = toGroupChatItemList currentTs userContactId ciRow_ Nothing -> pure []
stats = toChatStats statsRow Just lastItemId -> (: []) <$> getDirectChatItem db user contactId lastItemId
in AChat SCTGroup $ Chat (GroupChat groupInfo) ci_ stats getLastItemId :: IO (Maybe ChatItemId)
getLastItemId =
maybeFirstRow fromOnly $
DB.query
db
[sql|
SELECT chat_item_id FROM (
SELECT contact_id, chat_item_id, MAX(created_at)
FROM chat_items
WHERE contact_id = ?
GROUP BY contact_id
)
|]
(Only contactId)
getChatStats :: ExceptT StoreError IO ChatStats
getChatStats = do
r_ <- liftIO getUnreadStats
let (unreadCount, minUnreadItemId) = maybe (0, 0) (\(_, unreadCnt, minId) -> (unreadCnt, minId)) r_
-- unread_chat could be read into contact to not search twice
unreadChat <-
ExceptT . firstRow fromOnly (SEInternalError $ "unread_chat not found for contact " <> show contactId) $
DB.query db "SELECT unread_chat FROM contacts WHERE contact_id = ?" (Only contactId)
pure ChatStats {unreadCount, minUnreadItemId, unreadChat}
getUnreadStats :: IO (Maybe (ContactId, Int, ChatItemId))
getUnreadStats =
maybeFirstRow id $
DB.query
db
[sql|
SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread
FROM chat_items
WHERE contact_id = ? AND item_status = ?
GROUP BY contact_id
|]
(contactId, CISRcvNew)
getContactRequestChatPreviews_ :: DB.Connection -> User -> IO [AChat] findGroupChatPreviews_ :: DB.Connection -> User -> PaginationByTime -> ChatListQuery -> IO [AChatPreviewData]
getContactRequestChatPreviews_ db User {userId} = findGroupChatPreviews_ db User {userId} pagination clq =
map toContactRequestChatPreview map toPreview <$> getPreviews
<$> DB.query
db
[sql|
SELECT
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at,
cr.peer_chat_min_version, cr.peer_chat_max_version
FROM contact_requests cr
JOIN connections c ON c.user_contact_link_id = cr.user_contact_link_id
JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id
JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id
WHERE cr.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL
|]
(userId, userId)
where where
toContactRequestChatPreview :: ContactRequestRow -> AChat toPreview :: (GroupId, UTCTime) :. MaybeChatStatsRow -> AChatPreviewData
toContactRequestChatPreview cReqRow = toPreview ((groupId, ts) :. statsRow_) =
let cReq = toContactRequest cReqRow ACPD SCTGroup $ GroupChatPD ts groupId (toMaybeChatStats statsRow_)
(pagQuery, pagParams) = paginationByTimeFilter pagination
getPreviews = case clq of
CLQFilters {favorite = False, unread = False} ->
DB.queryNamed
db
( [sql|
SELECT g.group_id, g.chat_ts as ts, NULL, NULL, NULL
FROM groups g
WHERE g.user_id = :user_id
|]
<> pagQuery
)
([":user_id" := userId] <> pagParams)
CLQFilters {favorite = True, unread = False} ->
DB.queryNamed
db
( [sql|
SELECT g.group_id, g.chat_ts as ts, NULL, NULL, NULL
FROM groups g
WHERE g.user_id = :user_id
AND g.favorite = 1
|]
<> pagQuery
)
([":user_id" := userId] <> pagParams)
CLQFilters {favorite = False, unread = True} ->
DB.queryNamed
db
( [sql|
SELECT g.group_id, g.chat_ts as ts, COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), g.unread_chat
FROM groups g
LEFT JOIN (
SELECT group_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread
FROM chat_items
WHERE item_status = :rcv_new
GROUP BY group_id
) ChatStats ON ChatStats.group_id = g.group_id
WHERE g.user_id = :user_id
AND (g.unread_chat = 1 OR ChatStats.UnreadCount > 0)
|]
<> pagQuery
)
([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams)
CLQFilters {favorite = True, unread = True} ->
DB.queryNamed
db
( [sql|
SELECT g.group_id, g.chat_ts as ts, COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), g.unread_chat
FROM groups g
LEFT JOIN (
SELECT group_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread
FROM chat_items
WHERE item_status = :rcv_new
GROUP BY group_id
) ChatStats ON ChatStats.group_id = g.group_id
WHERE g.user_id = :user_id
AND (g.favorite = 1
OR g.unread_chat = 1 OR ChatStats.UnreadCount > 0)
|]
<> pagQuery
)
([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams)
CLQSearch {search} ->
DB.queryNamed
db
( [sql|
SELECT g.group_id, g.chat_ts as ts, NULL, NULL, NULL
FROM groups g
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
WHERE g.user_id = :user_id
AND (
g.local_display_name LIKE '%' || :search || '%'
OR gp.display_name LIKE '%' || :search || '%'
OR gp.full_name LIKE '%' || :search || '%'
OR gp.description LIKE '%' || :search || '%'
)
|]
<> pagQuery
)
([":user_id" := userId, ":search" := search] <> pagParams)
getGroupChatPreview_ :: DB.Connection -> User -> ChatPreviewData 'CTGroup -> ExceptT StoreError IO AChat
getGroupChatPreview_ db user (GroupChatPD _ groupId stats_) = do
groupInfo <- getGroupInfo db user groupId
lastItem <- getLastItem
stats <- maybe getChatStats pure stats_
pure $ AChat SCTGroup (Chat (GroupChat groupInfo) lastItem stats)
where
getLastItem :: ExceptT StoreError IO [CChatItem 'CTGroup]
getLastItem =
liftIO getLastItemId >>= \case
Nothing -> pure []
Just lastItemId -> (: []) <$> getGroupChatItem db user groupId lastItemId
getLastItemId :: IO (Maybe ChatItemId)
getLastItemId =
maybeFirstRow fromOnly $
DB.query
db
[sql|
SELECT chat_item_id FROM (
SELECT group_id, chat_item_id, MAX(item_ts)
FROM chat_items
WHERE group_id = ?
GROUP BY group_id
)
|]
(Only groupId)
getChatStats :: ExceptT StoreError IO ChatStats
getChatStats = do
r_ <- liftIO getUnreadStats
let (unreadCount, minUnreadItemId) = maybe (0, 0) (\(_, unreadCnt, minId) -> (unreadCnt, minId)) r_
-- unread_chat could be read into group to not search twice
unreadChat <-
ExceptT . firstRow fromOnly (SEInternalError $ "unread_chat not found for group " <> show groupId) $
DB.query db "SELECT unread_chat FROM groups WHERE group_id = ?" (Only groupId)
pure ChatStats {unreadCount, minUnreadItemId, unreadChat}
getUnreadStats :: IO (Maybe (GroupId, Int, ChatItemId))
getUnreadStats =
maybeFirstRow id $
DB.query
db
[sql|
SELECT group_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread
FROM chat_items
WHERE group_id = ? AND item_status = ?
GROUP BY group_id
|]
(groupId, CISRcvNew)
getContactRequestChatPreviews_ :: DB.Connection -> User -> PaginationByTime -> ChatListQuery -> IO [AChatPreviewData]
getContactRequestChatPreviews_ db User {userId} pagination clq = case clq of
CLQFilters {favorite = False, unread = False} -> query ""
CLQFilters {favorite = True, unread = False} -> pure []
CLQFilters {favorite = False, unread = True} -> query ""
CLQFilters {favorite = True, unread = True} -> query ""
CLQSearch {search} -> query search
where
(pagQuery, pagParams) = paginationByTimeFilter pagination
query search =
map toPreview
<$> DB.queryNamed
db
( [sql|
SELECT
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences,
cr.created_at, cr.updated_at as ts,
cr.peer_chat_min_version, cr.peer_chat_max_version
FROM contact_requests cr
JOIN connections c ON c.user_contact_link_id = cr.user_contact_link_id
JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id
JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id
WHERE cr.user_id = :user_id
AND uc.user_id = :user_id
AND uc.local_display_name = ''
AND uc.group_id IS NULL
AND (
cr.local_display_name LIKE '%' || :search || '%'
OR p.display_name LIKE '%' || :search || '%'
OR p.full_name LIKE '%' || :search || '%'
)
|]
<> pagQuery
)
([":user_id" := userId, ":search" := search] <> pagParams)
toPreview :: ContactRequestRow -> AChatPreviewData
toPreview cReqRow =
let cReq@UserContactRequest {updatedAt} = toContactRequest cReqRow
stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
in AChat SCTContactRequest $ Chat (ContactRequest cReq) [] stats aChat = AChat SCTContactRequest $ Chat (ContactRequest cReq) [] stats
in ACPD SCTContactRequest $ ContactRequestPD updatedAt aChat
getContactConnectionChatPreviews_ :: DB.Connection -> User -> Bool -> IO [AChat] getContactConnectionChatPreviews_ :: DB.Connection -> User -> PaginationByTime -> ChatListQuery -> IO [AChatPreviewData]
getContactConnectionChatPreviews_ _ _ False = pure [] getContactConnectionChatPreviews_ db User {userId} pagination clq = case clq of
getContactConnectionChatPreviews_ db User {userId} _ = CLQFilters {favorite = False, unread = False} -> query ""
map toContactConnectionChatPreview CLQFilters {favorite = True, unread = False} -> pure []
<$> DB.query CLQFilters {favorite = False, unread = True} -> pure []
db CLQFilters {favorite = True, unread = True} -> pure []
[sql| CLQSearch {search} -> query search
SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at
FROM connections
WHERE user_id = ? AND conn_type = ? AND contact_id IS NULL AND conn_level = 0 AND via_contact IS NULL AND (via_group_link = 0 || (via_group_link = 1 AND group_link_id IS NOT NULL))
|]
(userId, ConnContact)
where where
toContactConnectionChatPreview :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> AChat (pagQuery, pagParams) = paginationByTimeFilter pagination
toContactConnectionChatPreview connRow = query search =
let conn = toPendingContactConnection connRow map toPreview
<$> DB.queryNamed
db
( [sql|
SELECT
connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id,
custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at as ts
FROM connections
WHERE user_id = :user_id
AND conn_type = :conn_contact
AND contact_id IS NULL
AND conn_level = 0
AND via_contact IS NULL
AND (via_group_link = 0 || (via_group_link = 1 AND group_link_id IS NOT NULL))
AND local_alias LIKE '%' || :search || '%'
|]
<> pagQuery
)
([":user_id" := userId, ":conn_contact" := ConnContact, ":search" := search] <> pagParams)
toPreview :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> AChatPreviewData
toPreview connRow =
let conn@PendingContactConnection {updatedAt} = toPendingContactConnection connRow
stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
in AChat SCTContactConnection $ Chat (ContactConnection conn) [] stats aChat = AChat SCTContactConnection $ Chat (ContactConnection conn) [] stats
in ACPD SCTContactConnection $ ContactConnectionPD updatedAt aChat
getDirectChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect) getDirectChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect)
getDirectChat db user contactId pagination search_ = do getDirectChat db user contactId pagination search_ = do
@ -993,19 +1188,12 @@ setGroupChatItemDeleteAt db User {userId} groupId chatItemId deleteAt =
"UPDATE chat_items SET timed_delete_at = ? WHERE user_id = ? AND group_id = ? AND chat_item_id = ?" "UPDATE chat_items SET timed_delete_at = ? WHERE user_id = ? AND group_id = ? AND chat_item_id = ?"
(deleteAt, userId, groupId, chatItemId) (deleteAt, userId, groupId, chatItemId)
type ChatStatsRow = (Int, ChatItemId, Bool)
toChatStats :: ChatStatsRow -> ChatStats
toChatStats (unreadCount, minUnreadItemId, unreadChat) = ChatStats {unreadCount, minUnreadItemId, unreadChat}
type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe ACIFileStatus, Maybe FileProtocol) type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe ACIFileStatus, Maybe FileProtocol)
type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe Bool) type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe Bool)
type ChatItemRow = (Int64, ChatItemTs, AMsgDirection, Text, Text, ACIStatus, Maybe SharedMsgId) :. (Int, 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 Int, Maybe UTCTime, Maybe Bool, Maybe UTCTime, Maybe UTCTime) :. ChatItemModeRow :. MaybeCIFIleRow
type QuoteRow = (Maybe ChatItemId, Maybe SharedMsgId, Maybe UTCTime, Maybe MsgContent, Maybe Bool) type QuoteRow = (Maybe ChatItemId, Maybe SharedMsgId, Maybe UTCTime, Maybe MsgContent, Maybe Bool)
toDirectQuote :: QuoteRow -> Maybe (CIQuote 'CTDirect) toDirectQuote :: QuoteRow -> Maybe (CIQuote 'CTDirect)
@ -1055,15 +1243,8 @@ toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentT
ciTimed :: Maybe CITimed ciTimed :: Maybe CITimed
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
toDirectChatItemList :: UTCTime -> MaybeChatItemRow :. QuoteRow -> [CChatItem 'CTDirect]
toDirectChatItemList currentTs (((Just itemId, Just itemTs, Just msgDir, Just itemContent, Just itemText, Just itemStatus, sharedMsgId) :. (Just itemDeleted, deletedTs, itemEdited, Just createdAt, Just updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. quoteRow) =
either (const []) (: []) $ toDirectChatItem currentTs (((itemId, itemTs, msgDir, itemContent, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. quoteRow)
toDirectChatItemList _ _ = []
type GroupQuoteRow = QuoteRow :. MaybeGroupMemberRow type GroupQuoteRow = QuoteRow :. MaybeGroupMemberRow
type MaybeGroupChatItemRow = MaybeChatItemRow :. Only (Maybe GroupMemberId) :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow
toGroupQuote :: QuoteRow -> Maybe GroupMember -> Maybe (CIQuote 'CTGroup) toGroupQuote :: QuoteRow -> Maybe GroupMember -> Maybe (CIQuote 'CTGroup)
toGroupQuote qr@(_, _, _, _, quotedSent) quotedMember_ = toQuote qr $ direction quotedSent quotedMember_ toGroupQuote qr@(_, _, _, _, quotedSent) quotedMember_ = toQuote qr $ direction quotedSent quotedMember_
where where
@ -1114,11 +1295,6 @@ toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir,
ciTimed :: Maybe CITimed ciTimed :: Maybe CITimed
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
toGroupChatItemList :: UTCTime -> Int64 -> MaybeGroupChatItemRow -> [CChatItem 'CTGroup]
toGroupChatItemList currentTs userContactId (((Just itemId, Just itemTs, Just msgDir, Just itemContent, Just itemText, Just itemStatus, sharedMsgId) :. (Just itemDeleted, deletedTs, itemEdited, Just createdAt, Just updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. forwardedByMember :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) =
either (const []) (: []) $ toGroupChatItem currentTs userContactId (((itemId, itemTs, msgDir, itemContent, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. forwardedByMember :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_)
toGroupChatItemList _ _ _ = []
getAllChatItems :: DB.Connection -> User -> ChatPagination -> Maybe String -> ExceptT StoreError IO [AChatItem] getAllChatItems :: DB.Connection -> User -> ChatPagination -> Maybe String -> ExceptT StoreError IO [AChatItem]
getAllChatItems db user@User {userId} pagination search_ = do getAllChatItems db user@User {userId} pagination search_ = do
itemRefs <- itemRefs <-

View File

@ -91,6 +91,7 @@ import Simplex.Chat.Migrations.M20231107_indexes
import Simplex.Chat.Migrations.M20231113_group_forward import Simplex.Chat.Migrations.M20231113_group_forward
import Simplex.Chat.Migrations.M20231114_remote_control import Simplex.Chat.Migrations.M20231114_remote_control
import Simplex.Chat.Migrations.M20231126_remote_ctrl_address import Simplex.Chat.Migrations.M20231126_remote_ctrl_address
import Simplex.Chat.Migrations.M20231207_chat_list_pagination
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)] schemaMigrations :: [(String, Query, Maybe Query)]
@ -181,7 +182,8 @@ schemaMigrations =
("20231107_indexes", m20231107_indexes, Just down_m20231107_indexes), ("20231107_indexes", m20231107_indexes, Just down_m20231107_indexes),
("20231113_group_forward", m20231113_group_forward, Just down_m20231113_group_forward), ("20231113_group_forward", m20231113_group_forward, Just down_m20231113_group_forward),
("20231114_remote_control", m20231114_remote_control, Just down_m20231114_remote_control), ("20231114_remote_control", m20231114_remote_control, Just down_m20231114_remote_control),
("20231126_remote_ctrl_address", m20231126_remote_ctrl_address, Just down_m20231126_remote_ctrl_address) ("20231126_remote_ctrl_address", m20231126_remote_ctrl_address, Just down_m20231126_remote_ctrl_address),
("20231207_chat_list_pagination", m20231207_chat_list_pagination, Just down_m20231207_chat_list_pagination)
] ]
-- | The list of migrations in ascending order by date -- | The list of migrations in ascending order by date

View File

@ -116,8 +116,8 @@ createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, image,
profileId <- insertedRowId db profileId <- insertedRowId db
DB.execute DB.execute
db db
"INSERT INTO contacts (contact_profile_id, local_display_name, user_id, is_user, created_at, updated_at) VALUES (?,?,?,?,?,?)" "INSERT INTO contacts (contact_profile_id, local_display_name, user_id, is_user, created_at, updated_at, chat_ts) VALUES (?,?,?,?,?,?,?)"
(profileId, displayName, userId, True, currentTs, currentTs) (profileId, displayName, userId, True, currentTs, currentTs, currentTs)
contactId <- insertedRowId db contactId <- insertedRowId db
DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId) DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId)
pure $ toUser $ (userId, auId, contactId, profileId, activeUser, displayName, fullName, image, Nothing, userPreferences) :. (showNtfs, sendRcptsContacts, sendRcptsSmallGroups, Nothing, Nothing) pure $ toUser $ (userId, auId, contactId, profileId, activeUser, displayName, fullName, image, Nothing, userPreferences) :. (showNtfs, sendRcptsContacts, sendRcptsSmallGroups, Nothing, Nothing)
@ -429,9 +429,9 @@ getUserAddress db User {userId} =
|] |]
(Only userId) (Only userId)
getUserContactLinkById :: DB.Connection -> UserId -> Int64 -> IO (Maybe (UserContactLink, Maybe GroupId, GroupMemberRole)) getUserContactLinkById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO (UserContactLink, Maybe GroupId, GroupMemberRole)
getUserContactLinkById db userId userContactLinkId = getUserContactLinkById db userId userContactLinkId =
maybeFirstRow (\(ucl :. (groupId_, mRole_)) -> (toUserContactLink ucl, groupId_, fromMaybe GRMember mRole_)) $ ExceptT . firstRow (\(ucl :. (groupId_, mRole_)) -> (toUserContactLink ucl, groupId_, fromMaybe GRMember mRole_)) SEUserContactLinkNotFound $
DB.query DB.query
db db
[sql| [sql|

View File

@ -235,10 +235,10 @@ setCommandConnId db User {userId} cmdId connId = do
createContact :: DB.Connection -> User -> Profile -> ExceptT StoreError IO () createContact :: DB.Connection -> User -> Profile -> ExceptT StoreError IO ()
createContact db User {userId} profile = do createContact db User {userId} profile = do
currentTs <- liftIO getCurrentTime currentTs <- liftIO getCurrentTime
void $ createContact_ db userId profile "" Nothing currentTs Nothing void $ createContact_ db userId profile "" Nothing currentTs True
createContact_ :: DB.Connection -> UserId -> Profile -> LocalAlias -> Maybe Int64 -> UTCTime -> Maybe UTCTime -> ExceptT StoreError IO (Text, ContactId, ProfileId) createContact_ :: DB.Connection -> UserId -> Profile -> LocalAlias -> Maybe Int64 -> UTCTime -> Bool -> ExceptT StoreError IO (Text, ContactId, ProfileId)
createContact_ db userId Profile {displayName, fullName, image, contactLink, preferences} localAlias viaGroup currentTs chatTs = createContact_ db userId Profile {displayName, fullName, image, contactLink, preferences} localAlias viaGroup currentTs contactUsed =
ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do
DB.execute DB.execute
db db
@ -247,8 +247,8 @@ createContact_ db userId Profile {displayName, fullName, image, contactLink, pre
profileId <- insertedRowId db profileId <- insertedRowId db
DB.execute DB.execute
db db
"INSERT INTO contacts (contact_profile_id, local_display_name, user_id, via_group, created_at, updated_at, chat_ts) VALUES (?,?,?,?,?,?,?)" "INSERT INTO contacts (contact_profile_id, local_display_name, user_id, via_group, created_at, updated_at, chat_ts, contact_used) VALUES (?,?,?,?,?,?,?,?)"
(profileId, ldn, userId, viaGroup, currentTs, currentTs, chatTs) (profileId, ldn, userId, viaGroup, currentTs, currentTs, currentTs, contactUsed)
contactId <- insertedRowId db contactId <- insertedRowId db
pure $ Right (ldn, contactId, profileId) pure $ Right (ldn, contactId, profileId)

View File

@ -186,9 +186,10 @@ contactConnIncognito :: Contact -> IncognitoEnabled
contactConnIncognito = maybe False connIncognito . contactConn contactConnIncognito = maybe False connIncognito . contactConn
contactDirect :: Contact -> Bool contactDirect :: Contact -> Bool
contactDirect Contact {activeConn} = maybe True direct activeConn contactDirect Contact {activeConn} = maybe True connDirect activeConn
where
direct Connection {connLevel, viaGroupLink} = connLevel == 0 && not viaGroupLink connDirect :: Connection -> Bool
connDirect Connection {connLevel, viaGroupLink} = connLevel == 0 && not viaGroupLink
directOrUsed :: Contact -> Bool directOrUsed :: Contact -> Bool
directOrUsed ct@Contact {contactUsed} = directOrUsed ct@Contact {contactUsed} =

View File

@ -365,6 +365,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRMessageError u prefix err -> ttyUser u [plain prefix <> ": " <> plain err | prefix == "error" || logLevel <= CLLWarning] CRMessageError u prefix err -> ttyUser u [plain prefix <> ": " <> plain err | prefix == "error" || logLevel <= CLLWarning]
CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError logLevel testView e CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError logLevel testView e
CRChatError u e -> ttyUser' u $ viewChatError logLevel testView e CRChatError u e -> ttyUser' u $ viewChatError logLevel testView e
CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError logLevel testView) errs
CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)] CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)]
CRTimedAction _ _ -> [] CRTimedAction _ _ -> []
where where

View File

@ -276,8 +276,8 @@ getTermLine cc =
5000000 `timeout` atomically (readTQueue $ termQ cc) >>= \case 5000000 `timeout` atomically (readTQueue $ termQ cc) >>= \case
Just s -> do Just s -> do
-- remove condition to always echo virtual terminal -- remove condition to always echo virtual terminal
-- when True $ do
when (printOutput cc) $ do when (printOutput cc) $ do
-- when True $ do
name <- userName cc name <- userName cc
putStrLn $ name <> ": " <> s putStrLn $ name <> ": " <> s
pure s pure s

View File

@ -1,5 +1,6 @@
module ChatTests where module ChatTests where
import ChatTests.ChatList
import ChatTests.Direct import ChatTests.Direct
import ChatTests.Files import ChatTests.Files
import ChatTests.Groups import ChatTests.Groups
@ -12,3 +13,4 @@ chatTests = do
describe "group tests" chatGroupTests describe "group tests" chatGroupTests
describe "file tests" chatFileTests describe "file tests" chatFileTests
describe "profile tests" chatProfileTests describe "profile tests" chatProfileTests
describe "chat list pagination tests" chatListTests

227
tests/ChatTests/ChatList.hs Normal file
View File

@ -0,0 +1,227 @@
module ChatTests.ChatList where
import ChatClient
import ChatTests.Utils
import Data.Time.Clock (getCurrentTime)
import Data.Time.Format.ISO8601 (iso8601Show)
import Test.Hspec
chatListTests :: SpecWith FilePath
chatListTests = do
it "get last chats" testPaginationLast
it "get chats before/after timestamp" testPaginationTs
it "filter by search query" testFilterSearch
it "filter favorite" testFilterFavorite
it "filter unread" testFilterUnread
it "filter favorite or unread" testFilterFavoriteOrUnread
it "sort and filter chats of all types" testPaginationAllChatTypes
testPaginationLast :: HasCallStack => FilePath -> IO ()
testPaginationLast =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
connectUsers alice bob
alice <##> bob
connectUsers alice cath
cath <##> alice
alice ##> "/chats 0"
alice ##> "/chats 1"
alice <# "@cath hey"
alice ##> "/chats 2"
alice <# "bob> hey"
alice <# "@cath hey"
testPaginationTs :: HasCallStack => FilePath -> IO ()
testPaginationTs =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
tsStart <- iso8601Show <$> getCurrentTime
connectUsers alice bob
alice <##> bob
tsAliceBob <- iso8601Show <$> getCurrentTime
connectUsers alice cath
cath <##> alice
tsFinish <- iso8601Show <$> getCurrentTime
-- syntax smoke check
getChats_ alice "count=0" []
getChats_ alice ("after=" <> tsFinish <> " count=2") []
getChats_ alice ("before=" <> tsFinish <> " count=0") []
-- limited reads
getChats_ alice "count=1" [("@cath", "hey")]
getChats_ alice ("after=" <> tsStart <> " count=1") [("@bob", "hey")]
getChats_ alice ("before=" <> tsFinish <> " count=1") [("@cath", "hey")]
-- interval bounds
getChats_ alice ("after=" <> tsAliceBob <> " count=10") [("@cath", "hey")]
getChats_ alice ("before=" <> tsAliceBob <> " count=10") [("@bob", "hey")]
getChats_ :: HasCallStack => TestCC -> String -> [(String, String)] -> Expectation
getChats_ cc query expected = do
cc #$> ("/_get chats 1 pcc=on " <> query, chats, expected)
testFilterSearch :: HasCallStack => FilePath -> IO ()
testFilterSearch =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
connectUsers alice bob
alice <##> bob
connectUsers alice cath
cath <##> alice
let query s = "count=1 {\"type\": \"search\", \"search\": \"" <> s <> "\"}"
getChats_ alice (query "abc") []
getChats_ alice (query "alice") []
getChats_ alice (query "bob") [("@bob", "hey")]
getChats_ alice (query "Bob") [("@bob", "hey")]
testFilterFavorite :: HasCallStack => FilePath -> IO ()
testFilterFavorite =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
connectUsers alice bob
alice <##> bob
connectUsers alice cath
cath <##> alice
let query = "{\"type\": \"filters\", \"favorite\": true, \"unread\": false}"
-- no favorite chats
getChats_ alice query []
-- 1 favorite chat
alice ##> "/_settings @2 {\"enableNtfs\":\"all\",\"favorite\":true}"
alice <## "ok"
getChats_ alice query [("@bob", "hey")]
-- 1 favorite chat, unread chat not included
alice ##> "/_unread chat @3 on"
alice <## "ok"
getChats_ alice query [("@bob", "hey")]
testFilterUnread :: HasCallStack => FilePath -> IO ()
testFilterUnread =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
connectUsers alice bob
alice <##> bob
connectUsers alice cath
cath <##> alice
let query = "{\"type\": \"filters\", \"favorite\": false, \"unread\": true}"
-- no unread chats
getChats_ alice query []
-- 1 unread chat
alice ##> "/_unread chat @2 on"
alice <## "ok"
getChats_ alice query [("@bob", "hey")]
-- 1 unread chat, favorite chat not included
alice ##> "/_settings @3 {\"enableNtfs\":\"all\",\"favorite\":true}"
alice <## "ok"
getChats_ alice query [("@bob", "hey")]
testFilterFavoriteOrUnread :: HasCallStack => FilePath -> IO ()
testFilterFavoriteOrUnread =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
connectUsers alice bob
alice <##> bob
connectUsers alice cath
cath <##> alice
let query = "{\"type\": \"filters\", \"favorite\": true, \"unread\": true}"
-- no favorite or unread chats
getChats_ alice query []
-- 1 unread chat
alice ##> "/_unread chat @2 on"
alice <## "ok"
getChats_ alice query [("@bob", "hey")]
-- 1 favorite chat
alice ##> "/_unread chat @2 off"
alice <## "ok"
alice ##> "/_settings @3 {\"enableNtfs\":\"all\",\"favorite\":true}"
alice <## "ok"
getChats_ alice query [("@cath", "hey")]
-- 1 unread chat, 1 favorite chat
alice ##> "/_unread chat @2 on"
alice <## "ok"
getChats_ alice query [("@cath", "hey"), ("@bob", "hey")]
testPaginationAllChatTypes :: HasCallStack => FilePath -> IO ()
testPaginationAllChatTypes =
testChat4 aliceProfile bobProfile cathProfile danProfile $
\alice bob cath dan -> do
ts1 <- iso8601Show <$> getCurrentTime
-- @bob
connectUsers alice bob
alice <##> bob
ts2 <- iso8601Show <$> getCurrentTime
-- <@cath
alice ##> "/ad"
cLink <- getContactLink alice True
cath ##> ("/c " <> cLink)
alice <#? cath
ts3 <- iso8601Show <$> getCurrentTime
-- :3
alice ##> "/c"
_ <- getInvitation alice
ts4 <- iso8601Show <$> getCurrentTime
-- #team
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
ts5 <- iso8601Show <$> getCurrentTime
-- @dan
connectUsers alice dan
alice <##> dan
ts6 <- iso8601Show <$> getCurrentTime
getChats_ alice "count=10" [("@dan", "hey"), ("#team", ""), (":3", ""), ("<@cath", ""), ("@bob", "hey")]
getChats_ alice "count=3" [("@dan", "hey"), ("#team", ""), (":3", "")]
getChats_ alice ("after=" <> ts2 <> " count=2") [(":3", ""), ("<@cath", "")]
getChats_ alice ("before=" <> ts5 <> " count=2") [("#team", ""), (":3", "")]
getChats_ alice ("after=" <> ts3 <> " count=10") [("@dan", "hey"), ("#team", ""), (":3", "")]
getChats_ alice ("before=" <> ts4 <> " count=10") [(":3", ""), ("<@cath", ""), ("@bob", "hey")]
getChats_ alice ("after=" <> ts1 <> " count=10") [("@dan", "hey"), ("#team", ""), (":3", ""), ("<@cath", ""), ("@bob", "hey")]
getChats_ alice ("before=" <> ts6 <> " count=10") [("@dan", "hey"), ("#team", ""), (":3", ""), ("<@cath", ""), ("@bob", "hey")]
getChats_ alice ("after=" <> ts6 <> " count=10") []
getChats_ alice ("before=" <> ts1 <> " count=10") []
let queryFavorite = "{\"type\": \"filters\", \"favorite\": true, \"unread\": false}"
getChats_ alice queryFavorite []
alice ##> "/_settings @2 {\"enableNtfs\":\"all\",\"favorite\":true}"
alice <## "ok"
alice ##> "/_settings #1 {\"enableNtfs\":\"all\",\"favorite\":true}"
alice <## "ok"
getChats_ alice queryFavorite [("#team", ""), ("@bob", "hey")]
getChats_ alice ("before=" <> ts4 <> " count=1 " <> queryFavorite) [("@bob", "hey")]
getChats_ alice ("before=" <> ts5 <> " count=1 " <> queryFavorite) [("#team", "")]
getChats_ alice ("after=" <> ts1 <> " count=1 " <> queryFavorite) [("@bob", "hey")]
getChats_ alice ("after=" <> ts4 <> " count=1 " <> queryFavorite) [("#team", "")]
let queryUnread = "{\"type\": \"filters\", \"favorite\": false, \"unread\": true}"
getChats_ alice queryUnread [("<@cath", "")]
getChats_ alice ("before=" <> ts2 <> " count=10 " <> queryUnread) []
getChats_ alice ("before=" <> ts3 <> " count=10 " <> queryUnread) [("<@cath", "")]
getChats_ alice ("after=" <> ts2 <> " count=10 " <> queryUnread) [("<@cath", "")]
getChats_ alice ("after=" <> ts3 <> " count=10 " <> queryUnread) []