android: refresh chat screen header and composer baseline
Some checks failed
Android CI / android (push) Failing after 5m3s
Android Release / release (push) Has started running
CI / test (push) Has been cancelled

This commit is contained in:
Codex
2026-03-10 01:10:51 +03:00
parent 1099efc8c0
commit 895c132eb2
2 changed files with 81 additions and 45 deletions

View File

@@ -826,3 +826,15 @@
- Added notification cleanup on chat open: - Added notification cleanup on chat open:
- when push-open intent targets a chat in `MainActivity`, - when push-open intent targets a chat in `MainActivity`,
- when `ChatViewModel` enters a chat directly from app UI. - when `ChatViewModel` enters a chat directly from app UI.
### Step 119 - Chat screen visual baseline (Telegram-like start)
- Reworked chat top bar:
- icon back button instead of text button,
- cleaner title/subtitle styling,
- dedicated search icon in top bar (inline search is now collapsible).
- Updated pinned message strip:
- cleaner card styling,
- close icon action instead of full text button.
- Updated composer baseline:
- icon-based emoji/attach/send/mic controls,
- cleaner container styling closer to Telegram-like bottom bar.

View File

@@ -80,16 +80,21 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Forward import androidx.compose.material.icons.automirrored.filled.Forward
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.ArrowDownward import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.ArrowUpward import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.filled.AttachFile import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.DeleteOutline import androidx.compose.material.icons.filled.DeleteOutline
import androidx.compose.material.icons.filled.EmojiEmotions
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Movie import androidx.compose.material.icons.filled.Movie
import androidx.compose.material.icons.filled.RadioButtonChecked import androidx.compose.material.icons.filled.RadioButtonChecked
import androidx.compose.material.icons.filled.RadioButtonUnchecked import androidx.compose.material.icons.filled.RadioButtonUnchecked
import androidx.compose.material.icons.filled.Search
import coil.compose.AsyncImage import coil.compose.AsyncImage
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@@ -255,6 +260,7 @@ fun ChatScreen(
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) }
var showInlineSearch by remember { mutableStateOf(false) }
val actionSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val actionSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val forwardSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val forwardSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840 val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
@@ -304,12 +310,17 @@ fun ChatScreen(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant) .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.9f))
.padding(horizontal = 12.dp, vertical = 10.dp), .padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
Button(onClick = onBack) { Text("Back") } IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
)
}
if (!state.chatAvatarUrl.isNullOrBlank()) { if (!state.chatAvatarUrl.isNullOrBlank()) {
AsyncImage( AsyncImage(
model = state.chatAvatarUrl, model = state.chatAvatarUrl,
@@ -342,39 +353,48 @@ fun ChatScreen(
if (state.chatSubtitle.isNotBlank()) { if (state.chatSubtitle.isNotBlank()) {
Text( Text(
text = state.chatSubtitle, text = state.chatSubtitle,
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} }
} }
IconButton(
onClick = { showInlineSearch = !showInlineSearch },
enabled = !state.isLoadingMore,
) {
Icon(imageVector = Icons.Filled.Search, contentDescription = "Search in chat")
}
IconButton(onClick = onLoadMore, enabled = !state.isLoadingMore) { IconButton(onClick = onLoadMore, enabled = !state.isLoadingMore) {
Icon(imageVector = Icons.Filled.MoreVert, contentDescription = "More") Icon(imageVector = Icons.Filled.MoreVert, contentDescription = "More")
} }
} }
Row( if (showInlineSearch) {
modifier = Modifier Row(
.fillMaxWidth() modifier = Modifier
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.8f)) .fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 6.dp), .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.82f))
horizontalArrangement = Arrangement.spacedBy(6.dp), .padding(horizontal = 10.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp),
) { verticalAlignment = Alignment.CenterVertically,
OutlinedTextField( ) {
value = state.inlineSearchQuery, OutlinedTextField(
onValueChange = onInlineSearchChanged, value = state.inlineSearchQuery,
modifier = Modifier.weight(1f), onValueChange = onInlineSearchChanged,
label = { Text("Search in chat") }, modifier = Modifier.weight(1f),
singleLine = true, label = { Text("Search in chat") },
) singleLine = true,
IconButton( )
onClick = { onJumpInlineSearch(false) }, IconButton(
enabled = state.inlineSearchMatches.isNotEmpty(), onClick = { onJumpInlineSearch(false) },
) { Icon(imageVector = Icons.Filled.ArrowUpward, contentDescription = "Previous match") } enabled = state.inlineSearchMatches.isNotEmpty(),
IconButton( ) { Icon(imageVector = Icons.Filled.ArrowUpward, contentDescription = "Previous match") }
onClick = { onJumpInlineSearch(true) }, IconButton(
enabled = state.inlineSearchMatches.isNotEmpty(), onClick = { onJumpInlineSearch(true) },
) { Icon(imageVector = Icons.Filled.ArrowDownward, contentDescription = "Next match") } enabled = state.inlineSearchMatches.isNotEmpty(),
) { Icon(imageVector = Icons.Filled.ArrowDownward, contentDescription = "Next match") }
}
} }
if (state.inlineSearchMatches.isNotEmpty()) { if (showInlineSearch && state.inlineSearchMatches.isNotEmpty()) {
Text( Text(
text = "Matches: ${state.inlineSearchMatches.size}", text = "Matches: ${state.inlineSearchMatches.size}",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp), modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
@@ -386,7 +406,7 @@ fun ChatScreen(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.85f)) .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.88f))
.padding(horizontal = 12.dp, vertical = 8.dp), .padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -403,11 +423,8 @@ fun ChatScreen(
maxLines = 2, maxLines = 2,
) )
} }
Button( IconButton(onClick = { dismissedPinnedMessageId = pinnedMessage.id }) {
onClick = { dismissedPinnedMessageId = pinnedMessage.id }, Icon(imageVector = Icons.Filled.Close, contentDescription = "Hide pinned")
modifier = Modifier.padding(start = 4.dp),
) {
Text("Hide")
} }
} }
} }
@@ -668,8 +685,8 @@ fun ChatScreen(
.navigationBarsPadding() .navigationBarsPadding()
.imePadding() .imePadding()
.padding(horizontal = 10.dp, vertical = 8.dp), .padding(horizontal = 10.dp, vertical = 8.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.92f), color = MaterialTheme.colorScheme.surface.copy(alpha = 0.92f),
shape = RoundedCornerShape(22.dp), shape = RoundedCornerShape(24.dp),
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -678,11 +695,14 @@ fun ChatScreen(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp), horizontalArrangement = Arrangement.spacedBy(6.dp),
) { ) {
Button( IconButton(
onClick = { /* emoji placeholder */ }, onClick = { /* emoji picker step */ },
enabled = state.canSendMessages, enabled = state.canSendMessages,
) { ) {
Text("\uD83D\uDE03") Icon(
imageVector = Icons.Filled.EmojiEmotions,
contentDescription = "Emoji",
)
} }
OutlinedTextField( OutlinedTextField(
value = state.inputText, value = state.inputText,
@@ -691,22 +711,26 @@ fun ChatScreen(
placeholder = { Text("Message") }, placeholder = { Text("Message") },
maxLines = 4, maxLines = 4,
) )
Button( IconButton(
onClick = onPickMedia, onClick = onPickMedia,
enabled = state.canSendMessages && !state.isUploadingMedia && !state.isRecordingVoice, enabled = state.canSendMessages && !state.isUploadingMedia && !state.isRecordingVoice,
) { ) {
Text(if (state.isUploadingMedia) "..." else "\uD83D\uDCCE") if (state.isUploadingMedia) {
CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp)
} else {
Icon(imageVector = Icons.Filled.AttachFile, contentDescription = "Attach")
}
} }
val canSend = state.canSendMessages && val canSend = state.canSendMessages &&
!state.isSending && !state.isSending &&
!state.isUploadingMedia && !state.isUploadingMedia &&
state.inputText.isNotBlank() state.inputText.isNotBlank()
if (canSend) { if (canSend) {
Button( IconButton(
onClick = onSendClick, onClick = onSendClick,
enabled = state.canSendMessages && !state.isUploadingMedia, enabled = state.canSendMessages && !state.isUploadingMedia,
) { ) {
Text("\u27A4") Icon(imageVector = Icons.AutoMirrored.Filled.Send, contentDescription = "Send")
} }
} else { } else {
VoiceHoldToRecordButton( VoiceHoldToRecordButton(
@@ -1474,7 +1498,7 @@ private fun VoiceHoldToRecordButton(
enabled = enabled, enabled = enabled,
modifier = Modifier.semantics { contentDescription = "Hold to record voice message" }, modifier = Modifier.semantics { contentDescription = "Hold to record voice message" },
) { ) {
Text("\uD83C\uDFA4") Icon(imageVector = Icons.Filled.Mic, contentDescription = null)
} }
} }
} }