Merge branch 'master' into sqlcipher

This commit is contained in:
Evgeny Poberezkin
2022-09-05 15:37:08 +01:00
9 changed files with 168 additions and 70 deletions

View File

@@ -1,5 +1,6 @@
package chat.simplex.app.views.chatlist package chat.simplex.app.views.chatlist
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn 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.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Report import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.filled.TheaterComedy
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.R 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.ui.theme.Indigo
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.NewChatSheet import chat.simplex.app.views.newchat.NewChatSheet
@@ -70,8 +73,9 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
LaunchedEffect(chatModel.clearOverlays.value) { LaunchedEffect(chatModel.clearOverlays.value) {
if (chatModel.clearOverlays.value && scaffoldCtrl.expanded.value) scaffoldCtrl.collapse() if (chatModel.clearOverlays.value && scaffoldCtrl.expanded.value) scaffoldCtrl.collapse()
} }
var searchInList by rememberSaveable { mutableStateOf("") }
BottomSheetScaffold( BottomSheetScaffold(
topBar = { ChatListToolbar(chatModel, scaffoldCtrl, stopped) }, topBar = { ChatListToolbar(chatModel, scaffoldCtrl, stopped) { searchInList = it.trim() } },
scaffoldState = scaffoldCtrl.state, scaffoldState = scaffoldCtrl.state,
drawerContent = { SettingsView(chatModel, setPerformLA) }, drawerContent = { SettingsView(chatModel, setPerformLA) },
sheetPeekHeight = 0.dp, sheetPeekHeight = 0.dp,
@@ -85,7 +89,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
.background(MaterialTheme.colors.background) .background(MaterialTheme.colors.background)
) { ) {
if (chatModel.chats.isNotEmpty()) { if (chatModel.chats.isNotEmpty()) {
ChatList(chatModel) ChatList(chatModel, search = searchInList)
} else { } else {
MakeConnection(chatModel) MakeConnection(chatModel)
} }
@@ -103,9 +107,45 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
} }
@Composable @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( DefaultTopAppBar(
navigationButton = { NavigationButtonMenu { scaffoldCtrl.toggleDrawer() } }, navigationButton = { if (showSearch) NavigationButtonBack(hideSearchOnBack) else NavigationButtonMenu { scaffoldCtrl.toggleDrawer() } },
title = { title = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
@@ -124,40 +164,23 @@ fun ChatListToolbar(chatModel: ChatModel, scaffoldCtrl: ScaffoldController, stop
} }
}, },
onTitleClick = null, onTitleClick = null,
showSearch = false, showSearch = showSearch,
onSearchValueChanged = {}, onSearchValueChanged = onSearchValueChanged,
buttons = listOf{ buttons = barButtons
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)
)
}
}
}
) )
Divider() Divider()
} }
@Composable @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( LazyColumn(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
items(chatModel.chats) { chat -> items(chats) { chat ->
ChatListNavLinkView(chat, chatModel) ChatListNavLinkView(chat, chatModel)
} }
} }

View File

