core: fixes for mentions (initial chat load, update mentions, markdown) (#5603)

* core: fix mentions markdown

* test

* core: fix initial load for the first unread item

* core: fix updating messages with mentions

* fix CPP

* query plans
This commit is contained in:
Evgeny 2025-02-03 08:55:46 +00:00 committed by GitHub
parent 43e374cf20
commit 82dffd55a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 51 additions and 22 deletions

View file

@ -1793,7 +1793,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
ts@(_, ft_) = msgContentTexts mc
live = fromMaybe False live_
updateRcvChatItem = do
cci <- withStore $ \db -> getGroupChatItemBySharedMsgId db user groupId groupMemberId sharedMsgId
cci <- withStore $ \db -> getGroupChatItemBySharedMsgId db user gInfo groupMemberId sharedMsgId
case cci of
CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv m', meta = CIMeta {itemLive}, content = CIRcvMsgContent oldMC} ->
if sameMemberId memberId m'
@ -1992,9 +1992,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
receiveFileChunk ft Nothing meta chunk
xFileCancelGroup :: GroupInfo -> GroupMember -> SharedMsgId -> CM ()
xFileCancelGroup GroupInfo {groupId} GroupMember {groupMemberId, memberId} sharedMsgId = do
xFileCancelGroup g@GroupInfo {groupId} GroupMember {groupMemberId, memberId} sharedMsgId = do
fileId <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId
CChatItem msgDir ChatItem {chatDir} <- withStore $ \db -> getGroupChatItemBySharedMsgId db user groupId groupMemberId sharedMsgId
CChatItem msgDir ChatItem {chatDir} <- withStore $ \db -> getGroupChatItemBySharedMsgId db user g groupMemberId sharedMsgId
case (msgDir, chatDir) of
(SMDRcv, CIGroupRcv m) -> do
if sameMemberId memberId m

View file

@ -173,7 +173,7 @@ markdownP = mconcat <$> A.many' fragmentP
'`' -> formattedP '`' Snippet
'#' -> A.char '#' *> secretP
'!' -> coloredP <|> wordP
'@' -> mentionP
'@' -> mentionP <|> wordP
_
| isDigit c -> phoneP <|> wordP
| otherwise -> wordP

View file

@ -127,6 +127,7 @@ module Simplex.Chat.Store.Messages
)
where
import qualified Control.Exception as E
import Control.Monad
import Control.Monad.Except
import Control.Monad.IO.Class
@ -1335,8 +1336,8 @@ getGroupChatAround_ db user g contentFilter aroundId count search = do
getGroupChatAround' db user g contentFilter aroundId count search stats
getGroupChatAround' :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> 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
getGroupChatAround' db user g contentFilter aroundId count search stats = do
aroundCI <- getGroupCIWithReactions db user g aroundId
let beforeRange = GRBefore (chatItemTs aroundCI) (cChatItemId aroundCI)
afterRange = GRAfter (chatItemTs aroundCI) (cChatItemId aroundCI)
beforeIds <- liftIO $ getGroupChatItemIDs db user g contentFilter beforeRange count search
@ -2316,9 +2317,12 @@ updateGroupCIMentions db g ci@ChatItem {mentions} mentions'
unless (null mentions) $ deleteMentions
if null mentions'
then pure ci
else createGroupCIMentions db g ci mentions'
-- This is a fallback for the error that should not happen in practice.
-- In theory, it may happen in item mentions in database are different from item record.
else createMentions `E.catch` \e -> if constraintError e then deleteMentions >> createMentions else E.throwIO e
where
deleteMentions = DB.execute db "DELETE FROM chat_item_mentions WHERE chat_item_id = ?" (Only $ chatItemId' ci)
createMentions = createGroupCIMentions db g ci mentions'
deleteGroupChatItem :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> IO ()
deleteGroupChatItem db User {userId} g@GroupInfo {groupId} ci = do
@ -2439,8 +2443,8 @@ markMessageReportsDeleted db User {userId} GroupInfo {groupId} ChatItem {meta =
|]
(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
getGroupChatItemBySharedMsgId :: DB.Connection -> User -> GroupInfo -> GroupMemberId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTGroup)
getGroupChatItemBySharedMsgId db user@User {userId} g@GroupInfo {groupId} groupMemberId sharedMsgId = do
itemId <-
ExceptT . firstRow fromOnly (SEChatItemSharedMsgIdNotFound sharedMsgId) $
DB.query
@ -2453,7 +2457,7 @@ getGroupChatItemBySharedMsgId db user@User {userId} groupId groupMemberId shared
LIMIT 1
|]
(userId, groupId, groupMemberId, sharedMsgId)
getGroupChatItem db user groupId itemId
getGroupCIWithReactions db user g itemId
getGroupMemberCIBySharedMsgId :: DB.Connection -> User -> GroupId -> MemberId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTGroup)
getGroupMemberCIBySharedMsgId db user@User {userId} groupId memberId sharedMsgId = do
@ -2739,8 +2743,8 @@ getAChatItemBySharedMsgId db user cd sharedMsgId = case cd of
CDDirectRcv ct@Contact {contactId} -> do
(CChatItem msgDir ci) <- getDirectChatItemBySharedMsgId db user contactId sharedMsgId
pure $ AChatItem SCTDirect msgDir (DirectChat ct) ci
CDGroupRcv g@GroupInfo {groupId} GroupMember {groupMemberId} -> do
(CChatItem msgDir ci) <- getGroupChatItemBySharedMsgId db user groupId groupMemberId sharedMsgId
CDGroupRcv g GroupMember {groupMemberId} -> do
(CChatItem msgDir ci) <- getGroupChatItemBySharedMsgId db user g groupMemberId sharedMsgId
pure $ AChatItem SCTGroup msgDir (GroupChat g) ci
getChatItemVersions :: DB.Connection -> ChatItemId -> IO [ChatItemVersion]

