mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 04:45:21 +00:00
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>
This commit is contained in:
co-authored by
callebtc
parent
1d1a91108e
commit
b418aa3899
@@ -44,27 +44,27 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
val showMentionSuggestions by viewModel.showMentionSuggestions.observeAsState(false)
|
val showMentionSuggestions by viewModel.showMentionSuggestions.observeAsState(false)
|
||||||
val mentionSuggestions by viewModel.mentionSuggestions.observeAsState(emptyList())
|
val mentionSuggestions by viewModel.mentionSuggestions.observeAsState(emptyList())
|
||||||
val showAppInfo by viewModel.showAppInfo.observeAsState(false)
|
val showAppInfo by viewModel.showAppInfo.observeAsState(false)
|
||||||
|
|
||||||
var messageText by remember { mutableStateOf(TextFieldValue("")) }
|
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("") }
|
||||||
|
|
||||||
// Show password dialog when needed
|
// Show password dialog when needed
|
||||||
LaunchedEffect(showPasswordPrompt) {
|
LaunchedEffect(showPasswordPrompt) {
|
||||||
showPasswordDialog = showPasswordPrompt
|
showPasswordDialog = showPasswordPrompt
|
||||||
}
|
}
|
||||||
|
|
||||||
val isConnected by viewModel.isConnected.observeAsState(false)
|
val isConnected by viewModel.isConnected.observeAsState(false)
|
||||||
val passwordPromptChannel by viewModel.passwordPromptChannel.observeAsState(null)
|
val passwordPromptChannel by viewModel.passwordPromptChannel.observeAsState(null)
|
||||||
|
|
||||||
// Determine what messages to show
|
// Determine what messages to show
|
||||||
val displayMessages = when {
|
val displayMessages = when {
|
||||||
selectedPrivatePeer != null -> privateChats[selectedPrivatePeer] ?: emptyList()
|
selectedPrivatePeer != null -> privateChats[selectedPrivatePeer] ?: emptyList()
|
||||||
currentChannel != null -> channelMessages[currentChannel] ?: emptyList()
|
currentChannel != null -> channelMessages[currentChannel] ?: emptyList()
|
||||||
else -> messages
|
else -> messages
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use WindowInsets to handle keyboard properly
|
// Use WindowInsets to handle keyboard properly
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
val headerHeight = 42.dp
|
val headerHeight = 42.dp
|
||||||
@@ -78,7 +78,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
) {
|
) {
|
||||||
// Header spacer - creates space for the floating header
|
// Header spacer - creates space for the floating header
|
||||||
Spacer(modifier = Modifier.height(headerHeight))
|
Spacer(modifier = Modifier.height(headerHeight))
|
||||||
|
|
||||||
// Messages area - takes up available space, will compress when keyboard appears
|
// Messages area - takes up available space, will compress when keyboard appears
|
||||||
MessagesList(
|
MessagesList(
|
||||||
messages = displayMessages,
|
messages = displayMessages,
|
||||||
@@ -86,7 +86,6 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
meshService = viewModel.meshService,
|
meshService = viewModel.meshService,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Input area - stays at bottom
|
// Input area - stays at bottom
|
||||||
ChatInputSection(
|
ChatInputSection(
|
||||||
messageText = messageText,
|
messageText = messageText,
|
||||||
@@ -125,7 +124,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
colorScheme = colorScheme
|
colorScheme = colorScheme
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Floating header - positioned absolutely at top, ignores keyboard
|
// Floating header - positioned absolutely at top, ignores keyboard
|
||||||
ChatFloatingHeader(
|
ChatFloatingHeader(
|
||||||
headerHeight = headerHeight,
|
headerHeight = headerHeight,
|
||||||
@@ -168,7 +167,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
targetOffsetX = { it },
|
targetOffsetX = { it },
|
||||||
animationSpec = tween(250, easing = EaseInCubic)
|
animationSpec = tween(250, easing = EaseInCubic)
|
||||||
) + fadeOut(animationSpec = tween(250)),
|
) + fadeOut(animationSpec = tween(250)),
|
||||||
modifier = Modifier.zIndex(2f)
|
modifier = Modifier.zIndex(2f)
|
||||||
) {
|
) {
|
||||||
SidebarOverlay(
|
SidebarOverlay(
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
@@ -177,7 +176,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dialogs
|
// Dialogs
|
||||||
ChatDialogs(
|
ChatDialogs(
|
||||||
showPasswordDialog = showPasswordDialog,
|
showPasswordDialog = showPasswordDialog,
|
||||||
@@ -225,7 +224,6 @@ private fun ChatInputSection(
|
|||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
HorizontalDivider(color = colorScheme.outline.copy(alpha = 0.3f))
|
HorizontalDivider(color = colorScheme.outline.copy(alpha = 0.3f))
|
||||||
|
|
||||||
// Command suggestions box
|
// Command suggestions box
|
||||||
if (showCommandSuggestions && commandSuggestions.isNotEmpty()) {
|
if (showCommandSuggestions && commandSuggestions.isNotEmpty()) {
|
||||||
CommandSuggestionsBox(
|
CommandSuggestionsBox(
|
||||||
@@ -247,7 +245,7 @@ private fun ChatInputSection(
|
|||||||
|
|
||||||
HorizontalDivider(color = colorScheme.outline.copy(alpha = 0.2f))
|
HorizontalDivider(color = colorScheme.outline.copy(alpha = 0.2f))
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageInput(
|
MessageInput(
|
||||||
value = messageText,
|
value = messageText,
|
||||||
onValueChange = onMessageTextChange,
|
onValueChange = onMessageTextChange,
|
||||||
@@ -306,7 +304,7 @@ private fun ChatFloatingHeader(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Divider under header
|
// Divider under header
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -337,7 +335,7 @@ private fun ChatDialogs(
|
|||||||
onConfirm = onPasswordConfirm,
|
onConfirm = onPasswordConfirm,
|
||||||
onDismiss = onPasswordDismiss
|
onDismiss = onPasswordDismiss
|
||||||
)
|
)
|
||||||
|
|
||||||
// App info dialog
|
// App info dialog
|
||||||
AppInfoDialog(
|
AppInfoDialog(
|
||||||
show = showAppInfo,
|
show = showAppInfo,
|
||||||
|
|||||||
@@ -327,9 +327,9 @@ class CommandProcessor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Command Autocomplete
|
// MARK: - Command Autocomplete
|
||||||
|
|
||||||
fun updateCommandSuggestions(input: String) {
|
fun updateCommandSuggestions(input: String) {
|
||||||
if (!input.startsWith("/") || input.length < 1) {
|
if (!input.startsWith("/")) {
|
||||||
state.setShowCommandSuggestions(false)
|
state.setShowCommandSuggestions(false)
|
||||||
state.setCommandSuggestions(emptyList())
|
state.setCommandSuggestions(emptyList())
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -15,19 +15,23 @@ 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.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextRange
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
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.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.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.TransformedText
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
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 com.bitchat.android.R
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
|
|
||||||
@@ -45,13 +49,13 @@ class SlashCommandVisualTransformation : VisualTransformation {
|
|||||||
val slashCommandRegex = Regex("(/\\w+)(?=\\s|$)")
|
val slashCommandRegex = Regex("(/\\w+)(?=\\s|$)")
|
||||||
val annotatedString = buildAnnotatedString {
|
val annotatedString = buildAnnotatedString {
|
||||||
var lastIndex = 0
|
var lastIndex = 0
|
||||||
|
|
||||||
slashCommandRegex.findAll(text.text).forEach { match ->
|
slashCommandRegex.findAll(text.text).forEach { match ->
|
||||||
// Add text before the match
|
// Add text before the match
|
||||||
if (match.range.first > lastIndex) {
|
if (match.range.first > lastIndex) {
|
||||||
append(text.text.substring(lastIndex, match.range.first))
|
append(text.text.substring(lastIndex, match.range.first))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the styled slash command
|
// Add the styled slash command
|
||||||
withStyle(
|
withStyle(
|
||||||
style = SpanStyle(
|
style = SpanStyle(
|
||||||
@@ -63,16 +67,16 @@ class SlashCommandVisualTransformation : VisualTransformation {
|
|||||||
) {
|
) {
|
||||||
append(match.value)
|
append(match.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
lastIndex = match.range.last + 1
|
lastIndex = match.range.last + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add remaining text
|
// Add remaining text
|
||||||
if (lastIndex < text.text.length) {
|
if (lastIndex < text.text.length) {
|
||||||
append(text.text.substring(lastIndex))
|
append(text.text.substring(lastIndex))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return TransformedText(
|
return TransformedText(
|
||||||
text = annotatedString,
|
text = annotatedString,
|
||||||
offsetMapping = OffsetMapping.Identity
|
offsetMapping = OffsetMapping.Identity
|
||||||
@@ -162,7 +166,8 @@ fun MessageInput(
|
|||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), // Reduced padding
|
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
|
// Text input with placeholder
|
||||||
Box(
|
Box(
|
||||||
@@ -205,48 +210,64 @@ fun MessageInput(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp)) // Reduced spacing
|
Spacer(modifier = Modifier.width(8.dp)) // Reduced spacing
|
||||||
|
|
||||||
// Send button with enabled/disabled state
|
// Command quick access button
|
||||||
IconButton(
|
if (value.text.isEmpty()) {
|
||||||
onClick = { if (hasText) onSend() }, // Only execute if there's text
|
FilledTonalIconButton(
|
||||||
enabled = hasText, // Enable only when there's text
|
onClick = {
|
||||||
modifier = Modifier.size(32.dp)
|
onValueChange(TextFieldValue(text = "/", selection = TextRange("/".length)))
|
||||||
) {
|
},
|
||||||
Box(
|
modifier = Modifier.size(32.dp)
|
||||||
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(
|
Text(
|
||||||
imageVector = Icons.Filled.ArrowUpward,
|
text = "/",
|
||||||
contentDescription = "Send message",
|
textAlign = TextAlign.Center
|
||||||
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
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} 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
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
.background(colorScheme.surface)
|
.background(colorScheme.surface)
|
||||||
.border(1.dp, colorScheme.outline.copy(alpha = 0.3f), RoundedCornerShape(4.dp))
|
.border(1.dp, colorScheme.outline.copy(alpha = 0.3f), RoundedCornerShape(4.dp))
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
@@ -280,14 +302,15 @@ fun CommandSuggestionItem(
|
|||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { onClick() }
|
.clickable { onClick() }
|
||||||
.padding(horizontal = 12.dp, vertical = 3.dp)
|
.padding(horizontal = 12.dp, vertical = 3.dp)
|
||||||
.background(Color.Gray.copy(alpha = 0.1f)),
|
.background(Color.Gray.copy(alpha = 0.1f)),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
// Show all aliases together
|
// Show all aliases together
|
||||||
val allCommands = if (suggestion.aliases.isNotEmpty()) {
|
val allCommands = if (suggestion.aliases.isNotEmpty()) {
|
||||||
@@ -295,7 +318,7 @@ fun CommandSuggestionItem(
|
|||||||
} else {
|
} else {
|
||||||
listOf(suggestion.command)
|
listOf(suggestion.command)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = allCommands.joinToString(", "),
|
text = allCommands.joinToString(", "),
|
||||||
style = MaterialTheme.typography.bodySmall.copy(
|
style = MaterialTheme.typography.bodySmall.copy(
|
||||||
@@ -305,10 +328,9 @@ fun CommandSuggestionItem(
|
|||||||
color = colorScheme.primary,
|
color = colorScheme.primary,
|
||||||
fontSize = 11.sp
|
fontSize = 11.sp
|
||||||
)
|
)
|
||||||
|
|
||||||
// Show syntax if any
|
// Show syntax if any
|
||||||
suggestion.syntax?.let { syntax ->
|
suggestion.syntax?.let { syntax ->
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
Text(
|
||||||
text = syntax,
|
text = syntax,
|
||||||
style = MaterialTheme.typography.bodySmall.copy(
|
style = MaterialTheme.typography.bodySmall.copy(
|
||||||
@@ -318,9 +340,7 @@ fun CommandSuggestionItem(
|
|||||||
fontSize = 10.sp
|
fontSize = 10.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
|
|
||||||
// Show description
|
// Show description
|
||||||
Text(
|
Text(
|
||||||
text = suggestion.description,
|
text = suggestion.description,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<string name="join_channel">Join Channel</string>
|
<string name="join_channel">Join Channel</string>
|
||||||
<string name="leave_channel">Leave</string>
|
<string name="leave_channel">Leave</string>
|
||||||
<string name="send_message">Send</string>
|
<string name="send_message">Send</string>
|
||||||
|
<string name="show_commands">Show commands</string>
|
||||||
<string name="back">Back</string>
|
<string name="back">Back</string>
|
||||||
<string name="people">People</string>
|
<string name="people">People</string>
|
||||||
<string name="channels">Channels</string>
|
<string name="channels">Channels</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user