@@ -507,8 +507,8 @@ processChatCommand = \case
forM_ call_ $ \call -> updateCallItemStatus userId ct call WCSDisconnected Nothing forM_ call_ $ \call -> updateCallItemStatus userId ct call WCSDisconnected Nothing
toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci
pure CRCmdOk pure CRCmdOk
SendCallInvitation cName callType -> withUser $ \User {userId} -> do SendCallInvitation cName callType -> withUser $ \user -> do
contactId <- withStore $ \db -> getContactIdByName db userId cName contactId <- withStore $ \db -> getContactIdByName db user cName
processChatCommand $ APISendCallInvitation contactId callType processChatCommand $ APISendCallInvitation contactId callType
APIRejectCall contactId -> APIRejectCall contactId ->
-- party accepting call -- party accepting call
@@ -627,8 +627,14 @@ processChatCommand = \case
(g, m) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId
connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m) connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m)
pure $ CRGroupMemberInfo g m connectionStats pure $ CRGroupMemberInfo g m connectionStats
ContactInfo cName -> withUser $ \User {userId} -> do ShowMessages (ChatName cType name) ntfOn -> withUser $ \user -> do
contactId <- withStore $ \db -> getContactIdByName db userId cName 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 processChatCommand $ APIContactInfo contactId
GroupMemberInfo gName mName -> withUser $ \user -> do GroupMemberInfo gName mName -> withUser $ \user -> do
(gId, mId) <- withStore $ \db -> getGroupIdByName db user gName >>= \gId -> (gId,) <$> getGroupMemberIdByName db user gId mName (gId, mId) <- withStore $ \db -> getGroupIdByName db user gName >>= \gId -> (gId,) <$> getGroupMemberIdByName db user gId mName
@@ -659,11 +665,11 @@ processChatCommand = \case
ConnectSimplex -> withUser $ \User {userId, profile} -> ConnectSimplex -> withUser $ \User {userId, profile} ->
-- [incognito] generate profile to send -- [incognito] generate profile to send
connectViaContact userId adminContactReq $ fromLocalProfile profile connectViaContact userId adminContactReq $ fromLocalProfile profile
DeleteContact cName -> withUser $ \User {userId} -> do DeleteContact cName -> withUser $ \user -> do
contactId <- withStore $ \db -> getContactIdByName db userId cName contactId <- withStore $ \db -> getContactIdByName db user cName
processChatCommand $ APIDeleteChat (ChatRef CTDirect contactId) processChatCommand $ APIDeleteChat (ChatRef CTDirect contactId)
ClearContact cName -> withUser $ \User {userId} -> do ClearContact cName -> withUser $ \user -> do
contactId <- withStore $ \db -> getContactIdByName db userId cName contactId <- withStore $ \db -> getContactIdByName db user cName
processChatCommand $ APIClearChat (ChatRef CTDirect contactId) processChatCommand $ APIClearChat (ChatRef CTDirect contactId)
ListContacts -> withUser $ \user -> CRContactsList <$> withStore' (`getUserContacts` user) ListContacts -> withUser $ \user -> CRContactsList <$> withStore' (`getUserContacts` user)
CreateMyAddress -> withUser $ \User {userId} -> withChatLock . procCmd $ do CreateMyAddress -> withUser $ \User {userId} -> withChatLock . procCmd $ do
@@ -704,8 +710,8 @@ processChatCommand = \case
) )
`catchError` (toView . CRChatError) `catchError` (toView . CRChatError)
CRBroadcastSent mc (length cts) <$> liftIO getZonedTime CRBroadcastSent mc (length cts) <$> liftIO getZonedTime
SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \User {userId} -> do SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do
contactId <- withStore $ \db -> getContactIdByName db userId cName contactId <- withStore $ \db -> getContactIdByName db user cName
quotedItemId <- withStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir (safeDecodeUtf8 quotedMsg) quotedItemId <- withStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir (safeDecodeUtf8 quotedMsg)
let mc = MCText $ safeDecodeUtf8 msg let mc = MCText $ safeDecodeUtf8 msg
processChatCommand . APISendMessage (ChatRef CTDirect contactId) $ ComposedMessage Nothing (Just quotedItemId) mc processChatCommand . APISendMessage (ChatRef CTDirect contactId) $ ComposedMessage Nothing (Just quotedItemId) mc
@@ -805,8 +811,8 @@ processChatCommand = \case
withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemLeft withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemLeft
pure $ CRLeftMemberUser gInfo {membership = membership {memberStatus = GSMemLeft}} pure $ CRLeftMemberUser gInfo {membership = membership {memberStatus = GSMemLeft}}
APIListMembers groupId -> CRGroupMembers <$> withUser (\user -> withStore (\db -> getGroup db user groupId)) APIListMembers groupId -> CRGroupMembers <$> withUser (\user -> withStore (\db -> getGroup db user groupId))
AddMember gName cName memRole -> withUser $ \user@User {userId} -> do AddMember gName cName memRole -> withUser $ \user -> do
(groupId, contactId) <- withStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db userId cName (groupId, contactId) <- withStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName
processChatCommand $ APIAddMember groupId contactId memRole processChatCommand $ APIAddMember groupId contactId memRole
JoinGroup gName -> withUser $ \user -> do JoinGroup gName -> withUser $ \user -> do
groupId <- withStore $ \db -> getGroupIdByName db user gName groupId <- withStore $ \db -> getGroupIdByName db user gName
@@ -927,9 +933,9 @@ processChatCommand = \case
procCmd :: m ChatResponse -> m ChatResponse procCmd :: m ChatResponse -> m ChatResponse
procCmd = id procCmd = id
getChatRef :: User -> ChatName -> m ChatRef getChatRef :: User -> ChatName -> m ChatRef
getChatRef user@User {userId} (ChatName cType name) = getChatRef user (ChatName cType name) =
ChatRef cType <$> case cType of 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 CTGroup -> withStore $ \db -> getGroupIdByName db user name
_ -> throwChatError $ CECommandError "not supported" _ -> throwChatError $ CECommandError "not supported"
checkChatStopped :: m ChatResponse -> m ChatResponse checkChatStopped :: m ChatResponse -> m ChatResponse
@@ -1735,14 +1741,14 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
messageError = toView . CRMessageError "error" messageError = toView . CRMessageError "error"
newContentMessage :: Contact -> MsgContainer -> RcvMessage -> MsgMeta -> m () 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 checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
let (ExtMsgContent content fileInvitation_) = mcExtMsgContent mc let (ExtMsgContent content fileInvitation_) = mcExtMsgContent mc
ciFile_ <- processFileInvitation fileInvitation_ $ ciFile_ <- processFileInvitation fileInvitation_ $
\fi chSize -> withStore' $ \db -> createRcvFileTransfer db userId ct fi chSize \fi chSize -> withStore' $ \db -> createRcvFileTransfer db userId ct fi chSize
ci@ChatItem {formattedText} <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta (CIRcvMsgContent content) ciFile_ ci@ChatItem {formattedText} <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta (CIRcvMsgContent content) ciFile_
toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci
showMsgToast (c <> "> ") content formattedText when (enableNtfs chatSettings) $ showMsgToast (c <> "> ") content formattedText
setActive $ ActiveC c setActive $ ActiveC c
processFileInvitation :: Maybe FileInvitation -> (FileInvitation -> Integer -> m RcvFileTransfer) -> m (Maybe (CIFile 'MDRcv)) processFileInvitation :: Maybe FileInvitation -> (FileInvitation -> Integer -> m RcvFileTransfer) -> m (Maybe (CIFile 'MDRcv))
@@ -1762,9 +1768,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
-- This patches initial sharedMsgId into chat item when locally deleted chat item -- 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). -- 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... -- 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 toView . CRChatItemUpdated $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci
showMsgToast (c <> "> ") mc formattedText
setActive $ ActiveC c setActive $ ActiveC c
_ -> throwError e _ -> throwError e
where where
@@ -1791,14 +1796,14 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
SMDSnd -> messageError "x.msg.del: contact attempted invalid message delete" SMDSnd -> messageError "x.msg.del: contact attempted invalid message delete"
newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> MsgMeta -> m () 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 let (ExtMsgContent content fileInvitation_) = mcExtMsgContent mc
ciFile_ <- processFileInvitation fileInvitation_ $ ciFile_ <- processFileInvitation fileInvitation_ $
\fi chSize -> withStore' $ \db -> createRcvGroupFileTransfer db userId m fi chSize \fi chSize -> withStore' $ \db -> createRcvGroupFileTransfer db userId m fi chSize
ci@ChatItem {formattedText} <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvMsgContent content) ciFile_ ci@ChatItem {formattedText} <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvMsgContent content) ciFile_
groupMsgToView gInfo m ci msgMeta groupMsgToView gInfo m ci msgMeta
let g = groupName' gInfo let g = groupName' gInfo
showMsgToast ("#" <> g <> " " <> c <> "> ") content formattedText when (enableNtfs chatSettings) $ showMsgToast ("#" <> g <> " " <> c <> "> ") content formattedText
setActive $ ActiveG g setActive $ ActiveG g
groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> RcvMessage -> m () groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> RcvMessage -> m ()
@@ -2525,7 +2530,9 @@ withStore action = do
chatCommandP :: Parser ChatCommand chatCommandP :: Parser ChatCommand
chatCommandP = chatCommandP =
A.choice A.choice
[ ("/user " <|> "/u ") *> (CreateActiveUser <$> userProfile), [ "/mute " *> ((`ShowMessages` False) <$> chatNameP'),
"/unmute " *> ((`ShowMessages` True) <$> chatNameP'),
("/user " <|> "/u ") *> (CreateActiveUser <$> userProfile),
("/user" <|> "/u") $> ShowActiveUser, ("/user" <|> "/u") $> ShowActiveUser,
"/_start subscribe=" *> (StartChat <$> ("on" $> True <|> "off" $> False)), "/_start subscribe=" *> (StartChat <$> ("on" $> True <|> "off" $> False)),
"/_start" $> StartChat True, "/_start" $> StartChat True,
@@ -2593,9 +2600,9 @@ chatCommandP =
("/help" <|> "/h") $> ChatHelp HSMain, ("/help" <|> "/h") $> ChatHelp HSMain,
("/group #" <|> "/group " <|> "/g #" <|> "/g ") *> (NewGroup <$> groupProfile), ("/group #" <|> "/group " <|> "/g #" <|> "/g ") *> (NewGroup <$> groupProfile),
"/_group " *> (NewGroup <$> jsonP), "/_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), ("/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), ("/leave #" <|> "/leave " <|> "/l #" <|> "/l ") *> (LeaveGroup <$> displayName),
("/delete #" <|> "/d #") *> (DeleteGroup <$> displayName), ("/delete #" <|> "/d #") *> (DeleteGroup <$> displayName),
("/delete @" <|> "/delete " <|> "/d @" <|> "/d ") *> (DeleteContact <$> displayName), ("/delete @" <|> "/delete " <|> "/d @" <|> "/d ") *> (DeleteContact <$> displayName),

View File

@@ -20,7 +20,7 @@ import qualified Database.SQLite3 as SQL
import Simplex.Chat.Controller import Simplex.Chat.Controller
import Simplex.Messaging.Agent.Client (agentStore) import Simplex.Messaging.Agent.Client (agentStore)
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), sqlString) import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), sqlString)
import Simplex.Messaging.Util (ifM, unlessM, whenM) import Simplex.Messaging.Util (unlessM, whenM)
import System.FilePath import System.FilePath
import UnliftIO.Directory import UnliftIO.Directory
import UnliftIO.Exception (SomeException, bracket, catch) import UnliftIO.Exception (SomeException, bracket, catch)

