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.
|
||||
- Rendered pinned message bar under chat app bar with hide action.
|
||||
- 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.shape.RoundedCornerShape
|
||||
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.items
|
||||
import androidx.compose.material3.Button
|
||||
@@ -43,6 +45,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
@@ -125,15 +128,50 @@ fun ChatScreen(
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Button(onClick = onBack) { Text("Back") }
|
||||
Text(
|
||||
text = "Chat #${state.chatId}",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
if (!state.chatAvatarUrl.isNullOrBlank()) {
|
||||
AsyncImage(
|
||||
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) {
|
||||
Text(if (state.isLoadingMore) "..." else "Load older")
|
||||
Text("⋮")
|
||||
}
|
||||
}
|
||||
val pinnedMessage = state.pinnedMessage
|
||||
|
||||
@@ -455,11 +455,22 @@ class ChatViewModel @Inject constructor(
|
||||
} else {
|
||||
"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 {
|
||||
val pinnedMessage = chat.pinnedMessageId?.let { pinnedId ->
|
||||
it.messages.firstOrNull { message -> message.id == pinnedId }
|
||||
}
|
||||
it.copy(
|
||||
chatTitle = chatTitle,
|
||||
chatSubtitle = chatSubtitle,
|
||||
chatAvatarUrl = chat.avatarUrl ?: chat.counterpartAvatarUrl,
|
||||
canSendMessages = canSend,
|
||||
sendRestrictionText = restriction,
|
||||
pinnedMessageId = chat.pinnedMessageId,
|
||||
|
||||
@@ -10,6 +10,9 @@ data class MessageUiState(
|
||||
val isSending: Boolean = false,
|
||||
val isUploadingMedia: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
val chatTitle: String = "",
|
||||
val chatSubtitle: String = "",
|
||||
val chatAvatarUrl: String? = null,
|
||||
val messages: List<MessageItem> = emptyList(),
|
||||
val pinnedMessageId: Long? = null,
|
||||
val pinnedMessage: MessageItem? = null,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Источник: 10 скринов Telegram Android (чат, контекстные действия, медиа, поиск/чаты)
|
||||
|
||||
## 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 для перехода).
|
||||
- [ ] Message composer Telegram-стиля:
|
||||
- [ ] Полупрозрачный rounded input контейнер.
|
||||
|
||||
Reference in New Issue
Block a user