android: add reply-forward preview data foundation
Some checks are pending
CI / test (push) Has started running

This commit is contained in:
Codex
2026-03-09 13:56:27 +03:00
parent 071165c55b
commit 98e8ac8dfb
12 changed files with 68 additions and 5 deletions

View File

@@ -186,3 +186,12 @@
### Step 29 - Core base / multi-select delete execution ### 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. - 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). - 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`.

View File

@@ -16,7 +16,7 @@ import ru.daemonlord.messenger.data.message.local.entity.MessageEntity
MessageEntity::class, MessageEntity::class,
MessageAttachmentEntity::class, MessageAttachmentEntity::class,
], ],
version = 6, version = 7,
exportSchema = false, exportSchema = false,
) )
abstract class MessengerDatabase : RoomDatabase() { abstract class MessengerDatabase : RoomDatabase() {

View File

@@ -10,10 +10,22 @@ data class MessageReadDto(
val chatId: Long, val chatId: Long,
@SerialName("sender_id") @SerialName("sender_id")
val senderId: Long, 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") @SerialName("reply_to_message_id")
val replyToMessageId: Long? = null, 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") @SerialName("forwarded_from_message_id")
val forwardedFromMessageId: Long? = null, val forwardedFromMessageId: Long? = null,
@SerialName("forwarded_from_display_name")
val forwardedFromDisplayName: String? = null,
val type: String, val type: String,
val text: String? = null, val text: String? = null,
@SerialName("delivery_status") @SerialName("delivery_status")

View File

@@ -29,8 +29,14 @@ data class MessageEntity(
val senderAvatarUrl: String?, val senderAvatarUrl: String?,
@ColumnInfo(name = "reply_to_message_id") @ColumnInfo(name = "reply_to_message_id")
val replyToMessageId: Long?, 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") @ColumnInfo(name = "forwarded_from_message_id")
val forwardedFromMessageId: Long?, val forwardedFromMessageId: Long?,
@ColumnInfo(name = "forwarded_from_display_name")
val forwardedFromDisplayName: String?,
@ColumnInfo(name = "type") @ColumnInfo(name = "type")
val type: String, val type: String,
@ColumnInfo(name = "text") @ColumnInfo(name = "text")

View File

@@ -13,4 +13,9 @@ data class MessageLocalModel(
entityColumn = "message_id", entityColumn = "message_id",
) )
val attachments: List<MessageAttachmentEntity>, val attachments: List<MessageAttachmentEntity>,
@Relation(
parentColumn = "reply_to_message_id",
entityColumn = "id",
)
val replyToMessage: MessageEntity?,
) )

View File

@@ -18,11 +18,14 @@ fun MessageReadDto.toEntity(): MessageEntity {
id = id, id = id,
chatId = chatId, chatId = chatId,
senderId = senderId, senderId = senderId,
senderDisplayName = null, senderDisplayName = senderDisplayName,
senderUsername = null, senderUsername = senderUsername,
senderAvatarUrl = null, senderAvatarUrl = senderAvatarUrl,
replyToMessageId = replyToMessageId, replyToMessageId = replyToMessageId,
replyPreviewText = replyPreviewText,
replyPreviewSenderName = replyPreviewSenderName,
forwardedFromMessageId = forwardedFromMessageId, forwardedFromMessageId = forwardedFromMessageId,
forwardedFromDisplayName = forwardedFromDisplayName,
type = type, type = type,
text = text, text = text,
status = deliveryStatus, status = deliveryStatus,
@@ -33,6 +36,13 @@ fun MessageReadDto.toEntity(): MessageEntity {
} }
fun MessageLocalModel.toDomain(currentUserId: Long?): MessageItem { 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( return MessageItem(
id = message.id, id = message.id,
chatId = message.chatId, chatId = message.chatId,
@@ -45,7 +55,10 @@ fun MessageLocalModel.toDomain(currentUserId: Long?): MessageItem {
isOutgoing = currentUserId != null && currentUserId == message.senderId, isOutgoing = currentUserId != null && currentUserId == message.senderId,
status = message.status, status = message.status,
replyToMessageId = message.replyToMessageId, replyToMessageId = message.replyToMessageId,
replyPreviewText = resolvedReplyPreviewText,
replyPreviewSenderName = resolvedReplyPreviewSenderName,
forwardedFromMessageId = message.forwardedFromMessageId, forwardedFromMessageId = message.forwardedFromMessageId,
forwardedFromDisplayName = message.forwardedFromDisplayName,
attachmentWaveform = message.attachmentWaveformJson.toWaveformOrNull(), attachmentWaveform = message.attachmentWaveformJson.toWaveformOrNull(),
attachments = attachments.map { it.toDomain() }, attachments = attachments.map { it.toDomain() },
) )

View File

@@ -122,7 +122,10 @@ class NetworkMessageRepository @Inject constructor(
senderUsername = null, senderUsername = null,
senderAvatarUrl = null, senderAvatarUrl = null,
replyToMessageId = replyToMessageId, replyToMessageId = replyToMessageId,
replyPreviewText = null,
replyPreviewSenderName = null,
forwardedFromMessageId = null, forwardedFromMessageId = null,
forwardedFromDisplayName = null,
type = "text", type = "text",
text = text, text = text,
status = "pending", status = "pending",
@@ -208,7 +211,10 @@ class NetworkMessageRepository @Inject constructor(
senderUsername = null, senderUsername = null,
senderAvatarUrl = null, senderAvatarUrl = null,
replyToMessageId = replyToMessageId, replyToMessageId = replyToMessageId,
replyPreviewText = null,
replyPreviewSenderName = null,
forwardedFromMessageId = null, forwardedFromMessageId = null,
forwardedFromDisplayName = null,
type = messageType, type = messageType,
text = caption, text = caption,
status = "pending", status = "pending",

View File

@@ -12,7 +12,10 @@ data class MessageItem(
val isOutgoing: Boolean, val isOutgoing: Boolean,
val status: String?, val status: String?,
val replyToMessageId: Long?, val replyToMessageId: Long?,
val replyPreviewText: String?,
val replyPreviewSenderName: String?,
val forwardedFromMessageId: Long?, val forwardedFromMessageId: Long?,
val forwardedFromDisplayName: String?,
val attachmentWaveform: List<Int>?, val attachmentWaveform: List<Int>?,
val attachments: List<MessageAttachment>, val attachments: List<MessageAttachment>,
) )

View File

@@ -48,7 +48,10 @@ class HandleRealtimeEventsUseCase @Inject constructor(
senderUsername = null, senderUsername = null,
senderAvatarUrl = null, senderAvatarUrl = null,
replyToMessageId = event.replyToMessageId, replyToMessageId = event.replyToMessageId,
replyPreviewText = null,
replyPreviewSenderName = null,
forwardedFromMessageId = null, forwardedFromMessageId = null,
forwardedFromDisplayName = null,
type = event.type ?: "text", type = event.type ?: "text",
text = event.text, text = event.text,
status = null, status = null,

View File

@@ -92,7 +92,10 @@ class MessageDaoTest {
senderUsername = "user", senderUsername = "user",
senderAvatarUrl = null, senderAvatarUrl = null,
replyToMessageId = null, replyToMessageId = null,
replyPreviewText = null,
replyPreviewSenderName = null,
forwardedFromMessageId = null, forwardedFromMessageId = null,
forwardedFromDisplayName = null,
type = "text", type = "text",
text = text, text = text,
status = null, status = null,

View File

@@ -66,7 +66,10 @@ class MessageActionStateTest {
isOutgoing = true, isOutgoing = true,
status = "sent", status = "sent",
replyToMessageId = null, replyToMessageId = null,
replyPreviewText = null,
replyPreviewSenderName = null,
forwardedFromMessageId = null, forwardedFromMessageId = null,
forwardedFromDisplayName = null,
attachmentWaveform = null, attachmentWaveform = null,
attachments = emptyList(), attachments = emptyList(),
) )

View File

@@ -18,7 +18,7 @@
- [ ] Версионирование API и feature flags - [ ] Версионирование API и feature flags
## 3. Локальное хранение и sync ## 3. Локальное хранение и sync
- [ ] Room для чатов/сообщений/пользователей - [x] Room для чатов/сообщений/пользователей
- [x] DataStore для настроек - [x] DataStore для настроек
- [ ] Кэш медиа (Coil/Exo cache) - [ ] Кэш медиа (Coil/Exo cache)
- [ ] Offline-first чтение истории - [ ] Offline-first чтение истории