All Downloads are FREE. Search and download functionalities are using the official Maven repository.

main.com.sceyt.chatuikit.persistence.logicimpl.channel.PersistenceChannelsLogicImpl.kt Maven / Gradle / Ivy

There is a newer version: 1.7.2
Show newest version
package com.sceyt.chatuikit.persistence.logicimpl.channel

import android.content.Context
import com.google.gson.Gson
import com.sceyt.chat.models.SceytException
import com.sceyt.chat.models.message.DeliveryStatus
import com.sceyt.chat.models.message.MessageState
import com.sceyt.chat.models.role.Role
import com.sceyt.chat.models.user.UserState
import com.sceyt.chat.wrapper.ClientWrapper
import com.sceyt.chatuikit.SceytChatUIKit
import com.sceyt.chatuikit.data.managers.channel.event.ChannelEventData
import com.sceyt.chatuikit.data.managers.channel.event.ChannelEventEnum
import com.sceyt.chatuikit.data.managers.channel.event.ChannelEventEnum.ClearedHistory
import com.sceyt.chatuikit.data.managers.channel.event.ChannelEventEnum.Created
import com.sceyt.chatuikit.data.managers.channel.event.ChannelEventEnum.Deleted
import com.sceyt.chatuikit.data.managers.channel.event.ChannelEventEnum.Invited
import com.sceyt.chatuikit.data.managers.channel.event.ChannelEventEnum.Joined
import com.sceyt.chatuikit.data.managers.channel.event.ChannelEventEnum.Left
import com.sceyt.chatuikit.data.managers.channel.event.ChannelEventEnum.Updated
import com.sceyt.chatuikit.data.managers.channel.event.ChannelUnreadCountUpdatedEventData
import com.sceyt.chatuikit.data.managers.connection.ConnectionEventManager.awaitToConnectSceyt
import com.sceyt.chatuikit.data.managers.message.event.MessageStatusChangeData
import com.sceyt.chatuikit.data.models.LoadKeyData
import com.sceyt.chatuikit.data.models.PaginationResponse
import com.sceyt.chatuikit.data.models.PaginationResponse.LoadType.LoadNext
import com.sceyt.chatuikit.data.models.SceytResponse
import com.sceyt.chatuikit.data.models.channels.CreateChannelData
import com.sceyt.chatuikit.data.models.channels.EditChannelData
import com.sceyt.chatuikit.data.models.channels.GetAllChannelsResponse
import com.sceyt.chatuikit.data.models.channels.RoleTypeEnum
import com.sceyt.chatuikit.data.models.channels.SceytChannel
import com.sceyt.chatuikit.data.models.channels.SceytMember
import com.sceyt.chatuikit.data.models.channels.SelfChannelMetadata
import com.sceyt.chatuikit.data.models.messages.SceytMessage
import com.sceyt.chatuikit.data.models.messages.SceytReaction
import com.sceyt.chatuikit.data.models.messages.SceytUser
import com.sceyt.chatuikit.extensions.findIndexed
import com.sceyt.chatuikit.extensions.toSha256
import com.sceyt.chatuikit.koin.SceytKoinComponent
import com.sceyt.chatuikit.logger.SceytLog
import com.sceyt.chatuikit.persistence.dao.ChannelDao
import com.sceyt.chatuikit.persistence.dao.ChatUserReactionDao
import com.sceyt.chatuikit.persistence.dao.DraftMessageDao
import com.sceyt.chatuikit.persistence.dao.LoadRangeDao
import com.sceyt.chatuikit.persistence.dao.MessageDao
import com.sceyt.chatuikit.persistence.dao.PendingReactionDao
import com.sceyt.chatuikit.persistence.dao.UserDao
import com.sceyt.chatuikit.persistence.entity.channel.ChatUserReactionEntity
import com.sceyt.chatuikit.persistence.entity.channel.UserChatLink
import com.sceyt.chatuikit.persistence.entity.messages.DraftMessageEntity
import com.sceyt.chatuikit.persistence.entity.messages.DraftMessageUserLink
import com.sceyt.chatuikit.persistence.entity.user.UserDb
import com.sceyt.chatuikit.persistence.extensions.getPeer
import com.sceyt.chatuikit.persistence.extensions.isDirect
import com.sceyt.chatuikit.persistence.extensions.toArrayList
import com.sceyt.chatuikit.persistence.logic.PersistenceChannelsLogic
import com.sceyt.chatuikit.persistence.logic.PersistenceMessagesLogic
import com.sceyt.chatuikit.persistence.mappers.createEmptyUser
import com.sceyt.chatuikit.persistence.mappers.createPendingDirectChannelData
import com.sceyt.chatuikit.persistence.mappers.toBodyAttribute
import com.sceyt.chatuikit.persistence.mappers.toChannel
import com.sceyt.chatuikit.persistence.mappers.toChannelEntity
import com.sceyt.chatuikit.persistence.mappers.toDraftMessage
import com.sceyt.chatuikit.persistence.mappers.toReactionData
import com.sceyt.chatuikit.persistence.mappers.toSceytMessage
import com.sceyt.chatuikit.persistence.mappers.toSceytReaction
import com.sceyt.chatuikit.persistence.mappers.toSceytUiChannel
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.ChannelsRepository
import com.sceyt.chatuikit.persistence.workers.SendAttachmentWorkManager
import com.sceyt.chatuikit.persistence.workers.SendForwardMessagesWorkManager
import com.sceyt.chatuikit.presentation.components.channel.input.format.BodyStyleRange
import com.sceyt.chatuikit.presentation.components.channel.input.mention.Mention
import com.sceyt.chatuikit.presentation.extensions.isDeleted
import com.sceyt.chatuikit.presentation.extensions.isDeletedOrHardDeleted
import com.sceyt.chatuikit.presentation.extensions.isHardDeleted
import com.sceyt.chatuikit.push.RemoteMessageData
import com.sceyt.chatuikit.services.SceytPresenceChecker
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filterNotNull
import org.koin.core.component.inject

