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-04 21:57:23 +01:00
committed by GitHub
co-authored by Evgeny Poberezkin
parent ee7c5d2aac
commit 8f1302e1c6
13 changed files with 125 additions and 71 deletions
@@ -11,11 +11,11 @@ import android.view.ViewGroup
import android.view.inputmethod.*
import android.widget.EditText
import android.widget.TextView
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
@@ -51,6 +51,7 @@ actual fun PlatformTextField(
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
placeholder: String,
onMessageChange: (String) -> Unit,
onUpArrow: () -> Unit,
onFilesPasted: (List<URI>) -> Unit,
@@ -58,10 +59,11 @@ actual fun PlatformTextField(
) {
val cs = composeState.value
val textColor = MaterialTheme.colors.onBackground
val padding = PaddingValues(12.dp, 7.dp, 45.dp, 0.dp)
val paddingStart = with(LocalDensity.current) { 12.dp.roundToPx() }
val hintColor = MaterialTheme.colors.secondary
val padding = PaddingValues(0.dp, 7.dp, 50.dp, 0.dp)
val paddingStart = 0
val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() }
val paddingEnd = with(LocalDensity.current) { 45.dp.roundToPx() }
val paddingEnd = with(LocalDensity.current) { 50.dp.roundToPx() }
val paddingBottom = with(LocalDensity.current) { 7.dp.roundToPx() }
var showKeyboard by remember { mutableStateOf(false) }
var freeFocus by remember { mutableStateOf(false) }
@@ -113,6 +115,8 @@ actual fun PlatformTextField(
editText.background = ColorDrawable(Color.Transparent.toArgb())
editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom)
editText.setText(cs.message)
editText.hint = placeholder
editText.setHintTextColor(hintColor.toArgb())
if (Build.VERSION.SDK_INT >= 29) {
editText.textCursorDrawable?.let { DrawableCompat.setTint(it, CurrentColors.value.colors.secondary.toArgb()) }
} else {
@@ -135,6 +139,8 @@ actual fun PlatformTextField(
editText
}) {
it.setTextColor(textColor.toArgb())
it.setHintTextColor(hintColor.toArgb())
it.hint = placeholder
it.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get()
it.isFocusable = composeState.value.preview !is ComposePreview.VoicePreview
it.isFocusableInTouchMode = it.isFocusable
@@ -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>
@@ -1,9 +1,7 @@
package chat.simplex.common.platform
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
@@ -17,8 +15,7 @@ import androidx.compose.ui.input.key.*
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.*
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import chat.simplex.common.views.chat.*
@@ -49,6 +46,7 @@ actual fun PlatformTextField(
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
placeholder: String,
onMessageChange: (String) -> Unit,
onUpArrow: () -> Unit,
onFilesPasted: (List<URI>) -> Unit,
@@ -58,7 +56,7 @@ actual fun PlatformTextField(
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
val keyboard = LocalSoftwareKeyboardController.current
val padding = PaddingValues(12.dp, 12.dp, 45.dp, 0.dp)
val padding = PaddingValues(0.dp, 12.dp, 50.dp, 0.dp)
LaunchedEffect(cs.contextItem) {
if (cs.contextItem !is ComposeContextItem.QuotedItem) return@LaunchedEffect
// In replying state
@@ -169,9 +167,20 @@ actual fun PlatformTextField(
CompositionLocalProvider(
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
) {
Column(Modifier.weight(1f).padding(start = 12.dp, end = 32.dp)) {
Column(Modifier.weight(1f).padding(start = 0.dp, end = 50.dp)) {
Spacer(Modifier.height(8.dp))
innerTextField()
TextFieldDefaults.TextFieldDecorationBox(
value = textFieldValue.text,
innerTextField = innerTextField,
placeholder = { Text(placeholder, style = textStyle.value.copy(color = MaterialTheme.colors.secondary)) },
singleLine = false,
enabled = true,
isError = false,
trailingIcon = null,
interactionSource = remember { MutableInteractionSource() },
contentPadding = PaddingValues(),
visualTransformation = VisualTransformation.None,
)
Spacer(Modifier.height(10.dp))
}
}