diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 1859ef8..7240e46 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -530,3 +530,10 @@ - Added `ChatDao.markChatRead(chatId)` to clear `unread_count` and `unread_mentions_count` in Room. - Applied optimistic local unread reset on `markMessageRead(...)` in message repository. - Fixed realtime unread logic: incoming messages in currently active chat no longer increment unread badge. + +### Step 86 - Chats list visual pass toward Telegram reference +- Updated chats list row density: tighter vertical rhythm, larger avatar, stronger title hierarchy, cleaner secondary text. +- Restyled archive as dedicated list row with leading archive icon avatar, subtitle, and unread badge. +- Kept search in top app bar action and changed search field default to collapsed (opens via search icon). +- Returned message-type emoji markers in chat previews: + - `🖼` photo, `🎤` voice, `🎵` audio, `🎥` video, `⭕` circle video, `🔗` links. diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListScreen.kt index 0bead12..d8cd740 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListScreen.kt @@ -53,6 +53,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Inventory2 import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Search import kotlinx.coroutines.flow.collectLatest @@ -134,7 +135,7 @@ fun ChatListScreen( onUnbanMember: (Long, Long) -> Unit, ) { var managementExpanded by remember { mutableStateOf(false) } - var searchExpanded by remember { mutableStateOf(true) } + var searchExpanded by remember { mutableStateOf(false) } var createTitle by remember { mutableStateOf("") } var createMemberIds by remember { mutableStateOf("") } var createHandle by remember { mutableStateOf("") } @@ -539,49 +540,56 @@ private fun ChatRow( modifier = Modifier .fillMaxWidth() .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 12.dp), + .padding(horizontal = 14.dp, vertical = 8.dp), ) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { if (!chat.avatarUrl.isNullOrBlank()) { AsyncImage( model = chat.avatarUrl, contentDescription = "Avatar for ${chat.displayTitle}", modifier = Modifier - .size(44.dp) + .size(52.dp) .clip(CircleShape), ) } else { Box( modifier = Modifier - .size(44.dp) + .size(52.dp) .clip(CircleShape) .background(MaterialTheme.colorScheme.surfaceVariant), contentAlignment = Alignment.Center, ) { - Text(chat.displayTitle.firstOrNull()?.uppercase() ?: "?") + Text( + text = chat.displayTitle.firstOrNull()?.uppercase() ?: "?", + style = MaterialTheme.typography.titleMedium, + ) } } Column(modifier = Modifier.weight(1f)) { - Row(verticalAlignment = Alignment.CenterVertically) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { Text( text = chat.displayTitle, - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleLarge, fontWeight = if (chat.unreadCount > 0) FontWeight.SemiBold else FontWeight.Normal, + maxLines = 1, ) if (chat.pinned) { Text( - text = " [PIN]", - style = MaterialTheme.typography.bodySmall, + text = "📌", + style = MaterialTheme.typography.labelMedium, ) } if (chat.muted) { Text( - text = " [MUTE]", - style = MaterialTheme.typography.bodySmall, + text = "🔕", + style = MaterialTheme.typography.labelMedium, ) } } @@ -589,9 +597,10 @@ private fun ChatRow( if (preview.isNotBlank()) { Text( text = preview, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 2.dp), + maxLines = 1, + modifier = Modifier.padding(top = 1.dp), ) } if (chat.type == "private") { @@ -602,19 +611,19 @@ private fun ChatRow( } Text( text = presence, - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(top = 2.dp), + modifier = Modifier.padding(top = 1.dp), ) } } Column( horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(5.dp), ) { Text( text = formatChatTime(chat.lastMessageCreatedAt), - style = MaterialTheme.typography.labelSmall, + style = MaterialTheme.typography.labelMedium, ) if (chat.unreadMentionsCount > 0) { BadgeChip(label = "@") @@ -678,15 +687,35 @@ private fun ArchiveRow( modifier = Modifier .fillMaxWidth() .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 10.dp), - horizontalArrangement = Arrangement.SpaceBetween, + .padding(horizontal = 14.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, ) { - Text( - text = "Archive ($count)", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - ) + Box( + modifier = Modifier + .size(52.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Filled.Inventory2, + contentDescription = "Archived chats", + ) + } + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Archive", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = "$count chats", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + ) + } if (unreadCount > 0) { BadgeChip(label = unreadCount.toString()) } @@ -716,22 +745,33 @@ private fun CenterState( private fun ChatItem.previewText(): String { val raw = lastMessageText.orEmpty().trim() val prefix = when (lastMessageType) { - "image" -> "Photo" - "video" -> "Video" - "audio" -> "Audio" - "voice" -> "Voice" - "file" -> "File" - "circle_video" -> "Video message" + "image" -> "🖼" + "video" -> "🎥" + "audio" -> "🎵" + "voice" -> "🎤" + "file" -> "📎" + "circle_video", "video_note" -> "⭕" else -> "" } - if (raw.isNotEmpty()) return if (prefix.isBlank()) raw else "$prefix: $raw" + val linkPrefix = if (raw.startsWith("http://", ignoreCase = true) || raw.startsWith("https://", ignoreCase = true)) { + "🔗" + } else { + "" + } + if (raw.isNotEmpty()) { + return when { + prefix.isNotBlank() -> "$prefix $raw" + linkPrefix.isNotBlank() -> "$linkPrefix $raw" + else -> raw + } + } return when (lastMessageType) { - "image" -> "Photo" - "video" -> "Video" - "audio" -> "Audio" - "voice" -> "Voice message" - "file" -> "File" - "circle_video" -> "Video message" + "image" -> "🖼 Photo" + "video" -> "🎥 Video" + "audio" -> "🎵 Audio" + "voice" -> "🎤 Voice message" + "file" -> "📎 File" + "circle_video", "video_note" -> "⭕ Circle video" null, "text" -> "" else -> "Media" }