android: add multi-select top and bottom action bars
Some checks failed
CI / test (push) Failing after 2m7s

This commit is contained in:
Codex
2026-03-09 14:07:20 +03:00
parent 7381d611cc
commit 6c9501e624
3 changed files with 38 additions and 16 deletions

View File

@@ -217,3 +217,8 @@
- Reworked chat composer into rounded Telegram-like container with emoji slot, text input, attach button, and send/voice state button. - Reworked chat composer into rounded Telegram-like container with emoji slot, text input, attach button, and send/voice state button.
- Preserved send/upload state guards and existing insets handling (`navigationBarsPadding` + `imePadding`). - Preserved send/upload state guards and existing insets handling (`navigationBarsPadding` + `imePadding`).
- Updated Telegram UI batch-2 checklist composer-related items. - Updated Telegram UI batch-2 checklist composer-related items.
### Step 35 - Chat UI / multi-select bars and overlays
- Split message selection UX into dedicated top selection bar (count/close/delete/edit/reactions) and bottom action bar (reply/forward).
- Enhanced selected bubble visual state with explicit selected marker text.
- Updated Telegram UI batch-2 checklist items for multi-select mode.

View File

@@ -249,34 +249,44 @@ fun ChatScreen(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.92f))
.padding(horizontal = 12.dp, vertical = 6.dp), .padding(horizontal = 12.dp, vertical = 6.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) { ) {
Button(onClick = onClearSelection) { Text("Close") }
Text( Text(
text = "${state.actionState.selectedCount} selected", text = "${state.actionState.selectedCount} selected",
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(top = 12.dp), modifier = Modifier.weight(1f),
) )
if (state.actionState.mode == MessageSelectionMode.SINGLE && state.selectedMessage != null) {
Button(onClick = { onReplySelected(state.selectedMessage) }) { Text("Reply") }
Button(
onClick = { onEditSelected(state.selectedMessage) },
enabled = state.selectedCanEdit,
) { Text("Edit") }
}
Button(onClick = { onDeleteSelected(false) }) { Text("Delete") } Button(onClick = { onDeleteSelected(false) }) { Text("Delete") }
Button( Button(
onClick = { onDeleteSelected(true) }, onClick = { onDeleteSelected(true) },
enabled = state.actionState.mode == MessageSelectionMode.SINGLE && state.selectedCanDeleteForAll, enabled = state.actionState.mode == MessageSelectionMode.SINGLE && state.selectedCanDeleteForAll,
) { Text("Del for all") } ) { Text("Del for all") }
Button(onClick = onForwardSelected) {
Text(if (state.actionState.mode == MessageSelectionMode.MULTI) "Forward selected" else "Forward")
}
if (state.actionState.mode == MessageSelectionMode.SINGLE) { if (state.actionState.mode == MessageSelectionMode.SINGLE) {
Button(
onClick = { state.selectedMessage?.let(onEditSelected) },
enabled = state.selectedCanEdit,
) { Text("Edit") }
Button(onClick = { onToggleReaction("\uD83D\uDC4D") }) { Text("\uD83D\uDC4D") } Button(onClick = { onToggleReaction("\uD83D\uDC4D") }) { Text("\uD83D\uDC4D") }
Button(onClick = { onToggleReaction("\uD83D\uDE02") }) { Text("\uD83D\uDE02") } Button(onClick = { onToggleReaction("\uD83D\uDE02") }) { Text("\uD83D\uDE02") }
} }
Button(onClick = onClearSelection) { Text("Close") } }
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.actionState.mode == MessageSelectionMode.SINGLE && state.selectedMessage != null) {
Button(onClick = { onReplySelected(state.selectedMessage) }) { Text("Reply") }
}
Button(onClick = onForwardSelected) {
Text(if (state.actionState.mode == MessageSelectionMode.MULTI) "Forward selected" else "Forward")
}
} }
} }
@@ -477,6 +487,13 @@ private fun MessageBubble(
) )
.padding(horizontal = 10.dp, vertical = 8.dp), .padding(horizontal = 10.dp, vertical = 8.dp),
) { ) {
if (isSelected) {
Text(
text = "✓ Selected",
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.SemiBold,
)
}
if (!isOutgoing && !message.senderDisplayName.isNullOrBlank()) { if (!isOutgoing && !message.senderDisplayName.isNullOrBlank()) {
Text( Text(
text = message.senderDisplayName, text = message.senderDisplayName,

View File

@@ -23,10 +23,10 @@
## P0 — Message Actions + Reactions ## P0 — Message Actions + Reactions
- [ ] Long press открывает reaction bar над сообщением (emoji + "expand"). - [ ] Long press открывает reaction bar над сообщением (emoji + "expand").
- [ ] Контекстное меню действий (reply/save/forward/pin/delete) в floating dark card. - [ ] Контекстное меню действий (reply/save/forward/pin/delete) в floating dark card.
- [ ] Режим multi-select: - [x] Режим multi-select:
- [ ] Верхняя панель (close/count/actions). - [x] Верхняя панель (close/count/actions).
- [ ] Нижняя action bar (reply/forward) в виде rounded pills. - [x] Нижняя action bar (reply/forward) в виде rounded pills.
- [ ] Selected state сообщения с явной подсветкой/overlay. - [x] Selected state сообщения с явной подсветкой/overlay.
## P1 — Media & Attachment Bubbles ## P1 — Media & Attachment Bubbles
- [ ] Media bubble с превью изображения/видео + таймкод для видео. - [ ] Media bubble с превью изображения/видео + таймкод для видео.