diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/Message.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/Message.kt index 5edd7d0d61f..14143562ae3 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/Message.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/Message.kt @@ -423,9 +423,15 @@ sealed interface Message { MessageContent.NewConversationWithCellMessage -> mutableMapOf( typeKey to "newConversationWithCellMessage" ) + MessageContent.NewConversationWithCellSelfDeleteDisabledMessage -> mutableMapOf( typeKey to "newConversationWithCellSelfDeleteDisabledMessage" ) + + is MessageContent.ConversationAppsEnabledChanged -> mutableMapOf( + typeKey to "conversationAppsEnabledChanged", + "isEnabled" to "${content.isEnabled}" + ) } val standardProperties = mapOf( diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt index 55b8f06f63e..d47f3d84cdb 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt @@ -339,6 +339,10 @@ sealed interface MessageContent { val receiptMode: Boolean ) : System + data class ConversationAppsEnabledChanged( + val isEnabled: Boolean + ) : System + data class ConversationReceiptModeChanged( val receiptMode: Boolean ) : System @@ -499,6 +503,7 @@ fun MessageContent?.getType() = when (this) { null -> "null" MessageContent.NewConversationWithCellMessage -> "NewConversationWithCell" MessageContent.NewConversationWithCellSelfDeleteDisabledMessage -> "NewConversationWithCellSelfDeleteDisabled" + is MessageContent.ConversationAppsEnabledChanged -> "ConversationAppsEnabledChanged" } sealed interface MessagePreviewContent { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt index 68517eb319c..c6b5c256d04 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt @@ -177,6 +177,12 @@ internal class ConversationGroupRepositoryImpl( newGroupConversationSystemMessagesCreator.value.conversationStarted(conversationEntity) }.flatMap { newGroupConversationSystemMessagesCreator.value.conversationCellStatus(conversationEntity) + }.flatMap { + newGroupConversationSystemMessagesCreator.value.conversationAppsAccessIfEnabled( + conversationId = conversationEntity.id.toModel(), + hasAppsAccessEnabled = conversationResponse.hasAppsAccessEnabled(), + creatorId = selfUserId, + ) }.flatMap { when (protocol) { is Conversation.ProtocolInfo.Proteus -> Either.Right(setOf()) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/NewGroupConversationSystemMessagesCreator.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/NewGroupConversationSystemMessagesCreator.kt index 11f1ee8ce05..8f0ffb386ef 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/NewGroupConversationSystemMessagesCreator.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/NewGroupConversationSystemMessagesCreator.kt @@ -17,10 +17,10 @@ */ package com.wire.kalium.logic.data.conversation -import kotlin.uuid.Uuid import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.common.functional.Either import com.wire.kalium.common.functional.fold +import com.wire.kalium.common.functional.right import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.id.SelfTeamIdProvider @@ -37,6 +37,7 @@ import com.wire.kalium.persistence.dao.message.LocalId import io.mockative.Mockable import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import kotlin.uuid.Uuid /** * This class is responsible to generate system messages for new group conversations. @@ -66,6 +67,12 @@ internal interface NewGroupConversationSystemMessagesCreator { ): Either suspend fun conversationCellStatus(conversation: ConversationEntity): Either + suspend fun conversationAppsAccessIfEnabled( + eventId: String = LocalId.generate(), + conversationId: ConversationId, + hasAppsAccessEnabled: Boolean, + creatorId: UserId + ): Either } internal class NewGroupConversationSystemMessagesCreatorImpl( @@ -246,5 +253,29 @@ internal class NewGroupConversationSystemMessagesCreatorImpl( } ?: Either.Right(Unit) } + override suspend fun conversationAppsAccessIfEnabled( + eventId: String, + conversationId: ConversationId, + hasAppsAccessEnabled: Boolean, + creatorId: UserId + ): Either { + return if (hasAppsAccessEnabled) { + persistMessage( + Message.System( + eventId, + MessageContent.ConversationAppsEnabledChanged(isEnabled = true), + conversationId, + Clock.System.now(), + creatorId, + Message.Status.Sent, + Message.Visibility.VISIBLE, + expirationData = null + ) + ) + } else { + Unit.right() + } + } + private suspend fun isSelfATeamMember() = selfTeamIdProvider().fold({ false }, { it != null }) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageMapper.kt index 98b0beae51d..b620f83f6ca 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageMapper.kt @@ -333,6 +333,7 @@ class MessageMapperImpl( MessageEntity.ContentType.LEGAL_HOLD -> null MessageEntity.ContentType.CONVERSATION_WITH_CELL -> null MessageEntity.ContentType.CONVERSATION_WITH_CELL_SELF_DELETE_DISABLED -> null + MessageEntity.ContentType.CONVERSATION_APPS_ENABLED_CHANGED -> null MessageEntity.ContentType.MULTIPART -> LocalNotificationMessage.Text( messageId = message.id, @@ -498,9 +499,12 @@ fun MessageEntityContent.System.toMessageContent(): MessageContent.System = when MessageContent.LegalHold.ForMembers.Enabled(this.memberUserIdList.map { it.toModel() }) } } + is MessageEntityContent.NewConversationWithCellMessage -> MessageContent.NewConversationWithCellMessage is MessageEntityContent.NewConversationWithCellSelfDeleteDisabledMessage -> MessageContent.NewConversationWithCellSelfDeleteDisabledMessage + + is MessageEntityContent.ConversationAppsAccessChanged -> MessageContent.ConversationAppsEnabledChanged(isEnabled) } fun Message.Visibility.toEntityVisibility(): MessageEntity.Visibility = when (this) { @@ -812,6 +816,7 @@ fun MessageContent.System.toMessageEntityContent(): MessageEntityContent.System MessageContent.NewConversationWithCellMessage -> MessageEntityContent.NewConversationWithCellMessage MessageContent.NewConversationWithCellSelfDeleteDisabledMessage -> MessageEntityContent.NewConversationWithCellSelfDeleteDisabledMessage + is MessageContent.ConversationAppsEnabledChanged -> MessageEntityContent.ConversationAppsAccessChanged(isEnabled) } fun MessageAssetStatus.toDao(): MessageAssetStatusEntity { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCase.kt index e2cba8e1a7f..46ce0c625fe 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCase.kt @@ -19,13 +19,13 @@ package com.wire.kalium.logic.data.message import com.wire.kalium.common.error.CoreFailure -import com.wire.kalium.logic.data.conversation.Conversation -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.data.notification.NotificationEventsManager import com.wire.kalium.common.functional.Either import com.wire.kalium.common.functional.fold import com.wire.kalium.common.functional.map import com.wire.kalium.common.functional.onSuccess +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.notification.NotificationEventsManager +import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.persistence.dao.message.InsertMessageResult import io.mockative.Mockable @@ -134,6 +134,7 @@ internal class PersistMessageUseCaseImpl( is MessageContent.History -> false is MessageContent.NewConversationWithCellMessage -> false is MessageContent.NewConversationWithCellSelfDeleteDisabledMessage -> false + is MessageContent.ConversationAppsEnabledChanged -> false } @Suppress("ComplexMethod") @@ -194,6 +195,7 @@ internal class PersistMessageUseCaseImpl( is MessageContent.InCallEmoji, is MessageContent.History, is MessageContent.NewConversationWithCellMessage, + is MessageContent.ConversationAppsEnabledChanged, is MessageContent.NewConversationWithCellSelfDeleteDisabledMessage -> false } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/SystemMessageInserter.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/SystemMessageInserter.kt index f254881e0f9..c6c31e539d8 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/SystemMessageInserter.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/SystemMessageInserter.kt @@ -18,10 +18,10 @@ package com.wire.kalium.logic.data.message import com.wire.kalium.common.error.CoreFailure +import com.wire.kalium.common.functional.Either import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.common.functional.Either import com.wire.kalium.persistence.dao.message.LocalId import io.mockative.Mockable import kotlinx.datetime.Clock @@ -48,6 +48,13 @@ internal interface SystemMessageInserter { suspend fun insertLostCommitSystemMessage(conversationId: ConversationId, instant: Instant): Either suspend fun insertConversationStartedUnverifiedWarning(conversationId: ConversationId) + + suspend fun insertConversationAppsAccessChanged( + eventId: String = LocalId.generate(), + conversationId: ConversationId, + senderUserId: UserId, + isAppsAccessEnabled: Boolean + ) } internal class SystemMessageInserterImpl( @@ -137,4 +144,24 @@ internal class SystemMessageInserterImpl( ) ) } + + override suspend fun insertConversationAppsAccessChanged( + eventId: String, + conversationId: ConversationId, + senderUserId: UserId, + isAppsAccessEnabled: Boolean + ) { + persistMessage( + Message.System( + id = eventId, + content = MessageContent.ConversationAppsEnabledChanged(isAppsAccessEnabled), + conversationId = conversationId, + date = Clock.System.now(), + senderUserId = senderUserId, + status = Message.Status.Sent, + visibility = Message.Visibility.VISIBLE, + expirationData = null + ) + ) + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt index da7ad1047fa..982690912dd 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt @@ -1736,7 +1736,8 @@ class UserSessionScope internal constructor( private val conversationAccessUpdateEventHandler: AccessUpdateEventHandler get() = AccessUpdateEventHandler( conversationDAO = userStorage.database.conversationDAO, - selfUserId = userId + selfUserId = userId, + systemMessageInserter = systemMessageInserter ) private val mlsResetConversationEventHandler: MLSResetConversationEventHandler @@ -2120,6 +2121,7 @@ class UserSessionScope internal constructor( persistConversationsUseCase, cryptoTransactionProvider, resetMlsConversation, + systemMessageInserter ) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt index c4a1e4d0dcb..9d50be60244 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt @@ -41,6 +41,7 @@ import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.id.SelfTeamIdProvider import com.wire.kalium.logic.data.message.MessageRepository import com.wire.kalium.logic.data.message.PersistMessageUseCase +import com.wire.kalium.logic.data.message.SystemMessageInserter import com.wire.kalium.logic.data.properties.UserPropertyRepository import com.wire.kalium.logic.data.sync.SlowSyncRepository import com.wire.kalium.logic.data.team.TeamRepository @@ -52,6 +53,7 @@ import com.wire.kalium.logic.feature.connection.MarkConnectionRequestAsNotifiedU import com.wire.kalium.logic.feature.connection.ObserveConnectionListUseCase import com.wire.kalium.logic.feature.connection.ObservePendingConnectionRequestsUseCase import com.wire.kalium.logic.feature.connection.ObservePendingConnectionRequestsUseCaseImpl +import com.wire.kalium.logic.feature.conversation.apps.ChangeAccessForAppsInConversationUseCase import com.wire.kalium.logic.feature.conversation.createconversation.CreateChannelUseCase import com.wire.kalium.logic.feature.conversation.createconversation.CreateRegularGroupUseCase import com.wire.kalium.logic.feature.conversation.createconversation.CreateRegularGroupUseCaseImpl @@ -90,7 +92,6 @@ import com.wire.kalium.logic.feature.conversation.keyingmaterials.UpdateKeyingMa import com.wire.kalium.logic.feature.conversation.messagetimer.UpdateMessageTimerUseCase import com.wire.kalium.logic.feature.conversation.messagetimer.UpdateMessageTimerUseCaseImpl import com.wire.kalium.logic.feature.conversation.mls.OneOnOneResolver -import com.wire.kalium.messaging.sending.MessageSender import com.wire.kalium.logic.feature.message.ephemeral.DeleteEphemeralMessagesAfterEndDateUseCase import com.wire.kalium.logic.feature.message.receipt.ConversationWorkQueue import com.wire.kalium.logic.feature.message.receipt.ParallelConversationWorkQueue @@ -101,6 +102,7 @@ import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCaseImpl import com.wire.kalium.logic.sync.SyncManager import com.wire.kalium.logic.sync.receiver.conversation.RenamedConversationEventHandler import com.wire.kalium.logic.sync.receiver.handler.CodeUpdateHandlerImpl +import com.wire.kalium.messaging.sending.MessageSender import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl import kotlinx.coroutines.CoroutineScope @@ -140,6 +142,7 @@ class ConversationScope internal constructor( private val persistConversationsUseCase: PersistConversationsUseCase, private val transactionProvider: CryptoTransactionProvider, private val resetMLSConversationUseCase: ResetMLSConversationUseCase, + private val systemMessageInserter: SystemMessageInserter, internal val dispatcher: KaliumDispatcher = KaliumDispatcherImpl, ) { @@ -271,7 +274,10 @@ class ConversationScope internal constructor( ) val updateConversationAccess: UpdateConversationAccessRoleUseCase - get() = UpdateConversationAccessRoleUseCase(conversationRepository, conversationGroupRepository, syncManager) + get() = UpdateConversationAccessRoleUseCaseImpl(conversationRepository, conversationGroupRepository, syncManager) + + val changeAccessForAppsInConversation: ChangeAccessForAppsInConversationUseCase + get() = ChangeAccessForAppsInConversationUseCase(updateConversationAccess, systemMessageInserter, selfUserId) val updateConversationMemberRole: UpdateConversationMemberRoleUseCase get() = UpdateConversationMemberRoleUseCaseImpl(conversationRepository) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/UpdateConversationAccessRoleUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/UpdateConversationAccessRoleUseCase.kt index e993c7b8f7b..194bb155d55 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/UpdateConversationAccessRoleUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/UpdateConversationAccessRoleUseCase.kt @@ -19,14 +19,15 @@ package com.wire.kalium.logic.feature.conversation import com.wire.kalium.common.error.CoreFailure +import com.wire.kalium.common.functional.Either +import com.wire.kalium.common.functional.flatMap +import com.wire.kalium.common.functional.fold import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationGroupRepository import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.common.functional.Either -import com.wire.kalium.common.functional.flatMap -import com.wire.kalium.common.functional.fold import com.wire.kalium.logic.sync.SyncManager +import io.mockative.Mockable import kotlinx.coroutines.flow.first /** @@ -47,17 +48,36 @@ import kotlinx.coroutines.flow.first * * @see Conversation.Access */ +@Mockable +interface UpdateConversationAccessRoleUseCase { + /** + * @param conversationId the id of the conversation + * @param accessRoles the set of access roles to set + * @param access the set of access to set + * @return the [Result] indicating a successful operation, otherwise a [CoreFailure] + */ + suspend operator fun invoke( + conversationId: ConversationId, + accessRoles: Set, + access: Set, + ): Result + + sealed interface Result { + data object Success : Result + data class Failure(val cause: CoreFailure) : Result + } +} -class UpdateConversationAccessRoleUseCase internal constructor( +internal class UpdateConversationAccessRoleUseCaseImpl internal constructor( private val conversationRepository: ConversationRepository, private val conversationGroupRepository: ConversationGroupRepository, private val syncManager: SyncManager -) { - suspend operator fun invoke( +) : UpdateConversationAccessRoleUseCase { + override suspend operator fun invoke( conversationId: ConversationId, accessRoles: Set, access: Set, - ): Result { + ): UpdateConversationAccessRoleUseCase.Result { syncManager.waitUntilLiveOrFailure().flatMap { if (!accessRoles.contains(Conversation.AccessRole.GUEST) @@ -72,14 +92,9 @@ class UpdateConversationAccessRoleUseCase internal constructor( return conversationRepository .updateAccessInfo(conversationId, access, accessRoles) .fold({ - Result.Failure(it) + UpdateConversationAccessRoleUseCase.Result.Failure(it) }, { - Result.Success + UpdateConversationAccessRoleUseCase.Result.Success }) } - - sealed interface Result { - data object Success : Result - data class Failure(val cause: CoreFailure) : Result - } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/apps/ChangeAccessForAppsInConversationUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/apps/ChangeAccessForAppsInConversationUseCase.kt new file mode 100644 index 00000000000..b9bec29bde1 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/apps/ChangeAccessForAppsInConversationUseCase.kt @@ -0,0 +1,64 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation.apps + +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.SystemMessageInserter +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.conversation.UpdateConversationAccessRoleUseCase + +/** + * Use case to change access for apps in a conversation. + * It updates the access roles and access of the conversation for apps (old service bots) + * and inserts a system message indicating the change. + */ +class ChangeAccessForAppsInConversationUseCase internal constructor( + private val updateConversationAccessRole: UpdateConversationAccessRoleUseCase, + private val systemMessageInserter: SystemMessageInserter, + private val selfUserId: UserId, +) { + + suspend operator fun invoke( + conversationId: ConversationId, + accessRoles: Set, + access: Set, + ): UpdateConversationAccessRoleUseCase.Result { + val result = updateConversationAccessRole( + conversationId = conversationId, + accessRoles = accessRoles, + access = access + ) + + when (result) { + is UpdateConversationAccessRoleUseCase.Result.Failure -> { + // No system message is inserted on failure + } + + is UpdateConversationAccessRoleUseCase.Result.Success -> { + systemMessageInserter.insertConversationAppsAccessChanged( + conversationId = conversationId, + senderUserId = selfUserId, + isAppsAccessEnabled = accessRoles.contains(Conversation.AccessRole.SERVICE) + ) + } + } + + return result + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/AccessUpdateEventHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/AccessUpdateEventHandler.kt index c4f94f8838b..a2e409114fc 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/AccessUpdateEventHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/AccessUpdateEventHandler.kt @@ -18,14 +18,16 @@ package com.wire.kalium.logic.sync.receiver.conversation import com.wire.kalium.common.error.StorageFailure +import com.wire.kalium.common.error.wrapStorageRequest +import com.wire.kalium.common.functional.Either import com.wire.kalium.logic.data.conversation.ConversationMapper import com.wire.kalium.logic.data.event.Event import com.wire.kalium.logic.data.id.toDao +import com.wire.kalium.logic.data.message.SystemMessageInserter import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.di.MapperProvider -import com.wire.kalium.common.functional.Either -import com.wire.kalium.common.error.wrapStorageRequest import com.wire.kalium.persistence.dao.conversation.ConversationDAO +import com.wire.kalium.persistence.dao.conversation.ConversationEntity import io.mockative.Mockable @Mockable @@ -34,18 +36,57 @@ interface AccessUpdateEventHandler { } @Suppress("FunctionNaming") -fun AccessUpdateEventHandler( +internal fun AccessUpdateEventHandler( selfUserId: UserId, conversationDAO: ConversationDAO, + systemMessageInserter: SystemMessageInserter, conversationMapper: ConversationMapper = MapperProvider.conversationMapper(selfUserId) ) = object : AccessUpdateEventHandler { override suspend fun handle(event: Event.Conversation.AccessUpdate): Either = wrapStorageRequest { + val newAccessList = conversationMapper.fromModelToDAOAccess(event.access) + val newAccessRole = conversationMapper.fromModelToDAOAccessRole(event.accessRole) + + val oldAccessRole = conversationDAO.getConversationById(event.conversationId.toDao())?.accessRole + val hadServiceRole = oldAccessRole?.contains(ConversationEntity.AccessRole.SERVICE) == true + val hasServiceRoleNow = newAccessRole.contains(ConversationEntity.AccessRole.SERVICE) + conversationDAO.updateAccess( conversationID = event.conversationId.toDao(), - accessList = conversationMapper.fromModelToDAOAccess(event.access), - accessRoleList = conversationMapper.fromModelToDAOAccessRole(event.accessRole) + accessList = newAccessList, + accessRoleList = newAccessRole ) + + // Persist system message if apps access changed + persistConversationAppsAccessChangedMessageIfChanged(event, hadServiceRole, hasServiceRoleNow) + } + + private suspend fun persistConversationAppsAccessChangedMessageIfChanged( + event: Event.Conversation.AccessUpdate, + hadServiceRole: Boolean, + hasServiceRoleNow: Boolean + ) { + when { + hadServiceRole && !hasServiceRoleNow -> { + // Apps access was disabled + systemMessageInserter.insertConversationAppsAccessChanged( + eventId = event.id, + conversationId = event.conversationId, + senderUserId = event.qualifiedFrom, + isAppsAccessEnabled = false + ) + } + + !hadServiceRole && hasServiceRoleNow -> { + // Apps access was enabled + systemMessageInserter.insertConversationAppsAccessChanged( + eventId = event.id, + conversationId = event.conversationId, + senderUserId = event.qualifiedFrom, + isAppsAccessEnabled = true + ) + } } + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/NewConversationEventHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/NewConversationEventHandler.kt index 413da24b346..32916f1b357 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/NewConversationEventHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/NewConversationEventHandler.kt @@ -18,16 +18,6 @@ package com.wire.kalium.logic.sync.receiver.conversation -import com.wire.kalium.logic.data.conversation.ConversationRepository -import com.wire.kalium.logic.data.conversation.NewGroupConversationSystemMessagesCreator -import com.wire.kalium.logic.data.conversation.toConversationType -import com.wire.kalium.logic.data.event.Event -import com.wire.kalium.logic.data.id.SelfTeamIdProvider -import com.wire.kalium.logic.data.id.TeamId -import com.wire.kalium.logic.data.id.toDao -import com.wire.kalium.logic.data.id.toModel -import com.wire.kalium.logic.data.user.UserRepository -import com.wire.kalium.logic.feature.conversation.mls.OneOnOneResolver import com.wire.kalium.common.functional.Either import com.wire.kalium.common.functional.flatMap import com.wire.kalium.common.functional.getOrNull @@ -36,8 +26,18 @@ import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.functional.onSuccess import com.wire.kalium.common.logger.kaliumLogger import com.wire.kalium.cryptography.CryptoTransactionContext +import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.conversation.ConversationSyncReason +import com.wire.kalium.logic.data.conversation.NewGroupConversationSystemMessagesCreator import com.wire.kalium.logic.data.conversation.PersistConversationUseCase +import com.wire.kalium.logic.data.conversation.toConversationType +import com.wire.kalium.logic.data.event.Event +import com.wire.kalium.logic.data.id.SelfTeamIdProvider +import com.wire.kalium.logic.data.id.TeamId +import com.wire.kalium.logic.data.id.toDao +import com.wire.kalium.logic.data.id.toModel +import com.wire.kalium.logic.data.user.UserRepository +import com.wire.kalium.logic.feature.conversation.mls.OneOnOneResolver import com.wire.kalium.logic.util.createEventProcessingLogger import com.wire.kalium.persistence.dao.conversation.ConversationEntity import com.wire.kalium.util.DateTimeUtil @@ -130,6 +130,12 @@ internal class NewConversationEventHandlerImpl( event.conversation, event.dateTime ) + newGroupConversationSystemMessagesCreator.conversationAppsAccessIfEnabled( + eventId = event.id, + conversationId = event.conversationId, + hasAppsAccessEnabled = event.conversation.hasAppsAccessEnabled(), + creatorId = event.senderUserId + ) } } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepositoryTest.kt index b0804994550..574d2d4c766 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepositoryTest.kt @@ -22,6 +22,8 @@ import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.common.error.MLSFailure import com.wire.kalium.common.error.NetworkFailure import com.wire.kalium.common.error.StorageFailure +import com.wire.kalium.common.functional.Either +import com.wire.kalium.common.functional.right import com.wire.kalium.logic.data.conversation.mls.MLSAdditionResult import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.GroupID @@ -40,7 +42,6 @@ import com.wire.kalium.logic.framework.TestConversation import com.wire.kalium.logic.framework.TestConversation.ADD_MEMBER_TO_CONVERSATION_SUCCESSFUL_RESPONSE import com.wire.kalium.logic.framework.TestConversation.ADD_SERVICE_TO_CONVERSATION_SUCCESSFUL_RESPONSE import com.wire.kalium.logic.framework.TestUser -import com.wire.kalium.common.functional.Either import com.wire.kalium.logic.sync.local.LocalEventRepository import com.wire.kalium.logic.sync.receiver.conversation.ConversationMessageTimerEventHandler import com.wire.kalium.logic.sync.receiver.handler.legalhold.LegalHoldHandler @@ -109,6 +110,7 @@ class ConversationGroupRepositoryTest { .withSuccessfulNewConversationMemberHandled() .withSuccessfulNewConversationGroupStartedUnverifiedWarningHandled() .withSuccessfulLegalHoldHandleConversationMembersChanged() + .withConversationAppsAccessIfEnabled() .arrange() val result = conversationGroupRepository.createGroupConversation( @@ -141,6 +143,7 @@ class ConversationGroupRepositoryTest { .withSuccessfulNewConversationMemberHandled() .withSuccessfulNewConversationGroupStartedUnverifiedWarningHandled() .withSuccessfulLegalHoldHandleConversationMembersChanged() + .withConversationAppsAccessIfEnabled() .arrange() val result = conversationGroupRepository.createGroupConversation( @@ -173,6 +176,7 @@ class ConversationGroupRepositoryTest { .withSuccessfulNewConversationMemberHandled() .withSuccessfulNewConversationGroupStartedUnverifiedWarningHandled() .withSuccessfulLegalHoldHandleConversationMembersChanged() + .withConversationAppsAccessIfEnabled() .arrange() val result = conversationGroupRepository.createGroupConversation( @@ -188,6 +192,33 @@ class ConversationGroupRepositoryTest { }.wasInvoked(once) } + @Test + fun givenSuccess_whenCallingCreateGroupConversation_AndsAppsEnabledPersistSystemMessage() = runTest { + val (arrangement, conversationGroupRepository) = Arrangement() + .withCreateNewConversationAPIResponses(arrayOf(NetworkResponse.Success(CONVERSATION_RESPONSE, emptyMap(), 201))) + .withSelfTeamId(Either.Right(TestUser.SELF.teamId)) + .withInsertConversationSuccess() + .withConversationById(TestConversation.ENTITY_GROUP.copy(protocolInfo = PROTEUS_PROTOCOL_INFO)) + .withSuccessfulNewConversationGroupStartedHandled() + .withSuccessfulNewConversationMemberHandled() + .withSuccessfulNewConversationGroupStartedUnverifiedWarningHandled() + .withSuccessfulLegalHoldHandleConversationMembersChanged() + .withConversationAppsAccessIfEnabled() + .arrange() + + val result = conversationGroupRepository.createGroupConversation( + GROUP_NAME, + listOf(TestUser.USER_ID), + CreateConversationParam(protocol = CreateConversationParam.Protocol.PROTEUS) + ) + + result.shouldSucceed() + + coVerify { + arrangement.newGroupConversationSystemMessagesCreator.conversationAppsAccessIfEnabled(any(), any(), any(), any()) + }.wasInvoked(once) + } + @Test fun givenCreatingAGroupConversation_whenThereIsAnUnreachableError_thenRetryIsExecutedWithValidUsersOnly() = runTest { val (arrangement, conversationGroupRepository) = Arrangement() @@ -205,6 +236,7 @@ class ConversationGroupRepositoryTest { .withSuccessfulNewConversationGroupStartedUnverifiedWarningHandled() .withSuccessfulLegalHoldHandleConversationMembersChanged() .withInsertFailedToAddSystemMessageSuccess() + .withConversationAppsAccessIfEnabled() .arrange() val unreachableUserId = TestUser.USER_ID.copy(domain = "unstableDomain2.com") @@ -349,6 +381,7 @@ class ConversationGroupRepositoryTest { .withInsertFailedToAddSystemMessageSuccess() .withSuccessfulFetchUsersLegalHoldConsent(ListUsersLegalHoldConsent(usersWithConsent, usersWithoutConsent, usersFailed)) .withSuccessfulLegalHoldHandleConversationMembersChanged() + .withConversationAppsAccessIfEnabled() .arrange() val result = conversationGroupRepository.createGroupConversation( @@ -446,6 +479,7 @@ class ConversationGroupRepositoryTest { .withSuccessfulNewConversationMemberHandled() .withSuccessfulNewConversationGroupStartedUnverifiedWarningHandled() .withSuccessfulLegalHoldHandleConversationMembersChanged() + .withConversationAppsAccessIfEnabled() .arrange() val result = conversationGroupRepository.createGroupConversation( @@ -489,6 +523,7 @@ class ConversationGroupRepositoryTest { .withSuccessfulNewConversationGroupStartedUnverifiedWarningHandled() .withSuccessfulLegalHoldHandleConversationMembersChanged() .withInsertFailedToAddSystemMessageSuccess() + .withConversationAppsAccessIfEnabled() .arrange() val result = conversationGroupRepository.createGroupConversation( @@ -1458,6 +1493,7 @@ class ConversationGroupRepositoryTest { .withSuccessfulNewConversationMemberHandled() .withSuccessfulNewConversationGroupStartedUnverifiedWarningHandled() .withSuccessfulLegalHoldHandleConversationMembersChanged() + .withConversationAppsAccessIfEnabled() .arrange() val result = conversationGroupRepository.createGroupConversation( @@ -1781,6 +1817,7 @@ class ConversationGroupRepositoryTest { .withSuccessfulNewConversationMemberHandled() .withSuccessfulNewConversationGroupStartedUnverifiedWarningHandled() .withSuccessfulLegalHoldHandleConversationMembersChanged() + .withConversationAppsAccessIfEnabled() .arrange() val result = conversationGroupRepository.createGroupConversation( @@ -2091,6 +2128,12 @@ class ConversationGroupRepositoryTest { }.returns(result) } + suspend fun withConversationAppsAccessIfEnabled() = apply { + coEvery { + newGroupConversationSystemMessagesCreator.conversationAppsAccessIfEnabled(any(), any(), any(), any()) + }.returns(Unit.right()) + } + suspend fun withSuccessfulNewConversationGroupStartedHandled() = apply { coEvery { newGroupConversationSystemMessagesCreator.conversationStarted(any(), any(), any()) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/SystemMessageInserterTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/SystemMessageInserterTest.kt new file mode 100644 index 00000000000..f4c4a5b3e16 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/SystemMessageInserterTest.kt @@ -0,0 +1,269 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.message + +import com.wire.kalium.common.functional.right +import com.wire.kalium.logic.data.MockConversation +import com.wire.kalium.logic.framework.TestUser +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.matches +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class SystemMessageInserterTest { + + @Test + fun givenAppsAccessEnabledTrue_whenInsertingSystemMessage_thenMessageIsPersistedWithCorrectContent() = runTest { + // Given + val conversationId = MockConversation.ID + val senderUserId = TestUser.SELF.id + val isAppsAccessEnabled = true + + val (arrangement, systemMessageInserter) = Arrangement() + .arrange() + + // When + systemMessageInserter.insertConversationAppsAccessChanged( + conversationId = conversationId, + senderUserId = senderUserId, + isAppsAccessEnabled = isAppsAccessEnabled + ) + + // Then + coVerify { + arrangement.persistMessage( + matches { message -> + message is Message.System && + message.content is MessageContent.ConversationAppsEnabledChanged && + (message.content as MessageContent.ConversationAppsEnabledChanged).isEnabled + } + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenAppsAccessEnabledFalse_whenInsertingSystemMessage_thenMessageIsPersistedWithCorrectContent() = runTest { + // Given + val conversationId = MockConversation.ID + val senderUserId = TestUser.SELF.id + val isAppsAccessEnabled = false + + val (arrangement, systemMessageInserter) = Arrangement() + .arrange() + + // When + systemMessageInserter.insertConversationAppsAccessChanged( + conversationId = conversationId, + senderUserId = senderUserId, + isAppsAccessEnabled = isAppsAccessEnabled + ) + + // Then + coVerify { + arrangement.persistMessage( + matches { message -> + message is Message.System && + message.content is MessageContent.ConversationAppsEnabledChanged && + !(message.content as MessageContent.ConversationAppsEnabledChanged).isEnabled + } + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenEventId_whenInsertingSystemMessage_thenMessageUsesProvidedEventId() = runTest { + // Given + val eventId = "custom-event-id-456" + val conversationId = MockConversation.ID + val senderUserId = TestUser.SELF.id + + val (arrangement, systemMessageInserter) = Arrangement() + .arrange() + + // When + systemMessageInserter.insertConversationAppsAccessChanged( + eventId = eventId, + conversationId = conversationId, + senderUserId = senderUserId, + isAppsAccessEnabled = true + ) + + // Then + coVerify { + arrangement.persistMessage( + matches { message -> + message is Message.System && message.id == eventId + } + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenConversationId_whenInsertingSystemMessage_thenMessageHasCorrectConversationId() = runTest { + // Given + val conversationId = MockConversation.ID + val senderUserId = TestUser.SELF.id + + val (arrangement, systemMessageInserter) = Arrangement() + .arrange() + + // When + systemMessageInserter.insertConversationAppsAccessChanged( + conversationId = conversationId, + senderUserId = senderUserId, + isAppsAccessEnabled = true + ) + + // Then + coVerify { + arrangement.persistMessage( + matches { message -> + message is Message.System && message.conversationId == conversationId + } + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenSenderUserId_whenInsertingSystemMessage_thenMessageHasCorrectSender() = runTest { + // Given + val conversationId = MockConversation.ID + val senderUserId = TestUser.OTHER_USER_ID + + val (arrangement, systemMessageInserter) = Arrangement() + .arrange() + + // When + systemMessageInserter.insertConversationAppsAccessChanged( + conversationId = conversationId, + senderUserId = senderUserId, + isAppsAccessEnabled = true + ) + + // Then + coVerify { + arrangement.persistMessage( + matches { message -> + message is Message.System && message.senderUserId == senderUserId + } + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenSystemMessage_whenInserting_thenMessageVisibilityIsVisible() = runTest { + // Given + val conversationId = MockConversation.ID + val senderUserId = TestUser.SELF.id + + val (arrangement, systemMessageInserter) = Arrangement() + .arrange() + + // When + systemMessageInserter.insertConversationAppsAccessChanged( + conversationId = conversationId, + senderUserId = senderUserId, + isAppsAccessEnabled = true + ) + + // Then + coVerify { + arrangement.persistMessage( + matches { message -> + message is Message.System && message.visibility == Message.Visibility.VISIBLE + } + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenSystemMessage_whenInserting_thenMessageStatusIsSent() = runTest { + // Given + val conversationId = MockConversation.ID + val senderUserId = TestUser.SELF.id + + val (arrangement, systemMessageInserter) = Arrangement() + .arrange() + + // When + systemMessageInserter.insertConversationAppsAccessChanged( + conversationId = conversationId, + senderUserId = senderUserId, + isAppsAccessEnabled = true + ) + + // Then + coVerify { + arrangement.persistMessage( + matches { message -> + message is Message.System && message.status == Message.Status.Sent + } + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenSystemMessage_whenInserting_thenExpirationDataIsNull() = runTest { + // Given + val conversationId = MockConversation.ID + val senderUserId = TestUser.SELF.id + + val (arrangement, systemMessageInserter) = Arrangement() + .arrange() + + // When + systemMessageInserter.insertConversationAppsAccessChanged( + conversationId = conversationId, + senderUserId = senderUserId, + isAppsAccessEnabled = true + ) + + // Then + coVerify { + arrangement.persistMessage( + matches { message -> + message is Message.System && message.expirationData == null + } + ) + }.wasInvoked(exactly = once) + } + + private class Arrangement { + val persistMessage = mock(PersistMessageUseCase::class) + + init { + runBlocking { + coEvery { + persistMessage(any()) + }.returns(Unit.right()) + } + } + + private val systemMessageInserter = SystemMessageInserterImpl( + persistMessage = persistMessage, + selfUserId = TestUser.SELF.id + ) + + fun arrange() = this to systemMessageInserter + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/UpdateConversationAccessUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/UpdateConversationAccessUseCaseTest.kt index a0ac60eebc1..d38877f39db 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/UpdateConversationAccessUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/UpdateConversationAccessUseCaseTest.kt @@ -20,6 +20,7 @@ package com.wire.kalium.logic.feature.conversation import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.common.error.NetworkFailure +import com.wire.kalium.common.functional.Either import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.Conversation.ProtocolInfo import com.wire.kalium.logic.data.conversation.ConversationGroupRepository @@ -29,7 +30,6 @@ import com.wire.kalium.logic.data.conversation.MutedConversationStatus import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.TeamId import com.wire.kalium.logic.framework.TestConversation -import com.wire.kalium.common.functional.Either import com.wire.kalium.logic.sync.SyncManager import com.wire.kalium.util.time.UNIX_FIRST_DATE import io.mockative.any @@ -415,11 +415,11 @@ class UpdateConversationAccessUseCaseTest { } private class Arrangement { - val conversationRepository = mock(ConversationRepository::class) + val conversationRepository = mock(ConversationRepository::class) val conversationGroupRepository = mock(ConversationGroupRepository::class) val syncManager = mock(SyncManager::class) - val updateConversationAccess: UpdateConversationAccessRoleUseCase = UpdateConversationAccessRoleUseCase( + val updateConversationAccess: UpdateConversationAccessRoleUseCase = UpdateConversationAccessRoleUseCaseImpl( conversationRepository, conversationGroupRepository, syncManager diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/apps/ChangeAccessForAppsInConversationUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/apps/ChangeAccessForAppsInConversationUseCaseTest.kt new file mode 100644 index 00000000000..26ed0874d06 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/apps/ChangeAccessForAppsInConversationUseCaseTest.kt @@ -0,0 +1,283 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation.apps + +import com.wire.kalium.common.error.NetworkFailure +import com.wire.kalium.logic.data.MockConversation +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.message.SystemMessageInserter +import com.wire.kalium.logic.feature.conversation.UpdateConversationAccessRoleUseCase +import com.wire.kalium.logic.framework.TestUser +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.eq +import io.mockative.matches +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import okio.IOException +import kotlin.test.Test +import kotlin.test.assertIs + +class ChangeAccessForAppsInConversationUseCaseTest { + + @Test + fun givenSuccessfulUpdate_whenEnablingAppsAccess_thenSystemMessageIsInsertedWithEnabledTrue() = runTest { + // Given + val accessRoles = setOf( + Conversation.AccessRole.TEAM_MEMBER, + Conversation.AccessRole.NON_TEAM_MEMBER, + Conversation.AccessRole.SERVICE + ) + val access = setOf(Conversation.Access.INVITE, Conversation.Access.CODE) + + val (arrangement, changeAccessForApps) = Arrangement() + .withUpdateAccessRoleReturning(UpdateConversationAccessRoleUseCase.Result.Success) + .arrange() + + // When + val result = changeAccessForApps( + conversationId = MockConversation.ID, + accessRoles = accessRoles, + access = access + ) + + // Then + assertIs(result) + + coVerify { + arrangement.updateConversationAccessRole( + conversationId = eq(MockConversation.ID), + accessRoles = eq(accessRoles), + access = eq(access) + ) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.systemMessageInserter.insertConversationAppsAccessChanged( + eventId = any(), + conversationId = eq(MockConversation.ID), + senderUserId = eq(arrangement.selfUserId), + isAppsAccessEnabled = eq(true) + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenSuccessfulUpdate_whenDisablingAppsAccess_thenSystemMessageIsInsertedWithEnabledFalse() = runTest { + // Given + val accessRoles = setOf( + Conversation.AccessRole.TEAM_MEMBER, + Conversation.AccessRole.NON_TEAM_MEMBER + ) + val access = setOf(Conversation.Access.INVITE) + + val (arrangement, changeAccessForApps) = Arrangement() + .withUpdateAccessRoleReturning(UpdateConversationAccessRoleUseCase.Result.Success) + .arrange() + + // When + val result = changeAccessForApps( + conversationId = MockConversation.ID, + accessRoles = accessRoles, + access = access + ) + + // Then + assertIs(result) + + coVerify { + arrangement.systemMessageInserter.insertConversationAppsAccessChanged( + eventId = any(), + conversationId = eq(MockConversation.ID), + senderUserId = eq(arrangement.selfUserId), + isAppsAccessEnabled = eq(false) + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenFailedUpdate_whenChangingAppsAccess_thenNoSystemMessageIsInserted() = runTest { + // Given + val accessRoles = setOf( + Conversation.AccessRole.TEAM_MEMBER, + Conversation.AccessRole.SERVICE + ) + val access = setOf(Conversation.Access.INVITE) + val networkError = NetworkFailure.NoNetworkConnection(IOException()) + + val (arrangement, changeAccessForApps) = Arrangement() + .withUpdateAccessRoleReturning(UpdateConversationAccessRoleUseCase.Result.Failure(networkError)) + .arrange() + + // When + val result = changeAccessForApps( + conversationId = MockConversation.ID, + accessRoles = accessRoles, + access = access + ) + + // Then + assertIs(result) + assertIs(result.cause) + + coVerify { + arrangement.updateConversationAccessRole( + conversationId = any(), + accessRoles = any(), + access = any() + ) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.systemMessageInserter.insertConversationAppsAccessChanged( + eventId = any(), + conversationId = any(), + senderUserId = any(), + isAppsAccessEnabled = any() + ) + }.wasNotInvoked() + } + + @Test + fun givenSuccessfulUpdate_whenChangingAccess_thenCorrectAccessRolesArePassedToUpdateUseCase() = runTest { + // Given + val expectedAccessRoles = setOf( + Conversation.AccessRole.TEAM_MEMBER, + Conversation.AccessRole.NON_TEAM_MEMBER, + Conversation.AccessRole.GUEST, + Conversation.AccessRole.SERVICE + ) + val expectedAccess = setOf(Conversation.Access.INVITE, Conversation.Access.CODE) + + val (arrangement, changeAccessForApps) = Arrangement() + .withUpdateAccessRoleReturning(UpdateConversationAccessRoleUseCase.Result.Success) + .arrange() + + // When + changeAccessForApps( + conversationId = MockConversation.ID, + accessRoles = expectedAccessRoles, + access = expectedAccess + ) + + // Then + coVerify { + arrangement.updateConversationAccessRole( + conversationId = eq(MockConversation.ID), + accessRoles = matches { it == expectedAccessRoles }, + access = matches { it == expectedAccess } + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenSuccessfulUpdate_whenChangingAccess_thenSystemMessageContainsCorrectSender() = runTest { + // Given + val accessRoles = setOf(Conversation.AccessRole.SERVICE) + val access = setOf(Conversation.Access.INVITE) + + val (arrangement, changeAccessForApps) = Arrangement() + .withUpdateAccessRoleReturning(UpdateConversationAccessRoleUseCase.Result.Success) + .arrange() + + // When + changeAccessForApps( + conversationId = MockConversation.ID, + accessRoles = accessRoles, + access = access + ) + + // Then + coVerify { + arrangement.systemMessageInserter.insertConversationAppsAccessChanged( + eventId = any(), + conversationId = any(), + senderUserId = matches { it == arrangement.selfUserId }, + isAppsAccessEnabled = any() + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenSuccessfulUpdate_whenChangingAccess_thenSystemMessageContainsCorrectConversationId() = runTest { + // Given + val accessRoles = setOf(Conversation.AccessRole.SERVICE) + val access = setOf(Conversation.Access.INVITE) + + val (arrangement, changeAccessForApps) = Arrangement() + .withUpdateAccessRoleReturning(UpdateConversationAccessRoleUseCase.Result.Success) + .arrange() + + // When + changeAccessForApps( + conversationId = MockConversation.ID, + accessRoles = accessRoles, + access = access + ) + + // Then + coVerify { + arrangement.systemMessageInserter.insertConversationAppsAccessChanged( + eventId = any(), + conversationId = matches { it == MockConversation.ID }, + senderUserId = any(), + isAppsAccessEnabled = any() + ) + }.wasInvoked(exactly = once) + } + + private class Arrangement { + val updateConversationAccessRole = mock(UpdateConversationAccessRoleUseCase::class) + val systemMessageInserter = mock(SystemMessageInserter::class) + val selfUserId = TestUser.SELF.id + + val changeAccessForApps = ChangeAccessForAppsInConversationUseCase( + updateConversationAccessRole = updateConversationAccessRole, + systemMessageInserter = systemMessageInserter, + selfUserId = selfUserId + ) + + init { + runBlocking { + coEvery { + systemMessageInserter.insertConversationAppsAccessChanged( + eventId = any(), + conversationId = any(), + senderUserId = any(), + isAppsAccessEnabled = any() + ) + }.returns(Unit) + } + } + + suspend fun withUpdateAccessRoleReturning(result: UpdateConversationAccessRoleUseCase.Result) = apply { + coEvery { + updateConversationAccessRole( + conversationId = any(), + accessRoles = any(), + access = any() + ) + }.returns(result) + } + + fun arrange() = this to changeAccessForApps + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/AccessUpdateHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/AccessUpdateHandlerTest.kt index 57d8b87cce7..a7f70caf528 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/AccessUpdateHandlerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/AccessUpdateHandlerTest.kt @@ -21,6 +21,7 @@ import com.wire.kalium.logic.data.conversation.Conversation.Access import com.wire.kalium.logic.data.conversation.Conversation.AccessRole import com.wire.kalium.logic.data.conversation.ConversationMapper import com.wire.kalium.logic.data.id.PersistenceQualifiedId +import com.wire.kalium.logic.data.message.SystemMessageInserter import com.wire.kalium.logic.framework.TestConversation import com.wire.kalium.logic.framework.TestEvent import com.wire.kalium.logic.framework.TestUser @@ -29,9 +30,11 @@ import com.wire.kalium.persistence.dao.conversation.ConversationEntity import io.mockative.any import io.mockative.coEvery import io.mockative.coVerify +import io.mockative.eq import io.mockative.every import io.mockative.matches import io.mockative.mock +import io.mockative.once import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import kotlin.test.Test @@ -77,21 +80,209 @@ class AccessUpdateHandlerTest { } } + @Test + fun givenAppsAccessEnabled_whenHandlingAccessUpdate_thenSystemMessageIsInserted() = runTest { + // Given + val event = TestEvent.accessUpdate().copy( + accessRole = setOf(AccessRole.TEAM_MEMBER, AccessRole.SERVICE) + ) + + val (arrangement, eventHandler) = Arrangement() + .withMappingModelToDAOAccess( + event.access, + listOf(ConversationEntity.Access.PRIVATE) + ) + .withMappingModelToDAOAccessRole( + event.accessRole, + listOf(ConversationEntity.AccessRole.TEAM_MEMBER, ConversationEntity.AccessRole.SERVICE) + ) + .withExistingConversationAccessRole( + listOf(ConversationEntity.AccessRole.TEAM_MEMBER) + ) + .arrange() + + // When + eventHandler.handle(event) + + // Then + coVerify { + arrangement.systemMessageInserter.insertConversationAppsAccessChanged( + eventId = eq(event.id), + conversationId = eq(event.conversationId), + senderUserId = eq(event.qualifiedFrom), + isAppsAccessEnabled = eq(true) + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenAppsAccessDisabled_whenHandlingAccessUpdate_thenSystemMessageIsInserted() = runTest { + // Given + val event = TestEvent.accessUpdate().copy( + accessRole = setOf(AccessRole.TEAM_MEMBER) + ) + + val (arrangement, eventHandler) = Arrangement() + .withMappingModelToDAOAccess( + event.access, + listOf(ConversationEntity.Access.PRIVATE) + ) + .withMappingModelToDAOAccessRole( + event.accessRole, + listOf(ConversationEntity.AccessRole.TEAM_MEMBER) + ) + .withExistingConversationAccessRole( + listOf(ConversationEntity.AccessRole.TEAM_MEMBER, ConversationEntity.AccessRole.SERVICE) + ) + .arrange() + + // When + eventHandler.handle(event) + + // Then + coVerify { + arrangement.systemMessageInserter.insertConversationAppsAccessChanged( + eventId = eq(event.id), + conversationId = eq(event.conversationId), + senderUserId = eq(event.qualifiedFrom), + isAppsAccessEnabled = eq(false) + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenNoChangeInAppsAccess_whenHandlingAccessUpdate_thenNoSystemMessageIsInserted() = runTest { + // Given + val event = TestEvent.accessUpdate().copy( + accessRole = setOf(AccessRole.TEAM_MEMBER, AccessRole.SERVICE) + ) + + val (arrangement, eventHandler) = Arrangement() + .withMappingModelToDAOAccess( + event.access, + listOf(ConversationEntity.Access.PRIVATE) + ) + .withMappingModelToDAOAccessRole( + event.accessRole, + listOf(ConversationEntity.AccessRole.TEAM_MEMBER, ConversationEntity.AccessRole.SERVICE) + ) + .withExistingConversationAccessRole( + listOf(ConversationEntity.AccessRole.TEAM_MEMBER, ConversationEntity.AccessRole.SERVICE) + ) + .arrange() + + // When + eventHandler.handle(event) + + // Then + coVerify { + arrangement.systemMessageInserter.insertConversationAppsAccessChanged( + eventId = any(), + conversationId = any(), + senderUserId = any(), + isAppsAccessEnabled = any() + ) + }.wasNotInvoked() + } + + @Test + fun givenEventIdProvided_whenHandlingAccessUpdate_thenEventIdIsPassedToSystemMessage() = runTest { + // Given + val eventId = "custom-event-id-123" + val event = TestEvent.accessUpdate(eventId = eventId).copy( + accessRole = setOf(AccessRole.SERVICE) + ) + + val (arrangement, eventHandler) = Arrangement() + .withMappingModelToDAOAccess( + event.access, + listOf(ConversationEntity.Access.PRIVATE) + ) + .withMappingModelToDAOAccessRole( + event.accessRole, + listOf(ConversationEntity.AccessRole.SERVICE) + ) + .withExistingConversationAccessRole( + listOf(ConversationEntity.AccessRole.TEAM_MEMBER) + ) + .arrange() + + // When + eventHandler.handle(event) + + // Then + coVerify { + arrangement.systemMessageInserter.insertConversationAppsAccessChanged( + eventId = matches { it == eventId }, + conversationId = any(), + senderUserId = any(), + isAppsAccessEnabled = any() + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenSenderFromEvent_whenHandlingAccessUpdate_thenSenderIsPassedToSystemMessage() = runTest { + // Given + val senderId = TestUser.OTHER_USER_ID + val event = TestEvent.accessUpdate().copy( + qualifiedFrom = senderId, + accessRole = setOf(AccessRole.SERVICE) + ) + + val (arrangement, eventHandler) = Arrangement() + .withMappingModelToDAOAccess( + event.access, + listOf(ConversationEntity.Access.PRIVATE) + ) + .withMappingModelToDAOAccessRole( + event.accessRole, + listOf(ConversationEntity.AccessRole.SERVICE) + ) + .withExistingConversationAccessRole( + listOf(ConversationEntity.AccessRole.TEAM_MEMBER) + ) + .arrange() + + // When + eventHandler.handle(event) + + // Then + coVerify { + arrangement.systemMessageInserter.insertConversationAppsAccessChanged( + eventId = any(), + conversationId = any(), + senderUserId = matches { it == senderId }, + isAppsAccessEnabled = any() + ) + }.wasInvoked(exactly = once) + } + private class Arrangement { val conversationDAO = mock(ConversationDAO::class) val conversationMapper = mock(ConversationMapper::class) + val systemMessageInserter = mock(SystemMessageInserter::class) init { runBlocking { coEvery { conversationDAO.updateAccess(any(), any(), any()) }.returns(Unit) + coEvery { + systemMessageInserter.insertConversationAppsAccessChanged( + eventId = any(), + conversationId = any(), + senderUserId = any(), + isAppsAccessEnabled = any() + ) + }.returns(Unit) } } private val accessUpdateEventHandler: AccessUpdateEventHandler = AccessUpdateEventHandler( selfUserId = TestUser.USER_ID, conversationDAO = conversationDAO, - conversationMapper = conversationMapper + conversationMapper = conversationMapper, + systemMessageInserter = systemMessageInserter ) fun withMappingModelToDAOAccess(param: Set, result: List) = apply { @@ -106,6 +297,30 @@ class AccessUpdateHandlerTest { }.returns(result) } + fun withInsertMessageReturningUnit() = apply { + runBlocking { + coEvery { + systemMessageInserter.insertConversationAppsAccessChanged( + eventId = any(), + conversationId = any(), + senderUserId = any(), + isAppsAccessEnabled = any() + ) + }.returns(Unit) + } + } + + suspend fun withExistingConversationAccessRole(accessRoles: List?) = apply { + val conversationEntity = if (accessRoles != null) { + TestConversation.ENTITY.copy(accessRole = accessRoles) + } else { + null + } + coEvery { + conversationDAO.getConversationById(any()) + }.returns(conversationEntity) + } + fun arrange() = this to accessUpdateEventHandler } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/NewConversationEventHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/NewConversationEventHandlerTest.kt index 8012b076680..824f714ebb6 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/NewConversationEventHandlerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/NewConversationEventHandlerTest.kt @@ -21,6 +21,7 @@ package com.wire.kalium.logic.sync.receiver.conversation import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.common.error.StorageFailure import com.wire.kalium.common.functional.Either +import com.wire.kalium.common.functional.right import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.conversation.NewGroupConversationSystemMessagesCreator import com.wire.kalium.logic.data.conversation.PersistConversationUseCase @@ -30,6 +31,7 @@ import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.id.SelfTeamIdProvider import com.wire.kalium.logic.data.id.TeamId +import com.wire.kalium.logic.data.id.toApi import com.wire.kalium.logic.data.id.toDao import com.wire.kalium.logic.data.id.toModel import com.wire.kalium.logic.data.user.UserId @@ -79,6 +81,7 @@ class NewConversationEventHandlerTest { .withConversationResolvedMembersSystemMessage() .withReadReceiptsSystemMessage() .withQualifiedId(creatorQualifiedId) + .withConversationAppsAccessIfEnabled() .arrange() eventHandler.handle(arrangement.transactionContext, event) @@ -112,6 +115,7 @@ class NewConversationEventHandlerTest { .withConversationResolvedMembersSystemMessage() .withReadReceiptsSystemMessage() .withQualifiedId(creatorQualifiedId) + .withConversationAppsAccessIfEnabled() .arrange() eventHandler.handle(arrangement.transactionContext, event) @@ -126,6 +130,7 @@ class NewConversationEventHandlerTest { // given val event = testNewConversationEvent( conversation = TestConversation.CONVERSATION_RESPONSE.copy( + id = TestConversation.ID.toApi(), creator = "creatorId@creatorDomain", receiptMode = ReceiptMode.ENABLED ), @@ -147,6 +152,7 @@ class NewConversationEventHandlerTest { .withConversationUnverifiedWarningSystemMessage() .withReadReceiptsSystemMessage() .withQualifiedId(creatorQualifiedId) + .withConversationAppsAccessIfEnabled() .arrange() // when @@ -178,6 +184,15 @@ class NewConversationEventHandlerTest { eq(event.dateTime) ) }.wasInvoked(exactly = once) + + coVerify { + arrangement.newGroupConversationSystemMessagesCreator.conversationAppsAccessIfEnabled( + eq(event.id), + eq(event.conversation.id.toModel()), + eq(event.conversation.hasAppsAccessEnabled()), + eq(event.senderUserId) + ) + }.wasInvoked(exactly = once) } @Test @@ -379,6 +394,12 @@ class NewConversationEventHandlerTest { }.returns(result) } + suspend fun withConversationAppsAccessIfEnabled() = apply { + coEvery { + newGroupConversationSystemMessagesCreator.conversationAppsAccessIfEnabled(any(), any(), any(), any()) + }.returns(Unit.right()) + } + fun arrange() = this to newConversationEventHandler } diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/conversation/ConversationResponse.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/conversation/ConversationResponse.kt index 00e706ea8ef..d7878efc070 100644 --- a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/conversation/ConversationResponse.kt +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/conversation/ConversationResponse.kt @@ -142,6 +142,10 @@ data class ConversationResponse( @SerialName("channel") CHANNEL, } + + fun hasAppsAccessEnabled(): Boolean { + return accessRole?.any { it == ConversationAccessRoleDTO.SERVICE } == true + } } @Serializable diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDetailsView.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDetailsView.sq index 414fdfb59f9..369acbe59bc 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDetailsView.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDetailsView.sq @@ -141,6 +141,7 @@ QuotedAssetContent.asset_mime_type AS quotedAssetMimeType, QuotedAssetContent.asset_name AS quotedAssetName, QuotedLocationContent.name AS quotedLocationName, +ConversationAppsEnabledChanged.is_apps_enabled AS isConversationAppsEnabled, NewConversationReceiptMode.receipt_mode AS newConversationReceiptMode, ConversationReceiptModeChanged.receipt_mode AS conversationReceiptModeChanged, @@ -189,6 +190,7 @@ LEFT JOIN MessageTextContent AS QuotedTextContent ON QuotedTextContent.message_i LEFT JOIN MessageAssetContent AS QuotedAssetContent ON QuotedAssetContent.message_id = QuotedMessage.id AND QuotedMessage.conversation_id = TextContent.conversation_id LEFT JOIN MessageConversationLocationContent AS QuotedLocationContent ON QuotedLocationContent.message_id = QuotedMessage.id AND QuotedMessage.conversation_id = TextContent.conversation_id -- end joins for quoted messages +LEFT JOIN MessageConversationAppsEnabledChangedContent AS ConversationAppsEnabledChanged ON Message.id = ConversationAppsEnabledChanged.message_id AND Message.conversation_id = ConversationAppsEnabledChanged.conversation_id LEFT JOIN MessageNewConversationReceiptModeContent AS NewConversationReceiptMode ON Message.id = NewConversationReceiptMode.message_id AND Message.conversation_id = NewConversationReceiptMode.conversation_id LEFT JOIN MessageConversationReceiptModeChangedContent AS ConversationReceiptModeChanged ON Message.id = ConversationReceiptModeChanged.message_id AND Message.conversation_id = ConversationReceiptModeChanged.conversation_id LEFT JOIN MessageConversationTimerChangedContent AS ConversationTimerChangedContent ON Message.id = ConversationTimerChangedContent.message_id AND Message.conversation_id = ConversationTimerChangedContent.conversation_id diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq index fc71f4084a0..e9a63660b32 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq @@ -268,6 +268,16 @@ CREATE TABLE MessageLegalHoldContent ( PRIMARY KEY (message_id, conversation_id) ); +CREATE TABLE MessageConversationAppsEnabledChangedContent ( + message_id TEXT NOT NULL, + conversation_id TEXT AS QualifiedIDEntity NOT NULL, + + is_apps_enabled INTEGER AS Boolean NOT NULL DEFAULT(0), + + FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE ON UPDATE CASCADE, + PRIMARY KEY (message_id, conversation_id) +); + needsToBeNotified: WITH targetMessage(isSelfMessage, isMentioningSelfUser, isQuotingSelfUser, mutedStatus) AS ( SELECT isSelfMessage, @@ -427,6 +437,10 @@ insertConversationReceiptModeChanged: INSERT OR IGNORE INTO MessageConversationReceiptModeChangedContent(message_id, conversation_id, receipt_mode) VALUES(?, ?, ?); +insertConversationAppsEnabledChanged: +INSERT OR IGNORE INTO MessageConversationAppsEnabledChangedContent(message_id, conversation_id, is_apps_enabled) +VALUES(?, ?, ?); + insertConversationMessageTimerChanged: INSERT OR IGNORE INTO MessageConversationTimerChangedContent(message_id, conversation_id, message_timer) VALUES(?, ?, ?); diff --git a/persistence/src/commonMain/db_user/migrations/117.sqm b/persistence/src/commonMain/db_user/migrations/117.sqm new file mode 100644 index 00000000000..f03b9cb5a13 --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/117.sqm @@ -0,0 +1,201 @@ +CREATE TABLE MessageConversationAppsEnabledChangedContent ( + message_id TEXT NOT NULL, + conversation_id TEXT AS QualifiedIDEntity NOT NULL, + + is_apps_enabled INTEGER AS Boolean NOT NULL DEFAULT(0), + + FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE ON UPDATE CASCADE, + PRIMARY KEY (message_id, conversation_id) +); + +DROP VIEW IF EXISTS MessageDetailsView; + +CREATE VIEW IF NOT EXISTS MessageDetailsView +AS SELECT +Message.id AS id, +Message.conversation_id AS conversationId, +Message.content_type AS contentType, +Message.creation_date AS date, +Message.sender_user_id AS senderUserId, +Message.sender_client_id AS senderClientId, +Message.status AS status, +Message.last_edit_date AS lastEditTimestamp, +Message.visibility AS visibility, +Message.expects_read_confirmation AS expectsReadConfirmation, +Message.expire_after_millis AS expireAfterMillis, +Message.self_deletion_end_date AS selfDeletionEndDate, +IFNULL ((SELECT COUNT (*) FROM Receipt WHERE message_id = Message.id AND type = 'READ'), 0) AS readCount, +UserDetails.name AS senderName, +UserDetails.handle AS senderHandle, +UserDetails.email AS senderEmail, +UserDetails.phone AS senderPhone, +UserDetails.accent_id AS senderAccentId, +UserDetails.team AS senderTeamId, +UserDetails.connection_status AS senderConnectionStatus, +UserDetails.preview_asset_id AS senderPreviewAssetId, +UserDetails.complete_asset_id AS senderCompleteAssetId, +UserDetails.user_availability_status AS senderAvailabilityStatus, +UserDetails.user_type AS senderUserType, +UserDetails.bot_service AS senderBotService, +UserDetails.deleted AS senderIsDeleted, +UserDetails.expires_at AS senderExpiresAt, +UserDetails.defederated AS senderDefederated, +UserDetails.supported_protocols AS senderSupportedProtocols, +UserDetails.active_one_on_one_conversation_id AS senderActiveOneOnOneConversationId, +UserDetails.is_proteus_verified AS senderIsProteusVerified, +UserDetails.is_under_legal_hold AS senderIsUnderLegalHold, +(Message.sender_user_id == SelfUser.id) AS isSelfMessage, +TextContent.text_body AS text, +TextContent.is_quoting_self AS isQuotingSelfUser, +AssetContent.asset_size AS assetSize, +AssetContent.asset_name AS assetName, +AssetContent.asset_mime_type AS assetMimeType, +AssetContent.asset_otr_key AS assetOtrKey, +AssetContent.asset_sha256 AS assetSha256, +AssetContent.asset_id AS assetId, +AssetContent.asset_token AS assetToken, +AssetContent.asset_domain AS assetDomain, +AssetContent.asset_encryption_algorithm AS assetEncryptionAlgorithm, +AssetContent.asset_width AS assetWidth, +AssetContent.asset_height AS assetHeight, +AssetContent.asset_duration_ms AS assetDuration, +AssetContent.asset_normalized_loudness AS assetNormalizedLoudness, +AssetData.data_path AS assetDataPath, +MissedCallContent.caller_id AS callerId, +MemberChangeContent.member_change_list AS memberChangeList, +MemberChangeContent.member_change_type AS memberChangeType, +UnknownContent.unknown_type_name AS unknownContentTypeName, +UnknownContent.unknown_encoded_data AS unknownContentData, +RestrictedAssetContent.asset_mime_type AS restrictedAssetMimeType, +RestrictedAssetContent.asset_size AS restrictedAssetSize, +RestrictedAssetContent.asset_name AS restrictedAssetName, +FailedToDecryptContent.unknown_encoded_data AS failedToDecryptData, +FailedToDecryptContent.error_code AS decryptionErrorCode, +FailedToDecryptContent.is_decryption_resolved AS isDecryptionResolved, +ConversationNameChangedContent.conversation_name AS conversationName, +'{' || IFNULL( + (SELECT GROUP_CONCAT('"' || emoji || '":' || count) + FROM ( + SELECT COUNT(*) count, Reaction.emoji emoji + FROM Reaction + WHERE Reaction.message_id = Message.id + AND Reaction.conversation_id = Message.conversation_id + GROUP BY Reaction.emoji + )), + '') +|| '}' AS allReactionsJson, +IFNULL( + (SELECT '[' || GROUP_CONCAT('"' || Reaction.emoji || '"') || ']' + FROM Reaction + WHERE Reaction.message_id = Message.id + AND Reaction.conversation_id = Message.conversation_id + AND Reaction.sender_id = SelfUser.id + ), + '[]' +) AS selfReactionsJson, +IFNULL( + (SELECT '[' || GROUP_CONCAT( + '{"start":' || start || ', "length":' || length || + ', "userId":{"value":"' || replace(substr(user_id, 0, instr(user_id, '@')), '@', '') || '"' || + ',"domain":"' || replace(substr(user_id, instr(user_id, '@')+1, length(user_id)), '@', '') || '"' || + '}' || '}') || ']' + FROM MessageMention + WHERE MessageMention.message_id = Message.id + AND MessageMention.conversation_id = Message.conversation_id + ), + '[]' +) AS mentions, +IFNULL( + (SELECT '[' || GROUP_CONCAT('{ + "id":"' || asset_id || '", + "asset_index":"' || asset_index || '", + "mime_type":"' || asset_mime_type || '", + "cell_asset":"' || cell_asset || '", + "asset_path":"' || IFNULL(asset_path,'') || '", + "asset_size":' || IFNULL(asset_size,0) || ', + "local_path":"' || IFNULL(local_path,'') || '", + "asset_width":"' || IFNULL(asset_width,0) || '", + "asset_height":"' || IFNULL(asset_height,0) || '", + "asset_transfer_status":"' || asset_transfer_status || '", + "asset_duration_ms":"' || IFNULL(asset_duration_ms,0) || '", + "content_hash":"' || IFNULL(content_hash,'') || '", + "content_url":"' || IFNULL(content_url,'') || '", + "preview_url":"' || IFNULL(preview_url,'') || '"}') || ']' + FROM MessageAttachments + WHERE (MessageAttachments.message_id = Message.id + AND MessageAttachments.conversation_id = Message.conversation_id) + ), + '[]' +) AS attachments, +TextContent.quoted_message_id AS quotedMessageId, +QuotedMessage.sender_user_id AS quotedSenderId, +TextContent.is_quote_verified AS isQuoteVerified, +QuotedSender.name AS quotedSenderName, +QuotedSender.accent_id AS quotedSenderAccentId, +QuotedMessage.creation_date AS quotedMessageDateTime, +QuotedMessage.last_edit_date AS quotedMessageEditTimestamp, +QuotedMessage.visibility AS quotedMessageVisibility, +QuotedMessage.content_type AS quotedMessageContentType, +QuotedTextContent.text_body AS quotedTextBody, +QuotedAssetContent.asset_mime_type AS quotedAssetMimeType, +QuotedAssetContent.asset_name AS quotedAssetName, +QuotedLocationContent.name AS quotedLocationName, + +ConversationAppsEnabledChanged.is_apps_enabled AS isConversationAppsEnabled, +NewConversationReceiptMode.receipt_mode AS newConversationReceiptMode, + +ConversationReceiptModeChanged.receipt_mode AS conversationReceiptModeChanged, +ConversationTimerChangedContent.message_timer AS messageTimerChanged, +FailedRecipientsWithNoClients.recipient_failure_list AS recipientsFailedWithNoClientsList, +FailedRecipientsDeliveryFailed.recipient_failure_list AS recipientsFailedDeliveryList, + +IFNULL( + (SELECT '[' || + GROUP_CONCAT('{"text":"' || text || '", "id":"' || id || '""is_selected":' || is_selected || '}') + || ']' + FROM ButtonContent + WHERE ButtonContent.message_id = Message.id + AND ButtonContent.conversation_id = Message.conversation_id + ), + '[]' +) AS buttonsJson, +FederationTerminatedContent.domain_list AS federationDomainList, +FederationTerminatedContent.federation_type AS federationType, +ConversationProtocolChangedContent.protocol AS conversationProtocolChanged, +ConversationLocationContent.latitude AS latitude, +ConversationLocationContent.longitude AS longitude, +ConversationLocationContent.name AS locationName, +ConversationLocationContent.zoom AS locationZoom, +LegalHoldContent.legal_hold_member_list AS legalHoldMemberList, +LegalHoldContent.legal_hold_type AS legalHoldType + +FROM Message +JOIN UserDetails ON Message.sender_user_id = UserDetails.qualified_id +LEFT JOIN MessageTextContent AS TextContent ON Message.id = TextContent.message_id AND Message.conversation_id = TextContent.conversation_id +LEFT JOIN MessageAssetContent AS AssetContent ON Message.id = AssetContent.message_id AND Message.conversation_id = AssetContent.conversation_id +LEFT JOIN Asset AS AssetData ON AssetContent.asset_id = AssetData.key +LEFT JOIN MessageMissedCallContent AS MissedCallContent ON Message.id = MissedCallContent.message_id AND Message.conversation_id = MissedCallContent.conversation_id +LEFT JOIN MessageMemberChangeContent AS MemberChangeContent ON Message.id = MemberChangeContent.message_id AND Message.conversation_id = MemberChangeContent.conversation_id +LEFT JOIN MessageUnknownContent AS UnknownContent ON Message.id = UnknownContent.message_id AND Message.conversation_id = UnknownContent.conversation_id +LEFT JOIN MessageRestrictedAssetContent AS RestrictedAssetContent ON Message.id = RestrictedAssetContent.message_id AND RestrictedAssetContent.conversation_id = RestrictedAssetContent.conversation_id +LEFT JOIN MessageFailedToDecryptContent AS FailedToDecryptContent ON Message.id = FailedToDecryptContent.message_id AND Message.conversation_id = FailedToDecryptContent.conversation_id +LEFT JOIN MessageConversationChangedContent AS ConversationNameChangedContent ON Message.id = ConversationNameChangedContent.message_id AND Message.conversation_id = ConversationNameChangedContent.conversation_id +LEFT JOIN MessageRecipientFailure AS FailedRecipientsWithNoClients ON Message.id = FailedRecipientsWithNoClients.message_id AND Message.conversation_id = FailedRecipientsWithNoClients.conversation_id AND FailedRecipientsWithNoClients.recipient_failure_type = 'NO_CLIENTS_TO_DELIVER' +LEFT JOIN MessageRecipientFailure AS FailedRecipientsDeliveryFailed ON Message.id = FailedRecipientsDeliveryFailed.message_id AND Message.conversation_id = FailedRecipientsDeliveryFailed.conversation_id AND FailedRecipientsDeliveryFailed.recipient_failure_type = 'MESSAGE_DELIVERY_FAILED' + +-- joins for quoted messages +LEFT JOIN Message AS QuotedMessage ON QuotedMessage.id = TextContent.quoted_message_id AND QuotedMessage.conversation_id = TextContent.conversation_id +LEFT JOIN User AS QuotedSender ON QuotedMessage.sender_user_id = QuotedSender.qualified_id +LEFT JOIN MessageTextContent AS QuotedTextContent ON QuotedTextContent.message_id = QuotedMessage.id AND QuotedMessage.conversation_id = TextContent.conversation_id +LEFT JOIN MessageAssetContent AS QuotedAssetContent ON QuotedAssetContent.message_id = QuotedMessage.id AND QuotedMessage.conversation_id = TextContent.conversation_id +LEFT JOIN MessageConversationLocationContent AS QuotedLocationContent ON QuotedLocationContent.message_id = QuotedMessage.id AND QuotedMessage.conversation_id = TextContent.conversation_id +-- end joins for quoted messages +LEFT JOIN MessageConversationAppsEnabledChangedContent AS ConversationAppsEnabledChanged ON Message.id = ConversationAppsEnabledChanged.message_id AND Message.conversation_id = ConversationAppsEnabledChanged.conversation_id +LEFT JOIN MessageNewConversationReceiptModeContent AS NewConversationReceiptMode ON Message.id = NewConversationReceiptMode.message_id AND Message.conversation_id = NewConversationReceiptMode.conversation_id +LEFT JOIN MessageConversationReceiptModeChangedContent AS ConversationReceiptModeChanged ON Message.id = ConversationReceiptModeChanged.message_id AND Message.conversation_id = ConversationReceiptModeChanged.conversation_id +LEFT JOIN MessageConversationTimerChangedContent AS ConversationTimerChangedContent ON Message.id = ConversationTimerChangedContent.message_id AND Message.conversation_id = ConversationTimerChangedContent.conversation_id +LEFT JOIN MessageFederationTerminatedContent AS FederationTerminatedContent ON Message.id = FederationTerminatedContent.message_id AND Message.conversation_id = FederationTerminatedContent.conversation_id +LEFT JOIN MessageConversationProtocolChangedContent AS ConversationProtocolChangedContent ON Message.id = ConversationProtocolChangedContent.message_id AND Message.conversation_id = ConversationProtocolChangedContent.conversation_id +LEFT JOIN MessageConversationLocationContent AS ConversationLocationContent ON Message.id = ConversationLocationContent.message_id AND Message.conversation_id = ConversationLocationContent.conversation_id +LEFT JOIN MessageLegalHoldContent AS LegalHoldContent ON Message.id = LegalHoldContent.message_id AND Message.conversation_id = LegalHoldContent.conversation_id +LEFT JOIN SelfUser; diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt index e3352a2e223..c4c12f7318d 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt @@ -226,6 +226,9 @@ internal class MessageDAOImpl internal constructor( is MessageEntityContent.ConversationStartedUnverifiedWarning -> true + is MessageEntityContent.ConversationAppsAccessChanged -> + it.isConversationAppsEnabled == messageContent.isEnabled + else -> false } }?.let { @@ -572,10 +575,10 @@ internal class MessageDAOImpl internal constructor( } override suspend fun observeAssetStatuses(): Flow> = - assetStatusQueries.selectAllAssetTransferStatuses() - .asFlow() - .mapToList() - .flowOn(readDispatcher.value) + assetStatusQueries.selectAllAssetTransferStatuses() + .asFlow() + .mapToList() + .flowOn(readDispatcher.value) override suspend fun getAllMessageAssetIdsForConversationId( conversationId: QualifiedIDEntity diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageEntity.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageEntity.kt index fa259b8d72f..61872e0b8c0 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageEntity.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageEntity.kt @@ -148,7 +148,7 @@ sealed interface MessageEntity { CONVERSATION_DEGRADED_PROTEUS, CONVERSATION_VERIFIED_MLS, CONVERSATION_VERIFIED_PROTEUS, COMPOSITE, FEDERATION, CONVERSATION_PROTOCOL_CHANGED, CONVERSATION_PROTOCOL_CHANGED_DURING_CALL, CONVERSATION_STARTED_UNVERIFIED_WARNING, LOCATION, LEGAL_HOLD, MULTIPART, - CONVERSATION_WITH_CELL, CONVERSATION_WITH_CELL_SELF_DELETE_DISABLED, + CONVERSATION_WITH_CELL, CONVERSATION_WITH_CELL_SELF_DELETE_DISABLED, CONVERSATION_APPS_ENABLED_CHANGED } enum class MemberChangeType { @@ -299,7 +299,7 @@ sealed class MessageEntityContent { // Local path val assetDataPath: String? = null, - ) : Regular() + ) : Regular() data class Knock(val hotKnock: Boolean) : Regular() data class Location( @@ -350,6 +350,7 @@ sealed class MessageEntityContent { data class ConversationReceiptModeChanged(val receiptMode: Boolean) : System() data class ConversationMessageTimerChanged(val messageTimer: Long?) : System() data class ConversationProtocolChanged(val protocol: ConversationEntity.Protocol) : System() + data class ConversationAppsAccessChanged(val isEnabled: Boolean) : System() data object ConversationProtocolChangedDuringACall : System() data object HistoryLostProtocolChanged : System() data object HistoryLost : System() diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt index e4f43594342..bb90ad59cce 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt @@ -119,8 +119,8 @@ internal class MessageInsertExtensionImpl( sender_client_id = if (message is MessageEntity.Regular) message.senderClientId else null, visibility = message.visibility, last_edit_date = - if (message is MessageEntity.Regular && message.editStatus is MessageEntity.EditStatus.Edited) message.editStatus.lastDate - else null, + if (message is MessageEntity.Regular && message.editStatus is MessageEntity.EditStatus.Edited) message.editStatus.lastDate + else null, status = message.status, content_type = contentTypeOf(message.content), expects_read_confirmation = if (message is MessageEntity.Regular) message.expectsReadConfirmation else false, @@ -370,12 +370,20 @@ internal class MessageInsertExtensionImpl( ) } } + is MessageEntityContent.NewConversationWithCellMessage -> { /* no-op */ } + is MessageEntityContent.NewConversationWithCellSelfDeleteDisabledMessage -> { /* no-op */ } + + is MessageEntityContent.ConversationAppsAccessChanged -> messagesQueries.insertConversationAppsEnabledChanged( + message_id = message.id, + conversation_id = message.conversationId, + is_apps_enabled = content.isEnabled + ) } } @@ -439,6 +447,7 @@ internal class MessageInsertExtensionImpl( is MessageEntityContent.TeamMemberRemoved, is MessageEntityContent.LegalHold, is MessageEntityContent.NewConversationWithCellMessage, + is MessageEntityContent.ConversationAppsAccessChanged, is MessageEntityContent.NewConversationWithCellSelfDeleteDisabledMessage, -> { /* no-op */ @@ -544,5 +553,7 @@ internal class MessageInsertExtensionImpl( is MessageEntityContent.NewConversationWithCellMessage -> MessageEntity.ContentType.CONVERSATION_WITH_CELL is MessageEntityContent.NewConversationWithCellSelfDeleteDisabledMessage -> MessageEntity.ContentType.CONVERSATION_WITH_CELL_SELF_DELETE_DISABLED + + is MessageEntityContent.ConversationAppsAccessChanged -> MessageEntity.ContentType.CONVERSATION_APPS_ENABLED_CHANGED } } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMapper.kt index 3894a6437d8..798cfcc1438 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMapper.kt @@ -255,6 +255,7 @@ object MessageMapper { MessageEntity.ContentType.CONVERSATION_WITH_CELL -> MessagePreviewEntityContent.Unknown MessageEntity.ContentType.CONVERSATION_WITH_CELL_SELF_DELETE_DISABLED -> MessagePreviewEntityContent.Unknown + MessageEntity.ContentType.CONVERSATION_APPS_ENABLED_CHANGED -> MessagePreviewEntityContent.Unknown } } @@ -534,6 +535,7 @@ object MessageMapper { quotedAssetMimeType: String?, quotedAssetName: String?, quotedLocationName: String?, + isConversationAppsEnabled: Boolean?, newConversationReceiptMode: Boolean?, conversationReceiptModeChanged: Boolean?, messageTimerChanged: Long?, @@ -665,6 +667,10 @@ object MessageMapper { receiptMode = conversationReceiptModeChanged ?: false ) + MessageEntity.ContentType.CONVERSATION_APPS_ENABLED_CHANGED -> MessageEntityContent.ConversationAppsAccessChanged( + isEnabled = isConversationAppsEnabled ?: false + ) + MessageEntity.ContentType.HISTORY_LOST -> MessageEntityContent.HistoryLost MessageEntity.ContentType.HISTORY_LOST_PROTOCOL_CHANGED -> MessageEntityContent.HistoryLostProtocolChanged MessageEntity.ContentType.CONVERSATION_MESSAGE_TIMER_CHANGED -> MessageEntityContent.ConversationMessageTimerChanged( diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt index f478ec4898b..a53a5a576e0 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt @@ -28,14 +28,15 @@ import com.wire.kalium.persistence.Conversation import com.wire.kalium.persistence.ConversationFolder import com.wire.kalium.persistence.ConversationLegalHoldStatusChangeNotified import com.wire.kalium.persistence.HistoryClient -import com.wire.kalium.persistence.LastMessage import com.wire.kalium.persistence.LabeledConversation +import com.wire.kalium.persistence.LastMessage import com.wire.kalium.persistence.Member import com.wire.kalium.persistence.Message import com.wire.kalium.persistence.MessageAssetContent import com.wire.kalium.persistence.MessageAssetTransferStatus import com.wire.kalium.persistence.MessageAttachmentDraft import com.wire.kalium.persistence.MessageAttachments +import com.wire.kalium.persistence.MessageConversationAppsEnabledChangedContent import com.wire.kalium.persistence.MessageConversationChangedContent import com.wire.kalium.persistence.MessageConversationLocationContent import com.wire.kalium.persistence.MessageConversationProtocolChangedContent @@ -304,4 +305,8 @@ internal object TableMapper { conversation_idAdapter = QualifiedIDAdapter, creation_dateAdapter = InstantTypeAdapter, ) + + val conversationAppsAccessChangedAdapter = MessageConversationAppsEnabledChangedContent.Adapter( + conversation_idAdapter = QualifiedIDAdapter, + ) } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt index f97a462e910..94e26615770 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt @@ -187,6 +187,7 @@ class UserDatabaseBuilder internal constructor( MessageAttachmentDraftAdapter = TableMapper.messageAttachmentDraftAdapter, MessageAttachmentsAdapter = TableMapper.messageAttachmentsAdapter, HistoryClientAdapter = TableMapper.historyClientAdapter, + MessageConversationAppsEnabledChangedContentAdapter = TableMapper.conversationAppsAccessChangedAdapter ) init { diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageMapperTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageMapperTest.kt index c8e105eb916..54818350650 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageMapperTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageMapperTest.kt @@ -180,6 +180,7 @@ class MessageMapperTest { quotedAssetMimeType: String? = null, quotedAssetName: String? = null, quotedLocationName: String? = null, + isConversationAppsEnabled: Boolean? = null, newConversationReceiptMode: Boolean? = null, conversationReceiptModeChanged: Boolean? = null, messageTimerChanged: Long? = null, @@ -276,6 +277,7 @@ class MessageMapperTest { quotedAssetMimeType, quotedAssetName, quotedLocationName, + isConversationAppsEnabled, newConversationReceiptMode, conversationReceiptModeChanged, messageTimerChanged, diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageMapperUserTypeMappingTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageMapperUserTypeMappingTest.kt index 8495e98a407..25d5d9a09ce 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageMapperUserTypeMappingTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageMapperUserTypeMappingTest.kt @@ -322,6 +322,7 @@ class MessageMapperUserTypeMappingTest { quotedAssetMimeType: String? = null, quotedAssetName: String? = null, quotedLocationName: String? = null, + isConversationAppsEnabled: Boolean? = null, newConversationReceiptMode: Boolean? = null, conversationReceiptModeChanged: Boolean? = null, messageTimerChanged: Long? = null, @@ -417,6 +418,7 @@ class MessageMapperUserTypeMappingTest { quotedAssetMimeType, quotedAssetName, quotedLocationName, + isConversationAppsEnabled, newConversationReceiptMode, conversationReceiptModeChanged, messageTimerChanged,