View File

@@ -160,6 +160,7 @@ data ChatCommand
| APISetChatSettings ChatRef ChatSettings | APISetChatSettings ChatRef ChatSettings
| APIContactInfo ContactId | APIContactInfo ContactId
| APIGroupMemberInfo GroupId GroupMemberId | APIGroupMemberInfo GroupId GroupMemberId
| ShowMessages ChatName Bool
| ContactInfo ContactName | ContactInfo ContactName
| GroupMemberInfo GroupName ContactName | GroupMemberInfo GroupName ContactName
| ChatHelp HelpSection | ChatHelp HelpSection

View File

@@ -18,7 +18,7 @@ import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import Simplex.Chat.Markdown import Simplex.Chat.Markdown
import Simplex.Chat.Styled import Simplex.Chat.Styled
import Simplex.Chat.Types (User (..), LocalProfile (..)) import Simplex.Chat.Types (LocalProfile (..), User (..))
import System.Console.ANSI.Types import System.Console.ANSI.Types
highlight :: Text -> Markdown highlight :: Text -> Markdown
@@ -195,5 +195,7 @@ settingsInfo =
indent <> highlight "/network " <> " - show / set network access options", indent <> highlight "/network " <> " - show / set network access options",
indent <> highlight "/smp_servers " <> " - show / set custom SMP servers", indent <> highlight "/smp_servers " <> " - show / set custom SMP servers",
indent <> highlight "/info <contact> " <> " - information about contact connection", indent <> highlight "/info <contact> " <> " - information about contact connection",
indent <> highlight "/info #<group> <member> " <> " - information about member connection" indent <> highlight "/info #<group> <member> " <> " - information about member connection",
indent <> highlight "/(un)mute <contact> " <> " - (un)mute contact, the last messages can be printed with /tail command",
indent <> highlight "/(un)mute #<group> " <> " - (un)mute group"
] ]

