android: improve chat list row parity with avatar time and fab
Some checks are pending
CI / test (push) Has started running

This commit is contained in:
Codex
2026-03-09 14:28:16 +03:00
parent a5a940b749
commit ce585f62d2
3 changed files with 131 additions and 74 deletions

View File

@@ -253,3 +253,9 @@
- Added context actions from long-press: reply, edit, forward, delete, select, close.
- Added placeholder disabled pin action in the menu to keep action set consistent with Telegram-like flow.
- Updated Telegram UI batch-2 checklist items for long-press reactions and context menu.
### Step 42 - Chat list / row and FAB parity pass
- Updated chat list rows with avatar rendering, trailing message time, and richer right-side metadata layout.
- Kept unread/mention/pinned/muted indicators while aligning row structure closer to Telegram list pattern.
- Added floating compose FAB placeholder at bottom-right in chat list screen.
- Updated Telegram UI batch-2 checklist chat-list parity items.

View File

@@ -1,12 +1,15 @@
package ru.daemonlord.messenger.ui.chats
import androidx.compose.foundation.clickable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.WindowInsets
@@ -31,11 +34,17 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
import ru.daemonlord.messenger.domain.chat.model.ChatItem
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@Composable
fun ChatListRoute(
@@ -134,6 +143,7 @@ fun ChatListScreen(
)
}
Box(modifier = Modifier.fillMaxSize()) {
PullToRefreshBox(
isRefreshing = state.isRefreshing,
onRefresh = onRefresh,
@@ -178,6 +188,15 @@ fun ChatListScreen(
}
}
}
Button(
onClick = {},
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
) {
Text("+")
}
}
}
}
@@ -195,8 +214,28 @@ private fun ChatRow(
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
if (!chat.avatarUrl.isNullOrBlank()) {
AsyncImage(
model = chat.avatarUrl,
contentDescription = "Avatar",
modifier = Modifier
.size(44.dp)
.clip(CircleShape),
)
} else {
Box(
modifier = Modifier
.size(44.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center,
) {
Text(chat.displayTitle.firstOrNull()?.uppercase() ?: "?")
}
}
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = chat.displayTitle,
@@ -216,19 +255,6 @@ private fun ChatRow(
)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
if (chat.unreadMentionsCount > 0) {
BadgeChip(label = "@")
}
if (chat.unreadCount > 0) {
BadgeChip(label = chat.unreadCount.toString())
}
}
}
val preview = chat.previewText()
if (preview.isNotBlank()) {
Text(
@@ -238,7 +264,6 @@ private fun ChatRow(
modifier = Modifier.padding(top = 2.dp),
)
}
if (chat.type == "private") {
val presence = if (chat.counterpartIsOnline == true) {
"online"
@@ -253,6 +278,23 @@ private fun ChatRow(
)
}
}
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Text(
text = formatChatTime(chat.lastMessageCreatedAt),
style = MaterialTheme.typography.labelSmall,
)
if (chat.unreadMentionsCount > 0) {
BadgeChip(label = "@")
}
if (chat.unreadCount > 0) {
BadgeChip(label = chat.unreadCount.toString())
}
}
}
}
}
@Composable
@@ -268,6 +310,15 @@ private fun BadgeChip(label: String) {
)
}
private val chatTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")
private fun formatChatTime(iso: String?): String {
if (iso.isNullOrBlank()) return ""
return runCatching {
Instant.parse(iso).atZone(ZoneId.systemDefault()).toLocalTime().format(chatTimeFormatter)
}.getOrElse { "" }
}
@Composable
private fun FilterChip(
label: String,

View File

@@ -47,10 +47,10 @@
## P1 — Chat List Screen Parity
- [x] Верхние фильтр-чипы/табы ("Все", "Люди", ...), компактные rounded chips.
- [ ] Список чатов:
- [ ] Аватар, title, preview, date/time.
- [x] Аватар, title, preview, date/time.
- [x] Badge unread справа.
- [x] Иконки delivery/camera/attachments в preview строке.
- [ ] Плавающий FAB (compose/new chat) справа снизу.
- [x] Плавающий FAB (compose/new chat) справа снизу.
- [ ] Floating bottom navigation с blur/dark container и активным фиолетовым tab.
## P2 — Motion & Polish