diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 8e13afa5c0..e6bf7b9890 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -613,7 +613,8 @@ processChatCommand = \case groupChat <- withStore (\db -> getGroupChat db user cId pagination search) pure $ CRApiChat user (AChat SCTGroup groupChat) CTLocal -> do - error "TODO: APIGetChat.CTLocal" + localChat <- withStore (\db -> getLocalChat db user cId pagination search) + pure $ CRApiChat user (AChat SCTLocal localChat) CTContactRequest -> pure $ chatCmdError (Just user) "not implemented" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" APIGetChatItems pagination search -> withUser $ \user -> do @@ -762,7 +763,12 @@ processChatCommand = \case quoteData ChatItem {chatDir = CIGroupSnd, content = CISndMsgContent qmc} membership' = pure (qmc, CIQGroupSnd, True, membership') quoteData ChatItem {chatDir = CIGroupRcv m, content = CIRcvMsgContent qmc} _ = pure (qmc, CIQGroupRcv $ Just m, False, m) quoteData _ _ = throwChatError CEInvalidQuote - CTLocal -> pure $ chatCmdError (Just user) "TODO: send local" + CTLocal -> do + nf@NoteFolder {noteFolderId} <- withStore $ \db -> getNoteFolder db user chatId + -- TODO: files, voice, etc. + msg <- createSndMessage (XMsgNew . MCSimple $ extMsgContent mc Nothing) (NoteFolderId noteFolderId) + ci <- saveSndChatItem user (CDLocalSnd nf) msg (CISndMsgContent mc) + pure $ CRNewChatItem user (AChatItem SCTLocal SMDSnd (LocalChat nf) ci) CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" where @@ -1546,7 +1552,8 @@ processChatCommand = \case let chatRef = ChatRef CTGroup gId processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage Nothing Nothing mc CTLocal -> do - error "TODO: SendMessage.CTLocal" + chatRef <- withStore $ \db -> ChatRef CTLocal <$> getNoteFolderIdByName db user name + processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage Nothing Nothing mc _ -> throwChatError $ CECommandError "not supported" SendMemberContactMessage gName mName msg -> withUser $ \user -> do (gId, mId) <- getGroupAndMemberId user gName mName @@ -2064,7 +2071,7 @@ processChatCommand = \case ChatRef cType <$> case cType of CTDirect -> withStore $ \db -> getContactIdByName db user name CTGroup -> withStore $ \db -> getGroupIdByName db user name - CTLocal -> withStore $ \db -> error "TODO: getNoteFolderIdByName db user name" + CTLocal -> withStore $ \db -> getNoteFolderIdByName db user name _ -> throwChatError $ CECommandError "not supported" checkChatStopped :: m ChatResponse -> m ChatResponse checkChatStopped a = asks agentAsync >>= readTVarIO >>= maybe a (const $ throwChatError CEChatNotStopped) @@ -5682,11 +5689,11 @@ saveSndChatItem' user cd msg@SndMessage {sharedMsgId} content ciFile quotedItem pure ciId liftIO $ mkChatItem cd ciId content ciFile quotedItem (Just sharedMsgId) itemTimed live createdAt Nothing createdAt -saveRcvChatItem :: ChatMonad m => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> CIContent 'MDRcv -> m (ChatItem c 'MDRcv) +saveRcvChatItem :: (ChatMonad m, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> CIContent 'MDRcv -> m (ChatItem c 'MDRcv) saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} brokerTs content = saveRcvChatItem' user cd msg sharedMsgId_ brokerTs content Nothing Nothing False -saveRcvChatItem' :: ChatMonad m => User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> UTCTime -> CIContent 'MDRcv -> Maybe (CIFile 'MDRcv) -> Maybe CITimed -> Bool -> m (ChatItem c 'MDRcv) +saveRcvChatItem' :: (ChatMonad m, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> UTCTime -> CIContent 'MDRcv -> Maybe (CIFile 'MDRcv) -> Maybe CITimed -> Bool -> m (ChatItem c 'MDRcv) saveRcvChatItem' user cd msg@RcvMessage {forwardedByMember} sharedMsgId_ brokerTs content ciFile itemTimed live = do createdAt <- liftIO getCurrentTime (ciId, quotedItem) <- withStore' $ \db -> do @@ -6160,6 +6167,7 @@ chatCommandP = "/_invite member contact @" *> (APISendMemberContactInvitation <$> A.decimal <*> optional (A.space *> msgContentP)), (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <*> pure Nothing <*> quotedMsg <*> msgTextP), (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <* char_ '@' <*> (Just <$> displayName) <* A.space <*> quotedMsg <*> msgTextP), + -- "/notes" $> ListNoteFolders, -- TODO -- "/_new local chat " *> (APINewLocalChat <$> A.decimal <*> jsonP), "/note folder " *> (NewNoteFolder <$> (char_ '$' *> displayName)), "/_contacts " *> (APIListContacts <$> A.decimal), diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index cab299a943..284333fdd8 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -256,6 +256,8 @@ data ChatDirection (c :: ChatType) (d :: MsgDirection) where CDDirectRcv :: Contact -> ChatDirection 'CTDirect 'MDRcv CDGroupSnd :: GroupInfo -> ChatDirection 'CTGroup 'MDSnd CDGroupRcv :: GroupInfo -> GroupMember -> ChatDirection 'CTGroup 'MDRcv + CDLocalSnd :: NoteFolder -> ChatDirection 'CTLocal 'MDSnd + CDLocalRcv :: NoteFolder -> ChatDirection 'CTLocal 'MDRcv toCIDirection :: ChatDirection c d -> CIDirection c d toCIDirection = \case @@ -263,6 +265,8 @@ toCIDirection = \case CDDirectRcv _ -> CIDirectRcv CDGroupSnd _ -> CIGroupSnd CDGroupRcv _ m -> CIGroupRcv m + CDLocalSnd _ -> CILocalSnd + CDLocalRcv _ -> CILocalRcv toChatInfo :: ChatDirection c d -> ChatInfo c toChatInfo = \case @@ -270,6 +274,8 @@ toChatInfo = \case CDDirectRcv c -> DirectChat c CDGroupSnd g -> GroupChat g CDGroupRcv g _ -> GroupChat g + CDLocalSnd l -> LocalChat l + CDLocalRcv l -> LocalChat l data NewChatItem d = NewChatItem { createdByMsgId :: Maybe MessageId, @@ -825,7 +831,7 @@ data PendingGroupMessage = PendingGroupMessage type MessageId = Int64 -data ConnOrGroupId = ConnectionId Int64 | GroupId Int64 +data ConnOrGroupId = ConnectionId Int64 | GroupId Int64 | NoteFolderId Int64 data SndMsgDelivery = SndMsgDelivery { connId :: Int64, diff --git a/src/Simplex/Chat/Migrations/M20231219_note_folders.hs b/src/Simplex/Chat/Migrations/M20231219_note_folders.hs index ad6b313514..3baf074bb3 100644 --- a/src/Simplex/Chat/Migrations/M20231219_note_folders.hs +++ b/src/Simplex/Chat/Migrations/M20231219_note_folders.hs @@ -9,7 +9,7 @@ m20231219_note_folders :: Query m20231219_note_folders = [sql| CREATE TABLE note_folders ( - notes_folder_id INTEGER PRIMARY KEY AUTOINCREMENT, + note_folder_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, display_name TEXT NOT NULL, local_display_name TEXT NOT NULL, @@ -29,7 +29,9 @@ m20231219_note_folders = local_display_name ); - ALTER TABLE chat_items ADD COLUMN notes_folder_id INTEGER DEFAULT NULL REFERENCES note_folders ON DELETE CASCADE; + ALTER TABLE chat_items ADD COLUMN note_folder_id INTEGER DEFAULT NULL REFERENCES note_folders ON DELETE CASCADE; + ALTER TABLE messages ADD COLUMN note_folder_id INTEGER DEFAULT NULL REFERENCES note_folders ON DELETE CASCADE; + ALTER TABLE chat_item_reactions ADD COLUMN note_folder_id INTEGER DEFAULT NULL REFERENCES note_folders ON DELETE CASCADE; |] down_m20231219_note_folders :: Query @@ -37,5 +39,7 @@ down_m20231219_note_folders = [sql| DROP INDEX idx_note_folders_user_id_local_display_name; DROP TABLE note_folders; -ALTER TABLE chat_items DROP COLUMN notes_folder_id; +ALTER TABLE chat_items DROP COLUMN note_folder_id; +ALTER TABLE messages DROP COLUMN note_folder_id; +ALTER TABLE chat_item_reactions DROP COLUMN note_folder_id; |] diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 81c6e59691..f6a6f62355 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -37,6 +37,7 @@ module Simplex.Chat.Store.Messages getChatPreviews, getDirectChat, getGroupChat, + getLocalChat, getDirectChatItemsLast, getAllChatItems, getAChatItem, @@ -125,6 +126,7 @@ import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Groups +import Simplex.Chat.Store.NoteFolders (getNoteFolder) import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, MsgMeta (..), UserId) @@ -168,17 +170,18 @@ createNewSndMessage db gVar connOrGroupId mkMessage = db [sql| INSERT INTO messages ( - msg_sent, chat_msg_event, msg_body, connection_id, group_id, + msg_sent, chat_msg_event, msg_body, connection_id, group_id, note_folder_id, shared_msg_id, shared_msg_id_user, created_at, updated_at - ) VALUES (?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?) |] - (MDSnd, toCMEventTag chatMsgEvent, msgBody, connId_, groupId_, sharedMsgId, Just True, createdAt, createdAt) + (MDSnd, toCMEventTag chatMsgEvent, msgBody, connId_, groupId_, folderId_, sharedMsgId, Just True, createdAt, createdAt) msgId <- insertedRowId db pure SndMessage {msgId, sharedMsgId = SharedMsgId sharedMsgId, msgBody} where - (connId_, groupId_) = case connOrGroupId of - ConnectionId connId -> (Just connId, Nothing) - GroupId groupId -> (Nothing, Just groupId) + (connId_, groupId_, folderId_) = case connOrGroupId of + ConnectionId connId -> (Just connId, Nothing, Nothing) + GroupId groupId -> (Nothing, Just groupId, Nothing) + NoteFolderId folderId -> (Nothing, Nothing, Just folderId) createSndMsgDelivery :: DB.Connection -> SndMsgDelivery -> MessageId -> IO Int64 createSndMsgDelivery db sndMsgDelivery messageId = do @@ -211,6 +214,7 @@ createNewRcvMessage db connOrGroupId NewMessage {chatMsgEvent, msgBody} sharedMs throwError $ SEDuplicateGroupMessage groupId sharedMsgId duplAuthorId duplFwdMemberId Nothing -> liftIO $ insertRcvMsg Nothing $ Just groupId Nothing -> liftIO $ insertRcvMsg Nothing $ Just groupId + NoteFolderId _folderId -> error "TODO: createNewRcvMessage.NoteFolderId" where duplicateGroupMsgMemberIds :: Int64 -> SharedMsgId -> IO (Maybe (Maybe GroupMemberId, Maybe GroupMemberId)) duplicateGroupMsgMemberIds groupId sharedMsgId = @@ -367,7 +371,7 @@ createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciCon CIQGroupRcv (Just GroupMember {memberId}) -> (Just False, Just memberId) CIQGroupRcv Nothing -> (Just False, Nothing) -createNewRcvChatItem :: DB.Connection -> User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c)) +createNewRcvChatItem :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c)) createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, forwardedByMember} sharedMsgId_ ciContent timed live itemTs createdAt = do ciId <- createNewChatItem_ db user chatDirection (Just msgId) sharedMsgId_ ciContent quoteRow timed live itemTs forwardedByMember createdAt quotedItem <- mapM (getChatItemQuote_ db user chatDirection) quotedMsg @@ -397,13 +401,13 @@ createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent q [sql| INSERT INTO chat_items ( -- user and IDs - user_id, created_by_msg_id, contact_id, group_id, group_member_id, + user_id, created_by_msg_id, contact_id, group_id, group_member_id, note_folder_id, -- meta item_sent, item_ts, item_content, item_content_tag, item_text, item_status, shared_msg_id, forwarded_by_group_member_id, created_at, updated_at, item_live, timed_ttl, timed_delete_at, -- quote quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ((userId, msgId_) :. idsRow :. itemRow :. quoteRow) ciId <- insertedRowId db @@ -412,12 +416,14 @@ createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent q where itemRow :: (SMsgDirection d, UTCTime, CIContent d, Text, Text, CIStatus d, Maybe SharedMsgId, Maybe GroupMemberId) :. (UTCTime, UTCTime, Maybe Bool) :. (Maybe Int, Maybe UTCTime) itemRow = (msgDirection @d, itemTs, ciContent, toCIContentTag ciContent, ciContentToText ciContent, ciCreateStatus ciContent, sharedMsgId, forwardedByMember) :. (createdAt, createdAt, justTrue live) :. ciTimedRow timed - idsRow :: (Maybe Int64, Maybe Int64, Maybe Int64) + idsRow :: (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64) idsRow = case chatDirection of - CDDirectRcv Contact {contactId} -> (Just contactId, Nothing, Nothing) - CDDirectSnd Contact {contactId} -> (Just contactId, Nothing, Nothing) - CDGroupRcv GroupInfo {groupId} GroupMember {groupMemberId} -> (Nothing, Just groupId, Just groupMemberId) - CDGroupSnd GroupInfo {groupId} -> (Nothing, Just groupId, Nothing) + CDDirectRcv Contact {contactId} -> (Just contactId, Nothing, Nothing, Nothing) + CDDirectSnd Contact {contactId} -> (Just contactId, Nothing, Nothing, Nothing) + CDGroupRcv GroupInfo {groupId} GroupMember {groupMemberId} -> (Nothing, Just groupId, Just groupMemberId, Nothing) + CDGroupSnd GroupInfo {groupId} -> (Nothing, Just groupId, Nothing, Nothing) + CDLocalRcv NoteFolder {noteFolderId} -> (Nothing, Nothing, Nothing, Just noteFolderId) + CDLocalSnd NoteFolder {noteFolderId} -> (Nothing, Nothing, Nothing, Just noteFolderId) ciTimedRow :: Maybe CITimed -> (Maybe Int, Maybe UTCTime) ciTimedRow (Just CITimed {ttl, deleteAt}) = (Just ttl, deleteAt) @@ -426,7 +432,7 @@ ciTimedRow _ = (Nothing, Nothing) insertChatItemMessage_ :: DB.Connection -> ChatItemId -> MessageId -> UTCTime -> IO () insertChatItemMessage_ db ciId msgId ts = DB.execute db "INSERT INTO chat_item_messages (chat_item_id, message_id, created_at, updated_at) VALUES (?,?,?,?)" (ciId, msgId, ts, ts) -getChatItemQuote_ :: DB.Connection -> User -> ChatDirection c 'MDRcv -> QuotedMsg -> IO (CIQuote c) +getChatItemQuote_ :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> QuotedMsg -> IO (CIQuote c) getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRef = MsgRef {msgId, sentAt, sent, memberId}, content} = case chatDirection of CDDirectRcv Contact {contactId} -> getDirectChatItemQuote_ contactId (not sent) @@ -514,7 +520,7 @@ getChatPreviews db user withPCC pagination query = do getChatPreview (ACPD cType cpd) = case cType of SCTDirect -> getDirectChatPreview_ db user cpd SCTGroup -> getGroupChatPreview_ db user cpd - SCTLocal -> let (LocalChatPD _ chat) = cpd in pure chat + SCTLocal -> getLocalChatPreview_ db user cpd SCTContactRequest -> let (ContactRequestPD _ chat) = cpd in pure chat SCTContactConnection -> let (ContactConnectionPD _ chat) = cpd in pure chat @@ -1042,11 +1048,88 @@ getGroupChatBefore_ db user@User {userId} g@GroupInfo {groupId} beforeChatItemId |] (userId, groupId, search, beforeChatItemTs, beforeChatItemTs, beforeChatItemId, count) -toChatItemRef :: (ChatItemId, Maybe Int64, Maybe Int64) -> Either StoreError (ChatRef, ChatItemId) +getLocalChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTLocal) +getLocalChat db user folderId pagination search_ = do + let search = fromMaybe "" search_ + nf <- getNoteFolder db user folderId + case pagination of + CPLast count -> getLocalChatLast_ db user nf count search + CPAfter afterId count -> getLocalChatAfter_ db user nf afterId count search + CPBefore beforeId count -> getLocalChatBefore_ db user nf beforeId count search + +getLocalChatLast_ :: DB.Connection -> User -> NoteFolder -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal) +getLocalChatLast_ db user@User {userId} nf@NoteFolder {noteFolderId} count search = do + let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} + chatItemIds <- liftIO getLocalChatItemIdsLast_ + chatItems <- mapM (getLocalCIWithReactions db user nf) chatItemIds + pure $ Chat (LocalChat nf) (reverse chatItems) stats + where + getLocalChatItemIdsLast_ :: IO [ChatItemId] + getLocalChatItemIdsLast_ = + map fromOnly + <$> DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%' + ORDER BY item_ts DESC, chat_item_id DESC + LIMIT ? + |] + (userId, noteFolderId, search, count) + +getLocalChatAfter_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal) +getLocalChatAfter_ db user@User {userId} nf@NoteFolder {noteFolderId} afterChatItemId count search = do + let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} + afterChatItem <- getLocalChatItem db user noteFolderId afterChatItemId + chatItemIds <- liftIO $ getLocalChatItemIdsAfter_ (chatItemTs afterChatItem) + chatItems <- mapM (getLocalCIWithReactions db user nf) chatItemIds + pure $ Chat (LocalChat nf) chatItems stats + where + getLocalChatItemIdsAfter_ :: UTCTime -> IO [ChatItemId] + getLocalChatItemIdsAfter_ afterChatItemTs = + map fromOnly + <$> DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_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, noteFolderId, search, afterChatItemTs, afterChatItemTs, afterChatItemId, count) + +getLocalChatBefore_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal) +getLocalChatBefore_ db user@User {userId} nf@NoteFolder {noteFolderId} beforeChatItemId count search = do + let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} + beforeChatItem <- getLocalChatItem db user noteFolderId beforeChatItemId + chatItemIds <- liftIO $ getLocalChatItemIdsBefore_ (chatItemTs beforeChatItem) + chatItems <- mapM (getLocalCIWithReactions db user nf) chatItemIds + pure $ Chat (LocalChat nf) (reverse chatItems) stats + where + getLocalChatItemIdsBefore_ :: UTCTime -> IO [ChatItemId] + getLocalChatItemIdsBefore_ beforeChatItemTs = + map fromOnly + <$> DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_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, noteFolderId, search, beforeChatItemTs, beforeChatItemTs, beforeChatItemId, count) + +toChatItemRef :: (ChatItemId, Maybe Int64, Maybe Int64, Maybe Int64) -> Either StoreError (ChatRef, ChatItemId) toChatItemRef = \case - (itemId, Just contactId, Nothing) -> Right (ChatRef CTDirect contactId, itemId) - (itemId, Nothing, Just groupId) -> Right (ChatRef CTGroup groupId, itemId) - (itemId, _, _) -> Left $ SEBadChatItem itemId + (itemId, Just contactId, Nothing, Nothing) -> Right (ChatRef CTDirect contactId, itemId) + (itemId, Nothing, Just groupId, Nothing) -> Right (ChatRef CTGroup groupId, itemId) + (itemId, Nothing, Nothing, Just folderId) -> Right (ChatRef CTLocal folderId, itemId) + (itemId, _, _, _) -> Left $ SEBadChatItem itemId updateDirectChatItemsRead :: DB.Connection -> User -> ContactId -> Maybe (ChatItemId, ChatItemId) -> IO () updateDirectChatItemsRead db User {userId} contactId itemsRange_ = do @@ -1279,7 +1362,7 @@ getAllChatItems db user@User {userId} pagination search_ = do <$> DB.query db [sql| - SELECT chat_item_id, contact_id, group_id + SELECT chat_item_id, contact_id, group_id, note_folder_id FROM chat_items WHERE user_id = ? AND item_text LIKE '%' || ? || '%' ORDER BY item_ts DESC, chat_item_id DESC @@ -1290,7 +1373,7 @@ getAllChatItems db user@User {userId} pagination search_ = do DB.query db [sql| - SELECT chat_item_id, contact_id, group_id + SELECT chat_item_id, contact_id, group_id, note_folder_id FROM chat_items WHERE user_id = ? AND item_text LIKE '%' || ? || '%' AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?)) @@ -1303,7 +1386,7 @@ getAllChatItems db user@User {userId} pagination search_ = do <$> DB.query db [sql| - SELECT chat_item_id, contact_id, group_id + SELECT chat_item_id, contact_id, group_id, note_folder_id FROM chat_items WHERE user_id = ? AND item_text LIKE '%' || ? || '%' AND (item_ts < ? OR (item_ts = ? AND chat_item_id < ?)) @@ -1789,6 +1872,37 @@ getGroupChatItemIdByText' db User {userId} groupId msg = |] (userId, groupId, msg <> "%") +getLocalCIWithReactions :: DB.Connection -> User -> NoteFolder -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTLocal) +getLocalCIWithReactions db user nf@NoteFolder {noteFolderId} itemId = do + liftIO . localCIWithReactions db nf =<< getLocalChatItem db user noteFolderId itemId + +localCIWithReactions :: DB.Connection -> NoteFolder -> CChatItem 'CTLocal -> IO (CChatItem 'CTLocal) +localCIWithReactions db nf cci@(CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId}}) = case itemSharedMsgId of + Just sharedMsgId -> do + reactions <- getLocalCIReactions db nf sharedMsgId + pure $ CChatItem md ci {reactions} + Nothing -> pure cci + +getLocalChatItem :: DB.Connection -> User -> Int64 -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTLocal) +getLocalChatItem db User {userId} folderId itemId = ExceptT $ do + currentTs <- getCurrentTime + firstRow' (toLocalChatItem currentTs) (SEChatItemNotFound itemId) getItem + where + getItem = + DB.query + db + [sql| + SELECT + -- ChatItem + i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, + -- CIFile + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol + FROM chat_items i + LEFT JOIN files f ON f.chat_item_id = i.chat_item_id + WHERE i.user_id = ? AND i.note_folder_id = ? AND i.chat_item_id = ? + |] + (userId, folderId, itemId) + getChatItemByFileId :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO AChatItem getChatItemByFileId db user@User {userId} fileId = do (chatRef, itemId) <- @@ -1796,7 +1910,7 @@ getChatItemByFileId db user@User {userId} fileId = do DB.query db [sql| - SELECT i.chat_item_id, i.contact_id, i.group_id + SELECT i.chat_item_id, i.contact_id, i.group_id, i.note_folder_id FROM chat_items i JOIN files f ON f.chat_item_id = i.chat_item_id WHERE f.user_id = ? AND f.file_id = ? @@ -1812,7 +1926,7 @@ getChatItemByGroupId db user@User {userId} groupId = do DB.query db [sql| - SELECT i.chat_item_id, i.contact_id, i.group_id + SELECT i.chat_item_id, i.contact_id, i.group_id, i.note_folder_id FROM chat_items i JOIN groups g ON g.chat_item_id = i.chat_item_id WHERE g.user_id = ? AND g.group_id = ? @@ -1841,6 +1955,10 @@ getAChatItem db user chatRef itemId = case chatRef of gInfo <- getGroupInfo db user groupId (CChatItem msgDir ci) <- getGroupChatItem db user groupId itemId pure $ AChatItem SCTGroup msgDir (GroupChat gInfo) ci + ChatRef CTLocal folderId -> do + nf <- getNoteFolder db user folderId + CChatItem msgDir ci <- getLocalChatItem db user folderId itemId + pure $ AChatItem SCTLocal msgDir (LocalChat nf) ci _ -> throwError $ SEChatItemNotFound itemId getChatItemVersions :: DB.Connection -> ChatItemId -> IO [ChatItemVersion] @@ -1899,6 +2017,19 @@ getGroupCIReactions db GroupInfo {groupId} itemMemberId itemSharedMsgId = |] (groupId, itemMemberId, itemSharedMsgId) +getLocalCIReactions :: DB.Connection -> NoteFolder -> SharedMsgId -> IO [CIReactionCount] +getLocalCIReactions db NoteFolder {noteFolderId} itemSharedMsgId = do + map toCIReaction + <$> DB.query + db + [sql| + SELECT reaction, MAX(reaction_sent), COUNT(chat_item_reaction_id) + FROM chat_item_reactions + WHERE note_folder_id = ? AND shared_msg_id = ? + GROUP BY reaction + |] + (noteFolderId, itemSharedMsgId) + getACIReactions :: DB.Connection -> AChatItem -> IO AChatItem getACIReactions db aci@(AChatItem _ md chat ci@ChatItem {meta = CIMeta {itemSharedMsgId}}) = case itemSharedMsgId of Just itemSharedMId -> case chat of diff --git a/src/Simplex/Chat/Store/NoteFolders.hs b/src/Simplex/Chat/Store/NoteFolders.hs index 8c8a45dd7c..1a29cecd8d 100644 --- a/src/Simplex/Chat/Store/NoteFolders.hs +++ b/src/Simplex/Chat/Store/NoteFolders.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE QuasiQuotes #-} @@ -7,10 +8,12 @@ import Control.Monad.Except (ExceptT (..), runExceptT) import Control.Monad.IO.Class (liftIO) import Data.Text (Text) import Data.Time (getCurrentTime) +import Database.SQLite.Simple (Only (..)) import Database.SQLite.Simple.QQ (sql) -import Simplex.Chat.Store.Shared (StoreError, insertedRowId, withLocalDisplayName) -import Simplex.Chat.Types (NoteFolder (..)) +import Simplex.Chat.Store.Shared (StoreError (..), insertedRowId, withLocalDisplayName) +import Simplex.Chat.Types (NoteFolder (..), NoteFolderId, NoteFolderName, User (..)) import Simplex.Messaging.Agent.Protocol (UserId) +import Simplex.Messaging.Agent.Store.SQLite (firstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB createNewNoteFolder :: DB.Connection -> UserId -> Text -> ExceptT StoreError IO NoteFolder @@ -43,3 +46,25 @@ createNewNoteFolder db userId displayName = do where favorite = False unread = False + +getNoteFolderIdByName :: DB.Connection -> User -> NoteFolderName -> ExceptT StoreError IO NoteFolderId +getNoteFolderIdByName db User {userId} ldn = + ExceptT . firstRow fromOnly (SENoteFolderNotFoundByName ldn) $ + DB.query db [sql| SELECT note_folder_id FROM note_folders WHERE user_id = ? AND local_display_name = ? |] (userId, ldn) + +getNoteFolder :: DB.Connection -> User -> NoteFolderId -> ExceptT StoreError IO NoteFolder +getNoteFolder db User {userId} noteFolderId = + ExceptT . firstRow toNoteFolder (SENoteFolderNotFound noteFolderId) $ + DB.query + db + [sql| + SELECT + display_name, local_display_name, created_at, updated_at, chat_ts, favorite, unread_chat + FROM note_folders + WHERE user_id = ? + AND note_folder_id = ? + |] + (userId, noteFolderId) + where + toNoteFolder (displayName, localDisplayName, createdAt, updatedAt, chatTs, favorite, unread) = + NoteFolder {noteFolderId, userId, displayName, localDisplayName, createdAt, updatedAt, chatTs, favorite, unread} diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index e1125adc3a..1a67771d1f 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -68,6 +68,8 @@ data StoreError | SEDuplicateGroupMember | SEGroupAlreadyJoined | SEGroupInvitationNotFound + | SENoteFolderNotFound {noteFolderId :: NoteFolderId} + | SENoteFolderNotFoundByName {noteFolderName :: NoteFolderName} | SESndFileNotFound {fileId :: FileTransferId} | SESndFileInvalid {fileId :: FileTransferId} | SERcvFileNotFound {fileId :: FileTransferId} diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 5fe95ba5d3..f7efa39fa3 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -551,7 +551,24 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {forwardedByMember}, from = ttyFromGroup g m where quote = maybe [] (groupQuote g) quotedItem - _ -> [] + LocalChat nf -> case chatDir of + CILocalSnd -> case content of + CISndMsgContent mc -> hideLive meta $ withSndFile to $ sndMsg to quote mc + CISndGroupEvent {} -> showSndItemProhibited to + _ -> showSndItem to + where + to = ttyToLocal nf + CILocalRcv -> case content of + CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from quote mc + CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta + CIRcvGroupEvent {} -> showRcvItemProhibited from + _ -> showRcvItem from + where + from = ttyFromLocal nf + where + quote = [] + ContactRequest {} -> [] + ContactConnection {} -> [] withItemDeleted item = case chatItemDeletedText ci (chatInfoMembership chat) of Nothing -> item Just t -> item <> styled (colored Red) (" [" <> t <> "]") @@ -2035,6 +2052,9 @@ ttyToGroup g = membershipIncognito g <> ttyTo ("#" <> viewGroupName g <> " ") ttyToGroupEdited :: GroupInfo -> StyledString ttyToGroupEdited g = membershipIncognito g <> ttyTo ("#" <> viewGroupName g <> " [edited] ") +ttyToLocal :: NoteFolder -> StyledString +ttyToLocal NoteFolder {localDisplayName} = ttyFrom ("$" <> localDisplayName <> " ") + ttyFromLocal :: NoteFolder -> StyledString ttyFromLocal NoteFolder {localDisplayName} = ttyFrom ("$" <> localDisplayName <> "> ") diff --git a/tests/ChatTests/Local.hs b/tests/ChatTests/Local.hs index 4ec12bbe19..7c8d118b5c 100644 --- a/tests/ChatTests/Local.hs +++ b/tests/ChatTests/Local.hs @@ -18,25 +18,25 @@ testNotes tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do alice ##> "/note folder self" alice <## "new note folder created, write to $self to add notes" - alice ##> "$self keep in mind" - alice <## "ok" + alice #> "$self keep in mind" + alice ##> "/tail" + alice <# "$self keep in mind" testUserNotes :: FilePath -> IO () testUserNotes tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do alice ##> "/note folder self" alice <## "new note folder created, write to $self to add notes" - alice ##> "$self keep in mind" - alice <## "ok" - alice ##> "/chats" - alice <## "$self keep in mind" + alice #> "$self keep in mind" + alice ##> "/tail" + alice <# "$self keep in mind" alice ##> "/create user secret" alice <## "user profile: secret" alice <## "use /p to change it" alice <## "(the updated profile will be sent to all your contacts)" - alice ##> "/note folder $gossip Do not quote me on this" + alice ##> "/note folder gossip" alice <## "new note folder created, write to $gossip to add notes" - alice ##> "/chats" + alice ##> "/tail"