internal class PersistenceChannelsLogicImpl(
        private val channelsRepository: ChannelsRepository,
        private val channelDao: ChannelDao,
        private val usersDao: UserDao,
        private val messageDao: MessageDao,
        private val rangeDao: LoadRangeDao,
        private val draftMessageDao: DraftMessageDao,
        private val chatUserReactionDao: ChatUserReactionDao,
        private val pendingReactionDao: PendingReactionDao,
        private val context: Context,
        private val channelsCache: ChannelsCache) : PersistenceChannelsLogic, SceytKoinComponent {

    private val messageLogic: PersistenceMessagesLogic by inject()
    private val myId: String? get() = SceytChatUIKit.chatUIFacade.myId
    private val channelsLoadSize get() = SceytChatUIKit.config.queryLimits.channelListQueryLimit

    override suspend fun onChannelEvent(data: ChannelEventData) {
        when (val event = data.eventType) {
            is Created -> onChanelAdded(data.channel)
            is Joined -> onChanelJoined(data.channel)
            is Deleted -> data.channelId?.let { deleteChannelDb(it) }
            is Left -> {
                val leftUsers = event.leftMembers
                leftUsers.forEach { leftUser ->
                    data.channelId?.let { channelId ->
                        if (leftUser.id == myId) {
                            deleteChannelDb(channelId)
                            return
                        } else {
                            channelDao.deleteUserChatLinks(channelId, leftUser.id)
                            updateMembersCount(channelId, data.channel?.memberCount?.toInt() ?: 0)
                        }
                    }
                }
            }

            is ClearedHistory -> {
                data.channelId?.let { channelId ->
                    clearHistory(channelId)
                }
            }

            is Updated -> {
                data.channel?.let { sceytChannel ->
                    val withLastMessage = initPendingLastMessageBeforeInsert(sceytChannel)
                    channelDao.insertChannel(withLastMessage.toChannelEntity())
                    val updated = fillChannelsNeededInfo(withLastMessage)
                    channelsCache.upsertChannel(updated)
                }
            }

            is ChannelEventEnum.Mute -> {
                data.channelId?.let { channelId ->
                    if (event.muted) {
                        val time = data.channel?.mutedTill ?: 0
                        channelDao.updateMuteState(channelId, true, time)
                        channelsCache.updateMuteState(channelId, true, time)
                    } else {
                        channelDao.updateMuteState(channelId, false)
                        channelsCache.updateMuteState(channelId, false)
                    }
                }
            }

            is ChannelEventEnum.Pin -> onChannelPinStateChange(data.channel)
            is ChannelEventEnum.MarkedUs -> onChannelMarkedAsReadOrUnread(data.channel)
            is ChannelEventEnum.Block -> {
                if (event.blocked)
                    data.channelId?.let { deleteChannelDb(it) }
            }

            is ChannelEventEnum.Hide -> {
                if (event.hidden)
                    data.channelId?.let { deleteChannelDb(it) }
                else onChanelAdded(data.channel)
            }

            is Invited -> onChanelJoined(data.channel)
        }
    }

    private suspend fun onChanelJoined(channel: SceytChannel?) {
        val joinedMember = channel?.members?.getOrNull(0)?.id ?: return
        if (joinedMember == myId) {
            onChanelAdded(channel)
        } else updateMembersCount(channel.id, channel.memberCount.toInt())
    }

    private suspend fun updateMembersCount(channelId: Long, count: Int) {
        channelDao.updateMemberCount(channelId, count)
        channelDao.getChannelById(channelId)?.let { channel ->
            channelsCache.upsertChannel(fillChannelsNeededInfo(channel.toChannel()))
        }
    }

    private suspend fun onChanelAdded(channel: SceytChannel?) {
        channel?.let {
            val members = it.members ?: return
            insertChannel(channel, *members.toTypedArray())
            channelsCache.add(channel)
        }
    }

    override suspend fun onChannelUnreadCountUpdatedEvent(data: ChannelUnreadCountUpdatedEventData) {
        /* Todo need refactoring after sdk update. Commented code is the right way to update unread count
         data.channel ?: return
         channelDao.updateUnreadCount(data.channel.id, data.channel.unreadMessageCount.toInt())
         channelsCache.updateUnreadCount(data.channel.id, data.channel.unreadMessageCount.toInt())*/
        updateChannelDbAndCache((data.channel ?: return).toSceytUiChannel())
    }

    override suspend fun onMessageStatusChangeEvent(data: MessageStatusChangeData) {
        channelsCache.get(data.channel.id)?.let { channel ->
            channel.lastMessage?.let { lastMessage ->
                if (data.marker.messageIds.contains(lastMessage.id)) {
                    data.channel.lastMessage?.let {
                        channelsCache.updateLastMessage(channel.id, it)
                    }
                }
            }
        }
    }

    override suspend fun onMessage(data: Pair) {
        updateChannelDbAndCache(data.first)
    }

    override suspend fun onFcmMessage(data: RemoteMessageData) {
        val dataChannel = data.channel ?: return
        val dataMessage = data.message ?: return

        var channel: SceytChannel
        val channelDb = channelDao.getChannelById(dataChannel.id)
        if (channelDb != null) {
            channel = channelDb.toChannel()
            if ((channel.lastMessage?.id ?: 0) < dataMessage.id) {
                channel = channel.copy(lastMessage = dataMessage)
                channelDao.updateLastMessage(channelDb.channelEntity.id, dataMessage.id, dataMessage.createdAt)
            }
        } else {
            // Insert channel from push data
            channelDao.insertChannel(dataChannel.toChannelEntity())
            channel = dataChannel
        }
        channelsCache.upsertChannel(fillChannelsNeededInfo(channel))
    }

    override suspend fun onMessageEditedOrDeleted(message: SceytMessage) {
        val state = message.state
        if (state.isDeletedOrHardDeleted()) {
            chatUserReactionDao.deleteChannelMessageUserReaction(message.channelId, message.id)
            channelsCache.removeChannelMessageReactions(message.channelId, message.id)
        }

        channelDao.getChannelById(message.channelId)?.toChannel()?.let { channel ->
            channel.lastMessage?.let { lastMessage ->
                if (lastMessage.tid == message.tid) {
                    if (message.deliveryStatus == DeliveryStatus.Pending && message.state == MessageState.Deleted) {
                        channelsCache.updateLastMessage(message.channelId, null)
                    } else
                        channelsCache.updateLastMessage(message.channelId, message)
                }
            } ?: run {
                if (state.isHardDeleted() || message.deliveryStatus == DeliveryStatus.Pending && state.isDeleted())
                    deleteMessage(message.channelId, message)
                else
                    channelsCache.upsertChannel(channel.copy(lastMessage = message))
            }
        }
    }

    private suspend fun updateChannelDbAndCache(channel: SceytChannel?) {
        channel ?: return
        channelDao.updateChannel(channel.toChannelEntity())
        channelsCache.upsertChannel(fillChannelsNeededInfo(channel))
    }

    private suspend fun onChannelMarkedAsReadOrUnread(channel: SceytChannel?) {
        channel ?: return
        channelDao.updateChannel(channel.toChannelEntity())
        channelsCache.onChannelMarkedAsReadOrUnread(channel)
    }

    private suspend fun insertChannel(channel: SceytChannel, vararg members: SceytMember) {
        val updated = initPendingLastMessageBeforeInsert(channel)
        val users = members.map { it.toUserDb() }
        updated.lastMessage?.let { message ->
            message.userReactions?.map { it.user }?.let { userList ->
                (users as ArrayList).addAll(userList.mapNotNull { user -> user?.toUserDb() })
            }
        }
        usersDao.insertUsersWithMetadata(members.map { it.toUserDb() })
        channelDao.insertChannelAndLinks(updated.toChannelEntity(), members.map {
            UserChatLink(userId = it.id, chatId = updated.id, role = it.role.name)
        })
    }

    override fun loadChannels(offset: Int, searchQuery: String, loadKey: LoadKeyData?,
                              ignoreDb: Boolean): Flow> {
        return callbackFlow {
            if (offset == 0) channelsCache.clear()

            val dbChannels = getChannelsDb(offset, searchQuery)
            var hasNext = dbChannels.size == channelsLoadSize

            trySend(PaginationResponse.DBResponse(data = dbChannels, loadKey = loadKey, offset = offset,
                hasNext = hasNext, hasPrev = false, query = searchQuery))

            channelsCache.addAll(dbChannels, false)
            ChatReactionMessagesCache.getNeededMessages(dbChannels)

            awaitToConnectSceyt()

            val response = if (offset == 0) channelsRepository.getChannels(searchQuery)
            else channelsRepository.loadMoreChannels()

            if (response is SceytResponse.Success) {
                val channels = response.data ?: arrayListOf()

                val savedChannels = saveChannelsToDb(channels)
                val hasDiff = channelsCache.addAll(savedChannels, offset != 0) || offset == 0
                hasNext = response.data?.size == channelsLoadSize

                trySend(PaginationResponse.ServerResponse(data = response, cacheData = channelsCache.getSorted(),
                    loadKey = loadKey, offset = offset, hasDiff = hasDiff, hasNext = hasNext, hasPrev = false,
                    loadType = LoadNext, ignoredDb = ignoreDb, query = searchQuery))

                ChatReactionMessagesCache.getNeededMessages(response.data ?: arrayListOf())

                messageLogic.onSyncedChannels(channels)
            }

            channel.close()
            awaitClose()
        }
    }

    override suspend fun searchChannelsWithUserIds(offset: Int, limit: Int, searchQuery: String, userIds: List,
                                                   includeUserNames: Boolean, loadKey: LoadKeyData?,
                                                   onlyMine: Boolean, ignoreDb: Boolean): Flow> {
        return callbackFlow {
            if (offset == 0) channelsCache.clear()

            val searchUserIds = HashSet(userIds)
            if (includeUserNames) {
                val ids = usersDao.getUserIdsByDisplayName(searchQuery)
                searchUserIds.addAll(ids)
            }
            val dbChannels = channelDao.getChannelsByQueryAndUserIds(searchQuery, searchUserIds.toList(), limit, offset, onlyMine).map {
                it.toChannel()
            }
            var hasNext = dbChannels.size == limit

            channelsCache.addAll(dbChannels, false)
            trySend(PaginationResponse.DBResponse(data = dbChannels, loadKey = loadKey, offset = offset,
                hasNext = hasNext, hasPrev = false, query = searchQuery))

            awaitToConnectSceyt()

            val response = if (offset == 0) channelsRepository.getChannels(searchQuery)
            else channelsRepository.loadMoreChannels()

            if (response is SceytResponse.Success) {
                val channels = response.data ?: arrayListOf()

                val savedChannels = saveChannelsToDb(channels)
                val hasDiff = channelsCache.addAll(savedChannels, offset != 0) || offset == 0
                hasNext = response.data?.size == channelsLoadSize

                trySend(PaginationResponse.ServerResponse(data = response, cacheData = channelsCache.getSorted(),
                    loadKey = loadKey, offset = offset, hasDiff = hasDiff, hasNext = hasNext, hasPrev = false,
                    loadType = LoadNext, ignoredDb = ignoreDb, query = searchQuery))
            }

            channel.close()
            awaitClose()
        }
    }

    override suspend fun syncChannels(limit: Int) = callbackFlow {
        val oldChannelsIds = channelDao.getAllChannelsIds().toSet()
        awaitToConnectSceyt()
        val syncedChannels = arrayListOf()
        channelsRepository.getAllChannels(limit)
            .collect { response ->
                when (response) {
                    is GetAllChannelsResponse.Proportion -> {
                        val channels = response.channels
                        val savedChannels = saveChannelsToDb(channels)
                        syncedChannels.addAll(channels)
                        messageLogic.onSyncedChannels(channels)
                        channelsCache.upsertChannel(*savedChannels.toTypedArray())
                        trySend(response)
                    }

                    is GetAllChannelsResponse.SuccessfullyFinished -> {
                        if (syncedChannels.isNotEmpty()) {
                            val syncedIds = syncedChannels.map { it.id }
                            val deletedChannelIds = channelDao.getNotExistingChannelIdsByIds(syncedIds)
                            val addedChannelsIds = syncedIds.minus(oldChannelsIds)

                            if (deletedChannelIds.isNotEmpty())
                                SceytLog.i("syncChannelsResult", "deletedChannelsIds: ${deletedChannelIds.map { it }}")

                            deletedChannelIds.forEach { deleteChannelDb(channelId = it) }
                            upsertChannelsToCache(syncedChannels.filter { addedChannelsIds.contains(it.id) })
                        }
                        trySend(response)
                        channel.close()
                    }

                    is GetAllChannelsResponse.Error -> {
                        trySend(response)
                        channel.close()
                        SceytLog.e("syncChannelsResult", "syncChannels error: ${response.error}")
                    }
                }
            }

        awaitClose()
    }

    private suspend fun getChannelsDb(offset: Int, searchQuery: String): List {
        return if (searchQuery.isBlank()) {
            channelDao.getChannels(limit = channelsLoadSize, offset = offset).map { channel ->
                channel.toChannel()
            }
        } else {
            val ids = usersDao.getUserIdsByDisplayName(searchQuery)
            channelDao.getChannelsByQueryAndUserIds(query = searchQuery, userIds = ids, limit = channelsLoadSize,
                offset = offset, false).map { channel ->
                channel.toChannel()
            }
        }
    }

    private suspend fun saveChannelsToDb(list: List): List {
        if (list.isEmpty()) return emptyList()

        val links = arrayListOf()
        val users = arrayListOf()
        val directChatsWithDeletedPeers = arrayListOf()
        val lastMessages = arrayListOf()
        val userReactions = arrayListOf()

        fun addEntitiesToLists(channelId: Long, members: List?, lastMessage: SceytMessage?, userMessageReactions: List?) {
            members?.forEach { member ->
                links.add(UserChatLink(userId = member.id, chatId = channelId, role = member.role.name))
                users.add(member.toUserDb())
            }

            lastMessage?.let {
                lastMessages.add(it)
            }

            userMessageReactions?.forEach {
                userReactions.add(it.toUserReactionsEntity(channelId))
            }
        }

        var updatedList = list.map { channel ->
            if (channel.isGroup) {
                addEntitiesToLists(channel.id, channel.members, channel.lastMessage, channel.newReactions)
            } else {
                val members = arrayListOf()
                channel.getPeer()?.let {
                    if (it.user.state == UserState.Deleted)
                        directChatsWithDeletedPeers.add(channel.id)
                    members.add(it)
                }
                addEntitiesToLists(channel.id, members, channel.lastMessage, channel.newReactions)
            }

            fillChannelsNeededInfo(channel)
        }
        usersDao.insertUsersWithMetadata(users)
        messageLogic.saveChannelLastMessagesToDb(lastMessages)
        chatUserReactionDao.replaceChannelUserReactions(userReactions)

        // Delete old links where channel peer is deleted.
        directChatsWithDeletedPeers.forEach {
            myId?.let { id -> channelDao.deleteChatLinksExceptUser(it, id) }
        }

        updatedList = initPendingLastMessageBeforeInsert(updatedList)
        channelDao.insertChannelsAndLinks(updatedList.map { it.toChannelEntity() }, links)
        return updatedList
    }

    override suspend fun findOrCreateDirectChannel(user: SceytUser): SceytResponse {
        var metadata = ""
        val channelDb = if (user.id == myId) {
            metadata = Gson().toJson(SelfChannelMetadata(1))
            channelDao.getSelfChannel()
        } else channelDao.getDirectChannel(user.id)
        if (channelDb != null) {
            if (channelDb.channelEntity.pending)
                channelsCache.addPendingChannel(channelDb.toChannel())
            return SceytResponse.Success(channelDb.toChannel())
        }

        val fail = SceytResponse.Error(SceytException(0, "Failed to create direct channel myId is null"))
        val myId = myId ?: return fail
        val createdBy = ClientWrapper.currentUser?.toSceytUser()
                ?: usersDao.getUserById(myId)?.toSceytUser()
                ?: SceytUser(myId)

        val role = Role(RoleTypeEnum.Owner.value)
        val members = setOf(SceytMember(role, user), SceytMember(role, createdBy)).toList()
        val channelId = members.map { it.id }.toSet().sorted().joinToString(separator = "$").toSha256()
        val channel = createPendingDirectChannelData(channelId = channelId,
            createdBy = createdBy, members = members, role = role.name, metadata = metadata)

        insertChannel(channel, *members.toTypedArray())
        channelsCache.addPendingChannel(channel)
        return SceytResponse.Success(channel)
    }

    override suspend fun createChannel(createChannelData: CreateChannelData): SceytResponse {
        val response = channelsRepository.createChannel(createChannelData)

        if (response is SceytResponse.Success) {
            response.data?.let { channel ->
                channel.members?.toTypedArray()?.let {
                    insertChannel(channel, *it)
                }
                channelsCache.add(channel)
            }
        }
        return response
    }

    override suspend fun createNewChannelInsteadOfPendingChannel(channel: SceytChannel): SceytResponse {
        val pendingChannelId = channel.id
        val response = channelsRepository.createChannel(CreateChannelData(
            channelType = channel.type,
            uri = channel.uri ?: "",
            subject = channel.subject ?: "",
            avatarUrl = channel.avatarUrl ?: "",
            metadata = channel.metadata ?: "",
            members = channel.members ?: arrayListOf()))
        if (response is SceytResponse.Success) {
            var newChannel = response.data
                    ?: return SceytResponse.Error(SceytException(0, "create channel response is success, but channel is null"))

            val newChannelId = newChannel.id
            // Set new channel last message to pending channel last message with new channel id
            newChannel = newChannel.copy(lastMessage = channel.lastMessage?.copy(channelId = newChannelId))

            channelDao.deleteChannelAndLinks(pendingChannelId)
            channelDao.insertChannelAndLinks(newChannel.toChannelEntity(), newChannel.members?.map {
                UserChatLink(userId = it.id, chatId = newChannelId, role = it.role.name)
            } ?: emptyList())
            messageDao.updateMessagesChannelId(pendingChannelId, newChannelId)

            channelsCache.pendingChannelCreated(pendingChannelId, newChannel)
            SceytResponse.Success(newChannel)
        }
        return response
    }

    override suspend fun markChannelAsRead(channelId: Long): SceytResponse {
        val response = channelsRepository.markChannelAsRead(channelId)

        if (response is SceytResponse.Success) {
            response.data?.let {
                messageDao.updateAllIncomingMessagesStatusAsRead(channelId)
                onChannelMarkedAsReadOrUnread(it)
            }
        }

        return response
    }

    override suspend fun markChannelAsUnRead(channelId: Long): SceytResponse {
        val response = channelsRepository.markChannelAsUnRead(channelId)

        if (response is SceytResponse.Success)
            onChannelMarkedAsReadOrUnread(response.data)

        return response
    }

    override suspend fun clearHistory(channelId: Long, forEveryone: Boolean): SceytResponse {
        if (channelsCache.get(channelId)?.pending == true) {
            deleteChannelDb(channelId)
            return SceytResponse.Success(channelId)
        }

        val response = channelsRepository.clearHistory(channelId, forEveryone)

        if (response is SceytResponse.Success) {
            SendAttachmentWorkManager.cancelWorksByTag(context, channelId.toString())
            SendForwardMessagesWorkManager.cancelWorksByTag(context, channelId.toString())
            clearHistory(channelId)
        }

        return response
    }

    override suspend fun blockAndLeaveChannel(channelId: Long): SceytResponse {
        val response = channelsRepository.blockChannel(channelId)

        if (response is SceytResponse.Success) {
            SendAttachmentWorkManager.cancelWorksByTag(context, channelId.toString())
            SendForwardMessagesWorkManager.cancelWorksByTag(context, channelId.toString())
            deleteChannelDb(channelId)
        }

        return response
    }

    override suspend fun leaveChannel(channelId: Long): SceytResponse {
        val response = channelsRepository.leaveChannel(channelId)

        if (response is SceytResponse.Success) {
            SendAttachmentWorkManager.cancelWorksByTag(context, channelId.toString())
            SendForwardMessagesWorkManager.cancelWorksByTag(context, channelId.toString())
            deleteChannelDb(channelId)
        }

        return response
    }

    override suspend fun deleteChannel(channelId: Long): SceytResponse {
        if (channelsCache.get(channelId)?.pending == true) {
            deleteChannelDb(channelId)
            return SceytResponse.Success(channelId)
        }

        val response = channelsRepository.deleteChannel(channelId)

        if (response is SceytResponse.Success) {
            SendAttachmentWorkManager.cancelWorksByTag(context, channelId.toString())
            SendForwardMessagesWorkManager.cancelWorksByTag(context, channelId.toString())
            deleteChannelDb(channelId)
        }

        return response
    }

    override suspend fun muteChannel(channelId: Long, muteUntil: Long): SceytResponse {
        val response = channelsRepository.muteChannel(channelId, muteUntil)

        if (response is SceytResponse.Success) {
            channelDao.updateMuteState(channelId = channelId, muted = true, muteUntil = muteUntil)
            channelsCache.updateMuteState(channelId, true, muteUntil)
        }

        return response
    }

    override suspend fun unMuteChannel(channelId: Long): SceytResponse {
        val response = channelsRepository.unMuteChannel(channelId)

        if (response is SceytResponse.Success) {
            channelDao.updateMuteState(channelId = channelId, muted = false)
            channelsCache.updateMuteState(channelId, false)
        }

        return response
    }

    override suspend fun enableAutoDelete(channelId: Long, period: Long): SceytResponse {
        val response = channelsRepository.enableAutoDelete(channelId, period)

        if (response is SceytResponse.Success) {
            channelDao.updateAutoDeleteState(channelId, period)
            channelsCache.updateAutoDeleteState(channelId, period)
        }

        return response
    }

    override suspend fun disableAutoDelete(channelId: Long): SceytResponse {
        val response = channelsRepository.disableAutoDelete(channelId)

        if (response is SceytResponse.Success) {
            channelDao.updateAutoDeleteState(channelId, 0L)
            channelsCache.updateAutoDeleteState(channelId, 0L)
        }

        return response
    }

    override suspend fun pinChannel(channelId: Long): SceytResponse {
        val response = channelsRepository.pinChannel(channelId)

        if (response is SceytResponse.Success)
            onChannelPinStateChange(response.data)

        return response
    }

    override suspend fun unpinChannel(channelId: Long): SceytResponse {
        val response = channelsRepository.unpinChannel(channelId)

        if (response is SceytResponse.Success)
            onChannelPinStateChange(response.data)

        return response
    }

    override suspend fun hideChannel(channelId: Long): SceytResponse {
        val response = channelsRepository.hideChannel(channelId)

        if (response is SceytResponse.Success) {
            channelDao.deleteChannel(channelId = channelId)
            channelsCache.deleteChannel(channelId)
        }

        return response
    }

    override suspend fun getChannelFromDb(channelId: Long): SceytChannel? {
        return channelDao.getChannelById(channelId)?.toChannel()
    }

    override suspend fun getRetentionPeriodByChannelId(channelId: Long): Long {
        return channelDao.getRetentionPeriodByChannelId(channelId)
    }

    override suspend fun getDirectChannelFromDb(peerId: String): SceytChannel? {
        return channelDao.getDirectChannel(peerId)?.toChannel()
    }

    override suspend fun getChannelFromServer(channelId: Long): SceytResponse {
        val response = channelsRepository.getChannel(channelId)

        if (response is SceytResponse.Success) {
            response.data?.let { channel ->
                val withLastMessage = initPendingLastMessageBeforeInsert(channel)
                withLastMessage.toChannelEntity().let {
                    channelDao.insertChannel(it)
                    val updated = fillChannelsNeededInfo(withLastMessage)
                    channelsCache.upsertChannel(updated)
                    messageLogic.onSyncedChannels(arrayListOf(updated))
                }
            }
        } else {
            getChannelFromDb(channelId)?.let {
                if (it.pending)
                    return SceytResponse.Success(it)
            }
        }

        return response
    }

    override suspend fun getChannelFromServerByUrl(url: String): SceytResponse> {
        //Don't use yet
        return channelsRepository.getChannelFromServerByUrl(url)
    }

    override suspend fun editChannel(channelId: Long, data: EditChannelData): SceytResponse {
        if (data.avatarEdited && data.avatarUrl != null) {
            when (val uploadResult = channelsRepository.uploadAvatar(data.avatarUrl.toString())) {
                is SceytResponse.Success -> {
                    data.avatarUrl = uploadResult.data
                }

                is SceytResponse.Error -> return SceytResponse.Error(uploadResult.exception)
            }
        }
        val response = channelsRepository.editChannel(channelId, data)
        if (response is SceytResponse.Success) {
            response.data?.let {
                channelDao.updateChannel(it.toChannelEntity())
                channelsCache.upsertChannel(fillChannelsNeededInfo(it))
            }
        }

        return response
    }

    override suspend fun join(channelId: Long): SceytResponse {
        val response = channelsRepository.join(channelId)

        if (response is SceytResponse.Success)
            response.data?.let {
                it.members?.getOrNull(0)?.let { sceytMember ->
                    channelDao.insertUserChatLink(UserChatLink(
                        userId = sceytMember.id,
                        chatId = it.id,
                        role = sceytMember.role.name))

                    channelsCache.upsertChannel(it)
                }
            }

        return response
    }

    override suspend fun updateLastMessageWithLastRead(channelId: Long, message: SceytMessage) {
        // Check if message delivery status is pending, that means message is started to send
        if (message.deliveryStatus == DeliveryStatus.Pending) {
            channelDao.updateLastMessage(channelId, message.tid, message.createdAt)
            channelsCache.updateLastMessage(channelId, message)
        } else {
            // Check if sent message is last message of channel
            channelsCache.get(channelId)?.let {
                if (it.lastMessage?.tid != message.tid) return
            } ?: run {
                channelDao.getChannelById(channelId)?.let {
                    if (it.channelEntity.lastMessageTid != message.tid) return
                }
            }
            channelDao.updateLastMessageWithLastRead(channelId, message.tid, message.id, message.createdAt)
            channelsCache.updateLastMessageWithLastRead(channelId, message)
        }
    }

    override suspend fun updateLastMessageIfNeeded(channelId: Long, message: SceytMessage?) {
        if (message?.deliveryStatus != DeliveryStatus.Pending) {
            channelDao.updateLastMessage(channelId, message?.tid, message?.createdAt)
            channelsCache.updateLastMessage(channelId, message)
        }
    }

    override suspend fun setUnreadCount(channelId: Long, count: Int) {
        channelDao.updateUnreadCount(channelId, count)
        channelsCache.updateUnreadCount(channelId, count)
    }

    override suspend fun blockUnBlockUser(userId: String, block: Boolean) {
        val channels = channelDao.getChannelByPeerId(userId)
        channelsCache.upsertChannel(*channels.map { it.toChannel() }.toTypedArray())
    }

    override suspend fun updateDraftMessage(channelId: Long, message: String?, mentionUsers: List,
                                            styling: List?, replyOrEditMessage: SceytMessage?, isReply: Boolean) {
        val draftMessage = if (message.isNullOrBlank()) {
            draftMessageDao.deleteDraftByChannelId(channelId)
            null
        } else {
            val attributes = mentionUsers.map { it.toBodyAttribute() }.toMutableList()
            styling?.let {
                attributes.addAll(it.map { styleRange -> styleRange.toBodyAttribute() })
            }
            val draftMessageEntity = DraftMessageEntity(channelId, message, System.currentTimeMillis(),
                replyOrEditMessage?.id, isReply, attributes)
            val links = mentionUsers.map { DraftMessageUserLink(chatId = channelId, userId = it.recipientId) }
            draftMessageDao.insertWithUserLinks(draftMessageEntity, links)
            draftMessageEntity.toDraftMessage(mentionUsers.map {
                createEmptyUser(it.recipientId, it.name)
            }, replyOrEditMessage)
        }
        channelsCache.updateChannelDraftMessage(channelId, draftMessage)
    }

    override suspend fun getChannelsCountFromDb(): Int {
        return channelDao.getAllChannelsCount()
    }

    override fun getTotalUnreadCount(): Flow {
        return channelDao.getTotalUnreadCountAsFlow().filterNotNull()
    }

    override suspend fun onUserPresenceChanged(users: List) {
        users.forEach { presenceUser ->
            ArrayList(channelsCache.getData()).forEach { channel ->
                val user = presenceUser.user
                if (channel.isDirect() && channel.getPeer()?.id == user.id)
                    channelsCache.updateChannelPeer(channel.id, user)
            }
        }
    }

    private suspend fun initPendingLastMessageBeforeInsert(channels: List): List {
        if (channels.isEmpty()) return channels
        val mutableList = channels.toList().toArrayList()
        val messageTIds = channelDao.getChannelsLastMessageTIds(mutableList.map { it.id })
        if (messageTIds.isEmpty()) return channels

        val pendingLastMessages = messageDao.getPendingMessagesByTIds(messageTIds)

        pendingLastMessages.forEach { messageDb ->
            mutableList.findIndexed { it.id == messageDb.messageEntity.channelId }?.let { (index, item) ->
                mutableList[index] = item.copy(lastMessage = messageDb.toSceytMessage())
            }
        }
        return mutableList
    }

    private suspend fun initPendingLastMessageBeforeInsert(channel: SceytChannel): SceytChannel {
        val messageTIds = channelDao.getChannelLastMessageTid(channel.id) ?: return channel
        messageDao.getPendingMessageByTid(messageTIds)?.let { pendingLastMessage ->
            return channel.copy(lastMessage = pendingLastMessage.toSceytMessage())
        }
        return channel
    }

    private suspend fun deleteChannelDb(channelId: Long) {
        channelDao.deleteChannelAndLinks(channelId)
        messageDao.deleteAllMessages(channelId)
        rangeDao.deleteChannelLoadRanges(channelId)
        channelsCache.deleteChannel(channelId)
    }

    private suspend fun clearHistory(channelId: Long) {
        channelDao.updateLastMessage(channelId, null, null)
        messageDao.deleteAllMessages(channelId)
        rangeDao.deleteChannelLoadRanges(channelId)
        channelsCache.clearedHistory(channelId)
    }

    private fun upsertChannelsToCache(channels: List) {
        if (channels.isEmpty()) return
        channelsCache.upsertChannel(*channels.toTypedArray())
    }

    private suspend fun deleteMessage(channelId: Long, message: SceytMessage) {
        channelsCache.get(channelId)?.let {
            if (it.lastMessage?.id == message.id) {
                val lastMessage = messageDao.getLastMessage(channelId)
                with(lastMessage?.messageEntity) {
                    channelDao.updateLastMessage(channelId, this?.tid, this?.createdAt)
                }
                channelsCache.updateLastMessage(channelId, lastMessage?.toSceytMessage())
            }
        }
        chatUserReactionDao.deleteChannelMessageUserReaction(channelId, message.id)
    }

    private suspend fun fillChannelsNeededInfo(channel: SceytChannel): SceytChannel {
        val reactions = chatUserReactionDao.getChannelUserReactions(channel.id)
        val pendingReactions = pendingReactionDao.getAllByChannelId(channel.id)
        val draftMessage = draftMessageDao.getDraftByChannelId(channel.id)?.toDraftMessage()

        return channel.copy(
            newReactions = reactions.map { reactionDb -> reactionDb.toSceytReaction() },
            pendingReactions = pendingReactions.map { pendingReaction -> pendingReaction.toReactionData() },
            draftMessage = draftMessage
        )
    }

    private suspend fun onChannelPinStateChange(channel: SceytChannel?) {
        channel ?: return
        channelDao.updatePinState(channel.id, channel.pinnedAt)
        channelsCache.updatePinState(channel.id, channel.pinnedAt)
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy