android: tune chats list visuals closer to telegram
This commit is contained in:
@@ -530,3 +530,10 @@
|
|||||||
- Added `ChatDao.markChatRead(chatId)` to clear `unread_count` and `unread_mentions_count` in Room.
|
- Added `ChatDao.markChatRead(chatId)` to clear `unread_count` and `unread_mentions_count` in Room.
|
||||||
- Applied optimistic local unread reset on `markMessageRead(...)` in message repository.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Close
|
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.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@@ -134,7 +135,7 @@ fun ChatListScreen(
|
|||||||
onUnbanMember: (Long, Long) -> Unit,
|
onUnbanMember: (Long, Long) -> Unit,
|
||||||
) {
|
) {
|
||||||
var managementExpanded by remember { mutableStateOf(false) }
|
var managementExpanded by remember { mutableStateOf(false) }
|
||||||
var searchExpanded by remember { mutableStateOf(true) }
|
var searchExpanded by remember { mutableStateOf(false) }
|
||||||
var createTitle by remember { mutableStateOf("") }
|
var createTitle by remember { mutableStateOf("") }
|
||||||
var createMemberIds by remember { mutableStateOf("") }
|
var createMemberIds by remember { mutableStateOf("") }
|
||||||
var createHandle by remember { mutableStateOf("") }
|
var createHandle by remember { mutableStateOf("") }
|
||||||
@@ -539,49 +540,56 @@ private fun ChatRow(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 14.dp, vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
if (!chat.avatarUrl.isNullOrBlank()) {
|
if (!chat.avatarUrl.isNullOrBlank()) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = chat.avatarUrl,
|
model = chat.avatarUrl,
|
||||||
contentDescription = "Avatar for ${chat.displayTitle}",
|
contentDescription = "Avatar for ${chat.displayTitle}",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(44.dp)
|
.size(52.dp)
|
||||||
.clip(CircleShape),
|
.clip(CircleShape),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(44.dp)
|
.size(52.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Text(chat.displayTitle.firstOrNull()?.uppercase() ?: "?")
|
Text(
|
||||||
|
text = chat.displayTitle.firstOrNull()?.uppercase() ?: "?",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = chat.displayTitle,
|
text = chat.displayTitle,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
fontWeight = if (chat.unreadCount > 0) FontWeight.SemiBold else FontWeight.Normal,
|
fontWeight = if (chat.unreadCount > 0) FontWeight.SemiBold else FontWeight.Normal,
|
||||||
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
if (chat.pinned) {
|
if (chat.pinned) {
|
||||||
Text(
|
Text(
|
||||||
text = " [PIN]",
|
text = "📌",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (chat.muted) {
|
if (chat.muted) {
|
||||||
Text(
|
Text(
|
||||||
text = " [MUTE]",
|
text = "🔕",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -589,9 +597,10 @@ private fun ChatRow(
|
|||||||
if (preview.isNotBlank()) {
|
if (preview.isNotBlank()) {
|
||||||
Text(
|
Text(
|
||||||
text = preview,
|
text = preview,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.padding(top = 2.dp),
|
maxLines = 1,
|
||||||
|
modifier = Modifier.padding(top = 1.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (chat.type == "private") {
|
if (chat.type == "private") {
|
||||||
@@ -602,19 +611,19 @@ private fun ChatRow(
|
|||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = presence,
|
text = presence,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.padding(top = 2.dp),
|
modifier = Modifier.padding(top = 1.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.End,
|
horizontalAlignment = Alignment.End,
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
verticalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = formatChatTime(chat.lastMessageCreatedAt),
|
text = formatChatTime(chat.lastMessageCreatedAt),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
)
|
)
|
||||||
if (chat.unreadMentionsCount > 0) {
|
if (chat.unreadMentionsCount > 0) {
|
||||||
BadgeChip(label = "@")
|
BadgeChip(label = "@")
|
||||||
@@ -678,15 +687,35 @@ private fun ArchiveRow(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Box(
|
||||||
text = "Archive ($count)",
|
modifier = Modifier
|
||||||
style = MaterialTheme.typography.titleMedium,
|
.size(52.dp)
|
||||||
fontWeight = FontWeight.SemiBold,
|
.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) {
|
if (unreadCount > 0) {
|
||||||
BadgeChip(label = unreadCount.toString())
|
BadgeChip(label = unreadCount.toString())
|
||||||
}
|
}
|
||||||
@@ -716,22 +745,33 @@ private fun CenterState(
|
|||||||
private fun ChatItem.previewText(): String {
|
private fun ChatItem.previewText(): String {
|
||||||
val raw = lastMessageText.orEmpty().trim()
|
val raw = lastMessageText.orEmpty().trim()
|
||||||
val prefix = when (lastMessageType) {
|
val prefix = when (lastMessageType) {
|
||||||
"image" -> "Photo"
|
"image" -> "🖼"
|
||||||
"video" -> "Video"
|
"video" -> "🎥"
|
||||||
"audio" -> "Audio"
|
"audio" -> "🎵"
|
||||||
"voice" -> "Voice"
|
"voice" -> "🎤"
|
||||||
"file" -> "File"
|
"file" -> "📎"
|
||||||
"circle_video" -> "Video message"
|
"circle_video", "video_note" -> "⭕"
|
||||||
else -> ""
|
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) {
|
return when (lastMessageType) {
|
||||||
"image" -> "Photo"
|
"image" -> "🖼 Photo"
|
||||||
"video" -> "Video"
|
"video" -> "🎥 Video"
|
||||||
"audio" -> "Audio"
|
"audio" -> "🎵 Audio"
|
||||||
"voice" -> "Voice message"
|
"voice" -> "🎤 Voice message"
|
||||||
"file" -> "File"
|
"file" -> "📎 File"
|
||||||
"circle_video" -> "Video message"
|
"circle_video", "video_note" -> "⭕ Circle video"
|
||||||
null, "text" -> ""
|
null, "text" -> ""
|
||||||
else -> "Media"
|
else -> "Media"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user