android: align chat message actions with telegram-style selection
Some checks failed
Android CI / android (push) Failing after 5m2s
Android Release / release (push) Has started running
CI / test (push) Has been cancelled

This commit is contained in:
Codex
2026-03-10 01:22:08 +03:00
parent 4aa4946e82
commit 580a6683e3
2 changed files with 122 additions and 87 deletions

View File

@@ -852,3 +852,13 @@
- Refined reactions and attachments rendering inside bubbles:
- chip-like reaction containers,
- rounded image/file/media surfaces closer to Telegram-like visual rhythm.
### Step 121 - Chat selection and message action UX cleanup
- Added Telegram-like multi-select top bar in chat:
- close selection,
- selected counter,
- quick forward/delete actions.
- Simplified tap action menu flow for single message:
- richer reaction row (`❤️ 👍 👎 🔥 🥰 👏 😁`),
- reply/edit/forward/delete actions kept in one sheet.
- Removed duplicate/conflicting selection controls between top and bottom action rows.

View File

@@ -308,65 +308,103 @@ fun ChatScreen(
.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(horizontal = adaptiveHorizontalPadding),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.9f))
.padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
)
}
if (!state.chatAvatarUrl.isNullOrBlank()) {
AsyncImage(
model = state.chatAvatarUrl,
contentDescription = "Chat avatar for ${state.chatTitle}",
modifier = Modifier
.size(40.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop,
)
} else {
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center,
) {
Text(
text = state.chatTitle.firstOrNull()?.uppercase() ?: "?",
style = MaterialTheme.typography.titleSmall,
)
}
}
Column(modifier = Modifier.weight(1f)) {
Text(
text = state.chatTitle.ifBlank { "Chat #${state.chatId}" },
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
)
if (state.chatSubtitle.isNotBlank()) {
Text(
text = state.chatSubtitle,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
IconButton(
onClick = { showInlineSearch = !showInlineSearch },
enabled = !state.isLoadingMore,
if (state.actionState.mode == MessageSelectionMode.MULTI && state.actionState.hasSelection) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.92f))
.padding(horizontal = 8.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(imageVector = Icons.Filled.Search, contentDescription = "Search in chat")
IconButton(onClick = onClearSelection) {
Icon(imageVector = Icons.Filled.Close, contentDescription = "Close selection")
}
Text(
text = state.actionState.selectedCount.toString(),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
)
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = onForwardSelected) {
Icon(
imageVector = Icons.AutoMirrored.Filled.Forward,
contentDescription = "Forward selected",
)
}
IconButton(
onClick = {
pendingDeleteForAll = false
showDeleteDialog = true
},
) {
Icon(imageVector = Icons.Filled.DeleteOutline, contentDescription = "Delete selected")
}
IconButton(onClick = onLoadMore, enabled = !state.isLoadingMore) {
Icon(imageVector = Icons.Filled.MoreVert, contentDescription = "Selection menu")
}
}
IconButton(onClick = onLoadMore, enabled = !state.isLoadingMore) {
Icon(imageVector = Icons.Filled.MoreVert, contentDescription = "More")
} else {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.9f))
.padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
)
}
if (!state.chatAvatarUrl.isNullOrBlank()) {
AsyncImage(
model = state.chatAvatarUrl,
contentDescription = "Chat avatar for ${state.chatTitle}",
modifier = Modifier
.size(40.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop,
)
} else {
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center,
) {
Text(
text = state.chatTitle.firstOrNull()?.uppercase() ?: "?",
style = MaterialTheme.typography.titleSmall,
)
}
}
Column(modifier = Modifier.weight(1f)) {
Text(
text = state.chatTitle.ifBlank { "Chat #${state.chatId}" },
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
)
if (state.chatSubtitle.isNotBlank()) {
Text(
text = state.chatSubtitle,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
IconButton(
onClick = { showInlineSearch = !showInlineSearch },
enabled = !state.isLoadingMore,
) {
Icon(imageVector = Icons.Filled.Search, contentDescription = "Search in chat")
}
IconButton(onClick = onLoadMore, enabled = !state.isLoadingMore) {
Icon(imageVector = Icons.Filled.MoreVert, contentDescription = "More")
}
}
}
if (showInlineSearch) {
@@ -494,14 +532,21 @@ fun ChatScreen(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
listOf("👍", "❤️", "🔥", "😂").forEach { emoji ->
Button(
onClick = {
onSelectMessage(selected)
onToggleReaction(emoji)
actionMenuMessage = null
},
) { Text(emoji) }
listOf("❤️", "👍", "👎", "🔥", "🥰", "👏", "😁").forEach { emoji ->
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier
.clip(CircleShape)
.clickable {
onSelectMessage(selected)
onToggleReaction(emoji)
actionMenuMessage = null
}
.padding(horizontal = 12.dp, vertical = 8.dp),
) {
Text(text = emoji)
}
}
}
Button(
@@ -536,13 +581,6 @@ fun ChatScreen(
},
modifier = Modifier.fillMaxWidth(),
) { Text("Delete") }
Button(
onClick = {
onEnterMultiSelect(selected)
actionMenuMessage = null
},
modifier = Modifier.fillMaxWidth(),
) { Text("Select") }
TextButton(
onClick = {
actionMenuMessage = null
@@ -556,7 +594,8 @@ fun ChatScreen(
}
if (state.actionState.hasSelection &&
!(state.actionState.mode == MessageSelectionMode.SINGLE && actionMenuMessage != null)
state.actionState.mode == MessageSelectionMode.SINGLE &&
actionMenuMessage == null
) {
Row(
modifier = Modifier
@@ -602,20 +641,6 @@ fun ChatScreen(
Button(onClick = { onToggleReaction("\uD83D\uDE02") }) { Text("\uD83D\uDE02") }
}
}
if (state.actionState.mode == MessageSelectionMode.SINGLE) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.88f))
.padding(horizontal = 12.dp, vertical = 6.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
if (state.selectedMessage != null) {
Button(onClick = { onReplySelected(state.selectedMessage) }) { Text("Reply") }
}
Button(onClick = onForwardSelected) { Text("Forward") }
}
}
}
if (state.forwardingMessageIds.isNotEmpty()) {