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