android: start material icons migration for chat and list ui
Some checks failed
Android CI / android (push) Failing after 4m7s
Android Release / release (push) Failing after 4m36s
CI / test (push) Has started running

This commit is contained in:
Codex
2026-03-09 19:56:27 +03:00
parent e65714e45e
commit 448ed3243d
5 changed files with 79 additions and 23 deletions

View File

@@ -465,3 +465,12 @@
- Added full Telegram reference mapping checklist (`docs/android-ui-batch-4-checklist.md`) with screenshot-by-screenshot description. - Added full Telegram reference mapping checklist (`docs/android-ui-batch-4-checklist.md`) with screenshot-by-screenshot description.
- Added explicit icon policy: no emoji icons in production UI components, Material Icons/vector icons only. - Added explicit icon policy: no emoji icons in production UI components, Material Icons/vector icons only.
- Updated UI checklist index with Batch 4 entry. - Updated UI checklist index with Batch 4 entry.
### Step 75 - Material Icons migration (Batch 1 start)
- Replaced symbol/emoji-based UI controls in chat surfaces with Material Icons:
- chat header/menu/search controls (`more`, `up/down`),
- image viewer actions (`close`, `forward`, `delete`),
- multi-select markers (`radio checked/unchecked`, `selected` check),
- attachment/media markers (`movie`, `attach file`).
- Replaced chat list management FAB glyph toggle (`+`/`×`) with Material `Add`/`Close` icons.
- Added `androidx.compose.material:material-icons-extended` dependency for consistent icon usage.

View File

@@ -81,6 +81,7 @@ dependencies {
implementation("androidx.compose.ui:ui:1.7.6") implementation("androidx.compose.ui:ui:1.7.6")
implementation("androidx.compose.ui:ui-tooling-preview:1.7.6") implementation("androidx.compose.ui:ui-tooling-preview:1.7.6")
implementation("androidx.compose.material3:material3:1.3.1") implementation("androidx.compose.material3:material3:1.3.1")
implementation("androidx.compose.material:material-icons-extended:1.7.6")
implementation("io.coil-kt:coil:2.7.0") implementation("io.coil-kt:coil:2.7.0")
implementation("io.coil-kt:coil-compose:2.7.0") implementation("io.coil-kt:coil-compose:2.7.0")
implementation("androidx.media3:media3-exoplayer:1.4.1") implementation("androidx.media3:media3-exoplayer:1.4.1")

View File

@@ -40,6 +40,8 @@ import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@@ -75,6 +77,18 @@ import androidx.compose.ui.semantics.semantics
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Forward
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.DeleteOutline
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Movie
import androidx.compose.material.icons.filled.RadioButtonChecked
import androidx.compose.material.icons.filled.RadioButtonUnchecked
import coil.compose.AsyncImage import coil.compose.AsyncImage
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import ru.daemonlord.messenger.core.audio.AppAudioFocusCoordinator import ru.daemonlord.messenger.core.audio.AppAudioFocusCoordinator
@@ -316,8 +330,8 @@ fun ChatScreen(
) )
} }
} }
Button(onClick = onLoadMore, enabled = !state.isLoadingMore) { IconButton(onClick = onLoadMore, enabled = !state.isLoadingMore) {
Text("") Icon(imageVector = Icons.Filled.MoreVert, contentDescription = "More")
} }
} }
Row( Row(
@@ -335,14 +349,14 @@ fun ChatScreen(
label = { Text("Search in chat") }, label = { Text("Search in chat") },
singleLine = true, singleLine = true,
) )
Button( IconButton(
onClick = { onJumpInlineSearch(false) }, onClick = { onJumpInlineSearch(false) },
enabled = state.inlineSearchMatches.isNotEmpty(), enabled = state.inlineSearchMatches.isNotEmpty(),
) { Text("") } ) { Icon(imageVector = Icons.Filled.ArrowUpward, contentDescription = "Previous match") }
Button( IconButton(
onClick = { onJumpInlineSearch(true) }, onClick = { onJumpInlineSearch(true) },
enabled = state.inlineSearchMatches.isNotEmpty(), enabled = state.inlineSearchMatches.isNotEmpty(),
) { Text("") } ) { Icon(imageVector = Icons.Filled.ArrowDownward, contentDescription = "Next match") }
} }
if (state.inlineSearchMatches.isNotEmpty()) { if (state.inlineSearchMatches.isNotEmpty()) {
Text( Text(
@@ -781,14 +795,20 @@ fun ChatScreen(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Button(onClick = { viewerImageIndex = null }) { Text("") } IconButton(onClick = { viewerImageIndex = null }) {
Icon(imageVector = Icons.Filled.Close, contentDescription = "Close viewer")
}
Text( Text(
text = "${currentIndex + 1}/${allImageUrls.size.coerceAtLeast(1)}", text = "${currentIndex + 1}/${allImageUrls.size.coerceAtLeast(1)}",
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
) )
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Button(onClick = {}, enabled = false) { Text("") } IconButton(onClick = {}, enabled = false) {
Button(onClick = {}, enabled = false) { Text("🗑") } Icon(imageVector = Icons.AutoMirrored.Filled.Forward, contentDescription = "Forward image")
}
IconButton(onClick = {}, enabled = false) {
Icon(imageVector = Icons.Filled.DeleteOutline, contentDescription = "Delete image")
}
} }
} }
Box( Box(
@@ -894,9 +914,9 @@ private fun MessageBubble(
horizontalArrangement = if (isOutgoing) Arrangement.End else Arrangement.Start, horizontalArrangement = if (isOutgoing) Arrangement.End else Arrangement.Start,
) { ) {
if (isMultiSelecting && !isOutgoing) { if (isMultiSelecting && !isOutgoing) {
Text( Icon(
text = if (isSelected) "" else "", imageVector = if (isSelected) Icons.Filled.RadioButtonChecked else Icons.Filled.RadioButtonUnchecked,
style = MaterialTheme.typography.titleMedium, contentDescription = if (isSelected) "Selected" else "Not selected",
modifier = Modifier.padding(end = 6.dp), modifier = Modifier.padding(end = 6.dp),
) )
} }
@@ -918,11 +938,21 @@ private fun MessageBubble(
.padding(horizontal = 10.dp, vertical = 7.dp), .padding(horizontal = 10.dp, vertical = 7.dp),
) { ) {
if (isSelected) { if (isSelected) {
Text( Row(
text = "✓ Selected", verticalAlignment = Alignment.CenterVertically,
style = MaterialTheme.typography.labelSmall, horizontalArrangement = Arrangement.spacedBy(4.dp),
fontWeight = FontWeight.SemiBold, ) {
) Icon(
imageVector = Icons.Filled.Check,
contentDescription = "Selected",
modifier = Modifier.size(14.dp),
)
Text(
text = "Selected",
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.SemiBold,
)
}
} }
if (!isOutgoing && !message.senderDisplayName.isNullOrBlank()) { if (!isOutgoing && !message.senderDisplayName.isNullOrBlank()) {
Text( Text(
@@ -1087,9 +1117,9 @@ private fun MessageBubble(
} }
} }
if (isMultiSelecting && isOutgoing) { if (isMultiSelecting && isOutgoing) {
Text( Icon(
text = if (isSelected) "" else "", imageVector = if (isSelected) Icons.Filled.RadioButtonChecked else Icons.Filled.RadioButtonUnchecked,
style = MaterialTheme.typography.titleMedium, contentDescription = if (isSelected) "Selected" else "Not selected",
modifier = Modifier.padding(start = 6.dp), modifier = Modifier.padding(start = 6.dp),
) )
} }
@@ -1108,7 +1138,13 @@ private fun VideoAttachmentCard(
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.45f), RoundedCornerShape(10.dp)) .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.45f), RoundedCornerShape(10.dp))
.padding(8.dp), .padding(8.dp),
) { ) {
Text(text = "🎬 Video", style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.SemiBold) Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
Icon(imageVector = Icons.Filled.Movie, contentDescription = "Video")
Text(text = "Video", style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.SemiBold)
}
Text(text = extractFileName(url), style = MaterialTheme.typography.bodySmall, maxLines = 1) Text(text = extractFileName(url), style = MaterialTheme.typography.bodySmall, maxLines = 1)
Text(text = fileType, style = MaterialTheme.typography.labelSmall) Text(text = fileType, style = MaterialTheme.typography.labelSmall)
} }
@@ -1170,7 +1206,7 @@ private fun FileAttachmentRow(
.background(MaterialTheme.colorScheme.primaryContainer), .background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Text("📎") Icon(imageVector = Icons.Filled.AttachFile, contentDescription = "Attachment")
} }
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text(text = extractFileName(fileUrl), style = MaterialTheme.typography.bodySmall, maxLines = 1) Text(text = extractFileName(fileUrl), style = MaterialTheme.typography.bodySmall, maxLines = 1)

