Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
main.com.sceyt.chatuikit.persistence.logicimpl.PersistenceReactionsLogicImpl.kt Maven / Gradle / Ivy
package com.sceyt.chatuikit.persistence.logicimpl
import com.sceyt.chat.wrapper.ClientWrapper
import com.sceyt.chatuikit.SceytChatUIKit
import com.sceyt.chatuikit.data.managers.connection.ConnectionEventManager
import com.sceyt.chatuikit.data.managers.message.event.ReactionUpdateEventData
import com.sceyt.chatuikit.data.managers.message.event.ReactionUpdateEventEnum.Add
import com.sceyt.chatuikit.data.managers.message.event.ReactionUpdateEventEnum.Remove
import com.sceyt.chatuikit.data.models.LoadKeyData
import com.sceyt.chatuikit.data.models.PaginationResponse
import com.sceyt.chatuikit.data.models.SceytResponse
import com.sceyt.chatuikit.data.models.messages.SceytMessage
import com.sceyt.chatuikit.data.models.messages.SceytReaction
import com.sceyt.chatuikit.persistence.dao.ChannelDao
import com.sceyt.chatuikit.persistence.dao.ChatUserReactionDao
import com.sceyt.chatuikit.persistence.dao.MessageDao
import com.sceyt.chatuikit.persistence.dao.PendingReactionDao
import com.sceyt.chatuikit.persistence.dao.ReactionDao
import com.sceyt.chatuikit.persistence.dao.UserDao
import com.sceyt.chatuikit.persistence.entity.messages.ReactionTotalEntity
import com.sceyt.chatuikit.persistence.entity.pendings.PendingReactionEntity
import com.sceyt.chatuikit.persistence.extensions.toArrayList
import com.sceyt.chatuikit.persistence.logic.PersistenceReactionsLogic
import com.sceyt.chatuikit.persistence.logicimpl.channel.ChannelsCache
import com.sceyt.chatuikit.persistence.logicimpl.channel.ChatReactionMessagesCache
import com.sceyt.chatuikit.persistence.logicimpl.message.MessagesCache
import com.sceyt.chatuikit.persistence.mappers.toChannel
import com.sceyt.chatuikit.persistence.mappers.toMessageDb
import com.sceyt.chatuikit.persistence.mappers.toReactionEntity
import com.sceyt.chatuikit.persistence.mappers.toReactionTotalEntity
import com.sceyt.chatuikit.persistence.mappers.toSceytMessage
import com.sceyt.chatuikit.persistence.mappers.toSceytReaction
import com.sceyt.chatuikit.persistence.mappers.toSceytUser
import com.sceyt.chatuikit.persistence.mappers.toUserDb
import com.sceyt.chatuikit.persistence.mappers.toUserReactionsEntity
import com.sceyt.chatuikit.persistence.repositories.ReactionsRepository
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.collections.component1
import kotlin.collections.component2
internal class PersistenceReactionsLogicImpl(
private val reactionsRepository: ReactionsRepository,
private var messageDao: MessageDao,
private var usersDao: UserDao,
private var reactionDao: ReactionDao,
private var channelReactionsDao: ChatUserReactionDao,
private var channelDao: ChannelDao,
private var pendingReactionDao: PendingReactionDao,
private var channelsCache: ChannelsCache,
private var messagesCache: MessagesCache) : PersistenceReactionsLogic {
private val reactionUpdateMutex = Mutex()
private val reactionsLoadSize get() = SceytChatUIKit.config.queryLimits.reactionListQueryLimit
override suspend fun onMessageReactionUpdated(data: ReactionUpdateEventData) {
reactionUpdateMutex.withLock {
val messageId = data.message.id
val existMessage = messageDao.existsMessageById(messageId)
if (!existMessage)
messageDao.upsertMessage(data.message.toMessageDb(false))
when (data.eventType) {
Add -> {
// Check maybe reaction already added by my, and this event comes from carbon user,
// and ignore adding reactionTotal.
val reaction = data.reaction.user?.id?.let { userId ->
reactionDao.getUserReactionByKey(messageId, userId, data.reaction.key)
}
reactionDao.insertReaction(data.reaction.toReactionEntity())
if (reaction == null)
increaseReactionTotal(messageId, data.reaction.key, data.reaction.score)
}
Remove -> reactionDao.deleteReactionAndTotal(messageId, data.reaction.key, data.reaction.user?.id, data.reaction.score)
}
val message = messageDao.getMessageById(messageId)?.toSceytMessage() ?: data.message
messagesCache.messageUpdated(data.message.channelId, message)
handleChannelReaction(data, message)
}
}
private suspend fun handleChannelReaction(data: ReactionUpdateEventData, message: SceytMessage) {
if (data.message.incoming) return
when (data.eventType) {
Add -> {
if (!message.incoming) {
channelReactionsDao.insertChannelUserReaction(data.reaction.toUserReactionsEntity(message.channelId))
ChatReactionMessagesCache.addMessage(message)
}
}
Remove -> channelReactionsDao.deleteChannelUserReaction(message.channelId, message.id,
data.reaction.key, data.reaction.user?.id)
}
channelDao.getChannelById(message.channelId)?.toChannel()?.let {
channelsCache.upsertChannel(it)
}
}
override suspend fun loadReactions(messageId: Long, offset: Int, key: String, loadKey: LoadKeyData?, ignoreDb: Boolean): Flow> {
return callbackFlow {
var dbReactions = getReactionsDb(messageId, offset, reactionsLoadSize, key)
var hasNext = dbReactions.size == reactionsLoadSize
dbReactions = dbReactions.updateWithPendingReactions(messageId, key)
trySend(PaginationResponse.DBResponse(data = dbReactions, loadKey = loadKey, offset = offset,
hasNext = hasNext, hasPrev = false))
ConnectionEventManager.awaitToConnectSceyt()
val response = if (offset == 0) reactionsRepository.getReactions(messageId, key)
else reactionsRepository.loadMoreReactions(messageId, key)
if (response is SceytResponse.Success) {
val reactions = response.data ?: arrayListOf()
val deletedReactions = dbReactions.filter { dbReaction ->
!dbReaction.pending && reactions.none { it.id == dbReaction.id }
}
reactionDao.deleteReactionByIds(*deletedReactions.map { it.id }.toLongArray())
saveReactionsToDb(reactions)
val limit = reactionsLoadSize + offset
val cashData = getReactionsDb(messageId, 0, limit, key).updateWithPendingReactions(messageId, key)
hasNext = response.data?.size == reactionsLoadSize
trySend(PaginationResponse.ServerResponse(data = response, cacheData = cashData,
loadKey = loadKey, offset = offset, hasDiff = true, hasNext = hasNext, hasPrev = false,
loadType = PaginationResponse.LoadType.LoadNext, ignoredDb = ignoreDb))
}
channel.close()
awaitClose()
}
}
private suspend fun List.updateWithPendingReactions(messageId: Long, key: String): List {
val pendingData = (if (key.isBlank()) pendingReactionDao.getAllByMsgId(messageId)
else pendingReactionDao.getAllByMsgIdAndKey(messageId, key)).groupBy { it.isAdd }
var dbReactions = ArrayList(this)
val pendingAddedR = pendingData[true]
val pendingRemoveItems = pendingData[false]
if (!pendingRemoveItems.isNullOrEmpty()) {
dbReactions.apply {
val needTOBeRemoved = dbReactions.filter { reaction ->
pendingRemoveItems.any { it.key == reaction.key && reaction.user?.id == SceytChatUIKit.chatUIFacade.myId }
}
removeAll(needTOBeRemoved.toSet())
}
}
if (!pendingAddedR.isNullOrEmpty()) {
dbReactions = dbReactions.toArrayList().apply {
addAll(0, pendingAddedR.map { it.toSceytReaction() })
}
}
return dbReactions
}
override suspend fun getMessageReactionsDbByKey(messageId: Long, key: String): List {
return if (key.isEmpty())
reactionDao.getReactionsByMsgId(messageId).map { it.toSceytReaction() }
else
reactionDao.getReactionsByMsgIdAndKey(messageId, key).map { it.toSceytReaction() }
}
override suspend fun addReaction(channelId: Long, messageId: Long, key: String, score: Int,
reason: String, enforceUnique: Boolean): SceytResponse {
reactionUpdateMutex.withLock {
val wasPending = insertPendingReactionToDbAndGetWasPending(channelId, messageId, key,
reason, enforceUnique, true)
if (wasPending)
return SceytResponse.Success(null)
return addReactionImpl(channelId, messageId, key, reason, score, enforceUnique, emitUpdate = true)
}
}
override suspend fun deleteReaction(channelId: Long, messageId: Long, key: String): SceytResponse {
reactionUpdateMutex.withLock {
val wasPending = insertPendingReactionToDbAndGetWasPending(channelId, messageId, key,
"", enforceUnique = true, isAdd = false)
if (wasPending)
return SceytResponse.Success(null)
return deleteReactionImpl(channelId, messageId, key, true)
}
}
override suspend fun sendAllPendingReactions() {
val pendingReactions = pendingReactionDao.getAll()
if (pendingReactions.isNotEmpty()) {
val groupByChannel = pendingReactions.groupBy { it.channelId }
for ((channelId, reactions) in groupByChannel) {
sendPendingReactionsSync(channelId, reactions)
}
}
}
private suspend fun sendPendingReactionsSync(channelId: Long, reactions: List) {
reactions.groupBy { it.messageId }.forEach { (messageId, reactions) ->
reactions.forEachIndexed { index, entity ->
if (entity.isAdd) {
addReactionImpl(channelId = channelId,
messageId = messageId,
key = entity.key,
reason = entity.reason,
score = entity.score,
enforceUnique = entity.enforceUnique,
emitUpdate = index == reactions.lastIndex)
} else
deleteReactionImpl(channelId, messageId, entity.key, index == reactions.lastIndex)
}
}
}
private suspend fun addReactionImpl(channelId: Long, messageId: Long, key: String,
reason: String, score: Int, enforceUnique: Boolean,
emitUpdate: Boolean): SceytResponse {
val response = reactionsRepository.addReaction(channelId, messageId, key, score, reason, enforceUnique)
if (response is SceytResponse.Success) {
response.data?.let { resultMessage ->
resultMessage.userReactions?.let {
reactionDao.insertReactions(it.map { reaction -> reaction.toReactionEntity() })
}
resultMessage.reactionTotals?.let { totals ->
reactionDao.insertReactionTotals(totals.map { it.toReactionTotalEntity(messageId) })
}
messageDao.getMessageTidById(messageId)?.let { tid ->
messagesCache.deletePendingReaction(channelId, tid, key)
}
pendingReactionDao.deletePendingReaction(messageId, key)
if (emitUpdate) {
val message = messageDao.getMessageById(messageId)?.toSceytMessage()
?: response.data
messagesCache.messageUpdated(channelId, message)
ChatReactionMessagesCache.addMessage(message)
if (!message.incoming) {
val reaction = message.userReactions?.maxByOrNull { it.createdAt }
if (reaction != null)
handleChannelReaction(ReactionUpdateEventData(message, reaction, Add), message)
}
}
}
}
return response
}
private suspend fun deleteReactionImpl(channelId: Long, messageId: Long, key: String, emitUpdate: Boolean): SceytResponse {
val response = reactionsRepository.deleteReaction(channelId, messageId, key)
if (response is SceytResponse.Success) {
response.data?.let { resultMessage ->
reactionDao.deleteAllReactionsAndTotals(messageId)
resultMessage.userReactions?.let {
reactionDao.insertReactions(it.map { reaction -> reaction.toReactionEntity() })
}
resultMessage.reactionTotals?.let { totals ->
reactionDao.insertReactionTotals(totals.map { it.toReactionTotalEntity(messageId) })
}
pendingReactionDao.deletePendingReaction(messageId, key)
messageDao.getMessageTidById(messageId)?.let { tid ->
messagesCache.deletePendingReaction(channelId, tid, key)
}
if (emitUpdate) {
val message = messageDao.getMessageById(messageId)?.toSceytMessage()
?: resultMessage
messagesCache.messageUpdated(channelId, message)
if (!message.incoming) {
val reaction = SceytReaction(0, messageId, key, 1, "", 0,
ClientWrapper.currentUser?.toSceytUser(), false)
handleChannelReaction(ReactionUpdateEventData(message, reaction, Remove), message)
}
}
}
}
return response
}
private suspend fun insertPendingReactionToDbAndGetWasPending(channelId: Long,
messageId: Long,
key: String,
reason: String,
enforceUnique: Boolean,
isAdd: Boolean): Boolean {
val messageDb = messageDao.getMessageById(messageId) ?: return false
val pendingReactions = messageDb.pendingReactions?.toArrayList() ?: arrayListOf()
var wasPending = false
var pendingReactionEntity = pendingReactions.find { it.key == key }
if (pendingReactionEntity != null) {
// if pending reaction already exists, and isAdd is different, remove it.
if (pendingReactionEntity.isAdd != isAdd) {
pendingReactions.remove(pendingReactionEntity)
pendingReactionDao.deletePendingReaction(messageId, key)
messagesCache.deletePendingReaction(channelId, messageDb.messageEntity.tid, key)
pendingReactionEntity = null
wasPending = true
}
} else {
val entity = PendingReactionEntity(messageId = messageId, channelId = channelId, key = key,
score = 1, reason = reason, enforceUnique = enforceUnique,
createdAt = System.currentTimeMillis(), isAdd = isAdd,
incomingMsg = messageDb.messageEntity.incoming, count = 1)
pendingReactions.add(entity)
pendingReactionEntity = entity
}
val message = messageDb.copy(pendingReactions = pendingReactions).toSceytMessage()
messagesCache.messageUpdated(channelId, message)
ChatReactionMessagesCache.addMessage(message)
if (pendingReactionEntity != null)
pendingReactionDao.insert(pendingReactionEntity)
if (!message.incoming)
notifyChannelReactionUpdated(channelId)
return wasPending
}
private suspend fun increaseReactionTotal(messageId: Long, key: String, score: Int) {
reactionDao.getReactionTotal(messageId, key)?.let {
val newTotal = it.copy(score = it.score + score)
reactionDao.insertReactionTotal(newTotal)
} ?: run {
reactionDao.insertReactionTotal(ReactionTotalEntity(messageId = messageId,
key = key, score = score, count = 1))
}
}
private suspend fun notifyChannelReactionUpdated(channelId: Long) {
channelDao.getChannelById(channelId)?.toChannel()?.let {
channelsCache.upsertChannel(it)
}
}
private suspend fun getReactionsDb(messageId: Long, offset: Int, limit: Int, key: String): List {
return if (key.isBlank()) {
reactionDao.getReactions(messageId = messageId, limit = limit,
offset = offset).map { it.toSceytReaction() }
} else {
reactionDao.getReactionsByKey(messageId = messageId, limit = limit,
offset = offset, key = key).map { it.toSceytReaction() }
}
}
private suspend fun saveReactionsToDb(list: List) {
if (list.isEmpty()) return
reactionDao.insertReactions(list.map { it.toReactionEntity() })
usersDao.insertUsersWithMetadata(list.mapNotNull { it.user?.toUserDb() })
}
}