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