mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-02-02 20:02:26 -05:00
feat(ui): move quick chat menu to topBar (#2535)
This commit is contained in:
@@ -46,6 +46,7 @@ import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Chat
|
||||
import androidx.compose.material.icons.automirrored.filled.Reply
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
@@ -53,6 +54,7 @@ import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.SelectAll
|
||||
import androidx.compose.material.icons.filled.SpeakerNotesOff
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -101,9 +103,9 @@ import com.geeksville.mesh.ui.common.theme.AppTheme
|
||||
import com.geeksville.mesh.ui.node.components.NodeKeyStatusIcon
|
||||
import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
||||
import com.geeksville.mesh.ui.sharing.SharedContactDialog
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
private const val MESSAGE_CHARACTER_LIMIT_BYTES = 200
|
||||
private const val SNIPPET_CHARACTER_LIMIT = 50
|
||||
@@ -136,12 +138,8 @@ internal fun MessageScreen(
|
||||
val ourNode by viewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
val isConnected by viewModel.isConnected.collectAsStateWithLifecycle(initialValue = false)
|
||||
val channels by viewModel.channels.collectAsStateWithLifecycle()
|
||||
val quickChatActions by
|
||||
viewModel.quickChatActions.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val messages by
|
||||
viewModel
|
||||
.getMessagesFrom(contactKey)
|
||||
.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val quickChatActions by viewModel.quickChatActions.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val messages by viewModel.getMessagesFrom(contactKey).collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
|
||||
// UI State managed within this Composable
|
||||
var replyingTo by rememberSaveable { mutableStateOf<Message?>(null) }
|
||||
@@ -149,14 +147,14 @@ internal fun MessageScreen(
|
||||
var sharedContact by rememberSaveable { mutableStateOf<Node?>(null) }
|
||||
val selectedMessageIds = rememberSaveable { mutableStateOf(emptySet<Long>()) }
|
||||
val messageInputState = rememberTextFieldState(message)
|
||||
var showQuickChat by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
// Derived state, memoized for performance
|
||||
val channelInfo =
|
||||
remember(contactKey, channels) {
|
||||
val index = contactKey.firstOrNull()?.digitToIntOrNull()
|
||||
val id = contactKey.substring(1)
|
||||
val name =
|
||||
index?.let { channels.getChannel(it)?.name } // channels can be null initially
|
||||
val name = index?.let { channels.getChannel(it)?.name } // channels can be null initially
|
||||
Triple(index, id, name)
|
||||
}
|
||||
val (channelIndex, nodeId, rawChannelName) = channelInfo
|
||||
@@ -180,8 +178,7 @@ internal fun MessageScreen(
|
||||
|
||||
val listState =
|
||||
rememberLazyListState(
|
||||
initialFirstVisibleItemIndex =
|
||||
remember(messages) { messages.indexOfLast { !it.read }.coerceAtLeast(0) }
|
||||
initialFirstVisibleItemIndex = remember(messages) { messages.indexOfLast { !it.read }.coerceAtLeast(0) },
|
||||
)
|
||||
|
||||
val onEvent: (MessageScreenEvent) -> Unit =
|
||||
@@ -227,13 +224,10 @@ internal fun MessageScreen(
|
||||
|
||||
is MessageScreenEvent.SetTitle -> viewModel.setTitle(event.title)
|
||||
is MessageScreenEvent.NavigateToMessages -> navigateToMessages(event.contactKey)
|
||||
is MessageScreenEvent.NavigateToNodeDetails ->
|
||||
navigateToNodeDetails(event.nodeNum)
|
||||
is MessageScreenEvent.NavigateToNodeDetails -> navigateToNodeDetails(event.nodeNum)
|
||||
MessageScreenEvent.NavigateBack -> onNavigateBack()
|
||||
is MessageScreenEvent.CopyToClipboard -> {
|
||||
clipboardManager.nativeClipboard.setPrimaryClip(
|
||||
ClipData.newPlainText(event.text, event.text)
|
||||
)
|
||||
clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text))
|
||||
selectedMessageIds.value = emptySet()
|
||||
}
|
||||
}
|
||||
@@ -243,16 +237,12 @@ internal fun MessageScreen(
|
||||
if (showDeleteDialog) {
|
||||
DeleteMessageDialog(
|
||||
count = selectedMessageIds.value.size,
|
||||
onConfirm = {
|
||||
onEvent(MessageScreenEvent.DeleteMessages(selectedMessageIds.value.toList()))
|
||||
},
|
||||
onConfirm = { onEvent(MessageScreenEvent.DeleteMessages(selectedMessageIds.value.toList())) },
|
||||
onDismiss = { showDeleteDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
sharedContact?.let { contact ->
|
||||
SharedContactDialog(contact = contact, onDismiss = { sharedContact = null })
|
||||
}
|
||||
sharedContact?.let { contact -> SharedContactDialog(contact = contact, onDismiss = { sharedContact = null }) }
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@@ -291,6 +281,8 @@ internal fun MessageScreen(
|
||||
onNavigateBack = { onEvent(MessageScreenEvent.NavigateBack) },
|
||||
channels = channels,
|
||||
channelIndexParam = channelIndex,
|
||||
showQuickChat = showQuickChat,
|
||||
onToggleQuickChat = { showQuickChat = !showQuickChat },
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -302,40 +294,32 @@ internal fun MessageScreen(
|
||||
listState = listState,
|
||||
messages = messages,
|
||||
selectedIds = selectedMessageIds,
|
||||
onUnreadChanged = { messageId ->
|
||||
onEvent(MessageScreenEvent.ClearUnreadCount(messageId))
|
||||
},
|
||||
onSendReaction = { emoji, id ->
|
||||
onEvent(MessageScreenEvent.SendReaction(emoji, id))
|
||||
},
|
||||
onUnreadChanged = { messageId -> onEvent(MessageScreenEvent.ClearUnreadCount(messageId)) },
|
||||
onSendReaction = { emoji, id -> onEvent(MessageScreenEvent.SendReaction(emoji, id)) },
|
||||
viewModel = viewModel,
|
||||
contactKey = contactKey,
|
||||
onReply = { message -> replyingTo = message },
|
||||
onNodeMenuAction = { action ->
|
||||
onEvent(MessageScreenEvent.HandleNodeMenuAction(action))
|
||||
},
|
||||
onNodeMenuAction = { action -> onEvent(MessageScreenEvent.HandleNodeMenuAction(action)) },
|
||||
)
|
||||
// Show FAB if we can scroll towards the newest messages (index 0).
|
||||
if (listState.canScrollBackward) {
|
||||
ScrollToBottomFab(coroutineScope, listState)
|
||||
}
|
||||
}
|
||||
QuickChatRow(
|
||||
enabled = isConnected,
|
||||
actions = quickChatActions,
|
||||
onClick = { action ->
|
||||
handleQuickChatAction(
|
||||
action = action,
|
||||
messageInputState = messageInputState,
|
||||
onSendMessage = { text -> onEvent(MessageScreenEvent.SendMessage(text)) },
|
||||
)
|
||||
},
|
||||
)
|
||||
ReplySnippet(
|
||||
originalMessage = replyingTo,
|
||||
onClearReply = { replyingTo = null },
|
||||
ourNode = ourNode,
|
||||
)
|
||||
AnimatedVisibility(visible = showQuickChat) {
|
||||
QuickChatRow(
|
||||
enabled = isConnected,
|
||||
actions = quickChatActions,
|
||||
onClick = { action ->
|
||||
handleQuickChatAction(
|
||||
action = action,
|
||||
messageInputState = messageInputState,
|
||||
onSendMessage = { text -> onEvent(MessageScreenEvent.SendMessage(text)) },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
ReplySnippet(originalMessage = replyingTo, onClearReply = { replyingTo = null }, ourNode = ourNode)
|
||||
MessageInput(
|
||||
isEnabled = isConnected,
|
||||
textFieldState = messageInputState,
|
||||
@@ -391,10 +375,10 @@ private fun ReplySnippet(originalMessage: Message?, onClearReply: () -> Unit, ou
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
Modifier.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
@@ -404,11 +388,7 @@ private fun ReplySnippet(originalMessage: Message?, onClearReply: () -> Unit, ou
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
R.string.replying_to,
|
||||
replyingToNodeUser?.shortName ?: unknownUserText,
|
||||
),
|
||||
text = stringResource(R.string.replying_to, replyingToNodeUser?.shortName ?: unknownUserText),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
Text(
|
||||
@@ -421,8 +401,7 @@ private fun ReplySnippet(originalMessage: Message?, onClearReply: () -> Unit, ou
|
||||
IconButton(onClick = onClearReply) {
|
||||
Icon(
|
||||
Icons.Filled.Close,
|
||||
contentDescription =
|
||||
stringResource(R.string.cancel_reply), // Specific action
|
||||
contentDescription = stringResource(R.string.cancel_reply), // Specific action
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -437,12 +416,10 @@ private fun ReplySnippet(originalMessage: Message?, onClearReply: () -> Unit, ou
|
||||
* @return The ellipsized string.
|
||||
* @receiver The string to ellipsize.
|
||||
*/
|
||||
private fun String.ellipsize(maxLength: Int): String =
|
||||
if (length > maxLength) "${take(maxLength)}…" else this
|
||||
private fun String.ellipsize(maxLength: Int): String = if (length > maxLength) "${take(maxLength)}…" else this
|
||||
|
||||
/**
|
||||
* Handles a quick chat action, either appending its message to the input field or sending it
|
||||
* directly.
|
||||
* Handles a quick chat action, either appending its message to the input field or sending it directly.
|
||||
*
|
||||
* @param action The [QuickChatAction] to handle.
|
||||
* @param messageInputState The [TextFieldState] of the message input field.
|
||||
@@ -460,12 +437,12 @@ private fun handleQuickChatAction(
|
||||
if (!originalText.contains(action.message)) {
|
||||
val newText =
|
||||
buildString {
|
||||
append(originalText)
|
||||
if (originalText.isNotEmpty() && !originalText.endsWith(' ')) {
|
||||
append(' ')
|
||||
}
|
||||
append(action.message)
|
||||
append(originalText)
|
||||
if (originalText.isNotEmpty() && !originalText.endsWith(' ')) {
|
||||
append(' ')
|
||||
}
|
||||
append(action.message)
|
||||
}
|
||||
.limitBytes(MESSAGE_CHARACTER_LIMIT_BYTES)
|
||||
messageInputState.setTextAndPlaceCursorAtEnd(newText)
|
||||
}
|
||||
@@ -481,8 +458,7 @@ private fun handleQuickChatAction(
|
||||
/**
|
||||
* Truncates a string to ensure its UTF-8 byte representation does not exceed [maxBytes].
|
||||
*
|
||||
* This implementation iterates by characters and checks byte length to avoid splitting multi-byte
|
||||
* characters.
|
||||
* This implementation iterates by characters and checks byte length to avoid splitting multi-byte characters.
|
||||
*
|
||||
* @param maxBytes The maximum allowed byte length.
|
||||
* @return The truncated string, or the original string if it's within the byte limit.
|
||||
@@ -524,12 +500,8 @@ private fun DeleteMessageDialog(count: Int, onConfirm: () -> Unit, onDismiss: ()
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
title = { Text(stringResource(R.string.delete_messages_title)) },
|
||||
text = { Text(text = deleteMessagesString) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) { Text(stringResource(R.string.delete)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) }
|
||||
},
|
||||
confirmButton = { TextButton(onClick = onConfirm) { Text(stringResource(R.string.delete)) } },
|
||||
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -552,38 +524,31 @@ internal sealed class MessageMenuAction {
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ActionModeTopBar(selectedCount: Int, onAction: (MessageMenuAction) -> Unit) =
|
||||
TopAppBar(
|
||||
title = { Text(text = selectedCount.toString()) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onAction(MessageMenuAction.Dismiss) }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(id = R.string.clear_selection),
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { onAction(MessageMenuAction.ClipboardCopy) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ContentCopy,
|
||||
contentDescription = stringResource(id = R.string.copy),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { onAction(MessageMenuAction.Delete) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = stringResource(id = R.string.delete),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { onAction(MessageMenuAction.SelectAll) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SelectAll,
|
||||
contentDescription = stringResource(id = R.string.select_all),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
private fun ActionModeTopBar(selectedCount: Int, onAction: (MessageMenuAction) -> Unit) = TopAppBar(
|
||||
title = { Text(text = selectedCount.toString()) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onAction(MessageMenuAction.Dismiss) }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(id = R.string.clear_selection),
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { onAction(MessageMenuAction.ClipboardCopy) }) {
|
||||
Icon(imageVector = Icons.Default.ContentCopy, contentDescription = stringResource(id = R.string.copy))
|
||||
}
|
||||
IconButton(onClick = { onAction(MessageMenuAction.Delete) }) {
|
||||
Icon(imageVector = Icons.Default.Delete, contentDescription = stringResource(id = R.string.delete))
|
||||
}
|
||||
IconButton(onClick = { onAction(MessageMenuAction.SelectAll) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SelectAll,
|
||||
contentDescription = stringResource(id = R.string.select_all),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* The default top app bar for the message screen.
|
||||
@@ -604,32 +569,44 @@ private fun MessageTopBar(
|
||||
onNavigateBack: () -> Unit,
|
||||
channels: AppOnlyProtos.ChannelSet?,
|
||||
channelIndexParam: Int?,
|
||||
) =
|
||||
TopAppBar(
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text = title, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
showQuickChat: Boolean,
|
||||
onToggleQuickChat: () -> Unit,
|
||||
) = TopAppBar(
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text = title, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
|
||||
if (channels != null && channelIndexParam != null) {
|
||||
SecurityIcon(channels, channelIndexParam)
|
||||
}
|
||||
if (channels != null && channelIndexParam != null) {
|
||||
SecurityIcon(channels, channelIndexParam)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(id = R.string.navigate_back),
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (channelIndex == DataPacket.PKC_CHANNEL_INDEX) {
|
||||
NodeKeyStatusIcon(hasPKC = true, mismatchKey = mismatchKey)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(id = R.string.navigate_back),
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onToggleQuickChat) {
|
||||
Icon(
|
||||
imageVector = if (showQuickChat) Icons.Filled.SpeakerNotesOff else Icons.AutoMirrored.Filled.Chat,
|
||||
contentDescription =
|
||||
if (showQuickChat) {
|
||||
stringResource(id = R.string.quick_chat_hide)
|
||||
} else {
|
||||
stringResource(id = R.string.quick_chat_show)
|
||||
},
|
||||
)
|
||||
}
|
||||
if (channelIndex == DataPacket.PKC_CHANNEL_INDEX) {
|
||||
NodeKeyStatusIcon(hasPKC = true, mismatchKey = mismatchKey)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* A row of quick chat action buttons.
|
||||
@@ -659,10 +636,7 @@ private fun QuickChatRow(
|
||||
|
||||
val allActions = remember(alertAction, actions) { listOf(alertAction) + actions }
|
||||
|
||||
LazyRow(
|
||||
modifier = modifier.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
LazyRow(modifier = modifier.padding(vertical = 4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
items(allActions, key = { it.uuid }) { action ->
|
||||
Button(onClick = { onClick(action) }, enabled = enabled) { Text(text = action.name) }
|
||||
}
|
||||
@@ -676,8 +650,7 @@ private fun QuickChatRow(
|
||||
* @param textFieldState The [TextFieldState] managing the input's text.
|
||||
* @param modifier The modifier for this composable.
|
||||
* @param maxByteSize The maximum allowed size of the message in bytes.
|
||||
* @param onSendMessage Callback invoked when the send button is pressed or send IME action is
|
||||
* triggered.
|
||||
* @param onSendMessage Callback invoked when the send button is pressed or send IME action is triggered.
|
||||
*/
|
||||
@Suppress("LongMethod") // Due to multiple parts of the OutlinedTextField
|
||||
@Composable
|
||||
@@ -708,10 +681,7 @@ private fun MessageInput(
|
||||
isError = isOverLimit,
|
||||
placeholder = { Text(stringResource(R.string.type_a_message)) },
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
imeAction = ImeAction.Send,
|
||||
),
|
||||
KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, imeAction = ImeAction.Send),
|
||||
onKeyboardAction = {
|
||||
if (canSend) {
|
||||
onSendMessage()
|
||||
@@ -723,11 +693,11 @@ private fun MessageInput(
|
||||
text = "$currentByteLength/$maxByteSize",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color =
|
||||
if (isOverLimit) {
|
||||
MaterialTheme.colorScheme.error
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
if (isOverLimit) {
|
||||
MaterialTheme.colorScheme.error
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.End,
|
||||
)
|
||||
@@ -754,25 +724,17 @@ private fun MessageInputPreview() {
|
||||
AppTheme {
|
||||
Surface {
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
MessageInput(
|
||||
isEnabled = true,
|
||||
textFieldState = rememberTextFieldState("Hello"),
|
||||
onSendMessage = {},
|
||||
)
|
||||
MessageInput(isEnabled = true, textFieldState = rememberTextFieldState("Hello"), onSendMessage = {})
|
||||
Spacer(Modifier.size(16.dp))
|
||||
MessageInput(
|
||||
isEnabled = false,
|
||||
textFieldState = rememberTextFieldState("Disabled"),
|
||||
onSendMessage = {},
|
||||
)
|
||||
MessageInput(isEnabled = false, textFieldState = rememberTextFieldState("Disabled"), onSendMessage = {})
|
||||
Spacer(Modifier.size(16.dp))
|
||||
MessageInput(
|
||||
isEnabled = true,
|
||||
textFieldState =
|
||||
rememberTextFieldState(
|
||||
"A very long message that might exceed the byte limit " +
|
||||
"and cause an error state display for the user to see clearly."
|
||||
),
|
||||
rememberTextFieldState(
|
||||
"A very long message that might exceed the byte limit " +
|
||||
"and cause an error state display for the user to see clearly.",
|
||||
),
|
||||
onSendMessage = {},
|
||||
maxByteSize = 50, // Test with a smaller limit
|
||||
)
|
||||
|
||||
@@ -258,6 +258,8 @@
|
||||
<string name="quick_chat_edit">Edit quick chat</string>
|
||||
<string name="quick_chat_append">Append to message</string>
|
||||
<string name="quick_chat_instant">Instantly send</string>
|
||||
<string name="quick_chat_show">Show quick chat menu</string>
|
||||
<string name="quick_chat_hide">Hide quick chat menu</string>
|
||||
<string name="factory_reset">Factory reset</string>
|
||||
<string name="factory_reset_description">This will clear all device configuration you have done.</string>
|
||||
<string name="bluetooth_disabled">Bluetooth disabled</string>
|
||||
|
||||
Reference in New Issue
Block a user