diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index cded13f3a2..a66f1b379e 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -766,6 +766,7 @@ Group: - itemTimed: [CITimed](#citimed)? - itemLive: bool? - userMention: bool +- hasLink: bool - deletable: bool - editable: bool - forwardedByMember: int64? diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index c4371082cc..bd03d7d72b 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -778,6 +778,7 @@ export interface CIMeta { itemTimed?: CITimed itemLive?: boolean userMention: boolean + hasLink: boolean deletable: boolean editable: boolean forwardedByMember?: number // int64 diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 93186c8bca..4cb8da0c59 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -126,6 +126,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables Simplex.Chat.Store.Postgres.Migrations.M20260108_chat_indices + Simplex.Chat.Store.Postgres.Migrations.M20260122_has_link else exposed-modules: Simplex.Chat.Archive @@ -275,6 +276,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations Simplex.Chat.Store.SQLite.Migrations.M20251230_strict_tables Simplex.Chat.Store.SQLite.Migrations.M20260108_chat_indices + Simplex.Chat.Store.SQLite.Migrations.M20260122_has_link other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 994b435297..597d704523 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -2157,7 +2157,8 @@ processChatCommand vr nm = \case (errs, ctSndMsgs :: [(Contact, SndMessage)]) <- partitionEithers . L.toList . zipWith3' combineResults ctConns sndMsgs <$> deliverMessagesB msgReqs_ timestamp <- liftIO getCurrentTime - lift . void $ withStoreBatch' $ \db -> map (createCI db user timestamp) ctSndMsgs + let hasLink = msgContentHasLink mc $ parseMaybeMarkdownList $ msgContentText mc + lift . void $ withStoreBatch' $ \db -> map (createCI db user hasLink timestamp) ctSndMsgs pure CRBroadcastSent {user, msgContent = mc, successes = length ctSndMsgs, failures = length errs, timestamp} where addContactConn :: Contact -> [(Contact, Connection)] -> [(Contact, Connection)] @@ -2172,9 +2173,9 @@ processChatCommand vr nm = \case combineResults (ct, _) (Right msg') (Right _) = Right (ct, msg') combineResults _ (Left e) _ = Left e combineResults _ _ (Left e) = Left e - createCI :: DB.Connection -> User -> UTCTime -> (Contact, SndMessage) -> IO () - createCI db user createdAt (ct, sndMsg) = - void $ createNewSndChatItem db user (CDDirectSnd ct) sndMsg (CISndMsgContent mc) Nothing Nothing Nothing False createdAt + createCI :: DB.Connection -> User -> Bool -> UTCTime -> (Contact, SndMessage) -> IO () + createCI db user hasLink createdAt (ct, sndMsg) = + void $ createNewSndChatItem db user (CDDirectSnd ct) sndMsg (CISndMsgContent mc) Nothing Nothing Nothing False hasLink createdAt SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do contactId <- withFastStore $ \db -> getContactIdByName db user cName quotedItemId <- withFastStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 8e1e6705df..2225a472d0 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -550,7 +550,6 @@ markGroupCIsDeleted user gInfo chatScopeInfo items byGroupMember_ deletedTs = do (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (markDeleted db) items) unless (null errs) $ toView $ CEvtChatErrors errs pure deletions - -- pure $ CRChatItemsDeleted user deletions byUser False where markDeleted db (CChatItem md ci) = do ci' <- markGroupChatItemDeleted db user gInfo ci byGroupMember_ deletedTs @@ -1260,7 +1259,7 @@ encodeShortLinkData d = s' | B.length s > 10240 = B.cons 'X' $ Z1.compress compressionLevel s | otherwise = s - in UserLinkData s' + in UserLinkData s' decodeShortLinkData :: J.FromJSON a => ConnLinkData c -> IO (Maybe a) decodeShortLinkData cData @@ -2071,9 +2070,9 @@ memberSendAction GroupInfo {useRelays, membership} events members m@GroupMember readyMemberConn :: GroupMember -> Maybe (GroupMemberId, Connection) readyMemberConn GroupMember {groupMemberId, activeConn = Just conn@Connection {connStatus}, memberStatus} | (connStatus == ConnReady || connStatus == ConnSndReady) - && not (connDisabled conn) - && not (connInactive conn) - && memberStatus /= GSMemRejected = + && not (connDisabled conn) + && not (connInactive conn) + && memberStatus /= GSMemRejected = Just (groupMemberId, conn) | otherwise = Nothing readyMemberConn GroupMember {activeConn = Nothing} = Nothing @@ -2183,14 +2182,15 @@ saveSndChatItems user cd itemsData itemTimed live = do createdAt <- liftIO getCurrentTime vr <- chatVersionRange when (contactChatDeleted cd || any (\NewSndChatItemData {content} -> ciRequiresAttention content) (rights itemsData)) $ - void $ withStore' (\db -> updateChatTsStats db vr user cd createdAt Nothing) + void (withStore' $ \db -> updateChatTsStats db vr user cd createdAt Nothing) lift $ withStoreBatch (\db -> map (bindRight $ createItem db createdAt) itemsData) where createItem :: DB.Connection -> UTCTime -> NewSndChatItemData c -> IO (Either ChatError (ChatItem c 'MDSnd)) createItem db createdAt NewSndChatItemData {msg = msg@SndMessage {sharedMsgId}, content, itemTexts, itemMentions, ciFile, quotedItem, itemForwarded} = do - ciId <- createNewSndChatItem db user cd msg content quotedItem itemForwarded itemTimed live createdAt + let hasLink_ = ciContentHasLink content (snd itemTexts) + ciId <- createNewSndChatItem db user cd msg content quotedItem itemForwarded itemTimed live hasLink_ createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt - let ci = mkChatItem_ cd False ciId content itemTexts ciFile quotedItem (Just sharedMsgId) itemForwarded itemTimed live False createdAt Nothing createdAt + let ci = mkChatItem_ cd False ciId content itemTexts ciFile quotedItem (Just sharedMsgId) itemForwarded itemTimed live False hasLink_ createdAt Nothing createdAt Right <$> case cd of CDGroupSnd g _scope | not (null itemMentions) -> createGroupCIMentions db g ci itemMentions _ -> pure ci @@ -2219,12 +2219,14 @@ saveRcvChatItem' user cd msg@RcvMessage {chatMsgEvent, forwardedByMember} shared userMention' = userReply || any (\CIMention {memberId} -> sameMemberId memberId membership) mentions' in pure (mentions', userMention') CDDirectRcv _ -> pure (M.empty, False) - cInfo' <- if (ciRequiresAttention content || contactChatDeleted cd) - then updateChatTsStats db vr user cd createdAt (memberChatStats userMention) - else pure $ toChatInfo cd - (ciId, quotedItem, itemForwarded) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live userMention brokerTs createdAt + cInfo' <- + if ciRequiresAttention content || contactChatDeleted cd + then updateChatTsStats db vr user cd createdAt (memberChatStats userMention) + else pure $ toChatInfo cd + let hasLink_ = ciContentHasLink content ft_ + (ciId, quotedItem, itemForwarded) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live userMention hasLink_ brokerTs createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt - let ci = mkChatItem_ cd False ciId content (t, ft_) ciFile quotedItem sharedMsgId_ itemForwarded itemTimed live userMention brokerTs forwardedByMember createdAt + let ci = mkChatItem_ cd False ciId content (t, ft_) ciFile quotedItem sharedMsgId_ itemForwarded itemTimed live userMention hasLink_ brokerTs forwardedByMember createdAt ci' <- case cd of CDGroupRcv g _scope _m | not (null mentions') -> createGroupCIMentions db g ci mentions' _ -> pure ci @@ -2240,15 +2242,26 @@ saveRcvChatItem' user cd msg@RcvMessage {chatMsgEvent, forwardedByMember} shared -- TODO [mentions] optimize by avoiding unnecessary parsing mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ShowGroupAsSender -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d mkChatItem cd showGroupAsSender ciId content file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember currentTs = - let ts = ciContentTexts content - in mkChatItem_ cd showGroupAsSender ciId content ts file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember currentTs + let ts@(_, ft_) = ciContentTexts content + hasLink_ = ciContentHasLink content ft_ + in mkChatItem_ cd showGroupAsSender ciId content ts file quotedItem sharedMsgId itemForwarded itemTimed live userMention hasLink_ itemTs forwardedByMember currentTs -mkChatItem_ :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ShowGroupAsSender -> ChatItemId -> CIContent d -> (Text, Maybe MarkdownList) -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d -mkChatItem_ cd showGroupAsSender ciId content (itemText, formattedText) file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember currentTs = +mkChatItem_ :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ShowGroupAsSender -> ChatItemId -> CIContent d -> (Text, Maybe MarkdownList) -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d +mkChatItem_ cd showGroupAsSender ciId content (itemText, formattedText) file quotedItem sharedMsgId itemForwarded itemTimed live userMention hasLink_ itemTs forwardedByMember currentTs = let itemStatus = ciCreateStatus content - meta = mkCIMeta ciId content itemText itemStatus Nothing sharedMsgId itemForwarded Nothing False itemTimed (justTrue live) userMention currentTs itemTs forwardedByMember showGroupAsSender currentTs currentTs + meta = mkCIMeta ciId content itemText itemStatus Nothing sharedMsgId itemForwarded Nothing False itemTimed (justTrue live) userMention hasLink_ currentTs itemTs forwardedByMember showGroupAsSender currentTs currentTs in ChatItem {chatDir = toCIDirection cd, meta, content, mentions = M.empty, formattedText, quotedItem, reactions = [], file} +ciContentHasLink :: CIContent d -> Maybe MarkdownList -> Bool +ciContentHasLink content ft_ = case ciMsgContent content of + Just mc -> msgContentHasLink mc ft_ + Nothing -> False + +msgContentHasLink :: MsgContent -> Maybe MarkdownList -> Bool +msgContentHasLink mc ft_ = case msgContentTag mc of + MCLink_ -> True + _ -> maybe False hasLinks ft_ + createAgentConnectionAsync :: ConnectionModeI c => User -> CommandFunction -> Bool -> SConnectionMode c -> SubscriptionMode -> CM (CommandId, ConnId) createAgentConnectionAsync user cmdFunction enableNtfs cMode subMode = do cmdId <- withStore' $ \db -> createCommand db user Nothing cmdFunction @@ -2497,7 +2510,8 @@ createChatItems user itemTs_ dirsCIContents = do createACIs db itemTs createdAt (cd, showGroupAsSender, contents) = map createACI contents where createACI (content, sharedMsgId) = do - ciId <- createNewChatItemNoMsg db user cd showGroupAsSender content sharedMsgId itemTs createdAt + let hasLink_ = ciContentHasLink content Nothing + ciId <- createNewChatItemNoMsg db user cd showGroupAsSender content sharedMsgId hasLink_ itemTs createdAt let ci = mkChatItem cd showGroupAsSender ciId content Nothing Nothing Nothing Nothing Nothing False False itemTs Nothing createdAt pure $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci @@ -2527,10 +2541,11 @@ createLocalChatItems user cd itemsData createdAt = do pure items where createItem :: DB.Connection -> (CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom, (Text, Maybe MarkdownList)) -> IO (ChatItem 'CTLocal 'MDSnd) - createItem db (content, ciFile, itemForwarded, ts) = do - ciId <- createNewChatItem_ db user cd False Nothing Nothing content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False False createdAt Nothing createdAt + createItem db (content, ciFile, itemForwarded, ts@(_, ft_)) = do + let hasLink_ = ciContentHasLink content ft_ + ciId <- createNewChatItem_ db user cd False Nothing Nothing content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False False hasLink_ createdAt Nothing createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt - pure $ mkChatItem_ cd False ciId content ts ciFile Nothing Nothing itemForwarded Nothing False False createdAt Nothing createdAt + pure $ mkChatItem_ cd False ciId content ts ciFile Nothing Nothing itemForwarded Nothing False False hasLink_ createdAt Nothing createdAt withUser' :: (User -> CM ChatResponse) -> CM ChatResponse withUser' action = diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index fb44b416aa..3287b263d4 100644 --- a/src/Simplex/Chat/Markdown.hs +++ b/src/Simplex/Chat/Markdown.hs @@ -178,6 +178,16 @@ isSimplexLink = \case SimplexLink {} -> True _ -> False +isLink :: Format -> Bool +isLink = \case + Uri -> True + HyperLink {} -> True + SimplexLink {} -> True + _ -> False + +hasLinks :: MarkdownList -> Bool +hasLinks = any $ \(FormattedText f _) -> maybe False isLink f + markdownP :: Parser Markdown markdownP = mconcat <$> A.many' fragmentP where diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index f5d07d18af..056b857f80 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -499,6 +499,7 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta itemTimed :: Maybe CITimed, itemLive :: Maybe Bool, userMention :: Bool, -- True for messages that mention user or reply to user messages + hasLink :: BoolDef, deletable :: Bool, editable :: Bool, forwardedByMember :: Maybe GroupMemberId, @@ -510,11 +511,12 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta type ShowGroupAsSender = Bool -mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe Bool -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> Bool -> UTCTime -> UTCTime -> CIMeta c d -mkCIMeta itemId itemContent itemText itemStatus sentViaProxy itemSharedMsgId itemForwarded itemDeleted itemEdited itemTimed itemLive userMention currentTs itemTs forwardedByMember showGroupAsSender createdAt updatedAt = +mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe Bool -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> Bool -> Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> Bool -> UTCTime -> UTCTime -> CIMeta c d +mkCIMeta itemId itemContent itemText itemStatus sentViaProxy itemSharedMsgId itemForwarded itemDeleted itemEdited itemTimed itemLive userMention hasLink_ currentTs itemTs forwardedByMember showGroupAsSender createdAt updatedAt = let deletable = deletable' itemContent itemDeleted itemTs nominalDay currentTs editable = deletable && isNothing itemForwarded - in CIMeta {itemId, itemTs, itemText, itemStatus, sentViaProxy, itemSharedMsgId, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, userMention, deletable, editable, forwardedByMember, showGroupAsSender, createdAt, updatedAt} + hasLink = BoolDef hasLink_ + in CIMeta {itemId, itemTs, itemText, itemStatus, sentViaProxy, itemSharedMsgId, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, userMention, hasLink, deletable, editable, forwardedByMember, showGroupAsSender, createdAt, updatedAt} deletable' :: forall c d. ChatTypeI c => CIContent d -> Maybe (CIDeleted c) -> UTCTime -> NominalDiffTime -> UTCTime -> Bool deletable' itemContent itemDeleted itemTs allowedInterval currentTs = @@ -540,6 +542,7 @@ dummyMeta itemId ts itemText = itemTimed = Nothing, itemLive = Nothing, userMention = False, + hasLink = BoolDef False, deletable = False, editable = False, forwardedByMember = Nothing, diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index ce03edbdbd..f77c836b0d 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -525,9 +525,9 @@ setSupportChatMemberAttention db vr user g m memberAttention = do m_ <- runExceptT $ getGroupMemberById db vr user (groupMemberId' m) pure $ either (const m) id m_ -- Left shouldn't happen, but types require it -createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> UTCTime -> IO ChatItemId -createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciContent quotedItem itemForwarded timed live createdAt = - createNewChatItem_ db user chatDirection False createdByMsgId (Just sharedMsgId) ciContent quoteRow itemForwarded timed live False createdAt Nothing createdAt +createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> UTCTime -> IO ChatItemId +createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciContent quotedItem itemForwarded timed live hasLink createdAt = + createNewChatItem_ db user chatDirection False createdByMsgId (Just sharedMsgId) ciContent quoteRow itemForwarded timed live False hasLink createdAt Nothing createdAt where createdByMsgId = if msgId == 0 then Nothing else Just msgId quoteRow :: NewQuoteRow @@ -541,9 +541,9 @@ createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciCon CIQGroupRcv (Just GroupMember {memberId}) -> (Just False, Just memberId) CIQGroupRcv Nothing -> (Just False, Nothing) -createNewRcvChatItem :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c), Maybe CIForwardedFrom) -createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, forwardedByMember} sharedMsgId_ ciContent timed live userMention itemTs createdAt = do - ciId <- createNewChatItem_ db user chatDirection False (Just msgId) sharedMsgId_ ciContent quoteRow itemForwarded timed live userMention itemTs forwardedByMember createdAt +createNewRcvChatItem :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> Bool -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c), Maybe CIForwardedFrom) +createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, forwardedByMember} sharedMsgId_ ciContent timed live userMention hasLink itemTs createdAt = do + ciId <- createNewChatItem_ db user chatDirection False (Just msgId) sharedMsgId_ ciContent quoteRow itemForwarded timed live userMention hasLink itemTs forwardedByMember createdAt quotedItem <- mapM (getChatItemQuote_ db user chatDirection) quotedMsg pure (ciId, quotedItem, itemForwarded) where @@ -558,15 +558,15 @@ createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, forw CDGroupRcv GroupInfo {membership = GroupMember {memberId = userMemberId}} _ _ -> (Just $ Just userMemberId == memberId, memberId) -createNewChatItemNoMsg :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> ShowGroupAsSender -> CIContent d -> Maybe SharedMsgId -> UTCTime -> UTCTime -> IO ChatItemId -createNewChatItemNoMsg db user chatDirection showGroupAsSender ciContent sharedMsgId_ itemTs = - createNewChatItem_ db user chatDirection showGroupAsSender Nothing sharedMsgId_ ciContent quoteRow Nothing Nothing False False itemTs Nothing +createNewChatItemNoMsg :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> ShowGroupAsSender -> CIContent d -> Maybe SharedMsgId -> Bool -> UTCTime -> UTCTime -> IO ChatItemId +createNewChatItemNoMsg db user chatDirection showGroupAsSender ciContent sharedMsgId_ hasLink itemTs = + createNewChatItem_ db user chatDirection showGroupAsSender Nothing sharedMsgId_ ciContent quoteRow Nothing Nothing False False hasLink itemTs Nothing where quoteRow :: NewQuoteRow quoteRow = (Nothing, Nothing, Nothing, Nothing, Nothing) -createNewChatItem_ :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> ShowGroupAsSender -> Maybe MessageId -> Maybe SharedMsgId -> CIContent d -> NewQuoteRow -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> UTCTime -> Maybe GroupMemberId -> UTCTime -> IO ChatItemId -createNewChatItem_ db User {userId} chatDirection showGroupAsSender msgId_ sharedMsgId ciContent quoteRow itemForwarded timed live userMention itemTs forwardedByMember createdAt = do +createNewChatItem_ :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> ShowGroupAsSender -> Maybe MessageId -> Maybe SharedMsgId -> CIContent d -> NewQuoteRow -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> Bool -> UTCTime -> Maybe GroupMemberId -> UTCTime -> IO ChatItemId +createNewChatItem_ db User {userId} chatDirection showGroupAsSender msgId_ sharedMsgId ciContent quoteRow itemForwarded timed live userMention hasLink itemTs forwardedByMember createdAt = do DB.execute db [sql| @@ -575,20 +575,20 @@ createNewChatItem_ db User {userId} chatDirection showGroupAsSender msgId_ share user_id, created_by_msg_id, contact_id, group_id, group_member_id, note_folder_id, group_scope_tag, group_scope_group_member_id, -- meta item_sent, item_ts, item_content, item_content_tag, item_text, item_status, msg_content_tag, shared_msg_id, - forwarded_by_group_member_id, include_in_history, created_at, updated_at, item_live, user_mention, show_group_as_sender, timed_ttl, timed_delete_at, + forwarded_by_group_member_id, include_in_history, created_at, updated_at, item_live, user_mention, has_link, show_group_as_sender, timed_ttl, timed_delete_at, -- quote quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id, -- forwarded from fwd_from_tag, fwd_from_chat_name, fwd_from_msg_dir, fwd_from_contact_id, fwd_from_group_id, fwd_from_chat_item_id - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ((userId, msgId_) :. idsRow :. groupScopeRow :. itemRow :. quoteRow' :. forwardedFromRow) ciId <- insertedRowId db forM_ msgId_ $ \msgId -> insertChatItemMessage_ db ciId msgId createdAt pure ciId where - itemRow :: (SMsgDirection d, UTCTime, CIContent d, Text, Text, CIStatus d, Maybe MsgContentTag, Maybe SharedMsgId, Maybe GroupMemberId, BoolInt) :. (UTCTime, UTCTime, Maybe BoolInt, BoolInt, BoolInt) :. (Maybe Int, Maybe UTCTime) - itemRow = (msgDirection @d, itemTs, ciContent, toCIContentTag ciContent, ciContentToText ciContent, ciCreateStatus ciContent, msgContentTag <$> ciMsgContent ciContent, sharedMsgId, forwardedByMember, BI includeInHistory) :. (createdAt, createdAt, BI <$> (justTrue live), BI userMention, BI showGroupAsSender) :. ciTimedRow timed + itemRow :: (SMsgDirection d, UTCTime, CIContent d, Text, Text, CIStatus d, Maybe MsgContentTag, Maybe SharedMsgId, Maybe GroupMemberId, BoolInt) :. (UTCTime, UTCTime, Maybe BoolInt, BoolInt, BoolInt, BoolInt) :. (Maybe Int, Maybe UTCTime) + itemRow = (msgDirection @d, itemTs, ciContent, toCIContentTag ciContent, ciContentToText ciContent, ciCreateStatus ciContent, msgContentTag <$> ciMsgContent ciContent, sharedMsgId, forwardedByMember, BI includeInHistory) :. (createdAt, createdAt, BI <$> justTrue live, BI userMention, BI hasLink, BI showGroupAsSender) :. ciTimedRow timed quoteRow' = let (a, b, c, d, e) = quoteRow in (a, b, c, BI <$> d, e) idsRow :: (Maybe ContactId, Maybe GroupId, Maybe GroupMemberId, Maybe NoteFolderId) idsRow = case chatDirection of @@ -1045,7 +1045,7 @@ getLocalChatPreview_ db user (LocalChatPD _ noteFolderId lastItemId_ stats) = do -- this function can be changed so it never fails, not only avoid failure on invalid json toLocalChatItem :: UTCTime -> ChatItemRow -> Either StoreError (CChatItem 'CTLocal) -toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) = +toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention, BI hasLink) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) = chatItem $ fromRight invalid $ dbParseACIContent itemContentText where invalid = ACIContent msgDir $ CIInvalidJSON itemContentText @@ -1078,7 +1078,7 @@ toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentTex _ -> Just (CIDeleted @'CTLocal deletedTs) itemEdited' = maybe False unBI itemEdited itemForwarded = toCIForwardedFrom forwardedFromRow - in mkCIMeta itemId content itemText status (unBI <$> sentViaProxy) sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed (unBI <$> itemLive) userMention currentTs itemTs Nothing False createdAt updatedAt + in mkCIMeta itemId content itemText status (unBI <$> sentViaProxy) sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed (unBI <$> itemLive) userMention hasLink currentTs itemTs Nothing False createdAt updatedAt ciTimed :: Maybe CITimed ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} @@ -1470,6 +1470,12 @@ getChatItemIDs db User {userId} cInfo contentFilter range count search = case cI (grCond <> " AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL ") (userId, groupId) "item_ts" + (Nothing, Just MCLink_) -> + liftIO $ + idsQuery + (grCond <> " AND has_link = 1 ") + (userId, groupId) + "item_ts" (Nothing, Just mcTag) -> liftIO $ idsQuery @@ -1488,11 +1494,13 @@ getChatItemIDs db User {userId} cInfo contentFilter range count search = case cI grCond = " user_id = ? AND group_id = ? " DirectChat Contact {contactId} -> liftIO $ case contentFilter of Nothing -> idsQuery ctCond (userId, contactId) "created_at" + Just MCLink_ -> idsQuery (ctCond <> " AND has_link = 1 ") (userId, contactId) "created_at" Just mcTag -> idsQuery (ctCond <> " AND msg_content_tag = ? ") (userId, contactId, mcTag) "created_at" where ctCond = " user_id = ? AND contact_id = ? " LocalChat NoteFolder {noteFolderId} -> liftIO $ case contentFilter of Nothing -> idsQuery nfCond (userId, noteFolderId) "created_at" + Just MCLink_ -> idsQuery (nfCond <> " AND has_link = 1 ") (userId, noteFolderId) "created_at" Just mcTag -> idsQuery (nfCond <> " AND msg_content_tag = ? ") (userId, noteFolderId, mcTag) "created_at" where nfCond = " user_id = ? AND note_folder_id = ? " @@ -2191,7 +2199,7 @@ updateLocalChatItemsRead db User {userId} noteFolderId = do 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 BoolInt, BoolInt) +type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe BoolInt, BoolInt, BoolInt) type ChatItemForwardedFromRow = (Maybe CIForwardedFromTag, Maybe Text, Maybe MsgDirection, Maybe Int64, Maybe Int64, Maybe Int64) @@ -2215,7 +2223,7 @@ toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir -- this function can be changed so it never fails, not only avoid failure on invalid json toDirectChatItem :: UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect) -toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) = +toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention, BI hasLink) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) = chatItem $ fromRight invalid $ dbParseACIContent itemContentText where invalid = ACIContent msgDir $ CIInvalidJSON itemContentText @@ -2248,7 +2256,7 @@ toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentT _ -> Just (CIDeleted @'CTDirect deletedTs) itemEdited' = maybe False unBI itemEdited itemForwarded = toCIForwardedFrom forwardedFromRow - in mkCIMeta itemId content itemText status (unBI <$> sentViaProxy) sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed (unBI <$> itemLive) userMention currentTs itemTs Nothing False createdAt updatedAt + in mkCIMeta itemId content itemText status (unBI <$> sentViaProxy) sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed (unBI <$> itemLive) userMention hasLink currentTs itemTs Nothing False createdAt updatedAt ciTimed :: Maybe CITimed ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} @@ -2286,7 +2294,7 @@ toGroupChatItem ( ( (itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow - :. (timedTTL, timedDeleteAt, itemLive, BI userMention) + :. (timedTTL, timedDeleteAt, itemLive, BI userMention, BI hasLink) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_) ) :. (forwardedByMember, BI showGroupAsSender) @@ -2331,7 +2339,7 @@ toGroupChatItem _ -> Just (maybe (CIDeleted @'CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_) itemEdited' = maybe False unBI itemEdited itemForwarded = toCIForwardedFrom forwardedFromRow - in mkCIMeta itemId content itemText status (unBI <$> sentViaProxy) sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed (unBI <$> itemLive) userMention currentTs itemTs forwardedByMember showGroupAsSender createdAt updatedAt + in mkCIMeta itemId content itemText status (unBI <$> sentViaProxy) sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed (unBI <$> itemLive) userMention hasLink currentTs itemTs forwardedByMember showGroupAsSender createdAt updatedAt ciTimed :: Maybe CITimed ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} @@ -2604,7 +2612,7 @@ getDirectChatItem db User {userId} contactId itemId = ExceptT $ do i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, - i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, + i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, -- 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, -- DirectQuote @@ -2959,7 +2967,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, - i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, + i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, -- 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, showGroupAsSender @@ -3070,7 +3078,7 @@ getLocalChatItem db User {userId} folderId itemId = ExceptT $ do i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, - i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, + i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, -- 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 FROM chat_items i diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 3f8cc5b64b..be76b95388 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -25,6 +25,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20251117_member_relations_vector import Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations import Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables import Simplex.Chat.Store.Postgres.Migrations.M20260108_chat_indices +import Simplex.Chat.Store.Postgres.Migrations.M20260122_has_link import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -49,7 +50,8 @@ schemaMigrations = ("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector), ("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations), ("20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables), - ("20260108_chat_indices", m20260108_chat_indices, Just down_m20260108_chat_indices) + ("20260108_chat_indices", m20260108_chat_indices, Just down_m20260108_chat_indices), + ("20260122_has_link", m20260122_has_link, Just down_m20260122_has_link) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260122_has_link.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260122_has_link.hs new file mode 100644 index 0000000000..3ee456e958 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260122_has_link.hs @@ -0,0 +1,32 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260122_has_link where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260122_has_link :: Text +m20260122_has_link = + [r| +ALTER TABLE chat_items ADD COLUMN has_link SMALLINT NOT NULL DEFAULT 0; + +UPDATE chat_items SET msg_content_tag = 'text' WHERE msg_content_tag = 'liveText'; + +UPDATE chat_items SET has_link = 1 +WHERE msg_content_tag = 'link' OR item_text LIKE '%https://%'; + +CREATE INDEX idx_chat_items_groups_has_link_item_ts ON chat_items(user_id, group_id, has_link, item_ts); +CREATE INDEX idx_chat_items_contacts_has_link_created_at ON chat_items(user_id, contact_id, has_link, created_at); +CREATE INDEX idx_chat_items_note_folder_has_link_created_at ON chat_items(user_id, note_folder_id, has_link, created_at); +|] + +down_m20260122_has_link :: Text +down_m20260122_has_link = + [r| +DROP INDEX idx_chat_items_note_folder_has_link_created_at; +DROP INDEX idx_chat_items_contacts_has_link_created_at; +DROP INDEX idx_chat_items_groups_has_link_item_ts; + +ALTER TABLE chat_items DROP COLUMN has_link; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index c37d1360d6..baf5942bc8 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -342,7 +342,8 @@ CREATE TABLE test_chat_schema.chat_items ( user_mention smallint DEFAULT 0 NOT NULL, group_scope_tag text, group_scope_group_member_id bigint, - show_group_as_sender smallint DEFAULT 0 NOT NULL + show_group_as_sender smallint DEFAULT 0 NOT NULL, + has_link smallint DEFAULT 0 NOT NULL ); @@ -1813,6 +1814,10 @@ CREATE INDEX idx_chat_items_contacts_created_at ON test_chat_schema.chat_items U +CREATE INDEX idx_chat_items_contacts_has_link_created_at ON test_chat_schema.chat_items USING btree (user_id, contact_id, has_link, created_at); + + + CREATE INDEX idx_chat_items_contacts_msg_content_tag_created_at ON test_chat_schema.chat_items USING btree (user_id, contact_id, msg_content_tag, created_at); @@ -1873,6 +1878,10 @@ CREATE INDEX idx_chat_items_groups ON test_chat_schema.chat_items USING btree (u +CREATE INDEX idx_chat_items_groups_has_link_item_ts ON test_chat_schema.chat_items USING btree (user_id, group_id, has_link, item_ts); + + + CREATE INDEX idx_chat_items_groups_history ON test_chat_schema.chat_items USING btree (user_id, group_id, include_in_history, item_deleted, item_ts, chat_item_id); @@ -1901,6 +1910,10 @@ CREATE INDEX idx_chat_items_item_status ON test_chat_schema.chat_items USING btr +CREATE INDEX idx_chat_items_note_folder_has_link_created_at ON test_chat_schema.chat_items USING btree (user_id, note_folder_id, has_link, created_at); + + + CREATE INDEX idx_chat_items_note_folder_msg_content_tag_created_at ON test_chat_schema.chat_items USING btree (user_id, note_folder_id, msg_content_tag, created_at); diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index fc0da3c04a..352ea607c0 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -148,6 +148,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector import Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations import Simplex.Chat.Store.SQLite.Migrations.M20251230_strict_tables import Simplex.Chat.Store.SQLite.Migrations.M20260108_chat_indices +import Simplex.Chat.Store.SQLite.Migrations.M20260122_has_link import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -295,7 +296,8 @@ schemaMigrations = ("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector), ("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations), ("20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables), - ("20260108_chat_indices", m20260108_chat_indices, Just down_m20260108_chat_indices) + ("20260108_chat_indices", m20260108_chat_indices, Just down_m20260108_chat_indices), + ("20260122_has_link", m20260122_has_link, Just down_m20260122_has_link) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260122_has_link.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260122_has_link.hs new file mode 100644 index 0000000000..132dc26ccf --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260122_has_link.hs @@ -0,0 +1,33 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260122_has_link where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260122_has_link :: Query +m20260122_has_link = + [sql| +UPDATE chat_items SET msg_content_tag = 'text' WHERE msg_content_tag = 'liveText'; + +UPDATE chat_items SET msg_content_tag = CAST(msg_content_tag as TEXT) WHERE typeof(msg_content_tag) = 'blob'; + +ALTER TABLE chat_items ADD COLUMN has_link INTEGER NOT NULL DEFAULT 0; + +UPDATE chat_items SET has_link = 1 +WHERE msg_content_tag = 'link' OR item_text LIKE '%https://%'; + +CREATE INDEX idx_chat_items_groups_has_link_item_ts ON chat_items(user_id, group_id, has_link, item_ts); +CREATE INDEX idx_chat_items_contacts_has_link_created_at ON chat_items(user_id, contact_id, has_link, created_at); +CREATE INDEX idx_chat_items_note_folder_has_link_created_at ON chat_items(user_id, note_folder_id, has_link, created_at); +|] + +down_m20260122_has_link :: Query +down_m20260122_has_link = + [sql| +DROP INDEX idx_chat_items_note_folder_has_link_created_at; +DROP INDEX idx_chat_items_contacts_has_link_created_at; +DROP INDEX idx_chat_items_groups_has_link_item_ts; + +ALTER TABLE chat_items DROP COLUMN has_link; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 30751ee535..6201c8b2e8 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -765,7 +765,7 @@ Query: LIMIT ? Plan: -SEARCH chat_items USING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?) +SEARCH chat_items USING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=?) USE TEMP B-TREE FOR ORDER BY Query: @@ -776,7 +776,7 @@ Query: LIMIT ? Plan: -SEARCH chat_items USING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?) +SEARCH chat_items USING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=?) USE TEMP B-TREE FOR ORDER BY Query: @@ -1088,7 +1088,7 @@ Query: i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, - i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, + i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, -- 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 FROM chat_items i @@ -1105,7 +1105,7 @@ Query: i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, - i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, + i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, -- 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, showGroupAsSender @@ -1161,7 +1161,7 @@ Query: i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, - i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, + i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, -- 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, -- DirectQuote @@ -1292,7 +1292,7 @@ Query: LIMIT ? Plan: -SEARCH chat_items USING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?) +SEARCH chat_items USING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=?) USE TEMP B-TREE FOR ORDER BY Query: @@ -4166,7 +4166,7 @@ Query: Plan: SEARCH files USING INDEX idx_files_user_id (user_id=?) LIST SUBQUERY 1 -SEARCH chat_items USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=? AND note_folder_id=?) SEARCH extra_xftp_file_descriptions USING COVERING INDEX idx_extra_xftp_file_descriptions_file_id (file_id=?) SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) SEARCH snd_files USING COVERING INDEX idx_snd_files_file_id (file_id=?) @@ -4212,12 +4212,12 @@ Query: user_id, created_by_msg_id, contact_id, group_id, group_member_id, note_folder_id, group_scope_tag, group_scope_group_member_id, -- meta item_sent, item_ts, item_content, item_content_tag, item_text, item_status, msg_content_tag, shared_msg_id, - forwarded_by_group_member_id, include_in_history, created_at, updated_at, item_live, user_mention, show_group_as_sender, timed_ttl, timed_delete_at, + forwarded_by_group_member_id, include_in_history, created_at, updated_at, item_live, user_mention, has_link, show_group_as_sender, timed_ttl, timed_delete_at, -- quote quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id, -- forwarded from fwd_from_tag, fwd_from_chat_name, fwd_from_msg_dir, fwd_from_contact_id, fwd_from_group_id, fwd_from_chat_item_id - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -5141,7 +5141,7 @@ Query: JOIN files f ON f.chat_item_id = i.chat_item_id WHERE i.user_id = ? Plan: -SEARCH i USING COVERING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?) +SEARCH i USING COVERING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=?) SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) Query: @@ -5150,7 +5150,7 @@ Query: JOIN files f ON f.chat_item_id = i.chat_item_id WHERE i.user_id = ? AND i.contact_id = ? Plan: -SEARCH i USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) +SEARCH i USING COVERING INDEX idx_chat_items_contacts_has_link_created_at (user_id=? AND contact_id=?) SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) Query: @@ -5168,7 +5168,7 @@ Query: JOIN files f ON f.chat_item_id = i.chat_item_id WHERE i.user_id = ? AND i.group_id = ? Plan: -SEARCH i USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=?) +SEARCH i USING COVERING INDEX idx_chat_items_groups_has_link_item_ts (user_id=? AND group_id=?) SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) Query: @@ -5186,7 +5186,7 @@ Query: JOIN files f ON f.chat_item_id = i.chat_item_id WHERE i.user_id = ? AND i.note_folder_id = ? Plan: -SEARCH i USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) +SEARCH i USING COVERING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=? AND note_folder_id=?) SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) Query: @@ -5469,6 +5469,10 @@ Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND contact_id = Plan: SEARCH chat_items USING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND contact_id = ? AND has_link = 1 ORDER BY created_at DESC, chat_item_id DESC LIMIT ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_has_link_created_at (user_id=? AND contact_id=? AND has_link=?) + Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND contact_id = ? AND msg_content_tag = ? ORDER BY created_at DESC, chat_item_id DESC LIMIT ? Plan: SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_msg_content_tag_created_at (user_id=? AND contact_id=? AND msg_content_tag=?) @@ -5489,6 +5493,10 @@ Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? Plan: SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=?) +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND has_link = 1 ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_has_link_item_ts (user_id=? AND group_id=? AND has_link=?) + Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND msg_content_tag = ? ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? Plan: SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_item_ts (user_id=? AND group_id=? AND msg_content_tag=?) @@ -5497,6 +5505,10 @@ Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND note_folder_i Plan: SEARCH chat_items USING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND note_folder_id = ? AND has_link = 1 ORDER BY created_at DESC, chat_item_id DESC LIMIT ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=? AND note_folder_id=? AND has_link=?) + Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND note_folder_id = ? AND msg_content_tag = ? ORDER BY created_at DESC, chat_item_id DESC LIMIT ? Plan: SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=? AND note_folder_id=? AND msg_content_tag=?) @@ -5549,7 +5561,7 @@ SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_ Query: DELETE FROM chat_items WHERE user_id = ? AND contact_id = ? Plan: -SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_has_link_created_at (user_id=? AND contact_id=?) SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?) SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) @@ -5573,7 +5585,7 @@ SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?) Query: DELETE FROM chat_items WHERE user_id = ? AND contact_id = ? AND item_content_tag != 'chatBanner' Plan: -SEARCH chat_items USING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) +SEARCH chat_items USING INDEX idx_chat_items_contacts_has_link_created_at (user_id=? AND contact_id=?) SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?) SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) @@ -5585,7 +5597,7 @@ SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?) Query: DELETE FROM chat_items WHERE user_id = ? AND group_id = ? Plan: -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_has_link_item_ts (user_id=? AND group_id=?) SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?) SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) @@ -5609,7 +5621,7 @@ SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?) Query: DELETE FROM chat_items WHERE user_id = ? AND group_id = ? AND item_content_tag != 'chatBanner' Plan: -SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=?) +SEARCH chat_items USING INDEX idx_chat_items_groups_has_link_item_ts (user_id=? AND group_id=?) SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?) SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) @@ -5621,7 +5633,7 @@ SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?) Query: DELETE FROM chat_items WHERE user_id = ? AND note_folder_id = ? Plan: -SEARCH chat_items USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=? AND note_folder_id=?) SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?) SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) @@ -5903,7 +5915,7 @@ SEARCH protocol_servers USING COVERING INDEX idx_smp_servers_user_id (user_id=?) SEARCH settings USING COVERING INDEX idx_settings_user_id (user_id=?) SEARCH commands USING COVERING INDEX idx_commands_user_id (user_id=?) SEARCH calls USING COVERING INDEX idx_calls_user_id (user_id=?) -SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=?) SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_2 (user_id=?) SEARCH user_contact_links USING COVERING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) SEARCH connections USING COVERING INDEX idx_connections_to_subscribe (user_id=?) @@ -6067,7 +6079,7 @@ Query: SELECT EXISTS (SELECT 1 FROM chat_items WHERE user_id = ? AND contact_id Plan: SCAN CONSTANT ROW SCALAR SUBQUERY 1 -SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_has_link_created_at (user_id=? AND contact_id=?) Query: SELECT accepted_at FROM operator_usage_conditions WHERE server_operator_id = ? AND conditions_commit = ? Plan: diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 83b4c7dbe6..3169ec2eca 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -440,7 +440,8 @@ CREATE TABLE chat_items( user_mention INTEGER NOT NULL DEFAULT 0, group_scope_tag TEXT, group_scope_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE CASCADE, - show_group_as_sender INTEGER NOT NULL DEFAULT 0 + show_group_as_sender INTEGER NOT NULL DEFAULT 0, + has_link INTEGER NOT NULL DEFAULT 0 ) STRICT; CREATE TABLE sqlite_sequence(name,seq); CREATE TABLE chat_item_messages( @@ -1199,6 +1200,24 @@ CREATE INDEX idx_chat_items_note_folder_msg_content_tag_created_at ON chat_items msg_content_tag, created_at ); +CREATE INDEX idx_chat_items_groups_has_link_item_ts ON chat_items( + user_id, + group_id, + has_link, + item_ts +); +CREATE INDEX idx_chat_items_contacts_has_link_created_at ON chat_items( + user_id, + contact_id, + has_link, + created_at +); +CREATE INDEX idx_chat_items_note_folder_has_link_created_at ON chat_items( + user_id, + note_folder_id, + has_link, + created_at +); CREATE TRIGGER on_group_members_insert_update_summary AFTER INSERT ON group_members FOR EACH ROW diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index 45ae34be0a..1f183910bb 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -95,6 +95,7 @@ mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup webFolder = adminUsers = [], superUsers, ownersGroup, + noAddress = False, blockedFragmentsFile = Nothing, blockedWordsFile = Nothing, blockedExtensionRules = Nothing, diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 1fd9f340b2..7217852116 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -53,7 +53,7 @@ import Simplex.Messaging.Client (ProtocolClientConfig (..)) import Simplex.Messaging.Client.Agent (defaultSMPClientAgentConfig) import Simplex.Messaging.Crypto.Ratchet (supportedE2EEncryptVRange) import qualified Simplex.Messaging.Crypto.Ratchet as CR -import Simplex.Messaging.Protocol (srvHostnamesSMPClientVersion, sndAuthKeySMPClientVersion) +import Simplex.Messaging.Protocol (sndAuthKeySMPClientVersion) import Simplex.Messaging.Server (runSMPServerBlocking) import Simplex.Messaging.Server.Env.STM (ServerConfig (..), ServerStoreCfg (..), StartOptions (..), StorePaths (..), defaultMessageExpiration, defaultIdleQueueInterval, defaultNtfExpiration, defaultInactiveClientExpiration) import Simplex.Messaging.Server.MsgStore.STM (STMMsgStore) @@ -157,7 +157,7 @@ testCoreOpts = #if !defined(dbPostgres) getTestOpts :: Bool -> ScrubbedBytes -> ChatOpts -getTestOpts maintenance dbKey = testOpts {maintenance, coreOptions = testCoreOpts {dbOptions = (dbOptions testCoreOpts) {dbKey}}} +getTestOpts maintenance dbKey = testOpts {coreOptions = testCoreOpts {maintenance, dbOptions = (dbOptions testCoreOpts) {dbKey}}} #endif termSettings :: VirtualTerminalSettings diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 3a57aea02f..2490e97af6 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -156,6 +156,8 @@ chatDirectTests = do describe "delivery receipts" $ do it "should send delivery receipts" testSendDeliveryReceipts it "should send delivery receipts depending on configuration" testConfigureDeliveryReceipts + describe "link content filter" $ do + it "filter chat by link content" testLinkContentFilter describe "negotiate connection peer chat protocol version range" $ do describe "peer version range correctly set for new connection via invitation" $ do testInvVRange supportedChatVRange supportedChatVRange @@ -1443,7 +1445,7 @@ testMaintenanceModeWithFiles ps = withXFTPServer $ do testDatabaseEncryption :: HasCallStack => TestParams -> IO () testDatabaseEncryption ps = do withNewTestChat ps "bob" bobProfile $ \bob -> do - withNewTestChatOpts ps testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do + withNewTestChatOpts ps testOpts {coreOptions = testCoreOpts {maintenance = True}} "alice" aliceProfile $ \alice -> do alice ##> "/_start" alice <## "chat started" connectUsers alice bob @@ -3311,3 +3313,43 @@ contactInfoChatVRange cc (VersionRange minVer maxVer) = do cc <## "connection not verified, use /code command to see security code" cc <## "quantum resistant end-to-end encryption" cc <## ("peer chat protocol version range: (" <> show minVer <> ", " <> show maxVer <> ")") + +testLinkContentFilter :: HasCallStack => TestParams -> IO () +testLinkContentFilter = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + alice ##> "/c" + simplexLink <- getInvitation alice + + let linkPreview = "{\"msgContent\": {\"type\": \"link\", \"text\": \"https://simplex.chat\", \"preview\": {\"uri\": \"https://simplex.chat\", \"title\": \"SimpleX Chat\", \"description\": \"SimpleX Chat\", \"image\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}" + alice ##> ("/_send @2 json [" <> linkPreview <> "]") + alice <# "@bob https://simplex.chat" + bob <# "alice> https://simplex.chat" + + alice #> "@bob check out https://example.com" + bob <# "alice> check out https://example.com" + + bob #> "@alice visit http://test.org" + alice <# "bob> visit http://test.org" + + alice #> ("@bob " <> simplexLink) + bob <#. "alice> https://simplex.chat/invitation#" + + bob #> "@alice [click here](https://link.example.com)" + alice <# "bob> [click here](https://link.example.com)" + + alice #> "@bob visit example.com for info" + bob <# "alice> visit example.com for info" + + alice #> "@bob hello, no links here" + bob <# "alice> hello, no links here" + + alice ##> "/_get content types @2" + alice <## "Chat content types: link, text" + alice #$> ("/_get chat @2 content=link count=100", chat, [(1, "https://simplex.chat"), (1, "check out https://example.com"), (0, "visit http://test.org"), (1, simplexLink), (0, "[click here](https://link.example.com)"), (1, "visit example.com for info")]) + + bob ##> "/_get content types @2" + bob <## "Chat content types: link, text" + bob #$> ("/_get chat @2 content=link count=100", chat, [(0, "https://simplex.chat"), (0, "check out https://example.com"), (1, "visit http://test.org"), (0, simplexLink), (1, "[click here](https://link.example.com)"), (0, "visit example.com for info")]) diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs index 530b85fa91..7baa12fdd4 100644 --- a/tests/ChatTests/Files.hs +++ b/tests/ChatTests/Files.hs @@ -100,6 +100,26 @@ runTestMessageWithFile = testChat2 aliceProfile bobProfile $ \alice bob -> withX bob <## "Chat content types: file, text" bob #$> ("/_get chat @2 content=file count=100", chatF, [((0, "hi, sending a file"), Just "./tests/tmp/test.jpg")]) + -- Test file with link in text - should appear in both file and link filters + alice ##> "/_send @2 json [{\"filePath\": \"./tests/fixtures/test.pdf\", \"msgContent\": {\"type\": \"file\", \"text\": \"check https://example.com for docs\"}}]" + alice <# "@bob check https://example.com for docs" + alice <# "/f @bob ./tests/fixtures/test.pdf" + alice <## "use /fc 2 to cancel sending" + bob <# "alice> check https://example.com for docs" + bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + bob <## "use /fr 2 [