android, desktop: chat preview, compose message, new chat button (#4576)

* android, desktop: chat preview, compose message, new chat button

* padding on desktop

* multiplier

* no placeholder in console

* sheet elevation zero when hidden

* divider

* padding

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Stanislav Dmitrenko
2024-08-05 05:57:23 +09:00
committed by GitHub
parent ee7c5d2aac
commit 8f1302e1c6
13 changed files with 125 additions and 71 deletions
@@ -345,6 +345,9 @@ object ChatModel {
return withContext(Dispatchers.Main) {
// update current chat
if (chatId.value == cInfo.id) {
if (cItem.isDeletedContent || cItem.meta.itemDeleted != null) {
AudioPlayer.stop(cItem)
}
val items = chatItems.value
val itemIndex = items.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) {
@@ -2176,7 +2176,10 @@ object ChatController {
r.chatItemDeletions.forEach { (deletedChatItem, toChatItem) ->
val cInfo = deletedChatItem.chatInfo
val cItem = deletedChatItem.chatItem
AudioPlayer.stop(cItem)
if (chatModel.chatId.value != null) {
// Stop voice playback only inside a chat, allow to play in a chat list
AudioPlayer.stop(cItem)
}
val isLastChatItem = chatModel.getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id
if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) {
ntfManager.cancelNotificationsForChat(cInfo.id)
@@ -14,6 +14,7 @@ expect fun PlatformTextField(
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
placeholder: String,
onMessageChange: (String) -> Unit,
onUpArrow: () -> Unit,
onFilesPasted: (List<URI>) -> Unit,
@@ -20,6 +20,8 @@ import chat.simplex.common.views.chat.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
@Composable
fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
@@ -77,29 +79,33 @@ fun TerminalLayout(
Scaffold(
topBar = { CloseSheetBar(close) },
bottomBar = {
Box(Modifier.padding(horizontal = 8.dp)) {
SendMsgView(
composeState = composeState,
showVoiceRecordIcon = false,
recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = false,
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
sendMsgEnabled = true,
sendButtonEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = false,
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {},
sendMessage = { sendCommand() },
sendLiveMessage = null,
updateLiveMessage = null,
editPrevMessage = {},
onMessageChange = ::onMessageChange,
onFilesPasted = {},
textStyle = textStyle
)
Column {
Divider()
Box(Modifier.padding(horizontal = 8.dp)) {
SendMsgView(
composeState = composeState,
showVoiceRecordIcon = false,
recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = false,
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
sendMsgEnabled = true,
sendButtonEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = false,
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {},
placeholder = "",
sendMessage = { sendCommand() },
sendLiveMessage = null,
updateLiveMessage = null,
editPrevMessage = {},
onMessageChange = ::onMessageChange,
onFilesPasted = {},
textStyle = textStyle
)
}
}
},
contentColor = LocalContentColor.current,
@@ -478,7 +478,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
if (modalBackground) {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false)
} else {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, backgroundColorState.value, true, true)
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, backgroundColorState.value, true, false)
}
}
}
@@ -593,9 +593,11 @@ fun ChatLayout(
)
) {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
val elevation = remember { derivedStateOf { if (attachmentBottomSheetState.currentValue == ModalBottomSheetValue.Hidden) 0.dp else ModalBottomSheetDefaults.Elevation } }
ModalBottomSheetLayout(
scrimColor = Color.Black.copy(alpha = 0.12F),
modifier = Modifier.navigationBarsWithImePadding(),
sheetElevation = elevation.value,
sheetContent = {
ChooseAttachmentView(
attachmentOption,
@@ -872,11 +872,9 @@ fun ComposeView(
}
}
}
Column(Modifier.background(MaterialTheme.colors.background)) {
Divider()
Row(
modifier = Modifier.background(MaterialTheme.colors.background).padding(end = 8.dp),
verticalAlignment = Alignment.Bottom,
) {
Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) {
val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership)
val attachmentClicked = if (isGroupAndProhibitedFiles) {
{
@@ -896,7 +894,7 @@ fun ComposeView(
&& !nextSendGrpInv.value
IconButton(
attachmentClicked,
Modifier.padding(bottom = if (appPlatform.isAndroid) 2.dp else with(LocalDensity.current) { 7.sp.toDp() }),
Modifier.padding(bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier),
enabled = attachmentEnabled
) {
Icon(
@@ -925,7 +923,7 @@ fun ComposeView(
snapshotFlow { recState.value }
.distinctUntilChanged()
.collect {
when(it) {
when (it) {
is RecordingState.Started -> onAudioAdded(it.filePath, it.progressMs, false)
is RecordingState.Finished -> if (it.durationMs > 300) {
onAudioAdded(it.filePath, it.durationMs, true)
@@ -1005,6 +1003,7 @@ fun ComposeView(
sendButtonColor = sendButtonColor,
timedMessageAllowed = timedMessageAllowed,
customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime,
placeholder = stringResource(MR.strings.compose_message_placeholder),
sendMessage = { ttl ->
sendMessage(ttl)
resetLinkPreview()
@@ -1022,4 +1021,5 @@ fun ComposeView(
)
}
}
}
}
@@ -15,12 +15,10 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.helpers.*
@@ -51,6 +49,7 @@ fun SendMsgView(
allowVoiceToContact: () -> Unit,
timedMessageAllowed: Boolean = false,
customDisappearingMessageTimePref: SharedPreference<Int>? = null,
placeholder: String,
sendMessage: (Int?) -> Unit,
sendLiveMessage: (suspend () -> Unit)? = null,
updateLiveMessage: (suspend () -> Unit)? = null,
@@ -62,7 +61,7 @@ fun SendMsgView(
) {
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
Box(Modifier.padding(vertical = 8.dp)) {
Box(Modifier.padding(vertical = if (appPlatform.isAndroid) 8.dp else 6.dp)) {
val cs = composeState.value
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(composeState.value.inProgress) {
@@ -80,13 +79,24 @@ fun SendMsgView(
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
cs.endLiveDisabled ||
!sendButtonEnabled
PlatformTextField(composeState, sendMsgEnabled, sendMsgButtonDisabled, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage, onFilesPasted) {
val clicksOnTextFieldDisabled = !sendMsgEnabled || cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress
PlatformTextField(
composeState,
sendMsgEnabled,
sendMsgButtonDisabled,
textStyle,
showDeleteTextButton,
userIsObserver,
if (clicksOnTextFieldDisabled) "" else placeholder,
onMessageChange,
editPrevMessage,
onFilesPasted
) {
if (!cs.inProgress) {
sendMessage(null)
}
}
// Disable clicks on text field
if (!sendMsgEnabled || cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress) {
if (clicksOnTextFieldDisabled) {
Box(
Modifier
.matchParentSize()
@@ -101,7 +111,7 @@ fun SendMsgView(
if (showDeleteTextButton.value) {
DeleteTextButton(composeState)
}
Box(Modifier.align(Alignment.BottomEnd).padding(bottom = if (appPlatform.isAndroid) 0.dp else with(LocalDensity.current) { 5.sp.toDp() } * fontSizeSqrtMultiplier)) {
Box(Modifier.align(Alignment.BottomEnd).padding(bottom = if (appPlatform.isAndroid) 0.dp else 5.sp.toDp() * fontSizeSqrtMultiplier)) {
val sendButtonSize = remember { Animatable(36f) }
val sendButtonAlpha = remember { Animatable(1f) }
val scope = rememberCoroutineScope()
@@ -564,6 +574,7 @@ fun PreviewSendMsgView() {
userCanSend = true,
allowVoiceToContact = {},
timedMessageAllowed = false,
placeholder = "",
sendMessage = {},
editPrevMessage = {},
onMessageChange = { _ -> },
@@ -599,6 +610,7 @@ fun PreviewSendMsgViewEditing() {
userCanSend = true,
allowVoiceToContact = {},
timedMessageAllowed = false,
placeholder = "",
sendMessage = {},
editPrevMessage = {},
onMessageChange = { _ -> },
@@ -634,6 +646,7 @@ fun PreviewSendMsgViewInProgress() {
userCanSend = true,
allowVoiceToContact = {},
timedMessageAllowed = false,
placeholder = "",
sendMessage = {},
editPrevMessage = {},
onMessageChange = { _ -> },
@@ -269,7 +269,7 @@ fun CIImageView(
if (!smallView && (!showDownloadButton(file?.fileStatus) || !blurred.value)) {
loadingIndicator()
} else if (smallView && file?.showStatusIconInSmallView == true) {
Box(Modifier.align(Alignment.Center)) {
Box(Modifier.matchParentSize(), contentAlignment = Alignment.Center) {
loadingIndicator()
}
}
@@ -218,25 +218,26 @@ private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableSt
if (oneHandUI.state.value) {
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
barButtons.add {
IconButton(
onClick = {
if (!stopped) {
if (!stopped) {
barButtons.add {
IconButton(
onClick = {
showNewChatSheet(oneHandUI.state)
},
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(MaterialTheme.colors.primary, shape = CircleShape)
.size(33.dp * fontSizeSqrtMultiplier)
) {
Icon(
painterResource(MR.images.ic_edit_filled),
stringResource(MR.strings.add_contact_or_create_group),
Modifier.size(sp16),
tint = Color.White
)
}
},
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, shape = CircleShape)
.size(33.dp * fontSizeSqrtMultiplier)
){
Icon(
painterResource(MR.images.ic_edit_filled),
stringResource(MR.strings.add_contact_or_create_group),
Modifier.size(sp16),
tint = if (!stopped) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onSecondary)
}
}
}
@@ -341,10 +341,12 @@ fun ChatPreviewView(
val chat = activeVoicePreview.value?.chat ?: chat
val ci = activeVoicePreview.value?.ci ?: chat.chatItems.lastOrNull()
val mc = ci?.content?.msgContent
if ((showChatPreviews && chatModelDraftChatId != chat.id) || activeVoicePreview.value != null) {
val deleted = ci?.isDeletedContent == true || ci?.meta?.itemDeleted != null
val showContentPreview = (showChatPreviews && chatModelDraftChatId != chat.id && !deleted) || activeVoicePreview.value != null
if (ci != null && showContentPreview) {
chatItemContentPreview(chat, ci)
}
if (mc !is MsgContent.MCVoice || mc.text.isNotEmpty() || chatModelDraftChatId == chat.id) {
if (mc !is MsgContent.MCVoice || !showContentPreview || mc.text.isNotEmpty() || chatModelDraftChatId == chat.id) {
Box(Modifier.offset(x = if (mc is MsgContent.MCFile) -15.sp.toDp() else 0.dp)) {
chatPreviewText()
}
@@ -361,6 +363,13 @@ fun ChatPreviewView(
}
}
}
LaunchedEffect(chatModel.deletedChats.value) {
val voicePreview = activeVoicePreview.value
// Stop voice when deleting the chat
if (chatModel.deletedChats.value.contains(chatModel.remoteHostId() to chat.id) && voicePreview?.ci != null) {
AudioPlayer.stop(voicePreview.ci)
}
}
}
Spacer(Modifier.width(8.sp.toDp()))
@@ -398,6 +398,7 @@
<string name="simplex_links_not_allowed">SimpleX links not allowed</string>
<string name="files_and_media_not_allowed">Files and media not allowed</string>
<string name="voice_messages_not_allowed">Voice messages not allowed</string>
<string name="compose_message_placeholder">Message</string>
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
<string name="image_descr">Image</string>