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