Merge branch 'master' into sqlcipher
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user