From 98e8ac8dfb9ba532c7e67acb85544bc8a2e116c2 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 9 Mar 2026 13:56:27 +0300 Subject: [PATCH] android: add reply-forward preview data foundation --- android/CHANGELOG.md | 9 +++++++++ .../data/chat/local/db/MessengerDatabase.kt | 2 +- .../messenger/data/message/dto/MessageDtos.kt | 12 ++++++++++++ .../message/local/entity/MessageEntity.kt | 6 ++++++ .../message/local/model/MessageLocalModel.kt | 5 +++++ .../data/message/mapper/MessageMappers.kt | 19 ++++++++++++++++--- .../repository/NetworkMessageRepository.kt | 6 ++++++ .../domain/message/model/MessageItem.kt | 3 +++ .../usecase/HandleRealtimeEventsUseCase.kt | 3 +++ .../data/message/local/dao/MessageDaoTest.kt | 3 +++ .../ui/chat/MessageActionStateTest.kt | 3 +++ docs/android-checklist.md | 2 +- 12 files changed, 68 insertions(+), 5 deletions(-) diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 11cd653..10cad6b 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -186,3 +186,12 @@ ### Step 29 - Core base / multi-select delete execution - Fixed multi-select delete behavior in `ChatViewModel`: `Delete` now applies to all selected messages, not only focused one. - Added explicit guard for `Delete for all` in multi-select mode (single-message only). + +### Step 30 - Core base / reply-forward preview data foundation +- Extended message DTO/Room/domain models with optional preview metadata: + - `replyPreviewText`, `replyPreviewSenderName` + - `forwardedFromDisplayName` + - sender profile fields from API payload (`senderDisplayName`, `senderUsername`, `senderAvatarUrl`) +- Added Room self-relation in `MessageLocalModel` to resolve reply preview fallback from referenced message. +- Updated message mappers and repository/realtime temporary entity creation for new model fields. +- Bumped Room schema version to `7`. diff --git a/android/app/src/main/java/ru/daemonlord/messenger/data/chat/local/db/MessengerDatabase.kt b/android/app/src/main/java/ru/daemonlord/messenger/data/chat/local/db/MessengerDatabase.kt index c93cd25..207134a 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/data/chat/local/db/MessengerDatabase.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/data/chat/local/db/MessengerDatabase.kt @@ -16,7 +16,7 @@ import ru.daemonlord.messenger.data.message.local.entity.MessageEntity MessageEntity::class, MessageAttachmentEntity::class, ], - version = 6, + version = 7, exportSchema = false, ) abstract class MessengerDatabase : RoomDatabase() { diff --git a/android/app/src/main/java/ru/daemonlord/messenger/data/message/dto/MessageDtos.kt b/android/app/src/main/java/ru/daemonlord/messenger/data/message/dto/MessageDtos.kt index 7452b99..0f8c016 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/data/message/dto/MessageDtos.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/data/message/dto/MessageDtos.kt @@ -10,10 +10,22 @@ data class MessageReadDto( val chatId: Long, @SerialName("sender_id") val senderId: Long, + @SerialName("sender_display_name") + val senderDisplayName: String? = null, + @SerialName("sender_username") + val senderUsername: String? = null, + @SerialName("sender_avatar_url") + val senderAvatarUrl: String? = null, @SerialName("reply_to_message_id") val replyToMessageId: Long? = null, + @SerialName("reply_preview_text") + val replyPreviewText: String? = null, + @SerialName("reply_preview_sender_name") + val replyPreviewSenderName: String? = null, @SerialName("forwarded_from_message_id") val forwardedFromMessageId: Long? = null, + @SerialName("forwarded_from_display_name") + val forwardedFromDisplayName: String? = null, val type: String, val text: String? = null, @SerialName("delivery_status") diff --git a/android/app/src/main/java/ru/daemonlord/messenger/data/message/local/entity/MessageEntity.kt b/android/app/src/main/java/ru/daemonlord/messenger/data/message/local/entity/MessageEntity.kt index 1b93594..c37fe39 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/data/message/local/entity/MessageEntity.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/data/message/local/entity/MessageEntity.kt @@ -29,8 +29,14 @@ data class MessageEntity( val senderAvatarUrl: String?, @ColumnInfo(name = "reply_to_message_id") val replyToMessageId: Long?, + @ColumnInfo(name = "reply_preview_text") + val replyPreviewText: String?, + @ColumnInfo(name = "reply_preview_sender_name") + val replyPreviewSenderName: String?, @ColumnInfo(name = "forwarded_from_message_id") val forwardedFromMessageId: Long?, + @ColumnInfo(name = "forwarded_from_display_name") + val forwardedFromDisplayName: String?, @ColumnInfo(name = "type") val type: String, @ColumnInfo(name = "text") diff --git a/android/app/src/main/java/ru/daemonlord/messenger/data/message/local/model/MessageLocalModel.kt b/android/app/src/main/java/ru/daemonlord/messenger/data/message/local/model/MessageLocalModel.kt index 13fd368..95260ff 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/data/message/local/model/MessageLocalModel.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/data/message/local/model/MessageLocalModel.kt @@ -13,4 +13,9 @@ data class MessageLocalModel( entityColumn = "message_id", ) val attachments: List, + @Relation( + parentColumn = "reply_to_message_id", + entityColumn = "id", + ) + val replyToMessage: MessageEntity?, ) diff --git a/android/app/src/main/java/ru/daemonlord/messenger/data/message/mapper/MessageMappers.kt b/android/app/src/main/java/ru/daemonlord/messenger/data/message/mapper/MessageMappers.kt index 770a1f1..1c4b2ee 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/data/message/mapper/MessageMappers.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/data/message/mapper/MessageMappers.kt @@ -18,11 +18,14 @@ fun MessageReadDto.toEntity(): MessageEntity { id = id, chatId = chatId, senderId = senderId, - senderDisplayName = null, - senderUsername = null, - senderAvatarUrl = null, + senderDisplayName = senderDisplayName, + senderUsername = senderUsername, + senderAvatarUrl = senderAvatarUrl, replyToMessageId = replyToMessageId, + replyPreviewText = replyPreviewText, + replyPreviewSenderName = replyPreviewSenderName, forwardedFromMessageId = forwardedFromMessageId, + forwardedFromDisplayName = forwardedFromDisplayName, type = type, text = text, status = deliveryStatus, @@ -33,6 +36,13 @@ fun MessageReadDto.toEntity(): MessageEntity { } fun MessageLocalModel.toDomain(currentUserId: Long?): MessageItem { + val resolvedReplyPreviewText = message.replyPreviewText + ?: replyToMessage?.text + ?: replyToMessage?.type?.let { "[$it]" } + val resolvedReplyPreviewSenderName = message.replyPreviewSenderName + ?: replyToMessage?.senderDisplayName + ?: replyToMessage?.senderUsername?.takeIf { it.isNotBlank() }?.let { "@$it" } + ?: replyToMessage?.senderId?.let { "User #$it" } return MessageItem( id = message.id, chatId = message.chatId, @@ -45,7 +55,10 @@ fun MessageLocalModel.toDomain(currentUserId: Long?): MessageItem { isOutgoing = currentUserId != null && currentUserId == message.senderId, status = message.status, replyToMessageId = message.replyToMessageId, + replyPreviewText = resolvedReplyPreviewText, + replyPreviewSenderName = resolvedReplyPreviewSenderName, forwardedFromMessageId = message.forwardedFromMessageId, + forwardedFromDisplayName = message.forwardedFromDisplayName, attachmentWaveform = message.attachmentWaveformJson.toWaveformOrNull(), attachments = attachments.map { it.toDomain() }, ) diff --git a/android/app/src/main/java/ru/daemonlord/messenger/data/message/repository/NetworkMessageRepository.kt b/android/app/src/main/java/ru/daemonlord/messenger/data/message/repository/NetworkMessageRepository.kt index defffec..39b0a84 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/data/message/repository/NetworkMessageRepository.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/data/message/repository/NetworkMessageRepository.kt @@ -122,7 +122,10 @@ class NetworkMessageRepository @Inject constructor( senderUsername = null, senderAvatarUrl = null, replyToMessageId = replyToMessageId, + replyPreviewText = null, + replyPreviewSenderName = null, forwardedFromMessageId = null, + forwardedFromDisplayName = null, type = "text", text = text, status = "pending", @@ -208,7 +211,10 @@ class NetworkMessageRepository @Inject constructor( senderUsername = null, senderAvatarUrl = null, replyToMessageId = replyToMessageId, + replyPreviewText = null, + replyPreviewSenderName = null, forwardedFromMessageId = null, + forwardedFromDisplayName = null, type = messageType, text = caption, status = "pending", diff --git a/android/app/src/main/java/ru/daemonlord/messenger/domain/message/model/MessageItem.kt b/android/app/src/main/java/ru/daemonlord/messenger/domain/message/model/MessageItem.kt index 5365eb6..b5dae5f 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/domain/message/model/MessageItem.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/domain/message/model/MessageItem.kt @@ -12,7 +12,10 @@ data class MessageItem( val isOutgoing: Boolean, val status: String?, val replyToMessageId: Long?, + val replyPreviewText: String?, + val replyPreviewSenderName: String?, val forwardedFromMessageId: Long?, + val forwardedFromDisplayName: String?, val attachmentWaveform: List?, val attachments: List, ) diff --git a/android/app/src/main/java/ru/daemonlord/messenger/domain/realtime/usecase/HandleRealtimeEventsUseCase.kt b/android/app/src/main/java/ru/daemonlord/messenger/domain/realtime/usecase/HandleRealtimeEventsUseCase.kt index 9a330ff..4b5a016 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/domain/realtime/usecase/HandleRealtimeEventsUseCase.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/domain/realtime/usecase/HandleRealtimeEventsUseCase.kt @@ -48,7 +48,10 @@ class HandleRealtimeEventsUseCase @Inject constructor( senderUsername = null, senderAvatarUrl = null, replyToMessageId = event.replyToMessageId, + replyPreviewText = null, + replyPreviewSenderName = null, forwardedFromMessageId = null, + forwardedFromDisplayName = null, type = event.type ?: "text", text = event.text, status = null, diff --git a/android/app/src/test/java/ru/daemonlord/messenger/data/message/local/dao/MessageDaoTest.kt b/android/app/src/test/java/ru/daemonlord/messenger/data/message/local/dao/MessageDaoTest.kt index bd2d078..5c4eea9 100644 --- a/android/app/src/test/java/ru/daemonlord/messenger/data/message/local/dao/MessageDaoTest.kt +++ b/android/app/src/test/java/ru/daemonlord/messenger/data/message/local/dao/MessageDaoTest.kt @@ -92,7 +92,10 @@ class MessageDaoTest { senderUsername = "user", senderAvatarUrl = null, replyToMessageId = null, + replyPreviewText = null, + replyPreviewSenderName = null, forwardedFromMessageId = null, + forwardedFromDisplayName = null, type = "text", text = text, status = null, diff --git a/android/app/src/test/java/ru/daemonlord/messenger/ui/chat/MessageActionStateTest.kt b/android/app/src/test/java/ru/daemonlord/messenger/ui/chat/MessageActionStateTest.kt index 36257b0..bef388a 100644 --- a/android/app/src/test/java/ru/daemonlord/messenger/ui/chat/MessageActionStateTest.kt +++ b/android/app/src/test/java/ru/daemonlord/messenger/ui/chat/MessageActionStateTest.kt @@ -66,7 +66,10 @@ class MessageActionStateTest { isOutgoing = true, status = "sent", replyToMessageId = null, + replyPreviewText = null, + replyPreviewSenderName = null, forwardedFromMessageId = null, + forwardedFromDisplayName = null, attachmentWaveform = null, attachments = emptyList(), ) diff --git a/docs/android-checklist.md b/docs/android-checklist.md index 70d26bf..32f8bac 100644 --- a/docs/android-checklist.md +++ b/docs/android-checklist.md @@ -18,7 +18,7 @@ - [ ] Версионирование API и feature flags ## 3. Локальное хранение и sync -- [ ] Room для чатов/сообщений/пользователей +- [x] Room для чатов/сообщений/пользователей - [x] DataStore для настроек - [ ] Кэш медиа (Coil/Exo cache) - [ ] Offline-first чтение истории