View File

@@ -613,9 +613,9 @@ toContactOrError ((contactId, profileId, localDisplayName, viaGroup, displayName
-- TODO return the last connection that is ready, not any last connection -- TODO return the last connection that is ready, not any last connection
-- requires updating connection status -- requires updating connection status
getContactByName :: DB.Connection -> UserId -> ContactName -> ExceptT StoreError IO Contact getContactByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Contact
getContactByName db userId localDisplayName = do getContactByName db user@User {userId} localDisplayName = do
cId <- getContactIdByName db userId localDisplayName cId <- getContactIdByName db user localDisplayName
getContact db userId cId getContact db userId cId
getUserContacts :: DB.Connection -> User -> IO [Contact] getUserContacts :: DB.Connection -> User -> IO [Contact]
@@ -3016,8 +3016,8 @@ getDirectChatStats_ db userId contactId =
toChatStats' [statsRow] = toChatStats statsRow toChatStats' [statsRow] = toChatStats statsRow
toChatStats' _ = ChatStats {unreadCount = 0, minUnreadItemId = 0} toChatStats' _ = ChatStats {unreadCount = 0, minUnreadItemId = 0}
getContactIdByName :: DB.Connection -> UserId -> ContactName -> ExceptT StoreError IO Int64 getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64
getContactIdByName db userId cName = getContactIdByName db User {userId} cName =
ExceptT . firstRow fromOnly (SEContactNotFoundByName cName) $ ExceptT . firstRow fromOnly (SEContactNotFoundByName cName) $
DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND local_display_name = ?" (userId, cName) DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND local_display_name = ?" (userId, cName)

View File

@@ -9,6 +9,7 @@
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
@@ -214,6 +215,9 @@ instance ToJSON ChatSettings where toEncoding = J.genericToEncoding J.defaultOpt
defaultChatSettings :: ChatSettings defaultChatSettings :: ChatSettings
defaultChatSettings = ChatSettings {enableNtfs = True} defaultChatSettings = ChatSettings {enableNtfs = True}
pattern DisableNtfs :: ChatSettings
pattern DisableNtfs = ChatSettings {enableNtfs = False}
data Profile = Profile data Profile = Profile
{ displayName :: ContactName, { displayName :: ContactName,
fullName :: Text, fullName :: Text,

View File

@@ -66,11 +66,11 @@ responseToView testView = \case
CRNetworkConfig cfg -> viewNetworkConfig cfg CRNetworkConfig cfg -> viewNetworkConfig cfg
CRContactInfo ct cStats customUserProfile -> viewContactInfo ct cStats customUserProfile CRContactInfo ct cStats customUserProfile -> viewContactInfo ct cStats customUserProfile
CRGroupMemberInfo g m cStats -> viewGroupMemberInfo g m cStats 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 CRLastMessages chatItems -> concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True) chatItems
CRChatItemStatusUpdated _ -> [] CRChatItemStatusUpdated _ -> []
CRChatItemUpdated (AChatItem _ _ chat item) -> viewItemUpdate chat item CRChatItemUpdated (AChatItem _ _ chat item) -> unmuted chat item $ viewItemUpdate chat item
CRChatItemDeleted (AChatItem _ _ chat deletedItem) (AChatItem _ _ _ toItem) -> viewItemDelete chat deletedItem toItem CRChatItemDeleted (AChatItem _ _ chat deletedItem) (AChatItem _ _ _ toItem) -> unmuted chat deletedItem $ viewItemDelete chat deletedItem toItem
CRChatItemDeletedNotFound Contact {localDisplayName = c} _ -> [ttyFrom $ c <> "> [deleted - original message not found]"] CRChatItemDeletedNotFound Contact {localDisplayName = c} _ -> [ttyFrom $ c <> "> [deleted - original message not found]"]
CRBroadcastSent mc n ts -> viewSentBroadcast mc n ts CRBroadcastSent mc n ts -> viewSentBroadcast mc n ts
CRMsgIntegrityError mErr -> viewMsgIntegrityError mErr 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)] 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 :: [ContactRef] -> String
contactList cs = T.unpack . T.intercalate ", " $ map (\ContactRef {localDisplayName = n} -> "@" <> n) cs 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 :: GroupInfo -> [StyledString]
viewGroupSubscribed g@GroupInfo {membership} = viewGroupSubscribed g@GroupInfo {membership} =
@@ -383,7 +388,11 @@ viewContactsList :: [Contact] -> [StyledString]
viewContactsList = viewContactsList =
let ldn = T.toLower . (localDisplayName :: Contact -> ContactName) let ldn = T.toLower . (localDisplayName :: Contact -> ContactName)
incognito ct = if contactConnIncognito ct then incognitoPrefix else "" 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 :: [StyledString]
viewUserContactLinkDeleted = viewUserContactLinkDeleted =
@@ -504,7 +513,7 @@ viewGroupsList [] = ["you have no groups!", "to create: " <> highlight' "/g <nam
viewGroupsList gs = map groupSS $ sortOn ldn_ gs viewGroupsList gs = map groupSS $ sortOn ldn_ gs
where where
ldn_ = T.toLower . (localDisplayName :: GroupInfo -> GroupName) ldn_ = T.toLower . (localDisplayName :: GroupInfo -> GroupName)
groupSS g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership} = groupSS g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership, chatSettings} =
case memberStatus membership of case memberStatus membership of
GSMemInvited -> groupInvitation' g GSMemInvited -> groupInvitation' g
s -> incognito <> ttyGroup ldn <> optFullName ldn fullName <> viewMemberStatus s 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" GSMemRemoved -> delete "you are removed"
GSMemLeft -> delete "you left" GSMemLeft -> delete "you left"
GSMemGroupDeleted -> delete "group deleted" GSMemGroupDeleted -> delete "group deleted"
_ -> "" _
| enableNtfs chatSettings -> ""
| otherwise -> " (muted, you can " <> highlight ("/unmute #" <> ldn) <> ")"
delete reason = " (" <> reason <> ", delete local copy: " <> highlight ("/d #" <> ldn) <> ")" delete reason = " (" <> reason <> ", delete local copy: " <> highlight ("/d #" <> ldn) <> ")"
groupInvitation' :: GroupInfo -> StyledString groupInvitation' :: GroupInfo -> StyledString

