diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 7b216d35a4..2f7e6c2185 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1209,6 +1209,9 @@ processChatCommand = \case quotedItemId <- withStore $ \db -> getGroupChatItemIdByText db user groupId cName (safeDecodeUtf8 quotedMsg) let mc = MCText $ safeDecodeUtf8 msg processChatCommand . APISendMessage (ChatRef CTGroup groupId) False $ ComposedMessage Nothing (Just quotedItemId) mc + LastChats count_ -> withUser' $ \user -> do + chats <- withStore' $ \db -> getChatPreviews db user False + pure $ CRChats $ maybe id take count_ chats LastMessages (Just chatName) count search -> withUser $ \user -> do chatRef <- getChatRef user chatName chatResp <- processChatCommand $ APIGetChat chatRef (CPLast count) search @@ -3970,7 +3973,7 @@ chatCommandP = (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <*> pure Nothing <*> quotedMsg <*> A.takeByteString), (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <* char_ '@' <*> (Just <$> displayName) <* A.space <*> quotedMsg <*> A.takeByteString), "/_contacts " *> (APIListContacts <$> A.decimal), - ("/contacts" <|> "/cs") $> ListContacts, + "/contacts" $> ListContacts, "/_connect " *> (APIConnect <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeByteString $> Nothing)), "/_connect " *> (APIAddContact <$> A.decimal), ("/connect " <|> "/c ") *> (Connect <$> ((Just <$> strP) <|> A.takeByteString $> Nothing)), @@ -3982,6 +3985,7 @@ chatCommandP = ("\\ " <|> "\\") *> (DeleteMessage <$> chatNameP <* A.space <*> A.takeByteString), ("! " <|> "!") *> (EditMessage <$> chatNameP <* A.space <*> (quotedMsg <|> pure "") <*> A.takeByteString), "/feed " *> (SendMessageBroadcast <$> A.takeByteString), + ("/chats" <|> "/cs") *> (LastChats <$> (" all" $> Nothing <|> Just <$> (A.space *> A.decimal <|> pure 20))), ("/tail" <|> "/t") *> (LastMessages <$> optional (A.space *> chatNameP) <*> msgCountP <*> pure Nothing), ("/search" <|> "/?") *> (LastMessages <$> optional (A.space *> chatNameP) <*> msgCountP <*> (Just <$> (A.space *> stringP))), "/last_item_id" *> (LastChatItemId <$> optional (A.space *> chatNameP) <*> (A.space *> A.decimal <|> pure 0)), diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 8d016ea7d9..76fe7310e5 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -285,6 +285,7 @@ data ChatCommand | DeleteGroupLink GroupName | ShowGroupLink GroupName | SendGroupMessageQuote {groupName :: GroupName, contactName_ :: Maybe ContactName, quotedMsg :: ByteString, message :: ByteString} + | LastChats (Maybe Int) -- UserId (not used in UI) | LastMessages (Maybe ChatName) Int (Maybe String) -- UserId (not used in UI) | LastChatItemId (Maybe ChatName) Int -- UserId (not used in UI) | ShowChatItem (Maybe ChatItemId) -- UserId (not used in UI) @@ -320,6 +321,7 @@ data ChatResponse | CRChatStopped | CRChatSuspended | CRApiChats {user :: User, chats :: [AChat]} + | CRChats {chats :: [AChat]} | CRApiChat {user :: User, chat :: AChat} | CRChatItems {user :: User, chatItems :: [AChatItem]} | CRChatItemId User (Maybe ChatItemId) diff --git a/src/Simplex/Chat/Help.hs b/src/Simplex/Chat/Help.hs index 0605328127..b9650b36b8 100644 --- a/src/Simplex/Chat/Help.hs +++ b/src/Simplex/Chat/Help.hs @@ -87,7 +87,7 @@ chatHelpInfo = indent <> highlight "/help " <> " - help on: " <> listHighlight ["messages", "files", "groups", "address", "settings"], indent <> highlight "/profile " <> " - show / update user profile", indent <> highlight "/delete " <> " - delete contact and all messages with them", - indent <> highlight "/contacts " <> " - list contacts", + indent <> highlight "/chats " <> " - most recent chats", indent <> highlight "/markdown " <> " - supported markdown syntax", indent <> highlight "/version " <> " - SimpleX Chat version", indent <> highlight "/quit " <> " - quit chat", @@ -153,7 +153,11 @@ messagesHelpInfo :: [StyledString] messagesHelpInfo = map styleMarkdown - [ green "Show recent messages", + [ green "Show recent chats", + indent <> highlight "/chats [N] " <> " - the most recent N conversations (20 by default)", + indent <> highlight "/chats all " <> " - all conversations", + "", + green "Show recent messages", indent <> highlight "/tail @alice [N]" <> " - the last N messages with alice (10 by default)", indent <> highlight "/tail #team [N] " <> " - the last N messages in the group team", indent <> highlight "/tail [N] " <> " - the last N messages in all chats", diff --git a/src/Simplex/Chat/Styled.hs b/src/Simplex/Chat/Styled.hs index af11dc0177..27c7936009 100644 --- a/src/Simplex/Chat/Styled.hs +++ b/src/Simplex/Chat/Styled.hs @@ -1,5 +1,6 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} module Simplex.Chat.Styled ( StyledString (..), @@ -9,6 +10,7 @@ module Simplex.Chat.Styled unStyle, sLength, sShow, + sTake, ) where @@ -25,7 +27,7 @@ data StyledString = Styled [SGR] String | StyledString :<>: StyledString instance Semigroup StyledString where (<>) = (:<>:) -instance Monoid StyledString where mempty = plain "" +instance Monoid StyledString where mempty = "" instance IsString StyledString where fromString = plain @@ -34,7 +36,7 @@ styleMarkdown (s1 :|: s2) = styleMarkdown s1 <> styleMarkdown s2 styleMarkdown (Markdown f s) = styleFormat f s styleMarkdownList :: MarkdownList -> StyledString -styleMarkdownList [] = plain "" +styleMarkdownList [] = "" styleMarkdownList [FormattedText f s] = styleFormat f s styleMarkdownList (FormattedText f s : ts) = styleFormat f s <> styleMarkdownList ts @@ -82,3 +84,15 @@ unStyle (s1 :<>: s2) = unStyle s1 <> unStyle s2 sLength :: StyledString -> Int sLength (Styled _ s) = length s sLength (s1 :<>: s2) = sLength s1 + sLength s2 + +sTake :: Int -> StyledString -> StyledString +sTake n = go Nothing 0 + where + go res len = \case + Styled f s -> + let s' = Styled f $ take (n - len) s + in maybe id (<>) res s' + s1 :<>: s2 -> + let s1' = go res len s1 + len' = sLength s1' + in if len' >= n then s1' else go (Just s1') len' s2 diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 1edd066843..53f5c94d0b 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -65,6 +65,7 @@ responseToView user_ ChatConfig {logLevel, testView} liveItems ts = \case CRChatStopped -> ["chat stopped"] CRChatSuspended -> ["chat suspended"] CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [plain . bshow $ J.encode chats] + CRChats chats -> viewChats ts chats CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [plain . bshow $ J.encode chat] CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft] CRUserSMPServers u smpServers _ -> ttyUser u $ viewSMPServers (L.toList smpServers) testView @@ -288,6 +289,20 @@ showSMPServer = B.unpack . strEncode . host viewHostEvent :: AProtocolType -> TransportHost -> String viewHostEvent p h = map toUpper (B.unpack $ strEncode p) <> " host " <> B.unpack (strEncode h) +viewChats :: CurrentTime -> [AChat] -> [StyledString] +viewChats ts = concatMap chatPreview . reverse + where + chatPreview (AChat _ (Chat chat items _)) = case items of + CChatItem _ ci : _ -> case viewChatItem chat ci True ts of + s : _ -> [let s' = sTake 120 s in if sLength s' < sLength s then s' <> "..." else s'] + _ -> chatName + _ -> chatName + where + chatName = case chat of + DirectChat ct -> [" " <> ttyToContact' ct] + GroupChat g -> [" " <> ttyToGroup g] + _ -> [] + viewChatItem :: forall c d. MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> CurrentTime -> [StyledString] viewChatItem chat ChatItem {chatDir, meta = meta@CIMeta {itemDeleted}, content, quotedItem, file} doShow ts = withItemDeleted <$> case chat of diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 23fe332830..aba627a458 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -339,7 +339,7 @@ testDeleteContactDeletesProfile = -- bob deletes contact, profile is deleted bob ##> "/d alice" bob <## "alice: contact is deleted" - bob ##> "/cs" + bob ##> "/contacts" (bob "/cs" + alice ##> "/contacts" alice <## "bob (Bob)" alice <## "cath (Catherine)" -- remove member @@ -1558,28 +1558,28 @@ testGroupDeleteUnusedContacts = bob <## "use @cath to send messages" ] -- list contacts - bob ##> "/cs" + bob ##> "/contacts" bob <## "alice (Alice)" bob <## "cath (Catherine)" - cath ##> "/cs" + cath ##> "/contacts" cath <## "alice (Alice)" cath <## "bob (Bob)" -- delete group 1, contacts and profiles are kept deleteGroup alice bob cath "team" - bob ##> "/cs" + bob ##> "/contacts" bob <## "alice (Alice)" bob <## "cath (Catherine)" bob `hasContactProfiles` ["alice", "bob", "cath"] - cath ##> "/cs" + cath ##> "/contacts" cath <## "alice (Alice)" cath <## "bob (Bob)" cath `hasContactProfiles` ["alice", "bob", "cath"] -- delete group 2, unused contacts and profiles are deleted deleteGroup alice bob cath "club" - bob ##> "/cs" + bob ##> "/contacts" bob <## "alice (Alice)" bob `hasContactProfiles` ["alice", "bob"] - cath ##> "/cs" + cath ##> "/contacts" cath <## "alice (Alice)" cath `hasContactProfiles` ["alice", "cath"] where @@ -2921,25 +2921,25 @@ testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfi bob <## (aliceIncognito <> " updated preferences for you:") bob <## "Full deletion: off (you allow: no, contact allows: no)" -- list contacts - alice ##> "/cs" + alice ##> "/contacts" alice <### [ ConsoleString $ "i " <> bobIncognito, "cath (Catherine)" ] alice `hasContactProfiles` ["alice", T.pack aliceIncognito, T.pack bobIncognito, "cath"] - bob ##> "/cs" + bob ##> "/contacts" bob <## ("i " <> aliceIncognito) bob `hasContactProfiles` ["bob", T.pack aliceIncognito, T.pack bobIncognito] -- alice deletes contact, incognito profile is deleted alice ##> ("/d " <> bobIncognito) alice <## (bobIncognito <> ": contact is deleted") - alice ##> "/cs" + alice ##> "/contacts" alice <## "cath (Catherine)" alice `hasContactProfiles` ["alice", "cath"] -- bob deletes contact, incognito profile is deleted bob ##> ("/d " <> aliceIncognito) bob <## (aliceIncognito <> ": contact is deleted") - bob ##> "/cs" + bob ##> "/contacts" (bob "@alice I'm Batman" alice <# (bobIncognito <> "> I'm Batman") -- list contacts - bob ##> "/cs" + bob ##> "/contacts" bob <## "i alice (Alice)" bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognito] -- delete contact, incognito profile is deleted bob ##> "/d alice" bob <## "alice: contact is deleted" - bob ##> "/cs" + bob ##> "/contacts" (bob ("@" <> aliceIncognito <> " I know!") alice ?<# "bob> I know!" -- list contacts - alice ##> "/cs" + alice ##> "/contacts" alice <## "i bob (Bob)" alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognito] -- delete contact, incognito profile is deleted alice ##> "/d bob" alice <## "bob: contact is deleted" - alice ##> "/cs" + alice ##> "/contacts" (alice bobIncognito <> " joined the group")) (bob <## ("#team: you joined the group incognito as " <> bobIncognito)) - bob ##> "/cs" + bob ##> "/contacts" bob <## "i alice (Alice)" bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognito] -- delete contact bob ##> "/d alice" bob <## "alice: contact is deleted" - bob ##> "/cs" + bob ##> "/contacts" (bob bobIncognito <> " joined the group")) (bob <## ("#team: you joined the group incognito as " <> bobIncognito)) - bob ##> "/cs" + bob ##> "/contacts" bob <## "i alice (Alice)" bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognito] -- delete group @@ -3387,7 +3387,7 @@ testDeleteGroupThenContactDeletesIncognitoProfile = testChat2 aliceProfile bobPr -- delete contact bob ##> "/d alice" bob <## "alice: contact is deleted" - bob ##> "/cs" + bob ##> "/contacts" (bob do connectUsers alice bob alice #$> ("/_set alias @2 my friend bob", id, "contact bob alias updated: my friend bob") - alice ##> "/cs" + alice ##> "/contacts" alice <## "bob (Bob) (alias: my friend bob)" alice #$> ("/_set alias @2", id, "contact bob alias removed") - alice ##> "/cs" + alice ##> "/contacts" alice <## "bob (Bob)" testSetConnectionAlias :: IO () @@ -3417,7 +3417,7 @@ testSetConnectionAlias = testChat2 aliceProfile bobProfile $ (bob <## "alice (Alice): contact is connected") threadDelay 100000 alice @@@ [("@bob", "Voice messages: enabled")] - alice ##> "/cs" + alice ##> "/contacts" alice <## "bob (Bob) (alias: friend)" testSetContactPrefs :: IO () @@ -4352,11 +4352,11 @@ testMuteContact = bob <## "ok" alice #> "@bob hi" (bob "/cs" + bob ##> "/contacts" bob <## "alice (Alice) (muted, you can /unmute @alice)" bob ##> "/unmute alice" bob <## "ok" - bob ##> "/cs" + bob ##> "/contacts" bob <## "alice (Alice)" alice #> "@bob hi again" bob <# "alice> hi again" @@ -4899,16 +4899,16 @@ testGroupLinkUnusedHostContactDeleted = ] ] -- list contacts - bob ##> "/cs" + bob ##> "/contacts" bob <## "alice (Alice)" -- delete group 1, host contact and profile are kept bobLeaveDeleteGroup alice bob "team" - bob ##> "/cs" + bob ##> "/contacts" bob <## "alice (Alice)" bob `hasContactProfiles` ["alice", "bob"] -- delete group 2, unused host contact and profile are deleted bobLeaveDeleteGroup alice bob "club" - bob ##> "/cs" + bob ##> "/contacts" (bob "/cs" + bob ##> "/contacts" bob <## "i alice (Alice)" bob <## "i alice_1 (Alice)" bob `hasContactProfiles` ["alice", "alice", "bob", T.pack bobIncognitoTeam, T.pack bobIncognitoClub] -- delete group 1, unused host contact and profile are deleted bobLeaveDeleteGroup alice bob "team" bobIncognitoTeam - bob ##> "/cs" + bob ##> "/contacts" bob <## "i alice_1 (Alice)" bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognitoClub] -- delete group 2, unused host contact and profile are deleted bobLeaveDeleteGroup alice bob "club" bobIncognitoClub - bob ##> "/cs" + bob ##> "/contacts" (bob