android: align chat message actions with telegram-style selection
This commit is contained in:
@@ -852,3 +852,13 @@
|
|||||||
- Refined reactions and attachments rendering inside bubbles:
|
- Refined reactions and attachments rendering inside bubbles:
|
||||||
- chip-like reaction containers,
|
- chip-like reaction containers,
|
||||||
- rounded image/file/media surfaces closer to Telegram-like visual rhythm.
|
- 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.
|
||||||
|
|||||||
@@ -308,65 +308,103 @@ fun ChatScreen(
|
|||||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||||
.padding(horizontal = adaptiveHorizontalPadding),
|
.padding(horizontal = adaptiveHorizontalPadding),
|
||||||
) {
|
) {
|
||||||
Row(
|
if (state.actionState.mode == MessageSelectionMode.MULTI && state.actionState.hasSelection) {
|
||||||
modifier = Modifier
|
Row(
|
||||||
.fillMaxWidth()
|
modifier = Modifier
|
||||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.9f))
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.92f))
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(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 = 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) {
|
} else {
|
||||||
Icon(imageVector = Icons.Filled.MoreVert, contentDescription = "More")
|
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) {
|
if (showInlineSearch) {
|
||||||
@@ -494,14 +532,21 @@ fun ChatScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
listOf("👍", "❤️", "🔥", "😂").forEach { emoji ->
|
listOf("❤️", "👍", "👎", "🔥", "🥰", "👏", "😁").forEach { emoji ->
|
||||||
Button(
|
Surface(
|
||||||
onClick = {
|
shape = CircleShape,
|
||||||
onSelectMessage(selected)
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
onToggleReaction(emoji)
|
modifier = Modifier
|
||||||
actionMenuMessage = null
|
.clip(CircleShape)
|
||||||
},
|
.clickable {
|
||||||
) { Text(emoji) }
|
onSelectMessage(selected)
|
||||||
|
onToggleReaction(emoji)
|
||||||
|
actionMenuMessage = null
|
||||||
|
}
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
) {
|
||||||
|
Text(text = emoji)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Button(
|
Button(
|
||||||
@@ -536,13 +581,6 @@ fun ChatScreen(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) { Text("Delete") }
|
) { Text("Delete") }
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
onEnterMultiSelect(selected)
|
|
||||||
actionMenuMessage = null
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) { Text("Select") }
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
actionMenuMessage = null
|
actionMenuMessage = null
|
||||||
@@ -556,7 +594,8 @@ fun ChatScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state.actionState.hasSelection &&
|
if (state.actionState.hasSelection &&
|
||||||
!(state.actionState.mode == MessageSelectionMode.SINGLE && actionMenuMessage != null)
|
state.actionState.mode == MessageSelectionMode.SINGLE &&
|
||||||
|
actionMenuMessage == null
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -602,20 +641,6 @@ fun ChatScreen(
|
|||||||
Button(onClick = { onToggleReaction("\uD83D\uDE02") }) { Text("\uD83D\uDE02") }
|
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()) {
|
if (state.forwardingMessageIds.isNotEmpty()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user