diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index 39c404a16ed..c4c08b9fa71 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -9,6 +9,6 @@ jobs: name: Label PR based on title runs-on: ubuntu-latest steps: - - uses: srvaroa/labeler@v1.8.2 + - uses: srvaroa/labeler@v1.13.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallClient.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallClient.kt index 7545545157e..0c2f3bb9b79 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallClient.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallClient.kt @@ -18,21 +18,55 @@ package com.wire.kalium.logic.data.call +import com.wire.kalium.util.serialization.LenientJsonSerializer +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.nullable +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder @Serializable data class CallClient( @SerialName("userid") val userId: String, @SerialName("clientid") val clientId: String, - @SerialName("in_subconv") val isMemberOfSubconversation: Boolean = false + @SerialName("in_subconv") val isMemberOfSubconversation: Boolean = false, + @SerialName("quality") + @Serializable(with = CallQuality.CallQualityAsIntSerializer::class) + val quality: CallQuality = CallQuality.LOW ) @Serializable data class CallClientList( @SerialName("clients") val clients: List ) { - // TODO(optimization): Use a shared Json instance instead of creating one every time. - fun toJsonString(): String = Json { isLenient = true }.encodeToString(serializer(), this) + fun toJsonString(): String = LenientJsonSerializer.json.encodeToString(serializer(), this) +} + +enum class CallQuality { + ANY, + LOW, + HIGH; + + data object CallQualityAsIntSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("quality", PrimitiveKind.INT).nullable + + override fun serialize(encoder: Encoder, value: CallQuality) { + encoder.encodeInt(value.ordinal) + } + + @OptIn(ExperimentalSerializationApi::class) + override fun deserialize(decoder: Decoder): CallQuality { + val value = if (decoder.decodeNotNullMark()) decoder.decodeInt() else 0 + return when (value) { + 1 -> LOW + 2 -> HIGH + else -> ANY + } + } + } } diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/InCallReactionMessage.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/InCallReactionMessage.kt new file mode 100644 index 00000000000..d730284dc61 --- /dev/null +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/InCallReactionMessage.kt @@ -0,0 +1,27 @@ +/* + * Wire + * Copyright (C) 2024 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.call + +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedID + +data class InCallReactionMessage( + val conversationId: ConversationId, + val senderUserId: QualifiedID, + val emojis: Set, +) diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/RecentlyEndedCallMetadata.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/RecentlyEndedCallMetadata.kt new file mode 100644 index 00000000000..2d6468356f7 --- /dev/null +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/RecentlyEndedCallMetadata.kt @@ -0,0 +1,46 @@ +/* + * Wire + * Copyright (C) 2024 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.call + +import com.wire.kalium.logic.data.conversation.Conversation + +data class RecentlyEndedCallMetadata( + val callEndReason: Int, + val callDetails: CallDetails, + val conversationDetails: ConversationDetails, + val isTeamMember: Boolean +) { + data class CallDetails( + val isCallScreenShare: Boolean, + val screenShareDurationInSeconds: Long, + val callScreenShareUniques: Int, + val isOutgoingCall: Boolean, + val callDurationInSeconds: Long, + val callParticipantsCount: Int, + val conversationServices: Int, + val callAVSwitchToggle: Boolean, + val callVideoEnabled: Boolean + ) + + data class ConversationDetails( + val conversationType: Conversation.Type, + val conversationSize: Int, + val conversationGuests: Int, + val conversationGuestsPro: Int + ) +} diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFilter.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFilter.kt index f1ca2a7bf39..a76fac75ca0 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFilter.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFilter.kt @@ -17,9 +17,22 @@ */ package com.wire.kalium.logic.data.conversation -enum class ConversationFilter { - ALL, - FAVORITES, - GROUPS, - ONE_ON_ONE +import kotlinx.serialization.Serializable + +@Serializable +sealed class ConversationFilter { + @Serializable + data object All : ConversationFilter() + + @Serializable + data object Favorites : ConversationFilter() + + @Serializable + data object Groups : ConversationFilter() + + @Serializable + data object OneOnOne : ConversationFilter() + + @Serializable + data class Folder(val folderName: String, val folderId: String) : ConversationFilter() } diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFolder.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFolder.kt index 675e9f5794f..cf64352dd3d 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFolder.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFolder.kt @@ -18,11 +18,14 @@ package com.wire.kalium.logic.data.conversation import com.wire.kalium.logic.data.id.QualifiedID +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +@Serializable data class ConversationFolder( - val id: String, - val name: String, - val type: FolderType + @SerialName("id") val id: String, + @SerialName("name") val name: String, + @SerialName("folder_type") val type: FolderType ) data class FolderWithConversations( 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 5ea75254462..5f9ae5f1577 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 @@ -248,6 +248,11 @@ sealed interface Message { typeKey to "dataTransfer", "content" to content.toLogMap(), ) + + is MessageContent.InCallEmoji -> mutableMapOf( + typeKey to "inCallEmoji", + "content" to content.emojis + ) } 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 56103c2371f..e2a6eaf711a 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 @@ -259,7 +259,8 @@ sealed interface MessageContent { data class Cleared( val conversationId: ConversationId, - val time: Instant + val time: Instant, + val needToRemoveLocally: Boolean ) : Signaling // server message content types @@ -394,6 +395,10 @@ sealed interface MessageContent { data object Disabled : ForConversation() } } + + data class InCallEmoji( + val emojis: Map + ) : Signaling } /** @@ -454,6 +459,7 @@ fun MessageContent?.getType() = when (this) { is MessageContent.LegalHold.ForMembers.Disabled -> "LegalHold.ForMembers.Disabled" is MessageContent.LegalHold.ForMembers.Enabled -> "LegalHold.ForMembers.Enabled" is MessageContent.DataTransfer -> "DataTransfer" + is MessageContent.InCallEmoji -> "InCallEmoji" null -> "null" } diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContentLogging.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContentLogging.kt index 7cbfe0fbd57..d576fa6ddc5 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContentLogging.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContentLogging.kt @@ -41,4 +41,5 @@ inline fun MessageContent.FromProto.typeDescription(): String = when (this) { is MessageContent.Receipt -> "Receipt" is MessageContent.TextEdited -> "TextEdited" is MessageContent.DataTransfer -> "DataTransfer" + is MessageContent.InCallEmoji -> "InCallEmoji" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9060b93e117..1ff9d449dba 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,9 +5,9 @@ activity-compose = "1.9.0" app-compat = "1.6.1" android-paging3 = "3.2.1" cli-kt = "3.5.0" -coroutines = "1.8.0" +coroutines = "1.8.1" compose-compiler = "1.5.13" -compose-ui = "1.6.6" +compose-ui = "1.7.6" compose-material = "1.6.6" cryptobox4j = "1.4.0" cryptobox-android = "1.1.5" @@ -17,14 +17,14 @@ okio = "3.9.0" ok-http = "4.12.0" mockative = "2.2.0" android-work = "2.9.0" -android-test-runner = "1.5.2" -android-test-core-ktx = "1.5.0" -android-test-rules = "1.5.0" -android-test-core = "1.5.0" +android-test-runner = "1.6.2" +android-test-core-ktx = "1.6.1" +android-test-rules = "1.6.1" +android-test-core = "1.6.1" androidx-arch = "2.2.0" -androidx-test-orchestrator = "1.4.2" +androidx-test-orchestrator = "1.5.1" androidx-sqlite = "2.4.0" -benasher-uuid = "0.8.0" +benasher-uuid = "0.8.4" ktx-datetime = { strictly = "0.5.0" } ktx-serialization = "1.6.3" ktx-atomicfu = "0.24.0" @@ -37,20 +37,20 @@ sqldelight = "2.0.1" sqlcipher-android = "4.5.6" pbandk = "0.14.2" turbine = "1.1.0" -avs = "9.10.16" +avs = "10.0.1" jna = "5.14.0" -core-crypto = "2.0.0" +core-crypto = "3.0.0" core-crypto-multiplatform = "0.6.0-rc.3-multiplatform-pre1" completeKotlin = "1.1.0" -desugar-jdk = "2.0.4" +desugar-jdk = "2.1.3" kermit = "2.0.3" -detekt = "1.23.6" +detekt = "1.23.7" agp = "8.5.2" dokka = "1.8.20" carthage = "0.0.1" libsodiumBindings = "0.8.7" protobufCodegen = "0.9.4" -annotation = "1.7.1" +annotation = "1.9.1" mordant = "2.0.0-beta13" apache-tika = "2.9.2" mockk = "1.13.10" diff --git a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt index 2e89948aefd..7435be07478 100644 --- a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt +++ b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt @@ -36,6 +36,7 @@ import com.wire.kalium.logic.data.message.MessageContent import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater +import com.wire.kalium.logic.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs @@ -103,6 +104,9 @@ class CallManagerTest { @Mock private val getCallConversationType = mock(GetCallConversationTypeProvider::class) + @Mock + private val createAndPersistRecentlyEndedCallMetadata = mock(CreateAndPersistRecentlyEndedCallMetadataUseCase::class) + private val dispatcher = TestKaliumDispatcher private lateinit var callManagerImpl: CallManagerImpl @@ -132,7 +136,8 @@ class CallManagerTest { networkStateObserver = networkStateObserver, kaliumConfigs = kaliumConfigs, mediaManagerService = mediaManagerService, - flowManagerService = flowManagerService + flowManagerService = flowManagerService, + createAndPersistRecentlyEndedCallMetadata = createAndPersistRecentlyEndedCallMetadata ) } diff --git a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt index 1382845bf18..648c3d755ee 100644 --- a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt +++ b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt @@ -34,6 +34,7 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider +import com.wire.kalium.logic.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs import com.wire.kalium.network.NetworkStateObserver @@ -56,7 +57,8 @@ actual class GlobalCallManager { conversationClientsInCallUpdater: ConversationClientsInCallUpdater, getCallConversationType: GetCallConversationTypeProvider, networkStateObserver: NetworkStateObserver, - kaliumConfigs: KaliumConfigs + kaliumConfigs: KaliumConfigs, + createAndPersistRecentlyEndedCallMetadata: CreateAndPersistRecentlyEndedCallMetadataUseCase ): CallManager { return CallManagerImpl() } diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt index 693c76d98f9..0285d2491e4 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt @@ -75,6 +75,7 @@ import com.wire.kalium.logic.feature.call.scenario.OnSFTRequest import com.wire.kalium.logic.feature.call.scenario.OnSendOTR import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider +import com.wire.kalium.logic.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs import com.wire.kalium.logic.functional.fold @@ -119,6 +120,7 @@ class CallManagerImpl internal constructor( private val kaliumConfigs: KaliumConfigs, private val mediaManagerService: MediaManagerService, private val flowManagerService: FlowManagerService, + private val createAndPersistRecentlyEndedCallMetadata: CreateAndPersistRecentlyEndedCallMetadataUseCase, private val json: Json = Json { ignoreUnknownKeys = true }, private val shouldRemoteMuteChecker: ShouldRemoteMuteChecker = ShouldRemoteMuteCheckerImpl(), private val serverTimeHandler: ServerTimeHandler = ServerTimeHandlerImpl(), @@ -219,7 +221,8 @@ class CallManagerImpl internal constructor( callRepository = callRepository, networkStateObserver = networkStateObserver, scope = scope, - qualifiedIdMapper = qualifiedIdMapper + qualifiedIdMapper = qualifiedIdMapper, + createAndPersistRecentlyEndedCallMetadata = createAndPersistRecentlyEndedCallMetadata ).keepingStrongReference(), metricsHandler = metricsHandler, callConfigRequestHandler = OnConfigRequest(calling, callRepository, scope) @@ -463,14 +466,19 @@ class CallManagerImpl internal constructor( callClients: CallClientList ) { withCalling { - // Needed to support calls between federated and non federated environments + // Mapping Needed to support calls between federated and non federated environments (domain separation) val clients = callClients.clients.map { callClient -> CallClient( - federatedIdMapper.parseToFederatedId(callClient.userId), - callClient.clientId + userId = federatedIdMapper.parseToFederatedId(callClient.userId), + clientId = callClient.clientId, + isMemberOfSubconversation = callClient.isMemberOfSubconversation, + quality = callClient.quality ) } val clientsJson = CallClientList(clients).toJsonString() + callingLogger.d( + "$TAG - wcall_request_video_streams() called -> Requesting video streams for conversation = ${conversationId.toLogString()}" + ) val conversationIdString = federatedIdMapper.parseToFederatedId(conversationId) calling.wcall_request_video_streams( inst = it, diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt index 277eadc1662..1471ec8dd5e 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt @@ -39,6 +39,7 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider +import com.wire.kalium.logic.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs import com.wire.kalium.logic.util.CurrentPlatform @@ -94,7 +95,8 @@ actual class GlobalCallManager( conversationClientsInCallUpdater: ConversationClientsInCallUpdater, getCallConversationType: GetCallConversationTypeProvider, networkStateObserver: NetworkStateObserver, - kaliumConfigs: KaliumConfigs + kaliumConfigs: KaliumConfigs, + createAndPersistRecentlyEndedCallMetadata: CreateAndPersistRecentlyEndedCallMetadataUseCase ): CallManager { if (kaliumConfigs.enableCalling) { return callManagerHolder.computeIfAbsent(userId) { @@ -116,7 +118,8 @@ actual class GlobalCallManager( mediaManagerService = mediaManager, flowManagerService = flowManager, userConfigRepository = userConfigRepository, - kaliumConfigs = kaliumConfigs + kaliumConfigs = kaliumConfigs, + createAndPersistRecentlyEndedCallMetadata = createAndPersistRecentlyEndedCallMetadata ) } } else { diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCall.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCall.kt index 26d8c0b8194..cafbc99d38b 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCall.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCall.kt @@ -31,6 +31,7 @@ import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedIdMapper +import com.wire.kalium.logic.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase import com.wire.kalium.network.NetworkState import com.wire.kalium.network.NetworkStateObserver import kotlinx.coroutines.CoroutineScope @@ -41,7 +42,8 @@ class OnCloseCall( private val callRepository: CallRepository, private val scope: CoroutineScope, private val qualifiedIdMapper: QualifiedIdMapper, - private val networkStateObserver: NetworkStateObserver + private val networkStateObserver: NetworkStateObserver, + private val createAndPersistRecentlyEndedCallMetadata: CreateAndPersistRecentlyEndedCallMetadataUseCase ) : CloseCallHandler { override fun onClosedCall( reason: Int, @@ -62,6 +64,7 @@ class OnCloseCall( val conversationIdWithDomain = qualifiedIdMapper.fromStringToQualifiedID(conversationId) scope.launch { + val callMetadata = callRepository.getCallMetadataProfile()[conversationIdWithDomain] val isConnectedToInternet = networkStateObserver.observeNetworkState().value == NetworkState.ConnectedWithInternet @@ -78,11 +81,15 @@ class OnCloseCall( status = callStatus ) - if (callRepository.getCallMetadataProfile()[conversationIdWithDomain]?.protocol is Conversation.ProtocolInfo.MLS) { + if (callMetadata?.protocol is Conversation.ProtocolInfo.MLS) { callRepository.leaveMlsConference(conversationIdWithDomain) } callingLogger.i("[OnCloseCall] -> ConversationId: ${conversationId.obfuscateId()} | callStatus: $callStatus") } + + scope.launch { + createAndPersistRecentlyEndedCallMetadata(conversationIdWithDomain, reason) + } } private fun shouldPersistMissedCall( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/CoreFailure.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/CoreFailure.kt index 24e28261290..57a23005b8b 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/CoreFailure.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/CoreFailure.kt @@ -211,6 +211,7 @@ interface MLSFailure : CoreFailure { data object StaleProposal : MLSFailure data object StaleCommit : MLSFailure data object InternalErrors : MLSFailure + data object Disabled : MLSFailure data class Generic(internal val exception: Exception) : MLSFailure { val rootCause: Throwable get() = exception diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt index f45ce704881..4cc7ea71551 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt @@ -75,6 +75,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine @@ -136,6 +137,9 @@ interface CallRepository { suspend fun advanceEpoch(conversationId: ConversationId) fun currentCallProtocol(conversationId: ConversationId): Conversation.ProtocolInfo? suspend fun observeCurrentCall(conversationId: ConversationId): Flow + + suspend fun updateRecentlyEndedCallMetadata(recentlyEndedCallMetadata: RecentlyEndedCallMetadata) + suspend fun observeRecentlyEndedCallMetadata(): Flow } @Suppress("LongParameterList", "TooManyFunctions") @@ -164,25 +168,38 @@ internal class CallDataSource( private val scope = CoroutineScope(job + kaliumDispatchers.io) private val callJobs = ConcurrentMutableMap() private val staleParticipantJobs = ConcurrentMutableMap() + private val _recentlyEndedCallFlow = MutableSharedFlow( + extraBufferCapacity = 1 + ) override suspend fun observeCurrentCall(conversationId: ConversationId): Flow = _callMetadataProfile.map { - it[conversationId]?.let { currentCall -> - Call( - conversationId = conversationId, - status = currentCall.callStatus, - isMuted = currentCall.isMuted, - isCameraOn = currentCall.isCameraOn, - isCbrEnabled = currentCall.isCbrEnabled, - callerId = currentCall.callerId, - conversationName = currentCall.conversationName, - conversationType = currentCall.conversationType, - callerName = currentCall.callerName, - callerTeamName = currentCall.callerTeamName, - establishedTime = currentCall.establishedTime, - participants = currentCall.getFullParticipants(), - maxParticipants = currentCall.maxParticipants - ) - } + it[conversationId]?.mapCallMetadataToCall(conversationId) + } + + override suspend fun updateRecentlyEndedCallMetadata(recentlyEndedCallMetadata: RecentlyEndedCallMetadata) { + _recentlyEndedCallFlow.emit(recentlyEndedCallMetadata) + } + + private fun CallMetadata.mapCallMetadataToCall(conversationId: ConversationId): Call { + return Call( + conversationId = conversationId, + status = callStatus, + isMuted = isMuted, + isCameraOn = isCameraOn, + isCbrEnabled = isCbrEnabled, + callerId = callerId, + conversationName = conversationName, + conversationType = conversationType, + callerName = callerName, + callerTeamName = callerTeamName, + establishedTime = establishedTime, + participants = getFullParticipants(), + maxParticipants = maxParticipants + ) + } + + override suspend fun observeRecentlyEndedCallMetadata(): Flow { + return _recentlyEndedCallFlow } override suspend fun getCallConfigResponse(limit: Int?): Either = wrapApiRequest { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/InCallReactionsRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/InCallReactionsRepository.kt new file mode 100644 index 00000000000..c8a1717de62 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/InCallReactionsRepository.kt @@ -0,0 +1,48 @@ +/* + * Wire + * Copyright (C) 2024 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.call + +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.user.UserId +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.filter + +internal interface InCallReactionsRepository { + suspend fun addInCallReaction(conversationId: ConversationId, senderUserId: UserId, emojis: Set) + fun observeInCallReactions(conversationId: ConversationId): Flow +} + +internal class InCallReactionsDataSource : InCallReactionsRepository { + + private val inCallReactionsFlow: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = BUFFER_SIZE, onBufferOverflow = BufferOverflow.DROP_OLDEST) + + override suspend fun addInCallReaction(conversationId: ConversationId, senderUserId: UserId, emojis: Set) { + inCallReactionsFlow.emit(InCallReactionMessage(conversationId, senderUserId, emojis)) + } + + override fun observeInCallReactions(conversationId: ConversationId): Flow = inCallReactionsFlow.asSharedFlow() + .filter { it.conversationId == conversationId } + + private companion object { + const val BUFFER_SIZE = 32 // drop after this threshold + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/MLSClientProvider.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/MLSClientProvider.kt index 97d2dc0afe8..c86dae79bd3 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/MLSClientProvider.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/MLSClientProvider.kt @@ -28,6 +28,7 @@ import com.wire.kalium.cryptography.coreCryptoCentral import com.wire.kalium.logger.KaliumLogLevel import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.E2EIFailure +import com.wire.kalium.logic.MLSFailure import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.featureConfig.FeatureConfigRepository @@ -130,6 +131,10 @@ class MLSClientProviderImpl( } override suspend fun getOrFetchMLSConfig(): Either { + if (!userConfigRepository.isMLSEnabled().getOrElse(true)) { + kaliumLogger.w("$TAG: Cannot fetch MLS config, MLS is disabled.") + return MLSFailure.Disabled.left() + } return userConfigRepository.getSupportedCipherSuite().flatMapLeft { featureConfigRepository.getFeatureConfigs().map { it.mlsModel.supportedCipherSuite diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt index cc2d9995a75..6382559ee23 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt @@ -676,15 +676,16 @@ internal fun ConversationEntity.VerificationStatus.toModel(): Conversation.Verif } internal fun ConversationFilter.toDao(): ConversationFilterEntity = when (this) { - ConversationFilter.ALL -> ConversationFilterEntity.ALL - ConversationFilter.FAVORITES -> ConversationFilterEntity.FAVORITES - ConversationFilter.GROUPS -> ConversationFilterEntity.GROUPS - ConversationFilter.ONE_ON_ONE -> ConversationFilterEntity.ONE_ON_ONE + ConversationFilter.All -> ConversationFilterEntity.ALL + ConversationFilter.Favorites -> ConversationFilterEntity.FAVORITES + ConversationFilter.Groups -> ConversationFilterEntity.GROUPS + ConversationFilter.OneOnOne -> ConversationFilterEntity.ONE_ON_ONE + is ConversationFilter.Folder -> ConversationFilterEntity.ALL // TODO think how to secure that } internal fun ConversationFilterEntity.toModel(): ConversationFilter = when (this) { - ConversationFilterEntity.ALL -> ConversationFilter.ALL - ConversationFilterEntity.FAVORITES -> ConversationFilter.FAVORITES - ConversationFilterEntity.GROUPS -> ConversationFilter.GROUPS - ConversationFilterEntity.ONE_ON_ONE -> ConversationFilter.ONE_ON_ONE + ConversationFilterEntity.ALL -> ConversationFilter.All + ConversationFilterEntity.FAVORITES -> ConversationFilter.Favorites + ConversationFilterEntity.GROUPS -> ConversationFilter.Groups + ConversationFilterEntity.ONE_ON_ONE -> ConversationFilter.OneOnOne } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt index f199fe862f7..2820fa3d8df 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt @@ -132,12 +132,12 @@ interface ConversationRepository { suspend fun observeConversationList(): Flow> suspend fun observeConversationListDetails( fromArchive: Boolean, - conversationFilter: ConversationFilter = ConversationFilter.ALL + conversationFilter: ConversationFilter = ConversationFilter.All ): Flow> suspend fun observeConversationListDetailsWithEvents( fromArchive: Boolean = false, - conversationFilter: ConversationFilter = ConversationFilter.ALL + conversationFilter: ConversationFilter = ConversationFilter.All ): Flow> suspend fun getConversationIds( @@ -224,6 +224,7 @@ interface ConversationRepository { ): Either suspend fun deleteConversation(conversationId: ConversationId): Either + suspend fun deleteConversationLocally(conversationId: ConversationId): Either /** * Deletes all conversation messages @@ -432,16 +433,19 @@ internal class ConversationDataSource internal constructor( ): Either = wrapStorageRequest { val isNewConversation = conversationDAO.getConversationById(conversation.id.toDao()) == null if (isNewConversation) { - conversationDAO.insertConversation( - conversationMapper.fromApiModelToDaoModel( - conversation, - mlsGroupState = conversation.groupId?.let { mlsGroupState(idMapper.fromGroupIDEntity(it), originatedFromEvent) }, - selfTeamIdProvider().getOrNull(), + val mlsGroupState = conversation.groupId?.let { mlsGroupState(idMapper.fromGroupIDEntity(it), originatedFromEvent) } + if (shouldPersistMLSConversation(mlsGroupState)) { + conversationDAO.insertConversation( + conversationMapper.fromApiModelToDaoModel( + conversation, + mlsGroupState = mlsGroupState?.getOrNull(), + selfTeamIdProvider().getOrNull(), + ) ) - ) - memberDAO.insertMembersWithQualifiedId( - memberMapper.fromApiModelToDaoModel(conversation.members), idMapper.fromApiToDao(conversation.id) - ) + memberDAO.insertMembersWithQualifiedId( + memberMapper.fromApiModelToDaoModel(conversation.members), idMapper.fromApiToDao(conversation.id) + ) + } } isNewConversation } @@ -453,17 +457,19 @@ internal class ConversationDataSource internal constructor( invalidateMembers: Boolean ) = wrapStorageRequest { val conversationEntities = conversations - .map { conversationResponse -> - conversationMapper.fromApiModelToDaoModel( - conversationResponse, - mlsGroupState = conversationResponse.groupId?.let { - mlsGroupState( - idMapper.fromGroupIDEntity(it), - originatedFromEvent - ) - }, - selfTeamIdProvider().getOrNull(), - ) + .mapNotNull { conversationResponse -> + val mlsGroupState = conversationResponse.groupId?.let { + mlsGroupState(idMapper.fromGroupIDEntity(it), originatedFromEvent) + } + if (shouldPersistMLSConversation(mlsGroupState)) { + conversationMapper.fromApiModelToDaoModel( + conversationResponse, + mlsGroupState = mlsGroupState?.getOrNull(), + selfTeamIdProvider().getOrNull(), + ) + } else { + null + } } conversationDAO.insertConversations(conversationEntities) conversations.forEach { conversationsResponse -> @@ -483,10 +489,11 @@ internal class ConversationDataSource internal constructor( } } - private suspend fun mlsGroupState(groupId: GroupID, originatedFromEvent: Boolean = false): ConversationEntity.GroupState = - hasEstablishedMLSGroup(groupId).fold({ - throw IllegalStateException(it.toString()) // TODO find a more fitting exception? - }, { exists -> + private suspend fun mlsGroupState( + groupId: GroupID, + originatedFromEvent: Boolean = false + ): Either = hasEstablishedMLSGroup(groupId) + .map { exists -> if (exists) { ConversationEntity.GroupState.ESTABLISHED } else { @@ -496,7 +503,7 @@ internal class ConversationDataSource internal constructor( ConversationEntity.GroupState.PENDING_JOIN } } - }) + } private suspend fun hasEstablishedMLSGroup(groupID: GroupID): Either = mlsClientProvider.getMLSClient() @@ -506,6 +513,10 @@ internal class ConversationDataSource internal constructor( } } + // if group state is not null and is left, then we don't want to persist the MLS conversation + private fun shouldPersistMLSConversation(groupState: Either?): Boolean = + groupState?.fold({ true }, { false }) != true + @DelicateKaliumApi("This function does not get values from cache") override suspend fun getProteusSelfConversationId(): Either = wrapStorageRequest { conversationDAO.getSelfConversationId(ConversationEntity.Protocol.PROTEUS) } @@ -874,6 +885,12 @@ internal class ConversationDataSource internal constructor( } } + override suspend fun deleteConversationLocally(conversationId: ConversationId): Either { + return wrapStorageRequest { + conversationDAO.deleteConversationByQualifiedID(conversationId.toDao()) + } + } + override suspend fun clearContent(conversationId: ConversationId): Either = wrapStorageRequest { conversationDAO.clearContent(conversationId.toDao()) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt index aec18932a97..c41464ea225 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt @@ -73,5 +73,5 @@ data class ConversationQueryConfig( val fromArchive: Boolean = false, val onlyInteractionEnabled: Boolean = false, val newActivitiesOnTop: Boolean = false, - val conversationFilter: ConversationFilter = ConversationFilter.ALL, + val conversationFilter: ConversationFilter = ConversationFilter.All, ) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepository.kt index c6e8c608c27..c8c2af8c758 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepository.kt @@ -32,6 +32,7 @@ import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.client.MLSClientProvider import com.wire.kalium.logic.data.conversation.mls.MLSAdditionResult import com.wire.kalium.logic.data.e2ei.CertificateRevocationListRepository +import com.wire.kalium.logic.data.e2ei.RevocationListChecker import com.wire.kalium.logic.data.event.Event import com.wire.kalium.logic.data.event.EventDeliveryInfo import com.wire.kalium.logic.data.id.ConversationId @@ -45,12 +46,11 @@ import com.wire.kalium.logic.data.id.toModel import com.wire.kalium.logic.data.keypackage.KeyPackageLimitsProvider import com.wire.kalium.logic.data.keypackage.KeyPackageRepository import com.wire.kalium.logic.data.mls.CipherSuite +import com.wire.kalium.logic.data.mls.MLSPublicKeys import com.wire.kalium.logic.data.mlspublickeys.MLSPublicKeysRepository import com.wire.kalium.logic.data.mlspublickeys.getRemovalKey import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.di.MapperProvider -import com.wire.kalium.logic.data.e2ei.RevocationListChecker -import com.wire.kalium.logic.data.mls.MLSPublicKeys import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.flatMapLeft @@ -68,9 +68,9 @@ import com.wire.kalium.logic.sync.incremental.EventSource import com.wire.kalium.logic.wrapApiRequest import com.wire.kalium.logic.wrapMLSRequest import com.wire.kalium.logic.wrapStorageRequest +import com.wire.kalium.network.api.authenticated.notification.EventContentDTO import com.wire.kalium.network.api.base.authenticated.client.ClientApi import com.wire.kalium.network.api.base.authenticated.message.MLSMessageApi -import com.wire.kalium.network.api.authenticated.notification.EventContentDTO import com.wire.kalium.network.exceptions.KaliumException import com.wire.kalium.network.exceptions.isMlsClientMismatch import com.wire.kalium.network.exceptions.isMlsCommitMissingReferences @@ -481,7 +481,7 @@ internal class MLSConversationDataSource( val keyPackages = result.successfullyFetchedKeyPackages val clientKeyPackageList = keyPackages.map { it.keyPackage.decodeBase64Bytes() } wrapMLSRequest { - if (userIdList.isEmpty()) { + if (clientKeyPackageList.isEmpty()) { // We are creating a group with only our self client which technically // doesn't need be added with a commit, but our backend API requires one, // so we create a commit by updating our key material. @@ -566,6 +566,7 @@ internal class MLSConversationDataSource( keys.flatMap { externalSenders -> establishMLSGroup( + mlsClient = mlsClient, groupID = groupID, members = members, externalSenders = externalSenders, @@ -583,6 +584,7 @@ internal class MLSConversationDataSource( conversationDAO.getMLSGroupIdByConversationId(parentId.toDao())?.let { parentGroupId -> val externalSenderKey = mlsClient.getExternalSenders(GroupID(parentGroupId).toCrypto()) establishMLSGroup( + mlsClient = mlsClient, groupID = groupID, members = emptyList(), externalSenders = externalSenderKey.value, @@ -593,45 +595,44 @@ internal class MLSConversationDataSource( } private suspend fun establishMLSGroup( + mlsClient: MLSClient, groupID: GroupID, members: List, externalSenders: ByteArray, allowPartialMemberList: Boolean = false, ): Either = withContext(serialDispatcher) { kaliumLogger.d("establish MLS group: $groupID") - mlsClientProvider.getMLSClient().flatMap { mlsClient -> - wrapMLSRequest { - mlsClient.createConversation( - idMapper.toCryptoModel(groupID), - externalSenders - ) - }.flatMapLeft { - if (it is MLSFailure.ConversationAlreadyExists) { - Either.Right(Unit) - } else { - Either.Left(it) - } - }.flatMap { - internalAddMemberToMLSGroup( - groupID = groupID, - userIdList = members, - retryOnStaleMessage = false, - allowPartialMemberList = allowPartialMemberList, - cipherSuite = CipherSuite.fromTag(mlsClient.getDefaultCipherSuite()) - ).onFailure { - wrapMLSRequest { - mlsClient.wipeConversation(groupID.toCrypto()) - } + wrapMLSRequest { + mlsClient.createConversation( + idMapper.toCryptoModel(groupID), + externalSenders + ) + }.flatMapLeft { + if (it is MLSFailure.ConversationAlreadyExists) { + Either.Right(Unit) + } else { + Either.Left(it) + } + }.flatMap { + internalAddMemberToMLSGroup( + groupID = groupID, + userIdList = members, + retryOnStaleMessage = false, + allowPartialMemberList = allowPartialMemberList, + cipherSuite = CipherSuite.fromTag(mlsClient.getDefaultCipherSuite()) + ).onFailure { + wrapMLSRequest { + mlsClient.wipeConversation(groupID.toCrypto()) } - }.flatMap { additionResult -> - wrapStorageRequest { - conversationDAO.updateMlsGroupStateAndCipherSuite( - ConversationEntity.GroupState.ESTABLISHED, - ConversationEntity.CipherSuite.fromTag(mlsClient.getDefaultCipherSuite().toInt()), - idMapper.toGroupIDEntity(groupID) - ) - }.map { additionResult } } + }.flatMap { additionResult -> + wrapStorageRequest { + conversationDAO.updateMlsGroupStateAndCipherSuite( + ConversationEntity.GroupState.ESTABLISHED, + ConversationEntity.CipherSuite.fromTag(mlsClient.getDefaultCipherSuite().toInt()), + idMapper.toGroupIDEntity(groupID) + ) + }.map { additionResult } } } @@ -656,8 +657,8 @@ internal class MLSConversationDataSource( keyPackageRepository .replaceKeyPackages(clientId, rotateBundle.newKeyPackages, CipherSuite.fromTag(mlsClient.getDefaultCipherSuite())) .flatMapLeft { - return E2EIFailure.RotationAndMigration(it).left() - } + return E2EIFailure.RotationAndMigration(it).left() + } } kaliumLogger.w("send migration commits after key rotations") kaliumLogger.w("rotate bundles: ${rotateBundle.commits.size}") @@ -670,17 +671,23 @@ internal class MLSConversationDataSource( }) override suspend fun getClientIdentity(clientId: ClientId) = - wrapStorageRequest { conversationDAO.getE2EIConversationClientInfoByClientId(clientId.value) }.flatMap { - mlsClientProvider.getMLSClient().flatMap { mlsClient -> - wrapMLSRequest { + wrapStorageRequest { conversationDAO.getE2EIConversationClientInfoByClientId(clientId.value) } + .flatMap { conversationClientInfo -> + mlsClientProvider.getMLSClient().flatMap { mlsClient -> + wrapMLSRequest { - mlsClient.getDeviceIdentities( - it.mlsGroupId, - listOf(CryptoQualifiedClientId(it.clientId, it.userId.toModel().toCrypto())) - ).firstOrNull() + mlsClient.getDeviceIdentities( + conversationClientInfo.mlsGroupId, + listOf( + CryptoQualifiedClientId( + conversationClientInfo.clientId, + conversationClientInfo.userId.toModel().toCrypto() + ) + ) + ).firstOrNull() + } } } - } override suspend fun getUserIdentity(userId: UserId) = wrapStorageRequest { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt index 70ad373bdfa..5db72788cbd 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt @@ -34,6 +34,7 @@ import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.flatMapLeft import com.wire.kalium.logic.functional.map +import com.wire.kalium.logic.functional.mapRight import com.wire.kalium.logic.functional.onFailure import com.wire.kalium.logic.functional.onSuccess import com.wire.kalium.logic.kaliumLogger @@ -57,6 +58,7 @@ internal interface ConversationFolderRepository { suspend fun addConversationToFolder(conversationId: QualifiedID, folderId: String): Either suspend fun removeConversationFromFolder(conversationId: QualifiedID, folderId: String): Either suspend fun syncConversationFoldersFromLocal(): Either + suspend fun observeUserFolders(): Flow>> } internal class ConversationFolderDataSource internal constructor( @@ -72,7 +74,7 @@ internal class ConversationFolderDataSource internal constructor( } override suspend fun getFavoriteConversationFolder(): Either = wrapStorageRequest { - conversationFolderDAO.getFavoriteConversationFolder().toModel() + conversationFolderDAO.getFavoriteConversationFolder()?.toModel() } override suspend fun observeConversationsFromFolder(folderId: String): Flow> = @@ -152,4 +154,10 @@ internal class ConversationFolderDataSource internal constructor( } } } + + override suspend fun observeUserFolders(): Flow>> { + return conversationFolderDAO.observeUserFolders() + .wrapStorageRequest() + .mapRight { folderEntities -> folderEntities.map { it.toModel() } } + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt index dd0f47b5996..c6281fd79f0 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt @@ -253,6 +253,13 @@ internal interface MessageRepository { messageId: String, conversationId: ConversationId ): Either + + suspend fun getAllAssetIdsFromConversationId( + conversationId: ConversationId, + ): Either> + + suspend fun getSenderNameByMessageId(conversationId: ConversationId, messageId: String): Either + suspend fun getNextAudioMessageInConversation(conversationId: ConversationId, messageId: String): Either } // TODO: suppress TooManyFunctions for now, something we need to fix in the future @@ -706,4 +713,21 @@ internal class MessageDataSource internal constructor( ): Either = wrapStorageRequest { messageDAO.getMessageAssetTransferStatus(messageId, conversationId.toDao()).toModel() } + + override suspend fun getAllAssetIdsFromConversationId( + conversationId: ConversationId + ): Either> { + return wrapStorageRequest { + messageDAO.getAllMessageAssetIdsForConversationId(conversationId = conversationId.toDao()) + } + } + + override suspend fun getSenderNameByMessageId(conversationId: ConversationId, messageId: String): Either = + wrapStorageRequest { messageDAO.getSenderNameById(messageId, conversationId.toDao()) } + + override suspend fun getNextAudioMessageInConversation( + conversationId: ConversationId, + messageId: String + ): Either = + wrapStorageRequest { messageDAO.getNextAudioMessageInConversation(messageId, conversationId.toDao()) } } 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 0d0288de34f..69828375295 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 @@ -126,6 +126,7 @@ internal class PersistMessageUseCaseImpl( is MessageContent.MemberChange.RemovedFromTeam -> false is MessageContent.TeamMemberRemoved -> false is MessageContent.DataTransfer -> false + is MessageContent.InCallEmoji -> false } @Suppress("ComplexMethod") @@ -180,6 +181,7 @@ internal class PersistMessageUseCaseImpl( is MessageContent.LegalHold, is MessageContent.MemberChange.RemovedFromTeam, is MessageContent.TeamMemberRemoved, - is MessageContent.DataTransfer -> false + is MessageContent.DataTransfer, + is MessageContent.InCallEmoji -> false } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapper.kt index 036fa81bf0f..49f5dc765a2 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapper.kt @@ -45,6 +45,8 @@ import com.wire.kalium.protobuf.messages.DataTransfer import com.wire.kalium.protobuf.messages.Ephemeral import com.wire.kalium.protobuf.messages.External import com.wire.kalium.protobuf.messages.GenericMessage +import com.wire.kalium.protobuf.messages.GenericMessage.UnknownStrategy +import com.wire.kalium.protobuf.messages.InCallEmoji import com.wire.kalium.protobuf.messages.Knock import com.wire.kalium.protobuf.messages.LastRead import com.wire.kalium.protobuf.messages.LegalHoldStatus @@ -57,7 +59,6 @@ import com.wire.kalium.protobuf.messages.Quote import com.wire.kalium.protobuf.messages.Reaction import com.wire.kalium.protobuf.messages.Text import com.wire.kalium.protobuf.messages.TrackingIdentifier -import com.wire.kalium.protobuf.messages.UnknownStrategy import kotlinx.datetime.Instant import pbandk.ByteArr @@ -146,6 +147,7 @@ class ProtoContentMapperImpl( is MessageContent.Location -> packLocation(readableContent, expectsReadConfirmation, legalHoldStatus) is MessageContent.DataTransfer -> packDataTransfer(readableContent) + is MessageContent.InCallEmoji -> packInCallEmoji(readableContent) } } @@ -266,7 +268,8 @@ class ProtoContentMapperImpl( is MessageContent.ButtonAction, is MessageContent.ButtonActionConfirmation, is MessageContent.TextEdited, - is MessageContent.DataTransfer -> throw IllegalArgumentException( + is MessageContent.DataTransfer, + is MessageContent.InCallEmoji -> throw IllegalArgumentException( "Unexpected message content type: ${readableContent.getType()}" ) } @@ -377,6 +380,8 @@ class ProtoContentMapperImpl( MessageContent.Ignored } + is GenericMessage.Content.InCallEmoji -> unpackInCallEmoji(protoContent) + null -> { kaliumLogger.w( "Null content when parsing protobuf. Message UUID = ${genericMessage.messageId.obfuscateId()}" + @@ -390,6 +395,8 @@ class ProtoContentMapperImpl( null -> MessageContent.Ignored } } + + is GenericMessage.Content.InCallHandRaise -> MessageContent.Ignored } return readableContent } @@ -574,13 +581,15 @@ class ProtoContentMapperImpl( Cleared( conversationId = readableContent.conversationId.value, qualifiedConversationId = idMapper.toProtoModel(readableContent.conversationId), - clearedTimestamp = readableContent.time.toEpochMilliseconds() + clearedTimestamp = readableContent.time.toEpochMilliseconds(), + needToRemoveLocally = readableContent.needToRemoveLocally ) ) private fun unpackCleared(protoContent: GenericMessage.Content.Cleared) = MessageContent.Cleared( conversationId = extractConversationId(protoContent.value.qualifiedConversationId, protoContent.value.conversationId), - time = Instant.fromEpochMilliseconds(protoContent.value.clearedTimestamp) + time = Instant.fromEpochMilliseconds(protoContent.value.clearedTimestamp), + needToRemoveLocally = protoContent.value.needToRemoveLocally ?: false ) private fun toProtoLegalHoldStatus(legalHoldStatus: Conversation.LegalHoldStatus): LegalHoldStatus = @@ -750,6 +759,29 @@ class ProtoContentMapperImpl( ) } + private fun unpackInCallEmoji(protoContent: GenericMessage.Content.InCallEmoji): MessageContent.InCallEmoji { + return MessageContent.InCallEmoji( + // Map of emoji to senderId + emojis = protoContent.value.emojis + .mapNotNull { + val key = it.key ?: return@mapNotNull null + val value = it.value ?: return@mapNotNull null + key to value + } + .associateBy({ it.first }, { it.second }) + ) + } + + private fun packInCallEmoji(content: MessageContent.InCallEmoji): GenericMessage.Content.InCallEmoji { + return GenericMessage.Content.InCallEmoji( + inCallEmoji = InCallEmoji( + emojis = content.emojis.map { entry -> + InCallEmoji.EmojisEntry(key = entry.key, value = entry.value) + } + ) + ) + } + private fun extractConversationId( qualifiedConversationID: QualifiedConversationId?, unqualifiedConversationID: String diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt index e8760f83b79..b283f3158b6 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt @@ -23,6 +23,7 @@ import com.wire.kalium.logger.obfuscateDomain import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.conversation.MemberMapper import com.wire.kalium.logic.data.conversation.Recipient import com.wire.kalium.logic.data.conversation.mls.NameAndHandle @@ -100,7 +101,7 @@ interface UserRepository { * Fetches user information for all of users id stored in the DB */ suspend fun fetchAllOtherUsers(): Either - suspend fun fetchUsersByIds(qualifiedUserIdList: Set): Either + suspend fun fetchUsersByIds(qualifiedUserIdList: Set): Either suspend fun fetchUsersIfUnknownByIds(ids: Set): Either suspend fun observeSelfUser(): Flow suspend fun observeSelfUserWithTeam(): Flow> @@ -167,6 +168,7 @@ interface UserRepository { suspend fun getNameAndHandle(userId: UserId): Either suspend fun migrateUserToTeam(teamName: String): Either suspend fun updateTeamId(userId: UserId, teamId: TeamId): Either + suspend fun isClientMlsCapable(userId: UserId, clientId: ClientId): Either } @Suppress("LongParameterList", "TooManyFunctions") @@ -266,7 +268,7 @@ internal class UserDataSource internal constructor( override suspend fun fetchAllOtherUsers(): Either { val ids = userDAO.allOtherUsersId().map(UserIDEntity::toModel).toSet() - return fetchUsersByIds(ids) + return fetchUsersByIds(ids).map { } } override suspend fun fetchUserInfo(userId: UserId) = @@ -285,7 +287,7 @@ internal class UserDataSource internal constructor( } else { qualifiedUserIdList .chunked(BATCH_SIZE) - .foldToEitherWhileRight(ListUsersDTO(emptyList(), emptyList())) { chunk, acc -> + .foldToEitherWhileRight(ListUsersDTO(emptyList(), emptyList())) { chunk, usersDTO -> wrapApiRequest { kaliumLogger.d("Fetching ${chunk.size} users") userDetailsApi.getMultipleUsers( @@ -293,9 +295,9 @@ internal class UserDataSource internal constructor( ) }.map { kaliumLogger.d("Found ${it.usersFound.size} users and ${it.usersFailed.size} failed users") - acc.copy( - usersFound = (acc.usersFound + it.usersFound).distinct(), - usersFailed = (acc.usersFailed + it.usersFailed).distinct(), + usersDTO.copy( + usersFound = (usersDTO.usersFound + it.usersFound).distinct(), + usersFailed = (usersDTO.usersFailed + it.usersFailed).distinct(), ) } } @@ -328,8 +330,8 @@ internal class UserDataSource internal constructor( } } - override suspend fun fetchUsersByIds(qualifiedUserIdList: Set): Either = - fetchUsersByIdsReturningListUsersDTO(qualifiedUserIdList).map { } + override suspend fun fetchUsersByIds(qualifiedUserIdList: Set): Either = + fetchUsersByIdsReturningListUsersDTO(qualifiedUserIdList).map { it.usersFound.isNotEmpty() } private suspend fun fetchTeamMembersByIds(userProfileList: List): Either> { val selfUserDomain = selfUserId.domain @@ -411,7 +413,7 @@ internal class UserDataSource internal constructor( qualifiedIDList.filterNot { knownUsers.any { userEntity -> userEntity.id == it && !userEntity.name.isNullOrBlank() } } }.flatMap { missingIds -> if (missingIds.isEmpty()) Either.Right(Unit) - else fetchUsersByIds(missingIds.map { it.toModel() }.toSet()) + else fetchUsersByIds(missingIds.map { it.toModel() }.toSet()).map { } } @OptIn(ExperimentalCoroutinesApi::class) @@ -629,7 +631,7 @@ internal class UserDataSource internal constructor( }.flatMap { usersWithoutMetadata -> kaliumLogger.d("Numbers of users to refresh: ${usersWithoutMetadata.size}") val userIds = usersWithoutMetadata.map { it.id.toModel() }.toSet() - fetchUsersByIds(userIds) + fetchUsersByIds(userIds).map { } } override suspend fun removeUserBrokenAsset(qualifiedID: QualifiedID) = wrapStorageRequest { @@ -668,6 +670,10 @@ internal class UserDataSource internal constructor( userDAO.updateTeamId(userId.toDao(), teamId.value) } + override suspend fun isClientMlsCapable(userId: UserId, clientId: ClientId): Either = wrapStorageRequest { + clientDAO.isMLSCapable(userId.toDao(), clientId.value) + } + companion object { internal const val SELF_USER_ID_KEY = "selfUserID" 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 3b40fb7cab4..cfc17dd8449 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 @@ -40,6 +40,8 @@ import com.wire.kalium.logic.data.asset.KaliumFileSystem import com.wire.kalium.logic.data.asset.KaliumFileSystemImpl import com.wire.kalium.logic.data.call.CallDataSource import com.wire.kalium.logic.data.call.CallRepository +import com.wire.kalium.logic.data.call.InCallReactionsDataSource +import com.wire.kalium.logic.data.call.InCallReactionsRepository import com.wire.kalium.logic.data.call.VideoStateChecker import com.wire.kalium.logic.data.call.VideoStateCheckerImpl import com.wire.kalium.logic.data.call.mapper.CallMapper @@ -177,6 +179,8 @@ import com.wire.kalium.logic.feature.call.CallsScope import com.wire.kalium.logic.feature.call.GlobalCallManager import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdaterImpl +import com.wire.kalium.logic.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase +import com.wire.kalium.logic.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProviderImpl import com.wire.kalium.logic.feature.call.usecase.UpdateConversationClientsForCurrentCallUseCase @@ -1273,7 +1277,8 @@ class UserSessionScope internal constructor( conversationClientsInCallUpdater = conversationClientsInCallUpdater, getCallConversationType = getCallConversationType, networkStateObserver = networkStateObserver, - kaliumConfigs = kaliumConfigs + kaliumConfigs = kaliumConfigs, + createAndPersistRecentlyEndedCallMetadata = createAndPersistRecentlyEndedCallMetadata ) } @@ -1384,7 +1389,8 @@ class UserSessionScope internal constructor( receiptMessageHandler, buttonActionConfirmationHandler, dataTransferEventHandler, - userId + inCallReactionsRepository, + userId, ) private val staleEpochVerifier: StaleEpochVerifier @@ -1771,7 +1777,8 @@ class UserSessionScope internal constructor( cachedClientIdClearer, updateSupportedProtocolsAndResolveOneOnOnes, registerMLSClientUseCase, - syncFeatureConfigsUseCase + syncFeatureConfigsUseCase, + userConfigRepository ) } val conversations: ConversationScope by lazy { @@ -1802,7 +1809,9 @@ class UserSessionScope internal constructor( this, userScopedLogger, refreshUsersWithoutMetadata, - sessionManager.getServerConfig().links + sessionManager.getServerConfig().links, + messages.messageRepository, + assetRepository ) } @@ -1831,6 +1840,7 @@ class UserSessionScope internal constructor( legalHoldHandler, notificationTokenRepository, this, + userStorage, userScopedLogger, ) } @@ -1900,7 +1910,8 @@ class UserSessionScope internal constructor( selfTeamId, checkRevocationList, syncFeatureConfigsUseCase, - userScopedLogger + userScopedLogger, + getTeamUrlUseCase ) } @@ -2060,7 +2071,8 @@ class UserSessionScope internal constructor( userConfigRepository = userConfigRepository, getCallConversationType = getCallConversationType, conversationClientsInCallUpdater = conversationClientsInCallUpdater, - kaliumConfigs = kaliumConfigs + kaliumConfigs = kaliumConfigs, + inCallReactionsRepository = inCallReactionsRepository, ) val connection: ConnectionScope @@ -2106,8 +2118,15 @@ class UserSessionScope internal constructor( userScopedLogger ) + private val createAndPersistRecentlyEndedCallMetadata: CreateAndPersistRecentlyEndedCallMetadataUseCase + get() = CreateAndPersistRecentlyEndedCallMetadataUseCaseImpl( + callRepository = callRepository, + observeConversationMembers = conversations.observeConversationMembers, + selfTeamIdProvider = selfTeamId + ) + val migrateFromPersonalToTeam: MigrateFromPersonalToTeamUseCase - get() = MigrateFromPersonalToTeamUseCaseImpl(userId, userRepository, invalidateTeamId) + get() = MigrateFromPersonalToTeamUseCaseImpl(userId, userRepository, syncContacts, invalidateTeamId) internal val getProxyCredentials: GetProxyCredentialsUseCase get() = GetProxyCredentialsUseCaseImpl(sessionManager) @@ -2152,6 +2171,15 @@ class UserSessionScope internal constructor( ) } + val getTeamUrlUseCase: GetTeamUrlUseCase by lazy { + GetTeamUrlUseCase( + userId, + authenticationScope.serverConfigRepository, + ) + } + + private val inCallReactionsRepository: InCallReactionsRepository = InCallReactionsDataSource() + /** * This will start subscribers of observable work per user session, as long as the user is logged in. * When the user logs out, this work will be canceled. diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt index 59aead0b38f..9066eccd245 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt @@ -22,6 +22,7 @@ import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.call.CallingParticipantsOrder import com.wire.kalium.logic.data.call.CallingParticipantsOrderImpl +import com.wire.kalium.logic.data.call.InCallReactionsRepository import com.wire.kalium.logic.data.call.ParticipantsFilterImpl import com.wire.kalium.logic.data.call.ParticipantsOrderByNameImpl import com.wire.kalium.logic.data.conversation.ConversationRepository @@ -43,8 +44,6 @@ import com.wire.kalium.logic.feature.call.usecase.GetAllCallsWithSortedParticipa import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider import com.wire.kalium.logic.feature.call.usecase.GetIncomingCallsUseCase import com.wire.kalium.logic.feature.call.usecase.GetIncomingCallsUseCaseImpl -import com.wire.kalium.logic.feature.call.usecase.ObserveConferenceCallingEnabledUseCase -import com.wire.kalium.logic.feature.call.usecase.ObserveConferenceCallingEnabledUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.IsCallRunningUseCase import com.wire.kalium.logic.feature.call.usecase.IsEligibleToStartCallUseCase import com.wire.kalium.logic.feature.call.usecase.IsEligibleToStartCallUseCaseImpl @@ -53,16 +52,22 @@ import com.wire.kalium.logic.feature.call.usecase.IsLastCallClosedUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.MuteCallUseCase import com.wire.kalium.logic.feature.call.usecase.MuteCallUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.ObserveAskCallFeedbackUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveConferenceCallingEnabledUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveConferenceCallingEnabledUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.ObserveEndCallDueToConversationDegradationUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEndCallDueToConversationDegradationUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallWithSortedParticipantsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallWithSortedParticipantsUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCaseImpl +import com.wire.kalium.logic.feature.call.usecase.ObserveInCallReactionsUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveInCallReactionsUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.ObserveOngoingCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveOngoingCallsUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.ObserveOutgoingCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveOutgoingCallUseCaseImpl +import com.wire.kalium.logic.feature.call.usecase.ObserveRecentlyEndedCallMetadataUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveRecentlyEndedCallMetadataUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.ObserveSpeakerUseCase import com.wire.kalium.logic.feature.call.usecase.RejectCallUseCase import com.wire.kalium.logic.feature.call.usecase.RequestVideoStreamsUseCase @@ -101,6 +106,7 @@ class CallsScope internal constructor( private val conversationClientsInCallUpdater: ConversationClientsInCallUpdater, private val getCallConversationType: GetCallConversationTypeProvider, private val kaliumConfigs: KaliumConfigs, + private val inCallReactionsRepository: InCallReactionsRepository, internal val dispatcher: KaliumDispatcher = KaliumDispatcherImpl ) { @@ -232,4 +238,12 @@ class CallsScope internal constructor( val updateNextTimeCallFeedback: UpdateNextTimeCallFeedbackUseCase by lazy { UpdateNextTimeCallFeedbackUseCase(userConfigRepository) } + + val observeRecentlyEndedCallMetadata: ObserveRecentlyEndedCallMetadataUseCase + get() = ObserveRecentlyEndedCallMetadataUseCaseImpl( + callRepository = callRepository + ) + + val observeInCallReactions: ObserveInCallReactionsUseCase + get() = ObserveInCallReactionsUseCaseImpl(inCallReactionsRepository) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt index eb7bca57c8b..12f8ee9dadf 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt @@ -34,6 +34,7 @@ import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider +import com.wire.kalium.logic.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs import com.wire.kalium.network.NetworkStateObserver @@ -57,7 +58,8 @@ expect class GlobalCallManager { conversationClientsInCallUpdater: ConversationClientsInCallUpdater, getCallConversationType: GetCallConversationTypeProvider, networkStateObserver: NetworkStateObserver, - kaliumConfigs: KaliumConfigs + kaliumConfigs: KaliumConfigs, + createAndPersistRecentlyEndedCallMetadata: CreateAndPersistRecentlyEndedCallMetadataUseCase ): CallManager suspend fun removeInMemoryCallingManagerForUser(userId: UserId) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/CreateAndPersistRecentlyEndedCallMetadataUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/CreateAndPersistRecentlyEndedCallMetadataUseCase.kt new file mode 100644 index 00000000000..8c5f88e2691 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/CreateAndPersistRecentlyEndedCallMetadataUseCase.kt @@ -0,0 +1,99 @@ +/* + * Wire + * Copyright (C) 2024 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.call.usecase + +import com.wire.kalium.logic.data.call.CallMetadata +import com.wire.kalium.logic.data.call.CallRepository +import com.wire.kalium.logic.data.call.CallScreenSharingMetadata +import com.wire.kalium.logic.data.call.RecentlyEndedCallMetadata +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.SelfTeamIdProvider +import com.wire.kalium.logic.data.user.type.UserType +import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase +import com.wire.kalium.logic.functional.getOrNull +import com.wire.kalium.util.DateTimeUtil +import kotlinx.coroutines.flow.first + +/** + * Given a call and raw call end reason create metadata containing all information regarding + * a call. + */ +interface CreateAndPersistRecentlyEndedCallMetadataUseCase { + suspend operator fun invoke(conversationId: ConversationId, callEndedReason: Int) +} + +class CreateAndPersistRecentlyEndedCallMetadataUseCaseImpl internal constructor( + private val callRepository: CallRepository, + private val observeConversationMembers: ObserveConversationMembersUseCase, + private val selfTeamIdProvider: SelfTeamIdProvider, +) : CreateAndPersistRecentlyEndedCallMetadataUseCase { + override suspend fun invoke(conversationId: ConversationId, callEndedReason: Int) { + callRepository.getCallMetadataProfile()[conversationId]?.createMetadata( + conversationId = conversationId, + callEndedReason = callEndedReason + )?.let { metadata -> + callRepository.updateRecentlyEndedCallMetadata(metadata) + } + } + + private suspend fun CallMetadata.createMetadata(conversationId: ConversationId, callEndedReason: Int): RecentlyEndedCallMetadata { + val selfCallUser = getFullParticipants().firstOrNull { participant -> participant.userType == UserType.OWNER } + val conversationMembers = observeConversationMembers(conversationId).first() + val conversationServicesCount = conversationMembers.count { member -> member.user.userType == UserType.SERVICE } + val guestsCount = conversationMembers.count { member -> member.user.userType == UserType.GUEST } + val guestsProCount = conversationMembers.count { member -> member.user.userType == UserType.GUEST && member.user.teamId != null } + val isOutgoingCall = callerId.value == selfCallUser?.id?.value + val callDurationInSeconds = establishedTime?.let { + DateTimeUtil.calculateMillisDifference(it, DateTimeUtil.currentIsoDateTimeString()) / MILLIS_IN_SECOND + } ?: 0L + + return RecentlyEndedCallMetadata( + callEndReason = callEndedReason, + callDetails = RecentlyEndedCallMetadata.CallDetails( + isCallScreenShare = selfCallUser?.isSharingScreen ?: false, + screenShareDurationInSeconds = screenShareMetadata.totalDurationInSeconds(), + callScreenShareUniques = screenShareMetadata.uniqueSharingUsers.size, + isOutgoingCall = isOutgoingCall, + callDurationInSeconds = callDurationInSeconds, + callParticipantsCount = participants.size, + conversationServices = conversationServicesCount, + callAVSwitchToggle = selfCallUser?.isCameraOn ?: false, + callVideoEnabled = isCameraOn + ), + conversationDetails = RecentlyEndedCallMetadata.ConversationDetails( + conversationType = conversationType, + conversationSize = conversationMembers.size, + conversationGuests = guestsCount, + conversationGuestsPro = guestsProCount + ), + isTeamMember = selfTeamIdProvider().getOrNull() != null + ) + } + + private fun CallScreenSharingMetadata.totalDurationInSeconds(): Long { + val now = DateTimeUtil.currentInstant() + val activeScreenSharesDurationInSeconds = + activeScreenShares.values.sumOf { startTime -> DateTimeUtil.calculateMillisDifference(startTime, now) } + + return (activeScreenSharesDurationInSeconds + completedScreenShareDurationInMillis) / MILLIS_IN_SECOND + } + + private companion object { + const val MILLIS_IN_SECOND = 1_000L + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveInCallReactionsUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveInCallReactionsUseCase.kt new file mode 100644 index 00000000000..aaea05e3a9e --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveInCallReactionsUseCase.kt @@ -0,0 +1,39 @@ +/* + * Wire + * Copyright (C) 2024 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.call.usecase + +import com.wire.kalium.logic.data.call.InCallReactionMessage +import com.wire.kalium.logic.data.call.InCallReactionsRepository +import com.wire.kalium.logic.data.id.ConversationId +import kotlinx.coroutines.flow.Flow + +/** + * Observe incoming in-call reactions + */ +interface ObserveInCallReactionsUseCase { + operator fun invoke(conversationId: ConversationId): Flow +} + +internal class ObserveInCallReactionsUseCaseImpl( + private val inCallReactionsRepository: InCallReactionsRepository, +) : ObserveInCallReactionsUseCase { + + override fun invoke(conversationId: ConversationId): Flow { + return inCallReactionsRepository.observeInCallReactions(conversationId) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveRecentlyEndedCallMetadataUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveRecentlyEndedCallMetadataUseCase.kt new file mode 100644 index 00000000000..3f1c717e7a6 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveRecentlyEndedCallMetadataUseCase.kt @@ -0,0 +1,38 @@ +/* + * Wire + * Copyright (C) 2024 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.call.usecase + +import com.wire.kalium.logic.data.call.CallRepository +import com.wire.kalium.logic.data.call.RecentlyEndedCallMetadata +import kotlinx.coroutines.flow.Flow + +/** + * Use case to observe recently ended call metadata. This gives us all metadata assigned to a call. + * Used mainly for analytics. + */ +interface ObserveRecentlyEndedCallMetadataUseCase { + suspend operator fun invoke(): Flow +} + +class ObserveRecentlyEndedCallMetadataUseCaseImpl internal constructor( + private val callRepository: CallRepository, +) : ObserveRecentlyEndedCallMetadataUseCase { + override suspend fun invoke(): Flow { + return callRepository.observeRecentlyEndedCallMetadata() + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/ClientScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/ClientScope.kt index 01b97e5dcb4..c2e37c14161 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/ClientScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/ClientScope.kt @@ -18,6 +18,7 @@ package com.wire.kalium.logic.feature.client +import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.configuration.notification.NotificationTokenRepository import com.wire.kalium.logic.data.auth.verification.SecondFactorVerificationRepository import com.wire.kalium.logic.data.client.ClientRepository @@ -71,7 +72,8 @@ class ClientScope @OptIn(DelicateKaliumApi::class) internal constructor( private val cachedClientIdClearer: CachedClientIdClearer, private val updateSupportedProtocolsAndResolveOneOnOnes: UpdateSupportedProtocolsAndResolveOneOnOnesUseCase, private val registerMLSClientUseCase: RegisterMLSClientUseCase, - private val syncFeatureConfigsUseCase: SyncFeatureConfigsUseCase + private val syncFeatureConfigsUseCase: SyncFeatureConfigsUseCase, + private val userConfigRepository: UserConfigRepository ) { @OptIn(DelicateKaliumApi::class) val register: RegisterClientUseCase @@ -102,7 +104,7 @@ class ClientScope @OptIn(DelicateKaliumApi::class) internal constructor( val deregisterNativePushToken: DeregisterTokenUseCase get() = DeregisterTokenUseCaseImpl(clientRepository, notificationTokenRepository) val mlsKeyPackageCountUseCase: MLSKeyPackageCountUseCase - get() = MLSKeyPackageCountUseCaseImpl(keyPackageRepository, clientIdProvider, keyPackageLimitsProvider) + get() = MLSKeyPackageCountUseCaseImpl(keyPackageRepository, clientIdProvider, keyPackageLimitsProvider, userConfigRepository) val restartSlowSyncProcessForRecoveryUseCase: RestartSlowSyncProcessForRecoveryUseCase get() = RestartSlowSyncProcessForRecoveryUseCaseImpl(slowSyncRepository) val refillKeyPackages: RefillKeyPackagesUseCase diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/IsAllowedToRegisterMLSClientUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/IsAllowedToRegisterMLSClientUseCase.kt index 3feb0cd3b8e..e41f8f92111 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/IsAllowedToRegisterMLSClientUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/IsAllowedToRegisterMLSClientUseCase.kt @@ -21,7 +21,7 @@ package com.wire.kalium.logic.feature.client import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.data.mlspublickeys.MLSPublicKeysRepository import com.wire.kalium.logic.featureFlags.FeatureSupport -import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.functional.getOrElse import com.wire.kalium.logic.functional.isRight import com.wire.kalium.util.DelicateKaliumApi @@ -45,8 +45,8 @@ internal class IsAllowedToRegisterMLSClientUseCaseImpl( ) : IsAllowedToRegisterMLSClientUseCase { override suspend operator fun invoke(): Boolean { - return featureSupport.isMLSSupported && - mlsPublicKeysRepository.getKeys().isRight() && - userConfigRepository.isMLSEnabled().fold({ false }, { isEnabled -> isEnabled }) + return featureSupport.isMLSSupported + && userConfigRepository.isMLSEnabled().getOrElse(false) + && mlsPublicKeysRepository.getKeys().isRight() } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationAssetsLocallyUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationAssetsLocallyUseCase.kt new file mode 100644 index 00000000000..9a545184971 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationAssetsLocallyUseCase.kt @@ -0,0 +1,51 @@ +/* + * Wire + * Copyright (C) 2024 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 + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.asset.AssetRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.MessageRepository +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.flatMap + +interface ClearConversationAssetsLocallyUseCase { + /** + * Clear all conversation assets from local storage + * + * @param conversationId - id of conversation in which assets should be cleared + */ + suspend operator fun invoke(conversationId: ConversationId): Either +} + +internal class ClearConversationAssetsLocallyUseCaseImpl( + private val messageRepository: MessageRepository, + private val assetRepository: AssetRepository +) : ClearConversationAssetsLocallyUseCase { + override suspend fun invoke(conversationId: ConversationId): Either { + return messageRepository.getAllAssetIdsFromConversationId(conversationId) + .flatMap { ids -> + if (ids.isEmpty()) return Either.Right(Unit) + + ids.map { id -> assetRepository.deleteAssetLocally(id) } + .reduce { acc, either -> + acc.flatMap { either } + } + } + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationContentUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationContentUseCase.kt index 27c20d7eeff..d884b86693f 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationContentUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationContentUseCase.kt @@ -66,7 +66,8 @@ internal class ClearConversationContentUseCaseImpl( id = uuid4().toString(), content = MessageContent.Cleared( conversationId = conversationId, - time = DateTimeUtil.currentInstant() + time = DateTimeUtil.currentInstant(), + needToRemoveLocally = false // TODO Handle in upcoming tasks ), // sending the message to clear this conversation conversationId = selfConversationId, 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 0f3be756ff3..7f73c1c6cda 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 @@ -23,6 +23,7 @@ import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.logic.cache.SelfConversationIdProvider import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.configuration.server.ServerConfigRepository +import com.wire.kalium.logic.data.asset.AssetRepository import com.wire.kalium.logic.data.connection.ConnectionRepository import com.wire.kalium.logic.data.conversation.ConversationGroupRepository import com.wire.kalium.logic.data.conversation.ConversationRepository @@ -38,6 +39,7 @@ import com.wire.kalium.logic.data.conversation.folders.ConversationFolderReposit import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.id.QualifiedIdMapper 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.properties.UserPropertyRepository import com.wire.kalium.logic.data.team.TeamRepository @@ -55,6 +57,8 @@ import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCas import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCaseImpl import com.wire.kalium.logic.feature.conversation.folder.ObserveConversationsFromFolderUseCase import com.wire.kalium.logic.feature.conversation.folder.ObserveConversationsFromFolderUseCaseImpl +import com.wire.kalium.logic.feature.conversation.folder.ObserveUserFoldersUseCase +import com.wire.kalium.logic.feature.conversation.folder.ObserveUserFoldersUseCaseImpl import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFavoritesUseCase import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFavoritesUseCaseImpl import com.wire.kalium.logic.feature.conversation.guestroomlink.CanCreatePasswordProtectedLinksUseCase @@ -113,6 +117,8 @@ class ConversationScope internal constructor( private val kaliumLogger: KaliumLogger, private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, private val serverConfigLinks: ServerConfig.Links, + internal val messageRepository: MessageRepository, + internal val assetRepository: AssetRepository, internal val dispatcher: KaliumDispatcher = KaliumDispatcherImpl, ) { @@ -158,7 +164,12 @@ class ConversationScope internal constructor( get() = ObserveIsSelfUserMemberUseCaseImpl(conversationRepository, selfUserId) val observeConversationInteractionAvailabilityUseCase: ObserveConversationInteractionAvailabilityUseCase - get() = ObserveConversationInteractionAvailabilityUseCase(conversationRepository, userRepository) + get() = ObserveConversationInteractionAvailabilityUseCase( + conversationRepository, + selfUserId = selfUserId, + selfClientIdProvider = currentClientIdProvider, + userRepository = userRepository + ) val deleteTeamConversation: DeleteTeamConversationUseCase get() = DeleteTeamConversationUseCaseImpl(selfTeamIdProvider, teamRepository, conversationRepository) @@ -264,6 +275,18 @@ class ConversationScope internal constructor( selfConversationIdProvider ) + val clearConversationAssetsLocally: ClearConversationAssetsLocallyUseCase + get() = ClearConversationAssetsLocallyUseCaseImpl( + messageRepository, + assetRepository + ) + + val deleteConversationLocallyUseCase: DeleteConversationLocallyUseCase + get() = DeleteConversationLocallyUseCaseImpl( + conversationRepository, + clearConversationAssetsLocally + ) + val joinConversationViaCode: JoinConversationViaCodeUseCase get() = JoinConversationViaCodeUseCase(conversationGroupRepository, selfUserId) @@ -361,4 +384,6 @@ class ConversationScope internal constructor( get() = AddConversationToFavoritesUseCaseImpl(conversationFolderRepository) val removeConversationFromFavorites: RemoveConversationFromFavoritesUseCase get() = RemoveConversationFromFavoritesUseCaseImpl(conversationFolderRepository) + val observeUserFolders: ObserveUserFoldersUseCase + get() = ObserveUserFoldersUseCaseImpl(conversationFolderRepository) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCase.kt new file mode 100644 index 00000000000..aaf07b999b3 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCase.kt @@ -0,0 +1,48 @@ +/* + * Wire + * Copyright (C) 2024 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 + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.flatMap + +interface DeleteConversationLocallyUseCase { + /** + * Delete local conversation which: + * - Clear all local assets + * - Clear content + * - Remove conversation + * + * @param conversationId - id of conversation to delete + */ + suspend operator fun invoke(conversationId: ConversationId): Either +} + +internal class DeleteConversationLocallyUseCaseImpl( + private val conversationRepository: ConversationRepository, + private val clearLocalConversationAssets: ClearConversationAssetsLocallyUseCase +) : DeleteConversationLocallyUseCase { + + override suspend fun invoke(conversationId: ConversationId): Either { + return clearLocalConversationAssets(conversationId) + .flatMap { conversationRepository.clearContent(conversationId) } + .flatMap { conversationRepository.deleteConversationLocally(conversationId) } + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/GetOrCreateOneToOneConversationUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/GetOrCreateOneToOneConversationUseCase.kt index 7597da0ddb3..a601c05c4e8 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/GetOrCreateOneToOneConversationUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/GetOrCreateOneToOneConversationUseCase.kt @@ -33,10 +33,6 @@ import kotlinx.coroutines.flow.first /** * Operation that creates one-to-one Conversation with specific [UserId] (only if it is absent in local DB) * and returns [Conversation] data. - * - * @param otherUserId [UserId] private conversation with which we are interested in. - * @return Result with [Conversation] in case of success, or [CoreFailure] if something went wrong: - * can't get data from local DB, or can't create a conversation. */ interface GetOrCreateOneToOneConversationUseCase { suspend operator fun invoke(otherUserId: UserId): CreateConversationResult @@ -47,6 +43,14 @@ internal class GetOrCreateOneToOneConversationUseCaseImpl( private val userRepository: UserRepository, private val oneOnOneResolver: OneOnOneResolver ) : GetOrCreateOneToOneConversationUseCase { + + /** + * The use case operation operation params and return type. + * + * @param otherUserId [UserId] private conversation with which we are interested in. + * @return Result with [Conversation] in case of success, or [CoreFailure] if something went wrong: + * can't get data from local DB, or can't create a conversation. + */ override suspend operator fun invoke(otherUserId: UserId): CreateConversationResult { // TODO periodically re-resolve one-on-one return conversationRepository.observeOneToOneConversationWithOtherUser(otherUserId) @@ -66,6 +70,18 @@ internal class GetOrCreateOneToOneConversationUseCaseImpl( }) } + /** + * Resolves one-on-one conversation with the user. + * Resolving conversations is the process of: + * + * - Intersecting the supported protocols of the self user and the other user. + * - Selecting the common protocol, based on the team settings with the highest priority. + * - Get or create a conversation with the other user. + * - If the protocol now is MLS, migrate the existing Proteus conversation to MLS. + * - Mark the conversation as active. + * + * If no common protocol is found, and we have existing Proteus conversations, we do best effort to use them as fallback. + */ private suspend fun resolveOneOnOneConversationWithUser(otherUserId: UserId): Either = userRepository.userById(otherUserId).flatMap { otherUser -> // TODO support lazily establishing mls group for team 1-1 diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationInteractionAvailabilityUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationInteractionAvailabilityUseCase.kt index 9dab4f5fa45..8d25d6c82d2 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationInteractionAvailabilityUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationInteractionAvailabilityUseCase.kt @@ -25,15 +25,18 @@ import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.conversation.InteractionAvailability import com.wire.kalium.logic.data.conversation.interactionAvailability import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.message.MessageContent -import com.wire.kalium.logic.data.user.SelfUser -import com.wire.kalium.logic.data.user.SupportedProtocol +import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository +import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.functional.getOrElse +import com.wire.kalium.logic.kaliumLogger import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext @@ -48,6 +51,8 @@ import kotlinx.coroutines.withContext class ObserveConversationInteractionAvailabilityUseCase internal constructor( private val conversationRepository: ConversationRepository, private val userRepository: UserRepository, + private val selfUserId: UserId, + private val selfClientIdProvider: CurrentClientIdProvider, private val dispatcher: KaliumDispatcher = KaliumDispatcherImpl, ) { @@ -56,13 +61,21 @@ class ObserveConversationInteractionAvailabilityUseCase internal constructor( * @return an [IsInteractionAvailableResult] containing Success or Failure cases */ suspend operator fun invoke(conversationId: ConversationId): Flow = withContext(dispatcher.io) { - conversationRepository.observeConversationDetailsById(conversationId).combine( - userRepository.observeSelfUser() - ) { conversation, selfUser -> - conversation to selfUser - }.map { (eitherConversation, selfUser) -> + + val isSelfClientMlsCapable = selfClientIdProvider().flatMap { + userRepository.isClientMlsCapable(selfUserId, it) + }.getOrElse { + return@withContext flow { IsInteractionAvailableResult.Failure(it) } + } + + kaliumLogger.withTextTag("ObserveConversationInteractionAvailabilityUseCase").d("isSelfClientMlsCapable $isSelfClientMlsCapable") + + conversationRepository.observeConversationDetailsById(conversationId).map { eitherConversation -> eitherConversation.fold({ failure -> IsInteractionAvailableResult.Failure(failure) }, { conversationDetails -> - val isProtocolSupported = doesUserSupportConversationProtocol(conversationDetails, selfUser) + val isProtocolSupported = doesUserSupportConversationProtocol( + conversationDetails = conversationDetails, + isSelfClientMlsCapable = isSelfClientMlsCapable + ) if (!isProtocolSupported) { // short-circuit to Unsupported Protocol if it's the case return@fold IsInteractionAvailableResult.Success(InteractionAvailability.UNSUPPORTED_PROTOCOL) } @@ -74,19 +87,12 @@ class ObserveConversationInteractionAvailabilityUseCase internal constructor( private fun doesUserSupportConversationProtocol( conversationDetails: ConversationDetails, - selfUser: SelfUser - ): Boolean { - val protocolInfo = conversationDetails.conversation.protocol - val acceptableProtocols = when (protocolInfo) { - is Conversation.ProtocolInfo.MLS -> setOf(SupportedProtocol.MLS) - // Messages in mixed conversations are sent through Proteus - is Conversation.ProtocolInfo.Mixed -> setOf(SupportedProtocol.PROTEUS) - Conversation.ProtocolInfo.Proteus -> setOf(SupportedProtocol.PROTEUS) - } - val isProtocolSupported = selfUser.supportedProtocols?.any { supported -> - acceptableProtocols.contains(supported) - } ?: false - return isProtocolSupported + isSelfClientMlsCapable: Boolean + ): Boolean = when (conversationDetails.conversation.protocol) { + is Conversation.ProtocolInfo.MLS -> isSelfClientMlsCapable + // Messages in mixed conversations are sent through Proteus + is Conversation.ProtocolInfo.Mixed, + Conversation.ProtocolInfo.Proteus -> true } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsWithEventsUseCaseImpl.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsWithEventsUseCaseImpl.kt index 4f7f9529f61..5a3b59405e8 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsWithEventsUseCaseImpl.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsWithEventsUseCaseImpl.kt @@ -45,16 +45,26 @@ internal class ObserveConversationListDetailsWithEventsUseCaseImpl( fromArchive: Boolean, conversationFilter: ConversationFilter ): Flow> { - return if (conversationFilter == ConversationFilter.FAVORITES) { - when (val result = getFavoriteFolder()) { - GetFavoriteFolderUseCase.Result.Failure -> { - flowOf(emptyList()) + return when (conversationFilter) { + ConversationFilter.Favorites -> { + when (val result = getFavoriteFolder()) { + GetFavoriteFolderUseCase.Result.Failure -> { + flowOf(emptyList()) + } + + is GetFavoriteFolderUseCase.Result.Success -> + conversationFolderRepository.observeConversationsFromFolder(result.folder.id) } + } - is GetFavoriteFolderUseCase.Result.Success -> conversationFolderRepository.observeConversationsFromFolder(result.folder.id) + is ConversationFilter.Folder -> { + conversationFolderRepository.observeConversationsFromFolder(conversationFilter.folderId) } - } else { - conversationRepository.observeConversationListDetailsWithEvents(fromArchive, conversationFilter) + + ConversationFilter.All, + ConversationFilter.Groups, + ConversationFilter.OneOnOne -> + conversationRepository.observeConversationListDetailsWithEvents(fromArchive, conversationFilter) } } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCase.kt index b0ffa60fa93..549b951c5fc 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCase.kt @@ -19,7 +19,10 @@ package com.wire.kalium.logic.feature.conversation.folder import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn /** * This use case will observe and return the list of conversations from given folder. @@ -31,9 +34,10 @@ fun interface ObserveConversationsFromFolderUseCase { internal class ObserveConversationsFromFolderUseCaseImpl( private val conversationFolderRepository: ConversationFolderRepository, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl ) : ObserveConversationsFromFolderUseCase { - override suspend operator fun invoke(folderId: String): Flow> { - return conversationFolderRepository.observeConversationsFromFolder(folderId) - } + override suspend operator fun invoke(folderId: String): Flow> = + conversationFolderRepository.observeConversationsFromFolder(folderId) + .flowOn(dispatchers.io) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCase.kt new file mode 100644 index 00000000000..3935cd2af28 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCase.kt @@ -0,0 +1,46 @@ +/* + * Wire + * Copyright (C) 2024 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.folder + +import com.wire.kalium.logic.data.conversation.ConversationFolder +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.functional.mapToRightOr +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn + +/** + * This use case will observe and return the list of all user folders. + * @see ConversationFolder + */ +fun interface ObserveUserFoldersUseCase { + suspend operator fun invoke(): Flow> +} + +internal class ObserveUserFoldersUseCaseImpl( + private val conversationFolderRepository: ConversationFolderRepository, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl +) : ObserveUserFoldersUseCase { + + override suspend operator fun invoke(): Flow> { + return conversationFolderRepository.observeUserFolders() + .mapToRightOr(emptyList()) + .flowOn(dispatchers.io) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigrator.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigrator.kt index 5737853c8eb..2022a62263f 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigrator.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigrator.kt @@ -36,7 +36,19 @@ import com.wire.kalium.logic.functional.map import com.wire.kalium.logic.kaliumLogger interface OneOnOneMigrator { + /** + * Migrates the user's one-on-one Proteus. Without creating a new one since MLS is the default, marking it as active. + */ + suspend fun migrateExistingProteus(user: OtherUser): Either + + /** + * Get one-on-one conversation with the user, if not found, create a new one (Proteus still default) and mark it as active. + */ suspend fun migrateToProteus(user: OtherUser): Either + + /** + * Perform migration of Proteus to MLS keeping history and marking the new conversation as active. + */ suspend fun migrateToMLS(user: OtherUser): Either } @@ -100,19 +112,35 @@ internal class OneOnOneMigratorImpl( } } + override suspend fun migrateExistingProteus(user: OtherUser): Either = + conversationRepository.getOneOnOneConversationsWithOtherUser(user.id, Conversation.Protocol.PROTEUS).flatMap { conversationIds -> + if (conversationIds.isNotEmpty()) { + val conversationId = conversationIds.first() + Either.Right(conversationId) + } else { + Either.Left(StorageFailure.DataNotFound) + } + }.flatMap { conversationId -> + if (user.activeOneOnOneConversationId != conversationId) { + kaliumLogger.d("resolved existing one-on-one to proteus, user = ${user.id.toLogString()}") + userRepository.updateActiveOneOnOneConversation(user.id, conversationId) + } + Either.Right(conversationId) + } + private suspend fun migrateOneOnOneHistory(user: OtherUser, targetConversation: ConversationId): Either { - return conversationRepository.getOneOnOneConversationsWithOtherUser( - otherUserId = user.id, - protocol = Conversation.Protocol.PROTEUS - ).flatMap { proteusOneOnOneConversations -> - // We can theoretically have more than one proteus 1-1 conversation with - // team members since there was no backend safeguards against this - proteusOneOnOneConversations.foldToEitherWhileRight(Unit) { proteusOneOnOneConversation, _ -> - messageRepository.moveMessagesToAnotherConversation( - originalConversation = proteusOneOnOneConversation, - targetConversation = targetConversation - ) - } + return conversationRepository.getOneOnOneConversationsWithOtherUser( + otherUserId = user.id, + protocol = Conversation.Protocol.PROTEUS + ).flatMap { proteusOneOnOneConversations -> + // We can theoretically have more than one proteus 1-1 conversation with + // team members since there was no backend safeguards against this + proteusOneOnOneConversations.foldToEitherWhileRight(Unit) { proteusOneOnOneConversation, _ -> + messageRepository.moveMessagesToAnotherConversation( + originalConversation = proteusOneOnOneConversation, + targetConversation = targetConversation + ) } + } } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneResolver.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneResolver.kt index c9ba938d312..a136f97107c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneResolver.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneResolver.kt @@ -31,7 +31,9 @@ import com.wire.kalium.logic.feature.protocol.OneOnOneProtocolSelector import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.flatMapLeft +import com.wire.kalium.logic.functional.fold import com.wire.kalium.logic.functional.foldToEitherWhileRight +import com.wire.kalium.logic.functional.left import com.wire.kalium.logic.functional.map import com.wire.kalium.logic.kaliumLogger import com.wire.kalium.util.KaliumDispatcher @@ -154,11 +156,18 @@ internal class OneOnOneResolverImpl( if (invalidateCurrentKnownProtocols) { userRepository.fetchUsersByIds(setOf(user.id)) } - return oneOnOneProtocolSelector.getProtocolForUser(user.id).flatMap { supportedProtocol -> + return oneOnOneProtocolSelector.getProtocolForUser(user.id).fold({ coreFailure -> + if (coreFailure is CoreFailure.NoCommonProtocolFound.OtherNeedToUpdate) { + kaliumLogger.i("Resolving existing proteus 1:1 as not matching protocol found with: ${user.id.toLogString()}") + oneOnOneMigrator.migrateExistingProteus(user) + } else { + coreFailure.left() + } + }, { supportedProtocol -> when (supportedProtocol) { SupportedProtocol.PROTEUS -> oneOnOneMigrator.migrateToProteus(user) SupportedProtocol.MLS -> oneOnOneMigrator.migrateToMLS(user) } - } + }) } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ChangeProfilingUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ChangeProfilingUseCase.kt new file mode 100644 index 00000000000..83588fea472 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ChangeProfilingUseCase.kt @@ -0,0 +1,32 @@ +/* + * Wire + * Copyright (C) 2024 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.debug + +import com.wire.kalium.logic.di.UserStorage + +class ChangeProfilingUseCase( + private val userStorage: UserStorage, +) { + /** + * Changes the profiling of the database (cipher_profile) if the profile is specified and the database is encrypted + * @param enabled true to enable profiling, false to disable + */ + operator fun invoke(enabled: Boolean) { + userStorage.database.changeProfiling(enabled) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt index 92d7176faaf..34b31cdf4b7 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt @@ -40,6 +40,7 @@ import com.wire.kalium.logic.data.prekey.PreKeyRepository import com.wire.kalium.logic.data.sync.SlowSyncRepository import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository +import com.wire.kalium.logic.di.UserStorage import com.wire.kalium.logic.feature.message.MLSMessageCreator import com.wire.kalium.logic.feature.message.MLSMessageCreatorImpl import com.wire.kalium.logic.feature.message.MessageEnvelopeCreator @@ -92,6 +93,7 @@ class DebugScope internal constructor( private val legalHoldHandler: LegalHoldHandler, private val notificationTokenRepository: NotificationTokenRepository, private val scope: CoroutineScope, + userStorage: UserStorage, logger: KaliumLogger, internal val dispatcher: KaliumDispatcher = KaliumDispatcherImpl, ) { @@ -224,4 +226,6 @@ class DebugScope internal constructor( clientRepository, notificationTokenRepository, ) + + val changeProfiling: ChangeProfilingUseCase = ChangeProfilingUseCase(userStorage) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/incallreaction/SendInCallReactionUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/incallreaction/SendInCallReactionUseCase.kt new file mode 100644 index 00000000000..8b57a9735ef --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/incallreaction/SendInCallReactionUseCase.kt @@ -0,0 +1,70 @@ +/* + * Wire + * Copyright (C) 2024 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.incallreaction + +import com.benasher44.uuid.uuid4 +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.message.Message +import com.wire.kalium.logic.data.message.MessageContent +import com.wire.kalium.logic.feature.message.MessageSender +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.flatMap +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.datetime.Clock + +/** + * Sends in-call reaction to the call with the conversationId + */ +class SendInCallReactionUseCase( + private val selfUserId: QualifiedID, + private val provideClientId: CurrentClientIdProvider, + private val messageSender: MessageSender, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl, + private val scope: CoroutineScope +) { + + suspend operator fun invoke(conversationId: ConversationId, reaction: String): Either = + scope.async(dispatchers.io) { + + val generatedMessageUuid = uuid4().toString() + + provideClientId().flatMap { clientId -> + val message = Message.Signaling( + id = generatedMessageUuid, + content = MessageContent.InCallEmoji( + emojis = mapOf(reaction to 1) + ), + conversationId = conversationId, + date = Clock.System.now(), + senderUserId = selfUserId, + senderClientId = clientId, + status = Message.Status.Pending, + isSelfMessage = true, + expirationData = null, + ) + + messageSender.sendMessage(message) + } + }.await() +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/keypackage/MLSKeyPackageCountUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/keypackage/MLSKeyPackageCountUseCase.kt index 46647c3ed64..ab4528d6f9a 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/keypackage/MLSKeyPackageCountUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/keypackage/MLSKeyPackageCountUseCase.kt @@ -20,11 +20,13 @@ package com.wire.kalium.logic.feature.keypackage import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.keypackage.KeyPackageLimitsProvider import com.wire.kalium.logic.data.keypackage.KeyPackageRepository import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.functional.getOrElse /** * This use case will return the current number of key packages. @@ -37,6 +39,7 @@ internal class MLSKeyPackageCountUseCaseImpl( private val keyPackageRepository: KeyPackageRepository, private val currentClientIdProvider: CurrentClientIdProvider, private val keyPackageLimitsProvider: KeyPackageLimitsProvider, + private val userConfigRepository: UserConfigRepository ) : MLSKeyPackageCountUseCase { override suspend operator fun invoke(fromAPI: Boolean): MLSKeyPackageCountResult = when (fromAPI) { @@ -47,10 +50,15 @@ internal class MLSKeyPackageCountUseCaseImpl( private suspend fun validKeyPackagesCountFromAPI() = currentClientIdProvider().fold({ MLSKeyPackageCountResult.Failure.FetchClientIdFailure(it) }, { selfClient -> - keyPackageRepository.getAvailableKeyPackageCount(selfClient).fold( - { - MLSKeyPackageCountResult.Failure.NetworkCallFailure(it) - }, { MLSKeyPackageCountResult.Success(selfClient, it.count, keyPackageLimitsProvider.needsRefill(it.count)) }) + if (userConfigRepository.isMLSEnabled().getOrElse(false)) { + keyPackageRepository.getAvailableKeyPackageCount(selfClient) + .fold( + { MLSKeyPackageCountResult.Failure.NetworkCallFailure(it) }, + { MLSKeyPackageCountResult.Success(selfClient, it.count, keyPackageLimitsProvider.needsRefill(it.count)) } + ) + } else { + MLSKeyPackageCountResult.Failure.NotEnabled + } }) private suspend fun validKeyPackagesCountFromMLSClient() = @@ -70,6 +78,7 @@ sealed class MLSKeyPackageCountResult { sealed class Failure : MLSKeyPackageCountResult() { class NetworkCallFailure(val networkFailure: NetworkFailure) : Failure() class FetchClientIdFailure(val genericFailure: CoreFailure) : Failure() + data object NotEnabled : Failure() data class Generic(val genericFailure: CoreFailure) : Failure() } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetNextAudioMessageInConversationUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetNextAudioMessageInConversationUseCase.kt new file mode 100644 index 00000000000..78071f16970 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetNextAudioMessageInConversationUseCase.kt @@ -0,0 +1,53 @@ +/* + * Wire + * Copyright (C) 2024 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.message + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.MessageRepository +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.withContext + +/** + * Provides a way to get a messageId of next AudioMessage after [messageId] in [ConversationId] conversation. + */ +class GetNextAudioMessageInConversationUseCase internal constructor( + private val messageRepository: MessageRepository, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl +) { + suspend operator fun invoke( + conversationId: ConversationId, + messageId: String + ): Result = withContext(dispatchers.io) { + messageRepository.getNextAudioMessageInConversation(conversationId, messageId) + .fold({ Result.Failure(it) }, { Result.Success(it) }) + } + + sealed interface Result { + + data class Success(val messageId: String) : Result + + /** + * [StorageFailure.DataNotFound] in case there is no AudioMessage or some other generic error. + */ + data class Failure(val cause: CoreFailure) : Result + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCase.kt new file mode 100644 index 00000000000..276798ea6d3 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCase.kt @@ -0,0 +1,57 @@ +/* + * Wire + * Copyright (C) 2024 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.message + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.MessageRepository +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.withContext + +/** + * Provides a way to get a name of user that sent a message + * using its [ConversationId] and message ID coordinates. + */ +class GetSenderNameByMessageIdUseCase internal constructor( + private val messageRepository: MessageRepository, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl +) { + suspend operator fun invoke( + conversationId: ConversationId, + messageId: String + ): Result = withContext(dispatchers.io) { + messageRepository.getSenderNameByMessageId(conversationId, messageId).fold({ + Result.Failure(it) + }, { + Result.Success(it) + }) + } + + sealed interface Result { + + data class Success(val name: String) : Result + + /** + * [StorageFailure.DataNotFound] or some other generic error. + */ + data class Failure(val cause: CoreFailure) : Result + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt index bb49c7fda27..59566d18737 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt @@ -62,6 +62,7 @@ import com.wire.kalium.logic.feature.asset.UpdateAssetMessageTransferStatusUseCa import com.wire.kalium.logic.feature.asset.UpdateAssetMessageTransferStatusUseCaseImpl import com.wire.kalium.logic.feature.asset.ValidateAssetFileTypeUseCase import com.wire.kalium.logic.feature.asset.ValidateAssetFileTypeUseCaseImpl +import com.wire.kalium.logic.feature.incallreaction.SendInCallReactionUseCase import com.wire.kalium.logic.feature.message.composite.SendButtonActionConfirmationMessageUseCase import com.wire.kalium.logic.feature.message.composite.SendButtonActionMessageUseCase import com.wire.kalium.logic.feature.message.composite.SendButtonMessageUseCase @@ -453,4 +454,19 @@ class MessageScope internal constructor( val removeMessageDraftUseCase: RemoveMessageDraftUseCase get() = RemoveMessageDraftUseCaseImpl(messageDraftRepository) + + val sendInCallReactionUseCase: SendInCallReactionUseCase + get() = SendInCallReactionUseCase( + selfUserId = selfUserId, + provideClientId = currentClientIdProvider, + messageSender = messageSender, + dispatchers = dispatcher, + scope = scope, + ) + + val getSenderNameByMessageId: GetSenderNameByMessageIdUseCase + get() = GetSenderNameByMessageIdUseCase(messageRepository) + + val getNextAudioMessageInConversation: GetNextAudioMessageInConversationUseCase + get() = GetNextAudioMessageInConversationUseCase(messageRepository) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageSendFailureHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageSendFailureHandler.kt index 1ee884d653e..0a6ad334583 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageSendFailureHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageSendFailureHandler.kt @@ -111,7 +111,7 @@ class MessageSendFailureHandlerImpl internal constructor( private suspend fun syncUserIds(userId: Set): Either { return if (userId.isEmpty()) Either.Right(Unit) - else userRepository.fetchUsersByIds(userId) + else userRepository.fetchUsersByIds(userId).map { } } private suspend fun addMissingClients(missingClients: Map>): Either = diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/PersistMigratedMessagesUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/PersistMigratedMessagesUseCase.kt index ce4b80f2ee5..fd5b3509a21 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/PersistMigratedMessagesUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/PersistMigratedMessagesUseCase.kt @@ -212,5 +212,6 @@ internal class PersistMigratedMessagesUseCaseImpl( is MessageContent.ButtonActionConfirmation -> MessageEntity.Visibility.HIDDEN is MessageContent.Location -> MessageEntity.Visibility.VISIBLE is MessageContent.DataTransfer -> MessageEntity.Visibility.HIDDEN + is MessageContent.InCallEmoji -> MessageEntity.Visibility.HIDDEN } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/protocol/OneOnOneProtocolSelector.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/protocol/OneOnOneProtocolSelector.kt index 64ca6ef2bf8..ee4b335a197 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/protocol/OneOnOneProtocolSelector.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/protocol/OneOnOneProtocolSelector.kt @@ -24,7 +24,8 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap -import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.functional.getOrNull +import com.wire.kalium.logic.kaliumLogger internal interface OneOnOneProtocolSelector { suspend fun getProtocolForUser(userId: UserId): Either @@ -41,13 +42,17 @@ internal class OneOnOneProtocolSelectorImpl( return@flatMap Either.Left(CoreFailure.Unknown(error)) } + val teamDefaultProtocol = userConfigRepository.getDefaultProtocol().getOrNull() val selfUserProtocols = selfUser.supportedProtocols.orEmpty() val otherUserProtocols = otherUser.supportedProtocols.orEmpty() - val commonProtocols = userConfigRepository.getDefaultProtocol().fold({ - selfUserProtocols.intersect(otherUserProtocols) - }, { - selfUserProtocols.intersect(listOf(it).toSet()).intersect(otherUserProtocols) - }) + val commonProtocols = selfUserProtocols.intersect(otherUserProtocols) + + kaliumLogger.withTextTag(TAG).d( + "teamDefaultProtocol = $teamDefaultProtocol, " + + "selfUserProtocols = $selfUserProtocols, " + + "otherUserProtocols = $otherUserProtocols, " + + "commonProtocols = $commonProtocols" + ) return when { commonProtocols.contains(SupportedProtocol.MLS) -> Either.Right(SupportedProtocol.MLS) @@ -56,4 +61,8 @@ internal class OneOnOneProtocolSelectorImpl( else -> Either.Left(CoreFailure.NoCommonProtocolFound.SelfNeedToUpdate) } } + + private companion object { + const val TAG = "OneOnOneProtocolSelector" + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchScope.kt index bb5c11a9ff9..6581e818f7c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchScope.kt @@ -36,7 +36,7 @@ class SearchScope internal constructor( private val kaliumConfigs: KaliumConfigs ) { val searchUsers: SearchUsersUseCase - get() = SearchUsersUseCase( + get() = SearchUsersUseCaseImpl( searchUserRepository, selfUserId, kaliumConfigs.maxRemoteSearchResultCount diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchUsersUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchUsersUseCase.kt index 74b27af6a65..b9cd963b41b 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchUsersUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchUsersUseCase.kt @@ -30,16 +30,26 @@ import kotlinx.coroutines.coroutineScope /** * Use case for searching users. - * @param searchQuery The search query. - * @param excludingMembersOfConversation The conversation to exclude its members from the search. - * @param customDomain The custom domain to search in if null the search will be on the self user domain. */ -class SearchUsersUseCase internal constructor( +interface SearchUsersUseCase { + /** + * @param searchQuery The search query. + * @param excludingMembersOfConversation The conversation to exclude its members from the search. + * @param customDomain The custom domain to search in if null the search will be on the self user domain. + */ + suspend operator fun invoke( + searchQuery: String, + excludingMembersOfConversation: ConversationId?, + customDomain: String? + ): SearchUserResult +} + +class SearchUsersUseCaseImpl internal constructor( private val searchUserRepository: SearchUserRepository, private val selfUserId: UserId, private val maxRemoteSearchResultCount: Int -) { - suspend operator fun invoke( +) : SearchUsersUseCase { + override suspend operator fun invoke( searchQuery: String, excludingMembersOfConversation: ConversationId?, customDomain: String? diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/ObserveUserInfoUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/ObserveUserInfoUseCase.kt index 5688d266312..404110e096f 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/ObserveUserInfoUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/ObserveUserInfoUseCase.kt @@ -77,7 +77,15 @@ internal class ObserveUserInfoUseCaseImpl( either.fold({ storageFailure -> if (storageFailure is StorageFailure.DataNotFound) { userRepository.fetchUsersByIds(setOf(userId)) - .fold({ ObserveOtherUserResult(fetchUserError = it) }) { ObserveOtherUserResult() } + .fold({ ObserveOtherUserResult(fetchUserError = it) }) { usersFound -> + if (usersFound) { + // Fetched users are persisted + ObserveOtherUserResult() + } else { + // Users cannot be found + ObserveOtherUserResult(fetchUserError = StorageFailure.DataNotFound) + } + } } else { ObserveOtherUserResult(getKnownUserError = storageFailure) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt index 9bae507ae54..7b5552e66fc 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt @@ -75,6 +75,7 @@ import com.wire.kalium.logic.feature.publicuser.GetAllContactsUseCaseImpl import com.wire.kalium.logic.feature.publicuser.GetKnownUserUseCase import com.wire.kalium.logic.feature.publicuser.GetKnownUserUseCaseImpl import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase +import com.wire.kalium.logic.feature.server.GetTeamUrlUseCase import com.wire.kalium.logic.feature.user.readReceipts.ObserveReadReceiptsEnabledUseCase import com.wire.kalium.logic.feature.user.readReceipts.ObserveReadReceiptsEnabledUseCaseImpl import com.wire.kalium.logic.feature.user.readReceipts.PersistReadReceiptsStatusConfigUseCase @@ -117,7 +118,8 @@ class UserScope internal constructor( private val selfTeamIdProvider: SelfTeamIdProvider, private val checkRevocationList: RevocationListChecker, private val syncFeatureConfigs: SyncFeatureConfigsUseCase, - private val userScopedLogger: KaliumLogger + private val userScopedLogger: KaliumLogger, + private val teamUrlUseCase: GetTeamUrlUseCase ) { private val validateUserHandleUseCase: ValidateUserHandleUseCase get() = ValidateUserHandleUseCaseImpl() val getSelfUser: GetSelfUserUseCase get() = GetSelfUserUseCaseImpl(userRepository) @@ -127,6 +129,7 @@ class UserScope internal constructor( val getPublicAsset: GetAvatarAssetUseCase get() = GetAvatarAssetUseCaseImpl(assetRepository, userRepository) val enrollE2EI: EnrollE2EIUseCase get() = EnrollE2EIUseCaseImpl(e2EIRepository) + val getTeamUrl: GetTeamUrlUseCase get() = teamUrlUseCase val finalizeMLSClientAfterE2EIEnrollment: FinalizeMLSClientAfterE2EIEnrollment get() = FinalizeMLSClientAfterE2EIEnrollmentImpl( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCase.kt index 5f2ca1c35bf..4911063abdc 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCase.kt @@ -22,6 +22,7 @@ import com.wire.kalium.logic.data.id.TeamId import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.data.user.UserRepository +import com.wire.kalium.logic.feature.user.SyncContactsUseCase import com.wire.kalium.logic.functional.fold import com.wire.kalium.network.exceptions.KaliumException @@ -54,6 +55,7 @@ sealed class MigrateFromPersonalToTeamFailure { internal class MigrateFromPersonalToTeamUseCaseImpl internal constructor( private val selfUserId: UserId, private val userRepository: UserRepository, + private val syncContacts: SyncContactsUseCase, private val invalidateTeamId: () -> Unit ) : MigrateFromPersonalToTeamUseCase { override suspend operator fun invoke( @@ -94,7 +96,8 @@ internal class MigrateFromPersonalToTeamUseCaseImpl internal constructor( } }, { user -> userRepository.updateTeamId(selfUserId, TeamId(user.teamId)) - invalidateTeamId() + invalidateTeamId() + syncContacts() MigrateFromPersonalToTeamResult.Success }) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ApplicationMessageHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ApplicationMessageHandler.kt index 08f6fc47213..abbf425aa7e 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ApplicationMessageHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ApplicationMessageHandler.kt @@ -19,6 +19,7 @@ package com.wire.kalium.logic.sync.receiver.conversation.message import com.wire.kalium.logger.KaliumLogger.Companion.ApplicationFlow +import com.wire.kalium.logic.data.call.InCallReactionsRepository import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.AssetContent @@ -90,7 +91,8 @@ internal class ApplicationMessageHandlerImpl( private val receiptMessageHandler: ReceiptMessageHandler, private val buttonActionConfirmationHandler: ButtonActionConfirmationHandler, private val dataTransferEventHandler: DataTransferEventHandler, - private val selfUserId: UserId + private val inCallReactionsRepository: InCallReactionsRepository, + private val selfUserId: UserId, ) : ApplicationMessageHandler { private val logger by lazy { kaliumLogger.withFeatureId(ApplicationFlow.EVENT_RECEIVER) } @@ -106,18 +108,12 @@ internal class ApplicationMessageHandlerImpl( when (val protoContent = content.messageContent) { is MessageContent.Regular -> { val visibility = when (protoContent) { - is MessageContent.DeleteMessage -> Message.Visibility.HIDDEN - is MessageContent.TextEdited -> Message.Visibility.HIDDEN - is MessageContent.DeleteForMe -> Message.Visibility.HIDDEN is MessageContent.Unknown -> if (protoContent.hidden) Message.Visibility.HIDDEN else Message.Visibility.VISIBLE is MessageContent.Text -> Message.Visibility.VISIBLE - is MessageContent.Calling -> Message.Visibility.VISIBLE is MessageContent.Asset -> Message.Visibility.VISIBLE is MessageContent.Knock -> Message.Visibility.VISIBLE is MessageContent.RestrictedAsset -> Message.Visibility.VISIBLE is MessageContent.FailedDecryption -> Message.Visibility.VISIBLE - is MessageContent.LastRead -> Message.Visibility.HIDDEN - is MessageContent.Cleared -> Message.Visibility.HIDDEN is MessageContent.Composite -> Message.Visibility.VISIBLE is MessageContent.Location -> Message.Visibility.VISIBLE } @@ -222,6 +218,11 @@ internal class ApplicationMessageHandlerImpl( ) is MessageContent.DataTransfer -> dataTransferEventHandler.handle(signaling, content) + is MessageContent.InCallEmoji -> inCallReactionsRepository.addInCallReaction( + conversationId = signaling.conversationId, + senderUserId = signaling.senderUserId, + emojis = content.emojis.keys, + ) } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/MLSMessageFailureHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/MLSMessageFailureHandler.kt index 88f29b61b65..f1f5998a491 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/MLSMessageFailureHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/MLSMessageFailureHandler.kt @@ -43,6 +43,7 @@ internal object MLSMessageFailureHandler { is MLSFailure.StaleCommit -> MLSMessageFailureResolution.Ignore is MLSFailure.MessageEpochTooOld -> MLSMessageFailureResolution.Ignore is MLSFailure.InternalErrors -> MLSMessageFailureResolution.Ignore + is MLSFailure.Disabled -> MLSMessageFailureResolution.Ignore else -> MLSMessageFailureResolution.InformUser } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/handler/ClearConversationContentHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/handler/ClearConversationContentHandler.kt index 83402b724f5..044e6efe9df 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/handler/ClearConversationContentHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/handler/ClearConversationContentHandler.kt @@ -41,11 +41,18 @@ internal class ClearConversationContentHandlerImpl( message: Message.Signaling, messageContent: MessageContent.Cleared ) { - val isMessageComingFromOtherClient = message.senderUserId == selfUserId + val isMessageComingFromAnotherUser = message.senderUserId != selfUserId val isMessageDestinedForSelfConversation: Boolean = isMessageSentInSelfConversation(message) - if (isMessageComingFromOtherClient && isMessageDestinedForSelfConversation) { - conversationRepository.clearContent(messageContent.conversationId) + if (isMessageComingFromAnotherUser) { + when { + !messageContent.needToRemoveLocally && !isMessageDestinedForSelfConversation -> return + messageContent.needToRemoveLocally && !isMessageDestinedForSelfConversation -> conversationRepository.deleteConversation( + messageContent.conversationId + ) + + else -> conversationRepository.clearContent(messageContent.conversationId) + } } } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/InCallReactionsRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/InCallReactionsRepositoryTest.kt new file mode 100644 index 00000000000..ade513b3dc2 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/InCallReactionsRepositoryTest.kt @@ -0,0 +1,43 @@ +/* + * Wire + * Copyright (C) 2024 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.call + +import app.cash.turbine.test +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.framework.TestUser +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertEquals + +class InCallReactionsRepositoryTest { + @Test + fun whenNewReactionIsAdded_thenRepositoryEmitsNewReactionMessage() = runBlocking { + + // given + val repository: InCallReactionsRepository = InCallReactionsDataSource() + + repository.observeInCallReactions(TestConversation.id()).test { + + // when + repository.addInCallReaction(TestConversation.id(), TestUser.USER_ID, setOf("1")) + + // then + assertEquals(InCallReactionMessage(TestConversation.id(), TestUser.USER_ID, setOf("1")), awaitItem()) + } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/client/MLSClientProviderTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/client/MLSClientProviderTest.kt index fb5796ed7bf..e3b938cb0ed 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/client/MLSClientProviderTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/client/MLSClientProviderTest.kt @@ -17,6 +17,7 @@ */ package com.wire.kalium.logic.data.client +import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.featureConfig.FeatureConfigTest import com.wire.kalium.logic.data.featureConfig.MLSModel @@ -32,12 +33,15 @@ import com.wire.kalium.logic.util.arrangement.repository.FeatureConfigRepository import com.wire.kalium.logic.util.arrangement.repository.FeatureConfigRepositoryArrangementImpl import com.wire.kalium.logic.util.arrangement.repository.UserConfigRepositoryArrangement import com.wire.kalium.logic.util.arrangement.repository.UserConfigRepositoryArrangementImpl +import com.wire.kalium.logic.util.shouldFail import com.wire.kalium.logic.util.shouldSucceed import com.wire.kalium.persistence.dbPassphrase.PassphraseStorage +import io.ktor.util.reflect.instanceOf import io.mockative.Mock import io.mockative.coVerify import io.mockative.mock import io.mockative.once +import io.mockative.verify import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import kotlin.test.Test @@ -63,12 +67,16 @@ class MLSClientProviderTest { val (arrangement, mlsClientProvider) = Arrangement().arrange { withGetSupportedCipherSuitesReturning(StorageFailure.DataNotFound.left()) withGetFeatureConfigsReturning(FeatureConfigTest.newModel(mlsModel = expected).right()) + withGetMLSEnabledReturning(true.right()) } mlsClientProvider.getOrFetchMLSConfig().shouldSucceed { assertEquals(expected.supportedCipherSuite, it) } + verify { arrangement.userConfigRepository.isMLSEnabled() } + .wasInvoked(exactly = once) + coVerify { arrangement.userConfigRepository.getSupportedCipherSuite() } .wasInvoked(exactly = once) @@ -88,12 +96,17 @@ class MLSClientProviderTest { val (arrangement, mlsClientProvider) = Arrangement().arrange { withGetSupportedCipherSuitesReturning(expected.right()) + withGetMLSEnabledReturning(true.right()) + withGetFeatureConfigsReturning(FeatureConfigTest.newModel().right()) } mlsClientProvider.getOrFetchMLSConfig().shouldSucceed { assertEquals(expected, it) } + verify { arrangement.userConfigRepository.isMLSEnabled() } + .wasInvoked(exactly = once) + coVerify { arrangement.userConfigRepository.getSupportedCipherSuite() }.wasInvoked(exactly = once) @@ -103,6 +116,37 @@ class MLSClientProviderTest { }.wasNotInvoked() } + @Test + fun givenMLSDisabledWhenGetOrFetchMLSConfigIsCalledThenDoNotCallGetSupportedCipherSuiteOrGetFeatureConfigs() = runTest { + // given + val (arrangement, mlsClientProvider) = Arrangement().arrange { + withGetMLSEnabledReturning(false.right()) + withGetSupportedCipherSuitesReturning( + SupportedCipherSuite( + supported = listOf( + CipherSuite.MLS_128_DHKEMP256_AES128GCM_SHA256_P256, + CipherSuite.MLS_256_DHKEMP384_AES256GCM_SHA384_P384 + ), + default = CipherSuite.MLS_128_DHKEMP256_AES128GCM_SHA256_P256 + ).right() + ) + } + + // when + val result = mlsClientProvider.getOrFetchMLSConfig() + + // then + result.shouldFail { + it.instanceOf(CoreFailure.Unknown::class) + } + + coVerify { arrangement.userConfigRepository.getSupportedCipherSuite() } + .wasNotInvoked() + + coVerify { arrangement.featureConfigRepository.getFeatureConfigs() } + .wasNotInvoked() + } + private class Arrangement : UserConfigRepositoryArrangement by UserConfigRepositoryArrangementImpl(), FeatureConfigRepositoryArrangement by FeatureConfigRepositoryArrangementImpl() { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt index c7108c8b7b7..a780f65d707 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt @@ -20,6 +20,7 @@ package com.wire.kalium.logic.data.conversation import app.cash.turbine.test import com.wire.kalium.cryptography.MLSClient +import com.wire.kalium.logic.MLSFailure import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.client.MLSClientProvider import com.wire.kalium.logic.data.event.Event @@ -181,6 +182,40 @@ class ConversationRepositoryTest { } } + @Test + fun givenNewMLSConversationEvent_whenMLSIsDisabled_thenConversationShouldNotPersisted() = + runTest { + val event = Event.Conversation.NewConversation( + "id", + TestConversation.ID, + TestUser.SELF.id, + Instant.UNIX_FIRST_DATE, + CONVERSATION_RESPONSE.copy( + groupId = RAW_GROUP_ID, + protocol = MLS, + mlsCipherSuiteTag = ConversationEntity.CipherSuite.MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519.cipherSuiteTag + ) + ) + val selfUserFlow = flowOf(TestUser.SELF) + val (arrangement, conversationRepository) = Arrangement() + .withSelfUserFlow(selfUserFlow) + .withDisabledMlsClientProvider() + .withHasEstablishedMLSGroup(true) + .arrange() + + conversationRepository.persistConversation(event.conversation, "teamId") + + with(arrangement) { + coVerify { + conversationDAO.insertConversation( + matches { conversation -> + conversation.id.value == CONVERSATION_RESPONSE.id.value + } + ) + }.wasNotInvoked() + } + } + @Test fun givenNewConversationEvent_whenCallingPersistConversationFromEventAndExists_thenConversationPersistenceShouldBeSkipped() = runTest { @@ -1728,6 +1763,12 @@ class ConversationRepositoryTest { }.returns(updated) } + suspend fun withDisabledMlsClientProvider() = apply { + coEvery { + mlsClientProvider.getMLSClient(any()) + }.returns(Either.Left(MLSFailure.Disabled)) + } + suspend fun arrange() = this to conversationRepository.also { coEvery { conversationDAO.insertConversations(any()) } .returns(Unit) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepositoryTest.kt index dac38b1d1cf..02face9132e 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepositoryTest.kt @@ -99,7 +99,6 @@ import io.mockative.matches import io.mockative.mock import io.mockative.once import io.mockative.twice -import io.mockative.verify import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first @@ -304,13 +303,15 @@ class MLSConversationRepositoryTest { coVerify { arrangement.mlsClient.createConversation( groupId = eq(Arrangement.RAW_GROUP_ID), - externalSenders = any()) + externalSenders = any() + ) }.wasInvoked(once) coVerify { arrangement.mlsClient.addMember( groupId = eq(Arrangement.RAW_GROUP_ID), - membersKeyPackages = any()) + membersKeyPackages = any() + ) }.wasInvoked(once) coVerify { @@ -1512,7 +1513,7 @@ class MLSConversationRepositoryTest { val (arrangement, mlsConversationRepository) = Arrangement(testKaliumDispatcher) .withCommitPendingProposalsReturningNothing() - .withClaimKeyPackagesSuccessful() + .withClaimKeyPackagesSuccessful(emptyList()) // empty cause members is empty in case of establishMLSSubConversationGroup .withGetMLSClientSuccessful() .withGetMLSGroupIdByConversationIdReturns(Arrangement.GROUP_ID.value) .withGetExternalSenderKeySuccessful() @@ -1925,10 +1926,10 @@ class MLSConversationRepositoryTest { "user_handle", "wire.com" ), - "User Test", - "domain.com", - "certificate", - serialNumber = "serialNumber", + "User Test", + "domain.com", + "certificate", + serialNumber = "serialNumber", notAfter = 1899105093, notBefore = 1899205093 ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapperTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapperTest.kt index 9bdd5147807..16aa6bf4b26 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapperTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapperTest.kt @@ -29,9 +29,9 @@ import com.wire.kalium.protobuf.encodeToByteArray import com.wire.kalium.protobuf.messages.Asset import com.wire.kalium.protobuf.messages.Confirmation import com.wire.kalium.protobuf.messages.GenericMessage +import com.wire.kalium.protobuf.messages.GenericMessage.UnknownStrategy import com.wire.kalium.protobuf.messages.MessageEdit import com.wire.kalium.protobuf.messages.Text -import com.wire.kalium.protobuf.messages.UnknownStrategy import io.ktor.utils.io.core.toByteArray import kotlin.test.BeforeTest import kotlin.test.Test @@ -516,6 +516,24 @@ class ProtoContentMapperTest { assertEquals(decoded, protoContent) } + @Test + fun givenInCallEmojiContent_whenMappingToProtoDataAndBack_thenTheContentsShouldMatchTheOriginal() { + val messageContent = MessageContent.InCallEmoji( + emojis = mapOf("emoji" to 999) + ) + val protoContent = ProtoContent.Readable( + TEST_MESSAGE_UUID, + messageContent, + false, + legalHoldStatus = Conversation.LegalHoldStatus.UNKNOWN + ) + + val encoded = protoContentMapper.encodeToProtobuf(protoContent) + val decoded = protoContentMapper.decodeFromProtobuf(encoded) + + assertEquals(decoded, protoContent) + } + private companion object { const val TEST_MESSAGE_UUID = "testUuid" val TEST_CONVERSATION_ID = TestConversation.ID diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/prekey/MessageSendFailureHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/prekey/MessageSendFailureHandlerTest.kt index fce96785379..22fcd306f95 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/prekey/MessageSendFailureHandlerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/prekey/MessageSendFailureHandlerTest.kt @@ -400,7 +400,7 @@ class MessageSendFailureHandlerTest { suspend fun withFetchUsersByIdSuccess() = apply { coEvery { userRepository.fetchUsersByIds(any()) - }.returns(Either.Right(Unit)) + }.returns(Either.Right(true)) } suspend fun withFetchUsersByIdFailure(failure: CoreFailure) = apply { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/CreateAndPersistRecentlyEndedCallMetadataUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/CreateAndPersistRecentlyEndedCallMetadataUseCaseTest.kt new file mode 100644 index 00000000000..8a75f6c8c0c --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/CreateAndPersistRecentlyEndedCallMetadataUseCaseTest.kt @@ -0,0 +1,281 @@ +/* + * Wire + * Copyright (C) 2024 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.call.usecase + +import com.wire.kalium.logic.data.call.CallMetadata +import com.wire.kalium.logic.data.call.CallMetadataProfile +import com.wire.kalium.logic.data.call.CallRepository +import com.wire.kalium.logic.data.call.CallStatus +import com.wire.kalium.logic.data.call.ParticipantMinimized +import com.wire.kalium.logic.data.call.RecentlyEndedCallMetadata +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.conversation.MemberDetails +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.SelfTeamIdProvider +import com.wire.kalium.logic.data.user.type.UserType +import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase +import com.wire.kalium.logic.framework.TestCall.CALLER_ID +import com.wire.kalium.logic.framework.TestUser +import com.wire.kalium.logic.framework.TestUser.OTHER_MINIMIZED +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.every +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class CreateAndPersistRecentlyEndedCallMetadataUseCaseTest { + + @Test + fun givenCallAndEndCallReaction_whenUseCaseInvoked_thenRecentlyCallMetadataIsProperlyUpdated() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withOutgoingCall() + .withSelfTeamIdPresent() + .withConversationMembers() + .arrange() + + // when + useCase( + conversationId = CONVERSATION_ID, + callEndedReason = 2 + ) + + // then + coVerify { + arrangement.callRepository.updateRecentlyEndedCallMetadata(DEFAULT_ENDED_CALL_METADATA) + }.wasInvoked(exactly = once) + } + + @Test + fun givenCallDetailsWithinConversationWithGuests_whenUseCaseInvoked_thenRecentlyEndedCallMetadataHasProperGuestsCount() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withOutgoingCall() + .withSelfTeamIdPresent() + .withConversationGuests() + .arrange() + + // when + useCase( + conversationId = CONVERSATION_ID, + callEndedReason = 2 + ) + + // then + coVerify { + arrangement.callRepository.updateRecentlyEndedCallMetadata( + DEFAULT_ENDED_CALL_METADATA.copy( + conversationDetails = DEFAULT_ENDED_CALL_METADATA.conversationDetails.copy( + conversationGuests = 1 + ) + ) + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenCallDetailsWithinConversationWithGuests_whenUseCaseInvoked_thenRecentlyEndedCallMetadataHasProperGuestsProCount() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withOutgoingCall() + .withSelfTeamIdPresent() + .withConversationGuestsPro() + .arrange() + + // when + useCase( + conversationId = CONVERSATION_ID, + callEndedReason = 2 + ) + + // then + coVerify { + arrangement.callRepository.updateRecentlyEndedCallMetadata( + DEFAULT_ENDED_CALL_METADATA.copy( + conversationDetails = DEFAULT_ENDED_CALL_METADATA.conversationDetails.copy( + conversationGuests = 1, + conversationGuestsPro = 1 + ) + ) + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenIncomingCallDetails_whenUseCaseInvoked_thenReturnCorrectMetadataIncomingCall() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withIncomingCall() + .withSelfTeamIdPresent() + .withConversationMembers() + .arrange() + + // when + useCase( + conversationId = CONVERSATION_ID, + callEndedReason = 2 + ) + + // then + coVerify { + arrangement.callRepository.updateRecentlyEndedCallMetadata( + DEFAULT_ENDED_CALL_METADATA.copy( + callDetails = DEFAULT_ENDED_CALL_METADATA.callDetails.copy( + isOutgoingCall = false + ) + ) + ) + }.wasInvoked(exactly = once) + } + + private class Arrangement { + @Mock + val observeConversationMembers = mock(ObserveConversationMembersUseCase::class) + + @Mock + val selfTeamIdProvider = mock(SelfTeamIdProvider::class) + + @Mock + val callRepository = mock(CallRepository::class) + + fun withOutgoingCall() = apply { + every { callRepository.getCallMetadataProfile() } + .returns(CallMetadataProfile(mapOf(CONVERSATION_ID to callMetadata()))) + } + + fun withIncomingCall() = apply { + every { callRepository.getCallMetadataProfile() } + .returns(CallMetadataProfile(mapOf(CONVERSATION_ID to callMetadata().copy(callerId = CALLER_ID.copy(value = "external"))))) + } + + suspend fun withConversationMembers() = apply { + coEvery { observeConversationMembers(any()) }.returns( + flowOf( + listOf( + MemberDetails(TestUser.SELF, Conversation.Member.Role.Admin), + MemberDetails(TestUser.OTHER, Conversation.Member.Role.Member) + ) + ) + ) + } + + suspend fun withConversationGuests() = apply { + coEvery { observeConversationMembers(any()) }.returns( + flowOf( + listOf( + MemberDetails(TestUser.SELF, Conversation.Member.Role.Admin), + MemberDetails(TestUser.OTHER.copy(userType = UserType.GUEST, teamId = null), Conversation.Member.Role.Member) + ) + ) + ) + } + + suspend fun withConversationGuestsPro() = apply { + coEvery { observeConversationMembers(any()) }.returns( + flowOf( + listOf( + MemberDetails(TestUser.SELF, Conversation.Member.Role.Admin), + MemberDetails(TestUser.OTHER.copy(userType = UserType.GUEST), Conversation.Member.Role.Member) + ) + ) + ) + } + + suspend fun withSelfTeamIdPresent() = apply { + coEvery { selfTeamIdProvider() }.returns(Either.Right(TestUser.SELF.teamId)) + } + + fun arrange(): Pair = + this to CreateAndPersistRecentlyEndedCallMetadataUseCaseImpl( + callRepository = callRepository, + observeConversationMembers = observeConversationMembers, + selfTeamIdProvider = selfTeamIdProvider + ) + + private fun callMetadata(): CallMetadata { + return CallMetadata( + callerId = CALLER_ID.copy(value = "ownerId"), + isMuted = true, + isCameraOn = false, + isCbrEnabled = false, + conversationName = null, + users = listOf( + OTHER_MINIMIZED.copy(id = CALLER_ID.copy(value = "ownerId"), userType = UserType.OWNER), + OTHER_MINIMIZED + ), + participants = listOf( + ParticipantMinimized( + id = CALLER_ID.copy(value = "ownerId"), + userId = CALLER_ID.copy(value = "ownerId"), + clientId = "abcd", + isMuted = true, + isCameraOn = false, + isSharingScreen = false, + hasEstablishedAudio = true + ), + ParticipantMinimized( + id = CALLER_ID, + userId = CALLER_ID, + clientId = "abcd", + isMuted = true, + isCameraOn = false, + isSharingScreen = false, + hasEstablishedAudio = true + ) + ), + conversationType = Conversation.Type.ONE_ON_ONE, + callerName = "User Name", + callerTeamName = null, + callStatus = CallStatus.ESTABLISHED, + protocol = Conversation.ProtocolInfo.Proteus, + activeSpeakers = mapOf() + ) + } + } + + private companion object { + val CONVERSATION_ID = ConversationId(value = "value", domain = "domain") + val DEFAULT_ENDED_CALL_METADATA = RecentlyEndedCallMetadata( + callEndReason = 2, + isTeamMember = true, + callDetails = RecentlyEndedCallMetadata.CallDetails( + isCallScreenShare = false, + screenShareDurationInSeconds = 0L, + callScreenShareUniques = 0, + isOutgoingCall = true, + callDurationInSeconds = 0L, + callParticipantsCount = 2, + conversationServices = 0, + callAVSwitchToggle = false, + callVideoEnabled = false + ), + conversationDetails = RecentlyEndedCallMetadata.ConversationDetails( + conversationType = Conversation.Type.ONE_ON_ONE, + conversationSize = 2, + conversationGuests = 0, + conversationGuestsPro = 0 + ) + ) + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationAssetsLocallyUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationAssetsLocallyUseCaseTest.kt new file mode 100644 index 00000000000..2585230f30d --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationAssetsLocallyUseCaseTest.kt @@ -0,0 +1,111 @@ +/* + * Wire + * Copyright (C) 2024 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 + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.asset.AssetRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.MessageRepository +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertIs + +class ClearConversationAssetsLocallyUseCaseTest { + + @Test + fun givenConversationAssetIds_whenAllDeletionsAreSuccess_thenSuccessResultIsPropagated() = runTest { + // given + val ids = listOf("id_1", "id_2") + val (arrangement, useCase) = Arrangement() + .withAssetIdsResponse(ids) + .withAssetClearSuccess("id_1") + .withAssetClearSuccess("id_2") + .arrange() + + // when + val result = useCase(ConversationId("someValue", "someDomain")) + + // then + assertIs>(result) + coVerify { arrangement.assetRepository.deleteAssetLocally(any()) }.wasInvoked(exactly = 2) + } + + @Test + fun givenConversationAssetIds_whenOneDeletionFailed_thenFailureResultIsPropagated() = runTest { + // given + val ids = listOf("id_1", "id_2") + val (arrangement, useCase) = Arrangement() + .withAssetIdsResponse(ids) + .withAssetClearSuccess("id_1") + .withAssetClearError("id_2") + .arrange() + + // when + val result = useCase(ConversationId("someValue", "someDomain")) + + // then + assertIs>(result) + coVerify { arrangement.assetRepository.deleteAssetLocally(any()) }.wasInvoked(exactly = 2) + } + + @Test + fun givenEmptyConversationAssetIds_whenInvoked_thenDeletionsAreNotInvoked() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withAssetIdsResponse(emptyList()) + .arrange() + + // when + val result = useCase(ConversationId("someValue", "someDomain")) + + // then + assertIs>(result) + coVerify { arrangement.assetRepository.deleteAssetLocally(any()) }.wasNotInvoked() + } + + private class Arrangement { + @Mock + val messageRepository = mock(MessageRepository::class) + + @Mock + val assetRepository = mock(AssetRepository::class) + + suspend fun withAssetClearSuccess(id: String) = apply { + coEvery { assetRepository.deleteAssetLocally(id) }.returns(Either.Right(Unit)) + } + + suspend fun withAssetClearError(id: String) = apply { + coEvery { assetRepository.deleteAssetLocally(id) }.returns(Either.Left(CoreFailure.Unknown(null))) + } + + suspend fun withAssetIdsResponse(ids: List) = apply { + coEvery { messageRepository.getAllAssetIdsFromConversationId(any()) }.returns(Either.Right(ids)) + } + + fun arrange() = this to ClearConversationAssetsLocallyUseCaseImpl( + messageRepository = messageRepository, + assetRepository = assetRepository + ) + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCaseTest.kt new file mode 100644 index 00000000000..a6abd5c4edf --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCaseTest.kt @@ -0,0 +1,137 @@ +/* + * Wire + * Copyright (C) 2024 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 + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertIs + +class DeleteConversationLocallyUseCaseTest { + + companion object { + val SUCCESS = Either.Right(Unit) + val ERROR = Either.Left(CoreFailure.Unknown(null)) + val CONVERSATION_ID = ConversationId("someValue", "someDomain") + } + + @Test + fun givenDeleteLocalConversationInvoked_whenAllStepsAreSuccessful_thenSuccessResultIsPropagated() = runTest { + // given + val (_, useCase) = Arrangement() + .withClearContent(SUCCESS) + .withClearLocalAsset(SUCCESS) + .withDeleteLocalConversation(SUCCESS) + .arrange() + + // when + val result = useCase(CONVERSATION_ID) + + // then + assertIs>(result) + } + + @Test + fun givenDeleteLocalConversationInvoked_whenAssetClearIsUnsuccessful_thenErrorResultIsPropagated() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withClearContent(SUCCESS) + .withClearLocalAsset(ERROR) + .withDeleteLocalConversation(SUCCESS) + .arrange() + + // when + val result = useCase(CONVERSATION_ID) + + // then + assertIs>(result) + coVerify { arrangement.conversationRepository.clearContent(any()) }.wasNotInvoked() + coVerify { arrangement.conversationRepository.deleteConversationLocally(any()) }.wasNotInvoked() + } + + @Test + fun givenDeleteLocalConversationInvoked_whenContentClearIsUnsuccessful_thenErrorResultIsPropagated() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withClearContent(ERROR) + .withClearLocalAsset(SUCCESS) + .withDeleteLocalConversation(SUCCESS) + .arrange() + + // when + val result = useCase(CONVERSATION_ID) + + // then + assertIs>(result) + coVerify { arrangement.clearLocalConversationAssets(any()) }.wasInvoked(exactly = 1) + coVerify { arrangement.conversationRepository.clearContent(any()) }.wasInvoked(exactly = 1) + coVerify { arrangement.conversationRepository.deleteConversationLocally(any()) }.wasNotInvoked() + } + + @Test + fun givenDeleteLocalConversationInvoked_whenDeleteConversationIsUnsuccessful_thenErrorResultIsPropagated() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withClearContent(SUCCESS) + .withClearLocalAsset(SUCCESS) + .withDeleteLocalConversation(ERROR) + .arrange() + + // when + val result = useCase(CONVERSATION_ID) + + // then + assertIs>(result) + coVerify { arrangement.clearLocalConversationAssets(any()) }.wasInvoked(exactly = 1) + coVerify { arrangement.conversationRepository.clearContent(any()) }.wasInvoked(exactly = 1) + coVerify { arrangement.conversationRepository.deleteConversationLocally(any()) }.wasInvoked(exactly = 1) + } + + private class Arrangement { + @Mock + val conversationRepository = mock(ConversationRepository::class) + + @Mock + val clearLocalConversationAssets = mock(ClearConversationAssetsLocallyUseCase::class) + + suspend fun withClearContent(result: Either) = apply { + coEvery { conversationRepository.clearContent(any()) }.returns(result) + } + + suspend fun withDeleteLocalConversation(result: Either) = apply { + coEvery { conversationRepository.deleteConversationLocally(any()) }.returns(result) + } + + suspend fun withClearLocalAsset(result: Either) = apply { + coEvery { clearLocalConversationAssets(any()) }.returns(result) + } + + fun arrange() = this to DeleteConversationLocallyUseCaseImpl( + conversationRepository = conversationRepository, + clearLocalConversationAssets = clearLocalConversationAssets + ) + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationInteractionAvailabilityUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationInteractionAvailabilityUseCaseTest.kt index cc721631f91..2feef65b5cd 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationInteractionAvailabilityUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationInteractionAvailabilityUseCaseTest.kt @@ -20,16 +20,21 @@ package com.wire.kalium.logic.feature.conversation import app.cash.turbine.test import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.InteractionAvailability import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.SupportedProtocol +import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.framework.TestConversation import com.wire.kalium.logic.framework.TestConversationDetails import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.right import com.wire.kalium.logic.test_util.TestKaliumDispatcher import com.wire.kalium.logic.test_util.testKaliumDispatcher +import com.wire.kalium.logic.util.arrangement.provider.CurrentClientIdProviderArrangement +import com.wire.kalium.logic.util.arrangement.provider.CurrentClientIdProviderArrangementImpl import com.wire.kalium.logic.util.arrangement.repository.ConversationRepositoryArrangement import com.wire.kalium.logic.util.arrangement.repository.ConversationRepositoryArrangementImpl import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangement @@ -41,6 +46,7 @@ import io.mockative.once import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs @@ -52,6 +58,7 @@ class ObserveConversationInteractionAvailabilityUseCaseTest { val conversationId = TestConversation.ID val (arrangement, observeConversationInteractionAvailability) = arrange { + withIsClientMlsCapable(false.right()) dispatcher = testKaliumDispatcher withSelfUserBeingMemberOfConversation(isMember = true) } @@ -76,6 +83,7 @@ class ObserveConversationInteractionAvailabilityUseCaseTest { val (arrangement, observeConversationInteractionAvailability) = arrange { dispatcher = testKaliumDispatcher withSelfUserBeingMemberOfConversation(isMember = false) + withIsClientMlsCapable(false.right()) } observeConversationInteractionAvailability(conversationId).test { @@ -96,6 +104,7 @@ class ObserveConversationInteractionAvailabilityUseCaseTest { val conversationId = TestConversation.ID val (arrangement, observeConversationInteractionAvailability) = arrange { + withIsClientMlsCapable(false.right()) dispatcher = testKaliumDispatcher withGroupConversationError() } @@ -118,6 +127,7 @@ class ObserveConversationInteractionAvailabilityUseCaseTest { val conversationId = TestConversation.ID val (arrangement, observeConversationInteractionAvailability) = arrange { + withIsClientMlsCapable(false.right()) dispatcher = testKaliumDispatcher withBlockedUserConversation() } @@ -132,7 +142,6 @@ class ObserveConversationInteractionAvailabilityUseCaseTest { awaitComplete() } - } @Test @@ -140,6 +149,7 @@ class ObserveConversationInteractionAvailabilityUseCaseTest { val conversationId = TestConversation.ID val (arrangement, observeConversationInteractionAvailability) = arrange { + withIsClientMlsCapable(false.right()) dispatcher = testKaliumDispatcher withDeletedUserConversation() } @@ -156,11 +166,12 @@ class ObserveConversationInteractionAvailabilityUseCaseTest { } } + @Ignore // is this really a case that a client does not support Proteus @Test fun givenProteusConversationAndUserSupportsOnlyMLS_whenObserving_thenShouldReturnUnsupportedProtocol() = runTest { testProtocolSupport( conversationProtocolInfo = Conversation.ProtocolInfo.Proteus, - userSupportedProtocols = setOf(SupportedProtocol.MLS), + isMlsCapable = true.right(), expectedResult = InteractionAvailability.UNSUPPORTED_PROTOCOL ) } @@ -169,7 +180,7 @@ class ObserveConversationInteractionAvailabilityUseCaseTest { fun givenMLSConversationAndUserSupportsOnlyMLS_whenObserving_thenShouldReturnUnsupportedProtocol() = runTest { testProtocolSupport( conversationProtocolInfo = TestConversation.MLS_PROTOCOL_INFO, - userSupportedProtocols = setOf(SupportedProtocol.PROTEUS), + isMlsCapable = false.right(), expectedResult = InteractionAvailability.UNSUPPORTED_PROTOCOL ) } @@ -178,7 +189,7 @@ class ObserveConversationInteractionAvailabilityUseCaseTest { fun givenMixedConversationAndUserSupportsOnlyMLS_whenObserving_thenShouldReturnUnsupportedProtocol() = runTest { testProtocolSupport( conversationProtocolInfo = TestConversation.MIXED_PROTOCOL_INFO, - userSupportedProtocols = setOf(SupportedProtocol.PROTEUS), + isMlsCapable = false.right(), expectedResult = InteractionAvailability.ENABLED ) } @@ -187,7 +198,7 @@ class ObserveConversationInteractionAvailabilityUseCaseTest { fun givenMixedConversationAndUserSupportsProteus_whenObserving_thenShouldReturnEnabled() = runTest { testProtocolSupport( conversationProtocolInfo = TestConversation.MIXED_PROTOCOL_INFO, - userSupportedProtocols = setOf(SupportedProtocol.PROTEUS), + isMlsCapable = false.right(), expectedResult = InteractionAvailability.ENABLED ) } @@ -196,8 +207,8 @@ class ObserveConversationInteractionAvailabilityUseCaseTest { fun givenMLSConversationAndUserSupportsMLS_whenObserving_thenShouldReturnEnabled() = runTest { testProtocolSupport( conversationProtocolInfo = TestConversation.MLS_PROTOCOL_INFO, - userSupportedProtocols = setOf(SupportedProtocol.MLS), - expectedResult = InteractionAvailability.ENABLED + expectedResult = InteractionAvailability.ENABLED, + isMlsCapable = true.right() ) } @@ -205,18 +216,19 @@ class ObserveConversationInteractionAvailabilityUseCaseTest { fun givenProteusConversationAndUserSupportsProteus_whenObserving_thenShouldReturnEnabled() = runTest { testProtocolSupport( conversationProtocolInfo = TestConversation.PROTEUS_PROTOCOL_INFO, - userSupportedProtocols = setOf(SupportedProtocol.PROTEUS), - expectedResult = InteractionAvailability.ENABLED + expectedResult = InteractionAvailability.ENABLED, + isMlsCapable = false.right() ) } private suspend fun CoroutineScope.testProtocolSupport( conversationProtocolInfo: Conversation.ProtocolInfo, - userSupportedProtocols: Set, + isMlsCapable: Either, expectedResult: InteractionAvailability ) { val convId = TestConversationDetails.CONVERSATION_GROUP.conversation.id val (_, observeConversationInteractionAvailabilityUseCase) = arrange { + withIsClientMlsCapable(isMlsCapable) dispatcher = testKaliumDispatcher val proteusGroupDetails = TestConversationDetails.CONVERSATION_GROUP.copy( conversation = TestConversationDetails.CONVERSATION_GROUP.conversation.copy( @@ -224,7 +236,6 @@ class ObserveConversationInteractionAvailabilityUseCaseTest { ) ) withObserveConversationDetailsByIdReturning(Either.Right(proteusGroupDetails)) - withObservingSelfUserReturning(flowOf(TestUser.SELF.copy(supportedProtocols = userSupportedProtocols))) } observeConversationInteractionAvailabilityUseCase(convId).test { @@ -241,6 +252,7 @@ class ObserveConversationInteractionAvailabilityUseCaseTest { val (_, observeConversationInteractionAvailability) = arrange { dispatcher = testKaliumDispatcher withLegalHoldOneOnOneConversation(Conversation.LegalHoldStatus.ENABLED) + withIsClientMlsCapable(false.right()) } observeConversationInteractionAvailability(conversationId).test { val interactionResult = awaitItem() @@ -253,6 +265,7 @@ class ObserveConversationInteractionAvailabilityUseCaseTest { fun givenConversationLegalHoldIsDegraded_whenInvokingInteractionForConversation_thenInteractionShouldBeLegalHold() = runTest { val conversationId = TestConversation.ID val (_, observeConversationInteractionAvailability) = arrange { + withIsClientMlsCapable(false.right()) dispatcher = testKaliumDispatcher withLegalHoldOneOnOneConversation(Conversation.LegalHoldStatus.DEGRADED) } @@ -266,10 +279,12 @@ class ObserveConversationInteractionAvailabilityUseCaseTest { private class Arrangement( private val configure: suspend Arrangement.() -> Unit ) : UserRepositoryArrangement by UserRepositoryArrangementImpl(), - ConversationRepositoryArrangement by ConversationRepositoryArrangementImpl() { + ConversationRepositoryArrangement by ConversationRepositoryArrangementImpl(), + CurrentClientIdProviderArrangement by CurrentClientIdProviderArrangementImpl() { var dispatcher: KaliumDispatcher = TestKaliumDispatcher + val selfUser = UserId("self_value", "self_domain") suspend fun withSelfUserBeingMemberOfConversation(isMember: Boolean) = apply { withObserveConversationDetailsByIdReturning( Either.Right(TestConversationDetails.CONVERSATION_GROUP.copy(isSelfUserMember = isMember)) @@ -315,17 +330,15 @@ class ObserveConversationInteractionAvailabilityUseCaseTest { } suspend fun arrange(): Pair = run { - withObservingSelfUserReturning( - flowOf( - TestUser.SELF.copy(supportedProtocols = setOf(SupportedProtocol.MLS, SupportedProtocol.PROTEUS)) - ) - ) + withCurrentClientIdSuccess(ClientId("client_id")) configure() this@Arrangement to ObserveConversationInteractionAvailabilityUseCase( conversationRepository = conversationRepository, userRepository = userRepository, - dispatcher = dispatcher - ) + dispatcher = dispatcher, + selfUserId = selfUser, + selfClientIdProvider = currentClientIdProvider + ) } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigratorTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigratorTest.kt index d7f199fa13d..a80a0318de9 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigratorTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigratorTest.kt @@ -65,25 +65,6 @@ class OneOnOneMigratorTest { }.wasNotInvoked() } - @Test - fun givenUnassignedOneOnOne_whenMigratingToProteus_thenShouldAssignOneOnOneConversation() = runTest { - val user = TestUser.OTHER.copy( - activeOneOnOneConversationId = null - ) - - val (arrangement, oneOneMigrator) = arrange { - withGetOneOnOneConversationsWithOtherUserReturning(Either.Right(listOf(TestConversation.ID))) - withUpdateOneOnOneConversationReturning(Either.Right(Unit)) - } - - oneOneMigrator.migrateToProteus(user) - .shouldSucceed() - - coVerify { - arrangement.userRepository.updateActiveOneOnOneConversation(eq(user.id), eq(TestConversation.ID)) - }.wasInvoked() - } - @Test fun givenNoExistingTeamOneOnOne_whenMigratingToProteus_thenShouldCreateGroupConversation() = runTest { val user = TestUser.OTHER.copy( @@ -252,6 +233,33 @@ class OneOnOneMigratorTest { }.wasInvoked(exactly = once) } + @Test + fun givenExistingTeamOneOnOne_whenMigratingToProteus_thenShouldNOTCreateGroupConversation() = runTest { + val user = TestUser.OTHER.copy( + activeOneOnOneConversationId = null + ) + + val (arrangement, oneOneMigrator) = arrange { + withGetOneOnOneConversationsWithOtherUserReturning(Either.Right(listOf(TestConversation.ONE_ON_ONE().id))) + withUpdateOneOnOneConversationReturning(Either.Right(Unit)) + } + + oneOneMigrator.migrateExistingProteus(user) + .shouldSucceed() + + coVerify { + arrangement.conversationGroupRepository.createGroupConversation( + name = eq(null), + usersList = eq(listOf(TestUser.OTHER.id)), + options = eq(ConversationOptions()) + ) + }.wasNotInvoked() + + coVerify { + arrangement.userRepository.updateActiveOneOnOneConversation(eq(TestUser.OTHER.id), eq(TestConversation.ONE_ON_ONE().id)) + }.wasInvoked() + } + private class Arrangement(private val block: suspend Arrangement.() -> Unit) : MLSOneOnOneConversationResolverArrangement by MLSOneOnOneConversationResolverArrangementImpl(), MessageRepositoryArrangement by MessageRepositoryArrangementImpl(), diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneResolverTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneResolverTest.kt index 8d8a02ac310..d00102a78d4 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneResolverTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneResolverTest.kt @@ -22,6 +22,8 @@ import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.framework.TestConversation import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.left +import com.wire.kalium.logic.functional.right import com.wire.kalium.logic.util.arrangement.IncrementalSyncRepositoryArrangement import com.wire.kalium.logic.util.arrangement.IncrementalSyncRepositoryArrangementImpl import com.wire.kalium.logic.util.arrangement.mls.OneOnOneMigratorArrangement @@ -101,7 +103,7 @@ class OneOnOneResolverTest { // given val oneOnOneUser = TestUser.OTHER.copy(id = TestUser.OTHER_USER_ID) val (arrangement, resolver) = arrange { - withFetchUsersByIdReturning(Either.Right(Unit)) + withFetchUsersByIdReturning(Either.Right(true)) withGetProtocolForUser(Either.Right(SupportedProtocol.MLS)) withMigrateToMLSReturns(Either.Right(TestConversation.ID)) } @@ -128,7 +130,7 @@ class OneOnOneResolverTest { // given val oneOnOneUser = TestUser.OTHER.copy(id = TestUser.OTHER_USER_ID) val (arrangement, resolver) = arrange { - withFetchUsersByIdReturning(Either.Right(Unit)) + withFetchUsersByIdReturning(Either.Right(true)) withGetProtocolForUser(Either.Right(SupportedProtocol.MLS)) withMigrateToMLSReturns(Either.Right(TestConversation.ID)) } @@ -149,7 +151,7 @@ class OneOnOneResolverTest { val oneOnOneUser = TestUser.OTHER.copy(id = TestUser.OTHER_USER_ID) val (arrangement, resolver) = arrange { withGetKnownUserReturning(flowOf(oneOnOneUser)) - withFetchUsersByIdReturning(Either.Right(Unit)) + withFetchUsersByIdReturning(Either.Right(true)) withGetProtocolForUser(Either.Right(SupportedProtocol.MLS)) withMigrateToMLSReturns(Either.Right(TestConversation.ID)) } @@ -244,6 +246,23 @@ class OneOnOneResolverTest { }.wasInvoked(exactly = once) } + @Test + fun givenProtocolResolvesToOtherNeedToUpdate_whenResolveOneOnOneConversationWithUser_thenMigrateExistingToProteus() = runTest { + // given + val (arrangement, resolver) = arrange { + withGetProtocolForUser(CoreFailure.NoCommonProtocolFound.OtherNeedToUpdate.left()) + withMigrateExistingToProteusReturns(TestConversation.ID.right()) + } + + // when + resolver.resolveOneOnOneConversationWithUser(OTHER_USER, false).shouldSucceed() + + // then + coVerify { + arrangement.oneOnOneMigrator.migrateExistingProteus(eq(OTHER_USER)) + }.wasInvoked(exactly = once) + } + private class Arrangement(private val block: suspend Arrangement.() -> Unit) : UserRepositoryArrangement by UserRepositoryArrangementImpl(), OneOnOneProtocolSelectorArrangement by OneOnOneProtocolSelectorArrangementImpl(), diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/incallreaction/SendInCallReactionUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/incallreaction/SendInCallReactionUseCaseTest.kt new file mode 100644 index 00000000000..333f312cb79 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/incallreaction/SendInCallReactionUseCaseTest.kt @@ -0,0 +1,119 @@ +/* + * Wire + * Copyright (C) 2024 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.incallreaction + +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.data.conversation.ClientId +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.feature.message.MessageSender +import com.wire.kalium.logic.framework.TestClient +import com.wire.kalium.logic.framework.TestUser +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.test_util.testKaliumDispatcher +import com.wire.kalium.logic.util.shouldFail +import com.wire.kalium.logic.util.shouldSucceed +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class SendInCallReactionUseCaseTest { + + @Mock + val messageSender = mock(MessageSender::class) + + @Test + fun givenEstablishedConnection_WhenSending_ShouldReturnSuccess() = runTest { + + // Given + val (arrangement, sendReactionUseCase) = Arrangement(this) + .withCurrentClientProviderSuccess() + .withSendMessageSuccess() + .arrange() + + // When + val result = sendReactionUseCase(ConversationId("id", "domain"), "reaction") + + // Then + result.shouldSucceed() + + coVerify { + arrangement.messageSender.sendMessage(any(), any()) + }.wasInvoked(once) + } + + @Test + fun givenNoConnectionWhenSendingShouldFail() = runTest { + + // Given + val (arrangement, sendReactionUseCase) = Arrangement(this) + .withCurrentClientProviderSuccess() + .withSendMessageFailure() + .arrange() + + // When + val result = sendReactionUseCase(ConversationId("id", "domain"), "reaction") + + // Then + result.shouldFail() + + coVerify { + arrangement.messageSender.sendMessage(any(), any()) + }.wasInvoked(once) + } + + private class Arrangement(private val coroutineScope: CoroutineScope) { + @Mock + val messageSender = mock(MessageSender::class) + + @Mock + val currentClientIdProvider = mock(CurrentClientIdProvider::class) + + suspend fun withSendMessageSuccess() = apply { + coEvery { + messageSender.sendMessage(any(), any()) + }.returns(Either.Right(Unit)) + } + + suspend fun withSendMessageFailure() = apply { + coEvery { + messageSender.sendMessage(any(), any()) + }.returns(Either.Left(NetworkFailure.NoNetworkConnection(null))) + } + + suspend fun withCurrentClientProviderSuccess(clientId: ClientId = TestClient.CLIENT_ID) = apply { + coEvery { + currentClientIdProvider.invoke() + }.returns(Either.Right(clientId)) + } + + fun arrange() = this to SendInCallReactionUseCase( + selfUserId = TestUser.SELF.id, + provideClientId = currentClientIdProvider, + messageSender = messageSender, + dispatchers = coroutineScope.testKaliumDispatcher, + scope = coroutineScope, + ) + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/keypackage/MLSKeyPackageCountUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/keypackage/MLSKeyPackageCountUseCaseTest.kt index 356e010119d..5c8afbeb180 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/keypackage/MLSKeyPackageCountUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/keypackage/MLSKeyPackageCountUseCaseTest.kt @@ -30,6 +30,9 @@ import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountUseCaseTest.Ar import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountUseCaseTest.Arrangement.Companion.NETWORK_FAILURE import com.wire.kalium.logic.framework.TestClient import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.right +import com.wire.kalium.logic.util.arrangement.repository.UserConfigRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.UserConfigRepositoryArrangementImpl import com.wire.kalium.network.api.authenticated.keypackage.KeyPackageCountDTO import io.mockative.Mock import io.mockative.any @@ -39,20 +42,22 @@ import io.mockative.eq import io.mockative.every import io.mockative.mock import io.mockative.once -import kotlinx.coroutines.ExperimentalCoroutinesApi +import io.mockative.verify +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs -@OptIn(ExperimentalCoroutinesApi::class) class MLSKeyPackageCountUseCaseTest { @Test fun givenClientIdIsNotRegistered_ThenReturnGenericError() = runTest { val (arrangement, keyPackageCountUseCase) = Arrangement() .withClientId(Either.Left(CLIENT_FETCH_ERROR)) - .arrange() + .arrange{ + withGetMLSEnabledReturning(true.right()) + } val actual = keyPackageCountUseCase() @@ -70,7 +75,9 @@ class MLSKeyPackageCountUseCaseTest { .withAvailableKeyPackageCountReturn(Either.Right(KEY_PACKAGE_COUNT_DTO)) .withClientId(Either.Right(TestClient.CLIENT_ID)) .withKeyPackageLimitSucceed() - .arrange() + .arrange{ + withGetMLSEnabledReturning(true.right()) + } val actual = keyPackageCountUseCase() @@ -86,7 +93,9 @@ class MLSKeyPackageCountUseCaseTest { val (arrangement, keyPackageCountUseCase) = Arrangement() .withAvailableKeyPackageCountReturn(Either.Left(NETWORK_FAILURE)) .withClientId(Either.Right(TestClient.CLIENT_ID)) - .arrange() + .arrange{ + withGetMLSEnabledReturning(true.right()) + } val actual = keyPackageCountUseCase() @@ -97,7 +106,28 @@ class MLSKeyPackageCountUseCaseTest { assertEquals(actual.networkFailure, NETWORK_FAILURE) } - private class Arrangement { + @Test + fun givenClientID_whenCallingGetMLSEnabledReturnFalse_ThenReturnKeyPackageCountNotEnabledFailure() = runTest { + val (arrangement, keyPackageCountUseCase) = Arrangement() + .withAvailableKeyPackageCountReturn(Either.Right(KEY_PACKAGE_COUNT_DTO)) + .withClientId(Either.Right(TestClient.CLIENT_ID)) + .arrange{ + withGetMLSEnabledReturning(false.right()) + } + + val actual = keyPackageCountUseCase() + + verify { + arrangement.userConfigRepository.isMLSEnabled() + }.wasInvoked(once) + + coVerify { + arrangement.keyPackageRepository.getAvailableKeyPackageCount(eq(TestClient.CLIENT_ID)) + }.wasNotInvoked() + assertIs(actual) + } + + private class Arrangement : UserConfigRepositoryArrangement by UserConfigRepositoryArrangementImpl() { @Mock val keyPackageRepository = mock(KeyPackageRepository::class) @@ -125,9 +155,11 @@ class MLSKeyPackageCountUseCaseTest { }.returns(result) } - fun arrange() = this to MLSKeyPackageCountUseCaseImpl( - keyPackageRepository, currentClientIdProvider, keyPackageLimitsProvider - ) + fun arrange(block: suspend Arrangement.() -> Unit) = apply { runBlocking { block() } }.let { + this to MLSKeyPackageCountUseCaseImpl( + keyPackageRepository, currentClientIdProvider, keyPackageLimitsProvider, userConfigRepository + ) + } companion object { val NETWORK_FAILURE = NetworkFailure.NoNetworkConnection(null) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetNextAudioMessageInConversationUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetNextAudioMessageInConversationUseCaseTest.kt new file mode 100644 index 00000000000..a869cd8938a --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetNextAudioMessageInConversationUseCaseTest.kt @@ -0,0 +1,109 @@ +/* + * Wire + * Copyright (C) 2024 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.message + +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.Message +import com.wire.kalium.logic.data.message.MessageRepository +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.framework.TestMessage +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.test_util.TestKaliumDispatcher +import com.wire.kalium.util.KaliumDispatcher +import io.mockative.Mock +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class GetNextAudioMessageInConversationUseCaseTest { + + private val testDispatchers: KaliumDispatcher = TestKaliumDispatcher + + @Test + fun givenMessageAndConversationId_whenInvokingUseCase_thenShouldCallMessageRepository() = runTest(testDispatchers.io) { + val (arrangement, getMessageByIdUseCase) = Arrangement() + .withRepositoryMessageByIdReturning(CONVERSATION_ID, MESSAGE_ID, Either.Left(StorageFailure.DataNotFound)) + .arrange() + + getMessageByIdUseCase(CONVERSATION_ID, MESSAGE_ID) + + coVerify { + arrangement.messageRepository.getMessageById(CONVERSATION_ID, MESSAGE_ID) + }.wasInvoked(exactly = once) + } + + @Test + fun givenRepositoryFails_whenInvokingUseCase_thenShouldPropagateTheFailure() = runTest(testDispatchers.io) { + val cause = StorageFailure.DataNotFound + val (_, getMessageByIdUseCase) = Arrangement() + .withRepositoryMessageByIdReturning(CONVERSATION_ID, MESSAGE_ID, Either.Left(cause)) + .arrange() + + val result = getMessageByIdUseCase(CONVERSATION_ID, MESSAGE_ID) + + assertIs(result) + assertEquals(cause, result.cause) + } + + @Test + fun givenRepositorySucceeds_whenInvokingUseCase_thenShouldPropagateTheSuccess() = runTest(testDispatchers.io) { + val (_, getMessageByIdUseCase) = Arrangement() + .withRepositoryMessageByIdReturning(CONVERSATION_ID, MESSAGE_ID, Either.Right(MESSAGE)) + .arrange() + + val result = getMessageByIdUseCase(CONVERSATION_ID, MESSAGE_ID) + + assertIs(result) + assertEquals(MESSAGE, result.message) + } + + private inner class Arrangement { + + @Mock + val messageRepository: MessageRepository = mock(MessageRepository::class) + + private val getMessageById by lazy { + GetMessageByIdUseCase(messageRepository, testDispatchers) + } + + suspend fun withRepositoryMessageByIdReturning( + conversationId: ConversationId, + messageId: String, + response: Either + ) = apply { + coEvery { + messageRepository.getMessageById(conversationId, messageId) + }.returns(response) + } + + fun arrange() = this to getMessageById + } + + private companion object { + const val MESSAGE_ID = TestMessage.TEST_MESSAGE_ID + val MESSAGE = TestMessage.TEXT_MESSAGE + val CONVERSATION_ID = TestConversation.ID + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCaseTest.kt new file mode 100644 index 00000000000..137dd1839f9 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetSenderNameByMessageIdUseCaseTest.kt @@ -0,0 +1,108 @@ +/* + * Wire + * Copyright (C) 2024 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.message + +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.MessageRepository +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.framework.TestMessage +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.test_util.TestKaliumDispatcher +import com.wire.kalium.util.KaliumDispatcher +import io.mockative.Mock +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class GetSenderNameByMessageIdUseCaseTest { + + private val testDispatchers: KaliumDispatcher = TestKaliumDispatcher + + @Test + fun givenMessageAndConversationId_whenInvokingUseCase_thenShouldCallMessageRepository() = runTest(testDispatchers.io) { + val (arrangement, getSenderNameByMessageId) = Arrangement() + .withRepositorySenderNameByMessageIdReturning(CONVERSATION_ID, MESSAGE_ID, Either.Left(StorageFailure.DataNotFound)) + .arrange() + + getSenderNameByMessageId(CONVERSATION_ID, MESSAGE_ID) + + coVerify { + arrangement.messageRepository.getSenderNameByMessageId(CONVERSATION_ID, MESSAGE_ID) + }.wasInvoked(exactly = once) + } + + @Test + fun givenRepositoryFails_whenInvokingUseCase_thenShouldPropagateTheFailure() = runTest(testDispatchers.io) { + val cause = StorageFailure.DataNotFound + val (_, getSenderNameByMessageId) = Arrangement() + .withRepositorySenderNameByMessageIdReturning(CONVERSATION_ID, MESSAGE_ID, Either.Left(cause)) + .arrange() + + val result = getSenderNameByMessageId(CONVERSATION_ID, MESSAGE_ID) + + assertIs(result) + assertEquals(cause, result.cause) + } + + @Test + fun givenRepositorySucceeds_whenInvokingUseCase_thenShouldPropagateTheSuccess() = runTest(testDispatchers.io) { + val (_, getSenderNameByMessageId) = Arrangement() + .withRepositorySenderNameByMessageIdReturning(CONVERSATION_ID, MESSAGE_ID, Either.Right(NAME)) + .arrange() + + val result = getSenderNameByMessageId(CONVERSATION_ID, MESSAGE_ID) + + assertIs(result) + assertEquals(NAME, result.name) + } + + private inner class Arrangement { + + @Mock + val messageRepository: MessageRepository = mock(MessageRepository::class) + + private val getSenderNameByMessageId by lazy { + GetSenderNameByMessageIdUseCase(messageRepository, testDispatchers) + } + + suspend fun withRepositorySenderNameByMessageIdReturning( + conversationId: ConversationId, + messageId: String, + response: Either + ) = apply { + coEvery { + messageRepository.getSenderNameByMessageId(conversationId, messageId) + }.returns(response) + } + + fun arrange() = this to getSenderNameByMessageId + } + + private companion object { + const val MESSAGE_ID = TestMessage.TEST_MESSAGE_ID + const val NAME = "Test User" + val CONVERSATION_ID = TestConversation.ID + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/protocol/OneOnOneProtocolSelectorTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/protocol/OneOnOneProtocolSelectorTest.kt index 50623732ab5..b0a9f068757 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/protocol/OneOnOneProtocolSelectorTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/protocol/OneOnOneProtocolSelectorTest.kt @@ -166,48 +166,6 @@ class OneOnOneProtocolSelectorTest { } } - @Test - fun givenUsersHaveProtocolInCommonButDiffersWithDefaultProtocol_thenShouldReturnNoCommonProtocol() = runTest { - val (_, oneOnOneProtocolSelector) = arrange { - withSelfUserReturning(TestUser.SELF.copy(supportedProtocols = setOf(SupportedProtocol.MLS))) - withUserByIdReturning(Either.Right(TestUser.OTHER.copy(supportedProtocols = setOf(SupportedProtocol.MLS)))) - withGetDefaultProtocolReturning(SupportedProtocol.PROTEUS.right()) - } - - oneOnOneProtocolSelector.getProtocolForUser(TestUser.USER_ID) - .shouldFail { - assertIs(it) - } - } - - @Test - fun givenSelfUserSupportsDefaultProtocolButOtherUserDoesnt_thenShouldReturnNoCommonProtocol() = runTest { - val (_, oneOnOneProtocolSelector) = arrange { - withSelfUserReturning(TestUser.SELF.copy(supportedProtocols = setOf(SupportedProtocol.MLS, SupportedProtocol.PROTEUS))) - withUserByIdReturning(Either.Right(TestUser.OTHER.copy(supportedProtocols = setOf(SupportedProtocol.MLS)))) - withGetDefaultProtocolReturning(SupportedProtocol.PROTEUS.right()) - } - - oneOnOneProtocolSelector.getProtocolForUser(TestUser.USER_ID) - .shouldFail { - assertIs(it) - } - } - - @Test - fun givenSelfUserDoesntSupportsDefaultProtocolButOtherUserDoes_thenShouldReturnNoCommonProtocol() = runTest { - val (_, oneOnOneProtocolSelector) = arrange { - withSelfUserReturning(TestUser.SELF.copy(supportedProtocols = setOf(SupportedProtocol.MLS))) - withUserByIdReturning(Either.Right(TestUser.OTHER.copy(supportedProtocols = setOf(SupportedProtocol.MLS, SupportedProtocol.PROTEUS)))) - withGetDefaultProtocolReturning(SupportedProtocol.PROTEUS.right()) - } - - oneOnOneProtocolSelector.getProtocolForUser(TestUser.USER_ID) - .shouldFail { - assertIs(it) - } - } - @Test fun givenUsersHaveProtocolInCommonIncludingDefaultProtocol_thenShouldReturnDefaultProtocolAsCommonProtocol() = runTest { val (_, oneOnOneProtocolSelector) = arrange { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/SearchUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/SearchUseCaseTest.kt index 821f32c3a2b..aafedb5a00c 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/SearchUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/SearchUseCaseTest.kt @@ -243,7 +243,7 @@ class SearchUseCaseTest { private class Arrangement : SearchRepositoryArrangement by SearchRepositoryArrangementImpl() { - private val searchUseCase: SearchUsersUseCase = SearchUsersUseCase( + private val searchUseCase: SearchUsersUseCase = SearchUsersUseCaseImpl( searchUserRepository = searchUserRepository, selfUserId = selfUserID, maxRemoteSearchResultCount = 30 diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/ObserveUserInfoUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/ObserveUserInfoUseCaseTest.kt index 715110ba89b..686be4c5da1 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/ObserveUserInfoUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/ObserveUserInfoUseCaseTest.kt @@ -20,6 +20,7 @@ package com.wire.kalium.logic.feature.user import app.cash.turbine.test import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.team.TeamRepository import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository @@ -231,6 +232,32 @@ class ObserveUserInfoUseCaseTest { } } + @Test + fun givenAUserIdWhichIsNotInDBAndNotOnServer_whenInvokingObserveUserInfo_thenErrorIsReturned() = runTest { + // given + val (arrangement, useCase) = arrangement + .withFailingUserRetrieveFromDB() + .withSuccessfulTeamRetrieve(localTeamPresent = true) + .withSuccessfulUserFetchingNoUsersFound() + .arrange() + + // when + useCase(userId).test { + val result = awaitItem() + + // then + assertIs(result) + + with(arrangement) { + coVerify { + userRepository.fetchUsersByIds(any()) + }.wasInvoked(once) + } + + awaitComplete() + } + } + private class ObserveUserInfoUseCaseTestArrangement { @Mock @@ -276,7 +303,15 @@ class ObserveUserInfoUseCaseTest { suspend fun withSuccessfulUserFetching(): ObserveUserInfoUseCaseTestArrangement { coEvery { userRepository.fetchUsersByIds(any()) - }.returns(Either.Right(Unit)) + }.returns(Either.Right(true)) + + return this + } + + suspend fun withSuccessfulUserFetchingNoUsersFound(): ObserveUserInfoUseCaseTestArrangement { + coEvery { + userRepository.fetchUsersByIds(any()) + }.returns(Either.Right(false)) return this } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCaseTest.kt index 23f8ce4db9f..8cade1ee962 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCaseTest.kt @@ -23,6 +23,7 @@ import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.user.CreateUserTeam import com.wire.kalium.logic.data.user.UserRepository +import com.wire.kalium.logic.feature.user.SyncContactsUseCase import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.logic.functional.Either import com.wire.kalium.network.api.model.ErrorResponse @@ -47,6 +48,7 @@ class MigrateFromPersonalToTeamUseCaseTest { val (arrangement, useCase) = Arrangement() .withUpdateTeamIdReturning(Either.Right(Unit)) .withMigrationSuccess() + .withSyncContactsSuccess() .arrange() val result = useCase(teamName = "teamName") @@ -54,6 +56,10 @@ class MigrateFromPersonalToTeamUseCaseTest { coVerify { arrangement.userRepository.updateTeamId(any(), any()) }.wasInvoked(exactly = once) + + coVerify { + arrangement.syncContacts() }.wasInvoked(exactly = once) + assertTrue(arrangement.isCachedTeamIdInvalidated) assertIs(result) } @@ -107,9 +113,13 @@ class MigrateFromPersonalToTeamUseCaseTest { private class Arrangement { + @Mock val userRepository: UserRepository = mock(UserRepository::class) + @Mock + val syncContacts: SyncContactsUseCase = mock(SyncContactsUseCase::class) + var isCachedTeamIdInvalidated = false suspend fun withMigrationSuccess() = apply { @@ -134,7 +144,6 @@ class MigrateFromPersonalToTeamUseCaseTest { ) ) - suspend fun withMigrationUserNotFoundFailure() = withMigrationReturning( Either.Left( NetworkFailure.ServerMiscommunication( @@ -149,6 +158,10 @@ class MigrateFromPersonalToTeamUseCaseTest { ) ) + suspend fun withSyncContactsSuccess() = apply { + coEvery { syncContacts.invoke() }.returns(Either.Right(Unit)) + } + suspend fun withMigrationNoNetworkFailure() = withMigrationReturning( Either.Left(NetworkFailure.NoNetworkConnection(null)) ) @@ -163,6 +176,7 @@ class MigrateFromPersonalToTeamUseCaseTest { fun arrange() = this to MigrateFromPersonalToTeamUseCaseImpl(selfUserId = TestUser.SELF.id, userRepository = userRepository, + syncContacts = syncContacts, invalidateTeamId = { isCachedTeamIdInvalidated = true }) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/ClearConversationContentHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/ClearConversationContentHandlerTest.kt new file mode 100644 index 00000000000..39ce8e1d7ad --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/ClearConversationContentHandlerTest.kt @@ -0,0 +1,212 @@ +/* + * Wire + * Copyright (C) 2024 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.sync.receiver.conversation + +import com.wire.kalium.logic.data.conversation.ClientId +import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.IsMessageSentInSelfConversationUseCase +import com.wire.kalium.logic.data.message.Message +import com.wire.kalium.logic.data.message.MessageContent +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.framework.TestUser +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.sync.receiver.handler.ClearConversationContentHandler +import com.wire.kalium.logic.sync.receiver.handler.ClearConversationContentHandlerImpl +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant +import kotlin.test.Test + +class ClearConversationContentHandlerTest { + + @Test + fun givenMessageFromOtherClient_whenMessageNeedsToBeRemovedLocallyAndUserIsNotPartOfConversation_thenWholeConversationShouldBeDeleted() = + runTest { + // given + val (arrangement, handler) = Arrangement() + .withMessageSentInSelfConversation(false) + .arrange() + + // when + handler.handle( + message = MESSAGE, + messageContent = MessageContent.Cleared( + conversationId = CONVERSATION_ID, + time = Instant.DISTANT_PAST, + needToRemoveLocally = true + ) + ) + + // then + coVerify { arrangement.conversationRepository.deleteConversation(any()) } + .wasInvoked(exactly = once) + coVerify { arrangement.conversationRepository.clearContent(any()) } + .wasNotInvoked() + } + + @Test + fun givenMessageFromOtherClient_whenMessageNeedsToBeRemovedLocallyAndUserIsPartOfConversation_thenOnlyContentShouldBeCleared() = + runTest { + // given + val (arrangement, handler) = Arrangement() + .withMessageSentInSelfConversation(true) + .arrange() + + // when + handler.handle( + message = MESSAGE, + messageContent = MessageContent.Cleared( + conversationId = CONVERSATION_ID, + time = Instant.DISTANT_PAST, + needToRemoveLocally = true + ) + ) + + // then + coVerify { arrangement.conversationRepository.deleteConversation(any()) } + .wasNotInvoked() + coVerify { arrangement.conversationRepository.clearContent(any()) } + .wasInvoked(exactly = once) + } + + @Test + fun givenMessageFromOtherClient_whenMessageDoesNotNeedToBeRemovedAndUserIsNotPartOfConversation_thenContentNorConversationShouldBeRemoved() = + runTest { + // given + val (arrangement, handler) = Arrangement() + .withMessageSentInSelfConversation(false) + .arrange() + + // when + handler.handle( + message = MESSAGE, + messageContent = MessageContent.Cleared( + conversationId = CONVERSATION_ID, + time = Instant.DISTANT_PAST, + needToRemoveLocally = false + ) + ) + + // then + coVerify { arrangement.conversationRepository.deleteConversation(any()) } + .wasNotInvoked() + coVerify { arrangement.conversationRepository.clearContent(any()) } + .wasNotInvoked() + } + + @Test + fun givenMessageFromOtherClient_whenMessageDoesNotNeedToBeRemovedAndUserIsPartOfConversation_thenContentShouldBeRemoved() = runTest { + // given + val (arrangement, handler) = Arrangement() + .withMessageSentInSelfConversation(true) + .arrange() + + // when + handler.handle( + message = MESSAGE, + messageContent = MessageContent.Cleared( + conversationId = CONVERSATION_ID, + time = Instant.DISTANT_PAST, + needToRemoveLocally = false + ) + ) + + // then + coVerify { arrangement.conversationRepository.deleteConversation(any()) } + .wasNotInvoked() + coVerify { arrangement.conversationRepository.clearContent(any()) } + .wasInvoked(exactly = once) + } + + @Test + fun givenMessageFromTheSameClient_whenHandleIsInvoked_thenContentNorConversationShouldBeRemoved() = runTest { + // given + val (arrangement, handler) = Arrangement() + .withMessageSentInSelfConversation(true) + .arrange() + + // when + handler.handle( + message = OWN_MESSAGE, + messageContent = MessageContent.Cleared( + conversationId = CONVERSATION_ID, + time = Instant.DISTANT_PAST, + needToRemoveLocally = false + ) + ) + + // then + coVerify { arrangement.conversationRepository.deleteConversation(any()) } + .wasNotInvoked() + coVerify { arrangement.conversationRepository.clearContent(any()) } + .wasNotInvoked() + } + + + private class Arrangement { + @Mock + val conversationRepository = mock(ConversationRepository::class) + + @Mock + val isMessageSentInSelfConversationUseCase = mock(IsMessageSentInSelfConversationUseCase::class) + + suspend fun withMessageSentInSelfConversation(isSentInSelfConv: Boolean) = apply { + coEvery { isMessageSentInSelfConversationUseCase(any()) }.returns(isSentInSelfConv) + } + + suspend fun arrange(): Pair = + this to ClearConversationContentHandlerImpl( + conversationRepository = conversationRepository, + selfUserId = TestUser.USER_ID, + isMessageSentInSelfConversation = isMessageSentInSelfConversationUseCase, + ).apply { + coEvery { conversationRepository.deleteConversation(any()) }.returns(Either.Right(Unit)) + coEvery { conversationRepository.clearContent(any()) }.returns(Either.Right(Unit)) + } + } + + companion object { + private val CONVERSATION_ID = ConversationId("conversationId", "domain") + private val OTHER_USER_ID = UserId("otherUserId", "domain") + + private val MESSAGE_CONTENT = MessageContent.DataTransfer( + trackingIdentifier = MessageContent.DataTransfer.TrackingIdentifier( + identifier = "abcd-1234-efgh-5678" + ) + ) + val MESSAGE = Message.Signaling( + id = "messageId", + content = MESSAGE_CONTENT, + conversationId = CONVERSATION_ID, + date = Instant.DISTANT_PAST, + senderUserId = OTHER_USER_ID, + senderClientId = ClientId("deviceId"), + status = Message.Status.Sent, + isSelfMessage = false, + expirationData = null, + ) + + val OWN_MESSAGE = MESSAGE.copy(senderUserId = TestUser.USER_ID) + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ApplicationMessageHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ApplicationMessageHandlerTest.kt index 6aaa4706e59..b094687ec6b 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ApplicationMessageHandlerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ApplicationMessageHandlerTest.kt @@ -22,6 +22,7 @@ import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.configuration.FileSharingStatus import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.data.call.InCallReactionsRepository import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.message.AssetContent import com.wire.kalium.logic.data.message.MessageContent @@ -172,6 +173,41 @@ class ApplicationMessageHandlerTest { }.wasInvoked(exactly = once) } + @Test + fun givenInCallReactionReceived_whenHandling_thenCorrectHandlerIsInvoked() = runTest { + // given + val messageId = "messageId" + val inCallReactionContent = MessageContent.InCallEmoji( + emojis = mapOf("1" to 1) + ) + val protoContent = ProtoContent.Readable( + messageId, + inCallReactionContent, + false, + Conversation.LegalHoldStatus.DISABLED + ) + + val (arrangement, messageHandler) = Arrangement() + .arrange() + + val encodedEncryptedContent = Base64.encodeToBase64("Hello".encodeToByteArray()) + val messageEvent = TestEvent.newMessageEvent(encodedEncryptedContent.decodeToString()) + + // when + messageHandler.handleContent( + messageEvent.conversationId, + messageEvent.messageInstant, + messageEvent.senderUserId, + messageEvent.senderClientId, + protoContent + ) + + // then + coVerify { + arrangement.inCallReactionsRepository.addInCallReaction(messageEvent.conversationId, messageEvent.senderUserId, setOf("1")) + }.wasInvoked(exactly = once) + } + private class Arrangement { @Mock val persistMessage = mock(PersistMessageUseCase::class) @@ -215,6 +251,9 @@ class ApplicationMessageHandlerTest { @Mock val buttonActionConfirmationHandler = mock(ButtonActionConfirmationHandler::class) + @Mock + val inCallReactionsRepository = mock(InCallReactionsRepository::class) + @Mock val dataTransferEventHandler = mock(DataTransferEventHandler::class) @@ -234,6 +273,7 @@ class ApplicationMessageHandlerTest { receiptMessageHandler, buttonActionConfirmationHandler, dataTransferEventHandler, + inCallReactionsRepository, TestUser.SELF.id ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/mls/OneOnOneMigratorArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/mls/OneOnOneMigratorArrangement.kt index 36207e4774e..305b88f50fd 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/mls/OneOnOneMigratorArrangement.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/mls/OneOnOneMigratorArrangement.kt @@ -33,6 +33,8 @@ interface OneOnOneMigratorArrangement { suspend fun withMigrateToMLSReturns(result: Either) suspend fun withMigrateToProteusReturns(result: Either) + + suspend fun withMigrateExistingToProteusReturns(result: Either) } class OneOnOneMigratorArrangementImpl : OneOnOneMigratorArrangement { @@ -51,4 +53,10 @@ class OneOnOneMigratorArrangementImpl : OneOnOneMigratorArrangement { oneOnOneMigrator.migrateToProteus(any()) }.returns(result) } + + override suspend fun withMigrateExistingToProteusReturns(result: Either) { + coEvery { + oneOnOneMigrator.migrateExistingProteus(any()) + }.returns(result) + } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserConfigRepositoryArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserConfigRepositoryArrangement.kt index 501f9521a5c..97fbdecb2e8 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserConfigRepositoryArrangement.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserConfigRepositoryArrangement.kt @@ -38,6 +38,7 @@ internal interface UserConfigRepositoryArrangement { fun withSetDefaultProtocolSuccessful() fun withGetDefaultProtocolReturning(result: Either) fun withSetMLSEnabledSuccessful() + fun withGetMLSEnabledReturning(result: Either) suspend fun withSetMigrationConfigurationSuccessful() suspend fun withGetMigrationConfigurationReturning(result: Either) suspend fun withSetSupportedCipherSuite(result: Either) @@ -84,6 +85,12 @@ internal class UserConfigRepositoryArrangementImpl : UserConfigRepositoryArrange }.returns(Either.Right(Unit)) } + override fun withGetMLSEnabledReturning(result: Either) { + every { + userConfigRepository.isMLSEnabled() + }.returns(result) + } + override suspend fun withGetSupportedCipherSuitesReturning(result: Either) { coEvery { userConfigRepository.getSupportedCipherSuite() }.returns(result) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserRepositoryArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserRepositoryArrangement.kt index 25fecc6da7c..6f1ce40c292 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserRepositoryArrangement.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserRepositoryArrangement.kt @@ -19,6 +19,7 @@ package com.wire.kalium.logic.util.arrangement.repository import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.conversation.mls.NameAndHandle import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedID @@ -72,7 +73,7 @@ internal interface UserRepositoryArrangement { suspend fun withFetchUserInfoReturning(result: Either) suspend fun withFetchUsersByIdReturning( - result: Either, + result: Either, userIdList: Matcher> = AnyMatcher(valueOf()) ) @@ -95,6 +96,12 @@ internal interface UserRepositoryArrangement { ) suspend fun withNameAndHandle(result: Either, userId: Matcher = AnyMatcher(valueOf())) + + suspend fun withIsClientMlsCapable( + result: Either, + userId: Matcher = AnyMatcher(valueOf()), + clientId: Matcher = AnyMatcher(valueOf()) + ) } @Suppress("INAPPLICABLE_JVM_NAME") @@ -186,7 +193,7 @@ internal open class UserRepositoryArrangementImpl : UserRepositoryArrangement { } override suspend fun withFetchUsersByIdReturning( - result: Either, + result: Either, userIdList: Matcher> ) { coEvery { @@ -233,4 +240,13 @@ internal open class UserRepositoryArrangementImpl : UserRepositoryArrangement { override suspend fun withNameAndHandle(result: Either, userId: Matcher) { coEvery { userRepository.getNameAndHandle(matches { userId.matches(it) }) }.returns(result) } + + override suspend fun withIsClientMlsCapable(result: Either, userId: Matcher, clientId: Matcher) { + coEvery { + userRepository.isClientMlsCapable( + userId = matches { userId.matches(it) }, + clientId = matches { clientId.matches(it) } + ) + }.returns(result) + } } diff --git a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCallTest.kt b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCallTest.kt index 56fb0aec4e2..aa34f1b8a9a 100644 --- a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCallTest.kt +++ b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCallTest.kt @@ -19,7 +19,6 @@ package com.wire.kalium.logic.feature.call.scenario import com.wire.kalium.calling.CallClosedReason import com.wire.kalium.calling.types.Uint32_t -import com.wire.kalium.logic.data.call.CallHelper import com.wire.kalium.logic.data.call.CallMetadata import com.wire.kalium.logic.data.call.CallMetadataProfile import com.wire.kalium.logic.data.call.CallRepository @@ -29,11 +28,13 @@ import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.GroupID import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl import com.wire.kalium.logic.data.mls.CipherSuite +import com.wire.kalium.logic.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase import com.wire.kalium.logic.framework.TestCall import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.network.NetworkState import com.wire.kalium.network.NetworkStateObserver import io.mockative.Mock +import io.mockative.any import io.mockative.coVerify import io.mockative.eq import io.mockative.every @@ -55,6 +56,9 @@ class OnCloseCallTest { @Mock val networkStateObserver = mock(NetworkStateObserver::class) + @Mock + val createAndPersistRecentlyEndedCallMetadata = mock(CreateAndPersistRecentlyEndedCallMetadataUseCase::class) + val qualifiedIdMapper = QualifiedIdMapperImpl(TestUser.SELF.id) private lateinit var onCloseCall: OnCloseCall @@ -69,7 +73,8 @@ class OnCloseCallTest { callRepository, testScope, qualifiedIdMapper, - networkStateObserver + networkStateObserver, + createAndPersistRecentlyEndedCallMetadata ) every { @@ -338,6 +343,26 @@ class OnCloseCallTest { }.wasNotInvoked() } + @Test + fun givenClosedCall_whenOnCloseCallInvoked_thenCreateAndPersistRecentlyEndedCallIsInvoked() = + testScope.runTest { + val reason = CallClosedReason.CANCELLED.avsValue + + onCloseCall.onClosedCall( + reason, + conversationIdString, + time, + userIdString, + clientId, + null + ) + yield() + + coVerify { + createAndPersistRecentlyEndedCallMetadata(any(), any()) + }.wasInvoked(once) + } + companion object { private val conversationId = ConversationId("conversationId", "wire.com") private const val conversationIdString = "conversationId@wire.com" diff --git a/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt b/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt index 39f73fdb694..4491d1bf47c 100644 --- a/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt +++ b/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt @@ -51,7 +51,14 @@ actual fun userDatabaseBuilder( ) { isWALEnabled = enableWAL } - return UserDatabaseBuilder(userId, driver, dispatcher, platformDatabaseData, isEncryptionEnabled) + return UserDatabaseBuilder( + userId = userId, + sqlDriver = driver, + dispatcher = dispatcher, + platformDatabaseData = platformDatabaseData, + isEncrypted = isEncryptionEnabled, + cipherProfile = "logcat", + ) } actual fun userDatabaseDriverByPath( diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Clients.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Clients.sq index 709b3613d56..03bbdd7fbb5 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Clients.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Clients.sq @@ -74,6 +74,9 @@ SELECT * FROM Client WHERE user_id = :user_id AND id = :client_id; deleteClientsOfUserExcept: DELETE FROM Client WHERE user_id = :user_id AND id NOT IN :exception_ids; +isClientMLSCapable: +SELECT is_mls_capable FROM Client WHERE user_id = :user_id AND id = :client_id; + tryMarkAsInvalid: UPDATE OR IGNORE Client SET is_valid = 0 WHERE user_id = :user_id AND id IN :clientId_List; diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq index db44a482e77..38d2c6db4bf 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq @@ -17,6 +17,10 @@ CREATE TABLE LabeledConversation ( PRIMARY KEY (folder_id, conversation_id) ); +getUserFolders: +SELECT * FROM ConversationFolder +WHERE folder_type != 'FAVORITE'; + getAllFoldersWithConversations: SELECT conversationFolder.id AS label_id, @@ -60,8 +64,8 @@ VALUES(?, ?); deleteLabeledConversation: DELETE FROM LabeledConversation WHERE conversation_id = ? AND folder_id = ?; -clearFolders: -DELETE FROM ConversationFolder; - clearLabeledConversations: DELETE FROM LabeledConversation; + +clearFolders: +DELETE FROM ConversationFolder; diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageAssetView.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageAssetView.sq index 83085dafdbf..59762233238 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageAssetView.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageAssetView.sq @@ -81,3 +81,11 @@ AND assetMimeType NOT IN :mimeTypes AND assetId IS NOT NULL AND expireAfterMillis IS NULL ORDER BY date DESC; + +getAllAssetMessagesByConversationId: +SELECT assetId FROM MessageAssetView +WHERE conversationId = :conversationId +AND contentType IN :contentTypes +AND assetId IS NOT NULL +AND dataPath IS NOT NULL +ORDER BY date DESC; 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 90308374887..a25d9a938bb 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 @@ -608,3 +608,11 @@ WHERE conversation_id = :conversationId AND creation_date >= (SELECT creation_date FROM Message WHERE id = :messageId LIMIT 1) AND expire_after_millis IS NULL ORDER BY creation_date DESC; + +selectNextAudioMessage: +SELECT Message.id +FROM Message LEFT JOIN MessageAssetContent AS AssetContent ON Message.id = AssetContent.message_id AND Message.conversation_id = AssetContent.conversation_id +WHERE Message.conversation_id = :conversationId +AND AssetContent.asset_mime_type LIKE "%audio/%" +AND Message.creation_date > (SELECT Message.creation_date FROM Message WHERE Message.id = :messageId AND Message.conversation_id = :conversationId) +LIMIT 1; diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq index 385d13f2602..1a36f2801fa 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq @@ -276,3 +276,7 @@ SELECT name, handle FROM User WHERE qualified_id = :userId; updateTeamId: UPDATE User SET team = ? WHERE qualified_id = ?; + +selectNameByMessageId: +SELECT name FROM User +WHERE qualified_id = (SELECT Message.sender_user_id FROM Message WHERE Message.id = :messageId AND Message.conversation_id = :conversationId); diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAO.kt index 4b6d1a5e17b..7b5a9d87d18 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAO.kt @@ -97,4 +97,5 @@ interface ClientDAO { ): Map> suspend fun selectAllClients(): Map> + suspend fun isMLSCapable(userId: QualifiedIDEntity, clientId: String): Boolean? } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOImpl.kt index b30b2a8ebec..79935606535 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOImpl.kt @@ -155,6 +155,11 @@ internal class ClientDAOImpl internal constructor( .executeAsList() .groupBy { it.userId } + override suspend fun isMLSCapable(userId: QualifiedIDEntity, clientId: String): Boolean? = withContext(queriesContext) { + clientsQueries.isClientMLSCapable(userId, clientId) + .executeAsOneOrNull() + } + override suspend fun getClientsOfUserByQualifiedIDFlow(qualifiedID: QualifiedIDEntity): Flow> = clientsQueries.selectAllClientsByUserId(qualifiedID, mapper::fromClient) .asFlow() diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt index ed1ae82a9f8..0c201de5b8d 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt @@ -24,8 +24,9 @@ import kotlinx.coroutines.flow.Flow interface ConversationFolderDAO { suspend fun getFoldersWithConversations(): List suspend fun observeConversationListFromFolder(folderId: String): Flow> - suspend fun getFavoriteConversationFolder(): ConversationFolderEntity + suspend fun getFavoriteConversationFolder(): ConversationFolderEntity? suspend fun updateConversationFolders(folderWithConversationsList: List) suspend fun addConversationToFolder(conversationId: QualifiedIDEntity, folderId: String) suspend fun removeConversationFromFolder(conversationId: QualifiedIDEntity, folderId: String) + suspend fun observeUserFolders(): Flow> } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt index 3316f030bd9..6e703b82419 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt @@ -18,6 +18,7 @@ package com.wire.kalium.persistence.dao.conversation.folder import app.cash.sqldelight.coroutines.asFlow +import com.wire.kalium.persistence.ConversationFolder import com.wire.kalium.persistence.ConversationFoldersQueries import com.wire.kalium.persistence.GetAllFoldersWithConversations import com.wire.kalium.persistence.dao.QualifiedIDEntity @@ -26,6 +27,7 @@ import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEvent import com.wire.kalium.persistence.util.mapToList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import kotlin.coroutines.CoroutineContext @@ -35,6 +37,14 @@ class ConversationFolderDAOImpl internal constructor( ) : ConversationFolderDAO { private val conversationDetailsWithEventsMapper = ConversationDetailsWithEventsMapper + override suspend fun observeUserFolders(): Flow> { + return conversationFoldersQueries.getUserFolders() + .asFlow() + .mapToList() + .map { it.map(::toEntity) } + .flowOn(coroutineContext) + } + override suspend fun getFoldersWithConversations(): List = withContext(coroutineContext) { val labeledConversationList = conversationFoldersQueries.getAllFoldersWithConversations().executeAsList().map(::toEntity) @@ -59,6 +69,12 @@ class ConversationFolderDAOImpl internal constructor( conversationId = row.conversation_id ) + private fun toEntity(row: ConversationFolder) = ConversationFolderEntity( + id = row.id, + name = row.name, + type = row.folder_type + ) + override suspend fun observeConversationListFromFolder(folderId: String): Flow> { return conversationFoldersQueries.getConversationsFromFolder( folderId, @@ -69,11 +85,11 @@ class ConversationFolderDAOImpl internal constructor( .flowOn(coroutineContext) } - override suspend fun getFavoriteConversationFolder(): ConversationFolderEntity { + override suspend fun getFavoriteConversationFolder(): ConversationFolderEntity? { return conversationFoldersQueries.getFavoriteFolder { id, name, folderType -> ConversationFolderEntity(id, name, folderType) } - .executeAsOne() + .executeAsOneOrNull() } override suspend fun updateConversationFolders(folderWithConversationsList: List) = diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt index e37edbceea0..86963c7b9ba 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt @@ -161,4 +161,7 @@ interface MessageDAO { suspend fun observeAssetStatuses(conversationId: QualifiedIDEntity): Flow> suspend fun getMessageAssetTransferStatus(messageId: String, conversationId: QualifiedIDEntity): AssetTransferStatusEntity + suspend fun getAllMessageAssetIdsForConversationId(conversationId: QualifiedIDEntity): List + suspend fun getSenderNameById(id: String, conversationId: QualifiedIDEntity): String? + suspend fun getNextAudioMessageInConversation(prevMessageId: String, conversationId: QualifiedIDEntity): String? } 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 06d1f66ba3d..a27ccb945d3 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 @@ -27,6 +27,7 @@ import com.wire.kalium.persistence.MessagesQueries import com.wire.kalium.persistence.NotificationQueries import com.wire.kalium.persistence.ReactionsQueries import com.wire.kalium.persistence.UnreadEventsQueries +import com.wire.kalium.persistence.UsersQueries import com.wire.kalium.persistence.content.ButtonContentQueries import com.wire.kalium.persistence.dao.ConversationIDEntity import com.wire.kalium.persistence.dao.QualifiedIDEntity @@ -59,6 +60,7 @@ internal class MessageDAOImpl internal constructor( private val messagePreviewQueries: MessagePreviewQueries, private val selfUserId: UserIDEntity, private val reactionsQueries: ReactionsQueries, + private val userQueries: UsersQueries, private val coroutineContext: CoroutineContext, private val assetStatusQueries: MessageAssetTransferStatusQueries, buttonContentQueries: ButtonContentQueries @@ -505,6 +507,26 @@ internal class MessageDAOImpl internal constructor( .executeAsOne() } + override suspend fun getAllMessageAssetIdsForConversationId( + conversationId: QualifiedIDEntity + ): List { + return withContext(coroutineContext) { + assetViewQueries.getAllAssetMessagesByConversationId( + conversationId, + listOf(MessageEntity.ContentType.ASSET) + ).executeAsList().mapNotNull { it.assetId } + } + } + + override suspend fun getSenderNameById(id: String, conversationId: QualifiedIDEntity): String? = withContext(coroutineContext) { + userQueries.selectNameByMessageId(id, conversationId).executeAsOneOrNull()?.name + } + + override suspend fun getNextAudioMessageInConversation(prevMessageId: String, conversationId: QualifiedIDEntity): String? = + withContext(coroutineContext) { + queries.selectNextAudioMessage(conversationId, prevMessageId).executeAsOneOrNull() + } + override val platformExtensions: MessageExtensions = MessageExtensionsImpl(queries, assetViewQueries, mapper, coroutineContext) } 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 7c75ee447b6..76f9ab33e17 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 @@ -112,13 +112,15 @@ internal expect fun userDatabaseDriverByPath( enableWAL: Boolean ): SqlDriver +@Suppress("LongParameterList") class UserDatabaseBuilder internal constructor( private val userId: UserIDEntity, internal val sqlDriver: SqlDriver, dispatcher: CoroutineDispatcher, private val platformDatabaseData: PlatformDatabaseData, private val isEncrypted: Boolean, - private val queriesContext: CoroutineContext = KaliumDispatcherImpl.io + private val queriesContext: CoroutineContext = KaliumDispatcherImpl.io, + private val cipherProfile: String? = null, ) { internal val database: UserDatabase = UserDatabase( @@ -262,6 +264,7 @@ class UserDatabaseBuilder internal constructor( database.messagePreviewQueries, userId, database.reactionsQueries, + database.usersQueries, queriesContext, database.messageAssetTransferStatusQueries, database.buttonContentQueries @@ -316,6 +319,25 @@ class UserDatabaseBuilder internal constructor( */ fun dbFileLocation(): String? = getDatabaseAbsoluteFileLocation(platformDatabaseData, userId) + /** + * Changes the profiling of the database (cipher_profile) if the profile is specified and the database is encrypted + * @param enabled true to enable profiling, false to disable + */ + fun changeProfiling(enabled: Boolean) { + if (isEncrypted && cipherProfile != null) { + val cipherProfileValue = if (enabled) cipherProfile else "off" + sqlDriver.executeQuery( + identifier = null, + sql = "PRAGMA cipher_profile='$cipherProfileValue'", + mapper = { + it.next() + it.getLong(0).let { QueryResult.Value(it) } + }, + parameters = 0, + ) + } + } + /** * drops DB connection and delete the DB file */ diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOTest.kt index 7d352251f6e..4fa472f3ee1 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOTest.kt @@ -459,6 +459,69 @@ class ClientDAOTest : BaseDatabaseTest() { } } + @Test + fun givenClientIsNotMlsCapable_whenCallingIsMlsCapable_thenReturnFalse() = runTest { + val user = user + val client: InsertClientParam = insertedClient.copy(isMLSCapable = false) + userDAO.upsertUser(user) + clientDAO.insertClient(client) + assertFalse { clientDAO.isMLSCapable(userId, clientId = client.id)!! } + } + + @Test + fun givenClientIsMlsCapable_whenCallingIsMlsCapable_thenReturnTrue() = runTest { + val user = user + val client: InsertClientParam = insertedClient.copy(isMLSCapable = true) + userDAO.upsertUser(user) + clientDAO.insertClient(client) + assertTrue { clientDAO.isMLSCapable(userId, clientId = client.id)!! } + } + + @Test + fun givenNotFound_whenCallingIsMlsCapableForUser_thenReturnNull() = runTest { + val user = user + userDAO.upsertUser(user) + assertNull(clientDAO.isMLSCapable(userId, clientId = client.id)) + } + + @Test + fun givenPersistedClient_whenUpsertingTheSameExactClient_thenItShouldIgnoreAndNotNotifyOtherQueries() = runTest { + // Given + userDAO.upsertUser(user) + clientDAO.insertClient(insertedClient) + + clientDAO.observeClient(user.id, insertedClient.id).test { + val initialValue = awaitItem() + assertEquals(insertedClient.toClient(), initialValue) + + // When + clientDAO.insertClient(insertedClient) // the same exact client is being saved again + + // Then + expectNoEvents() // other query should not be notified + } + } + + @Test + fun givenPersistedClient_whenUpsertingUpdatedClient_thenItShouldBeSavedAndOtherQueriesShouldBeUpdated() = runTest { + // Given + userDAO.upsertUser(user) + clientDAO.insertClient(insertedClient) + val updatedInsertedClient = insertedClient.copy(label = "new_label") + + clientDAO.observeClient(user.id, insertedClient.id).test { + val initialValue = awaitItem() + assertEquals(insertedClient.toClient(), initialValue) + + // When + clientDAO.insertClient(updatedInsertedClient) // updated client is being saved that should replace the old one + + // Then + val updatedValue = awaitItem() // other query should be notified + assertEquals(updatedInsertedClient.toClient(), updatedValue) + } + } + private companion object { val userId = QualifiedIDEntity("test", "domain") val user = newUserEntity(userId) diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt index c55255dd733..d533ccc6dec 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt @@ -59,7 +59,8 @@ class ConversationFolderDAOTest : BaseDatabaseTest() { id = folderId, name = "folderName", type = ConversationFolderTypeEntity.USER, - conversationIdList = listOf(conversationEntity1.id)) + conversationIdList = listOf(conversationEntity1.id) + ) db.conversationFolderDAO.updateConversationFolders(listOf(conversationFolderEntity)) val result = db.conversationFolderDAO.observeConversationListFromFolder(folderId).first().first() @@ -79,12 +80,13 @@ class ConversationFolderDAOTest : BaseDatabaseTest() { id = folderId, name = "", type = ConversationFolderTypeEntity.FAVORITE, - conversationIdList = listOf(conversationEntity1.id)) + conversationIdList = listOf(conversationEntity1.id) + ) db.conversationFolderDAO.updateConversationFolders(listOf(conversationFolderEntity)) val result = db.conversationFolderDAO.getFavoriteConversationFolder() - assertEquals(folderId, result.id) + assertEquals(folderId, result?.id) } @Test diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt index da4724ba739..a365283d308 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt @@ -2344,6 +2344,116 @@ class MessageDAOTest : BaseDatabaseTest() { assertEquals(messages.size, assetStatuses.size) } + @Test + fun givenMessagesAndUsersAreInserted_whenGettingSenderNameByMessageId_thenOnlyRelevantNameReturned() = runTest { + insertInitialData() + + val userInQuestion = userDetailsEntity1 + val otherUser = userDetailsEntity2 + + val insertingMessages = listOf( + newRegularMessageEntity( + "1", + conversationId = conversationEntity1.id, + senderUserId = userInQuestion.id, + status = MessageEntity.Status.PENDING, + senderName = userInQuestion.name!!, + sender = userInQuestion + ), + newRegularMessageEntity( + "2", + conversationId = conversationEntity1.id, + senderUserId = otherUser.id, + status = MessageEntity.Status.PENDING, + senderName = otherUser.name!!, + sender = otherUser + ) + ) + messageDAO.insertOrIgnoreMessages(insertingMessages) + + val result = messageDAO.getSenderNameById("1", conversationEntity1.id) + + assertEquals(userDetailsEntity1.name, result) + } + + @Test + fun givenMessagesAreInserted_whenGettingSenderNameByMessageId_thenOnlyRelevantNameReturned() = runTest { + insertInitialData() + + val insertingMessages = listOf( + newRegularMessageEntity( + "1", + conversationId = conversationEntity1.id, + senderUserId = userDetailsEntity1.id, + status = MessageEntity.Status.PENDING, + senderName = userDetailsEntity1.name!!, + sender = userDetailsEntity1 + ), + newRegularMessageEntity( + "2", + conversationId = conversationEntity1.id, + senderUserId = userDetailsEntity2.id, + status = MessageEntity.Status.PENDING, + senderName = userDetailsEntity2.name!!, + sender = userDetailsEntity2 + ) + ) + messageDAO.insertOrIgnoreMessages(insertingMessages) + + val result = messageDAO.getSenderNameById("1", conversationEntity1.id) + + assertEquals(userDetailsEntity1.name, result) + } + + @Test + fun givenMessagesAreButNoUserInserted_whenGettingSenderNameByMessageId_thenNullNameReturned() = runTest { + insertInitialData() + + val insertingMessages = listOf( + newRegularMessageEntity( + "1", + conversationId = conversationEntity1.id, + senderUserId = userDetailsEntity1.id.copy(value = "absolutely_another_value"), + status = MessageEntity.Status.PENDING, + senderName = "s", + sender = userDetailsEntity1.copy(name = "s", id = userDetailsEntity1.id.copy(value = "absolutely_another_value")) + ), + newRegularMessageEntity( + "2", + conversationId = conversationEntity1.id, + senderUserId = userDetailsEntity2.id, + status = MessageEntity.Status.PENDING, + senderName = userDetailsEntity2.name!!, + sender = userDetailsEntity2 + ) + ) + messageDAO.insertOrIgnoreMessages(insertingMessages) + + val result = messageDAO.getSenderNameById("1", conversationEntity1.id) + + assertEquals(null, result) + } + + @Test + fun givenAudioMessagesAreInserted_whenGettingNextAudioMessageAfterTheLastOne_thenNullIdReturned() = runTest { + insertInitialData() + messageDAO.insertOrIgnoreMessages(listOfMessageWithAudioAssets()) + + val result = messageDAO.getNextAudioMessageInConversation("4", conversationEntity1.id) + + assertEquals(null, result) + } + + @Test + fun givenAudioMessagesAreInserted_whenGettingNextAudioMessageAfterTheFirstOne_thenCorrespondingIdReturned() = runTest { + insertInitialData() + messageDAO.insertOrIgnoreMessages(listOfMessageWithAudioAssets()) + + val result = messageDAO.getNextAudioMessageInConversation("1", conversationEntity1.id) + + assertEquals("3", result) + } + private suspend fun insertInitialData() { userDAO.upsertUsers(listOf(userEntity1, userEntity2)) conversationDAO.insertConversation( @@ -2389,4 +2499,54 @@ class MessageDAOTest : BaseDatabaseTest() { ), visibility = if (isComplete) MessageEntity.Visibility.VISIBLE else MessageEntity.Visibility.HIDDEN ) + + private fun listOfMessageWithAudioAssets(): List { + val messageTemplate = newRegularMessageEntity( + conversationId = conversationEntity1.id, + senderUserId = userDetailsEntity1.id, + status = MessageEntity.Status.DELIVERED, + sender = userDetailsEntity1, + content = MessageEntityContent.Asset( + assetSizeInBytes = 1000, + assetMimeType = "audio/mp4", + assetOtrKey = byteArrayOf(1), + assetSha256Key = byteArrayOf(1), + assetId = "assetId", + assetEncryptionAlgorithm = "", + assetDurationMs = 10 + ) + ) + + return listOf( + messageTemplate.copy(id = "1", date = messageTemplate.date.plus(10.seconds)), + messageTemplate.copy( + id = "2", + date = messageTemplate.date.plus(20.seconds), + content = MessageEntityContent.Text("Test Text") + ), + messageTemplate.copy(id = "3", date = messageTemplate.date.plus(30.seconds)), + messageTemplate.copy(id = "4", date = messageTemplate.date.plus(40.seconds)), + newRegularMessageEntity( + id = "5", + conversationId = conversationEntity1.id, + senderUserId = userDetailsEntity1.id, + status = MessageEntity.Status.DELIVERED, + sender = userDetailsEntity1, + date = messageTemplate.date.plus(50.seconds) + ), + messageTemplate.copy( + id = "6", + date = messageTemplate.date.plus(60.seconds), + content = MessageEntityContent.Asset( + assetSizeInBytes = 1000, + assetMimeType = "video/mp4", + assetOtrKey = byteArrayOf(1), + assetSha256Key = byteArrayOf(1), + assetId = "assetId", + assetEncryptionAlgorithm = "", + assetDurationMs = 10 + ) + ) + ) + } } diff --git a/protobuf-codegen/src/main/proto/messages.proto b/protobuf-codegen/src/main/proto/messages.proto index efd576ac5b9..ea5b30656e9 100644 --- a/protobuf-codegen/src/main/proto/messages.proto +++ b/protobuf-codegen/src/main/proto/messages.proto @@ -45,8 +45,19 @@ message GenericMessage { ButtonAction buttonAction = 21; ButtonActionConfirmation buttonActionConfirmation = 22; DataTransfer dataTransfer = 23; // client-side synchronization across devices of the same user + InCallEmoji inCallEmoji = 24; + // UnknownStrategy unknownStrategy = 25; -- Defined outside the oneof + // Next field should be 26 ↓ + InCallHandRaise inCallHandRaise = 26; + } + optional UnknownStrategy unknownStrategy = 25 [default = IGNORE]; + + // See internal RFC: "2024-07-18 RFC Improve future-proofing for new OTR message types" + enum UnknownStrategy { + IGNORE = 0; // Ignore the message completely. Trash. Bye + DISCARD_AND_WARN = 1; // Warn the user, but discard the message, as it won't be helpful in the future. + WARN_USER_ALLOW_RETRY = 2; // Warn the user. Client has freedom to store it and retry in the future. } - optional UnknownStrategy unknownStrategy = 24 [default = IGNORE]; } message QualifiedUserId { @@ -185,6 +196,7 @@ message Cleared { required int64 cleared_timestamp = 2; // only optional to maintain backwards compatibility optional QualifiedConversationId qualified_conversation_id = 3; + optional bool needToRemoveLocally = 4 [default = false]; } message MessageHide { @@ -331,6 +343,14 @@ message Reaction { optional LegalHoldStatus legal_hold_status = 3 [default = UNKNOWN]; // whether this message was sent to legal hold } +message InCallEmoji { + map emojis = 1; +} + +message InCallHandRaise { + required bool is_hand_up = 1; // true if the hand is raised, false if lowered +} + message Calling { required string content = 1; optional QualifiedConversationId qualified_conversation_id = 2; @@ -361,9 +381,3 @@ enum LegalHoldStatus { DISABLED = 1; ENABLED = 2; } - -enum UnknownStrategy { - IGNORE = 0; // Ignore the message completely. - DISCARD_AND_WARN = 1; // Warn the user, but discard the message, as it may not be helpful in the future - WARN_USER_ALLOW_RETRY = 2; // Warn the user. Client has freedom to store it and retry in the future. -} diff --git a/util/src/commonMain/kotlin/com.wire.kalium.util/serialization/LenientJsonSerializer.kt b/util/src/commonMain/kotlin/com.wire.kalium.util/serialization/LenientJsonSerializer.kt new file mode 100644 index 00000000000..3d6507cf8ff --- /dev/null +++ b/util/src/commonMain/kotlin/com.wire.kalium.util/serialization/LenientJsonSerializer.kt @@ -0,0 +1,32 @@ +/* + * Wire + * Copyright (C) 2024 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.util.serialization + +import kotlinx.serialization.json.Json + +/** + * The json serializer for shared usage. + */ +object LenientJsonSerializer { + + val json = Json { + isLenient = true + encodeDefaults = true + ignoreUnknownKeys = true + } +}