263 lines
9.2 KiB
Kotlin
263 lines
9.2 KiB
Kotlin
package chats.data.repository
|
|
|
|
import chats.data.remote.api.ChatApi
|
|
import chats.data.remote.api.SendMessageRequest
|
|
import chats.data.remote.dto.ChatDto
|
|
import chats.data.remote.dto.MessageDto
|
|
import chats.data.remote.dto.MediaItemDto
|
|
import chats.data.remote.dto.ReactionDto
|
|
import chats.domain.model.Chat
|
|
import chats.domain.model.Message
|
|
import chats.domain.model.MediaType
|
|
import chats.domain.repository.ChatRepository
|
|
import core.network.ServerConfig
|
|
import core.security.TokenManager
|
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
import okhttp3.MultipartBody
|
|
import okhttp3.RequestBody.Companion.asRequestBody
|
|
import javax.inject.Inject
|
|
import kotlinx.coroutines.flow.map
|
|
|
|
class ChatRepositoryImpl @Inject constructor(
|
|
private val api: ChatApi,
|
|
private val tokenManager: TokenManager,
|
|
private val serverConfig: ServerConfig,
|
|
private val messageDao: core.database.data.MessageDao,
|
|
private val hubClient: chats.data.remote.signalr.ChatHubClient
|
|
) : ChatRepository {
|
|
private val gson = com.google.gson.Gson()
|
|
|
|
override suspend fun getChats(): List<Chat> {
|
|
val currentUserId = tokenManager.getUserId() ?: ""
|
|
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
|
|
return api.getChats().map { it.toDomain(currentUserId, baseUrl) }
|
|
}
|
|
|
|
override fun getMessagesFlow(chatId: String): kotlinx.coroutines.flow.Flow<List<Message>> {
|
|
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
|
|
return messageDao.getMessages(chatId).map { entities: List<core.database.data.MessageEntity> ->
|
|
entities.map { it.toDomain(baseUrl, gson) }
|
|
}
|
|
}
|
|
|
|
override suspend fun getMessages(chatId: String): List<Message> {
|
|
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
|
|
return try {
|
|
val messages = api.getMessages(chatId)
|
|
// Save to DB
|
|
messageDao.insertMessages(messages.map { msg: MessageDto -> msg.toEntity(baseUrl, gson) })
|
|
messages.map { it.toDomain(baseUrl) }
|
|
} catch (e: Exception) {
|
|
// If network fails, caller should ideally use the Flow from DB
|
|
emptyList()
|
|
}
|
|
}
|
|
|
|
override suspend fun sendMessage(
|
|
chatId: String,
|
|
content: String?,
|
|
type: String,
|
|
attachments: List<chats.data.remote.api.AttachmentRequest>?
|
|
): Message {
|
|
val request = SendMessageRequest(
|
|
content = content,
|
|
type = type,
|
|
attachments = attachments
|
|
)
|
|
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
|
|
return api.sendMessage(chatId, request).toDomain(baseUrl)
|
|
}
|
|
|
|
override suspend fun addReaction(messageId: String, emoji: String) {
|
|
api.addReaction(messageId, emoji)
|
|
}
|
|
|
|
override suspend fun sendTypingStatus(chatId: String) {
|
|
api.sendTypingStatus(chatId)
|
|
}
|
|
|
|
override suspend fun markMessagesAsRead(chatId: String, lastMessageId: String, lastReadSequenceId: Int) {
|
|
val request = chats.data.remote.signalr.ReadMessagesRequest(
|
|
chatId = chatId,
|
|
lastReadMessageId = lastMessageId,
|
|
lastReadSequenceId = lastReadSequenceId
|
|
)
|
|
hubClient.readMessages(request)
|
|
}
|
|
|
|
override suspend fun saveMessage(message: Message) {
|
|
messageDao.insertMessages(listOf(message.toEntity(gson)))
|
|
}
|
|
|
|
override suspend fun deleteLocalMessage(messageId: String) {
|
|
messageDao.deleteMessage(messageId)
|
|
}
|
|
|
|
override suspend fun uploadMedia(file: java.io.File): String {
|
|
val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull())
|
|
val body = MultipartBody.Part.createFormData("file", file.name, requestFile)
|
|
return api.uploadFile(body).url
|
|
}
|
|
|
|
override suspend fun getTrendingGifs(page: Int): List<chats.data.remote.api.KlipyGifDto> {
|
|
return api.getTrendingGifs(page).data.data
|
|
}
|
|
|
|
override suspend fun searchGifs(query: String, page: Int): List<chats.data.remote.api.KlipyGifDto> {
|
|
return api.searchGifs(query, page).data.data
|
|
}
|
|
|
|
override suspend fun getGifCategories(): List<chats.data.remote.api.GifCategoryDto> {
|
|
return api.getGifCategories().data.categories
|
|
}
|
|
}
|
|
|
|
// Mappers
|
|
fun ChatDto.toDomain(currentUserId: String, baseUrl: String): Chat {
|
|
val chatName = name ?: if (type == "personal") {
|
|
members.firstOrNull { it.userId != currentUserId }?.user?.displayName ?: "Unknown Chat"
|
|
} else "Group Chat"
|
|
|
|
val chatAvatar = (avatar ?: if (type == "personal") {
|
|
members.firstOrNull { it.userId != currentUserId }?.user?.avatarUrl
|
|
} else null)?.ensureAbsoluteUrl(baseUrl)
|
|
|
|
return Chat(
|
|
id = id,
|
|
type = type,
|
|
name = chatName,
|
|
avatar = chatAvatar,
|
|
unreadCount = unreadCount,
|
|
lastMessage = messages.firstOrNull()?.toDomain(baseUrl)
|
|
)
|
|
}
|
|
|
|
fun Message.toEntity(gson: com.google.gson.Gson): core.database.data.MessageEntity {
|
|
return core.database.data.MessageEntity(
|
|
id = id,
|
|
chatId = chatId,
|
|
senderId = senderId,
|
|
senderName = senderName,
|
|
senderAvatar = senderAvatar,
|
|
content = content,
|
|
sequenceId = sequenceId,
|
|
createdAt = createdAt,
|
|
mediaType = mediaType.name.lowercase(),
|
|
mediaJson = gson.toJson(media),
|
|
reactionsJson = gson.toJson(reactions),
|
|
isRead = isRead,
|
|
replyToId = replyTo?.id
|
|
)
|
|
}
|
|
|
|
fun MessageDto.toDomain(baseUrl: String): Message {
|
|
val domainMediaType = when (type) {
|
|
"gif" -> MediaType.GIF
|
|
"image", "photo" -> MediaType.IMAGE
|
|
"video" -> MediaType.VIDEO
|
|
"audio", "voice" -> MediaType.AUDIO
|
|
"file" -> MediaType.FILE
|
|
else -> when (media.firstOrNull()?.type) {
|
|
"image", "photo" -> MediaType.IMAGE
|
|
"video" -> MediaType.VIDEO
|
|
"audio", "voice" -> MediaType.AUDIO
|
|
"file" -> MediaType.FILE
|
|
else -> MediaType.TEXT
|
|
}
|
|
}
|
|
|
|
return Message(
|
|
id = id,
|
|
chatId = chatId ?: "",
|
|
senderId = senderId ?: "",
|
|
senderName = sender?.displayName ?: "Unknown",
|
|
senderAvatar = sender?.avatarUrl?.ensureAbsoluteUrl(baseUrl),
|
|
content = content,
|
|
sequenceId = sequenceId ?: 0,
|
|
createdAt = createdAt ?: "",
|
|
media = media.map {
|
|
chats.domain.model.Media(
|
|
id = it.id,
|
|
type = it.type,
|
|
url = it.url.ensureAbsoluteUrl(baseUrl),
|
|
filename = it.filename,
|
|
size = it.size,
|
|
duration = it.duration
|
|
)
|
|
},
|
|
mediaType = domainMediaType,
|
|
reactions = reactions?.associate { it.emoji to it.count } ?: emptyMap(),
|
|
isRead = true, // Network messages are usually considered read when fetched or handled by server
|
|
replyTo = replyTo?.toDomain(baseUrl)
|
|
)
|
|
}
|
|
|
|
fun MessageDto.toEntity(baseUrl: String, gson: com.google.gson.Gson): core.database.data.MessageEntity {
|
|
return core.database.data.MessageEntity(
|
|
id = id,
|
|
chatId = chatId ?: "",
|
|
senderId = senderId ?: "",
|
|
senderName = sender?.displayName ?: "Unknown",
|
|
senderAvatar = sender?.avatarUrl?.ensureAbsoluteUrl(baseUrl),
|
|
content = content,
|
|
sequenceId = sequenceId ?: 0,
|
|
createdAt = createdAt ?: "",
|
|
mediaType = type ?: "text",
|
|
mediaJson = gson.toJson(media),
|
|
reactionsJson = gson.toJson(reactions),
|
|
isRead = true,
|
|
replyToId = replyTo?.id
|
|
)
|
|
}
|
|
|
|
fun core.database.data.MessageEntity.toDomain(baseUrl: String, gson: com.google.gson.Gson): Message {
|
|
val mediaTypeEnum = when (mediaType) {
|
|
"gif" -> MediaType.GIF
|
|
"image", "photo" -> MediaType.IMAGE
|
|
"video" -> MediaType.VIDEO
|
|
"audio", "voice" -> MediaType.AUDIO
|
|
"file" -> MediaType.FILE
|
|
else -> MediaType.TEXT
|
|
}
|
|
|
|
val mediaTypeToken = object : com.google.gson.reflect.TypeToken<List<chats.data.remote.dto.MediaItemDto>>() {}.type
|
|
val mediaDtos: List<chats.data.remote.dto.MediaItemDto> = gson.fromJson(mediaJson, mediaTypeToken) ?: emptyList()
|
|
|
|
val reactionsTypeToken = object : com.google.gson.reflect.TypeToken<List<chats.data.remote.dto.ReactionDto>>() {}.type
|
|
val reactionDtos: List<chats.data.remote.dto.ReactionDto> = gson.fromJson(reactionsJson, reactionsTypeToken) ?: emptyList()
|
|
|
|
return Message(
|
|
id = id,
|
|
chatId = chatId,
|
|
senderId = senderId,
|
|
senderName = senderName,
|
|
senderAvatar = senderAvatar,
|
|
content = content,
|
|
sequenceId = sequenceId,
|
|
createdAt = createdAt,
|
|
media = mediaDtos.map {
|
|
chats.domain.model.Media(
|
|
id = it.id,
|
|
type = it.type,
|
|
url = it.url.ensureAbsoluteUrl(baseUrl),
|
|
filename = it.filename,
|
|
size = it.size,
|
|
duration = it.duration
|
|
)
|
|
},
|
|
mediaType = mediaTypeEnum,
|
|
reactions = reactionDtos.associate { it.emoji to it.count },
|
|
isRead = isRead
|
|
)
|
|
}
|
|
|
|
fun String.ensureAbsoluteUrl(baseUrl: String): String {
|
|
return if (this.startsWith("http")) {
|
|
this
|
|
} else {
|
|
val base = baseUrl.removeSuffix("/")
|
|
val path = if (this.startsWith("/")) this else "/$this"
|
|
"$base$path"
|
|
}
|
|
}
|