mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 16:05:20 +00:00
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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user