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 4c9e30dc..7f8ce190 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt @@ -22,9 +22,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.TextRange 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.TextFieldValue import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.Dp @@ -65,7 +67,7 @@ fun ChatScreen(viewModel: ChatViewModel) { val commandSuggestions by viewModel.commandSuggestions.observeAsState(emptyList()) val showAppInfo by viewModel.showAppInfo.observeAsState(false) - var messageText by remember { mutableStateOf("") } + var messageText by remember { mutableStateOf(TextFieldValue("")) } var showPasswordPrompt by remember { mutableStateOf(false) } var showPasswordDialog by remember { mutableStateOf(false) } var passwordInput by remember { mutableStateOf("") } @@ -112,20 +114,24 @@ fun ChatScreen(viewModel: ChatViewModel) { // Input area - stays at bottom ChatInputSection( messageText = messageText, - onMessageTextChange = { newText: String -> + onMessageTextChange = { newText: TextFieldValue -> messageText = newText - viewModel.updateCommandSuggestions(newText) + viewModel.updateCommandSuggestions(newText.text) }, onSend = { - if (messageText.trim().isNotEmpty()) { - viewModel.sendMessage(messageText.trim()) - messageText = "" + if (messageText.text.trim().isNotEmpty()) { + viewModel.sendMessage(messageText.text.trim()) + messageText = TextFieldValue("") } }, showCommandSuggestions = showCommandSuggestions, commandSuggestions = commandSuggestions, onSuggestionClick = { suggestion: CommandSuggestion -> - messageText = viewModel.selectCommandSuggestion(suggestion) + val commandText = viewModel.selectCommandSuggestion(suggestion) + messageText = TextFieldValue( + text = commandText, + selection = TextRange(commandText.length) + ) }, selectedPrivatePeer = selectedPrivatePeer, currentChannel = currentChannel, @@ -195,8 +201,8 @@ fun ChatScreen(viewModel: ChatViewModel) { @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ChatInputSection( - messageText: String, - onMessageTextChange: (String) -> Unit, + messageText: TextFieldValue, + onMessageTextChange: (TextFieldValue) -> Unit, onSend: () -> Unit, showCommandSuggestions: Boolean, commandSuggestions: List, 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 761b502b..d04d84aa 100644 --- a/app/src/main/java/com/bitchat/android/ui/InputComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/InputComponents.kt @@ -15,23 +15,79 @@ 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.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.FontStyle 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.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 androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.text.withStyle /** * Input components for ChatScreen * Extracted from ChatScreen.kt for better organization */ +/** + * VisualTransformation that styles slash commands with background and color + * while preserving cursor positioning and click handling + */ +class SlashCommandVisualTransformation : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + 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( + color = Color(0xFF00FF7F), // Bright green + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Medium, + background = Color(0xFF2D2D2D) // Dark gray background + ) + ) { + 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 + ) + } +} + + + + + @Composable fun MessageInput( - value: String, - onValueChange: (String) -> Unit, + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, onSend: () -> Unit, selectedPrivatePeer: String?, currentChannel: String?, @@ -39,6 +95,7 @@ fun MessageInput( modifier: Modifier = Modifier ) { val colorScheme = MaterialTheme.colorScheme + val isFocused = remember { mutableStateOf(false) } Row( modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), // Reduced padding @@ -70,7 +127,12 @@ fun MessageInput( cursorBrush = SolidColor(colorScheme.primary), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), keyboardActions = KeyboardActions(onSend = { onSend() }), - modifier = Modifier.weight(1f) + visualTransformation = SlashCommandVisualTransformation(), + modifier = Modifier + .weight(1f) + .onFocusChanged { focusState -> + isFocused.value = focusState.isFocused + } ) Spacer(modifier = Modifier.width(8.dp)) // Reduced spacing