diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallMetadataProfile.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallMetadataProfile.kt index 721c460be58..203f744a4e6 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallMetadataProfile.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallMetadataProfile.kt @@ -20,6 +20,9 @@ package com.wire.kalium.logic.data.call import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.user.OtherUserMinimized +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.data.user.type.UserType data class CallMetadataProfile( val data: Map @@ -37,7 +40,26 @@ data class CallMetadata( val callerTeamName: String?, val establishedTime: String? = null, val callStatus: CallStatus, - val participants: List = emptyList(), + val participants: List = emptyList(), val maxParticipants: Int = 0, // Was used for tracking - val protocol: Conversation.ProtocolInfo -) + val protocol: Conversation.ProtocolInfo, + val activeSpeakers: Map> = mapOf(), + val users: List = listOf() +) { + fun getFullParticipants(): List = participants.map { participant -> + val user = users.firstOrNull { it.id == participant.userId } + val isSpeaking = (activeSpeakers[participant.id]?.contains(participant.clientId) ?: false) && !participant.isMuted + Participant( + id = participant.id, + clientId = participant.clientId, + name = user?.name, + isMuted = participant.isMuted, + isCameraOn = participant.isCameraOn, + isSpeaking = isSpeaking, + isSharingScreen = participant.isSharingScreen, + hasEstablishedAudio = participant.hasEstablishedAudio, + avatarAssetId = user?.completePicture, + userType = user?.userType ?: UserType.NONE + ) + } +} diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Participant.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Participant.kt index fe0960af619..83ff455e796 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Participant.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Participant.kt @@ -34,3 +34,13 @@ data class Participant( val avatarAssetId: UserAssetId? = null, val userType: UserType = UserType.NONE, ) + +data class ParticipantMinimized( + val id: QualifiedID, + val userId: QualifiedID, + val clientId: String, + val isMuted: Boolean, + val isCameraOn: Boolean, + val isSharingScreen: Boolean = false, + val hasEstablishedAudio: Boolean, +) 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 6834b9ea767..9af3912b90a 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 @@ -531,8 +531,7 @@ class CallManagerImpl internal constructor( val onParticipantListChanged = OnParticipantListChanged( callRepository = callRepository, qualifiedIdMapper = qualifiedIdMapper, - participantMapper = ParticipantMapperImpl(videoStateChecker, callMapper), - userRepository = userRepository, + participantMapper = ParticipantMapperImpl(videoStateChecker, callMapper, qualifiedIdMapper), callingScope = scope ).keepingStrongReference() diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnActiveSpeakers.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnActiveSpeakers.kt index e1bcbdfe157..8efdb663d2b 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnActiveSpeakers.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnActiveSpeakers.kt @@ -24,7 +24,6 @@ import com.wire.kalium.calling.types.Handle import com.wire.kalium.logic.data.call.CallActiveSpeakers import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.id.QualifiedIdMapper -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json class OnActiveSpeakers( @@ -36,9 +35,13 @@ class OnActiveSpeakers( val callActiveSpeakers = Json.decodeFromString(data) val conversationIdWithDomain = qualifiedIdMapper.fromStringToQualifiedID(conversationId) + val onlyActiveSpeakers = callActiveSpeakers.activeSpeakers.filter { activeSpeaker -> + activeSpeaker.audioLevel > 0 || activeSpeaker.audioLevelNow > 0 + }.groupBy({ qualifiedIdMapper.fromStringToQualifiedID(it.userId) }) { it.clientId } + callRepository.updateParticipantsActiveSpeaker( conversationId = conversationIdWithDomain, - activeSpeakers = callActiveSpeakers + activeSpeakers = onlyActiveSpeakers ) } } diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChanged.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChanged.kt index bf48b115217..1ffa9c6c950 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChanged.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChanged.kt @@ -24,12 +24,9 @@ import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.callingLogger import com.wire.kalium.logic.data.call.CallParticipants import com.wire.kalium.logic.data.call.CallRepository -import com.wire.kalium.logic.data.call.Participant +import com.wire.kalium.logic.data.call.ParticipantMinimized import com.wire.kalium.logic.data.call.mapper.ParticipantMapper import com.wire.kalium.logic.data.id.QualifiedIdMapper -import com.wire.kalium.logic.data.user.UserRepository -import com.wire.kalium.logic.functional.onFailure -import com.wire.kalium.logic.functional.onSuccess import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.serialization.json.Json @@ -39,7 +36,6 @@ class OnParticipantListChanged internal constructor( private val callRepository: CallRepository, private val qualifiedIdMapper: QualifiedIdMapper, private val participantMapper: ParticipantMapper, - private val userRepository: UserRepository, private val callingScope: CoroutineScope ) : ParticipantChangedHandler { @@ -48,22 +44,11 @@ class OnParticipantListChanged internal constructor( val participantsChange = Json.decodeFromString(data) callingScope.launch { - val participants = mutableListOf() + val participants = mutableListOf() val conversationIdWithDomain = qualifiedIdMapper.fromStringToQualifiedID(remoteConversationId) participantsChange.members.map { member -> - val participant = participantMapper.fromCallMemberToParticipant(member) - val userId = qualifiedIdMapper.fromStringToQualifiedID(member.userId) - userRepository.getKnownUserMinimized(userId).onSuccess { - val updatedParticipant = participant.copy( - name = it.name, - avatarAssetId = it.completePicture, - userType = it.userType - ) - participants.add(updatedParticipant) - }.onFailure { - participants.add(participant) - } + participants.add(participantMapper.fromCallMemberToParticipantMinimized(member)) } callRepository.updateCallParticipants( 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 6667c304c78..722a6b9f07b 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 @@ -25,7 +25,6 @@ import com.wire.kalium.logger.obfuscateDomain import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.callingLogger -import com.wire.kalium.logic.data.call.mapper.ActiveSpeakerMapper import com.wire.kalium.logic.data.call.mapper.CallMapper import com.wire.kalium.logic.data.client.MLSClientProvider import com.wire.kalium.logic.data.conversation.ClientId @@ -51,9 +50,9 @@ import com.wire.kalium.logic.data.message.PersistMessageUseCase import com.wire.kalium.logic.data.team.TeamRepository import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository -import com.wire.kalium.logic.di.MapperProvider import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap +import com.wire.kalium.logic.functional.getOrElse import com.wire.kalium.logic.functional.getOrNull import com.wire.kalium.logic.functional.map import com.wire.kalium.logic.functional.onFailure @@ -119,8 +118,8 @@ interface CallRepository { fun updateIsMutedById(conversationId: ConversationId, isMuted: Boolean) fun updateIsCbrEnabled(isCbrEnabled: Boolean) fun updateIsCameraOnById(conversationId: ConversationId, isCameraOn: Boolean) - fun updateCallParticipants(conversationId: ConversationId, participants: List) - fun updateParticipantsActiveSpeaker(conversationId: ConversationId, activeSpeakers: CallActiveSpeakers) + suspend fun updateCallParticipants(conversationId: ConversationId, participants: List) + fun updateParticipantsActiveSpeaker(conversationId: ConversationId, activeSpeakers: Map>) suspend fun getLastClosedCallCreatedByConversationId(conversationId: ConversationId): Flow suspend fun updateOpenCallsToClosedStatus() suspend fun persistMissedCall(conversationId: ConversationId) @@ -151,7 +150,6 @@ internal class CallDataSource( private val leaveSubconversation: LeaveSubconversationUseCase, private val callMapper: CallMapper, private val federatedIdMapper: FederatedIdMapper, - private val activeSpeakerMapper: ActiveSpeakerMapper = MapperProvider.activeSpeakerMapper(), kaliumDispatchers: KaliumDispatcher = KaliumDispatcherImpl ) : CallRepository { @@ -224,7 +222,8 @@ internal class CallDataSource( isCbrEnabled = isCbrEnabled, establishedTime = null, callStatus = status, - protocol = conversation.conversation.protocol + protocol = conversation.conversation.protocol, + activeSpeakers = mapOf() ) val isCallInCurrentSession = _callMetadataProfile.value.data.containsKey(conversationId) @@ -407,7 +406,8 @@ internal class CallDataSource( } } - override fun updateCallParticipants(conversationId: ConversationId, participants: List) { + @Suppress("NestedBlockDepth") + override suspend fun updateCallParticipants(conversationId: ConversationId, participants: List) { val callMetadataProfile = _callMetadataProfile.value callMetadataProfile.data[conversationId]?.let { call -> if (call.participants != participants) { @@ -417,10 +417,24 @@ internal class CallDataSource( " with size of: ${participants.size}" ) + val currentParticipantIds = call.participants.map { it.userId }.toSet() + val newParticipantIds = participants.map { it.userId }.toSet() + + val updatedUsers = call.users.toMutableList() + + newParticipantIds.minus(currentParticipantIds).let { missedUserIds -> + if (missedUserIds.isNotEmpty()) + updatedUsers.addAll( + userRepository.getUsersMinimizedByQualifiedIDs(missedUserIds.toList()).getOrElse { listOf() } + ) + + } + val updatedCallMetadata = callMetadataProfile.data.toMutableMap().apply { this[conversationId] = call.copy( participants = participants, - maxParticipants = max(call.maxParticipants, participants.size + 1) + maxParticipants = max(call.maxParticipants, participants.size + 1), + users = updatedUsers ) } @@ -441,14 +455,14 @@ internal class CallDataSource( } } - private fun clearStaleParticipantTimeout(participant: Participant) { + private fun clearStaleParticipantTimeout(participant: ParticipantMinimized) { callingLogger.i("Clear stale participant timer") val qualifiedClient = QualifiedClientID(ClientId(participant.clientId), participant.id) staleParticipantJobs.remove(qualifiedClient)?.cancel() } private fun removeStaleParticipantAfterTimeout( - participant: Participant, + participant: ParticipantMinimized, conversationId: ConversationId ) { val qualifiedClient = QualifiedClientID(ClientId(participant.clientId), participant.id) @@ -469,7 +483,7 @@ internal class CallDataSource( } } - override fun updateParticipantsActiveSpeaker(conversationId: ConversationId, activeSpeakers: CallActiveSpeakers) { + override fun updateParticipantsActiveSpeaker(conversationId: ConversationId, activeSpeakers: Map>) { val callMetadataProfile = _callMetadataProfile.value callMetadataProfile.data[conversationId]?.let { call -> @@ -477,19 +491,11 @@ internal class CallDataSource( "updateActiveSpeakers() -" + " conversationId: ${conversationId.value.obfuscateId()}" + "@${conversationId.domain.obfuscateDomain()}" + - "with size of: ${activeSpeakers.activeSpeakers.size}" - ) - - val updatedParticipants = activeSpeakerMapper.mapParticipantsActiveSpeaker( - participants = call.participants, - activeSpeakers = activeSpeakers + "with size of: ${activeSpeakers.size}" ) val updatedCallMetadata = callMetadataProfile.data.toMutableMap().apply { - this[conversationId] = call.copy( - participants = updatedParticipants, - maxParticipants = max(call.maxParticipants, updatedParticipants.size + 1) - ) + this[conversationId] = call.copy(activeSpeakers = activeSpeakers) } _callMetadataProfile.value = callMetadataProfile.copy( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/ActiveSpeakerMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/ActiveSpeakerMapper.kt deleted file mode 100644 index 12eeba64952..00000000000 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/ActiveSpeakerMapper.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.mapper - -import com.wire.kalium.logic.data.call.CallActiveSpeakers -import com.wire.kalium.logic.data.call.Participant - -interface ActiveSpeakerMapper { - fun mapParticipantsActiveSpeaker( - participants: List, - activeSpeakers: CallActiveSpeakers - ): List -} - -class ActiveSpeakerMapperImpl : ActiveSpeakerMapper { - override fun mapParticipantsActiveSpeaker( - participants: List, - activeSpeakers: CallActiveSpeakers - ): List = participants.toMutableList().apply { - activeSpeakers.activeSpeakers.forEach { activeSpeaker -> - find { participant -> - activeSpeaker.userId == participant.id.toString() && activeSpeaker.clientId == participant.clientId - }?.let { - this[indexOf(it)] = it.copy( - isSpeaking = activeSpeaker.audioLevel > 0 || activeSpeaker.audioLevelNow > 0 - ) - } - } - } -} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/CallMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/CallMapper.kt index d9f8cf80c02..19f18747811 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/CallMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/CallMapper.kt @@ -186,7 +186,7 @@ class CallMapperImpl( callerName = metadata?.callerName, callerTeamName = metadata?.callerTeamName, establishedTime = metadata?.establishedTime, - participants = metadata?.participants ?: emptyList(), + participants = metadata?.getFullParticipants() ?: emptyList(), maxParticipants = metadata?.maxParticipants ?: 0 ) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/ParticipantMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/ParticipantMapper.kt index 381d8b6f292..8102ee6382e 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/ParticipantMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/ParticipantMapper.kt @@ -19,29 +19,29 @@ package com.wire.kalium.logic.data.call.mapper import com.wire.kalium.logic.data.call.CallMember -import com.wire.kalium.logic.data.call.Participant +import com.wire.kalium.logic.data.call.ParticipantMinimized import com.wire.kalium.logic.data.call.VideoStateChecker import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.id.QualifiedIdMapper interface ParticipantMapper { - fun fromCallMemberToParticipant(member: CallMember): Participant + fun fromCallMemberToParticipantMinimized(member: CallMember): ParticipantMinimized } class ParticipantMapperImpl( private val videoStateChecker: VideoStateChecker, - private val callMapper: CallMapper + private val callMapper: CallMapper, + private val qualifiedIdMapper: QualifiedIdMapper ) : ParticipantMapper { - override fun fromCallMemberToParticipant(member: CallMember): Participant = with(member) { + override fun fromCallMemberToParticipantMinimized(member: CallMember): ParticipantMinimized = with(member) { val videoState = callMapper.fromIntToCallingVideoState(vrecv) val isCameraOn = videoStateChecker.isCameraOn(videoState) val isSharingScreen = videoStateChecker.isSharingScreen(videoState) - Participant( - id = QualifiedID( - value = userId.removeDomain(), - domain = userId.getDomain() - ), + ParticipantMinimized( + id = QualifiedID(value = member.userId.removeDomain(), domain = member.userId.getDomain()), + userId = qualifiedIdMapper.fromStringToQualifiedID(member.userId), clientId = clientId, isMuted = isMuted == 1, isCameraOn = isCameraOn, 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 cbae371636c..48e208af23c 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 @@ -57,14 +57,14 @@ import com.wire.kalium.logic.kaliumLogger import com.wire.kalium.logic.sync.receiver.handler.legalhold.LegalHoldHandler import com.wire.kalium.logic.wrapApiRequest import com.wire.kalium.logic.wrapStorageRequest -import com.wire.kalium.network.api.base.authenticated.TeamsApi -import com.wire.kalium.network.api.base.authenticated.self.SelfApi import com.wire.kalium.network.api.authenticated.teams.TeamMemberDTO import com.wire.kalium.network.api.authenticated.teams.TeamMemberIdList import com.wire.kalium.network.api.authenticated.userDetails.ListUserRequest import com.wire.kalium.network.api.authenticated.userDetails.ListUsersDTO -import com.wire.kalium.network.api.base.authenticated.userDetails.UserDetailsApi import com.wire.kalium.network.api.authenticated.userDetails.qualifiedIds +import com.wire.kalium.network.api.base.authenticated.TeamsApi +import com.wire.kalium.network.api.base.authenticated.self.SelfApi +import com.wire.kalium.network.api.base.authenticated.userDetails.UserDetailsApi import com.wire.kalium.network.api.model.LegalHoldStatusDTO import com.wire.kalium.network.api.model.SelfUserDTO import com.wire.kalium.network.api.model.UserProfileDTO @@ -159,6 +159,7 @@ interface UserRepository { suspend fun fetchUsersLegalHoldConsent(userIds: Set): Either suspend fun getOneOnOnConversationId(userId: QualifiedID): Either + suspend fun getUsersMinimizedByQualifiedIDs(userIds: List): Either> } @Suppress("LongParameterList", "TooManyFunctions") @@ -418,7 +419,9 @@ internal class UserDataSource internal constructor( kaliumLogger.i("$logPrefix: Succeeded") userDetailsRefreshInstantCache[selfUserId] = DateTimeUtil.currentInstant() }) - } else { refreshUserDetailsIfNeeded(selfUserId) } + } else { + refreshUserDetailsIfNeeded(selfUserId) + } }.filterNotNull().flatMapMerge { encodedValue -> val selfUserID: QualifiedIDEntity = Json.decodeFromString(encodedValue) userDAO.observeUserDetailsByQualifiedID(selfUserID) @@ -478,6 +481,12 @@ internal class UserDataSource internal constructor( } } + override suspend fun getUsersMinimizedByQualifiedIDs(userIds: List) = wrapStorageRequest { + userDAO.getUsersMinimizedByQualifiedIDs( + qualifiedIDs = userIds.map { it.toDao() } + ).map(userMapper::fromUserEntityToOtherUserMinimized) + } + override suspend fun observeUser(userId: UserId): Flow = userDAO.observeUserDetailsByQualifiedID(qualifiedID = userId.toDao()) .map { userEntity -> diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt index 797f6209d1a..bd1b7939132 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt @@ -24,8 +24,6 @@ import com.wire.kalium.logic.configuration.server.ServerConfigMapper import com.wire.kalium.logic.configuration.server.ServerConfigMapperImpl import com.wire.kalium.logic.data.asset.AssetMapper import com.wire.kalium.logic.data.asset.AssetMapperImpl -import com.wire.kalium.logic.data.call.mapper.ActiveSpeakerMapper -import com.wire.kalium.logic.data.call.mapper.ActiveSpeakerMapperImpl import com.wire.kalium.logic.data.call.mapper.CallMapper import com.wire.kalium.logic.data.call.mapper.CallMapperImpl import com.wire.kalium.logic.data.client.ClientMapper @@ -156,7 +154,6 @@ internal object MapperProvider { fun protoContentMapper(selfUserId: UserId): ProtoContentMapper = ProtoContentMapperImpl(selfUserId = selfUserId) fun qualifiedIdMapper(selfUserId: UserId): QualifiedIdMapper = QualifiedIdMapperImpl(selfUserId) fun callMapper(selfUserId: UserId): CallMapper = CallMapperImpl(qualifiedIdMapper(selfUserId)) - fun activeSpeakerMapper(): ActiveSpeakerMapper = ActiveSpeakerMapperImpl() fun connectionStatusMapper(): ConnectionStatusMapper = ConnectionStatusMapperImpl() fun featureConfigMapper(): FeatureConfigMapper = FeatureConfigMapperImpl() fun localNotificationMessageMapper(): LocalNotificationMessageMapper = LocalNotificationMessageMapperImpl() diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/ActiveSpeakerMapperTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/ActiveSpeakerMapperTest.kt deleted file mode 100644 index 0f73c360413..00000000000 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/ActiveSpeakerMapperTest.kt +++ /dev/null @@ -1,182 +0,0 @@ -/* - * 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.call.mapper.ActiveSpeakerMapperImpl -import com.wire.kalium.logic.data.id.QualifiedID -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals - -class ActiveSpeakerMapperTest { - - private val activeSpeakerMapper = ActiveSpeakerMapperImpl() - - @Test - fun givenUserAudioLevelNot0AndaudioLevelNowNot0_whenMapping_thenUserIsSpeaking() = runTest { - val dummyParticipantWithDifferentClientId = DUMMY_PARTICIPANT.copy( - clientId = "anotherClientId" - ) - - val callActiveSpeakerMap = activeSpeakerMapper.mapParticipantsActiveSpeaker( - participants = listOf( - DUMMY_PARTICIPANT, - dummyParticipantWithDifferentClientId - ), - activeSpeakers = CallActiveSpeakers( - activeSpeakers = listOf( - DUMMY_CALL_ACTIVE_SPEAKER.copy(audioLevel = 1, audioLevelNow = 1), - DUMMY_CALL_ACTIVE_SPEAKER1.copy(audioLevel = 1, audioLevelNow = 1) - ) - ) - ) - - val expectedParticipantsActiveSpeaker = listOf( - DUMMY_PARTICIPANT.copy( - isSpeaking = true - ), - dummyParticipantWithDifferentClientId.copy( - isSpeaking = true - ) - ) - - assertEquals(expectedParticipantsActiveSpeaker, callActiveSpeakerMap) - } - - - @Test - fun givenUserAudioLevelIs0AndaudioLevelNowNot0_whenMapping_thenUserIsSpeaking() = runTest { - val dummyParticipantWithDifferentClientId = DUMMY_PARTICIPANT.copy( - clientId = "anotherClientId" - ) - - val callActiveSpeakerMap = activeSpeakerMapper.mapParticipantsActiveSpeaker( - participants = listOf( - DUMMY_PARTICIPANT, - dummyParticipantWithDifferentClientId - ), - activeSpeakers = CallActiveSpeakers( - activeSpeakers = listOf( - DUMMY_CALL_ACTIVE_SPEAKER.copy(audioLevel = 0, audioLevelNow = 1), - DUMMY_CALL_ACTIVE_SPEAKER1.copy(audioLevel = 0, audioLevelNow = 1) - ) - ) - ) - - val expectedParticipantsActiveSpeaker = listOf( - DUMMY_PARTICIPANT.copy( - isSpeaking = true - ), - dummyParticipantWithDifferentClientId.copy( - isSpeaking = true - ) - ) - - assertEquals(expectedParticipantsActiveSpeaker, callActiveSpeakerMap) - } - - @Test - fun givenUserAudioLevelNot0AndaudioLevelNowIs0_whenMapping_thenUserIsSpeaking() = runTest { - val dummyParticipantWithDifferentClientId = DUMMY_PARTICIPANT.copy( - clientId = "anotherClientId" - ) - - val callActiveSpeakerMap = activeSpeakerMapper.mapParticipantsActiveSpeaker( - participants = listOf( - DUMMY_PARTICIPANT, - dummyParticipantWithDifferentClientId - ), - activeSpeakers = CallActiveSpeakers( - activeSpeakers = listOf( - DUMMY_CALL_ACTIVE_SPEAKER.copy(audioLevel = 1, audioLevelNow = 0), - DUMMY_CALL_ACTIVE_SPEAKER1.copy(audioLevel = 1, audioLevelNow = 0) - ) - ) - ) - - val expectedParticipantsActiveSpeaker = listOf( - DUMMY_PARTICIPANT.copy( - isSpeaking = true - ), - dummyParticipantWithDifferentClientId.copy( - isSpeaking = true - ) - ) - - assertEquals(expectedParticipantsActiveSpeaker, callActiveSpeakerMap) - } - - @Test - fun givenUserAudioLevelIs0AndaudioLevelNowIs0_whenMapping_thenUserIsNotSpeaking() = runTest { - val dummyParticipantWithDifferentClientId = DUMMY_PARTICIPANT.copy( - clientId = "anotherClientId" - ) - - val callActiveSpeakerMap = activeSpeakerMapper.mapParticipantsActiveSpeaker( - participants = listOf( - DUMMY_PARTICIPANT, - dummyParticipantWithDifferentClientId - ), - activeSpeakers = CallActiveSpeakers( - activeSpeakers = listOf( - DUMMY_CALL_ACTIVE_SPEAKER.copy(audioLevel = 0, audioLevelNow = 0), - DUMMY_CALL_ACTIVE_SPEAKER1.copy(audioLevel = 0, audioLevelNow = 0) - ) - ) - ) - - val expectedParticipantsActiveSpeaker = listOf( - DUMMY_PARTICIPANT.copy( - isSpeaking = false - ), - dummyParticipantWithDifferentClientId.copy( - isSpeaking = false - ) - ) - - assertEquals(expectedParticipantsActiveSpeaker, callActiveSpeakerMap) - } - - companion object { - private val DUMMY_PARTICIPANT = Participant( - id = QualifiedID( - value = "dummyId", - domain = "dummyDomain" - ), - clientId = "dummyClientId", - isMuted = false, - isSpeaking = false, - isCameraOn = false, - isSharingScreen = false, - hasEstablishedAudio = true - ) - private val DUMMY_CALL_ACTIVE_SPEAKER = CallActiveSpeaker( - userId = "dummyId@dummyDomain", - clientId = "dummyClientId", - audioLevel = 1, - audioLevelNow = 1 - ) - private val DUMMY_CALL_ACTIVE_SPEAKER1 = CallActiveSpeaker( - userId = "dummyId@dummyDomain", - clientId = "anotherClientId", - audioLevel = 1, - audioLevelNow = 0 - ) - } -} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt index dfcc9752f98..18cc5052316 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt @@ -29,6 +29,7 @@ import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.conversation.EpochChangesObserver import com.wire.kalium.logic.data.conversation.JoinSubconversationUseCase import com.wire.kalium.logic.data.conversation.LeaveSubconversationUseCase import com.wire.kalium.logic.data.conversation.MLSConversationRepository @@ -41,17 +42,17 @@ import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.id.toDao import com.wire.kalium.logic.data.message.PersistMessageUseCase +import com.wire.kalium.logic.data.mls.CipherSuite import com.wire.kalium.logic.data.session.SessionRepository import com.wire.kalium.logic.data.team.TeamRepository import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.data.user.type.UserType -import com.wire.kalium.logic.data.conversation.EpochChangesObserver -import com.wire.kalium.logic.data.mls.CipherSuite import com.wire.kalium.logic.framework.TestConversation import com.wire.kalium.logic.framework.TestTeam 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.util.shouldSucceed import com.wire.kalium.network.api.base.authenticated.CallApi @@ -63,9 +64,9 @@ import com.wire.kalium.persistence.dao.conversation.ConversationEntity import io.ktor.util.reflect.instanceOf import io.mockative.Mock import io.mockative.any -import io.mockative.eq import io.mockative.coEvery import io.mockative.coVerify +import io.mockative.eq import io.mockative.every import io.mockative.mock import io.mockative.once @@ -757,17 +758,17 @@ class CallRepositoryTest { @Test fun givenAConversationIdThatExistsInTheFlow_whenUpdateCallParticipantsIsCalled_thenUpdateCallStatusInTheFlow() = runTest { // given - val (_, callRepository) = Arrangement().arrange() + val (_, callRepository) = Arrangement() + .givenGetKnownUserMinimizedSucceeds() + .arrange() val participantsList = listOf( - Participant( - id = QualifiedID("participantId", "participantDomain"), + ParticipantMinimized( + id = QualifiedID("participantId", ""), + userId = QualifiedID("participantId", "participantDomain"), clientId = "abcd", - name = "name", isMuted = true, - isSpeaking = false, isCameraOn = false, isSharingScreen = false, - avatarAssetId = null, hasEstablishedAudio = true ) ) @@ -797,77 +798,101 @@ class CallRepositoryTest { } @Test - fun givenAConversationIdThatDoesNotExistsInTheFlow_whenUpdateParticipantsActiveSpeakerIsCalled_thenDoNotUpdateTheFlow() = runTest { - val (_, callRepository) = Arrangement().arrange() - callRepository.updateParticipantsActiveSpeaker( - Arrangement.randomConversationId, - CallActiveSpeakers(emptyList()) - ) + fun givenCallWithSomeParticipants_whenUpdateCallParticipantsIsCalledWithNewParticipants_thenOnlyNewUsersFetchedFromDB() = + runTest { + // given + val (arrangement, callRepository) = Arrangement() + .givenGetKnownUserMinimizedSucceeds() + .arrange() + val participant = ParticipantMinimized( + id = QualifiedID("participantId", ""), + userId = QualifiedID("participantId", "participantDomain"), + clientId = "abcd", + isMuted = true, + isCameraOn = false, + isSharingScreen = false, + hasEstablishedAudio = true + ) + val newParticipant = participant.copy(userId = QualifiedID("anotherParticipantId", "participantDomain")) + val participantsList = listOf(participant) + callRepository.updateCallMetadataProfileFlow( + callMetadataProfile = CallMetadataProfile( + data = mapOf( + Arrangement.conversationId to createCallMetadata().copy( + participants = participantsList, + maxParticipants = 0 + ) + ) + ) + ) - assertFalse { - callRepository.getCallMetadataProfile().data.containsKey(Arrangement.randomConversationId) + // when + callRepository.updateCallParticipants( + Arrangement.conversationId, + participantsList.plus(newParticipant) + ) + + // then + coVerify { + arrangement.userRepository.getUsersMinimizedByQualifiedIDs(listOf(newParticipant.userId)) + }.wasInvoked(exactly = once) } - } @Test - fun givenAConversationIdThatExistsInTheFlow_whenUpdateParticipantActiveSpeakerIsCalled_thenUpdateCallStatusInTheFlow() = runTest { - // given - val (_, callRepository) = Arrangement().arrange() - val participant = Participant( - id = QualifiedID("participantId", "participantDomain"), - clientId = "abcd", - name = "name", - isMuted = true, - isSpeaking = false, - isCameraOn = false, - avatarAssetId = null, - isSharingScreen = false, - hasEstablishedAudio = true - ) - val participantsList = listOf(participant) - val expectedParticipantsList = listOf(participant.copy(isSpeaking = true)) - callRepository.updateCallMetadataProfileFlow( - callMetadataProfile = CallMetadataProfile( - data = mapOf( - Arrangement.conversationId to createCallMetadata().copy( - participants = emptyList(), - maxParticipants = 0 - ) - ) + fun givenCallWithSomeParticipants_whenUpdateCallParticipantsIsCalledWithSameParticipants_thenNoFetchingUsersFromDB() = + runTest { + // given + val (arrangement, callRepository) = Arrangement() + .givenGetKnownUserMinimizedSucceeds() + .arrange() + val participant = ParticipantMinimized( + id = QualifiedID("participantId", ""), + userId = QualifiedID("participantId", "participantDomain"), + clientId = "abcd", + isMuted = true, + isCameraOn = false, + isSharingScreen = false, + hasEstablishedAudio = true ) - ) - val activeSpeakers = CallActiveSpeakers( - activeSpeakers = listOf( - CallActiveSpeaker( - userId = "participantId@participantDomain", - clientId = "abcd", - audioLevel = 1, - audioLevelNow = 1 + val otherParticipant = participant.copy(id = QualifiedID("anotherParticipantId", "participantDomain")) + val participantsList = listOf(participant, otherParticipant) + callRepository.updateCallMetadataProfileFlow( + callMetadataProfile = CallMetadataProfile( + data = mapOf( + Arrangement.conversationId to createCallMetadata().copy( + participants = participantsList, + maxParticipants = 0 + ) + ) ) ) - ) - callRepository.updateCallParticipants( - Arrangement.conversationId, - participantsList - ) + // when + callRepository.updateCallParticipants( + Arrangement.conversationId, + participantsList + ) - // when - callRepository.updateParticipantsActiveSpeaker(Arrangement.conversationId, activeSpeakers) + // then + coVerify { + arrangement.userRepository.getUsersMinimizedByQualifiedIDs(listOf(otherParticipant.id)) + }.wasNotInvoked() + } - // then - val metadata = callRepository.getCallMetadataProfile().data[Arrangement.conversationId] - assertEquals( - expectedParticipantsList, - metadata?.participants + @Test + fun givenAConversationIdThatDoesNotExistsInTheFlow_whenUpdateParticipantsActiveSpeakerIsCalled_thenDoNotUpdateTheFlow() = runTest { + val (_, callRepository) = Arrangement().arrange() + callRepository.updateParticipantsActiveSpeaker( + Arrangement.randomConversationId, + emptyMap() ) - assertEquals( - true, - metadata?.participants?.get(0)?.isSpeaking - ) + assertFalse { + callRepository.getCallMetadataProfile().data.containsKey(Arrangement.randomConversationId) + } } + @Test fun givenAnIncomingCall_whenRequestingIncomingCalls_thenReturnTheIncomingCall() = runTest { // given @@ -1312,6 +1337,7 @@ class CallRepositoryTest { TestKaliumDispatcher.main ) { val (arrangement, callRepository) = Arrangement() + .givenGetKnownUserMinimizedSucceeds() .givenGetSubconversationInfoReturns(Arrangement.subconversationGroupId) .givenRemoveClientsFromMLSGroupSucceeds() .arrange() @@ -1330,8 +1356,8 @@ class CallRepositoryTest { Arrangement.conversationId, listOf( Arrangement.participant.copy( - hasEstablishedAudio = false - ) + hasEstablishedAudio = false + ) ) ) advanceTimeBy(CallDataSource.STALE_PARTICIPANT_TIMEOUT.toLong(DurationUnit.MILLISECONDS)) @@ -1352,6 +1378,7 @@ class CallRepositoryTest { val (arrangement, callRepository) = Arrangement() .givenGetSubconversationInfoReturns(Arrangement.subconversationGroupId) .givenRemoveClientsFromMLSGroupSucceeds() + .givenGetKnownUserMinimizedSucceeds() .arrange() callRepository.updateCallMetadataProfileFlow( @@ -1423,6 +1450,30 @@ class CallRepositoryTest { } + @Test + fun givenAConversationIdThatExistsInTheFlow_whenUpdateParticipantsActiveSpeakerIsCalled_thenUpdateTheFlow() = runTest { + val (_, callRepository) = Arrangement().arrange() + val activeSpeakers = mapOf(QualifiedID("participantId", "participantDomain") to listOf("abcd")) + + callRepository.updateCallMetadataProfileFlow( + callMetadataProfile = CallMetadataProfile( + data = mapOf( + Arrangement.conversationId to createCallMetadata().copy( + participants = emptyList(), + maxParticipants = 0 + ) + ) + ) + ) + + callRepository.updateParticipantsActiveSpeaker( + Arrangement.conversationId, + activeSpeakers + ) + + assertEquals(activeSpeakers, callRepository.getCallMetadataProfile().data[Arrangement.conversationId]?.activeSpeakers) + } + private fun provideCall(id: ConversationId, status: CallStatus) = Call( conversationId = id, status = status, @@ -1459,7 +1510,8 @@ class CallRepositoryTest { callerName = null, callerTeamName = null, callStatus = CallStatus.ESTABLISHED, - protocol = Conversation.ProtocolInfo.Proteus + protocol = Conversation.ProtocolInfo.Proteus, + activeSpeakers = mapOf() ) private class Arrangement { @@ -1609,6 +1661,12 @@ class CallRepositoryTest { }.returns(flowOf(TestUser.OTHER)) } + suspend fun givenGetKnownUserMinimizedSucceeds() = apply { + coEvery { + userRepository.getUsersMinimizedByQualifiedIDs(any()) + }.returns(listOf(TestUser.OTHER_MINIMIZED).right()) + } + suspend fun givenGetTeamSucceeds() = apply { coEvery { teamRepository.getTeam(any()) @@ -1725,15 +1783,13 @@ class CallRepositoryTest { ClientId("abcd"), QualifiedID("participantId", "participantDomain") ) - val participant = Participant( + val participant = ParticipantMinimized( id = qualifiedClientID.userId, + userId = qualifiedClientID.userId, clientId = qualifiedClientID.clientId.value, - name = "name", isMuted = true, - isSpeaking = false, isCameraOn = false, isSharingScreen = false, - avatarAssetId = null, hasEstablishedAudio = true ) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/ParticipantMapperTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/ParticipantMapperTest.kt index 97f2b7a875c..73223ddcd3a 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/ParticipantMapperTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/ParticipantMapperTest.kt @@ -22,7 +22,9 @@ import com.wire.kalium.calling.VideoStateCalling import com.wire.kalium.logic.data.call.mapper.CallMapper import com.wire.kalium.logic.data.call.mapper.ParticipantMapperImpl import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.id.QualifiedIdMapper import io.mockative.Mock +import io.mockative.any import io.mockative.every import io.mockative.mock import kotlinx.coroutines.test.runTest @@ -38,7 +40,10 @@ class ParticipantMapperTest { @Mock private val callMapper = mock(CallMapper::class) - private val participantMapperImpl = ParticipantMapperImpl(videoStateChecker, callMapper) + @Mock + private val qualifiedIdMapper = mock(QualifiedIdMapper::class) + + private val participantMapperImpl = ParticipantMapperImpl(videoStateChecker, callMapper, qualifiedIdMapper) @BeforeTest fun setUp() { @@ -51,16 +56,22 @@ class ParticipantMapperTest { every { videoStateChecker.isSharingScreen(VideoStateCalling.STOPPED) }.returns(false) + every { qualifiedIdMapper.fromStringToQualifiedID(any()) } + .returns(DUMMY_USER_ID) } @Test fun whenMappingToParticipant_withCallMember_thenReturnParticipant() = runTest { - val participantMap = participantMapperImpl.fromCallMemberToParticipant( + val participantMap = participantMapperImpl.fromCallMemberToParticipantMinimized( member = DUMMY_CALL_MEMBER ) - val expectedParticipant = Participant( + val expectedParticipant = ParticipantMinimized( id = QualifiedID( + value = "dummyId", + domain = "" + ), + userId = QualifiedID( value = "dummyId", domain = "dummyDomain" ), @@ -76,11 +87,16 @@ class ParticipantMapperTest { companion object { private val DUMMY_CALL_MEMBER = CallMember( - userId = "dummyId@dummyDomain", + userId = "dummyId", clientId = "dummyClientId", aestab = 0, vrecv = 0, isMuted = 0 ) + + private val DUMMY_USER_ID = QualifiedID( + value = "dummyId", + domain = "dummyDomain" + ) } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestCall.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestCall.kt index 3980ad10eae..6bb4fc0532f 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestCall.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestCall.kt @@ -18,12 +18,17 @@ package com.wire.kalium.logic.framework +import com.wire.kalium.logic.data.call.Call import com.wire.kalium.logic.data.call.CallMetadata +import com.wire.kalium.logic.data.call.CallStatus +import com.wire.kalium.logic.data.call.Participant +import com.wire.kalium.logic.data.call.ParticipantMinimized import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.user.OtherUserMinimized import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.data.call.Call -import com.wire.kalium.logic.data.call.CallStatus +import com.wire.kalium.logic.data.user.type.UserType import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.call.CallEntity import com.wire.kalium.persistence.dao.conversation.ConversationEntity @@ -73,7 +78,27 @@ object TestCall { callerTeamName = CALLER_TEAM_NAME, establishedTime = null, callStatus = CallStatus.ESTABLISHED, - protocol = Conversation.ProtocolInfo.Proteus + protocol = Conversation.ProtocolInfo.Proteus, + participants = listOf( + ParticipantMinimized( + id = QualifiedID("participantId", ""), + userId = QualifiedID("participantId", "participantDomain"), + clientId = "abcd", + isMuted = true, + isCameraOn = false, + isSharingScreen = false, + hasEstablishedAudio = true + ) + ), + activeSpeakers = mapOf(QualifiedID("participantId", "participantDomain") to listOf("abcd")), + users = listOf( + OtherUserMinimized( + id = QualifiedID("participantId", "participantDomain"), + name = "User Name", + completePicture = null, + userType = UserType.ADMIN + ) + ) ) fun oneOnOneEstablishedCall() = Call( @@ -88,7 +113,20 @@ object TestCall { callerName = CALLER_NAME, callerTeamName = CALLER_TEAM_NAME, establishedTime = null, - participants = emptyList(), + participants = listOf( + Participant( + id = QualifiedID("participantId", ""), + clientId = "abcd", + isMuted = true, + isCameraOn = false, + isSharingScreen = false, + hasEstablishedAudio = true, + name = "User Name", + avatarAssetId = null, + userType = UserType.ADMIN, + isSpeaking = false + ) + ), maxParticipants = 0 ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestUser.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestUser.kt index e435e4f769b..d3ac5e3c9a9 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestUser.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestUser.kt @@ -21,6 +21,7 @@ package com.wire.kalium.logic.framework import com.wire.kalium.logic.data.id.TeamId import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.OtherUser +import com.wire.kalium.logic.data.user.OtherUserMinimized import com.wire.kalium.logic.data.user.SelfUser import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.data.user.UserAssetId @@ -182,4 +183,11 @@ object TestUser { usersFailed = emptyList(), usersFound = listOf(USER_PROFILE_DTO) ) + + val OTHER_MINIMIZED = OtherUserMinimized( + OTHER_USER_ID, + name = "otherUsername", + completePicture = UserAssetId("value2", "domain"), + userType = UserType.EXTERNAL, + ) } diff --git a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnActiveSpeakersTest.kt b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnActiveSpeakersTest.kt new file mode 100644 index 00000000000..d383b4f4bdb --- /dev/null +++ b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnActiveSpeakersTest.kt @@ -0,0 +1,92 @@ +/* + * 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.scenario + +import com.wire.kalium.calling.types.Handle +import com.wire.kalium.logic.data.call.CallActiveSpeaker +import com.wire.kalium.logic.data.call.CallActiveSpeakers +import com.wire.kalium.logic.data.call.CallRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl +import com.wire.kalium.logic.framework.TestUser +import io.mockative.Mock +import io.mockative.any +import io.mockative.coVerify +import io.mockative.every +import io.mockative.mock +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import org.junit.Test + +class OnActiveSpeakersTest { + + private val testScope = TestScope() + + @Test + fun givenCallActiveSpeakers_whenOnActiveSpeakersHappens_thenOnlySpeakingParticipantsPassedForward() = testScope.runTest { + // given + val speakers = listOf( + speaker(0, true), + speaker(1, true), + speaker(2, true), + speaker(3, false), + speaker(4, false) + ) + val data = Json.encodeToString(CallActiveSpeakers.serializer(), CallActiveSpeakers(speakers)) + val convId = ConversationId("conversation", "domain") + val expected = mapOf( + userId(0) to listOf("client0"), + userId(1) to listOf("client1"), + userId(2) to listOf("client2") + ) + val (arrangement, onActiveSpeakers) = Arrangement().arrange() + + // when + onActiveSpeakers.onActiveSpeakersChanged(Handle(), convId.toString(), data, null) + + // then + coVerify { arrangement.callRepository.updateParticipantsActiveSpeaker(convId, expected) } + .wasInvoked(exactly = 1) + } + + private fun speaker(suffix: Int = 1, isSpeaking: Boolean = false) = CallActiveSpeaker( + userId = userId(suffix).toString(), + clientId = "client$suffix", + audioLevelNow = if (isSpeaking) 10 else 0, + audioLevel = if (isSpeaking) 10 else 0 + ) + + private fun userId(suffix: Int) = QualifiedID("userId$suffix", "some-domain") + + internal class Arrangement { + @Mock + val callRepository = mock(CallRepository::class) + + val qualifiedIdMapper = QualifiedIdMapperImpl(TestUser.SELF.id) + + init { + every { + callRepository.updateParticipantsActiveSpeaker(any(), any()) + }.returns(Unit) + } + + fun arrange() = this to OnActiveSpeakers(callRepository, qualifiedIdMapper) + } +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt index f818cab0c32..08ee83381b1 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt @@ -307,4 +307,5 @@ interface UserDAO { suspend fun upsertConnectionStatuses(userStatuses: Map) suspend fun isAtLeastOneUserATeamMember(userId: List, teamId: String): Boolean suspend fun getOneOnOnConversationId(userId: UserIDEntity): QualifiedIDEntity? + suspend fun getUsersMinimizedByQualifiedIDs(qualifiedIDs: List): List } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt index f32727134b6..4ebf3ee2282 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt @@ -286,6 +286,13 @@ class UserDAOImpl internal constructor( }.executeAsOneOrNull() } + override suspend fun getUsersMinimizedByQualifiedIDs(qualifiedIDs: List): List = + withContext(queriesContext) { + userQueries.selectMinimizedByQualifiedId(qualifiedIDs) { qualifiedId, name, completeAssetId, userType -> + mapper.toModelMinimized(qualifiedId, name, completeAssetId, userType) + }.executeAsList() + } + override suspend fun getUsersDetailsByQualifiedIDList(qualifiedIDList: List): List = withContext(queriesContext) { userQueries.selectDetailsByQualifiedId(qualifiedIDList)