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.
|
- closing search now also clears active query/filter without re-entering chat.
|
||||||
- Added automatic inline-search collapse when entering multi-select mode.
|
- 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.
|
- 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 viewerImageIndex by remember { mutableStateOf<Int?>(null) }
|
||||||
var dismissedPinnedMessageId by remember { mutableStateOf<Long?>(null) }
|
var dismissedPinnedMessageId by remember { mutableStateOf<Long?>(null) }
|
||||||
|
var dismissedTopAudioMessageId by remember { mutableStateOf<Long?>(null) }
|
||||||
var actionMenuMessage by remember { mutableStateOf<MessageItem?>(null) }
|
var actionMenuMessage by remember { mutableStateOf<MessageItem?>(null) }
|
||||||
var pendingDeleteForAll by remember { mutableStateOf(false) }
|
var pendingDeleteForAll by remember { mutableStateOf(false) }
|
||||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||||
@@ -289,6 +290,9 @@ fun ChatScreen(
|
|||||||
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
|
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
|
||||||
val adaptiveHorizontalPadding = if (isTabletLayout) 72.dp else 0.dp
|
val adaptiveHorizontalPadding = if (isTabletLayout) 72.dp else 0.dp
|
||||||
val chatInfoEntries = remember(state.messages) { buildChatInfoEntries(state.messages) }
|
val chatInfoEntries = remember(state.messages) { buildChatInfoEntries(state.messages) }
|
||||||
|
val topAudioStrip = remember(state.messages, dismissedTopAudioMessageId) {
|
||||||
|
findTopAudioStrip(state.messages, dismissedTopAudioMessageId)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.isRecordingVoice) {
|
LaunchedEffect(state.isRecordingVoice) {
|
||||||
if (!state.isRecordingVoice) return@LaunchedEffect
|
if (!state.isRecordingVoice) return@LaunchedEffect
|
||||||
@@ -378,7 +382,7 @@ fun ChatScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.9f))
|
.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,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
@@ -414,13 +418,14 @@ fun ChatScreen(
|
|||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = state.chatTitle.ifBlank { "Chat #${state.chatId}" },
|
text = state.chatTitle.ifBlank { "Chat #${state.chatId}" },
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
if (state.chatSubtitle.isNotBlank()) {
|
if (state.chatSubtitle.isNotBlank()) {
|
||||||
Text(
|
Text(
|
||||||
text = state.chatSubtitle,
|
text = state.chatSubtitle,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -521,20 +526,27 @@ fun ChatScreen(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.88f))
|
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.92f))
|
||||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
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)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = "Pinned message",
|
text = "Pinned message",
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = pinnedMessage.text?.takeIf { it.isNotBlank() } ?: "[${pinnedMessage.type}]",
|
text = pinnedMessage.text?.takeIf { it.isNotBlank() } ?: "[${pinnedMessage.type}]",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
maxLines = 2,
|
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 {
|
when {
|
||||||
state.isLoading -> {
|
state.isLoading -> {
|
||||||
@@ -2042,3 +2104,38 @@ private fun buildChatInfoEntries(messages: List<MessageItem>): List<ChatInfoEntr
|
|||||||
}
|
}
|
||||||
return entries
|
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