android: add pinned message bar support in chat
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:
@@ -200,3 +200,9 @@
|
||||
- Added inline forwarded header rendering in message bubbles with display-name fallback.
|
||||
- Added inline reply preview block in message bubbles (author + snippet) based on new preview fields/fallbacks.
|
||||
- Updated Telegram UI batch-2 checklist items for reply-preview and forwarded header.
|
||||
|
||||
### Step 32 - Chat UI / pinned message bar
|
||||
- Added `pinned_message_id` support in chat DTO/local/domain models and DAO selects.
|
||||
- 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.
|
||||
|
||||
@@ -40,6 +40,8 @@ data class ChatReadDto(
|
||||
val lastMessageType: String? = null,
|
||||
@SerialName("last_message_created_at")
|
||||
val lastMessageCreatedAt: String? = null,
|
||||
@SerialName("pinned_message_id")
|
||||
val pinnedMessageId: Long? = null,
|
||||
@SerialName("my_role")
|
||||
val myRole: String? = null,
|
||||
@SerialName("created_at")
|
||||
|
||||
@@ -36,6 +36,7 @@ interface ChatDao {
|
||||
c.last_message_text,
|
||||
c.last_message_type,
|
||||
c.last_message_created_at,
|
||||
c.pinned_message_id,
|
||||
c.my_role,
|
||||
c.updated_sort_at
|
||||
FROM chats c
|
||||
@@ -69,6 +70,7 @@ interface ChatDao {
|
||||
c.last_message_text,
|
||||
c.last_message_type,
|
||||
c.last_message_created_at,
|
||||
c.pinned_message_id,
|
||||
c.my_role,
|
||||
c.updated_sort_at
|
||||
FROM chats c
|
||||
|
||||
@@ -16,7 +16,7 @@ import ru.daemonlord.messenger.data.message.local.entity.MessageEntity
|
||||
MessageEntity::class,
|
||||
MessageAttachmentEntity::class,
|
||||
],
|
||||
version = 7,
|
||||
version = 8,
|
||||
exportSchema = false,
|
||||
)
|
||||
abstract class MessengerDatabase : RoomDatabase() {
|
||||
|
||||
@@ -56,6 +56,8 @@ data class ChatEntity(
|
||||
val lastMessageType: String?,
|
||||
@ColumnInfo(name = "last_message_created_at")
|
||||
val lastMessageCreatedAt: String?,
|
||||
@ColumnInfo(name = "pinned_message_id")
|
||||
val pinnedMessageId: Long?,
|
||||
@ColumnInfo(name = "my_role")
|
||||
val myRole: String?,
|
||||
@ColumnInfo(name = "updated_sort_at")
|
||||
|
||||
@@ -43,6 +43,8 @@ data class ChatListLocalModel(
|
||||
val lastMessageType: String?,
|
||||
@ColumnInfo(name = "last_message_created_at")
|
||||
val lastMessageCreatedAt: String?,
|
||||
@ColumnInfo(name = "pinned_message_id")
|
||||
val pinnedMessageId: Long?,
|
||||
@ColumnInfo(name = "my_role")
|
||||
val myRole: String?,
|
||||
@ColumnInfo(name = "updated_sort_at")
|
||||
|
||||
@@ -26,6 +26,7 @@ fun ChatListLocalModel.toDomain(): ChatItem {
|
||||
lastMessageText = lastMessageText,
|
||||
lastMessageType = lastMessageType,
|
||||
lastMessageCreatedAt = lastMessageCreatedAt,
|
||||
pinnedMessageId = pinnedMessageId,
|
||||
myRole = myRole,
|
||||
updatedSortAt = updatedSortAt,
|
||||
)
|
||||
@@ -53,6 +54,7 @@ fun ChatEntity.toDomain(): ChatItem {
|
||||
lastMessageText = lastMessageText,
|
||||
lastMessageType = lastMessageType,
|
||||
lastMessageCreatedAt = lastMessageCreatedAt,
|
||||
pinnedMessageId = pinnedMessageId,
|
||||
myRole = myRole,
|
||||
updatedSortAt = updatedSortAt,
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ fun ChatReadDto.toChatEntity(): ChatEntity {
|
||||
lastMessageText = lastMessageText,
|
||||
lastMessageType = lastMessageType,
|
||||
lastMessageCreatedAt = lastMessageCreatedAt,
|
||||
pinnedMessageId = pinnedMessageId,
|
||||
myRole = myRole,
|
||||
updatedSortAt = lastMessageCreatedAt ?: createdAt,
|
||||
)
|
||||
|
||||
@@ -113,6 +113,7 @@ fun ChatScreen(
|
||||
onPickMedia: () -> Unit,
|
||||
) {
|
||||
var viewerImageUrl by remember { mutableStateOf<String?>(null) }
|
||||
var dismissedPinnedMessageId by remember { mutableStateOf<Long?>(null) }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -135,6 +136,36 @@ fun ChatScreen(
|
||||
Text(if (state.isLoadingMore) "..." else "Load older")
|
||||
}
|
||||
}
|
||||
val pinnedMessage = state.pinnedMessage
|
||||
if (pinnedMessage != null && dismissedPinnedMessageId != pinnedMessage.id) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.85f))
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Pinned message",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
text = pinnedMessage.text?.takeIf { it.isNotBlank() } ?: "[${pinnedMessage.type}]",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 2,
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = { dismissedPinnedMessageId = pinnedMessage.id },
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
) {
|
||||
Text("Hide")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
state.isLoading -> {
|
||||
|
||||
@@ -429,9 +429,11 @@ class ChatViewModel @Inject constructor(
|
||||
viewModelScope.launch {
|
||||
observeMessagesUseCase(chatId).collectLatest { messages ->
|
||||
_uiState.update {
|
||||
val pinnedId = it.pinnedMessageId
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
messages = messages.sortedBy { msg -> msg.id },
|
||||
pinnedMessage = pinnedId?.let { id -> messages.firstOrNull { msg -> msg.id == id } },
|
||||
)
|
||||
}
|
||||
acknowledgeLatestIncoming(messages)
|
||||
@@ -454,9 +456,14 @@ class ChatViewModel @Inject constructor(
|
||||
"Only channel owner/admin can send messages."
|
||||
}
|
||||
_uiState.update {
|
||||
val pinnedMessage = chat.pinnedMessageId?.let { pinnedId ->
|
||||
it.messages.firstOrNull { message -> message.id == pinnedId }
|
||||
}
|
||||
it.copy(
|
||||
canSendMessages = canSend,
|
||||
sendRestrictionText = restriction,
|
||||
pinnedMessageId = chat.pinnedMessageId,
|
||||
pinnedMessage = pinnedMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ data class MessageUiState(
|
||||
val isUploadingMedia: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
val messages: List<MessageItem> = emptyList(),
|
||||
val pinnedMessageId: Long? = null,
|
||||
val pinnedMessage: MessageItem? = null,
|
||||
val inputText: String = "",
|
||||
val replyToMessage: MessageItem? = null,
|
||||
val editingMessage: MessageItem? = null,
|
||||
|
||||
@@ -88,6 +88,7 @@ class ChatDaoTest {
|
||||
lastMessageText = "hi",
|
||||
lastMessageType = "text",
|
||||
lastMessageCreatedAt = "2026-03-08T10:00:00Z",
|
||||
pinnedMessageId = null,
|
||||
myRole = "member",
|
||||
updatedSortAt = "2026-03-08T10:00:00Z",
|
||||
)
|
||||
|
||||
@@ -51,6 +51,7 @@ class NetworkChatRepositoryTest {
|
||||
lastMessageText = "Cached message",
|
||||
lastMessageType = "text",
|
||||
lastMessageCreatedAt = "2026-03-08T10:00:00Z",
|
||||
pinnedMessageId = null,
|
||||
myRole = "member",
|
||||
updatedSortAt = "2026-03-08T10:00:00Z",
|
||||
)
|
||||
@@ -195,6 +196,7 @@ class NetworkChatRepositoryTest {
|
||||
lastMessageText = lastMessageText,
|
||||
lastMessageType = lastMessageType,
|
||||
lastMessageCreatedAt = lastMessageCreatedAt,
|
||||
pinnedMessageId = pinnedMessageId,
|
||||
myRole = myRole,
|
||||
updatedSortAt = updatedSortAt,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
## P0 — Chat Screen Parity (must-have)
|
||||
- [ ] Top app bar чата: back + avatar + name + status + call + menu, полупрозрачная подложка на фоне обоев.
|
||||
- [ ] Закреплённое сообщение блоком под app bar (2 строки, иконки pin/close, tap для перехода).
|
||||
- [x] Закреплённое сообщение блоком под app bar (2 строки, иконки pin/close, tap для перехода).
|
||||
- [ ] Message composer Telegram-стиля:
|
||||
- [ ] Полупрозрачный rounded input контейнер.
|
||||
- [ ] Иконка emoji слева, поле ввода, скрепка, кнопка отправки/голосового.
|
||||
|
||||
Reference in New Issue
Block a user