From b418aa389919a2f9b45e1bf1f89261b243df0649 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?H=C3=A9ctor=20de=20Isidro?=
<5445152+hector6872@users.noreply.github.com>
Date: Wed, 6 Aug 2025 01:25:15 +0200
Subject: [PATCH] Add CommandSuggestionsBox quick access (#146)
* Add CommandSuggestionsBox quick access
* Remove unnecessary evaluation
* Use a FilledTonalIconButton for the CommandSuggestionsBox quick access button
* fix import
* quick command instead of send
* simplify
---------
Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
---
.../java/com/bitchat/android/ui/ChatScreen.kt | 26 ++--
.../bitchat/android/ui/CommandProcessor.kt | 4 +-
.../com/bitchat/android/ui/InputComponents.kt | 130 ++++++++++--------
app/src/main/res/values/strings.xml | 1 +
4 files changed, 90 insertions(+), 71 deletions(-)
diff --git a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt
index 79e1331e..1b0ceaa3 100644
--- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt
+++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt
@@ -44,27 +44,27 @@ fun ChatScreen(viewModel: ChatViewModel) {
val showMentionSuggestions by viewModel.showMentionSuggestions.observeAsState(false)
val mentionSuggestions by viewModel.mentionSuggestions.observeAsState(emptyList())
val showAppInfo by viewModel.showAppInfo.observeAsState(false)
-
+
var messageText by remember { mutableStateOf(TextFieldValue("")) }
var showPasswordPrompt by remember { mutableStateOf(false) }
var showPasswordDialog by remember { mutableStateOf(false) }
var passwordInput by remember { mutableStateOf("") }
-
+
// Show password dialog when needed
LaunchedEffect(showPasswordPrompt) {
showPasswordDialog = showPasswordPrompt
}
-
+
val isConnected by viewModel.isConnected.observeAsState(false)
val passwordPromptChannel by viewModel.passwordPromptChannel.observeAsState(null)
-
+
// Determine what messages to show
val displayMessages = when {
selectedPrivatePeer != null -> privateChats[selectedPrivatePeer] ?: emptyList()
currentChannel != null -> channelMessages[currentChannel] ?: emptyList()
else -> messages
}
-
+
// Use WindowInsets to handle keyboard properly
Box(modifier = Modifier.fillMaxSize()) {
val headerHeight = 42.dp
@@ -78,7 +78,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
) {
// Header spacer - creates space for the floating header
Spacer(modifier = Modifier.height(headerHeight))
-
+
// Messages area - takes up available space, will compress when keyboard appears
MessagesList(
messages = displayMessages,
@@ -86,7 +86,6 @@ fun ChatScreen(viewModel: ChatViewModel) {
meshService = viewModel.meshService,
modifier = Modifier.weight(1f)
)
-
// Input area - stays at bottom
ChatInputSection(
messageText = messageText,
@@ -125,7 +124,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
colorScheme = colorScheme
)
}
-
+
// Floating header - positioned absolutely at top, ignores keyboard
ChatFloatingHeader(
headerHeight = headerHeight,
@@ -168,7 +167,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
targetOffsetX = { it },
animationSpec = tween(250, easing = EaseInCubic)
) + fadeOut(animationSpec = tween(250)),
- modifier = Modifier.zIndex(2f)
+ modifier = Modifier.zIndex(2f)
) {
SidebarOverlay(
viewModel = viewModel,
@@ -177,7 +176,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
)
}
}
-
+
// Dialogs
ChatDialogs(
showPasswordDialog = showPasswordDialog,
@@ -225,7 +224,6 @@ private fun ChatInputSection(
) {
Column {
HorizontalDivider(color = colorScheme.outline.copy(alpha = 0.3f))
-
// Command suggestions box
if (showCommandSuggestions && commandSuggestions.isNotEmpty()) {
CommandSuggestionsBox(
@@ -247,7 +245,7 @@ private fun ChatInputSection(
HorizontalDivider(color = colorScheme.outline.copy(alpha = 0.2f))
}
-
+
MessageInput(
value = messageText,
onValueChange = onMessageTextChange,
@@ -306,7 +304,7 @@ private fun ChatFloatingHeader(
)
)
}
-
+
// Divider under header
HorizontalDivider(
modifier = Modifier
@@ -337,7 +335,7 @@ private fun ChatDialogs(
onConfirm = onPasswordConfirm,
onDismiss = onPasswordDismiss
)
-
+
// App info dialog
AppInfoDialog(
show = showAppInfo,
diff --git a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt
index 81c77c3b..3c46410a 100644
--- a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt
+++ b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt
@@ -327,9 +327,9 @@ class CommandProcessor(
}
// MARK: - Command Autocomplete
-
+
fun updateCommandSuggestions(input: String) {
- if (!input.startsWith("/") || input.length < 1) {
+ if (!input.startsWith("/")) {
state.setShowCommandSuggestions(false)
state.setCommandSuggestions(emptyList())
return
diff --git a/app/src/main/java/com/bitchat/android/ui/InputComponents.kt b/app/src/main/java/com/bitchat/android/ui/InputComponents.kt
index 6be2507c..9cee91f1 100644
--- a/app/src/main/java/com/bitchat/android/ui/InputComponents.kt
+++ b/app/src/main/java/com/bitchat/android/ui/InputComponents.kt
@@ -15,19 +15,23 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
-import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import com.bitchat.android.R
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.text.withStyle
@@ -45,13 +49,13 @@ class SlashCommandVisualTransformation : VisualTransformation {
val slashCommandRegex = Regex("(/\\w+)(?=\\s|$)")
val annotatedString = buildAnnotatedString {
var lastIndex = 0
-
+
slashCommandRegex.findAll(text.text).forEach { match ->
// Add text before the match
if (match.range.first > lastIndex) {
append(text.text.substring(lastIndex, match.range.first))
}
-
+
// Add the styled slash command
withStyle(
style = SpanStyle(
@@ -63,16 +67,16 @@ class SlashCommandVisualTransformation : VisualTransformation {
) {
append(match.value)
}
-
+
lastIndex = match.range.last + 1
}
-
+
// Add remaining text
if (lastIndex < text.text.length) {
append(text.text.substring(lastIndex))
}
}
-
+
return TransformedText(
text = annotatedString,
offsetMapping = OffsetMapping.Identity
@@ -162,7 +166,8 @@ fun MessageInput(
Row(
modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), // Reduced padding
- verticalAlignment = Alignment.CenterVertically
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// Text input with placeholder
Box(
@@ -205,48 +210,64 @@ fun MessageInput(
Spacer(modifier = Modifier.width(8.dp)) // Reduced spacing
- // Send button with enabled/disabled state
- IconButton(
- onClick = { if (hasText) onSend() }, // Only execute if there's text
- enabled = hasText, // Enable only when there's text
- modifier = Modifier.size(32.dp)
- ) {
- Box(
- modifier = Modifier
- .size(30.dp)
- .background(
- color = if (!hasText) {
- // Disabled state - muted grey
- colorScheme.onSurface.copy(alpha = 0.3f)
- } else if (selectedPrivatePeer != null || currentChannel != null) {
- // Orange for both private messages and channels when enabled
- Color(0xFFFF9500).copy(alpha = 0.75f)
- } else if (colorScheme.background == Color.Black) {
- Color(0xFF00FF00).copy(alpha = 0.75f) // Bright green for dark theme
- } else {
- Color(0xFF008000).copy(alpha = 0.75f) // Dark green for light theme
- },
- shape = CircleShape
- ),
- contentAlignment = Alignment.Center
+ // Command quick access button
+ if (value.text.isEmpty()) {
+ FilledTonalIconButton(
+ onClick = {
+ onValueChange(TextFieldValue(text = "/", selection = TextRange("/".length)))
+ },
+ modifier = Modifier.size(32.dp)
) {
- Icon(
- imageVector = Icons.Filled.ArrowUpward,
- contentDescription = "Send message",
- modifier = Modifier.size(20.dp),
- tint = if (!hasText) {
- // Disabled state - muted grey icon
- colorScheme.onSurface.copy(alpha = 0.5f)
- } else if (selectedPrivatePeer != null || currentChannel != null) {
- // Black arrow on orange for both private and channel modes
- Color.Black
- } else if (colorScheme.background == Color.Black) {
- Color.Black // Black arrow on bright green in dark theme
- } else {
- Color.White // White arrow on dark green in light theme
- }
+ Text(
+ text = "/",
+ textAlign = TextAlign.Center
)
}
+ } else {
+ // Send button with enabled/disabled state
+ IconButton(
+ onClick = { if (hasText) onSend() }, // Only execute if there's text
+ enabled = hasText, // Enable only when there's text
+ modifier = Modifier.size(32.dp)
+ ) {
+ // Update send button to match input field colors
+ Box(
+ modifier = Modifier
+ .size(30.dp)
+ .background(
+ color = if (!hasText) {
+ // Disabled state - muted grey
+ colorScheme.onSurface.copy(alpha = 0.3f)
+ } else if (selectedPrivatePeer != null || currentChannel != null) {
+ // Orange for both private messages and channels when enabled
+ Color(0xFFFF9500).copy(alpha = 0.75f)
+ } else if (colorScheme.background == Color.Black) {
+ Color(0xFF00FF00).copy(alpha = 0.75f) // Bright green for dark theme
+ } else {
+ Color(0xFF008000).copy(alpha = 0.75f) // Dark green for light theme
+ },
+ shape = CircleShape
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Filled.ArrowUpward,
+ contentDescription = stringResource(id = R.string.send_message),
+ modifier = Modifier.size(20.dp),
+ tint = if (!hasText) {
+ // Disabled state - muted grey icon
+ colorScheme.onSurface.copy(alpha = 0.5f)
+ } else if (selectedPrivatePeer != null || currentChannel != null) {
+ // Black arrow on orange for both private and channel modes
+ Color.Black
+ } else if (colorScheme.background == Color.Black) {
+ Color.Black // Black arrow on bright green in dark theme
+ } else {
+ Color.White // White arrow on dark green in light theme
+ }
+ )
+ }
+ }
}
}
}
@@ -258,9 +279,10 @@ fun CommandSuggestionsBox(
modifier: Modifier = Modifier
) {
val colorScheme = MaterialTheme.colorScheme
-
+
Column(
modifier = modifier
+ .verticalScroll(rememberScrollState())
.background(colorScheme.surface)
.border(1.dp, colorScheme.outline.copy(alpha = 0.3f), RoundedCornerShape(4.dp))
.padding(vertical = 8.dp)
@@ -280,14 +302,15 @@ fun CommandSuggestionItem(
onClick: () -> Unit
) {
val colorScheme = MaterialTheme.colorScheme
-
+
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(horizontal = 12.dp, vertical = 3.dp)
.background(Color.Gray.copy(alpha = 0.1f)),
- verticalAlignment = Alignment.CenterVertically
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// Show all aliases together
val allCommands = if (suggestion.aliases.isNotEmpty()) {
@@ -295,7 +318,7 @@ fun CommandSuggestionItem(
} else {
listOf(suggestion.command)
}
-
+
Text(
text = allCommands.joinToString(", "),
style = MaterialTheme.typography.bodySmall.copy(
@@ -305,10 +328,9 @@ fun CommandSuggestionItem(
color = colorScheme.primary,
fontSize = 11.sp
)
-
+
// Show syntax if any
suggestion.syntax?.let { syntax ->
- Spacer(modifier = Modifier.width(8.dp))
Text(
text = syntax,
style = MaterialTheme.typography.bodySmall.copy(
@@ -318,9 +340,7 @@ fun CommandSuggestionItem(
fontSize = 10.sp
)
}
-
- Spacer(modifier = Modifier.weight(1f))
-
+
// Show description
Text(
text = suggestion.description,
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 699d394d..783120f2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -10,6 +10,7 @@
Join Channel
Leave
Send
+ Show commands
Back
People
Channels