# Agent H: Repository Implementations ## Phase: 3 (Core Logic) ## Depends on: Agent A, Agent C (models + Room database), Agent I (DI modules) ## Context Repositories provide the data layer between ViewModels and data sources (Room DB + server). They handle caching, data transformation between entities and domain models. Server communication is delegated to ChatClient (not implemented here). ## Task Create repository implementations for messages, conversations, and users. ## Files to Create ### 1. data/repository/MessageRepository.kt ```kotlin package com.kecalek.chat.data.repository import com.kecalek.chat.data.local.dao.MessageDao import com.kecalek.chat.data.local.entity.MessageEntity import com.kecalek.chat.data.model.* import com.kecalek.chat.util.DateFormatter import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.serialization.json.Json import java.util.Date import javax.inject.Inject import javax.inject.Singleton @Singleton class MessageRepository @Inject constructor( private val messageDao: MessageDao, ) { private val json = Json { ignoreUnknownKeys = true } fun getMessagesFlow(conversationId: String): Flow> = messageDao.getMessagesFlow(conversationId).map { entities -> entities.map { it.toDomain() } } suspend fun getMessages(conversationId: String): List = messageDao.getMessages(conversationId).map { it.toDomain() } suspend fun getMessage(messageId: String): Message? = messageDao.getMessage(messageId)?.toDomain() suspend fun saveMessages(messages: List) { messageDao.insertAll(messages.map { it.toEntity() }) } suspend fun saveMessage(message: Message) { messageDao.insert(message.toEntity()) } suspend fun markDeleted(messageId: String) { messageDao.markDeleted(messageId) } suspend fun updateReactions(messageId: String, reactions: List) { val reactionsJson = reactions.joinToString(",", "[", "]") { r -> """{"user_id":"${r.userId}","reaction":"${r.reaction}","created_at":"${DateFormatter.format(r.createdAt)}"}""" } messageDao.updateReactions(messageId, reactionsJson) } suspend fun updatePinStatus(messageId: String, pinnedAt: Date?, pinnedBy: String?) { messageDao.updatePinStatus(messageId, pinnedAt?.time, pinnedBy) } suspend fun updateReadBy(messageId: String, readBy: Set) { val readByJson = readBy.joinToString(",", "[", "]") { "\"$it\"" } messageDao.updateReadBy(messageId, readByJson) } suspend fun deleteByConversation(conversationId: String) { messageDao.deleteByConversation(conversationId) } suspend fun getLatestTimestamp(conversationId: String): Long? = messageDao.getLatestTimestamp(conversationId) suspend fun getPinnedMessages(conversationId: String): List = messageDao.getPinnedMessages(conversationId).map { it.toDomain() } suspend fun searchMessages(conversationId: String, query: String): List = messageDao.searchMessages(conversationId, query).map { it.toDomain() } // --- Entity <-> Domain mapping --- private fun MessageEntity.toDomain(): Message = Message( id = id, conversationId = conversationId, senderId = senderId, senderUsername = senderUsername, createdAt = Date(createdAt), text = text, replyTo = replyTo, imageFileId = imageFileId, file = fileJson?.let { parseFileInfo(it) }, image = imageJson?.let { parseImageInfo(it) }, isDeleted = isDeleted, readBy = readByJson?.let { parseStringSet(it) } ?: emptySet(), reactions = reactionsJson?.let { parseReactions(it) } ?: emptyList(), forwardedFrom = forwardedFromJson?.let { parseForwardedFrom(it) }, pinnedAt = pinnedAt?.let { Date(it) }, pinnedBy = pinnedBy, ) private fun Message.toEntity(): MessageEntity = MessageEntity( id = id, conversationId = conversationId, senderId = senderId, senderUsername = senderUsername, createdAt = createdAt.time, text = text, replyTo = replyTo, imageFileId = imageFileId, fileJson = file?.let { serializeFileInfo(it) }, imageJson = image?.let { serializeImageInfo(it) }, isDeleted = isDeleted, readByJson = if (readBy.isNotEmpty()) readBy.joinToString(",", "[", "]") { "\"$it\"" } else null, reactionsJson = if (reactions.isNotEmpty()) serializeReactions(reactions) else null, forwardedFromJson = forwardedFrom?.let { serializeForwardedFrom(it) }, pinnedAt = pinnedAt?.time, pinnedBy = pinnedBy, ) // TODO: Implement JSON serialization helpers private fun parseFileInfo(json: String): FileInfo? = null // TODO private fun parseImageInfo(json: String): ImageInfo? = null // TODO private fun parseStringSet(json: String): Set = emptySet() // TODO private fun parseReactions(json: String): List = emptyList() // TODO private fun parseForwardedFrom(json: String): ForwardedFrom? = null // TODO private fun serializeFileInfo(info: FileInfo): String = "" // TODO private fun serializeImageInfo(info: ImageInfo): String = "" // TODO private fun serializeReactions(reactions: List): String = "" // TODO private fun serializeForwardedFrom(fwd: ForwardedFrom): String = "" // TODO } ``` ### 2. data/repository/ConversationRepository.kt ```kotlin package com.kecalek.chat.data.repository import com.kecalek.chat.data.local.dao.ConversationDao import com.kecalek.chat.data.local.entity.ConversationEntity import com.kecalek.chat.data.model.Conversation import com.kecalek.chat.data.model.ConversationMember import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import java.util.Date import javax.inject.Inject import javax.inject.Singleton @Singleton class ConversationRepository @Inject constructor( private val conversationDao: ConversationDao, ) { fun getAllFlow(): Flow> = conversationDao.getAllFlow().map { entities -> entities.map { it.toDomain() } } suspend fun getAll(): List = conversationDao.getAll().map { it.toDomain() } suspend fun getById(conversationId: String): Conversation? = conversationDao.getById(conversationId)?.toDomain() suspend fun saveAll(conversations: List) { conversationDao.insertAll(conversations.map { it.toEntity() }) } suspend fun save(conversation: Conversation) { conversationDao.insert(conversation.toEntity()) } suspend fun updateUnreadCount(conversationId: String, count: Int) { conversationDao.updateUnreadCount(conversationId, count) } suspend fun toggleFavorite(conversationId: String, isFavorite: Boolean) { conversationDao.updateFavorite(conversationId, isFavorite) } suspend fun updateName(conversationId: String, name: String) { conversationDao.updateName(conversationId, name) } suspend fun delete(conversationId: String) { conversationDao.delete(conversationId) } // --- Entity <-> Domain mapping --- private fun ConversationEntity.toDomain(): Conversation = Conversation( id = id, name = name, members = membersJson?.let { parseMembers(it) } ?: emptyList(), createdBy = createdBy, avatarFile = avatarFile, unreadCount = unreadCount, isFavorite = isFavorite, lastMessageTime = lastMessageTime?.let { Date(it) }, ) private fun Conversation.toEntity(): ConversationEntity = ConversationEntity( id = id, name = name, createdBy = createdBy, avatarFile = avatarFile, unreadCount = unreadCount, isFavorite = isFavorite, lastMessageTime = lastMessageTime?.time, membersJson = serializeMembers(members), ) // TODO: Implement JSON serialization helpers private fun parseMembers(json: String): List = emptyList() // TODO private fun serializeMembers(members: List): String = "" // TODO } ``` ### 3. data/repository/UserRepository.kt ```kotlin package com.kecalek.chat.data.repository import com.kecalek.chat.data.local.dao.UserCacheDao import com.kecalek.chat.data.local.entity.UserCacheEntity import com.kecalek.chat.data.model.User import javax.inject.Inject import javax.inject.Singleton @Singleton class UserRepository @Inject constructor( private val userCacheDao: UserCacheDao, ) { suspend fun getUser(userId: String): User? = userCacheDao.getById(userId)?.toDomain() suspend fun getUserByEmail(email: String): User? = userCacheDao.getByEmail(email)?.toDomain() suspend fun cacheUser(user: User) { userCacheDao.insert(user.toEntity()) } suspend fun clearCache() { userCacheDao.deleteAll() } private fun UserCacheEntity.toDomain(): User = User( id = id, username = username, email = email, identityKey = identityKey, ) private fun User.toEntity(): UserCacheEntity = UserCacheEntity( id = id, username = username, email = email, identityKey = identityKey, ) } ``` ## Constraints - All repositories are `@Singleton` (Hilt scope) - All data operations are `suspend` functions - Flow-based observers for real-time UI updates - JSON serialization for complex fields (reactions, members, file info) - Entity <-> Domain model mapping is bidirectional ## DO NOT - Implement actual server API calls (that's in ChatClient) - Implement any crypto operations - Create new Room entities or DAOs (use existing from Agent C) - Add UI code