diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 38cc4d6..0ddd576 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -64,3 +64,10 @@ - Fixed Hilt dependency cycle by separating refresh `AuthApiService` with a dedicated qualifier. - Added `CoroutineDispatcher` DI provider and qualifier for repositories. - Fixed Material3 experimental API opt-in and removed deprecated `StateFlow.distinctUntilChanged()` usage. + +### Step 11 - Sprint A / 1) Message Room + models +- Added message domain model (`MessageItem`) for chat screen rendering. +- Added Room entities `messages` and `message_attachments` with chat-history indexes. +- Added `MessageDao` with observe/pagination/upsert/delete APIs. +- Updated `MessengerDatabase` schema to include message tables and DAO. +- Added Hilt DI provider for `MessageDao`. 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 c9866b6..760cfe2 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 @@ -5,15 +5,21 @@ import androidx.room.RoomDatabase import ru.daemonlord.messenger.data.chat.local.dao.ChatDao import ru.daemonlord.messenger.data.chat.local.entity.ChatEntity import ru.daemonlord.messenger.data.chat.local.entity.UserShortEntity +import ru.daemonlord.messenger.data.message.local.dao.MessageDao +import ru.daemonlord.messenger.data.message.local.entity.MessageAttachmentEntity +import ru.daemonlord.messenger.data.message.local.entity.MessageEntity @Database( entities = [ ChatEntity::class, UserShortEntity::class, + MessageEntity::class, + MessageAttachmentEntity::class, ], - version = 1, + version = 2, exportSchema = false, ) abstract class MessengerDatabase : RoomDatabase() { abstract fun chatDao(): ChatDao + abstract fun messageDao(): MessageDao } diff --git a/android/app/src/main/java/ru/daemonlord/messenger/data/message/local/dao/MessageDao.kt b/android/app/src/main/java/ru/daemonlord/messenger/data/message/local/dao/MessageDao.kt new file mode 100644 index 0000000..963398a --- /dev/null +++ b/android/app/src/main/java/ru/daemonlord/messenger/data/message/local/dao/MessageDao.kt @@ -0,0 +1,67 @@ +package ru.daemonlord.messenger.data.message.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import kotlinx.coroutines.flow.Flow +import ru.daemonlord.messenger.data.message.local.entity.MessageAttachmentEntity +import ru.daemonlord.messenger.data.message.local.entity.MessageEntity + +@Dao +interface MessageDao { + + @Query( + """ + SELECT * FROM messages + WHERE chat_id = :chatId + ORDER BY created_at DESC, id DESC + LIMIT :limit + """ + ) + fun observeRecentMessages( + chatId: Long, + limit: Int = 50, + ): Flow> + + @Query( + """ + SELECT * FROM messages + WHERE chat_id = :chatId + AND id < :beforeMessageId + ORDER BY created_at DESC, id DESC + LIMIT :limit + """ + ) + suspend fun getMessagesPage( + chatId: Long, + beforeMessageId: Long, + limit: Int = 50, + ): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertMessages(messages: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertAttachments(attachments: List) + + @Query("DELETE FROM messages WHERE chat_id = :chatId") + suspend fun clearChatMessages(chatId: Long) + + @Query("DELETE FROM messages WHERE id = :messageId") + suspend fun deleteMessage(messageId: Long) + + @Transaction + suspend fun clearAndReplaceMessages( + chatId: Long, + messages: List, + attachments: List, + ) { + clearChatMessages(chatId = chatId) + upsertMessages(messages) + if (attachments.isNotEmpty()) { + upsertAttachments(attachments) + } + } +} diff --git a/android/app/src/main/java/ru/daemonlord/messenger/data/message/local/entity/MessageAttachmentEntity.kt b/android/app/src/main/java/ru/daemonlord/messenger/data/message/local/entity/MessageAttachmentEntity.kt new file mode 100644 index 0000000..c50ca9e --- /dev/null +++ b/android/app/src/main/java/ru/daemonlord/messenger/data/message/local/entity/MessageAttachmentEntity.kt @@ -0,0 +1,26 @@ +package ru.daemonlord.messenger.data.message.local.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "message_attachments", + indices = [ + Index(value = ["message_id"]), + ], +) +data class MessageAttachmentEntity( + @PrimaryKey + @ColumnInfo(name = "id") + val id: Long, + @ColumnInfo(name = "message_id") + val messageId: Long, + @ColumnInfo(name = "file_url") + val fileUrl: String, + @ColumnInfo(name = "file_type") + val fileType: String, + @ColumnInfo(name = "file_size") + val fileSize: Long, +) 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 new file mode 100644 index 0000000..1115293 --- /dev/null +++ b/android/app/src/main/java/ru/daemonlord/messenger/data/message/local/entity/MessageEntity.kt @@ -0,0 +1,42 @@ +package ru.daemonlord.messenger.data.message.local.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "messages", + indices = [ + Index(value = ["chat_id", "created_at"]), + Index(value = ["chat_id", "id"]), + Index(value = ["chat_id", "updated_at"]), + ], +) +data class MessageEntity( + @PrimaryKey + @ColumnInfo(name = "id") + val id: Long, + @ColumnInfo(name = "chat_id") + val chatId: Long, + @ColumnInfo(name = "sender_id") + val senderId: Long, + @ColumnInfo(name = "sender_display_name") + val senderDisplayName: String?, + @ColumnInfo(name = "sender_username") + val senderUsername: String?, + @ColumnInfo(name = "sender_avatar_url") + val senderAvatarUrl: String?, + @ColumnInfo(name = "reply_to_message_id") + val replyToMessageId: Long?, + @ColumnInfo(name = "type") + val type: String, + @ColumnInfo(name = "text") + val text: String?, + @ColumnInfo(name = "status") + val status: String?, + @ColumnInfo(name = "created_at") + val createdAt: String, + @ColumnInfo(name = "updated_at") + val updatedAt: String?, +) diff --git a/android/app/src/main/java/ru/daemonlord/messenger/di/DatabaseModule.kt b/android/app/src/main/java/ru/daemonlord/messenger/di/DatabaseModule.kt index 36b8a40..622a4eb 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/di/DatabaseModule.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/di/DatabaseModule.kt @@ -9,6 +9,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import ru.daemonlord.messenger.data.chat.local.dao.ChatDao import ru.daemonlord.messenger.data.chat.local.db.MessengerDatabase +import ru.daemonlord.messenger.data.message.local.dao.MessageDao import javax.inject.Singleton @Module @@ -31,4 +32,8 @@ object DatabaseModule { @Provides @Singleton fun provideChatDao(database: MessengerDatabase): ChatDao = database.chatDao() + + @Provides + @Singleton + fun provideMessageDao(database: MessengerDatabase): MessageDao = database.messageDao() } 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 new file mode 100644 index 0000000..4ed9ed7 --- /dev/null +++ b/android/app/src/main/java/ru/daemonlord/messenger/domain/message/model/MessageItem.kt @@ -0,0 +1,15 @@ +package ru.daemonlord.messenger.domain.message.model + +data class MessageItem( + val id: Long, + val chatId: Long, + val senderId: Long, + val senderDisplayName: String?, + val type: String, + val text: String?, + val createdAt: String, + val updatedAt: String?, + val isOutgoing: Boolean, + val status: String?, + val replyToMessageId: Long?, +)