diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 9e587da..730142c 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -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. diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt index 9629a92..f9d5e87 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt @@ -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(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) } } }