View File

@@ -24,6 +24,7 @@ import androidx.compose.material3.AssistChipDefaults
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@@ -49,6 +50,9 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import coil.compose.AsyncImage import coil.compose.AsyncImage
import ru.daemonlord.messenger.domain.chat.model.ChatItem import ru.daemonlord.messenger.domain.chat.model.ChatItem
import java.time.Instant import java.time.Instant
@@ -296,7 +300,10 @@ fun ChatListScreen(
.align(Alignment.BottomEnd) .align(Alignment.BottomEnd)
.padding(end = 16.dp, bottom = 88.dp), .padding(end = 16.dp, bottom = 88.dp),
) { ) {
Text(if (managementExpanded) "×" else "+") Icon(
imageVector = if (managementExpanded) Icons.Filled.Close else Icons.Filled.Add,
contentDescription = if (managementExpanded) "Close management" else "Open management",
)
} }
Surface( Surface(
modifier = Modifier modifier = Modifier

View File

@@ -8,6 +8,9 @@
- [ ] Использовать только `Material Icons` (Outlined/Rounded) или векторные ассеты в том же стиле. - [ ] Использовать только `Material Icons` (Outlined/Rounded) или векторные ассеты в том же стиле.
- [ ] Все action-кнопки (`send`, `attach`, `mic`, `play`, `pause`, `delete`, `reply`, `forward`) перевести на иконки из `androidx.compose.material:material-icons-extended`. - [ ] Все action-кнопки (`send`, `attach`, `mic`, `play`, `pause`, `delete`, `reply`, `forward`) перевести на иконки из `androidx.compose.material:material-icons-extended`.
Прогресс:
- [x] Chat/List базовые action-иконки переведены на `Material Icons` (Step 75).
## Карта скриншотов (что на каждом) ## Карта скриншотов (что на каждом)
1. Энергосбережение: секционные карточки + тумблеры. 1. Энергосбережение: секционные карточки + тумблеры.
2. Уведомления и звуки: длинный список секций и switch-row. 2. Уведомления и звуки: длинный список секций и switch-row.