fix(chat): cursor location and enhance message input with slash command styling

- Change message input from String to TextFieldValue for better cursor control
- Add SlashCommandVisualTransformation to style slash commands with colored backgrounds
- Maintain cursor position when selecting command suggestions
This commit is contained in:
Faded
2025-07-14 15:51:50 +05:00
parent fe57c5684e
commit 21dc1a5b5e
2 changed files with 81 additions and 13 deletions
@@ -22,9 +22,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager 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.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
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 commandSuggestions by viewModel.commandSuggestions.observeAsState(emptyList())
val showAppInfo by viewModel.showAppInfo.observeAsState(false) 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 showPasswordPrompt by remember { mutableStateOf(false) }
var showPasswordDialog by remember { mutableStateOf(false) } var showPasswordDialog by remember { mutableStateOf(false) }
var passwordInput by remember { mutableStateOf("") } var passwordInput by remember { mutableStateOf("") }
@@ -112,20 +114,24 @@ fun ChatScreen(viewModel: ChatViewModel) {
// Input area - stays at bottom // Input area - stays at bottom
ChatInputSection( ChatInputSection(
messageText = messageText, messageText = messageText,
onMessageTextChange = { newText: String -> onMessageTextChange = { newText: TextFieldValue ->
messageText = newText messageText = newText
viewModel.updateCommandSuggestions(newText) viewModel.updateCommandSuggestions(newText.text)
}, },
onSend = { onSend = {
if (messageText.trim().isNotEmpty()) { if (messageText.text.trim().isNotEmpty()) {
viewModel.sendMessage(messageText.trim()) viewModel.sendMessage(messageText.text.trim())
messageText = "" messageText = TextFieldValue("")
} }
}, },
showCommandSuggestions = showCommandSuggestions, showCommandSuggestions = showCommandSuggestions,
commandSuggestions = commandSuggestions, commandSuggestions = commandSuggestions,
onSuggestionClick = { suggestion: CommandSuggestion -> onSuggestionClick = { suggestion: CommandSuggestion ->
messageText = viewModel.selectCommandSuggestion(suggestion) val commandText = viewModel.selectCommandSuggestion(suggestion)
messageText = TextFieldValue(
text = commandText,
selection = TextRange(commandText.length)
)
}, },
selectedPrivatePeer = selectedPrivatePeer, selectedPrivatePeer = selectedPrivatePeer,
currentChannel = currentChannel, currentChannel = currentChannel,
@@ -195,8 +201,8 @@ fun ChatScreen(viewModel: ChatViewModel) {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ChatInputSection( private fun ChatInputSection(
messageText: String, messageText: TextFieldValue,
onMessageTextChange: (String) -> Unit, onMessageTextChange: (TextFieldValue) -> Unit,
onSend: () -> Unit, onSend: () -> Unit,
showCommandSuggestions: Boolean, showCommandSuggestions: Boolean,
commandSuggestions: List<CommandSuggestion>, commandSuggestions: List<CommandSuggestion>,
@@ -15,23 +15,79 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor 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.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.text.withStyle
/** /**
* Input components for ChatScreen * Input components for ChatScreen
* Extracted from ChatScreen.kt for better organization * 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 @Composable
fun MessageInput( fun MessageInput(
value: String, value: TextFieldValue,
onValueChange: (String) -> Unit, onValueChange: (TextFieldValue) -> Unit,
onSend: () -> Unit, onSend: () -> Unit,
selectedPrivatePeer: String?, selectedPrivatePeer: String?,
currentChannel: String?, currentChannel: String?,
@@ -39,6 +95,7 @@ fun MessageInput(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val isFocused = remember { mutableStateOf(false) }
Row( Row(
modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), // Reduced padding modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), // Reduced padding
@@ -70,7 +127,12 @@ fun MessageInput(
cursorBrush = SolidColor(colorScheme.primary), cursorBrush = SolidColor(colorScheme.primary),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
keyboardActions = KeyboardActions(onSend = { onSend() }), 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 Spacer(modifier = Modifier.width(8.dp)) // Reduced spacing