Офлайн режим, начало
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -60,4 +60,5 @@
|
|||||||
<string name="reply_gif">GIF</string>
|
<string name="reply_gif">GIF</string>
|
||||||
<string name="reply_prefix">Reply to </string>
|
<string name="reply_prefix">Reply to </string>
|
||||||
<string name="reply_self">yourself</string>
|
<string name="reply_self">yourself</string>
|
||||||
|
<string name="no_messages_yet">No messages yet</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -82,4 +82,5 @@
|
|||||||
<string name="reply_gif">GIF</string>
|
<string name="reply_gif">GIF</string>
|
||||||
<string name="reply_prefix">Ответ </string>
|
<string name="reply_prefix">Ответ </string>
|
||||||
<string name="reply_self">самому себе</string>
|
<string name="reply_self">самому себе</string>
|
||||||
|
<string name="no_messages_yet">Сообщений пока нет</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import chats.domain.model.Message
|
|||||||
import chats.domain.model.MediaType
|
import chats.domain.model.MediaType
|
||||||
import chats.domain.repository.ChatRepository
|
import chats.domain.repository.ChatRepository
|
||||||
import core.database.data.ChatDatabase
|
import core.database.data.ChatDatabase
|
||||||
|
import core.database.data.ChatDao
|
||||||
import core.database.data.MessageDao
|
import core.database.data.MessageDao
|
||||||
import core.database.data.MessageEntity
|
import core.database.data.MessageEntity
|
||||||
import core.database.data.SyncStatus
|
import core.database.data.SyncStatus
|
||||||
@@ -51,7 +52,8 @@ class ChatRepositoryImpl @Inject constructor(
|
|||||||
private val api: ChatApi,
|
private val api: ChatApi,
|
||||||
private val tokenManager: TokenManager,
|
private val tokenManager: TokenManager,
|
||||||
private val serverConfig: ServerConfig,
|
private val serverConfig: ServerConfig,
|
||||||
private val dao: MessageDao,
|
private val messageDao: MessageDao,
|
||||||
|
private val chatDao: ChatDao,
|
||||||
private val database: ChatDatabase,
|
private val database: ChatDatabase,
|
||||||
private val hubClient: ChatHubClient,
|
private val hubClient: ChatHubClient,
|
||||||
private val signalRHandler: MessageSignalRHandler,
|
private val signalRHandler: MessageSignalRHandler,
|
||||||
@@ -68,7 +70,43 @@ class ChatRepositoryImpl @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun getChats(): List<Chat> {
|
override suspend fun getChats(): List<Chat> {
|
||||||
val currentUserId = tokenManager.getUserId() ?: ""
|
val currentUserId = tokenManager.getUserId() ?: ""
|
||||||
return api.getChats().map { it.toDomain(currentUserId, baseUrl) }
|
return try {
|
||||||
|
// Пробуем загрузить из сети
|
||||||
|
val chats = api.getChats().map { it.toDomain(currentUserId, baseUrl) }
|
||||||
|
// Кэшируем в Room
|
||||||
|
val entities = chats.map { it.toEntity() }
|
||||||
|
chatDao.insertChats(entities)
|
||||||
|
// Кэшируем последние сообщения
|
||||||
|
chats.forEach { chat ->
|
||||||
|
chat.lastMessage?.let { msg ->
|
||||||
|
messageDao.upsertMessage(msg.toEntity(gson))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
android.util.Log.d(TAG, "Cached ${entities.size} chats with messages")
|
||||||
|
chats
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.d(TAG, "Network load failed, using cache")
|
||||||
|
// При ошибке - возвращаем из кэша с загрузкой последних сообщений
|
||||||
|
chatDao.getAllChats().map { entity ->
|
||||||
|
val lastMessage = entity.lastMessageId?.let { messageId ->
|
||||||
|
messageDao.getMessageById(messageId)?.toDomain(baseUrl, gson)
|
||||||
|
}
|
||||||
|
entity.toDomain(currentUserId, baseUrl, lastMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChatsFlow(): Flow<List<Chat>> {
|
||||||
|
val currentUserId = tokenManager.getUserId() ?: ""
|
||||||
|
return chatDao.getAllChatsFlow().map { entities ->
|
||||||
|
entities.map { entity ->
|
||||||
|
// Загружаем последнее сообщение из базы для каждого чата
|
||||||
|
val lastMessage = entity.lastMessageId?.let { messageId ->
|
||||||
|
messageDao.getMessageById(messageId)?.toDomain(baseUrl, gson)
|
||||||
|
}
|
||||||
|
entity.toDomain(currentUserId, baseUrl, lastMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMessagesPaging(chatId: String): Flow<PagingData<Message>> {
|
override fun getMessagesPaging(chatId: String): Flow<PagingData<Message>> {
|
||||||
@@ -78,15 +116,15 @@ class ChatRepositoryImpl @Inject constructor(
|
|||||||
initialLoadSize = 50,
|
initialLoadSize = 50,
|
||||||
enablePlaceholders = false
|
enablePlaceholders = false
|
||||||
)
|
)
|
||||||
|
|
||||||
return Pager(
|
return Pager(
|
||||||
config = pagingConfig,
|
config = pagingConfig,
|
||||||
pagingSourceFactory = { dao.getMessagesPagingSource(chatId) },
|
pagingSourceFactory = { messageDao.getMessagesPagingSource(chatId) },
|
||||||
remoteMediator = MessageRemoteMediator(
|
remoteMediator = MessageRemoteMediator(
|
||||||
chatId = chatId,
|
chatId = chatId,
|
||||||
api = api,
|
api = api,
|
||||||
database = database,
|
database = database,
|
||||||
dao = dao,
|
dao = messageDao,
|
||||||
serverConfig = serverConfig,
|
serverConfig = serverConfig,
|
||||||
tokenManager = tokenManager
|
tokenManager = tokenManager
|
||||||
)
|
)
|
||||||
@@ -96,7 +134,7 @@ class ChatRepositoryImpl @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getMessagesFlow(chatId: String): Flow<List<Message>> {
|
override fun getMessagesFlow(chatId: String): Flow<List<Message>> {
|
||||||
return dao.getMessages(chatId).map { entities ->
|
return messageDao.getMessages(chatId).map { entities ->
|
||||||
entities.map { it.toDomain(baseUrl, gson) }
|
entities.map { it.toDomain(baseUrl, gson) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,7 +149,7 @@ class ChatRepositoryImpl @Inject constructor(
|
|||||||
|
|
||||||
if (messages.isNotEmpty()) {
|
if (messages.isNotEmpty()) {
|
||||||
val entities = messages.map { it.toEntity(baseUrl, currentUserId, gson) }
|
val entities = messages.map { it.toEntity(baseUrl, currentUserId, gson) }
|
||||||
dao.upsertMessages(entities)
|
messageDao.upsertMessages(entities)
|
||||||
Log.d(TAG, "Cached ${entities.size} messages")
|
Log.d(TAG, "Cached ${entities.size} messages")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +183,7 @@ class ChatRepositoryImpl @Inject constructor(
|
|||||||
editedContent = null, lastUpdated = currentTime
|
editedContent = null, lastUpdated = currentTime
|
||||||
)
|
)
|
||||||
|
|
||||||
dao.insertMessage(localMessage)
|
messageDao.insertMessage(localMessage)
|
||||||
Log.d(TAG, "Saved local message: $localId")
|
Log.d(TAG, "Saved local message: $localId")
|
||||||
MessageSyncWorker.scheduleSync(context)
|
MessageSyncWorker.scheduleSync(context)
|
||||||
|
|
||||||
@@ -191,20 +229,20 @@ class ChatRepositoryImpl @Inject constructor(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error marking messages as read", e)
|
Log.e(TAG, "Error marking messages as read", e)
|
||||||
}
|
}
|
||||||
dao.markMessagesAsRead(chatId, lastReadSequenceId)
|
messageDao.markMessagesAsRead(chatId, lastReadSequenceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun saveMessage(message: Message) {
|
override suspend fun saveMessage(message: Message) {
|
||||||
dao.insertMessage(message.toEntity(gson))
|
messageDao.insertMessage(message.toEntity(gson))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteLocalMessage(messageId: String) {
|
override suspend fun deleteLocalMessage(messageId: String) {
|
||||||
dao.markAsDeletedLocally(messageId)
|
messageDao.markAsDeletedLocally(messageId)
|
||||||
MessageSyncWorker.scheduleSync(context)
|
MessageSyncWorker.scheduleSync(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun editLocalMessage(messageId: String, newContent: String) {
|
override suspend fun editLocalMessage(messageId: String, newContent: String) {
|
||||||
dao.markAsEditedLocally(messageId, newContent)
|
messageDao.markAsEditedLocally(messageId, newContent)
|
||||||
MessageSyncWorker.scheduleSync(context)
|
MessageSyncWorker.scheduleSync(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,14 +277,14 @@ class ChatRepositoryImpl @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun deleteMessage(messageId: String, forEveryone: Boolean) {
|
override suspend fun deleteMessage(messageId: String, forEveryone: Boolean) {
|
||||||
api.deleteMessage(messageId, forEveryone)
|
api.deleteMessage(messageId, forEveryone)
|
||||||
dao.deleteMessage(messageId)
|
messageDao.deleteMessage(messageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun editMessage(messageId: String, content: String): Message {
|
override suspend fun editMessage(messageId: String, content: String): Message {
|
||||||
val request = SendMessageRequest(content = content)
|
val request = SendMessageRequest(content = content)
|
||||||
val currentUserId = tokenManager.getUserId() ?: ""
|
val currentUserId = tokenManager.getUserId() ?: ""
|
||||||
val response = api.editMessage(messageId, request)
|
val response = api.editMessage(messageId, request)
|
||||||
dao.insertMessage(response.toEntity(baseUrl, currentUserId, gson))
|
messageDao.insertMessage(response.toEntity(baseUrl, currentUserId, gson))
|
||||||
return response.toDomain(currentUserId, baseUrl)
|
return response.toDomain(currentUserId, baseUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package chats.data.repository
|
|||||||
|
|
||||||
import chats.data.remote.dto.*
|
import chats.data.remote.dto.*
|
||||||
import chats.domain.model.*
|
import chats.domain.model.*
|
||||||
|
import core.database.data.ChatEntity
|
||||||
|
import core.database.data.MessageEntity
|
||||||
|
import core.database.data.SyncStatus
|
||||||
|
|
||||||
// Mappers
|
// Mappers
|
||||||
fun ChatDto.toDomain(currentUserId: String, baseUrl: String): Chat {
|
fun ChatDto.toDomain(currentUserId: String, baseUrl: String): Chat {
|
||||||
@@ -23,6 +26,38 @@ fun ChatDto.toDomain(currentUserId: String, baseUrl: String): Chat {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Chat.toEntity(): ChatEntity {
|
||||||
|
return ChatEntity(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
avatar = avatar,
|
||||||
|
type = type,
|
||||||
|
lastMessageId = lastMessage?.id,
|
||||||
|
lastMessageText = lastMessage?.content,
|
||||||
|
lastMessageAt = lastMessage?.createdAt?.let {
|
||||||
|
try { java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", java.util.Locale.US).parse(it)?.time ?: 0L }
|
||||||
|
catch (e: Exception) { 0L }
|
||||||
|
} ?: 0L,
|
||||||
|
unreadCount = unreadCount,
|
||||||
|
isPinned = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ChatEntity.toDomain(
|
||||||
|
currentUserId: String,
|
||||||
|
baseUrl: String,
|
||||||
|
lastMessage: Message? = null
|
||||||
|
): Chat {
|
||||||
|
return Chat(
|
||||||
|
id = id,
|
||||||
|
type = type,
|
||||||
|
name = name,
|
||||||
|
avatar = avatar,
|
||||||
|
unreadCount = unreadCount,
|
||||||
|
lastMessage = lastMessage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun Message.toEntity(gson: com.google.gson.Gson): core.database.data.MessageEntity {
|
fun Message.toEntity(gson: com.google.gson.Gson): core.database.data.MessageEntity {
|
||||||
return core.database.data.MessageEntity(
|
return core.database.data.MessageEntity(
|
||||||
id = id,
|
id = id,
|
||||||
@@ -37,7 +72,12 @@ fun Message.toEntity(gson: com.google.gson.Gson): core.database.data.MessageEnti
|
|||||||
mediaJson = gson.toJson(media),
|
mediaJson = gson.toJson(media),
|
||||||
reactionsJson = gson.toJson(reactions),
|
reactionsJson = gson.toJson(reactions),
|
||||||
isRead = isRead,
|
isRead = isRead,
|
||||||
replyToId = replyTo?.id
|
replyToId = replyTo?.id,
|
||||||
|
syncStatus = SyncStatus.SYNCED,
|
||||||
|
isDeletedLocally = false,
|
||||||
|
isEditedLocally = false,
|
||||||
|
editedContent = null,
|
||||||
|
lastUpdated = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import chats.data.repository.ChatRepositoryImpl
|
|||||||
import chats.data.signalr.MessageSignalRHandler
|
import chats.data.signalr.MessageSignalRHandler
|
||||||
import chats.domain.repository.ChatRepository
|
import chats.domain.repository.ChatRepository
|
||||||
import core.database.data.ChatDatabase
|
import core.database.data.ChatDatabase
|
||||||
|
import core.database.data.ChatDao
|
||||||
import core.database.data.MessageDao
|
import core.database.data.MessageDao
|
||||||
import core.network.ServerConfig
|
import core.network.ServerConfig
|
||||||
import core.security.TokenManager
|
import core.security.TokenManager
|
||||||
@@ -39,19 +40,26 @@ object ChatModule {
|
|||||||
return MessageSignalRHandler(hubClient, dao, serverConfig, tokenManager)
|
return MessageSignalRHandler(hubClient, dao, serverConfig, tokenManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideChatDao(database: ChatDatabase): ChatDao {
|
||||||
|
return database.chatDao()
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideChatRepository(
|
fun provideChatRepository(
|
||||||
api: ChatApi,
|
api: ChatApi,
|
||||||
tokenManager: TokenManager,
|
tokenManager: TokenManager,
|
||||||
serverConfig: ServerConfig,
|
serverConfig: ServerConfig,
|
||||||
dao: MessageDao,
|
messageDao: MessageDao,
|
||||||
|
chatDao: ChatDao,
|
||||||
database: ChatDatabase,
|
database: ChatDatabase,
|
||||||
hubClient: ChatHubClient,
|
hubClient: ChatHubClient,
|
||||||
signalRHandler: MessageSignalRHandler,
|
signalRHandler: MessageSignalRHandler,
|
||||||
@ApplicationContext context: Context
|
@ApplicationContext context: Context
|
||||||
): ChatRepository {
|
): ChatRepository {
|
||||||
return ChatRepositoryImpl(api, tokenManager, serverConfig, dao, database, hubClient, signalRHandler, context)
|
return ChatRepositoryImpl(api, tokenManager, serverConfig, messageDao, chatDao, database, hubClient, signalRHandler, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
|
|
||||||
interface ChatRepository {
|
interface ChatRepository {
|
||||||
suspend fun getChats(): List<Chat>
|
suspend fun getChats(): List<Chat>
|
||||||
|
fun getChatsFlow(): Flow<List<Chat>>
|
||||||
|
|
||||||
// Flow для UI (простой список)
|
// Flow для UI (простой список)
|
||||||
fun getMessagesFlow(chatId: String): Flow<List<Message>>
|
fun getMessagesFlow(chatId: String): Flow<List<Message>>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import kotlinx.coroutines.launch
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import chats.data.repository.toDomain
|
import chats.data.repository.toDomain
|
||||||
|
|
||||||
|
private const val TAG = "ChatListViewModel"
|
||||||
|
|
||||||
data class ChatListState(
|
data class ChatListState(
|
||||||
val chats: List<Chat> = emptyList(),
|
val chats: List<Chat> = emptyList(),
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
@@ -77,11 +79,26 @@ class ChatListViewModel @Inject constructor(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.update { it.copy(isLoading = true) }
|
_state.update { it.copy(isLoading = true) }
|
||||||
try {
|
try {
|
||||||
val chats = repository.getChats()
|
// Пробуем загрузить из сети (это закэширует в Room)
|
||||||
_state.update { it.copy(chats = sortChats(chats), isLoading = false) }
|
repository.getChats()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_state.update { it.copy(isLoading = false, error = e.message) }
|
android.util.Log.d(TAG, "Initial load failed, will use cache")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Подписываемся на Flow из Room (всегда работает, даже оффлайн)
|
||||||
|
repository.getChatsFlow()
|
||||||
|
.catch { e ->
|
||||||
|
android.util.Log.e(TAG, "Flow error", e)
|
||||||
|
emit(emptyList())
|
||||||
|
}
|
||||||
|
.collect { chats ->
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
chats = sortChats(chats),
|
||||||
|
isLoading = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,24 +4,21 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import chats.domain.model.Chat
|
|
||||||
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import core.presentation.components.AppAvatar
|
|
||||||
import chats.domain.model.MediaType
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import chats.domain.model.Chat
|
||||||
|
import chats.domain.model.MediaType
|
||||||
|
import core.presentation.components.AppAvatar
|
||||||
|
import ru.knot.messager.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatItem(
|
fun ChatItem(
|
||||||
@@ -74,14 +71,15 @@ fun ChatItem(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
val previewText = remember(chat.lastMessage) {
|
val previewText = remember(chat.lastMessage) {
|
||||||
val msg = chat.lastMessage
|
val msg = chat.lastMessage
|
||||||
if (msg == null) return@remember "No messages yet"
|
if (msg == null) return@remember context.getString(R.string.no_messages_yet)
|
||||||
|
|
||||||
if (!msg.content.isNullOrBlank()) {
|
if (!msg.content.isNullOrBlank()) {
|
||||||
msg.content
|
msg.content
|
||||||
} else if (msg.mediaType == MediaType.AUDIO) {
|
} else if (msg.mediaType == MediaType.AUDIO) {
|
||||||
"Голосовое сообщение"
|
context.getString(R.string.voice_message)
|
||||||
} else if (msg.media.isNotEmpty()) {
|
} else if (msg.media.isNotEmpty()) {
|
||||||
when (msg.mediaType) {
|
when (msg.mediaType) {
|
||||||
MediaType.IMAGE -> "Фото"
|
MediaType.IMAGE -> "Фото"
|
||||||
@@ -90,7 +88,7 @@ fun ChatItem(
|
|||||||
else -> "Медиа"
|
else -> "Медиа"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
"Сообщение"
|
context.getString(R.string.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -13,6 +13,23 @@ enum class SyncStatus {
|
|||||||
FAILED // Ошибка синхронизации
|
FAILED // Ошибка синхронизации
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity для хранения чатов в локальной базе данных
|
||||||
|
*/
|
||||||
|
@Entity(tableName = "chats")
|
||||||
|
data class ChatEntity(
|
||||||
|
@PrimaryKey val id: String,
|
||||||
|
val name: String,
|
||||||
|
val avatar: String?,
|
||||||
|
val type: String = "personal", // personal, group, saved
|
||||||
|
val lastMessageId: String? = null,
|
||||||
|
val lastMessageText: String? = null,
|
||||||
|
val lastMessageAt: Long = 0,
|
||||||
|
val unreadCount: Int = 0,
|
||||||
|
val isPinned: Boolean = false,
|
||||||
|
val lastUpdated: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity для хранения сообщений в локальной базе данных
|
* Entity для хранения сообщений в локальной базе данных
|
||||||
* Поддерживает офлайн-работу и фоновую синхронизацию
|
* Поддерживает офлайн-работу и фоновую синхронизацию
|
||||||
@@ -187,10 +204,44 @@ interface MessageDao {
|
|||||||
suspend fun exists(id: String): Boolean
|
suspend fun exists(id: String): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@Database(entities = [MessageEntity::class], version = 2)
|
@Dao
|
||||||
|
interface ChatDao {
|
||||||
|
@Query("SELECT * FROM chats ORDER BY isPinned DESC, lastMessageAt DESC")
|
||||||
|
fun getAllChatsFlow(): Flow<List<ChatEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM chats ORDER BY isPinned DESC, lastMessageAt DESC")
|
||||||
|
suspend fun getAllChats(): List<ChatEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM chats WHERE id = :chatId LIMIT 1")
|
||||||
|
suspend fun getChatById(chatId: String): ChatEntity?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM chats WHERE id = :chatId LIMIT 1")
|
||||||
|
fun getChatByIdFlow(chatId: String): Flow<ChatEntity?>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertChat(chat: ChatEntity)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertChats(chats: List<ChatEntity>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM chats WHERE id = :chatId")
|
||||||
|
suspend fun deleteChat(chatId: String)
|
||||||
|
|
||||||
|
@Query("DELETE FROM chats")
|
||||||
|
suspend fun clearAll()
|
||||||
|
|
||||||
|
@Query("UPDATE chats SET unreadCount = :count WHERE id = :chatId")
|
||||||
|
suspend fun updateUnreadCount(chatId: String, count: Int)
|
||||||
|
|
||||||
|
@Query("UPDATE chats SET lastMessageId = :lastMessageId, lastMessageText = :lastMessageText, lastMessageAt = :lastMessageAt WHERE id = :chatId")
|
||||||
|
suspend fun updateLastMessage(chatId: String, lastMessageId: String?, lastMessageText: String?, lastMessageAt: Long)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Database(entities = [MessageEntity::class, ChatEntity::class], version = 3)
|
||||||
@TypeConverters(SyncStatusConverter::class)
|
@TypeConverters(SyncStatusConverter::class)
|
||||||
abstract class ChatDatabase : RoomDatabase() {
|
abstract class ChatDatabase : RoomDatabase() {
|
||||||
abstract fun messageDao(): MessageDao
|
abstract fun messageDao(): MessageDao
|
||||||
|
abstract fun chatDao(): ChatDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val DATABASE_NAME = "knot_chat_database"
|
const val DATABASE_NAME = "knot_chat_database"
|
||||||
|
|||||||
@@ -37,3 +37,26 @@ val MIGRATION_1_2 = object : Migration(1, 2) {
|
|||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Миграция с версии 2 на версию 3
|
||||||
|
* Добавляет таблицу chats для кэширования списка чатов
|
||||||
|
*/
|
||||||
|
val MIGRATION_2_3 = object : Migration(2, 3) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL("""
|
||||||
|
CREATE TABLE IF NOT EXISTS chats (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
avatar TEXT,
|
||||||
|
type TEXT NOT NULL DEFAULT 'personal',
|
||||||
|
lastMessageId TEXT,
|
||||||
|
lastMessageText TEXT,
|
||||||
|
lastMessageAt INTEGER NOT NULL DEFAULT 0,
|
||||||
|
unreadCount INTEGER NOT NULL DEFAULT 0,
|
||||||
|
isPinned INTEGER NOT NULL DEFAULT 0,
|
||||||
|
lastUpdated INTEGER NOT NULL DEFAULT 0
|
||||||
|
)
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.room.Room
|
|||||||
import core.database.data.ChatDatabase
|
import core.database.data.ChatDatabase
|
||||||
import core.database.data.MessageDao
|
import core.database.data.MessageDao
|
||||||
import core.database.data.MIGRATION_1_2
|
import core.database.data.MIGRATION_1_2
|
||||||
|
import core.database.data.MIGRATION_2_3
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -24,7 +25,7 @@ object DatabaseModule {
|
|||||||
ChatDatabase::class.java,
|
ChatDatabase::class.java,
|
||||||
ChatDatabase.DATABASE_NAME
|
ChatDatabase.DATABASE_NAME
|
||||||
)
|
)
|
||||||
.addMigrations(MIGRATION_1_2)
|
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user