android: refresh chat screen header and composer baseline
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user