android: add room schema and chat list domain models
This commit is contained in:
@@ -24,3 +24,9 @@
|
||||
### Step 4 - Unit tests
|
||||
- Added `DataStoreTokenRepositoryTest` for token save/read and clear behavior.
|
||||
- Added `NetworkAuthRepositoryTest` for login success path and 401 -> `InvalidCredentials` error mapping.
|
||||
|
||||
### Step 5 - Chat Room models and persistence core
|
||||
- Added domain chat model (`ChatItem`) for chat list rendering concerns.
|
||||
- Added Room entities: `chats`, `users_short` with sort-friendly indices.
|
||||
- Added `ChatDao` with `observeChats()`, `upsertChats()`, and transactional `clearAndReplaceChats()`.
|
||||
- Added `MessengerDatabase` and Hilt database wiring (`DatabaseModule`).
|
||||
|
||||
@@ -80,6 +80,9 @@ dependencies {
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
|
||||
|
||||
implementation("androidx.datastore:datastore-preferences:1.1.1")
|
||||
implementation("androidx.room:room-runtime:2.6.1")
|
||||
implementation("androidx.room:room-ktx:2.6.1")
|
||||
kapt("androidx.room:room-compiler:2.6.1")
|
||||
|
||||
implementation("com.google.dagger:hilt-android:2.52")
|
||||
kapt("com.google.dagger:hilt-compiler:2.52")
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package ru.daemonlord.messenger.data.chat.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.chat.local.entity.ChatEntity
|
||||
import ru.daemonlord.messenger.data.chat.local.entity.UserShortEntity
|
||||
import ru.daemonlord.messenger.data.chat.local.model.ChatListLocalModel
|
||||
|
||||
@Dao
|
||||
interface ChatDao {
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT
|
||||
c.id,
|
||||
c.public_id,
|
||||
c.type,
|
||||
c.title,
|
||||
COALESCE(c.display_title, u.display_name) AS display_title,
|
||||
c.handle,
|
||||
COALESCE(c.avatar_url, u.avatar_url) AS avatar_url,
|
||||
c.archived,
|
||||
c.pinned,
|
||||
c.muted,
|
||||
c.unread_count,
|
||||
c.unread_mentions_count,
|
||||
COALESCE(c.counterpart_name, u.display_name) AS counterpart_name,
|
||||
COALESCE(c.counterpart_username, u.username) AS counterpart_username,
|
||||
COALESCE(c.counterpart_avatar_url, u.avatar_url) AS counterpart_avatar_url,
|
||||
c.counterpart_is_online,
|
||||
c.counterpart_last_seen_at,
|
||||
c.last_message_text,
|
||||
c.last_message_type,
|
||||
c.last_message_created_at,
|
||||
c.updated_sort_at
|
||||
FROM chats c
|
||||
LEFT JOIN users_short u ON c.counterpart_user_id = u.id
|
||||
WHERE c.archived = :archived
|
||||
ORDER BY c.pinned DESC, c.updated_sort_at DESC, c.id DESC
|
||||
"""
|
||||
)
|
||||
fun observeChats(archived: Boolean): Flow<List<ChatListLocalModel>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsertChats(chats: List<ChatEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsertUsers(users: List<UserShortEntity>)
|
||||
|
||||
@Query("DELETE FROM chats WHERE archived = :archived")
|
||||
suspend fun deleteChatsByArchived(archived: Boolean)
|
||||
|
||||
@Query("DELETE FROM chats WHERE id = :chatId")
|
||||
suspend fun deleteChat(chatId: Long)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
UPDATE chats
|
||||
SET counterpart_is_online = :isOnline,
|
||||
counterpart_last_seen_at = :lastSeenAt
|
||||
WHERE id = :chatId
|
||||
"""
|
||||
)
|
||||
suspend fun updatePresence(chatId: Long, isOnline: Boolean, lastSeenAt: String?)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
UPDATE chats
|
||||
SET last_message_text = :lastMessageText,
|
||||
last_message_type = :lastMessageType,
|
||||
last_message_created_at = :lastMessageCreatedAt,
|
||||
updated_sort_at = :updatedSortAt
|
||||
WHERE id = :chatId
|
||||
"""
|
||||
)
|
||||
suspend fun updateLastMessage(
|
||||
chatId: Long,
|
||||
lastMessageText: String?,
|
||||
lastMessageType: String?,
|
||||
lastMessageCreatedAt: String?,
|
||||
updatedSortAt: String?,
|
||||
)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
UPDATE chats
|
||||
SET unread_count = CASE
|
||||
WHEN :incrementBy > 0 THEN unread_count + :incrementBy
|
||||
ELSE unread_count
|
||||
END
|
||||
WHERE id = :chatId
|
||||
"""
|
||||
)
|
||||
suspend fun incrementUnread(chatId: Long, incrementBy: Int = 1)
|
||||
|
||||
@Transaction
|
||||
suspend fun clearAndReplaceChats(
|
||||
archived: Boolean,
|
||||
chats: List<ChatEntity>,
|
||||
users: List<UserShortEntity>,
|
||||
) {
|
||||
upsertUsers(users)
|
||||
deleteChatsByArchived(archived = archived)
|
||||
upsertChats(chats)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package ru.daemonlord.messenger.data.chat.local.db
|
||||
|
||||
import androidx.room.Database
|
||||
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
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
ChatEntity::class,
|
||||
UserShortEntity::class,
|
||||
],
|
||||
version = 1,
|
||||
exportSchema = false,
|
||||
)
|
||||
abstract class MessengerDatabase : RoomDatabase() {
|
||||
abstract fun chatDao(): ChatDao
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package ru.daemonlord.messenger.data.chat.local.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
tableName = "chats",
|
||||
indices = [
|
||||
Index(value = ["archived", "pinned", "updated_sort_at"]),
|
||||
Index(value = ["archived", "last_message_created_at"]),
|
||||
],
|
||||
)
|
||||
data class ChatEntity(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "id")
|
||||
val id: Long,
|
||||
@ColumnInfo(name = "public_id")
|
||||
val publicId: String,
|
||||
@ColumnInfo(name = "type")
|
||||
val type: String,
|
||||
@ColumnInfo(name = "title")
|
||||
val title: String?,
|
||||
@ColumnInfo(name = "display_title")
|
||||
val displayTitle: String,
|
||||
@ColumnInfo(name = "handle")
|
||||
val handle: String?,
|
||||
@ColumnInfo(name = "avatar_url")
|
||||
val avatarUrl: String?,
|
||||
@ColumnInfo(name = "archived")
|
||||
val archived: Boolean,
|
||||
@ColumnInfo(name = "pinned")
|
||||
val pinned: Boolean,
|
||||
@ColumnInfo(name = "muted")
|
||||
val muted: Boolean,
|
||||
@ColumnInfo(name = "unread_count")
|
||||
val unreadCount: Int,
|
||||
@ColumnInfo(name = "unread_mentions_count")
|
||||
val unreadMentionsCount: Int,
|
||||
@ColumnInfo(name = "counterpart_user_id")
|
||||
val counterpartUserId: Long?,
|
||||
@ColumnInfo(name = "counterpart_name")
|
||||
val counterpartName: String?,
|
||||
@ColumnInfo(name = "counterpart_username")
|
||||
val counterpartUsername: String?,
|
||||
@ColumnInfo(name = "counterpart_avatar_url")
|
||||
val counterpartAvatarUrl: String?,
|
||||
@ColumnInfo(name = "counterpart_is_online")
|
||||
val counterpartIsOnline: Boolean?,
|
||||
@ColumnInfo(name = "counterpart_last_seen_at")
|
||||
val counterpartLastSeenAt: String?,
|
||||
@ColumnInfo(name = "last_message_text")
|
||||
val lastMessageText: String?,
|
||||
@ColumnInfo(name = "last_message_type")
|
||||
val lastMessageType: String?,
|
||||
@ColumnInfo(name = "last_message_created_at")
|
||||
val lastMessageCreatedAt: String?,
|
||||
@ColumnInfo(name = "updated_sort_at")
|
||||
val updatedSortAt: String?,
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
package ru.daemonlord.messenger.data.chat.local.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
tableName = "users_short",
|
||||
)
|
||||
data class UserShortEntity(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "id")
|
||||
val id: Long,
|
||||
@ColumnInfo(name = "display_name")
|
||||
val displayName: String,
|
||||
@ColumnInfo(name = "username")
|
||||
val username: String?,
|
||||
@ColumnInfo(name = "avatar_url")
|
||||
val avatarUrl: String?,
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
package ru.daemonlord.messenger.data.chat.local.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
|
||||
data class ChatListLocalModel(
|
||||
@ColumnInfo(name = "id")
|
||||
val id: Long,
|
||||
@ColumnInfo(name = "public_id")
|
||||
val publicId: String,
|
||||
@ColumnInfo(name = "type")
|
||||
val type: String,
|
||||
@ColumnInfo(name = "title")
|
||||
val title: String?,
|
||||
@ColumnInfo(name = "display_title")
|
||||
val displayTitle: String,
|
||||
@ColumnInfo(name = "handle")
|
||||
val handle: String?,
|
||||
@ColumnInfo(name = "avatar_url")
|
||||
val avatarUrl: String?,
|
||||
@ColumnInfo(name = "archived")
|
||||
val archived: Boolean,
|
||||
@ColumnInfo(name = "pinned")
|
||||
val pinned: Boolean,
|
||||
@ColumnInfo(name = "muted")
|
||||
val muted: Boolean,
|
||||
@ColumnInfo(name = "unread_count")
|
||||
val unreadCount: Int,
|
||||
@ColumnInfo(name = "unread_mentions_count")
|
||||
val unreadMentionsCount: Int,
|
||||
@ColumnInfo(name = "counterpart_name")
|
||||
val counterpartName: String?,
|
||||
@ColumnInfo(name = "counterpart_username")
|
||||
val counterpartUsername: String?,
|
||||
@ColumnInfo(name = "counterpart_avatar_url")
|
||||
val counterpartAvatarUrl: String?,
|
||||
@ColumnInfo(name = "counterpart_is_online")
|
||||
val counterpartIsOnline: Boolean?,
|
||||
@ColumnInfo(name = "counterpart_last_seen_at")
|
||||
val counterpartLastSeenAt: String?,
|
||||
@ColumnInfo(name = "last_message_text")
|
||||
val lastMessageText: String?,
|
||||
@ColumnInfo(name = "last_message_type")
|
||||
val lastMessageType: String?,
|
||||
@ColumnInfo(name = "last_message_created_at")
|
||||
val lastMessageCreatedAt: String?,
|
||||
@ColumnInfo(name = "updated_sort_at")
|
||||
val updatedSortAt: String?,
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
package ru.daemonlord.messenger.data.chat.mapper
|
||||
|
||||
import ru.daemonlord.messenger.data.chat.local.model.ChatListLocalModel
|
||||
import ru.daemonlord.messenger.domain.chat.model.ChatItem
|
||||
|
||||
fun ChatListLocalModel.toDomain(): ChatItem {
|
||||
return ChatItem(
|
||||
id = id,
|
||||
publicId = publicId,
|
||||
type = type,
|
||||
title = title,
|
||||
displayTitle = displayTitle,
|
||||
handle = handle,
|
||||
avatarUrl = avatarUrl,
|
||||
archived = archived,
|
||||
pinned = pinned,
|
||||
muted = muted,
|
||||
unreadCount = unreadCount,
|
||||
unreadMentionsCount = unreadMentionsCount,
|
||||
counterpartUsername = counterpartUsername,
|
||||
counterpartName = counterpartName,
|
||||
counterpartAvatarUrl = counterpartAvatarUrl,
|
||||
counterpartIsOnline = counterpartIsOnline,
|
||||
counterpartLastSeenAt = counterpartLastSeenAt,
|
||||
lastMessageText = lastMessageText,
|
||||
lastMessageType = lastMessageType,
|
||||
lastMessageCreatedAt = lastMessageCreatedAt,
|
||||
updatedSortAt = updatedSortAt,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package ru.daemonlord.messenger.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
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 javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabase(
|
||||
@ApplicationContext context: Context,
|
||||
): MessengerDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
MessengerDatabase::class.java,
|
||||
"messenger.db",
|
||||
).fallbackToDestructiveMigration()
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideChatDao(database: MessengerDatabase): ChatDao = database.chatDao()
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package ru.daemonlord.messenger.domain.chat.model
|
||||
|
||||
data class ChatItem(
|
||||
val id: Long,
|
||||
val publicId: String,
|
||||
val type: String,
|
||||
val title: String?,
|
||||
val displayTitle: String,
|
||||
val handle: String?,
|
||||
val avatarUrl: String?,
|
||||
val archived: Boolean,
|
||||
val pinned: Boolean,
|
||||
val muted: Boolean,
|
||||
val unreadCount: Int,
|
||||
val unreadMentionsCount: Int,
|
||||
val counterpartUsername: String?,
|
||||
val counterpartName: String?,
|
||||
val counterpartAvatarUrl: String?,
|
||||
val counterpartIsOnline: Boolean?,
|
||||
val counterpartLastSeenAt: String?,
|
||||
val lastMessageText: String?,
|
||||
val lastMessageType: String?,
|
||||
val lastMessageCreatedAt: String?,
|
||||
val updatedSortAt: String?,
|
||||
)
|
||||
Reference in New Issue
Block a user