View file

@ -4887,6 +4887,10 @@ Query: DELETE FROM calls WHERE user_id = ? AND contact_id = ?
Plan:
SEARCH calls USING INDEX idx_calls_contact_id (contact_id=?)
Query: DELETE FROM chat_item_mentions WHERE chat_item_id = ?
Plan:
SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?)
Query: DELETE FROM chat_item_moderations WHERE group_id = ? AND item_member_id = ? AND shared_msg_id = ?
Plan:
SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_group (group_id=? AND item_member_id=? AND shared_msg_id=?)

View file

@ -5964,7 +5964,13 @@ testMemberMention =
[ alice <# "#team cath> hello @Alice",
bob <# "#team cath> hello @Alice"
]
cath ##> "! #team hello @alice @bob"
cath ##> "! #team hello @alice" -- make it a mention
cath <# "#team [edited] hello @alice"
concurrentlyN_
[ alice <# "#team cath> [edited] hello @alice",
bob <# "#team cath> [edited] hello @alice"
]
cath ##> "! #team hello @alice @bob" -- add a mention
cath <# "#team [edited] hello @alice @bob"
concurrentlyN_
[ alice <# "#team cath> [edited] hello @alice @bob",

View file

@ -43,23 +43,26 @@ s <<== ft = T.concat (map markdownText ft) `shouldBe` s
(<<==>>) :: Text -> MarkdownList -> Expectation
s <<==>> ft = (s ==>> ft) >> (s <<== ft)
bold :: Text -> Markdown
bold = markdown Bold
textFormat :: Spec
textFormat = describe "text format (bold)" do
it "correct markdown" do
"this is *bold formatted* text"
<==> "this is " <> markdown Bold "bold formatted" <> " text"
<==> "this is " <> bold "bold formatted" <> " text"
"*bold formatted* text"
<==> markdown Bold "bold formatted" <> " text"
<==> bold "bold formatted" <> " text"
"this is *bold*"
<==> "this is " <> markdown Bold "bold"
<==> "this is " <> bold "bold"
" *bold* text"
<==> " " <> markdown Bold "bold" <> " text"
<==> " " <> bold "bold" <> " text"
" *bold* text"
<==> " " <> markdown Bold "bold" <> " text"
<==> " " <> bold "bold" <> " text"
"this is *bold* "
<==> "this is " <> markdown Bold "bold" <> " "
<==> "this is " <> bold "bold" <> " "
"this is *bold* "
<==> "this is " <> markdown Bold "bold" <> " "
<==> "this is " <> bold "bold" <> " "
it "ignored as markdown" do
"this is * unformatted * text"
<==> "this is * unformatted * text"
@ -73,9 +76,11 @@ textFormat = describe "text format (bold)" do
<==> "this is*unformatted* text"
"this is *unformatted text"
<==> "this is *unformatted text"
"*this* is *unformatted text"
<==> bold "this" <> " is *unformatted text"
it "ignored internal markdown" do
"this is *long _bold_ (not italic)* text"
<==> "this is " <> markdown Bold "long _bold_ (not italic)" <> " text"
<==> "this is " <> bold "long _bold_ (not italic)" <> " text"
"snippet: `this is *bold text*`"
<==> "snippet: " <> markdown Snippet "this is *bold text*"
@ -113,6 +118,8 @@ secretText = describe "secret text" do
<==> "this is#unformatted# text"
"this is #unformatted text"
<==> "this is #unformatted text"
"*this* is #unformatted text"
<==> bold "this" <> " is #unformatted text"
it "ignored internal markdown" do
"snippet: `this is #secret_text#`"
<==> "snippet: " <> markdown Snippet "this is #secret_text#"
@ -150,6 +157,8 @@ textColor = describe "text color (red)" do
<==> "this is!1 unformatted! text"
"this is !1 unformatted text"
<==> "this is !1 unformatted text"
"*this* is !1 unformatted text"
<==> bold "this" <> " is !1 unformatted text"
it "ignored internal markdown" do
"this is !1 long *red* (not bold)! text"
<==> "this is " <> red "long *red* (not bold)" <> " text"
@ -179,6 +188,7 @@ textWithUri = describe "text with Uri" do
it "ignored as markdown" do
"_https://simplex.chat" <==> "_https://simplex.chat"
"this is _https://simplex.chat" <==> "this is _https://simplex.chat"
"this is https://" <==> "this is https://"
it "SimpleX links" do
let inv = "/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
("https://simplex.chat" <> inv) <==> simplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] ("https://simplex.chat" <> inv)
@ -208,6 +218,7 @@ textWithEmail = describe "text with Email" do
"this is chat @simplex.chat" <==> "this is chat " <> mention "simplex.chat" "@simplex.chat"
"this is chat@ simplex.chat" <==> "this is chat@ simplex.chat"
"this is chat @ simplex.chat" <==> "this is chat @ simplex.chat"
"*this* is chat @ simplex.chat" <==> bold "this" <> " is chat @ simplex.chat"
phone :: Text -> Markdown
phone = Markdown $ Just Phone
@ -227,8 +238,9 @@ textWithPhone = describe "text with Phone" do
<==> "test " <> phone "+44 (0) 7777.777.777" <> " " <> uri "https://simplex.chat" <> " test"
it "ignored as markdown (too short)" $
"test 077777 test" <==> "test 077777 test"
it "ignored as markdown (double spaces)" $
it "ignored as markdown (double spaces)" $ do
"test 07777 777 777 test" <==> "test 07777 777 777 test"
"*test* 07777 777 777 test" <==> bold "test" <> " 07777 777 777 test"
mention :: Text -> Text -> Markdown
mention = Markdown . Just . Mention
@ -243,7 +255,10 @@ textWithMentions = describe "text with mentions" do
"hello @'alice jones'!" <==> "hello " <> mention "alice jones" "@'alice jones'" <> "!"
it "ignored as markdown" $ do
"hello @'alice jones!" <==> "hello @'alice jones!"
"hello @bob @'alice jones!" <==> "hello " <> mention "bob" "@bob" <> " @'alice jones!"
"hello @ alice!" <==> "hello @ alice!"
"hello @bob @ alice!" <==> "hello " <> mention "bob" "@bob" <> " @ alice!"
"hello @bob @" <==> "hello " <> mention "bob" "@bob" <> " @"
uri' :: Text -> FormattedText
uri' = FormattedText $ Just Uri