From f512298d10f6f01126f1edd90a62cbf105eae314 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 5 Sep 2022 17:22:28 +0300 Subject: [PATCH 1/2] Search in a list of chats (#1009) Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../app/views/chatlist/ChatListView.kt | 89 ++++++++++++------- 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index 1698cd2ca..7ea1ce4df 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -1,5 +1,6 @@ package chat.simplex.app.views.chatlist +import androidx.activity.compose.BackHandler import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -7,18 +8,20 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Report -import androidx.compose.material.icons.filled.TheaterComedy +import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.unit.dp import chat.simplex.app.R -import chat.simplex.app.model.ChatModel +import chat.simplex.app.model.* import chat.simplex.app.ui.theme.Indigo import chat.simplex.app.views.helpers.* import chat.simplex.app.views.newchat.NewChatSheet @@ -70,8 +73,9 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: LaunchedEffect(chatModel.clearOverlays.value) { if (chatModel.clearOverlays.value && scaffoldCtrl.expanded.value) scaffoldCtrl.collapse() } + var searchInList by rememberSaveable { mutableStateOf("") } BottomSheetScaffold( - topBar = { ChatListToolbar(chatModel, scaffoldCtrl, stopped) }, + topBar = { ChatListToolbar(chatModel, scaffoldCtrl, stopped) { searchInList = it.trim() } }, scaffoldState = scaffoldCtrl.state, drawerContent = { SettingsView(chatModel, setPerformLA) }, sheetPeekHeight = 0.dp, @@ -85,7 +89,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: .background(MaterialTheme.colors.background) ) { if (chatModel.chats.isNotEmpty()) { - ChatList(chatModel) + ChatList(chatModel, search = searchInList) } else { MakeConnection(chatModel) } @@ -103,9 +107,45 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: } @Composable -fun ChatListToolbar(chatModel: ChatModel, scaffoldCtrl: ScaffoldController, stopped: Boolean) { +fun ChatListToolbar(chatModel: ChatModel, scaffoldCtrl: ScaffoldController, stopped: Boolean, onSearchValueChanged: (String) -> Unit) { + var showSearch by rememberSaveable { mutableStateOf(false) } + val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false } + if (showSearch) { + BackHandler(onBack = hideSearchOnBack) + } + val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() + if (chatModel.chats.size >= 8) { + barButtons.add { + IconButton({ showSearch = true }) { + Icon(Icons.Outlined.Search, stringResource(android.R.string.search_go).capitalize(Locale.current), tint = MaterialTheme.colors.primary) + } + } + } + if (!stopped) { + barButtons.add { + IconButton(onClick = { scaffoldCtrl.toggleSheet() }) { + Icon( + Icons.Outlined.AddCircle, + stringResource(R.string.add_contact), + tint = MaterialTheme.colors.primary, + ) + } + } + } else { + barButtons.add { + IconButton(onClick = { AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_is_stopped_indication), + generalGetString(R.string.you_can_start_chat_via_setting_or_by_restarting_the_app)) }) { + Icon( + Icons.Filled.Report, + generalGetString(R.string.chat_is_stopped_indication), + tint = Color.Red, + ) + } + } + } + DefaultTopAppBar( - navigationButton = { NavigationButtonMenu { scaffoldCtrl.toggleDrawer() } }, + navigationButton = { if (showSearch) NavigationButtonBack(hideSearchOnBack) else NavigationButtonMenu { scaffoldCtrl.toggleDrawer() } }, title = { Row(verticalAlignment = Alignment.CenterVertically) { Text( @@ -124,40 +164,23 @@ fun ChatListToolbar(chatModel: ChatModel, scaffoldCtrl: ScaffoldController, stop } }, onTitleClick = null, - showSearch = false, - onSearchValueChanged = {}, - buttons = listOf{ - if (!stopped) { - IconButton(onClick = { scaffoldCtrl.toggleSheet() }) { - Icon( - Icons.Outlined.AddCircle, - stringResource(R.string.add_contact), - tint = MaterialTheme.colors.primary, - modifier = Modifier.padding(10.dp).size(26.dp) - ) - } - } else { - IconButton(onClick = { AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_is_stopped_indication), - generalGetString(R.string.you_can_start_chat_via_setting_or_by_restarting_the_app)) }) { - Icon( - Icons.Filled.Report, - generalGetString(R.string.chat_is_stopped_indication), - tint = Color.Red, - modifier = Modifier.padding(10.dp) - ) - } - } - } + showSearch = showSearch, + onSearchValueChanged = onSearchValueChanged, + buttons = barButtons ) Divider() } @Composable -fun ChatList(chatModel: ChatModel) { +fun ChatList(chatModel: ChatModel, search: String) { + val filter: (Chat) -> Boolean = { chat: Chat -> + chat.chatInfo.chatViewName.lowercase().contains(search.lowercase()) + } + val chats by remember(search) { derivedStateOf { if (search.isEmpty()) chatModel.chats else chatModel.chats.filter(filter) } } LazyColumn( modifier = Modifier.fillMaxWidth() ) { - items(chatModel.chats) { chat -> + items(chats) { chat -> ChatListNavLinkView(chat, chatModel) } } From b232b6132f0aa0268151c1b03378337c454f4f4b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 5 Sep 2022 15:23:38 +0100 Subject: [PATCH 2/2] terminal: commands to mute/unmute contacts and groups (#1018) * terminal: commands to mute/unmute contacts and groups * tests --- src/Simplex/Chat.hs | 56 +++++++++++++++++++--------------- src/Simplex/Chat/Controller.hs | 1 + src/Simplex/Chat/Help.hs | 6 ++-- src/Simplex/Chat/Store.hs | 10 +++--- src/Simplex/Chat/Types.hs | 4 +++ src/Simplex/Chat/View.hs | 23 ++++++++++---- tests/ChatTests.hs | 50 ++++++++++++++++++++++++++++++ 7 files changed, 112 insertions(+), 38 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index de5e74fa6..94d67e3f2 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -509,8 +509,8 @@ processChatCommand = \case forM_ call_ $ \call -> updateCallItemStatus userId ct call WCSDisconnected Nothing toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci pure CRCmdOk - SendCallInvitation cName callType -> withUser $ \User {userId} -> do - contactId <- withStore $ \db -> getContactIdByName db userId cName + SendCallInvitation cName callType -> withUser $ \user -> do + contactId <- withStore $ \db -> getContactIdByName db user cName processChatCommand $ APISendCallInvitation contactId callType APIRejectCall contactId -> -- party accepting call @@ -629,8 +629,14 @@ processChatCommand = \case (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m) pure $ CRGroupMemberInfo g m connectionStats - ContactInfo cName -> withUser $ \User {userId} -> do - contactId <- withStore $ \db -> getContactIdByName db userId cName + ShowMessages (ChatName cType name) ntfOn -> withUser $ \user -> do + chatId <- case cType of + CTDirect -> withStore $ \db -> getContactIdByName db user name + CTGroup -> withStore $ \db -> getGroupIdByName db user name + _ -> throwChatError $ CECommandError "not supported" + processChatCommand $ APISetChatSettings (ChatRef cType chatId) $ ChatSettings ntfOn + ContactInfo cName -> withUser $ \user -> do + contactId <- withStore $ \db -> getContactIdByName db user cName processChatCommand $ APIContactInfo contactId GroupMemberInfo gName mName -> withUser $ \user -> do (gId, mId) <- withStore $ \db -> getGroupIdByName db user gName >>= \gId -> (gId,) <$> getGroupMemberIdByName db user gId mName @@ -661,11 +667,11 @@ processChatCommand = \case ConnectSimplex -> withUser $ \User {userId, profile} -> -- [incognito] generate profile to send connectViaContact userId adminContactReq $ fromLocalProfile profile - DeleteContact cName -> withUser $ \User {userId} -> do - contactId <- withStore $ \db -> getContactIdByName db userId cName + DeleteContact cName -> withUser $ \user -> do + contactId <- withStore $ \db -> getContactIdByName db user cName processChatCommand $ APIDeleteChat (ChatRef CTDirect contactId) - ClearContact cName -> withUser $ \User {userId} -> do - contactId <- withStore $ \db -> getContactIdByName db userId cName + ClearContact cName -> withUser $ \user -> do + contactId <- withStore $ \db -> getContactIdByName db user cName processChatCommand $ APIClearChat (ChatRef CTDirect contactId) ListContacts -> withUser $ \user -> CRContactsList <$> withStore' (`getUserContacts` user) CreateMyAddress -> withUser $ \User {userId} -> withChatLock . procCmd $ do @@ -706,8 +712,8 @@ processChatCommand = \case ) `catchError` (toView . CRChatError) CRBroadcastSent mc (length cts) <$> liftIO getZonedTime - SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \User {userId} -> do - contactId <- withStore $ \db -> getContactIdByName db userId cName + SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do + contactId <- withStore $ \db -> getContactIdByName db user cName quotedItemId <- withStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir (safeDecodeUtf8 quotedMsg) let mc = MCText $ safeDecodeUtf8 msg processChatCommand . APISendMessage (ChatRef CTDirect contactId) $ ComposedMessage Nothing (Just quotedItemId) mc @@ -807,8 +813,8 @@ processChatCommand = \case withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemLeft pure $ CRLeftMemberUser gInfo {membership = membership {memberStatus = GSMemLeft}} APIListMembers groupId -> CRGroupMembers <$> withUser (\user -> withStore (\db -> getGroup db user groupId)) - AddMember gName cName memRole -> withUser $ \user@User {userId} -> do - (groupId, contactId) <- withStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db userId cName + AddMember gName cName memRole -> withUser $ \user -> do + (groupId, contactId) <- withStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName processChatCommand $ APIAddMember groupId contactId memRole JoinGroup gName -> withUser $ \user -> do groupId <- withStore $ \db -> getGroupIdByName db user gName @@ -929,9 +935,9 @@ processChatCommand = \case procCmd :: m ChatResponse -> m ChatResponse procCmd = id getChatRef :: User -> ChatName -> m ChatRef - getChatRef user@User {userId} (ChatName cType name) = + getChatRef user (ChatName cType name) = ChatRef cType <$> case cType of - CTDirect -> withStore $ \db -> getContactIdByName db userId name + CTDirect -> withStore $ \db -> getContactIdByName db user name CTGroup -> withStore $ \db -> getGroupIdByName db user name _ -> throwChatError $ CECommandError "not supported" checkChatStopped :: m ChatResponse -> m ChatResponse @@ -1715,8 +1721,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage (probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId ct void . sendDirectContactMessage ct $ XInfoProbe probe if connectedIncognito - then - withStore' $ \db -> deleteSentProbe db userId probeId + then withStore' $ \db -> deleteSentProbe db userId probeId else do cs <- withStore' $ \db -> getMatchingContacts db userId ct let probeHash = ProbeHash $ C.sha256Hash (unProbe probe) @@ -1734,14 +1739,14 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage messageError = toView . CRMessageError "error" newContentMessage :: Contact -> MsgContainer -> RcvMessage -> MsgMeta -> m () - newContentMessage ct@Contact {localDisplayName = c} mc msg msgMeta = do + newContentMessage ct@Contact {localDisplayName = c, chatSettings} mc msg msgMeta = do checkIntegrityCreateItem (CDDirectRcv ct) msgMeta let (ExtMsgContent content fileInvitation_) = mcExtMsgContent mc ciFile_ <- processFileInvitation fileInvitation_ $ \fi chSize -> withStore' $ \db -> createRcvFileTransfer db userId ct fi chSize ci@ChatItem {formattedText} <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta (CIRcvMsgContent content) ciFile_ toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci - showMsgToast (c <> "> ") content formattedText + when (enableNtfs chatSettings) $ showMsgToast (c <> "> ") content formattedText setActive $ ActiveC c processFileInvitation :: Maybe FileInvitation -> (FileInvitation -> Integer -> m RcvFileTransfer) -> m (Maybe (CIFile 'MDRcv)) @@ -1761,9 +1766,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage -- This patches initial sharedMsgId into chat item when locally deleted chat item -- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete). -- Chat item and update message which created it will have different sharedMsgId in this case... - ci@ChatItem {formattedText} <- saveRcvChatItem' user (CDDirectRcv ct) msg (Just sharedMsgId) msgMeta (CIRcvMsgContent mc) Nothing + ci <- saveRcvChatItem' user (CDDirectRcv ct) msg (Just sharedMsgId) msgMeta (CIRcvMsgContent mc) Nothing toView . CRChatItemUpdated $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci - showMsgToast (c <> "> ") mc formattedText setActive $ ActiveC c _ -> throwError e where @@ -1790,14 +1794,14 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage SMDSnd -> messageError "x.msg.del: contact attempted invalid message delete" newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> MsgMeta -> m () - newGroupContentMessage gInfo m@GroupMember {localDisplayName = c} mc msg msgMeta = do + newGroupContentMessage gInfo@GroupInfo {chatSettings} m@GroupMember {localDisplayName = c} mc msg msgMeta = do let (ExtMsgContent content fileInvitation_) = mcExtMsgContent mc ciFile_ <- processFileInvitation fileInvitation_ $ \fi chSize -> withStore' $ \db -> createRcvGroupFileTransfer db userId m fi chSize ci@ChatItem {formattedText} <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvMsgContent content) ciFile_ groupMsgToView gInfo m ci msgMeta let g = groupName' gInfo - showMsgToast ("#" <> g <> " " <> c <> "> ") content formattedText + when (enableNtfs chatSettings) $ showMsgToast ("#" <> g <> " " <> c <> "> ") content formattedText setActive $ ActiveG g groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> RcvMessage -> m () @@ -2524,7 +2528,9 @@ withStore action = do chatCommandP :: Parser ChatCommand chatCommandP = A.choice - [ ("/user " <|> "/u ") *> (CreateActiveUser <$> userProfile), + [ "/mute " *> ((`ShowMessages` False) <$> chatNameP'), + "/unmute " *> ((`ShowMessages` True) <$> chatNameP'), + ("/user " <|> "/u ") *> (CreateActiveUser <$> userProfile), ("/user" <|> "/u") $> ShowActiveUser, "/_start subscribe=" *> (StartChat <$> ("on" $> True <|> "off" $> False)), "/_start" $> StartChat True, @@ -2588,9 +2594,9 @@ chatCommandP = ("/help" <|> "/h") $> ChatHelp HSMain, ("/group #" <|> "/group " <|> "/g #" <|> "/g ") *> (NewGroup <$> groupProfile), "/_group " *> (NewGroup <$> jsonP), - ("/add #" <|> "/add " <|> "/a #" <|> "/a ") *> (AddMember <$> displayName <* A.space <*> displayName <*> memberRole), + ("/add #" <|> "/add " <|> "/a #" <|> "/a ") *> (AddMember <$> displayName <* A.space <* optional (A.char '@') <*> displayName <*> memberRole), ("/join #" <|> "/join " <|> "/j #" <|> "/j ") *> (JoinGroup <$> displayName), - ("/remove #" <|> "/remove " <|> "/rm #" <|> "/rm ") *> (RemoveMember <$> displayName <* A.space <*> displayName), + ("/remove #" <|> "/remove " <|> "/rm #" <|> "/rm ") *> (RemoveMember <$> displayName <* A.space <* optional (A.char '@') <*> displayName), ("/leave #" <|> "/leave " <|> "/l #" <|> "/l ") *> (LeaveGroup <$> displayName), ("/delete #" <|> "/d #") *> (DeleteGroup <$> displayName), ("/delete @" <|> "/delete " <|> "/d @" <|> "/d ") *> (DeleteContact <$> displayName), diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 0096ae733..c2881811c 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -154,6 +154,7 @@ data ChatCommand | APISetChatSettings ChatRef ChatSettings | APIContactInfo ContactId | APIGroupMemberInfo GroupId GroupMemberId + | ShowMessages ChatName Bool | ContactInfo ContactName | GroupMemberInfo GroupName ContactName | ChatHelp HelpSection diff --git a/src/Simplex/Chat/Help.hs b/src/Simplex/Chat/Help.hs index 7872284ae..ef4ed4991 100644 --- a/src/Simplex/Chat/Help.hs +++ b/src/Simplex/Chat/Help.hs @@ -18,7 +18,7 @@ import Data.Text (Text) import qualified Data.Text as T import Simplex.Chat.Markdown import Simplex.Chat.Styled -import Simplex.Chat.Types (User (..), LocalProfile (..)) +import Simplex.Chat.Types (LocalProfile (..), User (..)) import System.Console.ANSI.Types highlight :: Text -> Markdown @@ -195,5 +195,7 @@ settingsInfo = indent <> highlight "/network " <> " - show / set network access options", indent <> highlight "/smp_servers " <> " - show / set custom SMP servers", indent <> highlight "/info " <> " - information about contact connection", - indent <> highlight "/info # " <> " - information about member connection" + indent <> highlight "/info # " <> " - information about member connection", + indent <> highlight "/(un)mute " <> " - (un)mute contact, the last messages can be printed with /tail command", + indent <> highlight "/(un)mute # " <> " - (un)mute group" ] diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index e4391d651..fca0f85a1 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -609,9 +609,9 @@ toContactOrError ((contactId, profileId, localDisplayName, viaGroup, displayName -- TODO return the last connection that is ready, not any last connection -- requires updating connection status -getContactByName :: DB.Connection -> UserId -> ContactName -> ExceptT StoreError IO Contact -getContactByName db userId localDisplayName = do - cId <- getContactIdByName db userId localDisplayName +getContactByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Contact +getContactByName db user@User {userId} localDisplayName = do + cId <- getContactIdByName db user localDisplayName getContact db userId cId getUserContacts :: DB.Connection -> User -> IO [Contact] @@ -3012,8 +3012,8 @@ getDirectChatStats_ db userId contactId = toChatStats' [statsRow] = toChatStats statsRow toChatStats' _ = ChatStats {unreadCount = 0, minUnreadItemId = 0} -getContactIdByName :: DB.Connection -> UserId -> ContactName -> ExceptT StoreError IO Int64 -getContactIdByName db userId cName = +getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64 +getContactIdByName db User {userId} cName = ExceptT . firstRow fromOnly (SEContactNotFoundByName cName) $ DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND local_display_name = ?" (userId, cName) diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 51abca2a7..c2416cdd5 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -9,6 +9,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE UndecidableInstances #-} @@ -214,6 +215,9 @@ instance ToJSON ChatSettings where toEncoding = J.genericToEncoding J.defaultOpt defaultChatSettings :: ChatSettings defaultChatSettings = ChatSettings {enableNtfs = True} +pattern DisableNtfs :: ChatSettings +pattern DisableNtfs = ChatSettings {enableNtfs = False} + data Profile = Profile { displayName :: ContactName, fullName :: Text, diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 8e9825abb..699ac7e69 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -66,11 +66,11 @@ responseToView testView = \case CRNetworkConfig cfg -> viewNetworkConfig cfg CRContactInfo ct cStats customUserProfile -> viewContactInfo ct cStats customUserProfile CRGroupMemberInfo g m cStats -> viewGroupMemberInfo g m cStats - CRNewChatItem (AChatItem _ _ chat item) -> viewChatItem chat item False + CRNewChatItem (AChatItem _ _ chat item) -> unmuted chat item $ viewChatItem chat item False CRLastMessages chatItems -> concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True) chatItems CRChatItemStatusUpdated _ -> [] - CRChatItemUpdated (AChatItem _ _ chat item) -> viewItemUpdate chat item - CRChatItemDeleted (AChatItem _ _ chat deletedItem) (AChatItem _ _ _ toItem) -> viewItemDelete chat deletedItem toItem + CRChatItemUpdated (AChatItem _ _ chat item) -> unmuted chat item $ viewItemUpdate chat item + CRChatItemDeleted (AChatItem _ _ chat deletedItem) (AChatItem _ _ _ toItem) -> unmuted chat deletedItem $ viewItemDelete chat deletedItem toItem CRChatItemDeletedNotFound Contact {localDisplayName = c} _ -> [ttyFrom $ c <> "> [deleted - original message not found]"] CRBroadcastSent mc n ts -> viewSentBroadcast mc n ts CRMsgIntegrityError mErr -> viewMsgIntegrityError mErr @@ -207,6 +207,11 @@ responseToView testView = \case viewErrorsSummary summary s = [ttyError (T.pack . show $ length summary) <> s <> " (run with -c option to show each error)" | not (null summary)] contactList :: [ContactRef] -> String contactList cs = T.unpack . T.intercalate ", " $ map (\ContactRef {localDisplayName = n} -> "@" <> n) cs + unmuted :: ChatInfo c -> ChatItem c d -> [StyledString] -> [StyledString] + unmuted chat ChatItem {chatDir} s = case (chat, chatDir) of + (DirectChat Contact {chatSettings = DisableNtfs}, CIDirectRcv) -> [] + (GroupChat GroupInfo {chatSettings = DisableNtfs}, CIGroupRcv _) -> [] + _ -> s viewGroupSubscribed :: GroupInfo -> [StyledString] viewGroupSubscribed g@GroupInfo {membership} = @@ -383,7 +388,11 @@ viewContactsList :: [Contact] -> [StyledString] viewContactsList = let ldn = T.toLower . (localDisplayName :: Contact -> ContactName) incognito ct = if contactConnIncognito ct then incognitoPrefix else "" - in map (\ct -> incognito ct <> ttyFullContact ct) . sortOn ldn + in map (\ct -> incognito ct <> ttyFullContact ct <> muted ct) . sortOn ldn + where + muted Contact {chatSettings, localDisplayName = ldn} + | enableNtfs chatSettings = "" + | otherwise = " (muted, you can " <> highlight ("/unmute @" <> ldn) <> ")" viewUserContactLinkDeleted :: [StyledString] viewUserContactLinkDeleted = @@ -504,7 +513,7 @@ viewGroupsList [] = ["you have no groups!", "to create: " <> highlight' "/g GroupName) - groupSS g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership} = + groupSS g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership, chatSettings} = case memberStatus membership of GSMemInvited -> groupInvitation' g s -> incognito <> ttyGroup ldn <> optFullName ldn fullName <> viewMemberStatus s @@ -514,7 +523,9 @@ viewGroupsList gs = map groupSS $ sortOn ldn_ gs GSMemRemoved -> delete "you are removed" GSMemLeft -> delete "you left" GSMemGroupDeleted -> delete "group deleted" - _ -> "" + _ + | enableNtfs chatSettings -> "" + | otherwise -> " (muted, you can " <> highlight ("/unmute #" <> ldn) <> ")" delete reason = " (" <> reason <> ", delete local copy: " <> highlight ("/d #" <> ldn) <> ")" groupInvitation' :: GroupInfo -> StyledString diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 464cdd90c..d6625f8ff 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -115,6 +115,9 @@ chatTests = do describe "maintenance mode" $ do it "start/stop/export/import chat" testMaintenanceMode it "export/import chat with files" testMaintenanceModeWithFiles + describe "mute/unmute messages" $ do + it "mute/unmute contact" testMuteContact + it "mute/unmute group" testMuteGroup versionTestMatrix2 :: (TestCC -> TestCC -> IO ()) -> Spec versionTestMatrix2 runTest = do @@ -2757,6 +2760,53 @@ testMaintenanceModeWithFiles = withTmpFiles $ do -- works after full restart withTestChat "alice" $ \alice -> testChatWorking alice bob +testMuteContact :: IO () +testMuteContact = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + alice #> "@bob hello" + bob <# "alice> hello" + bob ##> "/mute alice" + bob <## "ok" + alice #> "@bob hi" + (bob "/cs" + bob <## "alice (Alice) (muted, you can /unmute @alice)" + bob ##> "/unmute alice" + bob <## "ok" + bob ##> "/cs" + bob <## "alice (Alice)" + alice #> "@bob hi again" + bob <# "alice> hi again" + +testMuteGroup :: IO () +testMuteGroup = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + threadDelay 1000000 + alice #> "#team hello!" + concurrently_ + (bob <# "#team alice> hello!") + (cath <# "#team alice> hello!") + bob ##> "/mute #team" + bob <## "ok" + alice #> "#team hi" + concurrently_ + (bob hi") + bob ##> "/gs" + bob <## "#team (muted, you can /unmute #team)" + bob ##> "/unmute #team" + bob <## "ok" + alice #> "#team hi again" + concurrently_ + (bob <# "#team alice> hi again") + (cath <# "#team alice> hi again") + bob ##> "/gs" + bob <## "#team" + withTestChatContactConnected :: String -> (TestCC -> IO a) -> IO a withTestChatContactConnected dbPrefix action = withTestChat dbPrefix $ \cc -> do