View File

@@ -116,6 +116,9 @@ chatTests = do
it "start/stop/export/import chat" testMaintenanceMode it "start/stop/export/import chat" testMaintenanceMode
it "export/import chat with files" testMaintenanceModeWithFiles it "export/import chat with files" testMaintenanceModeWithFiles
it "encrypt/decrypt database" testDatabaseEncryption it "encrypt/decrypt database" testDatabaseEncryption
describe "mute/unmute messages" $ do
it "mute/unmute contact" testMuteContact
it "mute/unmute group" testMuteGroup
versionTestMatrix2 :: (TestCC -> TestCC -> IO ()) -> Spec versionTestMatrix2 :: (TestCC -> TestCC -> IO ()) -> Spec
versionTestMatrix2 runTest = do versionTestMatrix2 runTest = do
@@ -2797,6 +2800,53 @@ testDatabaseEncryption = withTmpFiles $ do
alice <## "ok" alice <## "ok"
withTestChat "alice" $ \alice -> testChatWorking alice bob 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 </)
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 </)
(cath <# "#team alice> 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 :: String -> (TestCC -> IO a) -> IO a
withTestChatContactConnected dbPrefix action = withTestChatContactConnected dbPrefix action =
withTestChat dbPrefix $ \cc -> do withTestChat dbPrefix $ \cc -> do