android: refine chat header and top pinned/audio strips
This commit is contained in:
@@ -891,3 +891,13 @@
|
||||
- closing search now also clears active query/filter without re-entering chat.
|
||||
- Added automatic inline-search collapse when entering multi-select mode.
|
||||
- Reworked message tap action sheet to Telegram-like list actions with icons and clearer destructive `Delete` row styling.
|
||||
|
||||
### Step 125 - Chat header/top strips visual refinement
|
||||
- Refined chat header density and typography to be closer to Telegram-like proportions.
|
||||
- Updated pinned strip visual:
|
||||
- accent vertical marker,
|
||||
- tighter spacing,
|
||||
- cleaner title/content hierarchy.
|
||||
- Added top mini audio strip under pinned area:
|
||||
- shows latest audio/voice context from loaded chat,
|
||||
- includes play affordance, speed badge, and dismiss action.
|
||||
|
||||
@@ -276,6 +276,7 @@ fun ChatScreen(
|
||||
}
|
||||
var viewerImageIndex by remember { mutableStateOf<Int?>(null) }
|
||||
var dismissedPinnedMessageId by remember { mutableStateOf<Long?>(null) }
|
||||
var dismissedTopAudioMessageId by remember { mutableStateOf<Long?>(null) }
|
||||
var actionMenuMessage by remember { mutableStateOf<MessageItem?>(null) }
|
||||
var pendingDeleteForAll by remember { mutableStateOf(false) }
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
@@ -289,6 +290,9 @@ fun ChatScreen(
|
||||
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
|
||||
val adaptiveHorizontalPadding = if (isTabletLayout) 72.dp else 0.dp
|
||||
val chatInfoEntries = remember(state.messages) { buildChatInfoEntries(state.messages) }
|
||||
val topAudioStrip = remember(state.messages, dismissedTopAudioMessageId) {
|
||||
findTopAudioStrip(state.messages, dismissedTopAudioMessageId)
|
||||
}
|
||||
|
||||
LaunchedEffect(state.isRecordingVoice) {
|
||||
if (!state.isRecordingVoice) return@LaunchedEffect
|
||||
@@ -378,7 +382,7 @@ fun ChatScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.9f))
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
@@ -414,13 +418,14 @@ fun ChatScreen(
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = state.chatTitle.ifBlank { "Chat #${state.chatId}" },
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
)
|
||||
if (state.chatSubtitle.isNotBlank()) {
|
||||
Text(
|
||||
text = state.chatSubtitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
@@ -521,20 +526,27 @@ fun ChatScreen(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.88f))
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.92f))
|
||||
.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(3.dp)
|
||||
.height(30.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.background(MaterialTheme.colorScheme.primary),
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Pinned message",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
text = pinnedMessage.text?.takeIf { it.isNotBlank() } ?: "[${pinnedMessage.type}]",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
maxLines = 2,
|
||||
)
|
||||
}
|
||||
@@ -543,6 +555,56 @@ fun ChatScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (topAudioStrip != null) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.9f))
|
||||
.padding(horizontal = 10.dp, vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Surface(
|
||||
shape = CircleShape,
|
||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
|
||||
modifier = Modifier.size(30.dp),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PlayArrow,
|
||||
contentDescription = "Play top audio",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = topAudioStrip.title,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
maxLines = 1,
|
||||
)
|
||||
Text(
|
||||
text = topAudioStrip.subtitle,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
Surface(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.65f),
|
||||
) {
|
||||
Text(
|
||||
text = "1x",
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { dismissedTopAudioMessageId = topAudioStrip.messageId }) {
|
||||
Icon(imageVector = Icons.Filled.Close, contentDescription = "Hide top audio")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
state.isLoading -> {
|
||||
@@ -2042,3 +2104,38 @@ private fun buildChatInfoEntries(messages: List<MessageItem>): List<ChatInfoEntr
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
private data class TopAudioStrip(
|
||||
val messageId: Long,
|
||||
val title: String,
|
||||
val subtitle: String,
|
||||
)
|
||||
|
||||
private fun findTopAudioStrip(
|
||||
messages: List<MessageItem>,
|
||||
dismissedMessageId: Long?,
|
||||
): TopAudioStrip? {
|
||||
val candidate = messages
|
||||
.asReversed()
|
||||
.firstOrNull { message ->
|
||||
if (dismissedMessageId == message.id) return@firstOrNull false
|
||||
val isVoice = message.type.contains("voice", ignoreCase = true)
|
||||
val isAudio = message.type.contains("audio", ignoreCase = true)
|
||||
isVoice || isAudio || message.attachments.any {
|
||||
it.fileType.lowercase(Locale.getDefault()).startsWith("audio/")
|
||||
}
|
||||
} ?: return null
|
||||
val title = candidate.senderDisplayName?.ifBlank { null }
|
||||
?: candidate.text?.takeIf { it.isNotBlank() }?.take(36)
|
||||
?: "Audio"
|
||||
val subtitle = if (candidate.type.contains("voice", ignoreCase = true)) {
|
||||
"Voice message • ${formatMessageTime(candidate.createdAt)}"
|
||||
} else {
|
||||
"Audio • ${formatMessageTime(candidate.createdAt)}"
|
||||
}
|
||||
return TopAudioStrip(
|
||||
messageId = candidate.id,
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user