android: restructure chat top app bar with header metadata
Some checks are pending
CI / test (push) Has started running
Some checks are pending
CI / test (push) Has started running
This commit is contained in:
@@ -206,3 +206,9 @@
|
|||||||
- Extended `ChatViewModel` state with pinned message id + resolved pinned message object.
|
- Extended `ChatViewModel` state with pinned message id + resolved pinned message object.
|
||||||
- Rendered pinned message bar under chat app bar with hide action.
|
- Rendered pinned message bar under chat app bar with hide action.
|
||||||
- Updated Telegram UI batch-2 checklist item for pinned message block.
|
- Updated Telegram UI batch-2 checklist item for pinned message block.
|
||||||
|
|
||||||
|
### Step 33 - Chat UI / top app bar restructuring
|
||||||
|
- Extended chat UI state with resolved chat header fields (`chatTitle`, `chatSubtitle`, `chatAvatarUrl`).
|
||||||
|
- Updated chat top app bar layout to Telegram-like structure: back, avatar, title, status, call action, menu action.
|
||||||
|
- Kept load-more behavior accessible via menu placeholder action button.
|
||||||
|
- Updated Telegram UI batch-2 checklist item for chat top app bar.
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
@@ -43,6 +45,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
@@ -125,15 +128,50 @@ fun ChatScreen(
|
|||||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
) {
|
) {
|
||||||
Button(onClick = onBack) { Text("Back") }
|
Button(onClick = onBack) { Text("Back") }
|
||||||
Text(
|
if (!state.chatAvatarUrl.isNullOrBlank()) {
|
||||||
text = "Chat #${state.chatId}",
|
AsyncImage(
|
||||||
style = MaterialTheme.typography.titleMedium,
|
model = state.chatAvatarUrl,
|
||||||
)
|
contentDescription = "Chat avatar",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = state.chatTitle.firstOrNull()?.uppercase() ?: "?",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = state.chatTitle.ifBlank { "Chat #${state.chatId}" },
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
if (state.chatSubtitle.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = state.chatSubtitle,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(onClick = { /* call action placeholder */ }) {
|
||||||
|
Text("Call")
|
||||||
|
}
|
||||||
Button(onClick = onLoadMore, enabled = !state.isLoadingMore) {
|
Button(onClick = onLoadMore, enabled = !state.isLoadingMore) {
|
||||||
Text(if (state.isLoadingMore) "..." else "Load older")
|
Text("⋮")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val pinnedMessage = state.pinnedMessage
|
val pinnedMessage = state.pinnedMessage
|
||||||
|
|||||||
@@ -455,11 +455,22 @@ class ChatViewModel @Inject constructor(
|
|||||||
} else {
|
} else {
|
||||||
"Only channel owner/admin can send messages."
|
"Only channel owner/admin can send messages."
|
||||||
}
|
}
|
||||||
|
val chatTitle = chat.displayTitle.ifBlank { "Chat #$chatId" }
|
||||||
|
val chatSubtitle = when {
|
||||||
|
chat.type.equals("private", ignoreCase = true) && chat.counterpartIsOnline == true -> "online"
|
||||||
|
chat.type.equals("private", ignoreCase = true) && !chat.counterpartLastSeenAt.isNullOrBlank() -> "last seen recently"
|
||||||
|
chat.type.equals("group", ignoreCase = true) -> "group"
|
||||||
|
chat.type.equals("channel", ignoreCase = true) -> "channel"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
val pinnedMessage = chat.pinnedMessageId?.let { pinnedId ->
|
val pinnedMessage = chat.pinnedMessageId?.let { pinnedId ->
|
||||||
it.messages.firstOrNull { message -> message.id == pinnedId }
|
it.messages.firstOrNull { message -> message.id == pinnedId }
|
||||||
}
|
}
|
||||||
it.copy(
|
it.copy(
|
||||||
|
chatTitle = chatTitle,
|
||||||
|
chatSubtitle = chatSubtitle,
|
||||||
|
chatAvatarUrl = chat.avatarUrl ?: chat.counterpartAvatarUrl,
|
||||||
canSendMessages = canSend,
|
canSendMessages = canSend,
|
||||||
sendRestrictionText = restriction,
|
sendRestrictionText = restriction,
|
||||||
pinnedMessageId = chat.pinnedMessageId,
|
pinnedMessageId = chat.pinnedMessageId,
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ data class MessageUiState(
|
|||||||
val isSending: Boolean = false,
|
val isSending: Boolean = false,
|
||||||
val isUploadingMedia: Boolean = false,
|
val isUploadingMedia: Boolean = false,
|
||||||
val errorMessage: String? = null,
|
val errorMessage: String? = null,
|
||||||
|
val chatTitle: String = "",
|
||||||
|
val chatSubtitle: String = "",
|
||||||
|
val chatAvatarUrl: String? = null,
|
||||||
val messages: List<MessageItem> = emptyList(),
|
val messages: List<MessageItem> = emptyList(),
|
||||||
val pinnedMessageId: Long? = null,
|
val pinnedMessageId: Long? = null,
|
||||||
val pinnedMessage: MessageItem? = null,
|
val pinnedMessage: MessageItem? = null,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
Источник: 10 скринов Telegram Android (чат, контекстные действия, медиа, поиск/чаты)
|
Источник: 10 скринов Telegram Android (чат, контекстные действия, медиа, поиск/чаты)
|
||||||
|
|
||||||
## P0 — Chat Screen Parity (must-have)
|
## P0 — Chat Screen Parity (must-have)
|
||||||
- [ ] Top app bar чата: back + avatar + name + status + call + menu, полупрозрачная подложка на фоне обоев.
|
- [x] Top app bar чата: back + avatar + name + status + call + menu, полупрозрачная подложка на фоне обоев.
|
||||||
- [x] Закреплённое сообщение блоком под app bar (2 строки, иконки pin/close, tap для перехода).
|
- [x] Закреплённое сообщение блоком под app bar (2 строки, иконки pin/close, tap для перехода).
|
||||||
- [ ] Message composer Telegram-стиля:
|
- [ ] Message composer Telegram-стиля:
|
||||||
- [ ] Полупрозрачный rounded input контейнер.
|
- [ ] Полупрозрачный rounded input контейнер.
|
||||||
|
|||||||
Reference in New Issue
Block a user