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