diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index cdee6df558..571ac20684 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -3348,9 +3348,11 @@ public enum MREmojiChar: String, Codable, CaseIterable, Hashable { case thumbsup = "👍" case thumbsdown = "👎" case smile = "😀" + case laugh = "😂" case sad = "😢" case heart = "❤" case launch = "🚀" + case check = "✅" } extension MsgReaction: Decodable { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 0c99d5f42b..26af461615 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -3202,9 +3202,11 @@ enum class MREmojiChar(val value: String) { @SerialName("👍") ThumbsUp("👍"), @SerialName("👎") ThumbsDown("👎"), @SerialName("😀") Smile("😀"), + @SerialName("😂") Laugh("😂"), @SerialName("😢") Sad("😢"), @SerialName("❤") Heart("❤"), - @SerialName("🚀") Launch("🚀"); + @SerialName("🚀") Launch("🚀"), + @SerialName("✅") Check("✅"); } @Serializable diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index afcdb233e8..d2016ff1f5 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -688,7 +688,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe sendComposedMessage cc ct Nothing $ MCText text getContact :: ChatController -> ContactId -> IO (Maybe Contact) -getContact cc ctId = resp <$> sendChatCmd cc (APIGetChat (ChatRef CTDirect ctId) (CPLast 0) Nothing) +getContact cc ctId = resp <$> sendChatCmd cc (APIGetChat (ChatRef CTDirect ctId) Nothing (CPLast 0) Nothing) where resp :: ChatResponse -> Maybe Contact resp = \case diff --git a/docs/protocol/simplex-chat.schema.json b/docs/protocol/simplex-chat.schema.json index 2e94a4f2c2..50d41265f7 100644 --- a/docs/protocol/simplex-chat.schema.json +++ b/docs/protocol/simplex-chat.schema.json @@ -113,6 +113,12 @@ "properties": { "text": {"type": "string", "metadata": {"comment": "can be empty"}} } + }, + "report": { + "properties": { + "text": {"type": "string", "metadata": {"comment": "can be empty, includes report reason for old clients"}}, + "reason": {"enum": ["spam", "illegal", "community", "other"]} + } } }, "metadata": { diff --git a/simplex-chat.cabal b/simplex-chat.cabal index dd9d24eeed..66eecf141d 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -162,6 +162,7 @@ library Simplex.Chat.Migrations.M20241222_operator_conditions Simplex.Chat.Migrations.M20241223_chat_tags Simplex.Chat.Migrations.M20241230_reports + Simplex.Chat.Migrations.M20250105_indexes Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 9a4b35cb6a..f6f7416bb1 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -297,7 +297,7 @@ data ChatCommand | SlowSQLQueries | APIGetChatTags UserId | APIGetChats {userId :: UserId, pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery} - | APIGetChat ChatRef ChatPagination (Maybe String) + | APIGetChat ChatRef (Maybe ContentFilter) ChatPagination (Maybe String) | APIGetChatItems ChatPagination (Maybe String) | APIGetChatItemInfo ChatRef ChatItemId | APISendMessages {chatRef :: ChatRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessages :: NonEmpty ComposedMessage} @@ -635,6 +635,7 @@ data ChatResponse | CRChatItemReaction {user :: User, added :: Bool, reaction :: ACIReaction} | CRReactionMembers {user :: User, memberReactions :: [MemberReaction]} | CRChatItemsDeleted {user :: User, chatItemDeletions :: [ChatItemDeletion], byUser :: Bool, timed :: Bool} + | CRGroupChatItemsDeleted {user :: User, groupInfo :: GroupInfo, chatItemIDs :: [ChatItemId], byUser :: Bool, member_ :: Maybe GroupMember} | CRChatItemDeletedNotFound {user :: User, contact :: Contact, sharedMsgId :: SharedMsgId} | CRBroadcastSent {user :: User, msgContent :: MsgContent, successes :: Int, failures :: Int, timestamp :: UTCTime} | CRMsgIntegrityError {user :: User, msgError :: MsgErrorType} @@ -867,6 +868,12 @@ logResponseToFile = \case CRMessageError {} -> True _ -> False +data ContentFilter = ContentFilter + { mcTag :: MsgContentTag, + deleted :: Maybe Bool + } + deriving (Show) + data ChatPagination = CPLast Int | CPAfter ChatItemId Int diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index f0fbdd26f8..d25444a358 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -481,15 +481,17 @@ processChatCommand' vr = \case (errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user pendingConnections pagination query) unless (null errs) $ 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) contentFilter pagination search -> withUser $ \user -> case cType of -- TODO optimize queries calculating ChatStats, currently they're disabled CTDirect -> do + when (isJust contentFilter) $ throwChatError $ CECommandError "content filter not supported" (directChat, navInfo) <- withFastStore (\db -> getDirectChat db vr user cId pagination search) pure $ CRApiChat user (AChat SCTDirect directChat) navInfo CTGroup -> do - (groupChat, navInfo) <- withFastStore (\db -> getGroupChat db vr user cId pagination search) + (groupChat, navInfo) <- withFastStore (\db -> getGroupChat db vr user cId contentFilter pagination search) pure $ CRApiChat user (AChat SCTGroup groupChat) navInfo CTLocal -> do + when (isJust contentFilter) $ throwChatError $ CECommandError "content filter not supported" (localChat, navInfo) <- withFastStore (\db -> getLocalChat db user cId pagination search) pure $ CRApiChat user (AChat SCTLocal localChat) navInfo CTContactRequest -> pure $ chatCmdError (Just user) "not implemented" @@ -2158,14 +2160,14 @@ processChatCommand' vr = \case pure $ CRChats previews LastMessages (Just chatName) count search -> withUser $ \user -> do chatRef <- getChatRef user chatName - chatResp <- processChatCommand $ APIGetChat chatRef (CPLast count) search + chatResp <- processChatCommand $ APIGetChat chatRef Nothing (CPLast count) search pure $ CRChatItems user (Just chatName) (aChatItems . chat $ chatResp) LastMessages Nothing count search -> withUser $ \user -> do chatItems <- withFastStore $ \db -> getAllChatItems db vr user (CPLast count) search pure $ CRChatItems user Nothing chatItems LastChatItemId (Just chatName) index -> withUser $ \user -> do chatRef <- getChatRef user chatName - chatResp <- processChatCommand (APIGetChat chatRef (CPLast $ index + 1) Nothing) + chatResp <- processChatCommand (APIGetChat chatRef Nothing (CPLast $ index + 1) Nothing) pure $ CRChatItemId user (fmap aChatItemId . listToMaybe . aChatItems . chat $ chatResp) LastChatItemId Nothing index -> withUser $ \user -> do chatItems <- withFastStore $ \db -> getAllChatItems db vr user (CPLast $ index + 1) Nothing @@ -2639,6 +2641,9 @@ processChatCommand' vr = \case delGroupChatItems :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Maybe GroupMember -> CM ChatResponse delGroupChatItems user gInfo items byGroupMember = do deletedTs <- liftIO getCurrentTime + forM_ byGroupMember $ \byMember -> do + ciIds <- concat <$> withStore' (\db -> forM items $ \(CChatItem _ ci) -> markMessageReportsDeleted db user gInfo ci byMember deletedTs) + unless (null ciIds) $ toView $ CRGroupChatItemsDeleted user gInfo ciIds False (Just byMember) if groupFeatureAllowed SGFFullDelete gInfo then deleteGroupCIs user gInfo items True False byGroupMember deletedTs else markGroupCIsDeleted user gInfo items True byGroupMember deletedTs @@ -3573,7 +3578,7 @@ chatCommandP = <*> (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 <*> optional (contentFilterP <* A.space) <*> chatPaginationP <*> optional (" search=" *> stringP)), "/_get items " *> (APIGetChatItems <$> chatPaginationP <*> optional (" search=" *> stringP)), "/_get item info " *> (APIGetChatItemInfo <$> chatRefP <* A.space <*> A.decimal), "/_send " *> (APISendMessages <$> chatRefP <*> liveMessageP <*> sendMessageTTLP <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP)), @@ -3948,6 +3953,7 @@ chatCommandP = ct -> ChatName ct <$> displayName chatNameP' = ChatName <$> (chatTypeP <|> pure CTDirect) <*> displayName chatRefP = ChatRef <$> chatTypeP <*> A.decimal + contentFilterP = ContentFilter <$> ("content=" *> strP) <*> optional (" deleted=" *> onOffP) msgCountP = A.space *> A.decimal <|> pure 10 ciTTLDecimal = ("none" $> Nothing) <|> (Just <$> A.decimal) ciTTL = diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 59a765b674..b47855a18f 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -1840,7 +1840,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = moderate :: GroupMember -> CChatItem 'CTGroup -> CM () moderate mem cci = case sndMemberId_ of Just sndMemberId - | sameMemberId sndMemberId mem -> checkRole mem $ delete cci (Just m) >>= toView + | sameMemberId sndMemberId mem -> checkRole mem $ do + delete cci (Just m) >>= toView + archiveMessageReports cci m | otherwise -> messageError "x.msg.del: message of another member with incorrect memberId" _ -> messageError "x.msg.del: message of another member without memberId" checkRole GroupMember {memberRole} a @@ -1851,6 +1853,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = delete cci byGroupMember | groupFeatureAllowed SGFFullDelete gInfo = deleteGroupCIs user gInfo [cci] False False byGroupMember brokerTs | otherwise = markGroupCIsDeleted user gInfo [cci] False byGroupMember brokerTs + archiveMessageReports :: CChatItem 'CTGroup -> GroupMember -> CM () + archiveMessageReports (CChatItem _ ci) byMember = do + ciIds <- withStore' $ \db -> markMessageReportsDeleted db user gInfo ci byMember brokerTs + unless (null ciIds) $ toView $ CRGroupChatItemsDeleted user gInfo ciIds False (Just byMember) -- TODO remove once XFile is discontinued processFileInvitation' :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> CM () diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index a477deeb2c..25bea24e74 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -91,14 +91,6 @@ chatInfoChatTs = \case GroupChat GroupInfo {chatTs} -> chatTs _ -> Nothing -chatInfoUpdatedAt :: ChatInfo c -> UTCTime -chatInfoUpdatedAt = \case - DirectChat Contact {updatedAt} -> updatedAt - GroupChat GroupInfo {updatedAt} -> updatedAt - LocalChat NoteFolder {updatedAt} -> updatedAt - ContactRequest UserContactRequest {updatedAt} -> updatedAt - ContactConnection PendingContactConnection {updatedAt} -> updatedAt - chatInfoToRef :: ChatInfo c -> ChatRef chatInfoToRef = \case DirectChat Contact {contactId} -> ChatRef CTDirect contactId @@ -318,12 +310,17 @@ data AChat = forall c. ChatTypeI c => AChat (SChatType c) (Chat c) deriving instance Show AChat data ChatStats = ChatStats - { unreadCount :: Int, + { unreadCount :: Int, -- returned both in /_get chat initial API and in /_get chats API + reportsCount :: Int, -- returned both in /_get chat initial API and in /_get chats API + archivedReportsCount :: Int, -- only returned in /_get chat initial API minUnreadItemId :: ChatItemId, unreadChat :: Bool } deriving (Show) +emptyChatStats :: ChatStats +emptyChatStats = ChatStats 0 0 0 0 False + data NavigationInfo = NavigationInfo { afterUnread :: Int, afterTotal :: Int diff --git a/src/Simplex/Chat/Migrations/M20241230_reports.hs b/src/Simplex/Chat/Migrations/M20241230_reports.hs index d5b3c183cf..7d605824f5 100644 --- a/src/Simplex/Chat/Migrations/M20241230_reports.hs +++ b/src/Simplex/Chat/Migrations/M20241230_reports.hs @@ -1,18 +1,18 @@ {-# LANGUAGE QuasiQuotes #-} - module Simplex.Chat.Migrations.M20241230_reports where +module Simplex.Chat.Migrations.M20241230_reports where - import Database.SQLite.Simple (Query) - import Database.SQLite.Simple.QQ (sql) +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) - m20241230_reports :: Query - m20241230_reports = - [sql| +m20241230_reports :: Query +m20241230_reports = + [sql| ALTER TABLE chat_items ADD COLUMN msg_content_tag TEXT; |] - down_m20241230_reports :: Query - down_m20241230_reports = - [sql| +down_m20241230_reports :: Query +down_m20241230_reports = + [sql| ALTER TABLE chat_items DROP COLUMN msg_content_tag; |] diff --git a/src/Simplex/Chat/Migrations/M20250105_indexes.hs b/src/Simplex/Chat/Migrations/M20250105_indexes.hs new file mode 100644 index 0000000000..dd01f21389 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20250105_indexes.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20250105_indexes where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20250105_indexes :: Query +m20250105_indexes = + [sql| +CREATE INDEX idx_chat_items_groups_msg_content_tag_item_ts ON chat_items(user_id, group_id, msg_content_tag, item_ts); +CREATE INDEX idx_chat_items_groups_msg_content_tag_item_deleted_item_ts ON chat_items(user_id, group_id, msg_content_tag, item_deleted, item_ts); +|] + +down_m20250105_indexes :: Query +down_m20250105_indexes = + [sql| +DROP INDEX idx_chat_items_groups_msg_content_tag_item_ts; +DROP INDEX idx_chat_items_groups_msg_content_tag_item_deleted_item_ts; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index ee968383e0..870feba6b5 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -962,3 +962,16 @@ CREATE UNIQUE INDEX idx_chat_tags_chats_chat_tag_id_group_id ON chat_tags_chats( group_id, chat_tag_id ); +CREATE INDEX idx_chat_items_groups_msg_content_tag_item_ts ON chat_items( + user_id, + group_id, + msg_content_tag, + item_ts +); +CREATE INDEX idx_chat_items_groups_msg_content_tag_item_deleted_item_ts ON chat_items( + user_id, + group_id, + msg_content_tag, + item_deleted, + item_ts +); diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 89664d66f7..cda8cdf04c 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -70,7 +70,7 @@ import Simplex.Messaging.Version hiding (version) -- 9 - batch sending in direct connections (2024-07-24) -- 10 - business chats (2024-11-29) -- 11 - fix profile update in business chats (2024-12-05) --- 12 - fix profile update in business chats (2025-01-03) +-- 12 - support sending and receiving content reports (2025-01-03) -- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig. -- This indirection is needed for backward/forward compatibility testing. @@ -443,7 +443,7 @@ instance FromJSON MREmojiChar where mrEmojiChar :: Char -> Either String MREmojiChar mrEmojiChar c - | c `elem` ("👍👎😀😢❤️🚀" :: String) = Right $ MREmojiChar c + | c `elem` ("👍👎😀😂😢❤️🚀✅" :: String) = Right $ MREmojiChar c | otherwise = Left "bad emoji" data FileChunk = FileChunk {chunkNo :: Integer, chunkBytes :: ByteString} | FileChunkCancel @@ -485,7 +485,7 @@ cmToQuotedMsg = \case _ -> Nothing data MsgContentTag = MCText_ | MCLink_ | MCImage_ | MCVideo_ | MCVoice_ | MCFile_ | MCReport_ | MCUnknown_ Text - deriving (Eq) + deriving (Eq, Show) instance StrEncoding MsgContentTag where strEncode = \case @@ -522,6 +522,7 @@ instance ToField MsgContentTag where toField = toField . strEncode data MsgContainer = MCSimple ExtMsgContent | MCQuote QuotedMsg ExtMsgContent + | MCComment MsgRef ExtMsgContent | MCForward ExtMsgContent deriving (Eq, Show) @@ -529,13 +530,9 @@ mcExtMsgContent :: MsgContainer -> ExtMsgContent mcExtMsgContent = \case MCSimple c -> c MCQuote _ c -> c + MCComment _ c -> c MCForward c -> c -isQuote :: MsgContainer -> Bool -isQuote = \case - MCQuote {} -> True - _ -> False - data MsgContent = MCText Text | MCLink {text :: Text, preview :: LinkPreview} @@ -564,9 +561,6 @@ msgContentText = \case msg = "report " <> safeDecodeUtf8 (strEncode reason) MCUnknown {text} -> text -toMCText :: MsgContent -> MsgContent -toMCText = MCText . msgContentText - durationText :: Int -> Text durationText duration = let (mins, secs) = duration `divMod` 60 in T.pack $ "(" <> with0 mins <> ":" <> with0 secs <> ")" @@ -657,7 +651,10 @@ markCompressedBatch = B.cons 'X' parseMsgContainer :: J.Object -> JT.Parser MsgContainer parseMsgContainer v = MCQuote <$> v .: "quote" <*> mc + <|> MCComment <$> v .: "parent" <*> mc <|> (v .: "forward" >>= \f -> (if f then MCForward else MCSimple) <$> mc) + -- The support for arbitrary object in "forward" property is added to allow + -- forward compatibility with forwards that include public group links. <|> (MCForward <$> ((v .: "forward" :: JT.Parser J.Object) *> mc)) <|> MCSimple <$> mc where @@ -708,6 +705,7 @@ unknownMsgType = "unknown message type" msgContainerJSON :: MsgContainer -> J.Object msgContainerJSON = \case MCQuote qm mc -> o $ ("quote" .= qm) : msgContent mc + MCComment ref mc -> o $ ("parent" .= ref) : msgContent mc MCForward mc -> o $ ("forward" .= True) : msgContent mc MCSimple mc -> o $ msgContent mc where diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index f729c8d073..c8d31c713a 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -58,6 +58,7 @@ module Simplex.Chat.Store.Messages markGroupChatItemDeleted, markGroupChatItemBlocked, markGroupCIBlockedByAdmin, + markMessageReportsDeleted, deleteLocalChatItem, updateDirectChatItemsRead, getDirectUnreadTimedItems, @@ -139,9 +140,9 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Time (addUTCTime) import Data.Time.Clock (UTCTime (..), getCurrentTime) -import Database.SQLite.Simple (NamedParam (..), Only (..), Query, (:.) (..)) +import Database.SQLite.Simple (FromRow, NamedParam (..), Only (..), Query, ToRow, (:.) (..)) import Database.SQLite.Simple.QQ (sql) -import Simplex.Chat.Controller (ChatListQuery (..), ChatPagination (..), PaginationByTime (..)) +import Simplex.Chat.Controller (ChatListQuery (..), ChatPagination (..), ContentFilter (..), PaginationByTime (..)) import Simplex.Chat.Markdown import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent @@ -153,7 +154,8 @@ import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, MsgMeta (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, firstRow', maybeFirstRow) -import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) +import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Util (eitherToMaybe) @@ -548,16 +550,10 @@ data ChatPreviewData (c :: ChatType) where data AChatPreviewData = forall c. ChatTypeI c => ACPD (SChatType c) (ChatPreviewData c) -paginationByTimeFilter :: PaginationByTime -> (Query, [NamedParam]) -paginationByTimeFilter = \case - PTLast count -> ("\nORDER BY ts DESC LIMIT :count", [":count" := count]) - PTAfter ts count -> ("\nAND ts > :ts ORDER BY ts ASC LIMIT :count", [":ts" := ts, ":count" := count]) - PTBefore ts count -> ("\nAND ts < :ts ORDER BY ts DESC LIMIT :count", [":ts" := ts, ":count" := count]) - -type ChatStatsRow = (Int, ChatItemId, Bool) +type ChatStatsRow = (Int, Int, ChatItemId, BoolInt) toChatStats :: ChatStatsRow -> ChatStats -toChatStats (unreadCount, minUnreadItemId, unreadChat) = ChatStats {unreadCount, minUnreadItemId, unreadChat} +toChatStats (unreadCount, reportsCount, minUnreadItemId, BI unreadChat) = ChatStats {unreadCount, reportsCount, archivedReportsCount = 0, minUnreadItemId, unreadChat} findDirectChatPreviews_ :: DB.Connection -> User -> PaginationByTime -> ChatListQuery -> IO [AChatPreviewData] findDirectChatPreviews_ db User {userId} pagination clq = @@ -568,84 +564,76 @@ findDirectChatPreviews_ db User {userId} pagination clq = ACPD SCTDirect $ DirectChatPD ts contactId lastItemId_ (toChatStats statsRow) baseQuery = [sql| - SELECT ct.contact_id, ct.chat_ts as ts, LastItems.chat_item_id, COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat + SELECT ct.contact_id, ct.chat_ts as ts, LastItems.chat_item_id, COALESCE(ChatStats.UnreadCount, 0), 0, COALESCE(ChatStats.MinUnread, 0), ct.unread_chat FROM contacts ct LEFT JOIN ( SELECT contact_id, chat_item_id, MAX(created_at) FROM chat_items - WHERE user_id = :user_id AND contact_id IS NOT NULL + WHERE user_id = ? AND contact_id IS NOT NULL GROUP BY contact_id ) LastItems ON LastItems.contact_id = ct.contact_id LEFT JOIN ( SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread FROM chat_items - WHERE user_id = :user_id AND contact_id IS NOT NULL AND item_status = :rcv_new + WHERE user_id = ? AND contact_id IS NOT NULL AND item_status = ? GROUP BY contact_id ) ChatStats ON ChatStats.contact_id = ct.contact_id |] - (pagQuery, pagParams) = paginationByTimeFilter pagination + baseParams = (userId, userId, CISRcvNew) getPreviews = case clq of - CLQFilters {favorite = False, unread = False} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used + CLQFilters {favorite = False, unread = False} -> do + let q = baseQuery <> " WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used" + p = baseParams :. Only userId + queryWithPagination db q p pagination + CLQFilters {favorite = True, unread = False} -> do + let q = + baseQuery + <> [sql| + WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used + AND ct.favorite = 1 |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) - CLQFilters {favorite = True, unread = False} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used - AND ct.favorite = 1 + p = baseParams :. Only userId + queryWithPagination db q p pagination + CLQFilters {favorite = False, unread = True} -> do + let q = + baseQuery + <> [sql| + WHERE ct.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 = False, unread = True} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - 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) + p = baseParams :. Only userId + queryWithPagination db q p pagination + CLQFilters {favorite = True, unread = True} -> do + let q = + baseQuery + <> [sql| + WHERE ct.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) - CLQFilters {favorite = True, unread = True} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - 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) + p = baseParams :. Only userId + queryWithPagination db q p pagination + CLQSearch {search} -> do + let q = + baseQuery + <> [sql| + JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id + WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used + AND ( + ct.local_display_name LIKE '%' || ? || '%' + OR cp.display_name LIKE '%' || ? || '%' + OR cp.full_name LIKE '%' || ? || '%' + OR cp.local_alias LIKE '%' || ? || '%' + ) |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) - CLQSearch {search} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - 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, ":rcv_new" := CISRcvNew, ":search" := search] <> pagParams) + p = baseParams :. (userId, search, search, search, search) + queryWithPagination db q p pagination + +queryWithPagination :: ToRow p => DB.Connection -> Query -> p -> PaginationByTime -> IO [(ContactId, UTCTime, Maybe ChatItemId) :. ChatStatsRow] +queryWithPagination db query params = \case + PTLast count -> DB.query db (query <> " ORDER BY ts DESC LIMIT ?") (params :. Only count) + PTAfter ts count -> DB.query db (query <> " AND ts > ? ORDER BY ts ASC LIMIT ?") (params :. (ts, count)) + PTBefore ts count -> DB.query db (query <> " AND ts < ? ORDER BY ts DESC LIMIT ?") (params :. (ts, count)) getDirectChatPreview_ :: DB.Connection -> VersionRangeChat -> User -> ChatPreviewData 'CTDirect -> ExceptT StoreError IO AChat getDirectChatPreview_ db vr user (DirectChatPD _ contactId lastItemId_ stats) = do @@ -664,84 +652,77 @@ findGroupChatPreviews_ db User {userId} pagination clq = ACPD SCTGroup $ GroupChatPD ts groupId lastItemId_ (toChatStats statsRow) baseQuery = [sql| - SELECT g.group_id, g.chat_ts as ts, LastItems.chat_item_id, COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), g.unread_chat + SELECT g.group_id, g.chat_ts as ts, LastItems.chat_item_id, COALESCE(ChatStats.UnreadCount, 0), COALESCE(ReportCount.Count, 0), COALESCE(ChatStats.MinUnread, 0), g.unread_chat FROM groups g LEFT JOIN ( SELECT group_id, chat_item_id, MAX(item_ts) FROM chat_items - WHERE user_id = :user_id AND group_id IS NOT NULL + WHERE user_id = ? AND group_id IS NOT NULL GROUP BY group_id ) LastItems ON LastItems.group_id = g.group_id LEFT JOIN ( SELECT group_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread FROM chat_items - WHERE user_id = :user_id AND group_id IS NOT NULL AND item_status = :rcv_new + WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? GROUP BY group_id ) ChatStats ON ChatStats.group_id = g.group_id + LEFT JOIN ( + SELECT group_id, COUNT(1) AS Count + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL + AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 + GROUP BY group_id + ) ReportCount ON ReportCount.group_id = g.group_id |] - (pagQuery, pagParams) = paginationByTimeFilter pagination + baseParams = (userId, userId, CISRcvNew, userId, MCReport_, BI False) getPreviews = case clq of - CLQFilters {favorite = False, unread = False} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE g.user_id = :user_id + CLQFilters {favorite = False, unread = False} -> do + let q = baseQuery <> " WHERE g.user_id = ?" + p = baseParams :. Only userId + queryWithPagination db q p pagination + CLQFilters {favorite = True, unread = False} -> do + let q = + baseQuery + <> [sql| + WHERE g.user_id = ? + AND g.favorite = 1 |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) - CLQFilters {favorite = True, unread = False} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE g.user_id = :user_id - AND g.favorite = 1 + p = baseParams :. Only userId + queryWithPagination db q p pagination + CLQFilters {favorite = False, unread = True} -> do + let q = + baseQuery + <> [sql| + WHERE g.user_id = ? + AND (g.unread_chat = 1 OR ChatStats.UnreadCount > 0) |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) - CLQFilters {favorite = False, unread = True} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE g.user_id = :user_id - AND (g.unread_chat = 1 OR ChatStats.UnreadCount > 0) + p = baseParams :. Only userId + queryWithPagination db q p pagination + CLQFilters {favorite = True, unread = True} -> do + let q = + baseQuery + <> [sql| + WHERE g.user_id = ? + AND (g.favorite = 1 + OR g.unread_chat = 1 OR ChatStats.UnreadCount > 0) |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) - CLQFilters {favorite = True, unread = True} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE g.user_id = :user_id - AND (g.favorite = 1 - OR g.unread_chat = 1 OR ChatStats.UnreadCount > 0) + p = baseParams :. Only userId + queryWithPagination db q p pagination + CLQSearch {search} -> do + let q = + baseQuery + <> [sql| + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + WHERE g.user_id = ? + AND ( + g.local_display_name LIKE '%' || ? || '%' + OR gp.display_name LIKE '%' || ? || '%' + OR gp.full_name LIKE '%' || ? || '%' + OR gp.description LIKE '%' || ? || '%' + ) |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) - CLQSearch {search} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - 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, ":rcv_new" := CISRcvNew, ":search" := search] <> pagParams) + p = baseParams :. (userId, search, search, search, search) + queryWithPagination db q p pagination getGroupChatPreview_ :: DB.Connection -> VersionRangeChat -> User -> ChatPreviewData 'CTGroup -> ExceptT StoreError IO AChat getGroupChatPreview_ db vr user (GroupChatPD _ groupId lastItemId_ stats) = do @@ -760,67 +741,55 @@ findLocalChatPreviews_ db User {userId} pagination clq = ACPD SCTLocal $ LocalChatPD ts noteFolderId lastItemId_ (toChatStats statsRow) baseQuery = [sql| - SELECT nf.note_folder_id, nf.chat_ts as ts, LastItems.chat_item_id, COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), nf.unread_chat + SELECT nf.note_folder_id, nf.chat_ts as ts, LastItems.chat_item_id, COALESCE(ChatStats.UnreadCount, 0), 0, COALESCE(ChatStats.MinUnread, 0), nf.unread_chat FROM note_folders nf LEFT JOIN ( SELECT note_folder_id, chat_item_id, MAX(created_at) FROM chat_items - WHERE user_id = :user_id AND note_folder_id IS NOT NULL + WHERE user_id = ? AND note_folder_id IS NOT NULL GROUP BY note_folder_id ) LastItems ON LastItems.note_folder_id = nf.note_folder_id LEFT JOIN ( SELECT note_folder_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread FROM chat_items - WHERE user_id = :user_id AND note_folder_id IS NOT NULL AND item_status = :rcv_new + WHERE user_id = ? AND note_folder_id IS NOT NULL AND item_status = ? GROUP BY note_folder_id ) ChatStats ON ChatStats.note_folder_id = nf.note_folder_id |] - (pagQuery, pagParams) = paginationByTimeFilter pagination + baseParams = (userId, userId, CISRcvNew) getPreviews = case clq of - CLQFilters {favorite = False, unread = False} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE nf.user_id = :user_id + CLQFilters {favorite = False, unread = False} -> do + let q = baseQuery <> " WHERE nf.user_id = ?" + p = baseParams :. Only userId + queryWithPagination db q p pagination + CLQFilters {favorite = True, unread = False} -> do + let q = + baseQuery + <> [sql| + WHERE nf.user_id = ? + AND nf.favorite = 1 |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) - CLQFilters {favorite = True, unread = False} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE nf.user_id = :user_id - AND nf.favorite = 1 + p = baseParams :. Only userId + queryWithPagination db q p pagination + CLQFilters {favorite = False, unread = True} -> do + let q = + baseQuery + <> [sql| + WHERE nf.user_id = ? + AND (nf.unread_chat = 1 OR ChatStats.UnreadCount > 0) |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) - CLQFilters {favorite = False, unread = True} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE nf.user_id = :user_id - AND (nf.unread_chat = 1 OR ChatStats.UnreadCount > 0) + p = baseParams :. Only userId + queryWithPagination db q p pagination + CLQFilters {favorite = True, unread = True} -> do + let q = + baseQuery + <> [sql| + WHERE nf.user_id = ? + AND (nf.favorite = 1 + OR nf.unread_chat = 1 OR ChatStats.UnreadCount > 0) |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) - CLQFilters {favorite = True, unread = True} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE nf.user_id = :user_id - AND (nf.favorite = 1 - OR nf.unread_chat = 1 OR ChatStats.UnreadCount > 0) - |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) + p = baseParams :. Only userId + queryWithPagination db q p pagination CLQSearch {} -> pure [] getLocalChatPreview_ :: DB.Connection -> User -> ChatPreviewData 'CTLocal -> ExceptT StoreError IO AChat @@ -872,82 +841,76 @@ toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentTex 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 = False, unread = False} -> map toPreview <$> getPreviews "" CLQFilters {favorite = True, unread = False} -> pure [] - CLQFilters {favorite = False, unread = True} -> query "" - CLQFilters {favorite = True, unread = True} -> query "" - CLQSearch {search} -> query search + CLQFilters {favorite = False, unread = True} -> map toPreview <$> getPreviews "" + CLQFilters {favorite = True, unread = True} -> map toPreview <$> getPreviews "" + CLQSearch {search} -> map toPreview <$> getPreviews 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.contact_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, cr.pq_support, 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 + query = + [sql| + SELECT + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_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, cr.pq_support, 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 = ? + AND uc.user_id = ? + AND uc.local_display_name = '' + AND uc.group_id IS NULL + AND ( + cr.local_display_name LIKE '%' || ? || '%' + OR p.display_name LIKE '%' || ? || '%' + OR p.full_name LIKE '%' || ? || '%' ) - ([":user_id" := userId, ":search" := search] <> pagParams) + |] + params search = (userId, userId, search, search, search) + getPreviews search = case pagination of + PTLast count -> DB.query db (query <> " ORDER BY ts DESC LIMIT ?") (params search :. Only count) + PTAfter ts count -> DB.query db (query <> " AND ts > ? ORDER BY ts ASC LIMIT ?") (params search :. (ts, count)) + PTBefore ts count -> DB.query db (query <> " AND ts < ? ORDER BY ts DESC LIMIT ?") (params search :. (ts, count)) toPreview :: ContactRequestRow -> AChatPreviewData toPreview cReqRow = let cReq@UserContactRequest {updatedAt} = toContactRequest cReqRow - stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - aChat = AChat SCTContactRequest $ Chat (ContactRequest cReq) [] stats + aChat = AChat SCTContactRequest $ Chat (ContactRequest cReq) [] emptyChatStats in ACPD SCTContactRequest $ ContactRequestPD updatedAt aChat getContactConnectionChatPreviews_ :: DB.Connection -> User -> PaginationByTime -> ChatListQuery -> IO [AChatPreviewData] getContactConnectionChatPreviews_ db User {userId} pagination clq = case clq of - CLQFilters {favorite = False, unread = False} -> query "" + CLQFilters {favorite = False, unread = False} -> map toPreview <$> getPreviews "" CLQFilters {favorite = True, unread = False} -> pure [] CLQFilters {favorite = False, unread = True} -> pure [] CLQFilters {favorite = True, unread = True} -> pure [] - CLQSearch {search} -> query search + CLQSearch {search} -> map toPreview <$> getPreviews search where - (pagQuery, pagParams) = paginationByTimeFilter pagination - query search = - 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 conn_status != :conn_status - 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, ":conn_status" := ConnPrepared, ":search" := search] <> pagParams) + query = + [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 = ? + AND conn_type = ? + AND conn_status != ? + 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 '%' || ? || '%' + |] + params search = (userId, ConnContact, ConnPrepared, search) + getPreviews search = case pagination of + PTLast count -> DB.query db (query <> " ORDER BY ts DESC LIMIT ?") (params search :. Only count) + PTAfter ts count -> DB.query db (query <> " AND ts > ? ORDER BY ts ASC LIMIT ?") (params search :. (ts, count)) + PTBefore ts count -> DB.query db (query <> " AND ts < ? ORDER BY ts DESC LIMIT ?") (params search :. (ts, count)) 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} - aChat = AChat SCTContactConnection $ Chat (ContactConnection conn) [] stats + aChat = AChat SCTContactConnection $ Chat (ContactConnection conn) [] emptyChatStats in ACPD SCTContactConnection $ ContactConnectionPD updatedAt aChat getDirectChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) @@ -966,11 +929,10 @@ getDirectChat db vr user contactId pagination search_ = do -- the last items in reverse order (the last item in the conversation is the first in the returned list) getDirectChatLast_ :: DB.Connection -> User -> Contact -> Int -> String -> IO (Chat 'CTDirect) getDirectChatLast_ db user ct count search = do - let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} ciIds <- getDirectChatItemIdsLast_ db user ct count search ts <- getCurrentTime cis <- mapM (safeGetDirectItem db user ct ts) ciIds - pure $ Chat (DirectChat ct) (reverse cis) stats + pure $ Chat (DirectChat ct) (reverse cis) emptyChatStats getDirectChatItemIdsLast_ :: DB.Connection -> User -> Contact -> Int -> String -> IO [ChatItemId] getDirectChatItemIdsLast_ db User {userId} Contact {contactId} count search = @@ -1030,12 +992,11 @@ getDirectChatItemLast db user@User {userId} contactId = do getDirectChatAfter_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect) getDirectChatAfter_ db user ct@Contact {contactId} afterId count search = do - let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} afterCI <- getDirectChatItem db user contactId afterId ciIds <- liftIO $ getDirectCIsAfter_ db user ct afterCI count search ts <- liftIO getCurrentTime cis <- liftIO $ mapM (safeGetDirectItem db user ct ts) ciIds - pure $ Chat (DirectChat ct) cis stats + pure $ Chat (DirectChat ct) cis emptyChatStats getDirectCIsAfter_ :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> Int -> String -> IO [ChatItemId] getDirectCIsAfter_ db User {userId} Contact {contactId} afterCI count search = @@ -1054,12 +1015,11 @@ getDirectCIsAfter_ db User {userId} Contact {contactId} afterCI count search = getDirectChatBefore_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect) getDirectChatBefore_ db user ct@Contact {contactId} beforeId count search = do - let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} beforeCI <- getDirectChatItem db user contactId beforeId ciIds <- liftIO $ getDirectCIsBefore_ db user ct beforeCI count search ts <- liftIO getCurrentTime cis <- liftIO $ mapM (safeGetDirectItem db user ct ts) ciIds - pure $ Chat (DirectChat ct) (reverse cis) stats + pure $ Chat (DirectChat ct) (reverse cis) emptyChatStats getDirectCIsBefore_ :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> Int -> String -> IO [ChatItemId] getDirectCIsBefore_ db User {userId} Contact {contactId} beforeCI count search = @@ -1102,7 +1062,7 @@ getDirectChatInitial_ db user ct count = do liftIO (getContactMinUnreadId_ db user ct) >>= \case Just minUnreadItemId -> do unreadCount <- liftIO $ getContactUnreadCount_ db user ct - let stats = ChatStats {unreadCount, minUnreadItemId, unreadChat = False} + let stats = emptyChatStats {unreadCount, minUnreadItemId} getDirectChatAround' db user ct minUnreadItemId count "" stats Nothing -> liftIO $ (,Just $ NavigationInfo 0 0) <$> getDirectChatLast_ db user ct count "" @@ -1110,7 +1070,7 @@ getContactStats_ :: DB.Connection -> User -> Contact -> IO ChatStats getContactStats_ db user ct = do minUnreadItemId <- fromMaybe 0 <$> getContactMinUnreadId_ db user ct unreadCount <- getContactUnreadCount_ db user ct - pure ChatStats {unreadCount, minUnreadItemId, unreadChat = False} + pure emptyChatStats {unreadCount, minUnreadItemId} getContactMinUnreadId_ :: DB.Connection -> User -> Contact -> IO (Maybe ChatItemId) getContactMinUnreadId_ db User {userId} Contact {contactId} = @@ -1147,87 +1107,103 @@ getContactNavInfo_ db User {userId} Contact {contactId} afterCI = do getAfterUnreadCount :: IO Int getAfterUnreadCount = fromOnly . head - <$> DB.queryNamed + <$> DB.query db [sql| SELECT COUNT(1) FROM ( SELECT 1 FROM chat_items - WHERE user_id = :user_id AND contact_id = :contact_id AND item_status = :rcv_new - AND created_at > :created_at + WHERE user_id = ? AND contact_id = ? AND item_status = ? + AND created_at > ? UNION ALL SELECT 1 FROM chat_items - WHERE user_id = :user_id AND contact_id = :contact_id AND item_status = :rcv_new - AND created_at = :created_at AND chat_item_id > :item_id + WHERE user_id = ? AND contact_id = ? AND item_status = ? + AND created_at = ? AND chat_item_id > ? ) |] - [ ":user_id" := userId, - ":contact_id" := contactId, - ":rcv_new" := CISRcvNew, - ":created_at" := ciCreatedAt afterCI, - ":item_id" := cChatItemId afterCI - ] + ( (userId, contactId, CISRcvNew, ciCreatedAt afterCI) + :. (userId, contactId, CISRcvNew, ciCreatedAt afterCI, cChatItemId afterCI) + ) getAfterTotalCount :: IO Int getAfterTotalCount = fromOnly . head - <$> DB.queryNamed + <$> DB.query db [sql| SELECT COUNT(1) FROM ( SELECT 1 FROM chat_items - WHERE user_id = :user_id AND contact_id = :contact_id - AND created_at > :created_at + WHERE user_id = ? AND contact_id = ? + AND created_at > ? UNION ALL SELECT 1 FROM chat_items - WHERE user_id = :user_id AND contact_id = :contact_id - AND created_at = :created_at AND chat_item_id > :item_id + WHERE user_id = ? AND contact_id = ? + AND created_at = ? AND chat_item_id > ? ) |] - [ ":user_id" := userId, - ":contact_id" := contactId, - ":created_at" := ciCreatedAt afterCI, - ":item_id" := cChatItemId afterCI - ] + ( (userId, contactId, ciCreatedAt afterCI) + :. (userId, contactId, ciCreatedAt afterCI, cChatItemId afterCI) + ) -getGroupChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) -getGroupChat db vr user groupId pagination search_ = do +getGroupChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> Maybe ContentFilter -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) +getGroupChat db vr user groupId contentFilter pagination search_ = do let search = fromMaybe "" search_ g <- getGroupInfo db vr user groupId case pagination of - CPLast count -> liftIO $ (,Nothing) <$> getGroupChatLast_ db user g count search - CPAfter afterId count -> (,Nothing) <$> getGroupChatAfter_ db user g afterId count search - CPBefore beforeId count -> (,Nothing) <$> getGroupChatBefore_ db user g beforeId count search - CPAround aroundId count -> getGroupChatAround_ db user g aroundId count search + CPLast count -> liftIO $ (,Nothing) <$> getGroupChatLast_ db user g contentFilter count search + CPAfter afterId count -> (,Nothing) <$> getGroupChatAfter_ db user g contentFilter afterId count search + CPBefore beforeId count -> (,Nothing) <$> getGroupChatBefore_ db user g contentFilter beforeId count search + CPAround aroundId count -> getGroupChatAround_ db user g contentFilter aroundId count search CPInitial count -> do unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search" - getGroupChatInitial_ db user g count + getGroupChatInitial_ db user g contentFilter count -getGroupChatLast_ :: DB.Connection -> User -> GroupInfo -> Int -> String -> IO (Chat 'CTGroup) -getGroupChatLast_ db user g count search = do - let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - ciIds <- getGroupChatItemIdsLast_ db user g count search +getGroupChatLast_ :: DB.Connection -> User -> GroupInfo -> Maybe ContentFilter -> Int -> String -> IO (Chat 'CTGroup) +getGroupChatLast_ db user g contentFilter count search = do + ciIds <- getGroupChatItemIDs db user g contentFilter GRLast count search ts <- getCurrentTime cis <- mapM (safeGetGroupItem db user g ts) ciIds - pure $ Chat (GroupChat g) (reverse cis) stats + pure $ Chat (GroupChat g) (reverse cis) emptyChatStats -getGroupChatItemIdsLast_ :: DB.Connection -> User -> GroupInfo -> Int -> String -> IO [ChatItemId] -getGroupChatItemIdsLast_ db User {userId} GroupInfo {groupId} count search = - map fromOnly - <$> DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%' - ORDER BY item_ts DESC, chat_item_id DESC - LIMIT ? - |] - (userId, groupId, search, count) +data GroupItemIDsRange = GRLast | GRAfter UTCTime ChatItemId | GRBefore UTCTime ChatItemId + +getGroupChatItemIDs :: DB.Connection -> User -> GroupInfo -> Maybe ContentFilter -> GroupItemIDsRange -> Int -> String -> IO [ChatItemId] +getGroupChatItemIDs db User {userId} GroupInfo {groupId} contentFilter range count search = case contentFilter of + Just ContentFilter {mcTag, deleted} -> case deleted of + Just deleted' -> idsQuery (baseCond <> " AND msg_content_tag = ? AND item_deleted = ? ") (userId, groupId, mcTag, BI deleted') + Nothing -> idsQuery (baseCond <> " AND msg_content_tag = ? ") (userId, groupId, mcTag) + Nothing -> idsQuery baseCond (userId, groupId) + where + baseQuery = " SELECT chat_item_id FROM chat_items WHERE " + baseCond = " user_id = ? AND group_id = ? " + idsQuery :: ToRow p => Query -> p -> IO [ChatItemId] + idsQuery c p = case range of + GRLast -> rangeQuery c p " ORDER BY item_ts DESC, chat_item_id DESC " + GRAfter ts itemId -> + rangeQuery + (" item_ts > ? " `orCond` " item_ts = ? AND chat_item_id > ? ") + (orParams ts itemId) + " ORDER BY item_ts ASC, chat_item_id ASC " + GRBefore ts itemId -> + rangeQuery + (" item_ts < ? " `orCond` " item_ts = ? AND chat_item_id < ? ") + (orParams ts itemId) + " ORDER BY item_ts DESC, chat_item_id DESC " + where + orCond c1 c2 = " (" <> c <> " AND " <> c1 <> ") OR (" <> c <> " AND " <> c2 <> ") " + orParams ts itemId = (p :. (Only ts) :. p :. (ts, itemId)) + rangeQuery :: ToRow p => Query -> p -> Query -> IO [ChatItemId] + rangeQuery c p ob + | null search = searchQuery "" () + | otherwise = searchQuery " AND item_text LIKE '%' || ? || '%' " (Only search) + where + searchQuery :: ToRow p' => Query -> p' -> IO [ChatItemId] + searchQuery c' p' = + map fromOnly <$> DB.query db (baseQuery <> c <> c' <> ob <> " LIMIT ?") (p :. p' :. Only count) safeGetGroupItem :: DB.Connection -> User -> GroupInfo -> UTCTime -> ChatItemId -> IO (CChatItem 'CTGroup) safeGetGroupItem db user g currentTs itemId = @@ -1271,64 +1247,36 @@ getGroupMemberChatItemLast db user@User {userId} groupId groupMemberId = do (userId, groupId, groupMemberId) getGroupChatItem db user groupId chatItemId -getGroupChatAfter_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) -getGroupChatAfter_ db user g@GroupInfo {groupId} afterId count search = do - let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} +getGroupChatAfter_ :: DB.Connection -> User -> GroupInfo -> Maybe ContentFilter -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) +getGroupChatAfter_ db user g@GroupInfo {groupId} contentFilter afterId count search = do afterCI <- getGroupChatItem db user groupId afterId - ciIds <- liftIO $ getGroupCIsAfter_ db user g afterCI count search + let range = GRAfter (chatItemTs afterCI) (cChatItemId afterCI) + ciIds <- liftIO $ getGroupChatItemIDs db user g contentFilter range count search ts <- liftIO getCurrentTime cis <- liftIO $ mapM (safeGetGroupItem db user g ts) ciIds - pure $ Chat (GroupChat g) cis stats + pure $ Chat (GroupChat g) cis emptyChatStats -getGroupCIsAfter_ :: DB.Connection -> User -> GroupInfo -> CChatItem 'CTGroup -> Int -> String -> IO [ChatItemId] -getGroupCIsAfter_ db User {userId} GroupInfo {groupId} afterCI count search = - map fromOnly - <$> DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%' - AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?)) - ORDER BY item_ts ASC, chat_item_id ASC - LIMIT ? - |] - (userId, groupId, search, chatItemTs afterCI, chatItemTs afterCI, cChatItemId afterCI, count) - -getGroupChatBefore_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) -getGroupChatBefore_ db user g@GroupInfo {groupId} beforeId count search = do - let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} +getGroupChatBefore_ :: DB.Connection -> User -> GroupInfo -> Maybe ContentFilter -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) +getGroupChatBefore_ db user g@GroupInfo {groupId} contentFilter beforeId count search = do beforeCI <- getGroupChatItem db user groupId beforeId - ciIds <- liftIO $ getGroupCIsBefore_ db user g beforeCI count search + let range = GRBefore (chatItemTs beforeCI) (cChatItemId beforeCI) + ciIds <- liftIO $ getGroupChatItemIDs db user g contentFilter range count search ts <- liftIO getCurrentTime cis <- liftIO $ mapM (safeGetGroupItem db user g ts) ciIds - pure $ Chat (GroupChat g) (reverse cis) stats + pure $ Chat (GroupChat g) (reverse cis) emptyChatStats -getGroupCIsBefore_ :: DB.Connection -> User -> GroupInfo -> CChatItem 'CTGroup -> Int -> String -> IO [ChatItemId] -getGroupCIsBefore_ db User {userId} GroupInfo {groupId} beforeCI count search = - map fromOnly - <$> DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%' - AND (item_ts < ? OR (item_ts = ? AND chat_item_id < ?)) - ORDER BY item_ts DESC, chat_item_id DESC - LIMIT ? - |] - (userId, groupId, search, chatItemTs beforeCI, chatItemTs beforeCI, cChatItemId beforeCI, count) - -getGroupChatAround_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) -getGroupChatAround_ db user g aroundId count search = do +getGroupChatAround_ :: DB.Connection -> User -> GroupInfo -> Maybe ContentFilter -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) +getGroupChatAround_ db user g contentFilter aroundId count search = do stats <- liftIO $ getGroupStats_ db user g - getGroupChatAround' db user g aroundId count search stats + getGroupChatAround' db user g contentFilter aroundId count search stats -getGroupChatAround' :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ChatStats -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) -getGroupChatAround' db user g@GroupInfo {groupId} aroundId count search stats = do +getGroupChatAround' :: DB.Connection -> User -> GroupInfo -> Maybe ContentFilter -> ChatItemId -> Int -> String -> ChatStats -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) +getGroupChatAround' db user g@GroupInfo {groupId} contentFilter aroundId count search stats = do aroundCI <- getGroupChatItem db user groupId aroundId - beforeIds <- liftIO $ getGroupCIsBefore_ db user g aroundCI count search - afterIds <- liftIO $ getGroupCIsAfter_ db user g aroundCI count search + let beforeRange = GRBefore (chatItemTs aroundCI) (cChatItemId aroundCI) + afterRange = GRAfter (chatItemTs aroundCI) (cChatItemId aroundCI) + beforeIds <- liftIO $ getGroupChatItemIDs db user g contentFilter beforeRange count search + afterIds <- liftIO $ getGroupChatItemIDs db user g contentFilter afterRange count search ts <- liftIO getCurrentTime beforeCIs <- liftIO $ mapM (safeGetGroupItem db user g ts) beforeIds afterCIs <- liftIO $ mapM (safeGetGroupItem db user g ts) afterIds @@ -1340,46 +1288,66 @@ getGroupChatAround' db user g@GroupInfo {groupId} aroundId count search stats = [] -> pure $ NavigationInfo 0 0 cis -> getGroupNavInfo_ db user g (last cis) -getGroupChatInitial_ :: DB.Connection -> User -> GroupInfo -> Int -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) -getGroupChatInitial_ db user g count = - liftIO (getGroupMinUnreadId_ db user g) >>= \case +getGroupChatInitial_ :: DB.Connection -> User -> GroupInfo -> Maybe ContentFilter -> Int -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) +getGroupChatInitial_ db user g contentFilter count = + liftIO (getGroupMinUnreadId_ db user g contentFilter) >>= \case Just minUnreadItemId -> do - unreadCount <- liftIO $ getGroupUnreadCount_ db user g - let stats = ChatStats {unreadCount, minUnreadItemId, unreadChat = False} - getGroupChatAround' db user g minUnreadItemId count "" stats - Nothing -> liftIO $ (,Just $ NavigationInfo 0 0) <$> getGroupChatLast_ db user g count "" + unreadCount <- liftIO $ getGroupUnreadCount_ db user g Nothing + reportsCount <- liftIO $ getGroupReportsCount_ db user g False + archivedReportsCount <- liftIO $ getGroupReportsCount_ db user g True + let stats = ChatStats {unreadCount, reportsCount, archivedReportsCount, minUnreadItemId, unreadChat = False} + getGroupChatAround' db user g contentFilter minUnreadItemId count "" stats + Nothing -> liftIO $ (,Just $ NavigationInfo 0 0) <$> getGroupChatLast_ db user g contentFilter count "" getGroupStats_ :: DB.Connection -> User -> GroupInfo -> IO ChatStats getGroupStats_ db user g = do - minUnreadItemId <- fromMaybe 0 <$> getGroupMinUnreadId_ db user g - unreadCount <- getGroupUnreadCount_ db user g - pure ChatStats {unreadCount, minUnreadItemId, unreadChat = False} + minUnreadItemId <- fromMaybe 0 <$> getGroupMinUnreadId_ db user g Nothing + unreadCount <- getGroupUnreadCount_ db user g Nothing + reportsCount <- getGroupReportsCount_ db user g False + archivedReportsCount <- getGroupReportsCount_ db user g True + pure ChatStats {unreadCount, reportsCount, archivedReportsCount, minUnreadItemId, unreadChat = False} -getGroupMinUnreadId_ :: DB.Connection -> User -> GroupInfo -> IO (Maybe ChatItemId) -getGroupMinUnreadId_ db User {userId} GroupInfo {groupId} = +getGroupMinUnreadId_ :: DB.Connection -> User -> GroupInfo -> Maybe ContentFilter -> IO (Maybe ChatItemId) +getGroupMinUnreadId_ db user g contentFilter = fmap join . maybeFirstRow fromOnly $ - DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_status = ? - ORDER BY item_ts ASC, chat_item_id ASC - LIMIT 1 - |] - (userId, groupId, CISRcvNew) + queryUnreadGroupItems db user g contentFilter baseQuery orderLimit + where + baseQuery = "SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? " + orderLimit = " ORDER BY item_ts ASC, chat_item_id ASC LIMIT 1" -getGroupUnreadCount_ :: DB.Connection -> User -> GroupInfo -> IO Int -getGroupUnreadCount_ db User {userId} GroupInfo {groupId} = +getGroupUnreadCount_ :: DB.Connection -> User -> GroupInfo -> Maybe ContentFilter -> IO Int +getGroupUnreadCount_ db user g contentFilter = + fromOnly . head <$> queryUnreadGroupItems db user g contentFilter baseQuery "" + where + baseQuery = "SELECT COUNT(1) FROM chat_items WHERE user_id = ? AND group_id = ? " + +getGroupReportsCount_ :: DB.Connection -> User -> GroupInfo -> Bool -> IO Int +getGroupReportsCount_ db User {userId} GroupInfo {groupId} archived = fromOnly . head <$> DB.query db - [sql| - SELECT COUNT(1) - FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_status = ? - |] - (userId, groupId, CISRcvNew) + "SELECT COUNT(1) FROM chat_items WHERE user_id = ? AND group_id = ? AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0" + (userId, groupId, MCReport_, BI archived) + +queryUnreadGroupItems :: FromRow r => DB.Connection -> User -> GroupInfo -> Maybe ContentFilter -> Query -> Query -> IO [r] +queryUnreadGroupItems db User {userId} GroupInfo {groupId} contentFilter baseQuery orderLimit = + case contentFilter of + Just ContentFilter {mcTag, deleted} -> case deleted of + Just deleted' -> + DB.query + db + (baseQuery <> " AND msg_content_tag = ? AND item_deleted = ? AND item_status = ? " <> orderLimit) + (userId, groupId, mcTag, BI deleted', CISRcvNew) + Nothing -> + DB.query + db + (baseQuery <> " AND msg_content_tag = ? AND item_status = ? " <> orderLimit) + (userId, groupId, mcTag, CISRcvNew) + Nothing -> + DB.query + db + (baseQuery <> " AND item_status = ? " <> orderLimit) + (userId, groupId, CISRcvNew) getGroupNavInfo_ :: DB.Connection -> User -> GroupInfo -> CChatItem 'CTGroup -> IO NavigationInfo getGroupNavInfo_ db User {userId} GroupInfo {groupId} afterCI = do @@ -1390,52 +1358,47 @@ getGroupNavInfo_ db User {userId} GroupInfo {groupId} afterCI = do getAfterUnreadCount :: IO Int getAfterUnreadCount = fromOnly . head - <$> DB.queryNamed + <$> DB.query db [sql| SELECT COUNT(1) FROM ( SELECT 1 FROM chat_items - WHERE user_id = :user_id AND group_id = :group_id AND item_status = :rcv_new - AND item_ts > :item_ts + WHERE user_id = ? AND group_id = ? AND item_status = ? + AND item_ts > ? UNION ALL SELECT 1 FROM chat_items - WHERE user_id = :user_id AND group_id = :group_id AND item_status = :rcv_new - AND item_ts = :item_ts AND chat_item_id > :item_id + WHERE user_id = ? AND group_id = ? AND item_status = ? + AND item_ts = ? AND chat_item_id > ? ) |] - [ ":user_id" := userId, - ":group_id" := groupId, - ":rcv_new" := CISRcvNew, - ":item_ts" := chatItemTs afterCI, - ":item_id" := cChatItemId afterCI - ] + ( (userId, groupId, CISRcvNew, chatItemTs afterCI) + :. (userId, groupId, CISRcvNew, chatItemTs afterCI, cChatItemId afterCI) + ) getAfterTotalCount :: IO Int getAfterTotalCount = fromOnly . head - <$> DB.queryNamed + <$> DB.query db [sql| SELECT COUNT(1) FROM ( SELECT 1 FROM chat_items - WHERE user_id = :user_id AND group_id = :group_id - AND item_ts > :item_ts + WHERE user_id = ? AND group_id = ? + AND item_ts > ? UNION ALL SELECT 1 FROM chat_items - WHERE user_id = :user_id AND group_id = :group_id - AND item_ts = :item_ts AND chat_item_id > :item_id + WHERE user_id = ? AND group_id = ? + AND item_ts = ? AND chat_item_id > ? ) |] - [ ":user_id" := userId, - ":group_id" := groupId, - ":item_ts" := chatItemTs afterCI, - ":item_id" := cChatItemId afterCI - ] + ( (userId, groupId, chatItemTs afterCI) + :. (userId, groupId, chatItemTs afterCI, cChatItemId afterCI) + ) getLocalChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) getLocalChat db user folderId pagination search_ = do @@ -1452,11 +1415,10 @@ getLocalChat db user folderId pagination search_ = do getLocalChatLast_ :: DB.Connection -> User -> NoteFolder -> Int -> String -> IO (Chat 'CTLocal) getLocalChatLast_ db user nf count search = do - let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} ciIds <- getLocalChatItemIdsLast_ db user nf count search ts <- getCurrentTime cis <- mapM (safeGetLocalItem db user nf ts) ciIds - pure $ Chat (LocalChat nf) (reverse cis) stats + pure $ Chat (LocalChat nf) (reverse cis) emptyChatStats getLocalChatItemIdsLast_ :: DB.Connection -> User -> NoteFolder -> Int -> String -> IO [ChatItemId] getLocalChatItemIdsLast_ db User {userId} NoteFolder {noteFolderId} count search = @@ -1500,12 +1462,11 @@ safeToLocalItem currentTs itemId = \case getLocalChatAfter_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal) getLocalChatAfter_ db user nf@NoteFolder {noteFolderId} afterId count search = do - let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} afterCI <- getLocalChatItem db user noteFolderId afterId ciIds <- liftIO $ getLocalCIsAfter_ db user nf afterCI count search ts <- liftIO getCurrentTime cis <- liftIO $ mapM (safeGetLocalItem db user nf ts) ciIds - pure $ Chat (LocalChat nf) cis stats + pure $ Chat (LocalChat nf) cis emptyChatStats getLocalCIsAfter_ :: DB.Connection -> User -> NoteFolder -> CChatItem 'CTLocal -> Int -> String -> IO [ChatItemId] getLocalCIsAfter_ db User {userId} NoteFolder {noteFolderId} afterCI count search = @@ -1524,12 +1485,11 @@ getLocalCIsAfter_ db User {userId} NoteFolder {noteFolderId} afterCI count searc getLocalChatBefore_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal) getLocalChatBefore_ db user nf@NoteFolder {noteFolderId} beforeId count search = do - let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} beforeCI <- getLocalChatItem db user noteFolderId beforeId ciIds <- liftIO $ getLocalCIsBefore_ db user nf beforeCI count search ts <- liftIO getCurrentTime cis <- liftIO $ mapM (safeGetLocalItem db user nf ts) ciIds - pure $ Chat (LocalChat nf) (reverse cis) stats + pure $ Chat (LocalChat nf) (reverse cis) emptyChatStats getLocalCIsBefore_ :: DB.Connection -> User -> NoteFolder -> CChatItem 'CTLocal -> Int -> String -> IO [ChatItemId] getLocalCIsBefore_ db User {userId} NoteFolder {noteFolderId} beforeCI count search = @@ -1572,7 +1532,7 @@ getLocalChatInitial_ db user nf count = do liftIO (getLocalMinUnreadId_ db user nf) >>= \case Just minUnreadItemId -> do unreadCount <- liftIO $ getLocalUnreadCount_ db user nf - let stats = ChatStats {unreadCount, minUnreadItemId, unreadChat = False} + let stats = emptyChatStats {unreadCount, minUnreadItemId} getLocalChatAround' db user nf minUnreadItemId count "" stats Nothing -> liftIO $ (,Just $ NavigationInfo 0 0) <$> getLocalChatLast_ db user nf count "" @@ -1580,7 +1540,7 @@ getLocalStats_ :: DB.Connection -> User -> NoteFolder -> IO ChatStats getLocalStats_ db user nf = do minUnreadItemId <- fromMaybe 0 <$> getLocalMinUnreadId_ db user nf unreadCount <- getLocalUnreadCount_ db user nf - pure ChatStats {unreadCount, minUnreadItemId, unreadChat = False} + pure emptyChatStats {unreadCount, minUnreadItemId} getLocalMinUnreadId_ :: DB.Connection -> User -> NoteFolder -> IO (Maybe ChatItemId) getLocalMinUnreadId_ db User {userId} NoteFolder {noteFolderId} = @@ -1617,52 +1577,47 @@ getLocalNavInfo_ db User {userId} NoteFolder {noteFolderId} afterCI = do getAfterUnreadCount :: IO Int getAfterUnreadCount = fromOnly . head - <$> DB.queryNamed + <$> DB.query db [sql| SELECT COUNT(1) FROM ( SELECT 1 FROM chat_items - WHERE user_id = :user_id AND note_folder_id = :note_folder_id AND item_status = :rcv_new - AND created_at > :created_at + WHERE user_id = ? AND note_folder_id = ? AND item_status = ? + AND created_at > ? UNION ALL SELECT 1 FROM chat_items - WHERE user_id = :user_id AND note_folder_id = :note_folder_id AND item_status = :rcv_new - AND created_at = :created_at AND chat_item_id > :item_id + WHERE user_id = ? AND note_folder_id = ? AND item_status = ? + AND created_at = ? AND chat_item_id > ? ) |] - [ ":user_id" := userId, - ":note_folder_id" := noteFolderId, - ":rcv_new" := CISRcvNew, - ":created_at" := ciCreatedAt afterCI, - ":item_id" := cChatItemId afterCI - ] + ( (userId, noteFolderId, CISRcvNew, ciCreatedAt afterCI) + :. (userId, noteFolderId, CISRcvNew, ciCreatedAt afterCI, cChatItemId afterCI) + ) getAfterTotalCount :: IO Int getAfterTotalCount = fromOnly . head - <$> DB.queryNamed + <$> DB.query db [sql| SELECT COUNT(1) FROM ( SELECT 1 FROM chat_items - WHERE user_id = :user_id AND note_folder_id = :note_folder_id - AND created_at > :created_at + WHERE user_id = ? AND note_folder_id = ? + AND created_at > ? UNION ALL SELECT 1 FROM chat_items - WHERE user_id = :user_id AND note_folder_id = :note_folder_id - AND created_at = :created_at AND chat_item_id > :item_id + WHERE user_id = ? AND note_folder_id = ? + AND created_at = ? AND chat_item_id > ? ) |] - [ ":user_id" := userId, - ":note_folder_id" := noteFolderId, - ":created_at" := ciCreatedAt afterCI, - ":item_id" := cChatItemId afterCI - ] + ( (userId, noteFolderId, ciCreatedAt afterCI) + :. (userId, noteFolderId, ciCreatedAt afterCI, cChatItemId afterCI) + ) toChatItemRef :: (ChatItemId, Maybe Int64, Maybe Int64, Maybe Int64) -> Either StoreError (ChatRef, ChatItemId) toChatItemRef = \case @@ -2114,7 +2069,7 @@ createChatItemVersion db itemId itemVersionTs msgContent = INSERT INTO chat_item_versions (chat_item_id, msg_content, item_version_ts) VALUES (?,?,?) |] - (itemId, toMCText msgContent, itemVersionTs) + (itemId, MCText $ msgContentText msgContent, itemVersionTs) deleteDirectChatItem :: DB.Connection -> User -> Contact -> ChatItem 'CTDirect d -> IO () deleteDirectChatItem db User {userId} Contact {contactId} ci = do @@ -2388,6 +2343,20 @@ markGroupCIBlockedByAdmin db User {userId} GroupInfo {groupId} ci@ChatItem {meta (DBCIBlockedByAdmin, deletedTs, deletedTs, userId, groupId, chatItemId' ci) pure ci {meta = meta {itemDeleted = Just $ CIBlockedByAdmin $ Just deletedTs, editable = False, deletable = False}} +markMessageReportsDeleted :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> GroupMember -> UTCTime -> IO [ChatItemId] +markMessageReportsDeleted db User {userId} GroupInfo {groupId} ChatItem {meta = CIMeta {itemSharedMsgId}} GroupMember {groupMemberId} deletedTs = do + currentTs <- liftIO getCurrentTime + map fromOnly + <$> DB.query + db + [sql| + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND msg_content_tag = ? AND quoted_shared_msg_id = ? + RETURNING chat_item_id; + |] + (DBCIDeleted, deletedTs, groupMemberId, currentTs, userId, groupId, MCReport_, itemSharedMsgId) + getGroupChatItemBySharedMsgId :: DB.Connection -> User -> GroupId -> GroupMemberId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTGroup) getGroupChatItemBySharedMsgId db user@User {userId} groupId groupMemberId sharedMsgId = do itemId <- diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index be4c129d07..a6cb562aa1 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -122,6 +122,7 @@ import Simplex.Chat.Migrations.M20241205_business_chat_members import Simplex.Chat.Migrations.M20241222_operator_conditions import Simplex.Chat.Migrations.M20241223_chat_tags import Simplex.Chat.Migrations.M20241230_reports +import Simplex.Chat.Migrations.M20250105_indexes import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -243,7 +244,8 @@ schemaMigrations = ("20241205_business_chat_members", m20241205_business_chat_members, Just down_m20241205_business_chat_members), ("20241222_operator_conditions", m20241222_operator_conditions, Just down_m20241222_operator_conditions), ("20241223_chat_tags", m20241223_chat_tags, Just down_m20241223_chat_tags), - ("20241230_reports", m20241230_reports, Just down_m20241230_reports) + ("20241230_reports", m20241230_reports, Just down_m20241230_reports), + ("20250105_indexes", m20250105_indexes, Just down_m20250105_indexes) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 04a3be148c..d363c95461 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -156,6 +156,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe [ChatItemDeletion (AChatItem _ _ chat deletedItem) toItem] -> ttyUser u $ unmuted u chat deletedItem $ viewItemDelete chat deletedItem toItem byUser timed ts tz testView deletions' -> ttyUser u [sShow (length deletions') <> " messages deleted"] + CRGroupChatItemsDeleted u g ciIds byUser member_ -> ttyUser u [ttyGroup' g <> ": " <> sShow (length ciIds) <> " messages deleted by " <> if byUser then "user" else "member" <> maybe "" (\m -> " " <> ttyMember m) member_] CRChatItemReaction u added (ACIReaction _ _ chat reaction) -> ttyUser u $ unmutedReaction u chat reaction $ viewItemReaction showReactions chat reaction added ts tz CRReactionMembers u memberReactions -> ttyUser u $ viewReactionMembers memberReactions CRChatItemDeletedNotFound u Contact {localDisplayName = c} _ -> ttyUser u [ttyFrom $ c <> "> [deleted - original message not found]"] diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 7f1a9d3e56..f14a041f67 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -6600,3 +6600,34 @@ testGroupMemberReports = bob <## " report content", (cath ("/_get chat #1 content=report count=100", chat, [(0, "report content")]) + alice #$> ("/_get chat #1 content=report deleted=off count=100", chat, [(0, "report content")]) + alice #$> ("/_get chat #1 content=report deleted=on count=100", chat, []) + bob #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content")]) + bob #$> ("/_get chat #1 content=report deleted=off count=100", chat, [(0, "report content")]) + bob #$> ("/_get chat #1 content=report deleted=on count=100", chat, []) + dan #$> ("/_get chat #1 content=report count=100", chat, [(1, "report content")]) + dan #$> ("/_get chat #1 content=report deleted=off count=100", chat, [(1, "report content")]) + dan #$> ("/_get chat #1 content=report deleted=on count=100", chat, []) + alice ##> "\\\\ #jokes cath inappropriate joke" + concurrentlyN_ + [ do + alice <## "#jokes: 1 messages deleted by member alice" + alice <## "message marked deleted by you", + do + bob <# "#jokes cath> [marked deleted by alice] inappropriate joke" + bob <## "#jokes: 1 messages deleted by member alice", + cath <# "#jokes cath> [marked deleted by alice] inappropriate joke", + do + dan <# "#jokes cath> [marked deleted by alice] inappropriate joke" + dan <## "#jokes: 1 messages deleted by member alice" + ] + alice #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content [marked deleted by you]")]) + alice #$> ("/_get chat #1 content=report deleted=off count=100", chat, []) + alice #$> ("/_get chat #1 content=report deleted=on count=100", chat, [(0, "report content [marked deleted by you]")]) + bob #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content [marked deleted by alice]")]) + bob #$> ("/_get chat #1 content=report deleted=off count=100", chat, []) + bob #$> ("/_get chat #1 content=report deleted=on count=100", chat, [(0, "report content [marked deleted by alice]")]) + dan #$> ("/_get chat #1 content=report count=100", chat, [(1, "report content [marked deleted by alice]")]) + dan #$> ("/_get chat #1 content=report deleted=off count=100", chat, []) + dan #$> ("/_get chat #1 content=report deleted=on count=100", chat, [(1, "report content [marked deleted by alice]")])