From 580a6683e39df16f8550e94ee66aa912565df6ac Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 10 Mar 2026 01:22:08 +0300 Subject: [PATCH] android: align chat message actions with telegram-style selection --- android/CHANGELOG.md | 10 + .../messenger/ui/chat/ChatScreen.kt | 199 ++++++++++-------- 2 files changed, 122 insertions(+), 87 deletions(-) diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index d404584..6f91b42 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -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. diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt index c094c6e..87135fa 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt @@ -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()) {